circle-ir 3.1.0 → 3.1.1

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.
@@ -10175,15 +10175,16 @@ function findSources(calls, types, patterns) {
10175
10175
  const skipMethods = ["toString", "hashCode", "equals", "compareTo", "getDescription", "getVulnerabilityCount"];
10176
10176
  if (skipMethods.includes(method.name)) continue;
10177
10177
  for (const param of method.parameters) {
10178
- if (param.type && isInterproceduralTaintableType(param.type)) {
10178
+ const isTaintable = param.type ? isInterproceduralTaintableType(param.type) : true;
10179
+ if (isTaintable) {
10179
10180
  const paramLine = param.line ?? method.start_line;
10180
10181
  sources.push({
10181
10182
  type: "interprocedural_param",
10182
- location: `${param.type} ${param.name} in ${method.name}`,
10183
+ location: `${param.type || "any"} ${param.name} in ${method.name}`,
10183
10184
  severity: "medium",
10184
10185
  line: paramLine,
10185
- confidence: 0.7
10186
- // Lower confidence since we don't know the call site
10186
+ confidence: param.type ? 0.7 : 0.5
10187
+ // Lower confidence for untyped params
10187
10188
  });
10188
10189
  }
10189
10190
  }
@@ -10209,7 +10210,15 @@ function findSources(calls, types, patterns) {
10209
10210
  }
10210
10211
  }
10211
10212
  }
10212
- return sources;
10213
+ const sourceMap = /* @__PURE__ */ new Map();
10214
+ for (const source of sources) {
10215
+ const key = `${source.line}:${source.type}`;
10216
+ const existing = sourceMap.get(key);
10217
+ if (!existing || source.confidence > existing.confidence) {
10218
+ sourceMap.set(key, source);
10219
+ }
10220
+ }
10221
+ return Array.from(sourceMap.values());
10213
10222
  }
10214
10223
  function isInterproceduralTaintableType(typeName) {
10215
10224
  const baseType = typeName.split("<")[0].trim();
@@ -10293,21 +10302,27 @@ function isInterproceduralTaintableType(typeName) {
10293
10302
  return false;
10294
10303
  }
10295
10304
  function findSinks(calls, patterns) {
10296
- const sinks = [];
10305
+ const sinkMap = /* @__PURE__ */ new Map();
10297
10306
  for (const call of calls) {
10298
10307
  for (const pattern of patterns) {
10299
10308
  if (matchesSinkPattern(call, pattern)) {
10300
- sinks.push({
10301
- type: pattern.type,
10302
- cwe: pattern.cwe,
10303
- location: formatCallLocation(call),
10304
- line: call.location.line,
10305
- confidence: calculateSinkConfidence(call, pattern)
10306
- });
10309
+ const location = formatCallLocation(call);
10310
+ const key = `${location}:${call.location.line}:${pattern.cwe}`;
10311
+ const confidence = calculateSinkConfidence(call, pattern);
10312
+ const existing = sinkMap.get(key);
10313
+ if (!existing || confidence > existing.confidence) {
10314
+ sinkMap.set(key, {
10315
+ type: pattern.type,
10316
+ cwe: pattern.cwe,
10317
+ location,
10318
+ line: call.location.line,
10319
+ confidence
10320
+ });
10321
+ }
10307
10322
  }
10308
10323
  }
10309
10324
  }
10310
- return sinks;
10325
+ return Array.from(sinkMap.values());
10311
10326
  }
10312
10327
  function matchesSourcePattern(call, pattern) {
10313
10328
  if (pattern.method) {
@@ -14269,16 +14284,36 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
14269
14284
  confidence = Math.max(confidence, 0.95);
14270
14285
  indicators.push(`import: ${path}`);
14271
14286
  }
14272
- if (path === "react" || path.startsWith("react/")) {
14287
+ if (path === "react" || path.startsWith("react/") || path === "react-dom") {
14273
14288
  framework = framework || "react";
14274
14289
  confidence = Math.max(confidence, 0.8);
14275
14290
  indicators.push(`import: ${path}`);
14276
14291
  }
14292
+ if (path === "react-native" || path.startsWith("react-native/") || path.startsWith("@react-native/")) {
14293
+ framework = "react-native";
14294
+ confidence = Math.max(confidence, 0.95);
14295
+ indicators.push(`import: ${path}`);
14296
+ }
14297
+ if (path.startsWith("@react-navigation/")) {
14298
+ framework = framework || "react-native";
14299
+ confidence = Math.max(confidence, 0.9);
14300
+ indicators.push(`import: ${path}`);
14301
+ }
14302
+ if (path === "react-router" || path === "react-router-dom" || path.startsWith("react-router/")) {
14303
+ framework = framework || "react";
14304
+ confidence = Math.max(confidence, 0.85);
14305
+ indicators.push(`import: ${path}`);
14306
+ }
14277
14307
  if (path === "next" || path.startsWith("next/")) {
14278
14308
  framework = "nextjs";
14279
14309
  confidence = Math.max(confidence, 0.9);
14280
14310
  indicators.push(`import: ${path}`);
14281
14311
  }
14312
+ if (path === "expo" || path.startsWith("expo-")) {
14313
+ framework = framework || "react-native";
14314
+ confidence = Math.max(confidence, 0.85);
14315
+ indicators.push(`import: ${path}`);
14316
+ }
14282
14317
  }
14283
14318
  if (framework) {
14284
14319
  return { name: framework, confidence, indicators };
@@ -14390,6 +14425,167 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
14390
14425
  severity: "medium",
14391
14426
  confidence: 0.8,
14392
14427
  returnTainted: true
14428
+ },
14429
+ // =========================================================
14430
+ // React Router Sources
14431
+ // =========================================================
14432
+ {
14433
+ method: "useParams",
14434
+ type: "http_path",
14435
+ severity: "high",
14436
+ confidence: 0.95,
14437
+ returnTainted: true
14438
+ },
14439
+ {
14440
+ method: "useSearchParams",
14441
+ type: "http_param",
14442
+ severity: "high",
14443
+ confidence: 0.95,
14444
+ returnTainted: true
14445
+ },
14446
+ {
14447
+ method: "useLocation",
14448
+ type: "url_param",
14449
+ severity: "high",
14450
+ confidence: 0.9,
14451
+ returnTainted: true
14452
+ },
14453
+ // =========================================================
14454
+ // Next.js Sources
14455
+ // =========================================================
14456
+ {
14457
+ method: "useRouter",
14458
+ type: "http_param",
14459
+ severity: "high",
14460
+ confidence: 0.9,
14461
+ returnTainted: true
14462
+ // router.query, router.asPath
14463
+ },
14464
+ {
14465
+ method: "useSearchParams",
14466
+ // Next.js App Router
14467
+ type: "http_param",
14468
+ severity: "high",
14469
+ confidence: 0.95,
14470
+ returnTainted: true
14471
+ },
14472
+ {
14473
+ method: "usePathname",
14474
+ type: "http_path",
14475
+ severity: "high",
14476
+ confidence: 0.9,
14477
+ returnTainted: true
14478
+ },
14479
+ {
14480
+ // getServerSideProps/getStaticProps context.params
14481
+ method: "params",
14482
+ type: "http_path",
14483
+ severity: "high",
14484
+ confidence: 0.85,
14485
+ returnTainted: true
14486
+ },
14487
+ // =========================================================
14488
+ // React Native Sources
14489
+ // =========================================================
14490
+ {
14491
+ // React Navigation route params
14492
+ method: "useRoute",
14493
+ type: "navigation_param",
14494
+ severity: "high",
14495
+ confidence: 0.9,
14496
+ returnTainted: true
14497
+ },
14498
+ {
14499
+ // Deep linking
14500
+ method: "getInitialURL",
14501
+ class: "Linking",
14502
+ type: "url_param",
14503
+ severity: "high",
14504
+ confidence: 0.95,
14505
+ returnTainted: true
14506
+ },
14507
+ {
14508
+ method: "addEventListener",
14509
+ class: "Linking",
14510
+ type: "url_param",
14511
+ severity: "high",
14512
+ confidence: 0.9,
14513
+ returnTainted: true
14514
+ },
14515
+ {
14516
+ method: "parse",
14517
+ class: "Linking",
14518
+ type: "url_param",
14519
+ severity: "high",
14520
+ confidence: 0.9,
14521
+ returnTainted: true
14522
+ },
14523
+ {
14524
+ // Clipboard content
14525
+ method: "getString",
14526
+ class: "Clipboard",
14527
+ type: "user_input",
14528
+ severity: "medium",
14529
+ confidence: 0.85,
14530
+ returnTainted: true
14531
+ },
14532
+ {
14533
+ method: "getStringAsync",
14534
+ class: "Clipboard",
14535
+ type: "user_input",
14536
+ severity: "medium",
14537
+ confidence: 0.85,
14538
+ returnTainted: true
14539
+ },
14540
+ {
14541
+ // AsyncStorage (may contain user data)
14542
+ method: "getItem",
14543
+ class: "AsyncStorage",
14544
+ type: "storage_input",
14545
+ severity: "medium",
14546
+ confidence: 0.7,
14547
+ returnTainted: true
14548
+ },
14549
+ {
14550
+ method: "multiGet",
14551
+ class: "AsyncStorage",
14552
+ type: "storage_input",
14553
+ severity: "medium",
14554
+ confidence: 0.7,
14555
+ returnTainted: true
14556
+ },
14557
+ {
14558
+ // SecureStore (Expo)
14559
+ method: "getItemAsync",
14560
+ class: "SecureStore",
14561
+ type: "storage_input",
14562
+ severity: "medium",
14563
+ confidence: 0.7,
14564
+ returnTainted: true
14565
+ },
14566
+ // =========================================================
14567
+ // Browser/DOM Sources (React web apps)
14568
+ // =========================================================
14569
+ {
14570
+ method: "localStorage.getItem",
14571
+ type: "storage_input",
14572
+ severity: "medium",
14573
+ confidence: 0.7,
14574
+ returnTainted: true
14575
+ },
14576
+ {
14577
+ method: "sessionStorage.getItem",
14578
+ type: "storage_input",
14579
+ severity: "medium",
14580
+ confidence: 0.7,
14581
+ returnTainted: true
14582
+ },
14583
+ {
14584
+ method: "postMessage",
14585
+ type: "message_input",
14586
+ severity: "high",
14587
+ confidence: 0.85,
14588
+ returnTainted: true
14393
14589
  }
14394
14590
  ];
14395
14591
  }
@@ -14569,6 +14765,158 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
14569
14765
  cwe: "CWE-1321",
14570
14766
  severity: "high",
14571
14767
  argPositions: [0, 1]
14768
+ },
14769
+ // =========================================================
14770
+ // React XSS Sinks
14771
+ // =========================================================
14772
+ {
14773
+ // Most common React XSS vector
14774
+ method: "dangerouslySetInnerHTML",
14775
+ type: "xss",
14776
+ cwe: "CWE-79",
14777
+ severity: "critical",
14778
+ argPositions: [0]
14779
+ // The __html property value
14780
+ },
14781
+ {
14782
+ // Rendering user-controlled href with javascript:
14783
+ method: "href",
14784
+ type: "xss",
14785
+ cwe: "CWE-79",
14786
+ severity: "high",
14787
+ argPositions: [0]
14788
+ },
14789
+ {
14790
+ // createRef().current.innerHTML
14791
+ method: "current.innerHTML",
14792
+ type: "xss",
14793
+ cwe: "CWE-79",
14794
+ severity: "high",
14795
+ argPositions: [0]
14796
+ },
14797
+ // =========================================================
14798
+ // React Native Sinks
14799
+ // =========================================================
14800
+ {
14801
+ // WebView with user-controlled source
14802
+ method: "source",
14803
+ class: "WebView",
14804
+ type: "xss",
14805
+ cwe: "CWE-79",
14806
+ severity: "critical",
14807
+ argPositions: [0]
14808
+ // { html: userInput } or { uri: userInput }
14809
+ },
14810
+ {
14811
+ // Open arbitrary URLs
14812
+ method: "openURL",
14813
+ class: "Linking",
14814
+ type: "open_redirect",
14815
+ cwe: "CWE-601",
14816
+ severity: "high",
14817
+ argPositions: [0]
14818
+ },
14819
+ {
14820
+ method: "canOpenURL",
14821
+ class: "Linking",
14822
+ type: "ssrf",
14823
+ cwe: "CWE-918",
14824
+ severity: "medium",
14825
+ argPositions: [0]
14826
+ },
14827
+ {
14828
+ // Expo WebBrowser
14829
+ method: "openBrowserAsync",
14830
+ class: "WebBrowser",
14831
+ type: "open_redirect",
14832
+ cwe: "CWE-601",
14833
+ severity: "high",
14834
+ argPositions: [0]
14835
+ },
14836
+ {
14837
+ method: "openAuthSessionAsync",
14838
+ class: "WebBrowser",
14839
+ type: "open_redirect",
14840
+ cwe: "CWE-601",
14841
+ severity: "high",
14842
+ argPositions: [0]
14843
+ },
14844
+ // =========================================================
14845
+ // Next.js Sinks
14846
+ // =========================================================
14847
+ {
14848
+ // Server-side redirect
14849
+ method: "redirect",
14850
+ type: "open_redirect",
14851
+ cwe: "CWE-601",
14852
+ severity: "high",
14853
+ argPositions: [0]
14854
+ },
14855
+ {
14856
+ // Router push with user-controlled URL
14857
+ method: "push",
14858
+ class: "router",
14859
+ type: "open_redirect",
14860
+ cwe: "CWE-601",
14861
+ severity: "medium",
14862
+ argPositions: [0]
14863
+ },
14864
+ {
14865
+ method: "replace",
14866
+ class: "router",
14867
+ type: "open_redirect",
14868
+ cwe: "CWE-601",
14869
+ severity: "medium",
14870
+ argPositions: [0]
14871
+ },
14872
+ // =========================================================
14873
+ // React/General JS Security Sinks
14874
+ // =========================================================
14875
+ {
14876
+ // Dynamic component loading
14877
+ method: "createElement",
14878
+ class: "React",
14879
+ type: "code_injection",
14880
+ cwe: "CWE-94",
14881
+ severity: "high",
14882
+ argPositions: [0]
14883
+ // When first arg is user-controlled string
14884
+ },
14885
+ {
14886
+ // Importing user-controlled modules
14887
+ method: "import",
14888
+ type: "code_injection",
14889
+ cwe: "CWE-94",
14890
+ severity: "critical",
14891
+ argPositions: [0]
14892
+ },
14893
+ {
14894
+ method: "require",
14895
+ type: "code_injection",
14896
+ cwe: "CWE-94",
14897
+ severity: "critical",
14898
+ argPositions: [0]
14899
+ },
14900
+ // =========================================================
14901
+ // Data Exposure Sinks (React Native)
14902
+ // =========================================================
14903
+ {
14904
+ // Logging sensitive data
14905
+ method: "log",
14906
+ class: "console",
14907
+ type: "information_exposure",
14908
+ cwe: "CWE-532",
14909
+ severity: "low",
14910
+ argPositions: [0]
14911
+ },
14912
+ {
14913
+ // Storing sensitive data insecurely
14914
+ method: "setItem",
14915
+ class: "AsyncStorage",
14916
+ type: "insecure_storage",
14917
+ cwe: "CWE-922",
14918
+ severity: "medium",
14919
+ argPositions: [1]
14572
14920
  }
14573
14921
  ];
14574
14922
  }
@@ -15650,6 +15998,113 @@ function findGetterSources(types, instanceFieldTaint, sourceCode) {
15650
15998
  }
15651
15999
  return sources;
15652
16000
  }
16001
+ var JS_DOM_XSS_SINKS = [
16002
+ { pattern: /\.innerHTML\s*=/, type: "xss", cwe: "CWE-79", severity: "critical" },
16003
+ { pattern: /\.outerHTML\s*=/, type: "xss", cwe: "CWE-79", severity: "critical" },
16004
+ { pattern: /document\.write\s*\(/, type: "xss", cwe: "CWE-79", severity: "critical" },
16005
+ { pattern: /document\.writeln\s*\(/, type: "xss", cwe: "CWE-79", severity: "critical" },
16006
+ { pattern: /\.insertAdjacentHTML\s*\(/, type: "xss", cwe: "CWE-79", severity: "critical" },
16007
+ { pattern: /\.src\s*=/, type: "xss", cwe: "CWE-79", severity: "high" },
16008
+ { pattern: /\.href\s*=/, type: "xss", cwe: "CWE-79", severity: "high" }
16009
+ ];
16010
+ var JS_TAINTED_PATTERNS = [
16011
+ { pattern: /\breq\.query\b/, type: "http_param" },
16012
+ { pattern: /\breq\.params\b/, type: "http_param" },
16013
+ { pattern: /\breq\.body\b/, type: "http_body" },
16014
+ { pattern: /\breq\.headers\b/, type: "http_header" },
16015
+ { pattern: /\breq\.cookies\b/, type: "http_cookie" },
16016
+ { pattern: /\breq\.url\b/, type: "http_path" },
16017
+ { pattern: /\breq\.path\b/, type: "http_path" },
16018
+ { pattern: /\breq\.originalUrl\b/, type: "http_path" },
16019
+ { pattern: /\breq\.files?\b/, type: "file_input" },
16020
+ { pattern: /\brequest\.query\b/, type: "http_param" },
16021
+ { pattern: /\brequest\.params\b/, type: "http_param" },
16022
+ { pattern: /\brequest\.body\b/, type: "http_body" },
16023
+ { pattern: /\brequest\.headers\b/, type: "http_header" },
16024
+ { pattern: /\bctx\.query\b/, type: "http_param" },
16025
+ { pattern: /\bctx\.params\b/, type: "http_param" },
16026
+ { pattern: /\bctx\.request\b/, type: "http_body" },
16027
+ { pattern: /\bprocess\.env\b/, type: "env_input" },
16028
+ { pattern: /\bprocess\.argv\b/, type: "io_input" },
16029
+ { pattern: /\blocation\.search\b/, type: "http_param" },
16030
+ { pattern: /\blocation\.hash\b/, type: "http_param" },
16031
+ { pattern: /\blocation\.href\b/, type: "http_path" },
16032
+ { pattern: /\bdocument\.getElementById\b/, type: "dom_input" },
16033
+ { pattern: /\bdocument\.querySelector\b/, type: "dom_input" },
16034
+ { pattern: /\.value\b/, type: "dom_input" }
16035
+ ];
16036
+ function findJavaScriptAssignmentSources(sourceCode, language) {
16037
+ const sources = [];
16038
+ if (!["javascript", "typescript"].includes(language)) {
16039
+ return sources;
16040
+ }
16041
+ const lines = sourceCode.split("\n");
16042
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
16043
+ const line = lines[lineNum];
16044
+ const lineNumber = lineNum + 1;
16045
+ const assignmentMatch = line.match(/(?:(?:var|let|const)\s+)?(\w+)\s*=\s*(.+)/);
16046
+ if (assignmentMatch) {
16047
+ const varName = assignmentMatch[1];
16048
+ const rhs = assignmentMatch[2];
16049
+ for (const { pattern, type } of JS_TAINTED_PATTERNS) {
16050
+ if (pattern.test(rhs)) {
16051
+ const alreadyExists = sources.some(
16052
+ (s) => s.line === lineNumber && s.type === type
16053
+ );
16054
+ if (!alreadyExists) {
16055
+ sources.push({
16056
+ type,
16057
+ location: `${varName} = ${rhs.trim().substring(0, 50)}${rhs.length > 50 ? "..." : ""}`,
16058
+ severity: "high",
16059
+ line: lineNumber,
16060
+ confidence: 1,
16061
+ variable: varName
16062
+ });
16063
+ }
16064
+ break;
16065
+ }
16066
+ }
16067
+ }
16068
+ }
16069
+ return sources;
16070
+ }
16071
+ function findJavaScriptDOMSinks(sourceCode, language) {
16072
+ const sinks = [];
16073
+ if (!["javascript", "typescript"].includes(language)) {
16074
+ return sinks;
16075
+ }
16076
+ const lines = sourceCode.split("\n");
16077
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
16078
+ const line = lines[lineNum];
16079
+ const lineNumber = lineNum + 1;
16080
+ for (const { pattern, type, cwe, severity } of JS_DOM_XSS_SINKS) {
16081
+ if (pattern.test(line)) {
16082
+ let method = "innerHTML";
16083
+ if (line.includes(".outerHTML")) method = "outerHTML";
16084
+ else if (line.includes("document.write(")) method = "document.write";
16085
+ else if (line.includes("document.writeln(")) method = "document.writeln";
16086
+ else if (line.includes(".insertAdjacentHTML")) method = "insertAdjacentHTML";
16087
+ else if (line.includes(".src")) method = "src";
16088
+ else if (line.includes(".href")) method = "href";
16089
+ const alreadyExists = sinks.some(
16090
+ (s) => s.line === lineNumber && s.cwe === cwe
16091
+ );
16092
+ if (!alreadyExists) {
16093
+ sinks.push({
16094
+ type,
16095
+ cwe,
16096
+ severity,
16097
+ line: lineNumber,
16098
+ location: line.trim().substring(0, 80),
16099
+ method
16100
+ });
16101
+ }
16102
+ break;
16103
+ }
16104
+ }
16105
+ }
16106
+ return sinks;
16107
+ }
15653
16108
  var initialized = false;
15654
16109
  async function initAnalyzer(options = {}) {
15655
16110
  if (initialized) return;
@@ -15802,11 +16257,30 @@ async function analyze(code, filePath, language, options = {}) {
15802
16257
  const taint = analyzeTaint(calls, types, baseConfig);
15803
16258
  const getterSources = findGetterSources(types, constPropResult.instanceFieldTaint, code);
15804
16259
  taint.sources.push(...getterSources);
16260
+ const jsAssignmentSources = findJavaScriptAssignmentSources(code, language);
16261
+ taint.sources.push(...jsAssignmentSources);
16262
+ const jsDOMSinks = findJavaScriptDOMSinks(code, language);
16263
+ for (const domSink of jsDOMSinks) {
16264
+ const alreadyExists = taint.sinks.some(
16265
+ (s) => s.line === domSink.line && s.cwe === domSink.cwe
16266
+ );
16267
+ if (!alreadyExists) {
16268
+ taint.sinks.push({
16269
+ type: "xss",
16270
+ cwe: domSink.cwe,
16271
+ line: domSink.line,
16272
+ location: domSink.location,
16273
+ method: domSink.method,
16274
+ confidence: 1
16275
+ });
16276
+ }
16277
+ }
15805
16278
  logger.debug("Initial taint analysis", {
15806
16279
  sources: taint.sources.length,
15807
16280
  sinks: taint.sinks.length,
15808
16281
  sanitizers: taint.sanitizers?.length ?? 0,
15809
- getterSources: getterSources.length
16282
+ getterSources: getterSources.length,
16283
+ jsDOMSinks: jsDOMSinks.length
15810
16284
  });
15811
16285
  taint.sinks = taint.sinks.filter((sink) => !constPropResult.unreachableLines.has(sink.line));
15812
16286
  taint.sinks = filterCleanArraySinks(
@@ -10283,15 +10283,16 @@ function findSources(calls, types, patterns) {
10283
10283
  const skipMethods = ["toString", "hashCode", "equals", "compareTo", "getDescription", "getVulnerabilityCount"];
10284
10284
  if (skipMethods.includes(method.name)) continue;
10285
10285
  for (const param of method.parameters) {
10286
- if (param.type && isInterproceduralTaintableType(param.type)) {
10286
+ const isTaintable = param.type ? isInterproceduralTaintableType(param.type) : true;
10287
+ if (isTaintable) {
10287
10288
  const paramLine = param.line ?? method.start_line;
10288
10289
  sources.push({
10289
10290
  type: "interprocedural_param",
10290
- location: `${param.type} ${param.name} in ${method.name}`,
10291
+ location: `${param.type || "any"} ${param.name} in ${method.name}`,
10291
10292
  severity: "medium",
10292
10293
  line: paramLine,
10293
- confidence: 0.7
10294
- // Lower confidence since we don't know the call site
10294
+ confidence: param.type ? 0.7 : 0.5
10295
+ // Lower confidence for untyped params
10295
10296
  });
10296
10297
  }
10297
10298
  }
@@ -10317,7 +10318,15 @@ function findSources(calls, types, patterns) {
10317
10318
  }
10318
10319
  }
10319
10320
  }
10320
- return sources;
10321
+ const sourceMap = /* @__PURE__ */ new Map();
10322
+ for (const source of sources) {
10323
+ const key = `${source.line}:${source.type}`;
10324
+ const existing = sourceMap.get(key);
10325
+ if (!existing || source.confidence > existing.confidence) {
10326
+ sourceMap.set(key, source);
10327
+ }
10328
+ }
10329
+ return Array.from(sourceMap.values());
10321
10330
  }
10322
10331
  function isInterproceduralTaintableType(typeName) {
10323
10332
  const baseType = typeName.split("<")[0].trim();
@@ -10401,21 +10410,27 @@ function isInterproceduralTaintableType(typeName) {
10401
10410
  return false;
10402
10411
  }
10403
10412
  function findSinks(calls, patterns) {
10404
- const sinks = [];
10413
+ const sinkMap = /* @__PURE__ */ new Map();
10405
10414
  for (const call of calls) {
10406
10415
  for (const pattern of patterns) {
10407
10416
  if (matchesSinkPattern(call, pattern)) {
10408
- sinks.push({
10409
- type: pattern.type,
10410
- cwe: pattern.cwe,
10411
- location: formatCallLocation(call),
10412
- line: call.location.line,
10413
- confidence: calculateSinkConfidence(call, pattern)
10414
- });
10417
+ const location = formatCallLocation(call);
10418
+ const key = `${location}:${call.location.line}:${pattern.cwe}`;
10419
+ const confidence = calculateSinkConfidence(call, pattern);
10420
+ const existing = sinkMap.get(key);
10421
+ if (!existing || confidence > existing.confidence) {
10422
+ sinkMap.set(key, {
10423
+ type: pattern.type,
10424
+ cwe: pattern.cwe,
10425
+ location,
10426
+ line: call.location.line,
10427
+ confidence
10428
+ });
10429
+ }
10415
10430
  }
10416
10431
  }
10417
10432
  }
10418
- return sinks;
10433
+ return Array.from(sinkMap.values());
10419
10434
  }
10420
10435
  function matchesSourcePattern(call, pattern) {
10421
10436
  if (pattern.method) {