feat: complete Firecracker preview rollout coverage #589
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Supply Chain Scan | |
| # Deterministic supply-chain gate that runs on every pull request. | |
| # | |
| # Uses the gh-aw compile pipeline's opt-in Anchore scanners (Syft/Grype/Grant) | |
| # against the container images recorded in the `gh-aw-manifest` headers of the | |
| # compiled `.github/workflows/*.lock.yml` files. | |
| # | |
| # Scanner images are pinned to reviewed digests so scan behaviour is | |
| # reproducible: | |
| # --syft versioned via gh-aw (generates SBOMs) | |
| # grype anchore/grype@sha256:fd4ab4d... (v0.116.0) | |
| # grant anchore/grant@sha256:17246361... (v0.6.8) | |
| # | |
| # Blocking vs. report-only: | |
| # * BLOCKING gate = "Build and scan PR container images". It rebuilds the | |
| # first-party images (agent, api-proxy, cli-proxy, gh-aw-node) from the | |
| # Dockerfiles in the PR and fails on any High/Critical CVE. This is the | |
| # only step that reflects the changes in a PR, so it is the one that gates merges. | |
| # * REPORT-ONLY = the Grype and Grant scans of the `gh-aw-manifest` digests. | |
| # Those digests are IMMUTABLE already-published first-party images plus | |
| # upstream third-party images (playwright, github-mcp-server, mcpg, ...). | |
| # A PR cannot rebuild or patch them, so their findings are surfaced as | |
| # warnings but never fail the job (that would make the gate permanently red | |
| # until the next release / upstream bump). | |
| # | |
| # All scanners run via Docker (pre-installed on `ubuntu-latest`). | |
| # | |
| # To make this a *required* check, add | |
| # "Supply Chain Scan / Compile + Syft/Grype/Grant" | |
| # to the branch protection rule / ruleset for the default branch. | |
| on: | |
| pull_request: | |
| push: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| packages: read | |
| concurrency: | |
| group: supply-chain-scan-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| scan: | |
| name: Compile + Syft/Grype/Grant | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 40 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Install gh-aw | |
| uses: github/gh-aw-actions/setup-cli@598bade62fe1591518c081cd854cb931a2e8547f # v0.83.1 | |
| with: | |
| version: v0.83.1 | |
| - name: Compile + generate SBOMs (Syft) | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: gh aw compile --syft .github/workflows/*.md | |
| - name: Authenticate to GHCR for image scanning | |
| run: echo "${{ github.token }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin | |
| # Persist Grype's vulnerability DB across runs. The unique run_id key never | |
| # hits exactly, so each run restores the newest prior DB via restore-keys, | |
| # refreshes it if a newer DB exists (see `db update` below), then saves the | |
| # result under a fresh key. This keeps the DB current while avoiding a full | |
| # cold download on every run. | |
| # | |
| # Using separate restore + save steps so the DB is saved immediately after | |
| # a successful warm-up regardless of whether later Grype scans flag CVEs | |
| # (Grype exits nonzero on --fail-on high findings, which would prevent the | |
| # combined actions/cache post-step from uploading the warmed DB). | |
| - name: Restore Grype vulnerability DB cache | |
| id: grype-cache-restore | |
| uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ${{ runner.temp }}/grype-db | |
| key: grype-db-v6-${{ github.run_id }} | |
| restore-keys: | | |
| grype-db-v6- | |
| - name: Warm Grype vulnerability DB | |
| env: | |
| GRYPE_DB: ${{ runner.temp }}/grype-db | |
| run: | | |
| mkdir -p "$GRYPE_DB" | |
| # Warm (or refresh) the vulnerability DB exactly once. Without a shared | |
| # cache, every per-image `docker run --rm` would re-download the full DB | |
| # (~1 min each), which is the dominant cost of this job. | |
| docker run --rm \ | |
| -v "$GRYPE_DB:/cache" \ | |
| -e GRYPE_DB_CACHE_DIR=/cache \ | |
| anchore/grype@sha256:fd4ab4d1042b522c896e73bdf09ab8bf384fa417df99d6dd0d6e1008c7e7c821 \ | |
| db update | |
| - name: Save Grype vulnerability DB cache | |
| if: always() | |
| uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ${{ runner.temp }}/grype-db | |
| key: grype-db-v6-${{ github.run_id }} | |
| - name: Scan published & third-party images for CVEs (report-only, Grype v0.116.0) | |
| # Scans the immutable image digests recorded in the compiled .lock.yml | |
| # gh-aw-manifest headers: already-published first-party images plus | |
| # upstream third-party images that a PR cannot rebuild. Findings are | |
| # REPORTED as warnings but do NOT fail the job. The blocking CVE gate is | |
| # the "Build and scan PR container images" step below. | |
| # anchore/grype@sha256:fd4ab4d1042b522c896e73bdf09ab8bf384fa417df99d6dd0d6e1008c7e7c821 = v0.116.0 | |
| env: | |
| GRYPE_DB: ${{ runner.temp }}/grype-db | |
| run: | | |
| python3 - <<'PYEOF' | |
| import json, os, re, glob, subprocess, sys | |
| images = set() | |
| for path in glob.glob('.github/workflows/*.lock.yml'): | |
| m = re.search(r'# gh-aw-manifest: (\{.*\})', open(path).read()) | |
| if m: | |
| for c in json.loads(m.group(1)).get('containers', []): | |
| images.add(c.get('pinned_image', c.get('image'))) | |
| home = os.path.expanduser('~') | |
| db_cache = os.environ['GRYPE_DB'] | |
| cwd = os.getcwd() | |
| flagged = [] | |
| for image in sorted(images): | |
| print(f'::group::grype {image}', flush=True) | |
| r = subprocess.run([ | |
| 'docker', 'run', '--rm', | |
| '-v', f'{home}/.docker/config.json:/root/.docker/config.json:ro', | |
| '-v', f'{db_cache}:/cache', | |
| '-e', 'GRYPE_DB_CACHE_DIR=/cache', | |
| '-e', 'GRYPE_DB_AUTO_UPDATE=false', | |
| '-e', 'GRYPE_CHECK_FOR_APP_UPDATE=false', | |
| '-v', f'{cwd}/.grype.yaml:/root/.grype.yaml:ro', | |
| 'anchore/grype@sha256:fd4ab4d1042b522c896e73bdf09ab8bf384fa417df99d6dd0d6e1008c7e7c821', | |
| f'registry:{image}', '--fail-on', 'high', | |
| '--config', '/root/.grype.yaml', | |
| ]) | |
| print('::endgroup::', flush=True) | |
| if r.returncode != 0: | |
| flagged.append(image) | |
| if flagged: | |
| print(f'::warning::{len(flagged)} published/third-party image(s) have High/Critical CVEs ' | |
| f'(report-only, not blocking): ' + ', '.join(flagged), flush=True) | |
| # Report-only: never fail the job on immutable published / third-party images. | |
| sys.exit(0) | |
| PYEOF | |
| - name: Build and scan PR container images (BLOCKING gate, Grype v0.116.0) | |
| # Builds every production image from the Dockerfiles in | |
| # this PR and scans them with --fail-on high. This is the BLOCKING supply-chain | |
| # gate: it is the only scan that reflects the CVE fixes in a PR, so a | |
| # High/Critical finding here fails the job. | |
| env: | |
| GRYPE_DB: ${{ runner.temp }}/grype-db | |
| run: | | |
| python3 - <<'PYEOF' | |
| import os, subprocess, sys | |
| cwd = os.getcwd() | |
| db_cache = os.environ['GRYPE_DB'] | |
| containers = [ | |
| ('agent', 'containers/agent', []), | |
| ('api-proxy', 'containers/api-proxy', []), | |
| ('cli-proxy', 'containers/cli-proxy', []), | |
| ('gh-aw-node', 'containers/gh-aw-node', []), | |
| ('enclave-script', 'containers', ['-f', 'containers/enclave/Dockerfile', '--target', 'enclave-script']), | |
| ('enclave-agent', 'containers', ['-f', 'containers/enclave/Dockerfile', '--target', 'enclave-agent']), | |
| ('enclave-mcp-server', 'containers', ['-f', 'containers/enclave/Dockerfile', '--target', 'enclave-mcp-server']), | |
| ] | |
| rc = 0 | |
| for name, context, build_args in containers: | |
| tag = f'awf-pr-scan-{name}:pr' | |
| tar = f'/tmp/awf-pr-scan-{name}.tar' | |
| print(f'::group::docker build {name}', flush=True) | |
| b = subprocess.run(['docker', 'build', *build_args, '-t', tag, context]) | |
| print('::endgroup::', flush=True) | |
| if b.returncode != 0: | |
| print(f'::error::docker build failed for {name}', flush=True) | |
| rc = b.returncode | |
| continue | |
| subprocess.run(['docker', 'save', '-o', tar, tag], check=True) | |
| print(f'::group::grype {name} (PR build)', flush=True) | |
| r = subprocess.run([ | |
| 'docker', 'run', '--rm', | |
| '-v', f'{tar}:{tar}:ro', | |
| '-v', f'{db_cache}:/cache', | |
| '-e', 'GRYPE_DB_CACHE_DIR=/cache', | |
| '-e', 'GRYPE_DB_AUTO_UPDATE=false', | |
| '-e', 'GRYPE_CHECK_FOR_APP_UPDATE=false', | |
| '-v', f'{cwd}/.grype.yaml:/root/.grype.yaml:ro', | |
| 'anchore/grype@sha256:fd4ab4d1042b522c896e73bdf09ab8bf384fa417df99d6dd0d6e1008c7e7c821', | |
| f'docker-archive:{tar}', '--fail-on', 'high', | |
| '--config', '/root/.grype.yaml', | |
| ]) | |
| print('::endgroup::', flush=True) | |
| if r.returncode != 0: | |
| rc = r.returncode | |
| sys.exit(rc) | |
| PYEOF | |
| - name: Check image licenses (report-only, Grant v0.6.8) | |
| # License findings on the immutable published / third-party manifest | |
| # digests are reported as warnings but do NOT fail the job (same | |
| # rationale as the report-only CVE scan above). | |
| # anchore/grant@sha256:172463611795f43b77302cdfbd7b3f81295492a7330e0820cfe41c3674920237 = v0.6.8 | |
| run: | | |
| python3 - <<'PYEOF' | |
| import json, os, re, glob, subprocess, sys | |
| images = set() | |
| for path in glob.glob('.github/workflows/*.lock.yml'): | |
| m = re.search(r'# gh-aw-manifest: (\{.*\})', open(path).read()) | |
| if m: | |
| for c in json.loads(m.group(1)).get('containers', []): | |
| images.add(c.get('pinned_image', c.get('image'))) | |
| home = os.path.expanduser('~') | |
| cwd = os.getcwd() | |
| flagged = [] | |
| for image in sorted(images): | |
| print(f'::group::grant {image}', flush=True) | |
| r = subprocess.run([ | |
| 'docker', 'run', '--rm', | |
| '-v', f'{home}/.docker/config.json:/root/.docker/config.json:ro', | |
| '-v', f'{cwd}/.grant.yaml:/tmp/grant.yaml', | |
| 'anchore/grant@sha256:172463611795f43b77302cdfbd7b3f81295492a7330e0820cfe41c3674920237', | |
| 'check', '-c', '/tmp/grant.yaml', image, | |
| ]) | |
| print('::endgroup::', flush=True) | |
| if r.returncode != 0: | |
| flagged.append(image) | |
| if flagged: | |
| print(f'::warning::{len(flagged)} image(s) have license findings ' | |
| f'(report-only, not blocking): ' + ', '.join(flagged), flush=True) | |
| # Report-only: never fail the job on immutable published / third-party images. | |
| sys.exit(0) | |
| PYEOF |