circle-ir 3.141.0 → 3.144.3
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/sink-semantics.json +53 -0
- package/dist/analysis/config-loader.d.ts +24 -2
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +92 -2
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/findings.d.ts +32 -0
- package/dist/analysis/findings.d.ts.map +1 -1
- package/dist/analysis/findings.js +52 -2
- package/dist/analysis/findings.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts +19 -0
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +133 -3
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.js +37 -4
- package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +10 -1
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/sink-semantics-pass.d.ts +52 -0
- package/dist/analysis/passes/sink-semantics-pass.d.ts.map +1 -0
- package/dist/analysis/passes/sink-semantics-pass.js +100 -0
- package/dist/analysis/passes/sink-semantics-pass.js.map +1 -0
- package/dist/analysis/passes/source-semantics-pass.d.ts +66 -0
- package/dist/analysis/passes/source-semantics-pass.d.ts.map +1 -0
- package/dist/analysis/passes/source-semantics-pass.js +165 -0
- package/dist/analysis/passes/source-semantics-pass.js.map +1 -0
- package/dist/analysis/passes/taint-propagation-pass.js +91 -10
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +55 -3
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +14 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +363 -15
- package/dist/core/circle-ir-core.cjs +112 -8
- package/dist/core/circle-ir-core.js +112 -8
- package/dist/core/extractors/calls.js +14 -0
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/types/config.d.ts +47 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/index.d.ts +43 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -6337,6 +6337,12 @@ function resolveReceiverType(receiver, context) {
|
|
|
6337
6337
|
return { simpleName: null, fqn: null };
|
|
6338
6338
|
}
|
|
6339
6339
|
if (receiver === "super") return { simpleName: null, fqn: null };
|
|
6340
|
+
const ctorMatch = receiver.match(/^new\s+([A-Za-z_$][\w$.]*)\s*[<(]/);
|
|
6341
|
+
if (ctorMatch) {
|
|
6342
|
+
const ctorClass = ctorMatch[1];
|
|
6343
|
+
const simple = ctorClass.includes(".") ? ctorClass.substring(ctorClass.lastIndexOf(".") + 1) : ctorClass;
|
|
6344
|
+
return resolveFqn(simple, context);
|
|
6345
|
+
}
|
|
6340
6346
|
const declaredType = context.localVarTypes.get(receiver) ?? context.paramTypes.get(receiver) ?? context.fieldTypes.get(receiver);
|
|
6341
6347
|
if (declaredType) {
|
|
6342
6348
|
return resolveFqn(stripGenerics(declaredType), context);
|
|
@@ -11451,6 +11457,14 @@ var DEFAULT_SINKS = [
|
|
|
11451
11457
|
// reflection remains a downstream concern of body-writing sinks.
|
|
11452
11458
|
{ method: "setHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
|
|
11453
11459
|
{ method: "addHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
|
|
11460
|
+
// Cookie constructor + addCookie — HTTP response splitting via cookie
|
|
11461
|
+
// name/value (CWE-113). Reflects the #189 Sprint 92 V02SetCookie fixture
|
|
11462
|
+
// where `res.addCookie(new Cookie(name, req.getParameter("v")))` allows
|
|
11463
|
+
// CRLF injection into the Set-Cookie header. Both the ctor arg[1] (value)
|
|
11464
|
+
// and the addCookie arg[0] (Cookie handle) are flagged so intermediate-var
|
|
11465
|
+
// and inline-ctor shapes both surface.
|
|
11466
|
+
{ method: "Cookie", class: "constructor", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0, 1] },
|
|
11467
|
+
{ method: "addCookie", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0] },
|
|
11454
11468
|
// Note: `sendRedirect` is primarily classified as `ssrf` / open-redirect
|
|
11455
11469
|
// (CWE-601) further down — see entry near line 1195. CRLF via Location
|
|
11456
11470
|
// header is a secondary concern; keeping the canonical SSRF entry avoids
|
|
@@ -12890,11 +12904,62 @@ var DEFAULT_SANITIZERS = [
|
|
|
12890
12904
|
{ method: "UUID", class: "uuid", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12891
12905
|
{ method: "Decimal", class: "decimal", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] }
|
|
12892
12906
|
];
|
|
12907
|
+
var DEFAULT_SINK_SEMANTICS = [
|
|
12908
|
+
{
|
|
12909
|
+
signature: "Jedis#executeCommand",
|
|
12910
|
+
real_class: "db_protocol",
|
|
12911
|
+
overrides: ["command_injection", "code_injection"],
|
|
12912
|
+
note: "Redis wire-protocol serialization, not OS exec"
|
|
12913
|
+
},
|
|
12914
|
+
{
|
|
12915
|
+
signature: "Connection#executeCommand",
|
|
12916
|
+
real_class: "db_protocol",
|
|
12917
|
+
overrides: ["command_injection", "code_injection"],
|
|
12918
|
+
note: "Jedis abstract Connection base"
|
|
12919
|
+
},
|
|
12920
|
+
{
|
|
12921
|
+
signature: "JedisCluster#executeCommand",
|
|
12922
|
+
real_class: "db_protocol",
|
|
12923
|
+
overrides: ["command_injection", "code_injection"],
|
|
12924
|
+
note: "Jedis cluster client"
|
|
12925
|
+
},
|
|
12926
|
+
{
|
|
12927
|
+
signature: "Func1#exec",
|
|
12928
|
+
real_class: "functional_dispatch",
|
|
12929
|
+
overrides: ["command_injection", "code_injection"],
|
|
12930
|
+
note: "RxJava functional dispatch, not OS exec"
|
|
12931
|
+
},
|
|
12932
|
+
{
|
|
12933
|
+
signature: "Action0#call",
|
|
12934
|
+
real_class: "functional_dispatch",
|
|
12935
|
+
overrides: ["command_injection"],
|
|
12936
|
+
note: "RxJava Action0 dispatch"
|
|
12937
|
+
},
|
|
12938
|
+
{
|
|
12939
|
+
signature: "Action1#call",
|
|
12940
|
+
real_class: "functional_dispatch",
|
|
12941
|
+
overrides: ["command_injection"],
|
|
12942
|
+
note: "RxJava Action1 dispatch"
|
|
12943
|
+
},
|
|
12944
|
+
{
|
|
12945
|
+
signature: "Unsafe#defineAnonymousClass",
|
|
12946
|
+
real_class: "jdk_internal",
|
|
12947
|
+
overrides: ["code_injection"],
|
|
12948
|
+
note: "sun.misc.Unsafe JDK-internal reflective bridge"
|
|
12949
|
+
},
|
|
12950
|
+
{
|
|
12951
|
+
signature: "MethodHandle#invokeExact",
|
|
12952
|
+
real_class: "jdk_internal",
|
|
12953
|
+
overrides: ["code_injection"],
|
|
12954
|
+
note: "java.lang.invoke.MethodHandle \u2014 JDK-internal"
|
|
12955
|
+
}
|
|
12956
|
+
];
|
|
12893
12957
|
function getDefaultConfig() {
|
|
12894
12958
|
return {
|
|
12895
12959
|
sources: DEFAULT_SOURCES,
|
|
12896
12960
|
sinks: DEFAULT_SINKS,
|
|
12897
|
-
sanitizers: DEFAULT_SANITIZERS
|
|
12961
|
+
sanitizers: DEFAULT_SANITIZERS,
|
|
12962
|
+
sinkSemantics: DEFAULT_SINK_SEMANTICS
|
|
12898
12963
|
};
|
|
12899
12964
|
}
|
|
12900
12965
|
var DEFAULT_HEADER_RULES = [
|
|
@@ -13204,7 +13269,20 @@ function findSources(calls, types, patterns, sourceLines, language) {
|
|
|
13204
13269
|
line: paramLine,
|
|
13205
13270
|
confidence: param.type ? 0.7 : 0.5,
|
|
13206
13271
|
// Lower confidence for untyped params
|
|
13207
|
-
in_method: method.name
|
|
13272
|
+
in_method: method.name,
|
|
13273
|
+
// cognium-dev #220 — expose the parameter name to variable-scan flow
|
|
13274
|
+
// detection so uses of the param (including through concat-derived
|
|
13275
|
+
// aliases via `buildJavaTaintedVars`) can be bridged to sinks.
|
|
13276
|
+
//
|
|
13277
|
+
// Java-only: the Python/Rust alias-expansion branches in
|
|
13278
|
+
// taint-propagation-pass.ts use the earliest sourcesWithVar entry
|
|
13279
|
+
// as the anchor for synthetic derived sources. Adding
|
|
13280
|
+
// interprocedural_param to sourcesWithVar in those languages
|
|
13281
|
+
// would cause the anchor to become the low-confidence
|
|
13282
|
+
// interprocedural_param at the method decl line instead of the
|
|
13283
|
+
// real HTTP source, breaking downstream sink-type filters
|
|
13284
|
+
// (regressed #78, #92.1, #105 FP-31, #215 recall).
|
|
13285
|
+
...language === "java" ? { variable: param.name } : {}
|
|
13208
13286
|
});
|
|
13209
13287
|
}
|
|
13210
13288
|
}
|
|
@@ -13668,7 +13746,16 @@ function isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines) {
|
|
|
13668
13746
|
return false;
|
|
13669
13747
|
}
|
|
13670
13748
|
var TEMPLATE_LITERAL_RECEIVER_RE = /^Template\(\s*(?:"[^"\\]*"|'[^'\\]*')\s*\)$/;
|
|
13671
|
-
|
|
13749
|
+
var JINJA_AUTOESCAPE_TRUE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*True\b/;
|
|
13750
|
+
var JINJA_SELECT_AUTOESCAPE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*select_autoescape\s*\(/;
|
|
13751
|
+
var JINJA_AUTOESCAPE_FALSE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*False\b/;
|
|
13752
|
+
function fileHasSafeJinjaEnvironment(sourceLines) {
|
|
13753
|
+
if (!sourceLines || sourceLines.length === 0) return false;
|
|
13754
|
+
const text = sourceLines.join("\n");
|
|
13755
|
+
if (JINJA_AUTOESCAPE_FALSE_RE.test(text)) return false;
|
|
13756
|
+
return JINJA_AUTOESCAPE_TRUE_RE.test(text) || JINJA_SELECT_AUTOESCAPE_RE.test(text);
|
|
13757
|
+
}
|
|
13758
|
+
function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
|
|
13672
13759
|
if (language !== "python") return false;
|
|
13673
13760
|
if (pattern.type !== "xss" && pattern.type !== "code_injection") return false;
|
|
13674
13761
|
const method = call.method_name;
|
|
@@ -13682,7 +13769,8 @@ function isSafeJinjaRenderCall(call, pattern, language) {
|
|
|
13682
13769
|
}
|
|
13683
13770
|
if (method === "render") {
|
|
13684
13771
|
const receiver = (call.receiver ?? "").trim();
|
|
13685
|
-
|
|
13772
|
+
if (TEMPLATE_LITERAL_RECEIVER_RE.test(receiver)) return true;
|
|
13773
|
+
if (fileHasSafeJinjaEnvironment(sourceLines)) return true;
|
|
13686
13774
|
}
|
|
13687
13775
|
return false;
|
|
13688
13776
|
}
|
|
@@ -13730,7 +13818,7 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
|
|
|
13730
13818
|
if (isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines)) {
|
|
13731
13819
|
continue;
|
|
13732
13820
|
}
|
|
13733
|
-
if (isSafeJinjaRenderCall(call, pattern, language)) {
|
|
13821
|
+
if (isSafeJinjaRenderCall(call, pattern, language, sourceLines)) {
|
|
13734
13822
|
continue;
|
|
13735
13823
|
}
|
|
13736
13824
|
const location = formatCallLocation(call);
|
|
@@ -13738,6 +13826,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
|
|
|
13738
13826
|
const confidence = calculateSinkConfidence(call, pattern);
|
|
13739
13827
|
const existing = sinkMap.get(key);
|
|
13740
13828
|
if (!existing || confidence > existing.confidence) {
|
|
13829
|
+
const receiverType = call.receiver_type;
|
|
13830
|
+
const simpleClass = receiverType ? receiverType.split(".").pop() || void 0 : void 0;
|
|
13741
13831
|
sinkMap.set(key, {
|
|
13742
13832
|
type: pattern.type,
|
|
13743
13833
|
cwe: pattern.cwe,
|
|
@@ -13745,7 +13835,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
|
|
|
13745
13835
|
line: call.location.line,
|
|
13746
13836
|
confidence,
|
|
13747
13837
|
method: call.method_name,
|
|
13748
|
-
argPositions: pattern.arg_positions
|
|
13838
|
+
argPositions: pattern.arg_positions,
|
|
13839
|
+
class: simpleClass
|
|
13749
13840
|
});
|
|
13750
13841
|
}
|
|
13751
13842
|
}
|
|
@@ -14700,12 +14791,20 @@ function canSourceReachSink(sourceType, sinkType) {
|
|
|
14700
14791
|
// flow detector silently dropped `http_* → trust_boundary` co-located
|
|
14701
14792
|
// flows like `req.getSession().setAttribute("u", req.getParameter("u"))`
|
|
14702
14793
|
// (0% recall on OWASP Java trustbound category).
|
|
14703
|
-
|
|
14794
|
+
// deserialization added Sprint 93 (#189): SnakeYAML/Jackson/etc. sinks
|
|
14795
|
+
// like `new Yaml().load(req.getParameter("y"))` are real RCE gadget chains
|
|
14796
|
+
// (CWE-502). The reach map previously restricted deserialization to
|
|
14797
|
+
// http_body so http_param/http_query request-derived values feeding
|
|
14798
|
+
// Yaml.load, ObjectInputStream ctor, XMLDecoder ctor, etc. silently
|
|
14799
|
+
// dropped their inline-colocation flow. Typed-overload FPs are gated by
|
|
14800
|
+
// the sink pattern's `safe_if_class_literal_at` flag (Jackson readValue,
|
|
14801
|
+
// Yaml.loadAs, Gson.fromJson) so the wider reach does not regress.
|
|
14802
|
+
http_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment", "open_redirect", "trust_boundary", "deserialization"],
|
|
14704
14803
|
http_body: ["sql_injection", "command_injection", "deserialization", "xxe", "xss", "code_injection", "mybatis_mapper_call", "crlf", "mass_assignment", "open_redirect", "trust_boundary"],
|
|
14705
14804
|
http_header: ["sql_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "open_redirect", "trust_boundary"],
|
|
14706
14805
|
http_cookie: ["sql_injection", "xss", "mybatis_mapper_call", "code_injection", "crlf", "open_redirect", "trust_boundary"],
|
|
14707
14806
|
http_path: ["path_traversal", "sql_injection", "ssrf", "mybatis_mapper_call", "open_redirect", "trust_boundary"],
|
|
14708
|
-
http_query: ["sql_injection", "command_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment", "open_redirect", "trust_boundary"],
|
|
14807
|
+
http_query: ["sql_injection", "command_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment", "open_redirect", "trust_boundary", "deserialization"],
|
|
14709
14808
|
// ssrf added Sprint 57 #200: bash CGI/webhook handlers and scripts that
|
|
14710
14809
|
// take a URL on stdin or as a positional CLI arg (`curl "$1"`,
|
|
14711
14810
|
// `wget "$(read line)"`) and curl/wget it server-side are textbook SSRF
|
|
@@ -14727,6 +14826,15 @@ function canSourceReachSink(sourceType, sinkType) {
|
|
|
14727
14826
|
const validSinks = sourceToSinkMapping[sourceType];
|
|
14728
14827
|
return validSinks ? validSinks.includes(sinkType) : false;
|
|
14729
14828
|
}
|
|
14829
|
+
function sourceSemanticsAllowed(source, sinkType) {
|
|
14830
|
+
if (source.constant === true) {
|
|
14831
|
+
return false;
|
|
14832
|
+
}
|
|
14833
|
+
if (source.spi === true) {
|
|
14834
|
+
return sinkType === "code_injection";
|
|
14835
|
+
}
|
|
14836
|
+
return true;
|
|
14837
|
+
}
|
|
14730
14838
|
|
|
14731
14839
|
// src/analysis/findings-instrumentation.ts
|
|
14732
14840
|
var instrumentEnabled = false;
|
|
@@ -24649,6 +24757,65 @@ function buildRustTaintedVars(sourceCode, seedVars) {
|
|
|
24649
24757
|
}
|
|
24650
24758
|
return derived;
|
|
24651
24759
|
}
|
|
24760
|
+
function buildJavaTaintedVars(sourceCode, seedVars) {
|
|
24761
|
+
const derived = /* @__PURE__ */ new Map();
|
|
24762
|
+
const knownTainted = new Set(seedVars);
|
|
24763
|
+
const lines = sourceCode.split("\n");
|
|
24764
|
+
const declRe = /^\s*(?:public|private|protected|static|final|volatile|transient|\s)*\s*(?:[A-Za-z_][\w.]*(?:\s*<[^>]*>)?(?:\s*\[\s*\])*)\s+([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
|
|
24765
|
+
const assignRe = /^\s*([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
|
|
24766
|
+
const JAVA_KEYWORDS = /* @__PURE__ */ new Set([
|
|
24767
|
+
"if",
|
|
24768
|
+
"else",
|
|
24769
|
+
"while",
|
|
24770
|
+
"for",
|
|
24771
|
+
"do",
|
|
24772
|
+
"switch",
|
|
24773
|
+
"case",
|
|
24774
|
+
"return",
|
|
24775
|
+
"throw",
|
|
24776
|
+
"try",
|
|
24777
|
+
"catch",
|
|
24778
|
+
"finally",
|
|
24779
|
+
"new",
|
|
24780
|
+
"this",
|
|
24781
|
+
"super",
|
|
24782
|
+
"break",
|
|
24783
|
+
"continue",
|
|
24784
|
+
"default",
|
|
24785
|
+
"class",
|
|
24786
|
+
"interface",
|
|
24787
|
+
"enum"
|
|
24788
|
+
]);
|
|
24789
|
+
let changed = true;
|
|
24790
|
+
let guard = 0;
|
|
24791
|
+
while (changed && guard < lines.length + 2) {
|
|
24792
|
+
changed = false;
|
|
24793
|
+
guard++;
|
|
24794
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
24795
|
+
const line = lines[i2];
|
|
24796
|
+
const trimmed = line.trimStart();
|
|
24797
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
24798
|
+
const declMatch = declRe.exec(line);
|
|
24799
|
+
const assignMatch = !declMatch ? assignRe.exec(line) : null;
|
|
24800
|
+
const m = declMatch ?? assignMatch;
|
|
24801
|
+
if (!m) continue;
|
|
24802
|
+
const lhs = m[1];
|
|
24803
|
+
const rhs = m[2];
|
|
24804
|
+
if (JAVA_KEYWORDS.has(lhs)) continue;
|
|
24805
|
+
if (knownTainted.has(lhs)) continue;
|
|
24806
|
+
const escaped = (v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24807
|
+
const ref = [...knownTainted].some(
|
|
24808
|
+
(v) => new RegExp(`(?<![\\p{L}\\p{N}_])${escaped(v)}(?![\\p{L}\\p{N}_])`, "u").test(rhs)
|
|
24809
|
+
);
|
|
24810
|
+
if (ref) {
|
|
24811
|
+
derived.set(lhs, i2 + 1);
|
|
24812
|
+
knownTainted.add(lhs);
|
|
24813
|
+
changed = true;
|
|
24814
|
+
}
|
|
24815
|
+
}
|
|
24816
|
+
}
|
|
24817
|
+
return derived;
|
|
24818
|
+
}
|
|
24652
24819
|
var BASH_UNTRUSTED_ENV_PATTERNS = [
|
|
24653
24820
|
/^USER_INPUT$/i,
|
|
24654
24821
|
/^QUERY_STRING$/i,
|
|
@@ -25006,7 +25173,7 @@ function findBashRealpathPrefixGuardSanitizers(code) {
|
|
|
25006
25173
|
const caseOpen = /^\s*case\s+"?\$\{?\w+\}?"?\s+in\b/;
|
|
25007
25174
|
const esacClose = /^\s*esac\b/;
|
|
25008
25175
|
const armOpener = /^\s*([^)\s][^)]*?)\)/;
|
|
25009
|
-
const prefixArm = /^(?:"\$\{?\w+\}?"|"[^"]*"|\/[\w\-./]+|\$\{?\w+\}?|[\w\-./]+)(?:\/|\*)/;
|
|
25176
|
+
const prefixArm = /^(?:"\$\{?\w+\}?"|"[^"]*"|\/[\w\-./]+|\$\{?\w+\}?|https?:\/\/[\w\-.]+|[\w\-./]+)(?:\/|\*)/;
|
|
25010
25177
|
const catchAllArm = /^(?:\*|\\\*)$/;
|
|
25011
25178
|
let i2 = 0;
|
|
25012
25179
|
while (i2 < lines.length) {
|
|
@@ -25831,9 +25998,11 @@ function findJavaArgvFormExecSanitizers(code) {
|
|
|
25831
25998
|
const lines = code.split("\n");
|
|
25832
25999
|
const argvExecRe = /\.\s*exec\s*\(\s*new\s+String\s*\[\s*\]\s*\{/;
|
|
25833
26000
|
const argvPbRe = /\bnew\s+ProcessBuilder\s*\(\s*new\s+String\s*\[\s*\]\s*\{/;
|
|
26001
|
+
const shellInStringRe = /new\s+String\s*\[\s*\]\s*\{\s*"(?:\/(?:usr\/)?bin\/(?:sh|bash|zsh|ksh|dash)|(?:sh|bash|zsh|ksh|dash)|cmd(?:\.exe)?|powershell(?:\.exe)?|pwsh)"\s*,\s*"(?:-c|\/c|-Command|-command)"/i;
|
|
25834
26002
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
25835
26003
|
const text = lines[i2];
|
|
25836
26004
|
if (!argvExecRe.test(text) && !argvPbRe.test(text)) continue;
|
|
26005
|
+
if (shellInStringRe.test(text)) continue;
|
|
25837
26006
|
sanitizers.push({
|
|
25838
26007
|
type: "java_argv_form_exec",
|
|
25839
26008
|
method: "exec",
|
|
@@ -27770,6 +27939,22 @@ function findPythonMongoengineWhereNosqlInjectionFindings(code, file) {
|
|
|
27770
27939
|
}
|
|
27771
27940
|
return findings;
|
|
27772
27941
|
}
|
|
27942
|
+
function hasHostAllowlistBeforeSink(taintedVars, lines, sinkLineIdx) {
|
|
27943
|
+
for (let i2 = 0; i2 < sinkLineIdx; i2++) {
|
|
27944
|
+
const t = lines[i2];
|
|
27945
|
+
for (const v of taintedVars) {
|
|
27946
|
+
const containsRe = new RegExp(
|
|
27947
|
+
`\\.\\s*contains\\s*\\(\\s*${v}\\s*\\.\\s*getHost\\s*\\(\\s*\\)\\s*\\)`
|
|
27948
|
+
);
|
|
27949
|
+
if (containsRe.test(t)) return true;
|
|
27950
|
+
const equalsRe = new RegExp(
|
|
27951
|
+
`\\b${v}\\s*\\.\\s*getHost\\s*\\(\\s*\\)\\s*\\.\\s*equals(?:IgnoreCase)?\\s*\\(\\s*"[^"]+"\\s*\\)`
|
|
27952
|
+
);
|
|
27953
|
+
if (equalsRe.test(t)) return true;
|
|
27954
|
+
}
|
|
27955
|
+
}
|
|
27956
|
+
return false;
|
|
27957
|
+
}
|
|
27773
27958
|
function findJavaUrlOpenStreamSsrfFindings(code, file) {
|
|
27774
27959
|
const findings = [];
|
|
27775
27960
|
if (typeof code !== "string" || code.length === 0) return findings;
|
|
@@ -27825,6 +28010,7 @@ function findJavaUrlOpenStreamSsrfFindings(code, file) {
|
|
|
27825
28010
|
}
|
|
27826
28011
|
}
|
|
27827
28012
|
if (!tainted) continue;
|
|
28013
|
+
if (hasHostAllowlistBeforeSink(taintedVars, lines, i2)) continue;
|
|
27828
28014
|
const key = `${i2 + 1}:${op}`;
|
|
27829
28015
|
if (seen.has(key)) continue;
|
|
27830
28016
|
seen.add(key);
|
|
@@ -28967,6 +29153,80 @@ function findJsTemplateInjectionSstiFindings(code, file) {
|
|
|
28967
29153
|
return findings;
|
|
28968
29154
|
}
|
|
28969
29155
|
|
|
29156
|
+
// src/analysis/passes/source-semantics-pass.ts
|
|
29157
|
+
var DEMO_PATH_RE = /(?:^|\/)(?:demo|example|examples|samples|integration-tests|integration_tests)(?:\/|$)/i;
|
|
29158
|
+
var CONST_STRING_ASSIGN_RE = /^\s*(?:final\s+|static\s+final\s+)?[A-Za-z_][\w.<>\[\]]*\s+[A-Za-z_]\w*\s*=\s*"[^"]*"\s*;?\s*$/;
|
|
29159
|
+
var STATIC_FINAL_RE = /^\s*(?:public\s+|private\s+|protected\s+)?static\s+final\s+/;
|
|
29160
|
+
var ENUM_CONST_REF_RE = /=\s*[A-Z][A-Za-z0-9_]*\.[A-Z][A-Z0-9_]*\s*;?\s*$/;
|
|
29161
|
+
function isConstantSource(code) {
|
|
29162
|
+
if (!code) return false;
|
|
29163
|
+
if (CONST_STRING_ASSIGN_RE.test(code)) return true;
|
|
29164
|
+
if (STATIC_FINAL_RE.test(code)) {
|
|
29165
|
+
const rhs = code.split("=").slice(1).join("=").trim();
|
|
29166
|
+
if (rhs.length === 0) return false;
|
|
29167
|
+
if (/^"[^"]*"\s*;?\s*$/.test(rhs)) return true;
|
|
29168
|
+
if (/^-?\d+(?:\.\d+)?[fFdDlL]?\s*;?\s*$/.test(rhs)) return true;
|
|
29169
|
+
if (/^(?:true|false)\s*;?\s*$/.test(rhs)) return true;
|
|
29170
|
+
if (/^[A-Za-z_][\w.]*\s*;?\s*$/.test(rhs)) return true;
|
|
29171
|
+
return false;
|
|
29172
|
+
}
|
|
29173
|
+
if (ENUM_CONST_REF_RE.test(code)) return true;
|
|
29174
|
+
return false;
|
|
29175
|
+
}
|
|
29176
|
+
var SERVICE_LOADER_RE = /\bServiceLoader\.(?:load|loadInstalled|stream)\s*\(/;
|
|
29177
|
+
var CLASS_FOR_NAME_RE = /\bClass\.forName\s*\(/;
|
|
29178
|
+
var META_INF_SERVICES_RE = /getResources?\s*\(\s*"META-INF\/services\//;
|
|
29179
|
+
var SPI_WINDOW = 30;
|
|
29180
|
+
function isSpiSource(source, lines) {
|
|
29181
|
+
const code = source.code;
|
|
29182
|
+
if (!code) return false;
|
|
29183
|
+
if (SERVICE_LOADER_RE.test(code)) return true;
|
|
29184
|
+
if (CLASS_FOR_NAME_RE.test(code)) {
|
|
29185
|
+
const start2 = Math.max(0, source.line - 1 - SPI_WINDOW);
|
|
29186
|
+
const end = Math.min(lines.length, source.line - 1 + SPI_WINDOW + 1);
|
|
29187
|
+
for (let i2 = start2; i2 < end; i2++) {
|
|
29188
|
+
if (META_INF_SERVICES_RE.test(lines[i2])) return true;
|
|
29189
|
+
}
|
|
29190
|
+
}
|
|
29191
|
+
return false;
|
|
29192
|
+
}
|
|
29193
|
+
function isDemoPathFile(file) {
|
|
29194
|
+
if (!file) return false;
|
|
29195
|
+
return DEMO_PATH_RE.test(file);
|
|
29196
|
+
}
|
|
29197
|
+
var SourceSemanticsPass = class {
|
|
29198
|
+
name = "source-semantics";
|
|
29199
|
+
category = "security";
|
|
29200
|
+
run(ctx) {
|
|
29201
|
+
const { graph, code } = ctx;
|
|
29202
|
+
const sources = graph.ir.taint.sources;
|
|
29203
|
+
if (sources.length === 0) {
|
|
29204
|
+
return { constantCount: 0, spiCount: 0, demoPathCount: 0 };
|
|
29205
|
+
}
|
|
29206
|
+
const file = graph.ir.meta.file;
|
|
29207
|
+
const demoPath = isDemoPathFile(file);
|
|
29208
|
+
const lines = code.split("\n");
|
|
29209
|
+
let constantCount = 0;
|
|
29210
|
+
let spiCount = 0;
|
|
29211
|
+
let demoPathCount = 0;
|
|
29212
|
+
for (const source of sources) {
|
|
29213
|
+
if (isConstantSource(source.code)) {
|
|
29214
|
+
source.constant = true;
|
|
29215
|
+
constantCount++;
|
|
29216
|
+
}
|
|
29217
|
+
if (isSpiSource(source, lines)) {
|
|
29218
|
+
source.spi = true;
|
|
29219
|
+
spiCount++;
|
|
29220
|
+
}
|
|
29221
|
+
if (demoPath) {
|
|
29222
|
+
source.demoPath = true;
|
|
29223
|
+
demoPathCount++;
|
|
29224
|
+
}
|
|
29225
|
+
}
|
|
29226
|
+
return { constantCount, spiCount, demoPathCount };
|
|
29227
|
+
}
|
|
29228
|
+
};
|
|
29229
|
+
|
|
28970
29230
|
// src/analysis/passes/sink-filter-pass.ts
|
|
28971
29231
|
var JS_XSS_SANITIZERS = [
|
|
28972
29232
|
/\bDOMPurify\.sanitize\s*\(/,
|
|
@@ -29514,7 +29774,7 @@ var SinkFilterPass = class {
|
|
|
29514
29774
|
const sourceLines = ctx.code.split("\n");
|
|
29515
29775
|
filtered = filtered.filter((sink) => {
|
|
29516
29776
|
if (sink.type !== "command_injection") return true;
|
|
29517
|
-
if (sink.method !== "ProcessBuilder") return true;
|
|
29777
|
+
if (sink.method !== "ProcessBuilder" && sink.method !== "start") return true;
|
|
29518
29778
|
const sinkLineText = sourceLines[sink.line - 1] ?? "";
|
|
29519
29779
|
if (!/\bnew\s+ProcessBuilder\s*\(/.test(sinkLineText)) return true;
|
|
29520
29780
|
if (PROCESS_BUILDER_ARGV_FORM_RE.test(sinkLineText)) return false;
|
|
@@ -30258,6 +30518,50 @@ function filterSanitizedSinks(sinks, sanitizers, calls) {
|
|
|
30258
30518
|
});
|
|
30259
30519
|
}
|
|
30260
30520
|
|
|
30521
|
+
// src/analysis/passes/sink-semantics-pass.ts
|
|
30522
|
+
function buildRegistry(entries) {
|
|
30523
|
+
const registry = /* @__PURE__ */ new Map();
|
|
30524
|
+
for (const entry of entries) {
|
|
30525
|
+
const existing = registry.get(entry.signature);
|
|
30526
|
+
if (existing) {
|
|
30527
|
+
for (const t of entry.overrides) existing.add(t);
|
|
30528
|
+
} else {
|
|
30529
|
+
registry.set(entry.signature, new Set(entry.overrides));
|
|
30530
|
+
}
|
|
30531
|
+
}
|
|
30532
|
+
return registry;
|
|
30533
|
+
}
|
|
30534
|
+
var SinkSemanticsPass = class {
|
|
30535
|
+
name = "sink-semantics";
|
|
30536
|
+
category = "security";
|
|
30537
|
+
run(ctx) {
|
|
30538
|
+
const { graph, config } = ctx;
|
|
30539
|
+
const entries = config.sinkSemantics ?? [];
|
|
30540
|
+
if (entries.length === 0) {
|
|
30541
|
+
return { droppedCount: 0, registrySize: 0 };
|
|
30542
|
+
}
|
|
30543
|
+
const registry = buildRegistry(entries);
|
|
30544
|
+
const sinks = graph.ir.taint.sinks;
|
|
30545
|
+
let droppedCount = 0;
|
|
30546
|
+
const kept = sinks.filter((sink) => {
|
|
30547
|
+
if (!sink.class || !sink.method) return true;
|
|
30548
|
+
const signature = `${sink.class}#${sink.method}`;
|
|
30549
|
+
const overrides = registry.get(signature);
|
|
30550
|
+
if (!overrides) return true;
|
|
30551
|
+
if (overrides.has(sink.type)) {
|
|
30552
|
+
droppedCount++;
|
|
30553
|
+
return false;
|
|
30554
|
+
}
|
|
30555
|
+
return true;
|
|
30556
|
+
});
|
|
30557
|
+
if (droppedCount > 0) {
|
|
30558
|
+
sinks.length = 0;
|
|
30559
|
+
sinks.push(...kept);
|
|
30560
|
+
}
|
|
30561
|
+
return { droppedCount, registrySize: registry.size };
|
|
30562
|
+
}
|
|
30563
|
+
};
|
|
30564
|
+
|
|
30261
30565
|
// src/analysis/passes/taint-propagation-pass.ts
|
|
30262
30566
|
var TaintPropagationPass = class {
|
|
30263
30567
|
name = "taint-propagation";
|
|
@@ -30887,6 +31191,25 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
30887
31191
|
}
|
|
30888
31192
|
}
|
|
30889
31193
|
}
|
|
31194
|
+
if (language === "java" && typeof code === "string" && sourcesWithVar.length > 0) {
|
|
31195
|
+
const seedVars = new Set(sourcesWithVar.map((s) => s.variable));
|
|
31196
|
+
const derived = buildJavaTaintedVars(code, seedVars);
|
|
31197
|
+
if (derived.size > 0) {
|
|
31198
|
+
let anchor = sourcesWithVar[0];
|
|
31199
|
+
for (const s of sourcesWithVar) {
|
|
31200
|
+
if (s.line < anchor.line) anchor = s;
|
|
31201
|
+
}
|
|
31202
|
+
const existingVars = new Set(sourcesWithVar.map((s) => s.variable));
|
|
31203
|
+
for (const [varName] of derived) {
|
|
31204
|
+
if (!varName || existingVars.has(varName)) continue;
|
|
31205
|
+
sourcesWithVar.push({
|
|
31206
|
+
...anchor,
|
|
31207
|
+
variable: varName
|
|
31208
|
+
});
|
|
31209
|
+
existingVars.add(varName);
|
|
31210
|
+
}
|
|
31211
|
+
}
|
|
31212
|
+
}
|
|
30890
31213
|
const reCache = /* @__PURE__ */ new Map();
|
|
30891
31214
|
for (const s of sourcesWithVar) {
|
|
30892
31215
|
if (reCache.has(s.variable)) continue;
|
|
@@ -30916,6 +31239,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
30916
31239
|
}
|
|
30917
31240
|
const re = reCache.get(source.variable);
|
|
30918
31241
|
if (!re || !re.test(expr)) continue;
|
|
31242
|
+
if (!sourceSemanticsAllowed(source, sink.type)) continue;
|
|
30919
31243
|
if (flows.some(
|
|
30920
31244
|
(f) => f.source_line === source.line && f.sink_line === sink.line && f.sink_type === sink.type
|
|
30921
31245
|
)) continue;
|
|
@@ -30944,7 +31268,6 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
30944
31268
|
}
|
|
30945
31269
|
const sourcesByLine = /* @__PURE__ */ new Map();
|
|
30946
31270
|
for (const s of sources) {
|
|
30947
|
-
if (s.variable && s.variable.length > 0) continue;
|
|
30948
31271
|
const arr = sourcesByLine.get(s.line) ?? [];
|
|
30949
31272
|
arr.push(s);
|
|
30950
31273
|
sourcesByLine.set(s.line, arr);
|
|
@@ -30955,6 +31278,19 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
30955
31278
|
if (!colocSources || colocSources.length === 0) continue;
|
|
30956
31279
|
for (const source of colocSources) {
|
|
30957
31280
|
if (!canSourceReachSink(source.type, sink.type)) continue;
|
|
31281
|
+
if (!sourceSemanticsAllowed(source, sink.type)) continue;
|
|
31282
|
+
const sourceVar = source.variable;
|
|
31283
|
+
if (sourceVar && sourceVar.length > 0) {
|
|
31284
|
+
const sinkCode = sink.code;
|
|
31285
|
+
if (!sinkCode) {
|
|
31286
|
+
continue;
|
|
31287
|
+
}
|
|
31288
|
+
const assignMatch = sinkCode.match(/^\s*(?:[A-Za-z_][\w.<>[\]\s,?]*\s+)?[A-Za-z_]\w*\s*=(?!=)\s*/);
|
|
31289
|
+
const rhs = assignMatch ? sinkCode.slice(assignMatch[0].length) : sinkCode;
|
|
31290
|
+
if (new RegExp(`\\b${sourceVar}\\b`).test(rhs)) {
|
|
31291
|
+
continue;
|
|
31292
|
+
}
|
|
31293
|
+
}
|
|
30958
31294
|
if (source.type === "file_input" && sink.type === "path_traversal" && sink.method && source.location.includes(`${sink.method}(`)) {
|
|
30959
31295
|
continue;
|
|
30960
31296
|
}
|
|
@@ -35059,6 +35395,13 @@ function isProtocolMandatedCryptoFile(file, code) {
|
|
|
35059
35395
|
}
|
|
35060
35396
|
|
|
35061
35397
|
// src/analysis/passes/scan-secrets-pass.ts
|
|
35398
|
+
function applyDemoDowngrade(demoPath, severity, level) {
|
|
35399
|
+
if (!demoPath) return { severity, level };
|
|
35400
|
+
if (severity === "high") {
|
|
35401
|
+
return { severity: "low", level: "note" };
|
|
35402
|
+
}
|
|
35403
|
+
return { severity, level };
|
|
35404
|
+
}
|
|
35062
35405
|
var TEST_PATH_RE3 = /(?:^|[\\/])(?:test|tests|spec|specs|__tests?__|__mocks?__|fixtures?|testdata)(?:[\\/]|$)/i;
|
|
35063
35406
|
var TEST_FILENAME_RE = /(?:\.(?:test|spec)\.[cm]?[jt]sx?|_test\.go|_test\.py|Test\.java|Tests\.java)$/i;
|
|
35064
35407
|
function isTestFile(file) {
|
|
@@ -35374,6 +35717,7 @@ var ScanSecretsPass = class {
|
|
|
35374
35717
|
if (isTestFile(file) || isGeneratedFile(file)) {
|
|
35375
35718
|
return { providerFindings: 0, entropyFindings: 0 };
|
|
35376
35719
|
}
|
|
35720
|
+
const demoPath = DEMO_PATH_RE.test(file);
|
|
35377
35721
|
const lines = ctx.code.split("\n");
|
|
35378
35722
|
const prior = ctx.getFindings?.() ?? [];
|
|
35379
35723
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -35400,14 +35744,15 @@ var ScanSecretsPass = class {
|
|
|
35400
35744
|
const key = `${lineNum}:hardcoded-credential`;
|
|
35401
35745
|
if (seen.has(key)) continue;
|
|
35402
35746
|
seen.add(key);
|
|
35747
|
+
const dg = applyDemoDowngrade(demoPath, pattern.severity, pattern.level);
|
|
35403
35748
|
ctx.addFinding({
|
|
35404
35749
|
id: `hardcoded-credential-${file}-${lineNum}`,
|
|
35405
35750
|
pass: this.name,
|
|
35406
35751
|
category: this.category,
|
|
35407
35752
|
rule_id: "hardcoded-credential",
|
|
35408
35753
|
cwe: "CWE-798",
|
|
35409
|
-
severity:
|
|
35410
|
-
level:
|
|
35754
|
+
severity: dg.severity,
|
|
35755
|
+
level: dg.level,
|
|
35411
35756
|
message: `Hardcoded credential: ${pattern.name} detected`,
|
|
35412
35757
|
file,
|
|
35413
35758
|
line: lineNum,
|
|
@@ -35427,14 +35772,15 @@ var ScanSecretsPass = class {
|
|
|
35427
35772
|
const key = `${lineNum}:hardcoded-credential`;
|
|
35428
35773
|
if (seen.has(key)) continue;
|
|
35429
35774
|
seen.add(key);
|
|
35775
|
+
const dg = applyDemoDowngrade(demoPath, "high", "error");
|
|
35430
35776
|
ctx.addFinding({
|
|
35431
35777
|
id: `hardcoded-credential-${file}-${lineNum}`,
|
|
35432
35778
|
pass: this.name,
|
|
35433
35779
|
category: this.category,
|
|
35434
35780
|
rule_id: "hardcoded-credential",
|
|
35435
35781
|
cwe: "CWE-798",
|
|
35436
|
-
severity:
|
|
35437
|
-
level:
|
|
35782
|
+
severity: dg.severity,
|
|
35783
|
+
level: dg.level,
|
|
35438
35784
|
message: `Hardcoded credential: \`${hit.name}\` assigned a literal value`,
|
|
35439
35785
|
file,
|
|
35440
35786
|
line: lineNum,
|
|
@@ -39748,7 +40094,9 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
39748
40094
|
pipeline.add(new TaintMatcherPass());
|
|
39749
40095
|
pipeline.add(new ConstantPropagationPass(tree));
|
|
39750
40096
|
pipeline.add(new LanguageSourcesPass());
|
|
40097
|
+
if (!disabledPasses.has("source-semantics")) pipeline.add(new SourceSemanticsPass());
|
|
39751
40098
|
pipeline.add(new SinkFilterPass());
|
|
40099
|
+
if (!disabledPasses.has("sink-semantics")) pipeline.add(new SinkSemanticsPass());
|
|
39752
40100
|
pipeline.add(new TaintPropagationPass());
|
|
39753
40101
|
pipeline.add(new InterproceduralPass({
|
|
39754
40102
|
enableEntryPointGate: options.enableEntryPointGate ?? true
|