@chain305/x-security 0.2.6 → 0.3.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/bin/scripts/README.md +39 -39
- package/bin/scripts/{writ-refresh.service → x-security-refresh.service} +4 -4
- package/bin/scripts/{writ-refresh.sh → x-security-refresh.sh} +11 -11
- package/bin/scripts/{writ-refresh.timer → x-security-refresh.timer} +3 -3
- package/bin/scripts/{writ-resolve.sh → x-security-resolve.sh} +19 -19
- package/bin/scripts/{writ.logrotate → x-security.logrotate} +3 -3
- package/bin/xsecurity.mjs +472 -449
- package/package.json +1 -1
package/bin/xsecurity.mjs
CHANGED
|
@@ -978,7 +978,7 @@ function buildAuthzLua(authz, ctx) {
|
|
|
978
978
|
` if ${condition} then`,
|
|
979
979
|
` kong.log.warn("[${SS_BOLA_TAG}] rule ${i5 + 1} (${rule.field} ${rule.operator}) denied")`,
|
|
980
980
|
` return kong.response.exit(403, {`,
|
|
981
|
-
` message = "
|
|
981
|
+
` message = "XSecurity: BOLA denied",`,
|
|
982
982
|
` tag = "${SS_BOLA_TAG}",`,
|
|
983
983
|
` rule = ${i5 + 1}`,
|
|
984
984
|
` })`,
|
|
@@ -997,12 +997,12 @@ function buildAuthzLua(authz, ctx) {
|
|
|
997
997
|
});
|
|
998
998
|
}
|
|
999
999
|
const head = [
|
|
1000
|
-
`--
|
|
1000
|
+
`-- XSecurity K-1 BOLA pre-function for endpoint=${ctx.endpoint ?? "?"}`,
|
|
1001
1001
|
`local cjson = require("cjson.safe")`,
|
|
1002
1002
|
`local principal = ${refToLuaExpr("jwt.sub").expr}`,
|
|
1003
1003
|
`if not principal then`,
|
|
1004
1004
|
` kong.log.warn("[${SS_BOLA_TAG}] no authenticated principal; denying")`,
|
|
1005
|
-
` return kong.response.exit(401, {message = "
|
|
1005
|
+
` return kong.response.exit(401, {message = "XSecurity: no authenticated principal", tag = "${SS_BOLA_TAG}"})`,
|
|
1006
1006
|
`end`,
|
|
1007
1007
|
``
|
|
1008
1008
|
];
|
|
@@ -1011,21 +1011,21 @@ function buildAuthzLua(authz, ctx) {
|
|
|
1011
1011
|
const idExpr = refToLuaExpr(lookup.identifierFrom).expr;
|
|
1012
1012
|
head.push(
|
|
1013
1013
|
`-- W10-11: shared_dict cache (key=principal:resource_id, TTL=${SS_BOLA_CACHE_TTL_SECONDS}s)`,
|
|
1014
|
-
`local ss_cache = ngx.shared.
|
|
1014
|
+
`local ss_cache = ngx.shared.x_security_bola_cache`,
|
|
1015
1015
|
`local ss_resource_id = tostring(${idExpr} or "")`,
|
|
1016
1016
|
`local ss_cache_key = tostring(principal) .. ":" .. ss_resource_id`,
|
|
1017
1017
|
`local ss_cached_owner = ss_cache and ss_cache:get(ss_cache_key) or nil`,
|
|
1018
1018
|
`local ss_resource = nil`,
|
|
1019
1019
|
`if ss_cached_owner ~= nil then`,
|
|
1020
|
-
` kong.log.warn("[
|
|
1020
|
+
` kong.log.warn("[x-security-bola] cache_hit key=" .. ss_cache_key .. " owner=" .. tostring(ss_cached_owner))`,
|
|
1021
1021
|
` ss_resource = { ownerId = ss_cached_owner, _ss_cached = true }`,
|
|
1022
1022
|
`else`,
|
|
1023
1023
|
` -- resource lookup: ${lookup.endpoint} (identifier from ${lookup.identifierFrom})`,
|
|
1024
|
-
` kong.log.warn("[
|
|
1024
|
+
` kong.log.warn("[x-security-bola] cache_miss key=" .. ss_cache_key)`,
|
|
1025
1025
|
` local ok_req, http = pcall(require, "resty.http")`,
|
|
1026
1026
|
` if not ok_req then`,
|
|
1027
1027
|
` kong.log.warn("[${SS_BOLA_TAG}] resty.http not available; failing closed")`,
|
|
1028
|
-
` return kong.response.exit(403, {message = "
|
|
1028
|
+
` return kong.response.exit(403, {message = "XSecurity: resource access denied", tag = "${SS_BOLA_TAG}", reason = "resty_http_missing"})`,
|
|
1029
1029
|
` end`,
|
|
1030
1030
|
` local httpc = http.new()`,
|
|
1031
1031
|
` httpc:set_timeout(2000)`,
|
|
@@ -1035,19 +1035,19 @@ function buildAuthzLua(authz, ctx) {
|
|
|
1035
1035
|
` method = "GET",`,
|
|
1036
1036
|
` headers = {`,
|
|
1037
1037
|
` ["Authorization"] = kong.request.get_header("Authorization"),`,
|
|
1038
|
-
` ["X-
|
|
1038
|
+
` ["X-XSecurity-Internal"] = "rule-lookup"`,
|
|
1039
1039
|
` },`,
|
|
1040
1040
|
` ssl_verify = false`,
|
|
1041
1041
|
` })`,
|
|
1042
1042
|
` end)`,
|
|
1043
1043
|
` if not ok_call or not res or res.status ~= 200 then`,
|
|
1044
|
-
` kong.log.warn("[
|
|
1045
|
-
` return kong.response.exit(403, {message = "
|
|
1044
|
+
` kong.log.warn("[x-security-bola] resource lookup failed status=" .. tostring(res and res.status or "no_response"))`,
|
|
1045
|
+
` return kong.response.exit(403, {message = "XSecurity: resource access denied", tag = "${SS_BOLA_TAG}", reason = "lookup_failed"})`,
|
|
1046
1046
|
` end`,
|
|
1047
1047
|
` local ok_decode, body = pcall(cjson.decode, res.body or "{}")`,
|
|
1048
1048
|
` if not ok_decode or type(body) ~= "table" then`,
|
|
1049
|
-
` kong.log.warn("[
|
|
1050
|
-
` return kong.response.exit(403, {message = "
|
|
1049
|
+
` kong.log.warn("[x-security-bola] decode failed for " .. lookup_url)`,
|
|
1050
|
+
` return kong.response.exit(403, {message = "XSecurity: invalid resource response", tag = "${SS_BOLA_TAG}", reason = "decode_failed"})`,
|
|
1051
1051
|
` end`,
|
|
1052
1052
|
` ss_resource = body`,
|
|
1053
1053
|
` if ss_cache and body.ownerId ~= nil then`,
|
|
@@ -1112,7 +1112,7 @@ function buildSsrfPreFunctionPlugins(request9, ctx = {}) {
|
|
|
1112
1112
|
}
|
|
1113
1113
|
if (items.length === 0) return [];
|
|
1114
1114
|
const lines = [
|
|
1115
|
-
`--
|
|
1115
|
+
`-- XSecurity W19-A SSRF url-allowlist pre-function for endpoint=${ctx.endpoint ?? "?"}`,
|
|
1116
1116
|
`-- W21-C: pure Kong PDK + Lua stdlib (no external module loads). Hardened`,
|
|
1117
1117
|
`-- OSS deployments restrict the untrusted_lua sandbox; this snippet runs there.`,
|
|
1118
1118
|
``,
|
|
@@ -1142,13 +1142,13 @@ function buildSsrfPreFunctionPlugins(request9, ctx = {}) {
|
|
|
1142
1142
|
lines.push(` local ss_allow = ${luaHostSet(it.allow)}`);
|
|
1143
1143
|
lines.push(` if not host or not ss_allow[host] then`);
|
|
1144
1144
|
lines.push(` kong.log.warn("[${SS_SSRF_TAG}] host '" .. tostring(host) .. "' not in domainAllowlist for ${it.field}")`);
|
|
1145
|
-
lines.push(` return kong.response.exit(403, {message = "
|
|
1145
|
+
lines.push(` return kong.response.exit(403, {message = "XSecurity: SSRF url not in domainAllowlist", tag = "${SS_SSRF_TAG}", field = ${luaStr(it.field)}})`);
|
|
1146
1146
|
lines.push(` end`);
|
|
1147
1147
|
}
|
|
1148
1148
|
if (it.block) {
|
|
1149
1149
|
lines.push(` if ss_is_private(host) then`);
|
|
1150
1150
|
lines.push(` kong.log.warn("[${SS_SSRF_PRIVATE_TAG}] private-range host '" .. tostring(host) .. "' on ${it.field}")`);
|
|
1151
|
-
lines.push(` return kong.response.exit(403, {message = "
|
|
1151
|
+
lines.push(` return kong.response.exit(403, {message = "XSecurity: SSRF blocked private/loopback host", tag = "${SS_SSRF_PRIVATE_TAG}", field = ${luaStr(it.field)}})`);
|
|
1152
1152
|
lines.push(` end`);
|
|
1153
1153
|
}
|
|
1154
1154
|
lines.push(` end`);
|
|
@@ -1185,7 +1185,7 @@ function buildMassAssignPreFunctionPlugins(request9, ctx = {}) {
|
|
|
1185
1185
|
if (!allow) return [];
|
|
1186
1186
|
const luaAllowTable = "{" + allow.map((k5) => `[${luaStr(k5)}]=true`).join(", ") + "}";
|
|
1187
1187
|
const lua = [
|
|
1188
|
-
`--
|
|
1188
|
+
`-- XSecurity K-3 mass-assignment pre-function for endpoint=${ctx.endpoint ?? "?"}`,
|
|
1189
1189
|
`-- Allowlist sourced from ${explicitAllow ? "request.allowedFields" : "request.schema top-level keys"}.`,
|
|
1190
1190
|
`local ss_allow = ${luaAllowTable}`,
|
|
1191
1191
|
`local body = kong.request.get_body()`,
|
|
@@ -1194,7 +1194,7 @@ function buildMassAssignPreFunctionPlugins(request9, ctx = {}) {
|
|
|
1194
1194
|
` if not ss_allow[k] then`,
|
|
1195
1195
|
` kong.log.warn("[${SS_MASS_ASSIGN_TAG}] unknown field '" .. tostring(k) .. "' rejected")`,
|
|
1196
1196
|
` return kong.response.exit(403, {`,
|
|
1197
|
-
` message = "
|
|
1197
|
+
` message = "XSecurity: unknown field rejected",`,
|
|
1198
1198
|
` tag = "${SS_MASS_ASSIGN_TAG}",`,
|
|
1199
1199
|
` field = tostring(k)`,
|
|
1200
1200
|
` })`,
|
|
@@ -1219,7 +1219,7 @@ function buildSqliPreFunctionPlugins(request9, ctx = {}) {
|
|
|
1219
1219
|
checks.push(` if vl:match(${luaStr(SQLI_LUA_PATTERNS[i5])}) then return k, ${i5 + 1} end`);
|
|
1220
1220
|
}
|
|
1221
1221
|
const lua = [
|
|
1222
|
-
`--
|
|
1222
|
+
`-- XSecurity K-4 SQLi heuristic pre-function for endpoint=${ctx.endpoint ?? "?"}`,
|
|
1223
1223
|
`-- Scans JSON body string values for classic SQLi patterns; rejects on hit.`,
|
|
1224
1224
|
`local body = kong.request.get_body()`,
|
|
1225
1225
|
`if type(body) == "table" then`,
|
|
@@ -1235,7 +1235,7 @@ function buildSqliPreFunctionPlugins(request9, ctx = {}) {
|
|
|
1235
1235
|
` if hit_field ~= nil then`,
|
|
1236
1236
|
` kong.log.warn("[${SS_SQLI_TAG}] sqli pattern " .. tostring(hit_rule) .. " in field '" .. tostring(hit_field) .. "'")`,
|
|
1237
1237
|
` return kong.response.exit(403, {`,
|
|
1238
|
-
` message = "
|
|
1238
|
+
` message = "XSecurity: SQLi pattern rejected",`,
|
|
1239
1239
|
` tag = "${SS_SQLI_TAG}",`,
|
|
1240
1240
|
` field = tostring(hit_field),`,
|
|
1241
1241
|
` rule = hit_rule`,
|
|
@@ -1252,7 +1252,7 @@ function buildSqliPreFunctionPlugins(request9, ctx = {}) {
|
|
|
1252
1252
|
function buildDeprecatedEndpointPlugins(policy, ctx = {}) {
|
|
1253
1253
|
if (policy.deprecated !== true) return [];
|
|
1254
1254
|
const bodyFields = [
|
|
1255
|
-
`message = "
|
|
1255
|
+
`message = "XSecurity: endpoint is deprecated"`,
|
|
1256
1256
|
`tag = "${SS_DEPRECATED_TAG}"`
|
|
1257
1257
|
];
|
|
1258
1258
|
if (policy.sunsetDate) bodyFields.push(`sunset = ${luaStr(policy.sunsetDate)}`);
|
|
@@ -1271,7 +1271,7 @@ function buildDeprecatedEndpointPlugins(policy, ctx = {}) {
|
|
|
1271
1271
|
}`);
|
|
1272
1272
|
}
|
|
1273
1273
|
const lua = [
|
|
1274
|
-
`--
|
|
1274
|
+
`-- XSecurity K-5 deprecated-endpoint block for endpoint=${ctx.endpoint ?? "?"}`,
|
|
1275
1275
|
`kong.log.warn("[${SS_DEPRECATED_TAG}] request to deprecated endpoint blocked")`,
|
|
1276
1276
|
`return kong.response.exit(${exitArgs.join(", ")})`
|
|
1277
1277
|
].join("\n");
|
|
@@ -1517,12 +1517,12 @@ var init_plugins = __esm({
|
|
|
1517
1517
|
"hmac-sha384",
|
|
1518
1518
|
"hmac-sha512"
|
|
1519
1519
|
]);
|
|
1520
|
-
SS_BOLA_TAG = "
|
|
1520
|
+
SS_BOLA_TAG = "x-security-rule-bola-403";
|
|
1521
1521
|
SS_BOLA_CACHE_TTL_SECONDS = 60;
|
|
1522
|
-
SS_BOLA_CACHE_DICT = "
|
|
1522
|
+
SS_BOLA_CACHE_DICT = "x_security_bola_cache";
|
|
1523
1523
|
SS_BOLA_CACHE_DICT_SIZE = "10m";
|
|
1524
|
-
SS_SSRF_TAG = "
|
|
1525
|
-
SS_SSRF_PRIVATE_TAG = "
|
|
1524
|
+
SS_SSRF_TAG = "x-security-rule-ssrf-403";
|
|
1525
|
+
SS_SSRF_PRIVATE_TAG = "x-security-rule-ssrf-private-403";
|
|
1526
1526
|
SSRF_PRIVATE_LUA_PATTERNS = [
|
|
1527
1527
|
"^10%.",
|
|
1528
1528
|
"^127%.",
|
|
@@ -1540,8 +1540,8 @@ var init_plugins = __esm({
|
|
|
1540
1540
|
"^%[fd",
|
|
1541
1541
|
"^%[fe80"
|
|
1542
1542
|
];
|
|
1543
|
-
SS_MASS_ASSIGN_TAG = "
|
|
1544
|
-
SS_SQLI_TAG = "
|
|
1543
|
+
SS_MASS_ASSIGN_TAG = "x-security-mass-assign-403";
|
|
1544
|
+
SS_SQLI_TAG = "x-security-sqli-403";
|
|
1545
1545
|
SQLI_LUA_PATTERNS = [
|
|
1546
1546
|
`[' "]%s*or%s+%d+%s*=%s*%d+`,
|
|
1547
1547
|
// ' OR 1=1
|
|
@@ -1562,10 +1562,10 @@ var init_plugins = __esm({
|
|
|
1562
1562
|
"0x%x+"
|
|
1563
1563
|
// hex literal (loose; matched only when combined w/ above in practice)
|
|
1564
1564
|
];
|
|
1565
|
-
SS_DEPRECATED_TAG = "
|
|
1565
|
+
SS_DEPRECATED_TAG = "x-security-deprecated-endpoint-block";
|
|
1566
1566
|
DEFAULT_BURST_MULTIPLIER = 3;
|
|
1567
1567
|
DEFAULT_BURST_MINIMUM_HEADROOM = 20;
|
|
1568
|
-
SS_PER_ROUTE_RATELIMIT_TAG_PREFIX = "
|
|
1568
|
+
SS_PER_ROUTE_RATELIMIT_TAG_PREFIX = "x-security-per-route-ratelimit";
|
|
1569
1569
|
SEMANTIC_TO_JSON = {
|
|
1570
1570
|
string: { type: "string" },
|
|
1571
1571
|
integer: { type: "integer" },
|
|
@@ -1709,7 +1709,7 @@ function buildBolaRule(endpoint, param, method, ruleId, kind, profile) {
|
|
|
1709
1709
|
return m3[1] === param ? "([^/?]+)" : "[^/]+";
|
|
1710
1710
|
}).join("/");
|
|
1711
1711
|
const captureRx = `^(?:/[^/]+)?/${rebuilt}(?:[/?]|$)`;
|
|
1712
|
-
const tag2 = `
|
|
1712
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
1713
1713
|
const term = profile.legalCollections.has("user") ? "" : ' "t:none"';
|
|
1714
1714
|
return [
|
|
1715
1715
|
header2(
|
|
@@ -1717,13 +1717,13 @@ function buildBolaRule(endpoint, param, method, ruleId, kind, profile) {
|
|
|
1717
1717
|
lifted from W13-C identity-conf; emitted because authorization.rules
|
|
1718
1718
|
declares request.params.${param} equals principal.id (BOLA defense).`
|
|
1719
1719
|
),
|
|
1720
|
-
`SecRule REQUEST_METHOD "@streq ${method}" "id:${ruleId},phase:1,deny,status:403,log,auditlog,msg:'
|
|
1720
|
+
`SecRule REQUEST_METHOD "@streq ${method}" "id:${ruleId},phase:1,deny,status:403,log,auditlog,msg:'x-security B1: BOLA-${kind} denied (path-${param} != ${TRUSTED_PRINCIPAL_HEADER})',tag:'${esc(tag2)}',tag:'x-security/b1/bola-${kind}',chain"`,
|
|
1721
1721
|
` SecRule REQUEST_URI "@rx ${escRx(captureRx)}" "capture,chain"`,
|
|
1722
1722
|
` SecRule TX:1 "!@streq %{REQUEST_HEADERS.${TRUSTED_PRINCIPAL_HEADER}}"${term}`
|
|
1723
1723
|
].join("\n");
|
|
1724
1724
|
}
|
|
1725
1725
|
function buildBflaRules(endpoint, role, baseId, profile) {
|
|
1726
|
-
const tag2 = `
|
|
1726
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
1727
1727
|
const term = profile.legalCollections.has("user") ? "" : ' "t:none"';
|
|
1728
1728
|
const escapedPath = endpoint.path.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
1729
1729
|
const withParams = escapedPath.replace(/\\\{[^/]+?\\\}/g, "[^/]+");
|
|
@@ -1737,7 +1737,7 @@ function buildBflaRules(endpoint, role, baseId, profile) {
|
|
|
1737
1737
|
case (a): no authenticated principal. Upstream gate should have
|
|
1738
1738
|
returned 401, but we deny defensively in case the gate is bypassed.`
|
|
1739
1739
|
),
|
|
1740
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${idMissing},phase:1,deny,status:403,log,auditlog,msg:'
|
|
1740
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${idMissing},phase:1,deny,status:403,log,auditlog,msg:'x-security B1: BFLA denied (${role}-only route, no authenticated principal)',tag:'${esc(tag2)}',tag:'x-security/b1/bfla',chain"`,
|
|
1741
1741
|
` SecRule REQUEST_URI "@rx ${escRx(pathRx)}" "chain"`,
|
|
1742
1742
|
` SecRule &REQUEST_HEADERS:${TRUSTED_PRINCIPAL_HEADER} "@eq 0"${term}`
|
|
1743
1743
|
].join("\n"),
|
|
@@ -1746,7 +1746,7 @@ returned 401, but we deny defensively in case the gate is bypassed.`
|
|
|
1746
1746
|
`B1: BFLA \u2014 ${endpoint.method} ${endpoint.path}
|
|
1747
1747
|
case (b): authenticated but ${TRUSTED_PRINCIPAL_HEADER} != '${role}'.`
|
|
1748
1748
|
),
|
|
1749
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${idNonRole},phase:1,deny,status:403,log,auditlog,msg:'
|
|
1749
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${idNonRole},phase:1,deny,status:403,log,auditlog,msg:'x-security B1: BFLA denied (${role}-only route, non-${role} principal)',tag:'${esc(tag2)}',tag:'x-security/b1/bfla',chain"`,
|
|
1750
1750
|
` SecRule REQUEST_URI "@rx ${escRx(pathRx)}" "chain"`,
|
|
1751
1751
|
` SecRule REQUEST_HEADERS:${TRUSTED_PRINCIPAL_HEADER} "!@streq ${role}" "t:none"`
|
|
1752
1752
|
].join("\n")
|
|
@@ -1823,7 +1823,7 @@ function buildCorsRules(endpoint, profile = CORAZA_GO_PROFILE) {
|
|
|
1823
1823
|
if (!cors || !Array.isArray(cors.allowedOrigins) || cors.allowedOrigins.length === 0) {
|
|
1824
1824
|
return [];
|
|
1825
1825
|
}
|
|
1826
|
-
const tag2 = `
|
|
1826
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
1827
1827
|
const pathRx = pathRegex(endpoint.path);
|
|
1828
1828
|
const term = chainTerm(profile);
|
|
1829
1829
|
const slot = endpointHash3(endpoint.method, endpoint.path) % 1e3;
|
|
@@ -1839,7 +1839,7 @@ msg carries the literal substring 'id:339' so scorer attribution
|
|
|
1839
1839
|
(scoring_lib/attribution.py) maps the firing to cors-policy regardless
|
|
1840
1840
|
of audit-log format (libmodsec3 writes [id "339NNN"], not id:339NNN).`
|
|
1841
1841
|
),
|
|
1842
|
-
`SecRule REQUEST_FILENAME "@rx ${pathRx}" "id:${originId},phase:1,deny,status:403,msg:'
|
|
1842
|
+
`SecRule REQUEST_FILENAME "@rx ${pathRx}" "id:${originId},phase:1,deny,status:403,msg:'x-security id:339 CORS origin not allowed',tag:'${esc2(tag2)}',tag:'x-security-cors-policy',chain"`,
|
|
1843
1843
|
` SecRule &REQUEST_HEADERS:Origin "@gt 0" "chain"`,
|
|
1844
1844
|
` SecRule REQUEST_HEADERS:Origin "!@rx ${esc2(originRx)}"${term}`
|
|
1845
1845
|
].join("\n")
|
|
@@ -1854,7 +1854,7 @@ of audit-log format (libmodsec3 writes [id "339NNN"], not id:339NNN).`
|
|
|
1854
1854
|
phase:1 \u2014 on OPTIONS, deny when Access-Control-Request-Method is set
|
|
1855
1855
|
and not in allowedMethods. msg carries 'id:332' for attribution.`
|
|
1856
1856
|
),
|
|
1857
|
-
`SecRule REQUEST_METHOD "@streq OPTIONS" "id:${preflightMethodId},phase:1,deny,status:403,msg:'
|
|
1857
|
+
`SecRule REQUEST_METHOD "@streq OPTIONS" "id:${preflightMethodId},phase:1,deny,status:403,msg:'x-security id:332 CORS preflight method not allowed',tag:'${esc2(tag2)}',tag:'x-security-cors-policy',chain"`,
|
|
1858
1858
|
` SecRule REQUEST_FILENAME "@rx ${pathRx}" "chain"`,
|
|
1859
1859
|
` SecRule REQUEST_HEADERS:Access-Control-Request-Method "!@rx ^(${methodAlt})$"${term}`
|
|
1860
1860
|
].join("\n")
|
|
@@ -1871,7 +1871,7 @@ phase:1 \u2014 on OPTIONS, deny when Access-Control-Request-Headers is
|
|
|
1871
1871
|
anything other than a comma-list of allowedHeaders (case-insensitive).
|
|
1872
1872
|
RE2-safe: no negative lookahead \u2014 match the allow form and negate.`
|
|
1873
1873
|
),
|
|
1874
|
-
`SecRule REQUEST_METHOD "@streq OPTIONS" "id:${preflightHeadersId},phase:1,deny,status:403,msg:'
|
|
1874
|
+
`SecRule REQUEST_METHOD "@streq OPTIONS" "id:${preflightHeadersId},phase:1,deny,status:403,msg:'x-security id:332 CORS preflight header not allowed',tag:'${esc2(tag2)}',tag:'x-security-cors-policy',chain"`,
|
|
1875
1875
|
` SecRule REQUEST_FILENAME "@rx ${pathRx}" "chain"`,
|
|
1876
1876
|
` SecRule REQUEST_HEADERS:Access-Control-Request-Headers "!@rx ${esc2(allowedListRx)}" "t:lowercase${term ? ",t:none" : ""}"`
|
|
1877
1877
|
].join("\n")
|
|
@@ -1885,7 +1885,7 @@ RE2-safe: no negative lookahead \u2014 match the allow form and negate.`
|
|
|
1885
1885
|
`CORS credentials advertisement for ${endpoint.method} ${endpoint.path}
|
|
1886
1886
|
phase:3 \u2014 setenv Access-Control-Allow-Credentials:true for upstream add_header.`
|
|
1887
1887
|
),
|
|
1888
|
-
`SecAction "id:${id},phase:3,pass,nolog,msg:'
|
|
1888
|
+
`SecAction "id:${id},phase:3,pass,nolog,msg:'x-security id:333 CORS credentials true',tag:'${esc2(tag2)}',tag:'x-security-cors-policy',setenv:Access-Control-Allow-Credentials=true"`
|
|
1889
1889
|
].join("\n")
|
|
1890
1890
|
);
|
|
1891
1891
|
}
|
|
@@ -1898,7 +1898,7 @@ phase:3 \u2014 setenv Access-Control-Allow-Credentials:true for upstream add_hea
|
|
|
1898
1898
|
`CORS expose-headers advertisement for ${endpoint.method} ${endpoint.path}
|
|
1899
1899
|
phase:3 \u2014 setenv Access-Control-Expose-Headers:<list> for upstream add_header.`
|
|
1900
1900
|
),
|
|
1901
|
-
`SecAction "id:${id},phase:3,pass,nolog,msg:'
|
|
1901
|
+
`SecAction "id:${id},phase:3,pass,nolog,msg:'x-security id:334 CORS expose-headers',tag:'${esc2(tag2)}',tag:'x-security-cors-policy',setenv:Access-Control-Expose-Headers=${esc2(list2)}"`
|
|
1902
1902
|
].join("\n")
|
|
1903
1903
|
);
|
|
1904
1904
|
}
|
|
@@ -1910,7 +1910,7 @@ phase:3 \u2014 setenv Access-Control-Expose-Headers:<list> for upstream add_head
|
|
|
1910
1910
|
`CORS max-age advertisement for ${endpoint.method} ${endpoint.path}
|
|
1911
1911
|
phase:3 \u2014 setenv Access-Control-Max-Age:<seconds> for upstream add_header.`
|
|
1912
1912
|
),
|
|
1913
|
-
`SecAction "id:${id},phase:3,pass,nolog,msg:'
|
|
1913
|
+
`SecAction "id:${id},phase:3,pass,nolog,msg:'x-security id:335 CORS max-age',tag:'${esc2(tag2)}',tag:'x-security-cors-policy',setenv:Access-Control-Max-Age=${cors.maxAge}"`
|
|
1914
1914
|
].join("\n")
|
|
1915
1915
|
);
|
|
1916
1916
|
}
|
|
@@ -1965,7 +1965,7 @@ function buildOutputSanitizationRules(endpoint, profile = CORAZA_GO_PROFILE, war
|
|
|
1965
1965
|
});
|
|
1966
1966
|
return [];
|
|
1967
1967
|
}
|
|
1968
|
-
const tag2 = `
|
|
1968
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
1969
1969
|
const pathRx = pathRegex(endpoint.path);
|
|
1970
1970
|
const term = chainTerm2(profile);
|
|
1971
1971
|
const rules = [];
|
|
@@ -1980,7 +1980,7 @@ phase:4 \u2014 deny when RESPONSE_BODY contains a stack-frame shape
|
|
|
1980
1980
|
(common across Python / Node / Java / Go runtimes). msg carries 'id:268'
|
|
1981
1981
|
for scorer attribution.`
|
|
1982
1982
|
),
|
|
1983
|
-
`SecRule REQUEST_FILENAME "@rx ${pathRx}" "id:${id},phase:4,deny,status:500,msg:'
|
|
1983
|
+
`SecRule REQUEST_FILENAME "@rx ${pathRx}" "id:${id},phase:4,deny,status:500,msg:'x-security id:268 output sanitization (stack trace leak)',tag:'${esc3(tag2)}',tag:'x-security-output-sanitization',chain"`,
|
|
1984
1984
|
// Match the most common stack-frame markers: `at File.method`,
|
|
1985
1985
|
// `Traceback (most recent call last):`, `\tat com.example.Foo`,
|
|
1986
1986
|
// `Exception in thread`, `goroutine \d+ [running]:`.
|
|
@@ -1998,7 +1998,7 @@ phase:3 \u2014 strip Server / X-Powered-By response headers. ModSec has no
|
|
|
1998
1998
|
native header-rewrite primitive on Coraza-Go (action 'setenv'/'setvar'
|
|
1999
1999
|
do not touch response headers); we deny the response instead, msg carries 'id:268'.`
|
|
2000
2000
|
),
|
|
2001
|
-
`SecRule REQUEST_FILENAME "@rx ${pathRx}" "id:${id},phase:3,deny,status:500,msg:'
|
|
2001
|
+
`SecRule REQUEST_FILENAME "@rx ${pathRx}" "id:${id},phase:3,deny,status:500,msg:'x-security id:268 output sanitization (server-version leak)',tag:'${esc3(tag2)}',tag:'x-security-output-sanitization',chain"`,
|
|
2002
2002
|
` SecRule RESPONSE_HEADERS:Server|RESPONSE_HEADERS:X-Powered-By "@rx ^.+$"${term}`
|
|
2003
2003
|
].join("\n")
|
|
2004
2004
|
);
|
|
@@ -2012,7 +2012,7 @@ do not touch response headers); we deny the response instead, msg carries 'id:26
|
|
|
2012
2012
|
phase:4 \u2014 deny when RESPONSE_BODY exposes raw DB/runtime error keywords
|
|
2013
2013
|
(SQL state, file paths, "syntax error near"). msg carries 'id:268'.`
|
|
2014
2014
|
),
|
|
2015
|
-
`SecRule REQUEST_FILENAME "@rx ${pathRx}" "id:${id},phase:4,deny,status:500,msg:'
|
|
2015
|
+
`SecRule REQUEST_FILENAME "@rx ${pathRx}" "id:${id},phase:4,deny,status:500,msg:'x-security id:268 output sanitization (raw error leak)',tag:'${esc3(tag2)}',tag:'x-security-output-sanitization',chain"`,
|
|
2016
2016
|
` SecRule RESPONSE_BODY "@rx (?i)(?:syntax error near|ORA-\\d+|ER_\\w+|psycopg2\\.|SQLSTATE|\\bENOENT\\b|undefined method|NullPointerException|panic:\\s)"${term}`
|
|
2017
2017
|
].join("\n")
|
|
2018
2018
|
);
|
|
@@ -2035,7 +2035,7 @@ function buildDataExposurePiiRules(endpoint, profile = CORAZA_GO_PROFILE, warnin
|
|
|
2035
2035
|
([name, spec]) => spec?.pii === true || isSensitiveFieldName(name)
|
|
2036
2036
|
);
|
|
2037
2037
|
if (sensitiveFields.length === 0) return [];
|
|
2038
|
-
const tag2 = `
|
|
2038
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
2039
2039
|
const pathRx = pathRegex(endpoint.path);
|
|
2040
2040
|
const term = chainTerm2(profile);
|
|
2041
2041
|
const rules = [];
|
|
@@ -2049,8 +2049,8 @@ function buildDataExposurePiiRules(endpoint, profile = CORAZA_GO_PROFILE, warnin
|
|
|
2049
2049
|
phase:4 \u2014 deny when sensitive-named field appears in RESPONSE_BODY with
|
|
2050
2050
|
a non-empty string value. msg carries 'id:428' for scorer attribution.`
|
|
2051
2051
|
),
|
|
2052
|
-
`SecRule REQUEST_FILENAME "@rx ${pathRx}" "id:${id},phase:4,deny,status:500,msg:'
|
|
2053
|
-
` SecRule RESPONSE_BODY "@rx \\
|
|
2052
|
+
`SecRule REQUEST_FILENAME "@rx ${pathRx}" "id:${id},phase:4,deny,status:500,msg:'x-security id:428 data-exposure: response leaked sensitive field ${esc3(field)}',tag:'${esc3(tag2)}',tag:'x-security-data-exposure',chain"`,
|
|
2053
|
+
` SecRule RESPONSE_BODY "@rx \\x22${esc3(field)}\\x22\\s*:\\s*\\x22[^\\x22]+\\x22"${term}`
|
|
2054
2054
|
].join("\n")
|
|
2055
2055
|
);
|
|
2056
2056
|
}
|
|
@@ -2100,7 +2100,7 @@ function header5(comment) {
|
|
|
2100
2100
|
function buildLifecycleRules(endpoint, _profile = CORAZA_GO_PROFILE) {
|
|
2101
2101
|
const policy = endpoint.policy;
|
|
2102
2102
|
const rules = [];
|
|
2103
|
-
const tag2 = `
|
|
2103
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
2104
2104
|
const slot = endpointHash3(endpoint.method, endpoint.path) % 1e3;
|
|
2105
2105
|
if (policy.deprecated === true) {
|
|
2106
2106
|
const id = DEPRECATED_BASE_ID + slot;
|
|
@@ -2109,9 +2109,9 @@ function buildLifecycleRules(endpoint, _profile = CORAZA_GO_PROFILE) {
|
|
|
2109
2109
|
header5(
|
|
2110
2110
|
`lifecycle: endpoint deprecated for ${endpoint.method} ${endpoint.path}
|
|
2111
2111
|
phase:1 \u2014 deny with HTTP 410 Gone per RFC 8594 deprecation disposition.
|
|
2112
|
-
msg carries 'id:269' substring for scorer attribution (
|
|
2112
|
+
msg carries 'id:269' substring for scorer attribution (x-security-lifecycle-410).`
|
|
2113
2113
|
),
|
|
2114
|
-
`SecAction "id:${id},phase:1,deny,status:410,msg:'
|
|
2114
|
+
`SecAction "id:${id},phase:1,deny,status:410,msg:'x-security id:269 endpoint deprecated',tag:'${esc4(tag2)}',tag:'x-security-lifecycle-410'"`
|
|
2115
2115
|
].join("\n")
|
|
2116
2116
|
);
|
|
2117
2117
|
}
|
|
@@ -2122,9 +2122,9 @@ msg carries 'id:269' substring for scorer attribution (writ-lifecycle-410).`
|
|
|
2122
2122
|
header5(
|
|
2123
2123
|
`lifecycle: Sunset header for ${endpoint.method} ${endpoint.path}
|
|
2124
2124
|
phase:3 \u2014 setenv:Sunset=<iso> so the upstream proxy can add_header it.
|
|
2125
|
-
msg carries 'id:270' substring (
|
|
2125
|
+
msg carries 'id:270' substring (x-security-lifecycle-sunset).`
|
|
2126
2126
|
),
|
|
2127
|
-
`SecAction "id:${id},phase:3,pass,nolog,msg:'
|
|
2127
|
+
`SecAction "id:${id},phase:3,pass,nolog,msg:'x-security id:270 sunset header',tag:'${esc4(tag2)}',tag:'x-security-lifecycle-sunset',setenv:Sunset=${esc4(policy.sunsetDate)}"`
|
|
2128
2128
|
].join("\n")
|
|
2129
2129
|
);
|
|
2130
2130
|
}
|
|
@@ -2136,9 +2136,9 @@ msg carries 'id:270' substring (writ-lifecycle-sunset).`
|
|
|
2136
2136
|
header5(
|
|
2137
2137
|
`lifecycle: Link successor-version for ${endpoint.method} ${endpoint.path}
|
|
2138
2138
|
phase:3 \u2014 setenv:Link=<replacement>; rel="successor-version" per RFC 8594.
|
|
2139
|
-
msg carries 'id:271' substring (
|
|
2139
|
+
msg carries 'id:271' substring (x-security-lifecycle-replacement).`
|
|
2140
2140
|
),
|
|
2141
|
-
`SecAction "id:${id},phase:3,pass,nolog,msg:'
|
|
2141
|
+
`SecAction "id:${id},phase:3,pass,nolog,msg:'x-security id:271 replacement endpoint',tag:'${esc4(tag2)}',tag:'x-security-lifecycle-replacement',setenv:Link=${esc4(linkValue)}"`
|
|
2142
2142
|
].join("\n")
|
|
2143
2143
|
);
|
|
2144
2144
|
}
|
|
@@ -2174,7 +2174,7 @@ function buildCsrfRules(endpoint, profile = CORAZA_GO_PROFILE) {
|
|
|
2174
2174
|
const csrf = endpoint.policy.csrf;
|
|
2175
2175
|
if (!csrf) return [];
|
|
2176
2176
|
if (!STATE_CHANGING.has(endpoint.method.toUpperCase())) return [];
|
|
2177
|
-
const tag2 = `
|
|
2177
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
2178
2178
|
const pathRx = pathRegex(endpoint.path);
|
|
2179
2179
|
const term = chainTerm3(profile);
|
|
2180
2180
|
const slot = endpointHash3(endpoint.method, endpoint.path) % 1e3;
|
|
@@ -2189,9 +2189,9 @@ function buildCsrfRules(endpoint, profile = CORAZA_GO_PROFILE) {
|
|
|
2189
2189
|
header6(
|
|
2190
2190
|
`csrf: origin-check for ${endpoint.method} ${endpoint.path}
|
|
2191
2191
|
phase:1 \u2014 deny when Origin missing or not in allowedOrigins.
|
|
2192
|
-
msg carries 'id:272' substring (
|
|
2192
|
+
msg carries 'id:272' substring (x-security-csrf).`
|
|
2193
2193
|
),
|
|
2194
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:1,deny,status:403,msg:'
|
|
2194
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:1,deny,status:403,msg:'x-security id:272 CSRF origin not allowed',tag:'${esc5(tag2)}',tag:'x-security-csrf',chain"`,
|
|
2195
2195
|
` SecRule REQUEST_FILENAME "@rx ${pathRx}" "chain"`,
|
|
2196
2196
|
` SecRule REQUEST_HEADERS:Origin "!@rx ${esc5(originRx)}"${term}`
|
|
2197
2197
|
].join("\n")
|
|
@@ -2202,14 +2202,14 @@ msg carries 'id:272' substring (writ-csrf).`
|
|
|
2202
2202
|
if (!cookieName || !headerName) return [];
|
|
2203
2203
|
const captureId = CSRF_BASE_ID + 1e3 + slot;
|
|
2204
2204
|
const denyId = CSRF_BASE_ID + 2e3 + slot;
|
|
2205
|
-
const txVar = `
|
|
2205
|
+
const txVar = `x_security_csrf_${slot}`;
|
|
2206
2206
|
rules.push(
|
|
2207
2207
|
[
|
|
2208
2208
|
header6(
|
|
2209
2209
|
`csrf: double-submit capture for ${endpoint.method} ${endpoint.path}
|
|
2210
2210
|
phase:1 \u2014 capture cookie '${esc5(cookieName)}' into TX:${txVar} for the equality check.`
|
|
2211
2211
|
),
|
|
2212
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${captureId},phase:1,pass,nolog,tag:'${esc5(tag2)}',tag:'
|
|
2212
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${captureId},phase:1,pass,nolog,tag:'${esc5(tag2)}',tag:'x-security-csrf',chain"`,
|
|
2213
2213
|
` SecRule REQUEST_FILENAME "@rx ${pathRx}" "chain"`,
|
|
2214
2214
|
` SecRule REQUEST_COOKIES:${cookieName} "@rx .+" "capture,setvar:tx.${txVar}=%{MATCHED_VAR}${term ? ",t:none" : ""}"`
|
|
2215
2215
|
].join("\n")
|
|
@@ -2219,9 +2219,9 @@ phase:1 \u2014 capture cookie '${esc5(cookieName)}' into TX:${txVar} for the equ
|
|
|
2219
2219
|
header6(
|
|
2220
2220
|
`csrf: double-submit verify for ${endpoint.method} ${endpoint.path}
|
|
2221
2221
|
phase:1 \u2014 deny when header '${esc5(headerName)}' missing OR != TX:${txVar}.
|
|
2222
|
-
msg carries 'id:272' substring (
|
|
2222
|
+
msg carries 'id:272' substring (x-security-csrf).`
|
|
2223
2223
|
),
|
|
2224
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${denyId},phase:1,deny,status:403,msg:'
|
|
2224
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${denyId},phase:1,deny,status:403,msg:'x-security id:272 CSRF token mismatch',tag:'${esc5(tag2)}',tag:'x-security-csrf',chain"`,
|
|
2225
2225
|
` SecRule REQUEST_FILENAME "@rx ${pathRx}" "chain"`,
|
|
2226
2226
|
` SecRule REQUEST_HEADERS:${headerName} "!@streq %{TX.${txVar}}"${term}`
|
|
2227
2227
|
].join("\n")
|
|
@@ -2235,9 +2235,9 @@ msg carries 'id:272' substring (writ-csrf).`
|
|
|
2235
2235
|
header6(
|
|
2236
2236
|
`csrf: custom-header for ${endpoint.method} ${endpoint.path}
|
|
2237
2237
|
phase:1 \u2014 deny when '${esc5(headerName)}' header missing/empty.
|
|
2238
|
-
msg carries 'id:272' substring (
|
|
2238
|
+
msg carries 'id:272' substring (x-security-csrf).`
|
|
2239
2239
|
),
|
|
2240
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:1,deny,status:403,msg:'
|
|
2240
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:1,deny,status:403,msg:'x-security id:272 CSRF custom header missing',tag:'${esc5(tag2)}',tag:'x-security-csrf',chain"`,
|
|
2241
2241
|
` SecRule REQUEST_FILENAME "@rx ${pathRx}" "chain"`,
|
|
2242
2242
|
` SecRule &REQUEST_HEADERS:${headerName} "@eq 0"${term}`
|
|
2243
2243
|
].join("\n")
|
|
@@ -2270,7 +2270,7 @@ function buildDuplicateParamRules(endpoint, profile = CORAZA_GO_PROFILE) {
|
|
|
2270
2270
|
const req = endpoint.policy.request;
|
|
2271
2271
|
if (!req || req.duplicateParamPolicy !== "reject") return [];
|
|
2272
2272
|
if (!req.schema) return [];
|
|
2273
|
-
const tag2 = `
|
|
2273
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
2274
2274
|
const pathRx = pathRegex(endpoint.path);
|
|
2275
2275
|
const term = chainTerm4(profile);
|
|
2276
2276
|
const rules = [];
|
|
@@ -2283,9 +2283,9 @@ function buildDuplicateParamRules(endpoint, profile = CORAZA_GO_PROFILE) {
|
|
|
2283
2283
|
header7(
|
|
2284
2284
|
`request.duplicateParamPolicy=reject for ${endpoint.method} ${endpoint.path} field=${field}
|
|
2285
2285
|
phase:2 \u2014 deny when ARGS:${field} appears more than once (HPP defense).
|
|
2286
|
-
msg carries 'id:275' substring (
|
|
2286
|
+
msg carries 'id:275' substring (x-security-hpp-reject).`
|
|
2287
2287
|
),
|
|
2288
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:400,msg:'
|
|
2288
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:400,msg:'x-security id:275 duplicate parameter ${esc6(field)}',tag:'${esc6(tag2)}',tag:'x-security-hpp-reject',chain"`,
|
|
2289
2289
|
` SecRule REQUEST_FILENAME "@rx ${pathRx}" "chain"`,
|
|
2290
2290
|
` SecRule &ARGS:${field} "@gt 1"${term}`
|
|
2291
2291
|
].join("\n")
|
|
@@ -2316,7 +2316,7 @@ function chainTerm5(profile) {
|
|
|
2316
2316
|
function buildResponseContentTypeRules(endpoint, profile = CORAZA_GO_PROFILE) {
|
|
2317
2317
|
const resp = endpoint.policy.response;
|
|
2318
2318
|
if (!resp || !Array.isArray(resp.contentType) || resp.contentType.length === 0) return [];
|
|
2319
|
-
const tag2 = `
|
|
2319
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
2320
2320
|
const pathRx = pathRegex(endpoint.path);
|
|
2321
2321
|
const term = chainTerm5(profile);
|
|
2322
2322
|
const slot = endpointHash3(endpoint.method, endpoint.path) % 1e3;
|
|
@@ -2328,9 +2328,9 @@ function buildResponseContentTypeRules(endpoint, profile = CORAZA_GO_PROFILE) {
|
|
|
2328
2328
|
header8(
|
|
2329
2329
|
`response.contentType allowlist for ${endpoint.method} ${endpoint.path}
|
|
2330
2330
|
phase:3 \u2014 deny when RESPONSE_HEADERS:Content-Type not in allowlist.
|
|
2331
|
-
msg carries 'id:276' substring (
|
|
2331
|
+
msg carries 'id:276' substring (x-security-response-ct).`
|
|
2332
2332
|
),
|
|
2333
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:3,deny,status:500,msg:'
|
|
2333
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:3,deny,status:500,msg:'x-security id:276 response content-type not allowed',tag:'${esc7(tag2)}',tag:'x-security-response-ct',chain"`,
|
|
2334
2334
|
` SecRule REQUEST_FILENAME "@rx ${pathRx}" "chain"`,
|
|
2335
2335
|
` SecRule RESPONSE_HEADERS:Content-Type "!@rx ${esc7(allowRx)}"${term}`
|
|
2336
2336
|
].join("\n")
|
|
@@ -2422,10 +2422,10 @@ PARTIAL \u2014 edge serialization only: a crude same-key cap in a 1s window,
|
|
|
2422
2422
|
NOT an in-handler mutex. Does NOT provide transaction atomicity.`
|
|
2423
2423
|
),
|
|
2424
2424
|
// 1. open + increment the same-key counter for a 1s edge window.
|
|
2425
|
-
`SecRule REQUEST_URI "@rx ${pathRx}" "id:${initId},phase:1,pass,nolog,tag:'
|
|
2425
|
+
`SecRule REQUEST_URI "@rx ${pathRx}" "id:${initId},phase:1,pass,nolog,tag:'x-security-serialize',initcol:${initcolArg},setvar:${col}.${varName}=+1,expirevar:${col}.${varName}=1,chain"`,
|
|
2426
2426
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}"${term}`,
|
|
2427
2427
|
// 2. deny when the same-key in-window count exceeds the concurrency cap.
|
|
2428
|
-
`SecRule REQUEST_URI "@rx ${pathRx}" "id:${checkId},phase:1,deny,status:429,msg:'
|
|
2428
|
+
`SecRule REQUEST_URI "@rx ${pathRx}" "id:${checkId},phase:1,deny,status:429,msg:'x-security: serializeBy concurrency cap (${limit}) exceeded (edge serialization, not in-handler atomicity)',tag:'x-security-serialize',log,chain"`,
|
|
2429
2429
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}" "chain"`,
|
|
2430
2430
|
` SecRule ${col.toUpperCase()}:${varName} "@gt ${limit}"${term}`
|
|
2431
2431
|
].join("\n")
|
|
@@ -2452,7 +2452,7 @@ introspection meta-fields __schema / __type. A WAF cannot fully
|
|
|
2452
2452
|
validate GraphQL semantics, but the introspection token check is
|
|
2453
2453
|
genuinely enforcing at the byte level.`
|
|
2454
2454
|
),
|
|
2455
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:403,msg:'
|
|
2455
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:403,msg:'x-security: GraphQL introspection disabled',tag:'x-security-graphql',chain"`,
|
|
2456
2456
|
` SecRule REQUEST_URI "@rx ${pathRx}" "chain"`,
|
|
2457
2457
|
` SecRule REQUEST_BODY|ARGS "@rx (?i)__(?:schema|type)\\b"${term}`
|
|
2458
2458
|
].join("\n")
|
|
@@ -2468,7 +2468,7 @@ genuinely enforcing at the byte level.`
|
|
|
2468
2468
|
PARTIAL \u2014 crude non-parsing alias-token count (heuristic): deny when
|
|
2469
2469
|
>${g5.maxAliases} alias-shaped '<ident>:' tokens appear in the body.`
|
|
2470
2470
|
),
|
|
2471
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:403,msg:'
|
|
2471
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:403,msg:'x-security: GraphQL alias cap (${g5.maxAliases}) exceeded',tag:'x-security-graphql',chain"`,
|
|
2472
2472
|
` SecRule REQUEST_URI "@rx ${pathRx}" "chain"`,
|
|
2473
2473
|
` SecRule REQUEST_BODY "@rx (?s)(?:[A-Za-z_][A-Za-z0-9_]*\\s*:\\s*[A-Za-z_][A-Za-z0-9_]*[^:]*){${min},}"${term}`
|
|
2474
2474
|
].join("\n")
|
|
@@ -2491,9 +2491,9 @@ PARTIAL \u2014 crude non-parsing alias-token count (heuristic): deny when
|
|
|
2491
2491
|
PARTIAL \u2014 crude non-parsing batch count: deny when >${g5.batchLimit}
|
|
2492
2492
|
'"query"' members appear (batched array of operations).`
|
|
2493
2493
|
),
|
|
2494
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:403,msg:'
|
|
2494
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:403,msg:'x-security: GraphQL batch cap (${g5.batchLimit}) exceeded',tag:'x-security-graphql',chain"`,
|
|
2495
2495
|
` SecRule REQUEST_URI "@rx ${pathRx}" "chain"`,
|
|
2496
|
-
` SecRule REQUEST_BODY "@rx (?s)(?:\\
|
|
2496
|
+
` SecRule REQUEST_BODY "@rx (?s)(?:\\x22query\\x22\\s*:[^:]*){${min},}"${term}`
|
|
2497
2497
|
].join("\n")
|
|
2498
2498
|
);
|
|
2499
2499
|
warnings?.push({
|
|
@@ -2536,7 +2536,7 @@ function buildResidualScaffolding(endpoint) {
|
|
|
2536
2536
|
OVERRIDE-ONLY \u2014 SCAFFOLDING, NOT ENFORCED.
|
|
2537
2537
|
Per-resolver authorization requires an operator-supplied GraphQL-aware
|
|
2538
2538
|
processor: a WAF cannot parse the query, bind the resolved object to an
|
|
2539
|
-
identity claim, and evaluate ownership/role per operation.
|
|
2539
|
+
identity claim, and evaluate ownership/role per operation. x-security
|
|
2540
2540
|
emits NO enforcing SecRule for these. Operator contract \u2014 wire each
|
|
2541
2541
|
operation's authz in your GraphQL gateway / resolver middleware:
|
|
2542
2542
|
` + named
|
|
@@ -2588,7 +2588,7 @@ function supportsStatefulNamedCollection(profile) {
|
|
|
2588
2588
|
function buildPasswordPolicyRules(endpoint, profile = CORAZA_GO_PROFILE) {
|
|
2589
2589
|
const pol = endpoint.policy.authentication?.passwordPolicy;
|
|
2590
2590
|
if (!pol) return [];
|
|
2591
|
-
const tag2 = `
|
|
2591
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
2592
2592
|
const rx = pathRegex(endpoint.path);
|
|
2593
2593
|
const seedBase = endpointHash3(endpoint.method, `${endpoint.path}|pwpolicy`);
|
|
2594
2594
|
const term = chainTerm7(profile);
|
|
@@ -2616,7 +2616,7 @@ function buildPasswordPolicyRules(endpoint, profile = CORAZA_GO_PROFILE) {
|
|
|
2616
2616
|
Body-carried password strength (API2:2023). PCRE !@rx on the password
|
|
2617
2617
|
field \u2014 rejects a present-but-weak password. FULL (plain @rx).`
|
|
2618
2618
|
),
|
|
2619
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:422,log,msg:'
|
|
2619
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:422,log,msg:'x-security: ${esc9(c5.reason)}',tag:'${esc9(tag2)}',tag:'x-security-rule-password-policy',chain"`,
|
|
2620
2620
|
` SecRule REQUEST_FILENAME "@rx ${escRx2(rx)}" "chain"`,
|
|
2621
2621
|
` SecRule ${selector} "!@rx ${escRx2(c5.rx)}"${term}`
|
|
2622
2622
|
].join("\n")
|
|
@@ -2628,7 +2628,7 @@ field \u2014 rejects a present-but-weak password. FULL (plain @rx).`
|
|
|
2628
2628
|
out.push(
|
|
2629
2629
|
[
|
|
2630
2630
|
header10(`v0.7 passwordPolicy: blocklisted password for ${endpoint.method} ${endpoint.path}`),
|
|
2631
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:422,log,msg:'
|
|
2631
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:422,log,msg:'x-security: password is on the blocklist',tag:'${esc9(tag2)}',tag:'x-security-rule-password-policy',chain"`,
|
|
2632
2632
|
` SecRule REQUEST_FILENAME "@rx ${escRx2(rx)}" "chain"`,
|
|
2633
2633
|
` SecRule ${selector} "@rx ${escRx2(`(?i)^(?:${alt})$`)}"${term}`
|
|
2634
2634
|
].join("\n")
|
|
@@ -2658,7 +2658,7 @@ function buildAccountLockoutRules(endpoint, profile = CORAZA_GO_PROFILE, warning
|
|
|
2658
2658
|
});
|
|
2659
2659
|
return [];
|
|
2660
2660
|
}
|
|
2661
|
-
const tag2 = `
|
|
2661
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
2662
2662
|
const rx = pathRegex(endpoint.path);
|
|
2663
2663
|
const window2 = parseDurationSec(lock.window) || 900;
|
|
2664
2664
|
const { expansion, keyPhase } = lockoutVar(lock.identifier);
|
|
@@ -2679,7 +2679,7 @@ populated); increment at phase:5 on a >=400 auth response. FULL on ${profile.nam
|
|
|
2679
2679
|
`SecRule REQUEST_FILENAME "@rx ${escRx2(rx)}" "id:${base},phase:${keyPhase},pass,nolog,tag:'${esc9(tag2)}',${initcol},chain"`,
|
|
2680
2680
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}"${term}`,
|
|
2681
2681
|
// 2. Deny while locked out — counter already over budget.
|
|
2682
|
-
`SecRule REQUEST_FILENAME "@rx ${escRx2(rx)}" "id:${base + 1},phase:${keyPhase},deny,status:429,log,msg:'
|
|
2682
|
+
`SecRule REQUEST_FILENAME "@rx ${escRx2(rx)}" "id:${base + 1},phase:${keyPhase},deny,status:429,log,msg:'x-security: account locked (>${lock.attempts} failed logins / ${esc9(lock.window)})',tag:'${esc9(tag2)}',tag:'x-security-rule-account-lockout',chain"`,
|
|
2683
2683
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}" "chain"`,
|
|
2684
2684
|
` SecRule GLOBAL:${counterKey} "@gt ${lock.attempts}"${term}`,
|
|
2685
2685
|
// 3. On a failed-auth response (>=400), increment + refresh the TTL (phase:5).
|
|
@@ -2702,7 +2702,7 @@ function buildForbidArrayRootRules(endpoint, profile = CORAZA_GO_PROFILE, warnin
|
|
|
2702
2702
|
});
|
|
2703
2703
|
return [];
|
|
2704
2704
|
}
|
|
2705
|
-
const tag2 = `
|
|
2705
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
2706
2706
|
const rx = pathRegex(endpoint.path);
|
|
2707
2707
|
const seed = endpointHash3(endpoint.method, `${endpoint.path}|arrayroot`);
|
|
2708
2708
|
const id = FORBID_ARRAY_ROOT_BASE_ID + seed % 999;
|
|
@@ -2715,7 +2715,7 @@ JSON-hijacking defense (API3:2023). phase:4 RESPONSE_BODY @rx on the first
|
|
|
2715
2715
|
non-whitespace byte; an array-rooted body is denied (wrap in an object instead).
|
|
2716
2716
|
FULL on ${profile.name} (supportsResponseBodyAccess).`
|
|
2717
2717
|
),
|
|
2718
|
-
`SecRule REQUEST_FILENAME "@rx ${escRx2(rx)}" "id:${id},phase:4,deny,status:500,log,auditlog,msg:'
|
|
2718
|
+
`SecRule REQUEST_FILENAME "@rx ${escRx2(rx)}" "id:${id},phase:4,deny,status:500,log,auditlog,msg:'x-security: bare top-level JSON array response (forbidArrayRoot)',tag:'${esc9(tag2)}',tag:'x-security-rule-forbid-array-root',chain"`,
|
|
2719
2719
|
` SecRule RESPONSE_BODY "@rx ^[\\s\\xef\\xbb\\xbf]*\\["${term}`
|
|
2720
2720
|
].join("\n")
|
|
2721
2721
|
];
|
|
@@ -2724,7 +2724,7 @@ function buildIdempotencyKeyRules(endpoint, profile = CORAZA_GO_PROFILE, warning
|
|
|
2724
2724
|
const idem = endpoint.policy.request?.idempotencyKey;
|
|
2725
2725
|
if (!idem) return [];
|
|
2726
2726
|
const epId = `${endpoint.method} ${endpoint.path}`;
|
|
2727
|
-
const tag2 = `
|
|
2727
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
2728
2728
|
const rx = pathRegex(endpoint.path);
|
|
2729
2729
|
const ttl = parseDurationSec(idem.ttl) || 300;
|
|
2730
2730
|
const seed = endpointHash3(endpoint.method, `${endpoint.path}|idempotency`);
|
|
@@ -2739,7 +2739,7 @@ function buildIdempotencyKeyRules(endpoint, profile = CORAZA_GO_PROFILE, warning
|
|
|
2739
2739
|
`v0.7 idempotencyKey: require header ${idem.header} for ${endpoint.method} ${endpoint.path}
|
|
2740
2740
|
(API6:2023). Header-presence check \u2014 FULL on ${profile.name}.`
|
|
2741
2741
|
),
|
|
2742
|
-
`SecRule REQUEST_FILENAME "@rx ${escRx2(rx)}" "id:${base},phase:1,deny,status:400,log,msg:'
|
|
2742
|
+
`SecRule REQUEST_FILENAME "@rx ${escRx2(rx)}" "id:${base},phase:1,deny,status:400,log,msg:'x-security: missing ${esc9(idem.header)} header',tag:'${esc9(tag2)}',tag:'x-security-rule-idempotency-key',chain"`,
|
|
2743
2743
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}" "chain"`,
|
|
2744
2744
|
` SecRule &REQUEST_HEADERS:${esc9(idem.header)} "@eq 0"${term}`
|
|
2745
2745
|
].join("\n")
|
|
@@ -2771,7 +2771,7 @@ flight races (no atomic check-and-set at the WAF; handle that in the store).`
|
|
|
2771
2771
|
`SecRule REQUEST_FILENAME "@rx ${escRx2(rx)}" "id:${base + 2},phase:1,pass,nolog,tag:'${esc9(tag2)}',setvar:global.${counterKey}=+1,expirevar:global.${counterKey}=${ttl},chain"`,
|
|
2772
2772
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}"${term}`,
|
|
2773
2773
|
// Deny the replay — second+ request with this key inside the ttl window.
|
|
2774
|
-
`SecRule REQUEST_FILENAME "@rx ${escRx2(rx)}" "id:${base + 3},phase:1,deny,status:409,log,msg:'
|
|
2774
|
+
`SecRule REQUEST_FILENAME "@rx ${escRx2(rx)}" "id:${base + 3},phase:1,deny,status:409,log,msg:'x-security: replayed idempotency key (${esc9(idem.header)})',tag:'${esc9(tag2)}',tag:'x-security-rule-idempotency-key',chain"`,
|
|
2775
2775
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}" "chain"`,
|
|
2776
2776
|
` SecRule GLOBAL:${counterKey} "@gt 1"${term}`
|
|
2777
2777
|
].join("\n")
|
|
@@ -2781,7 +2781,7 @@ flight races (no atomic check-and-set at the WAF; handle that in the store).`
|
|
|
2781
2781
|
function buildLoggingRules(endpoint, profile = CORAZA_GO_PROFILE) {
|
|
2782
2782
|
const log = endpoint.policy.logging;
|
|
2783
2783
|
if (!log || !Array.isArray(log.events) || log.events.length === 0) return [];
|
|
2784
|
-
const tag2 = `
|
|
2784
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
2785
2785
|
const rx = pathRegex(endpoint.path);
|
|
2786
2786
|
const seed = endpointHash3(endpoint.method, `${endpoint.path}|logging`);
|
|
2787
2787
|
const id = LOGGING_BASE_ID + seed % 999;
|
|
@@ -2791,7 +2791,7 @@ function buildLoggingRules(endpoint, profile = CORAZA_GO_PROFILE) {
|
|
|
2791
2791
|
const notes = [];
|
|
2792
2792
|
notes.push(`events declared: ${log.events.join(", ")}`);
|
|
2793
2793
|
notes.push(`auth-failure / authz-deny / injection-block / rate-limit-trip are`);
|
|
2794
|
-
notes.push(`already audit-logged by the corresponding
|
|
2794
|
+
notes.push(`already audit-logged by the corresponding x-security deny rules.`);
|
|
2795
2795
|
if (log.sink && log.sink !== "stdout") {
|
|
2796
2796
|
notes.push(`sink='${log.sink}' is NOT enforced at the WAF \u2014 configure it at the`);
|
|
2797
2797
|
notes.push(`log-shipping layer (SecAuditLogStorageDir + syslog/fluent-bit sidecar).`);
|
|
@@ -2813,7 +2813,7 @@ function buildLoggingRules(endpoint, profile = CORAZA_GO_PROFILE) {
|
|
|
2813
2813
|
`v0.7 logging (SSEC-AUDIT): force audit log for ${endpoint.method} ${endpoint.path}
|
|
2814
2814
|
` + notes.join("\n")
|
|
2815
2815
|
),
|
|
2816
|
-
`SecRule REQUEST_FILENAME "@rx ${escRx2(rx)}" "id:${id},phase:5,pass,log,auditlog,msg:'
|
|
2816
|
+
`SecRule REQUEST_FILENAME "@rx ${escRx2(rx)}" "id:${id},phase:5,pass,log,auditlog,msg:'x-security: audit (request/response) for ${esc9(endpoint.method)} ${esc9(endpoint.path)}',tag:'${esc9(tag2)}',tag:'x-security-audit'${partsCtl},chain"`,
|
|
2817
2817
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}"${term}`
|
|
2818
2818
|
].join("\n")
|
|
2819
2819
|
);
|
|
@@ -2907,7 +2907,7 @@ function buildContentType(ctx, allowed, profile = CORAZA_GO_PROFILE) {
|
|
|
2907
2907
|
const term = chainTerm8(profile);
|
|
2908
2908
|
return [
|
|
2909
2909
|
header11(`request.contentType allowlist for ${endpoint.method} ${endpoint.path}`),
|
|
2910
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:1,deny,status:415,msg:'
|
|
2910
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:1,deny,status:415,msg:'x-security: unsupported Content-Type',tag:'${esc10(tag2)}',chain"`,
|
|
2911
2911
|
` SecRule REQUEST_URI "@rx ${pathRegex(endpoint.path)}" "chain"`,
|
|
2912
2912
|
` SecRule REQUEST_HEADERS:Content-Type "!@rx ^(${alt})(;.*)?$"${term}`
|
|
2913
2913
|
].join("\n");
|
|
@@ -2918,7 +2918,7 @@ function buildBodySize(ctx, bytes, profile = CORAZA_GO_PROFILE) {
|
|
|
2918
2918
|
const term = chainTerm8(profile);
|
|
2919
2919
|
return [
|
|
2920
2920
|
header11(`request.maxBodySize=${bytes} bytes for ${endpoint.method} ${endpoint.path}`),
|
|
2921
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:1,deny,status:413,msg:'
|
|
2921
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:1,deny,status:413,msg:'x-security: request body exceeds endpoint limit',tag:'${esc10(tag2)}',chain"`,
|
|
2922
2922
|
` SecRule REQUEST_URI "@rx ${pathRegex(endpoint.path)}" "chain"`,
|
|
2923
2923
|
` SecRule REQUEST_HEADERS:Content-Length "@gt ${bytes}"${term}`
|
|
2924
2924
|
].join("\n");
|
|
@@ -2930,7 +2930,7 @@ function buildAuth(ctx, headerName, profile = CORAZA_GO_PROFILE) {
|
|
|
2930
2930
|
const term = chainTerm8(profile);
|
|
2931
2931
|
return [
|
|
2932
2932
|
header11(`authentication required (${hdr}) for ${endpoint.method} ${endpoint.path}`),
|
|
2933
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:1,deny,status:401,msg:'
|
|
2933
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:1,deny,status:401,msg:'x-security: missing ${esc10(hdr)} header',tag:'${esc10(tag2)}',chain"`,
|
|
2934
2934
|
` SecRule REQUEST_URI "@rx ${pathRegex(endpoint.path)}" "chain"`,
|
|
2935
2935
|
` SecRule &REQUEST_HEADERS:${hdr} "@eq 0"${term}`
|
|
2936
2936
|
].join("\n");
|
|
@@ -2946,7 +2946,7 @@ function buildIpPolicy(ctx, ipPolicy, profile = CORAZA_GO_PROFILE) {
|
|
|
2946
2946
|
out.push(
|
|
2947
2947
|
[
|
|
2948
2948
|
header11(`ipPolicy.allow for ${endpoint.method} ${endpoint.path}`),
|
|
2949
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:1,deny,status:403,msg:'
|
|
2949
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:1,deny,status:403,msg:'x-security: source IP not in allowlist',tag:'${esc10(tag2)}',chain"`,
|
|
2950
2950
|
` SecRule REQUEST_URI "@rx ${pathRegex(endpoint.path)}" "chain"`,
|
|
2951
2951
|
` SecRule REMOTE_ADDR "!@ipMatch ${allow.join(",")}"${term}`
|
|
2952
2952
|
].join("\n")
|
|
@@ -2957,7 +2957,7 @@ function buildIpPolicy(ctx, ipPolicy, profile = CORAZA_GO_PROFILE) {
|
|
|
2957
2957
|
out.push(
|
|
2958
2958
|
[
|
|
2959
2959
|
header11(`ipPolicy.deny for ${endpoint.method} ${endpoint.path}`),
|
|
2960
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:1,deny,status:403,msg:'
|
|
2960
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:1,deny,status:403,msg:'x-security: source IP in denylist',tag:'${esc10(tag2)}',chain"`,
|
|
2961
2961
|
` SecRule REQUEST_URI "@rx ${pathRegex(endpoint.path)}" "chain"`,
|
|
2962
2962
|
` SecRule REMOTE_ADDR "@ipMatch ${deny.join(",")}"${term}`
|
|
2963
2963
|
].join("\n")
|
|
@@ -3085,7 +3085,7 @@ pattern: initcol \u2192 setvar/expirevar \u2192 @gt check (Coraza v3 collection
|
|
|
3085
3085
|
`SecRule REQUEST_URI "@rx ${pathRx}" "id:${counterId},phase:1,pass,nolog,tag:'${esc10(tag2)}',setvar:${col}.${counterKey}=+1,expirevar:${col}.${counterKey}=${window2},chain"`,
|
|
3086
3086
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}"${chainTerm9}`,
|
|
3087
3087
|
// 3. check — deny when counter exceeds the configured limit.
|
|
3088
|
-
`SecRule REQUEST_URI "@rx ${pathRx}" "id:${checkId},phase:1,deny,status:429,msg:'
|
|
3088
|
+
`SecRule REQUEST_URI "@rx ${pathRx}" "id:${checkId},phase:1,deny,status:429,msg:'x-security: rate limit exceeded (${rl.requests}/${esc10(rl.window)})',tag:'${esc10(tag2)}',log,chain"`,
|
|
3089
3089
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}" "chain"`,
|
|
3090
3090
|
` SecRule ${col.toUpperCase()}:${counterKey} "@gt ${rl.requests}"${chainTerm9}`
|
|
3091
3091
|
];
|
|
@@ -3102,7 +3102,7 @@ pattern: initcol \u2192 setvar/expirevar \u2192 @gt check (Coraza v3 collection
|
|
|
3102
3102
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}"${chainTerm9}`,
|
|
3103
3103
|
`SecRule REQUEST_URI "@rx ${pathRx}" "id:${burstCounterId},phase:1,pass,nolog,tag:'${esc10(tag2)}',setvar:${col}.${burstCounterKey}=+1,expirevar:${col}.${burstCounterKey}=1,chain"`,
|
|
3104
3104
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}"${chainTerm9}`,
|
|
3105
|
-
`SecRule REQUEST_URI "@rx ${pathRx}" "id:${burstCheckId},phase:1,deny,status:429,msg:'
|
|
3105
|
+
`SecRule REQUEST_URI "@rx ${pathRx}" "id:${burstCheckId},phase:1,deny,status:429,msg:'x-security: rate limit burst exceeded (${rl.burst}/1s)',tag:'${esc10(tag2)}',log,chain"`,
|
|
3106
3106
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}" "chain"`,
|
|
3107
3107
|
` SecRule ${col.toUpperCase()}:${burstCounterKey} "@gt ${rl.burst}"${chainTerm9}`
|
|
3108
3108
|
);
|
|
@@ -3120,7 +3120,7 @@ function buildSchemaRules(ctx, schema, profile = CORAZA_GO_PROFILE) {
|
|
|
3120
3120
|
rules.push(
|
|
3121
3121
|
[
|
|
3122
3122
|
header11(`request.schema.${field}.minLength=${ps.minLength} for ${endpoint.method} ${endpoint.path}`),
|
|
3123
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:400,msg:'
|
|
3123
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:400,msg:'x-security: field ${esc10(field)} too short',tag:'${esc10(tag2)}',chain"`,
|
|
3124
3124
|
` SecRule REQUEST_URI "@rx ${pathRegex(endpoint.path)}" "chain"`,
|
|
3125
3125
|
` SecRule ARGS:${field} "@lt ${ps.minLength}" "t:length"`
|
|
3126
3126
|
].join("\n")
|
|
@@ -3131,7 +3131,7 @@ function buildSchemaRules(ctx, schema, profile = CORAZA_GO_PROFILE) {
|
|
|
3131
3131
|
rules.push(
|
|
3132
3132
|
[
|
|
3133
3133
|
header11(`request.schema.${field}.maxLength=${ps.maxLength} for ${endpoint.method} ${endpoint.path}`),
|
|
3134
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:400,msg:'
|
|
3134
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:400,msg:'x-security: field ${esc10(field)} too long',tag:'${esc10(tag2)}',chain"`,
|
|
3135
3135
|
` SecRule REQUEST_URI "@rx ${pathRegex(endpoint.path)}" "chain"`,
|
|
3136
3136
|
` SecRule ARGS:${field} "@gt ${ps.maxLength}" "t:length"`
|
|
3137
3137
|
].join("\n")
|
|
@@ -3142,7 +3142,7 @@ function buildSchemaRules(ctx, schema, profile = CORAZA_GO_PROFILE) {
|
|
|
3142
3142
|
rules.push(
|
|
3143
3143
|
[
|
|
3144
3144
|
header11(`request.schema.${field}.fixedLength=${ps.fixedLength} for ${endpoint.method} ${endpoint.path}`),
|
|
3145
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:400,msg:'
|
|
3145
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:400,msg:'x-security: field ${esc10(field)} wrong length',tag:'${esc10(tag2)}',chain"`,
|
|
3146
3146
|
` SecRule REQUEST_URI "@rx ${pathRegex(endpoint.path)}" "chain"`,
|
|
3147
3147
|
` SecRule ARGS:${field} "!@eq ${ps.fixedLength}" "t:length"`
|
|
3148
3148
|
].join("\n")
|
|
@@ -3153,7 +3153,7 @@ function buildSchemaRules(ctx, schema, profile = CORAZA_GO_PROFILE) {
|
|
|
3153
3153
|
rules.push(
|
|
3154
3154
|
[
|
|
3155
3155
|
header11(`request.schema.${field}.min=${ps.min} for ${endpoint.method} ${endpoint.path}`),
|
|
3156
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:400,msg:'
|
|
3156
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:400,msg:'x-security: field ${esc10(field)} below min',tag:'${esc10(tag2)}',chain"`,
|
|
3157
3157
|
` SecRule REQUEST_URI "@rx ${pathRegex(endpoint.path)}" "chain"`,
|
|
3158
3158
|
` SecRule ARGS:${field} "@lt ${ps.min}"${term}`
|
|
3159
3159
|
].join("\n")
|
|
@@ -3164,7 +3164,7 @@ function buildSchemaRules(ctx, schema, profile = CORAZA_GO_PROFILE) {
|
|
|
3164
3164
|
rules.push(
|
|
3165
3165
|
[
|
|
3166
3166
|
header11(`request.schema.${field}.max=${ps.max} for ${endpoint.method} ${endpoint.path}`),
|
|
3167
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:400,msg:'
|
|
3167
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:400,msg:'x-security: field ${esc10(field)} above max',tag:'${esc10(tag2)}',chain"`,
|
|
3168
3168
|
` SecRule REQUEST_URI "@rx ${pathRegex(endpoint.path)}" "chain"`,
|
|
3169
3169
|
` SecRule ARGS:${field} "@gt ${ps.max}"${term}`
|
|
3170
3170
|
].join("\n")
|
|
@@ -3175,7 +3175,7 @@ function buildSchemaRules(ctx, schema, profile = CORAZA_GO_PROFILE) {
|
|
|
3175
3175
|
rules.push(
|
|
3176
3176
|
[
|
|
3177
3177
|
header11(`request.schema.${field}.pattern for ${endpoint.method} ${endpoint.path}`),
|
|
3178
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:400,msg:'
|
|
3178
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:400,msg:'x-security: field ${esc10(field)} pattern mismatch',tag:'${esc10(tag2)}',chain"`,
|
|
3179
3179
|
` SecRule REQUEST_URI "@rx ${pathRegex(endpoint.path)}" "chain"`,
|
|
3180
3180
|
` SecRule ARGS:${field} "!@rx ${esc10(ps.pattern)}"${term}`
|
|
3181
3181
|
].join("\n")
|
|
@@ -3188,7 +3188,7 @@ function buildSchemaRules(ctx, schema, profile = CORAZA_GO_PROFILE) {
|
|
|
3188
3188
|
rules.push(
|
|
3189
3189
|
[
|
|
3190
3190
|
header11(`request.schema.${field}.type=${ps.type} for ${endpoint.method} ${endpoint.path}`),
|
|
3191
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:400,msg:'
|
|
3191
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:400,msg:'x-security: field ${esc10(field)} not ${spec.what}',tag:'${esc10(tag2)}',chain"`,
|
|
3192
3192
|
// REQUEST_FILENAME (path-only), not REQUEST_URI (path+query): an
|
|
3193
3193
|
// anchored `^/path$` against REQUEST_URI never matches once the
|
|
3194
3194
|
// request carries a query string, so the type check would silently
|
|
@@ -3206,7 +3206,7 @@ function buildSchemaRules(ctx, schema, profile = CORAZA_GO_PROFILE) {
|
|
|
3206
3206
|
rules.push(
|
|
3207
3207
|
[
|
|
3208
3208
|
header11(`request.schema.${field}.allowedMimeTypes (request Content-Type) for ${endpoint.method} ${endpoint.path}`),
|
|
3209
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${ctId},phase:1,deny,status:415,msg:'
|
|
3209
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${ctId},phase:1,deny,status:415,msg:'x-security: field ${esc10(field)} Content-Type not in allowedMimeTypes',tag:'${esc10(tag2)}',chain"`,
|
|
3210
3210
|
` SecRule REQUEST_URI "@rx ${pathRegex(endpoint.path)}" "chain"`,
|
|
3211
3211
|
` SecRule REQUEST_HEADERS:Content-Type "!@rx ^(${alt})(;.*)?$"${term}`
|
|
3212
3212
|
].join("\n")
|
|
@@ -3215,7 +3215,7 @@ function buildSchemaRules(ctx, schema, profile = CORAZA_GO_PROFILE) {
|
|
|
3215
3215
|
rules.push(
|
|
3216
3216
|
[
|
|
3217
3217
|
header11(`request.schema.${field}.allowedMimeTypes (multipart part MIME) for ${endpoint.method} ${endpoint.path}`),
|
|
3218
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${fileId},phase:2,deny,status:415,msg:'
|
|
3218
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${fileId},phase:2,deny,status:415,msg:'x-security: field ${esc10(field)} upload MIME not allowed',tag:'${esc10(tag2)}',chain"`,
|
|
3219
3219
|
` SecRule REQUEST_URI "@rx ${pathRegex(endpoint.path)}" "chain"`,
|
|
3220
3220
|
` SecRule FILES_TMP_CONTENT:${field}|FILES:${field} "!@rx ^(${alt})$"${term}`
|
|
3221
3221
|
].join("\n")
|
|
@@ -3228,7 +3228,7 @@ function buildSsrfRules(endpoint, profile = CORAZA_GO_PROFILE, phase = 2) {
|
|
|
3228
3228
|
const schema = endpoint.policy.request?.schema;
|
|
3229
3229
|
if (!schema) return [];
|
|
3230
3230
|
const term = chainTerm8(profile);
|
|
3231
|
-
const tag2 = `
|
|
3231
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
3232
3232
|
const rules = [];
|
|
3233
3233
|
const slot = endpointHash3(endpoint.method, endpoint.path) % 4500;
|
|
3234
3234
|
let offset = 0;
|
|
@@ -3244,7 +3244,7 @@ function buildSsrfRules(endpoint, profile = CORAZA_GO_PROFILE, phase = 2) {
|
|
|
3244
3244
|
rules.push(
|
|
3245
3245
|
[
|
|
3246
3246
|
header11(`W19-A SSRF: ${field} must match domainAllowlist for ${endpoint.method} ${endpoint.path}`),
|
|
3247
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:${phase},deny,status:403,msg:'
|
|
3247
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:${phase},deny,status:403,msg:'x-security: ${esc10(field)} host not in domainAllowlist',tag:'${esc10(tag2)}',tag:'x-security-rule-ssrf-403',chain"`,
|
|
3248
3248
|
// W22-B: REQUEST_FILENAME (path-only), not REQUEST_URI. libmodsec3's
|
|
3249
3249
|
// REQUEST_URI is path *+ query string* — an anchored `^/path$` rx
|
|
3250
3250
|
// never matches once the endpoint receives the very ?url= it is
|
|
@@ -3263,7 +3263,7 @@ function buildSsrfRules(endpoint, profile = CORAZA_GO_PROFILE, phase = 2) {
|
|
|
3263
3263
|
rules.push(
|
|
3264
3264
|
[
|
|
3265
3265
|
header11(`W19-A SSRF: ${field} blockPrivateRanges for ${endpoint.method} ${endpoint.path}`),
|
|
3266
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:${phase},deny,status:403,msg:'
|
|
3266
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:${phase},deny,status:403,msg:'x-security: ${esc10(field)} resolves to private/loopback host',tag:'${esc10(tag2)}',tag:'x-security-rule-ssrf-private-403',chain"`,
|
|
3267
3267
|
// W22-B: REQUEST_FILENAME — see comment on the domainAllowlist rule above.
|
|
3268
3268
|
` SecRule REQUEST_FILENAME "@rx ${pathRegex(endpoint.path)}" "chain"`,
|
|
3269
3269
|
// W22-B: escRx — see comment on escRx. Same reasoning as the allowlist rule.
|
|
@@ -3281,7 +3281,7 @@ function hasJsonContentType(contentTypes) {
|
|
|
3281
3281
|
function buildJsonBodyProcessor(endpoint, profile = CORAZA_GO_PROFILE) {
|
|
3282
3282
|
const policy = endpoint.policy;
|
|
3283
3283
|
if (!hasJsonContentType(policy.request?.contentType)) return null;
|
|
3284
|
-
const tag2 = `
|
|
3284
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
3285
3285
|
const ctlId = BODY_ALLOWLIST_BASE_ID + 1e4 + endpointHash3(endpoint.method, endpoint.path) % 9e3;
|
|
3286
3286
|
const term = chainTerm8(profile);
|
|
3287
3287
|
return [
|
|
@@ -3320,7 +3320,7 @@ function buildBodyFieldAllowlistRules(endpoint, profile = CORAZA_GO_PROFILE) {
|
|
|
3320
3320
|
}
|
|
3321
3321
|
}
|
|
3322
3322
|
if (safe.length === 0) return [];
|
|
3323
|
-
const tag2 = `
|
|
3323
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
3324
3324
|
const id = BODY_ALLOWLIST_BASE_ID + endpointHash3(endpoint.method, endpoint.path) % 9e3;
|
|
3325
3325
|
const alt = safe.join("|");
|
|
3326
3326
|
const argsRegex = `^json\\.(${alt})$`;
|
|
@@ -3344,7 +3344,7 @@ function buildBodyFieldAllowlistRules(endpoint, profile = CORAZA_GO_PROFILE) {
|
|
|
3344
3344
|
out.push(
|
|
3345
3345
|
[
|
|
3346
3346
|
header11(headerLines.join("\n")),
|
|
3347
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:403,msg:'
|
|
3347
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:403,msg:'x-security: request body contains field outside allowlist (mass-assignment)',tag:'${esc10(tag2)}',tag:'x-security-api6-mass-assignment',chain"`,
|
|
3348
3348
|
` SecRule REQUEST_URI "@rx ${pathRegex(endpoint.path)}" "chain"`,
|
|
3349
3349
|
// Content-Type guard: only enforce the json.<key> allowlist when the body
|
|
3350
3350
|
// is actually JSON. Without this, a POST carrying query-string/form args
|
|
@@ -3380,7 +3380,7 @@ function buildResponseInspectionRules(endpoint, profile = CORAZA_GO_PROFILE, war
|
|
|
3380
3380
|
reason: `response inspection enabled (phase 4); expect ~10-15% throughput cost on libmodsecurity3 and an extra SPOE round-trip on coraza-spoa. Acceptable for high-value endpoints; disable response.schema on hot paths if latency budget is tight.`,
|
|
3381
3381
|
detail: { mechanism: "SecResponseBodyAccess", phase: 4 }
|
|
3382
3382
|
});
|
|
3383
|
-
const tag2 = `
|
|
3383
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
3384
3384
|
const base = RESPONSE_INSPECT_BASE_ID + endpointHash3(endpoint.method, endpoint.path) % 9e3 * 1;
|
|
3385
3385
|
const pathRx = pathRegex(endpoint.path);
|
|
3386
3386
|
const term = chainTerm8(profile);
|
|
@@ -3399,16 +3399,16 @@ function buildResponseInspectionRules(endpoint, profile = CORAZA_GO_PROFILE, war
|
|
|
3399
3399
|
`C-1 response.schema.${field}.maxLength=${ps.maxLength} for ${endpoint.method} ${endpoint.path}
|
|
3400
3400
|
phase:4 inspects RESPONSE_BODY for "${field}":"<value of length > ${cap2}>". Heuristic regex-over-JSON; nested / escaped values may not match.`
|
|
3401
3401
|
),
|
|
3402
|
-
`SecRule REQUEST_URI "@rx ${pathRx}" "id:${id},phase:4,deny,status:500,msg:'
|
|
3402
|
+
`SecRule REQUEST_URI "@rx ${pathRx}" "id:${id},phase:4,deny,status:500,msg:'x-security: response.${esc10(field)} exceeds maxLength=${ps.maxLength} (data exposure)',tag:'${esc10(tag2)}',tag:'x-security-api3-bopla',chain"`,
|
|
3403
3403
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}" "chain"`,
|
|
3404
|
-
` SecRule RESPONSE_BODY "@rx \\
|
|
3404
|
+
` SecRule RESPONSE_BODY "@rx \\x22${esc10(field)}\\x22\\s*:\\s*\\x22[^\\x22]{${cap2 + 1},}\\x22"${term}`
|
|
3405
3405
|
].join("\n")
|
|
3406
3406
|
);
|
|
3407
3407
|
}
|
|
3408
3408
|
if (ps.pattern) {
|
|
3409
3409
|
const captureId = nextId++;
|
|
3410
3410
|
const denyId = nextId++;
|
|
3411
|
-
const txVar = `
|
|
3411
|
+
const txVar = `x_security_${safeVarName2(field)}`;
|
|
3412
3412
|
rules.push(
|
|
3413
3413
|
[
|
|
3414
3414
|
header11(
|
|
@@ -3417,7 +3417,7 @@ phase:4 rule A: extract response.${field} value into TX:${txVar} for the inverse
|
|
|
3417
3417
|
),
|
|
3418
3418
|
`SecRule REQUEST_URI "@rx ${pathRx}" "id:${captureId},phase:4,pass,nolog,chain"`,
|
|
3419
3419
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}" "chain"`,
|
|
3420
|
-
` SecRule RESPONSE_BODY "@rx \\
|
|
3420
|
+
` SecRule RESPONSE_BODY "@rx \\x22${esc10(field)}\\x22\\s*:\\s*\\x22([^\\x22]*)\\x22" "capture,setvar:tx.${txVar}=%{TX.1}${term ? ",t:none" : ""}"`
|
|
3421
3421
|
].join("\n")
|
|
3422
3422
|
);
|
|
3423
3423
|
rules.push(
|
|
@@ -3426,7 +3426,7 @@ phase:4 rule A: extract response.${field} value into TX:${txVar} for the inverse
|
|
|
3426
3426
|
`C-1 response.schema.${field}.pattern (deny) for ${endpoint.method} ${endpoint.path}
|
|
3427
3427
|
phase:4 rule B: deny when TX:${txVar} does not match the required pattern (RE2-safe; no lookahead)`
|
|
3428
3428
|
),
|
|
3429
|
-
`SecRule TX:${txVar} "!@rx ${esc10(ps.pattern)}" "id:${denyId},phase:4,deny,status:500,msg:'
|
|
3429
|
+
`SecRule TX:${txVar} "!@rx ${esc10(ps.pattern)}" "id:${denyId},phase:4,deny,status:500,msg:'x-security: response.${esc10(field)} pattern mismatch (data exposure)',tag:'${esc10(tag2)}',tag:'x-security-api3-bopla'"`
|
|
3430
3430
|
].join("\n")
|
|
3431
3431
|
);
|
|
3432
3432
|
}
|
|
@@ -3440,14 +3440,14 @@ phase:4 rule B: deny when TX:${txVar} does not match the required pattern (RE2-s
|
|
|
3440
3440
|
`C-1 stored-XSS guard for ${endpoint.method} ${endpoint.path}
|
|
3441
3441
|
phase:4 \u2014 response.contentType is application/json, so raw <script> or
|
|
3442
3442
|
javascript: URIs in the body indicate a stored-XSS echo. msg carries
|
|
3443
|
-
'
|
|
3443
|
+
'x-security-xss-stored' for scorer attribution.`
|
|
3444
3444
|
),
|
|
3445
3445
|
// REQUEST_FILENAME (path-only) — REQUEST_URI on Coraza/libmodsec3
|
|
3446
3446
|
// includes the query string, so an anchored `^/path$` rx fails for
|
|
3447
3447
|
// any request carrying `?foo=bar`. The vAPI XSS attack hits
|
|
3448
3448
|
// /vapi/stickynotes?format=html; REQUEST_FILENAME strips the query
|
|
3449
3449
|
// and matches cleanly. (Same fix W22-B applied to the SSRF rules.)
|
|
3450
|
-
`SecRule REQUEST_FILENAME "@rx ${pathRx}" "id:${id},phase:4,deny,status:500,msg:'
|
|
3450
|
+
`SecRule REQUEST_FILENAME "@rx ${pathRx}" "id:${id},phase:4,deny,status:500,msg:'x-security: stored-XSS payload in JSON response body',tag:'${esc10(tag2)}',tag:'x-security-xss-stored',chain"`,
|
|
3451
3451
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}" "chain"`,
|
|
3452
3452
|
` SecRule RESPONSE_BODY "@rx (?i)(?:<script\\b|javascript:)"${term}`
|
|
3453
3453
|
].join("\n")
|
|
@@ -3486,9 +3486,9 @@ phase:4 denies when the response body contains a top-level JSON key NOT in the d
|
|
|
3486
3486
|
Note: this is a deny-on-unknown rule, NOT a strip \u2014 true stripping requires a Lua plugin.
|
|
3487
3487
|
PCRE-only (uses negative lookahead); RE2-backed engines (coraza-go/spoa) skip this rule.`
|
|
3488
3488
|
),
|
|
3489
|
-
`SecRule REQUEST_URI "@rx ${pathRx}" "id:${id},phase:4,deny,status:500,msg:'
|
|
3489
|
+
`SecRule REQUEST_URI "@rx ${pathRx}" "id:${id},phase:4,deny,status:500,msg:'x-security: response contains undeclared field (stripUnknownFields)',tag:'${esc10(tag2)}',tag:'x-security-api3-bopla',chain"`,
|
|
3490
3490
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}" "chain"`,
|
|
3491
|
-
` SecRule RESPONSE_BODY "@rx \\
|
|
3491
|
+
` SecRule RESPONSE_BODY "@rx \\x22(?!(?:${allowAlt})\\x22)[A-Za-z_][A-Za-z0-9_]*\\x22\\s*:"${term}`
|
|
3492
3492
|
].join("\n")
|
|
3493
3493
|
);
|
|
3494
3494
|
}
|
|
@@ -3503,7 +3503,7 @@ function buildSqliHeuristics(endpoint, profile = CORAZA_GO_PROFILE) {
|
|
|
3503
3503
|
const policy = endpoint.policy;
|
|
3504
3504
|
const req = policy.request;
|
|
3505
3505
|
if (!req || !req.schema) return [];
|
|
3506
|
-
const tag2 = `
|
|
3506
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
3507
3507
|
const term = chainTerm8(profile);
|
|
3508
3508
|
const out = [];
|
|
3509
3509
|
for (const [field, ps] of Object.entries(req.schema)) {
|
|
@@ -3516,7 +3516,7 @@ function buildSqliHeuristics(endpoint, profile = CORAZA_GO_PROFILE) {
|
|
|
3516
3516
|
const m3 = INJECTION_SINK_MATCHERS[sink];
|
|
3517
3517
|
const idSeed = endpointHash3(`${endpoint.method}|${endpoint.path}|${field}|${sink}`, "");
|
|
3518
3518
|
const id = INJECTION_GUARD_BASE_ID + idSeed % 9e3;
|
|
3519
|
-
const classTag = `
|
|
3519
|
+
const classTag = `x-security-${m3.category}`;
|
|
3520
3520
|
const ssecId = m3.category === "prompt" ? "SSEC-PROMPT" : "SSEC-INJECTION";
|
|
3521
3521
|
out.push(
|
|
3522
3522
|
[
|
|
@@ -3525,7 +3525,7 @@ function buildSqliHeuristics(endpoint, profile = CORAZA_GO_PROFILE) {
|
|
|
3525
3525
|
Author-declared injectionGuard sink; phase:2 denies the payload at write
|
|
3526
3526
|
time over the body/query field. Attributed to ${ssecId} by the reporter.`
|
|
3527
3527
|
),
|
|
3528
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:403,msg:'
|
|
3528
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:403,msg:'x-security: ${m3.msg} detected in ${esc10(field)}',tag:'${esc10(tag2)}',tag:'${classTag}',tag:'${classTag}-${m3.tagSuffix}',chain"`,
|
|
3529
3529
|
` SecRule REQUEST_URI "@rx ${pathRegex(endpoint.path)}" "chain"`,
|
|
3530
3530
|
` SecRule ARGS:${field}|ARGS:json.${field} "${m3.operator}"${term}`
|
|
3531
3531
|
].join("\n")
|
|
@@ -3539,7 +3539,7 @@ function buildXssHeuristics(endpoint, profile = CORAZA_GO_PROFILE) {
|
|
|
3539
3539
|
const req = policy.request;
|
|
3540
3540
|
if (!req || !req.schema) return [];
|
|
3541
3541
|
if (!hasJsonContentType(req.contentType)) return [];
|
|
3542
|
-
const tag2 = `
|
|
3542
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
3543
3543
|
const term = chainTerm8(profile);
|
|
3544
3544
|
const out = [];
|
|
3545
3545
|
for (const [field, ps] of Object.entries(req.schema)) {
|
|
@@ -3554,7 +3554,7 @@ phase:2 \u2014 denies when the free-text field carries a literal <script tag
|
|
|
3554
3554
|
or javascript: URI. Stops the payload at write time so the GET-side echo
|
|
3555
3555
|
(which coraza-spoa cannot inspect \u2014 no response body via SPOE) is moot.`
|
|
3556
3556
|
),
|
|
3557
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:403,msg:'
|
|
3557
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:403,msg:'x-security: stored-XSS payload in ${esc10(field)}',tag:'${esc10(tag2)}',tag:'x-security-xss-stored',chain"`,
|
|
3558
3558
|
` SecRule REQUEST_URI "@rx ${pathRegex(endpoint.path)}" "chain"`,
|
|
3559
3559
|
` SecRule ARGS:json.${field} "@rx (?i)(?:<script\\b|javascript:)"${term}`
|
|
3560
3560
|
].join("\n")
|
|
@@ -3565,7 +3565,7 @@ or javascript: URI. Stops the payload at write time so the GET-side echo
|
|
|
3565
3565
|
function buildPolicyRules(endpoint, profile = CORAZA_GO_PROFILE, warnings) {
|
|
3566
3566
|
const policy = endpoint.policy;
|
|
3567
3567
|
const base = ruleBase(endpoint);
|
|
3568
|
-
const tag2 = `
|
|
3568
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
3569
3569
|
const ctx = { endpoint, base, tag: tag2 };
|
|
3570
3570
|
const out = [];
|
|
3571
3571
|
out.push(buildScopeMarker(ctx));
|
|
@@ -3637,7 +3637,7 @@ var init_rules = __esm({
|
|
|
3637
3637
|
RESPONSE_INSPECT_BASE_ID = 42e4;
|
|
3638
3638
|
SLOT = {
|
|
3639
3639
|
scope: 0,
|
|
3640
|
-
// path/method match flag (tx.
|
|
3640
|
+
// path/method match flag (tx.x_security_match)
|
|
3641
3641
|
ctype: 1,
|
|
3642
3642
|
// content-type allowlist
|
|
3643
3643
|
bodySize: 2,
|
|
@@ -3942,7 +3942,7 @@ function buildHaproxyStickTables(spec, engine, warnings, peers = []) {
|
|
|
3942
3942
|
const key = resolveKey(rl, engine, epId);
|
|
3943
3943
|
if (key.warning) warnings.push(key.warning);
|
|
3944
3944
|
const slug = slugify(ep.method, ep.path) + (rls.length > 1 ? `_${idx}` : "");
|
|
3945
|
-
const backendName = `
|
|
3945
|
+
const backendName = `st_x_security_${slug}`;
|
|
3946
3946
|
if (seenBackends.has(backendName)) return;
|
|
3947
3947
|
seenBackends.add(backendName);
|
|
3948
3948
|
const peersClause = peers.length > 0 ? ` peers ${PEER_GROUP}` : "";
|
|
@@ -4000,7 +4000,7 @@ ${burstMethodLine}`,
|
|
|
4000
4000
|
}
|
|
4001
4001
|
if (emissions.length === 0) return null;
|
|
4002
4002
|
const out = [];
|
|
4003
|
-
out.push("#
|
|
4003
|
+
out.push("# x-security \u2192 HAProxy stick-tables \u2014 auto-generated. DO NOT EDIT BY HAND.");
|
|
4004
4004
|
out.push(`# engine: ${engine}`);
|
|
4005
4005
|
out.push(`# source: ${spec.info.title} ${spec.info.version}`);
|
|
4006
4006
|
out.push("#");
|
|
@@ -4011,7 +4011,7 @@ ${burstMethodLine}`,
|
|
|
4011
4011
|
out.push("#");
|
|
4012
4012
|
out.push("# How to integrate into an EXISTING haproxy.cfg:");
|
|
4013
4013
|
out.push("#");
|
|
4014
|
-
out.push("# 1. Append every `backend
|
|
4014
|
+
out.push("# 1. Append every `backend st_x_security_*` block below to your");
|
|
4015
4015
|
out.push("# haproxy.cfg (they are self-contained \u2014 no listener needed).");
|
|
4016
4016
|
out.push("# 2. Inside your existing `frontend` block, paste the ACL/track/deny");
|
|
4017
4017
|
out.push('# snippet from the "WRIT FRONTEND SNIPPET" section.');
|
|
@@ -4021,7 +4021,7 @@ ${burstMethodLine}`,
|
|
|
4021
4021
|
if (peers.length > 0) {
|
|
4022
4022
|
out.push("# \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
4023
4023
|
out.push(`# Peer replication (group: ${PEER_GROUP}) \u2014 ${peers.length} instance(s)`);
|
|
4024
|
-
out.push("# Each stick-table below references `peers
|
|
4024
|
+
out.push("# Each stick-table below references `peers x-security` so HAProxy");
|
|
4025
4025
|
out.push("# replicates counters between the listed nodes in near-real-time.");
|
|
4026
4026
|
out.push("# \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
4027
4027
|
out.push(`peers ${PEER_GROUP}`);
|
|
@@ -4058,7 +4058,7 @@ var init_haproxy_stick_tables = __esm({
|
|
|
4058
4058
|
"src/generators/coraza/templates/haproxy-stick-tables.ts"() {
|
|
4059
4059
|
"use strict";
|
|
4060
4060
|
init_rules();
|
|
4061
|
-
PEER_GROUP = "
|
|
4061
|
+
PEER_GROUP = "x-security";
|
|
4062
4062
|
}
|
|
4063
4063
|
});
|
|
4064
4064
|
|
|
@@ -4130,7 +4130,7 @@ function buildModsecNginxServerConf(spec) {
|
|
|
4130
4130
|
}
|
|
4131
4131
|
if (!tlsFloor && !ciphers && endpointBlocks.length === 0) return null;
|
|
4132
4132
|
const lines = [
|
|
4133
|
-
"#
|
|
4133
|
+
"# x-security \u2192 modsec-nginx server-side directives \u2014 auto-generated.",
|
|
4134
4134
|
"# Merge inside your `server { ... }` block (not at http {} scope).",
|
|
4135
4135
|
`# Source: ${spec.info.title} ${spec.info.version}`,
|
|
4136
4136
|
""
|
|
@@ -4195,12 +4195,12 @@ function smallestBodyLimit(endpoints) {
|
|
|
4195
4195
|
function buildDirectives(spec, profile, warnings) {
|
|
4196
4196
|
const lines = [];
|
|
4197
4197
|
if (profile.name === "coraza-go") {
|
|
4198
|
-
lines.push("#
|
|
4199
|
-
lines.push(`# generator:
|
|
4198
|
+
lines.push("# x-security \u2192 Coraza v3.x \u2014 auto-generated. DO NOT EDIT BY HAND.");
|
|
4199
|
+
lines.push(`# generator: x-security-coraza v${VERSION}`);
|
|
4200
4200
|
lines.push(`# source: ${spec.info.title} ${spec.info.version}`);
|
|
4201
4201
|
} else {
|
|
4202
|
-
lines.push("#
|
|
4203
|
-
lines.push(`# generator:
|
|
4202
|
+
lines.push("# x-security \u2192 Coraza/ModSecurity \u2014 auto-generated. DO NOT EDIT BY HAND.");
|
|
4203
|
+
lines.push(`# generator: x-security-coraza v${VERSION}`);
|
|
4204
4204
|
lines.push(`# engine: ${profile.name}`);
|
|
4205
4205
|
lines.push(`# source: ${spec.info.title} ${spec.info.version}`);
|
|
4206
4206
|
}
|
|
@@ -4286,20 +4286,20 @@ function buildDirectives(spec, profile, warnings) {
|
|
|
4286
4286
|
function buildIncludeSnippet(profile) {
|
|
4287
4287
|
if (profile.name === "modsec-nginx") {
|
|
4288
4288
|
return [
|
|
4289
|
-
"# ===
|
|
4289
|
+
"# === x-security include for owasp/modsecurity-crs:nginx ===",
|
|
4290
4290
|
"#",
|
|
4291
|
-
"# Mount
|
|
4291
|
+
"# Mount x-security.conf to /etc/modsecurity.d/x-security.conf and use",
|
|
4292
4292
|
"# the entrypoint wrapper documented in deployment-recipes/modsec-nginx.md",
|
|
4293
4293
|
"# to append this Include line after the image regenerates setup.conf:",
|
|
4294
4294
|
"#",
|
|
4295
|
-
"Include /etc/modsecurity.d/
|
|
4295
|
+
"Include /etc/modsecurity.d/x-security.conf",
|
|
4296
4296
|
""
|
|
4297
4297
|
].join("\n");
|
|
4298
4298
|
}
|
|
4299
4299
|
if (profile.name === "modsec-apache") {
|
|
4300
4300
|
return [
|
|
4301
4301
|
"# Drop into Apache config (e.g. /etc/apache2/mods-enabled/security2.conf):",
|
|
4302
|
-
"Include /etc/modsecurity/
|
|
4302
|
+
"Include /etc/modsecurity/x-security.conf",
|
|
4303
4303
|
""
|
|
4304
4304
|
].join("\n");
|
|
4305
4305
|
}
|
|
@@ -4307,7 +4307,7 @@ function buildIncludeSnippet(profile) {
|
|
|
4307
4307
|
}
|
|
4308
4308
|
function buildWarningsDoc(profile, warnings) {
|
|
4309
4309
|
const lines = [
|
|
4310
|
-
`#
|
|
4310
|
+
`# x-security \u2192 ${profile.name} \u2014 emission warnings`,
|
|
4311
4311
|
"",
|
|
4312
4312
|
`Generator version: ${VERSION}`,
|
|
4313
4313
|
`Engine profile: ${profile.name}`,
|
|
@@ -4368,21 +4368,21 @@ function createCorazaGenerator(opts = {}) {
|
|
|
4368
4368
|
const artifacts = [];
|
|
4369
4369
|
if (profile.fileExt === "conf") {
|
|
4370
4370
|
artifacts.push({
|
|
4371
|
-
path: "
|
|
4371
|
+
path: "x-security.conf",
|
|
4372
4372
|
content: directives + "\n",
|
|
4373
4373
|
format: "conf"
|
|
4374
4374
|
});
|
|
4375
4375
|
const snippet = buildIncludeSnippet(profile);
|
|
4376
4376
|
if (snippet) {
|
|
4377
4377
|
artifacts.push({
|
|
4378
|
-
path: "
|
|
4378
|
+
path: "x-security-include.conf",
|
|
4379
4379
|
content: snippet,
|
|
4380
4380
|
format: "conf"
|
|
4381
4381
|
});
|
|
4382
4382
|
}
|
|
4383
4383
|
} else {
|
|
4384
4384
|
const meta = {
|
|
4385
|
-
generator: "
|
|
4385
|
+
generator: "x-security-coraza",
|
|
4386
4386
|
version: VERSION
|
|
4387
4387
|
};
|
|
4388
4388
|
if (profile.name !== "coraza-go") meta["engine"] = profile.name;
|
|
@@ -4490,7 +4490,7 @@ function createCorazaGenerator(opts = {}) {
|
|
|
4490
4490
|
// python pickle \x80 opcode frame) [v0.7]
|
|
4491
4491
|
// ai-prompt → @rx LLM prompt-injection marker denylist (jailbreak /
|
|
4492
4492
|
// system-prompt-leak / role-override). Distinct tag
|
|
4493
|
-
//
|
|
4493
|
+
// x-security-prompt → SSEC-PROMPT (NOT SSEC-INJECTION) [v0.7]
|
|
4494
4494
|
// Every sink is a real enforcing matcher on all four profiles (sql/xss
|
|
4495
4495
|
// need the dedicated operator, present on all shipping engines; the rest
|
|
4496
4496
|
// are plain RE2-safe @rx). → full.
|
|
@@ -4716,10 +4716,10 @@ function buildRateLimitSettings(rl, url, index) {
|
|
|
4716
4716
|
if (primaryIdentifier(r5) === "user-id") {
|
|
4717
4717
|
const zoneName = `lazy_user_${n2}`;
|
|
4718
4718
|
const rate = rateLimitToBunkerRate(r5);
|
|
4719
|
-
out[`CUSTOM_CONF_HTTP_LIMIT_REQ_USER_${n2}`] = `#
|
|
4719
|
+
out[`CUSTOM_CONF_HTTP_LIMIT_REQ_USER_${n2}`] = `# x-security: per-user-id rate limit for ${url} (drift closure)
|
|
4720
4720
|
limit_req_zone $http_x_forwarded_user zone=${zoneName}:10m rate=${rate};
|
|
4721
4721
|
`;
|
|
4722
|
-
out[`
|
|
4722
|
+
out[`X_SECURITY_USER_RL_ZONE_${n2}`] = `${zoneName}@${url}`;
|
|
4723
4723
|
}
|
|
4724
4724
|
});
|
|
4725
4725
|
return out;
|
|
@@ -4773,7 +4773,7 @@ function escMsec(s) {
|
|
|
4773
4773
|
}
|
|
4774
4774
|
function buildAuthModSecRules(auth) {
|
|
4775
4775
|
const lines = [
|
|
4776
|
-
`#
|
|
4776
|
+
`# x-security-generated authentication rules (${auth.type})`,
|
|
4777
4777
|
// initcol is for ip/global/resource collections; tx is request-scoped and
|
|
4778
4778
|
// does not need initcol. We just zero the relevant TX var directly.
|
|
4779
4779
|
`SecAction "id:990000,phase:1,nolog,pass,setvar:tx.jwt_invalid=0"`
|
|
@@ -4784,7 +4784,7 @@ function buildAuthModSecRules(auth) {
|
|
|
4784
4784
|
case "oauth2": {
|
|
4785
4785
|
lines.push(
|
|
4786
4786
|
`SecRule REQUEST_HEADERS:${escMsec(header15)} "@rx ^Bearer (.+)$" "id:990010,phase:1,nolog,pass,capture,setvar:tx.bearer_token=%{TX.1}"`,
|
|
4787
|
-
`SecRule &TX:bearer_token "@eq 0" "id:990011,phase:1,deny,status:401,log,msg:'
|
|
4787
|
+
`SecRule &TX:bearer_token "@eq 0" "id:990011,phase:1,deny,status:401,log,msg:'x-security: missing bearer token (signature NOT verified \u2014 external auth layer required)'"`
|
|
4788
4788
|
);
|
|
4789
4789
|
if (auth.type === "oauth2" && auth.scopes?.length) {
|
|
4790
4790
|
lines.push(`# oauth2 required scopes (NOT enforced here \u2014 needs external auth): ${auth.scopes.join(" ")}`);
|
|
@@ -4794,13 +4794,13 @@ function buildAuthModSecRules(auth) {
|
|
|
4794
4794
|
case "api-key": {
|
|
4795
4795
|
lines.push(
|
|
4796
4796
|
`SecRule REQUEST_HEADERS:${escMsec(header15)} "@rx ^.+$" "id:990020,phase:1,nolog,pass,setvar:tx.api_key_present=1"`,
|
|
4797
|
-
`SecRule &TX:api_key_present "@eq 0" "id:990021,phase:1,deny,status:401,log,msg:'
|
|
4797
|
+
`SecRule &TX:api_key_present "@eq 0" "id:990021,phase:1,deny,status:401,log,msg:'x-security: missing API key (${escMsec(header15)})'"`
|
|
4798
4798
|
);
|
|
4799
4799
|
break;
|
|
4800
4800
|
}
|
|
4801
4801
|
case "basic": {
|
|
4802
4802
|
lines.push(
|
|
4803
|
-
`SecRule REQUEST_HEADERS:Authorization "!@rx ^Basic [A-Za-z0-9+/=]+$" "id:990030,phase:1,deny,status:401,log,msg:'
|
|
4803
|
+
`SecRule REQUEST_HEADERS:Authorization "!@rx ^Basic [A-Za-z0-9+/=]+$" "id:990030,phase:1,deny,status:401,log,msg:'x-security: missing/invalid Basic credentials'"`
|
|
4804
4804
|
);
|
|
4805
4805
|
break;
|
|
4806
4806
|
}
|
|
@@ -4826,13 +4826,13 @@ function buildAuthSettings(auth) {
|
|
|
4826
4826
|
const header15 = auth.headerName ?? (auth.type === "api-key" ? "X-API-Key" : "Authorization");
|
|
4827
4827
|
out.USE_MODSECURITY = "yes";
|
|
4828
4828
|
out.CUSTOM_CONF_MODSEC_1 = buildAuthModSecRules(auth);
|
|
4829
|
-
out.
|
|
4830
|
-
out.
|
|
4831
|
-
if (auth.jwksUri) out.
|
|
4832
|
-
if (auth.issuer) out.
|
|
4833
|
-
if (auth.audience) out.
|
|
4829
|
+
out.X_SECURITY_AUTH_HEADER = header15;
|
|
4830
|
+
out.X_SECURITY_AUTH_TYPE = auth.type;
|
|
4831
|
+
if (auth.jwksUri) out.X_SECURITY_JWKS_URI = String(auth.jwksUri);
|
|
4832
|
+
if (auth.issuer) out.X_SECURITY_AUTH_ISSUER = String(auth.issuer);
|
|
4833
|
+
if (auth.audience) out.X_SECURITY_AUTH_AUDIENCE = String(auth.audience);
|
|
4834
4834
|
if (auth.type === "oauth2" && auth.scopes?.length) {
|
|
4835
|
-
out.
|
|
4835
|
+
out.X_SECURITY_AUTH_SCOPES = auth.scopes.join(" ");
|
|
4836
4836
|
}
|
|
4837
4837
|
const jwtSettings = buildJwtNativeSettings(auth);
|
|
4838
4838
|
for (const [k5, v] of Object.entries(jwtSettings)) out[k5] = v;
|
|
@@ -4905,9 +4905,9 @@ function buildLifecycleRules2(endpoint) {
|
|
|
4905
4905
|
const pathRx = pathRegex2(endpoint.path);
|
|
4906
4906
|
const sunset = endpoint.policy.sunsetDate ? ` (sunset:${endpoint.policy.sunsetDate})` : "";
|
|
4907
4907
|
const replacement = endpoint.policy.replacementEndpoint ? ` use ${endpoint.policy.replacementEndpoint}` : "";
|
|
4908
|
-
const msg = `
|
|
4908
|
+
const msg = `x-security: ${DEPRECATED_TAG} ${endpoint.method} ${endpoint.path}${sunset}${replacement}`;
|
|
4909
4909
|
const lines = [
|
|
4910
|
-
`#
|
|
4910
|
+
`# x-security-generated lifecycle rule (deprecated endpoint \u2192 410 Gone)`,
|
|
4911
4911
|
`# Source: ${endpoint.method} ${endpoint.path}` + (endpoint.policy.sunsetDate ? ` (sunsetDate: ${endpoint.policy.sunsetDate})` : ""),
|
|
4912
4912
|
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:1,deny,status:410,log,auditlog,msg:'${escMsec2(msg)}',tag:'${DEPRECATED_TAG}',chain"`,
|
|
4913
4913
|
` SecRule REQUEST_FILENAME "@rx ${escRx4(pathRx)}" "t:none"`
|
|
@@ -4919,7 +4919,7 @@ var init_lifecycle = __esm({
|
|
|
4919
4919
|
"src/generators/bunkerweb/lifecycle.ts"() {
|
|
4920
4920
|
"use strict";
|
|
4921
4921
|
DEPRECATED_BASE_ID2 = 970500;
|
|
4922
|
-
DEPRECATED_TAG = "
|
|
4922
|
+
DEPRECATED_TAG = "x-security-deprecated-endpoint-block";
|
|
4923
4923
|
}
|
|
4924
4924
|
});
|
|
4925
4925
|
|
|
@@ -4959,24 +4959,24 @@ function buildAuthzMultiRoleRules(endpoint) {
|
|
|
4959
4959
|
const idMissing = base;
|
|
4960
4960
|
const idNoMatch = base + 1;
|
|
4961
4961
|
const altRx = `(?:^|,)(?:${safeRoles.map((r5) => r5.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("|")})(?:,|$)`;
|
|
4962
|
-
const tag2 = `
|
|
4962
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
4963
4963
|
const rolesStr = safeRoles.join(",");
|
|
4964
4964
|
const lines = [];
|
|
4965
4965
|
lines.push(
|
|
4966
4966
|
[
|
|
4967
|
-
`#
|
|
4967
|
+
`# x-security-generated authorization rules (rbac multi-role: ${rolesStr})`,
|
|
4968
4968
|
`# Source: ${endpoint.method} ${endpoint.path}`,
|
|
4969
4969
|
`# Chain on X-Forwarded-Groups (set by upstream OIDC sidecar / Kong+OIDC).`,
|
|
4970
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${idMissing},phase:1,deny,status:403,log,auditlog,msg:'
|
|
4970
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${idMissing},phase:1,deny,status:403,log,auditlog,msg:'x-security rbac-multi-role denied (no ${TRUSTED_GROUPS_HEADER} header)',tag:'${escMsec3(tag2)}',tag:'x-security-rule-rbac-multi-role',chain"`,
|
|
4971
4971
|
` SecRule REQUEST_FILENAME "@rx ${escRx5(pathRx)}" "chain"`,
|
|
4972
4972
|
` SecRule &REQUEST_HEADERS:${TRUSTED_GROUPS_HEADER} "@eq 0" "t:none"`
|
|
4973
4973
|
].join("\n")
|
|
4974
4974
|
);
|
|
4975
4975
|
lines.push(
|
|
4976
4976
|
[
|
|
4977
|
-
`#
|
|
4977
|
+
`# x-security-generated authorization rules (rbac multi-role: ${rolesStr})`,
|
|
4978
4978
|
`# Source: ${endpoint.method} ${endpoint.path}`,
|
|
4979
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${idNoMatch},phase:1,deny,status:403,log,auditlog,msg:'
|
|
4979
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${idNoMatch},phase:1,deny,status:403,log,auditlog,msg:'x-security rbac-multi-role denied (${TRUSTED_GROUPS_HEADER} lacks any of: ${escMsec3(rolesStr)})',tag:'${escMsec3(tag2)}',tag:'x-security-rule-rbac-multi-role',chain"`,
|
|
4980
4980
|
` SecRule REQUEST_FILENAME "@rx ${escRx5(pathRx)}" "chain"`,
|
|
4981
4981
|
` SecRule REQUEST_HEADERS:${TRUSTED_GROUPS_HEADER} "!@rx ${escRx5(altRx)}" "t:none"`
|
|
4982
4982
|
].join("\n")
|
|
@@ -5034,16 +5034,16 @@ function buildPiiResponseRules(endpoint) {
|
|
|
5034
5034
|
const fields = collectSensitiveFields(endpoint);
|
|
5035
5035
|
if (fields.length === 0) return [];
|
|
5036
5036
|
const pathRx = pathRegex4(endpoint.path);
|
|
5037
|
-
const tag2 = `
|
|
5037
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
5038
5038
|
const h5 = stableHash(endpoint.method, endpoint.path);
|
|
5039
5039
|
const rules = [];
|
|
5040
5040
|
fields.forEach((field, i5) => {
|
|
5041
5041
|
const id = DATA_EXPOSURE_PII_BASE + (h5 * 17 + 3 + i5) % 999;
|
|
5042
5042
|
rules.push(
|
|
5043
5043
|
[
|
|
5044
|
-
`#
|
|
5044
|
+
`# x-security-generated response PII filter (id:428)`,
|
|
5045
5045
|
`# Source: ${endpoint.method} ${endpoint.path}, field: ${field}`,
|
|
5046
|
-
`SecRule REQUEST_FILENAME "@rx ${escRx6(pathRx)}" "id:${id},phase:4,deny,status:500,log,auditlog,msg:'
|
|
5046
|
+
`SecRule REQUEST_FILENAME "@rx ${escRx6(pathRx)}" "id:${id},phase:4,deny,status:500,log,auditlog,msg:'x-security id:428 data-exposure: response leaked sensitive field ${escMsec4(field)}',tag:'${escMsec4(tag2)}',tag:'x-security-data-exposure',chain"`,
|
|
5047
5047
|
` SecRule RESPONSE_BODY "@rx \\"${escMsec4(field)}\\"\\s*:\\s*\\"[^\\"]+\\"" "t:none"`
|
|
5048
5048
|
].join("\n")
|
|
5049
5049
|
);
|
|
@@ -5054,7 +5054,7 @@ function buildErrorScrubbingRules(endpoint) {
|
|
|
5054
5054
|
const scrub = endpoint.policy.response?.errorScrubbing;
|
|
5055
5055
|
if (!scrub) return [];
|
|
5056
5056
|
const pathRx = pathRegex4(endpoint.path);
|
|
5057
|
-
const tag2 = `
|
|
5057
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
5058
5058
|
const h5 = stableHash(endpoint.method, endpoint.path);
|
|
5059
5059
|
const rules = [];
|
|
5060
5060
|
let slot = 0;
|
|
@@ -5062,9 +5062,9 @@ function buildErrorScrubbingRules(endpoint) {
|
|
|
5062
5062
|
const id = OUTPUT_SANITIZE_BASE + (h5 * 31 + 7 + slot++) % 999;
|
|
5063
5063
|
rules.push(
|
|
5064
5064
|
[
|
|
5065
|
-
`#
|
|
5065
|
+
`# x-security-generated response error-scrubbing (id:268, stack traces)`,
|
|
5066
5066
|
`# Source: ${endpoint.method} ${endpoint.path}`,
|
|
5067
|
-
`SecRule REQUEST_FILENAME "@rx ${escRx6(pathRx)}" "id:${id},phase:4,deny,status:500,log,auditlog,msg:'
|
|
5067
|
+
`SecRule REQUEST_FILENAME "@rx ${escRx6(pathRx)}" "id:${id},phase:4,deny,status:500,log,auditlog,msg:'x-security id:268 output sanitization (stack trace leak)',tag:'${escMsec4(tag2)}',tag:'x-security-output-sanitization',chain"`,
|
|
5068
5068
|
// Common stack-frame markers across Python / Node / Java / Go.
|
|
5069
5069
|
// RE2-safe: only character classes + non-capturing groups + bounded \\d+.
|
|
5070
5070
|
` SecRule RESPONSE_BODY "@rx (?:Traceback \\(most recent call last\\)|Exception in thread|\\bat\\s+[\\w.]+\\.[\\w$<>]+\\(|goroutine\\s+\\d+\\s+\\[)" "t:none"`
|
|
@@ -5075,9 +5075,9 @@ function buildErrorScrubbingRules(endpoint) {
|
|
|
5075
5075
|
const id = OUTPUT_SANITIZE_BASE + (h5 * 31 + 7 + slot++) % 999;
|
|
5076
5076
|
rules.push(
|
|
5077
5077
|
[
|
|
5078
|
-
`#
|
|
5078
|
+
`# x-security-generated response error-scrubbing (id:268, generic messages)`,
|
|
5079
5079
|
`# Source: ${endpoint.method} ${endpoint.path}`,
|
|
5080
|
-
`SecRule REQUEST_FILENAME "@rx ${escRx6(pathRx)}" "id:${id},phase:4,deny,status:500,log,auditlog,msg:'
|
|
5080
|
+
`SecRule REQUEST_FILENAME "@rx ${escRx6(pathRx)}" "id:${id},phase:4,deny,status:500,log,auditlog,msg:'x-security id:268 output sanitization (raw error leak)',tag:'${escMsec4(tag2)}',tag:'x-security-output-sanitization',chain"`,
|
|
5081
5081
|
` SecRule RESPONSE_BODY "@rx (?i)(?:syntax error near|ORA-\\d+|ER_\\w+|psycopg2\\.|SQLSTATE|\\bENOENT\\b|undefined method|NullPointerException|panic:\\s)" "t:none"`
|
|
5082
5082
|
].join("\n")
|
|
5083
5083
|
);
|
|
@@ -5139,7 +5139,7 @@ function buildRequestSchemaRules(endpoint) {
|
|
|
5139
5139
|
const ctx = {
|
|
5140
5140
|
endpoint,
|
|
5141
5141
|
base: ruleBase(endpoint),
|
|
5142
|
-
tag: `
|
|
5142
|
+
tag: `x-security/${endpoint.method} ${endpoint.path}`
|
|
5143
5143
|
};
|
|
5144
5144
|
return buildSchemaRules(ctx, schema, MODSEC_NGINX_PROFILE);
|
|
5145
5145
|
}
|
|
@@ -5204,7 +5204,7 @@ function sinkRule(sink) {
|
|
|
5204
5204
|
operator: "@rx (?i)(?:ignore\\s+(?:all\\s+)?(?:previous|prior|above)\\s+instructions|disregard\\s+(?:the\\s+)?(?:previous|above|system)|system\\s+prompt|you\\s+are\\s+now\\s+|act\\s+as\\s+(?:a\\s+)?(?:dan|developer\\s+mode)|reveal\\s+(?:your\\s+)?(?:system\\s+)?prompt|new\\s+instructions?\\s*:)",
|
|
5205
5205
|
msg: "LLM prompt injection",
|
|
5206
5206
|
slot: 8,
|
|
5207
|
-
attrTag: "
|
|
5207
|
+
attrTag: "x-security-ssec-prompt"
|
|
5208
5208
|
};
|
|
5209
5209
|
default:
|
|
5210
5210
|
return null;
|
|
@@ -5213,7 +5213,7 @@ function sinkRule(sink) {
|
|
|
5213
5213
|
function buildInjectionGuardRules(endpoint) {
|
|
5214
5214
|
const schema = endpoint.policy.request?.schema;
|
|
5215
5215
|
if (!schema || Object.keys(schema).length === 0) return [];
|
|
5216
|
-
const tag2 = `
|
|
5216
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
5217
5217
|
const rx = pathRegex(endpoint.path);
|
|
5218
5218
|
const out = [];
|
|
5219
5219
|
for (const [field, ps] of Object.entries(schema)) {
|
|
@@ -5225,16 +5225,16 @@ function buildInjectionGuardRules(endpoint) {
|
|
|
5225
5225
|
const seed = endpointHash3(`${endpoint.method}|${endpoint.path}|${field}`, "");
|
|
5226
5226
|
const id = INJECTION_GUARD_BASE_ID2 + seed % 1e3 * 9 + def.slot;
|
|
5227
5227
|
const selector = `ARGS:json.${field}|ARGS:${field}`;
|
|
5228
|
-
const attrTag = def.attrTag ?? "
|
|
5229
|
-
const attrName = attrTag === "
|
|
5228
|
+
const attrTag = def.attrTag ?? "x-security-ssec-injection";
|
|
5229
|
+
const attrName = attrTag === "x-security-ssec-prompt" ? "SSEC-PROMPT" : "SSEC-INJECTION";
|
|
5230
5230
|
out.push(
|
|
5231
5231
|
[
|
|
5232
5232
|
header12(
|
|
5233
5233
|
`W19 injectionGuard[${sink}] on request.schema.${field} for ${endpoint.method} ${endpoint.path}
|
|
5234
5234
|
Native libmodsec3 operator on the declared sink field. Attributed to
|
|
5235
|
-
${attrName} (
|
|
5235
|
+
${attrName} (x-security-native), not an OWASP-API cell.`
|
|
5236
5236
|
),
|
|
5237
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:403,msg:'
|
|
5237
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:403,msg:'x-security: ${esc11(def.msg)} in ${esc11(field)}',tag:'${esc11(tag2)}',tag:'${esc11(attrTag)}',chain"`,
|
|
5238
5238
|
` SecRule REQUEST_FILENAME "@rx ${escRx7(rx)}" "chain"`,
|
|
5239
5239
|
` SecRule ${selector} "${escRx7(def.operator)}"${CHAIN_TERM}`
|
|
5240
5240
|
].join("\n")
|
|
@@ -5254,7 +5254,7 @@ function redirectHostRx(domain) {
|
|
|
5254
5254
|
function buildRedirectAllowlistRules(endpoint) {
|
|
5255
5255
|
const schema = endpoint.policy.request?.schema;
|
|
5256
5256
|
if (!schema || Object.keys(schema).length === 0) return [];
|
|
5257
|
-
const tag2 = `
|
|
5257
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
5258
5258
|
const rx = pathRegex(endpoint.path);
|
|
5259
5259
|
const out = [];
|
|
5260
5260
|
for (const [field, ps] of Object.entries(schema)) {
|
|
@@ -5270,7 +5270,7 @@ function buildRedirectAllowlistRules(endpoint) {
|
|
|
5270
5270
|
header12(
|
|
5271
5271
|
`S-15 open-redirect: ${field} must match redirectAllowedDomains for ${endpoint.method} ${endpoint.path}`
|
|
5272
5272
|
),
|
|
5273
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:1,deny,status:403,msg:'
|
|
5273
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:1,deny,status:403,msg:'x-security: ${esc11(field)} redirect target not in redirectAllowedDomains',tag:'${esc11(tag2)}',tag:'x-security-rule-open-redirect-403',chain"`,
|
|
5274
5274
|
` SecRule REQUEST_FILENAME "@rx ${escRx7(rx)}" "chain"`,
|
|
5275
5275
|
` SecRule ARGS:${field}|ARGS:json.${field} "!@rx ${escRx7(allowRx)}"${CHAIN_TERM}`
|
|
5276
5276
|
].join("\n")
|
|
@@ -5284,7 +5284,7 @@ function buildXxeRules(endpoint) {
|
|
|
5284
5284
|
const disallowXml = req.disallowXml === true;
|
|
5285
5285
|
const disableEntities = req.disableExternalEntities === true;
|
|
5286
5286
|
if (!disallowXml && !disableEntities) return [];
|
|
5287
|
-
const tag2 = `
|
|
5287
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
5288
5288
|
const rx = pathRegex(endpoint.path);
|
|
5289
5289
|
const seed = endpointHash3(`${endpoint.method}|${endpoint.path}|xxe`, "");
|
|
5290
5290
|
const id = XXE_BASE_ID + seed % 9e3;
|
|
@@ -5295,7 +5295,7 @@ function buildXxeRules(endpoint) {
|
|
|
5295
5295
|
`S-5 XXE: reject XML body for ${endpoint.method} ${endpoint.path}
|
|
5296
5296
|
${reason}`
|
|
5297
5297
|
),
|
|
5298
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:1,deny,status:415,msg:'
|
|
5298
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:1,deny,status:415,msg:'x-security: ${esc11(reason)}',tag:'${esc11(tag2)}',tag:'x-security-rule-xxe-415',chain"`,
|
|
5299
5299
|
` SecRule REQUEST_FILENAME "@rx ${escRx7(rx)}" "chain"`,
|
|
5300
5300
|
` SecRule REQUEST_HEADERS:Content-Type "@rx ${escRx7(XML_CONTENT_TYPE_RX)}"${CHAIN_TERM}`
|
|
5301
5301
|
].join("\n")
|
|
@@ -5303,7 +5303,7 @@ ${reason}`
|
|
|
5303
5303
|
}
|
|
5304
5304
|
function buildPathCanonicalizationRules(endpoint) {
|
|
5305
5305
|
if (endpoint.policy.request?.pathCanonicalization !== true) return [];
|
|
5306
|
-
const tag2 = `
|
|
5306
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
5307
5307
|
const seed = endpointHash3(`${endpoint.method}|${endpoint.path}|pathcanon`, "");
|
|
5308
5308
|
const id = PATH_CANON_BASE_ID + seed % 9e3;
|
|
5309
5309
|
return [
|
|
@@ -5313,7 +5313,7 @@ function buildPathCanonicalizationRules(endpoint) {
|
|
|
5313
5313
|
Denies traversal / double-slash / percent-encoded separators so the
|
|
5314
5314
|
canonical path every later SecRule matches against can't be bypassed.`
|
|
5315
5315
|
),
|
|
5316
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:1,deny,status:400,msg:'
|
|
5316
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:1,deny,status:400,msg:'x-security: non-canonical request path',tag:'${esc11(tag2)}',tag:'x-security-rule-path-canon-400',chain"`,
|
|
5317
5317
|
` SecRule REQUEST_URI "@rx ${escRx7(PATH_TRAVERSAL_RX)}"${CHAIN_TERM}`
|
|
5318
5318
|
].join("\n")
|
|
5319
5319
|
];
|
|
@@ -5347,7 +5347,7 @@ function header13(comment) {
|
|
|
5347
5347
|
function buildPasswordPolicyRules2(endpoint) {
|
|
5348
5348
|
const pol = endpoint.policy.authentication?.passwordPolicy;
|
|
5349
5349
|
if (!pol) return [];
|
|
5350
|
-
const tag2 = `
|
|
5350
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
5351
5351
|
const rx = pathRegex(endpoint.path);
|
|
5352
5352
|
const seedBase = endpointHash3(`${endpoint.method}|${endpoint.path}|pwpolicy`, "");
|
|
5353
5353
|
const checks = [];
|
|
@@ -5374,7 +5374,7 @@ function buildPasswordPolicyRules2(endpoint) {
|
|
|
5374
5374
|
Body-carried password strength (API2:2023). libmodsec3 PCRE !@rx on the
|
|
5375
5375
|
password field \u2014 rejects a present-but-weak password.`
|
|
5376
5376
|
),
|
|
5377
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:422,log,msg:'
|
|
5377
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:422,log,msg:'x-security: ${esc12(c5.reason)}',tag:'${esc12(tag2)}',tag:'x-security-rule-password-policy',chain"`,
|
|
5378
5378
|
` SecRule REQUEST_FILENAME "@rx ${escRx8(rx)}" "chain"`,
|
|
5379
5379
|
` SecRule ${selector} "!@rx ${escRx8(c5.rx)}"${CHAIN_TERM2}`
|
|
5380
5380
|
].join("\n")
|
|
@@ -5388,7 +5388,7 @@ password field \u2014 rejects a present-but-weak password.`
|
|
|
5388
5388
|
header13(
|
|
5389
5389
|
`v0.7 passwordPolicy: blocklisted password for ${endpoint.method} ${endpoint.path}`
|
|
5390
5390
|
),
|
|
5391
|
-
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:422,log,msg:'
|
|
5391
|
+
`SecRule REQUEST_METHOD "@streq ${endpoint.method}" "id:${id},phase:2,deny,status:422,log,msg:'x-security: password is on the blocklist',tag:'${esc12(tag2)}',tag:'x-security-rule-password-policy',chain"`,
|
|
5392
5392
|
` SecRule REQUEST_FILENAME "@rx ${escRx8(rx)}" "chain"`,
|
|
5393
5393
|
` SecRule ${selector} "@rx ${escRx8(`(?i)^(?:${alt})$`)}"${CHAIN_TERM2}`
|
|
5394
5394
|
].join("\n")
|
|
@@ -5407,7 +5407,7 @@ function lockoutVar2(identifier) {
|
|
|
5407
5407
|
function buildAccountLockoutRules2(endpoint) {
|
|
5408
5408
|
const lock = endpoint.policy.authentication?.accountLockout;
|
|
5409
5409
|
if (!lock) return [];
|
|
5410
|
-
const tag2 = `
|
|
5410
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
5411
5411
|
const rx = pathRegex(endpoint.path);
|
|
5412
5412
|
const window2 = parseDurationSec(lock.window) || 900;
|
|
5413
5413
|
const { expansion, collectKeyPhase } = lockoutVar2(lock.identifier);
|
|
@@ -5428,7 +5428,7 @@ identifier source is populated); increment at phase:5 on a >=400 auth response.`
|
|
|
5428
5428
|
`SecRule REQUEST_FILENAME "@rx ${escRx8(rx)}" "id:${base},phase:${keyPhase},pass,nolog,tag:'${esc12(tag2)}',${initcol},chain"`,
|
|
5429
5429
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}"${CHAIN_TERM2}`,
|
|
5430
5430
|
// 2. Deny while locked out — counter already over budget.
|
|
5431
|
-
`SecRule REQUEST_FILENAME "@rx ${escRx8(rx)}" "id:${base + 1},phase:${keyPhase},deny,status:429,log,msg:'
|
|
5431
|
+
`SecRule REQUEST_FILENAME "@rx ${escRx8(rx)}" "id:${base + 1},phase:${keyPhase},deny,status:429,log,msg:'x-security: account locked (>${lock.attempts} failed logins / ${esc12(lock.window)})',tag:'${esc12(tag2)}',tag:'x-security-rule-account-lockout',chain"`,
|
|
5432
5432
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}" "chain"`,
|
|
5433
5433
|
` SecRule GLOBAL:${counterKey} "@gt ${lock.attempts}"${CHAIN_TERM2}`,
|
|
5434
5434
|
// 3. On a failed-auth response (>=400), increment + refresh the TTL (phase:5).
|
|
@@ -5440,7 +5440,7 @@ identifier source is populated); increment at phase:5 on a >=400 auth response.`
|
|
|
5440
5440
|
}
|
|
5441
5441
|
function buildForbidArrayRootRules2(endpoint) {
|
|
5442
5442
|
if (endpoint.policy.response?.forbidArrayRoot !== true) return [];
|
|
5443
|
-
const tag2 = `
|
|
5443
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
5444
5444
|
const rx = pathRegex(endpoint.path);
|
|
5445
5445
|
const seed = endpointHash3(`${endpoint.method}|${endpoint.path}|arrayroot`, "");
|
|
5446
5446
|
const id = FORBID_ARRAY_ROOT_BASE_ID2 + seed % 999;
|
|
@@ -5451,7 +5451,7 @@ function buildForbidArrayRootRules2(endpoint) {
|
|
|
5451
5451
|
JSON-hijacking defense (API3:2023). phase:4 RESPONSE_BODY @rx on the first
|
|
5452
5452
|
non-whitespace byte; an array-rooted body is denied (wrap in an object instead).`
|
|
5453
5453
|
),
|
|
5454
|
-
`SecRule REQUEST_FILENAME "@rx ${escRx8(rx)}" "id:${id},phase:4,deny,status:500,log,auditlog,msg:'
|
|
5454
|
+
`SecRule REQUEST_FILENAME "@rx ${escRx8(rx)}" "id:${id},phase:4,deny,status:500,log,auditlog,msg:'x-security: bare top-level JSON array response (forbidArrayRoot)',tag:'${esc12(tag2)}',tag:'x-security-rule-forbid-array-root',chain"`,
|
|
5455
5455
|
` SecRule RESPONSE_BODY "@rx ^[\\s\\xef\\xbb\\xbf]*\\[" "t:none"`
|
|
5456
5456
|
].join("\n")
|
|
5457
5457
|
];
|
|
@@ -5459,7 +5459,7 @@ non-whitespace byte; an array-rooted body is denied (wrap in an object instead).
|
|
|
5459
5459
|
function buildIdempotencyKeyRules2(endpoint) {
|
|
5460
5460
|
const idem = endpoint.policy.request?.idempotencyKey;
|
|
5461
5461
|
if (!idem) return [];
|
|
5462
|
-
const tag2 = `
|
|
5462
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
5463
5463
|
const rx = pathRegex(endpoint.path);
|
|
5464
5464
|
const ttl = parseDurationSec(idem.ttl) || 300;
|
|
5465
5465
|
const seed = endpointHash3(`${endpoint.method}|${endpoint.path}|idempotency`, "");
|
|
@@ -5476,7 +5476,7 @@ request replay via a persistent-collection seen-count, NOT concurrent in-
|
|
|
5476
5476
|
flight races (no atomic check-and-set at the WAF; handle that in the store).`
|
|
5477
5477
|
),
|
|
5478
5478
|
// 1. Require the idempotency-key header to be present at all.
|
|
5479
|
-
`SecRule REQUEST_FILENAME "@rx ${escRx8(rx)}" "id:${base},phase:1,deny,status:400,log,msg:'
|
|
5479
|
+
`SecRule REQUEST_FILENAME "@rx ${escRx8(rx)}" "id:${base},phase:1,deny,status:400,log,msg:'x-security: missing ${esc12(idem.header)} header',tag:'${esc12(tag2)}',tag:'x-security-rule-idempotency-key',chain"`,
|
|
5480
5480
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}" "chain"`,
|
|
5481
5481
|
` SecRule &REQUEST_HEADERS:${esc12(idem.header)} "@eq 0"${CHAIN_TERM2}`,
|
|
5482
5482
|
// 2. Open the per-key persistent collection.
|
|
@@ -5486,7 +5486,7 @@ flight races (no atomic check-and-set at the WAF; handle that in the store).`
|
|
|
5486
5486
|
`SecRule REQUEST_FILENAME "@rx ${escRx8(rx)}" "id:${base + 2},phase:1,pass,nolog,tag:'${esc12(tag2)}',setvar:global.${counterKey}=+1,expirevar:global.${counterKey}=${ttl},chain"`,
|
|
5487
5487
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}"${CHAIN_TERM2}`,
|
|
5488
5488
|
// 4. Deny the replay — second+ request with this key inside the ttl window.
|
|
5489
|
-
`SecRule REQUEST_FILENAME "@rx ${escRx8(rx)}" "id:${base + 3},phase:1,deny,status:409,log,msg:'
|
|
5489
|
+
`SecRule REQUEST_FILENAME "@rx ${escRx8(rx)}" "id:${base + 3},phase:1,deny,status:409,log,msg:'x-security: replayed idempotency key (${esc12(idem.header)})',tag:'${esc12(tag2)}',tag:'x-security-rule-idempotency-key',chain"`,
|
|
5490
5490
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}" "chain"`,
|
|
5491
5491
|
` SecRule GLOBAL:${counterKey} "@gt 1"${CHAIN_TERM2}`
|
|
5492
5492
|
].join("\n")
|
|
@@ -5495,7 +5495,7 @@ flight races (no atomic check-and-set at the WAF; handle that in the store).`
|
|
|
5495
5495
|
function buildLoggingRules2(endpoint) {
|
|
5496
5496
|
const log = endpoint.policy.logging;
|
|
5497
5497
|
if (!log || !Array.isArray(log.events) || log.events.length === 0) return [];
|
|
5498
|
-
const tag2 = `
|
|
5498
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
5499
5499
|
const rx = pathRegex(endpoint.path);
|
|
5500
5500
|
const seed = endpointHash3(`${endpoint.method}|${endpoint.path}|logging`, "");
|
|
5501
5501
|
const id = LOGGING_BASE_ID2 + seed % 999;
|
|
@@ -5504,7 +5504,7 @@ function buildLoggingRules2(endpoint) {
|
|
|
5504
5504
|
const notes = [];
|
|
5505
5505
|
notes.push(`events declared: ${log.events.join(", ")}`);
|
|
5506
5506
|
notes.push(`auth-failure / authz-deny / injection-block / rate-limit-trip are`);
|
|
5507
|
-
notes.push(`already audit-logged by the corresponding
|
|
5507
|
+
notes.push(`already audit-logged by the corresponding x-security deny rules.`);
|
|
5508
5508
|
if (log.sink && log.sink !== "stdout") {
|
|
5509
5509
|
notes.push(`sink='${log.sink}' is NOT enforced at libmodsec3 \u2014 configure it at`);
|
|
5510
5510
|
notes.push(`the nginx/bw-scheduler log-shipping layer (syslog/fluent-bit sidecar).`);
|
|
@@ -5523,7 +5523,7 @@ function buildLoggingRules2(endpoint) {
|
|
|
5523
5523
|
`v0.7 logging (SSEC-AUDIT): force audit log for ${endpoint.method} ${endpoint.path}
|
|
5524
5524
|
` + notes.join("\n")
|
|
5525
5525
|
),
|
|
5526
|
-
`SecRule REQUEST_FILENAME "@rx ${escRx8(rx)}" "id:${id},phase:5,pass,log,auditlog,msg:'
|
|
5526
|
+
`SecRule REQUEST_FILENAME "@rx ${escRx8(rx)}" "id:${id},phase:5,pass,log,auditlog,msg:'x-security: audit (request/response) for ${esc12(endpoint.method)} ${esc12(endpoint.path)}',tag:'${esc12(tag2)}',tag:'x-security-audit',chain"`,
|
|
5527
5527
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}"${CHAIN_TERM2}`
|
|
5528
5528
|
].join("\n")
|
|
5529
5529
|
);
|
|
@@ -5570,7 +5570,7 @@ function operationsWithAuthz(ops) {
|
|
|
5570
5570
|
function buildGraphqlOperationAuthzRules(endpoint) {
|
|
5571
5571
|
const ops = operationsWithAuthz(endpoint.policy.graphql?.operations);
|
|
5572
5572
|
if (ops.length === 0) return [];
|
|
5573
|
-
const tag2 = `
|
|
5573
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
5574
5574
|
const rx = pathRegex(endpoint.path);
|
|
5575
5575
|
const seed = endpointHash3(`${endpoint.method}|${endpoint.path}|gqlops`, "");
|
|
5576
5576
|
const id = GRAPHQL_OPS_BASE_ID + seed % 999;
|
|
@@ -5595,7 +5595,7 @@ Per-operation authz contract the processor MUST enforce:
|
|
|
5595
5595
|
// Route marker: pass-through (NOT a deny) so we never claim enforcement
|
|
5596
5596
|
// we don't have. The tag lets a downstream ext-filter / sidecar pick up
|
|
5597
5597
|
// the request; on its own it changes nothing about the response.
|
|
5598
|
-
`SecRule REQUEST_FILENAME "@rx ${escRx9(rx)}" "id:${id},phase:2,pass,nolog,msg:'
|
|
5598
|
+
`SecRule REQUEST_FILENAME "@rx ${escRx9(rx)}" "id:${id},phase:2,pass,nolog,msg:'x-security: graphql per-operation authz handoff (override-only)',tag:'${esc13(tag2)}',tag:'x-security-graphql-ops-authz',tag:'x-security-override-only',chain"`,
|
|
5599
5599
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}"${CHAIN_TERM3}`
|
|
5600
5600
|
].join("\n")
|
|
5601
5601
|
];
|
|
@@ -5603,7 +5603,7 @@ Per-operation authz contract the processor MUST enforce:
|
|
|
5603
5603
|
function buildGraphqlStaticLimitRules2(endpoint) {
|
|
5604
5604
|
const g5 = endpoint.policy.graphql;
|
|
5605
5605
|
if (!g5) return [];
|
|
5606
|
-
const tag2 = `
|
|
5606
|
+
const tag2 = `x-security/${endpoint.method} ${endpoint.path}`;
|
|
5607
5607
|
const rx = pathRegex(endpoint.path);
|
|
5608
5608
|
const seed = endpointHash3(`${endpoint.method}|${endpoint.path}|gqlstatic`, "");
|
|
5609
5609
|
const base = GRAPHQL_STATIC_BASE_ID + seed % 498 * 2;
|
|
@@ -5621,7 +5621,7 @@ function buildGraphqlStaticLimitRules2(endpoint) {
|
|
|
5621
5621
|
an introspection query MUST contain those meta-fields, so this is a real,
|
|
5622
5622
|
non-parsing block (not a heuristic guess).`
|
|
5623
5623
|
),
|
|
5624
|
-
`SecRule REQUEST_FILENAME "@rx ${escRx9(rx)}" "id:${base},phase:2,deny,status:403,log,msg:'
|
|
5624
|
+
`SecRule REQUEST_FILENAME "@rx ${escRx9(rx)}" "id:${base},phase:2,deny,status:403,log,msg:'x-security: GraphQL introspection disabled (graphql.disableIntrospection)',tag:'${esc13(tag2)}',tag:'x-security-rule-graphql-introspection',chain"`,
|
|
5625
5625
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}" "chain"`,
|
|
5626
5626
|
` SecRule REQUEST_BODY "@rx (?i)__(?:schema|type)\\b"${CHAIN_TERM3}`
|
|
5627
5627
|
].join("\n")
|
|
@@ -5637,7 +5637,7 @@ parser, so we reject any top-level JSON-array body (batched GraphQL is
|
|
|
5637
5637
|
[{...},{...}]). This caps batching at "none" \u2014 stricter than the numeric
|
|
5638
5638
|
limit, not the exact semantics, hence partial.`
|
|
5639
5639
|
),
|
|
5640
|
-
`SecRule REQUEST_FILENAME "@rx ${escRx9(rx)}" "id:${base + 1},phase:2,deny,status:403,log,msg:'
|
|
5640
|
+
`SecRule REQUEST_FILENAME "@rx ${escRx9(rx)}" "id:${base + 1},phase:2,deny,status:403,log,msg:'x-security: GraphQL request batching rejected (graphql.batchLimit)',tag:'${esc13(tag2)}',tag:'x-security-rule-graphql-batch',chain"`,
|
|
5641
5641
|
` SecRule REQUEST_METHOD "@streq ${endpoint.method}" "chain"`,
|
|
5642
5642
|
` SecRule REQUEST_BODY "@rx ^[\\s]*\\[" "t:none"`
|
|
5643
5643
|
].join("\n")
|
|
@@ -5651,7 +5651,7 @@ ${unenforceable.join(", ")} require GraphQL query-depth/complexity/alias
|
|
|
5651
5651
|
counting, which is NOT regular-language-expressible \u2014 libmodsec3 cannot
|
|
5652
5652
|
enforce it without a parser. Wire a GraphQL-aware processor (the same
|
|
5653
5653
|
sidecar the graphql.operations.authz scaffolding routes to) to enforce
|
|
5654
|
-
these.
|
|
5654
|
+
these. x-security does NOT fake them with regex (Rule D-1).`
|
|
5655
5655
|
)
|
|
5656
5656
|
);
|
|
5657
5657
|
}
|
|
@@ -5673,7 +5673,7 @@ function buildSerializeByHttpSnippet(endpoint, index) {
|
|
|
5673
5673
|
const limit = endpoint.policy.request?.concurrencyLimit;
|
|
5674
5674
|
const conc = typeof limit === "number" && limit >= 1 ? limit : 1;
|
|
5675
5675
|
const scope = ser.scope ?? "per-identifier";
|
|
5676
|
-
const keyVar = scope === "global" ? `"
|
|
5676
|
+
const keyVar = scope === "global" ? `"x_security_serial_global"` : serializeKeyToNginxVar(ser.key);
|
|
5677
5677
|
const zoneName = `ss_serial_${index}`;
|
|
5678
5678
|
const snippet = `# v0.8 request.serializeBy (API6:2023) for ${endpoint.method} ${endpoint.path}
|
|
5679
5679
|
# PARTIAL \u2014 edge serialization only; does NOT provide in-handler transaction
|
|
@@ -5693,7 +5693,7 @@ function buildDataAtRestRules(endpoint) {
|
|
|
5693
5693
|
ADVISORY-ONLY \u2014 NOT gateway-enforced. fields=[${dar.fields.join(", ")}] must be
|
|
5694
5694
|
'${dar.protection}' at rest, but the WAF never sees the datastore write, so this
|
|
5695
5695
|
compiles to NOTHING enforcing. Implement at-rest ${dar.protection} in the app's
|
|
5696
|
-
persistence layer.
|
|
5696
|
+
persistence layer. x-security surfaces this as an out-of-band SSEC-STORAGE
|
|
5697
5697
|
finding, not a control (Rule D-1: no fake 'full' for an unenforceable field).`
|
|
5698
5698
|
)
|
|
5699
5699
|
];
|
|
@@ -5814,7 +5814,7 @@ function bunkerwebCapabilities() {
|
|
|
5814
5814
|
// emits a native libmodsec3 operator per declared sink. v0.7 adds two
|
|
5815
5815
|
// sinks that ride the SAME cell — both are real @rx denylists on the
|
|
5816
5816
|
// field (deserialization preamble denylist; ai-prompt heuristic denylist,
|
|
5817
|
-
// tagged
|
|
5817
|
+
// tagged x-security-ssec-prompt) → cell stays 'full'.
|
|
5818
5818
|
// v0.7 (API2): authentication.passwordPolicy — phase:2 !@rx strength
|
|
5819
5819
|
// SecRules (minLength/upper/digit/symbol/blocklist) on the body password
|
|
5820
5820
|
// field. Real per-rule enforcement on a present password → full.
|
|
@@ -5880,7 +5880,7 @@ function bunkerwebCapabilities() {
|
|
|
5880
5880
|
"request.dataAtRest": "unsupported",
|
|
5881
5881
|
"mtls.pinnedCertificates": "unsupported",
|
|
5882
5882
|
// Promoted: deprecated emits phase:1 SecRule returning 410 with
|
|
5883
|
-
//
|
|
5883
|
+
// x-security-deprecated-endpoint-block tag (attribution.py:35).
|
|
5884
5884
|
"deprecated": "full",
|
|
5885
5885
|
"sunsetDate": "partial"
|
|
5886
5886
|
}
|
|
@@ -5943,8 +5943,8 @@ function detectMixedJwtAudienceIssuer(per) {
|
|
|
5943
5943
|
const audiences = /* @__PURE__ */ new Set();
|
|
5944
5944
|
const issuers = /* @__PURE__ */ new Set();
|
|
5945
5945
|
for (const { settings } of per) {
|
|
5946
|
-
if (settings.
|
|
5947
|
-
if (settings.
|
|
5946
|
+
if (settings.X_SECURITY_AUTH_AUDIENCE !== void 0) audiences.add(String(settings.X_SECURITY_AUTH_AUDIENCE));
|
|
5947
|
+
if (settings.X_SECURITY_AUTH_ISSUER !== void 0) issuers.add(String(settings.X_SECURITY_AUTH_ISSUER));
|
|
5948
5948
|
}
|
|
5949
5949
|
const errors = [];
|
|
5950
5950
|
if (audiences.size > 1) errors.push(`mixed JWT audience values across service: ${Array.from(audiences).join(", ")}`);
|
|
@@ -6181,8 +6181,8 @@ function groupByService(spec) {
|
|
|
6181
6181
|
}
|
|
6182
6182
|
function serializeModsecConf(spec, service) {
|
|
6183
6183
|
const lines = [];
|
|
6184
|
-
lines.push(`# Generated by
|
|
6185
|
-
lines.push("# Phase-1/phase-2
|
|
6184
|
+
lines.push(`# Generated by x-security from ${spec.info.title} v${spec.info.version}`);
|
|
6185
|
+
lines.push("# Phase-1/phase-2 x-security ModSecurity rules.");
|
|
6186
6186
|
lines.push("# Place this file under bw-scheduler's /data/configs/modsec/ volume.");
|
|
6187
6187
|
lines.push("# Do not edit by hand \u2014 regenerate via `lazy generate --target bunkerweb`.");
|
|
6188
6188
|
lines.push("");
|
|
@@ -6190,10 +6190,10 @@ function serializeModsecConf(spec, service) {
|
|
|
6190
6190
|
([k5]) => /^CUSTOM_CONF_MODSEC_\d+$/.test(k5)
|
|
6191
6191
|
);
|
|
6192
6192
|
const ssrfBlocks = modsecEntries.filter(
|
|
6193
|
-
([, v]) => String(v).includes("
|
|
6193
|
+
([, v]) => String(v).includes("x-security-rule-ssrf")
|
|
6194
6194
|
);
|
|
6195
6195
|
const otherBlocks = modsecEntries.filter(
|
|
6196
|
-
([, v]) => !String(v).includes("
|
|
6196
|
+
([, v]) => !String(v).includes("x-security-rule-ssrf")
|
|
6197
6197
|
);
|
|
6198
6198
|
for (const [key, val] of [...ssrfBlocks, ...otherBlocks]) {
|
|
6199
6199
|
const origins = service.provenance.get(key) ?? [];
|
|
@@ -6231,7 +6231,7 @@ function serializeModsecConf(spec, service) {
|
|
|
6231
6231
|
}
|
|
6232
6232
|
function serializeDeploymentMd(spec, warnings) {
|
|
6233
6233
|
const lines = [
|
|
6234
|
-
"#
|
|
6234
|
+
"# x-security \u2192 BunkerWeb deployment notes",
|
|
6235
6235
|
"",
|
|
6236
6236
|
`Generated from ${spec.info.title} v${spec.info.version}.`,
|
|
6237
6237
|
"",
|
|
@@ -6257,11 +6257,11 @@ function serializeDeploymentMd(spec, warnings) {
|
|
|
6257
6257
|
" Settings like `USE_MODSECURITY`, `ALLOWED_METHODS`, rate-limit thresholds, etc.",
|
|
6258
6258
|
" belong in your **docker-compose.yml** as per-service environment variables",
|
|
6259
6259
|
" (`<SERVICE>_<KEY>=<value>` in multisite mode). See the comment block at the",
|
|
6260
|
-
" bottom of `configs/modsec/
|
|
6260
|
+
" bottom of `configs/modsec/x-security.conf` for the recommended values.",
|
|
6261
6261
|
"",
|
|
6262
6262
|
"## JWT signature validation",
|
|
6263
6263
|
"",
|
|
6264
|
-
"BunkerWeb 1.6+ ships `nginx_jwt_module`;
|
|
6264
|
+
"BunkerWeb 1.6+ ships `nginx_jwt_module`; x-security now emits the native",
|
|
6265
6265
|
"`USE_AUTH_JWT`, `JWT_JWKS_URI`, and `JWT_ALGORITHMS` settings for every",
|
|
6266
6266
|
"`bearer-jwt` / `oauth2` endpoint that declares `jwksUri`. The WAF-side",
|
|
6267
6267
|
"header-presence SecRule chain (id:990010/990011) stays as defense-in-depth.",
|
|
@@ -6274,17 +6274,17 @@ function serializeDeploymentMd(spec, warnings) {
|
|
|
6274
6274
|
"",
|
|
6275
6275
|
"## Per-user rate limits (rateLimit.identifier=user-id)",
|
|
6276
6276
|
"",
|
|
6277
|
-
"For endpoints declaring `rateLimit.identifier: user-id`,
|
|
6277
|
+
"For endpoints declaring `rateLimit.identifier: user-id`, x-security emits a",
|
|
6278
6278
|
"`CUSTOM_CONF_HTTP_LIMIT_REQ_USER_<n>` snippet that declares a dedicated",
|
|
6279
6279
|
"`limit_req_zone` keyed on `$http_x_forwarded_user`. Paste the snippet into",
|
|
6280
|
-
"`configs/http/
|
|
6280
|
+
"`configs/http/x-security-limit-user.conf` in your bw-scheduler volume, then",
|
|
6281
6281
|
"wire the matching `limit_req zone=lazy_user_<n> burst=<b> nodelay;` into the",
|
|
6282
6282
|
"per-location server block (BunkerWeb's `CUSTOM_CONF_SERVER_*` overrides).",
|
|
6283
6283
|
"",
|
|
6284
6284
|
"## Deprecated endpoints",
|
|
6285
6285
|
"",
|
|
6286
6286
|
"Endpoints with `deprecated: true` get a phase:1 SecRule returning **410 Gone**",
|
|
6287
|
-
"with tag `
|
|
6287
|
+
"with tag `x-security-deprecated-endpoint-block` (consumed by the scorer's",
|
|
6288
6288
|
"attribution table). `sunsetDate` is surfaced in the deny message; emit a",
|
|
6289
6289
|
"`Sunset:` response header via `CUSTOM_HEADER_*` if the client needs it.",
|
|
6290
6290
|
"",
|
|
@@ -6314,7 +6314,7 @@ var init_bunkerweb = __esm({
|
|
|
6314
6314
|
init_ssrf_policy_check();
|
|
6315
6315
|
init_rules();
|
|
6316
6316
|
init_profiles();
|
|
6317
|
-
MODSEC_BLOCK_MARKER = "#
|
|
6317
|
+
MODSEC_BLOCK_MARKER = "# x-security-generated authentication rules";
|
|
6318
6318
|
bunkerwebGenerator = {
|
|
6319
6319
|
name: "bunkerweb",
|
|
6320
6320
|
targets: ["bunkerweb"],
|
|
@@ -6333,7 +6333,7 @@ var init_bunkerweb = __esm({
|
|
|
6333
6333
|
const modsecBody = serializeModsecConf(spec, service);
|
|
6334
6334
|
const artifacts = [
|
|
6335
6335
|
{
|
|
6336
|
-
path: "configs/modsec/
|
|
6336
|
+
path: "configs/modsec/x-security.conf",
|
|
6337
6337
|
content: modsecBody,
|
|
6338
6338
|
format: "conf"
|
|
6339
6339
|
},
|
|
@@ -6595,7 +6595,7 @@ function buildDoc(spec, opts = {}) {
|
|
|
6595
6595
|
}
|
|
6596
6596
|
if (hosts.size === 0) hosts.add("*");
|
|
6597
6597
|
const specificRules = Array.from(hosts).map((host) => ({
|
|
6598
|
-
name: `
|
|
6598
|
+
name: `x-security-asset-${sanitizeName(host) || "default"}`,
|
|
6599
6599
|
host,
|
|
6600
6600
|
triggers: [DEFAULT_TRIGGER],
|
|
6601
6601
|
mode: "prevent-learn",
|
|
@@ -6615,7 +6615,7 @@ function buildDoc(spec, opts = {}) {
|
|
|
6615
6615
|
practices,
|
|
6616
6616
|
"log-triggers": [buildDefaultTrigger()],
|
|
6617
6617
|
"custom-responses": [buildDefaultResponse()],
|
|
6618
|
-
"
|
|
6618
|
+
"x-security-extended": {
|
|
6619
6619
|
"schema-validation": schemaValidations
|
|
6620
6620
|
}
|
|
6621
6621
|
};
|
|
@@ -6642,10 +6642,10 @@ var DEFAULT_PRACTICE, DEFAULT_RATE_LIMIT_PRACTICE, DEFAULT_TRIGGER, DEFAULT_RESP
|
|
|
6642
6642
|
var init_policy = __esm({
|
|
6643
6643
|
"src/generators/openappsec/policy.ts"() {
|
|
6644
6644
|
"use strict";
|
|
6645
|
-
DEFAULT_PRACTICE = "
|
|
6646
|
-
DEFAULT_RATE_LIMIT_PRACTICE = "
|
|
6647
|
-
DEFAULT_TRIGGER = "
|
|
6648
|
-
DEFAULT_RESPONSE = "
|
|
6645
|
+
DEFAULT_PRACTICE = "x-security-threat-prevention";
|
|
6646
|
+
DEFAULT_RATE_LIMIT_PRACTICE = "x-security-rate-limit";
|
|
6647
|
+
DEFAULT_TRIGGER = "x-security-log-trigger";
|
|
6648
|
+
DEFAULT_RESPONSE = "x-security-blocked-response";
|
|
6649
6649
|
BYTE_UNITS = {
|
|
6650
6650
|
B: 1,
|
|
6651
6651
|
KB: 1024,
|
|
@@ -6666,7 +6666,7 @@ var init_openappsec = __esm({
|
|
|
6666
6666
|
init_ssrf_policy_check();
|
|
6667
6667
|
openappsecLastWarnings = [];
|
|
6668
6668
|
HEADER = [
|
|
6669
|
-
"# Generated by
|
|
6669
|
+
"# Generated by x-security \u2014 DO NOT EDIT BY HAND.",
|
|
6670
6670
|
"# Target: open-appsec.io (flat local_policy.yaml format)",
|
|
6671
6671
|
"# Source: annotated OpenAPI spec (x-security)",
|
|
6672
6672
|
"#",
|
|
@@ -6676,7 +6676,7 @@ var init_openappsec = __esm({
|
|
|
6676
6676
|
"# practice as `openapi-schema-validation.files:`. Mount both files at",
|
|
6677
6677
|
"# the same path inside the agent container (default: /ext/appsec/).",
|
|
6678
6678
|
"# - Per-property `mitigates:` hints and the full property tree are also",
|
|
6679
|
-
"# preserved under `
|
|
6679
|
+
"# preserved under `x-security-extended:` for x-security drift detection",
|
|
6680
6680
|
"# (open-appsec ignores unknown top-level keys).",
|
|
6681
6681
|
"# - Authentication (JWT/OAuth), authorization (rule-based), and request",
|
|
6682
6682
|
"# HMAC signatures are NOT enforceable by open-appsec \u2014 it is an L7",
|
|
@@ -6688,7 +6688,7 @@ var init_openappsec = __esm({
|
|
|
6688
6688
|
"# declares the anti-bot block but does not configure a provider.",
|
|
6689
6689
|
"# - The ML model may block attacks not explicitly in our policy (its",
|
|
6690
6690
|
"# default web-attacks heuristics). Such blocks are open-appsec doing",
|
|
6691
|
-
"# its own job, NOT
|
|
6691
|
+
"# its own job, NOT x-security enforcement.",
|
|
6692
6692
|
""
|
|
6693
6693
|
].join("\n");
|
|
6694
6694
|
openappsecGenerator = {
|
|
@@ -6714,7 +6714,7 @@ var init_openappsec = __esm({
|
|
|
6714
6714
|
noRefs: true,
|
|
6715
6715
|
sortKeys: false
|
|
6716
6716
|
});
|
|
6717
|
-
const fragmentContent = "# Generated by
|
|
6717
|
+
const fragmentContent = "# Generated by x-security \u2014 DO NOT EDIT BY HAND.\n# OpenAPI schema fragment for open-appsec schema validation.\n# Referenced by practices[].openapi-schema-validation.files: in policy.yaml.\n# Mount this file at the path declared there (default: /ext/appsec/openapi-schema.yaml).\n" + fragmentYaml;
|
|
6718
6718
|
return [
|
|
6719
6719
|
{ path: "openappsec/policy.yaml", content: policyContent, format: "yaml" },
|
|
6720
6720
|
{ path: "openappsec/openapi-schema.yaml", content: fragmentContent, format: "yaml" }
|
|
@@ -6730,7 +6730,7 @@ var init_openappsec = __esm({
|
|
|
6730
6730
|
// needs baseline traffic to reach steady-state recall — under
|
|
6731
6731
|
// prevent-learn with no learned state, it lets some malformed
|
|
6732
6732
|
// payloads through and (b) per-property `mitigates:` hints are
|
|
6733
|
-
//
|
|
6733
|
+
// x-security-internal and not consumed by the agent.
|
|
6734
6734
|
"request.schema": "partial",
|
|
6735
6735
|
"request.contentType": "partial",
|
|
6736
6736
|
"request.maxBodySize": "partial",
|
|
@@ -6790,10 +6790,10 @@ var init_metadata_blocks = __esm({
|
|
|
6790
6790
|
|
|
6791
6791
|
// src/generators/firewall/iptables.ts
|
|
6792
6792
|
function provenanceComment(endpoint, field, label) {
|
|
6793
|
-
return `#
|
|
6793
|
+
return `# x-security: ${endpoint} ${field} -- ${label}`;
|
|
6794
6794
|
}
|
|
6795
6795
|
function blockRule(entry, uid, endpoint, field) {
|
|
6796
|
-
const inlineComment = `
|
|
6796
|
+
const inlineComment = `x-security/${endpoint}/${field}`;
|
|
6797
6797
|
return {
|
|
6798
6798
|
comment: provenanceComment(endpoint, field, entry.label),
|
|
6799
6799
|
line: `-A OUTPUT -d ${entry.cidr} -m owner --uid-owner ${uid} -m comment --comment "${inlineComment}" -j DROP`,
|
|
@@ -6801,10 +6801,10 @@ function blockRule(entry, uid, endpoint, field) {
|
|
|
6801
6801
|
};
|
|
6802
6802
|
}
|
|
6803
6803
|
function allowlistRule(fqdn, uid, endpoint, field) {
|
|
6804
|
-
const inlineComment = `
|
|
6804
|
+
const inlineComment = `x-security/${endpoint}/${field}/allow=${fqdn}`;
|
|
6805
6805
|
return {
|
|
6806
6806
|
comment: provenanceComment(endpoint, field, `allow ${fqdn} (resolved at deploy)`),
|
|
6807
|
-
line: `-A OUTPUT @@
|
|
6807
|
+
line: `-A OUTPUT @@X_SECURITY_RESOLVE:${fqdn}@@ -m owner --uid-owner ${uid} -m comment --comment "${inlineComment}" -j ACCEPT`,
|
|
6808
6808
|
family: "v4"
|
|
6809
6809
|
};
|
|
6810
6810
|
}
|
|
@@ -6860,11 +6860,11 @@ function buildIptablesV6(spec, opts = {}) {
|
|
|
6860
6860
|
function renderRuleset(rules, family, uid) {
|
|
6861
6861
|
const filtered = rules.filter((r5) => r5.family === family);
|
|
6862
6862
|
const header15 = [
|
|
6863
|
-
`# Generated by
|
|
6863
|
+
`# Generated by XSecurity \u2014 DO NOT EDIT.`,
|
|
6864
6864
|
`# Family: IP${family === "v4" ? "v4" : "v6"}`,
|
|
6865
6865
|
`# Apply with: ${family === "v4" ? "iptables-restore" : "ip6tables-restore"} < this-file`,
|
|
6866
|
-
`# Deploy wrapper must substitute @@
|
|
6867
|
-
`# and ${"${
|
|
6866
|
+
`# Deploy wrapper must substitute @@X_SECURITY_RESOLVE:<fqdn>@@ tokens`,
|
|
6867
|
+
`# and ${"${X_SECURITY_APP_UID}"} before applying.`,
|
|
6868
6868
|
`*filter`,
|
|
6869
6869
|
`:INPUT ACCEPT [0:0]`,
|
|
6870
6870
|
`:FORWARD ACCEPT [0:0]`,
|
|
@@ -6875,9 +6875,9 @@ function renderRuleset(rules, family, uid) {
|
|
|
6875
6875
|
body.push(rule.comment);
|
|
6876
6876
|
body.push(rule.line);
|
|
6877
6877
|
}
|
|
6878
|
-
body.push(`#
|
|
6878
|
+
body.push(`# x-security: * default-deny -- fail-closed terminator`);
|
|
6879
6879
|
body.push(
|
|
6880
|
-
`-A OUTPUT -m owner --uid-owner ${uid} -m comment --comment "
|
|
6880
|
+
`-A OUTPUT -m owner --uid-owner ${uid} -m comment --comment "x-security/default-deny" -j DROP`
|
|
6881
6881
|
);
|
|
6882
6882
|
return [...header15, ...body, `COMMIT`, ``].join("\n");
|
|
6883
6883
|
}
|
|
@@ -6886,7 +6886,7 @@ var init_iptables = __esm({
|
|
|
6886
6886
|
"src/generators/firewall/iptables.ts"() {
|
|
6887
6887
|
"use strict";
|
|
6888
6888
|
init_metadata_blocks();
|
|
6889
|
-
DEFAULT_UID = "${
|
|
6889
|
+
DEFAULT_UID = "${X_SECURITY_APP_UID}";
|
|
6890
6890
|
}
|
|
6891
6891
|
});
|
|
6892
6892
|
|
|
@@ -6908,28 +6908,28 @@ function read(name) {
|
|
|
6908
6908
|
function loadWrapperScripts() {
|
|
6909
6909
|
return [
|
|
6910
6910
|
{
|
|
6911
|
-
filename: "
|
|
6912
|
-
content: read("
|
|
6911
|
+
filename: "x-security-resolve.sh",
|
|
6912
|
+
content: read("x-security-resolve.sh"),
|
|
6913
6913
|
format: "text"
|
|
6914
6914
|
},
|
|
6915
6915
|
{
|
|
6916
|
-
filename: "
|
|
6917
|
-
content: read("
|
|
6916
|
+
filename: "x-security-refresh.sh",
|
|
6917
|
+
content: read("x-security-refresh.sh"),
|
|
6918
6918
|
format: "text"
|
|
6919
6919
|
},
|
|
6920
6920
|
{
|
|
6921
|
-
filename: "
|
|
6922
|
-
content: read("
|
|
6921
|
+
filename: "x-security-refresh.service",
|
|
6922
|
+
content: read("x-security-refresh.service"),
|
|
6923
6923
|
format: "conf"
|
|
6924
6924
|
},
|
|
6925
6925
|
{
|
|
6926
|
-
filename: "
|
|
6927
|
-
content: read("
|
|
6926
|
+
filename: "x-security-refresh.timer",
|
|
6927
|
+
content: read("x-security-refresh.timer"),
|
|
6928
6928
|
format: "conf"
|
|
6929
6929
|
},
|
|
6930
6930
|
{
|
|
6931
|
-
filename: "
|
|
6932
|
-
content: read("
|
|
6931
|
+
filename: "x-security.logrotate",
|
|
6932
|
+
content: read("x-security.logrotate"),
|
|
6933
6933
|
format: "conf"
|
|
6934
6934
|
},
|
|
6935
6935
|
{
|
|
@@ -7139,7 +7139,7 @@ var init_clusters = __esm({
|
|
|
7139
7139
|
"src/generators/envoy/templates/clusters.ts"() {
|
|
7140
7140
|
"use strict";
|
|
7141
7141
|
init_yaml_util();
|
|
7142
|
-
UPSTREAM_CLUSTER = "
|
|
7142
|
+
UPSTREAM_CLUSTER = "x_security_upstream";
|
|
7143
7143
|
JWKS_CLUSTER = "jwks_cluster";
|
|
7144
7144
|
}
|
|
7145
7145
|
});
|
|
@@ -7184,7 +7184,7 @@ function denyLiteral(ruleClass) {
|
|
|
7184
7184
|
"{",
|
|
7185
7185
|
` "allowed": false,`,
|
|
7186
7186
|
` "http_status": 403,`,
|
|
7187
|
-
` "headers": {"x-
|
|
7187
|
+
` "headers": {"x-x-security-rule": ${regoString(marker)}},`,
|
|
7188
7188
|
` "body": ${regoString(marker)}`,
|
|
7189
7189
|
" }"
|
|
7190
7190
|
].join("\n ");
|
|
@@ -7626,7 +7626,7 @@ function emitExtAuthzFilter(lines) {
|
|
|
7626
7626
|
lines.push(" timeout: 0.5s");
|
|
7627
7627
|
lines.push(" failure_mode_allow: false");
|
|
7628
7628
|
lines.push(" include_peer_certificate: false");
|
|
7629
|
-
lines.push(" # W17-A: forward OPA-emitted x-
|
|
7629
|
+
lines.push(" # W17-A: forward OPA-emitted x-x-security-rule marker downstream.");
|
|
7630
7630
|
lines.push(" with_request_body:");
|
|
7631
7631
|
lines.push(" max_request_bytes: 8192");
|
|
7632
7632
|
lines.push(" allow_partial_message: true");
|
|
@@ -7660,11 +7660,11 @@ function buildRegoPolicyInner(cfg) {
|
|
|
7660
7660
|
const ssrf = cfg.ssrfPolicy ?? [];
|
|
7661
7661
|
const abac = cfg.abac ?? [];
|
|
7662
7662
|
const lines = [];
|
|
7663
|
-
lines.push("#
|
|
7664
|
-
lines.push(`# generator:
|
|
7663
|
+
lines.push("# x-security \u2192 OPA \u2014 auto-generated. DO NOT EDIT BY HAND.");
|
|
7664
|
+
lines.push(`# generator: x-security-envoy/extauthz v${VERSION2}`);
|
|
7665
7665
|
lines.push(`# source: ${cfg.specTitle} ${cfg.specVersion}`);
|
|
7666
7666
|
lines.push("# Wave-10 E-3: rule-based authorization \u2192 ext_authz + OPA sidecar.");
|
|
7667
|
-
lines.push("# Wave-17 W17-A: structured decision object emits per-class x-
|
|
7667
|
+
lines.push("# Wave-17 W17-A: structured decision object emits per-class x-x-security-rule");
|
|
7668
7668
|
lines.push("# headers (opa-bola-403, opa-jwt-claim-403, opa-default-403) so the scorer's");
|
|
7669
7669
|
lines.push("# intent-attribution layer can map a denial back to the right defense-class.");
|
|
7670
7670
|
lines.push("# Wave-18 W18-A: + opa-bfla-403 (rbac admin-only routes, principal-role check)");
|
|
@@ -7787,13 +7787,13 @@ function buildResponseSchemaConfig(items, specTitle, specVersion) {
|
|
|
7787
7787
|
};
|
|
7788
7788
|
});
|
|
7789
7789
|
const cfg = {
|
|
7790
|
-
$comment: "
|
|
7791
|
-
generator: "
|
|
7790
|
+
$comment: "x-security \u2192 Envoy ext_proc response-schema contract. Auto-generated. DO NOT EDIT BY HAND. Consumed by an operator-supplied ext_proc gRPC ExternalProcessor that parses the response body as JSON and enforces these per-field typed constraints on the parsed value. x-security does NOT ship that processor; this file + the envoy.yaml ext_proc filter are scaffolding. No regex over raw bytes \u2014 the processor MUST JSON.parse.",
|
|
7791
|
+
generator: "x-security-envoy/ext_proc",
|
|
7792
7792
|
source: `${specTitle} ${specVersion}`,
|
|
7793
7793
|
enforcement: {
|
|
7794
7794
|
status: "override-only",
|
|
7795
7795
|
enforcedBy: `operator-supplied ext_proc gRPC ExternalProcessor (cluster ${EXT_PROC_CLUSTER}, host ${EXT_PROC_HOST}:${EXT_PROC_PORT})`,
|
|
7796
|
-
operatorMustSupply: "A gRPC service implementing envoy.service.ext_proc.v3.ExternalProcessor that JSON-parses the response body and evaluates the rules below.
|
|
7796
|
+
operatorMustSupply: "A gRPC service implementing envoy.service.ext_proc.v3.ExternalProcessor that JSON-parses the response body and evaluates the rules below. x-security does not bundle it; the OPA sidecar (opa_grpc) is ext_authz-only.",
|
|
7797
7797
|
dataPath: "Envoy ext_proc filter streams the response body (processing_mode: response_body_mode=BUFFERED) to the processor, which returns an ImmediateResponse(502) to block a schema-violating body or continues otherwise."
|
|
7798
7798
|
},
|
|
7799
7799
|
routes
|
|
@@ -7803,7 +7803,7 @@ function buildResponseSchemaConfig(items, specTitle, specVersion) {
|
|
|
7803
7803
|
function emitExtProcFilter(lines) {
|
|
7804
7804
|
lines.push(" # response.schema validation (override-only): real JSON parse happens in the");
|
|
7805
7805
|
lines.push(" # operator-supplied ext_proc processor; this filter only delivers the body.");
|
|
7806
|
-
lines.push(" #
|
|
7806
|
+
lines.push(" # x-security does NOT ship the processor \u2014 see ext_proc/response-schema.json.");
|
|
7807
7807
|
lines.push(" - name: envoy.filters.http.ext_proc");
|
|
7808
7808
|
lines.push(" typed_config:");
|
|
7809
7809
|
lines.push(' "@type": type.googleapis.com/envoy.extensions.filters.http.ext_proc.v3.ExternalProcessor');
|
|
@@ -7842,9 +7842,9 @@ var init_ext_proc = __esm({
|
|
|
7842
7842
|
"src/generators/envoy/templates/ext_proc.ts"() {
|
|
7843
7843
|
"use strict";
|
|
7844
7844
|
init_extauthz_rego_util();
|
|
7845
|
-
EXT_PROC_CLUSTER = "
|
|
7845
|
+
EXT_PROC_CLUSTER = "x_security_ext_proc";
|
|
7846
7846
|
EXT_PROC_PORT = 9292;
|
|
7847
|
-
EXT_PROC_HOST = "
|
|
7847
|
+
EXT_PROC_HOST = "x-security-respval";
|
|
7848
7848
|
}
|
|
7849
7849
|
});
|
|
7850
7850
|
|
|
@@ -7903,7 +7903,7 @@ function emitSinkTypedConfig(lines, indent, r5) {
|
|
|
7903
7903
|
lines.push(`${indent} typed_config:`);
|
|
7904
7904
|
lines.push(`${indent} "@type": type.googleapis.com/envoy.extensions.access_loggers.grpc.v3.HttpGrpcAccessLogConfig`);
|
|
7905
7905
|
lines.push(`${indent} common_config:`);
|
|
7906
|
-
lines.push(`${indent} log_name:
|
|
7906
|
+
lines.push(`${indent} log_name: x_security_access`);
|
|
7907
7907
|
lines.push(`${indent} grpc_service:`);
|
|
7908
7908
|
lines.push(`${indent} envoy_grpc:`);
|
|
7909
7909
|
lines.push(`${indent} cluster_name: ${ALS_CLUSTER}`);
|
|
@@ -7929,11 +7929,11 @@ function eventFilter(event) {
|
|
|
7929
7929
|
];
|
|
7930
7930
|
switch (event) {
|
|
7931
7931
|
case "auth-failure":
|
|
7932
|
-
return f5(401, "
|
|
7932
|
+
return f5(401, "x_security_log_401");
|
|
7933
7933
|
case "authz-deny":
|
|
7934
|
-
return f5(403, "
|
|
7934
|
+
return f5(403, "x_security_log_403");
|
|
7935
7935
|
case "rate-limit-trip":
|
|
7936
|
-
return f5(429, "
|
|
7936
|
+
return f5(429, "x_security_log_429");
|
|
7937
7937
|
// injection-block / request / response have no single-status signature: they
|
|
7938
7938
|
// ride the always-on base logger (which records every transaction). Faking a
|
|
7939
7939
|
// narrower filter would mis-route, so we keep them on the base logger.
|
|
@@ -7954,7 +7954,7 @@ function emitAccessLog(lines, spec, prefix = " ") {
|
|
|
7954
7954
|
lines.push(`${prefix} inline_string: "${DEFAULT_FORMAT}"`);
|
|
7955
7955
|
return;
|
|
7956
7956
|
}
|
|
7957
|
-
lines.push(`${prefix} #
|
|
7957
|
+
lines.push(`${prefix} # x-security logging (SSEC-AUDIT) \u2014 NATIVE/full.`);
|
|
7958
7958
|
lines.push(`${prefix} # events: ${r5.events.join(", ")}`);
|
|
7959
7959
|
lines.push(`${prefix} # sink: ${r5.sink}${r5.sinkRef ? ` (sinkRef ${r5.sinkRef})` : ""}; piiRedaction: ${r5.piiRedaction}`);
|
|
7960
7960
|
if (r5.sink === "syslog") {
|
|
@@ -8002,9 +8002,9 @@ var init_access_log = __esm({
|
|
|
8002
8002
|
"src/generators/envoy/templates/access_log.ts"() {
|
|
8003
8003
|
"use strict";
|
|
8004
8004
|
init_yaml_util();
|
|
8005
|
-
ALS_CLUSTER = "
|
|
8005
|
+
ALS_CLUSTER = "x_security_access_log";
|
|
8006
8006
|
ALS_PORT = 9001;
|
|
8007
|
-
ALS_HOST = "
|
|
8007
|
+
ALS_HOST = "x-security-log-collector";
|
|
8008
8008
|
DEFAULT_FORMAT = '[%START_TIME%] \\"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\\" %RESPONSE_CODE% %RESPONSE_FLAGS% %RESPONSE_CODE_DETAILS% bytes=%BYTES_RECEIVED%/%BYTES_SENT% ua=\\"%REQ(USER-AGENT)%\\"\\n';
|
|
8009
8009
|
}
|
|
8010
8010
|
});
|
|
@@ -8156,7 +8156,7 @@ function emitRouteIpPolicy(lines, pol) {
|
|
|
8156
8156
|
if (pol.deny.length) {
|
|
8157
8157
|
lines.push(" action: DENY");
|
|
8158
8158
|
lines.push(" policies:");
|
|
8159
|
-
lines.push("
|
|
8159
|
+
lines.push(" x-security-ip-deny:");
|
|
8160
8160
|
lines.push(" permissions: [{ any: true }]");
|
|
8161
8161
|
lines.push(" principals:");
|
|
8162
8162
|
for (const c5 of pol.deny) {
|
|
@@ -8167,7 +8167,7 @@ function emitRouteIpPolicy(lines, pol) {
|
|
|
8167
8167
|
} else {
|
|
8168
8168
|
lines.push(" action: ALLOW");
|
|
8169
8169
|
lines.push(" policies:");
|
|
8170
|
-
lines.push("
|
|
8170
|
+
lines.push(" x-security-ip-allow:");
|
|
8171
8171
|
lines.push(" permissions: [{ any: true }]");
|
|
8172
8172
|
lines.push(" principals:");
|
|
8173
8173
|
for (const c5 of pol.allow) {
|
|
@@ -8210,7 +8210,7 @@ function collectJwtEndpoints(spec) {
|
|
|
8210
8210
|
}
|
|
8211
8211
|
if (!jwt.length || !jwksUri) return null;
|
|
8212
8212
|
return {
|
|
8213
|
-
providerName: "
|
|
8213
|
+
providerName: "x_security_jwt",
|
|
8214
8214
|
issuer,
|
|
8215
8215
|
audiences: [...audiences],
|
|
8216
8216
|
jwksUri,
|
|
@@ -8233,7 +8233,7 @@ function emitJwtAuthnFilter(lines, jwt) {
|
|
|
8233
8233
|
lines.push(" from_headers:");
|
|
8234
8234
|
lines.push(` - name: ${yamlString(jwt.headerName)}`);
|
|
8235
8235
|
lines.push(' value_prefix: "Bearer "');
|
|
8236
|
-
lines.push(" forward_payload_header: x-
|
|
8236
|
+
lines.push(" forward_payload_header: x-x-security-jwt-payload");
|
|
8237
8237
|
lines.push(` payload_in_metadata: ${jwt.providerName}`);
|
|
8238
8238
|
lines.push(" remote_jwks:");
|
|
8239
8239
|
lines.push(" http_uri:");
|
|
@@ -8266,7 +8266,7 @@ function emitLuaFilter(lines, luaSource) {
|
|
|
8266
8266
|
lines.push(" - name: envoy.filters.http.lua");
|
|
8267
8267
|
lines.push(" typed_config:");
|
|
8268
8268
|
lines.push(' "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua');
|
|
8269
|
-
lines.push(" # Residual
|
|
8269
|
+
lines.push(" # Residual x-security Lua \u2014 handles fields with no native filter equivalent");
|
|
8270
8270
|
lines.push(" # (duplicateParamPolicy, headerInjectionGuard, request.signature, \u2026).");
|
|
8271
8271
|
lines.push(" inline_code: |");
|
|
8272
8272
|
for (const ln of luaSource.replace(/\n$/, "").split("\n")) {
|
|
@@ -8307,7 +8307,7 @@ function emitRbacFilter(lines, rbac, jwtName) {
|
|
|
8307
8307
|
lines.push(" policies:");
|
|
8308
8308
|
for (const entry of rbac) {
|
|
8309
8309
|
for (const role of entry.roles) {
|
|
8310
|
-
const polName = `
|
|
8310
|
+
const polName = `x-security-rbac-${safeStatId(entry.endpoint)}-${role.replace(/[^a-z0-9]+/gi, "-")}`;
|
|
8311
8311
|
lines.push(` ${yamlString(polName)}:`);
|
|
8312
8312
|
lines.push(" permissions:");
|
|
8313
8313
|
lines.push(" - url_path:");
|
|
@@ -8347,7 +8347,7 @@ function collectRateLimits(spec) {
|
|
|
8347
8347
|
maxTokens,
|
|
8348
8348
|
tokensPerFill: primary.requests,
|
|
8349
8349
|
fillIntervalSec: sec,
|
|
8350
|
-
statPrefix: `
|
|
8350
|
+
statPrefix: `x_security_${safeStatId(ep)}_ratelimit`
|
|
8351
8351
|
});
|
|
8352
8352
|
}
|
|
8353
8353
|
return out;
|
|
@@ -8356,12 +8356,12 @@ function emitLocalRateLimitChain(lines) {
|
|
|
8356
8356
|
lines.push(" - name: envoy.filters.http.local_ratelimit");
|
|
8357
8357
|
lines.push(" typed_config:");
|
|
8358
8358
|
lines.push(' "@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit');
|
|
8359
|
-
lines.push(" stat_prefix:
|
|
8359
|
+
lines.push(" stat_prefix: x_security_chain_ratelimit");
|
|
8360
8360
|
lines.push(" filter_enabled:");
|
|
8361
|
-
lines.push(" runtime_key:
|
|
8361
|
+
lines.push(" runtime_key: x-security.local_ratelimit_enabled");
|
|
8362
8362
|
lines.push(" default_value: { numerator: 100, denominator: HUNDRED }");
|
|
8363
8363
|
lines.push(" filter_enforced:");
|
|
8364
|
-
lines.push(" runtime_key:
|
|
8364
|
+
lines.push(" runtime_key: x-security.local_ratelimit_enforced");
|
|
8365
8365
|
lines.push(" default_value: { numerator: 0, denominator: HUNDRED }");
|
|
8366
8366
|
}
|
|
8367
8367
|
var DEFAULT_BURST_MULTIPLIER2, DEFAULT_BURST_MINIMUM_HEADROOM2;
|
|
@@ -8505,7 +8505,7 @@ function emitRouteEntry(lines, ep, ctx, upstreamCluster) {
|
|
|
8505
8505
|
lines.push(" default_value: { numerator: 100, denominator: HUNDRED }");
|
|
8506
8506
|
lines.push(" response_headers_to_add:");
|
|
8507
8507
|
lines.push(" - append: false");
|
|
8508
|
-
lines.push(" header: { key: x-
|
|
8508
|
+
lines.push(" header: { key: x-x-security-ratelimit, value: enforced }");
|
|
8509
8509
|
}
|
|
8510
8510
|
if (cors) {
|
|
8511
8511
|
lines.push(" envoy.filters.http.cors:");
|
|
@@ -8557,8 +8557,8 @@ function buildEnvoyYaml(opts) {
|
|
|
8557
8557
|
adminPort = 9901
|
|
8558
8558
|
} = opts;
|
|
8559
8559
|
const lines = [];
|
|
8560
|
-
lines.push("#
|
|
8561
|
-
lines.push(`# generator:
|
|
8560
|
+
lines.push("# x-security \u2192 Envoy \u2014 auto-generated. DO NOT EDIT BY HAND.");
|
|
8561
|
+
lines.push(`# generator: x-security-envoy v${VERSION3}`);
|
|
8562
8562
|
lines.push(`# source: ${spec.info.title} ${spec.info.version}`);
|
|
8563
8563
|
lines.push("");
|
|
8564
8564
|
const ruleBased = collectRuleBased(spec);
|
|
@@ -8583,7 +8583,7 @@ function buildEnvoyYaml(opts) {
|
|
|
8583
8583
|
const globalBodyCap = smallestBodyLimit2(spec.endpoints);
|
|
8584
8584
|
lines.push("static_resources:");
|
|
8585
8585
|
lines.push(" listeners:");
|
|
8586
|
-
lines.push(" - name:
|
|
8586
|
+
lines.push(" - name: x_security_listener");
|
|
8587
8587
|
lines.push(" address:");
|
|
8588
8588
|
lines.push(` socket_address: { address: 0.0.0.0, port_value: ${listenerPort} }`);
|
|
8589
8589
|
lines.push(" filter_chains:");
|
|
@@ -8591,12 +8591,12 @@ function buildEnvoyYaml(opts) {
|
|
|
8591
8591
|
lines.push(" - name: envoy.filters.network.http_connection_manager");
|
|
8592
8592
|
lines.push(" typed_config:");
|
|
8593
8593
|
lines.push(' "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager');
|
|
8594
|
-
lines.push(" stat_prefix:
|
|
8594
|
+
lines.push(" stat_prefix: x_security_hcm");
|
|
8595
8595
|
emitAccessLog(lines, spec);
|
|
8596
8596
|
lines.push(" route_config:");
|
|
8597
|
-
lines.push(" name:
|
|
8597
|
+
lines.push(" name: x_security_routes");
|
|
8598
8598
|
lines.push(" virtual_hosts:");
|
|
8599
|
-
lines.push(" - name:
|
|
8599
|
+
lines.push(" - name: x_security_vhost");
|
|
8600
8600
|
lines.push(' domains: ["*"]');
|
|
8601
8601
|
lines.push(" routes:");
|
|
8602
8602
|
const sortedEps = [...spec.endpoints].sort(
|
|
@@ -8709,7 +8709,7 @@ function buildEndpointBlock2(opts) {
|
|
|
8709
8709
|
body.push(` -- request.headerInjectionGuard: reject CR/LF/NUL in header values`);
|
|
8710
8710
|
body.push(` for k, v in pairs(headers) do`);
|
|
8711
8711
|
body.push(` if type(v) == "string" and string.match(v, "[\\r\\n\\0]") then`);
|
|
8712
|
-
body.push(` request_handle:respond({[":status"] = "400"}, "
|
|
8712
|
+
body.push(` request_handle:respond({[":status"] = "400"}, "x-security: invalid header value")`);
|
|
8713
8713
|
body.push(` return`);
|
|
8714
8714
|
body.push(` end`);
|
|
8715
8715
|
body.push(` end`);
|
|
@@ -8723,7 +8723,7 @@ function buildEndpointBlock2(opts) {
|
|
|
8723
8723
|
body.push(` for kv in string.gmatch(string.sub(q_string, q_pos + 1), "([^&]+)") do`);
|
|
8724
8724
|
body.push(` local k = string.match(kv, "^([^=]+)")`);
|
|
8725
8725
|
body.push(` if k and seen[k] then`);
|
|
8726
|
-
body.push(` request_handle:respond({[":status"] = "400"}, "
|
|
8726
|
+
body.push(` request_handle:respond({[":status"] = "400"}, "x-security: duplicate query parameter")`);
|
|
8727
8727
|
body.push(` return`);
|
|
8728
8728
|
body.push(` end`);
|
|
8729
8729
|
body.push(` if k then seen[k] = true end`);
|
|
@@ -8740,7 +8740,7 @@ function buildEndpointBlock2(opts) {
|
|
|
8740
8740
|
body.push(` if string.match(ct, ${luaStr4(pat)}) then ct_ok = true end`);
|
|
8741
8741
|
}
|
|
8742
8742
|
body.push(` if not ct_ok then`);
|
|
8743
|
-
body.push(` request_handle:respond({[":status"] = "415"}, "
|
|
8743
|
+
body.push(` request_handle:respond({[":status"] = "415"}, "x-security: unsupported Content-Type")`);
|
|
8744
8744
|
body.push(` return`);
|
|
8745
8745
|
body.push(` end`);
|
|
8746
8746
|
}
|
|
@@ -8749,7 +8749,7 @@ function buildEndpointBlock2(opts) {
|
|
|
8749
8749
|
body.push(` -- request.maxBodySize=${bytes} bytes`);
|
|
8750
8750
|
body.push(` local cl = tonumber(request_handle:headers():get("content-length") or "0") or 0`);
|
|
8751
8751
|
body.push(` if cl > ${bytes} then`);
|
|
8752
|
-
body.push(` request_handle:respond({[":status"] = "413"}, "
|
|
8752
|
+
body.push(` request_handle:respond({[":status"] = "413"}, "x-security: request body exceeds endpoint limit")`);
|
|
8753
8753
|
body.push(` return`);
|
|
8754
8754
|
body.push(` end`);
|
|
8755
8755
|
}
|
|
@@ -8757,20 +8757,20 @@ function buildEndpointBlock2(opts) {
|
|
|
8757
8757
|
body.push(` -- request.signature (${policy.request.signature.algorithm}): TODO \u2014 needs body filter callback`);
|
|
8758
8758
|
}
|
|
8759
8759
|
body.push(` end`);
|
|
8760
|
-
const startMarker = ` --
|
|
8761
|
-
const endMarker = ` --
|
|
8760
|
+
const startMarker = ` -- xSecurity:${endpoint.method}:${endpoint.path}:START`;
|
|
8761
|
+
const endMarker = ` -- xSecurity:END`;
|
|
8762
8762
|
return [startMarker, ...body, endMarker].join("\n");
|
|
8763
8763
|
}
|
|
8764
8764
|
function buildLuaModule(specTitle, specVersion, blocks, methodMap) {
|
|
8765
8765
|
const lines = [];
|
|
8766
|
-
lines.push(`--
|
|
8767
|
-
lines.push(`-- generator:
|
|
8766
|
+
lines.push(`-- x-security \u2192 Envoy residual Lua filter \u2014 auto-generated. DO NOT EDIT BY HAND.`);
|
|
8767
|
+
lines.push(`-- generator: x-security-envoy v${VERSION4}`);
|
|
8768
8768
|
lines.push(`-- source: ${specTitle} ${specVersion}`);
|
|
8769
8769
|
lines.push(`-- This module handles ONLY fields with no native Envoy filter equivalent.`);
|
|
8770
8770
|
lines.push(`-- JWT auth, RBAC, rate-limit, and CORS are enforced by their native filters`);
|
|
8771
8771
|
lines.push(`-- earlier in the chain (jwt_authn, rbac, local_ratelimit, cors).`);
|
|
8772
8772
|
lines.push("");
|
|
8773
|
-
lines.push(`--
|
|
8773
|
+
lines.push(`-- xSecurity:method-allowlist:START`);
|
|
8774
8774
|
lines.push(`local method_allowlist = {`);
|
|
8775
8775
|
const sorted = [...methodMap].sort((a5, b5) => a5.path.localeCompare(b5.path));
|
|
8776
8776
|
for (const e5 of sorted) {
|
|
@@ -8778,7 +8778,7 @@ function buildLuaModule(specTitle, specVersion, blocks, methodMap) {
|
|
|
8778
8778
|
lines.push(` { pattern = ${luaStr4(e5.pattern)}, methods = { ${methods.map(luaStr4).join(", ")} } },`);
|
|
8779
8779
|
}
|
|
8780
8780
|
lines.push(`}`);
|
|
8781
|
-
lines.push(`--
|
|
8781
|
+
lines.push(`-- xSecurity:method-allowlist:END`);
|
|
8782
8782
|
lines.push("");
|
|
8783
8783
|
lines.push(`function envoy_on_request(request_handle)`);
|
|
8784
8784
|
lines.push(` local headers = request_handle:headers()`);
|
|
@@ -8797,7 +8797,7 @@ function buildLuaModule(specTitle, specVersion, blocks, methodMap) {
|
|
|
8797
8797
|
lines.push(` if m == method then method_ok = true; break end`);
|
|
8798
8798
|
lines.push(` end`);
|
|
8799
8799
|
lines.push(` if not method_ok then`);
|
|
8800
|
-
lines.push(` request_handle:respond({[":status"] = "405"}, "
|
|
8800
|
+
lines.push(` request_handle:respond({[":status"] = "405"}, "x-security: method not allowed")`);
|
|
8801
8801
|
lines.push(` return`);
|
|
8802
8802
|
lines.push(` end`);
|
|
8803
8803
|
lines.push(` break`);
|
|
@@ -8875,7 +8875,7 @@ var init_envoy = __esm({
|
|
|
8875
8875
|
init_extauthz();
|
|
8876
8876
|
init_ext_proc();
|
|
8877
8877
|
ENVOY_PATH = "envoy.yaml";
|
|
8878
|
-
LUA_PATH = "
|
|
8878
|
+
LUA_PATH = "x-security.lua";
|
|
8879
8879
|
OPA_POLICY_PATH = "opa/policy.rego";
|
|
8880
8880
|
RESPONSE_SCHEMA_PATH = "ext_proc/response-schema.json";
|
|
8881
8881
|
envoyLastWarnings = [];
|
|
@@ -9007,7 +9007,7 @@ var init_envoy = __esm({
|
|
|
9007
9007
|
// ext_proc scaffolding: envoy.filters.http.ext_proc + processing
|
|
9008
9008
|
// cluster + ext_proc/response-schema.json (typed per-field constraints,
|
|
9009
9009
|
// stripUnknownFields). The body IS delivered to a processor that does a
|
|
9010
|
-
// real JSON.parse — NO regex over raw bytes. But
|
|
9010
|
+
// real JSON.parse — NO regex over raw bytes. But x-security does not
|
|
9011
9011
|
// ship the processor (the opa_grpc sidecar is ext_authz-only; ext_proc
|
|
9012
9012
|
// is an unmerged upstream PR on a different port). Enforcement depends
|
|
9013
9013
|
// on an operator-supplied gRPC ExternalProcessor, so the honest status
|
|
@@ -9022,7 +9022,7 @@ var init_envoy = __esm({
|
|
|
9022
9022
|
// carry a per-route `forbidArrayRoot` flag the operator processor reads).
|
|
9023
9023
|
// Envoy ships no native response-body shape inspector and a Lua regex for
|
|
9024
9024
|
// a leading '[' over raw bytes is the Rule D-1 masked-quality shortcut.
|
|
9025
|
-
//
|
|
9025
|
+
// x-security does NOT ship the processor → override-only, never full.
|
|
9026
9026
|
"response.forbidArrayRoot": "override-only",
|
|
9027
9027
|
"response.headers.csp": "full",
|
|
9028
9028
|
// wave-22: per-route response_headers_to_add
|
|
@@ -9037,7 +9037,7 @@ var init_envoy = __esm({
|
|
|
9037
9037
|
// cannot evaluate per-operation/per-resolver authorization. The only
|
|
9038
9038
|
// honest path is the same ext_proc handoff as response.schema — stream
|
|
9039
9039
|
// the GraphQL POST body to an operator-supplied GraphQL-aware processor
|
|
9040
|
-
// that parses the query and enforces authz.
|
|
9040
|
+
// that parses the query and enforces authz. x-security ships NO such
|
|
9041
9041
|
// processor, so this is override-only, NEVER full. (Schema pins this
|
|
9042
9042
|
// override-only on every target.) No scaffolding is emitted this wave —
|
|
9043
9043
|
// the matrix is honest so --strict-fidelity/--feasible flag the gap.
|
|
@@ -58691,7 +58691,7 @@ async function runGenerate(specPath, opts) {
|
|
|
58691
58691
|
if (opts.strictFidelity) {
|
|
58692
58692
|
assertFidelity(spec, generator.capabilities(), { targetName: target });
|
|
58693
58693
|
}
|
|
58694
|
-
const outDir = opts.out ?? path.join(process.cwd(), "
|
|
58694
|
+
const outDir = opts.out ?? path.join(process.cwd(), "x-security-out", target);
|
|
58695
58695
|
const artifactPaths = [];
|
|
58696
58696
|
if (!opts.dryRun) {
|
|
58697
58697
|
await mkdir(outDir, { recursive: true });
|
|
@@ -59024,7 +59024,7 @@ function fieldForSlot(slot) {
|
|
|
59024
59024
|
}
|
|
59025
59025
|
function extractRateLimitThreshold(block) {
|
|
59026
59026
|
for (const l3 of block.lines) {
|
|
59027
|
-
const m3 = /SecRule\s+(?:IP|USER|GLOBAL|TX):(?:RATE_|
|
|
59027
|
+
const m3 = /SecRule\s+(?:IP|USER|GLOBAL|TX):(?:RATE_|x_security_rl_|rl_)([A-Za-z0-9_]+?)(?:_burst)?\s+"@gt\s+(\d+)"/.exec(l3);
|
|
59028
59028
|
if (m3 && m3[2] && !l3.includes("_burst")) return Number(m3[2]);
|
|
59029
59029
|
}
|
|
59030
59030
|
return null;
|
|
@@ -59122,7 +59122,7 @@ function diffEndpoint(ep, expectedBlocks, actualBlocks) {
|
|
|
59122
59122
|
severity: "LOW",
|
|
59123
59123
|
expected: "absent",
|
|
59124
59124
|
actual: actBlock.ruleId,
|
|
59125
|
-
message: `Unknown
|
|
59125
|
+
message: `Unknown x-security rule slot ${slot} (id=${actBlock.ruleId}) on deployed config`
|
|
59126
59126
|
});
|
|
59127
59127
|
}
|
|
59128
59128
|
return issues;
|
|
@@ -59223,7 +59223,8 @@ function rateToPerSecond(v) {
|
|
|
59223
59223
|
return unit === "h" ? n2 / 3600 : unit === "m" ? n2 / 60 : n2;
|
|
59224
59224
|
}
|
|
59225
59225
|
function severityForKey(key, expected, actual) {
|
|
59226
|
-
if ((key === "USE_AUTH_BASIC" || key === "USE_MODSECURITY" ||
|
|
59226
|
+
if ((key === "USE_AUTH_BASIC" || key === "USE_MODSECURITY" || // Accept legacy WRIT_* keys on pre-rebrand deployments (back-compat).
|
|
59227
|
+
key === "X_SECURITY_AUTH_TYPE" || key === "WRIT_AUTH_TYPE" || key === "X_SECURITY_JWKS_URI" || key === "WRIT_JWKS_URI" || key === "USE_CLIENT_SSL" || key === "X_SECURITY_AUTH_HEADER" || key === "WRIT_AUTH_HEADER" || key === "MODSECURITY_RULES_FILE") && (actual === void 0 || actual === "no" || actual === "")) {
|
|
59227
59228
|
return "CRITICAL";
|
|
59228
59229
|
}
|
|
59229
59230
|
if (/^LIMIT_REQ_RATE_\d+$/.test(key)) {
|
|
@@ -59460,7 +59461,7 @@ function diffRateLimitPractice(expected, actual) {
|
|
|
59460
59461
|
if (!actual) {
|
|
59461
59462
|
issues.push({
|
|
59462
59463
|
endpoint: "*",
|
|
59463
|
-
field: "practices.
|
|
59464
|
+
field: "practices.x-security-rate-limit",
|
|
59464
59465
|
severity: "CRITICAL",
|
|
59465
59466
|
expected: "present",
|
|
59466
59467
|
actual: "missing",
|
|
@@ -59517,8 +59518,8 @@ async function detectOpenAppSecDrift(spec, opts) {
|
|
|
59517
59518
|
const expectedArtifacts = await Promise.resolve(openappsecGenerator.generate(spec));
|
|
59518
59519
|
const expectedDoc = asDoc(yaml4.load(expectedArtifacts[0]?.content ?? ""));
|
|
59519
59520
|
const issues = [];
|
|
59520
|
-
const expSV = expectedDoc["
|
|
59521
|
-
const actSV = actualDoc["
|
|
59521
|
+
const expSV = expectedDoc["x-security-extended"]?.["schema-validation"] ?? [];
|
|
59522
|
+
const actSV = actualDoc["x-security-extended"]?.["schema-validation"] ?? [];
|
|
59522
59523
|
const actByKey = new Map(actSV.map((s) => [svKey(s), s]));
|
|
59523
59524
|
for (const e5 of expSV) {
|
|
59524
59525
|
const key = svKey(e5);
|
|
@@ -59548,8 +59549,8 @@ async function detectOpenAppSecDrift(spec, opts) {
|
|
|
59548
59549
|
}
|
|
59549
59550
|
const expPractices = expectedDoc.practices ?? [];
|
|
59550
59551
|
const actPractices = actualDoc.practices ?? [];
|
|
59551
|
-
const expRL = expPractices.find((p2) => p2.name === "
|
|
59552
|
-
const actRL = actPractices.find((p2) => p2.name === "
|
|
59552
|
+
const expRL = expPractices.find((p2) => p2.name === "x-security-rate-limit");
|
|
59553
|
+
const actRL = actPractices.find((p2) => p2.name === "x-security-rate-limit");
|
|
59553
59554
|
issues.push(...diffRateLimitPractice(expRL, actRL));
|
|
59554
59555
|
return {
|
|
59555
59556
|
kind: "drift",
|
|
@@ -59574,7 +59575,7 @@ function normalizeRuleLine(line) {
|
|
|
59574
59575
|
function destOf(rule) {
|
|
59575
59576
|
const cidr = /-d\s+(\S+)/.exec(rule);
|
|
59576
59577
|
if (cidr && cidr[1]) return cidr[1];
|
|
59577
|
-
const fqdn = /@@
|
|
59578
|
+
const fqdn = /@@(?:X_SECURITY|WRIT)_RESOLVE:([^@]+)@@/.exec(rule);
|
|
59578
59579
|
if (fqdn && fqdn[1]) return `fqdn:${fqdn[1]}`;
|
|
59579
59580
|
return "";
|
|
59580
59581
|
}
|
|
@@ -59584,7 +59585,7 @@ function extractTagged(content) {
|
|
|
59584
59585
|
for (let i5 = 0; i5 < lines.length; i5++) {
|
|
59585
59586
|
const l3 = lines[i5];
|
|
59586
59587
|
if (!l3) continue;
|
|
59587
|
-
if (!l3.startsWith("#
|
|
59588
|
+
if (!l3.startsWith("# x-security:")) continue;
|
|
59588
59589
|
let j5 = i5 + 1;
|
|
59589
59590
|
while (j5 < lines.length) {
|
|
59590
59591
|
const next = lines[j5];
|
|
@@ -59593,7 +59594,7 @@ function extractTagged(content) {
|
|
|
59593
59594
|
}
|
|
59594
59595
|
const ruleLine = lines[j5];
|
|
59595
59596
|
if (!ruleLine) continue;
|
|
59596
|
-
const body = l3.slice("#
|
|
59597
|
+
const body = l3.slice("# x-security:".length).trim();
|
|
59597
59598
|
const sepIdx = body.indexOf(" -- ");
|
|
59598
59599
|
const tag2 = sepIdx >= 0 ? body.slice(0, sepIdx).trim() : body;
|
|
59599
59600
|
out.push({
|
|
@@ -59667,7 +59668,7 @@ function diffRuleset(family, expected, actual) {
|
|
|
59667
59668
|
severity: "LOW",
|
|
59668
59669
|
expected: void 0,
|
|
59669
59670
|
actual: a5.rule,
|
|
59670
|
-
message: `Unknown
|
|
59671
|
+
message: `Unknown XSecurity-tagged firewall (${family}) rule: ${a5.comment}`
|
|
59671
59672
|
});
|
|
59672
59673
|
}
|
|
59673
59674
|
}
|
|
@@ -59834,7 +59835,7 @@ function extractRateLimitStatPrefixes(rawYaml) {
|
|
|
59834
59835
|
}
|
|
59835
59836
|
if (node && typeof node === "object") {
|
|
59836
59837
|
const obj = node;
|
|
59837
|
-
if (typeof obj.stat_prefix === "string" && obj.stat_prefix !== "
|
|
59838
|
+
if (typeof obj.stat_prefix === "string" && obj.stat_prefix !== "x_security_chain_ratelimit" && obj.stat_prefix !== "x_security_hcm") {
|
|
59838
59839
|
if (/_ratelimit$/.test(obj.stat_prefix)) out.add(obj.stat_prefix);
|
|
59839
59840
|
}
|
|
59840
59841
|
for (const v of Object.values(obj)) walk(v);
|
|
@@ -59934,7 +59935,7 @@ async function readSources(opts) {
|
|
|
59934
59935
|
envoyYaml2 = "";
|
|
59935
59936
|
}
|
|
59936
59937
|
try {
|
|
59937
|
-
lua = await readFile8(path3.join(opts.filePath, "
|
|
59938
|
+
lua = await readFile8(path3.join(opts.filePath, "x-security.lua"), "utf8");
|
|
59938
59939
|
} catch {
|
|
59939
59940
|
lua = null;
|
|
59940
59941
|
}
|
|
@@ -59950,7 +59951,7 @@ async function detectEnvoyDrift(spec, opts) {
|
|
|
59950
59951
|
const actualLua = actualLuaFile ?? extractInlineLua(actualYaml) ?? "";
|
|
59951
59952
|
const expectedArtifacts = await Promise.resolve(envoyGenerator.generate(spec));
|
|
59952
59953
|
const expectedYaml = expectedArtifacts.find((a5) => a5.path === "envoy.yaml")?.content ?? "";
|
|
59953
|
-
const expectedLua = expectedArtifacts.find((a5) => a5.path === "
|
|
59954
|
+
const expectedLua = expectedArtifacts.find((a5) => a5.path === "x-security.lua")?.content ?? "";
|
|
59954
59955
|
const expectedBlocks = extractEndpointBlocks(expectedLua);
|
|
59955
59956
|
const actualBlocks = extractEndpointBlocks(actualLua);
|
|
59956
59957
|
const issues = [];
|
|
@@ -60004,7 +60005,7 @@ async function detectEnvoyDrift(spec, opts) {
|
|
|
60004
60005
|
const authz = ep.policy.authorization;
|
|
60005
60006
|
if (!authz || authz.type !== "rbac" || !authz.roles?.length) continue;
|
|
60006
60007
|
for (const role of authz.roles) {
|
|
60007
|
-
const fragment = `"
|
|
60008
|
+
const fragment = `"x-security-rbac-`;
|
|
60008
60009
|
const roleFragment = role.replace(/[^a-z0-9]+/gi, "-");
|
|
60009
60010
|
if (!deployedHasRbacPolicy(actualYaml, fragment) || !actualYaml.includes(`-${roleFragment}":`)) {
|
|
60010
60011
|
issues.push({
|
|
@@ -60026,7 +60027,7 @@ async function detectEnvoyDrift(spec, opts) {
|
|
|
60026
60027
|
severity: "LOW",
|
|
60027
60028
|
expected: "absent",
|
|
60028
60029
|
actual: "present",
|
|
60029
|
-
message: `Unknown
|
|
60030
|
+
message: `Unknown x-security-tagged endpoint block in deployed Lua: ${block.method} ${block.pathTemplate}`
|
|
60030
60031
|
});
|
|
60031
60032
|
}
|
|
60032
60033
|
return {
|
|
@@ -60042,8 +60043,8 @@ var init_envoy2 = __esm({
|
|
|
60042
60043
|
"use strict";
|
|
60043
60044
|
init_envoy();
|
|
60044
60045
|
init_kong_shared();
|
|
60045
|
-
START_RE = /^\s*--\s*
|
|
60046
|
-
END_RE = /^\s*--\s*
|
|
60046
|
+
START_RE = /^\s*--\s*xSecurity:([A-Z]+):(.+):START\s*$/;
|
|
60047
|
+
END_RE = /^\s*--\s*xSecurity:END\s*$/;
|
|
60047
60048
|
}
|
|
60048
60049
|
});
|
|
60049
60050
|
|
|
@@ -60242,7 +60243,7 @@ function buildLog(toolName, rules, results) {
|
|
|
60242
60243
|
tool: {
|
|
60243
60244
|
driver: {
|
|
60244
60245
|
name: toolName,
|
|
60245
|
-
informationUri: "https://github.com/
|
|
60246
|
+
informationUri: "https://github.com/ZeeshanSultan/x-security",
|
|
60246
60247
|
rules
|
|
60247
60248
|
}
|
|
60248
60249
|
},
|
|
@@ -60279,7 +60280,7 @@ function driftToSarif(r5, specPath) {
|
|
|
60279
60280
|
properties: { severity: i5.severity }
|
|
60280
60281
|
};
|
|
60281
60282
|
});
|
|
60282
|
-
return buildLog("
|
|
60283
|
+
return buildLog("x-security-drift", Array.from(ruleSet.values()), results);
|
|
60283
60284
|
}
|
|
60284
60285
|
function owaspToSarif(r5, specPath) {
|
|
60285
60286
|
const rules = [
|
|
@@ -60301,7 +60302,7 @@ function owaspToSarif(r5, specPath) {
|
|
|
60301
60302
|
}
|
|
60302
60303
|
]
|
|
60303
60304
|
}));
|
|
60304
|
-
return buildLog("
|
|
60305
|
+
return buildLog("x-security-owasp", rules, results);
|
|
60305
60306
|
}
|
|
60306
60307
|
var init_sarif = __esm({
|
|
60307
60308
|
"src/reporters/sarif.ts"() {
|
|
@@ -60473,9 +60474,9 @@ function buildComposePlan(opts) {
|
|
|
60473
60474
|
const upstreamPort = opts.upstreamPort ?? 18080;
|
|
60474
60475
|
const gatewayPort = opts.gatewayPort ?? 18e3;
|
|
60475
60476
|
const suffix = randomSuffix();
|
|
60476
|
-
const network = `
|
|
60477
|
-
const upstreamName = `
|
|
60478
|
-
const gatewayName = `
|
|
60477
|
+
const network = `x-security-${opts.target}-net-${suffix}`;
|
|
60478
|
+
const upstreamName = `x-security-${opts.target}-upstream-${suffix}`;
|
|
60479
|
+
const gatewayName = `x-security-${opts.target}-gateway-${suffix}`;
|
|
60479
60480
|
const image = GATEWAY_IMAGE[opts.target];
|
|
60480
60481
|
let gatewayEnv = "";
|
|
60481
60482
|
if (opts.target === "kong") {
|
|
@@ -61103,9 +61104,9 @@ function fileHasWritHeader(container, path30) {
|
|
|
61103
61104
|
});
|
|
61104
61105
|
if (r5.error || r5.status !== 0) return false;
|
|
61105
61106
|
const head = r5.stdout || "";
|
|
61106
|
-
return
|
|
61107
|
+
return X_SECURITY_HEADER_MARKERS.some((m3) => head.includes(m3));
|
|
61107
61108
|
}
|
|
61108
|
-
function
|
|
61109
|
+
function xSecurityRulesAreIncluded(nginxDump, container) {
|
|
61109
61110
|
if (!nginxDump) return true;
|
|
61110
61111
|
const lines = nginxDump.split("\n");
|
|
61111
61112
|
const includes = [];
|
|
@@ -61113,12 +61114,12 @@ function writRulesAreIncluded(nginxDump, container) {
|
|
|
61113
61114
|
const m3 = line.match(/^\s*Include\s+(\S+)/i);
|
|
61114
61115
|
if (m3 && m3[1]) includes.push(m3[1]);
|
|
61115
61116
|
}
|
|
61116
|
-
if (includes.some((i5) => /writ/i.test(i5))) return true;
|
|
61117
|
+
if (includes.some((i5) => /x-security|writ/i.test(i5))) return true;
|
|
61117
61118
|
if (!container) return false;
|
|
61118
61119
|
const globs = includes.filter((i5) => /[*?[]/.test(i5));
|
|
61119
61120
|
for (const glob of globs) {
|
|
61120
61121
|
const matched = listGlobInContainer(container, glob);
|
|
61121
|
-
if (matched.some((p2) => /writ/i.test(p2))) return true;
|
|
61122
|
+
if (matched.some((p2) => /x-security|writ/i.test(p2))) return true;
|
|
61122
61123
|
for (const p2 of matched) {
|
|
61123
61124
|
if (fileHasWritHeader(container, p2)) return true;
|
|
61124
61125
|
}
|
|
@@ -61186,7 +61187,7 @@ function scanEmittedRules(directives) {
|
|
|
61186
61187
|
}
|
|
61187
61188
|
return out;
|
|
61188
61189
|
}
|
|
61189
|
-
var SECTION_BANNER, RULE_ID_RE2, SEC_RULE_LINE, RULES_ERROR_RE, GENERIC_PARSE_ERR_RE, RULES_LOADED_SUMMARY,
|
|
61190
|
+
var SECTION_BANNER, RULE_ID_RE2, SEC_RULE_LINE, RULES_ERROR_RE, GENERIC_PARSE_ERR_RE, RULES_LOADED_SUMMARY, X_SECURITY_HEADER_MARKERS, modsecNginxReader;
|
|
61190
61191
|
var init_modsec_nginx = __esm({
|
|
61191
61192
|
"src/verify/readers/modsec-nginx.ts"() {
|
|
61192
61193
|
"use strict";
|
|
@@ -61197,9 +61198,9 @@ var init_modsec_nginx = __esm({
|
|
|
61197
61198
|
RULES_ERROR_RE = /ModSecurity:\s*(?:Rules error\.?\s*)?File:\s*([^.]+?)\.\s*Line:\s*(\d+)\.\s*Column:\s*(\d+)\.\s*(.*)$/;
|
|
61198
61199
|
GENERIC_PARSE_ERR_RE = /ModSecurity:\s*(Something wrong with [^.\n]+|SecDefaultActions[^.\n]+)/;
|
|
61199
61200
|
RULES_LOADED_SUMMARY = /rules loaded inline\/local\/remote:\s*(\d+)\/(\d+)\/(\d+)/i;
|
|
61200
|
-
|
|
61201
|
-
"
|
|
61202
|
-
"generator:
|
|
61201
|
+
X_SECURITY_HEADER_MARKERS = [
|
|
61202
|
+
"x-security \u2192 Coraza",
|
|
61203
|
+
"generator: x-security-coraza"
|
|
61203
61204
|
];
|
|
61204
61205
|
modsecNginxReader = {
|
|
61205
61206
|
async readEmittedArtifacts(spec) {
|
|
@@ -61216,12 +61217,12 @@ var init_modsec_nginx = __esm({
|
|
|
61216
61217
|
const out = [];
|
|
61217
61218
|
const nginxDump = await readNginxConfigDump(gateway);
|
|
61218
61219
|
const container = gateway.startsWith("docker:") ? gateway.slice("docker:".length) : void 0;
|
|
61219
|
-
const included =
|
|
61220
|
+
const included = xSecurityRulesAreIncluded(nginxDump, container);
|
|
61220
61221
|
if (nginxDump && !included) {
|
|
61221
61222
|
out.push({
|
|
61222
61223
|
id: "__not-included__",
|
|
61223
61224
|
kind: "coraza-rule",
|
|
61224
|
-
rejectionReason: "
|
|
61225
|
+
rejectionReason: "x-security rules file is not Include'd by the running nginx config (nginx -T) \u2014 every emitted rule is unloaded"
|
|
61225
61226
|
});
|
|
61226
61227
|
}
|
|
61227
61228
|
for (const line of raw.split("\n")) {
|
|
@@ -61266,7 +61267,7 @@ var init_modsec_nginx = __esm({
|
|
|
61266
61267
|
diagnostics.push(`could not find "rules loaded inline/local/remote" startup line \u2014 coverage inferred from parse-error lines only`);
|
|
61267
61268
|
}
|
|
61268
61269
|
if (notIncluded) {
|
|
61269
|
-
diagnostics.push(notIncluded.rejectionReason ?? "
|
|
61270
|
+
diagnostics.push(notIncluded.rejectionReason ?? "x-security rules file not included by gateway config");
|
|
61270
61271
|
}
|
|
61271
61272
|
const rejectionsByLine = /* @__PURE__ */ new Map();
|
|
61272
61273
|
const genericRejections = [];
|
|
@@ -61283,7 +61284,7 @@ var init_modsec_nginx = __esm({
|
|
|
61283
61284
|
}
|
|
61284
61285
|
const rows = [];
|
|
61285
61286
|
const everythingRejected = !!notIncluded || summaryKnown && totalLoadedFromSummary === 0;
|
|
61286
|
-
const genericReason = notIncluded?.rejectionReason ?? genericRejections[0]?.rejectionReason ?? (everythingRejected ? "no
|
|
61287
|
+
const genericReason = notIncluded?.rejectionReason ?? genericRejections[0]?.rejectionReason ?? (everythingRejected ? "no x-security rules loaded (file likely not included by nginx, or parse aborted at engine-globals)" : "");
|
|
61287
61288
|
for (const [endpoint, arts] of byEndpoint) {
|
|
61288
61289
|
const rejected = [];
|
|
61289
61290
|
let loadedCount = 0;
|
|
@@ -61983,8 +61984,8 @@ function parseEmittedSnippet(yamlText) {
|
|
|
61983
61984
|
collectRbacPolicyNames(doc, rbacPolicies);
|
|
61984
61985
|
const ratelimitStatPrefixes = /* @__PURE__ */ new Set();
|
|
61985
61986
|
collectStatPrefixes(doc, ratelimitStatPrefixes);
|
|
61986
|
-
ratelimitStatPrefixes.delete("
|
|
61987
|
-
ratelimitStatPrefixes.delete("
|
|
61987
|
+
ratelimitStatPrefixes.delete("x_security_chain_ratelimit");
|
|
61988
|
+
ratelimitStatPrefixes.delete("x_security_hcm");
|
|
61988
61989
|
const corsRoutes = [];
|
|
61989
61990
|
const routes = pickRoutes(doc);
|
|
61990
61991
|
if (routes) {
|
|
@@ -62137,7 +62138,7 @@ var init_envoy3 = __esm({
|
|
|
62137
62138
|
"src/verify/readers/envoy.ts"() {
|
|
62138
62139
|
"use strict";
|
|
62139
62140
|
init_registry();
|
|
62140
|
-
SENTINEL_RE = /--\s*
|
|
62141
|
+
SENTINEL_RE = /--\s*xSecurity:([A-Z]+):([^\s:]+):START/g;
|
|
62141
62142
|
envoyReader = {
|
|
62142
62143
|
async readEmittedArtifacts(spec) {
|
|
62143
62144
|
const gen = await loadGenerator("envoy");
|
|
@@ -62166,8 +62167,8 @@ var init_envoy3 = __esm({
|
|
|
62166
62167
|
for (const p2 of rbacPolicies) out.push({ id: p2, kind: "envoy-rbac-policy" });
|
|
62167
62168
|
const statPrefixes = /* @__PURE__ */ new Set();
|
|
62168
62169
|
collectStatPrefixes(configDump, statPrefixes);
|
|
62169
|
-
statPrefixes.delete("
|
|
62170
|
-
statPrefixes.delete("
|
|
62170
|
+
statPrefixes.delete("x_security_chain_ratelimit");
|
|
62171
|
+
statPrefixes.delete("x_security_hcm");
|
|
62171
62172
|
for (const s of statPrefixes) out.push({ id: s, kind: "envoy-ratelimit-route" });
|
|
62172
62173
|
const corsRouteLabels = /* @__PURE__ */ new Set();
|
|
62173
62174
|
walkCorsRoutes(configDump, corsRouteLabels);
|
|
@@ -62203,7 +62204,7 @@ var init_envoy3 = __esm({
|
|
|
62203
62204
|
rows.sort((a5, b5) => a5.endpoint.localeCompare(b5.endpoint));
|
|
62204
62205
|
if (loaded.length === 0) {
|
|
62205
62206
|
diagnostics.push(
|
|
62206
|
-
"envoy /config_dump returned no
|
|
62207
|
+
"envoy /config_dump returned no x-security artifacts \u2014 the bootstrap may not have loaded"
|
|
62207
62208
|
);
|
|
62208
62209
|
} else {
|
|
62209
62210
|
const haveJwt = loaded.some((l3) => l3.kind === "envoy-http-filter" && l3.id === "envoy.filters.http.jwt_authn");
|
|
@@ -62312,7 +62313,7 @@ var init_bunkerweb3 = __esm({
|
|
|
62312
62313
|
init_registry();
|
|
62313
62314
|
RULE_ID_RE5 = /\bid:(\d{4,7})\b/g;
|
|
62314
62315
|
SOURCE_ENDPOINTS_RE = /^#\s*Source endpoints?:\s*(.+)$/i;
|
|
62315
|
-
SECTION_HEADER_RE = /^#\s*
|
|
62316
|
+
SECTION_HEADER_RE = /^#\s*x-security-generated [^\n]*$/i;
|
|
62316
62317
|
bunkerwebReader = {
|
|
62317
62318
|
async readEmittedArtifacts(spec) {
|
|
62318
62319
|
const gen = await loadGenerator("bunkerweb");
|
|
@@ -62341,7 +62342,7 @@ var init_bunkerweb3 = __esm({
|
|
|
62341
62342
|
out.push({
|
|
62342
62343
|
id: "__empty-dump__",
|
|
62343
62344
|
kind: "coraza-rule",
|
|
62344
|
-
rejectionReason: "nginx -T returned no
|
|
62345
|
+
rejectionReason: "nginx -T returned no x-security rule ids \u2014 the scheduler may not have synced configs/modsec into the bunkerweb container yet"
|
|
62345
62346
|
});
|
|
62346
62347
|
}
|
|
62347
62348
|
if (split2.scheduler) {
|
|
@@ -62451,7 +62452,7 @@ function parseOpenappsecPolicy(policyYaml) {
|
|
|
62451
62452
|
}
|
|
62452
62453
|
}
|
|
62453
62454
|
const schemaEntries = [];
|
|
62454
|
-
const ext = doc["
|
|
62455
|
+
const ext = doc["x-security-extended"];
|
|
62455
62456
|
const sv = ext?.["schema-validation"];
|
|
62456
62457
|
if (Array.isArray(sv)) {
|
|
62457
62458
|
for (const s of sv) {
|
|
@@ -62503,7 +62504,7 @@ function countWritVerdicts(container) {
|
|
|
62503
62504
|
container,
|
|
62504
62505
|
"sh",
|
|
62505
62506
|
"-c",
|
|
62506
|
-
'test -f /var/log/nano_agent/decision.log && grep -c "
|
|
62507
|
+
'test -f /var/log/nano_agent/decision.log && grep -c "x-security" /var/log/nano_agent/decision.log || echo MISSING'
|
|
62507
62508
|
],
|
|
62508
62509
|
{ encoding: "utf8", maxBuffer: 1 * 1024 * 1024 }
|
|
62509
62510
|
);
|
|
@@ -62548,12 +62549,12 @@ var init_openappsec3 = __esm({
|
|
|
62548
62549
|
out.push({
|
|
62549
62550
|
id: "__no-policy-loaded__",
|
|
62550
62551
|
kind: "envoy-endpoint-policy",
|
|
62551
|
-
rejectionReason: "/etc/cp/conf/ on the openappsec agent contains no policy/yaml files \u2014 agent did not load any
|
|
62552
|
+
rejectionReason: "/etc/cp/conf/ on the openappsec agent contains no policy/yaml files \u2014 agent did not load any x-security policy"
|
|
62552
62553
|
});
|
|
62553
62554
|
return out;
|
|
62554
62555
|
}
|
|
62555
62556
|
const presence = /* @__PURE__ */ new Set();
|
|
62556
|
-
const reToken = /\b(writ[-A-Za-z0-9_]+|(?:get|post|put|delete|patch|head|options)-[A-Za-z0-9_-]+)\b/gi;
|
|
62557
|
+
const reToken = /\b((?:x-security|writ)[-A-Za-z0-9_]+|(?:get|post|put|delete|patch|head|options)-[A-Za-z0-9_-]+)\b/gi;
|
|
62557
62558
|
let m3;
|
|
62558
62559
|
while ((m3 = reToken.exec(confBlob)) !== null) presence.add(m3[1]);
|
|
62559
62560
|
for (const tok of presence) out.push({ id: tok, kind: "envoy-endpoint-policy" });
|
|
@@ -62575,7 +62576,7 @@ var init_openappsec3 = __esm({
|
|
|
62575
62576
|
if (verdicts && verdicts.rejectionReason) {
|
|
62576
62577
|
const n2 = Number(verdicts.rejectionReason.replace(/^verdicts:/, ""));
|
|
62577
62578
|
if (Number.isFinite(n2)) {
|
|
62578
|
-
diagnostics.push(`/var/log/nano_agent/decision.log contains ${n2}
|
|
62579
|
+
diagnostics.push(`/var/log/nano_agent/decision.log contains ${n2} x-security-attributed verdict line(s)`);
|
|
62579
62580
|
}
|
|
62580
62581
|
}
|
|
62581
62582
|
const loadedIds = new Set(
|
|
@@ -62615,7 +62616,7 @@ var init_openappsec3 = __esm({
|
|
|
62615
62616
|
function renderTable2(r5) {
|
|
62616
62617
|
const lines = [];
|
|
62617
62618
|
const targetLabel = r5.engine ? `${r5.target}/${r5.engine}` : r5.target;
|
|
62618
|
-
lines.push(`
|
|
62619
|
+
lines.push(`x-security verify \u2014 target=${targetLabel} \u2014 gateway=${r5.gateway}`);
|
|
62619
62620
|
lines.push("");
|
|
62620
62621
|
if (r5.diagnostics.length > 0) {
|
|
62621
62622
|
for (const d5 of r5.diagnostics) lines.push(`! ${d5}`);
|
|
@@ -62691,8 +62692,8 @@ function renderSarif(r5) {
|
|
|
62691
62692
|
{
|
|
62692
62693
|
tool: {
|
|
62693
62694
|
driver: {
|
|
62694
|
-
name: "
|
|
62695
|
-
informationUri: "https://github.com/
|
|
62695
|
+
name: "x-security-verify",
|
|
62696
|
+
informationUri: "https://github.com/ZeeshanSultan/x-security",
|
|
62696
62697
|
rules: [
|
|
62697
62698
|
{
|
|
62698
62699
|
id: "writ.verify.load-failure",
|
|
@@ -62867,7 +62868,7 @@ async function runTest(specPath, opts) {
|
|
|
62867
62868
|
}
|
|
62868
62869
|
spec.servers = [{ url: upstreamForSpec }];
|
|
62869
62870
|
const artifacts = await gen.generate(spec);
|
|
62870
|
-
const tmpDir = path5.join(os.tmpdir(), `
|
|
62871
|
+
const tmpDir = path5.join(os.tmpdir(), `x-security-${target}-${Date.now()}-${process.pid}`);
|
|
62871
62872
|
await mkdir2(tmpDir, { recursive: true });
|
|
62872
62873
|
for (const a5 of artifacts) {
|
|
62873
62874
|
const full = path5.join(tmpDir, a5.path);
|
|
@@ -63254,7 +63255,7 @@ var init_feasibility = __esm({
|
|
|
63254
63255
|
{ capKey: "request.allowedHosts", present: (p2) => nonEmpty(p2.request?.allowedHosts) },
|
|
63255
63256
|
{ capKey: "mtls", present: (p2) => nonEmpty(p2.mtls) }
|
|
63256
63257
|
],
|
|
63257
|
-
// SSEC-INJECTION =
|
|
63258
|
+
// SSEC-INJECTION = x-security-native injection category (W19). Mitigated by
|
|
63258
63259
|
// per-arg sink hardening: request.schema.<field>.injectionGuard. The probe
|
|
63259
63260
|
// resolves against the `request.schema.injectionGuard` capability key, which
|
|
63260
63261
|
// kong + envoy currently advertise `unsupported` (no libinjection/@detectSQLi
|
|
@@ -63265,7 +63266,7 @@ var init_feasibility = __esm({
|
|
|
63265
63266
|
"SSEC-INJECTION": [
|
|
63266
63267
|
{ capKey: "request.schema.injectionGuard", present: hasInjectionGuard }
|
|
63267
63268
|
],
|
|
63268
|
-
// SSEC-PROMPT =
|
|
63269
|
+
// SSEC-PROMPT = x-security-native LLM prompt-injection category (v0.7). A
|
|
63269
63270
|
// distinct threat class from SSEC-INJECTION (one synthetic id per class), so
|
|
63270
63271
|
// it gets its own probe keyed on injectionGuard.includes('ai-prompt'). It
|
|
63271
63272
|
// resolves against the SAME `request.schema.injectionGuard` capability key —
|
|
@@ -63274,7 +63275,7 @@ var init_feasibility = __esm({
|
|
|
63274
63275
|
"SSEC-PROMPT": [
|
|
63275
63276
|
{ capKey: "request.schema.injectionGuard", present: hasAiPromptGuard }
|
|
63276
63277
|
],
|
|
63277
|
-
// SSEC-AUDIT =
|
|
63278
|
+
// SSEC-AUDIT = x-security-native audit-logging category (v0.7). Mitigated by
|
|
63278
63279
|
// the declarative `logging` policy (events/sink/sinkRef/piiRedaction). The
|
|
63279
63280
|
// probe resolves against the flat `logging` capability key; until a generator
|
|
63280
63281
|
// advertises it the verdict honestly resolves to none/partial rather than an
|
|
@@ -63282,7 +63283,7 @@ var init_feasibility = __esm({
|
|
|
63282
63283
|
"SSEC-AUDIT": [
|
|
63283
63284
|
{ capKey: "logging", present: (p2) => nonEmpty(p2.logging) }
|
|
63284
63285
|
],
|
|
63285
|
-
// SSEC-STORAGE =
|
|
63286
|
+
// SSEC-STORAGE = x-security-native at-rest storage posture (v0.8). ADVISORY
|
|
63286
63287
|
// ONLY. request.dataAtRest declares the protection the *app* must apply to
|
|
63287
63288
|
// named body fields; the gateway never sees the DB write, so this compiles to
|
|
63288
63289
|
// NOTHING enforcing. The capability is hard-pinned override-only/unsupported
|
|
@@ -63902,7 +63903,7 @@ var init_propose_annotation = __esm({
|
|
|
63902
63903
|
required: ["method", "path"]
|
|
63903
63904
|
};
|
|
63904
63905
|
proposeAnnotationTool = {
|
|
63905
|
-
name: "
|
|
63906
|
+
name: "x-security/propose-annotation",
|
|
63906
63907
|
description: "Propose an x-security block for a route signature. Pure heuristic; no network calls. Returns YAML the agent can paste under the operation in its OpenAPI spec.",
|
|
63907
63908
|
inputSchema,
|
|
63908
63909
|
handler: (raw) => {
|
|
@@ -63987,8 +63988,8 @@ var init_lint_annotation = __esm({
|
|
|
63987
63988
|
required: ["annotation"]
|
|
63988
63989
|
};
|
|
63989
63990
|
lintAnnotationTool = {
|
|
63990
|
-
name: "
|
|
63991
|
-
description: "Validate an x-security block against the
|
|
63991
|
+
name: "x-security/lint-annotation",
|
|
63992
|
+
description: "Validate an x-security block against the x-security schema and return a LOW/MEDIUM/HIGH confidence verdict plus structural warnings.",
|
|
63992
63993
|
inputSchema: inputSchema2,
|
|
63993
63994
|
handler: (raw) => {
|
|
63994
63995
|
const input = raw ?? {};
|
|
@@ -64018,8 +64019,8 @@ var init_lint_annotation = __esm({
|
|
|
64018
64019
|
// ../cursor-mcp/dist/tools/check-endpoint.js
|
|
64019
64020
|
import { request as request6 } from "undici";
|
|
64020
64021
|
async function checkEndpoint(input, fetcher = defaultFetcher) {
|
|
64021
|
-
const apiKey = input.apiKey ?? process.env.WRIT_API_KEY;
|
|
64022
|
-
const apiUrl = process.env.WRIT_API_URL ?? DEFAULT_API_URL;
|
|
64022
|
+
const apiKey = input.apiKey ?? process.env.X_SECURITY_API_KEY ?? process.env.WRIT_API_KEY;
|
|
64023
|
+
const apiUrl = process.env.X_SECURITY_API_URL ?? process.env.WRIT_API_URL ?? DEFAULT_API_URL;
|
|
64023
64024
|
if (!apiKey) {
|
|
64024
64025
|
return {
|
|
64025
64026
|
configured: false,
|
|
@@ -64068,8 +64069,8 @@ var init_check_endpoint = __esm({
|
|
|
64068
64069
|
return { statusCode: res.statusCode, body };
|
|
64069
64070
|
};
|
|
64070
64071
|
checkEndpointTool = {
|
|
64071
|
-
name: "
|
|
64072
|
-
description: "Look up an endpoint in the
|
|
64072
|
+
name: "x-security/check-endpoint",
|
|
64073
|
+
description: "Look up an endpoint in the x-security scan history. Returns existing analysis (annotations, OWASP coverage, prior findings) if WRIT_API_KEY is configured.",
|
|
64073
64074
|
inputSchema: inputSchema3,
|
|
64074
64075
|
handler: async (raw) => {
|
|
64075
64076
|
const input = raw ?? {};
|
|
@@ -64260,7 +64261,7 @@ var init_dist5 = __esm({
|
|
|
64260
64261
|
init_server();
|
|
64261
64262
|
init_transport();
|
|
64262
64263
|
dist_default = main;
|
|
64263
|
-
isDirectRun = import.meta.url === `file://${process.argv[1] ?? ""}` || process.argv[1]?.endsWith("cursor-mcp/dist/index.js") === true || process.argv[1]?.endsWith("
|
|
64264
|
+
isDirectRun = import.meta.url === `file://${process.argv[1] ?? ""}` || process.argv[1]?.endsWith("cursor-mcp/dist/index.js") === true || process.argv[1]?.endsWith("x-security-cursor-mcp") === true;
|
|
64264
64265
|
if (isDirectRun) {
|
|
64265
64266
|
main().catch((e5) => {
|
|
64266
64267
|
process.stderr.write(`cursor-mcp: fatal: ${e5.message}
|
|
@@ -64333,7 +64334,7 @@ async function runVerifyBundle(tarballPath, opts = {}) {
|
|
|
64333
64334
|
} else {
|
|
64334
64335
|
pubKeyPem = BUNDLE_VERIFY_PUBLIC_KEY;
|
|
64335
64336
|
}
|
|
64336
|
-
const tmpdir5 = fs.mkdtempSync(path6.join(os2.tmpdir(), "
|
|
64337
|
+
const tmpdir5 = fs.mkdtempSync(path6.join(os2.tmpdir(), "x-security-verify-"));
|
|
64337
64338
|
try {
|
|
64338
64339
|
await extractTarball(tarballPath, tmpdir5);
|
|
64339
64340
|
const manifestPath = path6.join(tmpdir5, MANIFEST);
|
|
@@ -64640,11 +64641,33 @@ function endpointToFileId(method, routePath) {
|
|
|
64640
64641
|
const safePath = routePath.replace(/^\//, "").replace(/\//g, "__").replace(/[^A-Za-z0-9_:.\-{}#]/g, "_");
|
|
64641
64642
|
return `${method.toUpperCase()}__${safePath || "root"}`;
|
|
64642
64643
|
}
|
|
64643
|
-
function
|
|
64644
|
+
function xSecurityDir(repoDir) {
|
|
64645
|
+
return path7.join(repoDir, ".x-security");
|
|
64646
|
+
}
|
|
64647
|
+
function legacyWritDir(repoDir) {
|
|
64644
64648
|
return path7.join(repoDir, ".writ");
|
|
64645
64649
|
}
|
|
64650
|
+
async function resolveArtifactDir(repoDir) {
|
|
64651
|
+
const canonical = xSecurityDir(repoDir);
|
|
64652
|
+
try {
|
|
64653
|
+
await fs2.access(canonical);
|
|
64654
|
+
return canonical;
|
|
64655
|
+
} catch {
|
|
64656
|
+
}
|
|
64657
|
+
const legacy = legacyWritDir(repoDir);
|
|
64658
|
+
try {
|
|
64659
|
+
await fs2.access(legacy);
|
|
64660
|
+
console.warn("[x-security] reading legacy .writ/ artifact dir; run a compile to migrate to .x-security/");
|
|
64661
|
+
return legacy;
|
|
64662
|
+
} catch {
|
|
64663
|
+
}
|
|
64664
|
+
return canonical;
|
|
64665
|
+
}
|
|
64646
64666
|
function policiesDir(repoDir) {
|
|
64647
|
-
return path7.join(
|
|
64667
|
+
return path7.join(xSecurityDir(repoDir), POLICIES_DIR);
|
|
64668
|
+
}
|
|
64669
|
+
async function resolvePoliciesDir(repoDir) {
|
|
64670
|
+
return path7.join(await resolveArtifactDir(repoDir), POLICIES_DIR);
|
|
64648
64671
|
}
|
|
64649
64672
|
async function persistPolicy(repoDir, route, policy, cites) {
|
|
64650
64673
|
const dir = policiesDir(repoDir);
|
|
@@ -70276,7 +70299,7 @@ var init_emit = __esm({
|
|
|
70276
70299
|
"../detect-core/dist/agentic/emit.js"() {
|
|
70277
70300
|
"use strict";
|
|
70278
70301
|
init_dist3();
|
|
70279
|
-
AUTH_JWKS_VAR = "${
|
|
70302
|
+
AUTH_JWKS_VAR = "${X_SECURITY_AUTH_JWKS_URI}";
|
|
70280
70303
|
AUTHZ_LOCATION_SEGMENT = {
|
|
70281
70304
|
path: "request.params",
|
|
70282
70305
|
query: "request.query",
|
|
@@ -72847,7 +72870,7 @@ async function citeMatches(repoDir, cite) {
|
|
|
72847
72870
|
return snap !== null;
|
|
72848
72871
|
}
|
|
72849
72872
|
async function runAudit(repoDir) {
|
|
72850
|
-
const dir =
|
|
72873
|
+
const dir = await resolvePoliciesDir(repoDir);
|
|
72851
72874
|
let entries;
|
|
72852
72875
|
try {
|
|
72853
72876
|
entries = (await fs18.readdir(dir)).filter((f5) => f5.endsWith(".yaml") || f5.endsWith(".yml"));
|
|
@@ -72956,7 +72979,7 @@ import { execFile } from "node:child_process";
|
|
|
72956
72979
|
import { promisify } from "node:util";
|
|
72957
72980
|
import yaml15 from "js-yaml";
|
|
72958
72981
|
function resolveApiUrl(env) {
|
|
72959
|
-
const override = env.WRIT_API_URL?.trim();
|
|
72982
|
+
const override = (env.X_SECURITY_API_URL ?? env.WRIT_API_URL)?.trim();
|
|
72960
72983
|
if (!override) return DEFAULT_API_URL2;
|
|
72961
72984
|
let parsed;
|
|
72962
72985
|
try {
|
|
@@ -72975,16 +72998,16 @@ function resolveApiUrl(env) {
|
|
|
72975
72998
|
const allowed = LOCAL_HOSTS.has(host) || HOST_ALLOWLIST_SUFFIXES.some((s) => host === s.slice(1) || host.endsWith(s));
|
|
72976
72999
|
if (!allowed) {
|
|
72977
73000
|
throw new PushError(
|
|
72978
|
-
`Refusing to send the API token to "${host}". WRIT_API_URL must be the
|
|
73001
|
+
`Refusing to send the API token to "${host}". WRIT_API_URL must be the x-security SaaS (host ending in ${HOST_ALLOWLIST_SUFFIXES.join(" or ")}, or localhost for dev). This guard prevents token exfiltration to an attacker-controlled host.`
|
|
72979
73002
|
);
|
|
72980
73003
|
}
|
|
72981
73004
|
return override.replace(/\/+$/, "");
|
|
72982
73005
|
}
|
|
72983
73006
|
function resolveToken(env) {
|
|
72984
|
-
const token = env.WRIT_API_TOKEN?.trim();
|
|
73007
|
+
const token = (env.X_SECURITY_API_TOKEN ?? env.WRIT_API_TOKEN)?.trim();
|
|
72985
73008
|
if (!token) {
|
|
72986
73009
|
throw new PushError(
|
|
72987
|
-
"WRIT_API_TOKEN is not set. Export your
|
|
73010
|
+
"WRIT_API_TOKEN is not set. Export your x-security API key (WRIT_API_TOKEN=...) \u2014 push reads it from the environment only, never from a flag."
|
|
72988
73011
|
);
|
|
72989
73012
|
}
|
|
72990
73013
|
return token;
|
|
@@ -73026,7 +73049,7 @@ async function resolveRepoIdentity(repoDir) {
|
|
|
73026
73049
|
return { repoUrl: normalizeRemoteUrl(remote), commitSha };
|
|
73027
73050
|
}
|
|
73028
73051
|
async function loadPolicies(repoDir) {
|
|
73029
|
-
const dir =
|
|
73052
|
+
const dir = await resolvePoliciesDir(repoDir);
|
|
73030
73053
|
let files;
|
|
73031
73054
|
try {
|
|
73032
73055
|
files = (await fs19.readdir(dir)).filter((f5) => f5.endsWith(".yaml") || f5.endsWith(".yml"));
|
|
@@ -73061,7 +73084,7 @@ async function loadPolicies(repoDir) {
|
|
|
73061
73084
|
}
|
|
73062
73085
|
async function loadReport(repoDir) {
|
|
73063
73086
|
try {
|
|
73064
|
-
return await fs19.readFile(path25.join(
|
|
73087
|
+
return await fs19.readFile(path25.join(await resolveArtifactDir(repoDir), REPORT_FILE), "utf8");
|
|
73065
73088
|
} catch {
|
|
73066
73089
|
return void 0;
|
|
73067
73090
|
}
|
|
@@ -73391,7 +73414,7 @@ var init_compile3 = __esm({
|
|
|
73391
73414
|
function renderReport3(audit, routes) {
|
|
73392
73415
|
const pct = (audit.coverage * 100).toFixed(1);
|
|
73393
73416
|
const lines = [];
|
|
73394
|
-
lines.push("#
|
|
73417
|
+
lines.push("# x-security report");
|
|
73395
73418
|
lines.push("");
|
|
73396
73419
|
lines.push(`- Routes with a compiled policy: **${audit.routes}**`);
|
|
73397
73420
|
lines.push(`- Enforced controls: **${audit.controls}**`);
|
|
@@ -73444,16 +73467,16 @@ var init_ci_gate = __esm({
|
|
|
73444
73467
|
'echo "$OUT"',
|
|
73445
73468
|
`CITE_BACKED="$(printf '%s' "$OUT" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.stdout.write(String(JSON.parse(s).citeBacked)))')"`,
|
|
73446
73469
|
'if [ "$CITE_BACKED" != "true" ]; then',
|
|
73447
|
-
' echo "::error::
|
|
73470
|
+
' echo "::error::x-security gate failed \u2014 an emitted control no longer cites your code." >&2',
|
|
73448
73471
|
" exit 1",
|
|
73449
73472
|
"fi",
|
|
73450
73473
|
""
|
|
73451
73474
|
].join("\n");
|
|
73452
73475
|
GITHUB_WORKFLOW = [
|
|
73453
|
-
"name:
|
|
73476
|
+
"name: x-security gate",
|
|
73454
73477
|
"on: [pull_request, push]",
|
|
73455
73478
|
"jobs:",
|
|
73456
|
-
"
|
|
73479
|
+
" x-security-audit:",
|
|
73457
73480
|
" runs-on: ubuntu-latest",
|
|
73458
73481
|
" steps:",
|
|
73459
73482
|
" - uses: actions/checkout@v4",
|
|
@@ -73466,7 +73489,7 @@ var init_ci_gate = __esm({
|
|
|
73466
73489
|
].join("\n");
|
|
73467
73490
|
GITLAB_SNIPPET = [
|
|
73468
73491
|
"# Append to .gitlab-ci.yml",
|
|
73469
|
-
"
|
|
73492
|
+
"x-security-audit:",
|
|
73470
73493
|
" image: node:20",
|
|
73471
73494
|
" script:",
|
|
73472
73495
|
" - bash .writ/ci/audit-gate.sh",
|
|
@@ -73483,7 +73506,7 @@ import { promises as fs20 } from "node:fs";
|
|
|
73483
73506
|
import path26 from "node:path";
|
|
73484
73507
|
import yaml16 from "js-yaml";
|
|
73485
73508
|
async function loadPolicies2(repoDir) {
|
|
73486
|
-
const dir =
|
|
73509
|
+
const dir = await resolvePoliciesDir(repoDir);
|
|
73487
73510
|
let files;
|
|
73488
73511
|
try {
|
|
73489
73512
|
files = (await fs20.readdir(dir)).filter((f5) => f5.endsWith(".yaml") || f5.endsWith(".yml"));
|
|
@@ -73517,7 +73540,7 @@ function toSpecIR(policies) {
|
|
|
73517
73540
|
return {
|
|
73518
73541
|
openapi: "3.1.0",
|
|
73519
73542
|
dialect: "3.1",
|
|
73520
|
-
info: { title: "
|
|
73543
|
+
info: { title: "x-security BYO-agent policies", version: "0.0.0" },
|
|
73521
73544
|
servers: [],
|
|
73522
73545
|
endpoints,
|
|
73523
73546
|
unprotectedEndpoints: []
|
|
@@ -73527,7 +73550,7 @@ async function emitWaf(repoDir, policies) {
|
|
|
73527
73550
|
const gen = await loadGenerator("bunkerweb");
|
|
73528
73551
|
if (!gen) throw new Error("bunkerweb generator unavailable");
|
|
73529
73552
|
const artifacts = await gen.generate(toSpecIR(policies));
|
|
73530
|
-
const outDir = path26.join(
|
|
73553
|
+
const outDir = path26.join(xSecurityDir(repoDir), WAF_DIR);
|
|
73531
73554
|
await fs20.mkdir(outDir, { recursive: true });
|
|
73532
73555
|
const written = [];
|
|
73533
73556
|
for (const a5 of artifacts) {
|
|
@@ -73539,13 +73562,13 @@ async function emitWaf(repoDir, policies) {
|
|
|
73539
73562
|
return written;
|
|
73540
73563
|
}
|
|
73541
73564
|
async function emitReport(repoDir, audit, policies) {
|
|
73542
|
-
await fs20.mkdir(
|
|
73543
|
-
const dest = path26.join(
|
|
73565
|
+
await fs20.mkdir(xSecurityDir(repoDir), { recursive: true });
|
|
73566
|
+
const dest = path26.join(xSecurityDir(repoDir), REPORT_FILE);
|
|
73544
73567
|
await fs20.writeFile(dest, renderReport3(audit, policies.map((p2) => ({ method: p2.method, path: p2.routePath }))), "utf8");
|
|
73545
73568
|
return [dest];
|
|
73546
73569
|
}
|
|
73547
73570
|
async function emitCi(repoDir) {
|
|
73548
|
-
const outDir = path26.join(
|
|
73571
|
+
const outDir = path26.join(xSecurityDir(repoDir), CI_DIR);
|
|
73549
73572
|
await fs20.mkdir(outDir, { recursive: true });
|
|
73550
73573
|
const gate = renderCiGate();
|
|
73551
73574
|
const written = [];
|
|
@@ -74873,7 +74896,7 @@ async function resolveSpecArg(arg, stdin = process.stdin) {
|
|
|
74873
74896
|
if (!content) {
|
|
74874
74897
|
throw new Error("expected a spec document on stdin, got empty input");
|
|
74875
74898
|
}
|
|
74876
|
-
const dir = fs22.mkdtempSync(path29.join(os4.tmpdir(), "
|
|
74899
|
+
const dir = fs22.mkdtempSync(path29.join(os4.tmpdir(), "x-security-stdin-"));
|
|
74877
74900
|
tempSpecDirs.add(dir);
|
|
74878
74901
|
if (!exitHandlerRegistered) {
|
|
74879
74902
|
exitHandlerRegistered = true;
|
|
@@ -75001,7 +75024,7 @@ var init_lazy = __esm({
|
|
|
75001
75024
|
init_dist4();
|
|
75002
75025
|
program = new Command2();
|
|
75003
75026
|
program.name(basename2(process.argv[1] ?? "lazy").replace(/\.(mjs|cjs|js)$/, "")).description("Compile, validate, test, and report on x-security policies in OpenAPI specs.").option("--quiet", "Suppress warnings/advisories on stderr (results still print)").option("--verbose", "Print extra progress detail to stderr").version(resolveVersion()).showHelpAfterError("(add --help for usage)");
|
|
75004
|
-
program.command("generate <spec>").description("Compile an annotated OpenAPI spec into target-specific gateway config. <spec> may be - to read from stdin.").requiredOption("--target <name>", "Target: kong|coraza|bunkerweb|openappsec|firewall").option("--out <dir>", "Output directory (default: ./
|
|
75027
|
+
program.command("generate <spec>").description("Compile an annotated OpenAPI spec into target-specific gateway config. <spec> may be - to read from stdin.").requiredOption("--target <name>", "Target: kong|coraza|bunkerweb|openappsec|firewall").option("--out <dir>", "Output directory (default: ./x-security-out/<target>)").option("--dry-run", "Print planned artifacts without writing to disk").option("--no-strict", "Allow unresolved variables (default: strict)").option("--vault", "Resolve $vault.* refs via HashiCorp Vault (uses VAULT_ADDR + VAULT_TOKEN or AppRole)").option("--aws-secrets", "Resolve $aws.* refs via AWS Secrets Manager (uses default credential chain + AWS_REGION)").option("--vault-kv-version <v>", "Vault KV engine version: 1 or 2 (default 2)", (v) => v === "1" ? 1 : 2).option("--no-with-consumers", "Kong only: emit spec-only output (omit consumers + per-plugin credentials). Consumers are emitted by default so OSS Kong gateways actually authenticate; pass this for the spec-only baseline.").option("--kong-deployment <mode>", "Kong only: standalone|behind-proxy|with-coraza|with-istio. with-coraza rewrites all service URLs to http://coraza:8080.", "standalone").option("--kong-edition <edition>", "Kong only: oss|enterprise. enterprise emits openid-connect (real JWKS+RS256) instead of HS256 jwt_secrets downgrade.", "oss").option("--kong-dbless", "Kong only: deployment runs in DB-less mode (KONG_DATABASE=off). Per-identity rate-limits fall back to policy=local with a structured warning instead of policy=cluster.", false).option("--kong-policy <mode>", "Kong only: rate-limit policy. local|cluster. Default local (safe for OSS DB-less). Pass cluster only when Kong is running in database mode (postgres/cassandra); cluster is a boot error in DB-less Kong.", "local").option("--coraza-engine <name>", "Coraza only: modsec-nginx|modsec-apache|coraza-go|coraza-spoa (default modsec-nginx).", "modsec-nginx").option("--coraza-peers <list>", 'Coraza only (coraza-spoa|coraza-go): HAProxy peer-replication for stick-tables. Format: "name1:host1:port1,name2:host2:port2". When set, emitted haproxy-stick-tables.cfg includes a `peers x-security` section so counters replicate across instances.').option("--strict-fidelity", "Exit 4 (S3) if any spec field cannot be enforced by the chosen target+engine. Independent of --strict.").addHelpText("after", generateExamples).action(async (spec, opts) => {
|
|
75005
75028
|
try {
|
|
75006
75029
|
const diag = makeDiagnostics(verbosity());
|
|
75007
75030
|
const fromStdin = spec === "-";
|
|
@@ -75371,14 +75394,14 @@ import { dump as yamlDump } from "js-yaml";
|
|
|
75371
75394
|
function luaStr2(s) {
|
|
75372
75395
|
return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r") + '"';
|
|
75373
75396
|
}
|
|
75374
|
-
var SS_RESPONSE_STRIP_UNKNOWN_TAG = "
|
|
75397
|
+
var SS_RESPONSE_STRIP_UNKNOWN_TAG = "x-security-response-strip-unknown";
|
|
75375
75398
|
function buildResponseStripUnknownPlugins(resp, ctx = {}) {
|
|
75376
75399
|
if (!resp || resp.stripUnknownFields !== true) return [];
|
|
75377
75400
|
const schemaKeys = resp.schema ? Object.keys(resp.schema) : [];
|
|
75378
75401
|
if (schemaKeys.length === 0) return [];
|
|
75379
75402
|
const luaAllowTable = "{" + schemaKeys.map((k5) => `[${luaStr2(k5)}]=true`).join(", ") + "}";
|
|
75380
75403
|
const lua = [
|
|
75381
|
-
`--
|
|
75404
|
+
`-- XSecurity W26 response strip-unknown-fields for endpoint=${ctx.endpoint ?? "?"}`,
|
|
75382
75405
|
`local cjson = require("cjson.safe")`,
|
|
75383
75406
|
`local ss_allow = ${luaAllowTable}`,
|
|
75384
75407
|
`local raw = kong.response.get_raw_body()`,
|
|
@@ -75403,7 +75426,7 @@ function buildResponseStripUnknownPlugins(resp, ctx = {}) {
|
|
|
75403
75426
|
tags: [SS_RESPONSE_STRIP_UNKNOWN_TAG]
|
|
75404
75427
|
}];
|
|
75405
75428
|
}
|
|
75406
|
-
var SS_RESPONSE_STRIP_TRACES_TAG = "
|
|
75429
|
+
var SS_RESPONSE_STRIP_TRACES_TAG = "x-security-response-strip-traces";
|
|
75407
75430
|
var STACK_TRACE_LUA_PATTERNS = [
|
|
75408
75431
|
"\n%s*at%s+[%w%._$<>]+%([^%)]*%)",
|
|
75409
75432
|
// Java/JS: " at com.foo.Bar.baz(File.java:42)"
|
|
@@ -75422,7 +75445,7 @@ function buildResponseStripTracesPlugins(resp, ctx = {}) {
|
|
|
75422
75445
|
if (!resp?.errorScrubbing?.stripStackTraces) return [];
|
|
75423
75446
|
const subs = STACK_TRACE_LUA_PATTERNS.map((p2) => ` body = body:gsub(${luaStr2(p2)}, "")`).join("\n");
|
|
75424
75447
|
const lua = [
|
|
75425
|
-
`--
|
|
75448
|
+
`-- XSecurity W26 response strip-stack-traces for endpoint=${ctx.endpoint ?? "?"}`,
|
|
75426
75449
|
`local status = kong.response.get_status()`,
|
|
75427
75450
|
`if status >= 400 then`,
|
|
75428
75451
|
` local body = kong.response.get_raw_body()`,
|
|
@@ -75442,11 +75465,11 @@ function buildResponseStripTracesPlugins(resp, ctx = {}) {
|
|
|
75442
75465
|
tags: [SS_RESPONSE_STRIP_TRACES_TAG]
|
|
75443
75466
|
}];
|
|
75444
75467
|
}
|
|
75445
|
-
var SS_RESPONSE_GENERIC_ERROR_TAG = "
|
|
75468
|
+
var SS_RESPONSE_GENERIC_ERROR_TAG = "x-security-response-generic-error";
|
|
75446
75469
|
function buildResponseGenericErrorPlugins(resp, ctx = {}) {
|
|
75447
75470
|
if (!resp?.errorScrubbing?.genericMessages) return [];
|
|
75448
75471
|
const lua = [
|
|
75449
|
-
`--
|
|
75472
|
+
`-- XSecurity W26 response generic-error-message for endpoint=${ctx.endpoint ?? "?"}`,
|
|
75450
75473
|
`local status = kong.response.get_status()`,
|
|
75451
75474
|
`if status >= 500 then`,
|
|
75452
75475
|
` kong.log.warn("[${SS_RESPONSE_GENERIC_ERROR_TAG}] rewrote 5xx body to generic message")`,
|
|
@@ -75460,13 +75483,13 @@ function buildResponseGenericErrorPlugins(resp, ctx = {}) {
|
|
|
75460
75483
|
tags: [SS_RESPONSE_GENERIC_ERROR_TAG]
|
|
75461
75484
|
}];
|
|
75462
75485
|
}
|
|
75463
|
-
var SS_RESPONSE_CONTENT_TYPE_TAG = "
|
|
75486
|
+
var SS_RESPONSE_CONTENT_TYPE_TAG = "x-security-response-contenttype";
|
|
75464
75487
|
function buildResponseContentTypeAssertPlugins(resp, ctx = {}) {
|
|
75465
75488
|
const allowed = resp?.contentType;
|
|
75466
75489
|
if (!allowed || allowed.length === 0) return [];
|
|
75467
75490
|
const luaSet = allowed.map((t) => `["${t.toLowerCase().replace(/["\\]/g, "")}"]=true`).join(", ");
|
|
75468
75491
|
const headerFilter = [
|
|
75469
|
-
`--
|
|
75492
|
+
`-- XSecurity response Content-Type assertion endpoint=${ctx.endpoint ?? "?"}`,
|
|
75470
75493
|
`local allowed = { ${luaSet} }`,
|
|
75471
75494
|
`local status = kong.response.get_status()`,
|
|
75472
75495
|
`if status >= 200 and status < 300 then`,
|
|
@@ -75494,7 +75517,7 @@ function buildResponseContentTypeAssertPlugins(resp, ctx = {}) {
|
|
|
75494
75517
|
tags: [SS_RESPONSE_CONTENT_TYPE_TAG]
|
|
75495
75518
|
}];
|
|
75496
75519
|
}
|
|
75497
|
-
var SS_RESPONSE_SCHEMA_TAG = "
|
|
75520
|
+
var SS_RESPONSE_SCHEMA_TAG = "x-security-response-schema";
|
|
75498
75521
|
var FORMAT_LUA = {
|
|
75499
75522
|
// RFC-ish email: local@domain.tld, no spaces, one @, a dotted domain.
|
|
75500
75523
|
email: `return type(v)=="string" and v:match("^[^@%s]+@[^@%s]+%.[^@%s]+$") ~= nil`,
|
|
@@ -75619,7 +75642,7 @@ function buildResponseMaxLengthPlugins(resp, ctx = {}) {
|
|
|
75619
75642
|
if (rules.length === 0) return [];
|
|
75620
75643
|
const validators = rules.flatMap((r5) => fieldValidatorLua(r5));
|
|
75621
75644
|
const lua = [
|
|
75622
|
-
`--
|
|
75645
|
+
`-- XSecurity W26 response typed-schema validation for endpoint=${ctx.endpoint ?? "?"}`,
|
|
75623
75646
|
`-- Validates the cjson-DECODED value (canonical: immune to pretty-printing,`,
|
|
75624
75647
|
`-- escaped quotes, key order, numeric formatting). Never matches raw body bytes.`,
|
|
75625
75648
|
`local cjson = require("cjson.safe")`,
|
|
@@ -75645,7 +75668,7 @@ function buildResponseMaxLengthPlugins(resp, ctx = {}) {
|
|
|
75645
75668
|
tags: [SS_RESPONSE_SCHEMA_TAG]
|
|
75646
75669
|
}];
|
|
75647
75670
|
}
|
|
75648
|
-
var SS_RATE_LIMIT_FINGERPRINT_TAG = "
|
|
75671
|
+
var SS_RATE_LIMIT_FINGERPRINT_TAG = "x-security-rate-limit-fingerprint";
|
|
75649
75672
|
function rateLimitsUseFingerprint(rl) {
|
|
75650
75673
|
if (!rl) return false;
|
|
75651
75674
|
const arr = Array.isArray(rl) ? rl : [rl];
|
|
@@ -75663,9 +75686,9 @@ function rateLimitsUseFingerprint(rl) {
|
|
|
75663
75686
|
function buildRateLimitFingerprintPlugins(rl, ctx = {}) {
|
|
75664
75687
|
if (!rateLimitsUseFingerprint(rl)) return [];
|
|
75665
75688
|
const lua = [
|
|
75666
|
-
`--
|
|
75689
|
+
`-- XSecurity W26 rate-limit fingerprint pre-function for endpoint=${ctx.endpoint ?? "?"}`,
|
|
75667
75690
|
`-- Composite key = client_ip + sha1(user-agent). Stored on ctx.shared and`,
|
|
75668
|
-
`-- mirrored to X-
|
|
75691
|
+
`-- mirrored to X-XSecurity-Fingerprint so rate-limiting limit_by=header picks it up.`,
|
|
75669
75692
|
`local sha1 = require("resty.sha1"):new()`,
|
|
75670
75693
|
`local ip = kong.client.get_forwarded_ip() or kong.client.get_ip() or "0.0.0.0"`,
|
|
75671
75694
|
`local ua = kong.request.get_header("user-agent") or ""`,
|
|
@@ -75677,8 +75700,8 @@ function buildRateLimitFingerprintPlugins(rl, ctx = {}) {
|
|
|
75677
75700
|
` for i = 1, #digest do hex = hex .. string.format("%02x", string.byte(digest, i)) end`,
|
|
75678
75701
|
` fp = ip .. ":" .. hex:sub(1, 16)`,
|
|
75679
75702
|
`end`,
|
|
75680
|
-
`kong.ctx.shared.
|
|
75681
|
-
`kong.service.request.set_header("X-
|
|
75703
|
+
`kong.ctx.shared.x_security_fp = fp`,
|
|
75704
|
+
`kong.service.request.set_header("X-XSecurity-Fingerprint", fp)`,
|
|
75682
75705
|
`kong.log.warn("[${SS_RATE_LIMIT_FINGERPRINT_TAG}] fp=" .. fp)`
|
|
75683
75706
|
].join("\n");
|
|
75684
75707
|
return [{
|
|
@@ -75687,7 +75710,7 @@ function buildRateLimitFingerprintPlugins(rl, ctx = {}) {
|
|
|
75687
75710
|
tags: [SS_RATE_LIMIT_FINGERPRINT_TAG]
|
|
75688
75711
|
}];
|
|
75689
75712
|
}
|
|
75690
|
-
var SS_BOT_DETECTED_TAG = "
|
|
75713
|
+
var SS_BOT_DETECTED_TAG = "x-security-bot-detected";
|
|
75691
75714
|
var BOT_UA_LUA_PATTERNS = [
|
|
75692
75715
|
"[Cc]url/",
|
|
75693
75716
|
"[Pp]ython%-requests/",
|
|
@@ -75711,14 +75734,14 @@ function buildBotProtectionPlugins(bot, ctx = {}) {
|
|
|
75711
75734
|
const checks = BOT_UA_LUA_PATTERNS.map((p2, i5) => ` if ua:match(${luaStr2(p2)}) then return ${i5 + 1} end`).join("\n");
|
|
75712
75735
|
const blockLua = enforce ? [
|
|
75713
75736
|
` return kong.response.exit(403, {`,
|
|
75714
|
-
` message = "
|
|
75737
|
+
` message = "XSecurity: bot detected",`,
|
|
75715
75738
|
` tag = "${SS_BOT_DETECTED_TAG}",`,
|
|
75716
75739
|
` provider = "${provider}",`,
|
|
75717
75740
|
` rule = hit`,
|
|
75718
75741
|
` })`
|
|
75719
75742
|
].join("\n") : ` -- observe mode: log but pass through`;
|
|
75720
75743
|
const lua = [
|
|
75721
|
-
`--
|
|
75744
|
+
`-- XSecurity W26 botProtection pre-function for endpoint=${ctx.endpoint ?? "?"}`,
|
|
75722
75745
|
`-- provider=${provider} mode=${bot.mode}`,
|
|
75723
75746
|
`local ua = kong.request.get_header("user-agent") or ""`,
|
|
75724
75747
|
`local hit = (function()`,
|
|
@@ -75735,7 +75758,7 @@ function buildBotProtectionPlugins(bot, ctx = {}) {
|
|
|
75735
75758
|
`if not challenge:match("ss_bot_challenge=") then`,
|
|
75736
75759
|
` kong.log.warn("[${SS_BOT_DETECTED_TAG}] missing js-challenge cookie")`,
|
|
75737
75760
|
` return kong.response.exit(403, {`,
|
|
75738
|
-
` message = "
|
|
75761
|
+
` message = "XSecurity: bot challenge required",`,
|
|
75739
75762
|
` tag = "${SS_BOT_DETECTED_TAG}",`,
|
|
75740
75763
|
` provider = "${provider}"`,
|
|
75741
75764
|
` })`,
|
|
@@ -75759,7 +75782,7 @@ function durationSeconds(window2) {
|
|
|
75759
75782
|
if (!m3) return 60;
|
|
75760
75783
|
return Number(m3[1]) * (WINDOW_TO_SECONDS2[m3[2].toLowerCase()] ?? 60);
|
|
75761
75784
|
}
|
|
75762
|
-
var SS_AUDIT_TAG = "
|
|
75785
|
+
var SS_AUDIT_TAG = "x-security-audit-log";
|
|
75763
75786
|
var LOGGING_SINK_PLUGIN = {
|
|
75764
75787
|
stdout: "file-log",
|
|
75765
75788
|
file: "file-log",
|
|
@@ -75867,7 +75890,7 @@ function buildLoggingPlugins(policy, ctx = {}) {
|
|
|
75867
75890
|
tags: [SS_AUDIT_TAG]
|
|
75868
75891
|
}];
|
|
75869
75892
|
}
|
|
75870
|
-
var SS_PASSWORD_POLICY_TAG = "
|
|
75893
|
+
var SS_PASSWORD_POLICY_TAG = "x-security-password-policy-422";
|
|
75871
75894
|
function buildPasswordPolicyPlugins(auth, ctx = {}) {
|
|
75872
75895
|
const pp = auth?.passwordPolicy;
|
|
75873
75896
|
if (!pp) return [];
|
|
@@ -75915,7 +75938,7 @@ function buildPasswordPolicyPlugins(auth, ctx = {}) {
|
|
|
75915
75938
|
);
|
|
75916
75939
|
}
|
|
75917
75940
|
const lua = [
|
|
75918
|
-
`--
|
|
75941
|
+
`-- XSecurity v0.7 password-policy pre-function for endpoint=${ctx.endpoint ?? "?"}`,
|
|
75919
75942
|
`local body = kong.request.get_body()`,
|
|
75920
75943
|
`if type(body) == "table" and type(body.password) == "string" then`,
|
|
75921
75944
|
` local pw = body.password`,
|
|
@@ -75926,7 +75949,7 @@ function buildPasswordPolicyPlugins(auth, ctx = {}) {
|
|
|
75926
75949
|
` if violation ~= nil then`,
|
|
75927
75950
|
` kong.log.warn("[${SS_PASSWORD_POLICY_TAG}] password rejected: " .. violation)`,
|
|
75928
75951
|
` return kong.response.exit(422, {`,
|
|
75929
|
-
` message = "
|
|
75952
|
+
` message = "XSecurity: password does not meet policy",`,
|
|
75930
75953
|
` tag = "${SS_PASSWORD_POLICY_TAG}",`,
|
|
75931
75954
|
` violation = violation`,
|
|
75932
75955
|
` })`,
|
|
@@ -75939,11 +75962,11 @@ function buildPasswordPolicyPlugins(auth, ctx = {}) {
|
|
|
75939
75962
|
tags: [SS_PASSWORD_POLICY_TAG]
|
|
75940
75963
|
}];
|
|
75941
75964
|
}
|
|
75942
|
-
var SS_FORBID_ARRAY_ROOT_TAG = "
|
|
75965
|
+
var SS_FORBID_ARRAY_ROOT_TAG = "x-security-forbid-array-root-502";
|
|
75943
75966
|
function buildForbidArrayRootPlugins(resp, ctx = {}) {
|
|
75944
75967
|
if (!resp || resp.forbidArrayRoot !== true) return [];
|
|
75945
75968
|
const lua = [
|
|
75946
|
-
`--
|
|
75969
|
+
`-- XSecurity v0.7 forbidArrayRoot post-function for endpoint=${ctx.endpoint ?? "?"}`,
|
|
75947
75970
|
`local cjson = require("cjson.safe")`,
|
|
75948
75971
|
`local raw = kong.response.get_raw_body()`,
|
|
75949
75972
|
`if raw and #raw > 0 then`,
|
|
@@ -75959,7 +75982,7 @@ function buildForbidArrayRootPlugins(resp, ctx = {}) {
|
|
|
75959
75982
|
` kong.log.warn("[${SS_FORBID_ARRAY_ROOT_TAG}] bare top-level array response blocked (JSON hijacking)")`,
|
|
75960
75983
|
` kong.response.set_status(502)`,
|
|
75961
75984
|
` kong.response.set_header("Content-Type", "application/json")`,
|
|
75962
|
-
` kong.response.set_raw_body('{"message":"
|
|
75985
|
+
` kong.response.set_raw_body('{"message":"XSecurity: bare top-level array response forbidden","tag":"${SS_FORBID_ARRAY_ROOT_TAG}"}')`,
|
|
75963
75986
|
` end`,
|
|
75964
75987
|
` end`,
|
|
75965
75988
|
`end`
|
|
@@ -75970,8 +75993,8 @@ function buildForbidArrayRootPlugins(resp, ctx = {}) {
|
|
|
75970
75993
|
tags: [SS_FORBID_ARRAY_ROOT_TAG]
|
|
75971
75994
|
}];
|
|
75972
75995
|
}
|
|
75973
|
-
var SS_IDEMPOTENCY_TAG = "
|
|
75974
|
-
var SS_IDEMPOTENCY_CACHE_DICT = "
|
|
75996
|
+
var SS_IDEMPOTENCY_TAG = "x-security-idempotency-replay-409";
|
|
75997
|
+
var SS_IDEMPOTENCY_CACHE_DICT = "x_security_idempotency_cache";
|
|
75975
75998
|
var SS_IDEMPOTENCY_CACHE_DICT_SIZE = "10m";
|
|
75976
75999
|
function buildIdempotencyKeyPlugins(request9, ctx = {}) {
|
|
75977
76000
|
const idem = request9?.idempotencyKey;
|
|
@@ -75987,8 +76010,8 @@ function buildIdempotencyKeyPlugins(request9, ctx = {}) {
|
|
|
75987
76010
|
});
|
|
75988
76011
|
}
|
|
75989
76012
|
const lua = [
|
|
75990
|
-
`--
|
|
75991
|
-
`-- Best-effort replay suppression only \u2014 NOT atomic idempotency. See
|
|
76013
|
+
`-- XSecurity v0.7 idempotencyKey pre-function (PARTIAL) for endpoint=${ctx.endpoint ?? "?"}`,
|
|
76014
|
+
`-- Best-effort replay suppression only \u2014 NOT atomic idempotency. See _x_security_warnings.`,
|
|
75992
76015
|
`local key = kong.request.get_header(${luaStr3(idem.header)})`,
|
|
75993
76016
|
`if key ~= nil and key ~= "" then`,
|
|
75994
76017
|
` local cache = ngx.shared.${SS_IDEMPOTENCY_CACHE_DICT}`,
|
|
@@ -75997,7 +76020,7 @@ function buildIdempotencyKeyPlugins(request9, ctx = {}) {
|
|
|
75997
76020
|
` if seen then`,
|
|
75998
76021
|
` kong.log.warn("[${SS_IDEMPOTENCY_TAG}] replayed idempotency key '" .. tostring(key) .. "' within ttl")`,
|
|
75999
76022
|
` return kong.response.exit(409, {`,
|
|
76000
|
-
` message = "
|
|
76023
|
+
` message = "XSecurity: idempotency key replay rejected",`,
|
|
76001
76024
|
` tag = "${SS_IDEMPOTENCY_TAG}"`,
|
|
76002
76025
|
` })`,
|
|
76003
76026
|
` end`,
|
|
@@ -76012,7 +76035,7 @@ function buildIdempotencyKeyPlugins(request9, ctx = {}) {
|
|
|
76012
76035
|
tags: [SS_IDEMPOTENCY_TAG]
|
|
76013
76036
|
}];
|
|
76014
76037
|
}
|
|
76015
|
-
var SS_ACCOUNT_LOCKOUT_TAG = "
|
|
76038
|
+
var SS_ACCOUNT_LOCKOUT_TAG = "x-security-account-lockout";
|
|
76016
76039
|
function lockoutBucket(seconds) {
|
|
76017
76040
|
if (seconds <= 1) return "second";
|
|
76018
76041
|
if (seconds <= 60) return "minute";
|
|
@@ -76116,31 +76139,31 @@ function buildConsumers(spec, options = {}) {
|
|
|
76116
76139
|
const username = consumerName(role);
|
|
76117
76140
|
bundle.consumers.push({
|
|
76118
76141
|
username,
|
|
76119
|
-
tags: [`
|
|
76142
|
+
tags: [`x_security_role=${role}`]
|
|
76120
76143
|
});
|
|
76121
76144
|
bundle.acls.push({ consumer: username, group: role });
|
|
76122
76145
|
if (hasApiKey) {
|
|
76123
76146
|
bundle.keyauth_credentials.push({
|
|
76124
76147
|
consumer: username,
|
|
76125
|
-
key: `${role}_test_key_${shortHash(`${spec.info?.title ?? "
|
|
76148
|
+
key: `${role}_test_key_${shortHash(`${spec.info?.title ?? "x-security"}|${role}`)}`
|
|
76126
76149
|
});
|
|
76127
76150
|
}
|
|
76128
76151
|
if (hasJwt) {
|
|
76129
|
-
const key = jwtIssuer ?? `
|
|
76152
|
+
const key = jwtIssuer ?? `x-security-${shortHash((spec.info?.title ?? "x-security") || "spec", 6)}`;
|
|
76130
76153
|
bundle.jwt_secrets.push({
|
|
76131
76154
|
consumer: username,
|
|
76132
76155
|
// Per-consumer key MUST be unique across the spec (Kong primary key
|
|
76133
76156
|
// on `key`), so we suffix the role.
|
|
76134
76157
|
key: `${key}#${role}`,
|
|
76135
76158
|
algorithm: "HS256",
|
|
76136
|
-
secret: `${role}_jwt_secret_${shortHash(`${spec.info?.title ?? "
|
|
76159
|
+
secret: `${role}_jwt_secret_${shortHash(`${spec.info?.title ?? "x-security"}|${role}|jwt`, 16)}`
|
|
76137
76160
|
});
|
|
76138
76161
|
}
|
|
76139
76162
|
if (hasHmac) {
|
|
76140
76163
|
bundle.hmacauth_credentials.push({
|
|
76141
76164
|
consumer: username,
|
|
76142
76165
|
username: role,
|
|
76143
|
-
secret: `${role}_hmac_secret_${shortHash(`${spec.info?.title ?? "
|
|
76166
|
+
secret: `${role}_hmac_secret_${shortHash(`${spec.info?.title ?? "x-security"}|${role}|hmac`, 16)}`
|
|
76144
76167
|
});
|
|
76145
76168
|
}
|
|
76146
76169
|
}
|
|
@@ -76174,8 +76197,8 @@ function buildConsumers(spec, options = {}) {
|
|
|
76174
76197
|
}
|
|
76175
76198
|
|
|
76176
76199
|
// src/generators/kong/index.ts
|
|
76177
|
-
var
|
|
76178
|
-
function uuidv5(name, namespace =
|
|
76200
|
+
var X_SECURITY_KONG_NAMESPACE = "a4f7e8c2-1b3d-4e5f-9a8b-7c6d5e4f3a2b";
|
|
76201
|
+
function uuidv5(name, namespace = X_SECURITY_KONG_NAMESPACE) {
|
|
76179
76202
|
const nsBytes = Buffer.from(namespace.replace(/-/g, ""), "hex");
|
|
76180
76203
|
const hash = createHash2("sha1").update(nsBytes).update(name).digest();
|
|
76181
76204
|
const bytes = Buffer.from(hash.subarray(0, 16));
|
|
@@ -76240,7 +76263,7 @@ function buildEndpointPlugins(ep, ctx) {
|
|
|
76240
76263
|
warn: ctx.warn
|
|
76241
76264
|
}),
|
|
76242
76265
|
// v0.7 authentication.accountLockout — per-credential rate-limit throttle
|
|
76243
|
-
// (PARTIAL — not a true failed-attempt lockout; see
|
|
76266
|
+
// (PARTIAL — not a true failed-attempt lockout; see _x_security_warnings).
|
|
76244
76267
|
...buildAccountLockoutPlugins(p2.authentication, {
|
|
76245
76268
|
...ep.operationId ? { endpoint: ep.operationId } : {},
|
|
76246
76269
|
warn: ctx.warn
|
|
@@ -76267,7 +76290,7 @@ function buildEndpointPlugins(ep, ctx) {
|
|
|
76267
76290
|
warn: ctx.warn
|
|
76268
76291
|
}),
|
|
76269
76292
|
// W26: rate-limit fingerprint composite key (ip+ua hash) — runs before
|
|
76270
|
-
// request-side checks so the X-
|
|
76293
|
+
// request-side checks so the X-XSecurity-Fingerprint header is set
|
|
76271
76294
|
// before any plugin (or upstream) might consume it.
|
|
76272
76295
|
...buildRateLimitFingerprintPlugins(p2.rateLimit, {
|
|
76273
76296
|
...ep.operationId ? { endpoint: ep.operationId } : {}
|
|
@@ -76295,7 +76318,7 @@ function buildEndpointPlugins(ep, ctx) {
|
|
|
76295
76318
|
warn: ctx.warn
|
|
76296
76319
|
}),
|
|
76297
76320
|
// v0.7 request.idempotencyKey — best-effort shared_dict replay suppression
|
|
76298
|
-
// (PARTIAL — not atomic idempotency; see
|
|
76321
|
+
// (PARTIAL — not atomic idempotency; see _x_security_warnings).
|
|
76299
76322
|
...buildIdempotencyKeyPlugins(p2.request, {
|
|
76300
76323
|
...ep.operationId ? { endpoint: ep.operationId } : {},
|
|
76301
76324
|
warn: ctx.warn
|
|
@@ -76478,9 +76501,9 @@ function createKongGenerator(opts = {}) {
|
|
|
76478
76501
|
"request.schema": "partial",
|
|
76479
76502
|
// request-validator is enterprise-only for body_schema; OSS users see degraded enforcement (mass-assign + sqli pre-functions partially cover)
|
|
76480
76503
|
"request.schema.injectionGuard": "unsupported",
|
|
76481
|
-
// HONEST: Kong OSS has no libinjection/@detectSQLi; a pre-function regex would be a fragile fake (Rule D-1). Abstain +
|
|
76504
|
+
// HONEST: Kong OSS has no libinjection/@detectSQLi; a pre-function regex would be a fragile fake (Rule D-1). Abstain + _x_security_warnings divergence note; front with coraza/bunkerweb which carry SSEC-INJECTION.
|
|
76482
76505
|
"request.denyUnknownFields": "full",
|
|
76483
|
-
// K-3: pre-function rejects unknown top-level body fields with
|
|
76506
|
+
// K-3: pre-function rejects unknown top-level body fields with x-security-mass-assign-403
|
|
76484
76507
|
"request.allowedFields": "full",
|
|
76485
76508
|
// K-3: pre-function allowlist enforcement
|
|
76486
76509
|
"request.signature": "partial",
|
|
@@ -76533,11 +76556,11 @@ function createKongGenerator(opts = {}) {
|
|
|
76533
76556
|
"response.forbidArrayRoot": "full",
|
|
76534
76557
|
// v0.7 API3: body_filter post-function cjson-decodes the body and rewrites a bare top-level array response to 502. Decoded-value check (not raw regex) → full.
|
|
76535
76558
|
"request.idempotencyKey": "partial",
|
|
76536
|
-
// v0.7 API6: HONEST — Kong OSS has no native idempotency. pre-function shared_dict dedupe is best-effort (per-instance, non-atomic check-and-set); suppresses sequential replays but races on concurrent first-requests.
|
|
76559
|
+
// v0.7 API6: HONEST — Kong OSS has no native idempotency. pre-function shared_dict dedupe is best-effort (per-instance, non-atomic check-and-set); suppresses sequential replays but races on concurrent first-requests. _x_security_warnings divergence note. Never full.
|
|
76537
76560
|
"authentication.accountLockout": "partial",
|
|
76538
|
-
// v0.7: HONEST — no native per-credential lockout. rate-limiting limit_by=header throttles per-credential attempts (blunts brute-force) but is NOT a fixed-duration lockout and counts all attempts (gateway can't see the auth verdict). Body-field identifiers degrade to unsupported+warning.
|
|
76561
|
+
// v0.7: HONEST — no native per-credential lockout. rate-limiting limit_by=header throttles per-credential attempts (blunts brute-force) but is NOT a fixed-duration lockout and counts all attempts (gateway can't see the auth verdict). Body-field identifiers degrade to unsupported+warning. _x_security_warnings note. Never full.
|
|
76539
76562
|
"deprecated": "full",
|
|
76540
|
-
// K-5: pre-function returns 410 with
|
|
76563
|
+
// K-5: pre-function returns 410 with x-security-deprecated-endpoint-block
|
|
76541
76564
|
"sunsetDate": "full",
|
|
76542
76565
|
// K-5: 410 + RFC 8594 Deprecation: true & Sunset: <date> response headers
|
|
76543
76566
|
"replacementEndpoint": "partial",
|
|
@@ -76551,9 +76574,9 @@ function createKongGenerator(opts = {}) {
|
|
|
76551
76574
|
}
|
|
76552
76575
|
function renderWarningsHeader(warnings, deployment, edition) {
|
|
76553
76576
|
const lines = [];
|
|
76554
|
-
lines.push(`#
|
|
76577
|
+
lines.push(`# XSecurity Kong generator \u2014 deployment=${deployment} edition=${edition}`);
|
|
76555
76578
|
if (!warnings.length) {
|
|
76556
|
-
lines.push("#
|
|
76579
|
+
lines.push("# _x_security_warnings: [] (no spec\u2192runtime divergences detected)");
|
|
76557
76580
|
lines.push("");
|
|
76558
76581
|
return lines.join("\n");
|
|
76559
76582
|
}
|
|
@@ -76562,7 +76585,7 @@ function renderWarningsHeader(warnings, deployment, edition) {
|
|
|
76562
76585
|
const ep = w.endpoint ? ` (endpoint=${w.endpoint})` : "";
|
|
76563
76586
|
lines.push(`# WARNING: ${w.field}${ep}: declared="${w.declared}" emitted="${w.emitted}" \u2014 ${w.reason}`);
|
|
76564
76587
|
}
|
|
76565
|
-
lines.push("#
|
|
76588
|
+
lines.push("# _x_security_warnings:");
|
|
76566
76589
|
for (const w of warnings) {
|
|
76567
76590
|
lines.push(`# - field: ${yamlScalar(w.field)}`);
|
|
76568
76591
|
if (w.endpoint) lines.push(`# endpoint: ${yamlScalar(w.endpoint)}`);
|