circle-ir 3.9.8 → 3.9.10

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 (35) hide show
  1. package/dist/analysis/passes/broad-catch-pass.d.ts +29 -0
  2. package/dist/analysis/passes/broad-catch-pass.js +79 -0
  3. package/dist/analysis/passes/broad-catch-pass.js.map +1 -0
  4. package/dist/analysis/passes/double-close-pass.d.ts +33 -0
  5. package/dist/analysis/passes/double-close-pass.js +109 -0
  6. package/dist/analysis/passes/double-close-pass.js.map +1 -0
  7. package/dist/analysis/passes/sink-filter-pass.js +7 -1
  8. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  9. package/dist/analysis/passes/swallowed-exception-pass.d.ts +35 -0
  10. package/dist/analysis/passes/swallowed-exception-pass.js +103 -0
  11. package/dist/analysis/passes/swallowed-exception-pass.js.map +1 -0
  12. package/dist/analysis/passes/unhandled-exception-pass.d.ts +34 -0
  13. package/dist/analysis/passes/unhandled-exception-pass.js +123 -0
  14. package/dist/analysis/passes/unhandled-exception-pass.js.map +1 -0
  15. package/dist/analysis/passes/use-after-close-pass.d.ts +30 -0
  16. package/dist/analysis/passes/use-after-close-pass.js +100 -0
  17. package/dist/analysis/passes/use-after-close-pass.js.map +1 -0
  18. package/dist/analysis/taint-matcher.js +1 -0
  19. package/dist/analysis/taint-matcher.js.map +1 -1
  20. package/dist/analyzer.d.ts +8 -3
  21. package/dist/analyzer.js +18 -3
  22. package/dist/analyzer.js.map +1 -1
  23. package/dist/browser/circle-ir.js +495 -3
  24. package/dist/core/circle-ir-core.cjs +2 -1
  25. package/dist/core/circle-ir-core.js +2 -1
  26. package/dist/graph/exception-flow-graph.d.ts +44 -0
  27. package/dist/graph/exception-flow-graph.js +75 -0
  28. package/dist/graph/exception-flow-graph.js.map +1 -0
  29. package/dist/graph/index.d.ts +1 -0
  30. package/dist/graph/index.js +1 -0
  31. package/dist/graph/index.js.map +1 -1
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +1 -0
  34. package/dist/index.js.map +1 -1
  35. package/package.json +1 -1
@@ -10483,7 +10483,8 @@ function findSinks(calls, patterns) {
10483
10483
  cwe: pattern.cwe,
10484
10484
  location,
10485
10485
  line: call.location.line,
10486
- confidence
10486
+ confidence,
10487
+ method: call.method_name
10487
10488
  });
10488
10489
  }
10489
10490
  }
@@ -11278,6 +11279,71 @@ var CodeGraph = class {
11278
11279
  }
11279
11280
  };
11280
11281
 
11282
+ // src/graph/exception-flow-graph.ts
11283
+ var ExceptionFlowGraph = class {
11284
+ /** All try/catch pairs found in the CFG. */
11285
+ pairs;
11286
+ /** Block IDs that are catch-handler entry blocks. */
11287
+ catchEntryIds;
11288
+ /** Block IDs that are try-body entry blocks. */
11289
+ tryEntryIds;
11290
+ tryCatchMap;
11291
+ // tryEntryId → [catchEntryId, …]
11292
+ catchTryMap;
11293
+ // catchEntryId → tryEntryId
11294
+ constructor(cfg, blockById) {
11295
+ this.pairs = [];
11296
+ this.catchEntryIds = /* @__PURE__ */ new Set();
11297
+ this.tryEntryIds = /* @__PURE__ */ new Set();
11298
+ this.tryCatchMap = /* @__PURE__ */ new Map();
11299
+ this.catchTryMap = /* @__PURE__ */ new Map();
11300
+ for (const edge of cfg.edges) {
11301
+ if (edge.type !== "exception") continue;
11302
+ const tryBlock = blockById.get(edge.from);
11303
+ const catchBlock = blockById.get(edge.to);
11304
+ if (!tryBlock || !catchBlock) continue;
11305
+ this.tryEntryIds.add(edge.from);
11306
+ this.catchEntryIds.add(edge.to);
11307
+ const catches = this.tryCatchMap.get(edge.from) ?? [];
11308
+ catches.push(edge.to);
11309
+ this.tryCatchMap.set(edge.from, catches);
11310
+ this.catchTryMap.set(edge.to, edge.from);
11311
+ this.pairs.push({
11312
+ tryEntryId: edge.from,
11313
+ catchEntryId: edge.to,
11314
+ tryBlock,
11315
+ catchBlock
11316
+ });
11317
+ }
11318
+ }
11319
+ /** True if at least one try/catch pair was found. */
11320
+ get hasTryCatch() {
11321
+ return this.pairs.length > 0;
11322
+ }
11323
+ /** True if the given block ID is a catch-handler entry block. */
11324
+ isCatchEntry(blockId) {
11325
+ return this.catchEntryIds.has(blockId);
11326
+ }
11327
+ /** True if the given block ID is a try-body entry block. */
11328
+ isTryEntry(blockId) {
11329
+ return this.tryEntryIds.has(blockId);
11330
+ }
11331
+ /**
11332
+ * Returns the catch-entry block IDs for the given try-entry block.
11333
+ * Multiple values mean multiple catch clauses for the same try.
11334
+ */
11335
+ catchBlocksFor(tryEntryId) {
11336
+ return this.tryCatchMap.get(tryEntryId) ?? [];
11337
+ }
11338
+ /**
11339
+ * Returns the try-entry block ID corresponding to a catch-entry block,
11340
+ * or `undefined` if the block is not a catch entry.
11341
+ */
11342
+ tryBlockFor(catchEntryId) {
11343
+ return this.catchTryMap.get(catchEntryId);
11344
+ }
11345
+ };
11346
+
11281
11347
  // src/graph/analysis-pass.ts
11282
11348
  var AnalysisPipeline = class {
11283
11349
  passes = [];
@@ -17422,7 +17488,8 @@ function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanit
17422
17488
  return sinks.filter((sink) => {
17423
17489
  const callsAtSink = callsByLine.get(sink.line) ?? [];
17424
17490
  const isInSynchronizedBlock = synchronizedLines?.has(sink.line) ?? false;
17425
- for (const call of callsAtSink) {
17491
+ const relevantCalls = sink.method ? callsAtSink.filter((c) => c.method_name === sink.method) : callsAtSink;
17492
+ for (const call of relevantCalls) {
17426
17493
  let allArgsAreClean = true;
17427
17494
  const methodName = call.in_method;
17428
17495
  for (const arg of call.arguments) {
@@ -19690,6 +19757,431 @@ var ReactInlineJsxPass = class {
19690
19757
  }
19691
19758
  };
19692
19759
 
19760
+ // src/analysis/passes/swallowed-exception-pass.ts
19761
+ var MEANINGFUL_ACTION_RE = /\b(throw|raise|log|logger|console\.(error|warn|log|debug|info)|System\.(out|err)\.|print(?:ln|f)?|warn|error|debug|info|fatal|LOGGER|LOG|logging\.(warning|error|debug|info|critical))\b|\breturn\s+\S/;
19762
+ var SwallowedExceptionPass = class {
19763
+ name = "swallowed-exception";
19764
+ category = "reliability";
19765
+ run(ctx) {
19766
+ const { graph, code, language } = ctx;
19767
+ if (language === "rust" || language === "bash") {
19768
+ return { swallowed: [] };
19769
+ }
19770
+ const { cfg } = graph.ir;
19771
+ if (cfg.blocks.length === 0) return { swallowed: [] };
19772
+ const exGraph = new ExceptionFlowGraph(cfg, graph.blockById);
19773
+ if (!exGraph.hasTryCatch) return { swallowed: [] };
19774
+ const file = graph.ir.meta.file;
19775
+ const codeLines = code.split("\n");
19776
+ const swallowed = [];
19777
+ const reported = /* @__PURE__ */ new Set();
19778
+ for (const pair of exGraph.pairs) {
19779
+ const catchLine = pair.catchBlock.start_line;
19780
+ if (reported.has(catchLine)) continue;
19781
+ const methodInfo = graph.methodAtLine(catchLine);
19782
+ const scanEnd = methodInfo ? methodInfo.method.end_line : codeLines.length;
19783
+ const catchBodyEnd = this.findCatchBodyEnd(codeLines, catchLine, scanEnd);
19784
+ let hasAction = false;
19785
+ for (let ln = catchLine; ln <= catchBodyEnd && ln <= codeLines.length; ln++) {
19786
+ if (MEANINGFUL_ACTION_RE.test(codeLines[ln - 1] ?? "")) {
19787
+ hasAction = true;
19788
+ break;
19789
+ }
19790
+ }
19791
+ if (!hasAction) {
19792
+ reported.add(catchLine);
19793
+ swallowed.push({ line: catchLine });
19794
+ const snippet = (codeLines[catchLine - 1] ?? "").trim();
19795
+ ctx.addFinding({
19796
+ id: `swallowed-exception-${file}-${catchLine}`,
19797
+ pass: this.name,
19798
+ category: this.category,
19799
+ rule_id: this.name,
19800
+ cwe: "CWE-390",
19801
+ severity: "medium",
19802
+ level: "warning",
19803
+ message: `Swallowed exception: catch block at line ${catchLine} has no throw, log, or return \u2014 the exception is silently discarded`,
19804
+ file,
19805
+ line: catchLine,
19806
+ snippet,
19807
+ fix: "At minimum log the exception, or re-throw it; never silently discard exceptions"
19808
+ });
19809
+ }
19810
+ }
19811
+ return { swallowed };
19812
+ }
19813
+ /**
19814
+ * Walks source lines starting at `startLine` counting brace depth.
19815
+ * Returns the line where the brace depth first returns to zero after
19816
+ * the opening brace (i.e., the closing brace of the catch block).
19817
+ * Capped at `maxLine`.
19818
+ */
19819
+ findCatchBodyEnd(lines, startLine, maxLine) {
19820
+ let depth = 0;
19821
+ let started = false;
19822
+ for (let ln = startLine; ln <= maxLine && ln <= lines.length; ln++) {
19823
+ const text = lines[ln - 1] ?? "";
19824
+ for (const ch of text) {
19825
+ if (ch === "{") {
19826
+ depth++;
19827
+ started = true;
19828
+ } else if (ch === "}" && started) {
19829
+ depth--;
19830
+ }
19831
+ }
19832
+ if (started && depth <= 0) return ln;
19833
+ }
19834
+ return maxLine;
19835
+ }
19836
+ };
19837
+
19838
+ // src/analysis/passes/broad-catch-pass.ts
19839
+ var JAVA_BROAD_RE = /catch\s*\(\s*(Exception|Throwable|RuntimeException|Error)\s/;
19840
+ var PYTHON_BROAD_RE = /^\s*except\s*:|except\s+(Exception|BaseException)\b/;
19841
+ var BroadCatchPass = class {
19842
+ name = "broad-catch";
19843
+ category = "reliability";
19844
+ run(ctx) {
19845
+ const { graph, code, language } = ctx;
19846
+ if (language !== "java" && language !== "python") {
19847
+ return { broadCatches: [] };
19848
+ }
19849
+ const { cfg } = graph.ir;
19850
+ if (cfg.blocks.length === 0) return { broadCatches: [] };
19851
+ const exGraph = new ExceptionFlowGraph(cfg, graph.blockById);
19852
+ if (!exGraph.hasTryCatch) return { broadCatches: [] };
19853
+ const file = graph.ir.meta.file;
19854
+ const codeLines = code.split("\n");
19855
+ const broadCatches = [];
19856
+ const reported = /* @__PURE__ */ new Set();
19857
+ const pattern = language === "java" ? JAVA_BROAD_RE : PYTHON_BROAD_RE;
19858
+ for (const pair of exGraph.pairs) {
19859
+ const catchLine = pair.catchBlock.start_line;
19860
+ if (reported.has(catchLine)) continue;
19861
+ const lineText = codeLines[catchLine - 1] ?? "";
19862
+ const match = pattern.exec(lineText);
19863
+ if (!match) continue;
19864
+ const caughtType = match[1] ?? "Exception";
19865
+ reported.add(catchLine);
19866
+ broadCatches.push({ line: catchLine, type: caughtType });
19867
+ const snippet = lineText.trim();
19868
+ ctx.addFinding({
19869
+ id: `broad-catch-${file}-${catchLine}`,
19870
+ pass: this.name,
19871
+ category: this.category,
19872
+ rule_id: this.name,
19873
+ cwe: "CWE-396",
19874
+ severity: "low",
19875
+ level: "warning",
19876
+ message: `Broad catch: catching \`${caughtType}\` at line ${catchLine} suppresses unexpected errors and hides bugs`,
19877
+ file,
19878
+ line: catchLine,
19879
+ snippet,
19880
+ fix: language === "java" ? `Catch the specific exception types your code can handle (e.g., \`IOException\`, \`SQLException\`)` : `Catch the specific exception types your code can handle (e.g., \`ValueError\`, \`KeyError\`)`,
19881
+ evidence: { caughtType }
19882
+ });
19883
+ }
19884
+ return { broadCatches };
19885
+ }
19886
+ };
19887
+
19888
+ // src/analysis/passes/unhandled-exception-pass.ts
19889
+ var JS_THROW_RE = /^\s*throw\s+/;
19890
+ var PYTHON_RAISE_RE = /^\s*raise\b/;
19891
+ var UnhandledExceptionPass = class {
19892
+ name = "unhandled-exception";
19893
+ category = "reliability";
19894
+ run(ctx) {
19895
+ const { graph, code, language } = ctx;
19896
+ if (language !== "javascript" && language !== "typescript" && language !== "python") {
19897
+ return { unhandled: [] };
19898
+ }
19899
+ const { cfg } = graph.ir;
19900
+ const file = graph.ir.meta.file;
19901
+ const codeLines = code.split("\n");
19902
+ const exGraph = new ExceptionFlowGraph(cfg, graph.blockById);
19903
+ const coveredRanges = [];
19904
+ for (const pair of exGraph.pairs) {
19905
+ if (pair.catchBlock.start_line > pair.tryBlock.start_line) {
19906
+ coveredRanges.push({
19907
+ start: pair.tryBlock.start_line,
19908
+ end: pair.catchBlock.start_line - 1
19909
+ });
19910
+ }
19911
+ }
19912
+ const catchStarts = new Set(
19913
+ exGraph.pairs.map((p) => p.catchBlock.start_line)
19914
+ );
19915
+ const throwRe = language === "python" ? PYTHON_RAISE_RE : JS_THROW_RE;
19916
+ const unhandled = [];
19917
+ const reportedMethods = /* @__PURE__ */ new Set();
19918
+ for (let ln = 1; ln <= codeLines.length; ln++) {
19919
+ const lineText = codeLines[ln - 1] ?? "";
19920
+ if (!throwRe.test(lineText)) continue;
19921
+ let inCatch = false;
19922
+ for (const cs of catchStarts) {
19923
+ if (ln >= cs) {
19924
+ inCatch = true;
19925
+ break;
19926
+ }
19927
+ }
19928
+ inCatch = false;
19929
+ for (const pair of exGraph.pairs) {
19930
+ if (ln >= pair.catchBlock.start_line) {
19931
+ const mThrow = graph.methodAtLine(ln);
19932
+ const mCatch = graph.methodAtLine(pair.catchBlock.start_line);
19933
+ if (mThrow && mCatch && mThrow.method.start_line === mCatch.method.start_line) {
19934
+ inCatch = true;
19935
+ break;
19936
+ }
19937
+ }
19938
+ }
19939
+ if (inCatch) continue;
19940
+ const isCovered = coveredRanges.some((r) => ln >= r.start && ln <= r.end);
19941
+ if (isCovered) continue;
19942
+ const methodInfo = graph.methodAtLine(ln);
19943
+ const methodKey = methodInfo ? `${methodInfo.method.start_line}-${methodInfo.method.end_line}` : `global-${ln}`;
19944
+ if (reportedMethods.has(methodKey)) continue;
19945
+ reportedMethods.add(methodKey);
19946
+ const methodName = methodInfo?.method.name ?? "<anonymous>";
19947
+ unhandled.push({ line: ln, method: methodName });
19948
+ const snippet = lineText.trim();
19949
+ ctx.addFinding({
19950
+ id: `unhandled-exception-${file}-${ln}`,
19951
+ pass: this.name,
19952
+ category: this.category,
19953
+ rule_id: this.name,
19954
+ cwe: "CWE-390",
19955
+ severity: "medium",
19956
+ level: "warning",
19957
+ message: `Unhandled exception: \`throw\` at line ${ln} in \`${methodName}\` is not inside a try/catch \u2014 callers receive an unexpected exception`,
19958
+ file,
19959
+ line: ln,
19960
+ snippet,
19961
+ fix: "Wrap throwing code in a try/catch, or document the exception in the function signature",
19962
+ evidence: { method: methodName }
19963
+ });
19964
+ }
19965
+ return { unhandled };
19966
+ }
19967
+ };
19968
+
19969
+ // src/analysis/passes/double-close-pass.ts
19970
+ var RESOURCE_CTORS2 = /* @__PURE__ */ new Set([
19971
+ "FileInputStream",
19972
+ "FileOutputStream",
19973
+ "FileReader",
19974
+ "FileWriter",
19975
+ "BufferedReader",
19976
+ "BufferedWriter",
19977
+ "PrintWriter",
19978
+ "InputStreamReader",
19979
+ "OutputStreamWriter",
19980
+ "RandomAccessFile",
19981
+ "DataInputStream",
19982
+ "DataOutputStream",
19983
+ "ObjectInputStream",
19984
+ "ObjectOutputStream",
19985
+ "ZipInputStream",
19986
+ "ZipOutputStream",
19987
+ "JarInputStream",
19988
+ "JarOutputStream",
19989
+ "GZIPInputStream",
19990
+ "GZIPOutputStream",
19991
+ "FileChannel",
19992
+ "Socket",
19993
+ "ServerSocket",
19994
+ "DatagramSocket"
19995
+ ]);
19996
+ var RESOURCE_FACTORY_METHODS2 = /* @__PURE__ */ new Set([
19997
+ "openConnection",
19998
+ "openStream",
19999
+ "newInputStream",
20000
+ "newOutputStream",
20001
+ "newBufferedReader",
20002
+ "newBufferedWriter",
20003
+ "newByteChannel",
20004
+ "open",
20005
+ "createReadStream",
20006
+ "createWriteStream",
20007
+ "createConnection"
20008
+ ]);
20009
+ var CLOSE_METHODS2 = /* @__PURE__ */ new Set([
20010
+ "close",
20011
+ "dispose",
20012
+ "shutdown",
20013
+ "disconnect",
20014
+ "release",
20015
+ "destroy",
20016
+ "free",
20017
+ "shutdownNow",
20018
+ "terminate"
20019
+ ]);
20020
+ var DoubleClosePass = class {
20021
+ name = "double-close";
20022
+ category = "reliability";
20023
+ run(ctx) {
20024
+ const { graph, code } = ctx;
20025
+ if (ctx.language === "bash") return { doubleCloses: [] };
20026
+ const file = graph.ir.meta.file;
20027
+ const codeLines = code.split("\n");
20028
+ const doubleCloses = [];
20029
+ for (const call of graph.ir.calls) {
20030
+ const name2 = call.method_name;
20031
+ const isConstructor = call.is_constructor === true && RESOURCE_CTORS2.has(name2);
20032
+ const isFactory = !call.is_constructor && RESOURCE_FACTORY_METHODS2.has(name2);
20033
+ if (!isConstructor && !isFactory) continue;
20034
+ const openLine = call.location.line;
20035
+ const defs = graph.defsAtLine(openLine);
20036
+ if (defs.length === 0) continue;
20037
+ const resourceVar = defs[0].variable;
20038
+ const methodInfo = graph.methodAtLine(openLine);
20039
+ if (!methodInfo) continue;
20040
+ const { start_line: methodStart, end_line: methodEnd } = methodInfo.method;
20041
+ const closeCalls = graph.ir.calls.filter(
20042
+ (c) => CLOSE_METHODS2.has(c.method_name) && c.receiver === resourceVar && c.location.line > openLine && c.location.line <= methodEnd
20043
+ );
20044
+ if (closeCalls.length < 2) continue;
20045
+ const closeLines = closeCalls.map((c) => c.location.line);
20046
+ const allInFinally = closeLines.every(
20047
+ (cl) => this.isInFinallyBlock(codeLines, cl, methodStart, methodEnd)
20048
+ );
20049
+ if (allInFinally) continue;
20050
+ doubleCloses.push({ openLine, closeLines, variable: resourceVar });
20051
+ const snippet = (codeLines[openLine - 1] ?? "").trim();
20052
+ const linesStr = closeLines.join(" and ");
20053
+ ctx.addFinding({
20054
+ id: `double-close-${file}-${openLine}`,
20055
+ pass: this.name,
20056
+ category: this.category,
20057
+ rule_id: this.name,
20058
+ cwe: "CWE-675",
20059
+ severity: "medium",
20060
+ level: "warning",
20061
+ message: `Double close: \`${resourceVar}\` is closed at lines ${linesStr} \u2014 closing an already-closed resource may throw`,
20062
+ file,
20063
+ line: openLine,
20064
+ snippet,
20065
+ fix: `Close the resource exactly once in a finally block; add a null/isClosed guard before the second close if closing on multiple paths`,
20066
+ evidence: { variable: resourceVar, close_lines: closeLines }
20067
+ });
20068
+ }
20069
+ return { doubleCloses };
20070
+ }
20071
+ /** True if the given line is inside a `finally` block in the method. */
20072
+ isInFinallyBlock(lines, targetLine, methodStart, methodEnd) {
20073
+ for (let ln = methodStart; ln <= targetLine && ln <= methodEnd && ln <= lines.length; ln++) {
20074
+ if (/\bfinally\b/.test(lines[ln - 1] ?? "")) return true;
20075
+ }
20076
+ return false;
20077
+ }
20078
+ };
20079
+
20080
+ // src/analysis/passes/use-after-close-pass.ts
20081
+ var RESOURCE_CTORS3 = /* @__PURE__ */ new Set([
20082
+ "FileInputStream",
20083
+ "FileOutputStream",
20084
+ "FileReader",
20085
+ "FileWriter",
20086
+ "BufferedReader",
20087
+ "BufferedWriter",
20088
+ "PrintWriter",
20089
+ "InputStreamReader",
20090
+ "OutputStreamWriter",
20091
+ "RandomAccessFile",
20092
+ "DataInputStream",
20093
+ "DataOutputStream",
20094
+ "ObjectInputStream",
20095
+ "ObjectOutputStream",
20096
+ "ZipInputStream",
20097
+ "ZipOutputStream",
20098
+ "JarInputStream",
20099
+ "JarOutputStream",
20100
+ "GZIPInputStream",
20101
+ "GZIPOutputStream",
20102
+ "FileChannel",
20103
+ "Socket",
20104
+ "ServerSocket",
20105
+ "DatagramSocket"
20106
+ ]);
20107
+ var RESOURCE_FACTORY_METHODS3 = /* @__PURE__ */ new Set([
20108
+ "openConnection",
20109
+ "openStream",
20110
+ "newInputStream",
20111
+ "newOutputStream",
20112
+ "newBufferedReader",
20113
+ "newBufferedWriter",
20114
+ "newByteChannel",
20115
+ "open",
20116
+ "createReadStream",
20117
+ "createWriteStream",
20118
+ "createConnection"
20119
+ ]);
20120
+ var CLOSE_METHODS3 = /* @__PURE__ */ new Set([
20121
+ "close",
20122
+ "dispose",
20123
+ "shutdown",
20124
+ "disconnect",
20125
+ "release",
20126
+ "destroy",
20127
+ "free",
20128
+ "shutdownNow",
20129
+ "terminate"
20130
+ ]);
20131
+ var UseAfterClosePass = class {
20132
+ name = "use-after-close";
20133
+ category = "reliability";
20134
+ run(ctx) {
20135
+ const { graph, code } = ctx;
20136
+ if (ctx.language === "bash") return { useAfterCloses: [] };
20137
+ const file = graph.ir.meta.file;
20138
+ const codeLines = code.split("\n");
20139
+ const useAfterCloses = [];
20140
+ for (const call of graph.ir.calls) {
20141
+ const name2 = call.method_name;
20142
+ const isConstructor = call.is_constructor === true && RESOURCE_CTORS3.has(name2);
20143
+ const isFactory = !call.is_constructor && RESOURCE_FACTORY_METHODS3.has(name2);
20144
+ if (!isConstructor && !isFactory) continue;
20145
+ const openLine = call.location.line;
20146
+ const defs = graph.defsAtLine(openLine);
20147
+ if (defs.length === 0) continue;
20148
+ const resourceVar = defs[0].variable;
20149
+ const methodInfo = graph.methodAtLine(openLine);
20150
+ if (!methodInfo) continue;
20151
+ const methodEnd = methodInfo.method.end_line;
20152
+ const firstClose = graph.ir.calls.filter(
20153
+ (c) => CLOSE_METHODS3.has(c.method_name) && c.receiver === resourceVar && c.location.line > openLine && c.location.line <= methodEnd
20154
+ ).sort((a, b) => a.location.line - b.location.line)[0];
20155
+ if (!firstClose) continue;
20156
+ const closeLine = firstClose.location.line;
20157
+ const usesAfterClose = graph.ir.calls.filter(
20158
+ (c) => c.receiver === resourceVar && c.location.line > closeLine && c.location.line <= methodEnd && !CLOSE_METHODS3.has(c.method_name)
20159
+ );
20160
+ for (const use of usesAfterClose) {
20161
+ const useLine = use.location.line;
20162
+ useAfterCloses.push({ openLine, closeLine, useLine, variable: resourceVar });
20163
+ const snippet = (codeLines[useLine - 1] ?? "").trim();
20164
+ ctx.addFinding({
20165
+ id: `use-after-close-${file}-${useLine}`,
20166
+ pass: this.name,
20167
+ category: this.category,
20168
+ rule_id: this.name,
20169
+ cwe: "CWE-672",
20170
+ severity: "high",
20171
+ level: "error",
20172
+ message: `Use after close: \`${resourceVar}.${use.method_name}()\` at line ${useLine} is called after \`${resourceVar}.close()\` at line ${closeLine}`,
20173
+ file,
20174
+ line: useLine,
20175
+ snippet,
20176
+ fix: `Do not use a resource after closing it; keep \`${resourceVar}\` open until all uses are complete`,
20177
+ evidence: { variable: resourceVar, close_line: closeLine, open_line: openLine }
20178
+ });
20179
+ }
20180
+ }
20181
+ return { useAfterCloses };
20182
+ }
20183
+ };
20184
+
19693
20185
  // src/analysis/metrics/passes/size-metrics-pass.ts
19694
20186
  var SizeMetricsPass = class {
19695
20187
  name = "size-metrics";
@@ -20489,7 +20981,7 @@ async function analyze(code, filePath, language, options = {}) {
20489
20981
  enriched: {}
20490
20982
  });
20491
20983
  const config = options.taintConfig ?? getDefaultConfig();
20492
- const { results, findings } = new AnalysisPipeline().add(new TaintMatcherPass()).add(new ConstantPropagationPass(tree)).add(new LanguageSourcesPass()).add(new SinkFilterPass()).add(new TaintPropagationPass()).add(new InterproceduralPass()).add(new DeadCodePass()).add(new MissingAwaitPass()).add(new NPlusOnePass()).add(new MissingPublicDocPass()).add(new TodoInProdPass()).add(new StringConcatLoopPass()).add(new SyncIoAsyncPass()).add(new UncheckedReturnPass()).add(new NullDerefPass()).add(new ResourceLeakPass()).add(new VariableShadowingPass()).add(new LeakedGlobalPass()).add(new UnusedVariablePass()).add(new DependencyFanOutPass()).add(new StaleDocRefPass()).add(new InfiniteLoopPass()).add(new DeepInheritancePass()).add(new RedundantLoopPass()).add(new UnboundedCollectionPass()).add(new SerialAwaitPass()).add(new ReactInlineJsxPass()).run(graph, code, language, config);
20984
+ const { results, findings } = new AnalysisPipeline().add(new TaintMatcherPass()).add(new ConstantPropagationPass(tree)).add(new LanguageSourcesPass()).add(new SinkFilterPass()).add(new TaintPropagationPass()).add(new InterproceduralPass()).add(new DeadCodePass()).add(new MissingAwaitPass()).add(new NPlusOnePass()).add(new MissingPublicDocPass()).add(new TodoInProdPass()).add(new StringConcatLoopPass()).add(new SyncIoAsyncPass()).add(new UncheckedReturnPass()).add(new NullDerefPass()).add(new ResourceLeakPass()).add(new VariableShadowingPass()).add(new LeakedGlobalPass()).add(new UnusedVariablePass()).add(new DependencyFanOutPass()).add(new StaleDocRefPass()).add(new InfiniteLoopPass()).add(new DeepInheritancePass()).add(new RedundantLoopPass()).add(new UnboundedCollectionPass()).add(new SerialAwaitPass()).add(new ReactInlineJsxPass()).add(new SwallowedExceptionPass()).add(new BroadCatchPass()).add(new UnhandledExceptionPass()).add(new DoubleClosePass()).add(new UseAfterClosePass()).run(graph, code, language, config);
20493
20985
  const sinkFilter = results.get("sink-filter");
20494
20986
  const interProc = results.get("interprocedural");
20495
20987
  const taint = {
@@ -10591,7 +10591,8 @@ function findSinks(calls, patterns) {
10591
10591
  cwe: pattern.cwe,
10592
10592
  location,
10593
10593
  line: call.location.line,
10594
- confidence
10594
+ confidence,
10595
+ method: call.method_name
10595
10596
  });
10596
10597
  }
10597
10598
  }
@@ -10526,7 +10526,8 @@ function findSinks(calls, patterns) {
10526
10526
  cwe: pattern.cwe,
10527
10527
  location,
10528
10528
  line: call.location.line,
10529
- confidence
10529
+ confidence,
10530
+ method: call.method_name
10530
10531
  });
10531
10532
  }
10532
10533
  }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * ExceptionFlowGraph — lightweight wrapper over CFG exception edges.
3
+ *
4
+ * The CFG builder emits edges with `type === 'exception'` connecting the
5
+ * first block of a try body (`from`) to the first block of the corresponding
6
+ * catch handler (`to`). This class indexes those edges so exception-aware
7
+ * passes can query try/catch structure without re-scanning the edge list.
8
+ */
9
+ import type { CFG, CFGBlock } from '../types/index.js';
10
+ export interface TryCatchInfo {
11
+ tryEntryId: number;
12
+ catchEntryId: number;
13
+ /** First block of the try body. */
14
+ tryBlock: CFGBlock;
15
+ /** First block of the catch handler. */
16
+ catchBlock: CFGBlock;
17
+ }
18
+ export declare class ExceptionFlowGraph {
19
+ /** All try/catch pairs found in the CFG. */
20
+ readonly pairs: TryCatchInfo[];
21
+ /** Block IDs that are catch-handler entry blocks. */
22
+ readonly catchEntryIds: Set<number>;
23
+ /** Block IDs that are try-body entry blocks. */
24
+ readonly tryEntryIds: Set<number>;
25
+ private readonly tryCatchMap;
26
+ private readonly catchTryMap;
27
+ constructor(cfg: CFG, blockById: Map<number, CFGBlock>);
28
+ /** True if at least one try/catch pair was found. */
29
+ get hasTryCatch(): boolean;
30
+ /** True if the given block ID is a catch-handler entry block. */
31
+ isCatchEntry(blockId: number): boolean;
32
+ /** True if the given block ID is a try-body entry block. */
33
+ isTryEntry(blockId: number): boolean;
34
+ /**
35
+ * Returns the catch-entry block IDs for the given try-entry block.
36
+ * Multiple values mean multiple catch clauses for the same try.
37
+ */
38
+ catchBlocksFor(tryEntryId: number): number[];
39
+ /**
40
+ * Returns the try-entry block ID corresponding to a catch-entry block,
41
+ * or `undefined` if the block is not a catch entry.
42
+ */
43
+ tryBlockFor(catchEntryId: number): number | undefined;
44
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * ExceptionFlowGraph — lightweight wrapper over CFG exception edges.
3
+ *
4
+ * The CFG builder emits edges with `type === 'exception'` connecting the
5
+ * first block of a try body (`from`) to the first block of the corresponding
6
+ * catch handler (`to`). This class indexes those edges so exception-aware
7
+ * passes can query try/catch structure without re-scanning the edge list.
8
+ */
9
+ // ---------------------------------------------------------------------------
10
+ // ExceptionFlowGraph
11
+ // ---------------------------------------------------------------------------
12
+ export class ExceptionFlowGraph {
13
+ /** All try/catch pairs found in the CFG. */
14
+ pairs;
15
+ /** Block IDs that are catch-handler entry blocks. */
16
+ catchEntryIds;
17
+ /** Block IDs that are try-body entry blocks. */
18
+ tryEntryIds;
19
+ tryCatchMap; // tryEntryId → [catchEntryId, …]
20
+ catchTryMap; // catchEntryId → tryEntryId
21
+ constructor(cfg, blockById) {
22
+ this.pairs = [];
23
+ this.catchEntryIds = new Set();
24
+ this.tryEntryIds = new Set();
25
+ this.tryCatchMap = new Map();
26
+ this.catchTryMap = new Map();
27
+ for (const edge of cfg.edges) {
28
+ if (edge.type !== 'exception')
29
+ continue;
30
+ const tryBlock = blockById.get(edge.from);
31
+ const catchBlock = blockById.get(edge.to);
32
+ if (!tryBlock || !catchBlock)
33
+ continue;
34
+ this.tryEntryIds.add(edge.from);
35
+ this.catchEntryIds.add(edge.to);
36
+ const catches = this.tryCatchMap.get(edge.from) ?? [];
37
+ catches.push(edge.to);
38
+ this.tryCatchMap.set(edge.from, catches);
39
+ this.catchTryMap.set(edge.to, edge.from);
40
+ this.pairs.push({
41
+ tryEntryId: edge.from,
42
+ catchEntryId: edge.to,
43
+ tryBlock,
44
+ catchBlock,
45
+ });
46
+ }
47
+ }
48
+ /** True if at least one try/catch pair was found. */
49
+ get hasTryCatch() {
50
+ return this.pairs.length > 0;
51
+ }
52
+ /** True if the given block ID is a catch-handler entry block. */
53
+ isCatchEntry(blockId) {
54
+ return this.catchEntryIds.has(blockId);
55
+ }
56
+ /** True if the given block ID is a try-body entry block. */
57
+ isTryEntry(blockId) {
58
+ return this.tryEntryIds.has(blockId);
59
+ }
60
+ /**
61
+ * Returns the catch-entry block IDs for the given try-entry block.
62
+ * Multiple values mean multiple catch clauses for the same try.
63
+ */
64
+ catchBlocksFor(tryEntryId) {
65
+ return this.tryCatchMap.get(tryEntryId) ?? [];
66
+ }
67
+ /**
68
+ * Returns the try-entry block ID corresponding to a catch-entry block,
69
+ * or `undefined` if the block is not a catch entry.
70
+ */
71
+ tryBlockFor(catchEntryId) {
72
+ return this.catchTryMap.get(catchEntryId);
73
+ }
74
+ }
75
+ //# sourceMappingURL=exception-flow-graph.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exception-flow-graph.js","sourceRoot":"","sources":["../../src/graph/exception-flow-graph.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAiBH,8EAA8E;AAC9E,qBAAqB;AACrB,8EAA8E;AAE9E,MAAM,OAAO,kBAAkB;IAC7B,4CAA4C;IACnC,KAAK,CAAiB;IAE/B,qDAAqD;IAC5C,aAAa,CAAc;IAEpC,gDAAgD;IACvC,WAAW,CAAc;IAEjB,WAAW,CAAwB,CAAC,iCAAiC;IACrE,WAAW,CAAsB,CAAG,4BAA4B;IAEjF,YAAY,GAAQ,EAAE,SAAgC;QACpD,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;QAChB,IAAI,CAAC,aAAa,GAAG,IAAI,GAAG,EAAE,CAAC;QAC/B,IAAI,CAAC,WAAW,GAAG,IAAI,GAAG,EAAE,CAAC;QAC7B,IAAI,CAAC,WAAW,GAAG,IAAI,GAAG,EAAE,CAAC;QAC7B,IAAI,CAAC,WAAW,GAAG,IAAI,GAAG,EAAE,CAAC;QAE7B,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;YAC7B,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW;gBAAE,SAAS;YAExC,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC1C,MAAM,UAAU,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAC1C,IAAI,CAAC,QAAQ,IAAI,CAAC,UAAU;gBAAE,SAAS;YAEvC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAChC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAEhC,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YACtD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACtB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YAEzC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YAEzC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;gBACd,UAAU,EAAE,IAAI,CAAC,IAAI;gBACrB,YAAY,EAAE,IAAI,CAAC,EAAE;gBACrB,QAAQ;gBACR,UAAU;aACX,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,qDAAqD;IACrD,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;IAC/B,CAAC;IAED,iEAAiE;IACjE,YAAY,CAAC,OAAe;QAC1B,OAAO,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACzC,CAAC;IAED,4DAA4D;IAC5D,UAAU,CAAC,OAAe;QACxB,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACvC,CAAC;IAED;;;OAGG;IACH,cAAc,CAAC,UAAkB;QAC/B,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;IAChD,CAAC;IAED;;;OAGG;IACH,WAAW,CAAC,YAAoB;QAC9B,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAC5C,CAAC;CACF"}
@@ -2,4 +2,5 @@ export { CodeGraph } from './code-graph.js';
2
2
  export { ProjectGraph } from './project-graph.js';
3
3
  export { ImportGraph } from './import-graph.js';
4
4
  export { DominatorGraph } from './dominator-graph.js';
5
+ export { ExceptionFlowGraph, type TryCatchInfo } from './exception-flow-graph.js';
5
6
  export { AnalysisPipeline, type AnalysisPass, type PassContext, type PipelineRunResult, } from './analysis-pass.js';