Skip to content

fix(cli): make the CI gate fail on an unreadable findings response - #163

Open
tim-thacker-nullify wants to merge 2 commits into
mainfrom
fix/agi-2835-ci-gate-cannot-fail
Open

tim-thacker-nullify wants to merge 2 commits into
mainfrom
fix/agi-2835-ci-gate-cannot-fail

Conversation

@tim-thacker-nullify

@tim-thacker-nullify tim-thacker-nullify commented Aug 15, 2026 •

Copy link
Copy Markdown
Member

Claude

nullify ci gate could not reach a verdict: unparseable responses counted as zero findings, and the query it sent made every CSPM request 500. (#155 already added the findings branch; the silent-zero fallback and the query were what remained.)

Silent zero:

  • countFindings parses the real {findings, numItems, nextToken} envelope and returns (int, error); an unrecognised envelope errors, never 0. The gate then exits ExitNetworkError instead of counting it clean, and ci report counts it a failed request, not a 0 row.
  • Dropped the dead items / total shapes and the duplicate totalFindingsCount.

Query, now built per endpoint from a capability table:

  • Severity goes out upper-case. /cspm/findings binds it into severity_label = $N::severity_level (cspm/internal/database/cspm_findings/postgres_repository.go:407) and the enum labels are upper-case, so critical was invalid input value for enum → status.Internal → apiErrors > 0 → exit 3 on every default run, clean repo or not. /sast/findings case-folds and took either.
  • Severity is sent only to /sast/findings and /cspm/findings — no other endpoint declares it. Those five are queried once, not once per severity, and their FAIL line states the threshold was not applied.
  • status=open is gone; no endpoint implements it. /cspm/findings declares Status but never copies it into CSPMFindingFilters; the rest use isResolved. Sending isResolved=false stops resolved and false-positive findings blocking a build with no way to clear them.
  • nullify status drops its finding count — limit=1 capped it at 1.

Mutation-checked: lower-casing severity, restoring status=open, or marking secrets severity-capable each fail TestScannerQueryParamsMatchEndpointFilters.

Known gaps, not fixed here:

  • --repo sends repository, which no endpoint declares (/sast/findings takes repositoryIds), so the gate is tenant-wide. Needs name-to-ID resolution.
  • ci report repeats one count across all four severity rows for the five scanners with no severity filter.
  • CSPM server-side normalisation via database.SeverityToNullSeverityLevel belongs in the monorepo.
  • Live-stack integration test (AGI-2835 item 5) — no harness here. ci report's limit=1000 exceeds the secrets 100 cap — pre-existing.

AGI-2835

countFindings treated any response it could not parse as zero findings,
so `nullify ci gate` printed "Gate passed: no findings above threshold".

Parse the real {findings, numItems, nextToken} envelope, return an error
for anything else, and exit ExitNetworkError rather than counting an
unreadable response as clean. Drop the dead items/total shapes and the
duplicate totalFindingsCount. The gate requests limit=1 and the API has
no grand total, so the printed counts were the number of failing
scanner/severity pairs; the messages now say that instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tim-thacker-nullify tim-thacker-nullify added the patch Patch version updates (fixes) label Aug 15, 2026
The gate sent `severity=critical` lower-case and `status=open` to all
seven scanner endpoints. /cspm/findings binds severity straight into
`severity_label = $N::severity_level` and the Postgres severity_level
enum labels are upper-case, so every CSPM request 500'd and the
pre-existing `apiErrors > 0` branch returned ExitNetworkError before the
gate could reach a verdict. `status` is implemented by no endpoint, so
resolved, fixed, false-positive and allowlisted findings were counted.

Build the query per endpoint from a capability table: upper-case
severity, send it only to /sast/findings and /cspm/findings, and send
isResolved=false where the endpoint implements it. Endpoints with no
server-side severity filter are queried once instead of once per
severity, and say so in their FAIL line rather than implying the
threshold was applied. `nullify status` drops its finding count, which
limit=1 capped at 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tim-thacker-nullify

Copy link
Copy Markdown
Member Author

Claude

Left as draft. Both review findings are closed in 163f889, but CI has not finished.

Checks at 163f889: build-test pass, require-labels pass, [code]smith skipped, Nullify still IN_PROGRESS. mergeStateStatus: BLOCKED. Nothing is failing — the matrix is unevaluated.

Closed:

  • Blocking, lowercase severity → CSPM enum 500: cmd/cli/cmd/ci.go:169 now sends strings.ToUpper(severity), and only to the two endpoints that declare the filter (cmd/cli/cmd/scanners.go:473, :479).
  • Major, status=open no-op: replaced by isResolved=false on the five endpoints that implement it (cmd/cli/cmd/ci.go:171-173); cmd/cli/cmd/ci_test.go:316 asserts status is never sent.

Open and disclosed, not fixed here:

  • --repo still sends repository (cmd/cli/cmd/ci.go:175), which no endpoint declares — /sast/findings takes repositoryIds. The gate is tenant-wide, not repo-scoped. Needs name-to-ID resolution.
  • ci report still emits duplicated severity rows.
  • CSPM server-side severity normalisation is a monorepo change (cspm/pkg/endpoints/cspm_findings_get.go:58).

Ready for promotion once Nullify reports.

@tim-thacker-nullify

Copy link
Copy Markdown
Member Author

Claude

Out of draft. Built from AGI-2835, then adversarially reviewed and fixed.

The ticket's defect: countFindings recognised [], {"items":…} and {"total":…}, while every scanner endpoint returns {"findings":…,"numItems":…}. Zero overlap, so it returned 0 on every real response, os.Exit(ExitFindings) was unreachable, and the gate always printed "Gate passed" and exited 0.

Review found the first cut still could not render a verdict in production; both are fixed on this head:

  • severity was sent lowercase to /cspm/findings, which binds it straight into a Postgres enum with uppercase labels — a 500 on every run, tripping the apiErrors > 0 branch to exit 3. Now strings.ToUpper(severity), and only on the two endpoints that declare the filter.
  • status=open was a no-op on all seven endpoints, so the newly-live fail path would have blocked builds on already-resolved findings. Replaced with isResolved=false; a test asserts status is never sent.

The root cause is addressed as well as the field names: an unrecognised envelope is now errUnreadableFindings rather than a silent zero, so the gate fails closed instead of passing.

Checks green: build-test, require-labels, Nullify.

Known and not fixed here — --repo still sends repository, which no endpoint declares, so the gate is tenant-wide rather than repo-scoped. Worth a follow-up; it makes the gate stricter, not weaker.

@tim-thacker-nullify
tim-thacker-nullify marked this pull request as ready for review August 15, 2026 01:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

patch Patch version updates (fixes)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant