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,155 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { verifyFullCrs } from "./verify-full-crs.mjs";
6
+
7
+ function option(name, fallback) {
8
+ const index = process.argv.indexOf(name);
9
+ if (index < 0) return fallback;
10
+ const value = process.argv[index + 1];
11
+ if (!value || value.startsWith("--")) throw new Error(`Missing ${name} path`);
12
+ return pathToFileURL(resolve(value));
13
+ }
14
+ const registryUrl = option(
15
+ "--registry",
16
+ new URL("../rules/seclang-upstreams.json", import.meta.url),
17
+ );
18
+ const registry = JSON.parse(await readFile(registryUrl, "utf8"));
19
+ if (registry.version !== 1 || !Array.isArray(registry.sources) || !registry.sources.length)
20
+ throw new Error("Invalid SecLang upstream registry");
21
+ const update = process.argv.includes("--latest");
22
+ const files = {},
23
+ upstreams = [];
24
+ let total = 0,
25
+ source = "";
26
+ const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
27
+ async function github(path, limit = 2 * 1024 * 1024) {
28
+ const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN;
29
+ const response = await fetch(`https://api.github.com/${path}`, {
30
+ redirect: "error",
31
+ signal: AbortSignal.timeout(30000),
32
+ headers: {
33
+ accept: "application/vnd.github+json",
34
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
35
+ },
36
+ });
37
+ if (!response.ok) throw new Error(`GitHub source request failed: HTTP ${response.status}`);
38
+ const chunks = [];
39
+ let length = 0;
40
+ for await (const chunk of response.body) {
41
+ length += chunk.length;
42
+ if (length > limit) throw new Error("Upstream response exceeds limit");
43
+ chunks.push(chunk);
44
+ }
45
+ return JSON.parse(Buffer.concat(chunks));
46
+ }
47
+ async function rawBlob(repository, commit, path) {
48
+ const response = await fetch(
49
+ `https://raw.githubusercontent.com/${repository}/${commit}/${path.split("/").map(encodeURIComponent).join("/")}`,
50
+ {
51
+ redirect: "error",
52
+ signal: AbortSignal.timeout(30000),
53
+ },
54
+ );
55
+ if (!response.ok) throw new Error(`GitHub source file request failed: HTTP ${response.status}`);
56
+ const chunks = [];
57
+ let size = 0;
58
+ for await (const chunk of response.body) {
59
+ size += chunk.length;
60
+ if (size > 2 * 1024 * 1024) throw new Error("Upstream file exceeds limit");
61
+ chunks.push(chunk);
62
+ }
63
+ return Buffer.concat(chunks);
64
+ }
65
+ const ids = new Set();
66
+ for (const spec of registry.sources) {
67
+ if (
68
+ !/^[a-z0-9-]{1,64}$/.test(spec.id) ||
69
+ ids.has(spec.id) ||
70
+ !/^[a-f0-9]{40}$/.test(spec.commit) ||
71
+ !/^[\w.-]+\/[\w.-]+$/.test(spec.repository) ||
72
+ ![spec.rulesDirectory, spec.setup, spec.license].every(
73
+ (path) =>
74
+ typeof path === "string" && !path.startsWith("/") && !path.split("/").includes(".."),
75
+ )
76
+ )
77
+ throw new Error("Invalid SecLang source");
78
+ ids.add(spec.id);
79
+ const ref = update
80
+ ? (await github(`repos/${spec.repository}/releases/latest`)).tag_name
81
+ : spec.ref;
82
+ if (typeof ref !== "string" || ref.length > 128) throw new Error("Invalid upstream ref");
83
+ const revision = update ? ref : spec.commit;
84
+ const commit = (await github(`repos/${spec.repository}/commits/${encodeURIComponent(revision)}`))
85
+ .sha;
86
+ if (!/^[a-f0-9]{40}$/.test(commit)) throw new Error("Invalid upstream commit");
87
+ if (!update && commit !== spec.commit) throw new Error("Upstream commit differs from its pin");
88
+ const tree = await github(
89
+ `repos/${spec.repository}/git/trees/${commit}?recursive=1`,
90
+ 8 * 1024 * 1024,
91
+ );
92
+ if (tree.truncated || !Array.isArray(tree.tree)) throw new Error("Incomplete upstream tree");
93
+ const entries = tree.tree.filter(
94
+ (item) =>
95
+ item.type === "blob" &&
96
+ ((item.path.startsWith(`${spec.rulesDirectory}/`) &&
97
+ /\.(conf|data|txt|ipset|list)$/.test(item.path)) ||
98
+ item.path === spec.setup ||
99
+ item.path === spec.license),
100
+ );
101
+ if (
102
+ !entries.some((item) => item.path === spec.setup) ||
103
+ !entries.some((item) => item.path === spec.license)
104
+ )
105
+ throw new Error("Missing upstream setup/license");
106
+ const manifest = [];
107
+ // Bound network concurrency while preserving deterministic upstream file order.
108
+ for (let offset = 0; offset < entries.length; offset += 8) {
109
+ const batch = await Promise.all(
110
+ entries.slice(offset, offset + 8).map(async (item) => {
111
+ if (process.env.GH_TOKEN || process.env.GITHUB_TOKEN) {
112
+ const blob = await github(
113
+ `repos/${spec.repository}/git/blobs/${item.sha}`,
114
+ 4 * 1024 * 1024,
115
+ );
116
+ if (blob.encoding !== "base64" || blob.sha !== item.sha)
117
+ throw new Error("Unexpected upstream blob");
118
+ return { item, bytes: Buffer.from(blob.content, "base64") };
119
+ }
120
+ return { item, bytes: await rawBlob(spec.repository, commit, item.path) };
121
+ }),
122
+ );
123
+ for (const { item, bytes } of batch) {
124
+ total += bytes.length;
125
+ if (total > 16 * 1024 * 1024 || bytes.length > 2 * 1024 * 1024)
126
+ throw new Error("SecLang source bundle exceeds limit");
127
+ const gitHash = createHash("sha1")
128
+ .update(`blob ${bytes.length}\0`)
129
+ .update(bytes)
130
+ .digest("hex");
131
+ if (gitHash !== item.sha) throw new Error("Upstream Git blob hash mismatch");
132
+ const path = `${spec.id}/${item.path === spec.setup ? "setup.conf" : item.path}`;
133
+ files[path] = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
134
+ manifest.push({ path, bytes: bytes.length, sha256: digest(bytes), gitBlob: item.sha });
135
+ }
136
+ }
137
+ source += `Include ${spec.id}/setup.conf\nInclude ${spec.id}/${spec.rulesDirectory}/*.conf\n`;
138
+ upstreams.push({ id: spec.id, repository: spec.repository, ref, commit, files: manifest });
139
+ spec.ref = ref;
140
+ spec.commit = commit;
141
+ }
142
+ const bundle = { version: 1, format: "seclang-source-bundle", source, files, upstreams };
143
+ const validation = await verifyFullCrs(bundle);
144
+ const destination = option("--output", new URL("../rules/seclang-bundle.json", import.meta.url));
145
+ await mkdir(new URL("./", destination), { recursive: true });
146
+ const temporary = new URL(`${destination.href}.tmp`);
147
+ await writeFile(temporary, `${JSON.stringify(bundle, null, 2)}\n`);
148
+ await rename(temporary, destination);
149
+ if (update) await writeFile(registryUrl, `${JSON.stringify(registry, null, 2)}\n`);
150
+ console.log(
151
+ JSON.stringify({
152
+ upstreams: upstreams.map(({ id, ref, commit }) => ({ id, ref, commit })),
153
+ validation,
154
+ }),
155
+ );
@@ -0,0 +1,118 @@
1
+ import assert from "node:assert/strict";
2
+ import { compileSecLang, executeSecLang } from "./.build/scr/seclang/index.js";
3
+ import { loadSecLangBackends } from "./.build/scr/seclang/node.js";
4
+ import { parseSecLangSyntax } from "./.build/scr/seclang/syntax.js";
5
+
6
+ /** Required full-corpus gate: any missing data or unsupported statement aborts compilation. */
7
+ export async function verifyFullCrs(bundle) {
8
+ const compiled = compileSecLang(bundle.source, {
9
+ strict: true,
10
+ files: bundle.files,
11
+ ...(await loadSecLangBackends()),
12
+ });
13
+ let steps = 0;
14
+ for (const root of compiled.instructions) {
15
+ if (root.type !== "rule") continue;
16
+ for (let node = root; node; node = node.next) steps++;
17
+ }
18
+ const expected = Object.entries(bundle.files)
19
+ .filter(([path]) => path.endsWith(".conf"))
20
+ .reduce(
21
+ (n, [, text]) =>
22
+ n +
23
+ parseSecLangSyntax(text).filter(
24
+ (stmt) => stmt.directive === "SecRule" || stmt.directive === "SecAction",
25
+ ).length,
26
+ 0,
27
+ );
28
+ assert.equal(steps, expected, "Every source rule/action must have an executable instruction");
29
+ const normal = [
30
+ "/?q=hello",
31
+ "/api?item=123",
32
+ "/health",
33
+ "/.well-known/security.txt",
34
+ `/?q=${encodeURIComponent("你好世界")}`,
35
+ ];
36
+ const attacks = [
37
+ ["SQL injection", "/?q=1%20UNION%20SELECT%20password%20FROM%20users"],
38
+ ["XSS", "/?q=%3Cscript%3Ealert(1)%3C/script%3E"],
39
+ ["LFI", "/?file=../../../../etc/passwd"],
40
+ ["RCE", "/?cmd=;cat%20/etc/passwd"],
41
+ ];
42
+ const request = (path) => ({
43
+ url: `https://example.test${path}`,
44
+ method: "GET",
45
+ protocol: "HTTP/1.1",
46
+ remoteAddr: "192.0.2.5",
47
+ headers: { host: "example.test", "user-agent": "Mozilla/5.0", accept: "text/html" },
48
+ });
49
+ const timings = [];
50
+ try {
51
+ for (const path of normal) {
52
+ const start = performance.now();
53
+ const result = executeSecLang(compiled, request(path));
54
+ timings.push(performance.now() - start);
55
+ assert.equal(result.blocked, false, `Normal request blocked: ${path}`);
56
+ const response = executeSecLang(
57
+ compiled,
58
+ {
59
+ ...request(path),
60
+ responseStatus: 200,
61
+ responseHeaders: { "content-type": "text/html" },
62
+ responseBody: "<html><p>Hello</p></html>",
63
+ },
64
+ { phases: [3, 4, 5], transaction: result.transaction },
65
+ );
66
+ assert.equal(response.blocked, false, `Normal response blocked: ${path}`);
67
+ }
68
+ for (const [name, path] of attacks) {
69
+ const result = executeSecLang(compiled, request(path));
70
+ assert.equal(result.blocked, true, `${name} bypassed full CRS`);
71
+ assert.ok(
72
+ result.matched.includes("949110") || result.matched.includes("949111"),
73
+ `${name} must reach actual CRS blocking evaluation`,
74
+ );
75
+ }
76
+ const incoming = executeSecLang(compiled, request("/"));
77
+ const leak = executeSecLang(
78
+ compiled,
79
+ {
80
+ ...request("/"),
81
+ responseStatus: 200,
82
+ responseHeaders: { "content-type": "text/html" },
83
+ responseBody: "<title>=[ 1n73ct10n privat shell ]=</title>",
84
+ },
85
+ { phases: [3, 4, 5], transaction: incoming.transaction },
86
+ );
87
+ assert.equal(leak.blocked, true, "Official PHP web-shell phrase file must execute");
88
+ assert.ok(
89
+ leak.matched.includes("955100"),
90
+ "Web-shell detection must be from official data, not a replacement regex",
91
+ );
92
+ assert.ok(
93
+ leak.matched.includes("959100"),
94
+ "Response must reach CRS outbound blocking evaluation",
95
+ );
96
+ for (let i = 0; i < 20; i++) {
97
+ const start = performance.now();
98
+ assert.equal(executeSecLang(compiled, request("/?q=normal")).blocked, false);
99
+ timings.push(performance.now() - start);
100
+ }
101
+ timings.sort((a, b) => a - b);
102
+ const p95 = timings[Math.floor(timings.length * 0.95)];
103
+ assert.ok(p95 < 500, "Full CRS normal-request p95 exceeds the 500ms regression ceiling");
104
+ return {
105
+ sourceRules: compiled.sourceRules,
106
+ executableSteps: steps,
107
+ rootRules: compiled.supportedRules,
108
+ normal: normal.length,
109
+ attacks: attacks.length,
110
+ responseAttack: 1,
111
+ p95Ms: Number(p95.toFixed(3)),
112
+ unsupported: 0,
113
+ convertedToIR: false,
114
+ };
115
+ } finally {
116
+ compiled.dispose();
117
+ }
118
+ }