cognium-dev 3.37.0 → 3.39.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/dist/cli.js +612 -5
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -18050,6 +18050,7 @@ class CrossFileResolver {
|
|
|
18050
18050
|
typeHierarchy;
|
|
18051
18051
|
fileIRs = new Map;
|
|
18052
18052
|
methodTaintInfo = new Map;
|
|
18053
|
+
fieldTaintInfo = new Map;
|
|
18053
18054
|
resolvedCalls = new Map;
|
|
18054
18055
|
constructor(symbolTable, typeHierarchy) {
|
|
18055
18056
|
this.symbolTable = symbolTable;
|
|
@@ -18060,6 +18061,7 @@ class CrossFileResolver {
|
|
|
18060
18061
|
this.symbolTable.addFromIR(ir, filePath);
|
|
18061
18062
|
this.typeHierarchy.addFromIR(ir, filePath);
|
|
18062
18063
|
this.analyzeMethodTaint(ir, filePath);
|
|
18064
|
+
this.analyzeFieldTaint(ir, filePath);
|
|
18063
18065
|
}
|
|
18064
18066
|
resolveCall(call, fromFile) {
|
|
18065
18067
|
const cacheKey = `${fromFile}:${call.location.line}:${call.method_name}`;
|
|
@@ -18226,6 +18228,113 @@ class CrossFileResolver {
|
|
|
18226
18228
|
}
|
|
18227
18229
|
}
|
|
18228
18230
|
}
|
|
18231
|
+
analyzeFieldTaint(ir, filePath) {
|
|
18232
|
+
const pkg = ir.meta.package || "";
|
|
18233
|
+
const ctorFieldRe = /^(\w+)\.(\w+)\(\) returns tainted field '([^']+)' \(from constructor param '([^']+)'\)/;
|
|
18234
|
+
for (const src of ir.taint.sources) {
|
|
18235
|
+
if (src.type !== "constructor_field")
|
|
18236
|
+
continue;
|
|
18237
|
+
const m = ctorFieldRe.exec(src.location);
|
|
18238
|
+
if (!m)
|
|
18239
|
+
continue;
|
|
18240
|
+
const [, className, , fieldName, sourceParam] = m;
|
|
18241
|
+
const typeFqn = pkg ? `${pkg}.${className}` : className;
|
|
18242
|
+
const type = ir.types.find((t) => t.name === className);
|
|
18243
|
+
if (!type)
|
|
18244
|
+
continue;
|
|
18245
|
+
const writerMethod = type.methods.find((mth) => mth.name === className && mth.parameters.some((p) => p.name === sourceParam)) ?? type.methods.find((mth) => mth.parameters.some((p) => p.name === sourceParam));
|
|
18246
|
+
if (!writerMethod)
|
|
18247
|
+
continue;
|
|
18248
|
+
const field = type.fields?.find((f) => f.name === fieldName);
|
|
18249
|
+
const key = `${typeFqn}.${fieldName}`;
|
|
18250
|
+
const existing = this.fieldTaintInfo.get(key);
|
|
18251
|
+
const writer = {
|
|
18252
|
+
methodFqn: `${typeFqn}.${writerMethod.name}`,
|
|
18253
|
+
methodName: writerMethod.name,
|
|
18254
|
+
writeLine: writerMethod.start_line,
|
|
18255
|
+
sourceType: "constructor_field",
|
|
18256
|
+
sourceLine: src.line
|
|
18257
|
+
};
|
|
18258
|
+
if (existing) {
|
|
18259
|
+
if (!existing.writers.some((w) => w.methodFqn === writer.methodFqn)) {
|
|
18260
|
+
existing.writers.push(writer);
|
|
18261
|
+
}
|
|
18262
|
+
} else {
|
|
18263
|
+
this.fieldTaintInfo.set(key, {
|
|
18264
|
+
typeFqn,
|
|
18265
|
+
fieldName,
|
|
18266
|
+
fieldType: field?.type ?? null,
|
|
18267
|
+
file: filePath,
|
|
18268
|
+
writers: [writer]
|
|
18269
|
+
});
|
|
18270
|
+
}
|
|
18271
|
+
}
|
|
18272
|
+
for (const type of ir.types) {
|
|
18273
|
+
const typeFqn = pkg ? `${pkg}.${type.name}` : type.name;
|
|
18274
|
+
for (const method of type.methods) {
|
|
18275
|
+
if (!method.name.startsWith("set") || method.name.length <= 3)
|
|
18276
|
+
continue;
|
|
18277
|
+
if (method.parameters.length !== 1)
|
|
18278
|
+
continue;
|
|
18279
|
+
const fieldName = method.name.charAt(3).toLowerCase() + method.name.substring(4);
|
|
18280
|
+
const field = type.fields?.find((f) => f.name === fieldName);
|
|
18281
|
+
if (!field)
|
|
18282
|
+
continue;
|
|
18283
|
+
const key = `${typeFqn}.${fieldName}`;
|
|
18284
|
+
const writer = {
|
|
18285
|
+
methodFqn: `${typeFqn}.${method.name}`,
|
|
18286
|
+
methodName: method.name,
|
|
18287
|
+
writeLine: method.start_line,
|
|
18288
|
+
sourceType: "setter_param",
|
|
18289
|
+
sourceLine: method.start_line
|
|
18290
|
+
};
|
|
18291
|
+
const existing = this.fieldTaintInfo.get(key);
|
|
18292
|
+
if (existing) {
|
|
18293
|
+
if (!existing.writers.some((w) => w.methodFqn === writer.methodFqn)) {
|
|
18294
|
+
existing.writers.push(writer);
|
|
18295
|
+
}
|
|
18296
|
+
} else {
|
|
18297
|
+
this.fieldTaintInfo.set(key, {
|
|
18298
|
+
typeFqn,
|
|
18299
|
+
fieldName,
|
|
18300
|
+
fieldType: field.type ?? null,
|
|
18301
|
+
file: filePath,
|
|
18302
|
+
writers: [writer]
|
|
18303
|
+
});
|
|
18304
|
+
}
|
|
18305
|
+
}
|
|
18306
|
+
}
|
|
18307
|
+
const injectAnnotations = new Set(["Autowired", "Inject", "Resource"]);
|
|
18308
|
+
for (const type of ir.types) {
|
|
18309
|
+
const typeFqn = pkg ? `${pkg}.${type.name}` : type.name;
|
|
18310
|
+
for (const field of type.fields ?? []) {
|
|
18311
|
+
if (!field.annotations?.some((a) => injectAnnotations.has(a)))
|
|
18312
|
+
continue;
|
|
18313
|
+
const key = `${typeFqn}.${field.name}`;
|
|
18314
|
+
const writer = {
|
|
18315
|
+
methodFqn: `${typeFqn}.<injected>`,
|
|
18316
|
+
methodName: "<injected>",
|
|
18317
|
+
writeLine: type.start_line,
|
|
18318
|
+
sourceType: "autowired_field",
|
|
18319
|
+
sourceLine: type.start_line
|
|
18320
|
+
};
|
|
18321
|
+
const existing = this.fieldTaintInfo.get(key);
|
|
18322
|
+
if (existing) {
|
|
18323
|
+
if (!existing.writers.some((w) => w.methodFqn === writer.methodFqn)) {
|
|
18324
|
+
existing.writers.push(writer);
|
|
18325
|
+
}
|
|
18326
|
+
} else {
|
|
18327
|
+
this.fieldTaintInfo.set(key, {
|
|
18328
|
+
typeFqn,
|
|
18329
|
+
fieldName: field.name,
|
|
18330
|
+
fieldType: field.type ?? null,
|
|
18331
|
+
file: filePath,
|
|
18332
|
+
writers: [writer]
|
|
18333
|
+
});
|
|
18334
|
+
}
|
|
18335
|
+
}
|
|
18336
|
+
}
|
|
18337
|
+
}
|
|
18229
18338
|
isMethodTaintSource(method, sources) {
|
|
18230
18339
|
const sourceAnnotations = ["RequestParam", "RequestBody", "PathVariable", "QueryParam"];
|
|
18231
18340
|
for (const param of method.parameters) {
|
|
@@ -18234,6 +18343,8 @@ class CrossFileResolver {
|
|
|
18234
18343
|
}
|
|
18235
18344
|
}
|
|
18236
18345
|
for (const source of sources) {
|
|
18346
|
+
if (source.type === "interprocedural_param")
|
|
18347
|
+
continue;
|
|
18237
18348
|
if (source.line >= method.start_line && source.line <= method.end_line) {
|
|
18238
18349
|
return true;
|
|
18239
18350
|
}
|
|
@@ -18242,6 +18353,8 @@ class CrossFileResolver {
|
|
|
18242
18353
|
}
|
|
18243
18354
|
getSourceType(method, sources) {
|
|
18244
18355
|
for (const source of sources) {
|
|
18356
|
+
if (source.type === "interprocedural_param")
|
|
18357
|
+
continue;
|
|
18245
18358
|
if (source.line >= method.start_line && source.line <= method.end_line) {
|
|
18246
18359
|
return source.type;
|
|
18247
18360
|
}
|
|
@@ -18249,15 +18362,45 @@ class CrossFileResolver {
|
|
|
18249
18362
|
return;
|
|
18250
18363
|
}
|
|
18251
18364
|
findTaintedParams(method, ir) {
|
|
18252
|
-
const taintedParams =
|
|
18365
|
+
const taintedParams = new Set;
|
|
18253
18366
|
const numParams = method.parameters.length;
|
|
18254
18367
|
for (let i2 = 0;i2 < numParams; i2++) {
|
|
18255
18368
|
const param = method.parameters[i2];
|
|
18256
18369
|
if (param.annotations.some((a) => ["RequestParam", "RequestBody", "PathVariable"].includes(a))) {
|
|
18257
|
-
taintedParams.
|
|
18370
|
+
taintedParams.add(i2);
|
|
18258
18371
|
}
|
|
18259
18372
|
}
|
|
18260
|
-
|
|
18373
|
+
const paramNameToIndex = new Map;
|
|
18374
|
+
for (let i2 = 0;i2 < numParams; i2++) {
|
|
18375
|
+
const name2 = method.parameters[i2].name;
|
|
18376
|
+
if (name2)
|
|
18377
|
+
paramNameToIndex.set(name2, i2);
|
|
18378
|
+
}
|
|
18379
|
+
for (const sink of ir.taint.sinks) {
|
|
18380
|
+
if (sink.line < method.start_line || sink.line > method.end_line)
|
|
18381
|
+
continue;
|
|
18382
|
+
const callsAtSink = ir.calls.filter((c) => c.location.line === sink.line);
|
|
18383
|
+
for (const call of callsAtSink) {
|
|
18384
|
+
for (const arg of call.arguments) {
|
|
18385
|
+
const candidates = [];
|
|
18386
|
+
if (arg.variable)
|
|
18387
|
+
candidates.push(arg.variable);
|
|
18388
|
+
if (arg.expression) {
|
|
18389
|
+
for (const [name2] of paramNameToIndex) {
|
|
18390
|
+
const re = new RegExp(`\\b${name2}\\b`);
|
|
18391
|
+
if (re.test(arg.expression))
|
|
18392
|
+
candidates.push(name2);
|
|
18393
|
+
}
|
|
18394
|
+
}
|
|
18395
|
+
for (const cand of candidates) {
|
|
18396
|
+
const idx = paramNameToIndex.get(cand);
|
|
18397
|
+
if (idx !== undefined)
|
|
18398
|
+
taintedParams.add(idx);
|
|
18399
|
+
}
|
|
18400
|
+
}
|
|
18401
|
+
}
|
|
18402
|
+
}
|
|
18403
|
+
return [...taintedParams].sort((a, b) => a - b);
|
|
18261
18404
|
}
|
|
18262
18405
|
isSanitizerMethod(methodName) {
|
|
18263
18406
|
const sanitizerPatterns = [
|
|
@@ -18309,12 +18452,24 @@ class CrossFileResolver {
|
|
|
18309
18452
|
for (const source of ir.taint.sources) {
|
|
18310
18453
|
if (source.type === "interprocedural_param")
|
|
18311
18454
|
continue;
|
|
18455
|
+
const sourceVar = source.variable ?? this.getLocalDefVarAt(ir, source.line);
|
|
18312
18456
|
for (const call of ir.calls) {
|
|
18313
18457
|
if (call.location.line < source.line)
|
|
18314
18458
|
continue;
|
|
18315
18459
|
const resolved = this.resolveCall(call, filePath);
|
|
18316
18460
|
if (!resolved || resolved.targetFile === filePath)
|
|
18317
18461
|
continue;
|
|
18462
|
+
if (sourceVar) {
|
|
18463
|
+
const argMentions = call.arguments.some((arg) => {
|
|
18464
|
+
if (arg.variable === sourceVar)
|
|
18465
|
+
return true;
|
|
18466
|
+
if (arg.expression && new RegExp(`\\b${sourceVar}\\b`).test(arg.expression))
|
|
18467
|
+
return true;
|
|
18468
|
+
return false;
|
|
18469
|
+
});
|
|
18470
|
+
if (!argMentions)
|
|
18471
|
+
continue;
|
|
18472
|
+
}
|
|
18318
18473
|
const targetIR = this.fileIRs.get(resolved.targetFile);
|
|
18319
18474
|
if (!targetIR || targetIR.taint.sinks.length === 0)
|
|
18320
18475
|
continue;
|
|
@@ -18353,6 +18508,407 @@ class CrossFileResolver {
|
|
|
18353
18508
|
}
|
|
18354
18509
|
return flows;
|
|
18355
18510
|
}
|
|
18511
|
+
findInterproceduralTaintPaths() {
|
|
18512
|
+
const paths = [];
|
|
18513
|
+
const seen = new Set;
|
|
18514
|
+
const methodIndex = this.buildMethodIndex();
|
|
18515
|
+
for (const [callerFile, callerIR] of this.fileIRs) {
|
|
18516
|
+
for (const type of callerIR.types) {
|
|
18517
|
+
for (const method of type.methods) {
|
|
18518
|
+
const tainted = new Map;
|
|
18519
|
+
for (const src of callerIR.taint.sources) {
|
|
18520
|
+
if (src.type === "interprocedural_param")
|
|
18521
|
+
continue;
|
|
18522
|
+
if (src.line < method.start_line || src.line > method.end_line)
|
|
18523
|
+
continue;
|
|
18524
|
+
if (!src.variable)
|
|
18525
|
+
continue;
|
|
18526
|
+
tainted.set(src.variable, {
|
|
18527
|
+
file: callerFile,
|
|
18528
|
+
line: src.line,
|
|
18529
|
+
type: src.type,
|
|
18530
|
+
hopChain: [{ file: callerFile, line: src.line, method: method.name, kind: "source" }]
|
|
18531
|
+
});
|
|
18532
|
+
}
|
|
18533
|
+
const callsInMethod = callerIR.calls.filter((c) => c.location.line >= method.start_line && c.location.line <= method.end_line).sort((a, b) => a.location.line - b.location.line);
|
|
18534
|
+
for (const call of callsInMethod) {
|
|
18535
|
+
const resolved = this.resolveCall(call, callerFile);
|
|
18536
|
+
if (!resolved)
|
|
18537
|
+
continue;
|
|
18538
|
+
const callee = this.methodTaintInfo.get(resolved.targetMethod);
|
|
18539
|
+
if (!callee)
|
|
18540
|
+
continue;
|
|
18541
|
+
if (callee.returnsSource && !callee.sanitizes && callee.sourceType) {
|
|
18542
|
+
const calleeNode = methodIndex.get(resolved.targetMethod);
|
|
18543
|
+
const calleeSourceLine = calleeNode ? this.findRealSourceLineInMethod(calleeNode.ir, calleeNode.method) : undefined;
|
|
18544
|
+
const sourceLine = calleeSourceLine ?? call.location.line;
|
|
18545
|
+
const sourceFile = callee.file;
|
|
18546
|
+
const sourceType = callee.sourceType;
|
|
18547
|
+
const defsAtLine = callerIR.dfg.defs.filter((d) => d.line === call.location.line && d.kind === "local");
|
|
18548
|
+
for (const def of defsAtLine) {
|
|
18549
|
+
if (!def.variable)
|
|
18550
|
+
continue;
|
|
18551
|
+
const baseChain = [
|
|
18552
|
+
{ file: sourceFile, line: sourceLine, method: resolved.targetMethod, kind: "source" },
|
|
18553
|
+
{ file: callerFile, line: call.location.line, method: method.name, kind: "wrapper_return" }
|
|
18554
|
+
];
|
|
18555
|
+
tainted.set(def.variable, {
|
|
18556
|
+
file: sourceFile,
|
|
18557
|
+
line: sourceLine,
|
|
18558
|
+
type: sourceType,
|
|
18559
|
+
hopChain: baseChain
|
|
18560
|
+
});
|
|
18561
|
+
}
|
|
18562
|
+
}
|
|
18563
|
+
if (callee.taintedParams.length === 0 || callee.sanitizes)
|
|
18564
|
+
continue;
|
|
18565
|
+
for (let argIdx = 0;argIdx < call.arguments.length; argIdx++) {
|
|
18566
|
+
if (!callee.taintedParams.includes(argIdx))
|
|
18567
|
+
continue;
|
|
18568
|
+
const arg = call.arguments[argIdx];
|
|
18569
|
+
const matched = this.matchTaintedArg(arg, tainted);
|
|
18570
|
+
if (!matched)
|
|
18571
|
+
continue;
|
|
18572
|
+
const calleeNode = methodIndex.get(resolved.targetMethod);
|
|
18573
|
+
if (!calleeNode)
|
|
18574
|
+
continue;
|
|
18575
|
+
const sinksInCallee = calleeNode.ir.taint.sinks.filter((s) => s.line >= calleeNode.method.start_line && s.line <= calleeNode.method.end_line);
|
|
18576
|
+
for (const sink of sinksInCallee) {
|
|
18577
|
+
const key = `${matched.origin.file}:${matched.origin.line}→${callee.file}:${sink.line}`;
|
|
18578
|
+
if (seen.has(key))
|
|
18579
|
+
continue;
|
|
18580
|
+
seen.add(key);
|
|
18581
|
+
const hops = [
|
|
18582
|
+
...matched.origin.hopChain,
|
|
18583
|
+
{ file: callerFile, line: call.location.line, method: method.name, kind: "sink_call" },
|
|
18584
|
+
{ file: callee.file, line: sink.line, method: resolved.targetMethod, kind: "sink" }
|
|
18585
|
+
];
|
|
18586
|
+
const decay = Math.max(0.3, Math.pow(0.85, Math.max(hops.length - 1, 0)));
|
|
18587
|
+
paths.push({
|
|
18588
|
+
source: {
|
|
18589
|
+
file: matched.origin.file,
|
|
18590
|
+
line: matched.origin.line,
|
|
18591
|
+
type: matched.origin.type
|
|
18592
|
+
},
|
|
18593
|
+
sink: {
|
|
18594
|
+
file: callee.file,
|
|
18595
|
+
line: sink.line,
|
|
18596
|
+
type: sink.type,
|
|
18597
|
+
cwe: sink.cwe
|
|
18598
|
+
},
|
|
18599
|
+
hops,
|
|
18600
|
+
confidence: decay
|
|
18601
|
+
});
|
|
18602
|
+
}
|
|
18603
|
+
}
|
|
18604
|
+
}
|
|
18605
|
+
if (tainted.size > 0) {
|
|
18606
|
+
const sinksInCaller = callerIR.taint.sinks.filter((s) => s.line >= method.start_line && s.line <= method.end_line);
|
|
18607
|
+
for (const sink of sinksInCaller) {
|
|
18608
|
+
const callsAtSink = callerIR.calls.filter((c) => c.location.line === sink.line);
|
|
18609
|
+
for (const sinkCall of callsAtSink) {
|
|
18610
|
+
for (const arg of sinkCall.arguments ?? []) {
|
|
18611
|
+
const matched = this.matchTaintedArg(arg, tainted);
|
|
18612
|
+
if (!matched)
|
|
18613
|
+
continue;
|
|
18614
|
+
const key = `${matched.origin.file}:${matched.origin.line}→${callerFile}:${sink.line}`;
|
|
18615
|
+
if (seen.has(key))
|
|
18616
|
+
continue;
|
|
18617
|
+
seen.add(key);
|
|
18618
|
+
const hops = [
|
|
18619
|
+
...matched.origin.hopChain,
|
|
18620
|
+
{ file: callerFile, line: sink.line, method: method.name, kind: "sink" }
|
|
18621
|
+
];
|
|
18622
|
+
const decay = Math.max(0.3, Math.pow(0.85, Math.max(hops.length - 1, 0)));
|
|
18623
|
+
paths.push({
|
|
18624
|
+
source: {
|
|
18625
|
+
file: matched.origin.file,
|
|
18626
|
+
line: matched.origin.line,
|
|
18627
|
+
type: matched.origin.type
|
|
18628
|
+
},
|
|
18629
|
+
sink: {
|
|
18630
|
+
file: callerFile,
|
|
18631
|
+
line: sink.line,
|
|
18632
|
+
type: sink.type,
|
|
18633
|
+
cwe: sink.cwe
|
|
18634
|
+
},
|
|
18635
|
+
hops,
|
|
18636
|
+
confidence: decay
|
|
18637
|
+
});
|
|
18638
|
+
}
|
|
18639
|
+
}
|
|
18640
|
+
}
|
|
18641
|
+
}
|
|
18642
|
+
}
|
|
18643
|
+
}
|
|
18644
|
+
}
|
|
18645
|
+
return paths;
|
|
18646
|
+
}
|
|
18647
|
+
findFieldBindingTaintPaths() {
|
|
18648
|
+
const paths = [];
|
|
18649
|
+
const seen = new Set;
|
|
18650
|
+
if (this.fieldTaintInfo.size === 0)
|
|
18651
|
+
return paths;
|
|
18652
|
+
const fieldExprRe = /^(\w+)\.(\w+)$/;
|
|
18653
|
+
const methodIndex = this.buildMethodIndex();
|
|
18654
|
+
for (const [callerFile, callerIR] of this.fileIRs) {
|
|
18655
|
+
for (const type of callerIR.types) {
|
|
18656
|
+
const callerTypeFqn = callerIR.meta.package ? `${callerIR.meta.package}.${type.name}` : type.name;
|
|
18657
|
+
for (const method of type.methods) {
|
|
18658
|
+
const tainted = new Map;
|
|
18659
|
+
for (const src of callerIR.taint.sources) {
|
|
18660
|
+
if (src.type === "interprocedural_param")
|
|
18661
|
+
continue;
|
|
18662
|
+
if (src.line < method.start_line || src.line > method.end_line)
|
|
18663
|
+
continue;
|
|
18664
|
+
if (!src.variable)
|
|
18665
|
+
continue;
|
|
18666
|
+
tainted.set(src.variable, {
|
|
18667
|
+
file: callerFile,
|
|
18668
|
+
line: src.line,
|
|
18669
|
+
type: src.type,
|
|
18670
|
+
hopChain: [{ file: callerFile, line: src.line, method: method.name, kind: "source" }]
|
|
18671
|
+
});
|
|
18672
|
+
}
|
|
18673
|
+
const defsInMethod = callerIR.dfg.defs.filter((d) => d.kind === "local" && d.line >= method.start_line && d.line <= method.end_line && !!d.variable);
|
|
18674
|
+
for (const def of defsInMethod) {
|
|
18675
|
+
const usesAtLine = callerIR.dfg.uses.filter((u) => u.line === def.line);
|
|
18676
|
+
if (usesAtLine.length < 2)
|
|
18677
|
+
continue;
|
|
18678
|
+
let receiver = null;
|
|
18679
|
+
let fieldName = null;
|
|
18680
|
+
if (def.expression) {
|
|
18681
|
+
const exprMatch = fieldExprRe.exec(def.expression.trim());
|
|
18682
|
+
if (exprMatch) {
|
|
18683
|
+
receiver = exprMatch[1];
|
|
18684
|
+
fieldName = exprMatch[2];
|
|
18685
|
+
}
|
|
18686
|
+
}
|
|
18687
|
+
const resolveReceiverType = (rcv) => {
|
|
18688
|
+
const param = method.parameters.find((p) => p.name === rcv);
|
|
18689
|
+
if (param?.type)
|
|
18690
|
+
return param.type;
|
|
18691
|
+
const fieldOnSelf = type.fields?.find((f) => f.name === rcv);
|
|
18692
|
+
if (fieldOnSelf?.type)
|
|
18693
|
+
return fieldOnSelf.type;
|
|
18694
|
+
return null;
|
|
18695
|
+
};
|
|
18696
|
+
let receiverType = null;
|
|
18697
|
+
if (receiver && fieldName) {
|
|
18698
|
+
receiverType = resolveReceiverType(receiver);
|
|
18699
|
+
}
|
|
18700
|
+
if (!receiverType) {
|
|
18701
|
+
for (const rcvUse of usesAtLine) {
|
|
18702
|
+
if (!rcvUse.variable || rcvUse.variable === def.variable)
|
|
18703
|
+
continue;
|
|
18704
|
+
const rt = resolveReceiverType(rcvUse.variable);
|
|
18705
|
+
if (!rt)
|
|
18706
|
+
continue;
|
|
18707
|
+
const fieldUse = usesAtLine.find((u) => u !== rcvUse && !!u.variable && u.variable !== def.variable && u.variable !== rcvUse.variable && this.typeHasField(rt, u.variable));
|
|
18708
|
+
if (fieldUse) {
|
|
18709
|
+
receiver = rcvUse.variable;
|
|
18710
|
+
fieldName = fieldUse.variable;
|
|
18711
|
+
receiverType = rt;
|
|
18712
|
+
break;
|
|
18713
|
+
}
|
|
18714
|
+
}
|
|
18715
|
+
}
|
|
18716
|
+
if (!receiver || !fieldName || !receiverType)
|
|
18717
|
+
continue;
|
|
18718
|
+
const fieldKey = this.resolveFieldTaintKey(receiverType, fieldName, callerIR);
|
|
18719
|
+
if (!fieldKey)
|
|
18720
|
+
continue;
|
|
18721
|
+
const fieldInfo = this.fieldTaintInfo.get(fieldKey);
|
|
18722
|
+
if (!fieldInfo || fieldInfo.writers.length === 0)
|
|
18723
|
+
continue;
|
|
18724
|
+
const writer = fieldInfo.writers.find((w) => w.sourceType === "constructor_field" || w.sourceType === "autowired_field") ?? null;
|
|
18725
|
+
if (!writer)
|
|
18726
|
+
continue;
|
|
18727
|
+
const hopChain = [
|
|
18728
|
+
{
|
|
18729
|
+
file: fieldInfo.file,
|
|
18730
|
+
line: writer.sourceLine,
|
|
18731
|
+
method: writer.methodName,
|
|
18732
|
+
kind: "source"
|
|
18733
|
+
},
|
|
18734
|
+
{
|
|
18735
|
+
file: fieldInfo.file,
|
|
18736
|
+
line: writer.writeLine,
|
|
18737
|
+
method: writer.methodName,
|
|
18738
|
+
kind: "field_write"
|
|
18739
|
+
},
|
|
18740
|
+
{
|
|
18741
|
+
file: callerFile,
|
|
18742
|
+
line: def.line,
|
|
18743
|
+
method: method.name,
|
|
18744
|
+
kind: "field_read"
|
|
18745
|
+
}
|
|
18746
|
+
];
|
|
18747
|
+
tainted.set(def.variable, {
|
|
18748
|
+
file: fieldInfo.file,
|
|
18749
|
+
line: writer.sourceLine,
|
|
18750
|
+
type: writer.sourceType,
|
|
18751
|
+
hopChain
|
|
18752
|
+
});
|
|
18753
|
+
}
|
|
18754
|
+
if (tainted.size === 0)
|
|
18755
|
+
continue;
|
|
18756
|
+
const sinksInCaller = callerIR.taint.sinks.filter((s) => s.line >= method.start_line && s.line <= method.end_line);
|
|
18757
|
+
for (const sink of sinksInCaller) {
|
|
18758
|
+
const callsAtSink = callerIR.calls.filter((c) => c.location.line === sink.line);
|
|
18759
|
+
for (const sinkCall of callsAtSink) {
|
|
18760
|
+
for (const arg of sinkCall.arguments ?? []) {
|
|
18761
|
+
const matched = this.matchTaintedArg(arg, tainted);
|
|
18762
|
+
if (!matched)
|
|
18763
|
+
continue;
|
|
18764
|
+
const key = `fb:${matched.origin.file}:${matched.origin.line}→${callerFile}:${sink.line}`;
|
|
18765
|
+
if (seen.has(key))
|
|
18766
|
+
continue;
|
|
18767
|
+
seen.add(key);
|
|
18768
|
+
const hops = [
|
|
18769
|
+
...matched.origin.hopChain,
|
|
18770
|
+
{ file: callerFile, line: sink.line, method: method.name, kind: "sink" }
|
|
18771
|
+
];
|
|
18772
|
+
const decay = Math.max(0.3, Math.pow(0.85, Math.max(hops.length - 1, 0)));
|
|
18773
|
+
paths.push({
|
|
18774
|
+
source: {
|
|
18775
|
+
file: matched.origin.file,
|
|
18776
|
+
line: matched.origin.line,
|
|
18777
|
+
type: matched.origin.type
|
|
18778
|
+
},
|
|
18779
|
+
sink: {
|
|
18780
|
+
file: callerFile,
|
|
18781
|
+
line: sink.line,
|
|
18782
|
+
type: sink.type,
|
|
18783
|
+
cwe: sink.cwe
|
|
18784
|
+
},
|
|
18785
|
+
hops,
|
|
18786
|
+
confidence: decay
|
|
18787
|
+
});
|
|
18788
|
+
}
|
|
18789
|
+
}
|
|
18790
|
+
}
|
|
18791
|
+
const callsInMethod = callerIR.calls.filter((c) => c.location.line >= method.start_line && c.location.line <= method.end_line).sort((a, b) => a.location.line - b.location.line);
|
|
18792
|
+
for (const call of callsInMethod) {
|
|
18793
|
+
const resolved = this.resolveCall(call, callerFile);
|
|
18794
|
+
if (!resolved)
|
|
18795
|
+
continue;
|
|
18796
|
+
const callee = this.methodTaintInfo.get(resolved.targetMethod);
|
|
18797
|
+
if (!callee || callee.sanitizes || callee.taintedParams.length === 0)
|
|
18798
|
+
continue;
|
|
18799
|
+
for (let argIdx = 0;argIdx < call.arguments.length; argIdx++) {
|
|
18800
|
+
if (!callee.taintedParams.includes(argIdx))
|
|
18801
|
+
continue;
|
|
18802
|
+
const matched = this.matchTaintedArg(call.arguments[argIdx], tainted);
|
|
18803
|
+
if (!matched)
|
|
18804
|
+
continue;
|
|
18805
|
+
const calleeNode = methodIndex.get(resolved.targetMethod);
|
|
18806
|
+
if (!calleeNode)
|
|
18807
|
+
continue;
|
|
18808
|
+
const sinksInCallee = calleeNode.ir.taint.sinks.filter((s) => s.line >= calleeNode.method.start_line && s.line <= calleeNode.method.end_line);
|
|
18809
|
+
for (const sink of sinksInCallee) {
|
|
18810
|
+
const key = `fb:${matched.origin.file}:${matched.origin.line}→${callee.file}:${sink.line}`;
|
|
18811
|
+
if (seen.has(key))
|
|
18812
|
+
continue;
|
|
18813
|
+
seen.add(key);
|
|
18814
|
+
const hops = [
|
|
18815
|
+
...matched.origin.hopChain,
|
|
18816
|
+
{ file: callerFile, line: call.location.line, method: method.name, kind: "sink_call" },
|
|
18817
|
+
{ file: callee.file, line: sink.line, method: resolved.targetMethod, kind: "sink" }
|
|
18818
|
+
];
|
|
18819
|
+
const decay = Math.max(0.3, Math.pow(0.85, Math.max(hops.length - 1, 0)));
|
|
18820
|
+
paths.push({
|
|
18821
|
+
source: {
|
|
18822
|
+
file: matched.origin.file,
|
|
18823
|
+
line: matched.origin.line,
|
|
18824
|
+
type: matched.origin.type
|
|
18825
|
+
},
|
|
18826
|
+
sink: {
|
|
18827
|
+
file: callee.file,
|
|
18828
|
+
line: sink.line,
|
|
18829
|
+
type: sink.type,
|
|
18830
|
+
cwe: sink.cwe
|
|
18831
|
+
},
|
|
18832
|
+
hops,
|
|
18833
|
+
confidence: decay
|
|
18834
|
+
});
|
|
18835
|
+
}
|
|
18836
|
+
}
|
|
18837
|
+
}
|
|
18838
|
+
}
|
|
18839
|
+
}
|
|
18840
|
+
}
|
|
18841
|
+
return paths;
|
|
18842
|
+
}
|
|
18843
|
+
typeHasField(typeName, fieldName) {
|
|
18844
|
+
for (const [, ir] of this.fileIRs) {
|
|
18845
|
+
for (const t of ir.types) {
|
|
18846
|
+
if (t.name !== typeName)
|
|
18847
|
+
continue;
|
|
18848
|
+
if ((t.fields ?? []).some((f) => f.name === fieldName))
|
|
18849
|
+
return true;
|
|
18850
|
+
}
|
|
18851
|
+
}
|
|
18852
|
+
return false;
|
|
18853
|
+
}
|
|
18854
|
+
resolveFieldTaintKey(receiverType, fieldName, _callerIR) {
|
|
18855
|
+
const direct = `${receiverType}.${fieldName}`;
|
|
18856
|
+
if (this.fieldTaintInfo.has(direct))
|
|
18857
|
+
return direct;
|
|
18858
|
+
const suffix = `.${receiverType}.${fieldName}`;
|
|
18859
|
+
for (const key of this.fieldTaintInfo.keys()) {
|
|
18860
|
+
if (key === direct)
|
|
18861
|
+
return key;
|
|
18862
|
+
if (key.endsWith(suffix))
|
|
18863
|
+
return key;
|
|
18864
|
+
}
|
|
18865
|
+
return;
|
|
18866
|
+
}
|
|
18867
|
+
matchTaintedArg(arg, tainted) {
|
|
18868
|
+
if (tainted.size === 0)
|
|
18869
|
+
return null;
|
|
18870
|
+
if (arg.variable && tainted.has(arg.variable)) {
|
|
18871
|
+
return { var: arg.variable, origin: tainted.get(arg.variable) };
|
|
18872
|
+
}
|
|
18873
|
+
if (arg.expression) {
|
|
18874
|
+
for (const [tv, origin] of tainted) {
|
|
18875
|
+
const re = new RegExp(`\\b${tv}\\b`);
|
|
18876
|
+
if (re.test(arg.expression))
|
|
18877
|
+
return { var: tv, origin };
|
|
18878
|
+
}
|
|
18879
|
+
}
|
|
18880
|
+
return null;
|
|
18881
|
+
}
|
|
18882
|
+
buildMethodIndex() {
|
|
18883
|
+
const idx = new Map;
|
|
18884
|
+
for (const [, ir] of this.fileIRs) {
|
|
18885
|
+
const pkg = ir.meta.package || "";
|
|
18886
|
+
for (const type of ir.types) {
|
|
18887
|
+
const typeFqn = pkg ? `${pkg}.${type.name}` : type.name;
|
|
18888
|
+
for (const method of type.methods) {
|
|
18889
|
+
idx.set(`${typeFqn}.${method.name}`, { ir, method });
|
|
18890
|
+
}
|
|
18891
|
+
}
|
|
18892
|
+
}
|
|
18893
|
+
return idx;
|
|
18894
|
+
}
|
|
18895
|
+
getLocalDefVarAt(ir, line) {
|
|
18896
|
+
for (const def of ir.dfg.defs) {
|
|
18897
|
+
if (def.line === line && def.kind === "local" && def.variable)
|
|
18898
|
+
return def.variable;
|
|
18899
|
+
}
|
|
18900
|
+
return;
|
|
18901
|
+
}
|
|
18902
|
+
findRealSourceLineInMethod(ir, method) {
|
|
18903
|
+
for (const src of ir.taint.sources) {
|
|
18904
|
+
if (src.type === "interprocedural_param")
|
|
18905
|
+
continue;
|
|
18906
|
+
if (src.line >= method.start_line && src.line <= method.end_line) {
|
|
18907
|
+
return src.line;
|
|
18908
|
+
}
|
|
18909
|
+
}
|
|
18910
|
+
return;
|
|
18911
|
+
}
|
|
18356
18912
|
getMethodTaintInfo(methodFqn) {
|
|
18357
18913
|
return this.methodTaintInfo.get(methodFqn);
|
|
18358
18914
|
}
|
|
@@ -18392,8 +18948,12 @@ class CrossFileResolver {
|
|
|
18392
18948
|
clear() {
|
|
18393
18949
|
this.fileIRs.clear();
|
|
18394
18950
|
this.methodTaintInfo.clear();
|
|
18951
|
+
this.fieldTaintInfo.clear();
|
|
18395
18952
|
this.resolvedCalls.clear();
|
|
18396
18953
|
}
|
|
18954
|
+
getFieldTaintInfo(typeFqn, fieldName) {
|
|
18955
|
+
return this.fieldTaintInfo.get(`${typeFqn}.${fieldName}`);
|
|
18956
|
+
}
|
|
18397
18957
|
}
|
|
18398
18958
|
// ../circle-ir/dist/graph/project-graph.js
|
|
18399
18959
|
class ProjectGraph {
|
|
@@ -18535,12 +19095,59 @@ class CrossFilePass {
|
|
|
18535
19095
|
confidence: 0.7
|
|
18536
19096
|
}];
|
|
18537
19097
|
});
|
|
19098
|
+
const ipPaths = [
|
|
19099
|
+
...resolver.findInterproceduralTaintPaths(),
|
|
19100
|
+
...resolver.findFieldBindingTaintPaths()
|
|
19101
|
+
];
|
|
19102
|
+
for (let i2 = 0;i2 < ipPaths.length; i2++) {
|
|
19103
|
+
const p = ipPaths[i2];
|
|
19104
|
+
const sinkIR = projectGraph.getIR(p.sink.file);
|
|
19105
|
+
if (!sinkIR)
|
|
19106
|
+
continue;
|
|
19107
|
+
const matchedSink = sinkIR.taint.sinks.find((s) => s.line === p.sink.line);
|
|
19108
|
+
if (!matchedSink)
|
|
19109
|
+
continue;
|
|
19110
|
+
const srcLines = sourceLines.get(p.source.file) ?? [];
|
|
19111
|
+
const tgtLines = sourceLines.get(p.sink.file) ?? [];
|
|
19112
|
+
const dupId = `${p.source.file}:${p.source.line}→${p.sink.file}:${p.sink.line}`;
|
|
19113
|
+
if (taintPaths.some((tp) => tp.source.file === p.source.file && tp.source.line === p.source.line && tp.sink.file === p.sink.file && tp.sink.line === p.sink.line)) {
|
|
19114
|
+
continue;
|
|
19115
|
+
}
|
|
19116
|
+
taintPaths.push({
|
|
19117
|
+
id: `cf-ip-${i2}-${dupId}`,
|
|
19118
|
+
source: {
|
|
19119
|
+
file: p.source.file,
|
|
19120
|
+
line: p.source.line,
|
|
19121
|
+
type: p.source.type,
|
|
19122
|
+
code: srcLines[p.source.line - 1] ?? ""
|
|
19123
|
+
},
|
|
19124
|
+
sink: {
|
|
19125
|
+
file: p.sink.file,
|
|
19126
|
+
line: p.sink.line,
|
|
19127
|
+
type: matchedSink.type,
|
|
19128
|
+
cwe: matchedSink.cwe,
|
|
19129
|
+
code: tgtLines[p.sink.line - 1] ?? ""
|
|
19130
|
+
},
|
|
19131
|
+
hops: p.hops.map((h) => ({
|
|
19132
|
+
file: h.file,
|
|
19133
|
+
method: h.method,
|
|
19134
|
+
line: h.line,
|
|
19135
|
+
code: (sourceLines.get(h.file) ?? [])[h.line - 1] ?? "",
|
|
19136
|
+
variable: ""
|
|
19137
|
+
})),
|
|
19138
|
+
sanitizers_in_path: [],
|
|
19139
|
+
path_exists: true,
|
|
19140
|
+
confidence: p.confidence
|
|
19141
|
+
});
|
|
19142
|
+
}
|
|
18538
19143
|
const crossFileCalls = [];
|
|
18539
19144
|
for (const filePath of projectGraph.filePaths) {
|
|
18540
19145
|
const resolved = resolver.getResolvedCallsFromFile(filePath);
|
|
18541
19146
|
for (const rc of resolved) {
|
|
18542
19147
|
if (rc.sourceFile === rc.targetFile)
|
|
18543
19148
|
continue;
|
|
19149
|
+
const calleeInfo = resolver.getMethodTaintInfo(rc.targetMethod);
|
|
19150
|
+
const taintedParamSet = new Set(calleeInfo?.taintedParams ?? []);
|
|
18544
19151
|
crossFileCalls.push({
|
|
18545
19152
|
id: `${rc.sourceFile}:${rc.call.location.line}:${rc.targetMethod}`,
|
|
18546
19153
|
from: {
|
|
@@ -18556,7 +19163,7 @@ class CrossFilePass {
|
|
|
18556
19163
|
args_mapping: (rc.call.arguments ?? []).map((_, i2) => ({
|
|
18557
19164
|
caller_arg: i2,
|
|
18558
19165
|
callee_param: i2,
|
|
18559
|
-
taint_propagates:
|
|
19166
|
+
taint_propagates: taintedParamSet.has(i2)
|
|
18560
19167
|
})),
|
|
18561
19168
|
resolved: rc.resolution === "exact"
|
|
18562
19169
|
});
|
|
@@ -26983,7 +27590,7 @@ var colors = {
|
|
|
26983
27590
|
};
|
|
26984
27591
|
|
|
26985
27592
|
// src/version.ts
|
|
26986
|
-
var version = "3.
|
|
27593
|
+
var version = "3.39.0";
|
|
26987
27594
|
|
|
26988
27595
|
// src/formatters.ts
|
|
26989
27596
|
var SINK_SEVERITY = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cognium-dev",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.39.0",
|
|
4
4
|
"description": "Static Application Security Testing CLI for detecting security vulnerabilities via taint tracking",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"registry": "https://registry.npmjs.org/"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"circle-ir": "^3.
|
|
68
|
+
"circle-ir": "^3.39.0"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@types/node": "^25.5.0",
|