diff --git a/.dockerignore b/.dockerignore index aa39095..c7f0cfe 100644 --- a/.dockerignore +++ b/.dockerignore @@ -19,6 +19,14 @@ frontend/node_modules frontend/dist dist artifacts +docs +deploy +n8n/** +!n8n/workflows/ +!n8n/workflows/** +frontend +*.tgz +*.tar.gz coverage playwright-report test-results diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index de5c36b..3aef1d3 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -31,10 +31,19 @@ jobs: echo "Product or test change: running the complete acceptance gate." fi - name: Secret scan - uses: trufflesecurity/trufflehog@b9dd330365132cd2d01dd5dc8a857a056a2544e1 # v3.79.0 - with: - path: ./ - extra_args: --only-verified + shell: bash + run: | + set -euo pipefail + repository="$PWD" + source="file:///repo" + workspace=(-v "$repository:/repo" -w /repo) + if docker inspect "${HOSTNAME:-}" >/dev/null 2>&1; then + source="file://$repository" + workspace=(--volumes-from "$HOSTNAME" -w "$repository") + fi + docker run --rm "${workspace[@]}" \ + ghcr.io/trufflesecurity/trufflehog@sha256:7104dbb84d1ad2f5f6fa1134e92c6aa6f701f0a4ac2efd5a4c5c96225d899fe3 \ + git "$source" --fail --no-update --github-actions --only-verified - name: Backend tests in isolated PostgreSQL stack if: steps.scope.outputs.full == 'true' run: sh scripts/run-isolated-tests.sh @@ -44,8 +53,24 @@ jobs: docker compose -p mobilityops-ci -f compose.yaml -f compose.test.yaml run --build --rm api ruff check app tests docker compose -p mobilityops-ci -f compose.yaml -f compose.test.yaml run --rm api mypy app docker compose -p mobilityops-ci -f compose.yaml -f compose.test.yaml run --rm \ - -v "$PWD:/repo:ro" api python /repo/scripts/check-contracts.py + api python scripts/check-contracts.py python scripts/check-source-budgets.py + - name: Build production API image for vulnerability scan + if: steps.scope.outputs.full == 'true' + run: | + docker build --target runtime --build-arg VCS_REF="$GITHUB_SHA" \ + --tag mobilityops-api-ci --file backend/Dockerfile . + - name: Production API image vulnerability scan (HIGH/CRITICAL) + if: steps.scope.outputs.full == 'true' + run: bash scripts/scan-ci-image.sh mobilityops-api-ci + - name: Build production web image for vulnerability scan + if: steps.scope.outputs.full == 'true' + run: | + docker build --build-arg VCS_REF="$GITHUB_SHA" \ + --tag mobilityops-web-ci frontend + - name: Production web image vulnerability scan (HIGH/CRITICAL) + if: steps.scope.outputs.full == 'true' + run: bash scripts/scan-ci-image.sh mobilityops-web-ci - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 if: steps.scope.outputs.full == 'true' with: diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index e6e037c..772f794 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -29,8 +29,8 @@ jobs: docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ -v "$PWD:/work" -w /work \ aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969 \ - image --format cyclonedx --output "mobilityops-${component}-sbom.cdx.json" \ - "mobilityops-${component}-release" + image --format cyclonedx --output "mobilityops-${component}-sbom.cdx.json" \ + "mobilityops-${component}-release" done - name: Record immutable image metadata run: | diff --git a/backend/Dockerfile b/backend/Dockerfile index d4ede03..293ea96 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -24,6 +24,9 @@ FROM runtime-base AS test COPY backend/requirements.lock ./requirements.lock RUN pip install --no-cache-dir -r requirements.lock COPY backend/tests ./tests +COPY contracts ./contracts +COPY scripts/check-contracts.py ./scripts/check-contracts.py +COPY n8n/workflows ./n8n/workflows USER app FROM runtime-base AS runtime diff --git a/frontend/Dockerfile b/frontend/Dockerfile index cf38a80..fb2d964 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -11,6 +11,9 @@ ENV VITE_API_BASE_URL=$VITE_API_BASE_URL RUN npm run build FROM nginx:1.30.4-alpine@sha256:97d490c12ba55b4946b01546d1c3ed324e8d41ab1c9fcb2a616aa470620e5b46 +# The pinned upstream image can lag Alpine security rebuilds. Apply the current +# fixes from its pinned Alpine release before shipping the runtime image. +RUN apk upgrade --no-cache ARG VCS_REF=development ARG BUILD_DATE=unknown LABEL org.opencontainers.image.title="Fleet Ops Web" \ diff --git a/frontend/e2e-live/smoke.spec.ts b/frontend/e2e-live/smoke.spec.ts index ecc28b5..778174b 100644 --- a/frontend/e2e-live/smoke.spec.ts +++ b/frontend/e2e-live/smoke.spec.ts @@ -26,6 +26,15 @@ test("non-destructive operator canary covers routes and grounded knowledge", asy await page.goto("/login"); await page.getByRole("button", { name: "Verken als Operationsmanager" }).click(); await expect(page).toHaveURL(/\/dashboard$/); + if (pageErrors.some((message) => message.toLowerCase().includes("dynamically imported module"))) { + // A deploy can replace the SPA between loading index.html and its lazy + // dashboard chunk. One clean reload must recover; a persistent chunk or + // server error is collected again and still fails the canary below. + pageErrors.length = 0; + await page.reload({ waitUntil: "domcontentloaded" }); + await expect(page).toHaveURL(/\/dashboard$/); + await expect(page.locator("main")).toBeVisible(); + } for (const [path, heading] of [ ["/vehicles", "Wagenpark"], diff --git a/scripts/scan-ci-image.sh b/scripts/scan-ci-image.sh new file mode 100644 index 0000000..abd2b6b --- /dev/null +++ b/scripts/scan-ci-image.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TARGET_IMAGE="${1:?usage: scan-ci-image.sh }" +TRIVY_IMAGE="aquasec/trivy@sha256:be1190afcb28352bfddc4ddeb71470835d16462af68d310f9f4bca710961a41e" +SAFE_NAME="$(printf '%s' "$TARGET_IMAGE" | tr -cs 'A-Za-z0-9._-' '-')" +ARCHIVE_RELATIVE="artifacts/.${SAFE_NAME}.tar" +ARCHIVE="$ROOT/$ARCHIVE_RELATIVE" +trap 'rm -f "$ARCHIVE"' EXIT + +mkdir -p "$ROOT/artifacts" +docker image inspect "$TARGET_IMAGE" >/dev/null +docker save --output "$ARCHIVE" "$TARGET_IMAGE" + +WORKSPACE_ARGS=(-v "$ROOT:/workspace:ro") +CONTAINER_ARCHIVE="/workspace/$ARCHIVE_RELATIVE" +if docker inspect "${HOSTNAME:-}" >/dev/null 2>&1; then + WORKSPACE_ARGS=(--volumes-from "$HOSTNAME") + CONTAINER_ARCHIVE="$ROOT/$ARCHIVE_RELATIVE" +fi + +docker run --rm "${WORKSPACE_ARGS[@]}" "$TRIVY_IMAGE" image \ + --input "$CONTAINER_ARCHIVE" \ + --scanners vuln \ + --severity HIGH,CRITICAL \ + --ignore-unfixed \ + --exit-code 1 \ + --format table