Repository navigation
fix(scanner): stop reporting test fixtures and quoted code as findings - #34
Conversation
The built-in scanner graded this repo F/0 on 17 findings, every one a false
positive. Two independent causes:
walk() had no test/fixture exclusion, so apps/web/tests/fixtures/vulnerable-app
— files deliberately full of fake keys and eval() — were walked as real source
(13 of 17). The exclusion matches the path relative to the scan root, not the
absolute path: security.test.ts passes the fixture directory in as the root, so
an absolute-path filter would make walk() return nothing there. walk() lost the
root on recursion, so it is now threaded through.
security.ts matched its own documentation: the literal text `eval()` inside
`title: 'Dangerous eval() usage'` matched /\beval\s*\(/ (4 of 17). This is a
general class that fires on linters, security tooling and XSS tutorials, so
strip-code.ts now blanks comments and string literals before the code-construct
patterns run, space-for-space with newlines preserved so line numbers are
unchanged. ${...} interpolations stay visible because they are real code.
The SQL-injection pattern is exempted from string blanking via
matchesStringContent — the fixture's SQL lives inside a template literal, so
blanking strings there would break detection. secrets.ts is untouched for the
same reason: a hardcoded key lives inside a string literal.
Stripping is applied only to .js/.jsx/.mjs/.cjs/.ts/.tsx — the syntax the
stripper understands. Other extensions match raw as before.
Self-scan now reports 0 findings, score 100, grade A.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYMoc47De5t8omQ3wPTZnV
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a90fa1190a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } else if (ch === '"' || ch === "'" || ch === '`') { | ||
| mode = ch === '"' ? 'double' : ch === "'" ? 'single' : 'template'; |
There was a problem hiding this comment.
Parse regex literals before stripping string delimiters
When valid JavaScript contains quote or backtick characters inside a regex literal, this branch treats them as the start of a string/template and blanks executable code that follows. For example, after const markdown = /([^]+)/g;, the stripper remains in template mode and removes a later eval(input)`, so the newly filtered security scan silently misses a real finding. Regex literals need to be recognized or parsed before applying these delimiter transitions.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 9810834. The stripper now reads a regex literal before the quote rules, so a backtick inside one does not start template mode. A / is division when the previous token is a value, so total / count stays code.
| const entries = await readdir(dir, { withFileTypes: true }); | ||
| for (const e of entries) { | ||
| if (SKIP_DIRS.has(e.name) || e.isSymbolicLink()) continue; | ||
| const full = path.join(dir, e.name); |
There was a problem hiding this comment.
Continue scanning test paths for exposed secrets
When a repository contains a real credential under tests/, e2e/, mocks/, or a *.test.* file, this unconditional exclusion prevents the shared walk() caller in secretsCheck from examining it. Secret validity does not depend on whether the file is test material, so a committed live AWS key, GitHub token, or webhook in one of these paths remains exploitable while the scan reports no finding; apply this exclusion only to checks for intentionally unsafe code, or otherwise retain secret scanning for these paths.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in bc5dc82. walk() now takes skipTestPaths. The code-pattern checks keep the exclusion, and secretsCheck turns it off, so a real key under tests/ is a finding again. The check filters on the value instead: it drops a match that carries a placeholder tell, which means a word such as example, a run of six identical characters, or a sequential run such as abcdef.
| for (let i = 0; i < codeLines.length; i++) { | ||
| for (const p of PATTERNS) { | ||
| if (p.regex.test(lines[i])) { | ||
| if (p.regex.test(p.matchesStringContent ? textLines[i] : codeLines[i])) { |
There was a problem hiding this comment.
Preserve executable source strings during filtering
When a JavaScript string is passed to an API that evaluates it as code, blanking every string hides real dangerous constructs rather than documentation. For example, setTimeout("document.write(location.hash)", 0) previously matched the document.write rule and actually executes that source in browsers, but this selection now scans only the blanked codeLines for that rule and reports nothing; executable-string contexts need to remain detectable.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
This is a deliberate trade. The blanked strings remove a false-positive class that fires on every linter repo and every security repo. setTimeout with a string argument is legacy and rare. The correct fix is a new rule that matches an evaluated string argument, not raw string text for every pattern. I logged it as a follow-up.
| // root, never the absolute path: scanning a repo skips its fixture directories, | ||
| // but scanning a fixture directory directly still returns the files inside it. | ||
| // `examples` is intentionally absent — example dirs do leak real keys. | ||
| const TEST_PATH = /(^|\/)(tests?|__tests__|spec|fixtures?|mocks?|testdata|e2e)(\/|$)|\.(test|spec)\./i; |
There was a problem hiding this comment.
Recognize underscored fixture and mock directories
When fixtures or mocks are colocated under the common Jest-style __fixtures__ or __mocks__ directories, this expression does not match those path segments, so walk() still scans their deliberately fake credentials and unsafe calls. The regex explicitly handles __tests__, making the omission of the corresponding underscored fixture and mock conventions likely accidental and leaving the false-positive class this change targets unresolved for those repositories.
Useful? React with 👍 / 👎.
Both from the Codex review on #34. A regex literal that contains a quote or a backtick put the stripper into string mode, so it blanked the rest of the file and lost real findings. After `const markdown = /`([^`]+)`/g;` a later `eval(input)` was not reported. The stripper now recognises regex literals before the quote rules and treats their body as text, with `/` inside a character class handled and a newline ending an unterminated literal. A `/` is read as division when the previous token is a value, so `total / count` is not mistaken for a literal. The test-path pattern matched `__tests__` but not the matching Jest conventions `__mocks__` and `__fixtures__`, so those directories were still scanned. It now accepts the underscored form of every name, and `specs?`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYMoc47De5t8omQ3wPTZnV
From the Codex review on #34. The test-path exclusion applied to `walk()` for every check, so a real AWS key or GitHub token committed under `tests/`, `e2e/` or a `*.test.*` file went unreported. A credential is exploitable wherever it lives, so the path it sits in must not suppress it. `walk()` now takes `{ skipTestPaths }`. The code-pattern checks keep the exclusion, because test material is deliberately unsafe. `secretsCheck` turns it off and filters on the value instead: a match is dropped when it carries a placeholder tell, which means a word such as `example` or `dummy`, a run of six or more identical characters, or a sequential run such as `abcdef`. That keeps the self-scan at zero without a blind spot. The three fixture secrets, `AKIAIOSFODNN7EXAMPLE`, `ghp_ABCDEF...` and `T0AAAAAA/B0AAAAAA/aaa...`, each carry one of those tells. The committed fixture must stay safe for GitHub push protection, so its secrets have to look fake, and the check is right to ignore them. The detection tests now write credential-shaped values to a temp directory at run time, and a new test pins the fixture as ignored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYMoc47De5t8omQ3wPTZnV
|
Codex review processed. Three findings fixed, one declined. Fixed — regex literals ( Fixed — underscored fixture dirs ( Fixed — secrets in test paths ( Declined — strings passed to evaluating APIs. Correct, and a real narrow regression: 61 tests pass. Self-scan reports 0 findings, score 100, grade A. |
What
The built-in scanner graded this repo F / 0 on 17 findings, every one a false positive. Both root causes are fixed; a self-scan now returns 0 findings, score 100, grade A.
Root cause 1 — no test/fixture exclusion (13 of 17)
walk()had no exclusion for tests, fixtures, specs or mocks, soapps/web/tests/fixtures/vulnerable-app/{app.js,config.js}— files deliberately full of fake AWS keys, GitHub tokens andeval()— were walked as real source.The exclusion matches the path relative to the scan root, not the absolute path.
security.test.tspasses the fixture directory in as the scan root, so an absolute-path filter would makewalk()return nothing there and take those tests red.walk()lost the root on recursion, so it is now threaded through aswalk(dir, root = dir).examples?is deliberately not excluded — example directories do leak real keys.Root cause 2 — the scanner detected its own documentation (4 of 17)
Not the regex literals: the human-readable strings.
title: 'Dangerous eval() usage'contains the literal texteval(), which/\beval\s*\(/matches.This is a general false-positive class that also fires on linters, security tooling, OWASP material and any XSS tutorial, so it's fixed generally rather than special-cased. New
strip-code.tsblanks comments and string literals before the code-construct patterns run — space-for-space with newlines preserved, so line and column numbers are unchanged.${...}interpolations stay visible because they are real code.Two deliberate carve-outs
matchesStringContent: true). The fixture's SQL isconst query = `SELECT * FROM users WHERE id = ${userId}`— the keyword lives inside a template literal, so blanking strings would break detection. It runs against comment-stripped-only text instead.secrets.tsis untouched for the same reason: a hardcoded key lives inside a string literal.Stripping is applied only to
.js/.jsx/.mjs/.cjs/.ts/.tsx— the syntax the stripper actually understands. Other extensions match raw, as before.Why both fixes were needed
packages/shared/src/scoring.tsdeducts 25 per critical, 15 per high, floor 0;getGrade()returns F below 50.Tests
walk.test.tsis new (3 cases, including the scan-root trap in both directions).security.test.tsgoes 4 → 10 cases, adding a regression that runssecurityCheckagainst the repo root and asserts zero findings — that pins both bugs at once.The line-number-preservation test was confirmed to actually fail by temporarily mutating the stripper to collapse newlines, then restored.
Not in scope
Follow-ups are written up locally in
docs/plans/HANDOFF-scanner-followups.md(gitignored). The short version: production scans are delegated to the Railway API (betterleaks/opengrep/trivy) wheneverSCANNER_BACKEND_URLis set, so this fix only changes the public grade if the built-in path is the one running; the SQL-injection pattern fires on ordinary English (sudo sbuild-update ... ${DIST}grades critical); and scores are bimodal — every scan in the DB is exactly 100 or exactly 0.🤖 Generated with Claude Code
https://claude.ai/code/session_01BYMoc47De5t8omQ3wPTZnV