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.
- package/configs/sinks/code_injection.yaml +2 -1
- package/configs/sinks/javascript_dom_xss.yaml +4 -2
- package/configs/sinks/xpath.yaml +40 -0
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +3 -0
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/dfg-walk.d.ts +36 -0
- package/dist/analysis/dfg-walk.d.ts.map +1 -0
- package/dist/analysis/dfg-walk.js +59 -0
- package/dist/analysis/dfg-walk.js.map +1 -0
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +22 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/mybatis-annotation-sql-sink-pass.d.ts +49 -0
- package/dist/analysis/passes/mybatis-annotation-sql-sink-pass.d.ts.map +1 -0
- package/dist/analysis/passes/mybatis-annotation-sql-sink-pass.js +329 -0
- package/dist/analysis/passes/mybatis-annotation-sql-sink-pass.js.map +1 -0
- package/dist/analysis/passes/weak-crypto-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-crypto-pass.js +6 -0
- package/dist/analysis/passes/weak-crypto-pass.js.map +1 -1
- package/dist/analysis/path-classification.d.ts +24 -0
- package/dist/analysis/path-classification.d.ts.map +1 -0
- package/dist/analysis/path-classification.js +69 -0
- package/dist/analysis/path-classification.js.map +1 -0
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +30 -0
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts +17 -1
- package/dist/analysis/taint-propagation.d.ts.map +1 -1
- package/dist/analysis/taint-propagation.js +47 -16
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +8 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +475 -12
- package/dist/core/circle-ir-core.cjs +96 -12
- package/dist/core/circle-ir-core.js +96 -12
- package/dist/graph/code-graph.d.ts +7 -0
- package/dist/graph/code-graph.d.ts.map +1 -1
- package/dist/graph/code-graph.js +17 -0
- package/dist/graph/code-graph.js.map +1 -1
- package/dist/resolution/type-hierarchy.d.ts +15 -0
- package/dist/resolution/type-hierarchy.d.ts.map +1 -1
- package/dist/resolution/type-hierarchy.js +159 -0
- package/dist/resolution/type-hierarchy.js.map +1 -1
- 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
|
-
|
|
16192
|
-
|
|
16193
|
-
|
|
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
|
-
}
|
|
16197
|
-
|
|
16198
|
-
|
|
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"))
|