circle-ir 4.17.0 → 4.18.1
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/dist/analysis/advisories.d.ts +186 -0
- package/dist/analysis/advisories.d.ts.map +1 -0
- package/dist/analysis/advisories.js +391 -0
- package/dist/analysis/advisories.js.map +1 -0
- package/dist/analysis/comment-lines.d.ts +14 -0
- package/dist/analysis/comment-lines.d.ts.map +1 -0
- package/dist/analysis/comment-lines.js +34 -0
- package/dist/analysis/comment-lines.js.map +1 -0
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +59 -5
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/constant-caller-gate.d.ts +2 -2
- package/dist/analysis/constant-caller-gate.d.ts.map +1 -1
- package/dist/analysis/constant-caller-gate.js +205 -29
- package/dist/analysis/constant-caller-gate.js.map +1 -1
- package/dist/analysis/constant-propagation/propagator.d.ts +4 -0
- package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/propagator.js +99 -0
- package/dist/analysis/constant-propagation/propagator.js.map +1 -1
- package/dist/analysis/entry-point-detection.d.ts.map +1 -1
- package/dist/analysis/entry-point-detection.js +72 -0
- package/dist/analysis/entry-point-detection.js.map +1 -1
- package/dist/analysis/interprocedural.d.ts.map +1 -1
- package/dist/analysis/interprocedural.js +75 -8
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/maven-declared.d.ts +72 -0
- package/dist/analysis/maven-declared.d.ts.map +1 -0
- package/dist/analysis/maven-declared.js +216 -0
- package/dist/analysis/maven-declared.js.map +1 -0
- package/dist/analysis/maven-version.d.ts +28 -0
- package/dist/analysis/maven-version.d.ts.map +1 -0
- package/dist/analysis/maven-version.js +172 -0
- package/dist/analysis/maven-version.js.map +1 -0
- package/dist/analysis/passes/constant-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/constant-propagation-pass.js +29 -1
- package/dist/analysis/passes/constant-propagation-pass.js.map +1 -1
- package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
- package/dist/analysis/passes/cross-file-pass.js +81 -1
- package/dist/analysis/passes/cross-file-pass.js.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +7 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.js +44 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.d.ts +2 -0
- package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.js +25 -3
- package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.d.ts +11 -14
- package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.js +562 -0
- package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts +18 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +1057 -14
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.js +66 -3
- package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts +44 -2
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +690 -12
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +133 -10
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/passes/tls-verify-disabled-pass.d.ts +18 -0
- package/dist/analysis/passes/tls-verify-disabled-pass.d.ts.map +1 -1
- package/dist/analysis/passes/tls-verify-disabled-pass.js +161 -0
- package/dist/analysis/passes/tls-verify-disabled-pass.js.map +1 -1
- package/dist/analysis/passes/weak-random-pass.d.ts +6 -0
- package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-random-pass.js +29 -0
- package/dist/analysis/passes/weak-random-pass.js.map +1 -1
- package/dist/analysis/promote-cross-file-flows.d.ts +17 -0
- package/dist/analysis/promote-cross-file-flows.d.ts.map +1 -0
- package/dist/analysis/promote-cross-file-flows.js +32 -0
- package/dist/analysis/promote-cross-file-flows.js.map +1 -0
- package/dist/analysis/sbom.d.ts +100 -0
- package/dist/analysis/sbom.d.ts.map +1 -1
- package/dist/analysis/sbom.js +514 -3
- package/dist/analysis/sbom.js.map +1 -1
- package/dist/analysis/semver-version.d.ts +30 -0
- package/dist/analysis/semver-version.d.ts.map +1 -0
- package/dist/analysis/semver-version.js +76 -0
- package/dist/analysis/semver-version.js.map +1 -0
- package/dist/analysis/taint-matcher.d.ts +5 -0
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +225 -4
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analyzer.d.ts +57 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +69 -6
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +2936 -291
- package/dist/core/circle-ir-core.cjs +634 -203
- package/dist/core/circle-ir-core.js +634 -203
- package/dist/core/extractors/dfg.js +205 -2
- package/dist/core/extractors/dfg.js.map +1 -1
- package/dist/core/extractors/ts-declaration-detail.d.ts +39 -0
- package/dist/core/extractors/ts-declaration-detail.d.ts.map +1 -0
- package/dist/core/extractors/ts-declaration-detail.js +266 -0
- package/dist/core/extractors/ts-declaration-detail.js.map +1 -0
- package/dist/graph/analysis-pass.d.ts +4 -1
- package/dist/graph/analysis-pass.d.ts.map +1 -1
- package/dist/graph/analysis-pass.js +4 -1
- package/dist/graph/analysis-pass.js.map +1 -1
- package/dist/graph-facts/dead-code.d.ts +62 -0
- package/dist/graph-facts/dead-code.d.ts.map +1 -0
- package/dist/graph-facts/dead-code.js +90 -0
- package/dist/graph-facts/dead-code.js.map +1 -0
- package/dist/graph-facts/dependency-calls.d.ts +86 -0
- package/dist/graph-facts/dependency-calls.d.ts.map +1 -0
- package/dist/graph-facts/dependency-calls.js +93 -0
- package/dist/graph-facts/dependency-calls.js.map +1 -0
- package/dist/graph-facts/index.d.ts +10 -0
- package/dist/graph-facts/index.d.ts.map +1 -0
- package/dist/graph-facts/index.js +10 -0
- package/dist/graph-facts/index.js.map +1 -0
- package/dist/graph-facts/maven-packages.d.ts +42 -0
- package/dist/graph-facts/maven-packages.d.ts.map +1 -0
- package/dist/graph-facts/maven-packages.js +133 -0
- package/dist/graph-facts/maven-packages.js.map +1 -0
- package/dist/graph-facts/reach.d.ts +203 -0
- package/dist/graph-facts/reach.d.ts.map +1 -0
- package/dist/graph-facts/reach.js +219 -0
- package/dist/graph-facts/reach.js.map +1 -0
- package/dist/graph-facts/removal-reach.d.ts +73 -0
- package/dist/graph-facts/removal-reach.d.ts.map +1 -0
- package/dist/graph-facts/removal-reach.js +64 -0
- package/dist/graph-facts/removal-reach.js.map +1 -0
- package/dist/index.d.ts +21 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -3
- package/dist/index.js.map +1 -1
- package/dist/navigation/go-graph.d.ts +68 -0
- package/dist/navigation/go-graph.d.ts.map +1 -0
- package/dist/navigation/go-graph.js +753 -0
- package/dist/navigation/go-graph.js.map +1 -0
- package/dist/navigation/index.d.ts +7 -1
- package/dist/navigation/index.d.ts.map +1 -1
- package/dist/navigation/index.js +6 -0
- package/dist/navigation/index.js.map +1 -1
- package/dist/navigation/java-entry-tables.d.ts +43 -0
- package/dist/navigation/java-entry-tables.d.ts.map +1 -0
- package/dist/navigation/java-entry-tables.js +56 -0
- package/dist/navigation/java-entry-tables.js.map +1 -0
- package/dist/navigation/java-graph.d.ts +64 -0
- package/dist/navigation/java-graph.d.ts.map +1 -0
- package/dist/navigation/java-graph.js +466 -0
- package/dist/navigation/java-graph.js.map +1 -0
- package/dist/navigation/navigation-index.d.ts +51 -1
- package/dist/navigation/navigation-index.d.ts.map +1 -1
- package/dist/navigation/navigation-index.js +72 -0
- package/dist/navigation/navigation-index.js.map +1 -1
- package/dist/navigation/python-graph.d.ts +73 -0
- package/dist/navigation/python-graph.d.ts.map +1 -0
- package/dist/navigation/python-graph.js +867 -0
- package/dist/navigation/python-graph.js.map +1 -0
- package/dist/navigation/ts-call-edges.d.ts +193 -0
- package/dist/navigation/ts-call-edges.d.ts.map +1 -0
- package/dist/navigation/ts-call-edges.js +2089 -0
- package/dist/navigation/ts-call-edges.js.map +1 -0
- package/dist/navigation/ts-entry-tables.d.ts +88 -0
- package/dist/navigation/ts-entry-tables.d.ts.map +1 -0
- package/dist/navigation/ts-entry-tables.js +50 -0
- package/dist/navigation/ts-entry-tables.js.map +1 -0
- package/dist/path-set/branches.d.ts +52 -0
- package/dist/path-set/branches.d.ts.map +1 -0
- package/dist/path-set/branches.js +124 -0
- package/dist/path-set/branches.js.map +1 -0
- package/dist/path-set/finding-diff.d.ts +63 -0
- package/dist/path-set/finding-diff.d.ts.map +1 -0
- package/dist/path-set/finding-diff.js +150 -0
- package/dist/path-set/finding-diff.js.map +1 -0
- package/dist/path-set/index.d.ts +18 -0
- package/dist/path-set/index.d.ts.map +1 -0
- package/dist/path-set/index.js +12 -0
- package/dist/path-set/index.js.map +1 -0
- package/dist/path-set/line-diff.d.ts +19 -0
- package/dist/path-set/line-diff.d.ts.map +1 -0
- package/dist/path-set/line-diff.js +98 -0
- package/dist/path-set/line-diff.js.map +1 -0
- package/dist/path-set/path-set.d.ts +88 -0
- package/dist/path-set/path-set.d.ts.map +1 -0
- package/dist/path-set/path-set.js +716 -0
- package/dist/path-set/path-set.js.map +1 -0
- package/dist/path-set/templates.d.ts +42 -0
- package/dist/path-set/templates.d.ts.map +1 -0
- package/dist/path-set/templates.js +119 -0
- package/dist/path-set/templates.js.map +1 -0
- package/dist/path-set/types.d.ts +195 -0
- package/dist/path-set/types.d.ts.map +1 -0
- package/dist/path-set/types.js +8 -0
- package/dist/path-set/types.js.map +1 -0
- package/dist/remedy-class/index.d.ts +12 -0
- package/dist/remedy-class/index.d.ts.map +1 -0
- package/dist/remedy-class/index.js +9 -0
- package/dist/remedy-class/index.js.map +1 -0
- package/dist/remedy-class/match.d.ts +165 -0
- package/dist/remedy-class/match.d.ts.map +1 -0
- package/dist/remedy-class/match.js +705 -0
- package/dist/remedy-class/match.js.map +1 -0
- package/dist/remedy-class/output-context.d.ts +25 -0
- package/dist/remedy-class/output-context.d.ts.map +1 -0
- package/dist/remedy-class/output-context.js +150 -0
- package/dist/remedy-class/output-context.js.map +1 -0
- package/dist/remedy-class/sha256.d.ts +18 -0
- package/dist/remedy-class/sha256.d.ts.map +1 -0
- package/dist/remedy-class/sha256.js +89 -0
- package/dist/remedy-class/sha256.js.map +1 -0
- package/dist/remedy-class/table.d.ts +130 -0
- package/dist/remedy-class/table.d.ts.map +1 -0
- package/dist/remedy-class/table.js +457 -0
- package/dist/remedy-class/table.js.map +1 -0
- package/dist/resolution/cross-file.d.ts +9 -0
- package/dist/resolution/cross-file.d.ts.map +1 -1
- package/dist/resolution/cross-file.js +60 -5
- package/dist/resolution/cross-file.js.map +1 -1
- package/dist/stable-ids/builder.d.ts +20 -0
- package/dist/stable-ids/builder.d.ts.map +1 -0
- package/dist/stable-ids/builder.js +65 -0
- package/dist/stable-ids/builder.js.map +1 -0
- package/dist/stable-ids/diff.d.ts +18 -0
- package/dist/stable-ids/diff.d.ts.map +1 -0
- package/dist/stable-ids/diff.js +37 -0
- package/dist/stable-ids/diff.js.map +1 -0
- package/dist/stable-ids/go.d.ts +28 -0
- package/dist/stable-ids/go.d.ts.map +1 -0
- package/dist/stable-ids/go.js +160 -0
- package/dist/stable-ids/go.js.map +1 -0
- package/dist/stable-ids/hash.d.ts +22 -0
- package/dist/stable-ids/hash.d.ts.map +1 -0
- package/dist/stable-ids/hash.js +167 -0
- package/dist/stable-ids/hash.js.map +1 -0
- package/dist/stable-ids/index.d.ts +30 -0
- package/dist/stable-ids/index.d.ts.map +1 -0
- package/dist/stable-ids/index.js +59 -0
- package/dist/stable-ids/index.js.map +1 -0
- package/dist/stable-ids/java.d.ts +28 -0
- package/dist/stable-ids/java.d.ts.map +1 -0
- package/dist/stable-ids/java.js +224 -0
- package/dist/stable-ids/java.js.map +1 -0
- package/dist/stable-ids/moves.d.ts +87 -0
- package/dist/stable-ids/moves.d.ts.map +1 -0
- package/dist/stable-ids/moves.js +222 -0
- package/dist/stable-ids/moves.js.map +1 -0
- package/dist/stable-ids/nodes.d.ts +32 -0
- package/dist/stable-ids/nodes.d.ts.map +1 -0
- package/dist/stable-ids/nodes.js +92 -0
- package/dist/stable-ids/nodes.js.map +1 -0
- package/dist/stable-ids/normalize.d.ts +35 -0
- package/dist/stable-ids/normalize.d.ts.map +1 -0
- package/dist/stable-ids/normalize.js +174 -0
- package/dist/stable-ids/normalize.js.map +1 -0
- package/dist/stable-ids/parse-errors.d.ts +24 -0
- package/dist/stable-ids/parse-errors.d.ts.map +1 -0
- package/dist/stable-ids/parse-errors.js +46 -0
- package/dist/stable-ids/parse-errors.js.map +1 -0
- package/dist/stable-ids/python.d.ts +27 -0
- package/dist/stable-ids/python.d.ts.map +1 -0
- package/dist/stable-ids/python.js +212 -0
- package/dist/stable-ids/python.js.map +1 -0
- package/dist/stable-ids/ts-parse-errors.d.ts +27 -0
- package/dist/stable-ids/ts-parse-errors.d.ts.map +1 -0
- package/dist/stable-ids/ts-parse-errors.js +75 -0
- package/dist/stable-ids/ts-parse-errors.js.map +1 -0
- package/dist/stable-ids/types.d.ts +106 -0
- package/dist/stable-ids/types.d.ts.map +1 -0
- package/dist/stable-ids/types.js +16 -0
- package/dist/stable-ids/types.js.map +1 -0
- package/dist/stable-ids/typescript.d.ts +30 -0
- package/dist/stable-ids/typescript.d.ts.map +1 -0
- package/dist/stable-ids/typescript.js +362 -0
- package/dist/stable-ids/typescript.js.map +1 -0
- package/dist/types/config.d.ts +9 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/index.d.ts +35 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -290,6 +290,106 @@ function argvFirstLiteralIsShell(lineText) {
|
|
|
290
290
|
.toLowerCase();
|
|
291
291
|
return SHELL_PROGRAMS.has(base);
|
|
292
292
|
}
|
|
293
|
+
/** Last RHS assigned to `varName` on a line before `beforeLine` inside the method span. */
|
|
294
|
+
function javaLocalInitRhsBeforeLine(varName, beforeLine, methodStart, methodEnd, sourceLines) {
|
|
295
|
+
const declAssign = new RegExp(`^\\s*(?:final\\s+)?[\\w<>,\\s\\[\\]]+\\s+${escapeReSf(varName)}\\s*=\\s*(.+?)\\s*;\\s*(?://.*)?$`);
|
|
296
|
+
const reassign = new RegExp(`^\\s*${escapeReSf(varName)}\\s*=\\s*(.+?)\\s*;\\s*(?://.*)?$`);
|
|
297
|
+
let lastRhs;
|
|
298
|
+
for (let l = methodStart; l < beforeLine && l <= methodEnd; l++) {
|
|
299
|
+
const line = (sourceLines[l - 1] ?? '').trim();
|
|
300
|
+
const m = declAssign.exec(line) ?? reassign.exec(line);
|
|
301
|
+
if (m)
|
|
302
|
+
lastRhs = m[1].trim();
|
|
303
|
+
}
|
|
304
|
+
return lastRhs;
|
|
305
|
+
}
|
|
306
|
+
function callArgLooksLikeShellArgv(expr, probePrefix) {
|
|
307
|
+
return argvFirstLiteralIsShell(`${probePrefix}${expr}`);
|
|
308
|
+
}
|
|
309
|
+
function parseSimpleJavaCall(expr) {
|
|
310
|
+
const trimmed = expr.trim();
|
|
311
|
+
const m = /^([\w][\w.]*)\s*\(/.exec(trimmed);
|
|
312
|
+
if (!m)
|
|
313
|
+
return null;
|
|
314
|
+
const head = m[1];
|
|
315
|
+
const dot = head.lastIndexOf('.');
|
|
316
|
+
if (dot < 0)
|
|
317
|
+
return { method: head };
|
|
318
|
+
return { qualifier: head.slice(0, dot), method: head.slice(dot + 1) };
|
|
319
|
+
}
|
|
320
|
+
function javaMethodBodyReturnLooksLikeShellArgv(methodStart, methodEnd, sourceLines) {
|
|
321
|
+
for (let l = methodStart; l <= methodEnd; l++) {
|
|
322
|
+
const line = (sourceLines[l - 1] ?? '').trim();
|
|
323
|
+
const ret = /\breturn\s+(.+?)\s*;/.exec(line);
|
|
324
|
+
if (!ret)
|
|
325
|
+
continue;
|
|
326
|
+
if (callArgLooksLikeShellArgv(ret[1].trim(), 'new ProcessBuilder('))
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
function javaResolveCalleeMethodLines(methodName, qualifier, refLine, types, sourceLines, ctx) {
|
|
332
|
+
const enclosing = types.find(t => refLine >= t.start_line && refLine <= t.end_line);
|
|
333
|
+
const typeName = qualifier
|
|
334
|
+
? (qualifier.includes('.') ? qualifier.split('.').pop() : qualifier)
|
|
335
|
+
: enclosing?.name;
|
|
336
|
+
if (!typeName)
|
|
337
|
+
return undefined;
|
|
338
|
+
const tryType = (t, lines) => {
|
|
339
|
+
const mm = t.methods.find(m => m.name === methodName);
|
|
340
|
+
if (!mm)
|
|
341
|
+
return undefined;
|
|
342
|
+
return { start: mm.start_line, end: mm.end_line, lines };
|
|
343
|
+
};
|
|
344
|
+
if (ctx?.projectGraph && ctx.sourceLinesByFile) {
|
|
345
|
+
for (const fp of ctx.projectGraph.filePaths) {
|
|
346
|
+
const ir = ctx.projectGraph.getIR(fp);
|
|
347
|
+
if (!ir)
|
|
348
|
+
continue;
|
|
349
|
+
const t = ir.types.find(tt => tt.name === typeName);
|
|
350
|
+
if (!t)
|
|
351
|
+
continue;
|
|
352
|
+
const lines = ctx.sourceLinesByFile.get(fp);
|
|
353
|
+
if (!lines)
|
|
354
|
+
continue;
|
|
355
|
+
const hit = tryType(t, lines);
|
|
356
|
+
if (hit)
|
|
357
|
+
return hit;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const local = types.find(t => t.name === typeName);
|
|
361
|
+
if (local)
|
|
362
|
+
return tryType(local, sourceLines);
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
function javaExpressionLooksLikeShellArgv(expr, refLine, types, sourceLines, ctx) {
|
|
366
|
+
if (callArgLooksLikeShellArgv(expr, 'new ProcessBuilder('))
|
|
367
|
+
return true;
|
|
368
|
+
const parsed = parseSimpleJavaCall(expr);
|
|
369
|
+
if (!parsed)
|
|
370
|
+
return false;
|
|
371
|
+
const body = javaResolveCalleeMethodLines(parsed.method, parsed.qualifier, refLine, types, sourceLines, ctx);
|
|
372
|
+
if (!body)
|
|
373
|
+
return false;
|
|
374
|
+
return javaMethodBodyReturnLooksLikeShellArgv(body.start, body.end, body.lines);
|
|
375
|
+
}
|
|
376
|
+
/** cognium-dev#626 — shell argv written to instance field `fieldName` before `beforeLine`. */
|
|
377
|
+
function javaFieldShellArgvWrittenBeforeLine(fieldName, beforeLine, methodStart, methodEnd, sourceLines, types, ctx) {
|
|
378
|
+
const cap = fieldName.charAt(0).toUpperCase() + fieldName.slice(1);
|
|
379
|
+
const beanSetter = new RegExp(`\\bset${escapeReSf(cap)}\\s*\\((.+)\\)\\s*;?\\s*$`);
|
|
380
|
+
const fieldAssign = new RegExp(`^\\s*(?:this\\.)?${escapeReSf(fieldName)}\\s*=\\s*(.+?)\\s*;\\s*(?://.*)?$`);
|
|
381
|
+
for (let l = methodStart; l < beforeLine && l <= methodEnd; l++) {
|
|
382
|
+
const line = (sourceLines[l - 1] ?? '').trim();
|
|
383
|
+
const sm = beanSetter.exec(line);
|
|
384
|
+
if (sm && javaExpressionLooksLikeShellArgv(sm[1].trim(), l, types, sourceLines, ctx)) {
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
const fm = fieldAssign.exec(line);
|
|
388
|
+
if (fm && callArgLooksLikeShellArgv(fm[1].trim(), 'new ProcessBuilder('))
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
293
393
|
/**
|
|
294
394
|
* cognium-dev#626 — does a same-file call of the wrapper method that contains
|
|
295
395
|
* the sink pass a shell argv (`Arrays.asList("/bin/sh", "-c", ...)`,
|
|
@@ -308,7 +408,7 @@ function argvFirstLiteralIsShell(lineText) {
|
|
|
308
408
|
* type is found by its line range). A parameter that is not found answers
|
|
309
409
|
* `false` and today's suppression stands.
|
|
310
410
|
*/
|
|
311
|
-
function sameFileCallerPassesShellArgv(paramName, methodName, sinkLine, types, calls) {
|
|
411
|
+
function sameFileCallerPassesShellArgv(paramName, methodName, sinkLine, types, calls, sourceLines) {
|
|
312
412
|
if (!methodName)
|
|
313
413
|
return false;
|
|
314
414
|
for (const t of types) {
|
|
@@ -316,8 +416,25 @@ function sameFileCallerPassesShellArgv(paramName, methodName, sinkLine, types, c
|
|
|
316
416
|
if (!m)
|
|
317
417
|
continue;
|
|
318
418
|
const pos = m.parameters.findIndex(p => p.name === paramName);
|
|
319
|
-
if (pos < 0)
|
|
320
|
-
|
|
419
|
+
if (pos < 0) {
|
|
420
|
+
const isField = t.fields?.some(f => f.name === paramName);
|
|
421
|
+
if (!isField)
|
|
422
|
+
return false;
|
|
423
|
+
return calls.some(c => {
|
|
424
|
+
if (c.method_name !== methodName)
|
|
425
|
+
return false;
|
|
426
|
+
if (c.receiver != null && c.receiver !== 'this')
|
|
427
|
+
return false;
|
|
428
|
+
if (c.location.line < t.start_line || c.location.line > t.end_line)
|
|
429
|
+
return false;
|
|
430
|
+
if (c.arguments.length !== m.parameters.length)
|
|
431
|
+
return false;
|
|
432
|
+
const callerMethod = t.methods.find(mm => c.location.line >= mm.start_line && c.location.line <= mm.end_line);
|
|
433
|
+
if (!callerMethod)
|
|
434
|
+
return false;
|
|
435
|
+
return javaFieldShellArgvWrittenBeforeLine(paramName, c.location.line, callerMethod.start_line, callerMethod.end_line, sourceLines, types);
|
|
436
|
+
});
|
|
437
|
+
}
|
|
321
438
|
return calls.some(c => {
|
|
322
439
|
if (c.method_name !== methodName)
|
|
323
440
|
return false;
|
|
@@ -338,12 +455,462 @@ function sameFileCallerPassesShellArgv(paramName, methodName, sinkLine, types, c
|
|
|
338
455
|
u.methods.some(um => um.name === methodName && um.parameters.length === c.arguments.length));
|
|
339
456
|
if (shadowed)
|
|
340
457
|
return false;
|
|
341
|
-
const expr = c.arguments[pos]?.expression;
|
|
342
|
-
|
|
458
|
+
const expr = (c.arguments[pos]?.expression ?? '').trim();
|
|
459
|
+
if (!expr)
|
|
460
|
+
return false;
|
|
461
|
+
if (javaExpressionLooksLikeShellArgv(expr, c.location.line, types, sourceLines)) {
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
const argVar = c.arguments[pos]?.variable;
|
|
465
|
+
if (argVar && expr === argVar && !/[.(]/.test(expr)) {
|
|
466
|
+
const callerMethod = t.methods.find(mm => c.location.line >= mm.start_line && c.location.line <= mm.end_line);
|
|
467
|
+
if (callerMethod) {
|
|
468
|
+
const rhs = javaLocalInitRhsBeforeLine(argVar, c.location.line, callerMethod.start_line, callerMethod.end_line, sourceLines);
|
|
469
|
+
if (rhs && javaExpressionLooksLikeShellArgv(rhs, c.location.line, types, sourceLines)) {
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return false;
|
|
343
475
|
});
|
|
344
476
|
}
|
|
345
477
|
return false;
|
|
346
478
|
}
|
|
479
|
+
function callArgumentIsShellArgv(call, argPos, types, sourceLines, ctx) {
|
|
480
|
+
const expr = (call.arguments[argPos]?.expression ?? '').trim();
|
|
481
|
+
if (!expr)
|
|
482
|
+
return false;
|
|
483
|
+
if (javaExpressionLooksLikeShellArgv(expr, call.location.line, types, sourceLines, ctx)) {
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
const argVar = call.arguments[argPos]?.variable;
|
|
487
|
+
if (argVar && expr === argVar && !/[.(]/.test(expr)) {
|
|
488
|
+
const callerType = types.find(t => call.location.line >= t.start_line && call.location.line <= t.end_line);
|
|
489
|
+
const callerMethod = callerType?.methods.find(mm => call.location.line >= mm.start_line && call.location.line <= mm.end_line);
|
|
490
|
+
if (callerMethod) {
|
|
491
|
+
const rhs = javaLocalInitRhsBeforeLine(argVar, call.location.line, callerMethod.start_line, callerMethod.end_line, sourceLines);
|
|
492
|
+
if (rhs && javaExpressionLooksLikeShellArgv(rhs, call.location.line, types, sourceLines, ctx)) {
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
function projectCallerPassesShellArgvForWrapper(paramName, wrapperMethod, sinkLine, calleeTypes, calleeCalls, calleeLines, projectGraph, sourceLinesByFile, getCallerContext, forEachCrossFileCall) {
|
|
500
|
+
const shellArgvCtx = { projectGraph, sourceLinesByFile };
|
|
501
|
+
if (sameFileCallerPassesShellArgv(paramName, wrapperMethod, sinkLine, calleeTypes, calleeCalls, calleeLines)) {
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
if (!wrapperMethod)
|
|
505
|
+
return false;
|
|
506
|
+
const calleeMethod = calleeTypes
|
|
507
|
+
.flatMap(t => t.methods)
|
|
508
|
+
.find(mm => mm.name === wrapperMethod && sinkLine >= mm.start_line && sinkLine <= mm.end_line);
|
|
509
|
+
const pos = calleeMethod?.parameters.findIndex(p => p.name === paramName) ?? -1;
|
|
510
|
+
if (pos < 0) {
|
|
511
|
+
if (!calleeMethod
|
|
512
|
+
|| !javaFieldArgvAtSink(projectGraph, calleeTypes, paramName, wrapperMethod, sinkLine)) {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
let found = false;
|
|
516
|
+
const setterName = `set${paramName.charAt(0).toUpperCase()}${paramName.slice(1)}`;
|
|
517
|
+
forEachCrossFileCall((callerFile, call, targetMethod) => {
|
|
518
|
+
const ctx = getCallerContext(callerFile);
|
|
519
|
+
if (!ctx)
|
|
520
|
+
return;
|
|
521
|
+
const callerMethod = ctx.types
|
|
522
|
+
.flatMap(t => t.methods)
|
|
523
|
+
.find(mm => call.location.line >= mm.start_line && call.location.line <= mm.end_line);
|
|
524
|
+
if (!callerMethod)
|
|
525
|
+
return;
|
|
526
|
+
if (targetMethod.endsWith(`.${setterName}`) || targetMethod.endsWith(setterName)) {
|
|
527
|
+
if (call.arguments.length > 0
|
|
528
|
+
&& callArgumentIsShellArgv(call, 0, ctx.types, ctx.lines, shellArgvCtx)) {
|
|
529
|
+
found = true;
|
|
530
|
+
}
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (!wrapperMethodMatchesTarget(wrapperMethod, targetMethod))
|
|
534
|
+
return;
|
|
535
|
+
if (call.arguments.length !== calleeMethod.parameters.length)
|
|
536
|
+
return;
|
|
537
|
+
if (javaFieldShellArgvWrittenBeforeLine(paramName, call.location.line, callerMethod.start_line, callerMethod.end_line, ctx.lines, ctx.types, shellArgvCtx)) {
|
|
538
|
+
found = true;
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
return found;
|
|
542
|
+
}
|
|
543
|
+
let found = false;
|
|
544
|
+
forEachCrossFileCall((callerFile, call, targetMethod) => {
|
|
545
|
+
if (!targetMethod.endsWith(`.${wrapperMethod}`) && !targetMethod.endsWith(wrapperMethod))
|
|
546
|
+
return;
|
|
547
|
+
if (call.arguments.length <= pos)
|
|
548
|
+
return;
|
|
549
|
+
const ctx = getCallerContext(callerFile);
|
|
550
|
+
if (!ctx)
|
|
551
|
+
return;
|
|
552
|
+
if (callArgumentIsShellArgv(call, pos, ctx.types, ctx.lines, shellArgvCtx))
|
|
553
|
+
found = true;
|
|
554
|
+
});
|
|
555
|
+
return found;
|
|
556
|
+
}
|
|
557
|
+
function pickCallerTaintSource(callerIr, beforeLine) {
|
|
558
|
+
const fromFlow = (callerIr.taint.flows ?? [])
|
|
559
|
+
.filter(f => f.sink_line <= beforeLine && f.source_line <= beforeLine)
|
|
560
|
+
.sort((a, b) => b.source_line - a.source_line)[0];
|
|
561
|
+
if (fromFlow) {
|
|
562
|
+
return { line: fromFlow.source_line, type: fromFlow.source_type };
|
|
563
|
+
}
|
|
564
|
+
const fromSource = (callerIr.taint.sources ?? [])
|
|
565
|
+
.filter(s => s.line <= beforeLine)
|
|
566
|
+
.sort((a, b) => b.line - a.line)[0];
|
|
567
|
+
if (fromSource) {
|
|
568
|
+
return { line: fromSource.line, type: fromSource.type };
|
|
569
|
+
}
|
|
570
|
+
return undefined;
|
|
571
|
+
}
|
|
572
|
+
function wrapperMethodMatchesTarget(wrapperMethod, targetMethod) {
|
|
573
|
+
return targetMethod.endsWith(`.${wrapperMethod}`) || targetMethod.endsWith(wrapperMethod);
|
|
574
|
+
}
|
|
575
|
+
/** Type that owns `wrapperMethod` at `sinkLine` in `types`. */
|
|
576
|
+
function javaHostTypeAtSink(types, wrapperMethod, sinkLine) {
|
|
577
|
+
for (const t of types) {
|
|
578
|
+
if (t.methods.some(mm => mm.name === wrapperMethod && sinkLine >= mm.start_line && sinkLine <= mm.end_line)) {
|
|
579
|
+
return t.name;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return undefined;
|
|
583
|
+
}
|
|
584
|
+
/** Field on `typeName` or a supertype declared anywhere in the project. */
|
|
585
|
+
function javaProjectTypeDeclaresField(projectGraph, typeName, fieldName) {
|
|
586
|
+
const seen = new Set();
|
|
587
|
+
const queue = [typeName];
|
|
588
|
+
while (queue.length > 0) {
|
|
589
|
+
const tn = queue.pop();
|
|
590
|
+
if (seen.has(tn))
|
|
591
|
+
continue;
|
|
592
|
+
seen.add(tn);
|
|
593
|
+
for (const fp of projectGraph.filePaths) {
|
|
594
|
+
const ir = projectGraph.getIR(fp);
|
|
595
|
+
if (!ir)
|
|
596
|
+
continue;
|
|
597
|
+
for (const t of ir.types) {
|
|
598
|
+
if (t.name !== tn)
|
|
599
|
+
continue;
|
|
600
|
+
if (t.fields?.some(f => f.name === fieldName))
|
|
601
|
+
return true;
|
|
602
|
+
if (t.extends)
|
|
603
|
+
queue.push(t.extends);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
/** Callee files for cross-file argv recall: sink file plus supertype declaration files. */
|
|
610
|
+
function javaRelatedCalleeFiles(projectGraph, primaryCalleeFile, hostTypeName) {
|
|
611
|
+
const files = new Set([primaryCalleeFile]);
|
|
612
|
+
if (!hostTypeName)
|
|
613
|
+
return [...files];
|
|
614
|
+
const seen = new Set();
|
|
615
|
+
const queue = [hostTypeName];
|
|
616
|
+
while (queue.length > 0) {
|
|
617
|
+
const tn = queue.pop();
|
|
618
|
+
if (seen.has(tn))
|
|
619
|
+
continue;
|
|
620
|
+
seen.add(tn);
|
|
621
|
+
for (const fp of projectGraph.filePaths) {
|
|
622
|
+
const ir = projectGraph.getIR(fp);
|
|
623
|
+
const t = ir?.types.find(tt => tt.name === tn);
|
|
624
|
+
if (!t)
|
|
625
|
+
continue;
|
|
626
|
+
files.add(fp);
|
|
627
|
+
if (t.extends)
|
|
628
|
+
queue.push(t.extends);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return [...files];
|
|
632
|
+
}
|
|
633
|
+
function javaFieldArgvAtSink(projectGraph, calleeTypes, paramName, wrapperMethod, sinkLine) {
|
|
634
|
+
const hostType = javaHostTypeAtSink(calleeTypes, wrapperMethod, sinkLine);
|
|
635
|
+
if (!hostType)
|
|
636
|
+
return false;
|
|
637
|
+
return javaProjectTypeDeclaresField(projectGraph, hostType, paramName);
|
|
638
|
+
}
|
|
639
|
+
function javaProjectFieldType(projectGraph, typeName, fieldName) {
|
|
640
|
+
const seen = new Set();
|
|
641
|
+
const queue = [typeName];
|
|
642
|
+
while (queue.length > 0) {
|
|
643
|
+
const tn = queue.pop();
|
|
644
|
+
if (seen.has(tn))
|
|
645
|
+
continue;
|
|
646
|
+
seen.add(tn);
|
|
647
|
+
for (const fp of projectGraph.filePaths) {
|
|
648
|
+
const ir = projectGraph.getIR(fp);
|
|
649
|
+
if (!ir)
|
|
650
|
+
continue;
|
|
651
|
+
for (const t of ir.types) {
|
|
652
|
+
if (t.name !== tn)
|
|
653
|
+
continue;
|
|
654
|
+
const f = t.fields?.find(ff => ff.name === fieldName);
|
|
655
|
+
if (f?.type)
|
|
656
|
+
return f.type;
|
|
657
|
+
if (t.extends)
|
|
658
|
+
queue.push(t.extends);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
function resolveJavaArgvIdentifierType(varName, wrapperMethod, sinkLine, types, projectGraph) {
|
|
665
|
+
const direct = resolveIdentifierType(varName, wrapperMethod, [...types]);
|
|
666
|
+
if (direct)
|
|
667
|
+
return direct;
|
|
668
|
+
if (!wrapperMethod)
|
|
669
|
+
return null;
|
|
670
|
+
const hostType = javaHostTypeAtSink(types, wrapperMethod, sinkLine);
|
|
671
|
+
if (!hostType)
|
|
672
|
+
return null;
|
|
673
|
+
return javaProjectFieldType(projectGraph, hostType, varName);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* cognium-dev#626 — per-file argv suppression drops the ProcessBuilder sink on
|
|
677
|
+
* isolated `List<String>` wrappers; restore cross-file recall by synthesizing
|
|
678
|
+
* flows (and project taint paths) when a resolved caller passes shell argv.
|
|
679
|
+
*/
|
|
680
|
+
export function synthesizeCrossFileArgvWrapperFlows(fileAnalyses, projectGraph, sourceLinesByFile, taintPaths) {
|
|
681
|
+
const getCallerContext = (file) => {
|
|
682
|
+
const ir = projectGraph.getIR(file);
|
|
683
|
+
if (!ir)
|
|
684
|
+
return undefined;
|
|
685
|
+
return {
|
|
686
|
+
types: ir.types,
|
|
687
|
+
calls: ir.calls,
|
|
688
|
+
lines: sourceLinesByFile.get(file) ?? [],
|
|
689
|
+
ir,
|
|
690
|
+
};
|
|
691
|
+
};
|
|
692
|
+
const forEachCrossFileCall = (calleeFile, cb) => {
|
|
693
|
+
for (const fromFile of projectGraph.filePaths) {
|
|
694
|
+
if (fromFile === calleeFile)
|
|
695
|
+
continue;
|
|
696
|
+
for (const rc of projectGraph.resolver.getResolvedCallsFromFile(fromFile)) {
|
|
697
|
+
if (rc.targetFile !== calleeFile)
|
|
698
|
+
continue;
|
|
699
|
+
cb(fromFile, rc.call, rc.targetMethod);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
for (const fa of fileAnalyses) {
|
|
704
|
+
const ir = fa.analysis;
|
|
705
|
+
if (ir.meta.language !== 'java')
|
|
706
|
+
continue;
|
|
707
|
+
const lines = sourceLinesByFile.get(fa.file) ?? [];
|
|
708
|
+
for (const ctorCall of ir.calls) {
|
|
709
|
+
if (ctorCall.method_name !== 'ProcessBuilder')
|
|
710
|
+
continue;
|
|
711
|
+
if (ctorCall.receiver != null && ctorCall.receiver !== undefined)
|
|
712
|
+
continue;
|
|
713
|
+
const sinkLine = ctorCall.location.line;
|
|
714
|
+
const sinkLineText = lines[sinkLine - 1] ?? '';
|
|
715
|
+
if (!/\bnew\s+ProcessBuilder\s*\(/.test(sinkLineText))
|
|
716
|
+
continue;
|
|
717
|
+
if (argvFirstLiteralIsShell(sinkLineText))
|
|
718
|
+
continue;
|
|
719
|
+
const arg0 = ctorCall.arguments[0];
|
|
720
|
+
if (!arg0)
|
|
721
|
+
continue;
|
|
722
|
+
const varName = arg0.variable ?? '';
|
|
723
|
+
const argExpr = (arg0.expression ?? '').trim();
|
|
724
|
+
if (!varName || argExpr !== varName || /[(.]/.test(argExpr))
|
|
725
|
+
continue;
|
|
726
|
+
const wrapperMethod = ctorCall.in_method;
|
|
727
|
+
const t = resolveJavaArgvIdentifierType(varName, wrapperMethod, sinkLine, ir.types, projectGraph);
|
|
728
|
+
if (!isArgvContainerType(t))
|
|
729
|
+
continue;
|
|
730
|
+
if (sameFileCallerPassesShellArgv(varName, wrapperMethod, sinkLine, ir.types, ir.calls, lines)) {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
const calleeMethod = ir.types
|
|
734
|
+
.flatMap(ty => ty.methods)
|
|
735
|
+
.find(mm => mm.name === wrapperMethod && sinkLine >= mm.start_line && sinkLine <= mm.end_line);
|
|
736
|
+
const pos = calleeMethod?.parameters.findIndex(p => p.name === varName) ?? -1;
|
|
737
|
+
const hostType = wrapperMethod
|
|
738
|
+
? javaHostTypeAtSink(ir.types, wrapperMethod, sinkLine)
|
|
739
|
+
: undefined;
|
|
740
|
+
const fieldArgv = pos < 0
|
|
741
|
+
&& wrapperMethod
|
|
742
|
+
&& javaFieldArgvAtSink(projectGraph, ir.types, varName, wrapperMethod, sinkLine);
|
|
743
|
+
if (pos < 0 && !fieldArgv)
|
|
744
|
+
continue;
|
|
745
|
+
const relatedCalleeFiles = javaRelatedCalleeFiles(projectGraph, fa.file, hostType);
|
|
746
|
+
const shellArgvCtx = { projectGraph, sourceLinesByFile };
|
|
747
|
+
for (const calleeFile of relatedCalleeFiles) {
|
|
748
|
+
forEachCrossFileCall(calleeFile, (callerFile, call, targetMethod) => {
|
|
749
|
+
const ctx = getCallerContext(callerFile);
|
|
750
|
+
if (!ctx)
|
|
751
|
+
return;
|
|
752
|
+
const setterName = `set${varName.charAt(0).toUpperCase()}${varName.slice(1)}`;
|
|
753
|
+
let shellProof = false;
|
|
754
|
+
if (pos >= 0) {
|
|
755
|
+
if (!wrapperMethod || !wrapperMethodMatchesTarget(wrapperMethod, targetMethod))
|
|
756
|
+
return;
|
|
757
|
+
if (call.arguments.length <= pos)
|
|
758
|
+
return;
|
|
759
|
+
shellProof = callArgumentIsShellArgv(call, pos, ctx.types, ctx.lines, shellArgvCtx);
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
const callerMethod = ctx.types
|
|
763
|
+
.flatMap(ty => ty.methods)
|
|
764
|
+
.find(mm => call.location.line >= mm.start_line && call.location.line <= mm.end_line);
|
|
765
|
+
if (!callerMethod || !calleeMethod)
|
|
766
|
+
return;
|
|
767
|
+
if (targetMethod.endsWith(`.${setterName}`) || targetMethod.endsWith(setterName)) {
|
|
768
|
+
shellProof =
|
|
769
|
+
call.arguments.length > 0
|
|
770
|
+
&& callArgumentIsShellArgv(call, 0, ctx.types, ctx.lines, shellArgvCtx);
|
|
771
|
+
}
|
|
772
|
+
else if (wrapperMethod && wrapperMethodMatchesTarget(wrapperMethod, targetMethod)) {
|
|
773
|
+
shellProof =
|
|
774
|
+
call.arguments.length === calleeMethod.parameters.length
|
|
775
|
+
&& javaFieldShellArgvWrittenBeforeLine(varName, call.location.line, callerMethod.start_line, callerMethod.end_line, ctx.lines, ctx.types, shellArgvCtx);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (!shellProof)
|
|
779
|
+
return;
|
|
780
|
+
const source = pickCallerTaintSource(ctx.ir, call.location.line);
|
|
781
|
+
if (!source)
|
|
782
|
+
return;
|
|
783
|
+
const flows = ir.taint.flows ?? [];
|
|
784
|
+
const flowExists = flows.some(f => f.sink_type === 'command_injection'
|
|
785
|
+
&& f.sink_line === sinkLine
|
|
786
|
+
&& f.source_line === source.line
|
|
787
|
+
&& f.source_type === source.type);
|
|
788
|
+
if (!flowExists) {
|
|
789
|
+
flows.push({
|
|
790
|
+
source_line: source.line,
|
|
791
|
+
sink_line: sinkLine,
|
|
792
|
+
source_type: source.type,
|
|
793
|
+
sink_type: 'command_injection',
|
|
794
|
+
path: [
|
|
795
|
+
{ variable: '', line: source.line, type: 'source' },
|
|
796
|
+
{ variable: varName, line: sinkLine, type: 'sink' },
|
|
797
|
+
],
|
|
798
|
+
confidence: 0.7,
|
|
799
|
+
sanitized: false,
|
|
800
|
+
});
|
|
801
|
+
ir.taint.flows = flows;
|
|
802
|
+
}
|
|
803
|
+
const pathDup = taintPaths.some(tp => tp.source.file === callerFile
|
|
804
|
+
&& tp.source.line === source.line
|
|
805
|
+
&& tp.sink.file === fa.file
|
|
806
|
+
&& tp.sink.line === sinkLine
|
|
807
|
+
&& tp.sink.type === 'command_injection');
|
|
808
|
+
if (pathDup)
|
|
809
|
+
return;
|
|
810
|
+
const callerLines = sourceLinesByFile.get(callerFile) ?? [];
|
|
811
|
+
const dupId = `${callerFile}:${source.line}→${fa.file}:${sinkLine}`;
|
|
812
|
+
taintPaths.push({
|
|
813
|
+
id: `cf-argv-626-${dupId}`,
|
|
814
|
+
source: {
|
|
815
|
+
file: callerFile,
|
|
816
|
+
line: source.line,
|
|
817
|
+
type: source.type,
|
|
818
|
+
code: callerLines[source.line - 1] ?? '',
|
|
819
|
+
},
|
|
820
|
+
sink: {
|
|
821
|
+
file: fa.file,
|
|
822
|
+
line: sinkLine,
|
|
823
|
+
type: 'command_injection',
|
|
824
|
+
cwe: 'CWE-78',
|
|
825
|
+
code: sinkLineText,
|
|
826
|
+
},
|
|
827
|
+
hops: [
|
|
828
|
+
{
|
|
829
|
+
file: callerFile,
|
|
830
|
+
method: call.in_method ?? '',
|
|
831
|
+
line: call.location.line,
|
|
832
|
+
code: callerLines[call.location.line - 1] ?? '',
|
|
833
|
+
variable: '',
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
file: fa.file,
|
|
837
|
+
method: wrapperMethod ?? '',
|
|
838
|
+
line: sinkLine,
|
|
839
|
+
code: sinkLineText,
|
|
840
|
+
variable: varName,
|
|
841
|
+
},
|
|
842
|
+
],
|
|
843
|
+
sanitizers_in_path: [],
|
|
844
|
+
path_exists: true,
|
|
845
|
+
confidence: 0.7,
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
/** cognium-dev#626 — drop argv-wrapper flows with no shell-argv caller (incl. cross-file). */
|
|
853
|
+
export function filterJavaArgvWrapperFlowsWithoutShellCaller(fileAnalyses, projectGraph, sourceLinesByFile) {
|
|
854
|
+
const getCallerContext = (file) => {
|
|
855
|
+
const ir = projectGraph.getIR(file);
|
|
856
|
+
if (!ir)
|
|
857
|
+
return undefined;
|
|
858
|
+
return {
|
|
859
|
+
types: ir.types,
|
|
860
|
+
calls: ir.calls,
|
|
861
|
+
lines: sourceLinesByFile.get(file) ?? [],
|
|
862
|
+
};
|
|
863
|
+
};
|
|
864
|
+
const forEachCrossFileCall = (calleeFile, cb) => {
|
|
865
|
+
for (const fromFile of projectGraph.filePaths) {
|
|
866
|
+
if (fromFile === calleeFile)
|
|
867
|
+
continue;
|
|
868
|
+
for (const rc of projectGraph.resolver.getResolvedCallsFromFile(fromFile)) {
|
|
869
|
+
if (rc.targetFile !== calleeFile)
|
|
870
|
+
continue;
|
|
871
|
+
cb(fromFile, rc.call, rc.targetMethod);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
for (const fa of fileAnalyses) {
|
|
876
|
+
const ir = fa.analysis;
|
|
877
|
+
if (ir.meta.language !== 'java')
|
|
878
|
+
continue;
|
|
879
|
+
const lines = sourceLinesByFile.get(fa.file) ?? [];
|
|
880
|
+
const flows = ir.taint.flows ?? [];
|
|
881
|
+
ir.taint.flows = flows.filter(flow => {
|
|
882
|
+
if (flow.sink_type !== 'command_injection')
|
|
883
|
+
return true;
|
|
884
|
+
const sinkLineText = lines[flow.sink_line - 1] ?? '';
|
|
885
|
+
if (!/\bnew\s+ProcessBuilder\s*\(/.test(sinkLineText))
|
|
886
|
+
return true;
|
|
887
|
+
const ctorCall = ir.calls.find(c => c.method_name === 'ProcessBuilder' &&
|
|
888
|
+
c.location.line === flow.sink_line &&
|
|
889
|
+
(c.receiver === null || c.receiver === undefined));
|
|
890
|
+
if (!ctorCall)
|
|
891
|
+
return true;
|
|
892
|
+
const arg0 = ctorCall.arguments[0];
|
|
893
|
+
if (!arg0)
|
|
894
|
+
return true;
|
|
895
|
+
const varName = arg0.variable ?? '';
|
|
896
|
+
const argExpr = (arg0.expression ?? '').trim();
|
|
897
|
+
if (!varName || argExpr !== varName || /[(.]/.test(argExpr))
|
|
898
|
+
return true;
|
|
899
|
+
const t = resolveJavaArgvIdentifierType(varName, ctorCall.in_method, flow.sink_line, ir.types, projectGraph);
|
|
900
|
+
if (!isArgvContainerType(t))
|
|
901
|
+
return true;
|
|
902
|
+
const hostType = ctorCall.in_method
|
|
903
|
+
? javaHostTypeAtSink(ir.types, ctorCall.in_method, flow.sink_line)
|
|
904
|
+
: undefined;
|
|
905
|
+
const relatedCalleeFiles = javaRelatedCalleeFiles(projectGraph, fa.file, hostType);
|
|
906
|
+
return projectCallerPassesShellArgvForWrapper(varName, ctorCall.in_method, flow.sink_line, ir.types, ir.calls, lines, projectGraph, sourceLinesByFile, getCallerContext, cb => {
|
|
907
|
+
for (const calleeFile of relatedCalleeFiles) {
|
|
908
|
+
forEachCrossFileCall(calleeFile, cb);
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
}
|
|
347
914
|
// ---------------------------------------------------------------------------
|
|
348
915
|
// Stage 12 — Java throw-statement FP suppression.
|
|
349
916
|
// (cognium-dev #157 — Sprint 45)
|
|
@@ -490,6 +1057,23 @@ const FETCH_MEMBER_RECEIVER_RE = /[\w$)\]]\s*\.\s*fetch\s*\(/;
|
|
|
490
1057
|
// unaffected.
|
|
491
1058
|
const NOSQL_ARRAY_METHODS = new Set(['find', 'filter', 'some', 'every', 'findIndex', 'findLast']);
|
|
492
1059
|
const FUNCTION_FIRST_ARG_RE = /\.\s*(?:find|filter|some|every|findIndex|findLast)\s*\(\s*(?:async\s+)?(?:function\b|(?:\([^()]*\)|[A-Za-z_$][\w$]*)\s*=>)/;
|
|
1060
|
+
// cognium-dev#1000 — `recv.find(name)` with a single bare identifier argument.
|
|
1061
|
+
const NAMED_ARRAY_PREDICATE_RE = /([A-Za-z_$][\w$]*)\s*\.\s*(find|filter|some|every|findIndex|findLast)\s*\(\s*([A-Za-z_$][\w$]*)\s*\)/;
|
|
1062
|
+
/** A function declared in this file under `name` (function / const arrow / function expression). */
|
|
1063
|
+
function isDeclaredFunctionName(name, lines) {
|
|
1064
|
+
const n = name.replace(/[$]/g, '\\$');
|
|
1065
|
+
const decls = [
|
|
1066
|
+
new RegExp(`\\bfunction\\s+${n}\\s*[(<]`),
|
|
1067
|
+
new RegExp(`\\b(?:const|let|var)\\s+${n}\\s*(?::[^=]*)?=\\s*(?:async\\s+)?(?:function\\b|\\(|[A-Za-z_$][\\w$]*\\s*=>)`),
|
|
1068
|
+
];
|
|
1069
|
+
return lines.some(l => decls.some(re => re.test(l)));
|
|
1070
|
+
}
|
|
1071
|
+
/** `name` declared as an array in this file: `= [`, `: T[]`, `Array<…>`, `new Array`, `Array.from`. */
|
|
1072
|
+
function isDeclaredArrayName(name, lines) {
|
|
1073
|
+
const n = name.replace(/[$]/g, '\\$');
|
|
1074
|
+
const re = new RegExp(`\\b${n}\\s*(?:\\??\\s*:\\s*(?:readonly\\s+)?(?:[\\w$.<>,\\s]+\\[\\]|(?:Readonly)?Array\\s*<)|(?::[^=;]*)?=\\s*(?:\\[|new\\s+Array\\b|Array\\.(?:from|of)\\b))`);
|
|
1075
|
+
return lines.some(l => re.test(l));
|
|
1076
|
+
}
|
|
493
1077
|
// cognium-ai#279 R-1 — an argument that is an object literal with an ORM
|
|
494
1078
|
// query-builder key (`where`/`select`/`populate`/…) is a *builder*, not a raw
|
|
495
1079
|
// query string: a value in a `{ field, value }` position binds as a parameter
|
|
@@ -519,6 +1103,23 @@ const SERVER_SIGNAL_RE = /["']use server["']|\b(?:getServerSideProps|getStaticPr
|
|
|
519
1103
|
const FIXED_HOST_TEMPLATE_RE = /`https?:\/\/[^/${}\s`]+[/`]/;
|
|
520
1104
|
/** A bash URL word whose scheme and host are literal and closed by `/` (#495). */
|
|
521
1105
|
const BASH_FIXED_HOST_URL_RE = /^(?:https?|ftp):\/\/[A-Za-z0-9.-]+(?::\d+)?\//;
|
|
1106
|
+
/** Rust `format!("https://host/{}", path)` — literal origin, path-only interpolation (#922). */
|
|
1107
|
+
const RUST_FIXED_HOST_FORMAT_RE = /format!\(\s*"https?:\/\/[^"{}]+\/\{\}"/;
|
|
1108
|
+
function rustSsrfRequestUsesFixedHostFormat(code, sinkLine) {
|
|
1109
|
+
const lines = code.split('\n');
|
|
1110
|
+
const sinkText = lines[sinkLine - 1] ?? '';
|
|
1111
|
+
const argMatch = sinkText.match(/(?:reqwest::)?get\s*\(\s*&?(\w+)/);
|
|
1112
|
+
if (!argMatch)
|
|
1113
|
+
return false;
|
|
1114
|
+
const varName = argMatch[1];
|
|
1115
|
+
const assignRe = new RegExp(`\\blet\\s+${escapeRegex(varName)}\\s*=\\s*format!`);
|
|
1116
|
+
for (let i = sinkLine - 2; i >= Math.max(0, sinkLine - 20); i--) {
|
|
1117
|
+
const line = lines[i] ?? '';
|
|
1118
|
+
if (assignRe.test(line) && RUST_FIXED_HOST_FORMAT_RE.test(line))
|
|
1119
|
+
return true;
|
|
1120
|
+
}
|
|
1121
|
+
return false;
|
|
1122
|
+
}
|
|
522
1123
|
/**
|
|
523
1124
|
* True when the string literal that ended at `end` is the WHOLE initializer:
|
|
524
1125
|
* the next code token is `;`, `,`, end of input, or (ASI) a token on a later
|
|
@@ -817,7 +1418,10 @@ function isUnwrittenLocalMapKeyRead(code, source) {
|
|
|
817
1418
|
return false;
|
|
818
1419
|
const [, recv, key] = read;
|
|
819
1420
|
// (a) locally constructed map
|
|
820
|
-
const ctorRe = new RegExp(
|
|
1421
|
+
const ctorRe = new RegExp(
|
|
1422
|
+
// `new HashMap<>()` or the qualified `new java.util.HashMap<>()` (OWASP
|
|
1423
|
+
// BenchmarkJava writes the qualified form throughout).
|
|
1424
|
+
`\\b${recv}\\s*=\\s*new\\s+(?:java\\.util\\.(?:concurrent\\.)?)?(?:Hash|Tree|LinkedHash|ConcurrentHash|Concurrent|Identity|Weak)?Map\\s*(?:<[^>]*>)?\\s*\\(`);
|
|
821
1425
|
if (!lines.some(l => ctorRe.test(l)))
|
|
822
1426
|
return false;
|
|
823
1427
|
// (b) no escape: the receiver must not appear inside any call's arguments.
|
|
@@ -831,10 +1435,11 @@ function isUnwrittenLocalMapKeyRead(code, source) {
|
|
|
831
1435
|
if (escapeRe.test(stripped))
|
|
832
1436
|
return false;
|
|
833
1437
|
}
|
|
834
|
-
// (c2) was this literal key ever written? If so, drop
|
|
1438
|
+
// (c2) was this literal key ever written? If so, drop when that key's puts are
|
|
1439
|
+
// all literals (#584 key-shuffle), even if another key holds tainted data.
|
|
835
1440
|
const putRe = new RegExp(`\\b${recv}\\s*\\.\\s*put\\s*\\(\\s*"${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`);
|
|
836
1441
|
if (lines.some(l => putRe.test(l)))
|
|
837
|
-
return
|
|
1442
|
+
return isLiteralOnlyLocalMapKey(lines, recv, key);
|
|
838
1443
|
return true;
|
|
839
1444
|
}
|
|
840
1445
|
/**
|
|
@@ -847,6 +1452,51 @@ function isUnwrittenLocalMapKeyRead(code, source) {
|
|
|
847
1452
|
* a value out of sight. Combined with (a) local construction and (b) no escape
|
|
848
1453
|
* in the caller, a read of such a map yields a literal and is not a source.
|
|
849
1454
|
*/
|
|
1455
|
+
/** Every `put` of `key` on `recv` stores a string literal (other keys ignored). */
|
|
1456
|
+
function isLiteralOnlyLocalMapKey(lines, recv, key) {
|
|
1457
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1458
|
+
const otherMutator = new RegExp(`\\b${recv}\\s*\\.\\s*(?:putAll|putIfAbsent|compute|computeIfAbsent|computeIfPresent|merge|replace|replaceAll|entrySet|values)\\s*\\(`);
|
|
1459
|
+
if (lines.some(l => otherMutator.test(l)))
|
|
1460
|
+
return false;
|
|
1461
|
+
const keyPut = new RegExp(`\\b${recv}\\s*\\.\\s*put\\s*\\(\\s*"${escapedKey}"\\s*,\\s*"(?:[^"\\\\]|\\\\.)*"\\s*\\)`);
|
|
1462
|
+
const anyKeyPut = new RegExp(`\\b${recv}\\s*\\.\\s*put\\s*\\(\\s*"${escapedKey}"`);
|
|
1463
|
+
let putsForKey = 0;
|
|
1464
|
+
let literalPutsForKey = 0;
|
|
1465
|
+
for (const l of lines) {
|
|
1466
|
+
putsForKey += (l.match(anyKeyPut) ?? []).length;
|
|
1467
|
+
literalPutsForKey += (l.match(keyPut) ?? []).length;
|
|
1468
|
+
}
|
|
1469
|
+
return putsForKey > 0 && putsForKey === literalPutsForKey;
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* cognium-dev#818 (src14) / #888 — `class: 'cache'` seeds `cache.get()` as db_input for
|
|
1473
|
+
* redis `createClient()` bindings. Drop in-memory shapes: local `new Map()`, object
|
|
1474
|
+
* property caches (`scope.cache.get`), and a bare `cache` variable not assigned from
|
|
1475
|
+
* `createClient()`.
|
|
1476
|
+
*/
|
|
1477
|
+
function isJsDisguisedCacheDbInputRead(code, source) {
|
|
1478
|
+
if (source.type !== 'db_input')
|
|
1479
|
+
return false;
|
|
1480
|
+
const loc = source.location ?? '';
|
|
1481
|
+
if (!/\.cache\.get\s*\(/i.test(loc) && !/\bcache\.get\s*\(/i.test(loc))
|
|
1482
|
+
return false;
|
|
1483
|
+
const lines = code.split('\n');
|
|
1484
|
+
const lineText = lines[source.line - 1] ?? '';
|
|
1485
|
+
// In-object texture / loader caches: `scope.cache.get`, `this.cache.get`, etc.
|
|
1486
|
+
if (/\w+\.cache\.get\s*\(/.test(lineText))
|
|
1487
|
+
return true;
|
|
1488
|
+
const read = /\b([A-Za-z_]\w*)\s*\.\s*get\s*\(/.exec(lineText);
|
|
1489
|
+
if (!read)
|
|
1490
|
+
return false;
|
|
1491
|
+
const recv = read[1];
|
|
1492
|
+
if (recv !== 'cache')
|
|
1493
|
+
return false;
|
|
1494
|
+
const mapCtor = new RegExp(`\\b${recv}\\s*=\\s*new\\s+(?:Hash|Tree|LinkedHash|ConcurrentHash|Concurrent|Identity|Weak)?Map\\s*(?:<[^>]*>)?\\s*\\(`);
|
|
1495
|
+
if (lines.some(l => mapCtor.test(l)))
|
|
1496
|
+
return true;
|
|
1497
|
+
const redisClient = new RegExp(`\\b${recv}\\s*=\\s*(?:await\\s+)?(?:redis\\.)?createClient\\s*\\(`);
|
|
1498
|
+
return !lines.some(l => redisClient.test(l));
|
|
1499
|
+
}
|
|
850
1500
|
function isLiteralOnlyLocalMap(lines, recv) {
|
|
851
1501
|
const anyPut = new RegExp(`\\b${recv}\\s*\\.\\s*put\\s*\\(`, 'g');
|
|
852
1502
|
const literalPut = new RegExp(`\\b${recv}\\s*\\.\\s*put\\s*\\(\\s*(?:"(?:[^"\\\\]|\\\\.)*"|[A-Za-z_][\\w.]*)\\s*,\\s*"(?:[^"\\\\]|\\\\.)*"\\s*\\)`, 'g');
|
|
@@ -1396,7 +2046,9 @@ export class SinkFilterPass {
|
|
|
1396
2046
|
// of a key that WAS put is also dropped when every value put on that local,
|
|
1397
2047
|
// non-escaping map is a string literal (`isLiteralOnlyLocalMap`).
|
|
1398
2048
|
const sources = ctx.code
|
|
1399
|
-
? executableSources
|
|
2049
|
+
? executableSources
|
|
2050
|
+
.filter(s => !isUnwrittenLocalMapKeyRead(ctx.code, s))
|
|
2051
|
+
.filter(s => !isJsDisguisedCacheDbInputRead(ctx.code, s))
|
|
1400
2052
|
: executableSources;
|
|
1401
2053
|
// Build merged sinks, deduplicating JS DOM sinks that may overlap with config sinks.
|
|
1402
2054
|
const sinks = [...taintMatcher.sinks];
|
|
@@ -1951,7 +2603,8 @@ export class SinkFilterPass {
|
|
|
1951
2603
|
// cognium-dev#626: a `List<String>` PARAMETER is argv-form only while no
|
|
1952
2604
|
// same-file caller hands it a shell argv (`/bin/sh -c <script>`): then
|
|
1953
2605
|
// the tainted element is the script, so the suppression proves nothing.
|
|
1954
|
-
|
|
2606
|
+
// Cross-file shell-argv recall is restored in analyzeProject (#626).
|
|
2607
|
+
if (isArgvContainerType(t) && !sameFileCallerPassesShellArgv(varName, ctorCall.in_method, ctorCall.location.line, irTypes, irCalls, sourceLines)) {
|
|
1955
2608
|
return false;
|
|
1956
2609
|
}
|
|
1957
2610
|
}
|
|
@@ -2375,7 +3028,21 @@ export class SinkFilterPass {
|
|
|
2375
3028
|
if (!NOSQL_ARRAY_METHODS.has(sink.method ?? ''))
|
|
2376
3029
|
return true;
|
|
2377
3030
|
const sinkLineText = sourceLines[sink.line - 1] ?? '';
|
|
2378
|
-
|
|
3031
|
+
if (FUNCTION_FIRST_ARG_RE.test(sinkLineText))
|
|
3032
|
+
return false;
|
|
3033
|
+
// cognium-dev#1000 — `ancestors.find(meaningful)`: a named predicate
|
|
3034
|
+
// declared as a function in this file, or a receiver declared as an
|
|
3035
|
+
// array, is Array.prototype iteration too. A Mongo
|
|
3036
|
+
// `collection.find(query)` with a query variable keeps firing.
|
|
3037
|
+
const named = NAMED_ARRAY_PREDICATE_RE.exec(sinkLineText);
|
|
3038
|
+
if (named) {
|
|
3039
|
+
const [, receiver, method, arg] = named;
|
|
3040
|
+
if (NOSQL_ARRAY_METHODS.has(method) &&
|
|
3041
|
+
(isDeclaredFunctionName(arg, sourceLines) || isDeclaredArrayName(receiver, sourceLines))) {
|
|
3042
|
+
return false;
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
return true;
|
|
2379
3046
|
});
|
|
2380
3047
|
}
|
|
2381
3048
|
// Stage 15h — JS/TS sql/nosql: ORM query-builder object argument.
|
|
@@ -2573,6 +3240,16 @@ export class SinkFilterPass {
|
|
|
2573
3240
|
return bodyCalls.length === 0 || !bodyCalls.every(rustBodyDeclaresNonHtmlType);
|
|
2574
3241
|
});
|
|
2575
3242
|
}
|
|
3243
|
+
// Stage 15n — rust ssrf: format! with a literal origin and a single `{}` path
|
|
3244
|
+
// slot (cognium-dev#922). Same intent as Stage 15i/15m: the host is fixed at
|
|
3245
|
+
// compile time, so taint in the path cannot redirect the request.
|
|
3246
|
+
if (language === 'rust') {
|
|
3247
|
+
filtered = filtered.filter(sink => {
|
|
3248
|
+
if (sink.type !== 'ssrf')
|
|
3249
|
+
return true;
|
|
3250
|
+
return !rustSsrfRequestUsesFixedHostFormat(ctx.code, sink.line);
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
2576
3253
|
// Stage 15m — bash ssrf: fixed-host URL word (cognium-dev#495, #669).
|
|
2577
3254
|
// The bash analogue of Stage 15i: `curl -- "https://api.example.com/${p}"`
|
|
2578
3255
|
// can only reach the literal host, whatever `$p` holds. Every word on the
|
|
@@ -3290,7 +3967,8 @@ export function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg
|
|
|
3290
3967
|
// same line (e.g. System.getProperty("user.dir") inside r.exec(args,...,new File(...))).
|
|
3291
3968
|
// sink.method is set by findSinks to call.method_name; language-sources sinks also carry it.
|
|
3292
3969
|
const relevantCalls = sink.method
|
|
3293
|
-
? callsAtSink.filter(c => c.method_name === sink.method
|
|
3970
|
+
? callsAtSink.filter(c => c.method_name === sink.method &&
|
|
3971
|
+
(sink.callColumn === undefined || c.location.column === sink.callColumn))
|
|
3294
3972
|
: callsAtSink;
|
|
3295
3973
|
// Whether to trust sink.argPositions for narrowing the cleanness check. In shell-like
|
|
3296
3974
|
// languages, flag-vs-positional ambiguity makes statically declared argument positions
|