circle-ir 3.8.3 → 3.9.7

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 (159) hide show
  1. package/README.md +82 -5
  2. package/dist/analysis/config-loader.js +1 -1
  3. package/dist/analysis/config-loader.js.map +1 -1
  4. package/dist/analysis/dfg-verifier.d.ts +3 -14
  5. package/dist/analysis/dfg-verifier.js +43 -74
  6. package/dist/analysis/dfg-verifier.js.map +1 -1
  7. package/dist/analysis/interprocedural.d.ts +5 -1
  8. package/dist/analysis/interprocedural.js +62 -60
  9. package/dist/analysis/interprocedural.js.map +1 -1
  10. package/dist/analysis/metrics/index.d.ts +2 -0
  11. package/dist/analysis/metrics/index.js +2 -0
  12. package/dist/analysis/metrics/index.js.map +1 -0
  13. package/dist/analysis/metrics/metric-pass.d.ts +27 -0
  14. package/dist/analysis/metrics/metric-pass.js +2 -0
  15. package/dist/analysis/metrics/metric-pass.js.map +1 -0
  16. package/dist/analysis/metrics/metric-runner.d.ts +21 -0
  17. package/dist/analysis/metrics/metric-runner.js +47 -0
  18. package/dist/analysis/metrics/metric-runner.js.map +1 -0
  19. package/dist/analysis/metrics/passes/cohesion-metrics-pass.d.ts +21 -0
  20. package/dist/analysis/metrics/passes/cohesion-metrics-pass.js +100 -0
  21. package/dist/analysis/metrics/passes/cohesion-metrics-pass.js.map +1 -0
  22. package/dist/analysis/metrics/passes/complexity-metrics-pass.d.ts +15 -0
  23. package/dist/analysis/metrics/passes/complexity-metrics-pass.js +76 -0
  24. package/dist/analysis/metrics/passes/complexity-metrics-pass.js.map +1 -0
  25. package/dist/analysis/metrics/passes/composite-metrics-pass.d.ts +17 -0
  26. package/dist/analysis/metrics/passes/composite-metrics-pass.js +77 -0
  27. package/dist/analysis/metrics/passes/composite-metrics-pass.js.map +1 -0
  28. package/dist/analysis/metrics/passes/coupling-metrics-pass.d.ts +19 -0
  29. package/dist/analysis/metrics/passes/coupling-metrics-pass.js +94 -0
  30. package/dist/analysis/metrics/passes/coupling-metrics-pass.js.map +1 -0
  31. package/dist/analysis/metrics/passes/data-flow-metrics-pass.d.ts +14 -0
  32. package/dist/analysis/metrics/passes/data-flow-metrics-pass.js +25 -0
  33. package/dist/analysis/metrics/passes/data-flow-metrics-pass.js.map +1 -0
  34. package/dist/analysis/metrics/passes/documentation-metrics-pass.d.ts +15 -0
  35. package/dist/analysis/metrics/passes/documentation-metrics-pass.js +64 -0
  36. package/dist/analysis/metrics/passes/documentation-metrics-pass.js.map +1 -0
  37. package/dist/analysis/metrics/passes/halstead-metrics-pass.d.ts +16 -0
  38. package/dist/analysis/metrics/passes/halstead-metrics-pass.js +95 -0
  39. package/dist/analysis/metrics/passes/halstead-metrics-pass.js.map +1 -0
  40. package/dist/analysis/metrics/passes/inheritance-metrics-pass.d.ts +18 -0
  41. package/dist/analysis/metrics/passes/inheritance-metrics-pass.js +73 -0
  42. package/dist/analysis/metrics/passes/inheritance-metrics-pass.js.map +1 -0
  43. package/dist/analysis/metrics/passes/size-metrics-pass.d.ts +11 -0
  44. package/dist/analysis/metrics/passes/size-metrics-pass.js +64 -0
  45. package/dist/analysis/metrics/passes/size-metrics-pass.js.map +1 -0
  46. package/dist/analysis/passes/circular-dependency-pass.d.ts +18 -0
  47. package/dist/analysis/passes/circular-dependency-pass.js +39 -0
  48. package/dist/analysis/passes/circular-dependency-pass.js.map +1 -0
  49. package/dist/analysis/passes/constant-propagation-pass.d.ts +22 -0
  50. package/dist/analysis/passes/constant-propagation-pass.js +44 -0
  51. package/dist/analysis/passes/constant-propagation-pass.js.map +1 -0
  52. package/dist/analysis/passes/cross-file-pass.d.ts +27 -0
  53. package/dist/analysis/passes/cross-file-pass.js +102 -0
  54. package/dist/analysis/passes/cross-file-pass.js.map +1 -0
  55. package/dist/analysis/passes/dead-code-pass.d.ts +25 -0
  56. package/dist/analysis/passes/dead-code-pass.js +117 -0
  57. package/dist/analysis/passes/dead-code-pass.js.map +1 -0
  58. package/dist/analysis/passes/dependency-fan-out-pass.d.ts +19 -0
  59. package/dist/analysis/passes/dependency-fan-out-pass.js +35 -0
  60. package/dist/analysis/passes/dependency-fan-out-pass.js.map +1 -0
  61. package/dist/analysis/passes/interprocedural-pass.d.ts +29 -0
  62. package/dist/analysis/passes/interprocedural-pass.js +169 -0
  63. package/dist/analysis/passes/interprocedural-pass.js.map +1 -0
  64. package/dist/analysis/passes/language-sources-pass.d.ts +76 -0
  65. package/dist/analysis/passes/language-sources-pass.js +491 -0
  66. package/dist/analysis/passes/language-sources-pass.js.map +1 -0
  67. package/dist/analysis/passes/leaked-global-pass.d.ts +34 -0
  68. package/dist/analysis/passes/leaked-global-pass.js +108 -0
  69. package/dist/analysis/passes/leaked-global-pass.js.map +1 -0
  70. package/dist/analysis/passes/missing-await-pass.d.ts +29 -0
  71. package/dist/analysis/passes/missing-await-pass.js +90 -0
  72. package/dist/analysis/passes/missing-await-pass.js.map +1 -0
  73. package/dist/analysis/passes/missing-public-doc-pass.d.ts +35 -0
  74. package/dist/analysis/passes/missing-public-doc-pass.js +148 -0
  75. package/dist/analysis/passes/missing-public-doc-pass.js.map +1 -0
  76. package/dist/analysis/passes/n-plus-one-pass.d.ts +29 -0
  77. package/dist/analysis/passes/n-plus-one-pass.js +100 -0
  78. package/dist/analysis/passes/n-plus-one-pass.js.map +1 -0
  79. package/dist/analysis/passes/null-deref-pass.d.ts +32 -0
  80. package/dist/analysis/passes/null-deref-pass.js +130 -0
  81. package/dist/analysis/passes/null-deref-pass.js.map +1 -0
  82. package/dist/analysis/passes/orphan-module-pass.d.ts +21 -0
  83. package/dist/analysis/passes/orphan-module-pass.js +38 -0
  84. package/dist/analysis/passes/orphan-module-pass.js.map +1 -0
  85. package/dist/analysis/passes/resource-leak-pass.d.ts +43 -0
  86. package/dist/analysis/passes/resource-leak-pass.js +156 -0
  87. package/dist/analysis/passes/resource-leak-pass.js.map +1 -0
  88. package/dist/analysis/passes/sink-filter-pass.d.ts +39 -0
  89. package/dist/analysis/passes/sink-filter-pass.js +231 -0
  90. package/dist/analysis/passes/sink-filter-pass.js.map +1 -0
  91. package/dist/analysis/passes/stale-doc-ref-pass.d.ts +21 -0
  92. package/dist/analysis/passes/stale-doc-ref-pass.js +96 -0
  93. package/dist/analysis/passes/stale-doc-ref-pass.js.map +1 -0
  94. package/dist/analysis/passes/string-concat-loop-pass.d.ts +26 -0
  95. package/dist/analysis/passes/string-concat-loop-pass.js +87 -0
  96. package/dist/analysis/passes/string-concat-loop-pass.js.map +1 -0
  97. package/dist/analysis/passes/sync-io-async-pass.d.ts +28 -0
  98. package/dist/analysis/passes/sync-io-async-pass.js +80 -0
  99. package/dist/analysis/passes/sync-io-async-pass.js.map +1 -0
  100. package/dist/analysis/passes/taint-matcher-pass.d.ts +24 -0
  101. package/dist/analysis/passes/taint-matcher-pass.js +71 -0
  102. package/dist/analysis/passes/taint-matcher-pass.js.map +1 -0
  103. package/dist/analysis/passes/taint-propagation-pass.d.ts +22 -0
  104. package/dist/analysis/passes/taint-propagation-pass.js +266 -0
  105. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -0
  106. package/dist/analysis/passes/todo-in-prod-pass.d.ts +28 -0
  107. package/dist/analysis/passes/todo-in-prod-pass.js +71 -0
  108. package/dist/analysis/passes/todo-in-prod-pass.js.map +1 -0
  109. package/dist/analysis/passes/unchecked-return-pass.d.ts +34 -0
  110. package/dist/analysis/passes/unchecked-return-pass.js +106 -0
  111. package/dist/analysis/passes/unchecked-return-pass.js.map +1 -0
  112. package/dist/analysis/passes/unused-variable-pass.d.ts +36 -0
  113. package/dist/analysis/passes/unused-variable-pass.js +150 -0
  114. package/dist/analysis/passes/unused-variable-pass.js.map +1 -0
  115. package/dist/analysis/passes/variable-shadowing-pass.d.ts +41 -0
  116. package/dist/analysis/passes/variable-shadowing-pass.js +211 -0
  117. package/dist/analysis/passes/variable-shadowing-pass.js.map +1 -0
  118. package/dist/analysis/path-finder.d.ts +3 -13
  119. package/dist/analysis/path-finder.js +48 -63
  120. package/dist/analysis/path-finder.js.map +1 -1
  121. package/dist/analysis/taint-matcher.js +8 -1
  122. package/dist/analysis/taint-matcher.js.map +1 -1
  123. package/dist/analysis/taint-propagation.d.ts +5 -1
  124. package/dist/analysis/taint-propagation.js +44 -41
  125. package/dist/analysis/taint-propagation.js.map +1 -1
  126. package/dist/analyzer.d.ts +42 -1
  127. package/dist/analyzer.js +234 -1476
  128. package/dist/analyzer.js.map +1 -1
  129. package/dist/browser/circle-ir.js +3414 -1272
  130. package/dist/core/circle-ir-core.cjs +361 -107
  131. package/dist/core/circle-ir-core.js +361 -107
  132. package/dist/core/extractors/imports.js +18 -0
  133. package/dist/core/extractors/imports.js.map +1 -1
  134. package/dist/graph/analysis-pass.d.ts +68 -0
  135. package/dist/graph/analysis-pass.js +51 -0
  136. package/dist/graph/analysis-pass.js.map +1 -0
  137. package/dist/graph/code-graph.d.ts +92 -0
  138. package/dist/graph/code-graph.js +262 -0
  139. package/dist/graph/code-graph.js.map +1 -0
  140. package/dist/graph/import-graph.d.ts +33 -0
  141. package/dist/graph/import-graph.js +170 -0
  142. package/dist/graph/import-graph.js.map +1 -0
  143. package/dist/graph/index.d.ts +4 -0
  144. package/dist/graph/index.js +5 -0
  145. package/dist/graph/index.js.map +1 -0
  146. package/dist/graph/project-graph.d.ts +43 -0
  147. package/dist/graph/project-graph.js +80 -0
  148. package/dist/graph/project-graph.js.map +1 -0
  149. package/dist/graph/scope-graph.d.ts +63 -0
  150. package/dist/graph/scope-graph.js +89 -0
  151. package/dist/graph/scope-graph.js.map +1 -0
  152. package/dist/index.d.ts +2 -2
  153. package/dist/index.js +1 -1
  154. package/dist/index.js.map +1 -1
  155. package/dist/resolution/cross-file.js +52 -19
  156. package/dist/resolution/cross-file.js.map +1 -1
  157. package/dist/types/index.d.ts +151 -0
  158. package/docs/SPEC.md +10 -6
  159. package/package.json +3 -2
@@ -6865,6 +6865,20 @@ function extractJavaScriptImports(tree) {
6865
6865
  const importInfos = extractJSImportInfo(importStmt);
6866
6866
  imports.push(...importInfos);
6867
6867
  }
6868
+ const exportStatements = findNodes(tree.rootNode, "export_statement");
6869
+ for (const exportStmt of exportStatements) {
6870
+ const sourceNode = exportStmt.childForFieldName("source");
6871
+ if (!sourceNode) continue;
6872
+ const fromPackage = getNodeText(sourceNode).replace(/['"]/g, "");
6873
+ if (!fromPackage) continue;
6874
+ imports.push({
6875
+ imported_name: "*",
6876
+ from_package: fromPackage,
6877
+ alias: null,
6878
+ is_wildcard: true,
6879
+ line_number: exportStmt.startPosition.row + 1
6880
+ });
6881
+ }
6868
6882
  const requireCalls = findRequireCalls(tree);
6869
6883
  imports.push(...requireCalls);
6870
6884
  return imports;
@@ -9631,7 +9645,7 @@ var DEFAULT_SINKS = [
9631
9645
  // Jenkins/CI Pipeline execution
9632
9646
  { method: "executeScript", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
9633
9647
  { method: "runScript", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
9634
- { method: "evaluate", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
9648
+ { method: "evaluate", class: "ScriptEngine", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
9635
9649
  { method: "execute", class: "Script", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [] },
9636
9650
  { method: "run", class: "Script", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [] },
9637
9651
  { method: "checkout", class: "SCM", type: "code_injection", cwe: "CWE-94", severity: "high", arg_positions: [0] },
@@ -10591,7 +10605,10 @@ function matchesSourcePattern(call, pattern) {
10591
10605
  return false;
10592
10606
  }
10593
10607
  if (pattern.class && pattern.class !== "constructor") {
10594
- if (call.receiver && !receiverMightBeClass(call.receiver, pattern.class)) {
10608
+ if (!call.receiver) {
10609
+ return false;
10610
+ }
10611
+ if (!receiverMightBeClass(call.receiver, pattern.class)) {
10595
10612
  return false;
10596
10613
  }
10597
10614
  }
@@ -10950,38 +10967,296 @@ function formatSanitizerMethod(call) {
10950
10967
  return `${call.method_name}()`;
10951
10968
  }
10952
10969
 
10953
- // src/analysis/taint-propagation.ts
10954
- function propagateTaint(dfg, calls, sources, sinks, sanitizers) {
10955
- const taintedVars = [];
10956
- const flows = [];
10957
- const reachableSinks = /* @__PURE__ */ new Map();
10958
- const defById = /* @__PURE__ */ new Map();
10959
- const defsByLine = /* @__PURE__ */ new Map();
10960
- const usesByLine = /* @__PURE__ */ new Map();
10961
- const callsByLine = /* @__PURE__ */ new Map();
10962
- const sanitizersByLine = /* @__PURE__ */ new Map();
10963
- for (const def of dfg.defs) {
10964
- defById.set(def.id, def);
10965
- const existing = defsByLine.get(def.line) ?? [];
10966
- existing.push(def);
10967
- defsByLine.set(def.line, existing);
10970
+ // src/graph/code-graph.ts
10971
+ var CodeGraph = class {
10972
+ ir;
10973
+ constructor(ir) {
10974
+ this.ir = ir;
10975
+ }
10976
+ // ---------------------------------------------------------------------------
10977
+ // DFG indexes
10978
+ // ---------------------------------------------------------------------------
10979
+ _defById = null;
10980
+ get defById() {
10981
+ if (!this._defById) {
10982
+ this._defById = /* @__PURE__ */ new Map();
10983
+ for (const def of this.ir.dfg.defs) {
10984
+ this._defById.set(def.id, def);
10985
+ }
10986
+ }
10987
+ return this._defById;
10988
+ }
10989
+ _defsByLine = null;
10990
+ get defsByLine() {
10991
+ if (!this._defsByLine) {
10992
+ this._defsByLine = /* @__PURE__ */ new Map();
10993
+ for (const def of this.ir.dfg.defs) {
10994
+ const arr = this._defsByLine.get(def.line) ?? [];
10995
+ arr.push(def);
10996
+ this._defsByLine.set(def.line, arr);
10997
+ }
10998
+ }
10999
+ return this._defsByLine;
11000
+ }
11001
+ _defsByVar = null;
11002
+ get defsByVar() {
11003
+ if (!this._defsByVar) {
11004
+ this._defsByVar = /* @__PURE__ */ new Map();
11005
+ for (const def of this.ir.dfg.defs) {
11006
+ const arr = this._defsByVar.get(def.variable) ?? [];
11007
+ arr.push(def);
11008
+ this._defsByVar.set(def.variable, arr);
11009
+ }
11010
+ }
11011
+ return this._defsByVar;
11012
+ }
11013
+ _usesByLine = null;
11014
+ get usesByLine() {
11015
+ if (!this._usesByLine) {
11016
+ this._usesByLine = /* @__PURE__ */ new Map();
11017
+ for (const use of this.ir.dfg.uses) {
11018
+ const arr = this._usesByLine.get(use.line) ?? [];
11019
+ arr.push(use);
11020
+ this._usesByLine.set(use.line, arr);
11021
+ }
11022
+ }
11023
+ return this._usesByLine;
11024
+ }
11025
+ _usesByDefId = null;
11026
+ get usesByDefId() {
11027
+ if (!this._usesByDefId) {
11028
+ this._usesByDefId = /* @__PURE__ */ new Map();
11029
+ for (const use of this.ir.dfg.uses) {
11030
+ if (use.def_id !== null) {
11031
+ const arr = this._usesByDefId.get(use.def_id) ?? [];
11032
+ arr.push(use);
11033
+ this._usesByDefId.set(use.def_id, arr);
11034
+ }
11035
+ }
11036
+ }
11037
+ return this._usesByDefId;
11038
+ }
11039
+ _chainsByFromDef = null;
11040
+ get chainsByFromDef() {
11041
+ if (!this._chainsByFromDef) {
11042
+ this._chainsByFromDef = /* @__PURE__ */ new Map();
11043
+ for (const chain of this.ir.dfg.chains ?? []) {
11044
+ const arr = this._chainsByFromDef.get(chain.from_def) ?? [];
11045
+ arr.push(chain);
11046
+ this._chainsByFromDef.set(chain.from_def, arr);
11047
+ }
11048
+ }
11049
+ return this._chainsByFromDef;
11050
+ }
11051
+ // ---------------------------------------------------------------------------
11052
+ // Call indexes
11053
+ // ---------------------------------------------------------------------------
11054
+ _callsByLine = null;
11055
+ get callsByLine() {
11056
+ if (!this._callsByLine) {
11057
+ this._callsByLine = /* @__PURE__ */ new Map();
11058
+ for (const call of this.ir.calls) {
11059
+ const arr = this._callsByLine.get(call.location.line) ?? [];
11060
+ arr.push(call);
11061
+ this._callsByLine.set(call.location.line, arr);
11062
+ }
11063
+ }
11064
+ return this._callsByLine;
11065
+ }
11066
+ _callsByMethod = null;
11067
+ get callsByMethod() {
11068
+ if (!this._callsByMethod) {
11069
+ this._callsByMethod = /* @__PURE__ */ new Map();
11070
+ for (const call of this.ir.calls) {
11071
+ const arr = this._callsByMethod.get(call.method_name) ?? [];
11072
+ arr.push(call);
11073
+ this._callsByMethod.set(call.method_name, arr);
11074
+ }
11075
+ }
11076
+ return this._callsByMethod;
10968
11077
  }
10969
- for (const use of dfg.uses) {
10970
- const existing = usesByLine.get(use.line) ?? [];
10971
- existing.push(use);
10972
- usesByLine.set(use.line, existing);
11078
+ // ---------------------------------------------------------------------------
11079
+ // Type / method indexes
11080
+ // ---------------------------------------------------------------------------
11081
+ _methodsByName = null;
11082
+ get methodsByName() {
11083
+ if (!this._methodsByName) {
11084
+ this._methodsByName = /* @__PURE__ */ new Map();
11085
+ for (const type of this.ir.types) {
11086
+ for (const method of type.methods) {
11087
+ const arr = this._methodsByName.get(method.name) ?? [];
11088
+ arr.push({ type, method });
11089
+ this._methodsByName.set(method.name, arr);
11090
+ }
11091
+ }
11092
+ }
11093
+ return this._methodsByName;
10973
11094
  }
10974
- for (const call of calls) {
10975
- const existing = callsByLine.get(call.location.line) ?? [];
10976
- existing.push(call);
10977
- callsByLine.set(call.location.line, existing);
11095
+ /**
11096
+ * Returns the TypeInfo + MethodInfo whose line range contains `line`, or null.
11097
+ * Used by passes that need the enclosing method context for a given line.
11098
+ */
11099
+ methodAtLine(line) {
11100
+ for (const type of this.ir.types) {
11101
+ for (const method of type.methods) {
11102
+ if (line >= method.start_line && line <= method.end_line) {
11103
+ return { type, method };
11104
+ }
11105
+ }
11106
+ }
11107
+ return null;
11108
+ }
11109
+ // ---------------------------------------------------------------------------
11110
+ // Taint indexes
11111
+ // ---------------------------------------------------------------------------
11112
+ _sanitizersByLine = null;
11113
+ get sanitizersByLine() {
11114
+ if (!this._sanitizersByLine) {
11115
+ this._sanitizersByLine = /* @__PURE__ */ new Map();
11116
+ for (const san of this.ir.taint.sanitizers ?? []) {
11117
+ const arr = this._sanitizersByLine.get(san.line) ?? [];
11118
+ arr.push(san);
11119
+ this._sanitizersByLine.set(san.line, arr);
11120
+ }
11121
+ }
11122
+ return this._sanitizersByLine;
11123
+ }
11124
+ // ---------------------------------------------------------------------------
11125
+ // Query primitives
11126
+ // ---------------------------------------------------------------------------
11127
+ /** All DFGDefs at a given line. Returns [] if none. */
11128
+ defsAtLine(line) {
11129
+ return this.defsByLine.get(line) ?? [];
11130
+ }
11131
+ /** All DFGUses at a given line. Returns [] if none. */
11132
+ usesAtLine(line) {
11133
+ return this.usesByLine.get(line) ?? [];
11134
+ }
11135
+ /** All DFGUses that reach a specific definition ID. Returns [] if none. */
11136
+ usesOfDef(defId) {
11137
+ return this.usesByDefId.get(defId) ?? [];
10978
11138
  }
10979
- for (const san of sanitizers) {
10980
- const existing = sanitizersByLine.get(san.line) ?? [];
10981
- existing.push(san);
10982
- sanitizersByLine.set(san.line, existing);
11139
+ /** All CallInfos at a given line. Returns [] if none. */
11140
+ callsAtLine(line) {
11141
+ return this.callsByLine.get(line) ?? [];
11142
+ }
11143
+ /** DFGChains outgoing from a definition ID. Returns [] if none. */
11144
+ chainsFrom(defId) {
11145
+ return this.chainsByFromDef.get(defId) ?? [];
11146
+ }
11147
+ /**
11148
+ * All definitions of `variable` that appear strictly after `afterLine`
11149
+ * and at or before `upToLine`. Used to detect whether a variable is
11150
+ * redefined between a taint source and a sink.
11151
+ */
11152
+ laterDefsOfVar(variable, afterLine, upToLine) {
11153
+ return (this.defsByVar.get(variable) ?? []).filter(
11154
+ (d) => d.line > afterLine && d.line <= upToLine
11155
+ );
10983
11156
  }
10984
- const rawInitialTaint = findInitialTaint(sources, dfg, callsByLine, defsByLine);
11157
+ // ---------------------------------------------------------------------------
11158
+ // CFG indexes
11159
+ // ---------------------------------------------------------------------------
11160
+ _blockById = null;
11161
+ get blockById() {
11162
+ if (!this._blockById) {
11163
+ this._blockById = /* @__PURE__ */ new Map();
11164
+ for (const block of this.ir.cfg.blocks) {
11165
+ this._blockById.set(block.id, block);
11166
+ }
11167
+ }
11168
+ return this._blockById;
11169
+ }
11170
+ /**
11171
+ * Returns the line range of each detected loop body in the file.
11172
+ *
11173
+ * A loop is identified by CFG back-edges (type = "back"). For each back edge
11174
+ * `A → B`, B is the loop header and A is the last block before the back-jump.
11175
+ * The loop body spans from `header.start_line` to `A.end_line` (inclusive).
11176
+ *
11177
+ * Returns `{ start_line, end_line }` — one entry per back-edge.
11178
+ * Overlapping ranges are returned separately; callers can merge as needed.
11179
+ *
11180
+ * Usage: check whether line L is inside any loop with
11181
+ * `graph.loopBodies().some(r => L >= r.start_line && L <= r.end_line)`
11182
+ */
11183
+ loopBodies() {
11184
+ const loops = [];
11185
+ for (const edge of this.ir.cfg.edges) {
11186
+ if (edge.type !== "back") continue;
11187
+ const header = this.blockById.get(edge.to);
11188
+ const tail = this.blockById.get(edge.from);
11189
+ if (header && tail) {
11190
+ loops.push({ start_line: header.start_line, end_line: tail.end_line });
11191
+ }
11192
+ }
11193
+ return loops;
11194
+ }
11195
+ /**
11196
+ * Propagate a set of tainted definition IDs through DFGChains to a fixpoint.
11197
+ *
11198
+ * Returns a new Set (does not mutate the input). Each chain edge
11199
+ * `from_def → to_def` spreads taint: if `from_def` is tainted, `to_def`
11200
+ * becomes tainted. Iterates until no new IDs are added.
11201
+ */
11202
+ propagateTaintedDefIds(seed) {
11203
+ const result = new Set(seed);
11204
+ let changed = true;
11205
+ while (changed) {
11206
+ changed = false;
11207
+ for (const [fromDef, chains] of this.chainsByFromDef) {
11208
+ if (!result.has(fromDef)) continue;
11209
+ for (const chain of chains) {
11210
+ if (!result.has(chain.to_def)) {
11211
+ result.add(chain.to_def);
11212
+ changed = true;
11213
+ }
11214
+ }
11215
+ }
11216
+ }
11217
+ return result;
11218
+ }
11219
+ };
11220
+
11221
+ // src/analysis/taint-propagation.ts
11222
+ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanitizers, sanitizersArg) {
11223
+ let graph;
11224
+ let sources;
11225
+ let sinks;
11226
+ let sanitizers;
11227
+ if (graphOrDfg instanceof CodeGraph) {
11228
+ graph = graphOrDfg;
11229
+ sources = callsOrSources;
11230
+ sinks = sourcesOrSinks;
11231
+ sanitizers = sinksOrSanitizers;
11232
+ } else {
11233
+ const dfg = graphOrDfg;
11234
+ const calls = callsOrSources;
11235
+ sources = sourcesOrSinks;
11236
+ sinks = sinksOrSanitizers;
11237
+ sanitizers = sanitizersArg ?? [];
11238
+ graph = new CodeGraph({
11239
+ meta: { circle_ir: "3.0", file: "", language: "java", loc: 0, hash: "" },
11240
+ types: [],
11241
+ calls,
11242
+ cfg: { blocks: [], edges: [] },
11243
+ dfg,
11244
+ taint: { sources: [], sinks: [], sanitizers },
11245
+ imports: [],
11246
+ exports: [],
11247
+ unresolved: [],
11248
+ enriched: {}
11249
+ });
11250
+ }
11251
+ const taintedVars = [];
11252
+ const flows = [];
11253
+ const reachableSinks = /* @__PURE__ */ new Map();
11254
+ const defsByLine = graph.defsByLine;
11255
+ const usesByLine = graph.usesByLine;
11256
+ const callsByLine = graph.callsByLine;
11257
+ const sanitizersByLine = graph.sanitizersByLine;
11258
+ const defById = graph.defById;
11259
+ const rawInitialTaint = findInitialTaint(sources, callsByLine, defsByLine);
10985
11260
  const initialTaint = rawInitialTaint.filter((tv) => {
10986
11261
  if (tv.line === tv.sourceLine) return true;
10987
11262
  const sanCheck = checkSanitized(tv.sourceLine, tv.line, tv.sourceType, sanitizersByLine);
@@ -10990,7 +11265,7 @@ function propagateTaint(dfg, calls, sources, sinks, sanitizers) {
10990
11265
  taintedVars.push(...initialTaint);
10991
11266
  const propagatedTaint = propagateThroughChains(
10992
11267
  initialTaint,
10993
- dfg.chains ?? [],
11268
+ graph.chainsByFromDef,
10994
11269
  defById,
10995
11270
  sanitizersByLine
10996
11271
  );
@@ -11024,9 +11299,7 @@ function propagateTaint(dfg, calls, sources, sinks, sanitizers) {
11024
11299
  const flow = buildTaintFlow(
11025
11300
  source,
11026
11301
  sink,
11027
- taintInfo,
11028
- dfg,
11029
- defById
11302
+ taintInfo
11030
11303
  );
11031
11304
  flows.push(flow);
11032
11305
  const existingSources = reachableSinks.get(sink) ?? [];
@@ -11046,7 +11319,7 @@ function propagateTaint(dfg, calls, sources, sinks, sanitizers) {
11046
11319
  }
11047
11320
  return { taintedVars, flows, reachableSinks };
11048
11321
  }
11049
- function findInitialTaint(sources, dfg, callsByLine, defsByLine) {
11322
+ function findInitialTaint(sources, callsByLine, defsByLine) {
11050
11323
  const tainted = [];
11051
11324
  for (const source of sources) {
11052
11325
  const defsOnLine = defsByLine.get(source.line) ?? [];
@@ -11078,19 +11351,13 @@ function findInitialTaint(sources, dfg, callsByLine, defsByLine) {
11078
11351
  }
11079
11352
  return tainted;
11080
11353
  }
11081
- function propagateThroughChains(initialTaint, chains, defById, sanitizersByLine) {
11354
+ function propagateThroughChains(initialTaint, chainsByFromDef, defById, sanitizersByLine) {
11082
11355
  const propagated = [];
11083
11356
  const taintedDefIds = new Set(initialTaint.map((t) => t.defId));
11084
11357
  const taintInfoByDefId = /* @__PURE__ */ new Map();
11085
11358
  for (const t of initialTaint) {
11086
11359
  taintInfoByDefId.set(t.defId, t);
11087
11360
  }
11088
- const chainsByFromDef = /* @__PURE__ */ new Map();
11089
- for (const chain of chains) {
11090
- const existing = chainsByFromDef.get(chain.from_def) ?? [];
11091
- existing.push(chain);
11092
- chainsByFromDef.set(chain.from_def, existing);
11093
- }
11094
11361
  const queue = [...initialTaint.map((t) => t.defId)];
11095
11362
  const visited = new Set(queue);
11096
11363
  while (queue.length > 0) {
@@ -11160,7 +11427,7 @@ function checkSanitized(_fromLine, toLine, sinkType, sanitizersByLine) {
11160
11427
  }
11161
11428
  return { sanitized: false };
11162
11429
  }
11163
- function buildTaintFlow(source, sink, taintInfo, dfg, defById) {
11430
+ function buildTaintFlow(source, sink, taintInfo) {
11164
11431
  const path = [];
11165
11432
  path.push({
11166
11433
  variable: taintInfo.variable,
@@ -13537,65 +13804,54 @@ function normalizeCondition(cond) {
13537
13804
 
13538
13805
  // src/analysis/path-finder.ts
13539
13806
  var PathFinder = class {
13540
- dfg;
13541
- calls;
13807
+ graph;
13542
13808
  sources;
13543
13809
  sinks;
13544
13810
  sanitizers;
13545
13811
  config;
13546
- // Lookup maps
13547
- defById = /* @__PURE__ */ new Map();
13548
- defsByLine = /* @__PURE__ */ new Map();
13549
- defsByVar = /* @__PURE__ */ new Map();
13550
- usesByLine = /* @__PURE__ */ new Map();
13551
- usesByDefId = /* @__PURE__ */ new Map();
13552
- callsByLine = /* @__PURE__ */ new Map();
13553
- sanitizerLines = /* @__PURE__ */ new Set();
13554
- constructor(dfg, calls, sources, sinks, sanitizers, config = {}) {
13555
- this.dfg = dfg;
13556
- this.calls = calls;
13557
- this.sources = sources;
13558
- this.sinks = sinks;
13559
- this.sanitizers = sanitizers;
13560
- this.config = {
13561
- maxPathLength: config.maxPathLength ?? 50,
13562
- maxPathsPerSink: config.maxPathsPerSink ?? 10,
13563
- includeCode: config.includeCode ?? false,
13564
- sourceLines: config.sourceLines ?? []
13565
- };
13566
- this.buildLookupMaps();
13567
- }
13568
- /**
13569
- * Build all lookup maps for efficient querying
13570
- */
13571
- buildLookupMaps() {
13572
- for (const def of this.dfg.defs) {
13573
- this.defById.set(def.id, def);
13574
- const byLine = this.defsByLine.get(def.line) ?? [];
13575
- byLine.push(def);
13576
- this.defsByLine.set(def.line, byLine);
13577
- const byVar = this.defsByVar.get(def.variable) ?? [];
13578
- byVar.push(def);
13579
- this.defsByVar.set(def.variable, byVar);
13580
- }
13581
- for (const use of this.dfg.uses) {
13582
- const byLine = this.usesByLine.get(use.line) ?? [];
13583
- byLine.push(use);
13584
- this.usesByLine.set(use.line, byLine);
13585
- if (use.def_id !== null) {
13586
- const byDefId = this.usesByDefId.get(use.def_id) ?? [];
13587
- byDefId.push(use);
13588
- this.usesByDefId.set(use.def_id, byDefId);
13589
- }
13590
- }
13591
- for (const call of this.calls) {
13592
- const byLine = this.callsByLine.get(call.location.line) ?? [];
13593
- byLine.push(call);
13594
- this.callsByLine.set(call.location.line, byLine);
13595
- }
13596
- for (const sanitizer of this.sanitizers) {
13597
- this.sanitizerLines.add(sanitizer.line);
13812
+ sanitizerLines;
13813
+ constructor(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanitizers, sanitizersOrConfig, config = {}) {
13814
+ if (graphOrDfg instanceof CodeGraph) {
13815
+ this.graph = graphOrDfg;
13816
+ this.sources = callsOrSources;
13817
+ this.sinks = sourcesOrSinks;
13818
+ this.sanitizers = sinksOrSanitizers;
13819
+ const cfg = sanitizersOrConfig;
13820
+ this.config = {
13821
+ maxPathLength: cfg?.maxPathLength ?? 50,
13822
+ maxPathsPerSink: cfg?.maxPathsPerSink ?? 10,
13823
+ includeCode: cfg?.includeCode ?? false,
13824
+ sourceLines: cfg?.sourceLines ?? []
13825
+ };
13826
+ } else {
13827
+ const dfg = graphOrDfg;
13828
+ const calls = callsOrSources;
13829
+ const sources = sourcesOrSinks;
13830
+ const sinks = sinksOrSanitizers;
13831
+ const sanitizers = sanitizersOrConfig ?? [];
13832
+ this.graph = new CodeGraph({
13833
+ meta: { circle_ir: "3.0", file: "", language: "java", loc: 0, hash: "" },
13834
+ types: [],
13835
+ calls,
13836
+ cfg: { blocks: [], edges: [] },
13837
+ dfg,
13838
+ taint: { sources: [], sinks: [], sanitizers },
13839
+ imports: [],
13840
+ exports: [],
13841
+ unresolved: [],
13842
+ enriched: {}
13843
+ });
13844
+ this.sources = sources;
13845
+ this.sinks = sinks;
13846
+ this.sanitizers = sanitizers;
13847
+ this.config = {
13848
+ maxPathLength: config.maxPathLength ?? 50,
13849
+ maxPathsPerSink: config.maxPathsPerSink ?? 10,
13850
+ includeCode: config.includeCode ?? false,
13851
+ sourceLines: config.sourceLines ?? []
13852
+ };
13598
13853
  }
13854
+ this.sanitizerLines = new Set(this.sanitizers.map((s) => s.line));
13599
13855
  }
13600
13856
  /**
13601
13857
  * Find all taint paths from sources to sinks
@@ -13604,7 +13860,7 @@ var PathFinder = class {
13604
13860
  const paths = [];
13605
13861
  let pathId = 1;
13606
13862
  for (const source of this.sources) {
13607
- const sourceDefs = this.defsByLine.get(source.line) ?? [];
13863
+ const sourceDefs = this.graph.defsAtLine(source.line);
13608
13864
  for (const sourceDef of sourceDefs) {
13609
13865
  const pathsFromSource = this.findPathsFromSource(source, sourceDef, pathId);
13610
13866
  paths.push(...pathsFromSource);
@@ -13660,7 +13916,7 @@ var PathFinder = class {
13660
13916
  description: `Flows into ${sink.type} sink`,
13661
13917
  code: this.getCodeAtLine(sink.line)
13662
13918
  };
13663
- const call = this.callsByLine.get(sink.line)?.[0];
13919
+ const call = this.graph.callsAtLine(sink.line)[0];
13664
13920
  paths.push({
13665
13921
  id: `path-${startPathId + paths.length}`,
13666
13922
  source: {
@@ -13684,7 +13940,7 @@ var PathFinder = class {
13684
13940
  pathsPerSink.set(sink.line, sinkCount + 1);
13685
13941
  }
13686
13942
  }
13687
- const uses = this.usesByDefId.get(state.currentDef.id) ?? [];
13943
+ const uses = this.graph.usesOfDef(state.currentDef.id);
13688
13944
  for (const use of uses) {
13689
13945
  let sanitizer = state.sanitizer;
13690
13946
  if (this.sanitizerLines.has(use.line) && !sanitizer) {
@@ -13693,7 +13949,7 @@ var PathFinder = class {
13693
13949
  sanitizer = { line: san.line, method: san.method };
13694
13950
  }
13695
13951
  }
13696
- const nextDefs = this.defsByLine.get(use.line) ?? [];
13952
+ const nextDefs = this.graph.defsAtLine(use.line);
13697
13953
  for (const nextDef of nextDefs) {
13698
13954
  if (state.visited.has(nextDef.id)) continue;
13699
13955
  const hop = this.createHop(state.currentDef, nextDef, use);
@@ -13706,7 +13962,7 @@ var PathFinder = class {
13706
13962
  sanitizer
13707
13963
  });
13708
13964
  }
13709
- const laterDefs = (this.defsByVar.get(use.variable) ?? []).filter((d) => d.line > use.line && !state.visited.has(d.id));
13965
+ const laterDefs = (this.graph.defsByVar.get(use.variable) ?? []).filter((d) => d.line > use.line && !state.visited.has(d.id));
13710
13966
  for (const laterDef of laterDefs.slice(0, 3)) {
13711
13967
  const hop = {
13712
13968
  line: laterDef.line,
@@ -13732,14 +13988,12 @@ var PathFinder = class {
13732
13988
  * Check if a definition reaches a sink
13733
13989
  */
13734
13990
  reachesSink(def, sink) {
13735
- const uses = this.usesByLine.get(sink.line) ?? [];
13736
- for (const use of uses) {
13991
+ for (const use of this.graph.usesAtLine(sink.line)) {
13737
13992
  if (use.variable === def.variable || use.def_id === def.id) {
13738
13993
  return true;
13739
13994
  }
13740
13995
  }
13741
- const calls = this.callsByLine.get(sink.line) ?? [];
13742
- for (const call of calls) {
13996
+ for (const call of this.graph.callsAtLine(sink.line)) {
13743
13997
  for (const arg of call.arguments) {
13744
13998
  if (arg.variable === def.variable) {
13745
13999
  return true;
@@ -13752,7 +14006,7 @@ var PathFinder = class {
13752
14006
  * Create a hop description between two definitions
13753
14007
  */
13754
14008
  createHop(fromDef, toDef, use) {
13755
- const call = this.callsByLine.get(toDef.line)?.[0];
14009
+ const call = this.graph.callsAtLine(toDef.line)[0];
13756
14010
  let operation = "assign";
13757
14011
  let description = `Assigned to ${toDef.variable}`;
13758
14012
  if (call) {