@clear-capabilities/agentic-security-scanner 0.151.3 → 0.152.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 +71 -0
- package/dist/agentic-security.mjs +80 -80
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +5 -5
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/dataflow/catalog.js +802 -13
- package/src/dataflow/engine.js +852 -89
- package/src/dataflow/incremental.js +13 -0
- package/src/dataflow/index.js +18 -5
- package/src/dataflow/path-feasibility.js +86 -9
- package/src/dataflow/string-domain.js +132 -0
- package/src/dataflow/summaries.js +69 -8
- package/src/engine.js +108 -3
- package/src/ir/CLAUDE.md +2 -1
- package/src/ir/balanced-call.js +33 -1
- package/src/ir/callgraph.js +26 -2
- package/src/ir/class-hierarchy.js +101 -6
- package/src/ir/csharp-ir.js +97 -8
- package/src/ir/index.js +21 -1
- package/src/ir/parser-cs.js +706 -24
- package/src/ir/parser-java.js +498 -17
- package/src/ir/parser-js.js +22 -0
- package/src/ir/parser-php.js +446 -43
- package/src/ir/parser-rust.js +1338 -0
- package/src/ir/php-include-merge.js +190 -0
- package/src/lineage/sink-registry.js +18 -0
- package/src/lineage/source-registry.js +23 -0
- package/src/posture/csharp-analysis.js +341 -43
- package/src/posture/finding-defaults.js +5 -1
- package/src/posture/fix-coverage.js +1 -0
- package/src/posture/proof-coverage.js +1 -0
- package/src/sast/CLAUDE.md +1 -1
- package/src/sast/authz.js +144 -1
- package/src/sast/crypto-protocol.js +65 -7
- package/src/sast/csharp-cross-file.js +125 -0
- package/src/sast/csharp-structural.js +78 -2
- package/src/sast/csharp.js +574 -42
- package/src/sast/java-ast-folding.js +268 -11
- package/src/sast/java-bench-extras.js +530 -2
- package/src/sast/java-deserialization.js +31 -2
- package/src/sast/java-structural-cross-file.js +146 -0
- package/src/sast/java-structural.js +499 -6
- package/src/sast/ldap-injection.js +133 -3
- package/src/sast/rust.js +57 -0
- package/src/sast/xss-reflected-multilang.js +252 -3
package/src/ir/parser-java.js
CHANGED
|
@@ -57,6 +57,18 @@ function exprFromCst(node) {
|
|
|
57
57
|
}
|
|
58
58
|
if (node.children) {
|
|
59
59
|
// CST node with named children — recurse into the most informative one.
|
|
60
|
+
// Assignment: java-parser models `x = y` (and `x += y`) as a
|
|
61
|
+
// binaryExpression whose children are {unaryExpression (LHS),
|
|
62
|
+
// AssignmentOperator, expression (RHS)}. There is no BinaryOperator
|
|
63
|
+
// token, so this shape previously fell through to the generic
|
|
64
|
+
// "recurse the first child" branch and lowered to the LHS alone: every
|
|
65
|
+
// expression-statement assignment in a method body vanished from the CFG.
|
|
66
|
+
if (node.children.AssignmentOperator) {
|
|
67
|
+
const op = node.children.AssignmentOperator[0]?.image || '=';
|
|
68
|
+
const lhs = node.children.unaryExpression?.[0];
|
|
69
|
+
const rhs = node.children.expression?.[0];
|
|
70
|
+
return { kind: 'binary', op, left: exprFromCst(lhs), right: exprFromCst(rhs) };
|
|
71
|
+
}
|
|
60
72
|
// Method invocation
|
|
61
73
|
if (node.children.methodInvocation) return _methodInvocation(node.children.methodInvocation[0]);
|
|
62
74
|
// `primary` is how java-parser actually models a call: the name lives in
|
|
@@ -118,11 +130,47 @@ function exprFromCst(node) {
|
|
|
118
130
|
// own args ahead of the constructor's, preserving the outermost-first
|
|
119
131
|
// convention.
|
|
120
132
|
const ctorPrefix = prefix?.children?.newExpression?.[0]?.children?.unqualifiedClassInstanceCreationExpression?.[0];
|
|
133
|
+
// `(new X(args)).m(a)`: the parenthesized-constructor form Juliet's
|
|
134
|
+
// multi-file flow variants use for every cross-class sink call. The
|
|
135
|
+
// prefix is a parenthesisExpression wrapping the constructor, so
|
|
136
|
+
// neither the fqn nor the ctorPrefix branch below sees it and the
|
|
137
|
+
// chain used to lower to a bare `m` with the class name dropped.
|
|
138
|
+
// Seed the chain from the inner lowered constructor exactly like the
|
|
139
|
+
// direct `new X().m()` form so callgraph.js's `Class.method` index
|
|
140
|
+
// can resolve it.
|
|
141
|
+
const parenInner = prefix?.children?.parenthesisExpression?.[0]?.children?.expression?.[0];
|
|
142
|
+
let parenExpr = null;
|
|
143
|
+
// Tracks whether the chain so far is STILL exactly the bare
|
|
144
|
+
// constructor call with no method invoked on the result yet — cleared
|
|
145
|
+
// the moment the suffix loop below consumes a real
|
|
146
|
+
// methodInvocationSuffix. Only in that still-true state does the
|
|
147
|
+
// eventual return carry `isNew: true`, matching class-hierarchy.js's
|
|
148
|
+
// `typeOfVar` contract (`b.action(data)` for `A_81_base b = new
|
|
149
|
+
// A_81_bad();` needs the RHS lowered with isNew so allocation-typed
|
|
150
|
+
// dispatch can resolve `b`'s declared type); `(new B()).badSink(x)`
|
|
151
|
+
// must NOT carry it, since the resulting call IS `B.badSink`, not a
|
|
152
|
+
// constructor.
|
|
153
|
+
let isCtorSeed = false;
|
|
121
154
|
if (fqn) {
|
|
122
155
|
chainCallee = _flattenFqnToString(fqn);
|
|
123
156
|
} else if (ctorPrefix) {
|
|
124
|
-
chainCallee = ctorPrefix
|
|
157
|
+
chainCallee = _ctorClassName(ctorPrefix);
|
|
125
158
|
chainArgs = (ctorPrefix.children?.argumentList?.[0]?.children?.expression || []).map(exprFromCst);
|
|
159
|
+
isCtorSeed = true;
|
|
160
|
+
} else if (parenInner) {
|
|
161
|
+
parenExpr = exprFromCst(parenInner);
|
|
162
|
+
if (parenExpr.kind === 'call' && parenExpr.isNew) {
|
|
163
|
+
chainCallee = parenExpr.callee;
|
|
164
|
+
chainArgs = parenExpr.args || [];
|
|
165
|
+
isCtorSeed = true;
|
|
166
|
+
} else if (!suffixes.length) {
|
|
167
|
+
return parenExpr;
|
|
168
|
+
} else {
|
|
169
|
+
// `(a + b).toString()`: no textual receiver name exists. Carry the
|
|
170
|
+
// inner expression as an extra argument so its taint still reaches
|
|
171
|
+
// the chained call (recall-preserving, mirrors receiver-taint).
|
|
172
|
+
chainArgs = [parenExpr];
|
|
173
|
+
}
|
|
126
174
|
}
|
|
127
175
|
// No FQN/constructor prefix — e.g. `this.foo(x)`, `super.foo(x)`
|
|
128
176
|
// (prefix is a keyword expression). chainCallee starts empty; the
|
|
@@ -140,16 +188,30 @@ function exprFromCst(node) {
|
|
|
140
188
|
appendPending();
|
|
141
189
|
const args = (inv.children?.argumentList?.[0]?.children?.expression || []).map(exprFromCst);
|
|
142
190
|
chainArgs = chainArgs === null ? args : args.concat(chainArgs);
|
|
191
|
+
isCtorSeed = false; // a real method is now being invoked on the constructed object
|
|
143
192
|
continue;
|
|
144
193
|
}
|
|
145
194
|
const ident = suf.children?.Identifier?.[0]?.image;
|
|
146
|
-
if (ident) pendingName.push(ident);
|
|
195
|
+
if (ident) { pendingName.push(ident); isCtorSeed = false; }
|
|
147
196
|
}
|
|
148
197
|
if (chainArgs !== null) {
|
|
149
198
|
appendPending(); // a trailing member access after the last call (e.g. a field read)
|
|
150
|
-
return { kind: 'call', callee: chainCallee || 'unknown', args: chainArgs };
|
|
199
|
+
return { kind: 'call', callee: chainCallee || 'unknown', args: chainArgs, ...(isCtorSeed ? { isNew: true } : {}) };
|
|
151
200
|
}
|
|
152
|
-
|
|
201
|
+
// No call anywhere in the chain: a plain member read such as
|
|
202
|
+
// `this.inst` or `obj.field` (an fqn prefix already lowers to a member
|
|
203
|
+
// chain on its own; this covers the `this`/`super`/parenthesized
|
|
204
|
+
// prefix shapes whose member suffixes were previously discarded).
|
|
205
|
+
if (pendingName.length) {
|
|
206
|
+
let cur = parenExpr || exprFromCst(prefix);
|
|
207
|
+
for (const p of pendingName) cur = { kind: 'member', object: cur, prop: p };
|
|
208
|
+
return cur;
|
|
209
|
+
}
|
|
210
|
+
return parenExpr || exprFromCst(prefix);
|
|
211
|
+
}
|
|
212
|
+
if (node.children.parenthesisExpression) {
|
|
213
|
+
const inner = node.children.parenthesisExpression[0]?.children?.expression?.[0];
|
|
214
|
+
if (inner) return exprFromCst(inner);
|
|
153
215
|
}
|
|
154
216
|
// Taint-recall PRD (80%): a cast expression — `(String) xp.evaluate(...)`,
|
|
155
217
|
// `(int) computeVal(x)` — was falling through to the generic "recurse the
|
|
@@ -175,7 +237,7 @@ function exprFromCst(node) {
|
|
|
175
237
|
if (node.children.fqnOrRefType) return _fqnExpr(node.children.fqnOrRefType[0]);
|
|
176
238
|
if (node.children.unqualifiedClassInstanceCreationExpression) {
|
|
177
239
|
const ci = node.children.unqualifiedClassInstanceCreationExpression[0];
|
|
178
|
-
const callee = (ci
|
|
240
|
+
const callee = _ctorClassName(ci) || 'new';
|
|
179
241
|
// Taint-recall PRD (80%): args was hardcoded to [] — every
|
|
180
242
|
// `new X(arg1, arg2)` constructor call lowered with its arguments
|
|
181
243
|
// silently discarded, so a sink modeled as a constructor call
|
|
@@ -260,6 +322,231 @@ function _fqnIdents(node) {
|
|
|
260
322
|
return out;
|
|
261
323
|
}
|
|
262
324
|
|
|
325
|
+
const _ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '>>>=']);
|
|
326
|
+
function _isAssignment(expr) {
|
|
327
|
+
return !!expr && expr.kind === 'binary' && _ASSIGN_OPS.has(expr.op);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Dotted access-path string for a member chain rooted at an identifier or
|
|
331
|
+
// `this`; null when the chain is rooted at something with no name (a call
|
|
332
|
+
// result, a literal).
|
|
333
|
+
function _memberPath(expr) {
|
|
334
|
+
if (!expr) return null;
|
|
335
|
+
if (expr.kind === 'ident') return expr.name;
|
|
336
|
+
if (expr.kind === 'member') {
|
|
337
|
+
const base = _memberPath(expr.object);
|
|
338
|
+
return base ? `${base}.${expr.prop}` : null;
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Targets for one lowered assignment LHS. See emitAssignment for why a
|
|
344
|
+
// field write yields a bare alias alongside its qualified path.
|
|
345
|
+
function _assignTargets(left) {
|
|
346
|
+
const path = _memberPath(left);
|
|
347
|
+
if (!path) return [];
|
|
348
|
+
if (!path.includes('.')) return [path];
|
|
349
|
+
const segs = path.split('.');
|
|
350
|
+
const out = [path];
|
|
351
|
+
if (segs.length === 2 && (segs[0] === 'this' || /^[A-Z]/.test(segs[0]))) out.push(segs[1]);
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// { name, line, bases, fields } for one class/interface declaration node.
|
|
356
|
+
// `classType` (extends) and each `interfaceType` (implements) both bottom
|
|
357
|
+
// out at a plain `Identifier` array — a fully-qualified supertype
|
|
358
|
+
// (`extends java.util.AbstractList`) yields multiple Identifier tokens, of
|
|
359
|
+
// which only the LAST is the actual type name, matching `_ctorClassName`'s
|
|
360
|
+
// same convention. Field names come from every `fieldDeclaration`'s
|
|
361
|
+
// `variableDeclaratorList` directly under `classBody` — one declaration can
|
|
362
|
+
// name several comma-separated fields (`private String inst, other;`).
|
|
363
|
+
function _classRecord(node, name) {
|
|
364
|
+
const bases = [];
|
|
365
|
+
const extendsIds = node.children?.classExtends?.[0]?.children?.classType?.[0]?.children?.Identifier;
|
|
366
|
+
if (Array.isArray(extendsIds) && extendsIds.length) bases.push(extendsIds[extendsIds.length - 1].image);
|
|
367
|
+
const interfaceTypes = node.children?.classImplements?.[0]?.children?.interfaceTypeList?.[0]?.children?.interfaceType || [];
|
|
368
|
+
for (const it of interfaceTypes) {
|
|
369
|
+
const ids = it.children?.classType?.[0]?.children?.Identifier;
|
|
370
|
+
if (Array.isArray(ids) && ids.length) bases.push(ids[ids.length - 1].image);
|
|
371
|
+
}
|
|
372
|
+
const fields = [];
|
|
373
|
+
const classBody = node.children?.classBody?.[0];
|
|
374
|
+
for (const cbd of classBody?.children?.classBodyDeclaration || []) {
|
|
375
|
+
const fd = cbd.children?.classMemberDeclaration?.[0]?.children?.fieldDeclaration?.[0];
|
|
376
|
+
const declarators = fd?.children?.variableDeclaratorList?.[0]?.children?.variableDeclarator || [];
|
|
377
|
+
for (const d of declarators) {
|
|
378
|
+
const fname = d.children?.variableDeclaratorId?.[0]?.children?.Identifier?.[0]?.image;
|
|
379
|
+
if (fname) fields.push(fname);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return { name, line: _lineOf(node), bases, fields };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// W2 (SARD_80_F1_EXECUTION_PRD.md) — exception-message taint (was the
|
|
386
|
+
// documented-but-deferred "Try/catch -> exception-flow scaffolding (P3.4
|
|
387
|
+
// will model)" gap at the top of this file). Juliet's CWE-81 "XSS Error
|
|
388
|
+
// Message" idiom throws a `new Exception(tainted)` inside a `try` and
|
|
389
|
+
// reflects it back via `catch (Exception e) { ...e.getMessage()... }` —
|
|
390
|
+
// completely invisible without this, since `throw`/`catch` is pure
|
|
391
|
+
// control flow in this IR with no data link between the thrown value and
|
|
392
|
+
// the caught variable. Recursively collects every `throwStatement`'s
|
|
393
|
+
// expression anywhere within a CST subtree (permissive: does not match
|
|
394
|
+
// the thrown type against the catch clause's declared type, since
|
|
395
|
+
// Juliet's dominant shape is one throw + one matching catch per method,
|
|
396
|
+
// and a wrong-type over-match only widens recall, it can't fabricate a
|
|
397
|
+
// source that wasn't already tainted). Does not descend into a nested
|
|
398
|
+
// class or lambda body (a distinct scope with its own catch semantics).
|
|
399
|
+
function _collectThrowExprs(node, out, depth = 0) {
|
|
400
|
+
if (!node || typeof node !== 'object' || depth > 40) return;
|
|
401
|
+
if (node.name === 'throwStatement') {
|
|
402
|
+
const expr = node.children?.expression?.[0];
|
|
403
|
+
if (expr) out.push(exprFromCst(expr));
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (node.name === 'classDeclaration' || node.name === 'lambdaExpression') return;
|
|
407
|
+
for (const k of Object.keys(node.children || {})) {
|
|
408
|
+
const arr = node.children[k];
|
|
409
|
+
if (!Array.isArray(arr)) continue;
|
|
410
|
+
for (const child of arr) _collectThrowExprs(child, out, depth + 1);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// The simple class name of a `new X(...)` expression. A fully-qualified
|
|
415
|
+
// instantiation (`new java.util.LinkedList<String>()`) carries one Identifier
|
|
416
|
+
// token per package segment plus the class, so taking the FIRST token
|
|
417
|
+
// yielded "java"; the class is always the LAST segment, and type arguments
|
|
418
|
+
// live under a separate typeArgumentsOrDiamond child that is simply not
|
|
419
|
+
// consulted.
|
|
420
|
+
// Ported from parser-cs.js's identical mechanism (see its own header comment
|
|
421
|
+
// for the full rationale). Closes Juliet's dominant abstract/interface
|
|
422
|
+
// dispatch idiom (flow variants 81/82): `A_81_base b = new A_81_bad();
|
|
423
|
+
// b.action(data);` lowers `b.action`'s callee to the local-variable-named
|
|
424
|
+
// string `"b.action"`, which `callgraph.js`'s `classMethods` index (keyed by
|
|
425
|
+
// real class name, `"A_81_bad.action"`) can never match. Since a variable's
|
|
426
|
+
// constructed type is unambiguous within one function (once assigned via a
|
|
427
|
+
// `{kind:'call', isNew:true}` source — see emitAssignment/isCtorSeed above),
|
|
428
|
+
// rewriting `b.action` to `A_81_bad.action` in place lets the EXISTING
|
|
429
|
+
// project-wide `classMethods` index resolve it with no engine changes.
|
|
430
|
+
// Refuses to guess when a variable is (re)assigned two distinct constructed
|
|
431
|
+
// types in the same function, matching this file's own `bareTailInFile`
|
|
432
|
+
// ambiguity-refusal convention elsewhere.
|
|
433
|
+
// SARD_80_F1 W2.3 — JDK collection/container types are NEVER entries in
|
|
434
|
+
// `callgraph.js`'s `classMethods` index (that index is built from this
|
|
435
|
+
// PROJECT's own `ir.classes`, never JDK builtins), so class-qualifying a
|
|
436
|
+
// call to one of these buys the dispatch-resolution mechanism nothing —
|
|
437
|
+
// while actively BREAKING collection-element taint tracking. Found via a
|
|
438
|
+
// direct CFG dump: `ArrayList<String> list = new ArrayList<>(); list.add(data);
|
|
439
|
+
// for (String s : list) { sink(s); }` rewrites `list.add` to `ArrayList.add`
|
|
440
|
+
// (mutating the callee string in place, so the taint engine's mutator rule
|
|
441
|
+
// — engine.js's `_MUTATORS` — records the taint under the FAKE receiver key
|
|
442
|
+
// "ArrayList" instead of the real variable "list"), but the for-each's own
|
|
443
|
+
// synthesized loop-variable binding (`s = list`, a bare IDENTIFIER
|
|
444
|
+
// reference, never itself a `call` node) is NEVER touched by this rewrite —
|
|
445
|
+
// there is no callee to rewrite on a bare ident. The write and the read end
|
|
446
|
+
// up keyed on two DIFFERENT names ("ArrayList" vs "list"), and the taint is
|
|
447
|
+
// silently lost. `map.values()`-style for-each reads happened to keep
|
|
448
|
+
// working by accident: that read is ALSO a call (`map.values()` also gets
|
|
449
|
+
// rewritten to `LinkedHashMap.values()`), so write and read stay
|
|
450
|
+
// consistently (if wrongly) keyed on the same class name — a coincidence
|
|
451
|
+
// that does not hold for the far more common bare-variable for-each shape.
|
|
452
|
+
//
|
|
453
|
+
// SARD_80_F1 W5.32 — the SAME defect class, for a NON-container JDK utility
|
|
454
|
+
// wrapper: `StringTokenizer tok = new StringTokenizer(request.getQueryString(),
|
|
455
|
+
// "&"); data = tok.nextToken();` (Juliet's dominant "parse a request field
|
|
456
|
+
// without getParameter()" source idiom, used across dozens of CWE test-case
|
|
457
|
+
// families) rewrites `tok.nextToken` to `StringTokenizer.nextToken` for the
|
|
458
|
+
// exact reason given above — `StringTokenizer` is never a project class, so
|
|
459
|
+
// the rewrite buys `classMethods` nothing, while `_calleeReceiverTainted`
|
|
460
|
+
// (engine.js) can no longer recognize `tok` as tainted, since the taint state
|
|
461
|
+
// was recorded under the variable name, not the class name. Confirmed via a
|
|
462
|
+
// direct CFG dump reproducing the exact bug (`tok.nextToken` silently became
|
|
463
|
+
// `StringTokenizer.nextToken`), then a real real-corpus `runScan` reproduction
|
|
464
|
+
// showing zero findings before the fix. Generalizing the FIX from "containers
|
|
465
|
+
// only" to "any well-known JDK/stdlib type with no chance of being a project
|
|
466
|
+
// class" rather than special-casing `StringTokenizer` alone, since this is
|
|
467
|
+
// evidence that the whole rewrite mechanism's risk applies to any non-project
|
|
468
|
+
// class, not just collections — this list covers the other common
|
|
469
|
+
// wrapper/reader/parser JDK types whose own methods return or carry data
|
|
470
|
+
// derived from a tainted constructor argument, the same shape that made
|
|
471
|
+
// `StringTokenizer` a real gap.
|
|
472
|
+
const _JDK_CONTAINER_TYPES = /^(?:List|ArrayList|LinkedList|Vector|Stack|CopyOnWriteArrayList|Map|HashMap|LinkedHashMap|TreeMap|Hashtable|ConcurrentHashMap|Properties|Set|HashSet|LinkedHashSet|TreeSet|CopyOnWriteArraySet|Queue|Deque|ArrayDeque|PriorityQueue|ConcurrentLinkedQueue|ConcurrentLinkedDeque|BlockingQueue|LinkedBlockingQueue|Collection|StringTokenizer|Scanner|BufferedReader|InputStreamReader|StringReader|StringBuilder|StringBuffer|ObjectInputStream|ObjectOutputStream|PrintWriter|PrintStream)$/;
|
|
473
|
+
|
|
474
|
+
function _localVarConstructedTypes(nodes) {
|
|
475
|
+
const varTypes = new Map(); // varName -> className | null (ambiguous)
|
|
476
|
+
for (const node of Object.values(nodes)) {
|
|
477
|
+
if (node.kind !== 'assign') continue;
|
|
478
|
+
const src = node.source;
|
|
479
|
+
if (!src || src.kind !== 'call' || !src.isNew || typeof src.callee !== 'string') continue;
|
|
480
|
+
if (_JDK_CONTAINER_TYPES.test(src.callee)) continue;
|
|
481
|
+
const varName = node.target;
|
|
482
|
+
if (!varName || varName.includes('.')) continue; // only a bare local, never a field-write target
|
|
483
|
+
if (varTypes.has(varName)) {
|
|
484
|
+
if (varTypes.get(varName) !== src.callee) varTypes.set(varName, null);
|
|
485
|
+
} else {
|
|
486
|
+
varTypes.set(varName, src.callee);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// A local's DECLARED type (`PrintWriter out = response.getWriter();`) is
|
|
490
|
+
// deliberately NOT folded into this rewrite. This function's rewrite is a
|
|
491
|
+
// destructive STRING REPLACEMENT of the callee (`out.println` -> literally
|
|
492
|
+
// `PrintWriter.println`) applied everywhere the call appears — and MANY
|
|
493
|
+
// existing catalog entries key `match.receiver` on the ORIGINAL variable
|
|
494
|
+
// NAME convention (e.g. an XPath sink scoped to `receiver: '^(?:xp|xpath)$'`
|
|
495
|
+
// for `XPath xp = XPathFactory.newInstance().newXPath(); xp.evaluate(...)`).
|
|
496
|
+
// A first attempt folded declared types in here and broke that exact case
|
|
497
|
+
// (`test/java-taint-flow.test.js`'s cast-wrapped XPath test went from firing
|
|
498
|
+
// to silent) — confirmed by direct regression, not theorized. Declared-type
|
|
499
|
+
// resolution belongs in `class-hierarchy.js`'s `typeOfVar` instead (see its
|
|
500
|
+
// `fn.paramTypes` seeding below `buildClassHierarchy`), which is consulted
|
|
501
|
+
// ADDITIVELY via `match.receiverTypeIn`/`_receiverTypeConfirms` — a type
|
|
502
|
+
// match there can only ADD a finding a name-based entry missed, never
|
|
503
|
+
// replace the string a name-based entry still needs to see.
|
|
504
|
+
return varTypes;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function _rewriteVarTypeCallees(expr, varTypes) {
|
|
508
|
+
if (!expr || typeof expr !== 'object') return;
|
|
509
|
+
if (expr.kind === 'call') {
|
|
510
|
+
if (typeof expr.callee === 'string') {
|
|
511
|
+
const dot = expr.callee.indexOf('.');
|
|
512
|
+
if (dot > 0) {
|
|
513
|
+
const varName = expr.callee.slice(0, dot);
|
|
514
|
+
const cls = varTypes.get(varName);
|
|
515
|
+
if (cls) expr.callee = `${cls}${expr.callee.slice(dot)}`;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (Array.isArray(expr.args)) for (const a of expr.args) _rewriteVarTypeCallees(a, varTypes);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (expr.kind === 'member') { _rewriteVarTypeCallees(expr.object, varTypes); return; }
|
|
522
|
+
if (expr.kind === 'binary' || expr.kind === 'logical') {
|
|
523
|
+
_rewriteVarTypeCallees(expr.left, varTypes); _rewriteVarTypeCallees(expr.right, varTypes); return;
|
|
524
|
+
}
|
|
525
|
+
if (expr.kind === 'tpl' && Array.isArray(expr.parts)) { for (const p of expr.parts) _rewriteVarTypeCallees(p, varTypes); return; }
|
|
526
|
+
if (expr.kind === 'union' && Array.isArray(expr.branches)) { for (const b of expr.branches) _rewriteVarTypeCallees(b, varTypes); return; }
|
|
527
|
+
if (expr.kind === 'array' && Array.isArray(expr.elements)) { for (const e of expr.elements) _rewriteVarTypeCallees(e, varTypes); return; }
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function _applyVarTypeRewrite(nodes) {
|
|
531
|
+
const varTypes = _localVarConstructedTypes(nodes);
|
|
532
|
+
let any = false;
|
|
533
|
+
for (const t of varTypes.values()) if (t) { any = true; break; }
|
|
534
|
+
if (!any) return;
|
|
535
|
+
for (const node of Object.values(nodes)) {
|
|
536
|
+
if (node.kind === 'call') _rewriteVarTypeCallees(node, varTypes);
|
|
537
|
+
else if (node.kind === 'assign') _rewriteVarTypeCallees(node.source, varTypes);
|
|
538
|
+
else if (node.kind === 'return' || node.kind === 'throw') _rewriteVarTypeCallees(node.value, varTypes);
|
|
539
|
+
else if (node.kind === 'if') _rewriteVarTypeCallees(node.cond, varTypes);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// The simple class name of a `new X(...)` expression.
|
|
544
|
+
function _ctorClassName(ci) {
|
|
545
|
+
const ids = ci?.children?.classOrInterfaceTypeToInstantiate?.[0]?.children?.Identifier;
|
|
546
|
+
if (!Array.isArray(ids) || !ids.length) return '';
|
|
547
|
+
return ids[ids.length - 1].image || '';
|
|
548
|
+
}
|
|
549
|
+
|
|
263
550
|
function _fqnExpr(node) {
|
|
264
551
|
if (!node || !node.children) return { kind: 'unknown' };
|
|
265
552
|
const idImages = _fqnIdents(node);
|
|
@@ -340,6 +627,40 @@ function buildCfgFromBody(bodyNode) {
|
|
|
340
627
|
return id;
|
|
341
628
|
}
|
|
342
629
|
|
|
630
|
+
// Lower one assignment expression (already through exprFromCst) to CFG
|
|
631
|
+
// assign node(s). A compound operator (`+=`) reads the target too, so its
|
|
632
|
+
// source is a tpl of [target, rhs], the same shape string interpolation
|
|
633
|
+
// lowers to. Field writes emit a bare alias next to the qualified target:
|
|
634
|
+
// `this.f = v` is read back as `f` elsewhere in the class, and a
|
|
635
|
+
// class-qualified static (`A.sf = v`) is read back as `sf`. An array
|
|
636
|
+
// element write taints the whole array (its read lowers to the array
|
|
637
|
+
// identifier, so the element-level path would never match).
|
|
638
|
+
function emitAssignment(expr, line) {
|
|
639
|
+
const targets = _assignTargets(expr.left);
|
|
640
|
+
if (!targets.length) return;
|
|
641
|
+
const source = expr.op === '='
|
|
642
|
+
? expr.right
|
|
643
|
+
: { kind: 'tpl', parts: [expr.left, expr.right] };
|
|
644
|
+
for (const target of targets) emit({ kind: 'assign', target, source, line, succ: [] });
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// `while ((line = r.readLine()) != null)` and the if-form: the assignment
|
|
648
|
+
// lives inside the condition expression, not in a statement of its own.
|
|
649
|
+
// Emit every assignment nested in the condition subtree ahead of the
|
|
650
|
+
// header node so the bound variable carries the call's taint into the
|
|
651
|
+
// body.
|
|
652
|
+
function emitCondAssignments(condCst, line) {
|
|
653
|
+
if (!condCst || typeof condCst !== 'object') return;
|
|
654
|
+
if (condCst.children?.AssignmentOperator) {
|
|
655
|
+
const expr = exprFromCst(condCst);
|
|
656
|
+
if (_isAssignment(expr)) emitAssignment(expr, line);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
for (const k of Object.keys(condCst.children || {})) {
|
|
660
|
+
for (const c of condCst.children[k]) emitCondAssignments(c, line);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
343
664
|
walkStmts(bodyNode);
|
|
344
665
|
|
|
345
666
|
function walkStmts(stmtNode) {
|
|
@@ -362,11 +683,30 @@ function buildCfgFromBody(bodyNode) {
|
|
|
362
683
|
const vdecl = lv.children?.localVariableDeclaration?.[0];
|
|
363
684
|
const declarators = vdecl?.children?.variableDeclaratorList?.[0]?.children?.variableDeclarator;
|
|
364
685
|
if (declarators) {
|
|
686
|
+
// Declared TYPE (not just name) — same PRD W2.5 rationale and CST
|
|
687
|
+
// path as `paramTypes` above, one level down (`localVariableType`
|
|
688
|
+
// instead of a `formalParameter`'s `variableParaRegularParameter`).
|
|
689
|
+
// A local declared as an INTERFACE/supertype (`PrintWriter out =
|
|
690
|
+
// response.getWriter();`) is exactly Java's dominant "get a writer,
|
|
691
|
+
// then use it" idiom — `_localVarConstructedTypes` below only ever
|
|
692
|
+
// sees an `isNew` constructor call, so this shape's receiver type
|
|
693
|
+
// (needed for `writer.println(x)` to resolve against a catalog
|
|
694
|
+
// sink keyed on the PrintWriter type) was completely invisible.
|
|
695
|
+
// `var` (Java 10+ inference) is not a real type name and is
|
|
696
|
+
// skipped, same as the `isVar` guard the hand-rolled parsers use
|
|
697
|
+
// elsewhere in this codebase.
|
|
698
|
+
const declTypeIds = vdecl?.children?.localVariableType?.[0]?.children?.unannType?.[0]
|
|
699
|
+
?.children?.unannReferenceType?.[0]?.children?.unannClassOrInterfaceType?.[0]
|
|
700
|
+
?.children?.unannClassType?.[0]?.children?.Identifier;
|
|
701
|
+
const rawDeclType = Array.isArray(declTypeIds) && declTypeIds.length ? declTypeIds[declTypeIds.length - 1]?.image : undefined;
|
|
702
|
+
const declType = rawDeclType && rawDeclType !== 'var' ? rawDeclType : undefined;
|
|
365
703
|
for (const d of declarators) {
|
|
366
704
|
const target = d.children?.variableDeclaratorId?.[0]?.children?.Identifier?.[0]?.image;
|
|
367
705
|
const initExpr = d.children?.variableInitializer?.[0]?.children?.expression?.[0];
|
|
368
706
|
if (target) {
|
|
369
|
-
|
|
707
|
+
const node = { kind: 'assign', target, source: initExpr ? exprFromCst(initExpr) : { kind: 'unknown' }, line: _lineOf(lv), succ: [], decl: true };
|
|
708
|
+
if (declType) node.declaredType = declType;
|
|
709
|
+
emit(node);
|
|
370
710
|
}
|
|
371
711
|
}
|
|
372
712
|
}
|
|
@@ -382,11 +722,9 @@ function buildCfgFromBody(bodyNode) {
|
|
|
382
722
|
const e = kids.expressionStatement[0]?.children?.statementExpression?.[0]?.children?.expression?.[0];
|
|
383
723
|
if (e) {
|
|
384
724
|
const expr = exprFromCst(e);
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
emit({ kind: 'assign', target: expr.left?.name || null, source: expr.right, line: _lineOf(kids.expressionStatement[0]), succ: [] });
|
|
389
|
-
}
|
|
725
|
+
const line = _lineOf(kids.expressionStatement[0]);
|
|
726
|
+
if (expr.kind === 'call') emit({ ...expr, line, succ: [] });
|
|
727
|
+
else if (_isAssignment(expr)) emitAssignment(expr, line);
|
|
390
728
|
}
|
|
391
729
|
}
|
|
392
730
|
if (kids.returnStatement) {
|
|
@@ -402,13 +740,58 @@ function buildCfgFromBody(bodyNode) {
|
|
|
402
740
|
if (kids.ifStatement) {
|
|
403
741
|
const i = kids.ifStatement[0];
|
|
404
742
|
const cond = i.children?.expression?.[0];
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
743
|
+
emitCondAssignments(cond, _lineOf(i));
|
|
744
|
+
const ifNodeId = emit({ kind: 'if', cond: cond ? exprFromCst(cond) : null, line: _lineOf(i), succ: [] });
|
|
745
|
+
// PRD W2 (SARD_80_F1_EXECUTION_PRD.md) — real branch + join, not a
|
|
746
|
+
// linear fall-through. The "v1 simplification" this replaces walked
|
|
747
|
+
// BOTH the then- and else-statement children (java-parser's
|
|
748
|
+
// ifStatement.children.statement is [thenStmt, elseStmt] in source
|
|
749
|
+
// order, confirmed via direct CST inspection) onto the SAME shared
|
|
750
|
+
// chain — so an else branch's assignment unconditionally overwrote
|
|
751
|
+
// whatever the then branch had just tainted, with no notion that the
|
|
752
|
+
// two are ALTERNATIVES. Found investigating why a real, verified
|
|
753
|
+
// taint chain (System.getenv/BufferedReader.readLine() -> concat ->
|
|
754
|
+
// sink) produced zero findings the instant it was wrapped in
|
|
755
|
+
// Juliet's own `if (true) { <tainted> } else { data = null; }`
|
|
756
|
+
// idiom — used throughout the ENTIRE corpus for control-flow-gated
|
|
757
|
+
// flow variants, so this was silently destroying an unknown but
|
|
758
|
+
// likely large fraction of Java's real recall, not a narrow gap.
|
|
759
|
+
// The then-branch starts right after the if-node (already linked by
|
|
760
|
+
// the emit() above); the else-branch is a SEPARATE second edge off
|
|
761
|
+
// the SAME if-node, not a continuation of the then-branch's tail.
|
|
762
|
+
// Both (and the no-else "condition false" case) converge on one
|
|
763
|
+
// synthetic join node, so a downstream read sees the union of what
|
|
764
|
+
// either branch could have left tainted — the same recall-preserving
|
|
765
|
+
// over-approximation every other control-flow join in this codebase
|
|
766
|
+
// relies on (case/when arms, try/catch/finally).
|
|
767
|
+
const stmts = i.children?.statement || [];
|
|
768
|
+
const thenStmt = stmts[0];
|
|
769
|
+
const elseStmt = stmts[1];
|
|
770
|
+
prev = ifNodeId;
|
|
771
|
+
if (thenStmt) walkStmts(thenStmt);
|
|
772
|
+
const thenExit = prev;
|
|
773
|
+
let elseExit = ifNodeId;
|
|
774
|
+
if (elseStmt) {
|
|
775
|
+
prev = ifNodeId;
|
|
776
|
+
walkStmts(elseStmt);
|
|
777
|
+
elseExit = prev;
|
|
778
|
+
}
|
|
779
|
+
const joinId = nextNodeId();
|
|
780
|
+
nodes[joinId] = { id: joinId, kind: 'noop', succ: [] };
|
|
781
|
+
const linkToJoin = (fromId) => {
|
|
782
|
+
if (!nodes[fromId]) return;
|
|
783
|
+
nodes[fromId].succ = nodes[fromId].succ || [];
|
|
784
|
+
if (!nodes[fromId].succ.includes(joinId)) nodes[fromId].succ.push(joinId);
|
|
785
|
+
};
|
|
786
|
+
linkToJoin(thenExit);
|
|
787
|
+
if (elseStmt) linkToJoin(elseExit);
|
|
788
|
+
else linkToJoin(ifNodeId); // no else: condition-false falls straight to the join
|
|
789
|
+
prev = joinId;
|
|
408
790
|
}
|
|
409
791
|
if (kids.whileStatement) {
|
|
410
792
|
const w = kids.whileStatement[0];
|
|
411
793
|
const cond = w.children?.expression?.[0];
|
|
794
|
+
emitCondAssignments(cond, _lineOf(w));
|
|
412
795
|
emit({ kind: 'loop-header', cond: cond ? exprFromCst(cond) : null, line: _lineOf(w), succ: [] });
|
|
413
796
|
for (const sub of (w.children?.statement || [])) walkStmts(sub);
|
|
414
797
|
}
|
|
@@ -460,6 +843,7 @@ function buildCfgFromBody(bodyNode) {
|
|
|
460
843
|
if (kids.doStatement) {
|
|
461
844
|
const d = kids.doStatement[0];
|
|
462
845
|
const cond = d.children?.expression?.[0];
|
|
846
|
+
emitCondAssignments(cond, _lineOf(d));
|
|
463
847
|
emit({ kind: 'loop-header', cond: cond ? exprFromCst(cond) : null, line: _lineOf(d), succ: [] });
|
|
464
848
|
for (const sub of (d.children?.statement || [])) walkStmts(sub);
|
|
465
849
|
}
|
|
@@ -499,8 +883,35 @@ function buildCfgFromBody(bodyNode) {
|
|
|
499
883
|
}
|
|
500
884
|
const bodyBlock = container.children?.block?.[0];
|
|
501
885
|
if (bodyBlock) walkStmts(bodyBlock);
|
|
886
|
+
const throwExprs = [];
|
|
887
|
+
if (bodyBlock) _collectThrowExprs(bodyBlock, throwExprs);
|
|
502
888
|
const catches = container.children?.catches?.[0]?.children?.catchClause || [];
|
|
503
889
|
for (const cc of catches) {
|
|
890
|
+
// Exception-message taint: bind the catch variable to whatever was
|
|
891
|
+
// thrown in the try body BEFORE walking the catch block, so a
|
|
892
|
+
// read of it inside (e.g. `e.getMessage()`) sees real taint via the
|
|
893
|
+
// engine's existing generic tainted-call-argument/receiver-taint
|
|
894
|
+
// mechanism — no catalog or engine change needed, since a `new
|
|
895
|
+
// Exception(x)` constructor call is already lowered to an ordinary
|
|
896
|
+
// `call` node and `exprTaint`'s case 'call' already treats a
|
|
897
|
+
// tainted arg as tainting the call's own result. `isNew` is
|
|
898
|
+
// stripped from each cloned expr: left as `true`, `_localVarConstructedTypes`
|
|
899
|
+
// (W2.5's abstract-dispatch fix, above) treats this assign as
|
|
900
|
+
// establishing `exceptCWE81`'s "constructed type" as `Exception`
|
|
901
|
+
// and rewrites every later `exceptCWE81.getMessage()` call to the
|
|
902
|
+
// class-qualified `Exception.getMessage` — which is correct for
|
|
903
|
+
// dispatch resolution but destroys the variable-name receiver this
|
|
904
|
+
// fix needs `_calleeReceiverTainted` to see. Confirmed via direct
|
|
905
|
+
// CFG dump: without stripping `isNew`, the rewrite fires and the
|
|
906
|
+
// probe fixture stays at zero findings even with the assign in place.
|
|
907
|
+
if (throwExprs.length) {
|
|
908
|
+
const paramName = cc.children?.catchFormalParameter?.[0]?.children?.variableDeclaratorId?.[0]?.children?.Identifier?.[0]?.image;
|
|
909
|
+
if (paramName) {
|
|
910
|
+
const cloned = throwExprs.map(e => ({ ...e, isNew: false }));
|
|
911
|
+
const source = cloned.length === 1 ? cloned[0] : { kind: 'union', branches: cloned };
|
|
912
|
+
emit({ kind: 'assign', target: paramName, source, line: _lineOf(cc), succ: [] });
|
|
913
|
+
}
|
|
914
|
+
}
|
|
504
915
|
const cblock = cc.children?.block?.[0];
|
|
505
916
|
if (cblock) walkStmts(cblock);
|
|
506
917
|
}
|
|
@@ -514,13 +925,34 @@ function buildCfgFromBody(bodyNode) {
|
|
|
514
925
|
if (kids.switchStatement) {
|
|
515
926
|
const sw = kids.switchStatement[0];
|
|
516
927
|
const cond = sw.children?.expression?.[0];
|
|
517
|
-
emit({ kind: 'if', cond: cond ? exprFromCst(cond) : null, line: _lineOf(sw), succ: [] });
|
|
928
|
+
const swHeaderId = emit({ kind: 'if', cond: cond ? exprFromCst(cond) : null, line: _lineOf(sw), succ: [] });
|
|
518
929
|
const swBlock = sw.children?.switchBlock?.[0];
|
|
930
|
+
// PRD W2 (SARD_80_F1_EXECUTION_PRD.md) — same defect class as the
|
|
931
|
+
// if/else fix directly above: each case group was walked onto the
|
|
932
|
+
// SAME shared linear chain, so `case 6: <tainted>; break;` and a
|
|
933
|
+
// later `default: data = null; break;` (Juliet's own constant-switch
|
|
934
|
+
// idiom, `switch(6){case 6:...break;default:...break;}`) fell
|
|
935
|
+
// through into each other exactly like if/else's then/else did.
|
|
936
|
+
// Every case group now starts as its OWN edge off the switch-header
|
|
937
|
+
// node (mirroring this codebase's existing case/when convention —
|
|
938
|
+
// see parser-rb.js/parser-py-cst.js's own case-arm handling, "every
|
|
939
|
+
// arm reachable directly from the construct's own entry point") and
|
|
940
|
+
// converges on one shared join node. Deliberately does not model
|
|
941
|
+
// fall-through-without-break between adjacent cases (rare in
|
|
942
|
+
// practice, and no other switch/case/when handling in this codebase
|
|
943
|
+
// models it either) — every case is treated as an independent
|
|
944
|
+
// alternative, which is recall-preserving even for the (uncommon)
|
|
945
|
+
// real fall-through case: the taint that WOULD have carried over is
|
|
946
|
+
// simply also visible via that case's own reachability from the
|
|
947
|
+
// header, just not chained through its neighbor.
|
|
948
|
+
const caseExits = [];
|
|
519
949
|
// Classic colon-form (`case 1: ...; break;`).
|
|
520
950
|
const groups = swBlock?.children?.switchBlockStatementGroup || [];
|
|
521
951
|
for (const g of groups) {
|
|
522
952
|
const bss = g.children?.blockStatements?.[0];
|
|
953
|
+
prev = swHeaderId;
|
|
523
954
|
if (bss) walkStmts(bss);
|
|
955
|
+
caseExits.push(prev);
|
|
524
956
|
}
|
|
525
957
|
// R8 Task 1 fix round 1: arrow-form (`case 1 -> ...;`, Java 14+) is a
|
|
526
958
|
// structurally distinct grammar rule — confirmed via direct CST
|
|
@@ -537,12 +969,16 @@ function buildCfgFromBody(bodyNode) {
|
|
|
537
969
|
// confirmed 0 IR-TAINT findings for the idiomatic modern-Java shape.
|
|
538
970
|
const rules = swBlock?.children?.switchRule || [];
|
|
539
971
|
for (const r of rules) {
|
|
972
|
+
prev = swHeaderId;
|
|
540
973
|
const rblock = r.children?.block?.[0];
|
|
541
|
-
if (rblock) { walkStmts(rblock); continue; }
|
|
974
|
+
if (rblock) { walkStmts(rblock); caseExits.push(prev); continue; }
|
|
542
975
|
const rthrow = r.children?.throwStatement?.[0];
|
|
543
976
|
if (rthrow) {
|
|
544
977
|
const texpr = rthrow.children?.expression?.[0];
|
|
545
978
|
emit({ kind: 'throw', value: texpr ? exprFromCst(texpr) : null, line: _lineOf(rthrow), succ: [] });
|
|
979
|
+
// A throw never falls through to the join — matches this
|
|
980
|
+
// codebase's existing (elsewhere-tolerated) convention of not
|
|
981
|
+
// adding a successor after a return/throw node.
|
|
546
982
|
continue;
|
|
547
983
|
}
|
|
548
984
|
const rexpr = r.children?.expression?.[0];
|
|
@@ -553,7 +989,21 @@ function buildCfgFromBody(bodyNode) {
|
|
|
553
989
|
emit({ kind: 'assign', target: expr.left?.name || null, source: expr.right, line: _lineOf(r), succ: [] });
|
|
554
990
|
}
|
|
555
991
|
}
|
|
992
|
+
caseExits.push(prev);
|
|
556
993
|
}
|
|
994
|
+
// Join: every case's exit (colon- or arrow-form) converges here,
|
|
995
|
+
// same as if/else's join above. A switch with zero case groups at
|
|
996
|
+
// all falls the header straight through to the join.
|
|
997
|
+
const swJoinId = nextNodeId();
|
|
998
|
+
nodes[swJoinId] = { id: swJoinId, kind: 'noop', succ: [] };
|
|
999
|
+
const linkSwToJoin = (fromId) => {
|
|
1000
|
+
if (!nodes[fromId]) return;
|
|
1001
|
+
nodes[fromId].succ = nodes[fromId].succ || [];
|
|
1002
|
+
if (!nodes[fromId].succ.includes(swJoinId)) nodes[fromId].succ.push(swJoinId);
|
|
1003
|
+
};
|
|
1004
|
+
if (caseExits.length) { for (const ex of caseExits) linkSwToJoin(ex); }
|
|
1005
|
+
else linkSwToJoin(swHeaderId);
|
|
1006
|
+
prev = swJoinId;
|
|
557
1007
|
}
|
|
558
1008
|
// Bare nested block `{ ... }` with no leading keyword. The
|
|
559
1009
|
// `statementWithoutTrailingSubstatement` branch above already recurses
|
|
@@ -594,6 +1044,15 @@ export async function parseJavaFile(file, raw) {
|
|
|
594
1044
|
if (!cst) return null;
|
|
595
1045
|
|
|
596
1046
|
const functions = [];
|
|
1047
|
+
// Class records for class-hierarchy analysis: `bases` (extends + all
|
|
1048
|
+
// implements names) feeds abstract/interface-receiver dispatch (Juliet
|
|
1049
|
+
// flow variants 81/82 — `A_81_base b = new A_81_bad(); b.action(data)`
|
|
1050
|
+
// needs to know A_81_bad extends A_81_base to resolve the call), `fields`
|
|
1051
|
+
// (instance and static declared names) lets cross-method taint tell a
|
|
1052
|
+
// field write (`sf = data;`, live across the whole class) apart from an
|
|
1053
|
+
// unrelated local variable of the same name in another method. Same
|
|
1054
|
+
// optional per-file shape `parser-cpp.js` already attaches.
|
|
1055
|
+
const classes = [];
|
|
597
1056
|
// Walk the CST for methodDeclaration nodes.
|
|
598
1057
|
function walkForMethods(node, className) {
|
|
599
1058
|
if (!node || !node.children) return;
|
|
@@ -605,6 +1064,7 @@ export async function parseJavaFile(file, raw) {
|
|
|
605
1064
|
const newClassName = child.children?.typeIdentifier?.[0]?.children?.Identifier?.[0]?.image
|
|
606
1065
|
|| child.children?.Identifier?.[0]?.image
|
|
607
1066
|
|| className;
|
|
1067
|
+
if (newClassName) classes.push(_classRecord(child, newClassName));
|
|
608
1068
|
walkForMethods(child, newClassName);
|
|
609
1069
|
continue;
|
|
610
1070
|
}
|
|
@@ -621,6 +1081,7 @@ export async function parseJavaFile(file, raw) {
|
|
|
621
1081
|
const fpl = md.children?.methodHeader?.[0]?.children?.methodDeclarator?.[0]?.children?.formalParameterList?.[0]
|
|
622
1082
|
|| md.children?.methodDeclarator?.[0]?.children?.formalParameterList?.[0];
|
|
623
1083
|
const paramAnnotations = [];
|
|
1084
|
+
const paramTypes = {};
|
|
624
1085
|
const params = (fpl?.children?.formalParameter || []).map((fp, idx) => {
|
|
625
1086
|
// Regular parameters nest under `variableParaRegularParameter`.
|
|
626
1087
|
// Varargs (`String... args`) instead use a distinct
|
|
@@ -634,6 +1095,24 @@ export async function parseJavaFile(file, raw) {
|
|
|
634
1095
|
// still yields a clean name.
|
|
635
1096
|
const vp = fp.children?.variableParaRegularParameter?.[0];
|
|
636
1097
|
const paramName = vp?.children?.variableDeclaratorId?.[0]?.children?.Identifier?.[0]?.image || null;
|
|
1098
|
+
// PRD W2.5 (SARD_80_F1_EXECUTION_PRD.md) — declared parameter
|
|
1099
|
+
// TYPE, not just name. Juliet's dominant abstract/interface
|
|
1100
|
+
// dispatch idiom passes the constructed object as a PARAMETER
|
|
1101
|
+
// (`void helper(BaseType b, String data) { b.action(data); }`),
|
|
1102
|
+
// not a local `new` assignment — `_localVarConstructedTypes`
|
|
1103
|
+
// above only ever sees the latter, so this shape's receiver type
|
|
1104
|
+
// was completely unknown to every downstream resolver. Only a
|
|
1105
|
+
// simple class/interface type name is extracted (the
|
|
1106
|
+
// `unannClassType` leaf of `unannType`); a primitive
|
|
1107
|
+
// (`unannPrimitiveType`) or array/generic type some CST shapes
|
|
1108
|
+
// don't reduce to a single Identifier here degrades to
|
|
1109
|
+
// undefined, same graceful-drop convention as the varargs case
|
|
1110
|
+
// above.
|
|
1111
|
+
const typeIds = vp?.children?.unannType?.[0]?.children?.unannReferenceType?.[0]
|
|
1112
|
+
?.children?.unannClassOrInterfaceType?.[0]?.children?.unannClassType?.[0]
|
|
1113
|
+
?.children?.Identifier;
|
|
1114
|
+
const paramType = Array.isArray(typeIds) && typeIds.length ? typeIds[typeIds.length - 1]?.image : undefined;
|
|
1115
|
+
if (paramName && paramType) paramTypes[paramName] = paramType;
|
|
637
1116
|
// Java allows multiple stacked annotations on one parameter
|
|
638
1117
|
// (`@NotNull @RequestParam String q`), each its own
|
|
639
1118
|
// `variableModifier` entry — loop over all of them, not just
|
|
@@ -663,6 +1142,7 @@ export async function parseJavaFile(file, raw) {
|
|
|
663
1142
|
if (body) {
|
|
664
1143
|
const methodLine = _lineOf(md);
|
|
665
1144
|
const cfg = buildCfgFromBody(body);
|
|
1145
|
+
_applyVarTypeRewrite(cfg.nodes);
|
|
666
1146
|
functions.push({
|
|
667
1147
|
// Sibling frontends (parser-js.js) suffix the qid with `@line`
|
|
668
1148
|
// so overloaded/same-named methods don't collide — without it,
|
|
@@ -676,6 +1156,7 @@ export async function parseJavaFile(file, raw) {
|
|
|
676
1156
|
file,
|
|
677
1157
|
calls: callSitesFromCfg(cfg),
|
|
678
1158
|
...(paramAnnotations.length ? { paramAnnotations } : {}),
|
|
1159
|
+
...(Object.keys(paramTypes).length ? { paramTypes } : {}),
|
|
679
1160
|
});
|
|
680
1161
|
}
|
|
681
1162
|
}
|
|
@@ -685,5 +1166,5 @@ export async function parseJavaFile(file, raw) {
|
|
|
685
1166
|
}
|
|
686
1167
|
walkForMethods(cst, null);
|
|
687
1168
|
|
|
688
|
-
return { file, functions, topLevel: null };
|
|
1169
|
+
return { file, functions, topLevel: null, ...(classes.length ? { classes } : {}) };
|
|
689
1170
|
}
|