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/.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 061ee07947d..764ddf37867 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,65 +1,132 @@ -# 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:26-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 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" +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="" +# `:` 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=":" + +ENV PELICAN_SITEURL=${PELICAN_SITEURL} \ + BANNER_ENABLED=${BANNER_ENABLED} \ + BANNER_MESSAGE=${BANNER_MESSAGE} \ + GOOGLE_ANALYTICS=${GOOGLE_ANALYTICS} \ + 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} \ + STIX_LOCATION_ENTERPRISE=${STIX_LOCATION_ENTERPRISE} \ + STIX_LOCATION_MOBILE=${STIX_LOCATION_MOBILE} \ + STIX_LOCATION_ICS=${STIX_LOCATION_ICS} \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl git \ + && sh -ec "${OS_CA_TRUST_SETUP_COMMAND}" \ + && 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 \ + && sh -ec "${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 + +# 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=cache,id=attack-website-artifacts-v1,target=/var/cache/attack-website,sharing=locked \ + if [ -f /run/secrets/workbench_api_key ]; then \ + 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}"; \ + set --; \ + if [ "${ATTACK_BRAND}" = "true" ]; then \ + set -- "$@" --attack-brand; \ + fi; \ + if [ -n "${UPDATE_ATTACK_EXTRAS}" ]; then \ + set -- "$@" --extras ${UPDATE_ATTACK_EXTRAS}; \ + fi; \ + 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 \ + 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=changelog-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;"]