@clear-capabilities/agentic-security-scanner 0.134.0 → 0.136.9
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 +432 -0
- package/bin/agentic-security-audit.js +2 -1
- package/bin/agentic-security-consistency.js +2 -1
- package/bin/agentic-security.js +448 -74
- package/dist/113.index.js +16 -7
- package/dist/117.index.js +3 -1
- package/dist/178.index.js +1 -1
- package/dist/207.index.js +5 -4
- package/dist/220.index.js +5 -3
- package/dist/238.index.js +4 -4
- package/dist/317.index.js +300 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +196 -21
- package/dist/444.index.js +20 -11
- package/dist/449.index.js +8 -1
- package/dist/513.index.js +7 -3
- package/dist/526.index.js +6 -6
- package/dist/637.index.js +1 -1
- package/dist/675.index.js +7 -5
- package/dist/839.index.js +4 -3
- package/dist/905.index.js +1173 -0
- package/dist/agentic-security.mjs +14 -14
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/ccpa.json +32 -0
- package/dist/compliance-frameworks/eu-ai-act.json +51 -0
- package/dist/compliance-frameworks/gdpr.json +45 -0
- package/dist/compliance-frameworks/hipaa-security-rule.json +56 -0
- package/dist/compliance-frameworks/nist-ai-600-1.json +51 -0
- package/dist/compliance-frameworks/nist-csf-2.json +73 -0
- package/dist/compliance-frameworks/nist-privacy-1-1.json +846 -0
- package/dist/compliance-frameworks/owasp-asvs-5.json +79 -0
- package/dist/compliance-frameworks/owasp-llm-top-10.json +69 -0
- package/package.json +24 -12
- package/src/badge.js +2 -1
- package/src/dataflow/CLAUDE.md +10 -4
- package/src/dataflow/builtin-summaries.js +1 -1
- package/src/dataflow/cross-service-taint.js +2 -1
- package/src/dataflow/engine.js +324 -60
- package/src/dataflow/ifds-precise.js +6 -4
- package/src/dataflow/implicit-flow.js +68 -36
- package/src/dataflow/incremental.js +25 -8
- package/src/dataflow/index.js +2 -1
- 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/discovery/CLAUDE.md +10 -0
- package/src/discovery/index.js +175 -3
- package/src/discovery/llm-invoke.js +90 -1
- package/src/discovery/memory.js +163 -0
- package/src/engine.js +247 -50
- package/src/integrations/tickets.js +7 -6
- package/src/ir/CLAUDE.md +4 -1
- package/src/ir/balanced-call.js +55 -0
- package/src/ir/ir-stats.js +1 -1
- package/src/ir/parser-cpp.js +1 -1
- package/src/ir/parser-cs.js +62 -9
- package/src/ir/parser-go.js +29 -11
- package/src/ir/parser-java.js +96 -19
- package/src/ir/parser-js.js +151 -20
- package/src/ir/parser-php.js +44 -9
- package/src/ir/parser-rb.js +37 -7
- package/src/ir/ssa.js +6 -1
- package/src/leaderboard.js +3 -2
- package/src/llm-validator/consistency.js +6 -2
- package/src/llm-validator/index.js +1 -2
- package/src/lsp/server.js +28 -2
- package/src/mcp/CLAUDE.md +9 -2
- package/src/mcp/audit.js +2 -1
- package/src/mcp/redact.js +26 -0
- package/src/mcp/tools.js +159 -17
- package/src/posture/CLAUDE.md +45 -8
- package/src/posture/accuracy-scorecard.js +67 -1
- package/src/posture/agents-memory.js +5 -3
- package/src/posture/aibom.js +12 -8
- package/src/posture/auditor-walkthrough.js +111 -10
- package/src/posture/auth-posture-import.js +5 -4
- 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/compliance-frameworks/nist-privacy-1-1.json +846 -0
- package/src/posture/compliance-frameworks/owasp-asvs-5.json +1 -1
- package/src/posture/compliance-policy.js +40 -10
- 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/custom-rules.js +7 -5
- package/src/posture/cve-alert-daemon.js +6 -5
- package/src/posture/dep-add-guard.js +2 -1
- package/src/posture/deploy-platform.js +4 -1
- package/src/posture/deterministic.js +3 -2
- package/src/posture/drift.js +7 -1
- package/src/posture/epss.js +13 -1
- package/src/posture/evidence-bundle.js +276 -0
- package/src/posture/exploitability-probability.js +15 -2
- package/src/posture/falsification.js +23 -2
- package/src/posture/feature-flags.js +3 -2
- package/src/posture/findings-memory.js +3 -3
- package/src/posture/fix-history.js +5 -2
- package/src/posture/fix-metrics.js +5 -5
- package/src/posture/fix-plan.js +2 -1
- package/src/posture/fix-verify-loop.js +10 -1
- package/src/posture/grader-calibration.js +3 -4
- package/src/posture/iac-reachability.js +14 -8
- package/src/posture/integrity.js +25 -7
- package/src/posture/intent-context.js +2 -1
- package/src/posture/learning.js +4 -3
- package/src/posture/license-attributions.js +5 -7
- package/src/posture/license-graph.js +2 -1
- package/src/posture/license-policy.js +2 -1
- package/src/posture/model-rescan.js +69 -3
- package/src/posture/mttr.js +5 -0
- package/src/posture/network-policy-import.js +3 -2
- package/src/posture/poc-inprocess.js +27 -8
- package/src/posture/pqc-migration-plan.js +7 -5
- package/src/posture/pr-augment.js +8 -5
- package/src/posture/privacy-framework.js +262 -0
- package/src/posture/regression-test-gen.js +23 -8
- package/src/posture/reverse-blast-radius.js +5 -1
- package/src/posture/risk-dollars.js +20 -3
- package/src/posture/router.js +5 -4
- package/src/posture/ruleset-version.js +2 -2
- package/src/posture/runtime-correlation.js +2 -1
- package/src/posture/sbom-diff.js +12 -3
- package/src/posture/sca-policy.js +7 -4
- package/src/posture/scan-checkpoint.js +15 -0
- 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/state-dir.js +34 -0
- package/src/posture/telemetry-ingest.js +4 -3
- package/src/posture/threat-model-auto.js +4 -1
- package/src/posture/threat-model-grounding.js +13 -3
- package/src/posture/time-to-fix.js +3 -2
- package/src/posture/triage-memory.js +3 -2
- package/src/posture/validator-metrics.js +10 -3
- package/src/posture/verifier.js +32 -57
- package/src/posture/waf-ingest.js +6 -5
- package/src/posture/watch-mode.js +4 -3
- 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/code-injection-multilang.js +29 -0
- 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 +9 -1
- package/src/sca/sarif-ingest.js +0 -187
|
@@ -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
|
+
}
|
package/src/ir/ir-stats.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// language, how many of the files we claim to support did we actually turn
|
|
5
5
|
// into IR? That is the difference between recognising an extension and
|
|
6
6
|
// supporting a language, and it is the headline metric of the proof corpus
|
|
7
|
-
// bench (
|
|
7
|
+
// bench (the Proof Corpus PRD §5.4 (removed post-implementation)).
|
|
8
8
|
//
|
|
9
9
|
// Enable by setting AGENTIC_SECURITY_IR_STATS to an output path. The sidecar
|
|
10
10
|
// deliberately contains NO timestamp so two runs over identical input produce
|
package/src/ir/parser-cpp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// C / C++ IR frontend.
|
|
2
2
|
//
|
|
3
3
|
// Hand-rolled, following the parser-cs.js / parser-go.js template. See
|
|
4
|
-
//
|
|
4
|
+
// the Proof Corpus PRD §6.3 (removed post-implementation) for why this is not tree-sitter or libclang:
|
|
5
5
|
// the build excludes the tree-sitter deps from the bundle, and libclang would
|
|
6
6
|
// require native bindings plus a compile database we deliberately never build.
|
|
7
7
|
//
|
package/src/ir/parser-cs.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
import * as crypto from 'node:crypto';
|
|
29
29
|
import { callSitesFromCfg } from './call-sites.js';
|
|
30
|
+
import { matchBalancedCall } from './balanced-call.js';
|
|
30
31
|
|
|
31
32
|
const METHOD_RE = new RegExp(
|
|
32
33
|
'(?:^|[\\s;{}])(?:public|private|protected|internal|static|virtual|override|async|sealed|abstract|new|readonly|partial)' +
|
|
@@ -80,17 +81,69 @@ function _lowerExpr(text) {
|
|
|
80
81
|
if (parts.length === 1) return { kind: 'ident', name: parts[0] };
|
|
81
82
|
return _buildMemberChain(parts);
|
|
82
83
|
}
|
|
83
|
-
//
|
|
84
|
-
|
|
84
|
+
// Object creation: `new Type(args)` — lowered to a call so taint flows into
|
|
85
|
+
// constructor arguments. Without this branch the expression fell through to
|
|
86
|
+
// the concat heuristic below, and because the `+` sits INSIDE the parens
|
|
87
|
+
// `_splitTopLevelPlus` returned the input unchanged, so that branch recursed
|
|
88
|
+
// on the identical string until the stack blew. `buildProjectIR` catches
|
|
89
|
+
// per-file, so the crash surfaced only as "this file has no IR" — 12 of 21 C#
|
|
90
|
+
// corpus entries, and the catalog's own `cs-sqlcommand` rule ("SQL Injection
|
|
91
|
+
// (new SqlCommand with concatenated user input)") could never fire.
|
|
92
|
+
const newMatch = matchBalancedCall(s, /^new\s+([\w.]+)/);
|
|
93
|
+
if (newMatch) {
|
|
94
|
+
const callee = newMatch.callee.split('.').pop();
|
|
95
|
+
const args = _splitTopLevelCommas(newMatch.argsText).map(_lowerExpr);
|
|
96
|
+
return { kind: 'call', callee, args, isNew: true };
|
|
97
|
+
}
|
|
98
|
+
// Call: foo.bar(args) or Bar(args). matchBalancedCall finds the paren
|
|
99
|
+
// that actually balances the FIRST '(' — not the greedy-to-end-of-string
|
|
100
|
+
// match the old `/\((.*)\)\s*$/` used, which corrupted the argument text
|
|
101
|
+
// for a chained call (`Sanitize(x).Trim()` produced argsText="x).Trim(",
|
|
102
|
+
// which then fell through to {kind:'unknown'} and silently dropped x).
|
|
103
|
+
const callMatch = matchBalancedCall(s, /^([\w.]+)/);
|
|
85
104
|
if (callMatch) {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
return { kind: 'call', callee, args };
|
|
105
|
+
const args = _splitTopLevelCommas(callMatch.argsText).map(_lowerExpr);
|
|
106
|
+
return { kind: 'call', callee: callMatch.callee, args };
|
|
89
107
|
}
|
|
90
108
|
// String concat / interpolation — heuristic.
|
|
109
|
+
//
|
|
110
|
+
// The `parts.length > 1` guard is load-bearing, not defensive tidiness: when
|
|
111
|
+
// the `+` is nested inside parens or brackets, `_splitTopLevelPlus` returns
|
|
112
|
+
// the input as a single part, and mapping `_lowerExpr` over it recurses on the
|
|
113
|
+
// identical string forever. Any future expression form that reaches here
|
|
114
|
+
// unsplit would otherwise reintroduce the same stack overflow.
|
|
91
115
|
if (s.includes('+') && /["']/.test(s)) {
|
|
92
|
-
const
|
|
93
|
-
return { kind: 'tpl', parts };
|
|
116
|
+
const rawParts = _splitTopLevelPlus(s);
|
|
117
|
+
if (rawParts.length > 1) return { kind: 'tpl', parts: rawParts.map(_lowerExpr) };
|
|
118
|
+
}
|
|
119
|
+
// Stage 3 correctness audit (detection depth, per-language-IR):
|
|
120
|
+
// interpolated strings ($"id={id}", $@"...", @$"...") were entirely
|
|
121
|
+
// unrecognized by every branch above — not even treated as an opaque
|
|
122
|
+
// literal, since nothing here tested for the leading `$` prefix — so
|
|
123
|
+
// they fell all the way through to {kind:'unknown'}, silently dropping
|
|
124
|
+
// any interpolated variable's taint. This is exactly
|
|
125
|
+
// `new SqlCommand($"SELECT ... WHERE id={id}", conn)`, one of the most
|
|
126
|
+
// common real C# SQL-injection shapes. `{expr}` / `{expr:format}` are
|
|
127
|
+
// lowered into a template, same shape as the `+`-concat branch above.
|
|
128
|
+
if (/^\$@?"/.test(s) || /^@\$"/.test(s)) {
|
|
129
|
+
const bodyStart = s.indexOf('"') + 1;
|
|
130
|
+
const inner = s.slice(bodyStart, -1);
|
|
131
|
+
const re = /\{([^{}:]+)(?::[^{}]*)?\}/g;
|
|
132
|
+
let lastIndex = 0;
|
|
133
|
+
const parts = [];
|
|
134
|
+
let matched = false;
|
|
135
|
+
let m;
|
|
136
|
+
while ((m = re.exec(inner)) !== null) {
|
|
137
|
+
matched = true;
|
|
138
|
+
if (m.index > lastIndex) parts.push({ kind: 'literal', value: inner.slice(lastIndex, m.index) });
|
|
139
|
+
parts.push(_lowerExpr(m[1].trim()));
|
|
140
|
+
lastIndex = re.lastIndex;
|
|
141
|
+
}
|
|
142
|
+
if (matched) {
|
|
143
|
+
if (lastIndex < inner.length) parts.push({ kind: 'literal', value: inner.slice(lastIndex) });
|
|
144
|
+
return { kind: 'tpl', parts };
|
|
145
|
+
}
|
|
146
|
+
return { kind: 'literal', value: s };
|
|
94
147
|
}
|
|
95
148
|
if (/^"|^@"/.test(s)) return { kind: 'literal', value: s };
|
|
96
149
|
if (/^\d/.test(s)) return { kind: 'literal', value: s };
|
|
@@ -169,9 +222,9 @@ function _lowerStmt(stmt, line) {
|
|
|
169
222
|
return { kind: 'assign', line, target, source: _lowerExpr(sourceText) };
|
|
170
223
|
}
|
|
171
224
|
// statement-form call
|
|
172
|
-
const cm = s
|
|
225
|
+
const cm = matchBalancedCall(s, /^([A-Za-z_][\w.]*)/);
|
|
173
226
|
if (cm) {
|
|
174
|
-
return { kind: 'call', line, callee: cm
|
|
227
|
+
return { kind: 'call', line, callee: cm.callee, args: _splitTopLevelCommas(cm.argsText).map(_lowerExpr) };
|
|
175
228
|
}
|
|
176
229
|
return { kind: 'unknown', line, text: s };
|
|
177
230
|
}
|
package/src/ir/parser-go.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import * as crypto from 'node:crypto';
|
|
24
24
|
import { callSitesFromCfg } from './call-sites.js';
|
|
25
|
+
import { matchBalancedCall } from './balanced-call.js';
|
|
25
26
|
|
|
26
27
|
const FUNC_RE = new RegExp(
|
|
27
28
|
'(?:^|[\\n;{}])\\s*func\\s+' +
|
|
@@ -76,7 +77,11 @@ function _lowerExpr(text) {
|
|
|
76
77
|
const s = String(text || '').trim();
|
|
77
78
|
if (!s) return { kind: 'unknown' };
|
|
78
79
|
if (/^fmt\.Sprintf\s*\(/.test(s)) {
|
|
79
|
-
|
|
80
|
+
// Same balanced-paren concern as the call patterns below — a chained
|
|
81
|
+
// call after fmt.Sprintf(...) (rare but not impossible via a wrapper)
|
|
82
|
+
// would otherwise have its args text swallow the chain's own parens.
|
|
83
|
+
const sprintf = matchBalancedCall(s, /^fmt\.Sprintf/);
|
|
84
|
+
const inner = sprintf ? sprintf.argsText : s.slice(s.indexOf('(') + 1, s.lastIndexOf(')'));
|
|
80
85
|
const parts = _splitTopLevelCommas(inner).map(_lowerExpr);
|
|
81
86
|
return { kind: 'tpl', parts };
|
|
82
87
|
}
|
|
@@ -91,17 +96,30 @@ function _lowerExpr(text) {
|
|
|
91
96
|
if (/^"/.test(s) || /^`/.test(s)) return { kind: 'literal', value: s };
|
|
92
97
|
if (/^\d/.test(s)) return { kind: 'literal', value: s };
|
|
93
98
|
if (/^(true|false|nil)\b/.test(s)) return { kind: 'literal', value: s };
|
|
94
|
-
// Call: foo.Bar(args) or Bar(args)
|
|
95
|
-
|
|
99
|
+
// Call: foo.Bar(args) or Bar(args). matchBalancedCall finds the paren
|
|
100
|
+
// that actually balances the FIRST '(' — not the greedy-to-end-of-string
|
|
101
|
+
// match the old `/\((.*)\)\s*$/` used, which corrupted the argument text
|
|
102
|
+
// for a chained call (`Sanitize(x).Trim()` produced args="x).Trim(",
|
|
103
|
+
// which then fell through to {kind:'unknown'} and silently dropped x).
|
|
104
|
+
const callMatch = matchBalancedCall(s, /^([\w.]+)/);
|
|
96
105
|
if (callMatch) {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
return { kind: 'call', callee, args };
|
|
106
|
+
const args = _splitTopLevelCommas(callMatch.argsText).map(_lowerExpr);
|
|
107
|
+
return { kind: 'call', callee: callMatch.callee, args };
|
|
100
108
|
}
|
|
101
|
-
// String concat with +
|
|
109
|
+
// String concat with + — a SECOND occurrence of the same check as above
|
|
110
|
+
// (line ~92), reached when the expression didn't match any branch in
|
|
111
|
+
// between (calls, literals, etc.). It's missing the `parts.length > 1`
|
|
112
|
+
// guard the first occurrence has: when the `+` sits inside an extra
|
|
113
|
+
// pair of parens (`("SELECT " + name)` — e.g. an over-parenthesized
|
|
114
|
+
// argument), `_splitTopLevelPlus` returns the input unchanged as ONE
|
|
115
|
+
// part (depth never returns to 0 at the `+`), and `.map(_lowerExpr)`
|
|
116
|
+
// then recurses on the IDENTICAL string forever — a stack overflow,
|
|
117
|
+
// uncaught here (only caught by buildProjectIR's per-file try/catch, so
|
|
118
|
+
// it surfaces only as "this file has no IR"). Same bug class, same fix,
|
|
119
|
+
// as parser-cs.js's `_splitTopLevelPlus` guard for `new Type(concat)`.
|
|
102
120
|
if (s.includes('+') && /["'`]/.test(s)) {
|
|
103
|
-
const parts = _splitTopLevelPlus(s)
|
|
104
|
-
return { kind: 'tpl', parts };
|
|
121
|
+
const parts = _splitTopLevelPlus(s);
|
|
122
|
+
if (parts.length > 1) return { kind: 'tpl', parts: parts.map(_lowerExpr) };
|
|
105
123
|
}
|
|
106
124
|
// Member: a.b.c
|
|
107
125
|
if (/^[A-Za-z_][\w.]*$/.test(s)) {
|
|
@@ -220,9 +238,9 @@ function _lowerStmt(stmt, line) {
|
|
|
220
238
|
return { kind: 'assign', line, target: varDecl[1], source: _lowerExpr(varDecl[2]) };
|
|
221
239
|
}
|
|
222
240
|
// Statement-form call: obj.Method(args) or Method(args)
|
|
223
|
-
const cm = s
|
|
241
|
+
const cm = matchBalancedCall(s, /^([\w.]+)/);
|
|
224
242
|
if (cm) {
|
|
225
|
-
return { kind: 'call', line, callee: cm
|
|
243
|
+
return { kind: 'call', line, callee: cm.callee, args: _splitTopLevelCommas(cm.argsText).map(_lowerExpr) };
|
|
226
244
|
}
|
|
227
245
|
return null;
|
|
228
246
|
}
|
package/src/ir/parser-java.js
CHANGED
|
@@ -58,6 +58,27 @@ function exprFromCst(node) {
|
|
|
58
58
|
// CST node with named children — recurse into the most informative one.
|
|
59
59
|
// Method invocation
|
|
60
60
|
if (node.children.methodInvocation) return _methodInvocation(node.children.methodInvocation[0]);
|
|
61
|
+
// `primary` is how java-parser actually models a call: the name lives in
|
|
62
|
+
// primaryPrefix (an fqnOrRefType) and the invocation itself in
|
|
63
|
+
// primarySuffix.methodInvocationSuffix. Without this branch the fall-through
|
|
64
|
+
// below recursed into the prefix and returned the NAME — so
|
|
65
|
+
// `req.getParameter("q")` lowered to a member access, never a call, and no
|
|
66
|
+
// source or sink in the Java catalog could ever match.
|
|
67
|
+
if (node.children.primaryPrefix) {
|
|
68
|
+
const prefix = node.children.primaryPrefix[0];
|
|
69
|
+
const suffixes = node.children.primarySuffix || [];
|
|
70
|
+
const invocation = suffixes
|
|
71
|
+
.map(s => s.children?.methodInvocationSuffix?.[0])
|
|
72
|
+
.find(Boolean);
|
|
73
|
+
if (invocation) {
|
|
74
|
+
const fqn = prefix?.children?.fqnOrRefType?.[0];
|
|
75
|
+
const callee = fqn ? _flattenFqnToString(fqn) : 'unknown';
|
|
76
|
+
const args = (invocation.children?.argumentList?.[0]?.children?.expression || [])
|
|
77
|
+
.map(exprFromCst);
|
|
78
|
+
return { kind: 'call', callee, args };
|
|
79
|
+
}
|
|
80
|
+
return exprFromCst(prefix);
|
|
81
|
+
}
|
|
61
82
|
// FQN ref
|
|
62
83
|
if (node.children.fqnOrRefType) return _fqnExpr(node.children.fqnOrRefType[0]);
|
|
63
84
|
if (node.children.unqualifiedClassInstanceCreationExpression) {
|
|
@@ -89,12 +110,37 @@ function exprFromCst(node) {
|
|
|
89
110
|
return { kind: 'unknown' };
|
|
90
111
|
}
|
|
91
112
|
|
|
113
|
+
/**
|
|
114
|
+
* The identifier images of an `fqnOrRefType`, in order.
|
|
115
|
+
*
|
|
116
|
+
* java-parser does NOT put `Identifier` directly on `fqnOrRefType`. The real
|
|
117
|
+
* shape is fqnOrRefTypePartFirst / fqnOrRefTypePartRest → fqnOrRefTypePartCommon
|
|
118
|
+
* → Identifier, so reading `children.Identifier` returned nothing for every
|
|
119
|
+
* real-world expression and both callers below degraded to 'unknown'. The
|
|
120
|
+
* direct-Identifier form is still accepted as a fallback.
|
|
121
|
+
*/
|
|
122
|
+
function _fqnIdents(node) {
|
|
123
|
+
if (!node || !node.children) return [];
|
|
124
|
+
const direct = node.children.Identifier;
|
|
125
|
+
if (Array.isArray(direct) && direct.length) return direct.map(t => t.image);
|
|
126
|
+
const out = [];
|
|
127
|
+
const parts = [
|
|
128
|
+
...(node.children.fqnOrRefTypePartFirst || []),
|
|
129
|
+
...(node.children.fqnOrRefTypePartRest || []),
|
|
130
|
+
];
|
|
131
|
+
for (const p of parts) {
|
|
132
|
+
const common = p.children?.fqnOrRefTypePartCommon?.[0];
|
|
133
|
+
const id = common?.children?.Identifier?.[0]?.image;
|
|
134
|
+
if (id) out.push(id);
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
|
|
92
139
|
function _fqnExpr(node) {
|
|
93
|
-
// java-parser fqnOrRefType has children { Identifier: [...] } sometimes
|
|
94
|
-
// and Dot tokens between them.
|
|
95
140
|
if (!node || !node.children) return { kind: 'unknown' };
|
|
96
|
-
const
|
|
97
|
-
if (!
|
|
141
|
+
const idImages = _fqnIdents(node);
|
|
142
|
+
if (!idImages.length) return { kind: 'unknown' };
|
|
143
|
+
const ids = idImages.map(image => ({ image }));
|
|
98
144
|
let cur = { kind: 'ident', name: ids[0].image };
|
|
99
145
|
for (let i = 1; i < ids.length; i++) {
|
|
100
146
|
cur = { kind: 'member', object: cur, prop: ids[i].image };
|
|
@@ -118,10 +164,28 @@ function _methodInvocation(node) {
|
|
|
118
164
|
}
|
|
119
165
|
|
|
120
166
|
function _flattenFqnToString(node) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
167
|
+
const ids = _fqnIdents(node);
|
|
168
|
+
return ids.length ? ids.join('.') : 'unknown';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Descend to the first leaf token in a CST subtree and return its
|
|
172
|
+
// startLine. java-parser (chevrotain) leaf tokens carry startLine directly;
|
|
173
|
+
// rule nodes only carry `children`. Returns 0 when nothing is found (e.g.
|
|
174
|
+
// an empty node), which callers already treat as "unknown".
|
|
175
|
+
function _lineOf(node) {
|
|
176
|
+
if (!node) return 0;
|
|
177
|
+
if (Array.isArray(node)) {
|
|
178
|
+
for (const n of node) { const l = _lineOf(n); if (l) return l; }
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
if (typeof node.startLine === 'number') return node.startLine;
|
|
182
|
+
if (node.children) {
|
|
183
|
+
for (const k of Object.keys(node.children)) {
|
|
184
|
+
const l = _lineOf(node.children[k]);
|
|
185
|
+
if (l) return l;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return 0;
|
|
125
189
|
}
|
|
126
190
|
|
|
127
191
|
/**
|
|
@@ -132,7 +196,7 @@ function _flattenFqnToString(node) {
|
|
|
132
196
|
* `if` / `loop-header` node and the body falls through linearly. This is
|
|
133
197
|
* coarser than the JS frontend; v2 will branch the succ array.
|
|
134
198
|
*/
|
|
135
|
-
function buildCfgFromBody(bodyNode
|
|
199
|
+
function buildCfgFromBody(bodyNode) {
|
|
136
200
|
const nodes = {};
|
|
137
201
|
const entry = nextNodeId();
|
|
138
202
|
const exit = nextNodeId();
|
|
@@ -157,6 +221,14 @@ function buildCfgFromBody(bodyNode, line) {
|
|
|
157
221
|
function walkStmts(stmtNode) {
|
|
158
222
|
if (!stmtNode || !stmtNode.children) return;
|
|
159
223
|
const kids = stmtNode.children;
|
|
224
|
+
// java-parser nests a `block` as block → blockStatements (PLURAL, an
|
|
225
|
+
// intermediate rule node) → blockStatement. Walking only `blockStatement`
|
|
226
|
+
// from the block node therefore descended into nothing and every method
|
|
227
|
+
// body lowered to an empty CFG — which is why Java had IR functions with
|
|
228
|
+
// entry/exit and no statements, and taint could never fire.
|
|
229
|
+
if (kids.blockStatements) {
|
|
230
|
+
for (const bss of kids.blockStatements) walkStmts(bss);
|
|
231
|
+
}
|
|
160
232
|
// Block statement children
|
|
161
233
|
if (kids.blockStatement) {
|
|
162
234
|
for (const bs of kids.blockStatement) walkStmts(bs);
|
|
@@ -170,7 +242,7 @@ function buildCfgFromBody(bodyNode, line) {
|
|
|
170
242
|
const target = d.children?.variableDeclaratorId?.[0]?.children?.Identifier?.[0]?.image;
|
|
171
243
|
const initExpr = d.children?.variableInitializer?.[0]?.children?.expression?.[0];
|
|
172
244
|
if (target) {
|
|
173
|
-
emit({ kind: 'assign', target, source: initExpr ? exprFromCst(initExpr) : { kind: 'unknown' }, line:
|
|
245
|
+
emit({ kind: 'assign', target, source: initExpr ? exprFromCst(initExpr) : { kind: 'unknown' }, line: _lineOf(lv), succ: [] });
|
|
174
246
|
}
|
|
175
247
|
}
|
|
176
248
|
}
|
|
@@ -186,34 +258,34 @@ function buildCfgFromBody(bodyNode, line) {
|
|
|
186
258
|
const e = kids.expressionStatement[0]?.children?.statementExpression?.[0]?.children?.expression?.[0];
|
|
187
259
|
if (e) {
|
|
188
260
|
const expr = exprFromCst(e);
|
|
189
|
-
if (expr.kind === 'call') emit({ ...expr, line:
|
|
261
|
+
if (expr.kind === 'call') emit({ ...expr, line: _lineOf(kids.expressionStatement[0]), succ: [] });
|
|
190
262
|
else if (expr.kind === 'binary' && expr.op === '=') {
|
|
191
263
|
// assignment expr `x = y;`
|
|
192
|
-
emit({ kind: 'assign', target: expr.left?.name || null, source: expr.right, line:
|
|
264
|
+
emit({ kind: 'assign', target: expr.left?.name || null, source: expr.right, line: _lineOf(kids.expressionStatement[0]), succ: [] });
|
|
193
265
|
}
|
|
194
266
|
}
|
|
195
267
|
}
|
|
196
268
|
if (kids.returnStatement) {
|
|
197
269
|
const r = kids.returnStatement[0];
|
|
198
270
|
const expr = r.children?.expression?.[0];
|
|
199
|
-
emit({ kind: 'return', value: expr ? exprFromCst(expr) : null, line:
|
|
271
|
+
emit({ kind: 'return', value: expr ? exprFromCst(expr) : null, line: _lineOf(r), succ: [] });
|
|
200
272
|
}
|
|
201
273
|
if (kids.throwStatement) {
|
|
202
274
|
const t = kids.throwStatement[0];
|
|
203
275
|
const expr = t.children?.expression?.[0];
|
|
204
|
-
emit({ kind: 'throw', value: expr ? exprFromCst(expr) : null, line:
|
|
276
|
+
emit({ kind: 'throw', value: expr ? exprFromCst(expr) : null, line: _lineOf(t), succ: [] });
|
|
205
277
|
}
|
|
206
278
|
if (kids.ifStatement) {
|
|
207
279
|
const i = kids.ifStatement[0];
|
|
208
280
|
const cond = i.children?.expression?.[0];
|
|
209
|
-
emit({ kind: 'if', cond: cond ? exprFromCst(cond) : null, line:
|
|
281
|
+
emit({ kind: 'if', cond: cond ? exprFromCst(cond) : null, line: _lineOf(i), succ: [] });
|
|
210
282
|
// Then branch body falls through linearly; v1 simplification.
|
|
211
283
|
for (const sub of (i.children?.statement || [])) walkStmts(sub);
|
|
212
284
|
}
|
|
213
285
|
if (kids.whileStatement) {
|
|
214
286
|
const w = kids.whileStatement[0];
|
|
215
287
|
const cond = w.children?.expression?.[0];
|
|
216
|
-
emit({ kind: 'loop-header', cond: cond ? exprFromCst(cond) : null, line:
|
|
288
|
+
emit({ kind: 'loop-header', cond: cond ? exprFromCst(cond) : null, line: _lineOf(w), succ: [] });
|
|
217
289
|
for (const sub of (w.children?.statement || [])) walkStmts(sub);
|
|
218
290
|
}
|
|
219
291
|
}
|
|
@@ -266,12 +338,17 @@ export async function parseJavaFile(file, raw) {
|
|
|
266
338
|
const params = []; // params extraction deferred
|
|
267
339
|
const body = md.children?.methodBody?.[0]?.children?.block?.[0];
|
|
268
340
|
if (body) {
|
|
341
|
+
const methodLine = _lineOf(md);
|
|
269
342
|
functions.push({
|
|
270
|
-
qid
|
|
343
|
+
// Sibling frontends (parser-js.js) suffix the qid with `@line`
|
|
344
|
+
// so overloaded/same-named methods don't collide — without it,
|
|
345
|
+
// callgraph.js's `functions.set(fn.qid, fn)` silently drops
|
|
346
|
+
// every overload but the last one declared.
|
|
347
|
+
qid: `${file}::${className || 'class'}::${name}@${methodLine}`,
|
|
271
348
|
name: className ? `${className}.${name}` : name,
|
|
272
|
-
line:
|
|
349
|
+
line: methodLine,
|
|
273
350
|
params,
|
|
274
|
-
cfg: buildCfgFromBody(body
|
|
351
|
+
cfg: buildCfgFromBody(body),
|
|
275
352
|
file,
|
|
276
353
|
});
|
|
277
354
|
}
|