circle-ir 3.153.0 → 3.155.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 +13 -3
- 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/library-profile-sink-gate-pass.d.ts +43 -1
- package/dist/analysis/passes/library-profile-sink-gate-pass.d.ts.map +1 -1
- package/dist/analysis/passes/library-profile-sink-gate-pass.js +75 -0
- package/dist/analysis/passes/library-profile-sink-gate-pass.js.map +1 -1
- package/dist/analysis/passes/library-profile-xss-gate-pass.d.ts +104 -0
- package/dist/analysis/passes/library-profile-xss-gate-pass.d.ts.map +1 -0
- package/dist/analysis/passes/library-profile-xss-gate-pass.js +196 -0
- package/dist/analysis/passes/library-profile-xss-gate-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-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 +25 -1
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +299 -15
- package/dist/core/circle-ir-core.cjs +103 -15
- package/dist/core/circle-ir-core.js +103 -15
- 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/package.json +1 -1
|
@@ -11304,9 +11304,16 @@ var DEFAULT_SINKS = [
|
|
|
11304
11304
|
{ method: "newBufferedWriter", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
|
|
11305
11305
|
{ method: "copy", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0, 1] },
|
|
11306
11306
|
{ method: "move", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0, 1] },
|
|
11307
|
-
|
|
11308
|
-
|
|
11309
|
-
|
|
11307
|
+
// NOTE: `Files.exists`, `Files.isDirectory`, `Files.isRegularFile`
|
|
11308
|
+
// were removed in 3.154.0 (#245 RC2). These NIO methods are pure
|
|
11309
|
+
// boolean queries — they read a filesystem attribute and cannot
|
|
11310
|
+
// cause traversal escape. A CWE-22 sink must consume the path to
|
|
11311
|
+
// open, read, write, delete, list, or link a filesystem entry;
|
|
11312
|
+
// check-only receivers reveal at most a boolean. Empirically
|
|
11313
|
+
// ~12 H+C FPs across the 10-repo Tier 2 cohort
|
|
11314
|
+
// (cognium-ai#189 §4). `java.io.File` instance methods
|
|
11315
|
+
// (`file.isDirectory()`, `file.exists()`, `file.canRead()`, …)
|
|
11316
|
+
// are already not registered as CWE-22 sinks.
|
|
11310
11317
|
// RandomAccessFile
|
|
11311
11318
|
{ method: "RandomAccessFile", class: "constructor", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
|
|
11312
11319
|
// Framework-specific resource loading (Cocoon, Spring, etc.)
|
|
@@ -12895,6 +12902,9 @@ var DEFAULT_SANITIZERS = [
|
|
|
12895
12902
|
{ method: "parseDouble", class: "Double", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12896
12903
|
{ method: "parseShort", class: "Short", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12897
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"] },
|
|
12898
12908
|
// Java UUID parse — UUID.fromString rejects non-UUID strings
|
|
12899
12909
|
{ method: "fromString", class: "UUID", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12900
12910
|
// JavaScript numeric coercion (Number/parseInt/parseFloat already covered above; add path_traversal/code_injection)
|
|
@@ -15026,6 +15036,23 @@ var CodeGraph = class {
|
|
|
15026
15036
|
}
|
|
15027
15037
|
return this._chainsByFromDef;
|
|
15028
15038
|
}
|
|
15039
|
+
/**
|
|
15040
|
+
* Mirror of chainsByFromDef keyed by chain.to_def. Enables backward
|
|
15041
|
+
* walks from a use's reaching def to earlier defs in the chain
|
|
15042
|
+
* (cognium-dev #238 — DFG-walk sanitizer credit).
|
|
15043
|
+
*/
|
|
15044
|
+
_chainsByToDef = null;
|
|
15045
|
+
get chainsByToDef() {
|
|
15046
|
+
if (!this._chainsByToDef) {
|
|
15047
|
+
this._chainsByToDef = /* @__PURE__ */ new Map();
|
|
15048
|
+
for (const chain of this.ir.dfg.chains ?? []) {
|
|
15049
|
+
const arr = this._chainsByToDef.get(chain.to_def) ?? [];
|
|
15050
|
+
arr.push(chain);
|
|
15051
|
+
this._chainsByToDef.set(chain.to_def, arr);
|
|
15052
|
+
}
|
|
15053
|
+
}
|
|
15054
|
+
return this._chainsByToDef;
|
|
15055
|
+
}
|
|
15029
15056
|
// ---------------------------------------------------------------------------
|
|
15030
15057
|
// Call indexes
|
|
15031
15058
|
// ---------------------------------------------------------------------------
|
|
@@ -15950,6 +15977,43 @@ var AnalysisPipeline = class {
|
|
|
15950
15977
|
}
|
|
15951
15978
|
};
|
|
15952
15979
|
|
|
15980
|
+
// src/analysis/dfg-walk.ts
|
|
15981
|
+
function walkBackwardDefs(startDefId, chainsByToDef, defById, options = {}) {
|
|
15982
|
+
const maxHops = options.maxHops ?? 32;
|
|
15983
|
+
const visited = /* @__PURE__ */ new Set();
|
|
15984
|
+
const lines = /* @__PURE__ */ new Set();
|
|
15985
|
+
let hopCapReached = false;
|
|
15986
|
+
const startDef = defById.get(startDefId);
|
|
15987
|
+
if (!startDef) {
|
|
15988
|
+
return { visited, lines, hopCapReached: false };
|
|
15989
|
+
}
|
|
15990
|
+
visited.add(startDefId);
|
|
15991
|
+
lines.add(startDef.line);
|
|
15992
|
+
const queue = [startDefId];
|
|
15993
|
+
let hops = 0;
|
|
15994
|
+
while (queue.length > 0) {
|
|
15995
|
+
if (hops >= maxHops) {
|
|
15996
|
+
hopCapReached = true;
|
|
15997
|
+
break;
|
|
15998
|
+
}
|
|
15999
|
+
hops++;
|
|
16000
|
+
const currentId = queue.shift();
|
|
16001
|
+
const incoming = chainsByToDef.get(currentId);
|
|
16002
|
+
if (!incoming) continue;
|
|
16003
|
+
for (const chain of incoming) {
|
|
16004
|
+
const fromId = chain.from_def;
|
|
16005
|
+
if (visited.has(fromId)) continue;
|
|
16006
|
+
visited.add(fromId);
|
|
16007
|
+
const fromDef = defById.get(fromId);
|
|
16008
|
+
if (fromDef) {
|
|
16009
|
+
lines.add(fromDef.line);
|
|
16010
|
+
}
|
|
16011
|
+
queue.push(fromId);
|
|
16012
|
+
}
|
|
16013
|
+
}
|
|
16014
|
+
return { visited, lines, hopCapReached };
|
|
16015
|
+
}
|
|
16016
|
+
|
|
15953
16017
|
// src/analysis/taint-propagation.ts
|
|
15954
16018
|
function buildSanitizersByLine(sanitizers) {
|
|
15955
16019
|
const out2 = /* @__PURE__ */ new Map();
|
|
@@ -16037,7 +16101,12 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
|
|
|
16037
16101
|
taintInfo.line,
|
|
16038
16102
|
sink.line,
|
|
16039
16103
|
sink.type,
|
|
16040
|
-
sanitizersByLine
|
|
16104
|
+
sanitizersByLine,
|
|
16105
|
+
{
|
|
16106
|
+
startDefId: use.def_id,
|
|
16107
|
+
chainsByToDef: graph.chainsByToDef,
|
|
16108
|
+
defById
|
|
16109
|
+
}
|
|
16041
16110
|
);
|
|
16042
16111
|
if (!isSanitized.sanitized) {
|
|
16043
16112
|
const source = sources.find((s) => s.line === taintInfo.sourceLine);
|
|
@@ -16175,20 +16244,39 @@ var KNOWN_SINK_TYPES = /* @__PURE__ */ new Set([
|
|
|
16175
16244
|
"crlf",
|
|
16176
16245
|
"mass_assignment"
|
|
16177
16246
|
]);
|
|
16178
|
-
function checkSanitized(_fromLine, toLine, sinkType, sanitizersByLine) {
|
|
16179
|
-
const sanitizersAtTarget = sanitizersByLine.get(toLine);
|
|
16180
|
-
if (!sanitizersAtTarget || sanitizersAtTarget.length === 0) {
|
|
16181
|
-
return { sanitized: false };
|
|
16182
|
-
}
|
|
16247
|
+
function checkSanitized(_fromLine, toLine, sinkType, sanitizersByLine, ctx) {
|
|
16183
16248
|
const isKnownSinkType = KNOWN_SINK_TYPES.has(sinkType);
|
|
16184
|
-
|
|
16185
|
-
|
|
16186
|
-
|
|
16249
|
+
const sanitizersAtTarget = sanitizersByLine.get(toLine);
|
|
16250
|
+
if (sanitizersAtTarget && sanitizersAtTarget.length > 0) {
|
|
16251
|
+
for (const san of sanitizersAtTarget) {
|
|
16252
|
+
if (isKnownSinkType) {
|
|
16253
|
+
if (san.sanitizes.includes(sinkType)) {
|
|
16254
|
+
return { sanitized: true, sanitizer: san };
|
|
16255
|
+
}
|
|
16256
|
+
} else if (san.sanitizes.length > 0) {
|
|
16187
16257
|
return { sanitized: true, sanitizer: san };
|
|
16188
16258
|
}
|
|
16189
|
-
}
|
|
16190
|
-
|
|
16191
|
-
|
|
16259
|
+
}
|
|
16260
|
+
}
|
|
16261
|
+
if (ctx) {
|
|
16262
|
+
const walk = walkBackwardDefs(
|
|
16263
|
+
ctx.startDefId,
|
|
16264
|
+
ctx.chainsByToDef,
|
|
16265
|
+
ctx.defById,
|
|
16266
|
+
{ maxHops: ctx.maxHops ?? 32 }
|
|
16267
|
+
);
|
|
16268
|
+
for (const line of walk.lines) {
|
|
16269
|
+
if (line === toLine) continue;
|
|
16270
|
+
const sansAtLine = sanitizersByLine.get(line);
|
|
16271
|
+
if (!sansAtLine || sansAtLine.length === 0) continue;
|
|
16272
|
+
for (const san of sansAtLine) {
|
|
16273
|
+
if (isKnownSinkType) {
|
|
16274
|
+
if (san.sanitizes.includes(sinkType)) {
|
|
16275
|
+
return { sanitized: true, sanitizer: san };
|
|
16276
|
+
}
|
|
16277
|
+
} else if (san.sanitizes.length > 0) {
|
|
16278
|
+
return { sanitized: true, sanitizer: san };
|
|
16279
|
+
}
|
|
16192
16280
|
}
|
|
16193
16281
|
}
|
|
16194
16282
|
}
|
|
@@ -24988,6 +25076,20 @@ function findJavaScriptDOMSinks(sourceCode, language) {
|
|
|
24988
25076
|
else if (line.includes(".href")) method = "href";
|
|
24989
25077
|
else if (line.includes(".cssText")) method = "cssText";
|
|
24990
25078
|
else if (line.includes("style.textContent")) method = "textContent";
|
|
25079
|
+
if (method === "document.write" || method === "document.writeln") {
|
|
25080
|
+
const argMatch = line.match(/document\.write(?:ln)?\s*\(\s*([^)]*)\)/);
|
|
25081
|
+
if (argMatch) {
|
|
25082
|
+
const arg = argMatch[1].trim();
|
|
25083
|
+
if (!arg.includes("+")) {
|
|
25084
|
+
const doubleQ = /^"(?:[^"\\]|\\.)*"$/;
|
|
25085
|
+
const singleQ = /^'(?:[^'\\]|\\.)*'$/;
|
|
25086
|
+
const backtick = /^`(?:[^`\\$]|\\.|\$(?!\{))*`$/;
|
|
25087
|
+
if (doubleQ.test(arg) || singleQ.test(arg) || backtick.test(arg)) {
|
|
25088
|
+
break;
|
|
25089
|
+
}
|
|
25090
|
+
}
|
|
25091
|
+
}
|
|
25092
|
+
}
|
|
24991
25093
|
const alreadyExists = sinks.some((s) => s.line === lineNumber && s.cwe === cwe);
|
|
24992
25094
|
if (!alreadyExists) {
|
|
24993
25095
|
sinks.push({ type, cwe, severity, line: lineNumber, location: line.trim().substring(0, 80), method });
|
|
@@ -31298,6 +31400,153 @@ var LibraryProfileSinkGatePass = class {
|
|
|
31298
31400
|
};
|
|
31299
31401
|
}
|
|
31300
31402
|
};
|
|
31403
|
+
var CWE22_SPECULATIVE_SOURCE_TYPES = /* @__PURE__ */ new Set([
|
|
31404
|
+
"interprocedural_param",
|
|
31405
|
+
"constructor_field"
|
|
31406
|
+
]);
|
|
31407
|
+
var LibraryProfileCwe22PathGatePass = class {
|
|
31408
|
+
name = "library-profile-cwe22-path-gate";
|
|
31409
|
+
category = "security";
|
|
31410
|
+
run(ctx) {
|
|
31411
|
+
const { graph } = ctx;
|
|
31412
|
+
const profile = graph.ir.meta.projectProfile;
|
|
31413
|
+
if (!isLibraryShape2(profile)) {
|
|
31414
|
+
return {
|
|
31415
|
+
profile,
|
|
31416
|
+
applied: false,
|
|
31417
|
+
dropped: 0,
|
|
31418
|
+
droppedBySourceType: {}
|
|
31419
|
+
};
|
|
31420
|
+
}
|
|
31421
|
+
const flows = graph.ir.taint.flows;
|
|
31422
|
+
if (!flows || flows.length === 0) {
|
|
31423
|
+
return {
|
|
31424
|
+
profile,
|
|
31425
|
+
applied: true,
|
|
31426
|
+
dropped: 0,
|
|
31427
|
+
droppedBySourceType: {}
|
|
31428
|
+
};
|
|
31429
|
+
}
|
|
31430
|
+
const droppedBySourceType = {};
|
|
31431
|
+
const kept = [];
|
|
31432
|
+
for (const flow of flows) {
|
|
31433
|
+
if (flow.sink_type === "path_traversal" && CWE22_SPECULATIVE_SOURCE_TYPES.has(flow.source_type)) {
|
|
31434
|
+
droppedBySourceType[flow.source_type] = (droppedBySourceType[flow.source_type] ?? 0) + 1;
|
|
31435
|
+
continue;
|
|
31436
|
+
}
|
|
31437
|
+
kept.push(flow);
|
|
31438
|
+
}
|
|
31439
|
+
const dropped = flows.length - kept.length;
|
|
31440
|
+
if (dropped > 0) {
|
|
31441
|
+
flows.length = 0;
|
|
31442
|
+
flows.push(...kept);
|
|
31443
|
+
}
|
|
31444
|
+
return {
|
|
31445
|
+
profile,
|
|
31446
|
+
applied: true,
|
|
31447
|
+
dropped,
|
|
31448
|
+
droppedBySourceType
|
|
31449
|
+
};
|
|
31450
|
+
}
|
|
31451
|
+
};
|
|
31452
|
+
|
|
31453
|
+
// src/analysis/passes/library-profile-xss-gate-pass.ts
|
|
31454
|
+
var XSS_NON_HTML_OUTPUT_CLASSES = /* @__PURE__ */ new Set([
|
|
31455
|
+
// In-memory buffers — no HTML output surface.
|
|
31456
|
+
"StringBuilder",
|
|
31457
|
+
"StringBuffer",
|
|
31458
|
+
"CharArrayWriter",
|
|
31459
|
+
"ByteArrayOutputStream",
|
|
31460
|
+
// CLI stdio — CLI apps, not web response bodies.
|
|
31461
|
+
"PrintStream",
|
|
31462
|
+
"System",
|
|
31463
|
+
// HTTP client builders — these are taint SOURCES (outbound reads),
|
|
31464
|
+
// not XSS sinks. `response.body()`, `.post()`, `.get()` on
|
|
31465
|
+
// hutool `HttpRequest` / `HttpResponse` reach us as sink
|
|
31466
|
+
// matches only because xss.yaml catches String-valued receivers.
|
|
31467
|
+
"HttpRequest",
|
|
31468
|
+
"HttpRequestBuilder",
|
|
31469
|
+
"HttpResponse",
|
|
31470
|
+
// Servlet non-body IO. `HttpSession.setAttribute`, `HttpSession.putValue`,
|
|
31471
|
+
// `HttpServletRequest.setAttribute` are session/request attribute
|
|
31472
|
+
// IO, not HTML output. Under `library/*` there is no JSP renderer
|
|
31473
|
+
// reflecting them back. `HttpServletResponse` itself is NOT on
|
|
31474
|
+
// this list — its writers are genuine XSS sinks.
|
|
31475
|
+
"HttpSession",
|
|
31476
|
+
"ServletRequest",
|
|
31477
|
+
"HttpServletRequest",
|
|
31478
|
+
// Wire-protocol writers (jedis internal).
|
|
31479
|
+
"RedisOutputStream",
|
|
31480
|
+
"SafeEncoder",
|
|
31481
|
+
"RESP2",
|
|
31482
|
+
"Protocol",
|
|
31483
|
+
// JSON parsers — these read JSON into POJOs. Source, not sink.
|
|
31484
|
+
"JSONUtil",
|
|
31485
|
+
"JSON",
|
|
31486
|
+
"ObjectMapper",
|
|
31487
|
+
"JsonReader",
|
|
31488
|
+
// Loggers — log injection is CWE-117 (already covered by #112);
|
|
31489
|
+
// these appear here because xss.yaml has a String-valued catch-all
|
|
31490
|
+
// that hits Logger receivers.
|
|
31491
|
+
"Logger",
|
|
31492
|
+
"LoggerFactory",
|
|
31493
|
+
"Log",
|
|
31494
|
+
"Slf4jLogger",
|
|
31495
|
+
// Router / interceptor context stores. Zuul `RequestContext`,
|
|
31496
|
+
// Sentinel `Context` are internal request-processing state, not
|
|
31497
|
+
// HTML output.
|
|
31498
|
+
"RequestContext",
|
|
31499
|
+
"Context"
|
|
31500
|
+
]);
|
|
31501
|
+
function isLibraryShape3(profile) {
|
|
31502
|
+
if (!profile || profile === "unknown") return false;
|
|
31503
|
+
return profile.startsWith("library/");
|
|
31504
|
+
}
|
|
31505
|
+
var LibraryProfileXssGatePass = class {
|
|
31506
|
+
name = "library-profile-xss-gate";
|
|
31507
|
+
category = "security";
|
|
31508
|
+
run(ctx) {
|
|
31509
|
+
const { graph } = ctx;
|
|
31510
|
+
const profile = graph.ir.meta.projectProfile;
|
|
31511
|
+
if (!isLibraryShape3(profile)) {
|
|
31512
|
+
return {
|
|
31513
|
+
profile,
|
|
31514
|
+
applied: false,
|
|
31515
|
+
dropped: 0,
|
|
31516
|
+
droppedByClass: {}
|
|
31517
|
+
};
|
|
31518
|
+
}
|
|
31519
|
+
const sinks = ctx.hasResult("sink-filter") ? ctx.getResult("sink-filter").sinks : graph.ir.taint.sinks;
|
|
31520
|
+
if (sinks.length === 0) {
|
|
31521
|
+
return {
|
|
31522
|
+
profile,
|
|
31523
|
+
applied: true,
|
|
31524
|
+
dropped: 0,
|
|
31525
|
+
droppedByClass: {}
|
|
31526
|
+
};
|
|
31527
|
+
}
|
|
31528
|
+
const droppedByClass = {};
|
|
31529
|
+
const kept = [];
|
|
31530
|
+
for (const sink of sinks) {
|
|
31531
|
+
if (sink.type === "xss" && sink.class && XSS_NON_HTML_OUTPUT_CLASSES.has(sink.class)) {
|
|
31532
|
+
droppedByClass[sink.class] = (droppedByClass[sink.class] ?? 0) + 1;
|
|
31533
|
+
continue;
|
|
31534
|
+
}
|
|
31535
|
+
kept.push(sink);
|
|
31536
|
+
}
|
|
31537
|
+
const dropped = sinks.length - kept.length;
|
|
31538
|
+
if (dropped > 0) {
|
|
31539
|
+
sinks.length = 0;
|
|
31540
|
+
sinks.push(...kept);
|
|
31541
|
+
}
|
|
31542
|
+
return {
|
|
31543
|
+
profile,
|
|
31544
|
+
applied: true,
|
|
31545
|
+
dropped,
|
|
31546
|
+
droppedByClass
|
|
31547
|
+
};
|
|
31548
|
+
}
|
|
31549
|
+
};
|
|
31301
31550
|
|
|
31302
31551
|
// src/analysis/passes/taint-propagation-pass.ts
|
|
31303
31552
|
var TaintPropagationPass = class {
|
|
@@ -37107,6 +37356,34 @@ var WeakHashPass = class {
|
|
|
37107
37356
|
}
|
|
37108
37357
|
};
|
|
37109
37358
|
|
|
37359
|
+
// src/analysis/path-classification.ts
|
|
37360
|
+
var TEST_PATH_PATTERNS = [
|
|
37361
|
+
// Directory-based conventions
|
|
37362
|
+
/(^|\/)test\//i,
|
|
37363
|
+
/(^|\/)tests\//i,
|
|
37364
|
+
/(^|\/)testing\//i,
|
|
37365
|
+
/(^|\/)__tests__\//i,
|
|
37366
|
+
/(^|\/)spec\//i,
|
|
37367
|
+
// Go test file suffix
|
|
37368
|
+
/_test\.go$/i,
|
|
37369
|
+
// Python pytest naming
|
|
37370
|
+
/(^|\/)test_[^/]+\.py$/i,
|
|
37371
|
+
/_test\.py$/i,
|
|
37372
|
+
// JS/TS unit / spec naming
|
|
37373
|
+
/\.test\.(?:tsx?|jsx?|mjs|cjs)$/i,
|
|
37374
|
+
/\.spec\.(?:tsx?|jsx?|mjs|cjs)$/i,
|
|
37375
|
+
// JVM alt naming
|
|
37376
|
+
/\.test\.(?:java|kt)$/i
|
|
37377
|
+
];
|
|
37378
|
+
function isTestPath(filepath) {
|
|
37379
|
+
if (!filepath) return false;
|
|
37380
|
+
const normalized = filepath.replace(/\\/g, "/");
|
|
37381
|
+
for (const pattern of TEST_PATH_PATTERNS) {
|
|
37382
|
+
if (pattern.test(normalized)) return true;
|
|
37383
|
+
}
|
|
37384
|
+
return false;
|
|
37385
|
+
}
|
|
37386
|
+
|
|
37110
37387
|
// src/analysis/passes/weak-crypto-pass.ts
|
|
37111
37388
|
var WEAK_CIPHER_BASES = /* @__PURE__ */ new Set([
|
|
37112
37389
|
"des",
|
|
@@ -37295,6 +37572,9 @@ var WeakCryptoPass = class {
|
|
|
37295
37572
|
if (isProtocolMandatedCryptoFile(file, code)) {
|
|
37296
37573
|
return { findings: [] };
|
|
37297
37574
|
}
|
|
37575
|
+
if (isTestPath(file)) {
|
|
37576
|
+
return { findings: [] };
|
|
37577
|
+
}
|
|
37298
37578
|
const findings = [];
|
|
37299
37579
|
const constProp = ctx.hasResult("constant-propagation") ? ctx.getResult("constant-propagation") : null;
|
|
37300
37580
|
const literalBindings = scanLiteralBindings(code, language);
|
|
@@ -40765,10 +41045,14 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
40765
41045
|
pipeline.add(new CliMainReflectionSuppressPass());
|
|
40766
41046
|
if (!disabledPasses.has("library-profile-sink-gate"))
|
|
40767
41047
|
pipeline.add(new LibraryProfileSinkGatePass());
|
|
41048
|
+
if (!disabledPasses.has("library-profile-xss-gate"))
|
|
41049
|
+
pipeline.add(new LibraryProfileXssGatePass());
|
|
40768
41050
|
pipeline.add(new TaintPropagationPass());
|
|
40769
41051
|
pipeline.add(new InterproceduralPass({
|
|
40770
41052
|
enableEntryPointGate: options.enableEntryPointGate ?? true
|
|
40771
41053
|
}));
|
|
41054
|
+
if (!disabledPasses.has("library-profile-cwe22-path-gate"))
|
|
41055
|
+
pipeline.add(new LibraryProfileCwe22PathGatePass());
|
|
40772
41056
|
if (!disabledPasses.has("scan-secrets")) pipeline.add(new ScanSecretsPass());
|
|
40773
41057
|
if (!disabledPasses.has("dead-code")) pipeline.add(new DeadCodePass());
|
|
40774
41058
|
if (!disabledPasses.has("missing-await")) pipeline.add(new MissingAwaitPass());
|
|
@@ -10699,9 +10699,16 @@ var DEFAULT_SINKS = [
|
|
|
10699
10699
|
{ method: "newBufferedWriter", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
|
|
10700
10700
|
{ method: "copy", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0, 1] },
|
|
10701
10701
|
{ method: "move", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0, 1] },
|
|
10702
|
-
|
|
10703
|
-
|
|
10704
|
-
|
|
10702
|
+
// NOTE: `Files.exists`, `Files.isDirectory`, `Files.isRegularFile`
|
|
10703
|
+
// were removed in 3.154.0 (#245 RC2). These NIO methods are pure
|
|
10704
|
+
// boolean queries — they read a filesystem attribute and cannot
|
|
10705
|
+
// cause traversal escape. A CWE-22 sink must consume the path to
|
|
10706
|
+
// open, read, write, delete, list, or link a filesystem entry;
|
|
10707
|
+
// check-only receivers reveal at most a boolean. Empirically
|
|
10708
|
+
// ~12 H+C FPs across the 10-repo Tier 2 cohort
|
|
10709
|
+
// (cognium-ai#189 §4). `java.io.File` instance methods
|
|
10710
|
+
// (`file.isDirectory()`, `file.exists()`, `file.canRead()`, …)
|
|
10711
|
+
// are already not registered as CWE-22 sinks.
|
|
10705
10712
|
// RandomAccessFile
|
|
10706
10713
|
{ method: "RandomAccessFile", class: "constructor", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
|
|
10707
10714
|
// Framework-specific resource loading (Cocoon, Spring, etc.)
|
|
@@ -12290,6 +12297,9 @@ var DEFAULT_SANITIZERS = [
|
|
|
12290
12297
|
{ method: "parseDouble", class: "Double", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12291
12298
|
{ method: "parseShort", class: "Short", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12292
12299
|
{ method: "parseByte", class: "Byte", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12300
|
+
// cognium-dev #238 A.3 — parseBoolean and expanded coercion coverage for XSS/CRLF/log/xpath/ldap/xxe.
|
|
12301
|
+
// A parsed boolean/numeric cannot carry a string-injection payload for any string-based sink.
|
|
12302
|
+
{ method: "parseBoolean", class: "Boolean", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection", "xss", "crlf", "log_injection", "xpath_injection", "ldap_injection", "xxe"] },
|
|
12293
12303
|
// Java UUID parse — UUID.fromString rejects non-UUID strings
|
|
12294
12304
|
{ method: "fromString", class: "UUID", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12295
12305
|
// JavaScript numeric coercion (Number/parseInt/parseFloat already covered above; add path_traversal/code_injection)
|
|
@@ -14039,6 +14049,23 @@ var CodeGraph = class {
|
|
|
14039
14049
|
}
|
|
14040
14050
|
return this._chainsByFromDef;
|
|
14041
14051
|
}
|
|
14052
|
+
/**
|
|
14053
|
+
* Mirror of chainsByFromDef keyed by chain.to_def. Enables backward
|
|
14054
|
+
* walks from a use's reaching def to earlier defs in the chain
|
|
14055
|
+
* (cognium-dev #238 — DFG-walk sanitizer credit).
|
|
14056
|
+
*/
|
|
14057
|
+
_chainsByToDef = null;
|
|
14058
|
+
get chainsByToDef() {
|
|
14059
|
+
if (!this._chainsByToDef) {
|
|
14060
|
+
this._chainsByToDef = /* @__PURE__ */ new Map();
|
|
14061
|
+
for (const chain of this.ir.dfg.chains ?? []) {
|
|
14062
|
+
const arr = this._chainsByToDef.get(chain.to_def) ?? [];
|
|
14063
|
+
arr.push(chain);
|
|
14064
|
+
this._chainsByToDef.set(chain.to_def, arr);
|
|
14065
|
+
}
|
|
14066
|
+
}
|
|
14067
|
+
return this._chainsByToDef;
|
|
14068
|
+
}
|
|
14042
14069
|
// ---------------------------------------------------------------------------
|
|
14043
14070
|
// Call indexes
|
|
14044
14071
|
// ---------------------------------------------------------------------------
|
|
@@ -14209,6 +14236,43 @@ var CodeGraph = class {
|
|
|
14209
14236
|
}
|
|
14210
14237
|
};
|
|
14211
14238
|
|
|
14239
|
+
// src/analysis/dfg-walk.ts
|
|
14240
|
+
function walkBackwardDefs(startDefId, chainsByToDef, defById, options = {}) {
|
|
14241
|
+
const maxHops = options.maxHops ?? 32;
|
|
14242
|
+
const visited = /* @__PURE__ */ new Set();
|
|
14243
|
+
const lines = /* @__PURE__ */ new Set();
|
|
14244
|
+
let hopCapReached = false;
|
|
14245
|
+
const startDef = defById.get(startDefId);
|
|
14246
|
+
if (!startDef) {
|
|
14247
|
+
return { visited, lines, hopCapReached: false };
|
|
14248
|
+
}
|
|
14249
|
+
visited.add(startDefId);
|
|
14250
|
+
lines.add(startDef.line);
|
|
14251
|
+
const queue = [startDefId];
|
|
14252
|
+
let hops = 0;
|
|
14253
|
+
while (queue.length > 0) {
|
|
14254
|
+
if (hops >= maxHops) {
|
|
14255
|
+
hopCapReached = true;
|
|
14256
|
+
break;
|
|
14257
|
+
}
|
|
14258
|
+
hops++;
|
|
14259
|
+
const currentId = queue.shift();
|
|
14260
|
+
const incoming = chainsByToDef.get(currentId);
|
|
14261
|
+
if (!incoming) continue;
|
|
14262
|
+
for (const chain of incoming) {
|
|
14263
|
+
const fromId = chain.from_def;
|
|
14264
|
+
if (visited.has(fromId)) continue;
|
|
14265
|
+
visited.add(fromId);
|
|
14266
|
+
const fromDef = defById.get(fromId);
|
|
14267
|
+
if (fromDef) {
|
|
14268
|
+
lines.add(fromDef.line);
|
|
14269
|
+
}
|
|
14270
|
+
queue.push(fromId);
|
|
14271
|
+
}
|
|
14272
|
+
}
|
|
14273
|
+
return { visited, lines, hopCapReached };
|
|
14274
|
+
}
|
|
14275
|
+
|
|
14212
14276
|
// src/analysis/taint-propagation.ts
|
|
14213
14277
|
function buildSanitizersByLine(sanitizers) {
|
|
14214
14278
|
const out2 = /* @__PURE__ */ new Map();
|
|
@@ -14296,7 +14360,12 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
|
|
|
14296
14360
|
taintInfo.line,
|
|
14297
14361
|
sink.line,
|
|
14298
14362
|
sink.type,
|
|
14299
|
-
sanitizersByLine
|
|
14363
|
+
sanitizersByLine,
|
|
14364
|
+
{
|
|
14365
|
+
startDefId: use.def_id,
|
|
14366
|
+
chainsByToDef: graph.chainsByToDef,
|
|
14367
|
+
defById
|
|
14368
|
+
}
|
|
14300
14369
|
);
|
|
14301
14370
|
if (!isSanitized.sanitized) {
|
|
14302
14371
|
const source = sources.find((s) => s.line === taintInfo.sourceLine);
|
|
@@ -14434,20 +14503,39 @@ var KNOWN_SINK_TYPES = /* @__PURE__ */ new Set([
|
|
|
14434
14503
|
"crlf",
|
|
14435
14504
|
"mass_assignment"
|
|
14436
14505
|
]);
|
|
14437
|
-
function checkSanitized(_fromLine, toLine, sinkType, sanitizersByLine) {
|
|
14438
|
-
const sanitizersAtTarget = sanitizersByLine.get(toLine);
|
|
14439
|
-
if (!sanitizersAtTarget || sanitizersAtTarget.length === 0) {
|
|
14440
|
-
return { sanitized: false };
|
|
14441
|
-
}
|
|
14506
|
+
function checkSanitized(_fromLine, toLine, sinkType, sanitizersByLine, ctx) {
|
|
14442
14507
|
const isKnownSinkType = KNOWN_SINK_TYPES.has(sinkType);
|
|
14443
|
-
|
|
14444
|
-
|
|
14445
|
-
|
|
14508
|
+
const sanitizersAtTarget = sanitizersByLine.get(toLine);
|
|
14509
|
+
if (sanitizersAtTarget && sanitizersAtTarget.length > 0) {
|
|
14510
|
+
for (const san of sanitizersAtTarget) {
|
|
14511
|
+
if (isKnownSinkType) {
|
|
14512
|
+
if (san.sanitizes.includes(sinkType)) {
|
|
14513
|
+
return { sanitized: true, sanitizer: san };
|
|
14514
|
+
}
|
|
14515
|
+
} else if (san.sanitizes.length > 0) {
|
|
14446
14516
|
return { sanitized: true, sanitizer: san };
|
|
14447
14517
|
}
|
|
14448
|
-
}
|
|
14449
|
-
|
|
14450
|
-
|
|
14518
|
+
}
|
|
14519
|
+
}
|
|
14520
|
+
if (ctx) {
|
|
14521
|
+
const walk = walkBackwardDefs(
|
|
14522
|
+
ctx.startDefId,
|
|
14523
|
+
ctx.chainsByToDef,
|
|
14524
|
+
ctx.defById,
|
|
14525
|
+
{ maxHops: ctx.maxHops ?? 32 }
|
|
14526
|
+
);
|
|
14527
|
+
for (const line of walk.lines) {
|
|
14528
|
+
if (line === toLine) continue;
|
|
14529
|
+
const sansAtLine = sanitizersByLine.get(line);
|
|
14530
|
+
if (!sansAtLine || sansAtLine.length === 0) continue;
|
|
14531
|
+
for (const san of sansAtLine) {
|
|
14532
|
+
if (isKnownSinkType) {
|
|
14533
|
+
if (san.sanitizes.includes(sinkType)) {
|
|
14534
|
+
return { sanitized: true, sanitizer: san };
|
|
14535
|
+
}
|
|
14536
|
+
} else if (san.sanitizes.length > 0) {
|
|
14537
|
+
return { sanitized: true, sanitizer: san };
|
|
14538
|
+
}
|
|
14451
14539
|
}
|
|
14452
14540
|
}
|
|
14453
14541
|
}
|