circle-ir 3.25.0 → 3.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3997,6 +3997,7 @@ var parserInitialized = false;
3997
3997
  var parserInitializing = null;
3998
3998
  var loadedLanguages = /* @__PURE__ */ new Map();
3999
3999
  var loadingLanguages = /* @__PURE__ */ new Map();
4000
+ var cachedParsers = /* @__PURE__ */ new Map();
4000
4001
  var configuredLanguagePaths = {};
4001
4002
  var configuredLanguageModules = {};
4002
4003
  async function initParser(options = {}) {
@@ -4066,9 +4067,14 @@ async function loadLanguage(language, wasmPath) {
4066
4067
  return loadPromise;
4067
4068
  }
4068
4069
  async function createParser(language) {
4070
+ const cached = cachedParsers.get(language);
4071
+ if (cached) {
4072
+ return cached;
4073
+ }
4069
4074
  const lang = await loadLanguage(language);
4070
4075
  const parser = new Parser();
4071
4076
  parser.setLanguage(lang);
4077
+ cachedParsers.set(language, parser);
4072
4078
  return parser;
4073
4079
  }
4074
4080
  async function parse(code, language) {
@@ -4079,6 +4085,13 @@ async function parse(code, language) {
4079
4085
  }
4080
4086
  return tree;
4081
4087
  }
4088
+ function disposeTree(tree) {
4089
+ if (!tree) return;
4090
+ try {
4091
+ tree.delete();
4092
+ } catch {
4093
+ }
4094
+ }
4082
4095
  function walkTree(node, visitor) {
4083
4096
  visitor(node);
4084
4097
  for (let i2 = 0; i2 < node.childCount; i2++) {
@@ -13302,6 +13315,9 @@ var AnalysisPipeline = class {
13302
13315
  },
13303
13316
  addFinding(finding) {
13304
13317
  findings.push(finding);
13318
+ },
13319
+ getFindings() {
13320
+ return findings;
13305
13321
  }
13306
13322
  };
13307
13323
  for (const pass of this.passes) {
@@ -25013,6 +25029,271 @@ function detectHandler(graph, calls) {
25013
25029
  return false;
25014
25030
  }
25015
25031
 
25032
+ // src/analysis/passes/scan-secrets-pass.ts
25033
+ var TEST_PATH_RE3 = /(?:^|[\\/])(?:test|tests|spec|specs|__tests?__|__mocks?__|fixtures?|testdata)(?:[\\/]|$)/i;
25034
+ var TEST_FILENAME_RE = /(?:\.(?:test|spec)\.[cm]?[jt]sx?|_test\.go|_test\.py|Test\.java|Tests\.java)$/i;
25035
+ function isTestFile(file) {
25036
+ return TEST_PATH_RE3.test(file) || TEST_FILENAME_RE.test(file);
25037
+ }
25038
+ var PROVIDER_PATTERNS = [
25039
+ {
25040
+ name: "AWS access key",
25041
+ regex: /\bAKIA[0-9A-Z]{16}\b/,
25042
+ severity: "critical",
25043
+ level: "error",
25044
+ fix: "Rotate the AWS access key immediately and move it to an environment variable or AWS Secrets Manager."
25045
+ },
25046
+ {
25047
+ name: "GitHub personal access token",
25048
+ regex: /\bghp_[A-Za-z0-9]{36}\b/,
25049
+ severity: "critical",
25050
+ level: "error",
25051
+ fix: "Revoke the token at https://github.com/settings/tokens and store secrets in CI/CD secrets, not source."
25052
+ },
25053
+ {
25054
+ name: "GitHub OAuth token",
25055
+ regex: /\bgho_[A-Za-z0-9]{36}\b/,
25056
+ severity: "critical",
25057
+ level: "error",
25058
+ fix: "Revoke the OAuth token and store secrets outside source control."
25059
+ },
25060
+ {
25061
+ name: "GitHub user-to-server token",
25062
+ regex: /\bghu_[A-Za-z0-9]{36}\b/,
25063
+ severity: "critical",
25064
+ level: "error",
25065
+ fix: "Revoke the GitHub user-to-server token and store secrets outside source control."
25066
+ },
25067
+ {
25068
+ name: "GitHub server-to-server token",
25069
+ regex: /\bghs_[A-Za-z0-9]{36}\b/,
25070
+ severity: "critical",
25071
+ level: "error",
25072
+ fix: "Revoke the GitHub server-to-server token and store secrets outside source control."
25073
+ },
25074
+ {
25075
+ name: "GitHub refresh token",
25076
+ regex: /\bghr_[A-Za-z0-9]{36}\b/,
25077
+ severity: "critical",
25078
+ level: "error",
25079
+ fix: "Revoke the GitHub refresh token and store secrets outside source control."
25080
+ },
25081
+ {
25082
+ name: "Stripe live secret key",
25083
+ regex: /\bsk_live_[A-Za-z0-9]{24,}\b/,
25084
+ severity: "critical",
25085
+ level: "error",
25086
+ fix: "Rotate the Stripe secret key in the Stripe Dashboard and load it from a secrets manager."
25087
+ },
25088
+ {
25089
+ name: "Stripe live publishable key",
25090
+ regex: /\bpk_live_[A-Za-z0-9]{24,}\b/,
25091
+ severity: "high",
25092
+ level: "warning",
25093
+ fix: "Publishable keys are not secret but should still not be checked in to back-end source files; verify front-end vs back-end context."
25094
+ },
25095
+ {
25096
+ name: "OpenAI API key",
25097
+ regex: /\bsk-[A-Za-z0-9]{48}\b/,
25098
+ severity: "critical",
25099
+ level: "error",
25100
+ fix: "Revoke the OpenAI key at https://platform.openai.com/api-keys and load from environment."
25101
+ },
25102
+ {
25103
+ name: "Anthropic API key",
25104
+ regex: /\bsk-ant-[A-Za-z0-9_-]{90,}\b/,
25105
+ severity: "critical",
25106
+ level: "error",
25107
+ fix: "Revoke the Anthropic key in the Console and load from environment."
25108
+ },
25109
+ {
25110
+ name: "Slack token",
25111
+ regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/,
25112
+ severity: "critical",
25113
+ level: "error",
25114
+ fix: "Revoke the Slack token and load from environment."
25115
+ },
25116
+ {
25117
+ name: "Google API key",
25118
+ regex: /\bAIza[0-9A-Za-z_-]{35}\b/,
25119
+ severity: "critical",
25120
+ level: "error",
25121
+ fix: "Restrict the Google API key by referrer / IP in the GCP console or revoke it."
25122
+ },
25123
+ {
25124
+ name: "JSON Web Token",
25125
+ regex: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/,
25126
+ severity: "critical",
25127
+ level: "error",
25128
+ fix: "JWTs in source carry whatever scope they were minted with; rotate signing keys and remove the token."
25129
+ },
25130
+ {
25131
+ name: "PEM private key",
25132
+ regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/,
25133
+ severity: "critical",
25134
+ level: "error",
25135
+ fix: "Remove the private key from source control immediately, rotate the corresponding public key, and store keys outside the repository."
25136
+ },
25137
+ {
25138
+ name: "npm access token",
25139
+ regex: /\bnpm_[A-Za-z0-9]{36}\b/,
25140
+ severity: "critical",
25141
+ level: "error",
25142
+ fix: "Revoke the npm token at https://www.npmjs.com/settings/<user>/tokens and load from environment."
25143
+ }
25144
+ ];
25145
+ var STRING_LITERAL_RE = /(["'`])((?:\\.|(?!\1).){8,200})\1/g;
25146
+ var BASE64ISH_RE = /^[A-Za-z0-9+/=_-]+$/;
25147
+ var HEXISH_RE = /^[a-fA-F0-9]+$/;
25148
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
25149
+ var PLACEHOLDER_RE = /(?:changeme|your[-_]?(?:key|secret|token|password)(?:[-_]?here)?|replace[-_]?me|example[-_]?(?:key|secret|token)?|placeholder|todo|fixme|test[-_]?(?:key|secret|token)|fake[-_]?(?:key|secret|token)|dummy|sample|insert[-_]?your)/i;
25150
+ function isBareHashShape(s) {
25151
+ const n = s.length;
25152
+ if (n !== 32 && n !== 40 && n !== 64) return false;
25153
+ return HEXISH_RE.test(s);
25154
+ }
25155
+ function isAllSameChar(s) {
25156
+ if (s.length < 2) return false;
25157
+ const c = s.charAt(0);
25158
+ for (let i2 = 1; i2 < s.length; i2++) if (s.charAt(i2) !== c) return false;
25159
+ return true;
25160
+ }
25161
+ function tryBase64Decode(s) {
25162
+ if (s.length % 4 !== 0 && !/=+$/.test(s)) return null;
25163
+ try {
25164
+ return globalThis.atob(s);
25165
+ } catch {
25166
+ return null;
25167
+ }
25168
+ }
25169
+ function looksLikeBase64Json(s) {
25170
+ const decoded = tryBase64Decode(s);
25171
+ if (!decoded) return false;
25172
+ const trimmed = decoded.trimStart();
25173
+ return trimmed.startsWith("{") || trimmed.startsWith("[");
25174
+ }
25175
+ function shannonEntropy(s) {
25176
+ const freq = /* @__PURE__ */ new Map();
25177
+ for (const ch of s) freq.set(ch, (freq.get(ch) ?? 0) + 1);
25178
+ const len = s.length;
25179
+ let h = 0;
25180
+ for (const n of freq.values()) {
25181
+ const p = n / len;
25182
+ h -= p * Math.log2(p);
25183
+ }
25184
+ return h;
25185
+ }
25186
+ var CREDENTIAL_NAME_RE = /(?:key|secret|token|password|passwd|credential|api[_-]?key)/i;
25187
+ var TEST_CALL_RE = /\b(?:expect|assert|describe|it|test)\s*\(/;
25188
+ var COMMENT_EXAMPLE_RE = /(?:\/\/|#)\s*(?:example|sample|test|fixture)/i;
25189
+ var ScanSecretsPass = class {
25190
+ name = "scan-secrets";
25191
+ category = "security";
25192
+ run(ctx) {
25193
+ const file = ctx.graph.ir.meta.file;
25194
+ if (isTestFile(file)) {
25195
+ return { providerFindings: 0, entropyFindings: 0 };
25196
+ }
25197
+ const lines = ctx.code.split("\n");
25198
+ const prior = ctx.getFindings?.() ?? [];
25199
+ const seen = /* @__PURE__ */ new Set();
25200
+ for (const f of prior) {
25201
+ if (f.file !== file) continue;
25202
+ if (f.rule_id === "hardcoded-credential" || f.rule_id === "hardcoded-credential-entropy") {
25203
+ seen.add(`${f.line}:${f.rule_id}`);
25204
+ }
25205
+ }
25206
+ let providerFindings = 0;
25207
+ let entropyFindings = 0;
25208
+ for (let i2 = 0; i2 < lines.length; i2++) {
25209
+ const lineText = lines[i2];
25210
+ const lineNum = i2 + 1;
25211
+ for (const pattern of PROVIDER_PATTERNS) {
25212
+ const m = pattern.regex.exec(lineText);
25213
+ if (!m) continue;
25214
+ const key = `${lineNum}:hardcoded-credential`;
25215
+ if (seen.has(key)) continue;
25216
+ seen.add(key);
25217
+ ctx.addFinding({
25218
+ id: `hardcoded-credential-${file}-${lineNum}`,
25219
+ pass: this.name,
25220
+ category: this.category,
25221
+ rule_id: "hardcoded-credential",
25222
+ cwe: "CWE-798",
25223
+ severity: pattern.severity,
25224
+ level: pattern.level,
25225
+ message: `Hardcoded credential: ${pattern.name} detected`,
25226
+ file,
25227
+ line: lineNum,
25228
+ snippet: lineText.trim().substring(0, 120),
25229
+ fix: pattern.fix,
25230
+ evidence: { provider: pattern.name, match: m[0].substring(0, 40) }
25231
+ });
25232
+ providerFindings += 1;
25233
+ break;
25234
+ }
25235
+ }
25236
+ for (let i2 = 0; i2 < lines.length; i2++) {
25237
+ const lineText = lines[i2];
25238
+ const lineNum = i2 + 1;
25239
+ if (TEST_CALL_RE.test(lineText)) continue;
25240
+ if (COMMENT_EXAMPLE_RE.test(lineText)) continue;
25241
+ STRING_LITERAL_RE.lastIndex = 0;
25242
+ let match;
25243
+ while ((match = STRING_LITERAL_RE.exec(lineText)) !== null) {
25244
+ const value = match[2];
25245
+ if (!this.isCandidate(value)) continue;
25246
+ if (!this.passesEntropyGate(value, lineText)) continue;
25247
+ const key = `${lineNum}:hardcoded-credential-entropy`;
25248
+ if (seen.has(key)) continue;
25249
+ if (seen.has(`${lineNum}:hardcoded-credential`)) continue;
25250
+ seen.add(key);
25251
+ ctx.addFinding({
25252
+ id: `hardcoded-credential-entropy-${file}-${lineNum}`,
25253
+ pass: this.name,
25254
+ category: this.category,
25255
+ rule_id: "hardcoded-credential-entropy",
25256
+ cwe: "CWE-798",
25257
+ severity: "high",
25258
+ level: "warning",
25259
+ message: `Possible hardcoded secret: high-entropy string literal (${value.length} chars)`,
25260
+ file,
25261
+ line: lineNum,
25262
+ snippet: lineText.trim().substring(0, 120),
25263
+ fix: "If this is a credential, move it to environment / secrets manager. If it is sample data, add an `example` / `test` marker or disable this pass via `disabledPasses: ['scan-secrets']`.",
25264
+ evidence: { kind: "entropy", length: value.length }
25265
+ });
25266
+ entropyFindings += 1;
25267
+ }
25268
+ }
25269
+ return { providerFindings, entropyFindings };
25270
+ }
25271
+ /** Length + shape + denylist filter before entropy is computed. */
25272
+ isCandidate(s) {
25273
+ if (s.length < 20 || s.length > 200) return false;
25274
+ if (!BASE64ISH_RE.test(s) && !HEXISH_RE.test(s)) return false;
25275
+ if (UUID_RE.test(s)) return false;
25276
+ if (isBareHashShape(s)) return false;
25277
+ if (isAllSameChar(s)) return false;
25278
+ if (PLACEHOLDER_RE.test(s)) return false;
25279
+ if (looksLikeBase64Json(s)) return false;
25280
+ return true;
25281
+ }
25282
+ /**
25283
+ * Shannon-entropy gate. Base64-shaped strings need higher entropy than
25284
+ * hex-shaped (hex alphabet is 4 bits/char by construction). When the
25285
+ * surrounding line contains a credential-shaped variable name, both
25286
+ * thresholds drop by 0.2 bits/char.
25287
+ */
25288
+ passesEntropyGate(value, lineText) {
25289
+ const isHex = HEXISH_RE.test(value);
25290
+ const boost = CREDENTIAL_NAME_RE.test(lineText) ? 0.2 : 0;
25291
+ const threshold = isHex ? 3.5 - boost : 4.3 - boost;
25292
+ const h = shannonEntropy(value);
25293
+ return h >= threshold;
25294
+ }
25295
+ };
25296
+
25016
25297
  // src/analysis/metrics/passes/size-metrics-pass.ts
25017
25298
  var SizeMetricsPass = class {
25018
25299
  name = "size-metrics";
@@ -25826,160 +26107,169 @@ async function analyze(code, filePath, language, options = {}) {
25826
26107
  }
25827
26108
  logger.debug("Analyzing file", { filePath, language, codeLength: code.length });
25828
26109
  const tree = await parse(code, language);
25829
- logger.trace("Parsed AST", { rootNodeType: tree.rootNode.type });
25830
- const nodeCache = collectAllNodes(tree.rootNode, getNodeTypesForLanguage(language));
25831
- const meta = extractMeta(code, tree, filePath, language);
25832
- const types = extractTypes(tree, nodeCache, language);
25833
- const calls = extractCalls(tree, nodeCache, language);
25834
- const imports = extractImports(tree, language);
25835
- const exports = extractExports(types);
25836
- const cfg = buildCFG(tree, language);
25837
- const dfg = buildDFG(tree, nodeCache, language);
25838
- const graph = new CodeGraph({
25839
- meta,
25840
- types,
25841
- calls,
25842
- cfg,
25843
- dfg,
25844
- taint: { sources: [], sinks: [], sanitizers: [] },
25845
- imports,
25846
- exports,
25847
- unresolved: [],
25848
- enriched: {}
25849
- });
25850
- const config = options.taintConfig ?? getDefaultConfig();
25851
- const disabledPasses = new Set(options.disabledPasses ?? []);
25852
- const passOpts = options.passOptions ?? {};
25853
- const pipeline = new AnalysisPipeline();
25854
- pipeline.add(new TaintMatcherPass());
25855
- pipeline.add(new ConstantPropagationPass(tree));
25856
- pipeline.add(new LanguageSourcesPass());
25857
- pipeline.add(new SinkFilterPass());
25858
- pipeline.add(new TaintPropagationPass());
25859
- pipeline.add(new InterproceduralPass());
25860
- if (!disabledPasses.has("dead-code")) pipeline.add(new DeadCodePass());
25861
- if (!disabledPasses.has("missing-await")) pipeline.add(new MissingAwaitPass());
25862
- if (!disabledPasses.has("n-plus-one")) pipeline.add(new NPlusOnePass());
25863
- if (!disabledPasses.has("missing-public-doc")) pipeline.add(new MissingPublicDocPass());
25864
- if (!disabledPasses.has("todo-in-prod")) pipeline.add(new TodoInProdPass());
25865
- if (!disabledPasses.has("string-concat-loop")) pipeline.add(new StringConcatLoopPass());
25866
- if (!disabledPasses.has("sync-io-async")) pipeline.add(new SyncIoAsyncPass());
25867
- if (!disabledPasses.has("unchecked-return")) pipeline.add(new UncheckedReturnPass());
25868
- if (!disabledPasses.has("null-deref")) pipeline.add(new NullDerefPass());
25869
- if (!disabledPasses.has("resource-leak")) pipeline.add(new ResourceLeakPass());
25870
- if (!disabledPasses.has("variable-shadowing")) pipeline.add(new VariableShadowingPass());
25871
- if (!disabledPasses.has("leaked-global")) pipeline.add(new LeakedGlobalPass());
25872
- if (!disabledPasses.has("unused-variable")) pipeline.add(new UnusedVariablePass());
25873
- if (!disabledPasses.has("dependency-fan-out")) pipeline.add(new DependencyFanOutPass(passOpts.dependencyFanOut));
25874
- if (!disabledPasses.has("stale-doc-ref")) pipeline.add(new StaleDocRefPass());
25875
- if (!disabledPasses.has("infinite-loop")) pipeline.add(new InfiniteLoopPass());
25876
- if (!disabledPasses.has("deep-inheritance")) pipeline.add(new DeepInheritancePass());
25877
- if (!disabledPasses.has("redundant-loop-computation")) pipeline.add(new RedundantLoopPass());
25878
- if (!disabledPasses.has("unbounded-collection")) pipeline.add(new UnboundedCollectionPass(passOpts.unboundedCollection));
25879
- if (!disabledPasses.has("serial-await")) pipeline.add(new SerialAwaitPass());
25880
- if (!disabledPasses.has("react-inline-jsx")) pipeline.add(new ReactInlineJsxPass());
25881
- if (!disabledPasses.has("swallowed-exception")) pipeline.add(new SwallowedExceptionPass());
25882
- if (!disabledPasses.has("broad-catch")) pipeline.add(new BroadCatchPass());
25883
- if (!disabledPasses.has("unhandled-exception")) pipeline.add(new UnhandledExceptionPass());
25884
- if (!disabledPasses.has("double-close")) pipeline.add(new DoubleClosePass());
25885
- if (!disabledPasses.has("use-after-close")) pipeline.add(new UseAfterClosePass());
25886
- if (!disabledPasses.has("cleanup-verify")) pipeline.add(new CleanupVerifyPass());
25887
- if (!disabledPasses.has("missing-override")) pipeline.add(new MissingOverridePass());
25888
- if (!disabledPasses.has("unused-interface-method")) pipeline.add(new UnusedInterfaceMethodPass());
25889
- if (!disabledPasses.has("blocking-main-thread")) pipeline.add(new BlockingMainThreadPass());
25890
- if (!disabledPasses.has("excessive-allocation")) pipeline.add(new ExcessiveAllocationPass());
25891
- if (!disabledPasses.has("missing-stream")) pipeline.add(new MissingStreamPass());
25892
- if (!disabledPasses.has("god-class")) pipeline.add(new GodClassPass());
25893
- if (!disabledPasses.has("naming-convention")) pipeline.add(new NamingConventionPass(passOpts.namingConvention));
25894
- if (!disabledPasses.has("security-headers")) pipeline.add(new SecurityHeadersPass(passOpts.securityHeaders));
25895
- const { results, findings } = pipeline.run(graph, code, language, config);
25896
- const sinkFilter = results.get("sink-filter");
25897
- const interProc = results.get("interprocedural");
25898
- const taint = {
25899
- sources: sinkFilter.sources,
25900
- sinks: [...sinkFilter.sinks, ...interProc.additionalSinks],
25901
- sanitizers: sinkFilter.sanitizers,
25902
- flows: interProc.additionalFlows,
25903
- interprocedural: interProc.interprocedural
25904
- };
25905
- const unresolved = detectUnresolved(calls, types, dfg);
25906
- const enriched = buildEnriched(types, calls, taint.sources, taint.sinks);
25907
- const metricValues = new MetricRunner().run(
25908
- { meta, types, calls, cfg, dfg, taint, imports, exports, unresolved, enriched },
25909
- code,
25910
- language
25911
- );
25912
- logger.debug("Analysis complete", {
25913
- filePath,
25914
- finalSources: taint.sources.length,
25915
- finalSinks: taint.sinks.length,
25916
- flows: taint.flows?.length ?? 0,
25917
- unresolvedItems: unresolved.length
25918
- });
25919
- return {
25920
- meta,
25921
- types,
25922
- calls,
25923
- cfg,
25924
- dfg,
25925
- taint,
25926
- imports,
25927
- exports,
25928
- unresolved,
25929
- enriched,
25930
- findings: findings.length > 0 ? findings : void 0,
25931
- metrics: { file: filePath, metrics: metricValues }
25932
- };
26110
+ try {
26111
+ logger.trace("Parsed AST", { rootNodeType: tree.rootNode.type });
26112
+ const nodeCache = collectAllNodes(tree.rootNode, getNodeTypesForLanguage(language));
26113
+ const meta = extractMeta(code, tree, filePath, language);
26114
+ const types = extractTypes(tree, nodeCache, language);
26115
+ const calls = extractCalls(tree, nodeCache, language);
26116
+ const imports = extractImports(tree, language);
26117
+ const exports = extractExports(types);
26118
+ const cfg = buildCFG(tree, language);
26119
+ const dfg = buildDFG(tree, nodeCache, language);
26120
+ const graph = new CodeGraph({
26121
+ meta,
26122
+ types,
26123
+ calls,
26124
+ cfg,
26125
+ dfg,
26126
+ taint: { sources: [], sinks: [], sanitizers: [] },
26127
+ imports,
26128
+ exports,
26129
+ unresolved: [],
26130
+ enriched: {}
26131
+ });
26132
+ const config = options.taintConfig ?? getDefaultConfig();
26133
+ const disabledPasses = new Set(options.disabledPasses ?? []);
26134
+ const passOpts = options.passOptions ?? {};
26135
+ const pipeline = new AnalysisPipeline();
26136
+ pipeline.add(new TaintMatcherPass());
26137
+ pipeline.add(new ConstantPropagationPass(tree));
26138
+ pipeline.add(new LanguageSourcesPass());
26139
+ pipeline.add(new SinkFilterPass());
26140
+ pipeline.add(new TaintPropagationPass());
26141
+ pipeline.add(new InterproceduralPass());
26142
+ if (!disabledPasses.has("scan-secrets")) pipeline.add(new ScanSecretsPass());
26143
+ if (!disabledPasses.has("dead-code")) pipeline.add(new DeadCodePass());
26144
+ if (!disabledPasses.has("missing-await")) pipeline.add(new MissingAwaitPass());
26145
+ if (!disabledPasses.has("n-plus-one")) pipeline.add(new NPlusOnePass());
26146
+ if (!disabledPasses.has("missing-public-doc")) pipeline.add(new MissingPublicDocPass());
26147
+ if (!disabledPasses.has("todo-in-prod")) pipeline.add(new TodoInProdPass());
26148
+ if (!disabledPasses.has("string-concat-loop")) pipeline.add(new StringConcatLoopPass());
26149
+ if (!disabledPasses.has("sync-io-async")) pipeline.add(new SyncIoAsyncPass());
26150
+ if (!disabledPasses.has("unchecked-return")) pipeline.add(new UncheckedReturnPass());
26151
+ if (!disabledPasses.has("null-deref")) pipeline.add(new NullDerefPass());
26152
+ if (!disabledPasses.has("resource-leak")) pipeline.add(new ResourceLeakPass());
26153
+ if (!disabledPasses.has("variable-shadowing")) pipeline.add(new VariableShadowingPass());
26154
+ if (!disabledPasses.has("leaked-global")) pipeline.add(new LeakedGlobalPass());
26155
+ if (!disabledPasses.has("unused-variable")) pipeline.add(new UnusedVariablePass());
26156
+ if (!disabledPasses.has("dependency-fan-out")) pipeline.add(new DependencyFanOutPass(passOpts.dependencyFanOut));
26157
+ if (!disabledPasses.has("stale-doc-ref")) pipeline.add(new StaleDocRefPass());
26158
+ if (!disabledPasses.has("infinite-loop")) pipeline.add(new InfiniteLoopPass());
26159
+ if (!disabledPasses.has("deep-inheritance")) pipeline.add(new DeepInheritancePass());
26160
+ if (!disabledPasses.has("redundant-loop-computation")) pipeline.add(new RedundantLoopPass());
26161
+ if (!disabledPasses.has("unbounded-collection")) pipeline.add(new UnboundedCollectionPass(passOpts.unboundedCollection));
26162
+ if (!disabledPasses.has("serial-await")) pipeline.add(new SerialAwaitPass());
26163
+ if (!disabledPasses.has("react-inline-jsx")) pipeline.add(new ReactInlineJsxPass());
26164
+ if (!disabledPasses.has("swallowed-exception")) pipeline.add(new SwallowedExceptionPass());
26165
+ if (!disabledPasses.has("broad-catch")) pipeline.add(new BroadCatchPass());
26166
+ if (!disabledPasses.has("unhandled-exception")) pipeline.add(new UnhandledExceptionPass());
26167
+ if (!disabledPasses.has("double-close")) pipeline.add(new DoubleClosePass());
26168
+ if (!disabledPasses.has("use-after-close")) pipeline.add(new UseAfterClosePass());
26169
+ if (!disabledPasses.has("cleanup-verify")) pipeline.add(new CleanupVerifyPass());
26170
+ if (!disabledPasses.has("missing-override")) pipeline.add(new MissingOverridePass());
26171
+ if (!disabledPasses.has("unused-interface-method")) pipeline.add(new UnusedInterfaceMethodPass());
26172
+ if (!disabledPasses.has("blocking-main-thread")) pipeline.add(new BlockingMainThreadPass());
26173
+ if (!disabledPasses.has("excessive-allocation")) pipeline.add(new ExcessiveAllocationPass());
26174
+ if (!disabledPasses.has("missing-stream")) pipeline.add(new MissingStreamPass());
26175
+ if (!disabledPasses.has("god-class")) pipeline.add(new GodClassPass());
26176
+ if (!disabledPasses.has("naming-convention")) pipeline.add(new NamingConventionPass(passOpts.namingConvention));
26177
+ if (!disabledPasses.has("security-headers")) pipeline.add(new SecurityHeadersPass(passOpts.securityHeaders));
26178
+ const { results, findings } = pipeline.run(graph, code, language, config);
26179
+ const sinkFilter = results.get("sink-filter");
26180
+ const interProc = results.get("interprocedural");
26181
+ const taint = {
26182
+ sources: sinkFilter.sources,
26183
+ sinks: [...sinkFilter.sinks, ...interProc.additionalSinks],
26184
+ sanitizers: sinkFilter.sanitizers,
26185
+ flows: interProc.additionalFlows,
26186
+ interprocedural: interProc.interprocedural
26187
+ };
26188
+ const unresolved = detectUnresolved(calls, types, dfg);
26189
+ const enriched = buildEnriched(types, calls, taint.sources, taint.sinks);
26190
+ const metricValues = new MetricRunner().run(
26191
+ { meta, types, calls, cfg, dfg, taint, imports, exports, unresolved, enriched },
26192
+ code,
26193
+ language
26194
+ );
26195
+ logger.debug("Analysis complete", {
26196
+ filePath,
26197
+ finalSources: taint.sources.length,
26198
+ finalSinks: taint.sinks.length,
26199
+ flows: taint.flows?.length ?? 0,
26200
+ unresolvedItems: unresolved.length
26201
+ });
26202
+ return {
26203
+ meta,
26204
+ types,
26205
+ calls,
26206
+ cfg,
26207
+ dfg,
26208
+ taint,
26209
+ imports,
26210
+ exports,
26211
+ unresolved,
26212
+ enriched,
26213
+ findings: findings.length > 0 ? findings : void 0,
26214
+ metrics: { file: filePath, metrics: metricValues }
26215
+ };
26216
+ } finally {
26217
+ disposeTree(tree);
26218
+ }
25933
26219
  }
25934
26220
  async function analyzeHtmlFile(code, filePath, options) {
25935
26221
  logger.debug("Analyzing HTML file", { filePath, codeLength: code.length });
25936
26222
  const tree = await parse(code, "html");
25937
- const meta = extractMeta(code, tree, filePath, "html");
25938
- const { scriptBlocks, eventHandlers } = extractHtmlContent(tree.rootNode);
25939
- logger.debug("HTML extraction", {
25940
- filePath,
25941
- inlineScripts: scriptBlocks.filter((b) => b.kind === "inline").length,
25942
- externalScripts: scriptBlocks.filter((b) => b.kind === "external-src").length,
25943
- eventHandlers: eventHandlers.length
25944
- });
25945
- const scriptResults = [];
25946
- for (const block of scriptBlocks) {
25947
- if (block.kind !== "inline" || !block.code.trim()) continue;
25948
- const scriptLang = block.scriptType === "ts" || block.scriptType === "typescript" || block.scriptType === "text/typescript" ? "typescript" : "javascript";
25949
- try {
25950
- const ir = await analyze(block.code, filePath, scriptLang, options);
25951
- scriptResults.push({ ir, lineOffset: block.lineOffset });
25952
- } catch (e) {
25953
- logger.warn("Failed to analyze script block", {
25954
- filePath,
25955
- lineOffset: block.lineOffset,
25956
- error: e instanceof Error ? e.message : String(e)
25957
- });
26223
+ try {
26224
+ const meta = extractMeta(code, tree, filePath, "html");
26225
+ const { scriptBlocks, eventHandlers } = extractHtmlContent(tree.rootNode);
26226
+ logger.debug("HTML extraction", {
26227
+ filePath,
26228
+ inlineScripts: scriptBlocks.filter((b) => b.kind === "inline").length,
26229
+ externalScripts: scriptBlocks.filter((b) => b.kind === "external-src").length,
26230
+ eventHandlers: eventHandlers.length
26231
+ });
26232
+ const scriptResults = [];
26233
+ for (const block of scriptBlocks) {
26234
+ if (block.kind !== "inline" || !block.code.trim()) continue;
26235
+ const scriptLang = block.scriptType === "ts" || block.scriptType === "typescript" || block.scriptType === "text/typescript" ? "typescript" : "javascript";
26236
+ try {
26237
+ const ir = await analyze(block.code, filePath, scriptLang, options);
26238
+ scriptResults.push({ ir, lineOffset: block.lineOffset });
26239
+ } catch (e) {
26240
+ logger.warn("Failed to analyze script block", {
26241
+ filePath,
26242
+ lineOffset: block.lineOffset,
26243
+ error: e instanceof Error ? e.message : String(e)
26244
+ });
26245
+ }
25958
26246
  }
25959
- }
25960
- for (const handler of eventHandlers) {
25961
- const wrappedCode = `function __${handler.eventName}_handler() { ${handler.code} }`;
25962
- try {
25963
- const ir = await analyze(wrappedCode, filePath, "javascript", options);
25964
- scriptResults.push({ ir, lineOffset: handler.line });
25965
- } catch (e) {
25966
- logger.warn("Failed to analyze event handler", {
25967
- filePath,
25968
- eventName: handler.eventName,
25969
- line: handler.line,
25970
- error: e instanceof Error ? e.message : String(e)
25971
- });
26247
+ for (const handler of eventHandlers) {
26248
+ const wrappedCode = `function __${handler.eventName}_handler() { ${handler.code} }`;
26249
+ try {
26250
+ const ir = await analyze(wrappedCode, filePath, "javascript", options);
26251
+ scriptResults.push({ ir, lineOffset: handler.line });
26252
+ } catch (e) {
26253
+ logger.warn("Failed to analyze event handler", {
26254
+ filePath,
26255
+ eventName: handler.eventName,
26256
+ line: handler.line,
26257
+ error: e instanceof Error ? e.message : String(e)
26258
+ });
26259
+ }
25972
26260
  }
26261
+ const attributeFindings = runHtmlAttributeSecurityChecks(tree.rootNode, filePath);
26262
+ const result = mergeHtmlResults(meta, scriptResults, attributeFindings);
26263
+ logger.debug("HTML analysis complete", {
26264
+ filePath,
26265
+ scriptBlocks: scriptResults.length,
26266
+ attributeFindings: attributeFindings.length,
26267
+ totalFindings: result.findings?.length ?? 0
26268
+ });
26269
+ return result;
26270
+ } finally {
26271
+ disposeTree(tree);
25973
26272
  }
25974
- const attributeFindings = runHtmlAttributeSecurityChecks(tree.rootNode, filePath);
25975
- const result = mergeHtmlResults(meta, scriptResults, attributeFindings);
25976
- logger.debug("HTML analysis complete", {
25977
- filePath,
25978
- scriptBlocks: scriptResults.length,
25979
- attributeFindings: attributeFindings.length,
25980
- totalFindings: result.findings?.length ?? 0
25981
- });
25982
- return result;
25983
26273
  }
25984
26274
  async function analyzeForAPI(code, filePath, language, options = {}) {
25985
26275
  const startTime = performance.now();
@@ -25989,75 +26279,79 @@ async function analyzeForAPI(code, filePath, language, options = {}) {
25989
26279
  const parseStart = performance.now();
25990
26280
  const tree = await parse(code, language);
25991
26281
  const parseTime = performance.now() - parseStart;
25992
- const analysisStart = performance.now();
25993
- const nodeCache = collectAllNodes(tree.rootNode, getNodeTypesForLanguage(language));
25994
- const types = extractTypes(tree, nodeCache, language);
25995
- const calls = extractCalls(tree, nodeCache, language);
25996
- const constPropResult = analyzeConstantPropagation(tree, code);
25997
- const config = options.taintConfig ?? getDefaultConfig();
25998
- const taint = analyzeTaint(calls, types, config);
25999
- let filteredSinks = taint.sinks.filter((sink) => !constPropResult.unreachableLines.has(sink.line));
26000
- filteredSinks = filterCleanVariableSinks(
26001
- filteredSinks,
26002
- calls,
26003
- constPropResult.tainted,
26004
- constPropResult.symbols,
26005
- void 0,
26006
- constPropResult.sanitizedVars,
26007
- constPropResult.synchronizedLines
26008
- );
26009
- filteredSinks = filterSanitizedSinks(filteredSinks, taint.sanitizers ?? [], calls);
26010
- let pythonTaintedVars = /* @__PURE__ */ new Map();
26011
- if (language === "python") {
26012
- pythonTaintedVars = buildPythonTaintedVars(code);
26013
- const pythonSanitizedVars = buildPythonSanitizedVars(code, pythonTaintedVars);
26014
- const sourceLines = code.split("\n");
26015
- filteredSinks = filteredSinks.filter((sink) => {
26016
- if (sink.type !== "xpath_injection") return true;
26017
- const sinkLineText = sourceLines[sink.line - 1] ?? "";
26018
- const taintedVarOnLine = [...pythonTaintedVars.keys()].find(
26019
- (v) => new RegExp(`\\b${v}\\b`).test(sinkLineText)
26020
- );
26021
- if (!taintedVarOnLine) return false;
26022
- if (pythonSanitizedVars.has(taintedVarOnLine)) return false;
26023
- if (new RegExp(`\\.xpath\\s*\\([^)]*\\b\\w+\\s*=\\s*\\b${taintedVarOnLine}\\b`).test(sinkLineText)) return false;
26024
- return true;
26025
- });
26026
- }
26027
- const vulnerabilities = findVulnerabilities(taint.sources, filteredSinks, calls, constPropResult);
26028
- if (language === "python") {
26029
- const trustViolations = findPythonTrustBoundaryViolations(code, pythonTaintedVars);
26030
- for (const v of trustViolations) {
26031
- const alreadyReported = vulnerabilities.some(
26032
- (existing) => existing.sink.line === v.sinkLine && existing.type === "trust_boundary"
26033
- );
26034
- if (!alreadyReported) {
26035
- vulnerabilities.push({
26036
- type: "trust_boundary",
26037
- cwe: "CWE-501",
26038
- severity: "medium",
26039
- source: { line: v.sourceLine, type: "http_param" },
26040
- sink: { line: v.sinkLine, type: "trust_boundary" },
26041
- confidence: 0.85
26042
- });
26282
+ try {
26283
+ const analysisStart = performance.now();
26284
+ const nodeCache = collectAllNodes(tree.rootNode, getNodeTypesForLanguage(language));
26285
+ const types = extractTypes(tree, nodeCache, language);
26286
+ const calls = extractCalls(tree, nodeCache, language);
26287
+ const constPropResult = analyzeConstantPropagation(tree, code);
26288
+ const config = options.taintConfig ?? getDefaultConfig();
26289
+ const taint = analyzeTaint(calls, types, config);
26290
+ let filteredSinks = taint.sinks.filter((sink) => !constPropResult.unreachableLines.has(sink.line));
26291
+ filteredSinks = filterCleanVariableSinks(
26292
+ filteredSinks,
26293
+ calls,
26294
+ constPropResult.tainted,
26295
+ constPropResult.symbols,
26296
+ void 0,
26297
+ constPropResult.sanitizedVars,
26298
+ constPropResult.synchronizedLines
26299
+ );
26300
+ filteredSinks = filterSanitizedSinks(filteredSinks, taint.sanitizers ?? [], calls);
26301
+ let pythonTaintedVars = /* @__PURE__ */ new Map();
26302
+ if (language === "python") {
26303
+ pythonTaintedVars = buildPythonTaintedVars(code);
26304
+ const pythonSanitizedVars = buildPythonSanitizedVars(code, pythonTaintedVars);
26305
+ const sourceLines = code.split("\n");
26306
+ filteredSinks = filteredSinks.filter((sink) => {
26307
+ if (sink.type !== "xpath_injection") return true;
26308
+ const sinkLineText = sourceLines[sink.line - 1] ?? "";
26309
+ const taintedVarOnLine = [...pythonTaintedVars.keys()].find(
26310
+ (v) => new RegExp(`\\b${v}\\b`).test(sinkLineText)
26311
+ );
26312
+ if (!taintedVarOnLine) return false;
26313
+ if (pythonSanitizedVars.has(taintedVarOnLine)) return false;
26314
+ if (new RegExp(`\\.xpath\\s*\\([^)]*\\b\\w+\\s*=\\s*\\b${taintedVarOnLine}\\b`).test(sinkLineText)) return false;
26315
+ return true;
26316
+ });
26317
+ }
26318
+ const vulnerabilities = findVulnerabilities(taint.sources, filteredSinks, calls, constPropResult);
26319
+ if (language === "python") {
26320
+ const trustViolations = findPythonTrustBoundaryViolations(code, pythonTaintedVars);
26321
+ for (const v of trustViolations) {
26322
+ const alreadyReported = vulnerabilities.some(
26323
+ (existing) => existing.sink.line === v.sinkLine && existing.type === "trust_boundary"
26324
+ );
26325
+ if (!alreadyReported) {
26326
+ vulnerabilities.push({
26327
+ type: "trust_boundary",
26328
+ cwe: "CWE-501",
26329
+ severity: "medium",
26330
+ source: { line: v.sourceLine, type: "http_param" },
26331
+ sink: { line: v.sinkLine, type: "trust_boundary" },
26332
+ confidence: 0.85
26333
+ });
26334
+ }
26043
26335
  }
26044
26336
  }
26337
+ const analysisTime = performance.now() - analysisStart;
26338
+ const totalTime = performance.now() - startTime;
26339
+ return {
26340
+ success: true,
26341
+ analysis: {
26342
+ sources: taint.sources,
26343
+ sinks: filteredSinks,
26344
+ vulnerabilities
26345
+ },
26346
+ meta: {
26347
+ parseTimeMs: Math.round(parseTime),
26348
+ analysisTimeMs: Math.round(analysisTime),
26349
+ totalTimeMs: Math.round(totalTime)
26350
+ }
26351
+ };
26352
+ } finally {
26353
+ disposeTree(tree);
26045
26354
  }
26046
- const analysisTime = performance.now() - analysisStart;
26047
- const totalTime = performance.now() - startTime;
26048
- return {
26049
- success: true,
26050
- analysis: {
26051
- sources: taint.sources,
26052
- sinks: filteredSinks,
26053
- vulnerabilities
26054
- },
26055
- meta: {
26056
- parseTimeMs: Math.round(parseTime),
26057
- analysisTimeMs: Math.round(analysisTime),
26058
- totalTimeMs: Math.round(totalTime)
26059
- }
26060
- };
26061
26355
  }
26062
26356
  function findVulnerabilities(sources, sinks, calls, constPropResult) {
26063
26357
  const vulnerabilities = [];