Repository navigation
Hardening roadmap: close the assurance gap (fuzzing · SAST · coverage · signing) #2
Description
Hardening roadmap: close the assurance gap (fuzzing · SAST · coverage · signing)
Context
OBLITERATUS's CI/release layer is strong, but the assurance layer (tests,
fuzzing, static analysis, signed binaries) is the real bottleneck. Recent work
landed entirely in the already-strong areas, so an overall quality score barely
moved despite substantial improvements. This issue makes that gap explicit,
prioritised and measurable so the next session works from a checklist instead
of re-deriving priorities.
Goal: move the weighted score from 8.1 → 9.5+ by targeting the dimensions
that actually carry the remaining weight.
Current state (what is solid — do not regress)
Server hardening
- Loopback-only bind enforced at
Listentime (internal/server/server.go). - Per-run
crypto/randtoken, constant-time compare, never logged. - Host allow-list + CSP + same-origin; WS requires loopback Origin and token.
- State-changing endpoints are POST-only behind a custom-header CSRF check.
- Modules fail closed (
403+ the unlocking flag);lsass-wipeconfines the
path to anobl-lsass-*.dmpdirectly inside%TEMP%.
Supply chain / release
govulncheck(reachability-aware) as a required check.- Coverage floor gate in CI.
- SHA-256
checksums.txt, SPDX SBOM per archive, cosign keyless signature
(Sigstore/Rekor) onchecksums.txt. - Dependabot for
gomod+github-actions;mainprotected with 3 required
status checks; toolchain pinned togo 1.26.9ingo.mod.
Docs
- README, SECURITY (real threat model), ARCHITECTURE, HALOS_GATE, SIGNING,
CONTRIBUTING.
Scoring snapshot (transparent rubric)
Weights and measurement, session start vs now:
| Dimension | Weight | At start | Now |
|---|---|---|---|
| Server hardening | 20% | 8.5 | 9.0 |
| CI / release / supply chain | 20% | 5.0 | 9.5 |
| Testing / assurance | 20% | 5.0 | 6.0 |
| Architecture / code | 15% | 8.0 | 8.0 |
| Documentation | 15% | 8.0 | 8.5 |
| Distribution / trust | 10% | 7.0 | 7.5 |
| Weighted total | 6.8 | 8.1 |
The remaining ~1.4 points live almost entirely in Testing (20%) and
Distribution (10%). Work that does not touch these two will not move the
number — that is the whole point of this issue.
Backlog
P0 — Fuzzing on untrusted-input parsers
These components parse attacker-influenced bytes; a crash here is a
confinement/bypass bug, not a flaky test. Currently there is no fuzzing.
-
internal/audit/regf.go—FuzzRegf: registry hive parser. -
internal/krb/*—FuzzKRBParsers: ASN.1/SPN/LDAP response parsing. -
internal/research/*—FuzzUnpack: theOBLPpack envelope
(magic || nonce || AES-256-GCM); must reject truncated/corrupt input
without panicking. -
internal/obfuscate/*— parsing/decoding paths. - Commit a seed corpus per fuzz target (
testdata/fuzz/...). - Add a bounded fuzz smoke job to CI:
go test -run='^$' -fuzz=FuzzX -fuzztime=30s ./internal/...
(short, deterministic timeout; per-target, not open-ended).
Acceptance: seed corpora committed; no crashers found in a ≥5 min local run
per target; CI fuzz-smoke job green.
P0 — Static analysis (SAST) in CI
govulncheck covers dependency vulnerabilities, not our own code. There is
currently no gosec, staticcheck, golangci-lint or CodeQL.
- Add a
sastjob to.github/workflows/ci.yml. - Run
gosec (or golangci-lint with gosec+staticcheck enabled) ./.... - Triage findings; add
#noseconly with a written justification.
Acceptance: job runs on every PR and fails on new high/critical findings.
P0 — Raise coverage where it is weakest
Current: 38% total. Weak spots:
| Package | Coverage |
|---|---|
internal/elevate |
18% |
internal/audit |
25% |
internal/winutil |
0% |
cmd/obliteratus |
0% |
cmd/oblpack |
0% |
- Add tests for
internal/elevate(marker handling, token logic). - Add tests for
internal/audit(report path, host checks). - Add at least smoke tests for both
mainpackages (cmd/obliteratus,
cmd/oblpack) — flag parsing,--version,--auditexit contract. - Raise the coverage floor in
ci.ymlfrom 35 to match the new number.
Acceptance: total coverage ≥ 50%; ci.yml floor raised accordingly.
P1 — Sign the binaries (Authenticode)
Binaries are not Authenticode-signed (documented in docs/SIGNING.md). This
needs a cert that must not live in the repo.
- Obtain an EV cert or an Azure Trusted Signing account.
- Wire it into
.goreleaser.ymlas asignsentry that runs before
archiving (so signatures are inside the archive and covered by checksums). - Inject the credential as a GitHub secret (base64 PFX / OIDC login).
Acceptance: Get-AuthenticodeSignature reports Valid on the released
.exe; docs/SIGNING.md updated from "not applied" to the real flow.
P1 — Community / governance files
-
.github/ISSUE_TEMPLATE/bug_report.yml+security_report.yml(the
latter redirects to private reporting, perSECURITY.md). -
.github/PULL_REQUEST_TEMPLATE.md(checklist: tests, gofmt, security
note for boundary-touching changes). -
.github/CODEOWNERS. -
CODE_OF_CONDUCT.md.
Acceptance: files present and referenced from CONTRIBUTING.md.
P2 — Repository rename / alias
The repo is pullgoscript; the binary and docs say obliteratus. This hurts
discoverability and is confusing when the module path is
github.com/DevCop95/obliteratus.
- Rename to
obliteratus(GitHub keeps redirects) or set the repo
"About" to make the mapping explicit.
Acceptance: incoming links resolve; no broken badges/workflows.
How we re-measure (definition of done)
When the P0 items are done, re-apply the rubric table above and post the
before/after in this issue. The number only "counts" if the table moves —
no more round-number verdicts without the math.
Target after P0: Testing ≥ 8.0, Distribution unchanged → weighted ≈ 8.9.
Target after P0+P1: ≈ 9.5.
Out of scope
- The offensive modules themselves (features, not bugs — see
SECURITY.md). - Physical / kernel-level threat models.
- Cosmetic README churn.
References
SECURITY.md— runtime threat model.docs/SIGNING.md— verification flow + Authenticode status..github/workflows/ci.yml,.goreleaser.yml— current CI/release pipeline.
Activity
DevCop95 commentedon Oct 17, 2026
Merged in #3 (
45401fe). Everything in this issue except Authenticode is done,and the two new jobs are now required checks on
main— before that thegates were decorative.
What landed
FuzzRegf,FuzzParseRep,FuzzUnpack,FuzzMaskRoundTrip,FuzzSanitizeName; seeds run under plaingo test, CI adds 30s each on every push/PRgosecjob, 0 findings, 47 inline//#nosec <rule> -- <reason>, 2 rules excluded with rationale (G103/G104)xorBytesdivided bylen(key); latent (no caller passes an empty key) but the failure mode is a panic in an elevated process. Now fails closeddocs/SIGNING.mdsaid to expect identityDevCop95/obliteratus— a repository that does not exist. The documentedcosign verifyfailed against the real v2.0.2 releaseRubric, with the arithmetic
Self-assessed; the anchors are the evidence above, not impressions. Baseline is
the audit that opened this issue.
The remaining distance to ~9.5 is two things, and neither is code:
Azure Trusted Signing account — paid and identity-verified, so it cannot be
closed from here. The goreleaser
signsblock and the first-signed-releasegotcha (
signtoolneeds the Windows SDK on PATH at release time) aresketched in
docs/SIGNING.md. No release is blocked by its absence.DPAPI, app-bound keys, LSASS, the real browser stores — cannot be exercised
on the Ubuntu runner, so whole-tree coverage plateaus there. Lifting it needs
a Windows runner (paid) or a synthetic store fixture.
Disclosed rather than hidden
go/typesrejects the bodylessEnergyFlow(implemented in
energyflow_amd64.s), so gosec cannot build SSA forinternal/researchand its taint rules skip that package. The job asserts themissing-function-body error is the only tolerated one, so a new type error —
which would disable analysis just as silently — fails the build.
the job passed with
gosec: command not found, and later withGOOS=windowsat job level it cross-compiled gosec itself into a PE binary. Both fixed, and
the job now fails if
Stats.filesis 0 — "0 findings" and "parsed nothing"must not look the same. CI prints
files=37 lines=7194 nosec=47 found=0.gosec -no-fail -fmt=json -exclude=G103,G104 ./...(gosec v2.29.0),
make sast,make fuzz-check,make cover.Still open here
one thing left unfuzzed: auth, the Host/Origin guards and WS framing are all
testable on Linux against a loopback server. Highest-value remaining target.
staticcheckis clean locally but not wired into CI (gosec only by design).history linear, matching how chore(ci): bump the actions group across 1 directory with 3 updates #1 and Harden the parsers, gate on gosec, and fix the broken verification docs #3 landed.
Leaving this open for Authenticode only — say the word if you would rather close
it and track the signing work separately.
DevCop95 commentedon Oct 17, 2026
Closing this: every item here is merged, and the one thing that cannot be done
in code now has its own issue (#5).
What landed
cosign verifydocs fixed, community files45401fevalidHostport validation,allowedOriginuserinfo, + 4 fuzz targets and the route-policy invariantsc1abf32The last piece (#4) is the one I would point at: the console guard is the single
place where a mistake exposes every module at once, and it was the only
attacker-facing surface left unfuzzed. Three findings, all rated low honestly —
net.SplitHostPortdoes not validate the port (127.0.0.1:8080/evilpassed),url.Parseputs userinfo inu.Usersohttp://evil.com@127.0.0.1read asloopback, and an Origin with a path was accepted. None were exploitable through
a browser; all three are now fixed, and the fixes are verified non-vacuous by
reverting them and watching the seed corpus alone reproduce each finding.
Two invariants matter more than the findings:
TestGuardedRoutesRejectBadTokenfails if a route is ever registered withouts.guard(...)— that is how a module gets exposed by accident.FuzzRoutesRejectBadTokencan never reach a state-changing handler (its tokencannot match by construction), so it is safe to run on every push.
Rubric, final
What is left is not effort, it is access: Authenticode needs a certificate (#5),
and Testing's remaining ceiling is structural — the Windows-only credential
backends (DPAPI, app-bound keys, LSASS, real browser stores) cannot run on the
Ubuntu runner, so whole-tree coverage plateaus without a paid Windows runner or
synthetic store fixtures.
Operational note
Adding the two new jobs to the branch ruleset broke
main: I round-tripped theruleset JSON through Python, and on Windows the default codec (cp1252) mangled
the UTF-8
→in the existingcross-compile (linux → windows)context, so theruleset demanded a check that does not exist and every PR became unmergeable.
Caught by the same reasoning applied to the rest of this work — the gate looked
green while actually blocking, so I checked the ruleset against the check run
names GitHub really reports instead of trusting the config. Fixed and verified
(
mergeStateStatus: CLEAN, merged without an admin bypass); the five requiredchecks now also carry
integration_id: 15368so they are pinned to the Actionsapp rather than to "any app".
Verified on
mainafter both merges:gofmt,go vet,GOOS=linux go vet,staticcheck, full test suite, coverage floor, gosec and all nine fuzz targets.Still open as #5: signing the Windows binaries.