circle-ir 3.154.0 → 3.156.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.
Files changed (46) hide show
  1. package/configs/sinks/code_injection.yaml +2 -1
  2. package/configs/sinks/javascript_dom_xss.yaml +4 -2
  3. package/configs/sinks/xpath.yaml +40 -0
  4. package/dist/analysis/config-loader.d.ts.map +1 -1
  5. package/dist/analysis/config-loader.js +3 -0
  6. package/dist/analysis/config-loader.js.map +1 -1
  7. package/dist/analysis/dfg-walk.d.ts +36 -0
  8. package/dist/analysis/dfg-walk.d.ts.map +1 -0
  9. package/dist/analysis/dfg-walk.js +59 -0
  10. package/dist/analysis/dfg-walk.js.map +1 -0
  11. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  12. package/dist/analysis/passes/language-sources-pass.js +22 -0
  13. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  14. package/dist/analysis/passes/mybatis-annotation-sql-sink-pass.d.ts +49 -0
  15. package/dist/analysis/passes/mybatis-annotation-sql-sink-pass.d.ts.map +1 -0
  16. package/dist/analysis/passes/mybatis-annotation-sql-sink-pass.js +329 -0
  17. package/dist/analysis/passes/mybatis-annotation-sql-sink-pass.js.map +1 -0
  18. package/dist/analysis/passes/weak-crypto-pass.d.ts.map +1 -1
  19. package/dist/analysis/passes/weak-crypto-pass.js +6 -0
  20. package/dist/analysis/passes/weak-crypto-pass.js.map +1 -1
  21. package/dist/analysis/path-classification.d.ts +24 -0
  22. package/dist/analysis/path-classification.d.ts.map +1 -0
  23. package/dist/analysis/path-classification.js +69 -0
  24. package/dist/analysis/path-classification.js.map +1 -0
  25. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  26. package/dist/analysis/taint-matcher.js +30 -0
  27. package/dist/analysis/taint-matcher.js.map +1 -1
  28. package/dist/analysis/taint-propagation.d.ts +17 -1
  29. package/dist/analysis/taint-propagation.d.ts.map +1 -1
  30. package/dist/analysis/taint-propagation.js +47 -16
  31. package/dist/analysis/taint-propagation.js.map +1 -1
  32. package/dist/analyzer.d.ts.map +1 -1
  33. package/dist/analyzer.js +8 -0
  34. package/dist/analyzer.js.map +1 -1
  35. package/dist/browser/circle-ir.js +475 -12
  36. package/dist/core/circle-ir-core.cjs +96 -12
  37. package/dist/core/circle-ir-core.js +96 -12
  38. package/dist/graph/code-graph.d.ts +7 -0
  39. package/dist/graph/code-graph.d.ts.map +1 -1
  40. package/dist/graph/code-graph.js +17 -0
  41. package/dist/graph/code-graph.js.map +1 -1
  42. package/dist/resolution/type-hierarchy.d.ts +15 -0
  43. package/dist/resolution/type-hierarchy.d.ts.map +1 -1
  44. package/dist/resolution/type-hierarchy.js +159 -0
  45. package/dist/resolution/type-hierarchy.js.map +1 -1
  46. package/package.json +1 -1
@@ -12902,6 +12902,9 @@ var DEFAULT_SANITIZERS = [
12902
12902
  { method: "parseDouble", class: "Double", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12903
12903
  { method: "parseShort", class: "Short", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12904
12904
  { method: "parseByte", class: "Byte", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12905
+ // cognium-dev #238 A.3 — parseBoolean and expanded coercion coverage for XSS/CRLF/log/xpath/ldap/xxe.
12906
+ // A parsed boolean/numeric cannot carry a string-injection payload for any string-based sink.
12907
+ { method: "parseBoolean", class: "Boolean", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection", "xss", "crlf", "log_injection", "xpath_injection", "ldap_injection", "xxe"] },
12905
12908
  // Java UUID parse — UUID.fromString rejects non-UUID strings
12906
12909
  { method: "fromString", class: "UUID", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12907
12910
  // JavaScript numeric coercion (Number/parseInt/parseFloat already covered above; add path_traversal/code_injection)
@@ -14182,8 +14185,11 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
14182
14185
  if (pattern.class === "constructor") {
14183
14186
  return true;
14184
14187
  }
14188
+ const subtypeArgArityOk = !pattern.arg_positions || pattern.arg_positions.length === 0 || pattern.arg_positions.every((pos) => pos < (call.arguments?.length ?? 0));
14185
14189
  if (call.receiver_type && call.receiver_type === pattern.class) {
14190
+ } else if (call.receiver_type && typeHierarchy && subtypeArgArityOk && typeHierarchy.isSubtypeOf(call.receiver_type, pattern.class)) {
14186
14191
  } else if (call.receiver_type_fqn && call.receiver_type_fqn.endsWith("." + pattern.class)) {
14192
+ } else if (call.receiver_type_fqn && typeHierarchy && subtypeArgArityOk && typeHierarchy.isSubtypeOf(call.receiver_type_fqn, pattern.class)) {
14187
14193
  } else if (call.receiver && !receiverMightBeClass(call.receiver, pattern.class)) {
14188
14194
  if (typeHierarchy && typeHierarchy.couldBeType(call.receiver, pattern.class)) {
14189
14195
  return true;
@@ -15033,6 +15039,23 @@ var CodeGraph = class {
15033
15039
  }
15034
15040
  return this._chainsByFromDef;
15035
15041
  }
15042
+ /**
15043
+ * Mirror of chainsByFromDef keyed by chain.to_def. Enables backward
15044
+ * walks from a use's reaching def to earlier defs in the chain
15045
+ * (cognium-dev #238 — DFG-walk sanitizer credit).
15046
+ */
15047
+ _chainsByToDef = null;
15048
+ get chainsByToDef() {
15049
+ if (!this._chainsByToDef) {
15050
+ this._chainsByToDef = /* @__PURE__ */ new Map();
15051
+ for (const chain of this.ir.dfg.chains ?? []) {
15052
+ const arr = this._chainsByToDef.get(chain.to_def) ?? [];
15053
+ arr.push(chain);
15054
+ this._chainsByToDef.set(chain.to_def, arr);
15055
+ }
15056
+ }
15057
+ return this._chainsByToDef;
15058
+ }
15036
15059
  // ---------------------------------------------------------------------------
15037
15060
  // Call indexes
15038
15061
  // ---------------------------------------------------------------------------
@@ -15668,8 +15691,150 @@ function createWithJdkTypes() {
15668
15691
  for (const type of [...jdbcTypes, ...ioTypes, ...servletTypes]) {
15669
15692
  resolver.addType(type, "jdk", type.package);
15670
15693
  }
15694
+ registerCommonLibraries(resolver);
15671
15695
  return resolver;
15672
15696
  }
15697
+ function registerCommonLibraries(resolver) {
15698
+ const apacheHttpClient4x = [
15699
+ {
15700
+ name: "HttpClient",
15701
+ kind: "interface",
15702
+ package: "org.apache.http.client",
15703
+ extends: null,
15704
+ implements: [],
15705
+ annotations: [],
15706
+ methods: [],
15707
+ fields: [],
15708
+ start_line: 0,
15709
+ end_line: 0
15710
+ },
15711
+ {
15712
+ name: "AbstractHttpClient",
15713
+ kind: "class",
15714
+ package: "org.apache.http.impl.client",
15715
+ extends: null,
15716
+ implements: ["org.apache.http.client.HttpClient"],
15717
+ annotations: [],
15718
+ methods: [],
15719
+ fields: [],
15720
+ start_line: 0,
15721
+ end_line: 0
15722
+ },
15723
+ {
15724
+ name: "CloseableHttpClient",
15725
+ kind: "class",
15726
+ package: "org.apache.http.impl.client",
15727
+ extends: null,
15728
+ implements: ["org.apache.http.client.HttpClient"],
15729
+ annotations: [],
15730
+ methods: [],
15731
+ fields: [],
15732
+ start_line: 0,
15733
+ end_line: 0
15734
+ },
15735
+ {
15736
+ name: "InternalHttpClient",
15737
+ kind: "class",
15738
+ package: "org.apache.http.impl.client",
15739
+ extends: "org.apache.http.impl.client.CloseableHttpClient",
15740
+ implements: [],
15741
+ annotations: [],
15742
+ methods: [],
15743
+ fields: [],
15744
+ start_line: 0,
15745
+ end_line: 0
15746
+ },
15747
+ {
15748
+ name: "MinimalHttpClient",
15749
+ kind: "class",
15750
+ package: "org.apache.http.impl.client",
15751
+ extends: "org.apache.http.impl.client.CloseableHttpClient",
15752
+ implements: [],
15753
+ annotations: [],
15754
+ methods: [],
15755
+ fields: [],
15756
+ start_line: 0,
15757
+ end_line: 0
15758
+ },
15759
+ {
15760
+ name: "DefaultHttpClient",
15761
+ kind: "class",
15762
+ package: "org.apache.http.impl.client",
15763
+ extends: "org.apache.http.impl.client.AbstractHttpClient",
15764
+ implements: [],
15765
+ annotations: [],
15766
+ methods: [],
15767
+ fields: [],
15768
+ start_line: 0,
15769
+ end_line: 0
15770
+ },
15771
+ {
15772
+ name: "SystemDefaultHttpClient",
15773
+ kind: "class",
15774
+ package: "org.apache.http.impl.client",
15775
+ extends: "org.apache.http.impl.client.DefaultHttpClient",
15776
+ implements: [],
15777
+ annotations: [],
15778
+ methods: [],
15779
+ fields: [],
15780
+ start_line: 0,
15781
+ end_line: 0
15782
+ }
15783
+ ];
15784
+ const apacheHttpClient5x = [
15785
+ {
15786
+ name: "HttpClient",
15787
+ kind: "interface",
15788
+ package: "org.apache.hc.client5.http.classic",
15789
+ extends: null,
15790
+ implements: [],
15791
+ annotations: [],
15792
+ methods: [],
15793
+ fields: [],
15794
+ start_line: 0,
15795
+ end_line: 0
15796
+ },
15797
+ {
15798
+ name: "CloseableHttpClient",
15799
+ kind: "class",
15800
+ package: "org.apache.hc.client5.http.impl.classic",
15801
+ extends: null,
15802
+ implements: ["org.apache.hc.client5.http.classic.HttpClient"],
15803
+ annotations: [],
15804
+ methods: [],
15805
+ fields: [],
15806
+ start_line: 0,
15807
+ end_line: 0
15808
+ },
15809
+ {
15810
+ name: "InternalHttpClient",
15811
+ kind: "class",
15812
+ package: "org.apache.hc.client5.http.impl.classic",
15813
+ extends: "org.apache.hc.client5.http.impl.classic.CloseableHttpClient",
15814
+ implements: [],
15815
+ annotations: [],
15816
+ methods: [],
15817
+ fields: [],
15818
+ start_line: 0,
15819
+ end_line: 0
15820
+ },
15821
+ {
15822
+ name: "MinimalHttpClient",
15823
+ kind: "class",
15824
+ package: "org.apache.hc.client5.http.impl.classic",
15825
+ extends: "org.apache.hc.client5.http.impl.classic.CloseableHttpClient",
15826
+ implements: [],
15827
+ annotations: [],
15828
+ methods: [],
15829
+ fields: [],
15830
+ start_line: 0,
15831
+ end_line: 0
15832
+ }
15833
+ ];
15834
+ for (const type of [...apacheHttpClient4x, ...apacheHttpClient5x]) {
15835
+ resolver.addType(type, "common-libraries", type.package);
15836
+ }
15837
+ }
15673
15838
 
15674
15839
  // src/graph/dominator-graph.ts
15675
15840
  function computeRPO(cfg, entryId) {
@@ -15957,6 +16122,43 @@ var AnalysisPipeline = class {
15957
16122
  }
15958
16123
  };
15959
16124
 
16125
+ // src/analysis/dfg-walk.ts
16126
+ function walkBackwardDefs(startDefId, chainsByToDef, defById, options = {}) {
16127
+ const maxHops = options.maxHops ?? 32;
16128
+ const visited = /* @__PURE__ */ new Set();
16129
+ const lines = /* @__PURE__ */ new Set();
16130
+ let hopCapReached = false;
16131
+ const startDef = defById.get(startDefId);
16132
+ if (!startDef) {
16133
+ return { visited, lines, hopCapReached: false };
16134
+ }
16135
+ visited.add(startDefId);
16136
+ lines.add(startDef.line);
16137
+ const queue = [startDefId];
16138
+ let hops = 0;
16139
+ while (queue.length > 0) {
16140
+ if (hops >= maxHops) {
16141
+ hopCapReached = true;
16142
+ break;
16143
+ }
16144
+ hops++;
16145
+ const currentId = queue.shift();
16146
+ const incoming = chainsByToDef.get(currentId);
16147
+ if (!incoming) continue;
16148
+ for (const chain of incoming) {
16149
+ const fromId = chain.from_def;
16150
+ if (visited.has(fromId)) continue;
16151
+ visited.add(fromId);
16152
+ const fromDef = defById.get(fromId);
16153
+ if (fromDef) {
16154
+ lines.add(fromDef.line);
16155
+ }
16156
+ queue.push(fromId);
16157
+ }
16158
+ }
16159
+ return { visited, lines, hopCapReached };
16160
+ }
16161
+
15960
16162
  // src/analysis/taint-propagation.ts
15961
16163
  function buildSanitizersByLine(sanitizers) {
15962
16164
  const out2 = /* @__PURE__ */ new Map();
@@ -16044,7 +16246,12 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
16044
16246
  taintInfo.line,
16045
16247
  sink.line,
16046
16248
  sink.type,
16047
- sanitizersByLine
16249
+ sanitizersByLine,
16250
+ {
16251
+ startDefId: use.def_id,
16252
+ chainsByToDef: graph.chainsByToDef,
16253
+ defById
16254
+ }
16048
16255
  );
16049
16256
  if (!isSanitized.sanitized) {
16050
16257
  const source = sources.find((s) => s.line === taintInfo.sourceLine);
@@ -16182,20 +16389,39 @@ var KNOWN_SINK_TYPES = /* @__PURE__ */ new Set([
16182
16389
  "crlf",
16183
16390
  "mass_assignment"
16184
16391
  ]);
16185
- function checkSanitized(_fromLine, toLine, sinkType, sanitizersByLine) {
16186
- const sanitizersAtTarget = sanitizersByLine.get(toLine);
16187
- if (!sanitizersAtTarget || sanitizersAtTarget.length === 0) {
16188
- return { sanitized: false };
16189
- }
16392
+ function checkSanitized(_fromLine, toLine, sinkType, sanitizersByLine, ctx) {
16190
16393
  const isKnownSinkType = KNOWN_SINK_TYPES.has(sinkType);
16191
- for (const san of sanitizersAtTarget) {
16192
- if (isKnownSinkType) {
16193
- if (san.sanitizes.includes(sinkType)) {
16394
+ const sanitizersAtTarget = sanitizersByLine.get(toLine);
16395
+ if (sanitizersAtTarget && sanitizersAtTarget.length > 0) {
16396
+ for (const san of sanitizersAtTarget) {
16397
+ if (isKnownSinkType) {
16398
+ if (san.sanitizes.includes(sinkType)) {
16399
+ return { sanitized: true, sanitizer: san };
16400
+ }
16401
+ } else if (san.sanitizes.length > 0) {
16194
16402
  return { sanitized: true, sanitizer: san };
16195
16403
  }
16196
- } else {
16197
- if (san.sanitizes.length > 0) {
16198
- return { sanitized: true, sanitizer: san };
16404
+ }
16405
+ }
16406
+ if (ctx) {
16407
+ const walk = walkBackwardDefs(
16408
+ ctx.startDefId,
16409
+ ctx.chainsByToDef,
16410
+ ctx.defById,
16411
+ { maxHops: ctx.maxHops ?? 32 }
16412
+ );
16413
+ for (const line of walk.lines) {
16414
+ if (line === toLine) continue;
16415
+ const sansAtLine = sanitizersByLine.get(line);
16416
+ if (!sansAtLine || sansAtLine.length === 0) continue;
16417
+ for (const san of sansAtLine) {
16418
+ if (isKnownSinkType) {
16419
+ if (san.sanitizes.includes(sinkType)) {
16420
+ return { sanitized: true, sanitizer: san };
16421
+ }
16422
+ } else if (san.sanitizes.length > 0) {
16423
+ return { sanitized: true, sanitizer: san };
16424
+ }
16199
16425
  }
16200
16426
  }
16201
16427
  }
@@ -24995,6 +25221,20 @@ function findJavaScriptDOMSinks(sourceCode, language) {
24995
25221
  else if (line.includes(".href")) method = "href";
24996
25222
  else if (line.includes(".cssText")) method = "cssText";
24997
25223
  else if (line.includes("style.textContent")) method = "textContent";
25224
+ if (method === "document.write" || method === "document.writeln") {
25225
+ const argMatch = line.match(/document\.write(?:ln)?\s*\(\s*([^)]*)\)/);
25226
+ if (argMatch) {
25227
+ const arg = argMatch[1].trim();
25228
+ if (!arg.includes("+")) {
25229
+ const doubleQ = /^"(?:[^"\\]|\\.)*"$/;
25230
+ const singleQ = /^'(?:[^'\\]|\\.)*'$/;
25231
+ const backtick = /^`(?:[^`\\$]|\\.|\$(?!\{))*`$/;
25232
+ if (doubleQ.test(arg) || singleQ.test(arg) || backtick.test(arg)) {
25233
+ break;
25234
+ }
25235
+ }
25236
+ }
25237
+ }
24998
25238
  const alreadyExists = sinks.some((s) => s.line === lineNumber && s.cwe === cwe);
24999
25239
  if (!alreadyExists) {
25000
25240
  sinks.push({ type, cwe, severity, line: lineNumber, location: line.trim().substring(0, 80), method });
@@ -29705,6 +29945,196 @@ var LibraryProfileSourceGatePass = class {
29705
29945
  }
29706
29946
  };
29707
29947
 
29948
+ // src/analysis/passes/mybatis-annotation-sql-sink-pass.ts
29949
+ var MYBATIS_SQL_ANNOTATIONS = /* @__PURE__ */ new Set([
29950
+ "Select",
29951
+ "Update",
29952
+ "Insert",
29953
+ "Delete",
29954
+ "SelectProvider",
29955
+ "UpdateProvider",
29956
+ "InsertProvider",
29957
+ "DeleteProvider"
29958
+ ]);
29959
+ var DOLLAR_BRACE_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?:\.[A-Za-z0-9_.]+)?\}/g;
29960
+ function parsePositionalRef(name2) {
29961
+ const paramMatch = /^param(\d+)$/.exec(name2);
29962
+ if (paramMatch) {
29963
+ const oneBased = Number.parseInt(paramMatch[1], 10);
29964
+ if (Number.isFinite(oneBased) && oneBased >= 1) return oneBased - 1;
29965
+ }
29966
+ const zeroMatch = /^(\d+)$/.exec(name2);
29967
+ if (zeroMatch) {
29968
+ const zeroBased = Number.parseInt(zeroMatch[1], 10);
29969
+ if (Number.isFinite(zeroBased) && zeroBased >= 0) return zeroBased;
29970
+ }
29971
+ return null;
29972
+ }
29973
+ function extractAnnotationBody(annotation) {
29974
+ const openIdx = annotation.indexOf("(");
29975
+ if (openIdx < 0) return null;
29976
+ const closeIdx = annotation.lastIndexOf(")");
29977
+ if (closeIdx <= openIdx) return null;
29978
+ return annotation.substring(openIdx + 1, closeIdx);
29979
+ }
29980
+ function annotationName(annotation) {
29981
+ const openIdx = annotation.indexOf("(");
29982
+ return openIdx < 0 ? annotation : annotation.substring(0, openIdx);
29983
+ }
29984
+ function extractParamName(annotation) {
29985
+ if (annotationName(annotation) !== "Param") return null;
29986
+ const body2 = extractAnnotationBody(annotation);
29987
+ if (body2 === null) return null;
29988
+ const strMatch = /^\s*["']([^"']*)["']/.exec(body2);
29989
+ return strMatch ? strMatch[1] : null;
29990
+ }
29991
+ function extractDollarBraceRefs(annotationBody) {
29992
+ const seen = /* @__PURE__ */ new Set();
29993
+ const out2 = [];
29994
+ DOLLAR_BRACE_RE.lastIndex = 0;
29995
+ let m;
29996
+ while ((m = DOLLAR_BRACE_RE.exec(annotationBody)) !== null) {
29997
+ const name2 = m[1];
29998
+ if (!seen.has(name2)) {
29999
+ seen.add(name2);
30000
+ out2.push(name2);
30001
+ }
30002
+ }
30003
+ return out2;
30004
+ }
30005
+ function fileImportsMyBatis(imports) {
30006
+ for (const imp of imports) {
30007
+ const pkg = imp.from_package;
30008
+ if (!pkg) continue;
30009
+ if (pkg === "org.apache.ibatis.annotations" || pkg.startsWith("org.apache.ibatis.annotations.") || pkg === "org.apache.ibatis" || pkg.startsWith("org.apache.ibatis.")) {
30010
+ return true;
30011
+ }
30012
+ }
30013
+ return false;
30014
+ }
30015
+ function correlateDollarBraceToArgPositions(method, refs) {
30016
+ const paramNameToIndex = /* @__PURE__ */ new Map();
30017
+ method.parameters.forEach((param, idx) => {
30018
+ for (const ann of param.annotations) {
30019
+ const paramName = extractParamName(ann);
30020
+ if (paramName !== null) {
30021
+ paramNameToIndex.set(paramName, idx);
30022
+ }
30023
+ }
30024
+ });
30025
+ const positions = /* @__PURE__ */ new Set();
30026
+ for (const ref of refs) {
30027
+ const namedIdx = paramNameToIndex.get(ref);
30028
+ if (namedIdx !== void 0) {
30029
+ positions.add(namedIdx);
30030
+ continue;
30031
+ }
30032
+ const positionalIdx = parsePositionalRef(ref);
30033
+ if (positionalIdx !== null && positionalIdx < method.parameters.length) {
30034
+ positions.add(positionalIdx);
30035
+ }
30036
+ }
30037
+ return Array.from(positions).sort((a, b) => a - b);
30038
+ }
30039
+ function isMapperMethodCall(call, interfaceSimpleName, interfaceFqn, methodName) {
30040
+ const target = call.resolution?.target;
30041
+ if (target) {
30042
+ const suffix = `${interfaceSimpleName}.${methodName}`;
30043
+ const suffixFqn = `${interfaceFqn}.${methodName}`;
30044
+ if (target === suffix || target === suffixFqn || target.endsWith("." + suffix) || target.endsWith("." + suffixFqn)) {
30045
+ return true;
30046
+ }
30047
+ }
30048
+ if (call.method_name !== methodName) return false;
30049
+ if (call.receiver_type === interfaceSimpleName) return true;
30050
+ if (call.receiver_type_fqn === interfaceFqn) return true;
30051
+ return false;
30052
+ }
30053
+ var MyBatisAnnotationSqlSinkPass = class {
30054
+ name = "mybatis-annotation-sql-sink";
30055
+ category = "security";
30056
+ run(ctx) {
30057
+ if (ctx.language !== "java") {
30058
+ return { annotatedMethodCount: 0, addedSinkCount: 0 };
30059
+ }
30060
+ const { types, imports, calls } = ctx.graph.ir;
30061
+ if (!fileImportsMyBatis(imports)) {
30062
+ return { annotatedMethodCount: 0, addedSinkCount: 0 };
30063
+ }
30064
+ const mapperMethods = [];
30065
+ for (const type of types) {
30066
+ if (type.kind !== "interface") continue;
30067
+ for (const method of type.methods) {
30068
+ let matchedRefs = [];
30069
+ for (const ann of method.annotations) {
30070
+ if (!MYBATIS_SQL_ANNOTATIONS.has(annotationName(ann))) continue;
30071
+ const body2 = extractAnnotationBody(ann);
30072
+ if (body2 === null) continue;
30073
+ const refs = extractDollarBraceRefs(body2);
30074
+ if (refs.length > 0) {
30075
+ matchedRefs = [...matchedRefs, ...refs];
30076
+ }
30077
+ }
30078
+ if (matchedRefs.length === 0) continue;
30079
+ const positions = correlateDollarBraceToArgPositions(
30080
+ method,
30081
+ matchedRefs
30082
+ );
30083
+ if (positions.length === 0) continue;
30084
+ const interfaceFqn = type.package ? `${type.package}.${type.name}` : type.name;
30085
+ mapperMethods.push({
30086
+ interfaceSimpleName: type.name,
30087
+ interfaceFqn,
30088
+ methodName: method.name,
30089
+ taintedArgPositions: positions
30090
+ });
30091
+ }
30092
+ }
30093
+ if (mapperMethods.length === 0) {
30094
+ return {
30095
+ annotatedMethodCount: 0,
30096
+ addedSinkCount: 0
30097
+ };
30098
+ }
30099
+ const sinks = ctx.hasResult("taint-matcher") ? ctx.getResult("taint-matcher").sinks : ctx.graph.ir.taint.sinks;
30100
+ let addedSinkCount = 0;
30101
+ for (const call of calls) {
30102
+ for (const rec of mapperMethods) {
30103
+ if (!isMapperMethodCall(
30104
+ call,
30105
+ rec.interfaceSimpleName,
30106
+ rec.interfaceFqn,
30107
+ rec.methodName
30108
+ )) {
30109
+ continue;
30110
+ }
30111
+ const line = call.location.line;
30112
+ if (sinks.some(
30113
+ (s) => s.line === line && s.type === "sql_injection" && s.method === rec.methodName
30114
+ )) {
30115
+ continue;
30116
+ }
30117
+ const receiverLoc = call.receiver ? `${call.receiver}.${rec.methodName}()` : `${rec.methodName}()`;
30118
+ sinks.push({
30119
+ type: "sql_injection",
30120
+ cwe: "CWE-89",
30121
+ location: call.in_method ? `${receiverLoc} in ${call.in_method}` : receiverLoc,
30122
+ line,
30123
+ confidence: 0.95,
30124
+ method: rec.methodName,
30125
+ argPositions: rec.taintedArgPositions,
30126
+ class: rec.interfaceSimpleName
30127
+ });
30128
+ addedSinkCount++;
30129
+ }
30130
+ }
30131
+ return {
30132
+ annotatedMethodCount: mapperMethods.length,
30133
+ addedSinkCount
30134
+ };
30135
+ }
30136
+ };
30137
+
29708
30138
  // src/analysis/passes/sink-filter-pass.ts
29709
30139
  var JS_XSS_SANITIZERS = [
29710
30140
  /\bDOMPurify\.sanitize\s*\(/,
@@ -37261,6 +37691,34 @@ var WeakHashPass = class {
37261
37691
  }
37262
37692
  };
37263
37693
 
37694
+ // src/analysis/path-classification.ts
37695
+ var TEST_PATH_PATTERNS = [
37696
+ // Directory-based conventions
37697
+ /(^|\/)test\//i,
37698
+ /(^|\/)tests\//i,
37699
+ /(^|\/)testing\//i,
37700
+ /(^|\/)__tests__\//i,
37701
+ /(^|\/)spec\//i,
37702
+ // Go test file suffix
37703
+ /_test\.go$/i,
37704
+ // Python pytest naming
37705
+ /(^|\/)test_[^/]+\.py$/i,
37706
+ /_test\.py$/i,
37707
+ // JS/TS unit / spec naming
37708
+ /\.test\.(?:tsx?|jsx?|mjs|cjs)$/i,
37709
+ /\.spec\.(?:tsx?|jsx?|mjs|cjs)$/i,
37710
+ // JVM alt naming
37711
+ /\.test\.(?:java|kt)$/i
37712
+ ];
37713
+ function isTestPath(filepath) {
37714
+ if (!filepath) return false;
37715
+ const normalized = filepath.replace(/\\/g, "/");
37716
+ for (const pattern of TEST_PATH_PATTERNS) {
37717
+ if (pattern.test(normalized)) return true;
37718
+ }
37719
+ return false;
37720
+ }
37721
+
37264
37722
  // src/analysis/passes/weak-crypto-pass.ts
37265
37723
  var WEAK_CIPHER_BASES = /* @__PURE__ */ new Set([
37266
37724
  "des",
@@ -37449,6 +37907,9 @@ var WeakCryptoPass = class {
37449
37907
  if (isProtocolMandatedCryptoFile(file, code)) {
37450
37908
  return { findings: [] };
37451
37909
  }
37910
+ if (isTestPath(file)) {
37911
+ return { findings: [] };
37912
+ }
37452
37913
  const findings = [];
37453
37914
  const constProp = ctx.hasResult("constant-propagation") ? ctx.getResult("constant-propagation") : null;
37454
37915
  const literalBindings = scanLiteralBindings(code, language);
@@ -40913,6 +41374,8 @@ async function analyze(code, filePath, language, options = {}) {
40913
41374
  if (!disabledPasses.has("source-semantics")) pipeline.add(new SourceSemanticsPass());
40914
41375
  if (!disabledPasses.has("library-profile-source-gate"))
40915
41376
  pipeline.add(new LibraryProfileSourceGatePass());
41377
+ if (!disabledPasses.has("mybatis-annotation-sql-sink"))
41378
+ pipeline.add(new MyBatisAnnotationSqlSinkPass());
40916
41379
  pipeline.add(new SinkFilterPass());
40917
41380
  if (!disabledPasses.has("sink-semantics")) pipeline.add(new SinkSemanticsPass());
40918
41381
  if (!disabledPasses.has("cli-main-reflection-suppress"))