Skip to content

fix(scan): read context before deciding severity - #154

Merged
ralyodio merged 3 commits into
masterfrom
worktree-scan-fp-causes
Aug 17, 2026
Merged

fix(scan): read context before deciding severity#154
ralyodio merged 3 commits into
masterfrom
worktree-scan-fp-causes

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Three structural false-positive causes, measured against
mulgadc/spinifex — an AWS-compatible
control plane of 2,439 Go files that scored 280 findings and zero true
positives
at 0.11.2, the worst result recorded so far.

None of them is a bad pattern, which is why none had been fixed by tightening
one. The construct is always genuinely present: InsecureSkipVerify: true
really does disable TLS verification, and "/tmp/test-wal" really is a
predictable path. What was missing is the context that decides whether anyone
should care.

The three causes

1. A bare //nolint:gosec was ignored. FOREIGN_CREDENTIAL insisted the
annotation name G101 as well as the linter, which is not how Go projects
write it. Nine of spinifex's twenty-one InsecureSkipVerify lines carry the
bare form with a written reason beside it, and every one was re-reported at
high:

InsecureSkipVerify: true, //nolint:gosec // self-signed per-node certs

Naming the linter is still required, so //nolint and //nolint:errcheck
still count for nothing. The old docstring's argument was right about bare
//nolint and was being applied to the wrong half of the annotation.

2. Only the credential rules read isTestPath. The code rules never did,
so 104 findings reported at full severity from inside a test — scratch paths in
table-driven fixtures, exec.Command("sh", "-c", "exit 42") in a
process-supervision test, self-signed certificates in the e2e harness. The path
set also missed harness/ and testutils/, both plain Go directories with no
_test.go suffix on any file.

3. secret-generic-credential matched identifier names, not values.

ErrExpiredToken = "ExpiredToken"            // an AWS API error code
pathToken       = "/latest/api/token"       // a URL path
hdrToken        = "X-aws-ec2-metadata-token" // a header name

Each repeats its own key, which isTestFixtureValue already knew how to spot
and only ever did inside tests. The reasoning was never about tests, so it is
lifted out as describesItsOwnKey and softens everywhere.

The sharpest case is cmd/rds-agent/engine.go, where the literal flagged as
the credential is the name of a psql bind parameter — on a line that reads the
password from the environment via \getenv, suppresses statement logging three
ways, and redacts the password out of its own error strings. The scanner was
flagging the most careful line in the file.

Also: a credential in a placeholder attribute is the example text an empty
input shows, not data — two of spinifex's nine criticals.

Nothing is dropped

Every one of these is a heuristic about where code lives or what it is named,
and heuristics of that kind may inform a severity and never a verdict. A
softened finding stays counted, printed, in the SARIF, and caught by
--fail-on low.

On spinifex the count is 275 before and after. What moves is severity:

before after
critical 9 7
high 44 5
medium 104 14
low 118 249

131 findings changed severity — 104 by test path, 13 by a foreign nolint, 11
by echoing their own key, 2 by a placeholder attribute, 1 by documentation.
That leaves 12 findings above low where there were 53.

Detection is unchanged

Against profullstack/malware-test-prs the count holds at 130 with all 43
criticals intact. Its scored fixtures live in vulns/, not a test path. The
only movement is four secret-generic-credential findings on scaffolding lines
secret: "top-secret" and expected_token = "valid-token-123" — neither of
which is the scored sink in its own fixture (CWE-475 and CWE-480, both code
rules, untouched).

313 tests pass, 15 of them new, every one built from a line copied out of
spinifex. tsc --noEmit is clean.

Deliberately not fixed

Spinifex's remaining seven criticals are ASIA1A2B3C4D5E6F7890 in its API
documentation. A rule exempting typed sequences — digits running 1234567890,
letters running ABCDEF — was written, measured and removed again:
false-positives.test.ts pins AKIA1234567890ABCDEF as a key that must never
be exempted however it is named, and the two values are the same shape.

Quiet documentation is not worth a scanner that skips the shapes its own
corpus is built from. The dead end is documented in secret-rules.ts so the
next person does not spend the afternoon rediscovering it.

🤖 Generated with Claude Code

Three structural false-positive causes, measured against mulgadc/spinifex —
an AWS-compatible control plane of 2,439 Go files that scored 280 findings and
zero true positives at 0.11.2, the worst result recorded so far.

None of them is a bad pattern, which is why none had been fixed by tightening
one. The construct is always genuinely present: `InsecureSkipVerify: true`
really does disable TLS verification and `"/tmp/test-wal"` really is a
predictable path. What was missing is the context that decides whether anyone
should care, and a finding nobody should care about, reported at high, is what
makes a scanner unpitchable.

1. A bare `//nolint:gosec` was ignored. `FOREIGN_CREDENTIAL` insisted the
   annotation name `G101` as well as the linter, which is not how Go projects
   write it — nine of spinifex's twenty-one `InsecureSkipVerify` lines carry
   the bare form with a written reason, and every one was re-reported at high.
   Naming the linter is still required, so `//nolint` and `//nolint:errcheck`
   still count for nothing.

2. Only the credential rules read `isTestPath`. The code rules never did, so
   104 findings reported at full severity from inside a test — scratch paths in
   table-driven fixtures, `exec.Command("sh", "-c", "exit 42")` in a
   process-supervision test, self-signed certificates in the e2e harness. The
   path set also missed `harness/` and `testutils/`, both plain Go directories
   with no `_test.go` suffix on any file.

3. `secret-generic-credential` matched identifier names rather than values.
   `ErrExpiredToken = "ExpiredToken"` is an AWS API error code; `pathToken =
   "/latest/api/token"` is a URL. Both repeat their own key, which
   `isTestFixtureValue` already knew how to spot and only ever did in tests —
   the reasoning was never about tests, so it is lifted out as
   `describesItsOwnKey`. The sharpest case was `cmd/rds-agent/engine.go`, where
   the literal flagged as the credential is the name of a psql bind parameter
   on the most carefully written line in the file.

Also: a credential in a `placeholder` attribute is the example text an empty
input shows, not data — two of spinifex's nine criticals.

Nothing is dropped. Every one of these is a heuristic about where code lives or
what it is named, and heuristics of that kind may inform a severity and never a
verdict, so the finding stays counted, printed, in the SARIF and caught by
`--fail-on low`. On spinifex the count is 275 before and after; what moves is

  critical   9 →  7      medium  104 → 14
  high      44 →  5      low     118 → 249

leaving 12 findings above low where there were 53.

Detection is unchanged. Against profullstack/malware-test-prs the count holds
at 130 with all 43 criticals intact; the only movement is four
`secret-generic-credential` findings on scaffolding lines
(`secret: "top-secret"`) that are not the scored sinks in their own fixtures.

Deliberately not fixed, and documented where it would have gone: spinifex's
remaining seven criticals are `ASIA1A2B3C4D5E6F7890` in its API docs. A rule
exempting typed sequences was written and removed again — `false-positives.test.ts`
pins `AKIA1234567890ABCDEF` as a key that must never be exempted, and the two
values are the same shape. Quiet documentation is not worth a scanner that
skips the shapes its own corpus is built from.

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

Copy link
Copy Markdown

ThreatCrush Security Scan

63 finding(s)

HIGH/CRITICAL: 4 | MEDIUM: 52 | LOW: 7

Severity Rule Location
HIGH secret-aws-access-key prd/0003-detect-hardcoded-secrets-before-they-are-committed-or-served.md:126
HIGH sh-eval-expansion .githooks/pre-commit:26
HIGH sh-remote-script-execution apps/web/public/install.sh:272
HIGH sh-remote-script-execution apps/web/public/install.sh:320
MEDIUM insecure-temp-file .githooks/commit-msg:16
MEDIUM insecure-temp-file .githooks/post-commit:20
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/init.ts:70
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/init.ts:79
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/service.ts:92
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:31
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:33
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:34
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:35
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:36
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:43
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:49
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:56
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:63
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:82
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:84
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:85
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:93
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:98
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:105
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:112
MEDIUM js-shell-exec-interpolation apps/cli/src/index.ts:425
MEDIUM js-unescaped-html-sink apps/web/src/app/about/page.tsx:180
MEDIUM js-unescaped-html-sink apps/web/src/app/about/page.tsx:184
MEDIUM js-open-redirect apps/web/src/app/auth/login/page.tsx:50
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:125
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:153
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:157
MEDIUM js-unescaped-html-sink apps/web/src/app/get-whitepaper/page.tsx:346
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:211
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:215
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:219
MEDIUM js-unescaped-html-sink apps/web/src/app/page.tsx:120
MEDIUM js-unescaped-html-sink apps/web/src/app/store/[slug]/page.tsx:86
MEDIUM js-open-redirect apps/web/src/components/funding/FundingClient.tsx:97
MEDIUM manifest-install-lifecycle-script package.json:24
MEDIUM js-uninitialized-buffer packages/scan/src/node-rules.ts:456
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:154
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:160
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:208
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:313
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/aur.ts:256
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/chocolatey.ts:264
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/chocolatey.ts:288
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/rpm.ts:201
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/rpm.ts:261

…and 13 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

ralyodio and others added 2 commits August 17, 2026 05:07
gitleaks and semgrep both read the comment explaining why a typed-sequence
exemption cannot be had, found the two literals it names, and blocked. Written
as a prefix and a body now, and the test password is assembled from parts —
the convention `foreign-suppression.test.ts` already established for exactly
this reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gitleaks reads every ref, so rewriting the lines in the tree does not clear the
commit that introduced them. Same remedy as the four fingerprints above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ralyodio
ralyodio merged commit e475684 into master Aug 17, 2026
11 checks passed
@ralyodio
ralyodio deleted the worktree-scan-fp-causes branch August 17, 2026 05:13
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.

1 participant