cognium-dev 3.86.0 → 3.89.2

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 +456 -46
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -18481,11 +18481,14 @@ var LOG_LEVELS = {
18481
18481
  fatal: 5,
18482
18482
  silent: 6
18483
18483
  };
18484
- var currentLevel = "info";
18484
+ var currentLevel = "silent";
18485
18485
  var customLogger = null;
18486
18486
  function shouldLog(level) {
18487
18487
  return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
18488
18488
  }
18489
+ function setLogLevel(level) {
18490
+ currentLevel = level;
18491
+ }
18489
18492
  var logger = {
18490
18493
  trace: (msg, obj) => {
18491
18494
  if (customLogger) {
@@ -18509,7 +18512,7 @@ var logger = {
18509
18512
  return;
18510
18513
  }
18511
18514
  if (shouldLog("info"))
18512
- console.log(obj ? `[INFO] ${msg} ${JSON.stringify(obj)}` : `[INFO] ${msg}`);
18515
+ console.error(obj ? `[INFO] ${msg} ${JSON.stringify(obj)}` : `[INFO] ${msg}`);
18513
18516
  },
18514
18517
  warn: (msg, obj) => {
18515
18518
  if (customLogger) {
@@ -19377,7 +19380,88 @@ class SymbolTable {
19377
19380
  }
19378
19381
  }
19379
19382
  // ../circle-ir/dist/resolution/cross-file.js
19383
+ function buildFileIndex(ir) {
19384
+ const callsByLine = new Map;
19385
+ for (const c of ir.calls) {
19386
+ const ln = c.location.line;
19387
+ let arr = callsByLine.get(ln);
19388
+ if (!arr) {
19389
+ arr = [];
19390
+ callsByLine.set(ln, arr);
19391
+ }
19392
+ arr.push(c);
19393
+ }
19394
+ const defsByLine = new Map;
19395
+ for (const d of ir.dfg.defs) {
19396
+ let arr = defsByLine.get(d.line);
19397
+ if (!arr) {
19398
+ arr = [];
19399
+ defsByLine.set(d.line, arr);
19400
+ }
19401
+ arr.push(d);
19402
+ }
19403
+ const usesByLine = new Map;
19404
+ for (const u of ir.dfg.uses) {
19405
+ let arr = usesByLine.get(u.line);
19406
+ if (!arr) {
19407
+ arr = [];
19408
+ usesByLine.set(u.line, arr);
19409
+ }
19410
+ arr.push(u);
19411
+ }
19412
+ const callsSorted = [...ir.calls].sort((a, b) => a.location.line - b.location.line);
19413
+ const sinksSorted = [...ir.taint.sinks].sort((a, b) => a.line - b.line);
19414
+ const defsSorted = [...ir.dfg.defs].sort((a, b) => a.line - b.line);
19415
+ const callsByMethod = new Map;
19416
+ const sinksByMethod = new Map;
19417
+ const defsByMethod = new Map;
19418
+ for (const type of ir.types) {
19419
+ for (const method of type.methods) {
19420
+ const start2 = method.start_line;
19421
+ const end = method.end_line;
19422
+ const inCalls = [];
19423
+ for (const c of callsSorted) {
19424
+ const ln = c.location.line;
19425
+ if (ln < start2)
19426
+ continue;
19427
+ if (ln > end)
19428
+ break;
19429
+ inCalls.push(c);
19430
+ }
19431
+ callsByMethod.set(method, inCalls);
19432
+ const inSinks = [];
19433
+ for (const s of sinksSorted) {
19434
+ if (s.line < start2)
19435
+ continue;
19436
+ if (s.line > end)
19437
+ break;
19438
+ inSinks.push(s);
19439
+ }
19440
+ sinksByMethod.set(method, inSinks);
19441
+ const inDefs = [];
19442
+ for (const d of defsSorted) {
19443
+ if (d.line < start2)
19444
+ continue;
19445
+ if (d.line > end)
19446
+ break;
19447
+ inDefs.push(d);
19448
+ }
19449
+ defsByMethod.set(method, inDefs);
19450
+ }
19451
+ }
19452
+ return { callsByLine, defsByLine, usesByLine, callsByMethod, sinksByMethod, defsByMethod };
19453
+ }
19454
+
19380
19455
  class CrossFileResolver {
19456
+ fileIndexes = new WeakMap;
19457
+ getFileIndex(ir) {
19458
+ let idx = this.fileIndexes.get(ir);
19459
+ if (idx)
19460
+ return idx;
19461
+ idx = buildFileIndex(ir);
19462
+ this.fileIndexes.set(ir, idx);
19463
+ return idx;
19464
+ }
19381
19465
  symbolTable;
19382
19466
  typeHierarchy;
19383
19467
  fileIRs = new Map;
@@ -19832,7 +19916,8 @@ class CrossFileResolver {
19832
19916
  }
19833
19917
  if (!targetMethod)
19834
19918
  continue;
19835
- const sinksInMethod = targetIR.taint.sinks.filter((s) => s.line >= targetMethod.start_line && s.line <= targetMethod.end_line);
19919
+ const targetIdx = this.getFileIndex(targetIR);
19920
+ const sinksInMethod = targetIdx.sinksByMethod.get(targetMethod) ?? [];
19836
19921
  if (sinksInMethod.length === 0)
19837
19922
  continue;
19838
19923
  for (const sink of sinksInMethod) {
@@ -19861,6 +19946,7 @@ class CrossFileResolver {
19861
19946
  const seen = new Set;
19862
19947
  const methodIndex = this.buildMethodIndex();
19863
19948
  for (const [callerFile, callerIR] of this.fileIRs) {
19949
+ const callerIdx = this.getFileIndex(callerIR);
19864
19950
  for (const type of callerIR.types) {
19865
19951
  for (const method of type.methods) {
19866
19952
  const tainted = new Map;
@@ -19878,7 +19964,7 @@ class CrossFileResolver {
19878
19964
  hopChain: [{ file: callerFile, line: src.line, method: method.name, kind: "source" }]
19879
19965
  });
19880
19966
  }
19881
- 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);
19967
+ const callsInMethod = callerIdx.callsByMethod.get(method) ?? [];
19882
19968
  for (const call of callsInMethod) {
19883
19969
  const resolved = this.resolveCall(call, callerFile);
19884
19970
  if (!resolved)
@@ -19892,7 +19978,7 @@ class CrossFileResolver {
19892
19978
  const sourceLine = calleeSourceLine ?? call.location.line;
19893
19979
  const sourceFile = callee.file;
19894
19980
  const sourceType = callee.sourceType;
19895
- const defsAtLine = callerIR.dfg.defs.filter((d) => d.line === call.location.line && d.kind === "local");
19981
+ const defsAtLine = (callerIdx.defsByLine.get(call.location.line) ?? []).filter((d) => d.kind === "local");
19896
19982
  for (const def of defsAtLine) {
19897
19983
  if (!def.variable)
19898
19984
  continue;
@@ -19920,7 +20006,8 @@ class CrossFileResolver {
19920
20006
  const calleeNode = methodIndex.get(resolved.targetMethod);
19921
20007
  if (!calleeNode)
19922
20008
  continue;
19923
- const sinksInCallee = calleeNode.ir.taint.sinks.filter((s) => s.line >= calleeNode.method.start_line && s.line <= calleeNode.method.end_line);
20009
+ const calleeIdx = this.getFileIndex(calleeNode.ir);
20010
+ const sinksInCallee = calleeIdx.sinksByMethod.get(calleeNode.method) ?? [];
19924
20011
  for (const sink of sinksInCallee) {
19925
20012
  const key = `${matched.origin.file}:${matched.origin.line}→${callee.file}:${sink.line}`;
19926
20013
  if (seen.has(key))
@@ -19951,9 +20038,9 @@ class CrossFileResolver {
19951
20038
  }
19952
20039
  }
19953
20040
  if (tainted.size > 0) {
19954
- const sinksInCaller = callerIR.taint.sinks.filter((s) => s.line >= method.start_line && s.line <= method.end_line);
20041
+ const sinksInCaller = callerIdx.sinksByMethod.get(method) ?? [];
19955
20042
  for (const sink of sinksInCaller) {
19956
- const callsAtSink = callerIR.calls.filter((c) => c.location.line === sink.line);
20043
+ const callsAtSink = callerIdx.callsByLine.get(sink.line) ?? [];
19957
20044
  for (const sinkCall of callsAtSink) {
19958
20045
  for (const arg of sinkCall.arguments ?? []) {
19959
20046
  const matched = this.matchTaintedArg(arg, tainted);
@@ -20000,6 +20087,7 @@ class CrossFileResolver {
20000
20087
  const fieldExprRe = /^(\w+)\.(\w+)$/;
20001
20088
  const methodIndex = this.buildMethodIndex();
20002
20089
  for (const [callerFile, callerIR] of this.fileIRs) {
20090
+ const callerIdx = this.getFileIndex(callerIR);
20003
20091
  for (const type of callerIR.types) {
20004
20092
  const callerTypeFqn = callerIR.meta.package ? `${callerIR.meta.package}.${type.name}` : type.name;
20005
20093
  for (const method of type.methods) {
@@ -20018,9 +20106,9 @@ class CrossFileResolver {
20018
20106
  hopChain: [{ file: callerFile, line: src.line, method: method.name, kind: "source" }]
20019
20107
  });
20020
20108
  }
20021
- const defsInMethod = callerIR.dfg.defs.filter((d) => d.kind === "local" && d.line >= method.start_line && d.line <= method.end_line && !!d.variable);
20109
+ const defsInMethod = (callerIdx.defsByMethod.get(method) ?? []).filter((d) => d.kind === "local" && !!d.variable);
20022
20110
  for (const def of defsInMethod) {
20023
- const usesAtLine = callerIR.dfg.uses.filter((u) => u.line === def.line);
20111
+ const usesAtLine = callerIdx.usesByLine.get(def.line) ?? [];
20024
20112
  if (usesAtLine.length < 2)
20025
20113
  continue;
20026
20114
  let receiver = null;
@@ -20101,9 +20189,9 @@ class CrossFileResolver {
20101
20189
  }
20102
20190
  if (tainted.size === 0)
20103
20191
  continue;
20104
- const sinksInCaller = callerIR.taint.sinks.filter((s) => s.line >= method.start_line && s.line <= method.end_line);
20192
+ const sinksInCaller = callerIdx.sinksByMethod.get(method) ?? [];
20105
20193
  for (const sink of sinksInCaller) {
20106
- const callsAtSink = callerIR.calls.filter((c) => c.location.line === sink.line);
20194
+ const callsAtSink = callerIdx.callsByLine.get(sink.line) ?? [];
20107
20195
  for (const sinkCall of callsAtSink) {
20108
20196
  for (const arg of sinkCall.arguments ?? []) {
20109
20197
  const matched = this.matchTaintedArg(arg, tainted);
@@ -20136,7 +20224,7 @@ class CrossFileResolver {
20136
20224
  }
20137
20225
  }
20138
20226
  }
20139
- 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);
20227
+ const callsInMethod = callerIdx.callsByMethod.get(method) ?? [];
20140
20228
  for (const call of callsInMethod) {
20141
20229
  const resolved = this.resolveCall(call, callerFile);
20142
20230
  if (!resolved)
@@ -20153,7 +20241,8 @@ class CrossFileResolver {
20153
20241
  const calleeNode = methodIndex.get(resolved.targetMethod);
20154
20242
  if (!calleeNode)
20155
20243
  continue;
20156
- const sinksInCallee = calleeNode.ir.taint.sinks.filter((s) => s.line >= calleeNode.method.start_line && s.line <= calleeNode.method.end_line);
20244
+ const calleeIdx = this.getFileIndex(calleeNode.ir);
20245
+ const sinksInCallee = calleeIdx.sinksByMethod.get(calleeNode.method) ?? [];
20157
20246
  for (const sink of sinksInCallee) {
20158
20247
  const key = `fb:${matched.origin.file}:${matched.origin.line}→${callee.file}:${sink.line}`;
20159
20248
  if (seen.has(key))
@@ -20395,9 +20484,20 @@ class AnalysisPipeline {
20395
20484
  }
20396
20485
  // ../circle-ir/dist/analysis/passes/cross-file-pass.js
20397
20486
  class CrossFilePass {
20398
- run(projectGraph, sourceLines) {
20487
+ run(projectGraph, sourceLines, options = {}) {
20399
20488
  const resolver = projectGraph.resolver;
20489
+ const budgetMs = options.budgetMs ?? 0;
20490
+ const startMs = Date.now();
20491
+ const budgetExceeded = () => budgetMs > 0 && Date.now() - startMs > budgetMs;
20492
+ const fileCount = projectGraph.filePaths.length;
20493
+ logger.info("cross-file: starting", { files: fileCount, budgetMs });
20494
+ const phase1Start = Date.now();
20495
+ logger.debug("cross-file: phase 1/4 starting (findCrossFileTaintFlows)");
20400
20496
  const flows = resolver.findCrossFileTaintFlows();
20497
+ logger.info("cross-file: phase 1/4 done", {
20498
+ flows: flows.length,
20499
+ elapsedMs: Date.now() - phase1Start
20500
+ });
20401
20501
  const taintPaths = flows.flatMap((flow, idx) => {
20402
20502
  const srcLines = sourceLines.get(flow.sourceFile) ?? [];
20403
20503
  const tgtLines = sourceLines.get(flow.targetFile) ?? [];
@@ -20443,11 +20543,59 @@ class CrossFilePass {
20443
20543
  confidence: 0.7
20444
20544
  }];
20445
20545
  });
20446
- const ipPaths = [
20447
- ...resolver.findInterproceduralTaintPaths(),
20448
- ...resolver.findFieldBindingTaintPaths(),
20449
- ...findCrossInstanceAliasingPaths(projectGraph, sourceLines)
20450
- ];
20546
+ let exceeded = false;
20547
+ const ipPaths = [];
20548
+ if (budgetExceeded()) {
20549
+ exceeded = true;
20550
+ logger.warn("cross-file: budget exceeded after phase 1/4, skipping phases 2-4", {
20551
+ budgetMs,
20552
+ elapsedMs: Date.now() - startMs,
20553
+ partialPaths: taintPaths.length
20554
+ });
20555
+ } else {
20556
+ const phase2Start = Date.now();
20557
+ logger.debug("cross-file: phase 2/4 starting (findInterproceduralTaintPaths)");
20558
+ const phase2 = resolver.findInterproceduralTaintPaths();
20559
+ ipPaths.push(...phase2);
20560
+ logger.info("cross-file: phase 2/4 done", {
20561
+ paths: phase2.length,
20562
+ elapsedMs: Date.now() - phase2Start
20563
+ });
20564
+ }
20565
+ if (!exceeded && budgetExceeded()) {
20566
+ exceeded = true;
20567
+ logger.warn("cross-file: budget exceeded after phase 2/4, skipping phases 3-4", {
20568
+ budgetMs,
20569
+ elapsedMs: Date.now() - startMs,
20570
+ partialPaths: taintPaths.length + ipPaths.length
20571
+ });
20572
+ } else if (!exceeded) {
20573
+ const phase3Start = Date.now();
20574
+ logger.debug("cross-file: phase 3/4 starting (findFieldBindingTaintPaths)");
20575
+ const phase3 = resolver.findFieldBindingTaintPaths();
20576
+ ipPaths.push(...phase3);
20577
+ logger.info("cross-file: phase 3/4 done", {
20578
+ paths: phase3.length,
20579
+ elapsedMs: Date.now() - phase3Start
20580
+ });
20581
+ }
20582
+ if (!exceeded && budgetExceeded()) {
20583
+ exceeded = true;
20584
+ logger.warn("cross-file: budget exceeded after phase 3/4, skipping phase 4", {
20585
+ budgetMs,
20586
+ elapsedMs: Date.now() - startMs,
20587
+ partialPaths: taintPaths.length + ipPaths.length
20588
+ });
20589
+ } else if (!exceeded) {
20590
+ const phase4Start = Date.now();
20591
+ logger.debug("cross-file: phase 4/4 starting (findCrossInstanceAliasingPaths)");
20592
+ const phase4 = findCrossInstanceAliasingPaths(projectGraph, sourceLines);
20593
+ ipPaths.push(...phase4);
20594
+ logger.info("cross-file: phase 4/4 done", {
20595
+ paths: phase4.length,
20596
+ elapsedMs: Date.now() - phase4Start
20597
+ });
20598
+ }
20451
20599
  for (let i2 = 0;i2 < ipPaths.length; i2++) {
20452
20600
  const p = ipPaths[i2];
20453
20601
  const sinkIR = projectGraph.getIR(p.sink.file);
@@ -20459,7 +20607,7 @@ class CrossFilePass {
20459
20607
  const srcLines = sourceLines.get(p.source.file) ?? [];
20460
20608
  const tgtLines = sourceLines.get(p.sink.file) ?? [];
20461
20609
  const dupId = `${p.source.file}:${p.source.line}→${p.sink.file}:${p.sink.line}`;
20462
- 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)) {
20610
+ 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 && tp.sink.type === matchedSink.type)) {
20463
20611
  continue;
20464
20612
  }
20465
20613
  taintPaths.push({
@@ -20519,7 +20667,16 @@ class CrossFilePass {
20519
20667
  }
20520
20668
  }
20521
20669
  const typeHierarchy = projectGraph.typeHierarchy.toTypeHierarchyData();
20522
- return { crossFileCalls, taintPaths, typeHierarchy };
20670
+ logger.info("cross-file: complete", {
20671
+ totalMs: Date.now() - startMs,
20672
+ paths: taintPaths.length,
20673
+ crossFileCalls: crossFileCalls.length,
20674
+ budgetExceeded: exceeded
20675
+ });
20676
+ const result = { crossFileCalls, taintPaths, typeHierarchy };
20677
+ if (exceeded)
20678
+ result.budgetExceeded = true;
20679
+ return result;
20523
20680
  }
20524
20681
  }
20525
20682
  function findCrossInstanceAliasingPaths(projectGraph, _sourceLines) {
@@ -24705,6 +24862,194 @@ function findTaintBridges2(result) {
24705
24862
  return bridges;
24706
24863
  }
24707
24864
 
24865
+ // ../circle-ir/dist/analysis/entry-point-detection.js
24866
+ var TIER_1_METHOD_ANNOTATIONS = new Set([
24867
+ "RequestMapping",
24868
+ "GetMapping",
24869
+ "PostMapping",
24870
+ "PutMapping",
24871
+ "DeleteMapping",
24872
+ "PatchMapping",
24873
+ "MessageMapping",
24874
+ "SubscribeMapping",
24875
+ "KafkaListener",
24876
+ "KafkaHandler",
24877
+ "RabbitListener",
24878
+ "RabbitHandler",
24879
+ "JmsListener",
24880
+ "StreamListener",
24881
+ "SqsListener",
24882
+ "SqsHandler",
24883
+ "EventListener",
24884
+ "Scheduled",
24885
+ "Path",
24886
+ "GET",
24887
+ "POST",
24888
+ "PUT",
24889
+ "DELETE",
24890
+ "PATCH",
24891
+ "HEAD",
24892
+ "OPTIONS"
24893
+ ]);
24894
+ var TIER_1_CLASS_ANNOTATIONS = new Set([
24895
+ "RestController",
24896
+ "Controller",
24897
+ "Path",
24898
+ "WebServlet",
24899
+ "ServerEndpoint",
24900
+ "FeignClient"
24901
+ ]);
24902
+ var TIER_1_BY_SUPERTYPE = new Map([
24903
+ ["HttpServlet", new Set(["doGet", "doPost", "doPut", "doDelete", "doHead", "doOptions", "doTrace", "service"])],
24904
+ ["GenericServlet", new Set(["service"])],
24905
+ ["Filter", new Set(["doFilter"])],
24906
+ ["HandlerInterceptor", new Set(["preHandle", "postHandle", "afterCompletion"])],
24907
+ ["AsyncHandlerInterceptor", new Set(["preHandle", "postHandle", "afterCompletion", "afterConcurrentHandlingStarted"])],
24908
+ ["CommandLineRunner", new Set(["run"])],
24909
+ ["ApplicationRunner", new Set(["run"])]
24910
+ ]);
24911
+ var TIER_3_CLASS_SUFFIXES = [
24912
+ "Util",
24913
+ "Utils",
24914
+ "Helper",
24915
+ "Helpers"
24916
+ ];
24917
+ var TIER_3_PACKAGE_FRAGMENTS = [
24918
+ ".template.",
24919
+ ".templates.",
24920
+ ".engine.",
24921
+ ".engines."
24922
+ ];
24923
+ var TIER_3_JDK_FACADE_INTERFACES = new Set([
24924
+ "Collection",
24925
+ "List",
24926
+ "Set",
24927
+ "Map",
24928
+ "Queue",
24929
+ "Deque",
24930
+ "SortedSet",
24931
+ "SortedMap",
24932
+ "NavigableSet",
24933
+ "NavigableMap",
24934
+ "Iterator",
24935
+ "Iterable",
24936
+ "ListIterator",
24937
+ "Comparator",
24938
+ "Comparable",
24939
+ "Serializable",
24940
+ "Externalizable",
24941
+ "Cloneable"
24942
+ ]);
24943
+ function classNameLooksLikeUtility(name2) {
24944
+ if (!name2)
24945
+ return false;
24946
+ for (const suffix of TIER_3_CLASS_SUFFIXES) {
24947
+ if (name2.length > suffix.length && name2.endsWith(suffix))
24948
+ return true;
24949
+ }
24950
+ return false;
24951
+ }
24952
+ function packageLooksLikeTemplateOrEngine(pkg) {
24953
+ if (!pkg)
24954
+ return false;
24955
+ const padded = `.${pkg}.`;
24956
+ for (const frag of TIER_3_PACKAGE_FRAGMENTS) {
24957
+ if (padded.includes(frag))
24958
+ return true;
24959
+ }
24960
+ return false;
24961
+ }
24962
+ function implementsJdkFacade(t) {
24963
+ if (!t)
24964
+ return false;
24965
+ for (const impl of t.implements ?? []) {
24966
+ if (TIER_3_JDK_FACADE_INTERFACES.has(simpleTypeName(impl)))
24967
+ return true;
24968
+ }
24969
+ return false;
24970
+ }
24971
+ function classShapeIsLibraryFacade(enclosingType) {
24972
+ if (!enclosingType)
24973
+ return false;
24974
+ if (classNameLooksLikeUtility(enclosingType.name))
24975
+ return true;
24976
+ if (packageLooksLikeTemplateOrEngine(enclosingType.package))
24977
+ return true;
24978
+ if (implementsJdkFacade(enclosingType))
24979
+ return true;
24980
+ return false;
24981
+ }
24982
+ function annotationsInclude(annotations, targets) {
24983
+ if (!annotations || annotations.length === 0)
24984
+ return false;
24985
+ for (const raw of annotations) {
24986
+ const simple = raw.replace(/^@/, "").replace(/[<(].*$/, "").trim();
24987
+ if (targets.has(simple))
24988
+ return true;
24989
+ }
24990
+ return false;
24991
+ }
24992
+ function simpleTypeName(ref) {
24993
+ return ref.replace(/<.*$/, "").trim();
24994
+ }
24995
+ function looksLikeMainMethod(method) {
24996
+ if (method.name !== "main")
24997
+ return false;
24998
+ const params = method.parameters ?? [];
24999
+ if (params.length !== 1)
25000
+ return false;
25001
+ const t = (params[0].type ?? "").replace(/\s+/g, "");
25002
+ return t === "String[]" || t === "String..." || t === "java.lang.String[]";
25003
+ }
25004
+ function methodIsSupertypeLifecycleEntryPoint(method, enclosingType) {
25005
+ if (!method.name)
25006
+ return false;
25007
+ if (!enclosingType)
25008
+ return false;
25009
+ const candidates = [];
25010
+ if (enclosingType.extends)
25011
+ candidates.push(simpleTypeName(enclosingType.extends));
25012
+ for (const i2 of enclosingType.implements ?? [])
25013
+ candidates.push(simpleTypeName(i2));
25014
+ for (const supertype of candidates) {
25015
+ const lifecycleMethods = TIER_1_BY_SUPERTYPE.get(supertype);
25016
+ if (lifecycleMethods?.has(method.name))
25017
+ return true;
25018
+ }
25019
+ return false;
25020
+ }
25021
+ function classifyEntryPointTier(method, enclosingType, ctx) {
25022
+ const language = (ctx.language ?? "").toLowerCase();
25023
+ if (language !== "java")
25024
+ return "TIER_UNKNOWN";
25025
+ if (!method)
25026
+ return "TIER_UNKNOWN";
25027
+ if (classShapeIsLibraryFacade(enclosingType)) {
25028
+ return "TIER_3_LIBRARY_API";
25029
+ }
25030
+ if (annotationsInclude(method.annotations, TIER_1_METHOD_ANNOTATIONS)) {
25031
+ return "TIER_1_ENTRY_POINT";
25032
+ }
25033
+ if (enclosingType && annotationsInclude(enclosingType.annotations, TIER_1_CLASS_ANNOTATIONS)) {
25034
+ return "TIER_1_ENTRY_POINT";
25035
+ }
25036
+ if (methodIsSupertypeLifecycleEntryPoint(method, enclosingType)) {
25037
+ return "TIER_1_ENTRY_POINT";
25038
+ }
25039
+ if (looksLikeMainMethod(method)) {
25040
+ return "TIER_1_ENTRY_POINT";
25041
+ }
25042
+ return "TIER_3_LIBRARY_API";
25043
+ }
25044
+ function shouldGateInterproceduralParam(sourceType, enclosingMethod, enclosingType, ctx) {
25045
+ if (sourceType !== "interprocedural_param")
25046
+ return false;
25047
+ if (!enclosingMethod)
25048
+ return false;
25049
+ const tier = classifyEntryPointTier(enclosingMethod, enclosingType, ctx);
25050
+ return tier === "TIER_3_LIBRARY_API";
25051
+ }
25052
+
24708
25053
  // ../circle-ir/dist/analysis/passes/interprocedural-pass.js
24709
25054
  class InterproceduralPass {
24710
25055
  name = "interprocedural";
@@ -24721,6 +25066,15 @@ class InterproceduralPass {
24721
25066
  const additionalSinks = [];
24722
25067
  const additionalFlows = [...taintProp.flows];
24723
25068
  let interprocedural;
25069
+ const methodNameIndex = new Map;
25070
+ for (const type of graph.ir.types ?? []) {
25071
+ for (const method of type.methods ?? []) {
25072
+ if (method.name && !methodNameIndex.has(method.name)) {
25073
+ methodNameIndex.set(method.name, { method, type });
25074
+ }
25075
+ }
25076
+ }
25077
+ const language = graph.ir.meta.language;
24724
25078
  if (sinks.length > 0) {
24725
25079
  const interProc = analyzeInterprocedural2(graph, sources, sinks, sanitizers, {
24726
25080
  taintedVariables: constProp.tainted
@@ -24758,6 +25112,12 @@ class InterproceduralPass {
24758
25112
  continue;
24759
25113
  if (source.type === "interprocedural_param" && source.confidence < 0.6)
24760
25114
  continue;
25115
+ if (source.type === "interprocedural_param" && source.in_method) {
25116
+ const enclosing = methodNameIndex.get(source.in_method);
25117
+ if (shouldGateInterproceduralParam(source.type, enclosing?.method, enclosing?.type, { language, types: graph.ir.types })) {
25118
+ continue;
25119
+ }
25120
+ }
24761
25121
  if (additionalFlows.some((f) => f.source_line === source.line && f.sink_line === sink.line))
24762
25122
  continue;
24763
25123
  additionalFlows.push({
@@ -33967,7 +34327,8 @@ async function analyzeProject(files, options = {}) {
33967
34327
  sourceLinesByFile.set(filePath, code.split(`
33968
34328
  `));
33969
34329
  }
33970
- const crossFileResult = new CrossFilePass().run(projectGraph, sourceLinesByFile);
34330
+ const crossFileBudgetMs = options.crossFileBudgetMs ?? 300000;
34331
+ const crossFileResult = new CrossFilePass().run(projectGraph, sourceLinesByFile, { budgetMs: crossFileBudgetMs });
33971
34332
  const disabledPasses = options.disabledPasses ?? [];
33972
34333
  if (!disabledPasses.includes("security-headers")) {
33973
34334
  const inheritedFindings = checkInheritedCorsHeaders(fileAnalyses, projectGraph.typeHierarchy, sourceLinesByFile);
@@ -33997,7 +34358,7 @@ async function analyzeProject(files, options = {}) {
33997
34358
  total_loc: totalLoc,
33998
34359
  analyzed_at: new Date().toISOString()
33999
34360
  };
34000
- return {
34361
+ const projectAnalysis = {
34001
34362
  meta,
34002
34363
  files: fileAnalyses,
34003
34364
  type_hierarchy: crossFileResult.typeHierarchy,
@@ -34005,6 +34366,10 @@ async function analyzeProject(files, options = {}) {
34005
34366
  taint_paths: crossFileResult.taintPaths,
34006
34367
  findings: []
34007
34368
  };
34369
+ if (crossFileResult.budgetExceeded) {
34370
+ projectAnalysis.cross_file_budget_exceeded = true;
34371
+ }
34372
+ return projectAnalysis;
34008
34373
  }
34009
34374
  function deriveProjectName(paths) {
34010
34375
  if (paths.length === 0)
@@ -34044,7 +34409,7 @@ var colors = {
34044
34409
  };
34045
34410
 
34046
34411
  // src/version.ts
34047
- var version = "3.86.0";
34412
+ var version = "3.89.2";
34048
34413
 
34049
34414
  // src/formatters.ts
34050
34415
  var SINK_SEVERITY = {
@@ -34405,6 +34770,11 @@ function formatResults(results, verbose, crossFileData) {
34405
34770
  lines.push("");
34406
34771
  lines.push(formatCrossFilePaths(crossFileData.taintPaths));
34407
34772
  }
34773
+ if (crossFileData?.budgetExceeded) {
34774
+ lines.push("");
34775
+ lines.push(colors.yellow("⚠ Cross-file budget exceeded — some cross-file taint paths may be missing."));
34776
+ lines.push(colors.yellow(" Raise the limit via the `crossFileBudgetMs` analyzer option (default 300000ms)."));
34777
+ }
34408
34778
  return lines.join(`
34409
34779
  `);
34410
34780
  }
@@ -34419,11 +34789,13 @@ function formatJSON(results, crossFileData) {
34419
34789
  })),
34420
34790
  cross_file_taint_paths: crossFileData?.taintPaths ?? [],
34421
34791
  cross_file_calls: crossFileData?.crossFileCalls ?? [],
34792
+ cross_file_budget_exceeded: crossFileData?.budgetExceeded ?? false,
34422
34793
  summary: {
34423
34794
  filesScanned: results.length,
34424
34795
  filesWithVulnerabilities: results.filter((r) => r.vulnerabilities.length > 0).length,
34425
34796
  totalVulnerabilities: results.reduce((sum, r) => sum + r.vulnerabilities.length, 0),
34426
34797
  crossFileTaintPaths: crossFileData?.taintPaths.length ?? 0,
34798
+ crossFileBudgetExceeded: crossFileData?.budgetExceeded ?? false,
34427
34799
  errors: results.filter((r) => r.error).length
34428
34800
  }
34429
34801
  };
@@ -34619,6 +34991,13 @@ SCAN OPTIONS:
34619
34991
  -o, --output <file> Write results to file
34620
34992
  -q, --quiet Suppress progress output
34621
34993
  -v, --verbose Show detailed output
34994
+ --log-level <level> circle-ir logger level (silent|trace|debug|info|warn|error|fatal)
34995
+ [default: silent — also settable via COGNIUM_LOG_LEVEL env var]
34996
+ --cross-file-budget-ms <n> Wall-time budget (ms) for the cross-file phase
34997
+ [default: 300000 (5 min) — 0 = unlimited]
34998
+ On exceed: partial taint paths kept, remaining
34999
+ cross-file phases skipped, cross_file_budget_exceeded
35000
+ surfaced in output (text warning / JSON / SARIF field).
34622
35001
 
34623
35002
  METRICS OPTIONS:
34624
35003
  -l, --language <lang> Analyze only files for language (bash|go|html|java|javascript|typescript|python|rust)
@@ -34641,6 +35020,10 @@ EXAMPLES:
34641
35020
  cognium-dev scan . --exclude-cwe CWE-330,CWE-327
34642
35021
  cognium-dev scan . --disable-pass naming-convention,todo-in-prod
34643
35022
  cognium-dev scan . --profile custom-config.json
35023
+ cognium-dev scan . --log-level info # phase markers to stderr
35024
+ COGNIUM_LOG_LEVEL=debug cognium-dev scan . # verbose via env var
35025
+ cognium-dev scan . --cross-file-budget-ms 60000 # 60s cross-file cap
35026
+ cognium-dev scan . --cross-file-budget-ms 0 # unlimited (pre-3.89.0 behaviour)
34644
35027
  cognium-dev metrics src/
34645
35028
  cognium-dev metrics src/ --category complexity
34646
35029
  cognium-dev metrics src/ --format json --profile custom-config.json
@@ -34670,10 +35053,10 @@ class Spinner {
34670
35053
  enabled;
34671
35054
  constructor(text) {
34672
35055
  this._text = text;
34673
- this.enabled = Boolean(process.stdout.isTTY);
35056
+ this.enabled = Boolean(process.stderr.isTTY);
34674
35057
  }
34675
35058
  render(frame) {
34676
- process.stdout.write(`\r\x1B[K${frame} ${this._text}`);
35059
+ process.stderr.write(`\r\x1B[K${frame} ${this._text}`);
34677
35060
  }
34678
35061
  start() {
34679
35062
  if (this.isSpinning)
@@ -34682,7 +35065,7 @@ class Spinner {
34682
35065
  return this;
34683
35066
  this.isSpinning = true;
34684
35067
  this.frameIndex = 0;
34685
- process.stdout.write("\x1B[?25l");
35068
+ process.stderr.write("\x1B[?25l");
34686
35069
  this.render(SPINNER_FRAMES[this.frameIndex]);
34687
35070
  this.intervalId = setInterval(() => {
34688
35071
  this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
@@ -34699,26 +35082,26 @@ class Spinner {
34699
35082
  this.intervalId = undefined;
34700
35083
  }
34701
35084
  this.isSpinning = false;
34702
- process.stdout.write("\r\x1B[K");
34703
- process.stdout.write("\x1B[?25h");
35085
+ process.stderr.write("\r\x1B[K");
35086
+ process.stderr.write("\x1B[?25h");
34704
35087
  return this;
34705
35088
  }
34706
35089
  succeed(text) {
34707
35090
  this.stop();
34708
35091
  const message = text || this._text;
34709
- console.log(`\x1B[32m${CHECKMARK}\x1B[0m ${message}`);
35092
+ console.error(`\x1B[32m${CHECKMARK}\x1B[0m ${message}`);
34710
35093
  return this;
34711
35094
  }
34712
35095
  fail(text) {
34713
35096
  this.stop();
34714
35097
  const message = text || this._text;
34715
- console.log(`\x1B[31m${CROSS}\x1B[0m ${message}`);
35098
+ console.error(`\x1B[31m${CROSS}\x1B[0m ${message}`);
34716
35099
  return this;
34717
35100
  }
34718
35101
  warn(text) {
34719
35102
  this.stop();
34720
35103
  const message = text || this._text;
34721
- console.log(`\x1B[33m${WARNING}\x1B[0m ${message}`);
35104
+ console.error(`\x1B[33m${WARNING}\x1B[0m ${message}`);
34722
35105
  return this;
34723
35106
  }
34724
35107
  set text(value) {
@@ -34948,7 +35331,7 @@ async function scanFile(filePath, language, analyzeOpts) {
34948
35331
  };
34949
35332
  }
34950
35333
  }
34951
- async function scanProject(files, language, analyzeOpts) {
35334
+ async function scanProject(files, language, analyzeOpts, crossFileBudgetMs) {
34952
35335
  const filesWithCode = files.map((f) => ({
34953
35336
  code: readFileSync(f, "utf-8"),
34954
35337
  filePath: f,
@@ -34956,7 +35339,8 @@ async function scanProject(files, language, analyzeOpts) {
34956
35339
  }));
34957
35340
  const projectResult = await analyzeProject(filesWithCode, {
34958
35341
  passOptions: analyzeOpts?.passOptions,
34959
- disabledPasses: analyzeOpts?.disabledPasses
35342
+ disabledPasses: analyzeOpts?.disabledPasses,
35343
+ ...crossFileBudgetMs !== undefined ? { crossFileBudgetMs } : {}
34960
35344
  });
34961
35345
  const results = projectResult.files.map(({ file, analysis }) => {
34962
35346
  const vulnerabilities = (analysis.taint.flows || []).map((flow) => ({
@@ -34984,7 +35368,8 @@ async function scanProject(files, language, analyzeOpts) {
34984
35368
  results,
34985
35369
  crossFileData: {
34986
35370
  taintPaths: projectResult.taint_paths,
34987
- crossFileCalls: projectResult.cross_file_calls
35371
+ crossFileCalls: projectResult.cross_file_calls,
35372
+ budgetExceeded: projectResult.cross_file_budget_exceeded === true
34988
35373
  }
34989
35374
  };
34990
35375
  }
@@ -35079,7 +35464,7 @@ async function runScan(targetPath, options) {
35079
35464
  disabledPasses = converted.disabledPasses;
35080
35465
  suppressions = config.suppressions ?? [];
35081
35466
  if (!options.quiet) {
35082
- console.log(colors.dim(`Loaded config: ${options.profile || "cognium.config.json"}`));
35467
+ console.error(colors.dim(`Loaded config: ${options.profile || "cognium.config.json"}`));
35083
35468
  }
35084
35469
  }
35085
35470
  if (options.disablePass) {
@@ -35124,7 +35509,7 @@ async function runScan(targetPath, options) {
35124
35509
  if ((await stat(absPath)).isDirectory()) {
35125
35510
  if (spin)
35126
35511
  spin.text = `Running project analysis on ${files.length} file(s)...`;
35127
- const projectScan = await scanProject(files, options.language, analyzeOpts);
35512
+ const projectScan = await scanProject(files, options.language, analyzeOpts, options.crossFileBudgetMs);
35128
35513
  results = projectScan.results;
35129
35514
  crossFileData = projectScan.crossFileData;
35130
35515
  } else {
@@ -35163,7 +35548,7 @@ async function runScan(targetPath, options) {
35163
35548
  results = applySuppressionsToResults(results, suppressions, process.cwd());
35164
35549
  const afterCount = results.reduce((sum, r) => sum + r.vulnerabilities.length, 0);
35165
35550
  if (!options.quiet && beforeCount !== afterCount) {
35166
- console.log(colors.dim(`Suppressed ${beforeCount - afterCount} finding(s) via config`));
35551
+ console.error(colors.dim(`Suppressed ${beforeCount - afterCount} finding(s) via config`));
35167
35552
  }
35168
35553
  }
35169
35554
  const severityOrder = ["low", "medium", "high", "critical"];
@@ -35246,7 +35631,7 @@ async function runScan(targetPath, options) {
35246
35631
  if (options.output) {
35247
35632
  const { writeFileSync } = await import("fs");
35248
35633
  writeFileSync(options.output, output);
35249
- console.log(colors.green(`Results written to ${options.output}`));
35634
+ console.error(colors.green(`Results written to ${options.output}`));
35250
35635
  } else if (output.trim()) {
35251
35636
  console.log(output);
35252
35637
  }
@@ -35292,7 +35677,7 @@ async function runMetrics(targetPath, options) {
35292
35677
  passOptions = converted.passOptions;
35293
35678
  disabledPasses = converted.disabledPasses;
35294
35679
  if (!options.quiet) {
35295
- console.log(colors.dim(`Loaded config: ${options.profile || "cognium.config.json"}`));
35680
+ console.error(colors.dim(`Loaded config: ${options.profile || "cognium.config.json"}`));
35296
35681
  }
35297
35682
  }
35298
35683
  try {
@@ -35400,7 +35785,7 @@ async function runMetrics(targetPath, options) {
35400
35785
  if (options.output) {
35401
35786
  const { writeFileSync } = await import("fs");
35402
35787
  writeFileSync(options.output, output);
35403
- console.log(colors.green(`Results written to ${options.output}`));
35788
+ console.error(colors.green(`Results written to ${options.output}`));
35404
35789
  } else {
35405
35790
  console.log(output);
35406
35791
  }
@@ -35511,8 +35896,32 @@ async function handleInit() {
35511
35896
  writeFileSync(configPath, JSON.stringify(config, null, 2));
35512
35897
  console.log(colors.green(`Created ${configPath}`));
35513
35898
  }
35899
+ function applyLogLevel(cliValue) {
35900
+ const raw = typeof cliValue === "string" && cliValue.length > 0 ? cliValue : process.env.COGNIUM_LOG_LEVEL;
35901
+ if (!raw)
35902
+ return;
35903
+ const valid = ["trace", "debug", "info", "warn", "error", "fatal", "silent"];
35904
+ const normalized = raw.toLowerCase();
35905
+ if (!valid.includes(normalized)) {
35906
+ console.error(colors.yellow(`Warning: invalid log level "${raw}" (expected one of: ${valid.join(", ")}); ignoring`));
35907
+ return;
35908
+ }
35909
+ setLogLevel(normalized);
35910
+ }
35911
+ function parseCrossFileBudgetMs(raw) {
35912
+ if (raw === undefined || raw === true || raw === "")
35913
+ return;
35914
+ const s = String(raw);
35915
+ const n = Number.parseInt(s, 10);
35916
+ if (!Number.isFinite(n) || n < 0 || String(n) !== s) {
35917
+ console.error(colors.yellow(`Warning: invalid --cross-file-budget-ms "${s}" (expected non-negative integer in ms, 0 = unlimited); ignoring`));
35918
+ return;
35919
+ }
35920
+ return n;
35921
+ }
35514
35922
  async function main() {
35515
35923
  const { command, args: args2, options } = parseArgs(process.argv.slice(2));
35924
+ applyLogLevel(options["log-level"]);
35516
35925
  if (options.help || options.h) {
35517
35926
  showHelp();
35518
35927
  return;
@@ -35532,7 +35941,7 @@ async function main() {
35532
35941
  if (command === "metrics") {
35533
35942
  if (args2.length === 0) {
35534
35943
  console.error(colors.red("Error: metrics command requires a path argument"));
35535
- console.log(`
35944
+ console.error(`
35536
35945
  Usage: cognium-dev metrics <path> [options]`);
35537
35946
  process.exit(1);
35538
35947
  }
@@ -35552,7 +35961,7 @@ Usage: cognium-dev metrics <path> [options]`);
35552
35961
  if (command === "scan") {
35553
35962
  if (args2.length === 0) {
35554
35963
  console.error(colors.red("Error: scan command requires a path argument"));
35555
- console.log(`
35964
+ console.error(`
35556
35965
  Usage: cognium-dev scan <path> [options]`);
35557
35966
  process.exit(1);
35558
35967
  }
@@ -35569,7 +35978,8 @@ Usage: cognium-dev scan <path> [options]`);
35569
35978
  excludeTests: options["exclude-tests"] === true,
35570
35979
  excludeCwe: options["exclude-cwe"],
35571
35980
  profile: options.profile || options.p,
35572
- disablePass: options["disable-pass"]
35981
+ disablePass: options["disable-pass"],
35982
+ crossFileBudgetMs: parseCrossFileBudgetMs(options["cross-file-budget-ms"])
35573
35983
  };
35574
35984
  await runScan(targetPath, scanOptions);
35575
35985
  return;
@@ -35578,7 +35988,7 @@ Usage: cognium-dev scan <path> [options]`);
35578
35988
  showHelp();
35579
35989
  } else {
35580
35990
  console.error(colors.red(`Error: Unknown command '${command}'`));
35581
- console.log(`
35991
+ console.error(`
35582
35992
  Run 'cognium-dev --help' for usage information`);
35583
35993
  process.exit(1);
35584
35994
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cognium-dev",
3
- "version": "3.86.0",
3
+ "version": "3.89.2",
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.86.0"
68
+ "circle-ir": "^3.89.1"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/node": "^25.5.0",