@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/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
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
// when the shape is unfamiliar.
|
|
23
23
|
|
|
24
24
|
import { blankComments } from '../sast/_comment-strip.js';
|
|
25
|
+
import { callSitesFromCfg } from './call-sites.js';
|
|
25
26
|
|
|
26
27
|
let _nodeIdSeq = 0;
|
|
27
28
|
function nextNodeId() { return 'jn' + (++_nodeIdSeq); }
|
|
@@ -58,6 +59,27 @@ function exprFromCst(node) {
|
|
|
58
59
|
// CST node with named children — recurse into the most informative one.
|
|
59
60
|
// Method invocation
|
|
60
61
|
if (node.children.methodInvocation) return _methodInvocation(node.children.methodInvocation[0]);
|
|
62
|
+
// `primary` is how java-parser actually models a call: the name lives in
|
|
63
|
+
// primaryPrefix (an fqnOrRefType) and the invocation itself in
|
|
64
|
+
// primarySuffix.methodInvocationSuffix. Without this branch the fall-through
|
|
65
|
+
// below recursed into the prefix and returned the NAME — so
|
|
66
|
+
// `req.getParameter("q")` lowered to a member access, never a call, and no
|
|
67
|
+
// source or sink in the Java catalog could ever match.
|
|
68
|
+
if (node.children.primaryPrefix) {
|
|
69
|
+
const prefix = node.children.primaryPrefix[0];
|
|
70
|
+
const suffixes = node.children.primarySuffix || [];
|
|
71
|
+
const invocation = suffixes
|
|
72
|
+
.map(s => s.children?.methodInvocationSuffix?.[0])
|
|
73
|
+
.find(Boolean);
|
|
74
|
+
if (invocation) {
|
|
75
|
+
const fqn = prefix?.children?.fqnOrRefType?.[0];
|
|
76
|
+
const callee = fqn ? _flattenFqnToString(fqn) : 'unknown';
|
|
77
|
+
const args = (invocation.children?.argumentList?.[0]?.children?.expression || [])
|
|
78
|
+
.map(exprFromCst);
|
|
79
|
+
return { kind: 'call', callee, args };
|
|
80
|
+
}
|
|
81
|
+
return exprFromCst(prefix);
|
|
82
|
+
}
|
|
61
83
|
// FQN ref
|
|
62
84
|
if (node.children.fqnOrRefType) return _fqnExpr(node.children.fqnOrRefType[0]);
|
|
63
85
|
if (node.children.unqualifiedClassInstanceCreationExpression) {
|
|
@@ -89,12 +111,37 @@ function exprFromCst(node) {
|
|
|
89
111
|
return { kind: 'unknown' };
|
|
90
112
|
}
|
|
91
113
|
|
|
114
|
+
/**
|
|
115
|
+
* The identifier images of an `fqnOrRefType`, in order.
|
|
116
|
+
*
|
|
117
|
+
* java-parser does NOT put `Identifier` directly on `fqnOrRefType`. The real
|
|
118
|
+
* shape is fqnOrRefTypePartFirst / fqnOrRefTypePartRest → fqnOrRefTypePartCommon
|
|
119
|
+
* → Identifier, so reading `children.Identifier` returned nothing for every
|
|
120
|
+
* real-world expression and both callers below degraded to 'unknown'. The
|
|
121
|
+
* direct-Identifier form is still accepted as a fallback.
|
|
122
|
+
*/
|
|
123
|
+
function _fqnIdents(node) {
|
|
124
|
+
if (!node || !node.children) return [];
|
|
125
|
+
const direct = node.children.Identifier;
|
|
126
|
+
if (Array.isArray(direct) && direct.length) return direct.map(t => t.image);
|
|
127
|
+
const out = [];
|
|
128
|
+
const parts = [
|
|
129
|
+
...(node.children.fqnOrRefTypePartFirst || []),
|
|
130
|
+
...(node.children.fqnOrRefTypePartRest || []),
|
|
131
|
+
];
|
|
132
|
+
for (const p of parts) {
|
|
133
|
+
const common = p.children?.fqnOrRefTypePartCommon?.[0];
|
|
134
|
+
const id = common?.children?.Identifier?.[0]?.image;
|
|
135
|
+
if (id) out.push(id);
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
92
140
|
function _fqnExpr(node) {
|
|
93
|
-
// java-parser fqnOrRefType has children { Identifier: [...] } sometimes
|
|
94
|
-
// and Dot tokens between them.
|
|
95
141
|
if (!node || !node.children) return { kind: 'unknown' };
|
|
96
|
-
const
|
|
97
|
-
if (!
|
|
142
|
+
const idImages = _fqnIdents(node);
|
|
143
|
+
if (!idImages.length) return { kind: 'unknown' };
|
|
144
|
+
const ids = idImages.map(image => ({ image }));
|
|
98
145
|
let cur = { kind: 'ident', name: ids[0].image };
|
|
99
146
|
for (let i = 1; i < ids.length; i++) {
|
|
100
147
|
cur = { kind: 'member', object: cur, prop: ids[i].image };
|
|
@@ -118,10 +165,28 @@ function _methodInvocation(node) {
|
|
|
118
165
|
}
|
|
119
166
|
|
|
120
167
|
function _flattenFqnToString(node) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
168
|
+
const ids = _fqnIdents(node);
|
|
169
|
+
return ids.length ? ids.join('.') : 'unknown';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Descend to the first leaf token in a CST subtree and return its
|
|
173
|
+
// startLine. java-parser (chevrotain) leaf tokens carry startLine directly;
|
|
174
|
+
// rule nodes only carry `children`. Returns 0 when nothing is found (e.g.
|
|
175
|
+
// an empty node), which callers already treat as "unknown".
|
|
176
|
+
function _lineOf(node) {
|
|
177
|
+
if (!node) return 0;
|
|
178
|
+
if (Array.isArray(node)) {
|
|
179
|
+
for (const n of node) { const l = _lineOf(n); if (l) return l; }
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
if (typeof node.startLine === 'number') return node.startLine;
|
|
183
|
+
if (node.children) {
|
|
184
|
+
for (const k of Object.keys(node.children)) {
|
|
185
|
+
const l = _lineOf(node.children[k]);
|
|
186
|
+
if (l) return l;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return 0;
|
|
125
190
|
}
|
|
126
191
|
|
|
127
192
|
/**
|
|
@@ -132,7 +197,7 @@ function _flattenFqnToString(node) {
|
|
|
132
197
|
* `if` / `loop-header` node and the body falls through linearly. This is
|
|
133
198
|
* coarser than the JS frontend; v2 will branch the succ array.
|
|
134
199
|
*/
|
|
135
|
-
function buildCfgFromBody(bodyNode
|
|
200
|
+
function buildCfgFromBody(bodyNode) {
|
|
136
201
|
const nodes = {};
|
|
137
202
|
const entry = nextNodeId();
|
|
138
203
|
const exit = nextNodeId();
|
|
@@ -157,6 +222,14 @@ function buildCfgFromBody(bodyNode, line) {
|
|
|
157
222
|
function walkStmts(stmtNode) {
|
|
158
223
|
if (!stmtNode || !stmtNode.children) return;
|
|
159
224
|
const kids = stmtNode.children;
|
|
225
|
+
// java-parser nests a `block` as block → blockStatements (PLURAL, an
|
|
226
|
+
// intermediate rule node) → blockStatement. Walking only `blockStatement`
|
|
227
|
+
// from the block node therefore descended into nothing and every method
|
|
228
|
+
// body lowered to an empty CFG — which is why Java had IR functions with
|
|
229
|
+
// entry/exit and no statements, and taint could never fire.
|
|
230
|
+
if (kids.blockStatements) {
|
|
231
|
+
for (const bss of kids.blockStatements) walkStmts(bss);
|
|
232
|
+
}
|
|
160
233
|
// Block statement children
|
|
161
234
|
if (kids.blockStatement) {
|
|
162
235
|
for (const bs of kids.blockStatement) walkStmts(bs);
|
|
@@ -170,7 +243,7 @@ function buildCfgFromBody(bodyNode, line) {
|
|
|
170
243
|
const target = d.children?.variableDeclaratorId?.[0]?.children?.Identifier?.[0]?.image;
|
|
171
244
|
const initExpr = d.children?.variableInitializer?.[0]?.children?.expression?.[0];
|
|
172
245
|
if (target) {
|
|
173
|
-
emit({ kind: 'assign', target, source: initExpr ? exprFromCst(initExpr) : { kind: 'unknown' }, line:
|
|
246
|
+
emit({ kind: 'assign', target, source: initExpr ? exprFromCst(initExpr) : { kind: 'unknown' }, line: _lineOf(lv), succ: [] });
|
|
174
247
|
}
|
|
175
248
|
}
|
|
176
249
|
}
|
|
@@ -186,36 +259,190 @@ function buildCfgFromBody(bodyNode, line) {
|
|
|
186
259
|
const e = kids.expressionStatement[0]?.children?.statementExpression?.[0]?.children?.expression?.[0];
|
|
187
260
|
if (e) {
|
|
188
261
|
const expr = exprFromCst(e);
|
|
189
|
-
if (expr.kind === 'call') emit({ ...expr, line:
|
|
262
|
+
if (expr.kind === 'call') emit({ ...expr, line: _lineOf(kids.expressionStatement[0]), succ: [] });
|
|
190
263
|
else if (expr.kind === 'binary' && expr.op === '=') {
|
|
191
264
|
// assignment expr `x = y;`
|
|
192
|
-
emit({ kind: 'assign', target: expr.left?.name || null, source: expr.right, line:
|
|
265
|
+
emit({ kind: 'assign', target: expr.left?.name || null, source: expr.right, line: _lineOf(kids.expressionStatement[0]), succ: [] });
|
|
193
266
|
}
|
|
194
267
|
}
|
|
195
268
|
}
|
|
196
269
|
if (kids.returnStatement) {
|
|
197
270
|
const r = kids.returnStatement[0];
|
|
198
271
|
const expr = r.children?.expression?.[0];
|
|
199
|
-
emit({ kind: 'return', value: expr ? exprFromCst(expr) : null, line:
|
|
272
|
+
emit({ kind: 'return', value: expr ? exprFromCst(expr) : null, line: _lineOf(r), succ: [] });
|
|
200
273
|
}
|
|
201
274
|
if (kids.throwStatement) {
|
|
202
275
|
const t = kids.throwStatement[0];
|
|
203
276
|
const expr = t.children?.expression?.[0];
|
|
204
|
-
emit({ kind: 'throw', value: expr ? exprFromCst(expr) : null, line:
|
|
277
|
+
emit({ kind: 'throw', value: expr ? exprFromCst(expr) : null, line: _lineOf(t), succ: [] });
|
|
205
278
|
}
|
|
206
279
|
if (kids.ifStatement) {
|
|
207
280
|
const i = kids.ifStatement[0];
|
|
208
281
|
const cond = i.children?.expression?.[0];
|
|
209
|
-
emit({ kind: 'if', cond: cond ? exprFromCst(cond) : null, line:
|
|
282
|
+
emit({ kind: 'if', cond: cond ? exprFromCst(cond) : null, line: _lineOf(i), succ: [] });
|
|
210
283
|
// Then branch body falls through linearly; v1 simplification.
|
|
211
284
|
for (const sub of (i.children?.statement || [])) walkStmts(sub);
|
|
212
285
|
}
|
|
213
286
|
if (kids.whileStatement) {
|
|
214
287
|
const w = kids.whileStatement[0];
|
|
215
288
|
const cond = w.children?.expression?.[0];
|
|
216
|
-
emit({ kind: 'loop-header', cond: cond ? exprFromCst(cond) : null, line:
|
|
289
|
+
emit({ kind: 'loop-header', cond: cond ? exprFromCst(cond) : null, line: _lineOf(w), succ: [] });
|
|
217
290
|
for (const sub of (w.children?.statement || [])) walkStmts(sub);
|
|
218
291
|
}
|
|
292
|
+
if (kids.forStatement) {
|
|
293
|
+
const f = kids.forStatement[0];
|
|
294
|
+
// Both basicForStatement (`for(init;test;step)`) and
|
|
295
|
+
// enhancedForStatement (`for(T x : xs)`) wrap a `statement` child for
|
|
296
|
+
// the loop body — same shape whileStatement already walks. Confirmed
|
|
297
|
+
// via direct java-parser CST inspection (java-parser@3.0.1): each
|
|
298
|
+
// forStatement's `children` has exactly one of these two keys, never
|
|
299
|
+
// both, and each carries `statement` directly.
|
|
300
|
+
const basic = f.children?.basicForStatement?.[0];
|
|
301
|
+
const enhanced = f.children?.enhancedForStatement?.[0];
|
|
302
|
+
const inner = basic || enhanced;
|
|
303
|
+
const cond = basic?.children?.expression?.[0];
|
|
304
|
+
emit({ kind: 'loop-header', cond: cond ? exprFromCst(cond) : null, line: _lineOf(f), succ: [] });
|
|
305
|
+
// R8 Task 1 fix round 1: `for (T x : xs)` never bound `x` to `xs` —
|
|
306
|
+
// the loop body was reachable (the fix above) but the loop variable
|
|
307
|
+
// itself carried no taint provenance, so a genuinely tainted
|
|
308
|
+
// collection reaching a sink through the loop variable still
|
|
309
|
+
// couldn't fire. Confirmed via direct CST inspection:
|
|
310
|
+
// enhancedForStatement.children = {..., localVariableDeclaration,
|
|
311
|
+
// Colon, expression, ..., statement} — `localVariableDeclaration` is
|
|
312
|
+
// the loop var's declaration (same variableDeclaratorList shape used
|
|
313
|
+
// elsewhere in this file) and `expression` is the iterated
|
|
314
|
+
// collection. Mirrors the established pattern in parser-js.js's
|
|
315
|
+
// ForOfStatement handling (PRD R13(b)): synthesize
|
|
316
|
+
// `{kind:'assign', target: loopVar, source: iterExpr}` BEFORE
|
|
317
|
+
// recursing into the body, so any body statement reading the loop
|
|
318
|
+
// variable sees its taint provenance already established. Java's
|
|
319
|
+
// for-each always declares a fresh block-scoped variable (there is
|
|
320
|
+
// no bare-assignment form the way JS's `for (x of xs)` has), so —
|
|
321
|
+
// unlike parser-js.js — there is no pre-existing outer variable of
|
|
322
|
+
// the same name to protect with a post-loop kill; the Java CFG
|
|
323
|
+
// builder does not model block scoping elsewhere either, so adding
|
|
324
|
+
// one only for this binding would be new, inconsistent behavior
|
|
325
|
+
// rather than a fix for this gap.
|
|
326
|
+
if (enhanced) {
|
|
327
|
+
const lvd = enhanced.children?.localVariableDeclaration?.[0];
|
|
328
|
+
const declarator = lvd?.children?.variableDeclaratorList?.[0]?.children?.variableDeclarator?.[0];
|
|
329
|
+
const loopVar = declarator?.children?.variableDeclaratorId?.[0]?.children?.Identifier?.[0]?.image;
|
|
330
|
+
const iterExpr = enhanced.children?.expression?.[0];
|
|
331
|
+
if (loopVar) {
|
|
332
|
+
emit({ kind: 'assign', target: loopVar, source: iterExpr ? exprFromCst(iterExpr) : { kind: 'unknown' }, line: _lineOf(enhanced), succ: [] });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
for (const sub of (inner?.children?.statement || [])) walkStmts(sub);
|
|
336
|
+
}
|
|
337
|
+
if (kids.doStatement) {
|
|
338
|
+
const d = kids.doStatement[0];
|
|
339
|
+
const cond = d.children?.expression?.[0];
|
|
340
|
+
emit({ kind: 'loop-header', cond: cond ? exprFromCst(cond) : null, line: _lineOf(d), succ: [] });
|
|
341
|
+
for (const sub of (d.children?.statement || [])) walkStmts(sub);
|
|
342
|
+
}
|
|
343
|
+
if (kids.tryStatement) {
|
|
344
|
+
const t = kids.tryStatement[0];
|
|
345
|
+
emit({ kind: 'noop', line: _lineOf(t), succ: [] });
|
|
346
|
+
// Plain `try { ... }` has direct `block`/`catches`/`finally` children.
|
|
347
|
+
// Try-with-resources (`try (Resource r = ...) { ... }` — the idiomatic
|
|
348
|
+
// JDBC shape) wraps in a distinct `tryWithResourcesStatement`
|
|
349
|
+
// intermediate node, and — confirmed via direct CST inspection, this
|
|
350
|
+
// is the one place the plan's illustrative code was wrong — that
|
|
351
|
+
// intermediate node carries its OWN `block`/`catches`/`finally`
|
|
352
|
+
// children; a plain tryStatement's outer `children` is just
|
|
353
|
+
// `{ tryWithResourcesStatement: [...] }` with nothing else, so
|
|
354
|
+
// reading `catches`/`finally` off the outer `t` (as the plan's draft
|
|
355
|
+
// did) silently drops the catch/finally bodies of every
|
|
356
|
+
// try-with-resources block. `container` below picks the node that
|
|
357
|
+
// actually owns them.
|
|
358
|
+
const twr = t.children?.tryWithResourcesStatement?.[0];
|
|
359
|
+
const container = twr || t;
|
|
360
|
+
if (twr) {
|
|
361
|
+
const resSpec = twr.children?.resourceSpecification?.[0];
|
|
362
|
+
// resourceSpecification -> resourceList -> resource[] -> each a
|
|
363
|
+
// localVariableDeclaration-shaped resource; reuse the existing
|
|
364
|
+
// localVariableDeclarationStatement lowering shape directly rather
|
|
365
|
+
// than duplicating it, by walking each resource as if it were one.
|
|
366
|
+
const resources = resSpec?.children?.resourceList?.[0]?.children?.resource || [];
|
|
367
|
+
for (const r of resources) {
|
|
368
|
+
const vdecl = r.children?.localVariableDeclaration?.[0] || r;
|
|
369
|
+
const declarators = vdecl?.children?.variableDeclaratorList?.[0]?.children?.variableDeclarator || [];
|
|
370
|
+
for (const d of declarators) {
|
|
371
|
+
const target = d.children?.variableDeclaratorId?.[0]?.children?.Identifier?.[0]?.image;
|
|
372
|
+
const initExpr = d.children?.variableInitializer?.[0]?.children?.expression?.[0] || d.children?.expression?.[0];
|
|
373
|
+
if (target) emit({ kind: 'assign', target, source: initExpr ? exprFromCst(initExpr) : { kind: 'unknown' }, line: _lineOf(r), succ: [] });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const bodyBlock = container.children?.block?.[0];
|
|
378
|
+
if (bodyBlock) walkStmts(bodyBlock);
|
|
379
|
+
const catches = container.children?.catches?.[0]?.children?.catchClause || [];
|
|
380
|
+
for (const cc of catches) {
|
|
381
|
+
const cblock = cc.children?.block?.[0];
|
|
382
|
+
if (cblock) walkStmts(cblock);
|
|
383
|
+
}
|
|
384
|
+
// Confirmed via direct CST inspection: the key is `finally`, no
|
|
385
|
+
// trailing underscore, in both the plain and try-with-resources
|
|
386
|
+
// shapes (java-parser@3.0.1).
|
|
387
|
+
const fin = container.children?.finally?.[0];
|
|
388
|
+
const finBlock = fin?.children?.block?.[0];
|
|
389
|
+
if (finBlock) walkStmts(finBlock);
|
|
390
|
+
}
|
|
391
|
+
if (kids.switchStatement) {
|
|
392
|
+
const sw = kids.switchStatement[0];
|
|
393
|
+
const cond = sw.children?.expression?.[0];
|
|
394
|
+
emit({ kind: 'if', cond: cond ? exprFromCst(cond) : null, line: _lineOf(sw), succ: [] });
|
|
395
|
+
const swBlock = sw.children?.switchBlock?.[0];
|
|
396
|
+
// Classic colon-form (`case 1: ...; break;`).
|
|
397
|
+
const groups = swBlock?.children?.switchBlockStatementGroup || [];
|
|
398
|
+
for (const g of groups) {
|
|
399
|
+
const bss = g.children?.blockStatements?.[0];
|
|
400
|
+
if (bss) walkStmts(bss);
|
|
401
|
+
}
|
|
402
|
+
// R8 Task 1 fix round 1: arrow-form (`case 1 -> ...;`, Java 14+) is a
|
|
403
|
+
// structurally distinct grammar rule — confirmed via direct CST
|
|
404
|
+
// inspection (java-parser@3.0.1) — not an alternate reading of
|
|
405
|
+
// switchBlockStatementGroup. A switchBlock's children carry EITHER
|
|
406
|
+
// switchBlockStatementGroup (colon-form) OR switchRule (arrow-form),
|
|
407
|
+
// never both (Java's grammar forbids mixing them in one switch
|
|
408
|
+
// block), so this is a separate, independent branch rather than a
|
|
409
|
+
// fallback path. Each switchRule carries exactly one body-shape key:
|
|
410
|
+
// `expression` (`case 1 -> sink(x);`), `block`
|
|
411
|
+
// (`case 1 -> { ...; }`), or `throwStatement`
|
|
412
|
+
// (`case 1 -> throw new X();`). Before this fix arrow-form switches
|
|
413
|
+
// fell through this whole branch producing zero walked statements —
|
|
414
|
+
// confirmed 0 IR-TAINT findings for the idiomatic modern-Java shape.
|
|
415
|
+
const rules = swBlock?.children?.switchRule || [];
|
|
416
|
+
for (const r of rules) {
|
|
417
|
+
const rblock = r.children?.block?.[0];
|
|
418
|
+
if (rblock) { walkStmts(rblock); continue; }
|
|
419
|
+
const rthrow = r.children?.throwStatement?.[0];
|
|
420
|
+
if (rthrow) {
|
|
421
|
+
const texpr = rthrow.children?.expression?.[0];
|
|
422
|
+
emit({ kind: 'throw', value: texpr ? exprFromCst(texpr) : null, line: _lineOf(rthrow), succ: [] });
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
const rexpr = r.children?.expression?.[0];
|
|
426
|
+
if (rexpr) {
|
|
427
|
+
const expr = exprFromCst(rexpr);
|
|
428
|
+
if (expr.kind === 'call') emit({ ...expr, line: _lineOf(r), succ: [] });
|
|
429
|
+
else if (expr.kind === 'binary' && expr.op === '=') {
|
|
430
|
+
emit({ kind: 'assign', target: expr.left?.name || null, source: expr.right, line: _lineOf(r), succ: [] });
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// Bare nested block `{ ... }` with no leading keyword. The
|
|
436
|
+
// `statementWithoutTrailingSubstatement` branch above already recurses
|
|
437
|
+
// into each such node via walkStmts; confirmed via direct CST
|
|
438
|
+
// inspection that a bare block lands as
|
|
439
|
+
// `statementWithoutTrailingSubstatement[0].children.block[0]`, so this
|
|
440
|
+
// branch — which fires for any node with a `block` child, not just
|
|
441
|
+
// this one — picks it up on the recursive call without a dedicated
|
|
442
|
+
// bare-block branch.
|
|
443
|
+
if (kids.block) {
|
|
444
|
+
for (const b of kids.block) walkStmts(b);
|
|
445
|
+
}
|
|
219
446
|
}
|
|
220
447
|
|
|
221
448
|
if (nodes[prev]) {
|
|
@@ -263,16 +490,69 @@ export async function parseJavaFile(file, raw) {
|
|
|
263
490
|
const name = md.children?.methodHeader?.[0]?.children?.methodDeclarator?.[0]?.children?.Identifier?.[0]?.image
|
|
264
491
|
|| md.children?.methodDeclarator?.[0]?.children?.Identifier?.[0]?.image
|
|
265
492
|
|| 'anonymous';
|
|
266
|
-
|
|
493
|
+
// Parameter list + annotations live in the same CST subtree, so
|
|
494
|
+
// both are extracted in one walk (PRD R14(a) Task 5). Mirrors the
|
|
495
|
+
// dual-path fallback pattern used above for `name`: depending on
|
|
496
|
+
// which `k` matched in the outer loop, the params live nested
|
|
497
|
+
// under a `methodHeader` node OR directly under `methodDeclarator`.
|
|
498
|
+
const fpl = md.children?.methodHeader?.[0]?.children?.methodDeclarator?.[0]?.children?.formalParameterList?.[0]
|
|
499
|
+
|| md.children?.methodDeclarator?.[0]?.children?.formalParameterList?.[0];
|
|
500
|
+
const paramAnnotations = [];
|
|
501
|
+
const params = (fpl?.children?.formalParameter || []).map((fp, idx) => {
|
|
502
|
+
// Regular parameters nest under `variableParaRegularParameter`.
|
|
503
|
+
// Varargs (`String... args`) instead use a distinct
|
|
504
|
+
// `variableArityParameter` node that this walk doesn't extract
|
|
505
|
+
// from — `vp` is undefined, so this degrades gracefully to a
|
|
506
|
+
// dropped (not corrupted/crashed) parameter, matching the
|
|
507
|
+
// codebase's existing "degrade gracefully rather than throw"
|
|
508
|
+
// convention for shapes that aren't cleanly extractable. Array
|
|
509
|
+
// types (`String[] args`) are unaffected — the `[]` lives in
|
|
510
|
+
// `unannType`, not the identifier, so `variableDeclaratorId`
|
|
511
|
+
// still yields a clean name.
|
|
512
|
+
const vp = fp.children?.variableParaRegularParameter?.[0];
|
|
513
|
+
const paramName = vp?.children?.variableDeclaratorId?.[0]?.children?.Identifier?.[0]?.image || null;
|
|
514
|
+
// Java allows multiple stacked annotations on one parameter
|
|
515
|
+
// (`@NotNull @RequestParam String q`), each its own
|
|
516
|
+
// `variableModifier` entry — loop over all of them, not just
|
|
517
|
+
// the first, or a stacked annotation is silently dropped (the
|
|
518
|
+
// same class of bug Task 3's C# regex needed a fix round for).
|
|
519
|
+
const variableModifiers = vp?.children?.variableModifier || [];
|
|
520
|
+
for (const vm of variableModifiers) {
|
|
521
|
+
const ann = vm.children?.annotation?.[0];
|
|
522
|
+
// `typeName.children.Identifier` is an array of ALL
|
|
523
|
+
// dot-separated segments for a fully-qualified annotation
|
|
524
|
+
// (`@org.springframework.web.bind.annotation.RequestParam`
|
|
525
|
+
// yields ['org','springframework','web','bind','annotation',
|
|
526
|
+
// 'RequestParam']) — the simple annotation name is the LAST
|
|
527
|
+
// segment, not the first. Taking [0] previously recorded the
|
|
528
|
+
// package root ("org") as the decorator name, a silent wrong
|
|
529
|
+
// value rather than the honest drop the C#/JS sibling parsers
|
|
530
|
+
// produce for the same shape (fix round 1, R14(a) Task 5).
|
|
531
|
+
const identifiers = ann?.children?.typeName?.[0]?.children?.Identifier;
|
|
532
|
+
const decoratorName = identifiers?.length ? identifiers[identifiers.length - 1]?.image : undefined;
|
|
533
|
+
if (decoratorName && paramName) {
|
|
534
|
+
paramAnnotations.push({ index: idx, name: paramName, decorator: decoratorName });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return paramName;
|
|
538
|
+
}).filter(Boolean);
|
|
267
539
|
const body = md.children?.methodBody?.[0]?.children?.block?.[0];
|
|
268
540
|
if (body) {
|
|
541
|
+
const methodLine = _lineOf(md);
|
|
542
|
+
const cfg = buildCfgFromBody(body);
|
|
269
543
|
functions.push({
|
|
270
|
-
qid
|
|
544
|
+
// Sibling frontends (parser-js.js) suffix the qid with `@line`
|
|
545
|
+
// so overloaded/same-named methods don't collide — without it,
|
|
546
|
+
// callgraph.js's `functions.set(fn.qid, fn)` silently drops
|
|
547
|
+
// every overload but the last one declared.
|
|
548
|
+
qid: `${file}::${className || 'class'}::${name}@${methodLine}`,
|
|
271
549
|
name: className ? `${className}.${name}` : name,
|
|
272
|
-
line:
|
|
550
|
+
line: methodLine,
|
|
273
551
|
params,
|
|
274
|
-
cfg
|
|
552
|
+
cfg,
|
|
275
553
|
file,
|
|
554
|
+
calls: callSitesFromCfg(cfg),
|
|
555
|
+
...(paramAnnotations.length ? { paramAnnotations } : {}),
|
|
276
556
|
});
|
|
277
557
|
}
|
|
278
558
|
}
|