cognium-dev 3.36.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 +270 -7
- 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);
|
|
18262
|
+
}
|
|
18263
|
+
}
|
|
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
|
+
}
|
|
18258
18292
|
}
|
|
18259
18293
|
}
|
|
18260
|
-
return taintedParams;
|
|
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
|
});
|
|
@@ -19451,6 +19685,15 @@ function buildPythonTaintedVars(sourceCode) {
|
|
|
19451
19685
|
containerTainted.set(`${obj}['${section}']['${key}']`, i2 + 1);
|
|
19452
19686
|
continue;
|
|
19453
19687
|
}
|
|
19688
|
+
const containerAppendMatch = line.match(/^\s*(\w+)\.(append|extend|insert|add|push|put|appendleft)\s*\(\s*(.+?)\s*\)\s*$/);
|
|
19689
|
+
if (containerAppendMatch) {
|
|
19690
|
+
const [, receiver, , argExpr] = containerAppendMatch;
|
|
19691
|
+
const argIsTainted = [...tainted.keys()].some((v) => new RegExp(`\\b${v}\\b`).test(argExpr));
|
|
19692
|
+
const argIsDirectSource = PYTHON_TAINTED_PATTERNS2.some((p) => p.pattern.test(argExpr));
|
|
19693
|
+
if (argIsTainted || argIsDirectSource)
|
|
19694
|
+
tainted.set(receiver, tainted.get(receiver) ?? i2 + 1);
|
|
19695
|
+
continue;
|
|
19696
|
+
}
|
|
19454
19697
|
const augAssign = line.match(/^\s*(\w+)\s*\+=\s*(.+)$/);
|
|
19455
19698
|
if (augAssign) {
|
|
19456
19699
|
const [, augLhs, augRhs] = augAssign;
|
|
@@ -20513,7 +20756,7 @@ class TaintPropagationPass {
|
|
|
20513
20756
|
flows.push(f);
|
|
20514
20757
|
}
|
|
20515
20758
|
}
|
|
20516
|
-
const exprScanFlows = detectExpressionScanFlows(calls, sources, sinks, constProp.unreachableLines) ?? [];
|
|
20759
|
+
const exprScanFlows = detectExpressionScanFlows(calls, sources, sinks, constProp.unreachableLines, ctx.code, ctx.language) ?? [];
|
|
20517
20760
|
for (const f of exprScanFlows) {
|
|
20518
20761
|
if (flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type))
|
|
20519
20762
|
continue;
|
|
@@ -20731,11 +20974,31 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
20731
20974
|
}
|
|
20732
20975
|
return flows;
|
|
20733
20976
|
}
|
|
20734
|
-
function detectExpressionScanFlows(calls, sources, sinks, unreachableLines) {
|
|
20977
|
+
function detectExpressionScanFlows(calls, sources, sinks, unreachableLines, code, language) {
|
|
20735
20978
|
const flows = [];
|
|
20736
20979
|
const sourcesWithVar = sources.filter((s) => typeof s.variable === "string" && s.variable.length > 0);
|
|
20737
20980
|
if (sourcesWithVar.length === 0)
|
|
20738
20981
|
return flows;
|
|
20982
|
+
if (language === "python" && typeof code === "string") {
|
|
20983
|
+
const derived = buildPythonTaintedVars(code);
|
|
20984
|
+
if (derived.size > 0) {
|
|
20985
|
+
let anchor = sourcesWithVar[0];
|
|
20986
|
+
for (const s of sourcesWithVar) {
|
|
20987
|
+
if (s.line < anchor.line)
|
|
20988
|
+
anchor = s;
|
|
20989
|
+
}
|
|
20990
|
+
const existingVars = new Set(sourcesWithVar.map((s) => s.variable));
|
|
20991
|
+
for (const [varName] of derived) {
|
|
20992
|
+
if (!varName || existingVars.has(varName))
|
|
20993
|
+
continue;
|
|
20994
|
+
sourcesWithVar.push({
|
|
20995
|
+
...anchor,
|
|
20996
|
+
variable: varName
|
|
20997
|
+
});
|
|
20998
|
+
existingVars.add(varName);
|
|
20999
|
+
}
|
|
21000
|
+
}
|
|
21001
|
+
}
|
|
20739
21002
|
const reCache = new Map;
|
|
20740
21003
|
for (const s of sourcesWithVar) {
|
|
20741
21004
|
if (reCache.has(s.variable))
|
|
@@ -26954,7 +27217,7 @@ var colors = {
|
|
|
26954
27217
|
};
|
|
26955
27218
|
|
|
26956
27219
|
// src/version.ts
|
|
26957
|
-
var version = "3.
|
|
27220
|
+
var version = "3.38.0";
|
|
26958
27221
|
|
|
26959
27222
|
// src/formatters.ts
|
|
26960
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",
|