cognium-dev 3.38.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 +375 -2
  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) {
@@ -18493,11 +18602,268 @@ class CrossFileResolver {
18493
18602
  }
18494
18603
  }
18495
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
+ }
18496
18642
  }
18497
18643
  }
18498
18644
  }
18499
18645
  return paths;
18500
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
+ }
18501
18867
  matchTaintedArg(arg, tainted) {
18502
18868
  if (tainted.size === 0)
18503
18869
  return null;
@@ -18582,8 +18948,12 @@ class CrossFileResolver {
18582
18948
  clear() {
18583
18949
  this.fileIRs.clear();
18584
18950
  this.methodTaintInfo.clear();
18951
+ this.fieldTaintInfo.clear();
18585
18952
  this.resolvedCalls.clear();
18586
18953
  }
18954
+ getFieldTaintInfo(typeFqn, fieldName) {
18955
+ return this.fieldTaintInfo.get(`${typeFqn}.${fieldName}`);
18956
+ }
18587
18957
  }
18588
18958
  // ../circle-ir/dist/graph/project-graph.js
18589
18959
  class ProjectGraph {
@@ -18725,7 +19095,10 @@ class CrossFilePass {
18725
19095
  confidence: 0.7
18726
19096
  }];
18727
19097
  });
18728
- const ipPaths = resolver.findInterproceduralTaintPaths();
19098
+ const ipPaths = [
19099
+ ...resolver.findInterproceduralTaintPaths(),
19100
+ ...resolver.findFieldBindingTaintPaths()
19101
+ ];
18729
19102
  for (let i2 = 0;i2 < ipPaths.length; i2++) {
18730
19103
  const p = ipPaths[i2];
18731
19104
  const sinkIR = projectGraph.getIR(p.sink.file);
@@ -27217,7 +27590,7 @@ var colors = {
27217
27590
  };
27218
27591
 
27219
27592
  // src/version.ts
27220
- var version = "3.38.0";
27593
+ var version = "3.39.0";
27221
27594
 
27222
27595
  // src/formatters.ts
27223
27596
  var SINK_SEVERITY = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cognium-dev",
3
- "version": "3.38.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.38.0"
68
+ "circle-ir": "^3.39.0"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/node": "^25.5.0",