Repository navigation
178 lines (172 loc) · 8.39 KB
/
Copy pathsecurity.yml
File metadata and controls
178 lines (172 loc) · 8.39 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
name: Security
# Dependency-vulnerability scan (cargo-audit) + license/advisory/source policy (cargo-deny).
#
# Pure-documentation pushes are skipped: they cannot change Cargo.lock or any `.rs` code, so
# there is nothing for these jobs to find. The weekly `schedule` cron still re-runs the full
# suite, so a NEW advisory published against an unchanged dependency is still caught even when no
# code is pushed.
on:
push:
branches: [main]
paths-ignore:
- "**/*.md"
- "docs/**"
- "ref-docs/**"
- "to-dos/**"
- "LICENSE-*"
- ".gitignore"
pull_request:
branches: [main]
paths-ignore:
- "**/*.md"
- "docs/**"
- "ref-docs/**"
- "to-dos/**"
- "LICENSE-*"
- ".gitignore"
schedule:
- cron: "0 0 * * 1"
workflow_dispatch:
inputs:
fuzz_seconds:
description: "Seconds per fuzz target (the weekly schedule uses 300)"
required: false
default: "300"
concurrency:
group: security-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
# Least-privilege default, inherited by EVERY job below (`audit`, `deny`, `fuzz`) — none of them
# needs more, and none declares its own block, so this is the effective grant for all three.
# `audit`/`deny` only read the checked-out tree and the lockfiles; `fuzz` additionally uploads a
# crash artifact on failure, which `actions/upload-artifact` does through the Actions API rather
# than the repo, so it needs no `contents: write` either.
permissions:
contents: read
jobs:
audit:
name: Dependency Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
# Install the PREBUILT binary, never `cargo install` (build-from-source): the repo pins
# rustc 1.96, but a from-source cargo-audit build can need a newer toolchain than that to
# COMPILE. The prebuilt binary runs fine under any toolchain -- it only parses Cargo.lock,
# it never compiles this project.
- uses: taiki-e/install-action@6a1bd70eaac3c8bdf093356838d7ee09fda951cf # v2
with:
tool: cargo-audit
- run: cargo audit
# `fuzz/` is its OWN workspace with its OWN lock (see `fuzz/README.md` for why), so the
# command above — which resolves the root workspace — never sees a single one of its 422
# dependencies. That gap is easy to miss precisely because the audit job goes green either
# way. `--file` scans the second lock explicitly.
#
# Worth auditing despite being dev-only tooling: a fuzz target links `libfuzzer-sys` and runs
# attacker-shaped input by design, and an advisory against something in that graph is exactly
# the kind of thing that would otherwise sit unnoticed until someone read the lock by hand.
- run: cargo audit --file fuzz/Cargo.lock
deny:
name: Cargo Deny Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
# Same rationale as the audit job: the prebuilt binary avoids needing a newer toolchain
# than the repo's pinned 1.96 just to build the checker itself. Policy lives in `deny.toml`.
- uses: taiki-e/install-action@6a1bd70eaac3c8bdf093356838d7ee09fda951cf # v2
with:
tool: cargo-deny
- run: cargo deny check
# `fuzz/`'s separate workspace is deliberately NOT checked here, unlike the audit job above
# which does scan its lock. `libfuzzer-sys` is `(MIT OR Apache-2.0) AND NCSA` — the NCSA
# coming from the LLVM libFuzzer runtime it vendors — and NCSA is not on this project's
# allow-list. The two ways to make this pass are both worse than the omission:
#
# * allowing NCSA in `deny.toml` loosens the policy for the SHIPPED artifact to accommodate
# a dev-only tool that is never built into, linked against, or distributed with any
# release binary;
# * a second `fuzz/deny.toml` would gate bans/sources/licenses on tooling nobody ships,
# while the one genuinely security-relevant half — advisories — is already covered by
# `cargo audit --file fuzz/Cargo.lock`.
#
# Recorded rather than silently skipped so the gap is a decision, not an oversight.
fuzz:
name: Fuzz Campaign
# SCHEDULED ONLY, never on push or pull_request. Per-commit fuzzing finds almost nothing — a
# 60-second campaign re-treads the corpus it already has — while adding ~15 minutes to every
# PR. The value is in long campaigns against an accumulating corpus, so this runs weekly on the
# cron above, or on demand.
#
# The per-PR guard that *does* belong on the critical path is the compile gate in `ci.yml`'s
# `lint` job: it catches a fuzz target rotting out of buildability, which is the failure mode
# that would otherwise make this job silently useless.
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
# Fourteen targets at five minutes each, plus a cold ~10-minute build of the frontend's wgpu
# stack. The ceiling is generous rather than tight: a job killed mid-campaign reports failure
# indistinguishably from a real finding.
timeout-minutes: 180
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
# cargo-fuzz needs nightly (`-Z sanitizer=address`); the project pins 1.96 stable in
# `rust-toolchain.toml`. Installing nightly alongside it, and invoking it explicitly as
# `+nightly`, keeps the pin authoritative for every other job.
- uses: dtolnay/rust-toolchain@4fd1da8b0805d2d2e936788875a7d65dbd677dc2 # nightly
- uses: taiki-e/install-action@6a1bd70eaac3c8bdf093356838d7ee09fda951cf # v2
with:
tool: cargo-fuzz
# `fuzz/` depends on `rustysnes-frontend` (the patch/preset/config/symbols boundaries live
# there), which links the winit/wgpu/cpal stack — so this job needs the same system libraries
# `ci.yml`'s frontend jobs do.
- name: Install frontend system dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libasound2-dev libudev-dev libxkbcommon-dev pkg-config
# Validated before use rather than interpolated into the shell: `workflow_dispatch` inputs are
# attacker-controlled in the same sense any repository input is, and this one reaches a
# command line. A non-numeric value is rejected outright instead of being quoted and hoped
# over. The schedule path supplies no input at all, hence the default.
- name: Resolve campaign length
id: length
env:
REQUESTED: ${{ github.event.inputs.fuzz_seconds }}
run: |
seconds="${REQUESTED:-300}"
case "$seconds" in
'' | *[!0-9]*)
echo "::error::fuzz_seconds must be a positive integer, got '$seconds'"
exit 1
;;
esac
if [ "$seconds" -lt 10 ] || [ "$seconds" -gt 3600 ]; then
echo "::error::fuzz_seconds must be between 10 and 3600, got '$seconds'"
exit 1
fi
echo "seconds=$seconds" >>"$GITHUB_OUTPUT"
# `run.sh` seeds each corpus from the committed permissive ROM corpus and applies the
# dictionaries. Both matter: unseeded, `rom_header` plateaus around 29 edges because header
# detection scores candidate offsets and a random image never scores above zero — the
# `$xFD8` unbounded-shift defect this infrastructure found was unreachable without seeding.
- name: Run campaign
env:
SECONDS_PER_TARGET: ${{ steps.length.outputs.seconds }}
run: bash fuzz/run.sh "$SECONDS_PER_TARGET"
# Only on failure, and this is the whole point of the job: a crashing input is worthless if it
# dies with the runner. Reproduce locally with
# cargo +nightly fuzz run <target> <artifact>
- name: Upload crashing inputs
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: fuzz-artifacts
path: |
fuzz/artifacts/
fuzz/target/*.campaign.log
retention-days: 30
if-no-files-found: warn