Skip to content

Refine site and dashboard frontend #31

Refine site and dashboard frontend

Refine site and dashboard frontend #31

Workflow file for this run

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 ./...