Skip to content

fix(ci): govulncheck-filter expiry-checks module-level allowlist entries (#717) - #726

Merged
MarkEdmondson1234 merged 2 commits into
devfrom
sprint/iter206-govulncheck-expiry
Aug 15, 2026
Merged

fix(ci): govulncheck-filter expiry-checks module-level allowlist entries (#717)#726
MarkEdmondson1234 merged 2 commits into
devfrom
sprint/iter206-govulncheck-expiry

Conversation

@sunholo-voight-kampff

Copy link
Copy Markdown
Collaborator

The defect (#717)

printModuleOnly annotated module-level findings from allowlist presence only — it never read
the entry's expires. The expiry computation in main() ran solely over the reaching findings,
so the allowlist's whole expires discipline was inert for the module-level class.

Reproduced first-party at 38641e216, two fixtures differing only in whether the trace frame
carries a function field, expires: "2020-01-01" in both:

arm output rc
module-only - GO-2026-9999 [allowlisted] 0
reaching (control) GO-2026-8888 (expired 2020-01-01) 1

GO-2026-5750 (Ollama path-traversal, expiry 2026-10-29) is in that class, so on that date it
would have kept reading [allowlisted] with no re-review prompt.

Second gap, same root cause, not named in the issue: a malformed module-only expiry was never
validated either — time.Parse + exit 2 also lived only in the reaching loop, so
expires: "not-a-date" rode out as [allowlisted] at rc=0.

The fix

Why decide was extracted, and the vacuous test it replaced

The non-gating invariant cannot be pinned through the per-class helpers — they are never handed
module-only IDs. The executor's first version asserted classifyReaching(nil, …) returned empty.
That passes for any implementation, and measured: it stayed GREEN under a mutant that appended
every id to blocking. The replacement arms red on that mutant.

Mutation drill

Each mutant asserted LANDED (sha256 changed) and BUILDS (go build rc=0) before its test
result was read, and neutered with if false && … so no import goes unused.

mutant reds
printModuleOnly expiry branch PrintModuleOnlyAnnotations/expired + both DecideExitCodes expired arms
validateModuleOnly error return ValidateModuleOnlyRejectsMalformedExpiry + DecideExitCodes/malformed_module-only_expiry_exits_2
classifyReaching gates everything DecideExitCodes/expired_module-only_alongside_allowlisted_reaching — the arm the vacuous test could not
precondition expired fixture → future date both expired arms
precondition module-only fixture → reaching both expired arms

Inverse arm: with the first mutant applied and the new tests -skipped, the package is rc=0 — so
the new tests are the killers, not bystanders. Files restored byte-identical by cp from a backup
(never git checkout --, which would have destroyed uncommitted work).

Gates

Run outside any sandbox on darwin/arm64 (windows/ubuntu legs unrun locally, left to CI):
go build · go test -v · go vet · gofmt -l on the package · go test ./tools/... ·
make check-changelog check-file-sizes check-boundaries fmt-check vet check-skills — all rc=0.

End-to-end against the built binary: module-only+expired → [allowlisted, EXPIRED 2020-01-01] rc=0;
module-only+future → [allowlisted] rc=0 (no EXPIRED); module-only+malformed → exit 2;
reaching+expired → unchanged rc=1.

Fixes #717

🤖 Generated with Claude Code

Voight-Kampff (bot) and others added 2 commits August 15, 2026 08:52
…ies (#717)

`printModuleOnly` annotated module-level findings from allowlist PRESENCE only;
the expiry computation ran solely over the reaching findings. An expired entry
therefore printed `[allowlisted]` forever, with no re-review prompt — the
`expires` discipline was inert for the whole module-level class. `GO-2026-5750`
(expiry 2026-10-29) is in that class.

Reproduced first-party at 38641e2 on two fixtures differing only in whether the
trace frame carries a `function`, allowlist `expires: "2020-01-01"` in both:
module-only -> `[allowlisted]` rc=0; reaching -> `(expired 2020-01-01)` rc=1.

Same root cause, not named in the issue: a MALFORMED module-only expiry was never
validated either (`time.Parse` + exit 2 also lived only in the reaching loop), so
`expires: "not-a-date"` rode out as `[allowlisted]` at rc=0.

- Module-level status is now `[NOT allowlisted]` / `[allowlisted]` /
  `[allowlisted, EXPIRED <date>]`, via one shared `classifyEntry` helper and one
  clock, so reaching and module-only cannot drift on the `!t.After(now)` boundary.
- A malformed module-only expiry is reported and exits 2 before any success output.
- STILL NON-GATING per #703: an expired module-only entry is surfaced and the
  process exits 0.

The exit-code decision moved out of `main` into a testable `decide`. This was not
cosmetic: the non-gating invariant cannot be pinned through the per-class helpers,
which are never handed module-only IDs. The executor's first version asserted
`classifyReaching(nil, ...)` returned empty — vacuous, and measured so: it stayed
GREEN under a mutant that appended every id to `blocking`. The replacement arms
red on that mutant.

Drill (each mutant asserted LANDED by sha256 and BUILDS rc=0 before its result was
read, neutered with `if false && ...` so no import goes unused):
- printModuleOnly expiry branch -> reds PrintModuleOnlyAnnotations/expired and both
  DecideExitCodes expired arms; inverse arm (`-skip`) rc=0, so they are the killers.
- validateModuleOnly error return -> reds ValidateModuleOnlyRejectsMalformedExpiry
  and DecideExitCodes/malformed_module-only_expiry_exits_2.
- classifyReaching gates everything -> reds DecideExitCodes/expired_module-only_
  alongside_allowlisted_reaching (the arm the vacuous test could not).
- Precondition neutering (expired fixture -> future date; module-only fixture ->
  reaching): both kill their dependent arms, so none is vacuous.

Gates outside the sandbox on darwin/arm64 (windows/ubuntu legs unrun locally):
go build / go test -v / go vet / gofmt -l on the package, `go test ./tools/...`,
and make check-changelog check-file-sizes check-boundaries fmt-check vet
check-skills — all rc=0.

Fixes #717

Co-Authored-By: codex gpt-5.6-sol <codex@openai.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d unguarded

The sonnet evaluator (95/100 PASS, zero blocking) enumerated the refusal branches
in `decide()` and found three with no test at any level. Verified first-party
before acting on it (a judge's finding is a claim too): neutering
`if parseErr != nil` in `classifyReaching` with `if false && ...` — mutant LANDED
by sha256, BUILDS rc=0 — redded NOTHING.

It was unpinned at the parent commit too (`grep -c 'not-a-date'` on
`38641e216:main_test.go` = 0, control `allowEntry` = 13), so this is pre-existing
debt rather than a regression. But it is the direct reaching-side sibling of the
#717 module-only deliverable and `decide()` now makes it a one-arm test, so
leaving it would be shipping the asymmetry the fix exists to remove.

The arm kills exactly that branch and nothing else: under the same mutant only
`TestDecideExitCodes/malformed_reaching_expiry_exits_2` reds. Restored
byte-identical (sha256 f215d765...).

The evaluator's other two findings — duplicate-allowlist-ID and
malformed-stdin-JSON — are genuinely unrelated to #717 and are filed separately
rather than expanding this sprint's scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@MarkEdmondson1234
MarkEdmondson1234 merged commit 640bab0 into dev Aug 15, 2026
21 checks passed
@MarkEdmondson1234
MarkEdmondson1234 deleted the sprint/iter206-govulncheck-expiry branch August 15, 2026 10:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ci] govulncheck-filter: module-level allowlist entries are never expiry-checked

2 participants