Repository navigation
Refine site and dashboard frontend #31
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 ./... |