Skip to content

feat(scan): path exclusion via --exclude and .threatcrushignore (v0.8.0) - #101

Merged
ralyodio merged 2 commits into
masterfrom
feat/exclude-paths
Aug 11, 2026
Merged

feat(scan): path exclusion via --exclude and .threatcrushignore (v0.8.0)#101
ralyodio merged 2 commits into
masterfrom
feat/exclude-paths

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Release 0.8.0. Adds path exclusion, and uses it to stop the self-scan drowning in the scanner detecting its own reflection.

The problem

Scanning the ThreatCrush repo with ThreatCrush reported 141 findings, and 57 were self-reference:

  • rule definitionscode-rules.ts, secret-rules.ts contain regexes and example strings that match the very patterns they describe;
  • test fixtures__tests__ holds deliberately-vulnerable sample code.

Real findings in the product code were buried under the tool detecting itself.

The feature

A general exclusion mechanism — useful to any consumer for generated output or vendored trees, not only the self-scan:

  • --exclude <glob> on threatcrush scan, repeatable
  • a .threatcrushignore at the scan root, read automatically and merged with --exclude

Globs are gitignore-flavoured: a bare name (__tests__) matches at any depth, a pattern with a slash is anchored to the root, * stays within a segment and ** crosses them. A directory match prunes the whole subtree.

Excluding ≠ finding nothing

ScanReport.excluded counts the skipped paths (a pruned directory once, not per file) and the CLI prints it:

· 33 path(s) excluded by --exclude or .threatcrushignore

— the same "never silently quiet" guarantee suppressed already carries. A scan hushed by a broad glob can't be mistaken for a clean one.

Applied to this repo

A committed .threatcrushignore excludes the rule sources and fixtures:

__tests__
packages/scan/src/code-rules.ts
packages/scan/src/secret-rules.ts
packages/scan/src/manifest-rules.ts
modules/code-scanner/src/**/rules.ts

Self-scan: 141 → 67. The 57 self-references drop to 2 — and those 2 are real example-config secrets in spend-guard, not rule definitions, so they're correctly left in. Everything read is scanPath, so the daemon and every scan honour the file, not just the CI workflow.

Verification

  • Self-scan exits 0 with a non-empty SARIF (advisory, job passes); no workflow change needed — the merged self-scan-from-source build reads the committed ignore file.
  • Coverage gate unchanged — the testbed has no ignore file, so exclusion is inert there: TPR 65.9% / FPR 0%.
  • 132 package tests (up from 125). The glob matcher is pinned in both directions — a new exclude.test.ts asserts bare-name-at-any-depth, slash-anchoring, * vs ** segment behaviour, subtree pruning, comment/blank handling, and that a . in a pattern is literal (not "any char").
  • tsc --noEmit clean in package and CLI.

Release 0.8.0.

Scanning the ThreatCrush repo with ThreatCrush reported 141 findings, and 57
of them were the scanner detecting its own reflection: the rule definitions
(regexes and example strings that match the very patterns they describe) and
the test fixtures (deliberately-vulnerable sample code). Real findings in the
product were buried under them.

Adds a general exclusion mechanism — useful to any consumer for generated
output or vendored trees, not just the self-scan:

  - `--exclude <glob>` on `threatcrush scan`, repeatable
  - a `.threatcrushignore` file at the scan root, read automatically and
    merged with `--exclude`

Globs are gitignore-flavoured: a bare name matches at any depth, a pattern
with a slash is anchored to the root, `*` stays within a path segment and `**`
crosses them. A directory match prunes the whole subtree.

Excluding is not the same as finding nothing. `ScanReport.excluded` counts the
skipped paths — a pruned directory once, not per file — and the CLI prints it,
so a scan quieted by a broad glob cannot be mistaken for a clean one, the same
guarantee `suppressed` already carries.

A committed `.threatcrushignore` excludes this repo's rule sources and
fixtures. The self-scan drops 141 to 67, and the residual is product code plus
two example-config secrets — real targets, not reflections. The daemon and any
scan honour the file too, since it is read in `scanPath`.

Coverage gate unchanged (the testbed has no ignore file): TPR 65.9% / FPR 0%.
132 package tests, up from 125; the glob matcher is pinned in both directions
(over- and under-matching) by its own tests.

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

Copy link
Copy Markdown

ThreatCrush Security Scan

67 finding(s)

HIGH/CRITICAL: 11 | MEDIUM: 55 | LOW: 1

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
HIGH secret-generic-credential modules/spend-guard/config/example.conf.toml:13
HIGH secret-generic-credential modules/spend-guard/README.md:84
HIGH secret-generic-credential PRD.md:268
HIGH tls-verification-disabled prd/0004-find-dangerous-code-patterns-without-pretending-to-be-a-compiler.md:121
HIGH tls-verification-disabled prd/0004-find-dangerous-code-patterns-without-pretending-to-be-a-compiler.md:122
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 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 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:419
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:107
MEDIUM js-open-redirect apps/web/src/components/funding/FundingClient.tsx:97
MEDIUM manifest-install-lifecycle-script package.json:24

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

Snippets are redacted; ThreatCrush never prints matched credential material.

Comment thread packages/scan/src/node/walk.ts Fixed
@ralyodio
ralyodio force-pushed the feat/exclude-paths branch from ed9e0ba to a89ff03 Compare August 11, 2026 06:34
Comment thread packages/scan/src/node/walk.ts Fixed
CodeQL flagged js/polynomial-redos in the glob compiler added for --exclude,
and it was right twice over:

  - whatever cross-segment regex the globs compiled to backtracks on a path
    full of slashes, the exact shape this scanner's own redos-nested-quantifier
    rule exists to catch;
  - the trailing-slash trim `pattern.replace(/\/+$/, '')` is unanchored, so
    `replace` retries at every start position and is quadratic on a pattern
    value that is all slashes.

So the path is no longer run through one generated regex, and the trims no
longer use a backtracking one. `compileExcludes` splits both the pattern and
the path on `/` and aligns them with a two-pointer — the classic `**` match —
where `**` consumes zero or more segments and every other part matches exactly
one. Each per-segment matcher is a trivial `^…$` with single quantifiers
(consecutive `*` are collapsed so no `[^/]*[^/]*` adjacency survives), tested
against one bounded segment, never the whole path. Trailing slashes are trimmed
with a loop rather than an unanchored regex.

Semantics unchanged — every existing exclusion test still passes — plus cases
pinning a trailing `**` (matches files beneath, not the directory itself) and
that a 50,000-slash path and a 50,000-slash / 5,000-star pattern all resolve in
under a millisecond. walk.ts carries no findings and needs no suppression; the
ReDoS surface is gone, not silenced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ralyodio
ralyodio force-pushed the feat/exclude-paths branch from a89ff03 to d4f4b07 Compare August 11, 2026 06:39
@ralyodio
ralyodio merged commit 6de8473 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.

2 participants