cognium-dev 3.85.1 → 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 +507 -46
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -12112,6 +12112,19 @@ function argIsClassLiteral(call, position) {
12112
12112
  return false;
12113
12113
  return CLASS_LITERAL_RE.test(expr);
12114
12114
  }
12115
+ var CWE_78_RECEIVER_ALLOWLIST = new Set([
12116
+ "Runtime",
12117
+ "ProcessBuilder",
12118
+ "Process",
12119
+ "CommandLine",
12120
+ "DefaultExecutor",
12121
+ "Executor",
12122
+ "Exec",
12123
+ "Launcher",
12124
+ "ProcStarter",
12125
+ "ProcessExecutor",
12126
+ "RuntimeUtil"
12127
+ ]);
12115
12128
  function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12116
12129
  const sinkMap = new Map;
12117
12130
  for (const call of calls) {
@@ -12132,6 +12145,18 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12132
12145
  if (pattern.safe_if_class_literal_at !== undefined && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
12133
12146
  continue;
12134
12147
  }
12148
+ if (pattern.type === "command_injection") {
12149
+ if (call.is_constructor) {
12150
+ if (!CWE_78_RECEIVER_ALLOWLIST.has(call.method_name)) {
12151
+ continue;
12152
+ }
12153
+ } else {
12154
+ const receiverClass = call.receiver_type;
12155
+ if (receiverClass && !CWE_78_RECEIVER_ALLOWLIST.has(receiverClass)) {
12156
+ continue;
12157
+ }
12158
+ }
12159
+ }
12135
12160
  const location = formatCallLocation(call);
12136
12161
  const key = `${location}:${call.location.line}:${pattern.cwe}`;
12137
12162
  const confidence = calculateSinkConfidence(call, pattern);
@@ -18456,11 +18481,14 @@ var LOG_LEVELS = {
18456
18481
  fatal: 5,
18457
18482
  silent: 6
18458
18483
  };
18459
- var currentLevel = "info";
18484
+ var currentLevel = "silent";
18460
18485
  var customLogger = null;
18461
18486
  function shouldLog(level) {
18462
18487
  return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
18463
18488
  }
18489
+ function setLogLevel(level) {
18490
+ currentLevel = level;
18491
+ }
18464
18492
  var logger = {
18465
18493
  trace: (msg, obj) => {
18466
18494
  if (customLogger) {
@@ -18484,7 +18512,7 @@ var logger = {
18484
18512
  return;
18485
18513
  }
18486
18514
  if (shouldLog("info"))
18487
- console.log(obj ? `[INFO] ${msg} ${JSON.stringify(obj)}` : `[INFO] ${msg}`);
18515
+ console.error(obj ? `[INFO] ${msg} ${JSON.stringify(obj)}` : `[INFO] ${msg}`);
18488
18516
  },
18489
18517
  warn: (msg, obj) => {
18490
18518
  if (customLogger) {
@@ -19352,7 +19380,88 @@ class SymbolTable {
19352
19380
  }
19353
19381
  }
19354
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
+
19355
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
+ }
19356
19465
  symbolTable;
19357
19466
  typeHierarchy;
19358
19467
  fileIRs = new Map;
@@ -19807,7 +19916,8 @@ class CrossFileResolver {
19807
19916
  }
19808
19917
  if (!targetMethod)
19809
19918
  continue;
19810
- 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) ?? [];
19811
19921
  if (sinksInMethod.length === 0)
19812
19922
  continue;
19813
19923
  for (const sink of sinksInMethod) {
@@ -19836,6 +19946,7 @@ class CrossFileResolver {
19836
19946
  const seen = new Set;
19837
19947
  const methodIndex = this.buildMethodIndex();
19838
19948
  for (const [callerFile, callerIR] of this.fileIRs) {
19949
+ const callerIdx = this.getFileIndex(callerIR);
19839
19950
  for (const type of callerIR.types) {
19840
19951
  for (const method of type.methods) {
19841
19952
  const tainted = new Map;
@@ -19853,7 +19964,7 @@ class CrossFileResolver {
19853
19964
  hopChain: [{ file: callerFile, line: src.line, method: method.name, kind: "source" }]
19854
19965
  });
19855
19966
  }
19856
- 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) ?? [];
19857
19968
  for (const call of callsInMethod) {
19858
19969
  const resolved = this.resolveCall(call, callerFile);
19859
19970
  if (!resolved)
@@ -19867,7 +19978,7 @@ class CrossFileResolver {
19867
19978
  const sourceLine = calleeSourceLine ?? call.location.line;
19868
19979
  const sourceFile = callee.file;
19869
19980
  const sourceType = callee.sourceType;
19870
- 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");
19871
19982
  for (const def of defsAtLine) {
19872
19983
  if (!def.variable)
19873
19984
  continue;
@@ -19895,7 +20006,8 @@ class CrossFileResolver {
19895
20006
  const calleeNode = methodIndex.get(resolved.targetMethod);
19896
20007
  if (!calleeNode)
19897
20008
  continue;
19898
- 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) ?? [];
19899
20011
  for (const sink of sinksInCallee) {
19900
20012
  const key = `${matched.origin.file}:${matched.origin.line}→${callee.file}:${sink.line}`;
19901
20013
  if (seen.has(key))
@@ -19926,9 +20038,9 @@ class CrossFileResolver {
19926
20038
  }
19927
20039
  }
19928
20040
  if (tainted.size > 0) {
19929
- const sinksInCaller = callerIR.taint.sinks.filter((s) => s.line >= method.start_line && s.line <= method.end_line);
20041
+ const sinksInCaller = callerIdx.sinksByMethod.get(method) ?? [];
19930
20042
  for (const sink of sinksInCaller) {
19931
- const callsAtSink = callerIR.calls.filter((c) => c.location.line === sink.line);
20043
+ const callsAtSink = callerIdx.callsByLine.get(sink.line) ?? [];
19932
20044
  for (const sinkCall of callsAtSink) {
19933
20045
  for (const arg of sinkCall.arguments ?? []) {
19934
20046
  const matched = this.matchTaintedArg(arg, tainted);
@@ -19975,6 +20087,7 @@ class CrossFileResolver {
19975
20087
  const fieldExprRe = /^(\w+)\.(\w+)$/;
19976
20088
  const methodIndex = this.buildMethodIndex();
19977
20089
  for (const [callerFile, callerIR] of this.fileIRs) {
20090
+ const callerIdx = this.getFileIndex(callerIR);
19978
20091
  for (const type of callerIR.types) {
19979
20092
  const callerTypeFqn = callerIR.meta.package ? `${callerIR.meta.package}.${type.name}` : type.name;
19980
20093
  for (const method of type.methods) {
@@ -19993,9 +20106,9 @@ class CrossFileResolver {
19993
20106
  hopChain: [{ file: callerFile, line: src.line, method: method.name, kind: "source" }]
19994
20107
  });
19995
20108
  }
19996
- 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);
19997
20110
  for (const def of defsInMethod) {
19998
- const usesAtLine = callerIR.dfg.uses.filter((u) => u.line === def.line);
20111
+ const usesAtLine = callerIdx.usesByLine.get(def.line) ?? [];
19999
20112
  if (usesAtLine.length < 2)
20000
20113
  continue;
20001
20114
  let receiver = null;
@@ -20076,9 +20189,9 @@ class CrossFileResolver {
20076
20189
  }
20077
20190
  if (tainted.size === 0)
20078
20191
  continue;
20079
- const sinksInCaller = callerIR.taint.sinks.filter((s) => s.line >= method.start_line && s.line <= method.end_line);
20192
+ const sinksInCaller = callerIdx.sinksByMethod.get(method) ?? [];
20080
20193
  for (const sink of sinksInCaller) {
20081
- const callsAtSink = callerIR.calls.filter((c) => c.location.line === sink.line);
20194
+ const callsAtSink = callerIdx.callsByLine.get(sink.line) ?? [];
20082
20195
  for (const sinkCall of callsAtSink) {
20083
20196
  for (const arg of sinkCall.arguments ?? []) {
20084
20197
  const matched = this.matchTaintedArg(arg, tainted);
@@ -20111,7 +20224,7 @@ class CrossFileResolver {
20111
20224
  }
20112
20225
  }
20113
20226
  }
20114
- 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) ?? [];
20115
20228
  for (const call of callsInMethod) {
20116
20229
  const resolved = this.resolveCall(call, callerFile);
20117
20230
  if (!resolved)
@@ -20128,7 +20241,8 @@ class CrossFileResolver {
20128
20241
  const calleeNode = methodIndex.get(resolved.targetMethod);
20129
20242
  if (!calleeNode)
20130
20243
  continue;
20131
- 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) ?? [];
20132
20246
  for (const sink of sinksInCallee) {
20133
20247
  const key = `fb:${matched.origin.file}:${matched.origin.line}→${callee.file}:${sink.line}`;
20134
20248
  if (seen.has(key))
@@ -20370,9 +20484,20 @@ class AnalysisPipeline {
20370
20484
  }
20371
20485
  // ../circle-ir/dist/analysis/passes/cross-file-pass.js
20372
20486
  class CrossFilePass {
20373
- run(projectGraph, sourceLines) {
20487
+ run(projectGraph, sourceLines, options = {}) {
20374
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)");
20375
20496
  const flows = resolver.findCrossFileTaintFlows();
20497
+ logger.info("cross-file: phase 1/4 done", {
20498
+ flows: flows.length,
20499
+ elapsedMs: Date.now() - phase1Start
20500
+ });
20376
20501
  const taintPaths = flows.flatMap((flow, idx) => {
20377
20502
  const srcLines = sourceLines.get(flow.sourceFile) ?? [];
20378
20503
  const tgtLines = sourceLines.get(flow.targetFile) ?? [];
@@ -20418,11 +20543,59 @@ class CrossFilePass {
20418
20543
  confidence: 0.7
20419
20544
  }];
20420
20545
  });
20421
- const ipPaths = [
20422
- ...resolver.findInterproceduralTaintPaths(),
20423
- ...resolver.findFieldBindingTaintPaths(),
20424
- ...findCrossInstanceAliasingPaths(projectGraph, sourceLines)
20425
- ];
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
+ }
20426
20599
  for (let i2 = 0;i2 < ipPaths.length; i2++) {
20427
20600
  const p = ipPaths[i2];
20428
20601
  const sinkIR = projectGraph.getIR(p.sink.file);
@@ -20434,7 +20607,7 @@ class CrossFilePass {
20434
20607
  const srcLines = sourceLines.get(p.source.file) ?? [];
20435
20608
  const tgtLines = sourceLines.get(p.sink.file) ?? [];
20436
20609
  const dupId = `${p.source.file}:${p.source.line}→${p.sink.file}:${p.sink.line}`;
20437
- 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)) {
20438
20611
  continue;
20439
20612
  }
20440
20613
  taintPaths.push({
@@ -20494,7 +20667,16 @@ class CrossFilePass {
20494
20667
  }
20495
20668
  }
20496
20669
  const typeHierarchy = projectGraph.typeHierarchy.toTypeHierarchyData();
20497
- 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;
20498
20680
  }
20499
20681
  }
20500
20682
  function findCrossInstanceAliasingPaths(projectGraph, _sourceLines) {
@@ -24680,6 +24862,194 @@ function findTaintBridges2(result) {
24680
24862
  return bridges;
24681
24863
  }
24682
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
+
24683
25053
  // ../circle-ir/dist/analysis/passes/interprocedural-pass.js
24684
25054
  class InterproceduralPass {
24685
25055
  name = "interprocedural";
@@ -24696,6 +25066,15 @@ class InterproceduralPass {
24696
25066
  const additionalSinks = [];
24697
25067
  const additionalFlows = [...taintProp.flows];
24698
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;
24699
25078
  if (sinks.length > 0) {
24700
25079
  const interProc = analyzeInterprocedural2(graph, sources, sinks, sanitizers, {
24701
25080
  taintedVariables: constProp.tainted
@@ -24733,6 +25112,12 @@ class InterproceduralPass {
24733
25112
  continue;
24734
25113
  if (source.type === "interprocedural_param" && source.confidence < 0.6)
24735
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
+ }
24736
25121
  if (additionalFlows.some((f) => f.source_line === source.line && f.sink_line === sink.line))
24737
25122
  continue;
24738
25123
  additionalFlows.push({
@@ -29073,6 +29458,20 @@ var CRED_KEYWORD_RE = /\b([A-Za-z_$][\w$]*?(?:password|passwd|secret|api[_-]?key
29073
29458
  var CRED_DYNAMIC_VALUE_RE = /\$\{|process\.env|os\.environ|os\.Getenv|System\.getenv/;
29074
29459
  var CRED_FUNCTION_DECL_RE = /\b(?:function|func|def|fn)\s+\w+\s*\(/;
29075
29460
  var CRED_COMPARISON_RE = /(?:===?|!==?|>=|<=|<>)\s*["'`]/;
29461
+ var PROPERTY_KEY_RE = /^[a-z][a-zA-Z0-9_-]*\.[a-zA-Z][a-zA-Z0-9_.-]*$/;
29462
+ var PLAIN_IDENTIFIER_RE = /^[a-z][a-zA-Z_]*$/;
29463
+ function charClassDiversity(s) {
29464
+ let n = 0;
29465
+ if (/[a-z]/.test(s))
29466
+ n++;
29467
+ if (/[A-Z]/.test(s))
29468
+ n++;
29469
+ if (/[0-9]/.test(s))
29470
+ n++;
29471
+ if (/[^a-zA-Z0-9]/.test(s))
29472
+ n++;
29473
+ return n;
29474
+ }
29076
29475
  function isLikelyCredentialAssignment(line) {
29077
29476
  if (CRED_FUNCTION_DECL_RE.test(line))
29078
29477
  return null;
@@ -29091,6 +29490,18 @@ function isLikelyCredentialAssignment(line) {
29091
29490
  return null;
29092
29491
  if (isAllSameChar(value))
29093
29492
  return null;
29493
+ if (value.length < 12)
29494
+ return null;
29495
+ if (shannonEntropy(value) < 3.5)
29496
+ return null;
29497
+ if (charClassDiversity(value) < 2)
29498
+ return null;
29499
+ if (PROPERTY_KEY_RE.test(value))
29500
+ return null;
29501
+ if (PLAIN_IDENTIFIER_RE.test(value))
29502
+ return null;
29503
+ if (/^[0-9]+$/.test(value) && value.length < 16)
29504
+ return null;
29094
29505
  return { name: name2, value };
29095
29506
  }
29096
29507
  var STRING_LITERAL_RE = /(["'`])((?:\\.|(?!\1).){8,200})\1/g;
@@ -33916,7 +34327,8 @@ async function analyzeProject(files, options = {}) {
33916
34327
  sourceLinesByFile.set(filePath, code.split(`
33917
34328
  `));
33918
34329
  }
33919
- const crossFileResult = new CrossFilePass().run(projectGraph, sourceLinesByFile);
34330
+ const crossFileBudgetMs = options.crossFileBudgetMs ?? 300000;
34331
+ const crossFileResult = new CrossFilePass().run(projectGraph, sourceLinesByFile, { budgetMs: crossFileBudgetMs });
33920
34332
  const disabledPasses = options.disabledPasses ?? [];
33921
34333
  if (!disabledPasses.includes("security-headers")) {
33922
34334
  const inheritedFindings = checkInheritedCorsHeaders(fileAnalyses, projectGraph.typeHierarchy, sourceLinesByFile);
@@ -33946,7 +34358,7 @@ async function analyzeProject(files, options = {}) {
33946
34358
  total_loc: totalLoc,
33947
34359
  analyzed_at: new Date().toISOString()
33948
34360
  };
33949
- return {
34361
+ const projectAnalysis = {
33950
34362
  meta,
33951
34363
  files: fileAnalyses,
33952
34364
  type_hierarchy: crossFileResult.typeHierarchy,
@@ -33954,6 +34366,10 @@ async function analyzeProject(files, options = {}) {
33954
34366
  taint_paths: crossFileResult.taintPaths,
33955
34367
  findings: []
33956
34368
  };
34369
+ if (crossFileResult.budgetExceeded) {
34370
+ projectAnalysis.cross_file_budget_exceeded = true;
34371
+ }
34372
+ return projectAnalysis;
33957
34373
  }
33958
34374
  function deriveProjectName(paths) {
33959
34375
  if (paths.length === 0)
@@ -33993,7 +34409,7 @@ var colors = {
33993
34409
  };
33994
34410
 
33995
34411
  // src/version.ts
33996
- var version = "3.85.1";
34412
+ var version = "3.89.2";
33997
34413
 
33998
34414
  // src/formatters.ts
33999
34415
  var SINK_SEVERITY = {
@@ -34354,6 +34770,11 @@ function formatResults(results, verbose, crossFileData) {
34354
34770
  lines.push("");
34355
34771
  lines.push(formatCrossFilePaths(crossFileData.taintPaths));
34356
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
+ }
34357
34778
  return lines.join(`
34358
34779
  `);
34359
34780
  }
@@ -34368,11 +34789,13 @@ function formatJSON(results, crossFileData) {
34368
34789
  })),
34369
34790
  cross_file_taint_paths: crossFileData?.taintPaths ?? [],
34370
34791
  cross_file_calls: crossFileData?.crossFileCalls ?? [],
34792
+ cross_file_budget_exceeded: crossFileData?.budgetExceeded ?? false,
34371
34793
  summary: {
34372
34794
  filesScanned: results.length,
34373
34795
  filesWithVulnerabilities: results.filter((r) => r.vulnerabilities.length > 0).length,
34374
34796
  totalVulnerabilities: results.reduce((sum, r) => sum + r.vulnerabilities.length, 0),
34375
34797
  crossFileTaintPaths: crossFileData?.taintPaths.length ?? 0,
34798
+ crossFileBudgetExceeded: crossFileData?.budgetExceeded ?? false,
34376
34799
  errors: results.filter((r) => r.error).length
34377
34800
  }
34378
34801
  };
@@ -34568,6 +34991,13 @@ SCAN OPTIONS:
34568
34991
  -o, --output <file> Write results to file
34569
34992
  -q, --quiet Suppress progress output
34570
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).
34571
35001
 
34572
35002
  METRICS OPTIONS:
34573
35003
  -l, --language <lang> Analyze only files for language (bash|go|html|java|javascript|typescript|python|rust)
@@ -34590,6 +35020,10 @@ EXAMPLES:
34590
35020
  cognium-dev scan . --exclude-cwe CWE-330,CWE-327
34591
35021
  cognium-dev scan . --disable-pass naming-convention,todo-in-prod
34592
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)
34593
35027
  cognium-dev metrics src/
34594
35028
  cognium-dev metrics src/ --category complexity
34595
35029
  cognium-dev metrics src/ --format json --profile custom-config.json
@@ -34619,10 +35053,10 @@ class Spinner {
34619
35053
  enabled;
34620
35054
  constructor(text) {
34621
35055
  this._text = text;
34622
- this.enabled = Boolean(process.stdout.isTTY);
35056
+ this.enabled = Boolean(process.stderr.isTTY);
34623
35057
  }
34624
35058
  render(frame) {
34625
- process.stdout.write(`\r\x1B[K${frame} ${this._text}`);
35059
+ process.stderr.write(`\r\x1B[K${frame} ${this._text}`);
34626
35060
  }
34627
35061
  start() {
34628
35062
  if (this.isSpinning)
@@ -34631,7 +35065,7 @@ class Spinner {
34631
35065
  return this;
34632
35066
  this.isSpinning = true;
34633
35067
  this.frameIndex = 0;
34634
- process.stdout.write("\x1B[?25l");
35068
+ process.stderr.write("\x1B[?25l");
34635
35069
  this.render(SPINNER_FRAMES[this.frameIndex]);
34636
35070
  this.intervalId = setInterval(() => {
34637
35071
  this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
@@ -34648,26 +35082,26 @@ class Spinner {
34648
35082
  this.intervalId = undefined;
34649
35083
  }
34650
35084
  this.isSpinning = false;
34651
- process.stdout.write("\r\x1B[K");
34652
- process.stdout.write("\x1B[?25h");
35085
+ process.stderr.write("\r\x1B[K");
35086
+ process.stderr.write("\x1B[?25h");
34653
35087
  return this;
34654
35088
  }
34655
35089
  succeed(text) {
34656
35090
  this.stop();
34657
35091
  const message = text || this._text;
34658
- console.log(`\x1B[32m${CHECKMARK}\x1B[0m ${message}`);
35092
+ console.error(`\x1B[32m${CHECKMARK}\x1B[0m ${message}`);
34659
35093
  return this;
34660
35094
  }
34661
35095
  fail(text) {
34662
35096
  this.stop();
34663
35097
  const message = text || this._text;
34664
- console.log(`\x1B[31m${CROSS}\x1B[0m ${message}`);
35098
+ console.error(`\x1B[31m${CROSS}\x1B[0m ${message}`);
34665
35099
  return this;
34666
35100
  }
34667
35101
  warn(text) {
34668
35102
  this.stop();
34669
35103
  const message = text || this._text;
34670
- console.log(`\x1B[33m${WARNING}\x1B[0m ${message}`);
35104
+ console.error(`\x1B[33m${WARNING}\x1B[0m ${message}`);
34671
35105
  return this;
34672
35106
  }
34673
35107
  set text(value) {
@@ -34897,7 +35331,7 @@ async function scanFile(filePath, language, analyzeOpts) {
34897
35331
  };
34898
35332
  }
34899
35333
  }
34900
- async function scanProject(files, language, analyzeOpts) {
35334
+ async function scanProject(files, language, analyzeOpts, crossFileBudgetMs) {
34901
35335
  const filesWithCode = files.map((f) => ({
34902
35336
  code: readFileSync(f, "utf-8"),
34903
35337
  filePath: f,
@@ -34905,7 +35339,8 @@ async function scanProject(files, language, analyzeOpts) {
34905
35339
  }));
34906
35340
  const projectResult = await analyzeProject(filesWithCode, {
34907
35341
  passOptions: analyzeOpts?.passOptions,
34908
- disabledPasses: analyzeOpts?.disabledPasses
35342
+ disabledPasses: analyzeOpts?.disabledPasses,
35343
+ ...crossFileBudgetMs !== undefined ? { crossFileBudgetMs } : {}
34909
35344
  });
34910
35345
  const results = projectResult.files.map(({ file, analysis }) => {
34911
35346
  const vulnerabilities = (analysis.taint.flows || []).map((flow) => ({
@@ -34933,7 +35368,8 @@ async function scanProject(files, language, analyzeOpts) {
34933
35368
  results,
34934
35369
  crossFileData: {
34935
35370
  taintPaths: projectResult.taint_paths,
34936
- crossFileCalls: projectResult.cross_file_calls
35371
+ crossFileCalls: projectResult.cross_file_calls,
35372
+ budgetExceeded: projectResult.cross_file_budget_exceeded === true
34937
35373
  }
34938
35374
  };
34939
35375
  }
@@ -35028,7 +35464,7 @@ async function runScan(targetPath, options) {
35028
35464
  disabledPasses = converted.disabledPasses;
35029
35465
  suppressions = config.suppressions ?? [];
35030
35466
  if (!options.quiet) {
35031
- console.log(colors.dim(`Loaded config: ${options.profile || "cognium.config.json"}`));
35467
+ console.error(colors.dim(`Loaded config: ${options.profile || "cognium.config.json"}`));
35032
35468
  }
35033
35469
  }
35034
35470
  if (options.disablePass) {
@@ -35073,7 +35509,7 @@ async function runScan(targetPath, options) {
35073
35509
  if ((await stat(absPath)).isDirectory()) {
35074
35510
  if (spin)
35075
35511
  spin.text = `Running project analysis on ${files.length} file(s)...`;
35076
- const projectScan = await scanProject(files, options.language, analyzeOpts);
35512
+ const projectScan = await scanProject(files, options.language, analyzeOpts, options.crossFileBudgetMs);
35077
35513
  results = projectScan.results;
35078
35514
  crossFileData = projectScan.crossFileData;
35079
35515
  } else {
@@ -35112,7 +35548,7 @@ async function runScan(targetPath, options) {
35112
35548
  results = applySuppressionsToResults(results, suppressions, process.cwd());
35113
35549
  const afterCount = results.reduce((sum, r) => sum + r.vulnerabilities.length, 0);
35114
35550
  if (!options.quiet && beforeCount !== afterCount) {
35115
- console.log(colors.dim(`Suppressed ${beforeCount - afterCount} finding(s) via config`));
35551
+ console.error(colors.dim(`Suppressed ${beforeCount - afterCount} finding(s) via config`));
35116
35552
  }
35117
35553
  }
35118
35554
  const severityOrder = ["low", "medium", "high", "critical"];
@@ -35195,7 +35631,7 @@ async function runScan(targetPath, options) {
35195
35631
  if (options.output) {
35196
35632
  const { writeFileSync } = await import("fs");
35197
35633
  writeFileSync(options.output, output);
35198
- console.log(colors.green(`Results written to ${options.output}`));
35634
+ console.error(colors.green(`Results written to ${options.output}`));
35199
35635
  } else if (output.trim()) {
35200
35636
  console.log(output);
35201
35637
  }
@@ -35241,7 +35677,7 @@ async function runMetrics(targetPath, options) {
35241
35677
  passOptions = converted.passOptions;
35242
35678
  disabledPasses = converted.disabledPasses;
35243
35679
  if (!options.quiet) {
35244
- console.log(colors.dim(`Loaded config: ${options.profile || "cognium.config.json"}`));
35680
+ console.error(colors.dim(`Loaded config: ${options.profile || "cognium.config.json"}`));
35245
35681
  }
35246
35682
  }
35247
35683
  try {
@@ -35349,7 +35785,7 @@ async function runMetrics(targetPath, options) {
35349
35785
  if (options.output) {
35350
35786
  const { writeFileSync } = await import("fs");
35351
35787
  writeFileSync(options.output, output);
35352
- console.log(colors.green(`Results written to ${options.output}`));
35788
+ console.error(colors.green(`Results written to ${options.output}`));
35353
35789
  } else {
35354
35790
  console.log(output);
35355
35791
  }
@@ -35460,8 +35896,32 @@ async function handleInit() {
35460
35896
  writeFileSync(configPath, JSON.stringify(config, null, 2));
35461
35897
  console.log(colors.green(`Created ${configPath}`));
35462
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
+ }
35463
35922
  async function main() {
35464
35923
  const { command, args: args2, options } = parseArgs(process.argv.slice(2));
35924
+ applyLogLevel(options["log-level"]);
35465
35925
  if (options.help || options.h) {
35466
35926
  showHelp();
35467
35927
  return;
@@ -35481,7 +35941,7 @@ async function main() {
35481
35941
  if (command === "metrics") {
35482
35942
  if (args2.length === 0) {
35483
35943
  console.error(colors.red("Error: metrics command requires a path argument"));
35484
- console.log(`
35944
+ console.error(`
35485
35945
  Usage: cognium-dev metrics <path> [options]`);
35486
35946
  process.exit(1);
35487
35947
  }
@@ -35501,7 +35961,7 @@ Usage: cognium-dev metrics <path> [options]`);
35501
35961
  if (command === "scan") {
35502
35962
  if (args2.length === 0) {
35503
35963
  console.error(colors.red("Error: scan command requires a path argument"));
35504
- console.log(`
35964
+ console.error(`
35505
35965
  Usage: cognium-dev scan <path> [options]`);
35506
35966
  process.exit(1);
35507
35967
  }
@@ -35518,7 +35978,8 @@ Usage: cognium-dev scan <path> [options]`);
35518
35978
  excludeTests: options["exclude-tests"] === true,
35519
35979
  excludeCwe: options["exclude-cwe"],
35520
35980
  profile: options.profile || options.p,
35521
- disablePass: options["disable-pass"]
35981
+ disablePass: options["disable-pass"],
35982
+ crossFileBudgetMs: parseCrossFileBudgetMs(options["cross-file-budget-ms"])
35522
35983
  };
35523
35984
  await runScan(targetPath, scanOptions);
35524
35985
  return;
@@ -35527,7 +35988,7 @@ Usage: cognium-dev scan <path> [options]`);
35527
35988
  showHelp();
35528
35989
  } else {
35529
35990
  console.error(colors.red(`Error: Unknown command '${command}'`));
35530
- console.log(`
35991
+ console.error(`
35531
35992
  Run 'cognium-dev --help' for usage information`);
35532
35993
  process.exit(1);
35533
35994
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cognium-dev",
3
- "version": "3.85.1",
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.85.1"
68
+ "circle-ir": "^3.89.1"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/node": "^25.5.0",