Repository navigation
164 lines (146 loc) · 7.93 KB
/
Copy pathsecurity.yml
File metadata and controls
164 lines (146 loc) · 7.93 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
# Three gates that block a merge, and none of them needs a cargo output.
#
# That is why they live here rather than in ci.yml. The header of that file already names this as
# the split worth making: a second job running only the checks that need nothing built, in
# parallel. All three run on Linux, where the scanners are at home and where a Windows runner
# would buy nothing - they read source and metadata, never a binary.
#
# 🔴 THE VERDICT IS NOT THE TOOL'S EXIT CODE, in any job. Each script reads the machine-readable
# report and decides, and each treats a report it cannot read as a failure. The reasons are written
# at the top of each script, and they are not stylistic - measured on 2026-09-08 with semgrep
# 1.175.0, `semgrep scan` pointed at a config that does not exist exits ZERO and writes a JSON file
# saying nothing was found. Every signal a caller would normally trust says the code is clean.
# OSV-Scanner has the same shape: a clean scan and a scan that read nothing write the same empty
# report (measured 2026-10-14, 2.7.0), which is why its gate counts what was read.
name: Security
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: security-${{ github.ref }}
cancel-in-progress: true
jobs:
semgrep:
name: Semgrep
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: '3.14'
# Pinned, in .github/semgrep/requirements.txt, so that Dependabot can propose the next version
# (its pip entry). A scanner that updates itself changes the verdict without anybody changing
# the code, which is the one thing a merge gate must not do. Bumping this is a deliberate act,
# and it means measuring again - `p/default` against this tree, and against the release
# workflows as they were before commit 3b94c4e, where it must still report six.
#
# The numbers above were measured with 1.175.0. The move to 1.179.0 (2026-10-14) is measured
# by this job against the tree of the pull request that made it, and NOT against the release
# workflows before 3b94c4e - that second measurement is still owed.
- name: Install the scanner
run: pip install -r .github/semgrep/requirements.txt
- name: Scan
run: |
set +e
semgrep scan --config p/default --metrics=off --json --output semgrep.json
code=$?
set -e
echo "semgrep exit code: $code"
# Measured: this is zero whatever the scan finds, and zero for a config that does not
# exist. So it decides nothing and the verdict belongs to the script below. A code above
# one is still treated as a hard failure, in case a later version starts using it.
if [ "$code" -gt 1 ]; then exit "$code"; fi
# Blocks on ERROR, HIGH and CRITICAL, on a scan error, and on a report that read no files.
- name: Verdict
run: python .github/scripts/semgrep_gate.py semgrep.json
dependency-review:
name: Dependency review
# The data behind both steps is a comparison of two commits of a pull request, so there is
# nothing to ask on a push to the branch itself.
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# The vulnerability half, which this action does well. `comment-summary-in-pr: never` is
# also its default today, and it is written out anyway: a default is somebody else's
# decision, and this one is ours. Nothing this job learns is posted onto the pull request -
# it is in the log, and a red check is the message.
- name: Dependencies with a known vulnerability
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
comment-summary-in-pr: never
fail-on-severity: low
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: '3.14'
# The licence half, which the action documents that it will not do: it reports a licence it
# could not determine and passes anyway. For a GPL-3.0 project that ships a binary, "we could
# not tell" is the one answer nobody can act on, so it blocks here. The allowed list is read
# out of deny.toml rather than repeated, so this gate and `cargo deny check licenses` cannot
# come to disagree.
- name: Licences of what this pull request adds
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}
run: |
gh api "repos/$REPO/dependency-graph/compare/$BASE...$HEAD" > deps.json
python .github/scripts/dependency_gate.py deps.json
osv:
name: OSV-Scanner
# A second vulnerability database beside the one Dependency review reads: OSV, which carries the
# RustSec advisories and the ecosystems' own feeds. It blocks on what the pull request brings in,
# and a vulnerability already on the base is left to the weekly run in audit.yml, which blocks on
# everything and files it under Security. Why the split, and why a version bump that keeps a
# package vulnerable is not "brought in": the top of .github/scripts/osv_gate.py.
#
# The comparison needs the merge commit and the base it was built on, and a push to the branch
# itself has only one of them.
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
# Depth two is the merge commit of the pull request and its first parent, which is the base
# the merge was built on - exactly the tree the pull request is judged against.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 2
persist-credentials: false
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: '3.14'
# The gate is checked before it judges, so a gate that stopped blocking, or started blocking on
# nothing, fails here as itself rather than as a verdict about the tree.
- name: The gate checks itself
run: python -m unittest discover -s .github/scripts -p test_osv_gate.py
# The published binary, pinned to a version and to its checksum in the script rather than
# Google's action, which pulls a container by a tag. Bumping it is a deliberate act.
- name: Install the scanner
run: python .github/scripts/osv_gate.py install "$RUNNER_TEMP/osv"
# Both trees are scanned with the same arguments, so the two reports differ only where the
# trees do. --no-resolve keeps the scanner to what the files declare: measured, its guess at
# what pip installs under the Semgrep pin named versions the runner never installs.
- name: Scan the pull request and its base
run: |
scan() {
set +e
"$RUNNER_TEMP/osv/osv-scanner" scan source --no-resolve --all-packages \
--format json --output-file "$1" -r ./
code=$?
set -e
echo "osv-scanner exit code: $code"
# Zero is clean and one is "found something", which the verdict reads from the report.
# Anything above one is the scan itself failing, and that is never a pass.
if [ "$code" -gt 1 ]; then exit "$code"; fi
}
scan "$RUNNER_TEMP/osv-new.json"
git checkout --quiet --detach HEAD^1
scan "$RUNNER_TEMP/osv-old.json"
git checkout --quiet --detach "$GITHUB_SHA"
- name: Verdict
run: python .github/scripts/osv_gate.py verdict "$RUNNER_TEMP/osv-new.json" --old "$RUNNER_TEMP/osv-old.json"