circle-ir 3.140.0 → 3.144.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/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/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 +60 -9
- 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 +10 -0
- 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 +295 -17
- package/dist/core/circle-ir-core.cjs +142 -11
- package/dist/core/circle-ir-core.js +142 -11
- package/dist/core/extractors/calls.js +104 -18
- 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);
|
|
@@ -6347,19 +6353,43 @@ function resolveReceiverType(receiver, context) {
|
|
|
6347
6353
|
return resolveFqn(simple, context);
|
|
6348
6354
|
}
|
|
6349
6355
|
}
|
|
6350
|
-
const
|
|
6351
|
-
if (
|
|
6352
|
-
const
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
if (varType) {
|
|
6356
|
-
const returnType = JAVA_CHAINED_FACTORY_RETURN_TYPES[stripGenerics(varType)]?.[methodName];
|
|
6356
|
+
const chain = splitChainedReceiver(receiver);
|
|
6357
|
+
if (chain) {
|
|
6358
|
+
const prefixType = resolveReceiverType(chain.prefix, context);
|
|
6359
|
+
if (prefixType.simpleName) {
|
|
6360
|
+
const returnType = JAVA_CHAINED_FACTORY_RETURN_TYPES[prefixType.simpleName]?.[chain.methodName];
|
|
6357
6361
|
if (returnType) return resolveFqn(returnType, context);
|
|
6358
6362
|
}
|
|
6359
6363
|
}
|
|
6360
6364
|
return { simpleName: null, fqn: null };
|
|
6361
6365
|
}
|
|
6366
|
+
function splitChainedReceiver(receiver) {
|
|
6367
|
+
if (!receiver.endsWith(")")) return null;
|
|
6368
|
+
let depth = 0;
|
|
6369
|
+
let openIdx = -1;
|
|
6370
|
+
for (let i2 = receiver.length - 1; i2 >= 0; i2--) {
|
|
6371
|
+
const c = receiver[i2];
|
|
6372
|
+
if (c === ")") depth++;
|
|
6373
|
+
else if (c === "(") {
|
|
6374
|
+
depth--;
|
|
6375
|
+
if (depth === 0) {
|
|
6376
|
+
openIdx = i2;
|
|
6377
|
+
break;
|
|
6378
|
+
}
|
|
6379
|
+
}
|
|
6380
|
+
}
|
|
6381
|
+
if (openIdx <= 0) return null;
|
|
6382
|
+
const beforeParen = receiver.substring(0, openIdx);
|
|
6383
|
+
const dotIdx = beforeParen.lastIndexOf(".");
|
|
6384
|
+
if (dotIdx <= 0) return null;
|
|
6385
|
+
const methodName = beforeParen.substring(dotIdx + 1).trim();
|
|
6386
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(methodName)) return null;
|
|
6387
|
+
const prefix = beforeParen.substring(0, dotIdx).trim();
|
|
6388
|
+
if (!prefix) return null;
|
|
6389
|
+
return { prefix, methodName };
|
|
6390
|
+
}
|
|
6362
6391
|
var JAVA_CHAINED_FACTORY_RETURN_TYPES = {
|
|
6392
|
+
// Servlet API (Sprint 91 / #117)
|
|
6363
6393
|
HttpServletRequest: {
|
|
6364
6394
|
getSession: "HttpSession",
|
|
6365
6395
|
getServletContext: "ServletContext",
|
|
@@ -6370,6 +6400,32 @@ var JAVA_CHAINED_FACTORY_RETURN_TYPES = {
|
|
|
6370
6400
|
},
|
|
6371
6401
|
ServletContext: {
|
|
6372
6402
|
getRequestDispatcher: "RequestDispatcher"
|
|
6403
|
+
},
|
|
6404
|
+
// JAXP DOM / SAX / XPath / Transformer / StAX factories (Sprint 92 / #189)
|
|
6405
|
+
DocumentBuilderFactory: {
|
|
6406
|
+
newInstance: "DocumentBuilderFactory",
|
|
6407
|
+
newDocumentBuilder: "DocumentBuilder"
|
|
6408
|
+
},
|
|
6409
|
+
SAXParserFactory: {
|
|
6410
|
+
newInstance: "SAXParserFactory",
|
|
6411
|
+
newSAXParser: "SAXParser"
|
|
6412
|
+
},
|
|
6413
|
+
SAXParser: {
|
|
6414
|
+
getXMLReader: "XMLReader"
|
|
6415
|
+
},
|
|
6416
|
+
XPathFactory: {
|
|
6417
|
+
newInstance: "XPathFactory",
|
|
6418
|
+
newXPath: "XPath"
|
|
6419
|
+
},
|
|
6420
|
+
TransformerFactory: {
|
|
6421
|
+
newInstance: "TransformerFactory",
|
|
6422
|
+
newTransformer: "Transformer"
|
|
6423
|
+
},
|
|
6424
|
+
XMLInputFactory: {
|
|
6425
|
+
newInstance: "XMLInputFactory",
|
|
6426
|
+
newFactory: "XMLInputFactory",
|
|
6427
|
+
createXMLStreamReader: "XMLStreamReader",
|
|
6428
|
+
createXMLEventReader: "XMLEventReader"
|
|
6373
6429
|
}
|
|
6374
6430
|
};
|
|
6375
6431
|
function resolveFqn(simpleName, context) {
|
|
@@ -11401,6 +11457,14 @@ var DEFAULT_SINKS = [
|
|
|
11401
11457
|
// reflection remains a downstream concern of body-writing sinks.
|
|
11402
11458
|
{ method: "setHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
|
|
11403
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] },
|
|
11404
11468
|
// Note: `sendRedirect` is primarily classified as `ssrf` / open-redirect
|
|
11405
11469
|
// (CWE-601) further down — see entry near line 1195. CRLF via Location
|
|
11406
11470
|
// header is a secondary concern; keeping the canonical SSRF entry avoids
|
|
@@ -12840,11 +12904,62 @@ var DEFAULT_SANITIZERS = [
|
|
|
12840
12904
|
{ method: "UUID", class: "uuid", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12841
12905
|
{ method: "Decimal", class: "decimal", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] }
|
|
12842
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
|
+
];
|
|
12843
12957
|
function getDefaultConfig() {
|
|
12844
12958
|
return {
|
|
12845
12959
|
sources: DEFAULT_SOURCES,
|
|
12846
12960
|
sinks: DEFAULT_SINKS,
|
|
12847
|
-
sanitizers: DEFAULT_SANITIZERS
|
|
12961
|
+
sanitizers: DEFAULT_SANITIZERS,
|
|
12962
|
+
sinkSemantics: DEFAULT_SINK_SEMANTICS
|
|
12848
12963
|
};
|
|
12849
12964
|
}
|
|
12850
12965
|
var DEFAULT_HEADER_RULES = [
|
|
@@ -13688,6 +13803,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
|
|
|
13688
13803
|
const confidence = calculateSinkConfidence(call, pattern);
|
|
13689
13804
|
const existing = sinkMap.get(key);
|
|
13690
13805
|
if (!existing || confidence > existing.confidence) {
|
|
13806
|
+
const receiverType = call.receiver_type;
|
|
13807
|
+
const simpleClass = receiverType ? receiverType.split(".").pop() || void 0 : void 0;
|
|
13691
13808
|
sinkMap.set(key, {
|
|
13692
13809
|
type: pattern.type,
|
|
13693
13810
|
cwe: pattern.cwe,
|
|
@@ -13695,7 +13812,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
|
|
|
13695
13812
|
line: call.location.line,
|
|
13696
13813
|
confidence,
|
|
13697
13814
|
method: call.method_name,
|
|
13698
|
-
argPositions: pattern.arg_positions
|
|
13815
|
+
argPositions: pattern.arg_positions,
|
|
13816
|
+
class: simpleClass
|
|
13699
13817
|
});
|
|
13700
13818
|
}
|
|
13701
13819
|
}
|
|
@@ -14650,12 +14768,20 @@ function canSourceReachSink(sourceType, sinkType) {
|
|
|
14650
14768
|
// flow detector silently dropped `http_* → trust_boundary` co-located
|
|
14651
14769
|
// flows like `req.getSession().setAttribute("u", req.getParameter("u"))`
|
|
14652
14770
|
// (0% recall on OWASP Java trustbound category).
|
|
14653
|
-
|
|
14771
|
+
// deserialization added Sprint 93 (#189): SnakeYAML/Jackson/etc. sinks
|
|
14772
|
+
// like `new Yaml().load(req.getParameter("y"))` are real RCE gadget chains
|
|
14773
|
+
// (CWE-502). The reach map previously restricted deserialization to
|
|
14774
|
+
// http_body so http_param/http_query request-derived values feeding
|
|
14775
|
+
// Yaml.load, ObjectInputStream ctor, XMLDecoder ctor, etc. silently
|
|
14776
|
+
// dropped their inline-colocation flow. Typed-overload FPs are gated by
|
|
14777
|
+
// the sink pattern's `safe_if_class_literal_at` flag (Jackson readValue,
|
|
14778
|
+
// Yaml.loadAs, Gson.fromJson) so the wider reach does not regress.
|
|
14779
|
+
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"],
|
|
14654
14780
|
http_body: ["sql_injection", "command_injection", "deserialization", "xxe", "xss", "code_injection", "mybatis_mapper_call", "crlf", "mass_assignment", "open_redirect", "trust_boundary"],
|
|
14655
14781
|
http_header: ["sql_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "open_redirect", "trust_boundary"],
|
|
14656
14782
|
http_cookie: ["sql_injection", "xss", "mybatis_mapper_call", "code_injection", "crlf", "open_redirect", "trust_boundary"],
|
|
14657
14783
|
http_path: ["path_traversal", "sql_injection", "ssrf", "mybatis_mapper_call", "open_redirect", "trust_boundary"],
|
|
14658
|
-
http_query: ["sql_injection", "command_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment", "open_redirect", "trust_boundary"],
|
|
14784
|
+
http_query: ["sql_injection", "command_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment", "open_redirect", "trust_boundary", "deserialization"],
|
|
14659
14785
|
// ssrf added Sprint 57 #200: bash CGI/webhook handlers and scripts that
|
|
14660
14786
|
// take a URL on stdin or as a positional CLI arg (`curl "$1"`,
|
|
14661
14787
|
// `wget "$(read line)"`) and curl/wget it server-side are textbook SSRF
|
|
@@ -14677,6 +14803,15 @@ function canSourceReachSink(sourceType, sinkType) {
|
|
|
14677
14803
|
const validSinks = sourceToSinkMapping[sourceType];
|
|
14678
14804
|
return validSinks ? validSinks.includes(sinkType) : false;
|
|
14679
14805
|
}
|
|
14806
|
+
function sourceSemanticsAllowed(source, sinkType) {
|
|
14807
|
+
if (source.constant === true) {
|
|
14808
|
+
return false;
|
|
14809
|
+
}
|
|
14810
|
+
if (source.spi === true) {
|
|
14811
|
+
return sinkType === "code_injection";
|
|
14812
|
+
}
|
|
14813
|
+
return true;
|
|
14814
|
+
}
|
|
14680
14815
|
|
|
14681
14816
|
// src/analysis/findings-instrumentation.ts
|
|
14682
14817
|
var instrumentEnabled = false;
|
|
@@ -28917,6 +29052,80 @@ function findJsTemplateInjectionSstiFindings(code, file) {
|
|
|
28917
29052
|
return findings;
|
|
28918
29053
|
}
|
|
28919
29054
|
|
|
29055
|
+
// src/analysis/passes/source-semantics-pass.ts
|
|
29056
|
+
var DEMO_PATH_RE = /(?:^|\/)(?:demo|example|examples|samples|integration-tests|integration_tests)(?:\/|$)/i;
|
|
29057
|
+
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*$/;
|
|
29058
|
+
var STATIC_FINAL_RE = /^\s*(?:public\s+|private\s+|protected\s+)?static\s+final\s+/;
|
|
29059
|
+
var ENUM_CONST_REF_RE = /=\s*[A-Z][A-Za-z0-9_]*\.[A-Z][A-Z0-9_]*\s*;?\s*$/;
|
|
29060
|
+
function isConstantSource(code) {
|
|
29061
|
+
if (!code) return false;
|
|
29062
|
+
if (CONST_STRING_ASSIGN_RE.test(code)) return true;
|
|
29063
|
+
if (STATIC_FINAL_RE.test(code)) {
|
|
29064
|
+
const rhs = code.split("=").slice(1).join("=").trim();
|
|
29065
|
+
if (rhs.length === 0) return false;
|
|
29066
|
+
if (/^"[^"]*"\s*;?\s*$/.test(rhs)) return true;
|
|
29067
|
+
if (/^-?\d+(?:\.\d+)?[fFdDlL]?\s*;?\s*$/.test(rhs)) return true;
|
|
29068
|
+
if (/^(?:true|false)\s*;?\s*$/.test(rhs)) return true;
|
|
29069
|
+
if (/^[A-Za-z_][\w.]*\s*;?\s*$/.test(rhs)) return true;
|
|
29070
|
+
return false;
|
|
29071
|
+
}
|
|
29072
|
+
if (ENUM_CONST_REF_RE.test(code)) return true;
|
|
29073
|
+
return false;
|
|
29074
|
+
}
|
|
29075
|
+
var SERVICE_LOADER_RE = /\bServiceLoader\.(?:load|loadInstalled|stream)\s*\(/;
|
|
29076
|
+
var CLASS_FOR_NAME_RE = /\bClass\.forName\s*\(/;
|
|
29077
|
+
var META_INF_SERVICES_RE = /getResources?\s*\(\s*"META-INF\/services\//;
|
|
29078
|
+
var SPI_WINDOW = 30;
|
|
29079
|
+
function isSpiSource(source, lines) {
|
|
29080
|
+
const code = source.code;
|
|
29081
|
+
if (!code) return false;
|
|
29082
|
+
if (SERVICE_LOADER_RE.test(code)) return true;
|
|
29083
|
+
if (CLASS_FOR_NAME_RE.test(code)) {
|
|
29084
|
+
const start2 = Math.max(0, source.line - 1 - SPI_WINDOW);
|
|
29085
|
+
const end = Math.min(lines.length, source.line - 1 + SPI_WINDOW + 1);
|
|
29086
|
+
for (let i2 = start2; i2 < end; i2++) {
|
|
29087
|
+
if (META_INF_SERVICES_RE.test(lines[i2])) return true;
|
|
29088
|
+
}
|
|
29089
|
+
}
|
|
29090
|
+
return false;
|
|
29091
|
+
}
|
|
29092
|
+
function isDemoPathFile(file) {
|
|
29093
|
+
if (!file) return false;
|
|
29094
|
+
return DEMO_PATH_RE.test(file);
|
|
29095
|
+
}
|
|
29096
|
+
var SourceSemanticsPass = class {
|
|
29097
|
+
name = "source-semantics";
|
|
29098
|
+
category = "security";
|
|
29099
|
+
run(ctx) {
|
|
29100
|
+
const { graph, code } = ctx;
|
|
29101
|
+
const sources = graph.ir.taint.sources;
|
|
29102
|
+
if (sources.length === 0) {
|
|
29103
|
+
return { constantCount: 0, spiCount: 0, demoPathCount: 0 };
|
|
29104
|
+
}
|
|
29105
|
+
const file = graph.ir.meta.file;
|
|
29106
|
+
const demoPath = isDemoPathFile(file);
|
|
29107
|
+
const lines = code.split("\n");
|
|
29108
|
+
let constantCount = 0;
|
|
29109
|
+
let spiCount = 0;
|
|
29110
|
+
let demoPathCount = 0;
|
|
29111
|
+
for (const source of sources) {
|
|
29112
|
+
if (isConstantSource(source.code)) {
|
|
29113
|
+
source.constant = true;
|
|
29114
|
+
constantCount++;
|
|
29115
|
+
}
|
|
29116
|
+
if (isSpiSource(source, lines)) {
|
|
29117
|
+
source.spi = true;
|
|
29118
|
+
spiCount++;
|
|
29119
|
+
}
|
|
29120
|
+
if (demoPath) {
|
|
29121
|
+
source.demoPath = true;
|
|
29122
|
+
demoPathCount++;
|
|
29123
|
+
}
|
|
29124
|
+
}
|
|
29125
|
+
return { constantCount, spiCount, demoPathCount };
|
|
29126
|
+
}
|
|
29127
|
+
};
|
|
29128
|
+
|
|
28920
29129
|
// src/analysis/passes/sink-filter-pass.ts
|
|
28921
29130
|
var JS_XSS_SANITIZERS = [
|
|
28922
29131
|
/\bDOMPurify\.sanitize\s*\(/,
|
|
@@ -29464,7 +29673,7 @@ var SinkFilterPass = class {
|
|
|
29464
29673
|
const sourceLines = ctx.code.split("\n");
|
|
29465
29674
|
filtered = filtered.filter((sink) => {
|
|
29466
29675
|
if (sink.type !== "command_injection") return true;
|
|
29467
|
-
if (sink.method !== "ProcessBuilder") return true;
|
|
29676
|
+
if (sink.method !== "ProcessBuilder" && sink.method !== "start") return true;
|
|
29468
29677
|
const sinkLineText = sourceLines[sink.line - 1] ?? "";
|
|
29469
29678
|
if (!/\bnew\s+ProcessBuilder\s*\(/.test(sinkLineText)) return true;
|
|
29470
29679
|
if (PROCESS_BUILDER_ARGV_FORM_RE.test(sinkLineText)) return false;
|
|
@@ -30208,6 +30417,50 @@ function filterSanitizedSinks(sinks, sanitizers, calls) {
|
|
|
30208
30417
|
});
|
|
30209
30418
|
}
|
|
30210
30419
|
|
|
30420
|
+
// src/analysis/passes/sink-semantics-pass.ts
|
|
30421
|
+
function buildRegistry(entries) {
|
|
30422
|
+
const registry = /* @__PURE__ */ new Map();
|
|
30423
|
+
for (const entry of entries) {
|
|
30424
|
+
const existing = registry.get(entry.signature);
|
|
30425
|
+
if (existing) {
|
|
30426
|
+
for (const t of entry.overrides) existing.add(t);
|
|
30427
|
+
} else {
|
|
30428
|
+
registry.set(entry.signature, new Set(entry.overrides));
|
|
30429
|
+
}
|
|
30430
|
+
}
|
|
30431
|
+
return registry;
|
|
30432
|
+
}
|
|
30433
|
+
var SinkSemanticsPass = class {
|
|
30434
|
+
name = "sink-semantics";
|
|
30435
|
+
category = "security";
|
|
30436
|
+
run(ctx) {
|
|
30437
|
+
const { graph, config } = ctx;
|
|
30438
|
+
const entries = config.sinkSemantics ?? [];
|
|
30439
|
+
if (entries.length === 0) {
|
|
30440
|
+
return { droppedCount: 0, registrySize: 0 };
|
|
30441
|
+
}
|
|
30442
|
+
const registry = buildRegistry(entries);
|
|
30443
|
+
const sinks = graph.ir.taint.sinks;
|
|
30444
|
+
let droppedCount = 0;
|
|
30445
|
+
const kept = sinks.filter((sink) => {
|
|
30446
|
+
if (!sink.class || !sink.method) return true;
|
|
30447
|
+
const signature = `${sink.class}#${sink.method}`;
|
|
30448
|
+
const overrides = registry.get(signature);
|
|
30449
|
+
if (!overrides) return true;
|
|
30450
|
+
if (overrides.has(sink.type)) {
|
|
30451
|
+
droppedCount++;
|
|
30452
|
+
return false;
|
|
30453
|
+
}
|
|
30454
|
+
return true;
|
|
30455
|
+
});
|
|
30456
|
+
if (droppedCount > 0) {
|
|
30457
|
+
sinks.length = 0;
|
|
30458
|
+
sinks.push(...kept);
|
|
30459
|
+
}
|
|
30460
|
+
return { droppedCount, registrySize: registry.size };
|
|
30461
|
+
}
|
|
30462
|
+
};
|
|
30463
|
+
|
|
30211
30464
|
// src/analysis/passes/taint-propagation-pass.ts
|
|
30212
30465
|
var TaintPropagationPass = class {
|
|
30213
30466
|
name = "taint-propagation";
|
|
@@ -30866,6 +31119,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
30866
31119
|
}
|
|
30867
31120
|
const re = reCache.get(source.variable);
|
|
30868
31121
|
if (!re || !re.test(expr)) continue;
|
|
31122
|
+
if (!sourceSemanticsAllowed(source, sink.type)) continue;
|
|
30869
31123
|
if (flows.some(
|
|
30870
31124
|
(f) => f.source_line === source.line && f.sink_line === sink.line && f.sink_type === sink.type
|
|
30871
31125
|
)) continue;
|
|
@@ -30894,7 +31148,6 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
30894
31148
|
}
|
|
30895
31149
|
const sourcesByLine = /* @__PURE__ */ new Map();
|
|
30896
31150
|
for (const s of sources) {
|
|
30897
|
-
if (s.variable && s.variable.length > 0) continue;
|
|
30898
31151
|
const arr = sourcesByLine.get(s.line) ?? [];
|
|
30899
31152
|
arr.push(s);
|
|
30900
31153
|
sourcesByLine.set(s.line, arr);
|
|
@@ -30905,6 +31158,19 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
30905
31158
|
if (!colocSources || colocSources.length === 0) continue;
|
|
30906
31159
|
for (const source of colocSources) {
|
|
30907
31160
|
if (!canSourceReachSink(source.type, sink.type)) continue;
|
|
31161
|
+
if (!sourceSemanticsAllowed(source, sink.type)) continue;
|
|
31162
|
+
const sourceVar = source.variable;
|
|
31163
|
+
if (sourceVar && sourceVar.length > 0) {
|
|
31164
|
+
const sinkCode = sink.code;
|
|
31165
|
+
if (!sinkCode) {
|
|
31166
|
+
continue;
|
|
31167
|
+
}
|
|
31168
|
+
const assignMatch = sinkCode.match(/^\s*(?:[A-Za-z_][\w.<>[\]\s,?]*\s+)?[A-Za-z_]\w*\s*=(?!=)\s*/);
|
|
31169
|
+
const rhs = assignMatch ? sinkCode.slice(assignMatch[0].length) : sinkCode;
|
|
31170
|
+
if (new RegExp(`\\b${sourceVar}\\b`).test(rhs)) {
|
|
31171
|
+
continue;
|
|
31172
|
+
}
|
|
31173
|
+
}
|
|
30908
31174
|
if (source.type === "file_input" && sink.type === "path_traversal" && sink.method && source.location.includes(`${sink.method}(`)) {
|
|
30909
31175
|
continue;
|
|
30910
31176
|
}
|
|
@@ -35009,6 +35275,13 @@ function isProtocolMandatedCryptoFile(file, code) {
|
|
|
35009
35275
|
}
|
|
35010
35276
|
|
|
35011
35277
|
// src/analysis/passes/scan-secrets-pass.ts
|
|
35278
|
+
function applyDemoDowngrade(demoPath, severity, level) {
|
|
35279
|
+
if (!demoPath) return { severity, level };
|
|
35280
|
+
if (severity === "high") {
|
|
35281
|
+
return { severity: "low", level: "note" };
|
|
35282
|
+
}
|
|
35283
|
+
return { severity, level };
|
|
35284
|
+
}
|
|
35012
35285
|
var TEST_PATH_RE3 = /(?:^|[\\/])(?:test|tests|spec|specs|__tests?__|__mocks?__|fixtures?|testdata)(?:[\\/]|$)/i;
|
|
35013
35286
|
var TEST_FILENAME_RE = /(?:\.(?:test|spec)\.[cm]?[jt]sx?|_test\.go|_test\.py|Test\.java|Tests\.java)$/i;
|
|
35014
35287
|
function isTestFile(file) {
|
|
@@ -35324,6 +35597,7 @@ var ScanSecretsPass = class {
|
|
|
35324
35597
|
if (isTestFile(file) || isGeneratedFile(file)) {
|
|
35325
35598
|
return { providerFindings: 0, entropyFindings: 0 };
|
|
35326
35599
|
}
|
|
35600
|
+
const demoPath = DEMO_PATH_RE.test(file);
|
|
35327
35601
|
const lines = ctx.code.split("\n");
|
|
35328
35602
|
const prior = ctx.getFindings?.() ?? [];
|
|
35329
35603
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -35350,14 +35624,15 @@ var ScanSecretsPass = class {
|
|
|
35350
35624
|
const key = `${lineNum}:hardcoded-credential`;
|
|
35351
35625
|
if (seen.has(key)) continue;
|
|
35352
35626
|
seen.add(key);
|
|
35627
|
+
const dg = applyDemoDowngrade(demoPath, pattern.severity, pattern.level);
|
|
35353
35628
|
ctx.addFinding({
|
|
35354
35629
|
id: `hardcoded-credential-${file}-${lineNum}`,
|
|
35355
35630
|
pass: this.name,
|
|
35356
35631
|
category: this.category,
|
|
35357
35632
|
rule_id: "hardcoded-credential",
|
|
35358
35633
|
cwe: "CWE-798",
|
|
35359
|
-
severity:
|
|
35360
|
-
level:
|
|
35634
|
+
severity: dg.severity,
|
|
35635
|
+
level: dg.level,
|
|
35361
35636
|
message: `Hardcoded credential: ${pattern.name} detected`,
|
|
35362
35637
|
file,
|
|
35363
35638
|
line: lineNum,
|
|
@@ -35377,14 +35652,15 @@ var ScanSecretsPass = class {
|
|
|
35377
35652
|
const key = `${lineNum}:hardcoded-credential`;
|
|
35378
35653
|
if (seen.has(key)) continue;
|
|
35379
35654
|
seen.add(key);
|
|
35655
|
+
const dg = applyDemoDowngrade(demoPath, "high", "error");
|
|
35380
35656
|
ctx.addFinding({
|
|
35381
35657
|
id: `hardcoded-credential-${file}-${lineNum}`,
|
|
35382
35658
|
pass: this.name,
|
|
35383
35659
|
category: this.category,
|
|
35384
35660
|
rule_id: "hardcoded-credential",
|
|
35385
35661
|
cwe: "CWE-798",
|
|
35386
|
-
severity:
|
|
35387
|
-
level:
|
|
35662
|
+
severity: dg.severity,
|
|
35663
|
+
level: dg.level,
|
|
35388
35664
|
message: `Hardcoded credential: \`${hit.name}\` assigned a literal value`,
|
|
35389
35665
|
file,
|
|
35390
35666
|
line: lineNum,
|
|
@@ -39698,7 +39974,9 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
39698
39974
|
pipeline.add(new TaintMatcherPass());
|
|
39699
39975
|
pipeline.add(new ConstantPropagationPass(tree));
|
|
39700
39976
|
pipeline.add(new LanguageSourcesPass());
|
|
39977
|
+
if (!disabledPasses.has("source-semantics")) pipeline.add(new SourceSemanticsPass());
|
|
39701
39978
|
pipeline.add(new SinkFilterPass());
|
|
39979
|
+
if (!disabledPasses.has("sink-semantics")) pipeline.add(new SinkSemanticsPass());
|
|
39702
39980
|
pipeline.add(new TaintPropagationPass());
|
|
39703
39981
|
pipeline.add(new InterproceduralPass({
|
|
39704
39982
|
enableEntryPointGate: options.enableEntryPointGate ?? true
|