@clear-capabilities/agentic-security-scanner 0.151.3 → 0.153.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 +125 -0
- package/dist/1336.index.js +16010 -0
- package/dist/agentic-security.mjs +80 -80
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +7 -6
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/dataflow/backward.js +104 -7
- package/src/dataflow/catalog.js +802 -13
- package/src/dataflow/cross-service-taint.js +154 -11
- package/src/dataflow/engine.js +875 -90
- package/src/dataflow/exploit-prover.js +111 -23
- package/src/dataflow/incremental.js +13 -0
- package/src/dataflow/index.js +29 -6
- package/src/dataflow/path-feasibility.js +86 -9
- package/src/dataflow/string-domain.js +168 -0
- package/src/dataflow/summaries.js +69 -8
- package/src/engine.js +118 -9
- 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 +261 -7
- 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/report/index.js +12 -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/dataflow/smt-feasibility.js +0 -189
|
@@ -39,11 +39,30 @@ const _AST_CACHE = new WeakMap();
|
|
|
39
39
|
* Build the CHA over a perFileIR map (file → parsed IR with raw AST attached
|
|
40
40
|
* under `_ast`). When AST isn't attached, fall back to the IR's own
|
|
41
41
|
* structural hints (class names appearing in qids).
|
|
42
|
+
*
|
|
43
|
+
* `callGraph` (optional, from `../ir/callgraph.js#buildCallGraph`) enables
|
|
44
|
+
* one additional, otherwise-skipped pass: cross-call-site parameter type
|
|
45
|
+
* inference from the argument a caller actually passes (see that pass's own
|
|
46
|
+
* header comment below for the full rationale). Every existing caller that
|
|
47
|
+
* only ever passed `perFileIR` keeps working unchanged — the pass simply
|
|
48
|
+
* doesn't run without it.
|
|
42
49
|
*/
|
|
43
|
-
export function buildClassHierarchy(perFileIR) {
|
|
50
|
+
export function buildClassHierarchy(perFileIR, callGraph) {
|
|
44
51
|
const classes = new Map(); // className -> { file, line, methods, extends }
|
|
45
52
|
const methodOwners = new Map(); // qid -> className
|
|
46
53
|
const typeOfVar = new Map(); // file::scope::var -> className
|
|
54
|
+
// A variable assigned two DIFFERENT constructed types within one function
|
|
55
|
+
// must never resolve to either — same "refuse to guess on ambiguity"
|
|
56
|
+
// convention `_localVarConstructedTypes` (parser-java.js/parser-cs.js)
|
|
57
|
+
// already enforces at parse time for the callee-string-rewrite path. This
|
|
58
|
+
// map had NO ambiguity tracking of its own (last assign silently won) —
|
|
59
|
+
// latent since day one, but never exercised for hand-rolled-parser
|
|
60
|
+
// languages because `_resolveMemberCalleeViaCHA` (engine.js) only ever
|
|
61
|
+
// consulted `classOfVar` for the Babel `{kind:'member'}` shape (JS-only)
|
|
62
|
+
// until PRD W2.5 extended it to flat dotted-string callees too. Caught by
|
|
63
|
+
// `test/catalog-cs-p1.test.js`'s existing ambiguity-refusal regression
|
|
64
|
+
// test the moment that extension shipped.
|
|
65
|
+
const ambiguousVarKeys = new Set();
|
|
47
66
|
|
|
48
67
|
if (!perFileIR || typeof perFileIR !== 'object') {
|
|
49
68
|
return { classes, methodOwners, typeOfVar };
|
|
@@ -59,15 +78,37 @@ export function buildClassHierarchy(perFileIR) {
|
|
|
59
78
|
if (!c || !c.name) continue;
|
|
60
79
|
let cls = classes.get(c.name);
|
|
61
80
|
if (!cls) {
|
|
62
|
-
cls = { name: c.name, file, line: c.line || 0, methods: new Set(), extends: null };
|
|
81
|
+
cls = { name: c.name, file, line: c.line || 0, methods: new Set(), extends: null, bases: [], fields: new Set() };
|
|
63
82
|
classes.set(c.name, cls);
|
|
64
83
|
}
|
|
84
|
+
if (!cls.bases) cls.bases = [];
|
|
85
|
+
if (!cls.fields) cls.fields = new Set();
|
|
65
86
|
// v1 keeps a single base: the CHA walk in resolveMethod follows one
|
|
66
87
|
// chain. Multiple inheritance is flattened to the first base, which is
|
|
67
88
|
// a deliberate over-simplification recorded in PRD §6.8.
|
|
68
89
|
if (!cls.extends && Array.isArray(c.bases) && c.bases.length) {
|
|
69
90
|
cls.extends = c.bases[0];
|
|
70
91
|
}
|
|
92
|
+
// Full base/interface list (SARD_80_F1_SCANNER_PRD.md — abstract/
|
|
93
|
+
// interface-receiver dispatch, Juliet flow variants 81/82): `extends`
|
|
94
|
+
// above follows exactly one chain for resolveMethod's walk, but a
|
|
95
|
+
// receiver typed as an INTERFACE (`IAction b = new BadImpl();`) needs
|
|
96
|
+
// to know every implemented name, not just the first. Purely additive
|
|
97
|
+
// — nothing previously read `cls.bases`.
|
|
98
|
+
if (Array.isArray(c.bases)) {
|
|
99
|
+
for (const b of c.bases) if (b && !cls.bases.includes(b)) cls.bases.push(b);
|
|
100
|
+
}
|
|
101
|
+
// Declared field names (SARD_80_F1_SCANNER_PRD.md — cross-method field
|
|
102
|
+
// taint, Juliet flow variants 45/65-68): lets the taint engine tell a
|
|
103
|
+
// field write (`sf = data;`, `this.inst = data;`) apart from an
|
|
104
|
+
// ordinary local variable with the same name in a DIFFERENT method of
|
|
105
|
+
// the same class, without which a field write in one method could
|
|
106
|
+
// never be told to taint a read in another. Merged across every file
|
|
107
|
+
// that contributes to this class (partial classes, or a parser that
|
|
108
|
+
// emits one `ir.classes` entry per method's enclosing file).
|
|
109
|
+
if (Array.isArray(c.fields)) {
|
|
110
|
+
for (const f of c.fields) if (f) cls.fields.add(f);
|
|
111
|
+
}
|
|
71
112
|
}
|
|
72
113
|
}
|
|
73
114
|
if (!Array.isArray(ir.functions)) continue;
|
|
@@ -94,9 +135,15 @@ export function buildClassHierarchy(perFileIR) {
|
|
|
94
135
|
if (dotIdx > 0) {
|
|
95
136
|
className = tail.slice(0, dotIdx);
|
|
96
137
|
methodName = tail.slice(dotIdx + 1).replace(/@\d+#[0-9a-f]+$/, '');
|
|
97
|
-
} else if (segs.length >= 3 && /^[A-Z]/.test(segs[segs.length - 2])) {
|
|
98
|
-
//
|
|
99
|
-
//
|
|
138
|
+
} else if (segs.length >= 3 && (classes.has(segs[segs.length - 2]) || /^[A-Z]/.test(segs[segs.length - 2]))) {
|
|
139
|
+
// PRD W1 (SARD_80_F1_EXECUTION_PRD.md) — `classes` was already
|
|
140
|
+
// populated from this file's real `ir.classes` structural facts
|
|
141
|
+
// above (lines 57-94), so `classes.has(...)` recognizes a scrambled
|
|
142
|
+
// class name (`case_<hash>`, no longer PascalCase) exactly as
|
|
143
|
+
// reliably as a real one. The `/^[A-Z]/` fallback stays for
|
|
144
|
+
// languages that don't emit `ir.classes` yet (JS/Python/PHP/Ruby/Go/
|
|
145
|
+
// Kotlin — W1.1) so an ordinary nested-function scope segment still
|
|
146
|
+
// doesn't misfire there, same as before this change.
|
|
100
147
|
className = segs[segs.length - 2];
|
|
101
148
|
// Two sequential strips, not one `(#[0-9a-f]+)?` optional group: the
|
|
102
149
|
// combined form is flagged by this project's own self-scan gate
|
|
@@ -113,7 +160,7 @@ export function buildClassHierarchy(perFileIR) {
|
|
|
113
160
|
methodOwners.set(fn.qid, className);
|
|
114
161
|
let cls = classes.get(className);
|
|
115
162
|
if (!cls) {
|
|
116
|
-
cls = { name: className, file, line: fn.line || 0, methods: new Set(), extends: null };
|
|
163
|
+
cls = { name: className, file, line: fn.line || 0, methods: new Set(), extends: null, bases: [], fields: new Set() };
|
|
117
164
|
classes.set(className, cls);
|
|
118
165
|
}
|
|
119
166
|
cls.methods.add(methodName);
|
|
@@ -160,7 +207,214 @@ export function buildClassHierarchy(perFileIR) {
|
|
|
160
207
|
if (classes.has(className) || /^[A-Z]/.test(className)) {
|
|
161
208
|
// Convention: PascalCase `new` callees treated as constructors.
|
|
162
209
|
const target = typeof n.target === 'string' ? n.target : null;
|
|
163
|
-
if (target)
|
|
210
|
+
if (target) {
|
|
211
|
+
const key = `${file}::${fn.qid}::${target}`;
|
|
212
|
+
if (ambiguousVarKeys.has(key)) continue;
|
|
213
|
+
if (typeOfVar.has(key) && typeOfVar.get(key) !== className) {
|
|
214
|
+
ambiguousVarKeys.add(key);
|
|
215
|
+
typeOfVar.delete(key);
|
|
216
|
+
} else {
|
|
217
|
+
typeOfVar.set(key, className);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// PRD W2.5 (SARD_80_F1_EXECUTION_PRD.md) — a PARAMETER's declared type is
|
|
224
|
+
// just as real a type binding as a local `let x = new Foo()`, and it's
|
|
225
|
+
// Juliet's OWN dominant abstract/interface dispatch idiom: the "runTest"
|
|
226
|
+
// driver constructs the concrete (bad/good) instance and passes it as a
|
|
227
|
+
// parameter to a helper that invokes the virtual method — `_localVarConstructedTypes`
|
|
228
|
+
// (parser-java.js/parser-cs.js) only ever sees the CONSTRUCTOR site, not
|
|
229
|
+
// this call site, so `b` in `void helper(BaseType b, ...) { b.action(x); }`
|
|
230
|
+
// was completely untyped. Reuses the EXACT same `typeOfVar` map and key
|
|
231
|
+
// shape `classOfVar` already reads — no new lookup mechanism, so every
|
|
232
|
+
// existing consumer (engine.js's `_resolveMemberCalleeViaCHA` once it
|
|
233
|
+
// learns to read a flat dotted-string callee, `receiver-context.js`)
|
|
234
|
+
// benefits with no changes on their end. Only populated when
|
|
235
|
+
// `fn.paramTypes` exists (currently parser-java.js; parser-cs.js is a
|
|
236
|
+
// candidate follow-up, same shape). Deliberately does NOT overwrite an
|
|
237
|
+
// existing binding for the same key — a parameter name can never collide
|
|
238
|
+
// with a local var name in the same scope in Java, so this is purely
|
|
239
|
+
// additive, but the guard costs nothing and documents the intent.
|
|
240
|
+
for (const fn of ir.functions) {
|
|
241
|
+
if (!fn.paramTypes) continue;
|
|
242
|
+
for (const [paramName, paramType] of Object.entries(fn.paramTypes)) {
|
|
243
|
+
const key = `${file}::${fn.qid}::${paramName}`;
|
|
244
|
+
if (!typeOfVar.has(key) && !ambiguousVarKeys.has(key)) typeOfVar.set(key, paramType);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// SARD_80_F1 W4: a local's DECLARED type (`PrintWriter out =
|
|
248
|
+
// response.getWriter();`, parser-java.js's `declaredType` on the assign
|
|
249
|
+
// node) is the third source of a type binding, alongside a constructor
|
|
250
|
+
// call above and a parameter's declared type. This is Java's dominant
|
|
251
|
+
// "get an object via a factory/getter method, not `new`" idiom — the
|
|
252
|
+
// constructor pass above can never see it (there is no `new` at all).
|
|
253
|
+
// Deliberately consulted via `receiverTypeIn` only (additive — see
|
|
254
|
+
// `catalog.js`'s `java-writer-*` entries), never via the destructive
|
|
255
|
+
// callee-string rewrite `parser-java.js`'s `_localVarConstructedTypes`
|
|
256
|
+
// does for constructor types: that mechanism was tried for declared
|
|
257
|
+
// types too and reverted after it broke an unrelated, name-scoped
|
|
258
|
+
// catalog entry (see its own comment). Populated by both
|
|
259
|
+
// parser-java.js and parser-cs.js (C#'s `Type x = expr;` decl-typeClause
|
|
260
|
+
// capture, same field name and rationale).
|
|
261
|
+
for (const fn of ir.functions) {
|
|
262
|
+
if (!fn.cfg || !fn.cfg.nodes) continue;
|
|
263
|
+
for (const node of Object.values(fn.cfg.nodes)) {
|
|
264
|
+
if (node.kind !== 'assign' || !node.declaredType || typeof node.target !== 'string' || node.target.includes('.')) continue;
|
|
265
|
+
const key = `${file}::${fn.qid}::${node.target}`;
|
|
266
|
+
if (!typeOfVar.has(key) && !ambiguousVarKeys.has(key)) typeOfVar.set(key, node.declaredType);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Next-gen taint capability #4b — factory-function RETURN-type inference,
|
|
272
|
+
// the sibling gap this file's OWN header comment names and explicitly
|
|
273
|
+
// marks unimplemented ("`function buildFoo(): Foo { ... }` typed-return
|
|
274
|
+
// inference — NOT implemented. `const x = buildFoo()` is untyped
|
|
275
|
+
// (correctly: it is a plain call, not a `new`)"). Confirmed still real via
|
|
276
|
+
// a direct reproduction: `function makeHelper() { return new Helper(); }
|
|
277
|
+
// const h = makeHelper(); h.getValue(req);` misses the vulnerability
|
|
278
|
+
// entirely, the identical failure shape 4a (above) closes for the
|
|
279
|
+
// parameter case. `_inferredFactoryReturnClass` is deliberately
|
|
280
|
+
// conservative, same "refuse rather than guess" discipline as every other
|
|
281
|
+
// pass in this file: a function qualifies ONLY when every one of its own
|
|
282
|
+
// `return` statements is a `new ClassName(...)` literal of the SAME
|
|
283
|
+
// class — a function with no return, a non-constructor return on any
|
|
284
|
+
// path, or disagreeing classes across returns all correctly infer
|
|
285
|
+
// nothing, rather than picking one arbitrarily.
|
|
286
|
+
function _inferredFactoryReturnClass(fn) {
|
|
287
|
+
if (!fn || !fn.cfg || !fn.cfg.nodes) return null;
|
|
288
|
+
let className = null;
|
|
289
|
+
for (const node of Object.values(fn.cfg.nodes)) {
|
|
290
|
+
if (node.kind !== 'return' || !node.value) continue;
|
|
291
|
+
const v = node.value;
|
|
292
|
+
if (v.kind !== 'call' || !v.isNew) return null;
|
|
293
|
+
const callee = v.callee;
|
|
294
|
+
const cn = callee?.kind === 'ident' ? callee.name
|
|
295
|
+
: typeof callee === 'string' ? (callee.includes('.') ? callee.slice(callee.lastIndexOf('.') + 1) : callee)
|
|
296
|
+
: null;
|
|
297
|
+
if (!cn || !(classes.has(cn) || /^[A-Z]/.test(cn))) return null;
|
|
298
|
+
if (className === null) className = cn;
|
|
299
|
+
else if (className !== cn) return null;
|
|
300
|
+
}
|
|
301
|
+
return className;
|
|
302
|
+
}
|
|
303
|
+
if (callGraph && typeof callGraph.resolveKnownCallee === 'function' && callGraph.functions) {
|
|
304
|
+
const factoryReturnCache = new Map(); // qid -> className|null, memoized
|
|
305
|
+
const factoryReturnClassOf = (qid, fn) => {
|
|
306
|
+
if (factoryReturnCache.has(qid)) return factoryReturnCache.get(qid);
|
|
307
|
+
const cn = _inferredFactoryReturnClass(fn);
|
|
308
|
+
factoryReturnCache.set(qid, cn);
|
|
309
|
+
return cn;
|
|
310
|
+
};
|
|
311
|
+
for (const [callerFile, ir] of Object.entries(perFileIR)) {
|
|
312
|
+
if (!ir || !Array.isArray(ir.functions)) continue;
|
|
313
|
+
for (const fn of ir.functions) {
|
|
314
|
+
if (!fn.cfg || !fn.cfg.nodes) continue;
|
|
315
|
+
for (const node of Object.values(fn.cfg.nodes)) {
|
|
316
|
+
if (node.kind !== 'assign' || typeof node.target !== 'string' || node.target.includes('.')) continue;
|
|
317
|
+
const src = node.source;
|
|
318
|
+
// A plain call (isNew must be FALSY — a real `new Foo()` is
|
|
319
|
+
// already handled by the constructor-assignment pass above, and
|
|
320
|
+
// conflating the two would just redundantly re-derive the same
|
|
321
|
+
// answer through a slower path).
|
|
322
|
+
if (!src || src.kind !== 'call' || src.isNew) continue;
|
|
323
|
+
const calleeName = typeof src.callee === 'string' ? src.callee
|
|
324
|
+
: src.callee && src.callee.kind === 'ident' ? src.callee.name : null;
|
|
325
|
+
if (!calleeName) continue;
|
|
326
|
+
const calleeQid = callGraph.resolveKnownCallee(calleeName, callerFile);
|
|
327
|
+
if (!calleeQid) continue;
|
|
328
|
+
const calleeFn = callGraph.functions.get(calleeQid);
|
|
329
|
+
if (!calleeFn) continue;
|
|
330
|
+
const className = factoryReturnClassOf(calleeQid, calleeFn);
|
|
331
|
+
if (!className) continue;
|
|
332
|
+
const key = `${callerFile}::${fn.qid}::${node.target}`;
|
|
333
|
+
if (ambiguousVarKeys.has(key)) continue;
|
|
334
|
+
if (typeOfVar.has(key) && typeOfVar.get(key) !== className) {
|
|
335
|
+
ambiguousVarKeys.add(key);
|
|
336
|
+
typeOfVar.delete(key);
|
|
337
|
+
} else {
|
|
338
|
+
typeOfVar.set(key, className);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Next-gen taint capability #4 (a narrow, call-site-scoped slice of
|
|
346
|
+
// "Andersen-style points-to", not the full thing): cross-call-site
|
|
347
|
+
// PARAMETER type inference from the ARGUMENT a caller actually passes, for
|
|
348
|
+
// JS/TS — which has no declared-type mechanism at all (`fn.paramTypes`
|
|
349
|
+
// just above only ever fires for Java/C#). Juliet's own dominant
|
|
350
|
+
// "driver constructs the concrete instance, passes it as a parameter to a
|
|
351
|
+
// helper that invokes the virtual method" idiom (already solved for
|
|
352
|
+
// Java/C# via fn.paramTypes) has no JS/TS equivalent: `function
|
|
353
|
+
// wrapper(h, req) { return h.getValue(req); }`, called only as `const h =
|
|
354
|
+
// new Helper(); wrapper(h, req);`, leaves `h` permanently untyped inside
|
|
355
|
+
// wrapper's own body, so `h.getValue()` can never resolve via CHA —
|
|
356
|
+
// confirmed via a direct reproduction (cold-cache AND warm-incremental-
|
|
357
|
+
// cache both miss it identically, ruling out a caching-specific cause)
|
|
358
|
+
// showing a real cross-file taint-recall false negative for exactly this
|
|
359
|
+
// shape. Requires `callGraph` (optional, backward-compatible — every
|
|
360
|
+
// existing caller that only ever passed `perFileIR` simply skips this
|
|
361
|
+
// pass, matching this file's own established graceful-degradation
|
|
362
|
+
// convention). Deliberately runs as a LAST, separate pass over every file
|
|
363
|
+
// (not merged into the per-file loop above) because it needs BOTH a
|
|
364
|
+
// cross-file callee lookup (`callGraph.resolveKnownCallee`) AND the
|
|
365
|
+
// CALLER's own already-inferred `typeOfVar` entries (an identifier
|
|
366
|
+
// argument's type, established by the constructor-assignment pass above)
|
|
367
|
+
// — neither is available mid-way through that loop for an arbitrary file
|
|
368
|
+
// ordering.
|
|
369
|
+
if (callGraph && typeof callGraph.resolveKnownCallee === 'function' && callGraph.functions) {
|
|
370
|
+
for (const [callerFile, ir] of Object.entries(perFileIR)) {
|
|
371
|
+
if (!ir || !Array.isArray(ir.functions)) continue;
|
|
372
|
+
for (const callerFn of ir.functions) {
|
|
373
|
+
if (!Array.isArray(callerFn.calls)) continue;
|
|
374
|
+
for (const call of callerFn.calls) {
|
|
375
|
+
const calleeName = typeof call.callee === 'string' ? call.callee
|
|
376
|
+
: call.callee && call.callee.kind === 'ident' ? call.callee.name : null;
|
|
377
|
+
if (!calleeName) continue;
|
|
378
|
+
const calleeQid = callGraph.resolveKnownCallee(calleeName, callerFile);
|
|
379
|
+
if (!calleeQid) continue;
|
|
380
|
+
const calleeFn = callGraph.functions.get(calleeQid);
|
|
381
|
+
if (!calleeFn || !Array.isArray(calleeFn.params) || !calleeFn.params.length || !calleeFn.file) continue;
|
|
382
|
+
const args = Array.isArray(call.args) ? call.args : [];
|
|
383
|
+
for (let i = 0; i < args.length && i < calleeFn.params.length; i++) {
|
|
384
|
+
const arg = args[i];
|
|
385
|
+
let className = null;
|
|
386
|
+
if (arg && arg.kind === 'call' && arg.isNew) {
|
|
387
|
+
// A literal `new ClassName(...)` passed directly as the argument.
|
|
388
|
+
const argCallee = arg.callee;
|
|
389
|
+
className = argCallee?.kind === 'ident' ? argCallee.name
|
|
390
|
+
: typeof argCallee === 'string' ? (argCallee.includes('.') ? argCallee.slice(argCallee.lastIndexOf('.') + 1) : argCallee)
|
|
391
|
+
: null;
|
|
392
|
+
} else if (arg && arg.kind === 'ident' && arg.name) {
|
|
393
|
+
// Juliet's own dominant idiom: `const h = new Helper(); f(h,
|
|
394
|
+
// ...)`. `h`'s type was already established by the
|
|
395
|
+
// constructor-assignment pass above (it runs first, in the
|
|
396
|
+
// SAME outer file loop) — reuse it directly rather than
|
|
397
|
+
// re-deriving, so this stays in sync with that pass's own
|
|
398
|
+
// ambiguity handling by construction.
|
|
399
|
+
className = typeOfVar.get(`${callerFile}::${callerFn.qid}::${arg.name}`) || null;
|
|
400
|
+
}
|
|
401
|
+
if (!className || !(classes.has(className) || /^[A-Z]/.test(className))) continue;
|
|
402
|
+
const paramName = calleeFn.params[i];
|
|
403
|
+
if (typeof paramName !== 'string' || !paramName) continue;
|
|
404
|
+
// Same collision-refusal discipline as every other typeOfVar-
|
|
405
|
+
// populating pass in this file: a wrong "confidently resolved"
|
|
406
|
+
// type is worse than staying unknown, so a SECOND call site
|
|
407
|
+
// disagreeing with an already-recorded type for this exact
|
|
408
|
+
// parameter permanently refuses it, never picks a side.
|
|
409
|
+
const key = `${calleeFn.file}::${calleeQid}::${paramName}`;
|
|
410
|
+
if (ambiguousVarKeys.has(key)) continue;
|
|
411
|
+
if (typeOfVar.has(key) && typeOfVar.get(key) !== className) {
|
|
412
|
+
ambiguousVarKeys.add(key);
|
|
413
|
+
typeOfVar.delete(key);
|
|
414
|
+
} else {
|
|
415
|
+
typeOfVar.set(key, className);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
164
418
|
}
|
|
165
419
|
}
|
|
166
420
|
}
|
package/src/ir/csharp-ir.js
CHANGED
|
@@ -47,6 +47,10 @@ const TYPE_MODIFIERS = new Set(['readonly', 'const', 'static', 'public', 'privat
|
|
|
47
47
|
|
|
48
48
|
const BUILTIN_TYPES = new Set(['void', 'bool', 'byte', 'sbyte', 'char', 'short', 'ushort', 'int', 'uint', 'long', 'ulong', 'float', 'double', 'decimal', 'string', 'object', 'var', 'dynamic']);
|
|
49
49
|
|
|
50
|
+
// SARD_80_F1 W4.C11 — compound assignment operators, tokenized as single
|
|
51
|
+
// 'op' tokens by csharp-tokenizer.js but previously never checked for here.
|
|
52
|
+
const CS_COMPOUND_ASSIGN_OPS = new Set(['+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '??=']);
|
|
53
|
+
|
|
50
54
|
function isType(tok) {
|
|
51
55
|
if (!tok) return false;
|
|
52
56
|
if (tok.kind === 'kw' && BUILTIN_TYPES.has(tok.value)) return true;
|
|
@@ -268,7 +272,7 @@ function readMethodHeader(tokens, i, attachedAttrs) {
|
|
|
268
272
|
}
|
|
269
273
|
|
|
270
274
|
function walkMethodBody(method) {
|
|
271
|
-
const out = { calls: [], ctors: [], assignments: [], decls: [], strings: [] };
|
|
275
|
+
const out = { calls: [], ctors: [], assignments: [], decls: [], strings: [], returns: [] };
|
|
272
276
|
const tokens = method.bodyTokens;
|
|
273
277
|
for (let i = 0; i < tokens.length; i++) {
|
|
274
278
|
const t = tokens[i];
|
|
@@ -277,6 +281,45 @@ function walkMethodBody(method) {
|
|
|
277
281
|
out.strings.push({ kind: t.kind, value: t.value || tokenText([t]), line: t.line, parts: t.parts || null });
|
|
278
282
|
continue;
|
|
279
283
|
}
|
|
284
|
+
// `return <expr>;` (bare `return;` produces no expr and is skipped —
|
|
285
|
+
// interprocedural return-taint resolution has nothing to check there).
|
|
286
|
+
// SARD_80_F1 W3.x: gives `analyzeMethodFlow` visibility into what a
|
|
287
|
+
// same-file callee actually returns, so a call-shaped assignment RHS
|
|
288
|
+
// (`data = GoodG2BSource(req, resp);`) can be resolved against the
|
|
289
|
+
// callee's OWN return taint instead of blindly matching every
|
|
290
|
+
// identifier NAME in the call text (which wrongly taints on `req`/
|
|
291
|
+
// `resp` merely being passed as arguments — see the taintMap loop
|
|
292
|
+
// below and its `_asBareCall`/`resolveCalleeReturnTaint` usage).
|
|
293
|
+
if (t.kind === 'kw' && t.value === 'return') {
|
|
294
|
+
let j = i + 1;
|
|
295
|
+
let depth = 0;
|
|
296
|
+
while (j < tokens.length && (depth > 0 || tokens[j].kind !== 'semi')) {
|
|
297
|
+
const tk = tokens[j];
|
|
298
|
+
if (tk.kind === 'lparen' || tk.kind === 'lbrace' || tk.kind === 'lbracket') depth++;
|
|
299
|
+
else if (tk.kind === 'rparen' || tk.kind === 'rbrace' || tk.kind === 'rbracket') {
|
|
300
|
+
// SARD_80_F1 W3.x — an unbalanced closer (depth already 0) closes
|
|
301
|
+
// an ENCLOSING construct we didn't open (e.g. this `return` sits
|
|
302
|
+
// inside a `using (...)`/`if (...)` clause with no `;` of its
|
|
303
|
+
// own before the surrounding `)`), not part of our own
|
|
304
|
+
// expression — stop here rather than scanning past it into
|
|
305
|
+
// whatever follows. See the identical fix on the decl/assign/
|
|
306
|
+
// field loops below for the full incident writeup.
|
|
307
|
+
if (depth === 0) break;
|
|
308
|
+
depth--;
|
|
309
|
+
}
|
|
310
|
+
j++;
|
|
311
|
+
}
|
|
312
|
+
const exprTokens = tokens.slice(i + 1, j);
|
|
313
|
+
if (exprTokens.length) {
|
|
314
|
+
out.returns.push({ line: t.line, exprTokens, exprText: tokenText(exprTokens) });
|
|
315
|
+
}
|
|
316
|
+
// Deliberately do NOT skip past the expression tokens (`i = j`): the
|
|
317
|
+
// main loop's own ident/call/assignment branches below still need to
|
|
318
|
+
// walk them, so a statement-level call inside the return
|
|
319
|
+
// (`return string.Format(...)`) is still recorded in `out.calls`
|
|
320
|
+
// exactly as it was before this branch existed.
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
280
323
|
// new Type(args)
|
|
281
324
|
if (t.kind === 'kw' && t.value === 'new') {
|
|
282
325
|
const tr = readType(tokens, i + 1);
|
|
@@ -301,8 +344,27 @@ function walkMethodBody(method) {
|
|
|
301
344
|
let j = tr.next + 2;
|
|
302
345
|
let depth = 0;
|
|
303
346
|
while (j < tokens.length && (depth > 0 || tokens[j].kind !== 'semi')) {
|
|
304
|
-
|
|
305
|
-
if (
|
|
347
|
+
const tk = tokens[j];
|
|
348
|
+
if (tk.kind === 'lparen' || tk.kind === 'lbrace' || tk.kind === 'lbracket') depth++;
|
|
349
|
+
else if (tk.kind === 'rparen' || tk.kind === 'rbrace' || tk.kind === 'rbracket') {
|
|
350
|
+
// SARD_80_F1 W3.x — a declaration with no `;` of its own,
|
|
351
|
+
// sitting inside an enclosing `using (Type x = expr)` / `for
|
|
352
|
+
// (Type x = expr; …)` clause: the FIRST `)` we see here is
|
|
353
|
+
// that outer construct's own close, not one WE opened (depth
|
|
354
|
+
// is already 0), so stop before consuming it. Previously this
|
|
355
|
+
// fell through to `depth--` unconditionally, going negative
|
|
356
|
+
// and then scanning straight through into the following
|
|
357
|
+
// block body looking for a `;` that could be arbitrarily far
|
|
358
|
+
// away — corrupting this decl's rhsText with everything up to
|
|
359
|
+
// that stray semicolon AND silently absorbing whatever real
|
|
360
|
+
// statement lived in between (found via SARD_80_F1 W4.C14:
|
|
361
|
+
// `using (StreamReader sr = new StreamReader("f")) { data =
|
|
362
|
+
// sr.ReadLine(); }` swallowed the `data = sr.ReadLine()`
|
|
363
|
+
// assignment into `sr`'s own rhsText, deleting it from
|
|
364
|
+
// `out.assignments` entirely).
|
|
365
|
+
if (depth === 0) break;
|
|
366
|
+
depth--;
|
|
367
|
+
}
|
|
306
368
|
j++;
|
|
307
369
|
}
|
|
308
370
|
const rhs = tokens.slice(tr.next + 2, j);
|
|
@@ -329,12 +391,31 @@ function walkMethodBody(method) {
|
|
|
329
391
|
targetParts.push(tokens[j + 1].value);
|
|
330
392
|
j += 2;
|
|
331
393
|
}
|
|
332
|
-
|
|
394
|
+
// SARD_80_F1 W4.C11 — compound assignment (`+=`, `-=`, …) was
|
|
395
|
+
// previously invisible here (only bare `=` matched), so a statement
|
|
396
|
+
// like `cmd.CommandText += "..." + tainted + "...";` — Juliet's own
|
|
397
|
+
// universal convention for building CommandText — produced NO
|
|
398
|
+
// assignment record at all, silently dropping it from this
|
|
399
|
+
// detector's taint model. This model (`analyzeMethodFlow`, see
|
|
400
|
+
// csharp-analysis.js) has no un-taint step — a taintMap entry is
|
|
401
|
+
// only ever set true, never cleared — so treating the compound RHS
|
|
402
|
+
// exactly like a plain assign's RHS is already correct: if the
|
|
403
|
+
// target was tainted before, that status is never touched (no
|
|
404
|
+
// clearing branch exists); if this RHS is tainted, it becomes
|
|
405
|
+
// tainted now, same as a `+=` should.
|
|
406
|
+
if (tokens[j] && tokens[j].kind === 'op' &&
|
|
407
|
+
(tokens[j].value === '=' || CS_COMPOUND_ASSIGN_OPS.has(tokens[j].value))) {
|
|
333
408
|
let k = j + 1;
|
|
334
409
|
let depth = 0;
|
|
335
410
|
while (k < tokens.length && (depth > 0 || tokens[k].kind !== 'semi')) {
|
|
336
|
-
|
|
337
|
-
if (
|
|
411
|
+
const tk = tokens[k];
|
|
412
|
+
if (tk.kind === 'lparen' || tk.kind === 'lbrace' || tk.kind === 'lbracket') depth++;
|
|
413
|
+
else if (tk.kind === 'rparen' || tk.kind === 'rbrace' || tk.kind === 'rbracket') {
|
|
414
|
+
// SARD_80_F1 W3.x — see the decl "typed init" loop above for the
|
|
415
|
+
// full incident writeup; same enclosing-construct boundary fix.
|
|
416
|
+
if (depth === 0) break;
|
|
417
|
+
depth--;
|
|
418
|
+
}
|
|
338
419
|
k++;
|
|
339
420
|
}
|
|
340
421
|
const rhs = tokens.slice(j + 1, k);
|
|
@@ -419,6 +500,7 @@ function walkMethodBody(method) {
|
|
|
419
500
|
method.ctors = out.ctors;
|
|
420
501
|
method.assignments = out.assignments;
|
|
421
502
|
method.decls = out.decls;
|
|
503
|
+
method.returns = out.returns;
|
|
422
504
|
method.strings = out.strings;
|
|
423
505
|
return out;
|
|
424
506
|
}
|
|
@@ -555,8 +637,15 @@ export function buildCSharpIR(source) {
|
|
|
555
637
|
if (tokens[f] && tokens[f].kind === 'op' && tokens[f].value === '=') {
|
|
556
638
|
let fj = f + 1, depth = 0;
|
|
557
639
|
while (fj < tokens.length && (depth > 0 || tokens[fj].kind !== 'semi')) {
|
|
558
|
-
|
|
559
|
-
if (
|
|
640
|
+
const tk = tokens[fj];
|
|
641
|
+
if (tk.kind === 'lparen' || tk.kind === 'lbrace' || tk.kind === 'lbracket') depth++;
|
|
642
|
+
else if (tk.kind === 'rparen' || tk.kind === 'rbrace' || tk.kind === 'rbracket') {
|
|
643
|
+
// SARD_80_F1 W3.x — see the decl "typed init" loop's
|
|
644
|
+
// header comment for the full incident writeup; same
|
|
645
|
+
// enclosing-construct boundary fix.
|
|
646
|
+
if (depth === 0) break;
|
|
647
|
+
depth--;
|
|
648
|
+
}
|
|
560
649
|
fj++;
|
|
561
650
|
}
|
|
562
651
|
ir.decls.push({ name: tokens[tr.next].value, type: tr.type, rhsTokens: tokens.slice(f + 1, fj), rhsText: tokenText(tokens.slice(f + 1, fj)), line: tk.line, isVar: false, isField: true });
|
package/src/ir/index.js
CHANGED
|
@@ -18,7 +18,9 @@ import {
|
|
|
18
18
|
import { parseJavaFile } from './parser-java.js';
|
|
19
19
|
import { parseGoFile } from './parser-go.js';
|
|
20
20
|
import { parsePhpFile } from './parser-php.js';
|
|
21
|
+
import { mergePhpIncludes } from './php-include-merge.js';
|
|
21
22
|
import { parseRubyFile } from './parser-rb.js';
|
|
23
|
+
import { parseRustFile } from './parser-rust.js';
|
|
22
24
|
import { parseCppFile, cppExtRe } from './parser-cpp.js';
|
|
23
25
|
import { buildCallGraph } from './callgraph.js';
|
|
24
26
|
import { buildClassHierarchy } from './class-hierarchy.js';
|
|
@@ -164,6 +166,9 @@ export function buildProjectIR(fileContents) {
|
|
|
164
166
|
} else if (/\.rb$/i.test(file)) {
|
|
165
167
|
const ir = parseRubyFile(file, code);
|
|
166
168
|
if (ir) perFile[file] = ir;
|
|
169
|
+
} else if (/\.rs$/i.test(file)) {
|
|
170
|
+
const ir = parseRustFile(file, code);
|
|
171
|
+
if (ir) perFile[file] = ir;
|
|
167
172
|
} else if (cppExtRe().test(file)) {
|
|
168
173
|
const ir = parseCppFile(file, code);
|
|
169
174
|
if (ir) perFile[file] = ir;
|
|
@@ -175,6 +180,12 @@ export function buildProjectIR(fileContents) {
|
|
|
175
180
|
if (ir && ir.file) perFile[ir.file] = ir;
|
|
176
181
|
}
|
|
177
182
|
}
|
|
183
|
+
// SARD_80_F1 W5.7 — merge PHP include/require targets' top-level
|
|
184
|
+
// assignments into the includer's own scope before anything else touches
|
|
185
|
+
// the CFGs (SSA numbering included), since PHP's `include` genuinely
|
|
186
|
+
// executes in the caller's variable namespace. See php-include-merge.js's
|
|
187
|
+
// module header for the full rationale and scope.
|
|
188
|
+
try { mergePhpIncludes(perFile); } catch {}
|
|
178
189
|
if (isSSAEnabled()) {
|
|
179
190
|
for (const ir of Object.values(perFile)) {
|
|
180
191
|
for (const fn of (ir.functions || [])) {
|
|
@@ -226,6 +237,9 @@ export async function buildProjectIRAsync(fileContents) {
|
|
|
226
237
|
} else if (/\.rb$/i.test(file)) {
|
|
227
238
|
const ir = parseRubyFile(file, code);
|
|
228
239
|
if (ir) perFile[file] = ir;
|
|
240
|
+
} else if (/\.rs$/i.test(file)) {
|
|
241
|
+
const ir = parseRustFile(file, code);
|
|
242
|
+
if (ir) perFile[file] = ir;
|
|
229
243
|
} else if (cppExtRe().test(file)) {
|
|
230
244
|
const ir = parseCppFile(file, code);
|
|
231
245
|
if (ir) perFile[file] = ir;
|
|
@@ -237,6 +251,12 @@ export async function buildProjectIRAsync(fileContents) {
|
|
|
237
251
|
if (ir && ir.file) perFile[ir.file] = ir;
|
|
238
252
|
}
|
|
239
253
|
}
|
|
254
|
+
// SARD_80_F1 W5.7 — merge PHP include/require targets' top-level
|
|
255
|
+
// assignments into the includer's own scope before anything else touches
|
|
256
|
+
// the CFGs (SSA numbering included), since PHP's `include` genuinely
|
|
257
|
+
// executes in the caller's variable namespace. See php-include-merge.js's
|
|
258
|
+
// module header for the full rationale and scope.
|
|
259
|
+
try { mergePhpIncludes(perFile); } catch {}
|
|
240
260
|
if (isSSAEnabled()) {
|
|
241
261
|
for (const ir of Object.values(perFile)) {
|
|
242
262
|
for (const fn of (ir.functions || [])) {
|
|
@@ -269,4 +289,4 @@ export function parsePythonFile(file, code) {
|
|
|
269
289
|
return parsePythonFileRegex(file, code);
|
|
270
290
|
}
|
|
271
291
|
|
|
272
|
-
export { parseJsFile, parseJavaFile, parseCSharpFile, parseKotlinFile, parseGoFile, parsePhpFile, parseRubyFile, parseCppFile, buildCallGraph, buildClassHierarchy, computeSSA, isSSAEnabled, probePythonAvailable, pythonParserDegradation, resetPythonParserDegradation };
|
|
292
|
+
export { parseJsFile, parseJavaFile, parseCSharpFile, parseKotlinFile, parseGoFile, parsePhpFile, parseRubyFile, parseRustFile, parseCppFile, buildCallGraph, buildClassHierarchy, computeSSA, isSSAEnabled, probePythonAvailable, pythonParserDegradation, resetPythonParserDegradation };
|