# Container image vulnerability scan (advisory) # # Trivy builds the application image and scans it for known-vulnerable OS and # Python packages. Advisory only -- it reports findings to the repo's Security # tab without blocking a merge, because the image inevitably contains # already-known CVEs in upstream packages that are not this project's bug. # # Split from the Dockerfile lint (container-scan.yml) for two reasons: # # - Least privilege. The image build runs Dockerfile instructions, which on a # pull request are attacker-influenceable. That path (the `scan` job) is # held to a read-only token and never publishes results. Only `publish`, # which runs on push to main (curated, fast-forwarded from reviewed dev), # gets security-events:write to upload SARIF. # - Cost. Docs-only changes do not rebuild the image (paths-ignore below), # matching docker-publish.yml. hadolint stays on the broad trigger in # container-scan.yml so the blocking gate always reports. name: Container scan (Trivy) on: pull_request: paths-ignore: - '**.md' - 'docs/**' - '.github/ISSUE_TEMPLATE/**' push: branches: [main] paths-ignore: - '**.md' - 'docs/**' - '.github/ISSUE_TEMPLATE/**' workflow_dispatch: permissions: {} concurrency: group: container-trivy-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: # Pull requests and manual runs: build and scan under a read-only token. # The build executes PR-supplied Dockerfile instructions, so this job must # not hold any write scope, and it does not upload to the Security tab. scan: name: Trivy (image scan, advisory) if: github.event_name != 'push' runs-on: ubuntu-latest # Advisory: a CVE in an upstream package must not block a PR. continue-on-error: true permissions: contents: read steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Set up Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 # Build without pushing so a broken Dockerfile is caught here, and the # exact image we ship is what gets scanned. - name: Build image uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . push: false load: true tags: odysseus:ci - name: Scan image with Trivy uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: odysseus:ci format: table ignore-unfixed: true env: # Pin the vuln DB source to GHCR to avoid rate-limited Docker Hub # mirrors that flake on shared runners. TRIVY_DB_REPOSITORY: ghcr.io/aquasecurity/trivy-db:2 # Push to main only: build, scan, and publish SARIF to the Security tab. # This is the only path that runs trusted code, so it is the only one granted # security-events:write. publish: name: Trivy (image scan + SARIF upload) if: github.event_name == 'push' runs-on: ubuntu-latest continue-on-error: true permissions: contents: read security-events: write # upload SARIF to the Security tab steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Set up Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Build image uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . push: false load: true tags: odysseus:ci - name: Scan image with Trivy uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: odysseus:ci format: sarif output: trivy-results.sarif ignore-unfixed: true env: TRIVY_DB_REPOSITORY: ghcr.io/aquasecurity/trivy-db:2 - name: Upload Trivy results uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 with: sarif_file: trivy-results.sarif category: trivy-image