-
Notifications
You must be signed in to change notification settings - Fork 1k
224 lines (203 loc) · 9.86 KB
/
Copy pathadvisory.yml
File metadata and controls
224 lines (203 loc) · 9.86 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
name: Advisory Tests
# START OF COMMON SECTION
on:
push:
branches: [ 'master', 'main', 'release/**' ]
pull_request:
branches: [ '*' ]
# Defence-in-depth: this workflow only reads the tree and validates generated
# advisories (no API writes, no git push, no release upload), so pin the token
# to read-only per GitHub's supply-chain hardening guidance.
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# END OF COMMON SECTION
jobs:
# Tier 1 - pure-Python unit + semantic tests for scripts/gen-advisory.
# No build, no pip deps. Runs in seconds and is the cheapest gate for the
# record->model logic and the CSAF semantic invariants (every product_id
# defined/used, no contradicting status, flags only on not-affected
# products, no cvss_v4 in CSAF 2.0 scores, canonical CWE names, ...).
unit:
name: gen-advisory unit tests
if: github.repository_owner == 'wolfssl'
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Syntax check
run: python3 -m py_compile scripts/gen-advisory
- name: Unit tests
run: python3 -W error::ResourceWarning -m unittest scripts/test_gen_advisory.py -v
# Tier 2 - format-level validation: generate per-CVE and bundled advisories
# from the committed CVE fixtures + example overlay, then validate the
# CycloneDX VEX against the 1.6 strict schema (same validator the SBOM
# workflow uses) and the VEX overlay against its JSON Schema. Also pins
# SOURCE_DATE_EPOCH reproducibility for both emitters.
schema:
name: advisory schema validation
if: github.repository_owner == 'wolfssl'
runs-on: ubuntu-24.04
needs: unit
timeout-minutes: 10
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install validators
# cyclonedx-bom provides the CycloneDX 1.6 strict JSON validator (same
# pin as .github/workflows/sbom.yml); jsonschema validates the VEX
# overlay against scripts/advisory-vex-overlay.schema.json. Pinned so
# a validator release cannot silently change what "valid" means.
run: |
python3 -m pip install --user --upgrade pip
python3 -m pip install --user 'cyclonedx-bom==7.*' 'jsonschema==4.*'
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Overlay validates against its JSON Schema
run: |
python3 - <<'PY'
import json, jsonschema
schema = json.load(open('scripts/advisory-vex-overlay.schema.json'))
overlay = json.load(open('scripts/advisory-vex-overlay.example.json'))
jsonschema.Draft202012Validator.check_schema(schema)
jsonschema.Draft202012Validator(schema).validate(overlay)
print('OK: example overlay matches advisory-vex-overlay.schema.json')
PY
- name: Generate advisories (per-CVE + bundled)
# Mirrors how a release would be cut: one document per CVE, plus a
# bundled per-release advisory carrying both. SOURCE_DATE_EPOCH makes
# the run deterministic for the reproducibility check below.
run: |
mkdir -p /tmp/adv
for id in CVE-2026-5501 CVE-2026-5778 CVE-2026-5999; do
SOURCE_DATE_EPOCH=1700000000 \
python3 scripts/gen-advisory \
--cve-record "scripts/testdata/$id.json" \
--vex-overlay scripts/advisory-vex-overlay.example.json \
--csaf-out "/tmp/adv/$id.csaf.json" \
--cdx-vex-out "/tmp/adv/$id.cdx.json"
done
SOURCE_DATE_EPOCH=1700000000 \
python3 scripts/gen-advisory \
--cve-record scripts/testdata/CVE-2026-5501.json \
--cve-record scripts/testdata/CVE-2026-5778.json \
--vex-overlay scripts/advisory-vex-overlay.example.json \
--advisory-id wolfSSL-SA-5.9.1 \
--csaf-out /tmp/adv/wolfSSL-SA-5.9.1.csaf.json \
--cdx-vex-out /tmp/adv/wolfSSL-SA-5.9.1.cdx.json
- name: CycloneDX VEX validates per CycloneDX 1.6 strict schema
run: |
python3 - <<'PY'
import glob, sys
from cyclonedx.validation.json import JsonStrictValidator
from cyclonedx.schema import SchemaVersion
v = JsonStrictValidator(SchemaVersion.V1_6)
paths = sorted(glob.glob('/tmp/adv/*.cdx.json'))
assert paths, 'no CycloneDX VEX documents were generated'
for p in paths:
errs = v.validate_str(open(p).read())
if errs:
print(f'INVALID: {p}: {errs}', file=sys.stderr)
sys.exit(1)
print(f'OK: {p}')
PY
- name: Reproducibility - two runs are byte-identical
run: |
mkdir -p /tmp/adv-r2
SOURCE_DATE_EPOCH=1700000000 \
python3 scripts/gen-advisory \
--cve-record scripts/testdata/CVE-2026-5501.json \
--cve-record scripts/testdata/CVE-2026-5778.json \
--vex-overlay scripts/advisory-vex-overlay.example.json \
--advisory-id wolfSSL-SA-5.9.1 \
--csaf-out /tmp/adv-r2/wolfSSL-SA-5.9.1.csaf.json \
--cdx-vex-out /tmp/adv-r2/wolfSSL-SA-5.9.1.cdx.json
diff /tmp/adv/wolfSSL-SA-5.9.1.csaf.json \
/tmp/adv-r2/wolfSSL-SA-5.9.1.csaf.json
diff /tmp/adv/wolfSSL-SA-5.9.1.cdx.json \
/tmp/adv-r2/wolfSSL-SA-5.9.1.cdx.json
- name: Default/batch path matches `make advisory`
# No record flags: gen-advisory falls back to the canonical
# advisories/ tree (the exact inputs `make advisory` feeds it via
# --records-dir/--vex-overlay), proving the script and the build target
# are interchangeable and that the committed real records + overlay
# generate and validate.
run: |
python3 scripts/gen-advisory --out-dir /tmp/adv-default
python3 - <<'PY'
import glob, sys
from cyclonedx.validation.json import JsonStrictValidator
from cyclonedx.schema import SchemaVersion
v = JsonStrictValidator(SchemaVersion.V1_6)
paths = sorted(glob.glob('/tmp/adv-default/*.cdx.json'))
assert paths, 'batch mode produced no CycloneDX documents'
for p in paths:
errs = v.validate_str(open(p).read())
if errs:
print(f'INVALID: {p}: {errs}', file=sys.stderr)
sys.exit(1)
print(f'OK: {p}')
PY
- name: Upload generated advisories
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: advisories-${{ github.sha }}
path: /tmp/adv/*.json
if-no-files-found: warn
retention-days: 90
# Tier 2 - CSAF 2.0 conformance: the real gate. JSON-schema validity is
# necessary but not sufficient; CSAF defines mandatory tests (section 6.1.*)
# -- CVSS/vector consistency, contradicting product status, product_id
# defined/used, tracking.version vs revision_history, CWE name match, ... --
# that a bare schema pass accepts. scripts/csaf_validate.mjs runs the strict
# 2.0 schema + all mandatory tests via the Secvisogram reference
# implementation (bundles every schema incl. the first.org CVSS schemas, so
# it is fully offline once installed).
csaf-conformance:
name: CSAF 2.0 mandatory tests
if: github.repository_owner == 'wolfssl'
runs-on: ubuntu-24.04
needs: unit
timeout-minutes: 10
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: '20'
- name: Install csaf-validator-lib (pinned)
# Pinned: csaf-validator-lib implements the CSAF mandatory tests, and
# an unpinned upgrade could change pass/fail semantics under us. The
# bare `csaf-validator-lib` name on npm is an unrelated placeholder;
# the reference implementation is the @secvisogram scope.
run: npm install --no-save @secvisogram/csaf-validator-lib@2.0.25
- name: Generate CSAF advisories (per-CVE + bundled)
run: |
mkdir -p /tmp/adv
for id in CVE-2026-5501 CVE-2026-5778 CVE-2026-5999; do
python3 scripts/gen-advisory \
--cve-record "scripts/testdata/$id.json" \
--vex-overlay scripts/advisory-vex-overlay.example.json \
--csaf-out "/tmp/adv/$id.csaf.json"
done
python3 scripts/gen-advisory \
--cve-record scripts/testdata/CVE-2026-5501.json \
--cve-record scripts/testdata/CVE-2026-5778.json \
--vex-overlay scripts/advisory-vex-overlay.example.json \
--advisory-id wolfSSL-SA-5.9.1 \
--csaf-out /tmp/adv/wolfSSL-SA-5.9.1.csaf.json
- name: csaf_validate.mjs runner self-test (exit-code contract)
# Pins the gate's own logic (summarize + 0/1/2 exit codes) so a
# csaf-validator-lib bump that changed the result shape cannot silently
# turn the conformance gate into a no-op that still exits 0. Passes a
# known-good generated document to also assert the exit-0 path.
run: node scripts/test_csaf_validate.mjs /tmp/adv/CVE-2026-5501.csaf.json
- name: CSAF strict schema + mandatory tests
run: node scripts/csaf_validate.mjs /tmp/adv/*.csaf.json
- name: CSAF default/batch path (canonical advisories/ tree)
# Same conformance gate, but driven through the zero-argument default
# path `make advisory` uses, against the committed real records +
# advisories/vex-overlay.json.
run: |
python3 scripts/gen-advisory --out-dir /tmp/adv-default
node scripts/csaf_validate.mjs /tmp/adv-default/*.csaf.json