@clear-capabilities/agentic-security-scanner 0.136.2 → 0.137.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +880 -0
- package/bin/agentic-security.js +189 -37
- package/dist/113.index.js +13 -4
- package/dist/178.index.js +1 -1
- package/dist/207.index.js +5 -4
- package/dist/238.index.js +1 -1
- package/dist/317.index.js +36 -6
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +192 -15
- package/dist/444.index.js +20 -11
- package/dist/449.index.js +8 -1
- package/dist/526.index.js +3 -3
- package/dist/637.index.js +1 -1
- package/dist/agentic-security.mjs +15 -15
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/nist-privacy-1-1.json +2 -2
- package/dist/compliance-frameworks/owasp-asvs-5.json +1 -1
- package/package.json +21 -13
- package/src/dataflow/CLAUDE.md +12 -4
- package/src/dataflow/builtin-summaries.js +1 -1
- package/src/dataflow/catalog-expanded.js +1 -0
- package/src/dataflow/catalog.js +157 -31
- package/src/dataflow/engine.js +639 -112
- package/src/dataflow/implicit-flow.js +68 -36
- package/src/dataflow/incremental.js +18 -3
- package/src/dataflow/index.js +17 -1
- package/src/dataflow/points-to.js +19 -6
- package/src/dataflow/proven-clean.js +41 -0
- package/src/dataflow/sanitizer-gate.js +35 -9
- package/src/dataflow/sanitizer-proof.js +21 -3
- package/src/dataflow/stub-aware-filter.js +36 -13
- package/src/dataflow/summaries.js +21 -2
- package/src/engine.js +430 -196
- package/src/ir/CLAUDE.md +16 -2
- package/src/ir/balanced-call.js +55 -0
- package/src/ir/class-hierarchy.js +57 -11
- package/src/ir/index.js +14 -2
- package/src/ir/parser-cs.js +513 -40
- package/src/ir/parser-go.js +29 -11
- package/src/ir/parser-java.js +300 -20
- package/src/ir/parser-js.js +300 -22
- package/src/ir/parser-kt.js +436 -18
- package/src/ir/parser-php.js +631 -38
- package/src/ir/parser-py.helper.py +32 -2
- package/src/ir/parser-py.js +31 -4
- package/src/ir/parser-rb.js +161 -26
- package/src/ir/ssa.js +6 -1
- package/src/lsp/server.js +35 -3
- package/src/mcp/CLAUDE.md +9 -2
- package/src/mcp/redact.js +26 -0
- package/src/mcp/tools.js +164 -15
- package/src/posture/CLAUDE.md +19 -7
- package/src/posture/accuracy-scorecard.js +9 -1
- package/src/posture/aibom.js +12 -8
- package/src/posture/auditor-walkthrough.js +102 -3
- package/src/posture/autopilot.js +8 -1
- package/src/posture/calibration-drift.js +11 -5
- package/src/posture/calibration.js +24 -2
- package/src/posture/clustering.js +12 -1
- package/src/posture/compliance-frameworks/nist-privacy-1-1.json +2 -2
- package/src/posture/compliance-frameworks/owasp-asvs-5.json +1 -1
- package/src/posture/compliance-policy.js +33 -1
- package/src/posture/confidence.js +44 -10
- package/src/posture/corpus-enroll.js +9 -5
- package/src/posture/corpus-match.js +19 -0
- package/src/posture/csharp-analysis.js +62 -3
- package/src/posture/deploy-platform.js +4 -1
- package/src/posture/drift.js +7 -1
- package/src/posture/epss.js +13 -1
- package/src/posture/evidence-bundle.js +36 -6
- package/src/posture/exploitability-probability.js +13 -1
- package/src/posture/falsification.js +23 -2
- package/src/posture/fix-metrics.js +1 -1
- package/src/posture/fix-verify-loop.js +10 -1
- package/src/posture/iac-reachability.js +14 -8
- package/src/posture/integrity.js +25 -7
- package/src/posture/model-rescan.js +65 -0
- package/src/posture/mttr.js +5 -0
- package/src/posture/poc-inprocess.js +27 -8
- package/src/posture/regression-test-gen.js +23 -8
- package/src/posture/reverse-blast-radius.js +5 -1
- package/src/posture/risk-dollars.js +18 -1
- package/src/posture/sbom.js +2 -2
- package/src/posture/secret-history.js +20 -11
- package/src/posture/security-trend.js +7 -1
- package/src/posture/stack-playbook.js +22 -1
- package/src/posture/threat-model-grounding.js +2 -2
- package/src/posture/validator-metrics.js +10 -3
- package/src/posture/verifier.js +32 -57
- package/src/report/index.js +183 -14
- package/src/runScan.js +1 -1
- package/src/sast/_comment-strip.js +15 -4
- package/src/sast/_secret-entropy.js +1 -1
- package/src/sast/authz.js +6 -4
- package/src/sast/bench-shape/index.js +2 -7
- package/src/sast/claude-md-prompt-injection.js +14 -3
- package/src/sast/cloud-iam.js +60 -7
- package/src/sast/cpp-bench-extras.js +1 -1
- package/src/sast/csrf.js +7 -5
- package/src/sast/env-hygiene.js +5 -2
- package/src/sast/iac-terraform.js +25 -0
- package/src/sast/java-bench-extras.js +1 -1
- package/src/sast/java-constant-fold.js +5 -5
- package/src/sast/llm-owasp.js +4 -2
- package/src/sast/mcp-audit.js +7 -0
- package/src/sast/pipeline.js +8 -0
- package/src/sast/prompt-template.js +8 -6
- package/src/sast/prototype-pollution.js +6 -2
- package/src/sast/redos-nfa.js +6 -6
- package/src/sast/secret-concat.js +13 -2
- package/src/sast/ssrf-cloud-metadata.js +6 -3
- package/src/sast/xss-reflected-multilang.js +1 -1
- package/src/sast/xxe.js +1 -1
- package/src/sca/CLAUDE.md +3 -4
- package/src/sca/container.js +35 -3
- package/src/sca/dep-confusion.js +7 -0
- package/src/sca/sarif-ingest.js +0 -187
package/src/ir/CLAUDE.md
CHANGED
|
@@ -10,7 +10,12 @@ consumed by `scanner/src/dataflow/` for taint analysis.
|
|
|
10
10
|
| JS / TS | `parser-js.js` | `@babel/parser` |
|
|
11
11
|
| Python | `parser-py-cst.js` | Python 3.8+ stdlib `ast` via subprocess (default when available) |
|
|
12
12
|
| Python | `parser-py.js` | Hand-rolled regex parser (fallback when python3 missing) |
|
|
13
|
-
| Java | `parser-java.js` | `java-parser` npm package (async)
|
|
13
|
+
| Java | `parser-java.js` | `java-parser` npm package (**async only** — the deep path in `engine.js` therefore awaits `buildProjectIRAsync` when any `.java` file is present, and uses the sync builder otherwise).
|
|
14
|
+
⚠ Three defects made Java taint impossible until v0.136.3+: the sync-only call site; a CST walk looking for `blockStatement` on a `block` (java-parser nests `block → blockStatements → blockStatement`), which emptied every method CFG; and `exprFromCst` missing the `primary → primaryPrefix + primarySuffix` form that models **every** method call. Guarded by `test/java-taint-flow.test.js`. Real parameter names (previously always `params: []`, marked "deferred") plus Spring `@RequestParam`/`@PathVariable`/`@RequestBody`/`@RequestHeader` param annotations (`fn.paramAnnotations`) are extracted in one CST walk over `formalParameterList` (PRD R14(a) Task 5). Varargs parameters (`String... args`) live under a distinct `variableArityParameter` node this walk doesn't extract from — they're gracefully dropped, not corrupted or crashed on. **PRD R9 (partial): `fn.calls` is now populated** via the shared `call-sites.js#callSitesFromCfg` (the same language-agnostic helper `parser-py-cst.js` uses) — Java's CFG nodes (`call`, `assign`, `return`, `if`) already matched the documented contract. This creates real cross-file call-graph edges for Java (`callgraph.js`'s `edges`/`callersOf`/`resolveKnownCallee`, previously always empty for Java) — it does NOT change the generic tainted-call-argument fallback in `engine.js`'s `exprTaint`, which reads CFG expression args directly off `expr.args` and already worked for Java independent of `fn.calls`. However, **same-class (intra-class) method calls do not resolve, in both the bare and `this.`-qualified forms**: `parser-java.js` names functions `"App.buildCmd"` (class-qualified), but a bare call extracts `"buildCmd"` (unqualified) and `callgraph.js`'s name-based resolution cannot match them — this is the most idiomatic Java call shape (private helpers, intra-class delegation) and remains a real gap, documented here as a candidate follow-up PRD item (a per-file bare-tail fallback in `callgraph.js`, mirroring the existing `~bare~`-key collision-refusal pattern, would plausibly fix it without touching `parser-java.js`). `this.buildCmd(id)` is not merely unresolved, it's worse: `parser-java.js:76` lowers any `this.`-qualified call whose prefix isn't a plain FQN to the literal callee string `"unknown"`, so it doesn't fail closed, it fails to a fabricated name. Guarded by `test/parser-java-calls.test.js` (the test's honest caveat discloses this; the fixture shows that edges exist but unresolved) (mirrors `test/parser-rb-calls.test.js`, the identical `fn.calls` wiring, though Ruby's unqualified names make bare-call resolution work there). **PRD R8: `walkStmts` now recurses into `for`/`try`/`switch`/`do`/bare-block bodies** (previously only `if`/`while` were walked — every other braced statement kind silently dropped its body from the CFG, including try-with-resources, the single most idiomatic JDBC shape). A fix round closed two further gaps the initial review found: enhanced-for (`for (x : xs)`) now synthesizes an assign binding the loop variable to the iterated expression, so the variable itself carries taint provenance (mirroring `parser-js.js`'s `ForOfStatement` pattern); and Java 14+ arrow-form `switch` (`case 1 -> …`) is now recognized via a second CST-shape branch. Deferred, not fixed: `forInit`/`forUpdate` clauses of a basic 3-clause `for` loop are still not walked (only the loop body is); `synchronized` blocks and labeled statements are still fully dropped. Guarded by `test/parser-java-control-flow.test.js`. **Measured `bench/layer-recall` impact: unchanged, 1/25 before and after** — the fix is real and directly proven by the dedicated unit tests above, but this corpus's existing 25 Java fixtures happen not to place a sink genuinely inside a braced control-flow body (the ones with `if`/`try` syntax use it as a single-line guard clause ahead of a flat-level sink, not a nested one) — see the PRD R8 status entry for the full explanation and the same finding for C#. |
|
|
15
|
+
| Ruby | `parser-rb.js` | Hand-rolled. **`DEF_RE` must not let `\s*` cross a newline** — it did, and the body slice then started after the method's first statement, silently dropping it from every method (a one-statement body became empty). Measured as Ruby 0/20 IR-TAINT recall in `bench/layer-recall`. Guarded by `test/parser-php-rb.test.js`. ⚠ Also emitted no `fn.calls` at all (every OTHER parser does) — `callgraph.js`'s edges/callersOf/resolveKnownCallee are built entirely from `fn.calls`, so this left dead-code demotion and any interprocedural signal that depends on real call-graph resolution (rather than engine.js's generic tainted-call-argument fallback) permanently blind to Ruby. Fixed by deriving `fn.calls` from the CFG via the shared `call-sites.js#callSitesFromCfg` (the same helper `parser-py-cst.js` uses) — Ruby's node shapes already matched its documented contract. Guarded by `test/parser-rb-calls.test.js`. |
|
|
16
|
+
| PHP | `parser-php.js` | Regex-based, hand-rolled. ⚠ **PRD R8 was this codebase's hardest single task — a genuine 3-fix-round debugging saga, all substantially about line-number precision, not detection shape.** The core fix flushes the statement splitter on a closing `}` (previously only on `;`), and adds `try`/`switch` recognizers plus a recursion guard, so statements inside `if`/`while`/`foreach`/`try`/`switch` bodies are now real CFG nodes instead of being dropped or folded into a bogus call node — this alone also resolved a pre-existing bug where `if`/`while`/`foreach` bodies were already being mis-split even before R8 touched them. Round 1 fixed the naive `}`-flush breaking `if`/`else` and multi-clause `try` (via a continuation-keyword lookahead) and switch/case's first-statement drop (via a `:`-based flush, careful to exclude `::` so PHP 8.1 enum cases and `case Foo::BAR:` class-constant labels aren't false-positived) — but round 1's own line-tracking approach was then found wrong for comment-bearing bodies and multi-line headers, a regression the round itself introduced. Round 2 fixed that (comment-skip handlers were discarding newlines uncounted; use exact `_countNewlines`-based computation everywhere) but its own re-review found the overall "exact line" property still failed, due to a *different*, genuinely pre-existing bug in the function-body's own base-line computation (wrong for Allman-brace style, multi-line signatures, blank-line-preceded functions) plus a genuine new regression from round 1 (a comment between `}` and `else`/`catch`/`finally` dropped that continuation's body entirely). Round 3 fixed both and was confirmed clean by an 8-shape holistic sweep. Also fixed along the way: dead `finally` support (greedy regex capture bug), unrecognized `try{}finally{}` with no `catch`, and the `::` case-label false-positive — via a hand-rolled balanced-brace scanner replacing the fragile regex approach. **Known, deliberately deferred gaps** (full list + grouping in the PRD R8 status entry): the PHP 8 `match` expression is unmodeled (same class as Java's arrow-switch); `if`/`else`'s pre-existing greedy-capture bug still drops the else-body's first statement; a heredoc containing a bare `}` loses its sink entirely (a real regression from R8's original commit, not the fix rounds); `#`-style comments are invisible to the splitter (pre-existing, more exposed now that control-flow bodies are reachable); `_extractBody` is not comment-aware, so an apostrophe inside *any* `//` comment (e.g. "don't" — extremely common in real PHP) silently drops the **entire file's** IR (pre-existing, large, real — see the Kotlin row below for the same defect class); `elseif`/`else if` chains are still fully unsupported (pre-existing). Guarded by `test/parser-php-control-flow.test.js`. **Measured `bench/layer-recall` impact: unchanged, 1/23 before and after — the corpus's own `1/23 → 2/23` movement is real but belongs to R14(b) (PHP `<module>` top-level lowering), not this task**, confirmed by commit-swap A/B testing (the pre-R8 parser still reproduces 2/23; the pre-R14(b) parser reproduces only 1/23). None of this corpus's `pre/` PHP fixtures place a sink genuinely inside a braced control-flow body — same explanation as the other three languages' rows. |
|
|
17
|
+
| C# | `parser-cs.js` | Hand-rolled. ⚠ `_lowerExpr`'s string-concat branch **must** guard on `_splitTopLevelPlus` returning more than one part — when the `+` is nested inside parens the splitter returns the input unchanged and the branch recurses on the identical string (stack overflow, swallowed by `buildProjectIR`'s per-file catch, surfacing only as "no IR"). `new Type(args)` is lowered to a call so taint reaches constructor sinks such as `new SqlCommand`. Guarded by `test/parser-cs-kt.test.js`. **PRD R8: `_buildCfg` was rewritten from a flat linear CFG loop into a recursive builder** (ported from `parser-cpp.js`'s proven pattern) that recurses into `if`/`else`/`while`/`for`/`foreach`/`switch`/`do`/`try`/`catch`/`finally` bodies, with exact character-offset line computation and a `foreach`/`for`-init loop-variable assign (same lesson Java's for-each fix needed). Landed with one fix round: `using (...) { }` and `lock (...) { }` bodies were completely invisible (not in the recognized-keyword regex) — `using` is THE canonical ADO.NET wrapper around exactly the sinks this task targets, so this was a real, significant gap, fixed with a one-word regex addition. The task also deliberately deviated from its own brief in one place: it added a guarded `}`-flush to the statement splitter (the brief said not to; the reviewer confirmed the brief's own illustrative code would have dropped every statement following a control-flow block, and independently verified the deviation safe against collection/object initializers and lambda arguments). Deferred, not fixed: a collection-initializer-then-chained-call shape mis-splits (not a regression); the pre-existing `@"C:\"` verbatim-string escape bug (confirmed unchanged). Guarded by `test/parser-cs-control-flow.test.js`. **Measured `bench/layer-recall` impact: unchanged, 1/21 before and after** — same explanation as Java's row: this corpus's existing C# fixtures with `if`/`try`/`using` syntax use it as a guard clause ahead of a flat-level sink rather than nesting the sink inside the body, so they don't exercise the exact shape this fix targets. |
|
|
18
|
+
| Kotlin | `parser-kt.js` | Hand-rolled, parallel approach to `parser-cs.js`. **PRD R8: `_buildCfg` was rewritten from a flat linear CFG loop into a new recursive builder mirroring C#'s**, with Kotlin-specific adaptations — a `_consumeChunk` chain-consumer (Kotlin's splitter doesn't flush on `}` the way C#'s does) and dedicated `_buildWhenArms` parsing (`when`'s `else` arm would otherwise collide with `if`/`else` chaining in a generic keyword scan). This was the cleanest of the four R8 tasks — zero fix rounds — in part because the implementer was briefed on the other three tasks' hard-won lessons up front: it self-caught and fixed, before it could become a fix round, the exact same function-body-base-line-anchor bug PHP's hardest round found; it correctly identified and fixed a real gap (trailing-lambda calls like `xs.forEach { x -> ... }` genuinely fell through to `{kind:'unknown'}` before the fix); and it made a deliberate, correctly-scoped decision **not** to special-case `synchronized(lock) { }` — confirmed to be an ordinary Kotlin stdlib `inline fun`, not real keyword grammar, unlike C#'s `lock`. Statement-position control flow (`if`/`while`/`for`/`when`/`do`/`try`/`catch`/`finally`) is now covered; was previously the documented 0% Kotlin taint recall in `bench/layer-recall` (0/20). **Measured `bench/layer-recall` impact: unchanged, 0/20 before and after** — the dedicated unit tests below directly prove the fix works for a sink genuinely nested inside a control-flow body, but none of this corpus's 20 existing Kotlin fixtures happen to place a sink that way (same explanation as Java's and C#'s rows) — a candidate future item is enrolling a Kotlin corpus fixture that actually exercises this shape. **Two important, NOT-a-regression scope boundaries, worth reading before assuming Kotlin's control-flow support is complete:** (1) control flow *inside* any trailing lambda (`.forEach{}`, `.use{}` — the idiomatic Kotlin JDBC/resource-management shape — `.run{}`/`.apply{}`/`.let{}`/`.also{}`) is still invisible; the lambda's call SITE is captured, but its body is not parsed, so a sink one level inside a trailing lambda is exactly as invisible as before this task — and this is plausibly where a lot of real Kotlin taint-relevant code actually lives. (2) expression-position `if` (`val r = if (...) {...} else {...}`, Kotlin's ternary-replacement idiom) still drops both branch bodies. Also deferred: `_extractBody`'s comment-unawareness — the same defect class as PHP's (see the PHP row), an apostrophe inside any `//` comment silently drops the whole function, confirmed possibly larger real-world impact than the CFG gap this task closed. Guarded by `test/parser-kt-control-flow.test.js`. ⚠ **`bench:self-scan:check`, run during this PRD's own Task 5 (full-gate verification), caught a genuine ReDoS this task's new trailing-lambda regex introduced** (`/^([\w.]+)\s*(\([^()]*\))?\s*\{[\s\S]*\}\s*$/` — an optional paren group sandwiched between two `\s*` quantifiers, the identical defect class R14(a)'s C# `attrRegex` ReDoS; confirmed genuinely quadratic by direct timing, not a detector false alarm). Fixed the same way that precedent was: restructured into two mutually-exclusive alternatives (no-parens / with-parens) rather than one optional group, re-verified linear and byte-identical across a 15-shape sweep; no `bench/self-scan/BASELINE.json` bump was needed once fixed. **Separately, and NOT fixed here** (pre-existing since commit `99c2b6a`, 2026-05-20 — predates this PRD entirely, already counted in the pre-R8 self-scan baseline): the variable-declaration regex a few lines above (`decl`, matching `val`/`var … : Type = expr`) has the same adjacent-`\s*`-around-ambiguous-content shape and is also confirmed genuinely quadratic — logged as a candidate future item, matching this PRD's own precedent for pre-existing bugs found incidentally (e.g. the R14(a) C# parenthesized-attribute-argument note). |
|
|
14
19
|
| C / C++ | `parser-cpp.js` | Hand-rolled parser (functions, qualified names, CFG lowering). Dispatched by extension (`c/cc/cpp/cxx/h/hh/hpp/hxx`) in both `buildProjectIR` and `buildProjectIRAsync`. |
|
|
15
20
|
| Long-tail (rust/solidity/go/swift/dart) | `tree-sitter-loader.js` | **Optional** `web-tree-sitter` + `tree-sitter-wasms` (ABI-pinned 0.20.8 ↔ 0.1.13), lazy + degrades when absent. Powers `sast/tree-sitter-sinks.js` (opt-in via `AGENTIC_SECURITY_TREE_SITTER=1`). Marked `--external` in the build so the committed bundle never embeds WASM. |
|
|
16
21
|
|
|
@@ -153,6 +158,11 @@ Every parser must produce this shape:
|
|
|
153
158
|
name: 'function_name',
|
|
154
159
|
line: 42,
|
|
155
160
|
params: ['arg1', 'arg2', ...],
|
|
161
|
+
paramAnnotations: [{index, name, decorator}], // R14(a): OPTIONAL — present only
|
|
162
|
+
// when a param carries a recognized
|
|
163
|
+
// framework decorator/attribute.
|
|
164
|
+
// Populated by parser-cs.js,
|
|
165
|
+
// parser-js.js, parser-java.js.
|
|
156
166
|
file: 'file.py',
|
|
157
167
|
cfg: {
|
|
158
168
|
entry: 'nodeId',
|
|
@@ -174,7 +184,11 @@ Every parser must produce this shape:
|
|
|
174
184
|
}
|
|
175
185
|
}
|
|
176
186
|
],
|
|
177
|
-
topLevel: null, //
|
|
187
|
+
topLevel: null, // qid of the synthetic <module> function wrapping
|
|
188
|
+
// top-level statements, or null if the file has none
|
|
189
|
+
// worth lowering. JS, Python (CST + regex), PHP, and
|
|
190
|
+
// Ruby all populate this (PRD R14(b)); the other IR
|
|
191
|
+
// languages still leave it null.
|
|
178
192
|
}
|
|
179
193
|
```
|
|
180
194
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Shared by every hand-rolled regex IR parser (parser-cs.js, parser-go.js,
|
|
2
|
+
// parser-php.js, parser-rb.js) for extracting a call's callee + argument
|
|
3
|
+
// text without corruption when the call is followed by a chained member
|
|
4
|
+
// access (`Sanitize(x).Trim()`, `$obj->clean($x)->trim()`).
|
|
5
|
+
//
|
|
6
|
+
// The naive pattern every one of those files used —
|
|
7
|
+
// `/^(calleeRe)\s*\((.*)\)\s*$/s` — matches `(.*)` GREEDILY against the
|
|
8
|
+
// LAST `)` in the string, not the one balancing the FIRST `(`. For a
|
|
9
|
+
// chained call that swallows the chain's own parens into the argument
|
|
10
|
+
// text (`Sanitize(x).Trim()` produced argsText = "x).Trim("), which then
|
|
11
|
+
// fails to parse as any recognized expression shape in `_lowerExpr` and
|
|
12
|
+
// falls through to `{kind:'unknown'}` — silently losing whatever
|
|
13
|
+
// taint-relevant identifiers were inside the FIRST call's real argument
|
|
14
|
+
// list, exactly the shape a wrapped-and-then-methodcalled sanitizer or
|
|
15
|
+
// helper produces in real code.
|
|
16
|
+
//
|
|
17
|
+
// This scans forward from the first `(` tracking paren/bracket/brace
|
|
18
|
+
// depth and string-literal state (so a `)` or `,` inside a nested call or
|
|
19
|
+
// a string literal doesn't miscount) to find the REAL matching close
|
|
20
|
+
// paren. It deliberately does NOT require that close paren to be the end
|
|
21
|
+
// of the string — a trailing chain (`.Trim()`, `->trim()`, `::foo()`) is
|
|
22
|
+
// simply left unconsumed rather than corrupting anything. Recovering the
|
|
23
|
+
// first call's real signature is a strict improvement over the prior
|
|
24
|
+
// choice between silent corruption and no match at all.
|
|
25
|
+
export function matchBalancedCall(s, calleeRe) {
|
|
26
|
+
if (typeof s !== 'string') return null;
|
|
27
|
+
const m = calleeRe.exec(s);
|
|
28
|
+
if (!m || m.index !== 0) return null;
|
|
29
|
+
let i = m[0].length;
|
|
30
|
+
while (s[i] === ' ' || s[i] === '\t') i++;
|
|
31
|
+
if (s[i] !== '(') return null;
|
|
32
|
+
const openIdx = i;
|
|
33
|
+
let depth = 0;
|
|
34
|
+
let inStr = null;
|
|
35
|
+
let escape = false;
|
|
36
|
+
for (; i < s.length; i++) {
|
|
37
|
+
const c = s[i];
|
|
38
|
+
if (escape) { escape = false; continue; }
|
|
39
|
+
if (inStr) {
|
|
40
|
+
if (c === '\\') { escape = true; continue; }
|
|
41
|
+
if (c === inStr) inStr = null;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (c === '"' || c === '\'') { inStr = c; continue; }
|
|
45
|
+
if (c === '(' || c === '[' || c === '{') { depth++; continue; }
|
|
46
|
+
if (c === ')' || c === ']' || c === '}') {
|
|
47
|
+
depth--;
|
|
48
|
+
if (depth === 0) break;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (depth !== 0 || s[i] !== ')') return null; // unbalanced — refuse to guess
|
|
53
|
+
const callee = m[1] !== undefined ? m[1] : m[0];
|
|
54
|
+
return { callee, argsText: s.slice(openIdx + 1, i) };
|
|
55
|
+
}
|
|
@@ -20,9 +20,18 @@
|
|
|
20
20
|
// What we DO catch:
|
|
21
21
|
// - `class Foo {}` declarations + their method signatures.
|
|
22
22
|
// - `class Bar extends Foo {}` extends relationships.
|
|
23
|
-
// - `let x = new Foo()` typed-LHS inference.
|
|
24
|
-
//
|
|
25
|
-
//
|
|
23
|
+
// - `let x = new Foo()` typed-LHS inference — and ONLY this shape. The
|
|
24
|
+
// assign's RHS must be a call with `isNew: true` and a bare PascalCase
|
|
25
|
+
// (or already-known-class) identifier callee.
|
|
26
|
+
//
|
|
27
|
+
// What this header used to claim and the code has never done (corrected in
|
|
28
|
+
// whole-branch review; do not re-add the claim without the code):
|
|
29
|
+
// - `const x: Foo = ...` TS-annotated-LHS inference — NOT implemented. A
|
|
30
|
+
// TS type annotation never reaches typeOfVar.
|
|
31
|
+
// - `function buildFoo(): Foo { ... }` typed-return inference — NOT
|
|
32
|
+
// implemented. `const x = buildFoo()` is untyped (correctly: it is a
|
|
33
|
+
// plain call, not a `new`), so classOfVar returns null for `x`.
|
|
34
|
+
// Both are "unknown", which downstream consumers must treat as permissive.
|
|
26
35
|
|
|
27
36
|
const _AST_CACHE = new WeakMap();
|
|
28
37
|
|
|
@@ -62,16 +71,45 @@ export function buildClassHierarchy(perFileIR) {
|
|
|
62
71
|
}
|
|
63
72
|
}
|
|
64
73
|
if (!Array.isArray(ir.functions)) continue;
|
|
65
|
-
// Recover class names from method qids
|
|
66
|
-
//
|
|
67
|
-
//
|
|
74
|
+
// Recover class names from method qids. Two shapes are recognized:
|
|
75
|
+
// 1. "Foo.bar@line#hash" — class and method dot-joined in the qid's
|
|
76
|
+
// last segment (parser-cpp.js's convention).
|
|
77
|
+
// 2. "<file>::ClassName::method@line#hash" — class and method as
|
|
78
|
+
// separate `::`-joined qid segments (parser-js.js's and
|
|
79
|
+
// parser-java.js's ACTUAL convention — this was previously
|
|
80
|
+
// unrecognized, which silently left `classes` permanently empty
|
|
81
|
+
// for JS/Java, making resolveMethod() a no-op for those languages;
|
|
82
|
+
// the only prior test for this coded the dot-joined shape by hand
|
|
83
|
+
// rather than checking real parser output, which is how the gap
|
|
84
|
+
// went uncaught. See test/receiver-type-and-nested-calls.test.js's
|
|
85
|
+
// "registers a real JS class method from actual parser output"
|
|
86
|
+
// regression test.)
|
|
68
87
|
for (const fn of ir.functions) {
|
|
69
88
|
if (!fn.qid) continue;
|
|
70
|
-
const
|
|
89
|
+
const segs = fn.qid.split('::');
|
|
90
|
+
const tail = segs[segs.length - 1] || '';
|
|
71
91
|
const dotIdx = tail.indexOf('.');
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
92
|
+
let className = null;
|
|
93
|
+
let methodName = null;
|
|
94
|
+
if (dotIdx > 0) {
|
|
95
|
+
className = tail.slice(0, dotIdx);
|
|
96
|
+
methodName = tail.slice(dotIdx + 1).replace(/@\d+#[0-9a-f]+$/, '');
|
|
97
|
+
} else if (segs.length >= 3 && /^[A-Z]/.test(segs[segs.length - 2])) {
|
|
98
|
+
// Gated on the second-to-last segment being PascalCase so this
|
|
99
|
+
// doesn't misfire on an ordinary nested-function scope segment.
|
|
100
|
+
className = segs[segs.length - 2];
|
|
101
|
+
// Two sequential strips, not one `(#[0-9a-f]+)?` optional group: the
|
|
102
|
+
// combined form is flagged by this project's own self-scan gate
|
|
103
|
+
// (engine.js's safe-regex-backed ReDoS heuristic says unsafe; the
|
|
104
|
+
// NFA-based analyzer in sast/redos-nfa.js says safe — no genuine
|
|
105
|
+
// superlinear ambiguity exists here, safe-regex's star-height
|
|
106
|
+
// heuristic is just coarser). Splitting into two definitively-safe
|
|
107
|
+
// regexes (each independently passes both checkers) is behavior-
|
|
108
|
+
// identical — verified against "tail@5#hash" and "tail@5" — and
|
|
109
|
+
// avoids relying on any one detector's judgment call.
|
|
110
|
+
methodName = tail.replace(/#[0-9a-f]+$/, '').replace(/@\d+$/, '');
|
|
111
|
+
}
|
|
112
|
+
if (!className || !methodName) continue;
|
|
75
113
|
methodOwners.set(fn.qid, className);
|
|
76
114
|
let cls = classes.get(className);
|
|
77
115
|
if (!cls) {
|
|
@@ -91,11 +129,19 @@ export function buildClassHierarchy(perFileIR) {
|
|
|
91
129
|
const src = n.source;
|
|
92
130
|
if (!src || src.kind !== 'call') continue;
|
|
93
131
|
// `new Foo()` is shaped as { kind: 'call', callee: { kind: 'ident', name: 'Foo' }, isNew: true }
|
|
132
|
+
// `isNew` is REQUIRED, not optional: without it a plain call to a
|
|
133
|
+
// PascalCase-named function (`let x = SomeFactoryFn()`) is
|
|
134
|
+
// indistinguishable from a real constructor call and silently
|
|
135
|
+
// mistypes `x` as class `SomeFactoryFn`. That mistype then flows
|
|
136
|
+
// into the dataflow engine's receiver-type gate, where a wrong
|
|
137
|
+
// "confidently resolved" type can suppress a real finding. Same
|
|
138
|
+
// `n.source.isNew` test collectInstantiatedClasses uses below.
|
|
139
|
+
if (!src.isNew) continue;
|
|
94
140
|
const callee = src.callee;
|
|
95
141
|
const className = callee?.kind === 'ident' ? callee.name : null;
|
|
96
142
|
if (!className) continue;
|
|
97
143
|
if (classes.has(className) || /^[A-Z]/.test(className)) {
|
|
98
|
-
// Convention: PascalCase callees treated as constructors.
|
|
144
|
+
// Convention: PascalCase `new` callees treated as constructors.
|
|
99
145
|
const target = typeof n.target === 'string' ? n.target : null;
|
|
100
146
|
if (target) typeOfVar.set(`${file}::${fn.qid}::${target}`, className);
|
|
101
147
|
}
|
package/src/ir/index.js
CHANGED
|
@@ -182,7 +182,13 @@ export function buildProjectIR(fileContents) {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
|
-
|
|
185
|
+
// PRD R7 (docs/DETECTION_GAP_REMEDIATION_PRD.md): buildCallGraph's
|
|
186
|
+
// re-export map (aliased `export {x as y} from './z'`, CJS
|
|
187
|
+
// `module.exports = require('./z')`) only populates when fileContents is
|
|
188
|
+
// passed — without it, a call to an aliased re-export can never resolve
|
|
189
|
+
// (no function is literally named the alias anywhere), a dropped edge,
|
|
190
|
+
// not just an imprecise one.
|
|
191
|
+
const cg = buildCallGraph(perFile, fileContents);
|
|
186
192
|
const cha = buildClassHierarchy(perFile);
|
|
187
193
|
return { perFile, callGraph: cg, cha };
|
|
188
194
|
}
|
|
@@ -238,7 +244,13 @@ export async function buildProjectIRAsync(fileContents) {
|
|
|
238
244
|
}
|
|
239
245
|
}
|
|
240
246
|
}
|
|
241
|
-
|
|
247
|
+
// PRD R7 (docs/DETECTION_GAP_REMEDIATION_PRD.md): buildCallGraph's
|
|
248
|
+
// re-export map (aliased `export {x as y} from './z'`, CJS
|
|
249
|
+
// `module.exports = require('./z')`) only populates when fileContents is
|
|
250
|
+
// passed — without it, a call to an aliased re-export can never resolve
|
|
251
|
+
// (no function is literally named the alias anywhere), a dropped edge,
|
|
252
|
+
// not just an imprecise one.
|
|
253
|
+
const cg = buildCallGraph(perFile, fileContents);
|
|
242
254
|
const cha = buildClassHierarchy(perFile);
|
|
243
255
|
return { perFile, callGraph: cg, cha };
|
|
244
256
|
}
|