Skip to content

feat: complete Firecracker preview rollout coverage #589

feat: complete Firecracker preview rollout coverage

feat: complete Firecracker preview rollout coverage #589

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