Skip to content

feat(code-scanner): add the sast subsystem (PRD 0004) - #51

Merged
ralyodio merged 1 commit into
masterfrom
feat/code-scanner-sast
Jul 28, 2026
Merged

feat(code-scanner): add the sast subsystem (PRD 0004)#51
ralyodio merged 1 commit into
masterfrom
feat/code-scanner-sast

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Implements PRD 0004 — the sast/ subsystem of code-scanner.

The honest framing

Real static analysis — proving a path from an untrusted source to a dangerous sink — needs a parser, a CFG and interprocedural taint tracking per language. CodeQL and Semgrep are years of work each, and both already run in this repo's CI.

So this is not a SAST engine, and does not claim to be one. It is line-oriented pattern matching, and its value is coverage: source on a running server that never passed through the repository — vendored trees, generated files, a hotfix applied in place.

Confidence is part of the finding

Level Meaning Max severity
pattern the dangerous construct exists medium
contextual the construct sits alongside apparent untrusted input rule severity (up to critical)

The cap is enforced in severityFor() rather than per-rule, so no future rule can opt out of it by accident, and a test asserts it holds for every rule and every finding a real scan produces.

Presenting a regex match with the confidence of a proven vulnerability is what discredits a scanner — and, worse, discredits the accurate findings standing next to it.

Rules

Code execution (eval, new Function, vm), shell/SQL interpolation, unsafe HTML (innerHTML, dangerouslySetInnerHTML), disabled TLS verification, weak hashes on credentials, Math.random() for tokens, path traversal, unsafe YAML load, prototype pollution. Each carries a CWE and a consequence — an operator triages on what happens if it's real, not on a rule name.

Noise control is most of the work

  • exec('ls') with a literal is not a finding; exec(\${x}`)` is.
  • Math.random() is only flagged when assigned to something security-shaped — const jitter = Math.random() * 100 is silent.
  • Comment lines are skipped. Without that, the subsystem reports findings about its own rule descriptions.
  • Rules describing ordinary operations (reading a computed path) require context before firing at all.
  • Vendored/minified/generated code is ranked down, never hidden — a vulnerability in vendor/ still executes.

Suppressions can't be used to go quiet

// threatcrush-disable-next-line <rule> <reason> is per-rule, requires a reason, and every suppression is counted and reported. A scan reporting 0 findings and 200 suppressions must not read as clean.

Verification

86 tests (20 new), build clean.

Implements PRD 0004: line-oriented rules for dangerous constructs — dynamic
code execution, shell/SQL interpolation, unsafe HTML, disabled TLS
verification, Math.random for tokens, path traversal, prototype pollution.

The design constraint is honesty about what a regex can know. CodeQL and
Semgrep already run in this repo's CI and do real interprocedural analysis;
this covers what they cannot see — source on a running server that never
passed through the repository. So every finding carries a confidence:

  pattern     the construct exists. Capped at medium severity, enforced in
              severityFor() rather than per-rule so no future rule can opt out
              of the cap by accident.
  contextual  the construct sits alongside apparent untrusted input. The only
              level allowed to escalate to high or critical.

Presenting a bare pattern match with the confidence of a proven vulnerability
is the failure mode that discredits a scanner and every accurate finding
standing next to it. There is a test asserting the cap holds for every rule.

Noise control is the rest of the work: literal `exec('ls')` is not a finding
while `exec(\`${x}\`)` is; Math.random is only flagged when assigned to
something security-shaped; comment lines are skipped, without which the
subsystem reports findings about its own rule descriptions; rules describing
ordinary operations (reading a computed path) require context before firing at
all.

Suppressions are inline, per-rule, carry a reason, and are counted and
reported — a quiet scan full of suppressions is not a clean one.

86 tests (20 new).

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

Copy link
Copy Markdown

vu1nz Security Review

0 finding(s) in PR #?

No security issues found.


let text: string;
try {
text = await readFile(file, 'utf8');
@ralyodio
ralyodio merged commit dfb7c7b into master Jul 28, 2026
8 of 9 checks passed
@ralyodio
ralyodio deleted the feat/code-scanner-sast branch July 28, 2026 17:20
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