-
Notifications
You must be signed in to change notification settings - Fork 0
275 lines (258 loc) · 12.8 KB
/
Copy pathdependency-audit.yml
File metadata and controls
275 lines (258 loc) · 12.8 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
266
267
268
269
270
271
272
273
274
275
name: dependency-audit
# todo8 5.1 / todo7 V-14b. Before this workflow, `safety scan` was run by hand on
# a maintainer laptop and nothing checked the Rust tree at all. A dependency
# advisory published between two releases was therefore invisible until someone
# remembered to look.
#
# BLOCKING POLICY (see docs/DEPENDENCY_POLICY.md for the full rationale):
#
# * pull_request / push / schedule -> NON-BLOCKING but LOUD. Findings are
# emitted as `::warning` annotations so they appear on the Checks tab and in
# the PR summary, but they do not fail the run. A new upstream CVE must not
# be able to wedge an unrelated PR at 03:00.
# * called from release-build.yml with `strict: true` -> BLOCKING. Nothing is
# published to PyPI while a known advisory or a licence-policy violation is
# outstanding. Four yanks in one month (0.6.3/0.6.4/0.6.5/0.6.7) is the
# evidence that the release path is where the gate belongs.
#
# The one deliberate exception is the Python 3.9 leg, which is warn-only even in
# strict mode: `pyproject.toml` pins `pytest>=8.4.2` on 3.9 because pytest 9.x
# requires >=3.10, and that residual advisory is an accepted, dated exception
# recorded in docs/DEPENDENCY_POLICY.md. It must stay VISIBLE, so the leg runs;
# it must not block a release, so it does not enforce.
#
# Note that no job here needs the proprietary Rust core: `cargo audit` and
# `cargo deny` work from `Cargo.lock` and the manifest (only `src/lib.rs` is
# tracked, which is all `cargo metadata` requires). That means this workflow
# also runs to completion on a fork PR, where the RUST_SRC_B64_* secrets are
# unavailable.
on:
pull_request:
branches: [main, develop]
push:
branches: [main]
schedule:
# Mondays 06:17 UTC. Advisories are published on upstream's schedule, not
# ours, so a code-change-only trigger is not sufficient coverage.
- cron: "17 6 * * 1"
workflow_dispatch:
workflow_call:
inputs:
strict:
description: "Fail the run on any advisory or licence violation (used by release-build before publishing)"
type: boolean
required: false
default: false
secrets:
# Declared explicitly rather than relying on `secrets: inherit` at the
# call site: the caller is release-build.yml, which holds the twelve
# RUST_SRC_B64_* chunks of the proprietary core, and this workflow has no
# business seeing them.
SAFETY_API_KEY:
required: false
permissions:
contents: read
jobs:
cargo:
name: Rust advisories + licence policy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
# Prebuilt binaries. `cargo install cargo-audit cargo-deny --locked`
# would work identically but costs ~8 minutes of compile on every run.
#
# cargo-deny is PINNED to a major.minor: the deny.toml schema changed at
# 0.16 (the `[licenses] copyleft` / `allow-osi-fsf-free` / `default` keys
# were replaced by an explicit `allow` list), and .github/deny.toml is
# written against the 0.18 schema. An unpinned upgrade would turn a
# schema change into a failed release gate.
- uses: taiki-e/install-action@v2
with:
tool: cargo-audit,cargo-deny@0.18
# Known vulnerabilities and YANKED crates, straight from Cargo.lock.
# Plain `cargo audit` (no `--deny warnings`): it exits non-zero for
# vulnerabilities and yanked crates, and treats RUSTSEC informational
# advisories (unmaintained/unsound/notice) as warnings. That split is
# intentional — an unmaintained transitive crate is a planning problem,
# not a reason to refuse to ship a security fix.
- name: cargo audit (vulnerabilities + yanked crates)
env:
STRICT: ${{ inputs.strict == true }}
run: |
set +e
cargo audit
rc=$?
set -e
if [ "$rc" -ne 0 ]; then
if [ "$STRICT" = "true" ]; then
echo "::error title=cargo audit::RustSec advisories in Cargo.lock. Release blocked. See docs/DEPENDENCY_POLICY.md."
exit "$rc"
fi
echo "::warning title=cargo audit::RustSec advisories in Cargo.lock. Not blocking this run, but a release tag WILL block. See docs/DEPENDENCY_POLICY.md."
fi
# Licence policy is a release gate, not advice. QECTOR ships under
# LicenseRef-QECTOR-Source-Available; a transitive GPL/AGPL dependency
# would be a live legal problem, and NOTICE.md would silently go stale.
# `--all-features` so the grpc/opencl/prometheus trees are covered too,
# not just the feature set the published wheel happens to use.
- name: cargo deny (licences, bans, sources)
env:
STRICT: ${{ inputs.strict == true }}
run: |
set +e
# `--config` belongs to the `check` subcommand in cargo-deny 0.18;
# `--all-features` stays global. It used to sit before `check`, which
# 0.18 rejects with "unexpected argument '--config' found" and exit 2.
cargo deny --all-features check --config .github/deny.toml licenses bans sources
rc=$?
set -e
# Exit 2 is clap: cargo deny never ran. That is NOT a policy warning,
# and downgrading it to one is how a broken invocation went unnoticed -
# non-strict runs reported success while the licence gate executed
# nothing at all. A gate that cannot run is always a hard failure,
# in every mode; only an actual policy violation (exit 1) is subject
# to `strict`.
if [ "$rc" -gt 1 ]; then
echo "::error title=cargo deny::cargo deny failed to execute (exit $rc) - the licence/bans/sources gate did NOT run. This is never downgraded to a warning."
exit "$rc"
fi
if [ "$rc" -ne 0 ]; then
if [ "$STRICT" = "true" ]; then
echo "::error title=cargo deny::Licence/source policy violation. Release blocked. Update .github/deny.toml AND NOTICE.md only after a deliberate legal review."
exit "$rc"
fi
echo "::warning title=cargo deny::Licence/source policy violation. Not blocking this run, but a release tag WILL block. See docs/DEPENDENCY_POLICY.md and NOTICE.md."
fi
# Informational advisories (unmaintained / unsound / notice). Never
# blocking, in any mode — these are inputs to the quarterly review in
# docs/DEPENDENCY_POLICY.md, not release gates.
- name: cargo deny (informational advisories, never blocking)
run: |
set +e
# Same 0.18 flag-position fix as the gating step above.
cargo deny --all-features check --config .github/deny.toml advisories
rc=$?
set -e
# This leg never blocks, by design. But distinguish "advisories found"
# from "the scan did not run": the latter reported a clean warning for
# as long as the invocation was malformed, which is indistinguishable
# from good news in the log.
if [ "$rc" -gt 1 ]; then
echo "::warning title=cargo deny advisories::cargo deny failed to execute (exit $rc) - the informational advisory scan did NOT run. Not blocking, but the absence of findings below means nothing."
elif [ "$rc" -ne 0 ]; then
echo "::warning title=cargo deny advisories::Informational RustSec advisories present (unmaintained/unsound/notice). Advisory only — never blocks. Triage at the quarterly review."
fi
python:
name: Python advisories (py${{ matrix.target-python }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
# 3.9 is the floor `requires-python` allows and the leg that carries
# the accepted pytest exception. Warn-only by construction.
- { target-python: "3.9", enforce: false }
# 3.13 is the newest interpreter in the tests.yml matrix and the one
# the release gate actually enforces against.
- { target-python: "3.13", enforce: true }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
# uv is here for one specific capability pip does not have:
# `--resolution lowest-direct`. todo7 V-14 found 45 suppressed advisories
# precisely because the *floors* in pyproject.toml admitted ancient
# releases while the maintainer's own environment happened to hold new
# ones. Auditing whatever pip resolves today would have reported zero and
# missed all 45. Auditing the lowest versions our own floors permit is the
# check that actually tests the policy.
- uses: astral-sh/setup-uv@v6
- name: Collect declared dependencies from pyproject.toml
run: |
mkdir -p .audit-work
python - <<'PY'
import pathlib, tomllib
data = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8"))
project = data["project"]
reqs = list(project.get("dependencies", []))
for extra_reqs in project.get("optional-dependencies", {}).values():
reqs.extend(extra_reqs)
seen, ordered = set(), []
for req in reqs:
if req not in seen:
seen.add(req)
ordered.append(req)
out = pathlib.Path(".audit-work/declared.txt")
out.write_text("\n".join(ordered) + "\n", encoding="utf-8")
print(f"{len(ordered)} distinct declared requirements (runtime + every extra)")
print(out.read_text(encoding="utf-8"))
PY
- name: Resolve the lowest versions our floors admit
env:
TARGET_PY: ${{ matrix.target-python }}
run: |
set +e
uv pip compile .audit-work/declared.txt \
--resolution lowest-direct \
--python-version "$TARGET_PY" \
--no-annotate \
-o .audit-work/floor-${TARGET_PY}.txt
rc=$?
set -e
if [ "$rc" -ne 0 ]; then
echo "::warning title=uv resolve::Could not resolve the floor set for Python ${TARGET_PY}. The audit below has nothing to scan; investigate before trusting a green result."
: > ".audit-work/floor-${TARGET_PY}.txt"
fi
echo "--- resolved floor set (Python ${TARGET_PY}) ---"
cat ".audit-work/floor-${TARGET_PY}.txt"
- name: pip-audit against the floor set
env:
TARGET_PY: ${{ matrix.target-python }}
# `inputs.strict` is null for push/PR/schedule/dispatch, so this is
# false unless release-build.yml called us. The 3.9 leg never
# enforces (accepted pytest exception, see the header comment).
STRICT: ${{ inputs.strict == true && matrix.enforce }}
run: |
python -m pip install --upgrade pip
pip install pip-audit
if [ ! -s ".audit-work/floor-${TARGET_PY}.txt" ]; then
echo "::warning title=pip-audit::Floor set is empty for Python ${TARGET_PY}; nothing audited."
exit 0
fi
set +e
pip-audit --requirement ".audit-work/floor-${TARGET_PY}.txt" --strict --progress-spinner=off
rc=$?
set -e
if [ "$rc" -ne 0 ]; then
if [ "$STRICT" = "true" ]; then
echo "::error title=pip-audit::Advisories reachable at the declared dependency floors (Python ${TARGET_PY}). Release blocked. Raise the floor per docs/DEPENDENCY_POLICY.md."
exit "$rc"
fi
echo "::warning title=pip-audit::Advisories reachable at the declared dependency floors (Python ${TARGET_PY}). Not blocking this run. If this is the accepted py3.9 pytest exception it is expected; anything else must be fixed by raising the floor."
fi
# `safety scan` is the tool that produced the original 45-finding report in
# todo7 V-14. The modern CLI requires an account, so it runs only where a
# SAFETY_API_KEY secret exists and is advisory everywhere — pip-audit above
# is the gate. This step is a no-op (not a failure) on forks and on any
# checkout without the secret.
- name: safety scan (optional, advisory only)
env:
SAFETY_API_KEY: ${{ secrets.SAFETY_API_KEY }}
TARGET_PY: ${{ matrix.target-python }}
run: |
if [ -z "${SAFETY_API_KEY}" ]; then
echo "SAFETY_API_KEY not configured; skipping safety scan (pip-audit above is the gate)."
exit 0
fi
pip install safety
set +e
safety --key "${SAFETY_API_KEY}" scan --file ".audit-work/floor-${TARGET_PY}.txt"
rc=$?
set -e
if [ "$rc" -ne 0 ]; then
echo "::warning title=safety::safety scan reported findings (advisory only; pip-audit is the gate)."
fi