Repository navigation
241 lines (233 loc) · 10.9 KB
/
Copy pathci.yml
File metadata and controls
241 lines (233 loc) · 10.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
name: ci
on:
push:
branches: [main, master]
pull_request:
permissions:
contents: read
jobs:
# Lint + unit + integration tests on a real Windows runner: the audit and
# the server auth paths only compile meaningfully with windows build tags.
test-windows:
name: vet + test (windows)
runs-on: windows-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- name: gofmt
shell: bash
run: |
out=$(gofmt -l .)
if [ -n "$out" ]; then
echo "files need gofmt:"; echo "$out"; exit 1
fi
- name: vet
run: go vet ./...
- name: test
run: go test ./...
- name: coverage gate
shell: bash
run: |
go test ./... -coverprofile=cover.out -count=1
total=$(go tool cover -func=cover.out | awk '/^total:/ {gsub(/%/,"",$3); print $3}')
echo "total statement coverage: ${total}%"
# Ratchet, not a target. The remainder is code that needs something
# this runner does not have: a SYSTEM/browser-user token (the DPAPI
# and app-bound-key collection backends), a live LSASS, a real Chrome
# or Edge profile, a spawning schtasks.exe / powershell.exe, or a
# domain to talk to. This floor exists to stop coverage going *down*
# — raise it whenever it improves, never lower it to green a build.
min=43
if awk "BEGIN {exit !(${total} < ${min})}"; then
echo "::error::coverage ${total}% is below the ${min}% floor"
exit 1
fi
- name: build
run: go build ./...
- name: headless audit smoke test
shell: pwsh
run: |
go build -o obliteratus.exe ./cmd/obliteratus
./obliteratus.exe --audit --json > audit.json
$code = $LASTEXITCODE
# Exit 1 is the documented contract for "a CRITICAL/HIGH control is
# failing" — always true on a stock CI runner (Defender off, etc).
# Only 2+ means the binary crashed.
if ($code -ge 2) { Write-Error "audit crashed (exit $code)"; exit $code }
Write-Host "audit exit code: $code (1 = failing CRITICAL/HIGH control, expected on CI)"
$json = Get-Content audit.json -Raw | ConvertFrom-Json
$count = @($json.findings).Count
Write-Host "findings: $count"
if ($count -lt 1) { Write-Error "audit produced no findings"; exit 1 }
# Explicit exit: the runner's pwsh wrapper appends
# `if (Test-Path variable:\LASTEXITCODE) { exit $LASTEXITCODE }`,
# so the audit's own exit 1 would otherwise fail the step even
# after every assertion above passed.
exit 0
# Cross-compile from Linux: catches build-tag drift that would break
# contributors on macOS/Linux running `go test ./...` with the stubs.
cross-compile:
name: cross-compile (linux → windows)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- name: build all packages for windows/amd64
env:
GOOS: windows
GOARCH: amd64
run: go build ./...
- name: unit tests (platform-neutral packages)
run: go test ./internal/token ./internal/audit ./internal/ui ./internal/server
# The docs site's JS (docs/site.js) is the file GitHub Pages actually serves,
# and no Go test can see it. A wrong API field name, a broken URL check or a
# renamed element id in index.html all fail *silently*: the reader gets an
# empty download table and no error on either side. That is not hypothetical —
# the first shipped URL check accepted ".../releases/download/v9/../../../evil",
# which resolves to https://github.com/DevCop95/evil, and it was found by
# running the file, not reading it. So the shipped file is run in node against
# a stub DOM, with hostile payloads. No install, no network: fixtures are
# trimmed copies of a real API response, so this gate cannot go red because
# GitHub was slow.
site-test:
name: site js test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 20
- name: run docs/site.test.js against docs/site.js
run: node docs/site.test.js
# Fuzzing on every push/PR. Two groups of targets:
# * the module parsers — registry hives, KDC replies, payload envelopes —
# which read bytes this tool did not author;
# * the console's own guard: the Host allow-list (DNS rebinding), the
# WebSocket Origin parser and the route policy (never reaches a handler
# without the run token; its inputs cannot match the token by
# construction, so it only ever exercises refusals).
# A panic in the first group is an availability bug in a process holding an
# elevated token; a hole in the second exposes every module at once. Runs are
# short and bounded: this is a smoke gate, not a soak. Any crasher is written
# to testdata/fuzz/ and fails the job.
#
# One target per invocation: `go test -fuzz` matches a single target.
fuzz-smoke:
name: fuzz smoke (parsers)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- name: fuzz regf hive parser
run: go test -run='^$' -fuzz=FuzzRegf -fuzztime=30s ./internal/audit
- name: fuzz krb response parser
run: go test -run='^$' -fuzz=FuzzParseRep -fuzztime=30s ./internal/krb
- name: fuzz payload envelope
run: go test -run='^$' -fuzz=FuzzUnpack -fuzztime=30s ./internal/research
- name: fuzz obfuscator round-trip
run: go test -run='^$' -fuzz=FuzzMaskRoundTrip -fuzztime=30s ./internal/obfuscate
- name: fuzz persist name sanitizer
run: go test -run='^$' -fuzz=FuzzSanitizeName -fuzztime=30s ./internal/persist
# The console targets are cheaper per execution and carry hostile seeds,
# so they get a shorter budget than the parsers.
- name: fuzz host allow-list
run: go test -run='^$' -fuzz=FuzzValidHost -fuzztime=20s ./internal/server
- name: fuzz ws origin guard
run: go test -run='^$' -fuzz=FuzzAllowedOrigin -fuzztime=20s ./internal/server
- name: fuzz route policy
run: go test -run='^$' -fuzz=FuzzRoutesRejectBadToken -fuzztime=20s ./internal/server
- name: fuzz token comparison
run: go test -run='^$' -fuzz=FuzzEqual -fuzztime=20s ./internal/token
# Static analysis (gosec) of exactly the code that ships. GOOS is pinned to
# windows even though the runner is Linux, because the entire
# security-sensitive surface lives in the windows-only packages; on linux the
# scan would silently see a third of the tree.
#
# The scan runs at full strength bar two rules excluded on the command line,
# each because every instance is deliberate and re-reviewing adds no signal:
# * G103 (use of unsafe) — the tool *is* an unsafe.Pointer/syscall
# bridge; 37 sites, all hand-reviewed.
# * G104 (unhandled error) — 14 sites, every one a cleanup path (deferred
# Close on a handle we are finished with).
# Every other finding was either fixed (file modes 0755/0644 -> 0700/0600) or
# suppressed inline as `//#nosec <rule> -- <reason>`, so the reason travels
# with the code: 47 suppressions, mostly protocol-mandated weak crypto
# (DES/RC4/MD4 per MS-SAMR, MS-NLMP, RFC 4757) and width narrowing in the
# binary parsers. `gosec ./...` without -exclude lists them all.
#
# Known blind spot: go/types cannot type-check a bodyless function, so gosec
# fails to build SSA for internal/research (EnergyFlow is implemented in
# energyflow_amd64.s) and the taint rules skip that one package. The job
# asserts that this is the *only* tolerated error — a new type error would
# likewise disable analysis silently, so it fails the build too.
sast:
name: gosec (static analysis)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
# Installed with the *runner's* GOOS. Setting GOOS=windows here would make
# this step cross-compile gosec to a PE binary (gosec.exe) that the Linux
# runner cannot execute; it is scoped to the scan step below instead.
- name: install gosec
run: go install github.com/securego/gosec/v2/cmd/gosec@latest
- name: gosec
env:
GOOS: windows
GOARCH: amd64
run: |
# setup-go does not put $(go env GOPATH)/bin on PATH, so resolve the
# binary explicitly rather than relying on `gosec` being found.
GOSEC="$(go env GOPATH)/bin/gosec"
# -no-fail: gosec exits non-zero on the tolerated assembly type error
# even with no findings; the gates below decide pass/fail instead.
"$GOSEC" -no-fail -fmt=json -exclude=G103,G104 ./... > gosec.json
# A scan that parsed nothing must not read as a clean scan.
files=$(jq '.Stats.files' gosec.json)
if [ "${files:-0}" -eq 0 ]; then
echo "::error::gosec analysed 0 files — the scan is vacuous, not clean"
exit 1
fi
jq -r '"gosec stats: files=\(.Stats.files) lines=\(.Stats.lines) nosec=\(.Stats.nosec) found=\(.Stats.found)"' gosec.json
issues=$(jq '.Issues | length' gosec.json)
if [ "$issues" -ne 0 ]; then
jq -r '.Issues[] | "::error file=\(.file),line=\(.line)::\(.rule_id) \(.details)"' gosec.json
echo "::error::gosec reported ${issues} finding(s)"
exit 1
fi
if jq -r '."Golang errors"[]?[]?.error' gosec.json \
| grep -v '^# command-line-arguments$' \
| grep -qv 'missing function body'; then
echo "::error::unexpected gosec type error — a package was not fully analysed"
jq -r '."Golang errors"[]?[]?.error' gosec.json
exit 1
fi
echo "gosec: no findings"
# Reachability-aware vulnerability scanning: govulncheck reports only the
# advisories your code can actually reach, so its verdict is actionable
# rather than a noisy list of every CVE in the module graph. It exits
# non-zero on a finding, which is what gates the merge.
#
# The Go *toolchain* matters here as much as the dependencies: the standard
# library ships in the binary, so an old toolchain is a vulnerable
# dependency. go.mod pins a patched floor and this job reads it.
security:
name: govulncheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- name: install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest
- name: govulncheck ./...
run: govulncheck ./...