-
Notifications
You must be signed in to change notification settings - Fork 62
265 lines (248 loc) · 11.9 KB
/
Copy pathsupply-chain-scan.yml
File metadata and controls
265 lines (248 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
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: Checkout enclave-compatible gh-aw compiler
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: github/gh-aw
ref: 1cf55bdc4f19e5f371b1e7fe888df649695ee48a
path: .tmp/gh-aw-compiler
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: .tmp/gh-aw-compiler/go.mod
cache: false
- name: Install enclave-compatible gh-aw
run: |
mkdir -p "$HOME/.local/share/gh/extensions/gh-aw"
go build \
-C .tmp/gh-aw-compiler \
-ldflags="-X main.version=v0.87.5-108-g1cf55bdc4f -X main.isRelease=true" \
-o "$HOME/.local/share/gh/extensions/gh-aw/gh-aw" \
./cmd/gh-aw
rm -rf .tmp/gh-aw-compiler
gh aw version
- 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