@de-otio/repo-aegis-core 0.2.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/dist/age.d.ts +32 -0
- package/dist/age.d.ts.map +1 -0
- package/dist/age.js +98 -0
- package/dist/age.js.map +1 -0
- package/dist/audit-log.d.ts +50 -0
- package/dist/audit-log.d.ts.map +1 -0
- package/dist/audit-log.js +183 -0
- package/dist/audit-log.js.map +1 -0
- package/dist/audit-log.test.d.ts +2 -0
- package/dist/audit-log.test.d.ts.map +1 -0
- package/dist/audit-log.test.js +181 -0
- package/dist/audit-log.test.js.map +1 -0
- package/dist/deny-set.d.ts +43 -0
- package/dist/deny-set.d.ts.map +1 -0
- package/dist/deny-set.js +165 -0
- package/dist/deny-set.js.map +1 -0
- package/dist/deny-set.test.d.ts +2 -0
- package/dist/deny-set.test.d.ts.map +1 -0
- package/dist/deny-set.test.js +155 -0
- package/dist/deny-set.test.js.map +1 -0
- package/dist/exceptions.d.ts +96 -0
- package/dist/exceptions.d.ts.map +1 -0
- package/dist/exceptions.js +143 -0
- package/dist/exceptions.js.map +1 -0
- package/dist/exit-codes.d.ts +4 -0
- package/dist/exit-codes.d.ts.map +1 -0
- package/dist/exit-codes.js +6 -0
- package/dist/exit-codes.js.map +1 -0
- package/dist/first-touch.d.ts +57 -0
- package/dist/first-touch.d.ts.map +1 -0
- package/dist/first-touch.js +112 -0
- package/dist/first-touch.js.map +1 -0
- package/dist/import-graph.test.d.ts +2 -0
- package/dist/import-graph.test.d.ts.map +1 -0
- package/dist/import-graph.test.js +210 -0
- package/dist/import-graph.test.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +68 -0
- package/dist/index.js.map +1 -0
- package/dist/lock.d.ts +22 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/lock.js +86 -0
- package/dist/lock.js.map +1 -0
- package/dist/lock.test.d.ts +2 -0
- package/dist/lock.test.d.ts.map +1 -0
- package/dist/lock.test.js +125 -0
- package/dist/lock.test.js.map +1 -0
- package/dist/paths.d.ts +22 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +46 -0
- package/dist/paths.js.map +1 -0
- package/dist/paths.test.d.ts +2 -0
- package/dist/paths.test.d.ts.map +1 -0
- package/dist/paths.test.js +78 -0
- package/dist/paths.test.js.map +1 -0
- package/dist/redaction.d.ts +29 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/redaction.js +48 -0
- package/dist/redaction.js.map +1 -0
- package/dist/redaction.test.d.ts +2 -0
- package/dist/redaction.test.d.ts.map +1 -0
- package/dist/redaction.test.js +67 -0
- package/dist/redaction.test.js.map +1 -0
- package/dist/regex-safety.d.ts +87 -0
- package/dist/regex-safety.d.ts.map +1 -0
- package/dist/regex-safety.js +322 -0
- package/dist/regex-safety.js.map +1 -0
- package/dist/regex-safety.test.d.ts +2 -0
- package/dist/regex-safety.test.d.ts.map +1 -0
- package/dist/regex-safety.test.js +149 -0
- package/dist/regex-safety.test.js.map +1 -0
- package/dist/registry-mutate.d.ts +35 -0
- package/dist/registry-mutate.d.ts.map +1 -0
- package/dist/registry-mutate.js +149 -0
- package/dist/registry-mutate.js.map +1 -0
- package/dist/registry-mutate.test.d.ts +2 -0
- package/dist/registry-mutate.test.d.ts.map +1 -0
- package/dist/registry-mutate.test.js +96 -0
- package/dist/registry-mutate.test.js.map +1 -0
- package/dist/registry.d.ts +64 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +120 -0
- package/dist/registry.js.map +1 -0
- package/dist/registry.test.d.ts +2 -0
- package/dist/registry.test.d.ts.map +1 -0
- package/dist/registry.test.js +316 -0
- package/dist/registry.test.js.map +1 -0
- package/dist/remote-url.d.ts +18 -0
- package/dist/remote-url.d.ts.map +1 -0
- package/dist/remote-url.js +66 -0
- package/dist/remote-url.js.map +1 -0
- package/dist/remote-url.test.d.ts +2 -0
- package/dist/remote-url.test.d.ts.map +1 -0
- package/dist/remote-url.test.js +116 -0
- package/dist/remote-url.test.js.map +1 -0
- package/dist/render.d.ts +54 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +182 -0
- package/dist/render.js.map +1 -0
- package/dist/render.test.d.ts +2 -0
- package/dist/render.test.d.ts.map +1 -0
- package/dist/render.test.js +152 -0
- package/dist/render.test.js.map +1 -0
- package/dist/repo.d.ts +40 -0
- package/dist/repo.d.ts.map +1 -0
- package/dist/repo.js +214 -0
- package/dist/repo.js.map +1 -0
- package/dist/repo.test.d.ts +2 -0
- package/dist/repo.test.d.ts.map +1 -0
- package/dist/repo.test.js +234 -0
- package/dist/repo.test.js.map +1 -0
- package/dist/scan.d.ts +103 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +436 -0
- package/dist/scan.js.map +1 -0
- package/dist/scan.test.d.ts +2 -0
- package/dist/scan.test.d.ts.map +1 -0
- package/dist/scan.test.js +437 -0
- package/dist/scan.test.js.map +1 -0
- package/dist/schemas.d.ts +50 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +190 -0
- package/dist/schemas.js.map +1 -0
- package/dist/secret-markers.d.ts +34 -0
- package/dist/secret-markers.d.ts.map +1 -0
- package/dist/secret-markers.js +118 -0
- package/dist/secret-markers.js.map +1 -0
- package/dist/secret-markers.test.d.ts +2 -0
- package/dist/secret-markers.test.d.ts.map +1 -0
- package/dist/secret-markers.test.js +154 -0
- package/dist/secret-markers.test.js.map +1 -0
- package/dist/trust-boundary.d.ts +33 -0
- package/dist/trust-boundary.d.ts.map +1 -0
- package/dist/trust-boundary.js +77 -0
- package/dist/trust-boundary.js.map +1 -0
- package/dist/trust-boundary.test.d.ts +2 -0
- package/dist/trust-boundary.test.d.ts.map +1 -0
- package/dist/trust-boundary.test.js +170 -0
- package/dist/trust-boundary.test.js.map +1 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/working-tree.d.ts +38 -0
- package/dist/working-tree.d.ts.map +1 -0
- package/dist/working-tree.js +133 -0
- package/dist/working-tree.js.map +1 -0
- package/dist/working-tree.test.d.ts +2 -0
- package/dist/working-tree.test.d.ts.map +1 -0
- package/dist/working-tree.test.js +162 -0
- package/dist/working-tree.test.js.map +1 -0
- package/package.json +40 -0
- package/src/age.ts +113 -0
- package/src/audit-log.test.ts +222 -0
- package/src/audit-log.ts +215 -0
- package/src/deny-set.test.ts +208 -0
- package/src/deny-set.ts +231 -0
- package/src/exceptions.ts +134 -0
- package/src/exit-codes.ts +5 -0
- package/src/first-touch.ts +172 -0
- package/src/import-graph.test.ts +239 -0
- package/src/index.ts +191 -0
- package/src/lock.test.ts +151 -0
- package/src/lock.ts +88 -0
- package/src/paths.test.ts +94 -0
- package/src/paths.ts +55 -0
- package/src/redaction.test.ts +81 -0
- package/src/redaction.ts +49 -0
- package/src/regex-safety.test.ts +194 -0
- package/src/regex-safety.ts +349 -0
- package/src/registry-mutate.test.ts +134 -0
- package/src/registry-mutate.ts +185 -0
- package/src/registry.test.ts +460 -0
- package/src/registry.ts +178 -0
- package/src/remote-url.test.ts +121 -0
- package/src/remote-url.ts +78 -0
- package/src/render.test.ts +206 -0
- package/src/render.ts +215 -0
- package/src/repo.test.ts +275 -0
- package/src/repo.ts +245 -0
- package/src/scan.test.ts +580 -0
- package/src/scan.ts +531 -0
- package/src/schemas.ts +207 -0
- package/src/secret-markers.test.ts +183 -0
- package/src/secret-markers.ts +145 -0
- package/src/trust-boundary.test.ts +198 -0
- package/src/trust-boundary.ts +98 -0
- package/src/types.ts +55 -0
- package/src/working-tree.test.ts +193 -0
- package/src/working-tree.ts +130 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Richard Myers and contributors.
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
// Probe `re2` once at module load via `createRequire` so we keep
|
|
6
|
+
// validatePattern fully sync. `re2` is an optionalDependency: install
|
|
7
|
+
// failures (no native build toolchain) leave us on the in-process
|
|
8
|
+
// fallback, which still provides best-effort ReDoS detection.
|
|
9
|
+
const _require = createRequire(import.meta.url);
|
|
10
|
+
let re2Ctor = null;
|
|
11
|
+
let re2Probed = false;
|
|
12
|
+
function probeRe2() {
|
|
13
|
+
if (re2Probed)
|
|
14
|
+
return re2Ctor;
|
|
15
|
+
re2Probed = true;
|
|
16
|
+
try {
|
|
17
|
+
const mod = _require("re2");
|
|
18
|
+
re2Ctor =
|
|
19
|
+
typeof mod === "function"
|
|
20
|
+
? mod
|
|
21
|
+
: "default" in mod
|
|
22
|
+
? mod.default
|
|
23
|
+
: mod;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
re2Ctor = null;
|
|
27
|
+
}
|
|
28
|
+
return re2Ctor;
|
|
29
|
+
}
|
|
30
|
+
let forcedBackend = null;
|
|
31
|
+
/**
|
|
32
|
+
* Test-only override. Forces {@link getRegexBackend} and
|
|
33
|
+
* {@link validatePattern} to use a specific backend regardless of which
|
|
34
|
+
* backends are installed. Pass `null` to clear. Production callers must
|
|
35
|
+
* not use this.
|
|
36
|
+
*
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
export function setRegexBackendForTesting(backend) {
|
|
40
|
+
forcedBackend = backend;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Report which regex backend repo-aegis is using for *additional*
|
|
44
|
+
* pattern-safety validation:
|
|
45
|
+
*
|
|
46
|
+
* - `"re2"`: the optional `re2` dependency is installed. Patterns that
|
|
47
|
+
* compile cleanly in re2 are provably safe from catastrophic
|
|
48
|
+
* backtracking (re2's hybrid NFA/DFA evaluator is linear-time by
|
|
49
|
+
* construction). For patterns that re2 can't parse (lookahead /
|
|
50
|
+
* look-behind / backreferences are unsupported in re2), validation
|
|
51
|
+
* falls back to the `"in-process"` time-budget heuristic.
|
|
52
|
+
* - `"in-process"`: re2 is unavailable; the in-process timer fires
|
|
53
|
+
* after the test completes (best-effort, may exceed budget).
|
|
54
|
+
* {@link validatePatterns} `{ strict: true }` upgrades to
|
|
55
|
+
* `"subprocess"` for the duration of that call.
|
|
56
|
+
* - `"subprocess"`: only returned by {@link getRegexBackend} when set
|
|
57
|
+
* via {@link setRegexBackendForTesting}; otherwise an internal
|
|
58
|
+
* detail of {@link validatePatterns}.
|
|
59
|
+
*
|
|
60
|
+
* Note: re2 affects *validation*, not the scanner's regex engine. The
|
|
61
|
+
* scanner still uses Node's native RegExp because the marker patterns
|
|
62
|
+
* may legitimately use lookahead constructs that re2 doesn't support.
|
|
63
|
+
*/
|
|
64
|
+
export function getRegexBackend() {
|
|
65
|
+
if (forcedBackend !== null)
|
|
66
|
+
return forcedBackend;
|
|
67
|
+
return probeRe2() !== null ? "re2" : "in-process";
|
|
68
|
+
}
|
|
69
|
+
const MAX_PATTERN_LENGTH = 2048;
|
|
70
|
+
const MAX_COMBINED_BYTES = 128 * 1024;
|
|
71
|
+
const REDOS_STRESS_LENGTH = 1000;
|
|
72
|
+
const REDOS_TIMEOUT_MS = 100;
|
|
73
|
+
const STRICT_BATCH_TIMEOUT_MS = 5000;
|
|
74
|
+
/**
|
|
75
|
+
* Validate a single regex pattern for use as a marker.
|
|
76
|
+
*
|
|
77
|
+
* Checks:
|
|
78
|
+
* 1. Compiles as a JavaScript RegExp without throwing.
|
|
79
|
+
* 2. Length <= 2048 chars.
|
|
80
|
+
* 3. Backtracking-bound test against `'a'.repeat(1000)` completes within 100ms.
|
|
81
|
+
* Catastrophic-backtracking patterns (e.g., `(a+)+$`) hang here and are
|
|
82
|
+
* rejected as ReDoS-suspected.
|
|
83
|
+
*
|
|
84
|
+
* Run at `render` time; bad patterns must not reach the hot path of `check`.
|
|
85
|
+
*
|
|
86
|
+
* @internal Prefer {@link validatePatterns} (which can run strict,
|
|
87
|
+
* subprocess-backed validation that is preemptable on catastrophic
|
|
88
|
+
* backtracking). This single-pattern, in-process helper is exposed for
|
|
89
|
+
* intra-repo callers that already pre-validate adversary-trust boundaries
|
|
90
|
+
* but is not part of the supported public API.
|
|
91
|
+
*/
|
|
92
|
+
export function validatePattern(pattern) {
|
|
93
|
+
if (typeof pattern !== "string" || pattern.length === 0) {
|
|
94
|
+
return { ok: false, reason: "empty pattern" };
|
|
95
|
+
}
|
|
96
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
97
|
+
return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LENGTH} characters` };
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
new RegExp(pattern, "i");
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
return { ok: false, reason: `invalid regex: ${err.message}` };
|
|
104
|
+
}
|
|
105
|
+
// Backend-dependent ReDoS check.
|
|
106
|
+
const backend = getRegexBackend();
|
|
107
|
+
if (backend === "re2") {
|
|
108
|
+
const Re2 = probeRe2();
|
|
109
|
+
if (Re2 !== null) {
|
|
110
|
+
try {
|
|
111
|
+
new Re2(pattern, "i");
|
|
112
|
+
// re2 compile succeeded → linear-time evaluation guaranteed.
|
|
113
|
+
return { ok: true };
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// re2 rejected (typically: pattern uses lookahead/lookbehind/
|
|
117
|
+
// backreferences which re2 doesn't support). Fall through to
|
|
118
|
+
// the in-process timer — the pattern is still valid for the
|
|
119
|
+
// scanner's RegExp engine, just not provably safe under re2.
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Synchronous in-process timing check. Best-effort: see SECURITY
|
|
124
|
+
// WARNING on isInTimeBudget. Worker-based watchdog adds startup
|
|
125
|
+
// overhead disproportionate to per-pattern cost; for marker-list sizes
|
|
126
|
+
// we expect (tens to low hundreds of patterns) the in-process check
|
|
127
|
+
// is fine for trusted-by-policy operator input.
|
|
128
|
+
if (!isInTimeBudget(pattern, REDOS_STRESS_LENGTH, REDOS_TIMEOUT_MS)) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
reason: `pattern took >${REDOS_TIMEOUT_MS}ms on stress input ` +
|
|
132
|
+
`(possible catastrophic backtracking; consider re-anchoring)`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return { ok: true };
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Validate a list of patterns. Returns split valid/invalid.
|
|
139
|
+
*
|
|
140
|
+
* With `strict: true`, runs the backtracking-bound check in a subprocess
|
|
141
|
+
* that can be preemptively killed if any pattern hangs the regex engine.
|
|
142
|
+
* Recommended for `render` and other one-time-cost paths; not for the
|
|
143
|
+
* per-scan hot path.
|
|
144
|
+
*/
|
|
145
|
+
export function validatePatterns(patterns, opts = {}) {
|
|
146
|
+
if (opts.strict) {
|
|
147
|
+
return validatePatternsStrict(patterns);
|
|
148
|
+
}
|
|
149
|
+
const valid = [];
|
|
150
|
+
const invalid = [];
|
|
151
|
+
for (const p of patterns) {
|
|
152
|
+
const r = validatePattern(p);
|
|
153
|
+
if (r.ok)
|
|
154
|
+
valid.push(p);
|
|
155
|
+
else
|
|
156
|
+
invalid.push({ pattern: p, reason: r.reason ?? "unknown" });
|
|
157
|
+
}
|
|
158
|
+
return { valid, invalid };
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Subprocess-backed strict validation. Spawns a child node process that
|
|
162
|
+
* runs each pattern's stress test sequentially, streaming a one-line
|
|
163
|
+
* JSON result per pattern. If the parent kills the child by timeout,
|
|
164
|
+
* the partial output identifies which pattern was in flight.
|
|
165
|
+
*/
|
|
166
|
+
function validatePatternsStrict(patterns) {
|
|
167
|
+
if (patterns.length === 0)
|
|
168
|
+
return { valid: [], invalid: [] };
|
|
169
|
+
// First pass: catch syntax + length errors in-process so we don't
|
|
170
|
+
// pay process-spawn cost for them.
|
|
171
|
+
const valid = [];
|
|
172
|
+
const invalid = [];
|
|
173
|
+
const toCheckRedos = [];
|
|
174
|
+
for (const p of patterns) {
|
|
175
|
+
if (typeof p !== "string" || p.length === 0) {
|
|
176
|
+
invalid.push({ pattern: p, reason: "empty pattern" });
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (p.length > MAX_PATTERN_LENGTH) {
|
|
180
|
+
invalid.push({
|
|
181
|
+
pattern: p,
|
|
182
|
+
reason: `pattern exceeds ${MAX_PATTERN_LENGTH} characters`,
|
|
183
|
+
});
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
new RegExp(p, "i");
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
invalid.push({ pattern: p, reason: `invalid regex: ${err.message}` });
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
toCheckRedos.push(p);
|
|
194
|
+
}
|
|
195
|
+
if (toCheckRedos.length === 0) {
|
|
196
|
+
return { valid, invalid };
|
|
197
|
+
}
|
|
198
|
+
const script = `
|
|
199
|
+
const fs = require('fs');
|
|
200
|
+
const input = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
201
|
+
const { patterns, stressLength, perPatternBudgetMs } = input;
|
|
202
|
+
const stress = 'a'.repeat(stressLength);
|
|
203
|
+
for (let i = 0; i < patterns.length; i++) {
|
|
204
|
+
const p = patterns[i];
|
|
205
|
+
const start = Date.now();
|
|
206
|
+
let outcome;
|
|
207
|
+
try {
|
|
208
|
+
new RegExp(p, 'i').test(stress);
|
|
209
|
+
const elapsed = Date.now() - start;
|
|
210
|
+
if (elapsed > perPatternBudgetMs * 10) {
|
|
211
|
+
outcome = { i, ok: false, reason:
|
|
212
|
+
'pattern took >' + perPatternBudgetMs + 'ms on stress input ' +
|
|
213
|
+
'(possible catastrophic backtracking; consider re-anchoring)' };
|
|
214
|
+
} else {
|
|
215
|
+
outcome = { i, ok: true };
|
|
216
|
+
}
|
|
217
|
+
} catch (err) {
|
|
218
|
+
outcome = { i, ok: false, reason: 'invalid regex: ' + err.message };
|
|
219
|
+
}
|
|
220
|
+
process.stdout.write(JSON.stringify(outcome) + '\\n');
|
|
221
|
+
}
|
|
222
|
+
`;
|
|
223
|
+
const result = spawnSync(process.execPath, ["-e", script], {
|
|
224
|
+
input: JSON.stringify({
|
|
225
|
+
patterns: toCheckRedos,
|
|
226
|
+
stressLength: REDOS_STRESS_LENGTH,
|
|
227
|
+
perPatternBudgetMs: REDOS_TIMEOUT_MS,
|
|
228
|
+
}),
|
|
229
|
+
encoding: "utf8",
|
|
230
|
+
timeout: STRICT_BATCH_TIMEOUT_MS,
|
|
231
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
232
|
+
});
|
|
233
|
+
// Parse partial stdout (one JSON object per line).
|
|
234
|
+
const seenResults = new Map();
|
|
235
|
+
for (const line of (result.stdout ?? "").split("\n")) {
|
|
236
|
+
if (!line.trim())
|
|
237
|
+
continue;
|
|
238
|
+
try {
|
|
239
|
+
const obj = JSON.parse(line);
|
|
240
|
+
seenResults.set(obj.i, { ok: obj.ok, reason: obj.reason });
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
/* skip malformed line */
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const timedOut = result.signal === "SIGTERM" || result.signal === "SIGKILL";
|
|
247
|
+
for (let i = 0; i < toCheckRedos.length; i++) {
|
|
248
|
+
const p = toCheckRedos[i];
|
|
249
|
+
const r = seenResults.get(i);
|
|
250
|
+
if (r) {
|
|
251
|
+
if (r.ok)
|
|
252
|
+
valid.push(p);
|
|
253
|
+
else
|
|
254
|
+
invalid.push({ pattern: p, reason: r.reason ?? "unknown" });
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
// No result for this pattern: either the worker died on this
|
|
258
|
+
// pattern (likeliest culprit on timeout) or output was truncated.
|
|
259
|
+
const reason = timedOut
|
|
260
|
+
? "strict validation timed out on this pattern (likely catastrophic backtracking)"
|
|
261
|
+
: "strict validation produced no result";
|
|
262
|
+
invalid.push({ pattern: p, reason });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return { valid, invalid };
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Validate that a combined alternation regex is within the size cap.
|
|
269
|
+
* Used by render and the deny-set computation as a safety net.
|
|
270
|
+
*/
|
|
271
|
+
export function validateCombinedSize(combined) {
|
|
272
|
+
if (Buffer.byteLength(combined, "utf8") > MAX_COMBINED_BYTES) {
|
|
273
|
+
return {
|
|
274
|
+
ok: false,
|
|
275
|
+
reason: `combined regex exceeds ${MAX_COMBINED_BYTES} bytes`,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return { ok: true };
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* SECURITY WARNING — adversary input.
|
|
282
|
+
*
|
|
283
|
+
* `isInTimeBudget` (and therefore the non-strict {@link validatePattern}
|
|
284
|
+
* default that calls it) is **not** a preemptive ReDoS guard. Node's
|
|
285
|
+
* regex engine has no timeout; this function runs the pattern in-process
|
|
286
|
+
* against a stress input and measures wall-clock elapsed time *after* it
|
|
287
|
+
* returns. A genuinely catastrophic pattern can hang the event loop for
|
|
288
|
+
* seconds-to-minutes before the timer reading even runs, during which
|
|
289
|
+
* nothing else in the process makes progress.
|
|
290
|
+
*
|
|
291
|
+
* As a consequence, the non-strict {@link validatePattern} **must not**
|
|
292
|
+
* be called on adversary-controlled input. Use it only for marker
|
|
293
|
+
* patterns the operator has authored (registry / `engagements.yaml`),
|
|
294
|
+
* which are trusted-by-policy.
|
|
295
|
+
*
|
|
296
|
+
* For any path that takes pattern strings from outside that trust
|
|
297
|
+
* boundary — third-party config, network input, future MCP tool input —
|
|
298
|
+
* use the strict mode of {@link validatePatterns} (`{ strict: true }`),
|
|
299
|
+
* which spawns a subprocess that the parent can preemptively kill on
|
|
300
|
+
* timeout via `SIGTERM`/`SIGKILL`.
|
|
301
|
+
*/
|
|
302
|
+
function isInTimeBudget(pattern, stressLength, budgetMs) {
|
|
303
|
+
// Best-effort time-bounded check. Node has no preemptive regex timeout, so
|
|
304
|
+
// we rely on the regex engine being well-behaved enough that 'a'-fuzzing
|
|
305
|
+
// against a pathological pattern still returns within seconds (not hours).
|
|
306
|
+
// For genuinely catastrophic patterns this may exceed the budget by a
|
|
307
|
+
// small multiple, which is still survivable. The check exists to flag
|
|
308
|
+
// patterns that show signs of being problematic; it does not guarantee
|
|
309
|
+
// safety against an adversary who controls pattern input.
|
|
310
|
+
const re = new RegExp(pattern, "i");
|
|
311
|
+
const stress = "a".repeat(stressLength);
|
|
312
|
+
const start = Date.now();
|
|
313
|
+
try {
|
|
314
|
+
re.test(stress);
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
const elapsed = Date.now() - start;
|
|
320
|
+
return elapsed <= budgetMs * 10; // generous: 10x to account for noisy CI
|
|
321
|
+
}
|
|
322
|
+
//# sourceMappingURL=regex-safety.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"regex-safety.js","sourceRoot":"","sources":["../src/regex-safety.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"regex-safety.test.d.ts","sourceRoot":"","sources":["../src/regex-safety.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Richard Myers and contributors.
|
|
3
|
+
import { describe, it, after } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { validatePattern, validatePatterns, getRegexBackend, setRegexBackendForTesting, } from "./regex-safety.js";
|
|
6
|
+
describe("validatePattern", () => {
|
|
7
|
+
it("accepts ordinary patterns", () => {
|
|
8
|
+
assert.equal(validatePattern("acme-corp").ok, true);
|
|
9
|
+
assert.equal(validatePattern("\\d{12}").ok, true);
|
|
10
|
+
assert.equal(validatePattern("[a-z]+@example\\.com").ok, true);
|
|
11
|
+
});
|
|
12
|
+
it("rejects empty patterns", () => {
|
|
13
|
+
const r = validatePattern("");
|
|
14
|
+
assert.equal(r.ok, false);
|
|
15
|
+
assert.match(r.reason, /empty/);
|
|
16
|
+
});
|
|
17
|
+
it("rejects non-string input", () => {
|
|
18
|
+
const r = validatePattern(undefined);
|
|
19
|
+
assert.equal(r.ok, false);
|
|
20
|
+
});
|
|
21
|
+
it("rejects syntactically invalid regex", () => {
|
|
22
|
+
const r = validatePattern("(unclosed");
|
|
23
|
+
assert.equal(r.ok, false);
|
|
24
|
+
assert.match(r.reason, /invalid regex/);
|
|
25
|
+
});
|
|
26
|
+
it("rejects patterns over the length cap", () => {
|
|
27
|
+
const r = validatePattern("a".repeat(2049));
|
|
28
|
+
assert.equal(r.ok, false);
|
|
29
|
+
assert.match(r.reason, /exceeds|length/i);
|
|
30
|
+
});
|
|
31
|
+
it("accepts patterns just under the length cap", () => {
|
|
32
|
+
const r = validatePattern("a".repeat(2000));
|
|
33
|
+
assert.equal(r.ok, true);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe("validatePatterns", () => {
|
|
37
|
+
it("splits valid and invalid patterns", () => {
|
|
38
|
+
const r = validatePatterns(["acme-corp", "(unclosed", "\\d+"]);
|
|
39
|
+
assert.equal(r.valid.length, 2);
|
|
40
|
+
assert.equal(r.invalid.length, 1);
|
|
41
|
+
assert.equal(r.invalid[0].pattern, "(unclosed");
|
|
42
|
+
});
|
|
43
|
+
it("returns empty when all patterns are valid", () => {
|
|
44
|
+
const r = validatePatterns(["a", "b", "c"]);
|
|
45
|
+
assert.equal(r.valid.length, 3);
|
|
46
|
+
assert.equal(r.invalid.length, 0);
|
|
47
|
+
});
|
|
48
|
+
it("returns empty when no patterns provided", () => {
|
|
49
|
+
const r = validatePatterns([]);
|
|
50
|
+
assert.equal(r.valid.length, 0);
|
|
51
|
+
assert.equal(r.invalid.length, 0);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe("validatePatterns({ strict: true })", () => {
|
|
55
|
+
it("accepts ordinary patterns", () => {
|
|
56
|
+
const r = validatePatterns(["acme-corp", "\\d{12}"], { strict: true });
|
|
57
|
+
assert.equal(r.valid.length, 2);
|
|
58
|
+
assert.equal(r.invalid.length, 0);
|
|
59
|
+
});
|
|
60
|
+
it("rejects syntactically invalid regex without spawning", () => {
|
|
61
|
+
const r = validatePatterns(["(unclosed"], { strict: true });
|
|
62
|
+
assert.equal(r.invalid.length, 1);
|
|
63
|
+
assert.match(r.invalid[0].reason, /invalid regex/);
|
|
64
|
+
});
|
|
65
|
+
it("rejects patterns over the length cap without spawning", () => {
|
|
66
|
+
const r = validatePatterns(["a".repeat(3000)], { strict: true });
|
|
67
|
+
assert.equal(r.invalid.length, 1);
|
|
68
|
+
assert.match(r.invalid[0].reason, /exceeds/);
|
|
69
|
+
});
|
|
70
|
+
it("flags catastrophic-backtracking patterns via subprocess", () => {
|
|
71
|
+
// Classic ReDoS shape: nested unbounded quantifier with a literal
|
|
72
|
+
// that the all-'a' stress input cannot satisfy, forcing the regex
|
|
73
|
+
// engine to try every possible split.
|
|
74
|
+
const r = validatePatterns(["^(a+)+b$"], { strict: true });
|
|
75
|
+
assert.equal(r.invalid.length, 1, `expected the pattern to be rejected; got valid=${JSON.stringify(r.valid)}`);
|
|
76
|
+
assert.match(r.invalid[0].reason, /catastrophic|timed out|>/i);
|
|
77
|
+
});
|
|
78
|
+
it("returns empty for empty input without spawning", () => {
|
|
79
|
+
const r = validatePatterns([], { strict: true });
|
|
80
|
+
assert.equal(r.valid.length, 0);
|
|
81
|
+
assert.equal(r.invalid.length, 0);
|
|
82
|
+
});
|
|
83
|
+
it("preserves order across mixed valid/invalid input", () => {
|
|
84
|
+
const r = validatePatterns(["acme-corp", "(bad", "\\d+", ""], { strict: true });
|
|
85
|
+
assert.deepEqual(r.valid, ["acme-corp", "\\d+"]);
|
|
86
|
+
assert.equal(r.invalid.length, 2);
|
|
87
|
+
});
|
|
88
|
+
it("reports trailing patterns when subprocess is killed mid-batch", () => {
|
|
89
|
+
// Strict-batch truncation contract: if the worker subprocess hangs on
|
|
90
|
+
// a catastrophic pattern and gets SIGTERMed, we must still report the
|
|
91
|
+
// patterns it never got to. The first pattern is fine and should be
|
|
92
|
+
// valid, the second is catastrophic-backtracking and should be in
|
|
93
|
+
// invalid (with a "timed out" or "catastrophic" reason), and the
|
|
94
|
+
// third — which the subprocess likely never reached — must also be
|
|
95
|
+
// reported (most likely as "produced no result" if truncation
|
|
96
|
+
// occurred mid-batch).
|
|
97
|
+
const r = validatePatterns(["acme-corp", "^(a+)+b$", "\\d{12}"], { strict: true });
|
|
98
|
+
// The first pattern is well-formed; it should always come back valid.
|
|
99
|
+
assert.ok(r.valid.includes("acme-corp"), `expected first pattern to validate; got valid=${JSON.stringify(r.valid)}`);
|
|
100
|
+
// The catastrophic pattern must end up in the invalid bucket with a
|
|
101
|
+
// reason that names the failure mode.
|
|
102
|
+
const cata = r.invalid.find(x => x.pattern === "^(a+)+b$");
|
|
103
|
+
assert.ok(cata, `expected ^(a+)+b$ in invalid; got invalid=${JSON.stringify(r.invalid)}`);
|
|
104
|
+
assert.match(cata.reason, /timed out|catastrophic|>/i);
|
|
105
|
+
// The third pattern is well-formed *but* may have been trampled by
|
|
106
|
+
// the kill-on-timeout. It must show up in *one* of the result sets:
|
|
107
|
+
// valid (if the worker got that far before timeout), or invalid with
|
|
108
|
+
// a "no result" / "timed out" reason (if truncation hit it).
|
|
109
|
+
const reported = r.valid.includes("\\d{12}") ||
|
|
110
|
+
r.invalid.some(x => x.pattern === "\\d{12}");
|
|
111
|
+
assert.ok(reported, `third pattern must be reported in valid or invalid; ` +
|
|
112
|
+
`valid=${JSON.stringify(r.valid)} invalid=${JSON.stringify(r.invalid)}`);
|
|
113
|
+
// Total accounting: every input pattern must show up exactly once
|
|
114
|
+
// across the two buckets.
|
|
115
|
+
assert.equal(r.valid.length + r.invalid.length, 3);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe("getRegexBackend", () => {
|
|
119
|
+
after(() => setRegexBackendForTesting(null));
|
|
120
|
+
it("returns 're2' or 'in-process' depending on optional dep availability", () => {
|
|
121
|
+
setRegexBackendForTesting(null);
|
|
122
|
+
const backend = getRegexBackend();
|
|
123
|
+
assert.ok(backend === "re2" || backend === "in-process", `unexpected backend: ${backend}`);
|
|
124
|
+
});
|
|
125
|
+
it("respects setRegexBackendForTesting override", () => {
|
|
126
|
+
setRegexBackendForTesting("in-process");
|
|
127
|
+
assert.equal(getRegexBackend(), "in-process");
|
|
128
|
+
setRegexBackendForTesting("re2");
|
|
129
|
+
assert.equal(getRegexBackend(), "re2");
|
|
130
|
+
setRegexBackendForTesting(null);
|
|
131
|
+
});
|
|
132
|
+
it("validatePattern accepts ordinary patterns under both backends", () => {
|
|
133
|
+
setRegexBackendForTesting("in-process");
|
|
134
|
+
assert.equal(validatePattern("acme-corp").ok, true);
|
|
135
|
+
setRegexBackendForTesting("re2");
|
|
136
|
+
assert.equal(validatePattern("acme-corp").ok, true);
|
|
137
|
+
setRegexBackendForTesting(null);
|
|
138
|
+
});
|
|
139
|
+
it("validatePattern falls back to time-budget when re2 rejects (e.g. lookahead)", () => {
|
|
140
|
+
// Lookahead is a re2-incompatible feature. Whether re2 is installed
|
|
141
|
+
// or not, validatePattern must still accept this pattern because
|
|
142
|
+
// the scanner uses native RegExp, which supports lookahead.
|
|
143
|
+
setRegexBackendForTesting("re2");
|
|
144
|
+
const r = validatePattern("foo(?=bar)");
|
|
145
|
+
assert.equal(r.ok, true, `expected lookahead pattern to validate; reason=${r.reason}`);
|
|
146
|
+
setRegexBackendForTesting(null);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
//# sourceMappingURL=regex-safety.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"regex-safety.test.js","sourceRoot":"","sources":["../src/regex-safety.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface AddMarkerPatternOptions {
|
|
2
|
+
/** Override the registry path (defaults to ~/.config/repo-aegis/engagements.yaml). */
|
|
3
|
+
registryPath?: string;
|
|
4
|
+
/**
|
|
5
|
+
* When true, audit-log writes record the source the pattern came from
|
|
6
|
+
* (e.g. `"suggest-markers"`). Caller-provided so the caller's verb
|
|
7
|
+
* shows up in the trail. Default: `"manual"`.
|
|
8
|
+
*/
|
|
9
|
+
source?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface AddMarkerPatternResult {
|
|
12
|
+
added: string[];
|
|
13
|
+
skipped: string[];
|
|
14
|
+
rendered: {
|
|
15
|
+
written: number;
|
|
16
|
+
removed: number;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Append one or more validated regex patterns to an engagement's
|
|
21
|
+
* markers. Held under a single registry lock for the entire
|
|
22
|
+
* load-modify-write-render cycle (see [SEC M-3]). Idempotent: patterns
|
|
23
|
+
* already present are reported in `skipped`, not re-added.
|
|
24
|
+
*
|
|
25
|
+
* Throws:
|
|
26
|
+
* - `EngagementNotFoundError` if no engagement with that id exists.
|
|
27
|
+
* - `PatternValidationError` if any pattern fails `validatePattern`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function addMarkerPatterns(engagementId: string, patterns: string[], opts?: AddMarkerPatternOptions): AddMarkerPatternResult;
|
|
30
|
+
/**
|
|
31
|
+
* Convenience wrapper: append a single pattern. Same semantics as
|
|
32
|
+
* `addMarkerPatterns([pattern])`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function addMarkerPattern(engagementId: string, pattern: string, opts?: AddMarkerPatternOptions): AddMarkerPatternResult;
|
|
35
|
+
//# sourceMappingURL=registry-mutate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry-mutate.d.ts","sourceRoot":"","sources":["../src/registry-mutate.ts"],"names":[],"mappings":"AAyBA,MAAM,WAAW,uBAAuB;IACtC,sFAAsF;IACtF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAChD;AA0BD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAAE,EAClB,IAAI,GAAE,uBAA4B,GACjC,sBAAsB,CA4FxB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,uBAA4B,GACjC,sBAAsB,CAExB"}
|