edgeaegis 2026.10.1

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.
Files changed (158) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +10 -0
  3. package/README.md +215 -0
  4. package/config.yml +29 -0
  5. package/origin/cloudflare/app/config.yml +2 -0
  6. package/origin/cloudflare/app/index.js +4 -0
  7. package/origin/edgeone/app/config.yml +2 -0
  8. package/origin/edgeone/app/index.js +4 -0
  9. package/origin/esa/app/config.yml +2 -0
  10. package/origin/esa/app/index.js +4 -0
  11. package/origin/netlify/app/config.yml +2 -0
  12. package/origin/netlify/app/index.js +4 -0
  13. package/origin/vercel/app/config.yml +2 -0
  14. package/origin/vercel/app/index.js +4 -0
  15. package/package.json +123 -0
  16. package/respond/403.html +2 -0
  17. package/respond/403.json +1 -0
  18. package/rules/allowlist.yml +2 -0
  19. package/rules/emergency.conf +4 -0
  20. package/rules/local.conf +2 -0
  21. package/rules/seclang-bundle.json +360 -0
  22. package/rules/seclang-upstreams.json +14 -0
  23. package/rules/setup.conf +15 -0
  24. package/secret/credentials.example.yml +7 -0
  25. package/secret/relay-server.example.yml +10 -0
  26. package/tools/.build/scr/adapters/cloudflare/runtime.d.ts +18 -0
  27. package/tools/.build/scr/adapters/cloudflare/runtime.js +16 -0
  28. package/tools/.build/scr/adapters/cloudflare/runtime.js.map +1 -0
  29. package/tools/.build/scr/adapters/cloudflare/worker.js +25 -0
  30. package/tools/.build/scr/adapters/edgeone/worker.js +7 -0
  31. package/tools/.build/scr/adapters/esa/worker.js +5 -0
  32. package/tools/.build/scr/adapters/forward.d.ts +4 -0
  33. package/tools/.build/scr/adapters/forward.js +25 -0
  34. package/tools/.build/scr/adapters/forward.js.map +1 -0
  35. package/tools/.build/scr/adapters/handler.js +39 -0
  36. package/tools/.build/scr/adapters/ingress.d.ts +10 -0
  37. package/tools/.build/scr/adapters/ingress.js +62 -0
  38. package/tools/.build/scr/adapters/ingress.js.map +1 -0
  39. package/tools/.build/scr/adapters/netlify/worker.js +7 -0
  40. package/tools/.build/scr/adapters/runtime.d.ts +33 -0
  41. package/tools/.build/scr/adapters/runtime.js +50 -0
  42. package/tools/.build/scr/adapters/runtime.js.map +1 -0
  43. package/tools/.build/scr/adapters/vercel/worker.js +7 -0
  44. package/tools/.build/scr/cli/config.d.ts +2 -0
  45. package/tools/.build/scr/cli/config.js +23 -0
  46. package/tools/.build/scr/cli/config.js.map +1 -0
  47. package/tools/.build/scr/cli/deployment.d.ts +15 -0
  48. package/tools/.build/scr/cli/deployment.js +108 -0
  49. package/tools/.build/scr/cli/deployment.js.map +1 -0
  50. package/tools/.build/scr/cli/init.d.ts +2 -0
  51. package/tools/.build/scr/cli/init.js +62 -0
  52. package/tools/.build/scr/cli/init.js.map +1 -0
  53. package/tools/.build/scr/cli/main.d.ts +2 -0
  54. package/tools/.build/scr/cli/main.js +99 -0
  55. package/tools/.build/scr/cli/main.js.map +1 -0
  56. package/tools/.build/scr/cli/relay-serve.d.ts +1 -0
  57. package/tools/.build/scr/cli/relay-serve.js +48 -0
  58. package/tools/.build/scr/cli/relay-serve.js.map +1 -0
  59. package/tools/.build/scr/cli/responses.d.ts +2 -0
  60. package/tools/.build/scr/cli/responses.js +31 -0
  61. package/tools/.build/scr/cli/responses.js.map +1 -0
  62. package/tools/.build/scr/cli/serve.d.ts +1 -0
  63. package/tools/.build/scr/cli/serve.js +132 -0
  64. package/tools/.build/scr/cli/serve.js.map +1 -0
  65. package/tools/.build/scr/config.d.ts +39 -0
  66. package/tools/.build/scr/config.js +89 -0
  67. package/tools/.build/scr/config.js.map +1 -0
  68. package/tools/.build/scr/core/types.d.ts +27 -0
  69. package/tools/.build/scr/core/types.js +34 -0
  70. package/tools/.build/scr/core/types.js.map +1 -0
  71. package/tools/.build/scr/core/version.d.ts +1 -0
  72. package/tools/.build/scr/core/version.js +3 -0
  73. package/tools/.build/scr/core/version.js.map +1 -0
  74. package/tools/.build/scr/http-boundary/index.d.ts +19 -0
  75. package/tools/.build/scr/http-boundary/index.js +173 -0
  76. package/tools/.build/scr/http-boundary/index.js.map +1 -0
  77. package/tools/.build/scr/http-boundary/json.d.ts +2 -0
  78. package/tools/.build/scr/http-boundary/json.js +90 -0
  79. package/tools/.build/scr/http-boundary/json.js.map +1 -0
  80. package/tools/.build/scr/origin/relay.d.ts +20 -0
  81. package/tools/.build/scr/origin/relay.js +221 -0
  82. package/tools/.build/scr/origin/relay.js.map +1 -0
  83. package/tools/.build/scr/origin/replay-node.d.ts +8 -0
  84. package/tools/.build/scr/origin/replay-node.js +36 -0
  85. package/tools/.build/scr/origin/replay-node.js.map +1 -0
  86. package/tools/.build/scr/origin/types.d.ts +18 -0
  87. package/tools/.build/scr/origin/types.js +2 -0
  88. package/tools/.build/scr/origin/types.js.map +1 -0
  89. package/tools/.build/scr/runtime/allowlist.d.ts +21 -0
  90. package/tools/.build/scr/runtime/allowlist.js +84 -0
  91. package/tools/.build/scr/runtime/allowlist.js.map +1 -0
  92. package/tools/.build/scr/runtime/browser-cache.d.ts +7 -0
  93. package/tools/.build/scr/runtime/browser-cache.js +50 -0
  94. package/tools/.build/scr/runtime/browser-cache.js.map +1 -0
  95. package/tools/.build/scr/runtime/error-pages.d.ts +10 -0
  96. package/tools/.build/scr/runtime/error-pages.js +72 -0
  97. package/tools/.build/scr/runtime/error-pages.js.map +1 -0
  98. package/tools/.build/scr/runtime/index.d.ts +54 -0
  99. package/tools/.build/scr/runtime/index.js +355 -0
  100. package/tools/.build/scr/runtime/index.js.map +1 -0
  101. package/tools/.build/scr/runtime/node.d.ts +3 -0
  102. package/tools/.build/scr/runtime/node.js +97 -0
  103. package/tools/.build/scr/runtime/node.js.map +1 -0
  104. package/tools/.build/scr/runtime/proxy.d.ts +12 -0
  105. package/tools/.build/scr/runtime/proxy.js +177 -0
  106. package/tools/.build/scr/runtime/proxy.js.map +1 -0
  107. package/tools/.build/scr/runtime/stream.d.ts +8 -0
  108. package/tools/.build/scr/runtime/stream.js +72 -0
  109. package/tools/.build/scr/runtime/stream.js.map +1 -0
  110. package/tools/.build/scr/seclang/body.d.ts +13 -0
  111. package/tools/.build/scr/seclang/body.js +109 -0
  112. package/tools/.build/scr/seclang/body.js.map +1 -0
  113. package/tools/.build/scr/seclang/bytes.d.ts +5 -0
  114. package/tools/.build/scr/seclang/bytes.js +38 -0
  115. package/tools/.build/scr/seclang/bytes.js.map +1 -0
  116. package/tools/.build/scr/seclang/index.d.ts +166 -0
  117. package/tools/.build/scr/seclang/index.js +1519 -0
  118. package/tools/.build/scr/seclang/index.js.map +1 -0
  119. package/tools/.build/scr/seclang/json.d.ts +2 -0
  120. package/tools/.build/scr/seclang/json.js +87 -0
  121. package/tools/.build/scr/seclang/json.js.map +1 -0
  122. package/tools/.build/scr/seclang/libinjection.d.ts +9 -0
  123. package/tools/.build/scr/seclang/libinjection.js +59 -0
  124. package/tools/.build/scr/seclang/libinjection.js.map +1 -0
  125. package/tools/.build/scr/seclang/node.d.ts +22 -0
  126. package/tools/.build/scr/seclang/node.js +113 -0
  127. package/tools/.build/scr/seclang/node.js.map +1 -0
  128. package/tools/.build/scr/seclang/pcre2.d.ts +23 -0
  129. package/tools/.build/scr/seclang/pcre2.js +153 -0
  130. package/tools/.build/scr/seclang/pcre2.js.map +1 -0
  131. package/tools/.build/scr/seclang/sources.d.ts +6 -0
  132. package/tools/.build/scr/seclang/sources.js +59 -0
  133. package/tools/.build/scr/seclang/sources.js.map +1 -0
  134. package/tools/.build/scr/seclang/state.d.ts +17 -0
  135. package/tools/.build/scr/seclang/state.js +53 -0
  136. package/tools/.build/scr/seclang/state.js.map +1 -0
  137. package/tools/.build/scr/seclang/syntax.d.ts +9 -0
  138. package/tools/.build/scr/seclang/syntax.js +74 -0
  139. package/tools/.build/scr/seclang/syntax.js.map +1 -0
  140. package/tools/.build/scr/seclang/transforms.d.ts +3 -0
  141. package/tools/.build/scr/seclang/transforms.js +157 -0
  142. package/tools/.build/scr/seclang/transforms.js.map +1 -0
  143. package/tools/.build/scr/seclang/unicode.d.ts +2 -0
  144. package/tools/.build/scr/seclang/unicode.js +19 -0
  145. package/tools/.build/scr/seclang/unicode.js.map +1 -0
  146. package/tools/.build/scr/seclang/vendor/LIBINJECTION-LICENSE +33 -0
  147. package/tools/.build/scr/seclang/vendor/PCRE2-NOTICE +47 -0
  148. package/tools/.build/scr/seclang/vendor/PCRE2-WRAPPER-LICENSE +21 -0
  149. package/tools/.build/scr/seclang/vendor/UNICODE-DATA-NOTICE +6 -0
  150. package/tools/.build/scr/seclang/vendor/libinjection.json +7 -0
  151. package/tools/.build/scr/seclang/vendor/libinjection.wasm +0 -0
  152. package/tools/.build/scr/seclang/vendor/pcre2.json +7 -0
  153. package/tools/.build/scr/seclang/vendor/pcre2.wasm +0 -0
  154. package/tools/.build/scr/seclang/vendor/unicode.json +378 -0
  155. package/tools/build-platform.mjs +199 -0
  156. package/tools/platform-command.mjs +59 -0
  157. package/tools/sync-native-seclang.mjs +155 -0
  158. package/tools/verify-full-crs.mjs +118 -0
@@ -0,0 +1,1519 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * EdgeAegis's first-party SecLang state machine (no ModSecurity process).
4
+ * Compiles native rules, source includes and data into executable transaction instructions.
5
+ * Data is a Request-like object or real Web Request; no I/O occurs while executing.
6
+ */
7
+ import ipaddr from "ipaddr.js";
8
+ import { bodyProcessor, parseBody, xmlValues } from "./body.js";
9
+ import { asciiLower, formEntries, fromOctets, octets, utf8Bytes } from "./bytes.js";
10
+ import { expandSecLangSources, sourcePath } from "./sources.js";
11
+ import { CollectionStore, collectionRecord } from "./state.js";
12
+ import { transform, transformations } from "./transforms.js";
13
+ import { defaultUnicodeMap, parseUnicodeMap } from "./unicode.js";
14
+ export { createInjectionDetectors } from "./libinjection.js";
15
+ export { createPCRE2Backend, PCRE2Error } from "./pcre2.js";
16
+ export function splitActions(source) {
17
+ const entries = [];
18
+ let current = "";
19
+ let quote = false;
20
+ let escaped = false;
21
+ for (const ch of source) {
22
+ if (escaped) {
23
+ current += ch;
24
+ escaped = false;
25
+ continue;
26
+ }
27
+ if (ch === "\\") {
28
+ current += ch;
29
+ escaped = true;
30
+ continue;
31
+ }
32
+ if (ch === "'")
33
+ quote = !quote;
34
+ if (ch === "," && !quote) {
35
+ entries.push(current.trim());
36
+ current = "";
37
+ }
38
+ else
39
+ current += ch;
40
+ }
41
+ if (quote)
42
+ throw new Error("Unterminated SecLang action quote");
43
+ if (current.trim())
44
+ entries.push(current.trim());
45
+ return entries;
46
+ }
47
+ function parseActions(text) {
48
+ const out = [];
49
+ for (const part of splitActions(text)) {
50
+ const colon = part.indexOf(":");
51
+ const name = (colon < 0 ? part : part.slice(0, colon)).toLowerCase();
52
+ let arg = colon < 0 ? undefined : part.slice(colon + 1);
53
+ if (arg?.startsWith("'") && arg.endsWith("'"))
54
+ arg = arg.slice(1, -1);
55
+ out.push({ name, arg });
56
+ }
57
+ return out;
58
+ }
59
+ const ACTIONS = new Set([
60
+ "id",
61
+ "phase",
62
+ "deny",
63
+ "block",
64
+ "pass",
65
+ "log",
66
+ "nolog",
67
+ "auditlog",
68
+ "noauditlog",
69
+ "t",
70
+ "msg",
71
+ "logdata",
72
+ "tag",
73
+ "ver",
74
+ "severity",
75
+ "rev",
76
+ "maturity",
77
+ "accuracy",
78
+ "chain",
79
+ "setvar",
80
+ "skipafter",
81
+ "status",
82
+ "capture",
83
+ "ctl",
84
+ "multimatch",
85
+ "initcol",
86
+ "expirevar",
87
+ "deprecatevar",
88
+ "skip",
89
+ ]);
90
+ const OPERATOR_NAMES = new Set([
91
+ "streq",
92
+ "contains",
93
+ "beginswith",
94
+ "endswith",
95
+ "within",
96
+ "eq",
97
+ "lt",
98
+ "le",
99
+ "ge",
100
+ "gt",
101
+ "rx",
102
+ "pm",
103
+ "pmfromfile",
104
+ "detectsqli",
105
+ "detectxss",
106
+ "ipmatch",
107
+ "ipmatchfromfile",
108
+ "unconditionalmatch",
109
+ "validatebyterange",
110
+ "validateurlencoding",
111
+ "validateutf8encoding",
112
+ ]);
113
+ const TRANSFORM_NAMES = new Set(Object.keys(transformations));
114
+ const BYTE_COLLECTIONS = new Set([
115
+ "ARGS",
116
+ "ARGS_NAMES",
117
+ "ARGS_GET",
118
+ "ARGS_POST",
119
+ "ARGS_GET_NAMES",
120
+ "ARGS_POST_NAMES",
121
+ "TX",
122
+ "IP",
123
+ "GLOBAL",
124
+ "MATCHED_VAR",
125
+ "MATCHED_VAR_NAME",
126
+ "MATCHED_VARS",
127
+ "REQUEST_HEADERS",
128
+ "REQUEST_HEADERS_NAMES",
129
+ "REQUEST_COOKIES",
130
+ "REQUEST_COOKIES_NAMES",
131
+ "FILES",
132
+ "FILES_NAMES",
133
+ "MULTIPART_PART_HEADERS",
134
+ "RESPONSE_BODY",
135
+ "REQUEST_BODY",
136
+ "REQUEST_FILENAME",
137
+ "REQUEST_BASENAME",
138
+ ]);
139
+ const VARIABLES = new Set([
140
+ "REQUEST_METHOD",
141
+ "REQUEST_URI",
142
+ "REQUEST_URI_RAW",
143
+ "REQUEST_FILENAME",
144
+ "REQUEST_BASENAME",
145
+ "REQUEST_HEADERS",
146
+ "REQUEST_HEADERS_NAMES",
147
+ "REQUEST_COOKIES",
148
+ "REQUEST_COOKIES_NAMES",
149
+ "REQUEST_BODY",
150
+ "ARGS",
151
+ "ARGS_NAMES",
152
+ "ARGS_GET",
153
+ "ARGS_GET_NAMES",
154
+ "ARGS_POST",
155
+ "ARGS_POST_NAMES",
156
+ "ARGS_COMBINED_SIZE",
157
+ "REQUEST_BODY_LENGTH",
158
+ "MATCHED_VAR",
159
+ "MATCHED_VAR_NAME",
160
+ "QUERY_STRING",
161
+ "TX",
162
+ "REQUEST_PROTOCOL",
163
+ "REQUEST_LINE",
164
+ "REMOTE_ADDR",
165
+ "UNIQUE_ID",
166
+ "REQBODY_PROCESSOR",
167
+ "REQBODY_ERROR",
168
+ "REQBODY_ERROR_MSG",
169
+ "REQBODY_PROCESSOR_ERROR",
170
+ "REQBODY_PROCESSOR_ERROR_MSG",
171
+ "INBOUND_DATA_ERROR",
172
+ "MULTIPART_STRICT_ERROR",
173
+ "FILES",
174
+ "FILES_NAMES",
175
+ "FILES_COMBINED_SIZE",
176
+ "MULTIPART_PART_HEADERS",
177
+ "XML",
178
+ "RESPONSE_BODY",
179
+ "RESPONSE_HEADERS",
180
+ "RESPONSE_STATUS",
181
+ "MATCHED_VARS",
182
+ "IP",
183
+ "GLOBAL",
184
+ ]);
185
+ function compilerError(message, statement) {
186
+ return {
187
+ line: statement.line,
188
+ file: statement.file,
189
+ reason: message,
190
+ rule: statement.args?.[2]?.match(/id:(\d+)/)?.[1],
191
+ };
192
+ }
193
+ function compileSelector(text, regexBackend) {
194
+ const selectors = [];
195
+ const parts = [];
196
+ let begin = 0;
197
+ let regex = false;
198
+ let escaped = false;
199
+ for (let i = 0; i < text.length; i++) {
200
+ const ch = text[i];
201
+ if (escaped) {
202
+ escaped = false;
203
+ continue;
204
+ }
205
+ if (ch === "\\") {
206
+ escaped = true;
207
+ continue;
208
+ }
209
+ if (ch === "/" && (regex || (text[i - 1] === ":" && !/^!?&?XML:/i.test(text.slice(begin, i)))))
210
+ regex = !regex;
211
+ if (ch === "|" && !regex) {
212
+ parts.push(text.slice(begin, i));
213
+ begin = i + 1;
214
+ }
215
+ }
216
+ if (regex)
217
+ throw new Error("Unterminated selector regex");
218
+ parts.push(text.slice(begin));
219
+ for (const entry of parts) {
220
+ const negative = entry.startsWith("!");
221
+ const part = negative ? entry.slice(1) : entry;
222
+ const count = part.startsWith("&");
223
+ const field = count ? part.slice(1) : part;
224
+ const colon = field.indexOf(":");
225
+ const key = colon < 0 ? undefined : field.slice(colon + 1);
226
+ const name = (colon < 0 ? field : field.slice(0, colon)).toUpperCase();
227
+ if (!VARIABLES.has(name))
228
+ throw new Error(`Unsupported variable collection: ${name}`);
229
+ if (key?.includes("%{"))
230
+ throw new Error("Dynamic selector macros are not supported");
231
+ let keyRegex;
232
+ if (name !== "XML" && key?.startsWith("/") && key.endsWith("/")) {
233
+ if (!regexBackend)
234
+ throw new Error("Regex selectors require the PCRE2 backend");
235
+ keyRegex = regexBackend.compile(`(?i)${key.slice(1, -1)}`);
236
+ }
237
+ selectors.push({
238
+ negative,
239
+ count,
240
+ name,
241
+ key,
242
+ keyRegex,
243
+ foldedKey: key === undefined ? undefined : asciiLower(utf8Bytes(key)),
244
+ });
245
+ }
246
+ if (!selectors.length || selectors.every((x) => x.negative))
247
+ throw new Error("No positive variable selector");
248
+ return selectors;
249
+ }
250
+ function compileOperator(text, phraseFiles, regexBackend, detectors, fileName) {
251
+ const negated = text.startsWith("!");
252
+ const raw = negated ? text.slice(1) : text;
253
+ const m = /^@([A-Za-z][A-Za-z0-9]*)(?:\s+([\s\S]*))?$/.exec(raw);
254
+ if (!m && raw.startsWith("@"))
255
+ throw new Error("Malformed operator");
256
+ const op = m ? m[1].toLowerCase() : "rx";
257
+ const operand = m ? (m[2] ?? "") : raw;
258
+ if (!OPERATOR_NAMES.has(op))
259
+ throw new Error(`Unsupported operator ${op}`);
260
+ const dynamic = operand.includes("%{");
261
+ if (dynamic) {
262
+ if (["pm", "pmfromfile", "ipmatch", "ipmatchfromfile", "validatebyterange"].includes(op))
263
+ throw new Error("Unsupported operator macro");
264
+ validateMacros(operand);
265
+ }
266
+ let regex;
267
+ if (op === "rx" && !regexBackend)
268
+ throw new Error("Regex operators require the PCRE2 backend");
269
+ if (op === "rx" && !dynamic) {
270
+ try {
271
+ regex = regexBackend.compile(operand);
272
+ }
273
+ catch (error) {
274
+ throw new Error(`Regex compile failed: ${String(error.message)}`);
275
+ }
276
+ }
277
+ let phrases;
278
+ if (["pmfromfile", "ipmatchfromfile"].includes(op)) {
279
+ phrases = [];
280
+ for (const file of operand.split(/\s+/).filter(Boolean)) {
281
+ const path = sourcePath(fileName ?? "main.conf", file);
282
+ const entries = phraseFiles?.[path] ?? phraseFiles?.[file];
283
+ if (!Array.isArray(entries) || !entries.every((v) => typeof v === "string"))
284
+ throw new Error(`Missing explicit operator data file: ${file}`);
285
+ phrases.push(...entries);
286
+ }
287
+ }
288
+ else if (op === "pm")
289
+ phrases = operand.split(/\s+/).filter(Boolean);
290
+ if (["detectsqli", "detectxss"].includes(op) && !detectors)
291
+ throw new Error(`Operator ${op} requires the libinjection primitive backend`);
292
+ let ranges = [];
293
+ if (op === "validatebyterange") {
294
+ ranges = operand.split(",").map((part) => {
295
+ const range = /^(\d+)(?:-(\d+))?$/.exec(part.trim());
296
+ if (!range)
297
+ throw new Error("Invalid byte range");
298
+ const start = Number(range[1]), end = Number(range[2] ?? range[1]);
299
+ if (start > end || end > 255)
300
+ throw new Error("Invalid byte range");
301
+ return [start, end];
302
+ });
303
+ }
304
+ let networks = [];
305
+ if (["ipmatch", "ipmatchfromfile"].includes(op))
306
+ networks = (op === "ipmatchfromfile" ? phrases.join("\n") : operand)
307
+ .split(/[\s,]+/)
308
+ .filter(Boolean)
309
+ .map((value) => value.includes("/")
310
+ ? ipaddr.parseCIDR(value)
311
+ : [ipaddr.parse(value), ipaddr.parse(value).kind() === "ipv4" ? 32 : 128]);
312
+ return {
313
+ op,
314
+ operand: utf8Bytes(operand),
315
+ negated,
316
+ regex,
317
+ phrases: phrases?.map((phrase) => asciiLower(utf8Bytes(phrase))),
318
+ dynamic,
319
+ detectors,
320
+ ranges,
321
+ networks,
322
+ dynamicRegexBackend: regexBackend?.untracked ?? regexBackend,
323
+ };
324
+ }
325
+ function compileActionSet(actions) {
326
+ const data = {
327
+ phase: undefined,
328
+ transformations: [],
329
+ changes: [],
330
+ controls: [],
331
+ initializers: [],
332
+ effects: [],
333
+ tags: [],
334
+ log: true,
335
+ audit: true,
336
+ };
337
+ for (const { name, arg } of actions) {
338
+ if (!ACTIONS.has(name))
339
+ throw new Error(`Unsupported action ${name}`);
340
+ if (name === "phase") {
341
+ const phase = Number(arg);
342
+ if (![1, 2, 3, 4, 5].includes(phase))
343
+ throw new Error("Unsupported SecLang phase");
344
+ data.phase = phase;
345
+ }
346
+ else if (name === "id") {
347
+ if (!/^\d{1,10}$/.test(arg ?? ""))
348
+ throw new Error("Invalid rule id");
349
+ data.id = arg;
350
+ }
351
+ else if (name === "chain")
352
+ data.chain = true;
353
+ else if (["deny", "block", "pass"].includes(name))
354
+ data.decision = name;
355
+ else if (name === "t") {
356
+ const v = arg?.toLowerCase();
357
+ if (!TRANSFORM_NAMES.has(v))
358
+ throw new Error(`Unsupported transform ${arg}`);
359
+ if (v === "none")
360
+ data.transformations = [];
361
+ else
362
+ data.transformations.push(v);
363
+ }
364
+ else if (name === "setvar") {
365
+ const m = /^(!)?(tx|ip|global)\.([^=]+)(?:=([\s\S]*))?$/i.exec(arg ?? "");
366
+ if (!m)
367
+ throw new Error("Unsupported setvar expression");
368
+ validateMacros(m[3]);
369
+ validateMacros(m[4] ?? "");
370
+ const change = {
371
+ collection: m[2].toUpperCase(),
372
+ name: utf8Bytes(m[3]),
373
+ value: utf8Bytes(m[4] ?? "1"),
374
+ remove: Boolean(m[1]),
375
+ };
376
+ data.changes.push(change);
377
+ data.effects.push({ type: "setvar", ...change });
378
+ }
379
+ else if (name === "expirevar" || name === "deprecatevar") {
380
+ const match = /^(tx|ip|global)\.([^=]+)=([\s\S]+)$/i.exec(arg ?? "");
381
+ if (!match)
382
+ throw new Error(`Invalid ${name} expression`);
383
+ validateMacros(match[2]);
384
+ validateMacros(match[3]);
385
+ if (name === "deprecatevar" && match[1].toUpperCase() === "TX")
386
+ throw new Error("deprecatevar requires a persistent collection");
387
+ data.effects.push({
388
+ type: name,
389
+ collection: match[1].toUpperCase(),
390
+ name: utf8Bytes(match[2]),
391
+ value: utf8Bytes(match[3]),
392
+ });
393
+ }
394
+ else if (name === "skipafter")
395
+ data.skipAfter = arg;
396
+ else if (name === "skip") {
397
+ if (!/^\d+$/.test(arg ?? ""))
398
+ throw new Error("Invalid skip count");
399
+ data.skip = Number(arg);
400
+ }
401
+ else if (name === "status") {
402
+ if (!/^[45]\d\d$/.test(arg ?? ""))
403
+ throw new Error("Unsupported status");
404
+ data.status = Number(arg);
405
+ }
406
+ else if (name === "capture")
407
+ data.capture = true;
408
+ else if (name === "msg") {
409
+ validateMacros(arg ?? "");
410
+ data.message = arg;
411
+ }
412
+ else if (name === "severity")
413
+ data.severity = arg;
414
+ else if (name === "log")
415
+ data.log = true;
416
+ else if (name === "nolog")
417
+ data.log = false;
418
+ else if (name === "auditlog")
419
+ data.audit = true;
420
+ else if (name === "noauditlog")
421
+ data.audit = false;
422
+ else if (name === "logdata") {
423
+ validateMacros(arg ?? "");
424
+ data.logdata = utf8Bytes(arg ?? "");
425
+ }
426
+ else if (name === "tag")
427
+ data.tags.push(arg);
428
+ else if (name === "multimatch")
429
+ data.multiMatch = true;
430
+ else if (name === "initcol") {
431
+ const m = /^(ip|global)=([\s\S]+)$/i.exec(arg ?? "");
432
+ if (!m)
433
+ throw new Error("Unsupported initcol expression");
434
+ validateMacros(m[2]);
435
+ const init = { name: m[1].toUpperCase(), key: m[2] };
436
+ data.initializers.push(init);
437
+ data.effects.push({ type: "initcol", ...init });
438
+ }
439
+ else if (name === "ctl") {
440
+ const m = /^([a-z]+)=([\s\S]+)$/i.exec(arg ?? "");
441
+ if (!m)
442
+ throw new Error("Malformed ctl action");
443
+ const option = m[1].toLowerCase(), value = m[2];
444
+ const allowed = {
445
+ auditengine: ["On", "Off", "RelevantOnly"],
446
+ ruleengine: ["On", "Off", "DetectionOnly"],
447
+ forcerequestbodyvariable: ["On", "Off"],
448
+ requestbodyprocessor: ["URLENCODED", "JSON", "XML", "MULTIPART"],
449
+ requestbodyaccess: ["On", "Off"],
450
+ responsebodyaccess: ["On", "Off"],
451
+ }[option];
452
+ if (allowed
453
+ ? !allowed.includes(value)
454
+ : ![
455
+ "ruleremovebyid",
456
+ "ruleremovebytag",
457
+ "ruleremovetargetbyid",
458
+ "ruleremovetargetbytag",
459
+ ].includes(option))
460
+ throw new Error(`Unsupported ctl ${arg}`);
461
+ if (option === "ruleremovebyid" && !/^\d+(?:-\d+)?$/.test(value))
462
+ throw new Error("Invalid ctl rule ID range");
463
+ if (option.startsWith("ruleremovetarget") && !/^[^;]+;[^;]+$/.test(value))
464
+ throw new Error("Invalid ctl target exclusion");
465
+ data.controls.push({ option, value });
466
+ }
467
+ }
468
+ if (data.transformations.length > 24)
469
+ throw new Error("Too many transforms");
470
+ return data;
471
+ }
472
+ export function compileSecLang(source, { phraseFiles = {}, strict = false, regexBackend, detectors, fileName, files, requestBodyLimit = 2 * 1024 * 1024, now = Date.now, } = {}) {
473
+ if (!Number.isSafeInteger(requestBodyLimit) ||
474
+ requestBodyLimit < 0 ||
475
+ requestBodyLimit > 16_777_216)
476
+ throw new Error("Invalid SecLang request body limit");
477
+ const hostRequestBodyLimit = requestBodyLimit;
478
+ const statements = expandSecLangSources(source, { fileName, files });
479
+ phraseFiles = {
480
+ ...Object.fromEntries(Object.entries(files ?? {})
481
+ .filter(([name]) => /\.(data|txt|ipset|list)$/.test(name))
482
+ .map(([name, text]) => [
483
+ name,
484
+ text.split(/\r?\n/).filter((line) => line.trim() && !line.trimStart().startsWith("#")),
485
+ ])),
486
+ ...phraseFiles,
487
+ };
488
+ const resources = new Set();
489
+ if (typeof now !== "function")
490
+ throw new Error("Invalid SecLang clock");
491
+ const collections = new CollectionStore(now);
492
+ let disposed = false;
493
+ if (regexBackend) {
494
+ const underlying = regexBackend;
495
+ regexBackend = {
496
+ untracked: underlying,
497
+ compile(pattern) {
498
+ const regex = underlying.compile(pattern);
499
+ resources.add(regex);
500
+ return regex;
501
+ },
502
+ };
503
+ }
504
+ const dispose = () => {
505
+ for (const regex of resources)
506
+ regex.destroy?.();
507
+ resources.clear();
508
+ collections.clear();
509
+ disposed = true;
510
+ };
511
+ const instructions = [];
512
+ const unsupported = [];
513
+ let prior = null;
514
+ const ids = new Set();
515
+ const defaults = new Map();
516
+ const defaultDecisions = new Map();
517
+ const targetUpdates = [];
518
+ const components = [];
519
+ let unicodeMap = defaultUnicodeMap;
520
+ const settings = {
521
+ engine: "On",
522
+ requestBodyAccess: true,
523
+ responseBodyAccess: true,
524
+ removedIds: [],
525
+ };
526
+ for (const stmt of statements) {
527
+ if ([
528
+ "SecRuleEngine",
529
+ "SecRequestBodyAccess",
530
+ "SecResponseBodyAccess",
531
+ "SecRequestBodyLimit",
532
+ "SecRuleRemoveById",
533
+ ].includes(stmt.directive)) {
534
+ try {
535
+ if (stmt.directive === "SecRuleRemoveById") {
536
+ const ranges = stmt.args.flatMap((arg) => arg.split(/\s+/)).filter(Boolean);
537
+ if (!ranges.length || ranges.some((range) => !/^\d+(?:-\d+)?$/.test(range)))
538
+ throw new Error("Invalid static rule ID exclusion");
539
+ settings.removedIds.push(...ranges);
540
+ }
541
+ else {
542
+ if (stmt.args.length !== 1)
543
+ throw new Error(`Malformed ${stmt.directive}`);
544
+ const value = stmt.args[0];
545
+ if (stmt.directive === "SecRuleEngine") {
546
+ if (!["On", "Off", "DetectionOnly"].includes(value))
547
+ throw new Error("Invalid SecRuleEngine");
548
+ settings.engine = value;
549
+ }
550
+ else if (stmt.directive === "SecRequestBodyLimit") {
551
+ if (!/^\d+$/.test(value) || Number(value) > 16_777_216)
552
+ throw new Error("Invalid SecRequestBodyLimit");
553
+ requestBodyLimit =
554
+ Number(value) > 0
555
+ ? Math.min(hostRequestBodyLimit, Number(value))
556
+ : hostRequestBodyLimit;
557
+ }
558
+ else {
559
+ if (!["On", "Off"].includes(value))
560
+ throw new Error(`Invalid ${stmt.directive}`);
561
+ settings[stmt.directive === "SecRequestBodyAccess" ? "requestBodyAccess" : "responseBodyAccess"] = value === "On";
562
+ }
563
+ }
564
+ }
565
+ catch (error) {
566
+ unsupported.push(compilerError(String(error.message), stmt));
567
+ }
568
+ continue;
569
+ }
570
+ if (stmt.directive === "SecUnicodeMapFile") {
571
+ try {
572
+ if (stmt.args.length !== 2 || !/^\d{1,5}$/.test(stmt.args[1]))
573
+ throw new Error("Malformed SecUnicodeMapFile");
574
+ const path = sourcePath(stmt.file ?? "main.conf", stmt.args[0]);
575
+ if (typeof files?.[path] !== "string")
576
+ throw new Error(`Missing Unicode map: ${path}`);
577
+ unicodeMap = parseUnicodeMap(files[path], Number(stmt.args[1]));
578
+ }
579
+ catch (error) {
580
+ unsupported.push(compilerError(String(error.message), stmt));
581
+ }
582
+ continue;
583
+ }
584
+ if (stmt.directive === "SecDefaultAction") {
585
+ try {
586
+ if (stmt.args.length !== 1)
587
+ throw new Error("Malformed SecDefaultAction");
588
+ const list = parseActions(stmt.args[0]);
589
+ const data = compileActionSet(list);
590
+ if (!data.phase || !data.decision || data.id || data.chain)
591
+ throw new Error("SecDefaultAction requires phase and disruptive action without id/chain");
592
+ defaults.set(data.phase, list);
593
+ defaultDecisions.set(data.phase, data.decision);
594
+ }
595
+ catch (error) {
596
+ unsupported.push(compilerError(String(error.message), stmt));
597
+ }
598
+ continue;
599
+ }
600
+ if (stmt.directive === "SecComponentSignature") {
601
+ if (stmt.args.length !== 1)
602
+ unsupported.push(compilerError("Malformed SecComponentSignature", stmt));
603
+ else
604
+ components.push(stmt.args[0]);
605
+ continue;
606
+ }
607
+ if (stmt.directive === "SecRuleUpdateTargetById") {
608
+ try {
609
+ if (stmt.args.length !== 2 || !/^\d+$/.test(stmt.args[0]))
610
+ throw new Error("Malformed SecRuleUpdateTargetById");
611
+ // Exclusion-only target lists are valid updates, unlike standalone rule selectors.
612
+ const targets = compileSelector(`TX|${stmt.args[1]}`, regexBackend).slice(1);
613
+ targetUpdates.push({ id: stmt.args[0], targets, statement: stmt });
614
+ }
615
+ catch (error) {
616
+ unsupported.push(compilerError(String(error.message), stmt));
617
+ }
618
+ continue;
619
+ }
620
+ if (stmt.directive === "SecMarker" && stmt.args.length === 1) {
621
+ instructions.push({ type: "marker", name: stmt.args[0], line: stmt.line });
622
+ prior = null;
623
+ continue;
624
+ }
625
+ if (stmt.directive !== "SecRule" && stmt.directive !== "SecAction") {
626
+ unsupported.push(compilerError(`Unsupported directive: ${stmt.directive}`, stmt));
627
+ prior = null;
628
+ continue;
629
+ }
630
+ try {
631
+ const isAction = stmt.directive === "SecAction";
632
+ const [selectorsText, operatorText, actionText = ""] = stmt.args;
633
+ const selectors = isAction ? [] : compileSelector(selectorsText, regexBackend);
634
+ const operator = isAction
635
+ ? undefined
636
+ : compileOperator(operatorText, phraseFiles, regexBackend, detectors, stmt.file);
637
+ const explicit = parseActions(isAction ? selectorsText : actionText);
638
+ if (isAction &&
639
+ (explicit.some(({ name }) => name === "chain" || name === "capture") ||
640
+ prior?.actions.chain))
641
+ throw new Error("SecAction cannot participate in a chain or capture");
642
+ if (prior?.actions.chain &&
643
+ explicit.some(({ name }) => [
644
+ "id",
645
+ "phase",
646
+ "deny",
647
+ "block",
648
+ "pass",
649
+ "status",
650
+ "skip",
651
+ "skipafter",
652
+ "msg",
653
+ "tag",
654
+ "severity",
655
+ "logdata",
656
+ "rev",
657
+ ].includes(name)))
658
+ throw new Error("Chain child contains an action restricted to the chain starter");
659
+ const explicitPhase = explicit.findLast(({ name }) => name === "phase");
660
+ const phase = explicitPhase
661
+ ? Number(explicitPhase.arg)
662
+ : prior?.actions.chain
663
+ ? prior.actions.phase
664
+ : 2;
665
+ const actions = compileActionSet([...(defaults.get(phase) ?? []), ...explicit]);
666
+ for (const control of actions.controls) {
667
+ if (control.option.startsWith("ruleremovetarget")) {
668
+ const [filter, target] = control.value.split(";");
669
+ control.filter = filter;
670
+ control.selectors = compileSelector(`TX|!${target.replace(/^!/, "")}`, regexBackend).slice(1);
671
+ }
672
+ }
673
+ if (actions.decision === "block") {
674
+ const inherited = defaultDecisions.get(phase);
675
+ actions.decision = inherited && inherited !== "block" ? inherited : "deny";
676
+ }
677
+ if (phase === 5 && ["deny", "block"].includes(actions.decision))
678
+ throw new Error("Logging phase cannot use disruptive actions");
679
+ if (isAction && (actions.chain || actions.capture || prior?.actions.chain))
680
+ throw new Error("SecAction cannot participate in a chain or capture");
681
+ const step = {
682
+ type: "rule",
683
+ selectors,
684
+ operator,
685
+ actions,
686
+ unconditional: isAction,
687
+ line: stmt.line,
688
+ file: stmt.file,
689
+ };
690
+ if (prior?.actions.chain) {
691
+ step.actions.phase ??= prior.actions.phase;
692
+ prior.next = step;
693
+ }
694
+ else {
695
+ if (!actions.id)
696
+ throw new Error("Root rule requires id");
697
+ actions.phase ??= 2;
698
+ if (ids.has(actions.id))
699
+ throw new Error(`Duplicate rule id ${actions.id}`);
700
+ ids.add(actions.id);
701
+ instructions.push(step);
702
+ }
703
+ prior = step;
704
+ }
705
+ catch (error) {
706
+ unsupported.push(compilerError(String(error.message), stmt));
707
+ prior = null;
708
+ }
709
+ }
710
+ for (const update of targetUpdates) {
711
+ const rule = instructions.find((item) => item.type === "rule" && item.actions.id === update.id);
712
+ if (!rule)
713
+ unsupported.push(compilerError(`Target update references missing rule ${update.id}`, update.statement));
714
+ else
715
+ rule.selectors.push(...update.targets);
716
+ }
717
+ // Do not silently execute a truncated/incomplete chain.
718
+ for (const item of instructions.filter((i) => i.type === "rule")) {
719
+ let node = item;
720
+ while (node.actions.chain) {
721
+ if (!node.next) {
722
+ unsupported.push({ line: node.line, reason: "Incomplete chain", rule: item.actions.id });
723
+ item.invalid = true;
724
+ break;
725
+ }
726
+ node = node.next;
727
+ }
728
+ }
729
+ if (unsupported.length && strict) {
730
+ const first = unsupported[0];
731
+ dispose();
732
+ throw new Error(`SecLang unsupported at ${first.file ?? "main.conf"}:${first.line}: ${first.reason}`);
733
+ }
734
+ const executable = instructions.filter((item) => !item.invalid);
735
+ // Markers participate in every phase; keep their original positions for skipAfter.
736
+ const phaseInstructions = new Map([1, 2, 3, 4, 5].map((phase) => [
737
+ phase,
738
+ executable.filter((item) => item.type === "marker" || item.actions.phase === phase),
739
+ ]));
740
+ return {
741
+ instructions: executable,
742
+ phaseInstructions,
743
+ unsupported,
744
+ supportedRules: instructions.filter((item) => item.type === "rule" && !item.invalid).length,
745
+ sourceRules: statements.filter((item) => item.directive === "SecRule").length,
746
+ components,
747
+ requestBodyLimit,
748
+ unicodeMap,
749
+ settings,
750
+ now,
751
+ collections,
752
+ dispose,
753
+ get disposed() {
754
+ return disposed;
755
+ },
756
+ };
757
+ }
758
+ function transformed(value, transforms, context) {
759
+ return transform(String(value), transforms, context.program.unicodeMap);
760
+ }
761
+ function values(selector, request, tx, phase, context = {}) {
762
+ if (selector.name === "TX") {
763
+ if (phase >= 2 && context.requestBodyAccess !== false && !context.body)
764
+ values({ name: "REQBODY_ERROR" }, request, tx, phase, context);
765
+ if (selector.key !== undefined && !selector.keyRegex) {
766
+ selector.foldedKey ??= asciiLower(utf8Bytes(selector.key));
767
+ return tx.has(selector.foldedKey)
768
+ ? [[selector.foldedKey, String(tx.get(selector.foldedKey))]]
769
+ : [];
770
+ }
771
+ const entries = [...tx].map(([key, value]) => [key, String(value)]);
772
+ return selector.key === undefined
773
+ ? entries
774
+ : entries.filter(([key]) => selectorKeyMatches(selector, key));
775
+ }
776
+ context.url ??= new URL(request.url);
777
+ context.headers ??= new Headers(request.headers ?? {});
778
+ context.target ??=
779
+ request.rawTarget ?? (/^https?:\/\/[^/?#]+([^#]*)/i.exec(request.url)?.[1] || "/");
780
+ const target = context.target;
781
+ if (!context.targetParts) {
782
+ const question = target.indexOf("?");
783
+ context.targetParts = {
784
+ rawPath: question < 0 ? target : target.slice(0, question),
785
+ query: question < 0 ? "" : target.slice(question + 1),
786
+ };
787
+ }
788
+ const { rawPath, query } = context.targetParts;
789
+ context.filename ??= utf8Bytes(rawPath).replace(/%([\da-f]{2})/gi, (_all, hex) => String.fromCharCode(Number.parseInt(hex, 16)));
790
+ const headers = context.headers;
791
+ let entries = [];
792
+ context.getArgs ??= formEntries(query);
793
+ const getArgs = context.getArgs;
794
+ if (context.processor === undefined || context.processor === null)
795
+ context.defaultBodyProcessor ??= bodyProcessor(headers);
796
+ const processor = context.processor ?? context.defaultBodyProcessor;
797
+ if (phase >= 2 && context.requestBodyAccess !== false && !context.body) {
798
+ context.bodyParseError = false;
799
+ context.bodyErrorMessage = "";
800
+ try {
801
+ context.body = parseBody(request.bodyText ?? "", headers, processor, request.bodyBytes, context.program?.requestBodyLimit);
802
+ }
803
+ catch {
804
+ context.bodyParseError = true;
805
+ context.bodyErrorMessage = `Invalid ${processor || "raw"} request body`;
806
+ context.body = { args: [], files: [], partHeaders: [], xml: undefined };
807
+ }
808
+ }
809
+ const body = phase >= 2 && context.requestBodyAccess !== false ? context.body : undefined;
810
+ const postArgs = body?.args ?? [];
811
+ // Immutable request collections are materialized only when selected. Never cache TX,
812
+ // ctl-dependent body collections or matched variables: actions can change them mid-chain.
813
+ switch (selector.name) {
814
+ case "REQUEST_METHOD":
815
+ entries = [["REQUEST_METHOD", request.method]];
816
+ break;
817
+ case "REQUEST_URI":
818
+ entries = [["REQUEST_URI", target]];
819
+ break;
820
+ case "REQUEST_URI_RAW":
821
+ entries = [["REQUEST_URI_RAW", target]];
822
+ break;
823
+ case "REQUEST_FILENAME":
824
+ entries = [["REQUEST_FILENAME", context.filename]];
825
+ break;
826
+ case "REQUEST_BASENAME":
827
+ entries = [["REQUEST_BASENAME", context.filename.split("/").pop()]];
828
+ break;
829
+ case "REQUEST_PROTOCOL":
830
+ if (!request.protocol)
831
+ throw new Error("SecLang REQUEST_PROTOCOL requires trusted transport metadata");
832
+ entries = [["REQUEST_PROTOCOL", request.protocol]];
833
+ break;
834
+ case "REQUEST_LINE":
835
+ if (!request.requestLine && !request.protocol)
836
+ throw new Error("SecLang REQUEST_LINE requires trusted transport metadata");
837
+ entries = [
838
+ ["REQUEST_LINE", request.requestLine ?? `${request.method} ${target} ${request.protocol}`],
839
+ ];
840
+ break;
841
+ case "REMOTE_ADDR":
842
+ if (!request.remoteAddr)
843
+ throw new Error("SecLang REMOTE_ADDR requires a trusted client address");
844
+ entries = [["REMOTE_ADDR", request.remoteAddr]];
845
+ break;
846
+ case "UNIQUE_ID":
847
+ entries = [["UNIQUE_ID", context.uniqueId]];
848
+ break;
849
+ case "REQBODY_PROCESSOR":
850
+ entries = [["REQBODY_PROCESSOR", processor]];
851
+ break;
852
+ case "REQBODY_ERROR":
853
+ case "REQBODY_PROCESSOR_ERROR":
854
+ entries = phase >= 2 ? [[selector.name, context.bodyParseError ? "1" : "0"]] : [];
855
+ break;
856
+ case "REQBODY_ERROR_MSG":
857
+ case "REQBODY_PROCESSOR_ERROR_MSG":
858
+ entries = phase >= 2 ? [[selector.name, context.bodyErrorMessage ?? ""]] : [];
859
+ break;
860
+ case "MULTIPART_STRICT_ERROR":
861
+ entries =
862
+ phase >= 2
863
+ ? [[selector.name, processor === "MULTIPART" && context.bodyParseError ? "1" : "0"]]
864
+ : [];
865
+ break;
866
+ case "INBOUND_DATA_ERROR":
867
+ entries =
868
+ phase >= 2
869
+ ? [
870
+ [
871
+ selector.name,
872
+ (request.bodyBytes?.length ??
873
+ new TextEncoder().encode(request.bodyText ?? "").length) >
874
+ context.program.requestBodyLimit
875
+ ? "1"
876
+ : "0",
877
+ ],
878
+ ]
879
+ : [];
880
+ break;
881
+ case "FILES":
882
+ entries = (body?.files ?? []).map((file) => [file.name, file.filename]);
883
+ break;
884
+ case "FILES_NAMES":
885
+ entries = (body?.files ?? []).map((file) => [file.name, file.name]);
886
+ break;
887
+ case "FILES_COMBINED_SIZE":
888
+ entries = body
889
+ ? [["FILES_COMBINED_SIZE", String(body.files.reduce((n, file) => n + file.size, 0))]]
890
+ : [];
891
+ break;
892
+ case "MULTIPART_PART_HEADERS":
893
+ entries = body?.partHeaders ?? [];
894
+ break;
895
+ case "XML":
896
+ return body?.xml
897
+ ? xmlValues(body.xml, selector.key ?? "/*").map(([k, v]) => [k, utf8Bytes(v)])
898
+ : [];
899
+ case "RESPONSE_BODY":
900
+ entries =
901
+ phase >= 4 &&
902
+ context.responseBodyAccess !== false &&
903
+ (request.responseBody !== undefined || request.responseBodyBytes !== undefined)
904
+ ? [
905
+ [
906
+ "RESPONSE_BODY",
907
+ request.responseBodyBytes
908
+ ? fromOctets(request.responseBodyBytes)
909
+ : utf8Bytes(request.responseBody),
910
+ ],
911
+ ]
912
+ : [];
913
+ break;
914
+ case "RESPONSE_STATUS":
915
+ entries =
916
+ phase >= 3 && request.responseStatus !== undefined
917
+ ? [["RESPONSE_STATUS", String(request.responseStatus)]]
918
+ : [];
919
+ break;
920
+ case "RESPONSE_HEADERS":
921
+ entries = phase >= 3 ? [...new Headers(request.responseHeaders ?? {}).entries()] : [];
922
+ break;
923
+ case "MATCHED_VARS":
924
+ entries = [...(context.matchedVars ?? [])];
925
+ break;
926
+ case "IP":
927
+ case "GLOBAL":
928
+ entries = [...(context.collections?.get(selector.name) ?? [])].map(([k, v]) => [
929
+ k,
930
+ String(v),
931
+ ]);
932
+ break;
933
+ case "QUERY_STRING":
934
+ entries = [["QUERY_STRING", query]];
935
+ break;
936
+ case "REQUEST_HEADERS":
937
+ context.headerEntries ??= [...headers.entries()];
938
+ entries = context.headerEntries;
939
+ break;
940
+ case "REQUEST_HEADERS_NAMES":
941
+ context.headerNames ??= [...headers.keys()].map((v) => [v, v]);
942
+ entries = context.headerNames;
943
+ break;
944
+ case "REQUEST_BODY":
945
+ entries =
946
+ phase >= 2 &&
947
+ context.requestBodyAccess !== false &&
948
+ ((processor !== "" && processor !== "MULTIPART") || context.forceBody)
949
+ ? [
950
+ [
951
+ "REQUEST_BODY",
952
+ request.bodyBytes
953
+ ? fromOctets(request.bodyBytes)
954
+ : utf8Bytes(request.bodyText ?? ""),
955
+ ],
956
+ ]
957
+ : [];
958
+ break;
959
+ case "ARGS":
960
+ entries = [...getArgs, ...postArgs];
961
+ break;
962
+ case "ARGS_GET":
963
+ entries = getArgs;
964
+ break;
965
+ case "ARGS_POST":
966
+ entries = postArgs;
967
+ break;
968
+ case "ARGS_GET_NAMES":
969
+ entries = getArgs.map(([k]) => [k, k]);
970
+ break;
971
+ case "ARGS_POST_NAMES":
972
+ entries = postArgs.map(([k]) => [k, k]);
973
+ break;
974
+ case "ARGS_COMBINED_SIZE":
975
+ entries = [
976
+ [
977
+ "ARGS_COMBINED_SIZE",
978
+ String([...getArgs, ...postArgs].reduce((n, [k, v]) => n + k.length + v.length, 0)),
979
+ ],
980
+ ];
981
+ break;
982
+ case "REQUEST_BODY_LENGTH":
983
+ entries =
984
+ phase >= 2
985
+ ? [
986
+ [
987
+ "REQUEST_BODY_LENGTH",
988
+ String(request.bodyBytes?.length ??
989
+ new TextEncoder().encode(request.bodyText ?? "").length),
990
+ ],
991
+ ]
992
+ : [];
993
+ break;
994
+ case "MATCHED_VAR":
995
+ entries = context.matchedValue === undefined ? [] : [["MATCHED_VAR", context.matchedValue]];
996
+ break;
997
+ case "MATCHED_VAR_NAME":
998
+ entries =
999
+ context.matchedName === undefined ? [] : [["MATCHED_VAR_NAME", context.matchedName]];
1000
+ break;
1001
+ case "ARGS_NAMES":
1002
+ entries = [...getArgs, ...postArgs].map(([k]) => [k, k]);
1003
+ break;
1004
+ case "REQUEST_COOKIES":
1005
+ case "REQUEST_COOKIES_NAMES":
1006
+ context.cookieEntries ??= (headers.get("cookie") || "")
1007
+ .split(/;\s*/)
1008
+ .filter(Boolean)
1009
+ .map((part) => {
1010
+ const at = part.indexOf("=");
1011
+ return [
1012
+ at < 0 ? part.trim() : part.slice(0, at).trim(),
1013
+ at < 0 ? "" : part.slice(at + 1),
1014
+ ];
1015
+ });
1016
+ entries =
1017
+ selector.name === "REQUEST_COOKIES"
1018
+ ? context.cookieEntries
1019
+ : context.cookieEntries.map(([k]) => [k, k]);
1020
+ break;
1021
+ }
1022
+ if (selector.key !== undefined)
1023
+ entries = entries.filter(([k]) => selectorKeyMatches(selector, k));
1024
+ if (!BYTE_COLLECTIONS.has(selector.name))
1025
+ entries = entries.map(([key, value]) => [key, utf8Bytes(String(value))]);
1026
+ return entries;
1027
+ }
1028
+ function selectorKeyMatches(selector, key) {
1029
+ if (selector.key === undefined)
1030
+ return true;
1031
+ if (selector.keyRegex)
1032
+ return selector.keyRegex.matcher(key).find();
1033
+ selector.foldedKey ??= asciiLower(utf8Bytes(selector.key));
1034
+ return asciiLower(key) === selector.foldedKey;
1035
+ }
1036
+ function getValues(selectors, request, tx, phase, context) {
1037
+ if (context.excludedTargets?.length)
1038
+ selectors = [...selectors, ...context.excludedTargets];
1039
+ const hasExclusions = selectors.some((item) => item.negative);
1040
+ const selected = [];
1041
+ for (const item of selectors) {
1042
+ if (item.negative)
1043
+ continue;
1044
+ let entries = values(item, request, tx, phase, context);
1045
+ if (hasExclusions)
1046
+ entries = entries.filter(([key]) => {
1047
+ const excluded = selectors.some((negative) => negative.negative && negative.name === item.name && selectorKeyMatches(negative, key));
1048
+ return !excluded;
1049
+ });
1050
+ if (item.count)
1051
+ selected.push([item.name, String(entries.length)]);
1052
+ else
1053
+ for (const [key, value] of entries)
1054
+ selected.push([key === item.name ? key : `${item.name}:${key}`, String(value)]);
1055
+ }
1056
+ return selected;
1057
+ }
1058
+ function integer(value) {
1059
+ const match = /^[\t\n\v\f\r ]*([+-]?\d+)/.exec(String(value));
1060
+ const number = match ? Number(match[1]) : 0;
1061
+ return Number.isInteger(number) && number >= -2147483648 && number <= 2147483647 ? number : 0;
1062
+ }
1063
+ function compares(value, condition, tx, context) {
1064
+ const { op, regex, phrases } = condition;
1065
+ const operand = condition.dynamic ? macro(condition.operand, tx, context) : condition.operand;
1066
+ switch (op) {
1067
+ case "streq":
1068
+ return value === operand;
1069
+ case "contains":
1070
+ return value.includes(operand);
1071
+ case "beginswith":
1072
+ return value.startsWith(operand);
1073
+ case "endswith":
1074
+ return value.endsWith(operand);
1075
+ case "within":
1076
+ return operand.includes(value);
1077
+ case "unconditionalmatch":
1078
+ return true;
1079
+ case "detectsqli":
1080
+ return condition.detectors.sqli(value).matched;
1081
+ case "detectxss":
1082
+ return condition.detectors.xss(value);
1083
+ case "validateurlencoding":
1084
+ return /%(?![\da-f]{2})/i.test(value);
1085
+ case "validatebyterange":
1086
+ return [...octets(value)].some((n) => !condition.ranges.some(([start, end]) => n >= start && n <= end));
1087
+ case "validateutf8encoding": {
1088
+ try {
1089
+ new TextDecoder("utf-8", { fatal: true }).decode(octets(value));
1090
+ return false;
1091
+ }
1092
+ catch {
1093
+ return true;
1094
+ }
1095
+ }
1096
+ case "ipmatchfromfile":
1097
+ case "ipmatch": {
1098
+ try {
1099
+ const address = ipaddr.parse(value);
1100
+ return condition.networks.some(([network, bits]) => network.kind() === address.kind() && address.match(network, bits));
1101
+ }
1102
+ catch {
1103
+ return false;
1104
+ }
1105
+ }
1106
+ case "rx":
1107
+ return regex.matcher(value).find();
1108
+ case "pm":
1109
+ case "pmfromfile": {
1110
+ const lower = asciiLower(value);
1111
+ return phrases.some((phrase) => lower.includes(phrase));
1112
+ }
1113
+ case "eq":
1114
+ return integer(value) === integer(operand);
1115
+ case "lt":
1116
+ return integer(value) < integer(operand);
1117
+ case "le":
1118
+ return integer(value) <= integer(operand);
1119
+ case "gt":
1120
+ return integer(value) > integer(operand);
1121
+ case "ge":
1122
+ return integer(value) >= integer(operand);
1123
+ }
1124
+ return false;
1125
+ }
1126
+ function match(step, request, tx, phase, context) {
1127
+ if (step.unconditional) {
1128
+ applyEffects(step.actions, tx, context);
1129
+ return true;
1130
+ }
1131
+ const source = getValues(step.selectors, request, tx, phase, context);
1132
+ context.matchedVars.length = 0;
1133
+ let matched = false;
1134
+ for (const [name, raw] of source) {
1135
+ const candidates = [];
1136
+ if (step.actions.multiMatch) {
1137
+ let value = raw;
1138
+ candidates.push(value);
1139
+ for (const name of step.actions.transformations) {
1140
+ const next = transformed(value, [name], context);
1141
+ if (next !== value)
1142
+ candidates.push(next);
1143
+ value = next;
1144
+ }
1145
+ }
1146
+ else
1147
+ candidates.push(transformed(raw, step.actions.transformations, context));
1148
+ for (const value of candidates) {
1149
+ let dynamicRegex = null;
1150
+ if (step.operator.op === "rx" && step.operator.dynamic)
1151
+ dynamicRegex = step.operator.dynamicRegexBackend.compile(macro(step.operator.operand, tx, context), { binary: true });
1152
+ const condition = dynamicRegex ? { ...step.operator, regex: dynamicRegex } : step.operator;
1153
+ try {
1154
+ let result = false;
1155
+ let matcher = null;
1156
+ let capture;
1157
+ if (step.actions.capture && step.operator.op === "rx") {
1158
+ matcher = condition.regex.matcher(value);
1159
+ result = matcher.find();
1160
+ }
1161
+ else if (step.operator.op === "detectsqli") {
1162
+ const detected = step.operator.detectors.sqli(value);
1163
+ result = detected.matched;
1164
+ capture = detected.fingerprint;
1165
+ }
1166
+ else if (["pm", "pmfromfile"].includes(step.operator.op)) {
1167
+ const lower = asciiLower(value);
1168
+ const phrase = step.operator.phrases.find((p) => lower.includes(p));
1169
+ result = phrase !== undefined;
1170
+ if (result) {
1171
+ const at = lower.indexOf(phrase);
1172
+ capture = value.slice(at, at + phrase.length);
1173
+ }
1174
+ }
1175
+ else
1176
+ result = compares(value, condition, tx, context);
1177
+ if (result && matcher) {
1178
+ for (let i = 0; i <= condition.regex.groupCount(); i++) {
1179
+ const group = matcher.group(i);
1180
+ if (group !== null)
1181
+ tx.set(String(i), group);
1182
+ }
1183
+ }
1184
+ if (result && step.actions.capture && capture !== undefined)
1185
+ tx.set("0", capture);
1186
+ if (step.operator.negated ? !result : result) {
1187
+ matched = true;
1188
+ context.matchedValue = value;
1189
+ context.matchedName = name;
1190
+ context.matchedVars.push([name, value]);
1191
+ applyEffects(step.actions, tx, context);
1192
+ }
1193
+ }
1194
+ finally {
1195
+ dynamicRegex?.destroy?.();
1196
+ }
1197
+ }
1198
+ }
1199
+ return matched;
1200
+ }
1201
+ function macroVariable(name) {
1202
+ const dot = name.indexOf(".");
1203
+ const collection = (dot < 0 ? name : name.slice(0, dot)).toUpperCase();
1204
+ if (!VARIABLES.has(collection) || (dot >= 0 && dot === name.length - 1))
1205
+ return undefined;
1206
+ return { name: collection, key: dot < 0 ? undefined : name.slice(dot + 1) };
1207
+ }
1208
+ function validateMacros(value) {
1209
+ if (!value.includes("%{"))
1210
+ return;
1211
+ for (const match of value.matchAll(/%\{([^}]+)\}/g)) {
1212
+ if (!/^(?:(?:tx|ip|global)\.[\w.-]+|rule\.(?:id|msg|severity)|matched_var|matched_var_name|remote_addr|request_line|request_headers\.[^}]+|args\.[^}]+)$/i.test(match[1]) &&
1213
+ !macroVariable(match[1]))
1214
+ throw new Error(`Unsupported macro ${match[1]}`);
1215
+ }
1216
+ if (value.replace(/%\{[^}]+\}/g, "").includes("%{"))
1217
+ throw new Error("Malformed macro");
1218
+ }
1219
+ function macro(value, tx, context = {}) {
1220
+ if (!value.includes("%{"))
1221
+ return value;
1222
+ return value.replace(/%\{([^}]+)\}/gi, (_, name) => {
1223
+ const key = asciiLower(name);
1224
+ if (key.startsWith("tx."))
1225
+ return String(tx.get(key.slice(3)) ?? "");
1226
+ if (/^(ip|global)\./.test(key)) {
1227
+ const at = key.indexOf(".");
1228
+ return String(context.collections?.get(key.slice(0, at).toUpperCase())?.get(key.slice(at + 1)) ?? "");
1229
+ }
1230
+ if (key.startsWith("rule."))
1231
+ return String(context.rule?.[key.slice(5)] ?? "");
1232
+ if (key === "matched_var")
1233
+ return context.matchedValue ?? "";
1234
+ if (key === "matched_var_name")
1235
+ return context.matchedName ?? "";
1236
+ if (key === "remote_addr") {
1237
+ if (!context.request.remoteAddr)
1238
+ throw new Error("REMOTE_ADDR macro requires a trusted client address");
1239
+ return context.request.remoteAddr;
1240
+ }
1241
+ if (key === "request_line")
1242
+ return (values({ name: "REQUEST_LINE" }, context.request, tx, context.phase, context)[0]?.[1] ?? "");
1243
+ if (key.startsWith("request_headers."))
1244
+ return new Headers(context.request.headers).get(name.slice(16)) ?? "";
1245
+ if (key.startsWith("args."))
1246
+ return (values({ name: "ARGS", key: name.slice(5) }, context.request, tx, context.phase, context)[0]?.[1] ?? "");
1247
+ const selector = macroVariable(name);
1248
+ if (selector)
1249
+ return values(selector, context.request, tx, context.phase, context)[0]?.[1] ?? "";
1250
+ throw new Error(`Unsupported macro ${name}`);
1251
+ });
1252
+ }
1253
+ function updateState(changes, tx, context) {
1254
+ for (const change of changes) {
1255
+ const store = change.collection === "TX" ? tx : context.collections?.get(change.collection);
1256
+ if (!store)
1257
+ throw new Error(`Collection ${change.collection} has not been initialized`);
1258
+ const name = asciiLower(macro(change.name, tx, context));
1259
+ if (name.length > 4096)
1260
+ throw new Error("TX variable name exceeds limit");
1261
+ if (!store.has(name) && store.size >= (change.collection === "TX" ? 4096 : 1024))
1262
+ throw new Error("SecLang collection variable limit exceeded");
1263
+ if (change.remove) {
1264
+ store.delete(name);
1265
+ const record = change.collection === "TX"
1266
+ ? context.txRecord
1267
+ : context.collectionRecords?.get(change.collection);
1268
+ record?.expires.delete(name);
1269
+ record?.decay.delete(name);
1270
+ continue;
1271
+ }
1272
+ const evaluated = macro(change.value, tx, context);
1273
+ const record = change.collection === "TX"
1274
+ ? context.txRecord
1275
+ : context.collectionRecords?.get(change.collection);
1276
+ if (record && !store.has(name))
1277
+ record.decay.set(name, context.program.now());
1278
+ const arithmetic = /^([+-])([\s\S]*)$/.exec(evaluated);
1279
+ if (arithmetic) {
1280
+ const current = integer(store.get(name) ?? 0);
1281
+ const delta = integer(arithmetic[2]) * (arithmetic[1] === "+" ? 1 : -1);
1282
+ if (!Number.isSafeInteger(current + delta) ||
1283
+ current + delta < -2147483648 ||
1284
+ current + delta > 2147483647)
1285
+ throw new Error("TX arithmetic overflow");
1286
+ store.set(name, current + delta);
1287
+ }
1288
+ else
1289
+ store.set(name, evaluated);
1290
+ }
1291
+ }
1292
+ function idMatches(id, range) {
1293
+ const [start, end = start] = range.split("-").map(Number);
1294
+ return Number(id) >= start && Number(id) <= end;
1295
+ }
1296
+ function initializeCollections(actions, program, tx, context) {
1297
+ for (const init of actions.initializers) {
1298
+ const key = `${init.name}:${macro(init.key, tx, context)}`;
1299
+ const record = program.collections.get(key);
1300
+ context.collectionRecords.set(init.name, record);
1301
+ context.collections.set(init.name, record.values);
1302
+ }
1303
+ }
1304
+ function applyEffects(actions, tx, context) {
1305
+ for (const effect of actions.effects) {
1306
+ if (effect.type === "initcol") {
1307
+ initializeCollections({ initializers: [effect] }, context.program, tx, context);
1308
+ continue;
1309
+ }
1310
+ if (effect.type === "setvar") {
1311
+ updateState([effect], tx, context);
1312
+ continue;
1313
+ }
1314
+ const record = effect.collection === "TX"
1315
+ ? context.txRecord
1316
+ : context.collectionRecords.get(effect.collection);
1317
+ if (!record)
1318
+ throw new Error(`Collection ${effect.collection} has not been initialized`);
1319
+ const name = asciiLower(macro(effect.name, tx, context));
1320
+ if (!record.values.has(name))
1321
+ continue;
1322
+ const value = macro(effect.value, tx, context), now = context.program.now();
1323
+ if (effect.type === "expirevar") {
1324
+ if (!/^\d+$/.test(value) || Number(value) > 31_536_000)
1325
+ throw new Error("Invalid expirevar interval");
1326
+ record.expires.set(name, now + Number(value) * 1000);
1327
+ context.program.collections.refresh(record);
1328
+ }
1329
+ else {
1330
+ const match = /^(\d+)\/(\d+)$/.exec(value);
1331
+ if (!match || Number(match[1]) < 1 || Number(match[2]) < 1 || Number(match[2]) > 31_536_000)
1332
+ throw new Error("Invalid deprecatevar rate");
1333
+ const interval = Number(match[2]) * 1000, prior = record.decay.get(name) ?? now, steps = Math.max(0, Math.floor((now - prior) / interval));
1334
+ const current = Number(record.values.get(name));
1335
+ if (!Number.isFinite(current))
1336
+ throw new Error("deprecatevar requires a numeric value");
1337
+ record.values.set(name, Math.max(0, current - steps * Number(match[1])));
1338
+ record.decay.set(name, prior + steps * interval);
1339
+ }
1340
+ }
1341
+ }
1342
+ function controls(actions, _program, _tx, context) {
1343
+ for (const control of actions.controls) {
1344
+ const { option, value } = control;
1345
+ if (option === "ruleremovebyid")
1346
+ context.removedIds.push(value);
1347
+ else if (option === "ruleremovebytag")
1348
+ context.removedTags.add(value);
1349
+ else if (option.startsWith("ruleremovetarget"))
1350
+ context.targetRemovals.push(control);
1351
+ else if (option === "requestbodyprocessor") {
1352
+ context.processor = value;
1353
+ context.body = undefined;
1354
+ }
1355
+ else if (option === "forcerequestbodyvariable")
1356
+ context.forceBody = value === "On";
1357
+ else if (option === "ruleengine")
1358
+ context.engine = value;
1359
+ else if (option === "auditengine")
1360
+ context.auditEngine = value;
1361
+ else if (option === "requestbodyaccess")
1362
+ context.requestBodyAccess = value === "On";
1363
+ else if (option === "responsebodyaccess")
1364
+ context.responseBodyAccess = value === "On";
1365
+ }
1366
+ }
1367
+ export function executeSecLang(program, request, { tx: inputTx = {}, phases = [1, 2], anomalyThreshold, shouldSkip, transaction, } = {}) {
1368
+ if (program.disposed)
1369
+ throw new Error("Cannot execute a disposed SecLang program");
1370
+ if (program.unsupported.length)
1371
+ throw new Error("Cannot run an incompletely compiled SecLang policy");
1372
+ if (!request || typeof request.url !== "string" || typeof request.method !== "string")
1373
+ throw new TypeError("SecLang VM requires a URL and method");
1374
+ if (phases.some((phase, i) => ![1, 2, 3, 4, 5].includes(phase) ||
1375
+ phase <= (i ? phases[i - 1] : (transaction?.phase ?? 0))))
1376
+ throw new Error("SecLang phases must execute once in ascending order");
1377
+ if (transaction?.request &&
1378
+ (transaction.request.url !== request.url || transaction.request.method !== request.method))
1379
+ throw new Error("SecLang transaction request changed");
1380
+ const tx = transaction?.tx ??
1381
+ new Map(Object.entries(inputTx).map(([k, v]) => [
1382
+ asciiLower(utf8Bytes(k)),
1383
+ typeof v === "string" ? utf8Bytes(v) : v,
1384
+ ]));
1385
+ const matched = [];
1386
+ const events = [];
1387
+ const context = transaction ?? {
1388
+ tx,
1389
+ matchedVars: [],
1390
+ uniqueId: crypto.randomUUID(),
1391
+ removedIds: [...program.settings.removedIds],
1392
+ removedTags: new Set(),
1393
+ targetRemovals: [],
1394
+ collections: new Map(),
1395
+ collectionRecords: new Map(),
1396
+ txRecord: collectionRecord(program.now(), tx),
1397
+ engine: program.settings.engine,
1398
+ requestBodyAccess: program.settings.requestBodyAccess,
1399
+ responseBodyAccess: program.settings.responseBodyAccess,
1400
+ };
1401
+ context.request = request;
1402
+ context.program = program;
1403
+ program.collections.refresh(context.txRecord);
1404
+ for (const record of context.collectionRecords.values())
1405
+ program.collections.refresh(record);
1406
+ let blocked = false;
1407
+ let status = 403;
1408
+ for (const phase of phases) {
1409
+ if (blocked && phase !== 5)
1410
+ continue;
1411
+ context.phase = phase;
1412
+ if (phase >= 2 &&
1413
+ context.engine !== "Off" &&
1414
+ context.requestBodyAccess !== false &&
1415
+ !context.body)
1416
+ values({ name: "REQBODY_ERROR" }, request, tx, phase, context);
1417
+ let skipUntil;
1418
+ let skip = 0;
1419
+ for (const rule of program.phaseInstructions.get(phase)) {
1420
+ if (rule.type === "marker") {
1421
+ if (skipUntil === rule.name)
1422
+ skipUntil = undefined;
1423
+ continue;
1424
+ }
1425
+ if (skipUntil) {
1426
+ if (rule.actions.phase === phase && rule.actions.id === skipUntil)
1427
+ skipUntil = undefined;
1428
+ continue;
1429
+ }
1430
+ if (rule.actions.phase !== phase)
1431
+ continue;
1432
+ if (skip > 0) {
1433
+ skip--;
1434
+ continue;
1435
+ }
1436
+ if (shouldSkip?.(rule.actions.id))
1437
+ continue;
1438
+ if (context.engine === "Off")
1439
+ break;
1440
+ if (context.removedIds.some((range) => idMatches(rule.actions.id, range)) ||
1441
+ rule.actions.tags.some((tag) => context.removedTags.has(tag)))
1442
+ continue;
1443
+ context.excludedTargets = context.targetRemovals
1444
+ .filter((control) => control.option.endsWith("byid")
1445
+ ? idMatches(rule.actions.id, control.filter)
1446
+ : rule.actions.tags.includes(control.filter))
1447
+ .flatMap((control) => control.selectors);
1448
+ context.rule = {
1449
+ id: rule.actions.id,
1450
+ msg: rule.actions.message,
1451
+ severity: rule.actions.severity,
1452
+ };
1453
+ let current = rule, allMatched = true;
1454
+ while (current) {
1455
+ const effectivePhase = current.actions.phase ?? rule.actions.phase;
1456
+ if (effectivePhase !== phase || !match(current, request, tx, phase, context)) {
1457
+ allMatched = false;
1458
+ break;
1459
+ }
1460
+ current = current.next;
1461
+ }
1462
+ if (!allMatched)
1463
+ continue;
1464
+ matched.push(rule.actions.id);
1465
+ const message = macro(utf8Bytes(rule.actions.message ?? ""), tx, context);
1466
+ const logdata = macro(rule.actions.logdata ?? "", tx, context);
1467
+ events.push({
1468
+ id: rule.actions.id,
1469
+ phase,
1470
+ log: rule.actions.log,
1471
+ audit: rule.actions.audit && context.auditEngine !== "Off",
1472
+ message: message.slice(0, 4096),
1473
+ logdata: logdata.slice(0, 4096),
1474
+ truncated: message.length > 4096 || logdata.length > 4096,
1475
+ severity: rule.actions.severity,
1476
+ tags: rule.actions.tags,
1477
+ });
1478
+ current = rule;
1479
+ while (current) {
1480
+ controls(current.actions, program, tx, context);
1481
+ if (current.actions.skipAfter)
1482
+ skipUntil = current.actions.skipAfter;
1483
+ if (current.actions.skip)
1484
+ skip = current.actions.skip;
1485
+ if (context.engine !== "DetectionOnly" &&
1486
+ ["deny", "block"].includes(current.actions.decision))
1487
+ blocked = true;
1488
+ if (current.actions.status)
1489
+ status = current.actions.status;
1490
+ current = current.next;
1491
+ }
1492
+ if (blocked && phase !== 5)
1493
+ break;
1494
+ }
1495
+ }
1496
+ if (context.auditEngine === "Off")
1497
+ for (const event of events)
1498
+ event.audit = false;
1499
+ const score = Number(tx.get(phases.some((phase) => phase >= 3)
1500
+ ? "blocking_outbound_anomaly_score"
1501
+ : "blocking_inbound_anomaly_score") ??
1502
+ tx.get("inbound_anomaly_score") ??
1503
+ tx.get("anomaly_score") ??
1504
+ 0);
1505
+ if (context.engine !== "DetectionOnly" &&
1506
+ anomalyThreshold !== undefined &&
1507
+ score >= anomalyThreshold)
1508
+ blocked = true;
1509
+ return {
1510
+ blocked,
1511
+ status: blocked ? status : 200,
1512
+ matched,
1513
+ events,
1514
+ score,
1515
+ tx: Object.fromEntries(tx),
1516
+ transaction: context,
1517
+ };
1518
+ }
1519
+ //# sourceMappingURL=index.js.map