diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index a283f73..088acb5 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -35,46 +35,49 @@ jobs: set -e docker build -t "${IMAGE}:${GITHUB_SHA}" -t "${IMAGE}:latest" . - # --- Feature 4: Trivy vulnerability scan (gate) -------------------------- - # Install the Trivy binary from the official script (the runner reaches the - # internet; a pinned binary is more reproducible here than the - # trivy-action, which resolves from github.com/master). The scan runs - # against the locally-built image over the runner's docker.sock. - - name: Install Trivy - run: | - set -e - curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \ - | sh -s -- -b /usr/local/bin v0.58.1 - trivy --version - - # Gate: fail ci on any fixable CRITICAL/HIGH so a vulnerable image never - # gets pushed or deployed. ignore-unfixed keeps the gate real but avoids - # failing on distro CVEs with no available fix (nginx:alpine currently has - # zero fixable CRITICAL/HIGH — the gate passes clean but is genuinely - # enforcing). --scanners vuln keeps it to CVEs (no secret/misconfig noise). + # --- Feature 4: Trivy vulnerability scan (gate) + SBOM ------------------ + # Trivy runs from its official container over the runner's docker.sock, so + # there is no fragile binary install and it can scan the locally-built + # image. A shared cache + shared workdir are bind-mounted; the SBOM is + # written to the job workspace so the upload step can find it. A single + # `docker run` per action keeps the runner-in-runner path simple. - name: Trivy scan (gate CRITICAL,HIGH) run: | set -e - trivy image \ - --exit-code 1 \ - --severity CRITICAL,HIGH \ - --ignore-unfixed \ - --scanners vuln \ - --no-progress \ - "${IMAGE}:${GITHUB_SHA}" + # Gate: fail ci on any fixable CRITICAL/HIGH so a vulnerable image never + # gets pushed or deployed. ignore-unfixed keeps the gate real but avoids + # failing on distro CVEs with no available fix (nginx:alpine currently + # has zero fixable CRITICAL/HIGH — the gate passes clean but is + # genuinely enforcing). --scanners vuln keeps it to CVEs. + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v trivy-cache:/root/.cache/ \ + aquasec/trivy:0.58.1 image \ + --exit-code 1 \ + --severity CRITICAL,HIGH \ + --ignore-unfixed \ + --scanners vuln \ + --no-progress \ + "${IMAGE}:${GITHUB_SHA}" echo "trivy gate passed (no fixable CRITICAL/HIGH)" - # --- Feature 4: SBOM (CycloneDX) generation + artifact upload ----------- - name: Generate SBOM (CycloneDX) if: always() run: | set -e - trivy image \ - --format cyclonedx \ - --scanners vuln \ - --no-progress \ - --output sbom.cdx.json \ - "${IMAGE}:${GITHUB_SHA}" + # Write the SBOM to trivy's stdout and redirect it into the job + # workspace. A bind-mounted --output would land on the HOST path + # (docker.sock is the host daemon, so a sibling container's -v resolves + # host-side, not inside this job container); the stdout redirect writes + # straight into the job filesystem where the upload step can find it. + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v trivy-cache:/root/.cache/ \ + aquasec/trivy:0.58.1 image \ + --format cyclonedx \ + --scanners vuln \ + --no-progress \ + "${IMAGE}:${GITHUB_SHA}" > sbom.cdx.json echo "SBOM components: $(python3 -c 'import json; print(len(json.load(open("sbom.cdx.json")).get("components",[])))' 2>/dev/null || echo n/a)" ls -l sbom.cdx.json