Skip to content

fix(scan): soften findings inside Rust inline #[cfg(test)] blocks - #174

Merged
ralyodio merged 2 commits into
masterfrom
worktree-rust-cfg-test-softening
Sep 2, 2026
Merged

fix(scan): soften findings inside Rust inline #[cfg(test)] blocks#174
ralyodio merged 2 commits into
masterfrom
worktree-rust-cfg-test-softening

Conversation

@ralyodio

@ralyodio ralyodio commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Cause 4: a language whose tests live inside the file they test

The three structural false-positive causes fixed in #154 and #158 all key on where a file sits — _test.go, testutils/, docs/. Rust does not work that way. cargo test compiles unit tests from a #[cfg(test)] mod tests block at the bottom of the very file they cover, so production code and its fixtures share one path and isTestPath can never separate them.

Measured on ferriskey/ferriskey (Rust IAM server, 689 stars) at 0.11.8: 135 findings, zero true positives, of which 15 are exactly this — a fixture password inside a test module, reported at high from a path that looks like production.

The sharpest is core/src/domain/trident/services.rs:2719:

new_password: "Str0ng!P@ssword#2024".to_string(),

Its #[cfg(test)] opens at line 2137 of 4042. Being a good fake password is what kept it from being softened by any of the existing value-side rules — describesItsOwnKey catches let password = "my_password" and correctly ignores this one. Only the block it sits in says what it is.

What this adds

inlineTestLines() returns the line indices a file's own test blocks occupy, and a new test-block softening reason sits alongside test.

Keyed on lines, not on the file. services.rs is half production code. A "everything after the first #[cfg(test)]" rule would soften nearly 2000 lines of it, and a real credential committed below the test module would report at low for a reason nobody could see.

Finding the end of a block means counting braces, and counting braces in Rust means lexing it first. format!("{}", x) is on more lines than most constructs this engine looks for; counted naively it closes the module early and undoes the fix from the inside. rustCodeLines() blanks comments, strings and char literals, handling the three things a generic stripper gets wrong:

  • block comments nest, so the first */ does not necessarily end one
  • raw strings suspend escaping and close only on a quote followed by as many hashes as opened them — r#"a "quoted" string"#
  • a lone ' is far more often a lifetime (&'a str) than a char literal

An unbalanced file claims only its attribute line, so a parse that has already gone wrong cannot quietly silence the rest of the file.

Only Rust gets this. Go's toolchain will not run a test outside a _test.go file, and Python and JavaScript convention give tests their own files — all three already read by isTestPath.

Two things it deliberately does not do:

  • #[cfg(not(test))] gates code compiled when tests are off. It is production code by definition and is excluded.
  • #[cfg(feature = "test-util")] is a feature flag that merely has "test" in its name. The string blanking is what keeps it from reading as a gate.

Verification

End to end through the built CLI, not the source:

before after
ferriskey findings 135 135 (0 dropped)
ferriskey high 39 24
malware-test-prs 134 findings, 46 critical 134 findings, 46 critical, 0 severity moves

All 15 moved lines were confirmed to sit inside a #[cfg(test)] module by a check independent of the implementation (no top-level } closes the module between the attribute and the finding). No production line moved. Scan wall clock on ferriskey is unchanged at ~4.0s.

packages/scan 347/347 and apps/cli 73/73 green.

The line this does not cross

As with every other softening here, this moves severity and never drops a finding — the count, the SARIF and --fail-on low are all unaffected. The fixture exemption that actually skips a finding stays keyed on isTestPath alone: dropping is a verdict, and a block boundary inferred from brace counting is evidence for a severity, not for silence.

Credential fixtures in the new test file are assembled at runtime rather than written out, since the gitleaks job reads every ref and a password-shaped literal on this branch would redden unrelated PRs.

What this does not fix

ferriskey still scores 0 true positives out of 135, and this change does not make it pitchable — the remaining 120 are the same secret-database-url on local dev DSNs, js-open-redirect on a server-supplied URL the backend validates, and js-jwt-decode-without-verify on a browser reading its own token. Rust is 63% of that repository and the engine has no Rust rules at all. This fixes one structural cause; it does not change the verdict on Rust-majority repos.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CVMa6BbfoMTWAiGvkFXAdk

The three structural false-positive causes fixed in #154 and #158 all key on
where a *file* sits: `_test.go`, `testutils/`, `docs/`. Rust does not work that
way. `cargo test` compiles unit tests from a `#[cfg(test)] mod tests` block at
the bottom of the very file they cover, so production code and its fixtures
share one path and `isTestPath` can never separate them.

Measured on ferriskey/ferriskey (Rust IAM, 689 stars) at 0.11.8: 135 findings,
zero true positives, of which 15 are exactly this — a fixture password in a test
module reported at `high` from a path that looks like production. The sharpest is
`core/src/domain/trident/services.rs:2719`, where the test module opens at 2137
of 4042 lines; being a *good* fake password is what kept it from being softened
by any of the existing value-side rules.

Adds `inlineTestLines()`, which returns the line indices a file's own test blocks
occupy, and a `test-block` softening reason alongside `test`. Keyed on lines
rather than on the file, because the file is half production code: a
to-end-of-file rule would soften nearly 2000 lines of `services.rs` and hide a
real credential committed below the test module.

Finding the end of a block means counting braces, and counting braces in Rust
means lexing it first — `format!("{}", x)` would otherwise close the module early
and undo the fix from the inside. `rustCodeLines()` blanks comments, strings and
char literals, handling the three things a generic stripper gets wrong: nested
block comments, raw strings (`r#"a "quoted" string"#`), and `'a` lifetimes that
are not char literals. An unbalanced file claims only its attribute line, so a
parse that has gone wrong cannot quietly silence the rest of the file.

Only Rust gets this. Go's toolchain will not run a test outside a `_test.go`
file, and Python and JavaScript convention give tests their own files — all three
already read by `isTestPath`.

Verified end to end through the built CLI:

- ferriskey: 135 findings before and after, nothing dropped, high 39 -> 24. All
  15 moved lines confirmed inside a `#[cfg(test)]` module by an independent
  check; no production line moved.
- malware-test-prs: 134 findings, 46 critical, identical before and after, zero
  severity moves. Detection is unchanged.
- packages/scan 347/347 and apps/cli 73/73 green.

As with every other softening here, this moves severity and never drops a
finding: the count, the SARIF and `--fail-on low` are all unaffected. The fixture
exemption that *skips* a finding stays keyed on `isTestPath` alone — dropping is
a verdict, and a block boundary inferred from brace counting is evidence for a
severity, not for silence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVMa6BbfoMTWAiGvkFXAdk
Comment thread packages/scan/src/text.ts Fixed
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

12 finding(s)

HIGH/CRITICAL: 1 | MEDIUM: 6 | LOW: 5

Severity Rule Location
HIGH secret-aws-access-key prd/0003-detect-hardcoded-secrets-before-they-are-committed-or-served.md:126
MEDIUM js-open-redirect apps/web/src/app/auth/login/page.tsx:50
MEDIUM js-unescaped-html-sink apps/web/src/app/hire/page.tsx:96
MEDIUM js-unescaped-html-sink apps/web/src/app/hire/page.tsx:100
MEDIUM js-open-redirect apps/web/src/components/funding/FundingClient.tsx:97
MEDIUM js-unescaped-html-sink apps/web/src/components/GuideReader.tsx:265
MEDIUM js-uninitialized-buffer packages/scan/src/node-rules.ts:456
LOW secret-generic-credential PRD.md:269
LOW tls-verification-disabled prd/0004-find-dangerous-code-patterns-without-pretending-to-be-a-compiler.md:121
LOW tls-verification-disabled prd/0004-find-dangerous-code-patterns-without-pretending-to-be-a-compiler.md:122
LOW sh-remote-script-execution scripts/smoke-test.sh:47
LOW secret-aws-access-key scripts/smoke-test.sh:112

Snippets are redacted; ThreatCrush never prints matched credential material.

…omment

The scanner flagged its own new doc comment: `text.ts` quoted the ferriskey line
it was written to explain, credential and all, and `text.ts` is a production path
where no softener applies. That is the rule working exactly as intended, on the
commit that shipped it.

Describes the measurement instead of transcribing it. Also corrects 22 to 15 —
22 was the count of findings sitting inside a `#[cfg(test)]` block, but 7 of
those were already `low` from a value-side rule, so 15 is the number this change
actually moves. Comment only; ferriskey scans identically before and after
(135 findings, high 24), and `packages/scan` stays 347/347.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVMa6BbfoMTWAiGvkFXAdk
@ralyodio
ralyodio merged commit 421d208 into master Sep 2, 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.

2 participants