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.
Files changed (2) hide show
  1. package/dist/cli.js +612 -5
  2. 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.push(i2);
18370
+ taintedParams.add(i2);
18258
18371
  }
18259
18372
  }
18260
- return taintedParams;
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: false
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.37.0";
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.37.0",
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.37.0"
68
+ "circle-ir": "^3.39.0"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/node": "^25.5.0",