Skip to content

ci: gate on detection coverage against the testbed corpus - #98

Merged
ralyodio merged 1 commit into
masterfrom
feat/coverage-gate
Aug 11, 2026
Merged

ci: gate on detection coverage against the testbed corpus#98
ralyodio merged 1 commit into
masterfrom
feat/coverage-gate

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Wires up the "vuln db" — profullstack/malware-test-prs, the public testbed catalog the rule set has been tuned against all along — as a coverage gate in CI.

What the testbed is

vulns/VULNERABILITY_CATALOG.json: 67 test cases across seven languages, each with line-level attribution — vulnerable_lines a scanner must flag, safe_lines it must not. The SAFE lines are a control group: every one is a correct implementation of the exact pattern the neighbouring vulnerable code gets wrong. A scanner that flags them is matching syntax, not following data.

The repo ships a scorer (scripts/validate-coverage.py) that already grades inbound SARIF against the catalog. This PR points it the other way: threatcrush scoring itself on every change.

The number

At the pinned corpus commit, the CLI scores:

metric result
true-positive rate 65.9% 85 / 129 vulnerable lines
false-positive rate 0% 0 / 78 control lines

code-rules.ts records the starting point: 15.6% true-positive when the code rules were still only secret detection. The shell, PHP, Java and Go rule sets added since moved it to 65.9% — and the false-positive rate sat at 0% throughout. That second number is the one that was hard to hold: every rule was built against the corrected shape as well as the vulnerable one precisely so the control group stays silent, and across the whole corpus it does.

Why not higher

The 44 misses are spread across ~32 CWE classes, almost all singletons, and they are dominated by the classes KNOWN_GAPS in code-rules.ts explicitly declines to approximate with line-oriented matching: CSRF, IDOR, mass assignment, NoSQL injection, session fixation, TOCTOU, missing authorization. A regex that "caught" those would catch every session read and every Object.assign in the codebase — it would buy true positives with the false-positive rate this whole effort exists to protect.

The one recoverable cluster is CWE-327 / CWE-338 (weak crypto, predictable PRNG — 9 lines), where threatcrush has rules but the credential-context guard is too tight for the testbed's multi-line Python fixtures. That is a real follow-up; it is a rule-tuning task, not a new class.

The gate

Floors at TPR 60 / FPR 2, under the current result with room for ordinary noise but not for a real regression — a rule that stops firing, or one that starts flagging the control group, moves the number past a floor and fails the job.

The corpus is pinned to a commit, not tracked to its default branch. A new upstream test case would otherwise lower the rate and fail this repository's gate on someone else's change. The pin is bumped deliberately, in a PR whose diff is the rule that covers the new case.

Fail-closed: an empty SARIF scores as 0% and would read as a total regression, so the scan step distinguishes "scanner produced nothing" from "scanner regressed" and names the right cause.

Verification

Reproduced the CI steps locally against the pinned commit f9f4fce:

  • clean master build → scan vulns/ → score: TPR 65.9%, FPR 0%, PASS
  • gate exits 0 at the 60/2 floor, exits 1 at 90/5 — the threshold logic is real, not decorative
  • YAML validates

The scorer's Markdown summary lands in the PR's job summary, so every run shows the rate and the full miss list inline.

Adds a CI job that scores the CLI against profullstack/malware-test-prs —
the public catalog of vulnerable/safe line pairs the rule set has been tuned
against all along — and fails if the true-positive rate falls or the
false-positive rate rises.

Unit tests prove a rule fires on one hand-written line. This proves the whole
set still catches what it caught across 67 real cases, and still stays silent
on the 78 corrected implementations sitting beside them. A change that trades
one false positive for three misses passes every unit test and fails here.

At the pinned corpus commit the CLI scores TPR 65.9% (85/129) and FPR 0%
(0/78) — up from the 15.6% baseline recorded in code-rules.ts when the code
rules were only secret detection, with the false-positive rate held at zero
throughout. The remaining misses are dominated by classes KNOWN_GAPS
deliberately declines to approximate with line-oriented matching — CSRF,
IDOR, TOCTOU, NoSQL injection, session fixation — so the floor is set at
TPR 60 / FPR 2, under the current result with room for noise but not for a
real regression.

The corpus is pinned to a commit, not tracked to its default branch: a new
upstream test case would otherwise lower the rate and fail this repository's
gate on someone else's change. Bump the pin in a PR whose diff is the rule
that covers the new case.

Verified locally against the pinned commit: gate passes at 60/2, fails at
90/5, and scores identically from a clean master build.

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

Copy link
Copy Markdown

ThreatCrush Security Scan

157 finding(s)

HIGH/CRITICAL: 14 | MEDIUM: 107 | LOW: 36

Severity Rule Location
HIGH js-shell-exec-interpolation modules/code-scanner/src/__tests__/sast.test.ts:31
HIGH js-shell-exec-interpolation modules/code-scanner/src/__tests__/sast.test.ts:102
HIGH js-shell-exec-interpolation modules/code-scanner/src/__tests__/sast.test.ts:108
HIGH secret-aws-access-key modules/code-scanner/src/secrets/rules.ts:74
HIGH sql-template-interpolation packages/scan/src/__tests__/code-rules.test.ts:31
HIGH secret-aws-access-key packages/scan/src/secret-rules.ts:192
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 secret-generic-credential modules/spend-guard/config/example.conf.toml:13
HIGH secret-generic-credential modules/spend-guard/README.md:84
HIGH js-unsafe-yaml-load packages/scan/src/__tests__/code-rules.test.ts:216
HIGH secret-generic-credential PRD.md:268
HIGH sh-remote-script-execution scripts/smoke-test.sh:46
HIGH sh-remote-script-execution scripts/smoke-test.sh:47
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 sql-template-interpolation apps/cli/src/commands/properties.ts:226
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/service.ts:88
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/service.ts:111
MEDIUM sql-template-interpolation apps/cli/src/core/state.ts:121
MEDIUM sql-template-interpolation apps/cli/src/core/state.ts:125
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 sql-template-interpolation apps/cli/src/daemon/firewall/adapters.ts:49
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 sql-template-interpolation apps/cli/src/index.ts:105
MEDIUM sql-template-interpolation apps/cli/src/index.ts:110
MEDIUM sql-template-interpolation apps/cli/src/index.ts:120
MEDIUM js-shell-exec-interpolation apps/cli/src/index.ts:411
MEDIUM sql-template-interpolation apps/extension/scripts/build.js:320
MEDIUM sql-template-interpolation apps/extension/scripts/build.js:326
MEDIUM sh-remote-script-execution apps/web/public/install.sh:272
MEDIUM sh-remote-script-execution apps/web/public/install.sh:320
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

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

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit e8cf5bb into master Aug 11, 2026
11 checks passed
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