From 922b6eb3145f77ba3afd09704465274306dd1554 Mon Sep 17 00:00:00 2001 From: Scot Lunsford Date: Wed, 22 Jul 2026 13:51:35 -0500 Subject: [PATCH 01/17] feat: add production website Dockerfile --- Dockerfile | 147 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 87 insertions(+), 60 deletions(-) diff --git a/Dockerfile b/Dockerfile index 061ee07947d..bca87a8306e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,65 +1,92 @@ -# Use multi-stage build -FROM node:16 as node-build - -WORKDIR /app - -# Copy package.json, package-lock.json, and webpack configuration -COPY attack-search/package*.json ./attack-search/ -COPY attack-search/webpack.config.cjs ./attack-search/ - -# Copy src/ folder with all subdirectories -COPY attack-search/src ./attack-search/src - -# Generate the webpack bundle containing the search service -RUN cd attack-search && \ - npm ci && \ - npm run build - -# Use the official Python image as the base image -FROM python:3.13-slim-bullseye as python-build - -ENV DEBIAN_FRONTEND noninteractive -ENV LANG en_US.UTF-8 -ENV LC_ALL en_US.UTF-8 - -# Install dependencies -RUN apt-get update --fix-missing && \ - apt-get upgrade -y && \ - apt-get install -y -qq --no-install-recommends locales sudo git apt-transport-https ca-certificates && \ - echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \ - locale-gen && \ - update-ca-certificates - -WORKDIR /home/attackuser/attack-website - -# Copy the source code -COPY . . - -# Install all Python dependencies -RUN python3 -m pip install --no-cache-dir wheel && \ - python3 -m pip install --no-cache-dir -r requirements.txt - -# Build the website -RUN python3 update-attack.py --no-test-exitstatus - -# Copy the search service webpack bundle from the node-build stage -COPY --from=node-build /app/attack-search/dist/search_bundle.js output/theme/scripts/search_bundle.js - -# Nginx stage -FROM nginx:stable-alpine as production-stage - -COPY --from=python-build /home/attackuser/attack-website/output /var/www/html +# syntax=docker/dockerfile:1.7 + +FROM node:18-bookworm-slim AS search-build + +WORKDIR /src/attack-search + +COPY attack-search/package*.json ./ +RUN npm ci + +COPY attack-search/webpack.config.cjs ./ +COPY attack-search/src ./src +RUN npm run build + + +FROM python:3.13-slim-bookworm AS site-build + +ARG PELICAN_SITEURL="" +ARG BANNER_ENABLED="true" +ARG BANNER_MESSAGE="" +ARG INCLUDE_OSANO="false" +ARG GOOGLE_ANALYTICS="" +ARG GOOGLE_SITE_VERIFICATION="" +ARG UPDATE_ATTACK_EXTRAS="resources blog stixtests benefactors versions" +ARG VERSION_ARCHIVE_DIR="/opt/attack-version-archives" +ARG STIX_LOCATION_ENTERPRISE="https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json" +ARG STIX_LOCATION_MOBILE="https://raw.githubusercontent.com/mitre/cti/master/mobile-attack/mobile-attack.json" +ARG STIX_LOCATION_ICS="https://raw.githubusercontent.com/mitre/cti/master/ics-attack/ics-attack.json" +ARG WORKBENCH_USER="" + +ENV PELICAN_SITEURL=${PELICAN_SITEURL} \ + BANNER_ENABLED=${BANNER_ENABLED} \ + BANNER_MESSAGE=${BANNER_MESSAGE} \ + INCLUDE_OSANO=${INCLUDE_OSANO} \ + GOOGLE_ANALYTICS=${GOOGLE_ANALYTICS} \ + GOOGLE_SITE_VERIFICATION=${GOOGLE_SITE_VERIFICATION} \ + UPDATE_ATTACK_EXTRAS=${UPDATE_ATTACK_EXTRAS} \ + VERSION_ARCHIVE_DIR=${VERSION_ARCHIVE_DIR} \ + STIX_LOCATION_ENTERPRISE=${STIX_LOCATION_ENTERPRISE} \ + STIX_LOCATION_MOBILE=${STIX_LOCATION_MOBILE} \ + STIX_LOCATION_ICS=${STIX_LOCATION_ICS} \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates git \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /src/attack-website + +COPY requirements.txt ./ +RUN python3 -m pip install --no-cache-dir wheel \ + && python3 -m pip install --no-cache-dir -r requirements.txt + +COPY . ./ + +# The generator copies theme assets into output/, so place the generated search bundle +# in the theme before running it. This mirrors the production GitLab pipeline. +COPY --from=search-build /src/attack-search/dist/search_bundle.js attack-theme/static/scripts/search_bundle.js + +# A Workbench API key and internal CA are optional for local/upstream builds. When supplied +# as BuildKit secrets, they are available only to this command and are not persisted in an image layer. +RUN --mount=type=secret,id=workbench_api_key,required=false \ + --mount=type=secret,id=internal_ca,target=/usr/local/share/ca-certificates/internal-ca.crt,required=false \ + if [ -f /usr/local/share/ca-certificates/internal-ca.crt ]; then \ + update-ca-certificates; \ + fi; \ + if [ -f /run/secrets/workbench_api_key ]; then \ + export WORKBENCH_API_KEY="$(cat /run/secrets/workbench_api_key)"; \ + export WORKBENCH_USER; \ + fi; \ + mkdir -p "${VERSION_ARCHIVE_DIR}"; \ + python3 update-attack.py --attack-brand --extras ${UPDATE_ATTACK_EXTRAS} --no-test-exitstatus --version-archive-dir "${VERSION_ARCHIVE_DIR}" + + +FROM nginx:stable-alpine AS production + +COPY --from=site-build /src/attack-website/output /var/www/html COPY nginx.conf /etc/nginx/conf.d/default.conf -# Label metadata -LABEL name="attack-website" \ - description="Dockerfile for the ATT&CK Website" \ - usage="https://github.com/mitre-attack/attack-website/blob/master/README.md#install-and-build" \ - url="https://attack.mitre.org/" \ - vcs-url="https://github.com/mitre-attack/attack-website" \ - vendor="MITRE ATT&CK" \ - maintainer="attack@mitre.org" +LABEL org.opencontainers.image.title="ATT&CK Website" \ + org.opencontainers.image.description="Static ATT&CK Website served by Nginx" \ + org.opencontainers.image.source="https://github.com/mitre-attack/attack-website" \ + org.opencontainers.image.url="https://attack.mitre.org/" \ + org.opencontainers.image.vendor="MITRE ATT&CK" EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget -q -O /dev/null http://127.0.0.1/ || exit 1 + +CMD ["nginx", "-g", "daemon off;"] From d69364d40963b9c1046848fb0abfa09d0e22ce0e Mon Sep 17 00:00:00 2001 From: Scot Lunsford Date: Thu, 23 Jul 2026 22:45:14 -0500 Subject: [PATCH 02/17] feat: cache website build artifacts Retain the non-SSH legacy report outputs while caching release archives and revalidating STIX inputs between BuildKit builds. --- Dockerfile | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index bca87a8306e..f5ef99bffa2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,9 @@ ARG INCLUDE_OSANO="false" ARG GOOGLE_ANALYTICS="" ARG GOOGLE_SITE_VERIFICATION="" ARG UPDATE_ATTACK_EXTRAS="resources blog stixtests benefactors versions" -ARG VERSION_ARCHIVE_DIR="/opt/attack-version-archives" +ARG VERSION_ARCHIVE_DIR="/var/cache/attack-website/version-archives" +ARG ATTACK_RELEASES_DIR="/var/cache/attack-website/attack-releases" +ARG DIFF_STIX_VERSION="v19.0" ARG STIX_LOCATION_ENTERPRISE="https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json" ARG STIX_LOCATION_MOBILE="https://raw.githubusercontent.com/mitre/cti/master/mobile-attack/mobile-attack.json" ARG STIX_LOCATION_ICS="https://raw.githubusercontent.com/mitre/cti/master/ics-attack/ics-attack.json" @@ -35,6 +37,9 @@ ENV PELICAN_SITEURL=${PELICAN_SITEURL} \ GOOGLE_SITE_VERIFICATION=${GOOGLE_SITE_VERIFICATION} \ UPDATE_ATTACK_EXTRAS=${UPDATE_ATTACK_EXTRAS} \ VERSION_ARCHIVE_DIR=${VERSION_ARCHIVE_DIR} \ + ATTACK_RELEASES_DIR=${ATTACK_RELEASES_DIR} \ + DIFF_STIX_VERSION=${DIFF_STIX_VERSION} \ + ATTACK_STIX_CACHE_DIR=/var/cache/attack-website/stix \ STIX_LOCATION_ENTERPRISE=${STIX_LOCATION_ENTERPRISE} \ STIX_LOCATION_MOBILE=${STIX_LOCATION_MOBILE} \ STIX_LOCATION_ICS=${STIX_LOCATION_ICS} \ @@ -58,10 +63,16 @@ COPY . ./ # in the theme before running it. This mirrors the production GitLab pipeline. COPY --from=search-build /src/attack-search/dist/search_bundle.js attack-theme/static/scripts/search_bundle.js +# Preserve the legacy STIX release input for the generated changelog. The BuildKit cache +# keeps these downloaded artifacts on the site host without adding them to the runtime image. +RUN --mount=type=cache,id=attack-website-artifacts-v1,target=/var/cache/attack-website,sharing=locked \ + download_attack_stix --download-dir "${ATTACK_RELEASES_DIR}" --all --stix21 + # A Workbench API key and internal CA are optional for local/upstream builds. When supplied # as BuildKit secrets, they are available only to this command and are not persisted in an image layer. RUN --mount=type=secret,id=workbench_api_key,required=false \ --mount=type=secret,id=internal_ca,target=/usr/local/share/ca-certificates/internal-ca.crt,required=false \ + --mount=type=cache,id=attack-website-artifacts-v1,target=/var/cache/attack-website,sharing=locked \ if [ -f /usr/local/share/ca-certificates/internal-ca.crt ]; then \ update-ca-certificates; \ fi; \ @@ -72,6 +83,28 @@ RUN --mount=type=secret,id=workbench_api_key,required=false \ mkdir -p "${VERSION_ARCHIVE_DIR}"; \ python3 update-attack.py --attack-brand --extras ${UPDATE_ATTACK_EXTRAS} --no-test-exitstatus --version-archive-dir "${VERSION_ARCHIVE_DIR}" +# Preserve the non-SSH report phase from legacy CI: native website reports, STIX diff output, +# and the combined report. The data-quality reports that require Workbench SSH are omitted. +RUN --mount=type=secret,id=attack_update_scripts_token,required=true \ + --mount=type=cache,id=attack-website-artifacts-v1,target=/var/cache/attack-website,sharing=locked \ + mkdir -p output/reports output/changes \ + && cp reports/* output/reports/ \ + && cp reports/tests.html output/ \ + && diff_stix -v \ + --old "${ATTACK_RELEASES_DIR}/stix-2.0/${DIFF_STIX_VERSION}/" \ + --new output/stix/ \ + --show-key \ + --contributors \ + --html-file output/changes/index.html \ + --html-file-detailed output/changes/changelog-detailed.html \ + --markdown-file output/changes/changelog.md \ + --json-file output/changes/changelog.json \ + --layers output/changes/layer-enterprise.json output/changes/layer-mobile.json output/changes/layer-ics.json \ + && git clone "https://gitlab-ci-token:$(cat /run/secrets/attack_update_scripts_token)@gitlab.mitre.org/attack-strategy/attack_update_scripts.git" /tmp/attack_update_scripts \ + && git -C /tmp/attack_update_scripts remote set-url origin https://gitlab.mitre.org/attack-strategy/attack_update_scripts.git \ + && python3 -m pip install --no-cache-dir -r /tmp/attack_update_scripts/requirements.txt \ + && python3 /tmp/attack_update_scripts/website-cicd/combine-test-reports.py --reports-dir output/reports/ --output-dir output/ + FROM nginx:stable-alpine AS production From 67541161577ca385ac62bed0f4c368667179868d Mon Sep 17 00:00:00 2001 From: Scot Lunsford Date: Thu, 23 Jul 2026 22:58:24 -0500 Subject: [PATCH 03/17] fix: bootstrap MITRE build certificates Install the MITRE trust bundle before downloading release artifacts or contacting internal services during the website image build. --- Dockerfile | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index f5ef99bffa2..87726842880 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,8 @@ ENV PELICAN_SITEURL=${PELICAN_SITEURL} \ PYTHONUNBUFFERED=1 RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates git \ + && apt-get install -y --no-install-recommends ca-certificates curl git \ + && curl -ksSL https://gitlab.mitre.org/mitre-scripts/mitre-pki/raw/master/os_scripts/install_certs.sh | sh \ && rm -rf /var/lib/apt/lists/* WORKDIR /src/attack-website @@ -68,14 +69,10 @@ COPY --from=search-build /src/attack-search/dist/search_bundle.js attack-theme/s RUN --mount=type=cache,id=attack-website-artifacts-v1,target=/var/cache/attack-website,sharing=locked \ download_attack_stix --download-dir "${ATTACK_RELEASES_DIR}" --all --stix21 -# A Workbench API key and internal CA are optional for local/upstream builds. When supplied -# as BuildKit secrets, they are available only to this command and are not persisted in an image layer. +# A Workbench API key is optional for local/upstream builds. When supplied as a BuildKit secret, +# it is available only to this command and is not persisted in an image layer. RUN --mount=type=secret,id=workbench_api_key,required=false \ - --mount=type=secret,id=internal_ca,target=/usr/local/share/ca-certificates/internal-ca.crt,required=false \ --mount=type=cache,id=attack-website-artifacts-v1,target=/var/cache/attack-website,sharing=locked \ - if [ -f /usr/local/share/ca-certificates/internal-ca.crt ]; then \ - update-ca-certificates; \ - fi; \ if [ -f /run/secrets/workbench_api_key ]; then \ export WORKBENCH_API_KEY="$(cat /run/secrets/workbench_api_key)"; \ export WORKBENCH_USER; \ From d6db4e04cb4689d4f150f18916ad6bb5726097df Mon Sep 17 00:00:00 2001 From: Scot Lunsford Date: Fri, 24 Jul 2026 15:38:14 -0500 Subject: [PATCH 04/17] fix: install MITRE trust for Python clients --- Dockerfile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 87726842880..5bd49b7b3f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,7 +43,6 @@ ENV PELICAN_SITEURL=${PELICAN_SITEURL} \ STIX_LOCATION_ENTERPRISE=${STIX_LOCATION_ENTERPRISE} \ STIX_LOCATION_MOBILE=${STIX_LOCATION_MOBILE} \ STIX_LOCATION_ICS=${STIX_LOCATION_ICS} \ - REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 @@ -56,7 +55,11 @@ WORKDIR /src/attack-website COPY requirements.txt ./ RUN python3 -m pip install --no-cache-dir wheel \ - && python3 -m pip install --no-cache-dir -r requirements.txt + && python3 -m pip install --no-cache-dir -r requirements.txt \ + # requests and certifi use their own CA bundle. Install MITRE trust there after + # dependencies are present, and do not override it with REQUESTS_CA_BUNDLE. + && curl -ksSL https://gitlab.mitre.org/mitre-scripts/mitre-pki/raw/master/tool_scripts/install_certs.sh \ + | MODE=python PYTHON=python3 sh COPY . ./ From 27bb56ad52573d93e1da67e6fe519d7be52b072a Mon Sep 17 00:00:00 2001 From: Scot Lunsford Date: Fri, 24 Jul 2026 16:10:53 -0500 Subject: [PATCH 05/17] fix: use compatible Python TLS validation --- Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5bd49b7b3f1..9dbae1f239a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,10 @@ COPY attack-search/src ./src RUN npm run build -FROM python:3.13-slim-bookworm AS site-build +# Python 3.13 enables strict X.509 chain validation by default. The enterprise +# TLS interception chain is trusted but does not meet that new strict profile. +# Keep normal certificate verification with the compatible Python 3.12 runtime. +FROM python:3.12-slim-bookworm AS site-build ARG PELICAN_SITEURL="" ARG BANNER_ENABLED="true" From bfcfeb589488ade66e9bdfad5565f64eff96cc62 Mon Sep 17 00:00:00 2001 From: Scot Lunsford Date: Fri, 24 Jul 2026 16:21:26 -0500 Subject: [PATCH 06/17] chore: remove TLS implementation comments --- Dockerfile | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9dbae1f239a..20e0058b8ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,9 +12,6 @@ COPY attack-search/src ./src RUN npm run build -# Python 3.13 enables strict X.509 chain validation by default. The enterprise -# TLS interception chain is trusted but does not meet that new strict profile. -# Keep normal certificate verification with the compatible Python 3.12 runtime. FROM python:3.12-slim-bookworm AS site-build ARG PELICAN_SITEURL="" @@ -59,8 +56,6 @@ WORKDIR /src/attack-website COPY requirements.txt ./ RUN python3 -m pip install --no-cache-dir wheel \ && python3 -m pip install --no-cache-dir -r requirements.txt \ - # requests and certifi use their own CA bundle. Install MITRE trust there after - # dependencies are present, and do not override it with REQUESTS_CA_BUNDLE. && curl -ksSL https://gitlab.mitre.org/mitre-scripts/mitre-pki/raw/master/tool_scripts/install_certs.sh \ | MODE=python PYTHON=python3 sh From 07e3a4fd172edb2566b0cdeb069bf209321b77e6 Mon Sep 17 00:00:00 2001 From: Scot Lunsford Date: Fri, 24 Jul 2026 16:43:55 -0500 Subject: [PATCH 07/17] chore: remove combined report dependency --- Dockerfile | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 20e0058b8ce..e00c60278d8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -81,10 +81,7 @@ RUN --mount=type=secret,id=workbench_api_key,required=false \ mkdir -p "${VERSION_ARCHIVE_DIR}"; \ python3 update-attack.py --attack-brand --extras ${UPDATE_ATTACK_EXTRAS} --no-test-exitstatus --version-archive-dir "${VERSION_ARCHIVE_DIR}" -# Preserve the non-SSH report phase from legacy CI: native website reports, STIX diff output, -# and the combined report. The data-quality reports that require Workbench SSH are omitted. -RUN --mount=type=secret,id=attack_update_scripts_token,required=true \ - --mount=type=cache,id=attack-website-artifacts-v1,target=/var/cache/attack-website,sharing=locked \ +RUN --mount=type=cache,id=attack-website-artifacts-v1,target=/var/cache/attack-website,sharing=locked \ mkdir -p output/reports output/changes \ && cp reports/* output/reports/ \ && cp reports/tests.html output/ \ @@ -97,11 +94,7 @@ RUN --mount=type=secret,id=attack_update_scripts_token,required=true \ --html-file-detailed output/changes/changelog-detailed.html \ --markdown-file output/changes/changelog.md \ --json-file output/changes/changelog.json \ - --layers output/changes/layer-enterprise.json output/changes/layer-mobile.json output/changes/layer-ics.json \ - && git clone "https://gitlab-ci-token:$(cat /run/secrets/attack_update_scripts_token)@gitlab.mitre.org/attack-strategy/attack_update_scripts.git" /tmp/attack_update_scripts \ - && git -C /tmp/attack_update_scripts remote set-url origin https://gitlab.mitre.org/attack-strategy/attack_update_scripts.git \ - && python3 -m pip install --no-cache-dir -r /tmp/attack_update_scripts/requirements.txt \ - && python3 /tmp/attack_update_scripts/website-cicd/combine-test-reports.py --reports-dir output/reports/ --output-dir output/ + --layers output/changes/layer-enterprise.json output/changes/layer-mobile.json output/changes/layer-ics.json FROM nginx:stable-alpine AS production From 71f16beaa7ed1e54e05fb90900ce4ad82eb29c03 Mon Sep 17 00:00:00 2001 From: Scot Lunsford Date: Fri, 24 Jul 2026 17:17:41 -0500 Subject: [PATCH 08/17] fix: omit Osano when disabled --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e00c60278d8..880e284d085 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,7 +32,6 @@ ARG WORKBENCH_USER="" ENV PELICAN_SITEURL=${PELICAN_SITEURL} \ BANNER_ENABLED=${BANNER_ENABLED} \ BANNER_MESSAGE=${BANNER_MESSAGE} \ - INCLUDE_OSANO=${INCLUDE_OSANO} \ GOOGLE_ANALYTICS=${GOOGLE_ANALYTICS} \ GOOGLE_SITE_VERIFICATION=${GOOGLE_SITE_VERIFICATION} \ UPDATE_ATTACK_EXTRAS=${UPDATE_ATTACK_EXTRAS} \ @@ -78,6 +77,7 @@ RUN --mount=type=secret,id=workbench_api_key,required=false \ export WORKBENCH_API_KEY="$(cat /run/secrets/workbench_api_key)"; \ export WORKBENCH_USER; \ fi; \ + if [ "${INCLUDE_OSANO}" = "true" ]; then export INCLUDE_OSANO; else unset INCLUDE_OSANO; fi; \ mkdir -p "${VERSION_ARCHIVE_DIR}"; \ python3 update-attack.py --attack-brand --extras ${UPDATE_ATTACK_EXTRAS} --no-test-exitstatus --version-archive-dir "${VERSION_ARCHIVE_DIR}" From 7773efe8c13e8b264e82fabaeedea9000456cbb2 Mon Sep 17 00:00:00 2001 From: Scot Lunsford Date: Mon, 27 Jul 2026 14:35:11 -0500 Subject: [PATCH 09/17] feat: parameterize CA trust setup Keep the public Dockerfile generic while allowing trusted builds to supply organization-specific trust setup commands. --- Dockerfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 880e284d085..b4ea2eda971 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,6 +28,8 @@ ARG STIX_LOCATION_ENTERPRISE="https://raw.githubusercontent.com/mitre/cti/master ARG STIX_LOCATION_MOBILE="https://raw.githubusercontent.com/mitre/cti/master/mobile-attack/mobile-attack.json" ARG STIX_LOCATION_ICS="https://raw.githubusercontent.com/mitre/cti/master/ics-attack/ics-attack.json" ARG WORKBENCH_USER="" +ARG OS_CA_TRUST_SETUP_COMMAND=":" +ARG PYTHON_CA_TRUST_SETUP_COMMAND=":" ENV PELICAN_SITEURL=${PELICAN_SITEURL} \ BANNER_ENABLED=${BANNER_ENABLED} \ @@ -47,7 +49,7 @@ ENV PELICAN_SITEURL=${PELICAN_SITEURL} \ RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates curl git \ - && curl -ksSL https://gitlab.mitre.org/mitre-scripts/mitre-pki/raw/master/os_scripts/install_certs.sh | sh \ + && sh -ec "${OS_CA_TRUST_SETUP_COMMAND}" \ && rm -rf /var/lib/apt/lists/* WORKDIR /src/attack-website @@ -55,8 +57,7 @@ WORKDIR /src/attack-website COPY requirements.txt ./ RUN python3 -m pip install --no-cache-dir wheel \ && python3 -m pip install --no-cache-dir -r requirements.txt \ - && curl -ksSL https://gitlab.mitre.org/mitre-scripts/mitre-pki/raw/master/tool_scripts/install_certs.sh \ - | MODE=python PYTHON=python3 sh + && sh -ec "${PYTHON_CA_TRUST_SETUP_COMMAND}" COPY . ./ From 164b98dba1f5cf2679c7186cf2f7edaba8cae21a Mon Sep 17 00:00:00 2001 From: Scot Lunsford Date: Mon, 27 Jul 2026 16:05:56 -0500 Subject: [PATCH 10/17] chore: upgrade Node build environment Align the Docker image builder and GitHub Pages workflow on Node 26. --- .github/workflows/gh-pages.yml | 2 +- Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 35750c5c087..df3486861e0 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -23,7 +23,7 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v4 with: - node-version: '18.x' + node-version: '26.x' - name: Upgrade pip run: python3 -m pip install --upgrade pip diff --git a/Dockerfile b/Dockerfile index b4ea2eda971..9ace54fd574 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1.7 -FROM node:18-bookworm-slim AS search-build +FROM node:26-bookworm-slim AS search-build WORKDIR /src/attack-search From 3026f914868a64d8aaeafe8f44de1cf65fc78554 Mon Sep 17 00:00:00 2001 From: Scot Lunsford Date: Mon, 27 Jul 2026 16:10:19 -0500 Subject: [PATCH 11/17] chore: upgrade Python build environment Align the Docker builder with the Python 3.13 GitHub Pages workflow. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9ace54fd574..ee02f628828 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ COPY attack-search/src ./src RUN npm run build -FROM python:3.12-slim-bookworm AS site-build +FROM python:3.13-slim-bookworm AS site-build ARG PELICAN_SITEURL="" ARG BANNER_ENABLED="true" From 2aa1a470d60e7c3d25b8cbebaf224175fc4d5b5d Mon Sep 17 00:00:00 2001 From: Scot Lunsford Date: Mon, 27 Jul 2026 16:16:27 -0500 Subject: [PATCH 12/17] fix: make website extras opt-in Only pass the update-attack extras flag when the build argument is supplied. --- Dockerfile | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index ee02f628828..d3f6645c1b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ ARG BANNER_MESSAGE="" ARG INCLUDE_OSANO="false" ARG GOOGLE_ANALYTICS="" ARG GOOGLE_SITE_VERIFICATION="" -ARG UPDATE_ATTACK_EXTRAS="resources blog stixtests benefactors versions" +ARG UPDATE_ATTACK_EXTRAS="" ARG VERSION_ARCHIVE_DIR="/var/cache/attack-website/version-archives" ARG ATTACK_RELEASES_DIR="/var/cache/attack-website/attack-releases" ARG DIFF_STIX_VERSION="v19.0" @@ -80,7 +80,13 @@ RUN --mount=type=secret,id=workbench_api_key,required=false \ fi; \ if [ "${INCLUDE_OSANO}" = "true" ]; then export INCLUDE_OSANO; else unset INCLUDE_OSANO; fi; \ mkdir -p "${VERSION_ARCHIVE_DIR}"; \ - python3 update-attack.py --attack-brand --extras ${UPDATE_ATTACK_EXTRAS} --no-test-exitstatus --version-archive-dir "${VERSION_ARCHIVE_DIR}" + if [ -n "${UPDATE_ATTACK_EXTRAS}" ]; then \ + set -- --extras ${UPDATE_ATTACK_EXTRAS}; \ + else \ + set --; \ + fi; \ + python3 update-attack.py --attack-brand "$@" --no-test-exitstatus \ + --version-archive-dir "${VERSION_ARCHIVE_DIR}" RUN --mount=type=cache,id=attack-website-artifacts-v1,target=/var/cache/attack-website,sharing=locked \ mkdir -p output/reports output/changes \ From a420160f18e6638c92b6b499ded8b57735bf40e8 Mon Sep 17 00:00:00 2001 From: Scot Lunsford Date: Mon, 27 Jul 2026 16:16:44 -0500 Subject: [PATCH 13/17] fix: update default STIX version Align the Docker build default with the current ATT&CK 19.1 release. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d3f6645c1b8..c54509b6dcb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,7 @@ ARG GOOGLE_SITE_VERIFICATION="" ARG UPDATE_ATTACK_EXTRAS="" ARG VERSION_ARCHIVE_DIR="/var/cache/attack-website/version-archives" ARG ATTACK_RELEASES_DIR="/var/cache/attack-website/attack-releases" -ARG DIFF_STIX_VERSION="v19.0" +ARG DIFF_STIX_VERSION="v19.1" ARG STIX_LOCATION_ENTERPRISE="https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json" ARG STIX_LOCATION_MOBILE="https://raw.githubusercontent.com/mitre/cti/master/mobile-attack/mobile-attack.json" ARG STIX_LOCATION_ICS="https://raw.githubusercontent.com/mitre/cti/master/ics-attack/ics-attack.json" From 97fbc65f07d2f6f6f29ab412c541f87f1c56fcfc Mon Sep 17 00:00:00 2001 From: Scot Lunsford Date: Mon, 27 Jul 2026 16:23:45 -0500 Subject: [PATCH 14/17] fix: make ATT&CK branding opt-in Only pass the update-attack branding flag when the build argument is enabled. --- Dockerfile | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index c54509b6dcb..9f4a5a873da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,6 +20,7 @@ ARG BANNER_MESSAGE="" ARG INCLUDE_OSANO="false" ARG GOOGLE_ANALYTICS="" ARG GOOGLE_SITE_VERIFICATION="" +ARG ATTACK_BRAND="false" ARG UPDATE_ATTACK_EXTRAS="" ARG VERSION_ARCHIVE_DIR="/var/cache/attack-website/version-archives" ARG ATTACK_RELEASES_DIR="/var/cache/attack-website/attack-releases" @@ -80,12 +81,14 @@ RUN --mount=type=secret,id=workbench_api_key,required=false \ fi; \ if [ "${INCLUDE_OSANO}" = "true" ]; then export INCLUDE_OSANO; else unset INCLUDE_OSANO; fi; \ mkdir -p "${VERSION_ARCHIVE_DIR}"; \ + set --; \ + if [ "${ATTACK_BRAND}" = "true" ]; then \ + set -- "$@" --attack-brand; \ + fi; \ if [ -n "${UPDATE_ATTACK_EXTRAS}" ]; then \ - set -- --extras ${UPDATE_ATTACK_EXTRAS}; \ - else \ - set --; \ + set -- "$@" --extras ${UPDATE_ATTACK_EXTRAS}; \ fi; \ - python3 update-attack.py --attack-brand "$@" --no-test-exitstatus \ + python3 update-attack.py "$@" --no-test-exitstatus \ --version-archive-dir "${VERSION_ARCHIVE_DIR}" RUN --mount=type=cache,id=attack-website-artifacts-v1,target=/var/cache/attack-website,sharing=locked \ From 74fbadcb3066c805b09bf1f44277d199dc49e15e Mon Sep 17 00:00:00 2001 From: Scot Lunsford Date: Mon, 27 Jul 2026 16:24:05 -0500 Subject: [PATCH 15/17] docs: clarify optional setup command defaults Explain the POSIX no-op used when no CA setup command is provided. --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 9f4a5a873da..2b0ee2dc77c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,6 +29,7 @@ ARG STIX_LOCATION_ENTERPRISE="https://raw.githubusercontent.com/mitre/cti/master ARG STIX_LOCATION_MOBILE="https://raw.githubusercontent.com/mitre/cti/master/mobile-attack/mobile-attack.json" ARG STIX_LOCATION_ICS="https://raw.githubusercontent.com/mitre/cti/master/ics-attack/ics-attack.json" ARG WORKBENCH_USER="" +# `:` is the POSIX shell no-op, used when optional setup commands are not supplied. ARG OS_CA_TRUST_SETUP_COMMAND=":" ARG PYTHON_CA_TRUST_SETUP_COMMAND=":" From 5ba47a68e1ff316fceceac1b90a3eb076920ff7a Mon Sep 17 00:00:00 2001 From: Scot Lunsford Date: Mon, 27 Jul 2026 16:26:49 -0500 Subject: [PATCH 16/17] chore: reduce Docker build context Exclude local VCS and build artifacts, and remove the unused raw-STIX cache setting. --- .dockerignore | 9 ++++++++- Dockerfile | 3 +-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.dockerignore b/.dockerignore index fe425eb07f1..83b9b0c838e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,12 @@ Dockerfile +.git/ -__pycache__/ +**/__pycache__/ +*.py[cod] +.pytest_cache/ +.ruff_cache/ +.coverage +htmlcov/ .vscode/ content/ @@ -10,4 +16,5 @@ attack-releases/ venv/ .venv/ +**/node_modules/ .env diff --git a/Dockerfile b/Dockerfile index 2b0ee2dc77c..18ee3a936e4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,7 +42,6 @@ ENV PELICAN_SITEURL=${PELICAN_SITEURL} \ VERSION_ARCHIVE_DIR=${VERSION_ARCHIVE_DIR} \ ATTACK_RELEASES_DIR=${ATTACK_RELEASES_DIR} \ DIFF_STIX_VERSION=${DIFF_STIX_VERSION} \ - ATTACK_STIX_CACHE_DIR=/var/cache/attack-website/stix \ STIX_LOCATION_ENTERPRISE=${STIX_LOCATION_ENTERPRISE} \ STIX_LOCATION_MOBILE=${STIX_LOCATION_MOBILE} \ STIX_LOCATION_ICS=${STIX_LOCATION_ICS} \ @@ -64,7 +63,7 @@ RUN python3 -m pip install --no-cache-dir wheel \ COPY . ./ # The generator copies theme assets into output/, so place the generated search bundle -# in the theme before running it. This mirrors the production GitLab pipeline. +# in the theme before running it. COPY --from=search-build /src/attack-search/dist/search_bundle.js attack-theme/static/scripts/search_bundle.js # Preserve the legacy STIX release input for the generated changelog. The BuildKit cache From 792add00790e45cce42385060cf1f91925ff0a59 Mon Sep 17 00:00:00 2001 From: Scot Lunsford Date: Mon, 27 Jul 2026 16:36:45 -0500 Subject: [PATCH 17/17] feat: make STIX changelog generation optional Isolate release comparison in a dedicated Docker stage and skip it by default. --- Dockerfile | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/Dockerfile b/Dockerfile index 18ee3a936e4..764ddf37867 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,7 @@ ARG GOOGLE_ANALYTICS="" ARG GOOGLE_SITE_VERIFICATION="" ARG ATTACK_BRAND="false" ARG UPDATE_ATTACK_EXTRAS="" +ARG GENERATE_STIX_CHANGELOG="false" ARG VERSION_ARCHIVE_DIR="/var/cache/attack-website/version-archives" ARG ATTACK_RELEASES_DIR="/var/cache/attack-website/attack-releases" ARG DIFF_STIX_VERSION="v19.1" @@ -66,11 +67,6 @@ COPY . ./ # in the theme before running it. COPY --from=search-build /src/attack-search/dist/search_bundle.js attack-theme/static/scripts/search_bundle.js -# Preserve the legacy STIX release input for the generated changelog. The BuildKit cache -# keeps these downloaded artifacts on the site host without adding them to the runtime image. -RUN --mount=type=cache,id=attack-website-artifacts-v1,target=/var/cache/attack-website,sharing=locked \ - download_attack_stix --download-dir "${ATTACK_RELEASES_DIR}" --all --stix21 - # A Workbench API key is optional for local/upstream builds. When supplied as a BuildKit secret, # it is available only to this command and is not persisted in an image layer. RUN --mount=type=secret,id=workbench_api_key,required=false \ @@ -91,25 +87,35 @@ RUN --mount=type=secret,id=workbench_api_key,required=false \ python3 update-attack.py "$@" --no-test-exitstatus \ --version-archive-dir "${VERSION_ARCHIVE_DIR}" + +FROM site-build AS changelog-build + +RUN --mount=type=cache,id=attack-website-artifacts-v1,target=/var/cache/attack-website,sharing=locked \ + if [ "${GENERATE_STIX_CHANGELOG}" = "true" ]; then \ + download_attack_stix --download-dir "${ATTACK_RELEASES_DIR}" --all --stix21; \ + fi + RUN --mount=type=cache,id=attack-website-artifacts-v1,target=/var/cache/attack-website,sharing=locked \ - mkdir -p output/reports output/changes \ - && cp reports/* output/reports/ \ - && cp reports/tests.html output/ \ - && diff_stix -v \ - --old "${ATTACK_RELEASES_DIR}/stix-2.0/${DIFF_STIX_VERSION}/" \ - --new output/stix/ \ - --show-key \ - --contributors \ - --html-file output/changes/index.html \ - --html-file-detailed output/changes/changelog-detailed.html \ - --markdown-file output/changes/changelog.md \ - --json-file output/changes/changelog.json \ - --layers output/changes/layer-enterprise.json output/changes/layer-mobile.json output/changes/layer-ics.json + if [ "${GENERATE_STIX_CHANGELOG}" = "true" ]; then \ + mkdir -p output/reports output/changes \ + && cp reports/* output/reports/ \ + && cp reports/tests.html output/ \ + && diff_stix -v \ + --old "${ATTACK_RELEASES_DIR}/stix-2.0/${DIFF_STIX_VERSION}/" \ + --new output/stix/ \ + --show-key \ + --contributors \ + --html-file output/changes/index.html \ + --html-file-detailed output/changes/changelog-detailed.html \ + --markdown-file output/changes/changelog.md \ + --json-file output/changes/changelog.json \ + --layers output/changes/layer-enterprise.json output/changes/layer-mobile.json output/changes/layer-ics.json; \ + fi FROM nginx:stable-alpine AS production -COPY --from=site-build /src/attack-website/output /var/www/html +COPY --from=changelog-build /src/attack-website/output /var/www/html COPY nginx.conf /etc/nginx/conf.d/default.conf LABEL org.opencontainers.image.title="ATT&CK Website" \