cognium-dev 3.37.0 → 3.38.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 +239 -5
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -18234,6 +18234,8 @@ class CrossFileResolver {
|
|
|
18234
18234
|
}
|
|
18235
18235
|
}
|
|
18236
18236
|
for (const source of sources) {
|
|
18237
|
+
if (source.type === "interprocedural_param")
|
|
18238
|
+
continue;
|
|
18237
18239
|
if (source.line >= method.start_line && source.line <= method.end_line) {
|
|
18238
18240
|
return true;
|
|
18239
18241
|
}
|
|
@@ -18242,6 +18244,8 @@ class CrossFileResolver {
|
|
|
18242
18244
|
}
|
|
18243
18245
|
getSourceType(method, sources) {
|
|
18244
18246
|
for (const source of sources) {
|
|
18247
|
+
if (source.type === "interprocedural_param")
|
|
18248
|
+
continue;
|
|
18245
18249
|
if (source.line >= method.start_line && source.line <= method.end_line) {
|
|
18246
18250
|
return source.type;
|
|
18247
18251
|
}
|
|
@@ -18249,15 +18253,45 @@ class CrossFileResolver {
|
|
|
18249
18253
|
return;
|
|
18250
18254
|
}
|
|
18251
18255
|
findTaintedParams(method, ir) {
|
|
18252
|
-
const taintedParams =
|
|
18256
|
+
const taintedParams = new Set;
|
|
18253
18257
|
const numParams = method.parameters.length;
|
|
18254
18258
|
for (let i2 = 0;i2 < numParams; i2++) {
|
|
18255
18259
|
const param = method.parameters[i2];
|
|
18256
18260
|
if (param.annotations.some((a) => ["RequestParam", "RequestBody", "PathVariable"].includes(a))) {
|
|
18257
|
-
taintedParams.
|
|
18261
|
+
taintedParams.add(i2);
|
|
18258
18262
|
}
|
|
18259
18263
|
}
|
|
18260
|
-
|
|
18264
|
+
const paramNameToIndex = new Map;
|
|
18265
|
+
for (let i2 = 0;i2 < numParams; i2++) {
|
|
18266
|
+
const name2 = method.parameters[i2].name;
|
|
18267
|
+
if (name2)
|
|
18268
|
+
paramNameToIndex.set(name2, i2);
|
|
18269
|
+
}
|
|
18270
|
+
for (const sink of ir.taint.sinks) {
|
|
18271
|
+
if (sink.line < method.start_line || sink.line > method.end_line)
|
|
18272
|
+
continue;
|
|
18273
|
+
const callsAtSink = ir.calls.filter((c) => c.location.line === sink.line);
|
|
18274
|
+
for (const call of callsAtSink) {
|
|
18275
|
+
for (const arg of call.arguments) {
|
|
18276
|
+
const candidates = [];
|
|
18277
|
+
if (arg.variable)
|
|
18278
|
+
candidates.push(arg.variable);
|
|
18279
|
+
if (arg.expression) {
|
|
18280
|
+
for (const [name2] of paramNameToIndex) {
|
|
18281
|
+
const re = new RegExp(`\\b${name2}\\b`);
|
|
18282
|
+
if (re.test(arg.expression))
|
|
18283
|
+
candidates.push(name2);
|
|
18284
|
+
}
|
|
18285
|
+
}
|
|
18286
|
+
for (const cand of candidates) {
|
|
18287
|
+
const idx = paramNameToIndex.get(cand);
|
|
18288
|
+
if (idx !== undefined)
|
|
18289
|
+
taintedParams.add(idx);
|
|
18290
|
+
}
|
|
18291
|
+
}
|
|
18292
|
+
}
|
|
18293
|
+
}
|
|
18294
|
+
return [...taintedParams].sort((a, b) => a - b);
|
|
18261
18295
|
}
|
|
18262
18296
|
isSanitizerMethod(methodName) {
|
|
18263
18297
|
const sanitizerPatterns = [
|
|
@@ -18309,12 +18343,24 @@ class CrossFileResolver {
|
|
|
18309
18343
|
for (const source of ir.taint.sources) {
|
|
18310
18344
|
if (source.type === "interprocedural_param")
|
|
18311
18345
|
continue;
|
|
18346
|
+
const sourceVar = source.variable ?? this.getLocalDefVarAt(ir, source.line);
|
|
18312
18347
|
for (const call of ir.calls) {
|
|
18313
18348
|
if (call.location.line < source.line)
|
|
18314
18349
|
continue;
|
|
18315
18350
|
const resolved = this.resolveCall(call, filePath);
|
|
18316
18351
|
if (!resolved || resolved.targetFile === filePath)
|
|
18317
18352
|
continue;
|
|
18353
|
+
if (sourceVar) {
|
|
18354
|
+
const argMentions = call.arguments.some((arg) => {
|
|
18355
|
+
if (arg.variable === sourceVar)
|
|
18356
|
+
return true;
|
|
18357
|
+
if (arg.expression && new RegExp(`\\b${sourceVar}\\b`).test(arg.expression))
|
|
18358
|
+
return true;
|
|
18359
|
+
return false;
|
|
18360
|
+
});
|
|
18361
|
+
if (!argMentions)
|
|
18362
|
+
continue;
|
|
18363
|
+
}
|
|
18318
18364
|
const targetIR = this.fileIRs.get(resolved.targetFile);
|
|
18319
18365
|
if (!targetIR || targetIR.taint.sinks.length === 0)
|
|
18320
18366
|
continue;
|
|
@@ -18353,6 +18399,150 @@ class CrossFileResolver {
|
|
|
18353
18399
|
}
|
|
18354
18400
|
return flows;
|
|
18355
18401
|
}
|
|
18402
|
+
findInterproceduralTaintPaths() {
|
|
18403
|
+
const paths = [];
|
|
18404
|
+
const seen = new Set;
|
|
18405
|
+
const methodIndex = this.buildMethodIndex();
|
|
18406
|
+
for (const [callerFile, callerIR] of this.fileIRs) {
|
|
18407
|
+
for (const type of callerIR.types) {
|
|
18408
|
+
for (const method of type.methods) {
|
|
18409
|
+
const tainted = new Map;
|
|
18410
|
+
for (const src of callerIR.taint.sources) {
|
|
18411
|
+
if (src.type === "interprocedural_param")
|
|
18412
|
+
continue;
|
|
18413
|
+
if (src.line < method.start_line || src.line > method.end_line)
|
|
18414
|
+
continue;
|
|
18415
|
+
if (!src.variable)
|
|
18416
|
+
continue;
|
|
18417
|
+
tainted.set(src.variable, {
|
|
18418
|
+
file: callerFile,
|
|
18419
|
+
line: src.line,
|
|
18420
|
+
type: src.type,
|
|
18421
|
+
hopChain: [{ file: callerFile, line: src.line, method: method.name, kind: "source" }]
|
|
18422
|
+
});
|
|
18423
|
+
}
|
|
18424
|
+
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);
|
|
18425
|
+
for (const call of callsInMethod) {
|
|
18426
|
+
const resolved = this.resolveCall(call, callerFile);
|
|
18427
|
+
if (!resolved)
|
|
18428
|
+
continue;
|
|
18429
|
+
const callee = this.methodTaintInfo.get(resolved.targetMethod);
|
|
18430
|
+
if (!callee)
|
|
18431
|
+
continue;
|
|
18432
|
+
if (callee.returnsSource && !callee.sanitizes && callee.sourceType) {
|
|
18433
|
+
const calleeNode = methodIndex.get(resolved.targetMethod);
|
|
18434
|
+
const calleeSourceLine = calleeNode ? this.findRealSourceLineInMethod(calleeNode.ir, calleeNode.method) : undefined;
|
|
18435
|
+
const sourceLine = calleeSourceLine ?? call.location.line;
|
|
18436
|
+
const sourceFile = callee.file;
|
|
18437
|
+
const sourceType = callee.sourceType;
|
|
18438
|
+
const defsAtLine = callerIR.dfg.defs.filter((d) => d.line === call.location.line && d.kind === "local");
|
|
18439
|
+
for (const def of defsAtLine) {
|
|
18440
|
+
if (!def.variable)
|
|
18441
|
+
continue;
|
|
18442
|
+
const baseChain = [
|
|
18443
|
+
{ file: sourceFile, line: sourceLine, method: resolved.targetMethod, kind: "source" },
|
|
18444
|
+
{ file: callerFile, line: call.location.line, method: method.name, kind: "wrapper_return" }
|
|
18445
|
+
];
|
|
18446
|
+
tainted.set(def.variable, {
|
|
18447
|
+
file: sourceFile,
|
|
18448
|
+
line: sourceLine,
|
|
18449
|
+
type: sourceType,
|
|
18450
|
+
hopChain: baseChain
|
|
18451
|
+
});
|
|
18452
|
+
}
|
|
18453
|
+
}
|
|
18454
|
+
if (callee.taintedParams.length === 0 || callee.sanitizes)
|
|
18455
|
+
continue;
|
|
18456
|
+
for (let argIdx = 0;argIdx < call.arguments.length; argIdx++) {
|
|
18457
|
+
if (!callee.taintedParams.includes(argIdx))
|
|
18458
|
+
continue;
|
|
18459
|
+
const arg = call.arguments[argIdx];
|
|
18460
|
+
const matched = this.matchTaintedArg(arg, tainted);
|
|
18461
|
+
if (!matched)
|
|
18462
|
+
continue;
|
|
18463
|
+
const calleeNode = methodIndex.get(resolved.targetMethod);
|
|
18464
|
+
if (!calleeNode)
|
|
18465
|
+
continue;
|
|
18466
|
+
const sinksInCallee = calleeNode.ir.taint.sinks.filter((s) => s.line >= calleeNode.method.start_line && s.line <= calleeNode.method.end_line);
|
|
18467
|
+
for (const sink of sinksInCallee) {
|
|
18468
|
+
const key = `${matched.origin.file}:${matched.origin.line}→${callee.file}:${sink.line}`;
|
|
18469
|
+
if (seen.has(key))
|
|
18470
|
+
continue;
|
|
18471
|
+
seen.add(key);
|
|
18472
|
+
const hops = [
|
|
18473
|
+
...matched.origin.hopChain,
|
|
18474
|
+
{ file: callerFile, line: call.location.line, method: method.name, kind: "sink_call" },
|
|
18475
|
+
{ file: callee.file, line: sink.line, method: resolved.targetMethod, kind: "sink" }
|
|
18476
|
+
];
|
|
18477
|
+
const decay = Math.max(0.3, Math.pow(0.85, Math.max(hops.length - 1, 0)));
|
|
18478
|
+
paths.push({
|
|
18479
|
+
source: {
|
|
18480
|
+
file: matched.origin.file,
|
|
18481
|
+
line: matched.origin.line,
|
|
18482
|
+
type: matched.origin.type
|
|
18483
|
+
},
|
|
18484
|
+
sink: {
|
|
18485
|
+
file: callee.file,
|
|
18486
|
+
line: sink.line,
|
|
18487
|
+
type: sink.type,
|
|
18488
|
+
cwe: sink.cwe
|
|
18489
|
+
},
|
|
18490
|
+
hops,
|
|
18491
|
+
confidence: decay
|
|
18492
|
+
});
|
|
18493
|
+
}
|
|
18494
|
+
}
|
|
18495
|
+
}
|
|
18496
|
+
}
|
|
18497
|
+
}
|
|
18498
|
+
}
|
|
18499
|
+
return paths;
|
|
18500
|
+
}
|
|
18501
|
+
matchTaintedArg(arg, tainted) {
|
|
18502
|
+
if (tainted.size === 0)
|
|
18503
|
+
return null;
|
|
18504
|
+
if (arg.variable && tainted.has(arg.variable)) {
|
|
18505
|
+
return { var: arg.variable, origin: tainted.get(arg.variable) };
|
|
18506
|
+
}
|
|
18507
|
+
if (arg.expression) {
|
|
18508
|
+
for (const [tv, origin] of tainted) {
|
|
18509
|
+
const re = new RegExp(`\\b${tv}\\b`);
|
|
18510
|
+
if (re.test(arg.expression))
|
|
18511
|
+
return { var: tv, origin };
|
|
18512
|
+
}
|
|
18513
|
+
}
|
|
18514
|
+
return null;
|
|
18515
|
+
}
|
|
18516
|
+
buildMethodIndex() {
|
|
18517
|
+
const idx = new Map;
|
|
18518
|
+
for (const [, ir] of this.fileIRs) {
|
|
18519
|
+
const pkg = ir.meta.package || "";
|
|
18520
|
+
for (const type of ir.types) {
|
|
18521
|
+
const typeFqn = pkg ? `${pkg}.${type.name}` : type.name;
|
|
18522
|
+
for (const method of type.methods) {
|
|
18523
|
+
idx.set(`${typeFqn}.${method.name}`, { ir, method });
|
|
18524
|
+
}
|
|
18525
|
+
}
|
|
18526
|
+
}
|
|
18527
|
+
return idx;
|
|
18528
|
+
}
|
|
18529
|
+
getLocalDefVarAt(ir, line) {
|
|
18530
|
+
for (const def of ir.dfg.defs) {
|
|
18531
|
+
if (def.line === line && def.kind === "local" && def.variable)
|
|
18532
|
+
return def.variable;
|
|
18533
|
+
}
|
|
18534
|
+
return;
|
|
18535
|
+
}
|
|
18536
|
+
findRealSourceLineInMethod(ir, method) {
|
|
18537
|
+
for (const src of ir.taint.sources) {
|
|
18538
|
+
if (src.type === "interprocedural_param")
|
|
18539
|
+
continue;
|
|
18540
|
+
if (src.line >= method.start_line && src.line <= method.end_line) {
|
|
18541
|
+
return src.line;
|
|
18542
|
+
}
|
|
18543
|
+
}
|
|
18544
|
+
return;
|
|
18545
|
+
}
|
|
18356
18546
|
getMethodTaintInfo(methodFqn) {
|
|
18357
18547
|
return this.methodTaintInfo.get(methodFqn);
|
|
18358
18548
|
}
|
|
@@ -18535,12 +18725,56 @@ class CrossFilePass {
|
|
|
18535
18725
|
confidence: 0.7
|
|
18536
18726
|
}];
|
|
18537
18727
|
});
|
|
18728
|
+
const ipPaths = resolver.findInterproceduralTaintPaths();
|
|
18729
|
+
for (let i2 = 0;i2 < ipPaths.length; i2++) {
|
|
18730
|
+
const p = ipPaths[i2];
|
|
18731
|
+
const sinkIR = projectGraph.getIR(p.sink.file);
|
|
18732
|
+
if (!sinkIR)
|
|
18733
|
+
continue;
|
|
18734
|
+
const matchedSink = sinkIR.taint.sinks.find((s) => s.line === p.sink.line);
|
|
18735
|
+
if (!matchedSink)
|
|
18736
|
+
continue;
|
|
18737
|
+
const srcLines = sourceLines.get(p.source.file) ?? [];
|
|
18738
|
+
const tgtLines = sourceLines.get(p.sink.file) ?? [];
|
|
18739
|
+
const dupId = `${p.source.file}:${p.source.line}→${p.sink.file}:${p.sink.line}`;
|
|
18740
|
+
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)) {
|
|
18741
|
+
continue;
|
|
18742
|
+
}
|
|
18743
|
+
taintPaths.push({
|
|
18744
|
+
id: `cf-ip-${i2}-${dupId}`,
|
|
18745
|
+
source: {
|
|
18746
|
+
file: p.source.file,
|
|
18747
|
+
line: p.source.line,
|
|
18748
|
+
type: p.source.type,
|
|
18749
|
+
code: srcLines[p.source.line - 1] ?? ""
|
|
18750
|
+
},
|
|
18751
|
+
sink: {
|
|
18752
|
+
file: p.sink.file,
|
|
18753
|
+
line: p.sink.line,
|
|
18754
|
+
type: matchedSink.type,
|
|
18755
|
+
cwe: matchedSink.cwe,
|
|
18756
|
+
code: tgtLines[p.sink.line - 1] ?? ""
|
|
18757
|
+
},
|
|
18758
|
+
hops: p.hops.map((h) => ({
|
|
18759
|
+
file: h.file,
|
|
18760
|
+
method: h.method,
|
|
18761
|
+
line: h.line,
|
|
18762
|
+
code: (sourceLines.get(h.file) ?? [])[h.line - 1] ?? "",
|
|
18763
|
+
variable: ""
|
|
18764
|
+
})),
|
|
18765
|
+
sanitizers_in_path: [],
|
|
18766
|
+
path_exists: true,
|
|
18767
|
+
confidence: p.confidence
|
|
18768
|
+
});
|
|
18769
|
+
}
|
|
18538
18770
|
const crossFileCalls = [];
|
|
18539
18771
|
for (const filePath of projectGraph.filePaths) {
|
|
18540
18772
|
const resolved = resolver.getResolvedCallsFromFile(filePath);
|
|
18541
18773
|
for (const rc of resolved) {
|
|
18542
18774
|
if (rc.sourceFile === rc.targetFile)
|
|
18543
18775
|
continue;
|
|
18776
|
+
const calleeInfo = resolver.getMethodTaintInfo(rc.targetMethod);
|
|
18777
|
+
const taintedParamSet = new Set(calleeInfo?.taintedParams ?? []);
|
|
18544
18778
|
crossFileCalls.push({
|
|
18545
18779
|
id: `${rc.sourceFile}:${rc.call.location.line}:${rc.targetMethod}`,
|
|
18546
18780
|
from: {
|
|
@@ -18556,7 +18790,7 @@ class CrossFilePass {
|
|
|
18556
18790
|
args_mapping: (rc.call.arguments ?? []).map((_, i2) => ({
|
|
18557
18791
|
caller_arg: i2,
|
|
18558
18792
|
callee_param: i2,
|
|
18559
|
-
taint_propagates:
|
|
18793
|
+
taint_propagates: taintedParamSet.has(i2)
|
|
18560
18794
|
})),
|
|
18561
18795
|
resolved: rc.resolution === "exact"
|
|
18562
18796
|
});
|
|
@@ -26983,7 +27217,7 @@ var colors = {
|
|
|
26983
27217
|
};
|
|
26984
27218
|
|
|
26985
27219
|
// src/version.ts
|
|
26986
|
-
var version = "3.
|
|
27220
|
+
var version = "3.38.0";
|
|
26987
27221
|
|
|
26988
27222
|
// src/formatters.ts
|
|
26989
27223
|
var SINK_SEVERITY = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cognium-dev",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.38.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.38.0"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@types/node": "^25.5.0",
|