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