Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
55660a5
fix: standardize STIX file path handling
jondricek Aug 7, 2026
509e547
fix(search): settle bulkPut when an IndexedDB write fails
ppcvote Aug 23, 2026
7742c5c
docs(changelog): note the bulkPut settle fix
ppcvote Aug 23, 2026
05ee4b2
docs: update pull request template
jondricek Aug 26, 2026
d0b48f2
fix: fix loading sidebar on https redirects
jondricek Sep 2, 2026
062f477
fix(search): surface a failed index build instead of spinning forever
ppcvote Sep 4, 2026
0d58ced
test(search): cover the warm restore path as well as the cold start
ppcvote Sep 4, 2026
156341f
fix: update search filtering to give more accurate results
adpare Sep 10, 2026
9412110
feat: add dark mode
jondricek Sep 14, 2026
e16e200
Merge remote-tracking branch 'origin/develop' into search-fixes
jondricek Sep 14, 2026
0c1c87d
fix: update search cache schema version to 4
jondricek Sep 14, 2026
d95d666
fix: update search service tests and adjust attack ID path prefixes
jondricek Sep 14, 2026
d0196b5
Merge pull request #645 from mitre-attack/search-fixes
jondricek Sep 14, 2026
ffd0522
fix(search): implement cache invalidation for failed search index builds
jondricek Sep 14, 2026
8fc68d0
Merge pull request #637 from ppcvote/fix/bulkput-settle
jondricek Sep 14, 2026
91264f6
feat: implement theme toggle synchronization and update navigation la…
jondricek Sep 15, 2026
4b239e2
Merge branch 'develop' of https://github.com/mitre-attack/attack-webs…
jondricek Sep 15, 2026
8858f23
fix(changelog): update bug fixes section with improved search results…
jondricek Sep 15, 2026
fee027d
feat(theme): enhance theme toggle functionality and update matrix nav…
jondricek Sep 15, 2026
a3a7ec4
fix(theme): handle toggle clicks before DOM ready
jondricek Sep 15, 2026
3594d08
feat: use Just to build the various website components and update doc…
jondricek Sep 21, 2026
4fff8b9
chore: update changelog for v5.1.0 release
jondricek Sep 21, 2026
93ec28f
chore: update website version to 5.1.0
jondricek Sep 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 5 additions & 17 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,7 @@
<!-- PULL REQUEST TEMPLATE -->
<!-- 1. fill in the below sections to the best of your ability -->
<!-- 2. Assign and/or mention a reviewer (typically @isaisabel) -->
<!-- 3. Pull requests should target the develop branch -->
<!-- 4. Make sure to update CHANGELOG.md to reflect what has changed. -->
## Description

## Description of what has changed
<!-- Add a short description of what changed -->
<!-- -->
<!-- For example, "changed a thing on /page to be better" -->
## Problem Solved

## Issues addressed by pull request
<!-- If relevant, add list of issues addressed by the pull request -->
<!-- If no issues are relevant, omit this section -->
<!-- Prefix issue list with keywords such as "closes", -->
<!-- "resolves", or "fixes" to automatically close the -->
<!-- issue s when the request is merged. -->
<!-- -->
<!-- For example, "Closes #24. See also #25, #26" -->
## Alternatives Considered

## Related issue(s)
16 changes: 4 additions & 12 deletions .github/workflows/gh-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@
python-version: '3.13'
cache: 'pip'

- name: Setup Node.js environment
uses: actions/setup-node@v4
- name: Install Just
uses: extractions/setup-just@v3

Check failure on line 24 in .github/workflows/gh-pages.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use full commit SHA hash for this dependency.

See more on https://sonarcloud.io/project/issues?id=mitre-attack_attack-website&issues=AaDFUibp1xtiWTEJzt6K&open=AaDFUibp1xtiWTEJzt6K&pullRequest=646
with:
node-version: '26.x'
just-version: '1.58.0'

- name: Upgrade pip
run: python3 -m pip install --upgrade pip

Check warning on line 29 in .github/workflows/gh-pages.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Omitting "--only-binary :all:" can lead to the execution of setup scripts. Make sure it is safe here.

See more on https://sonarcloud.io/project/issues?id=mitre-attack_attack-website&issues=AaDFUibp1xtiWTEJzt6L&open=AaDFUibp1xtiWTEJzt6L&pullRequest=646

- name: Install python requirements
run: python3 -m pip install -r requirements.txt

Check warning on line 32 in .github/workflows/gh-pages.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Omitting "--only-binary :all:" can lead to the execution of setup scripts. Make sure it is safe here.

See more on https://sonarcloud.io/project/issues?id=mitre-attack_attack-website&issues=AaDFUibp1xtiWTEJzt6M&open=AaDFUibp1xtiWTEJzt6M&pullRequest=646

- name: Get banner message
run: echo "ATTACK_WEBSITE_BANNER_MESSAGE=$(<website-banner.production)" >> $GITHUB_ENV
Expand All @@ -44,7 +44,7 @@

- name: Build website
run: >-
python3 update-attack.py --attack-brand
just build-website --attack-brand
--all-extras
--no-test-exitstatus
env:
Expand All @@ -59,14 +59,6 @@
- name: Remove STIX directory
run: rm -rf output/stix/

- name: Build ATT&CK Search module
run: |
cd attack-search
npm ci
npm run build
cp dist/search_bundle.js ../output/theme/scripts/
cd ..

- name: Add BlueSky Identification
if: ${{ vars.BLUESKY_ID != '' }}
run: |
Expand Down
8 changes: 5 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ attack-theme/templates/general/sidebar-resources.html
content/
data/pelican_settings.json

# this file is generated by the search module and should not be committed
search_bundle.js
node_modules/

attack-style/dist/
# Intermediate attack-search and attack-style outputs
/attack-search/dist/
/attack-search/compilation-stats.json
/attack-style/dist/

attack-version-archives/
tmp/
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
26
26 changes: 18 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This file is guidance for coding agents working in `attack-website`.
- `attack-search/` is a separate Node/CommonJS project for the search bundle.
- Search source and tests live in `attack-search/src/` and `attack-search/__tests__/`.
- `attack-style/` is a separate Node/Sass project for CSS output.
- SCSS entrypoints are `attack-style/style-attack.scss` and `attack-style/style-user.scss`.
- SCSS entrypoints are `attack-style/style-attack.scss`, `attack-style/style-user.scss`, and `attack-style/style-archive.scss`.
- `attack-theme/` contains Jinja templates, static assets, and legacy browser JS.
- Theme templates and static assets live in `attack-theme/templates/` and `attack-theme/static/`.
- `modules/` contains Python modules that generate ATT&CK site content.
Expand All @@ -28,7 +28,8 @@ This file is guidance for coding agents working in `attack-website`.
- When managing a local Python environment, prefer `uv` with a virtual environment at `.venv` in the git repository root.
- Node.js and npm are required for `attack-search/` and `attack-style/`.
- Docker is the preferred way to validate the final static output in an Nginx-like environment.
- CI currently uses Python `3.13` and Node `18.x` in `.github/workflows/gh-pages.yml`.
- Just `1.58.0` or newer is required for shared build commands. CI and Docker pin Just `1.58.0`.
- CI uses Python `3.13`; Docker uses Python `3.13` and Node `26`. CI consumes committed assets without npm.
- Prefer CI versions when reproducing CI behavior; Docker and development docs may reference older base images.
- Production-like builds may depend on environment variables from `.github/workflows/gh-pages.yml`, including `ATTACK_WEBSITE_GOOGLE_ANALYTICS`, `ATTACK_WEBSITE_GOOGLE_SITE_VERIFICATION`, `ATTACK_WEBSITE_INCLUDE_OSANO`, and `PELICAN_SITEURL`.

Expand All @@ -38,18 +39,25 @@ Run commands from the repo root unless a subdirectory is called out.

### Install

- Preferred Python env: `uv venv .venv`
- All local dependencies: `just install-deps` (creates/reuses root `.venv` and runs `npm ci` in both packages)
- Preferred Python env for manual setup: `uv venv --python 3.13 .venv`
- Python deps: `uv pip install -r requirements.txt`
- Search deps: `cd attack-search && npm ci`
- Style deps: `cd attack-style && npm ci`
- Prefer `npm ci` over `npm install`; do not update lockfiles unless dependency changes are part of the task.

### Build

- Main website build: `uv run python update-attack.py --attack-brand --all-extras --no-test-exitstatus`
- Complete website build: `just build-full-website --attack-brand --all-extras`
- Website using staged assets: `just build-website --attack-brand --all-extras`
- Compile and stage assets: `just build-search`, `just build-style`, or `just build-assets`
- Website targets retain Python CLI defaults unless flags are explicitly supplied; builds do not install dependencies.
- Python generator and targeted checks: `uv run python update-attack.py ...`
- Shared build ordering, prerequisites, and Docker integration: `docs/DEVELOPMENT.md`
- Search bundle: `cd attack-search && npm run build`
- Search dev bundle: `cd attack-search && npm run build:dev`
- Copy built search bundle into site output: `cd attack-search && npm run copy`
- Copy the compiled search bundle into theme static assets: `cd attack-search && npm run copy`
- Search build + copy into theme static assets: `cd attack-search && npm run build-copy`
- Style build: `cd attack-style && npm run build`
- Style build + copy into theme static assets: `cd attack-style && npm run build-copy`

Expand Down Expand Up @@ -81,8 +89,8 @@ Run commands from the repo root unless a subdirectory is called out.

### Important Command Notes

- There is no root `package.json`, `Makefile`, or single universal test runner.
- CI clearly builds the site and search bundle, but does not currently enforce Jest, ESLint, Stylelint, Ruff, or type checks.
- `justfile` defines the shared asset/site build commands. There is no root `package.json`, `Makefile`, or single universal test runner.
- CI builds the site using committed assets without rebuilding or verifying them against source. Docker rebuilds assets. CI does not currently enforce Jest, ESLint, Stylelint, Ruff, or type checks.
- For Python-side testing, the narrowest supported scope is a named category (`size`, `links`, `external_links`, `citations`), not an individual test file.
- Preferred production-like validation is Nginx via Docker, not Pelican's built-in dev server.
- Pelican's built-in development server does not match production Nginx routing behavior.
Expand All @@ -99,8 +107,10 @@ Run commands from the repo root unless a subdirectory is called out.

- Do not edit `output/` as source; regenerate it through the build pipeline.
- Avoid direct edits to `attack-search/dist/` and `attack-style/dist/` unless the task explicitly targets generated artifacts.
- Avoid direct edits to copied assets in `attack-theme/static/` when a source file in `attack-style/` or another generator owns the output.
- Avoid direct edits to copied assets in `attack-theme/static/` when a source file in `attack-style/`, `attack-search/` or another generator owns the output.
- Preserve generated-file comments and edit the named source template or source asset instead.
- Ignore intermediate outputs in `attack-search/dist/` and `attack-style/dist/`, but commit the compiled theme CSS and `attack-theme/static/scripts/search_bundle.js` together with frontend source changes. After changing anything in either package, regenerate its assets with `just build-search`, `just build-style`, or `just build-assets`. Keeping them current is the developer's responsibility.
- Keep the build-specific `attack-theme/static/scripts/settings.js` ignored; see `docs/DEVELOPMENT.md` for its runtime configuration role.

## Python Style

Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Website Changelog

## v5.1.0 (2026-09-21)

### Features

* Add dark mode!
* Move Docker's Node and Python build stages to Debian Trixie.
* Add Just build commands for Search, Style, assets, and website generation.

### Bug Fixes

* Improve search results for exact ATT&CK ID and numeric ID queries by prioritizing matching object pages and relevant references.
* Fix sidebar loading for HTTPS redirects.
* Settle the search index write when an IndexedDB write fails, instead of leaving the promise pending and the search spinner up.
* Disable the search controls and explain why when the search index cannot be built, instead of leaving the spinner running for as long as the page is open.

## v5.0.0 (2026-08-06)

* Release ATT&CK content version 19.2.
Expand Down
51 changes: 37 additions & 14 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
# syntax=docker/dockerfile:1.7

FROM node:26-bookworm-slim AS search-build
FROM node:26-trixie-slim AS assets-build

WORKDIR /src/attack-search
ARG ATTACK_WEBSITE_OS_CA_TRUST_SETUP_COMMAND=":"

COPY attack-search/package*.json ./
RUN npm ci
RUN apt update \
&& apt install -y --no-install-recommends ca-certificates curl \
&& sh -ec "${ATTACK_WEBSITE_OS_CA_TRUST_SETUP_COMMAND}" \
&& rm -rf /var/lib/apt/lists/*

COPY attack-search/webpack.config.cjs ./
COPY attack-search/src ./src
RUN npm run build
# Install pre-built Just (official installer)
RUN curl --proto '=https' --tlsv1.2 -fsSL https://just.systems/install.sh -o /tmp/install-just.sh \

Check warning on line 13 in Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this invocation of "curl" with the ADD instruction.

See more on https://sonarcloud.io/project/issues?id=mitre-attack_attack-website&issues=AaDFUieF1xtiWTEJzt6N&open=AaDFUieF1xtiWTEJzt6N&pullRequest=646
&& bash /tmp/install-just.sh --tag 1.58.0 --to /usr/local/bin \

Check failure on line 14 in Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Avoid executing downloaded artifacts directly without verification.

See more on https://sonarcloud.io/project/issues?id=mitre-attack_attack-website&issues=AaDFUieF1xtiWTEJzt6O&open=AaDFUieF1xtiWTEJzt6O&pullRequest=646
Comment on lines +13 to +14
Comment on lines +13 to +14
&& rm /tmp/install-just.sh \
&& test "$(just --version)" = "just 1.58.0"

WORKDIR /src/attack-website

COPY attack-search/package*.json ./attack-search/
COPY attack-style/package*.json ./attack-style/
RUN npm --prefix attack-search ci && npm --prefix attack-style ci

Check warning on line 22 in Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Omitting "--ignore-scripts" allows lifecycle scripts to run during package installation.

See more on https://sonarcloud.io/project/issues?id=mitre-attack_attack-website&issues=AaDFUieF1xtiWTEJzt6P&open=AaDFUieF1xtiWTEJzt6P&pullRequest=646

Check warning on line 22 in Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Omitting "--ignore-scripts" allows lifecycle scripts to run during package installation.

See more on https://sonarcloud.io/project/issues?id=mitre-attack_attack-website&issues=AaDFUieF1xtiWTEJzt6Q&open=AaDFUieF1xtiWTEJzt6Q&pullRequest=646

FROM python:3.13-slim-bookworm AS site-base
COPY justfile ./
COPY attack-search/webpack.config.cjs ./attack-search/
COPY attack-search/.babelrc ./attack-search/
COPY attack-search/src ./attack-search/src
COPY attack-style/ ./attack-style/

# Regenerate the committed theme assets using the same copy commands as local builds.
RUN just build-assets


FROM python:3.13-slim-trixie AS site-base

ARG PELICAN_SITEURL=""
ARG ATTACK_WEBSITE_BANNER_ENABLED=""
Expand Down Expand Up @@ -55,23 +74,27 @@
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1

RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl git \
RUN apt update \
&& apt install -y --no-install-recommends ca-certificates curl git \
&& sh -ec "${ATTACK_WEBSITE_OS_CA_TRUST_SETUP_COMMAND}" \
&& rm -rf /var/lib/apt/lists/*

RUN curl --proto '=https' --tlsv1.2 -fsSL https://just.systems/install.sh -o /tmp/install-just.sh \

Check warning on line 82 in Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this invocation of "curl" with the ADD instruction.

See more on https://sonarcloud.io/project/issues?id=mitre-attack_attack-website&issues=AaDFUieF1xtiWTEJzt6R&open=AaDFUieF1xtiWTEJzt6R&pullRequest=646
&& bash /tmp/install-just.sh --tag 1.58.0 --to /usr/local/bin \

Check failure on line 83 in Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Avoid executing downloaded artifacts directly without verification.

See more on https://sonarcloud.io/project/issues?id=mitre-attack_attack-website&issues=AaDFUieF1xtiWTEJzt6S&open=AaDFUieF1xtiWTEJzt6S&pullRequest=646
Comment on lines +82 to +83
Comment on lines +82 to +83
&& rm /tmp/install-just.sh \
&& test "$(just --version)" = "just 1.58.0"

WORKDIR /src/attack-website

COPY requirements.txt ./
RUN python3 -m pip install --no-cache-dir wheel \

Check warning on line 90 in Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Omitting "--only-binary :all:" can lead to the execution of setup scripts. Make sure it is safe here.

See more on https://sonarcloud.io/project/issues?id=mitre-attack_attack-website&issues=AaDFUieF1xtiWTEJzt6T&open=AaDFUieF1xtiWTEJzt6T&pullRequest=646
&& python3 -m pip install --no-cache-dir -r requirements.txt \

Check warning on line 91 in Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Omitting "--only-binary :all:" can lead to the execution of setup scripts. Make sure it is safe here.

See more on https://sonarcloud.io/project/issues?id=mitre-attack_attack-website&issues=AaDFUieF1xtiWTEJzt6U&open=AaDFUieF1xtiWTEJzt6U&pullRequest=646
&& sh -ec "${ATTACK_WEBSITE_PYTHON_CA_TRUST_SETUP_COMMAND}"

COPY . ./

# The generator copies theme assets into output/, so place the generated search bundle
# in the theme before running it.
COPY --from=search-build /src/attack-search/dist/search_bundle.js attack-theme/static/scripts/search_bundle.js
# Copy the complete staged asset set before Pelican renders and preserves the site.
COPY --from=assets-build /src/attack-website/attack-theme/static/ attack-theme/static/


FROM site-base AS website-build
Expand All @@ -94,7 +117,7 @@
set -- "$@" --extras "$extra"; \
done; \
fi; \
python3 update-attack.py "$@" \
just build-website "$@" \
--version-archive-dir "${ATTACK_WEBSITE_VERSION_ARCHIVE_DIR}"


Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ If you find errors or typos in the site content, let us know by sending an email
Check out our [developer guide](docs/DEVELOPMENT.md) if you are interested in extending the style, content, or functionality of this site.
It includes instructions on setting up a local version of the site, and workflows for building and running the site using Docker or locally.

Local build commands require [Just](https://just.systems/man/en/installation.html)
1.58.0 or newer. Install Just, uv, and Node.js 26, then run:

```sh
just install-deps
just build-full-website --attack-brand --all-extras
```

We also have the additional following guides:

* A [deployment guide](./test/README.md) for setting up our testing environment
Expand Down
29 changes: 29 additions & 0 deletions attack-search/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# ATT&CK Search

This project builds the browser search code. The Python search module generates
search JSON separately from the rendered website pages.

Use Node.js 26 to match Docker. From this directory:

```sh
npm ci
npm run build-copy
```

`npm run build` creates `dist/search_bundle.js`, and `npm run copy` copies it to
`../attack-theme/static/scripts/search_bundle.js`. The copy command creates the
destination directory if needed. It does not update `output/` or existing version
snapshots. `build-copy` runs both commands in order. Use production builds for
committed theme assets. Use `npm run build:dev` for local debugging.

Git ignores all `dist/` output and `compilation-stats.json`. After changing anything
in `attack-search/`, commit the production bundle in the theme. GitHub Pages uses
the committed bundle without rebuilding it or comparing it with the source.

The repository build interface uses [Just](../docs/DEVELOPMENT.md). Run
`just build-search` to compile and stage this package. To rebuild both asset sets
and generate the branded website with current-version snapshots, run
`just build-full-website --attack-brand --all-extras`.

The generated theme `settings.js` is ignored. The developer guide describes its
dataset- and deployment-specific contents.
25 changes: 25 additions & 0 deletions attack-search/__tests__/indexed-db-wrapper.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,29 @@ describe('IndexedDBWrapper', () => {
const count = await contentDb.count();
expect(count).toEqual(data.length);
});

// A failed write must settle the promise. Racing against a sentinel tells a
// rejection apart from a promise that never settles at all, which a plain
// rejects assertion cannot do: it would time out and look like a slow test.
const settle = (promise) => Promise.race([
promise.then(() => 'resolved', (error) => `rejected:${error.message}`),
new Promise((resolve) => setTimeout(() => resolve('HUNG'), 1000)),
]);

test('Bulk put rejects when the underlying write fails', async () => {
jest.spyOn(contentDb.indexeddb[contentDb.tableName], 'bulkPut')
.mockRejectedValue(new Error('QuotaExceededError'));

await expect(settle(contentDb.bulkPut(data))).resolves.toBe('rejected:QuotaExceededError');
});

test('Bulk put rejects when a later chunk fails', async () => {
let calls = 0;
jest.spyOn(contentDb.indexeddb[contentDb.tableName], 'bulkPut')
.mockImplementation(() => (++calls === 2
? Promise.reject(new Error('DatabaseClosedError'))
: Promise.resolve()));

await expect(settle(contentDb.bulkPut(data, 1))).resolves.toBe('rejected:DatabaseClosedError');
});
});
Loading
Loading