@resq-systems/security 1.0.5 → 2.1.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/README.md +236 -33
- package/lib/controls/address.d.mts +142 -0
- package/lib/controls/address.d.mts.map +1 -0
- package/lib/controls/address.mjs +533 -0
- package/lib/controls/address.mjs.map +1 -0
- package/lib/controls/csrf.d.mts +91 -0
- package/lib/controls/csrf.d.mts.map +1 -0
- package/lib/controls/csrf.mjs +200 -0
- package/lib/controls/csrf.mjs.map +1 -0
- package/lib/controls/index.d.mts +8 -0
- package/lib/controls/index.mjs +8 -0
- package/lib/controls/origin.d.mts +95 -0
- package/lib/controls/origin.d.mts.map +1 -0
- package/lib/controls/origin.mjs +156 -0
- package/lib/controls/origin.mjs.map +1 -0
- package/lib/controls/payload.d.mts +84 -0
- package/lib/controls/payload.d.mts.map +1 -0
- package/lib/controls/payload.mjs +147 -0
- package/lib/controls/payload.mjs.map +1 -0
- package/lib/controls/query.d.mts +169 -0
- package/lib/controls/query.d.mts.map +1 -0
- package/lib/controls/query.mjs +386 -0
- package/lib/controls/query.mjs.map +1 -0
- package/lib/controls/redirect.d.mts +92 -0
- package/lib/controls/redirect.d.mts.map +1 -0
- package/lib/controls/redirect.mjs +110 -0
- package/lib/controls/redirect.mjs.map +1 -0
- package/lib/controls/upload.d.mts +108 -0
- package/lib/controls/upload.d.mts.map +1 -0
- package/lib/controls/upload.mjs +374 -0
- package/lib/controls/upload.mjs.map +1 -0
- package/lib/crypto.d.mts +18 -5
- package/lib/crypto.d.mts.map +1 -1
- package/lib/crypto.mjs +35 -24
- package/lib/crypto.mjs.map +1 -1
- package/lib/hash.d.mts +51 -6
- package/lib/hash.d.mts.map +1 -1
- package/lib/hash.mjs +51 -6
- package/lib/hash.mjs.map +1 -1
- package/lib/index.d.mts +17 -2
- package/lib/index.mjs +19 -2
- package/lib/paths.d.mts +92 -0
- package/lib/paths.d.mts.map +1 -0
- package/lib/paths.mjs +140 -0
- package/lib/paths.mjs.map +1 -0
- package/lib/sanitize.d.mts +137 -35
- package/lib/sanitize.d.mts.map +1 -1
- package/lib/sanitize.mjs +170 -46
- package/lib/sanitize.mjs.map +1 -1
- package/lib/threats/capec.generated.d.mts +59 -0
- package/lib/threats/capec.generated.d.mts.map +1 -0
- package/lib/threats/capec.generated.mjs +644 -0
- package/lib/threats/capec.generated.mjs.map +1 -0
- package/lib/threats/engine.d.mts +94 -0
- package/lib/threats/engine.d.mts.map +1 -0
- package/lib/threats/engine.mjs +167 -0
- package/lib/threats/engine.mjs.map +1 -0
- package/lib/threats/index.d.mts +11 -0
- package/lib/threats/index.mjs +11 -0
- package/lib/threats/rules/datastore.d.mts +13 -0
- package/lib/threats/rules/datastore.d.mts.map +1 -0
- package/lib/threats/rules/datastore.mjs +366 -0
- package/lib/threats/rules/datastore.mjs.map +1 -0
- package/lib/threats/rules/index.d.mts +54 -0
- package/lib/threats/rules/index.d.mts.map +1 -0
- package/lib/threats/rules/index.mjs +121 -0
- package/lib/threats/rules/index.mjs.map +1 -0
- package/lib/threats/rules/markup.d.mts +28 -0
- package/lib/threats/rules/markup.d.mts.map +1 -0
- package/lib/threats/rules/markup.mjs +373 -0
- package/lib/threats/rules/markup.mjs.map +1 -0
- package/lib/threats/rules/protocol.d.mts +49 -0
- package/lib/threats/rules/protocol.d.mts.map +1 -0
- package/lib/threats/rules/protocol.mjs +175 -0
- package/lib/threats/rules/protocol.mjs.map +1 -0
- package/lib/threats/rules/system.d.mts +19 -0
- package/lib/threats/rules/system.d.mts.map +1 -0
- package/lib/threats/rules/system.mjs +455 -0
- package/lib/threats/rules/system.mjs.map +1 -0
- package/lib/threats/rules/web.d.mts +26 -0
- package/lib/threats/rules/web.d.mts.map +1 -0
- package/lib/threats/rules/web.mjs +412 -0
- package/lib/threats/rules/web.mjs.map +1 -0
- package/lib/threats/scoring.d.mts +59 -0
- package/lib/threats/scoring.d.mts.map +1 -0
- package/lib/threats/scoring.mjs +111 -0
- package/lib/threats/scoring.mjs.map +1 -0
- package/lib/threats/types.d.mts +245 -0
- package/lib/threats/types.d.mts.map +1 -0
- package/lib/threats/types.mjs +52 -0
- package/lib/threats/types.mjs.map +1 -0
- package/lib/threats/variants.d.mts +57 -0
- package/lib/threats/variants.d.mts.map +1 -0
- package/lib/threats/variants.mjs +144 -0
- package/lib/threats/variants.mjs.map +1 -0
- package/lib/unicode/confusables.d.mts +82 -0
- package/lib/unicode/confusables.d.mts.map +1 -0
- package/lib/unicode/confusables.mjs +954 -0
- package/lib/unicode/confusables.mjs.map +1 -0
- package/lib/unicode/index.d.mts +126 -0
- package/lib/unicode/index.d.mts.map +1 -0
- package/lib/unicode/index.mjs +288 -0
- package/lib/unicode/index.mjs.map +1 -0
- package/lib/validators.d.mts +341 -164
- package/lib/validators.d.mts.map +1 -1
- package/lib/validators.mjs +519 -338
- package/lib/validators.mjs.map +1 -1
- package/package.json +35 -8
package/lib/sanitize.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { Exit, Option, Schema } from "effect";
|
|
|
2
2
|
import DOMPurify from "dompurify";
|
|
3
3
|
//#region src/sanitize.ts
|
|
4
4
|
/**
|
|
5
|
-
* Schema
|
|
5
|
+
* Schema constraining a URL protocol to the recognized safe set.
|
|
6
6
|
* @compliance NIST 800-53 SI-10 (Information Input Validation)
|
|
7
7
|
*/
|
|
8
8
|
const UrlProtocolSchema = Schema.Literals([
|
|
@@ -13,7 +13,7 @@ const UrlProtocolSchema = Schema.Literals([
|
|
|
13
13
|
"ftp:"
|
|
14
14
|
]);
|
|
15
15
|
/**
|
|
16
|
-
* Schema for
|
|
16
|
+
* Schema for the per-category toggles that drive {@link redactPII}.
|
|
17
17
|
* @compliance NIST 800-53 AU-3 (Content of Audit Records)
|
|
18
18
|
*/
|
|
19
19
|
const PIIRedactionOptionsSchema = Schema.Struct({
|
|
@@ -25,7 +25,7 @@ const PIIRedactionOptionsSchema = Schema.Struct({
|
|
|
25
25
|
redactDates: Schema.optional(Schema.Boolean)
|
|
26
26
|
});
|
|
27
27
|
/**
|
|
28
|
-
* Schema for
|
|
28
|
+
* Schema for the options controlling {@link validateUserInputEffect}.
|
|
29
29
|
* @compliance NIST 800-53 SI-10 (Information Input Validation)
|
|
30
30
|
*/
|
|
31
31
|
const UserInputOptionsSchema = Schema.Struct({
|
|
@@ -35,12 +35,29 @@ const UserInputOptionsSchema = Schema.Struct({
|
|
|
35
35
|
trimWhitespace: Schema.optional(Schema.Boolean)
|
|
36
36
|
});
|
|
37
37
|
/**
|
|
38
|
-
* Schema for safe URL
|
|
38
|
+
* Schema for a safe URL — validates URL format and restricts to safe protocols.
|
|
39
39
|
* @compliance NIST 800-53 SI-10 (Information Input Validation)
|
|
40
40
|
*/
|
|
41
|
+
/**
|
|
42
|
+
* Whether a reference opens a URL *authority* component — i.e. can name a different
|
|
43
|
+
* host once resolved against a base.
|
|
44
|
+
*
|
|
45
|
+
* `//evil.example` resolved against `https://trusted.example/page` yields host
|
|
46
|
+
* `evil.example`, and so do `///evil.example` and `/\evil.example`: the WHATWG URL
|
|
47
|
+
* parser treats a backslash as a slash in the relative-slash state.
|
|
48
|
+
*
|
|
49
|
+
* This must be checked **before** the root-relative fast path. A `startsWith("//")`
|
|
50
|
+
* guard alone is insufficient twice over: it misses the backslash forms entirely, and
|
|
51
|
+
* `//evil.example` that falls through to `new URL()` throws without a base and lands in
|
|
52
|
+
* the catch branch, whose `[a-zA-Z0-9/_.-]` character class happily accepts it.
|
|
53
|
+
*
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
const opensAuthority = (url) => /^[/\\]{2}/.test(url.trim());
|
|
41
57
|
const SafeUrlSchema = Schema.String.check(Schema.makeFilter((url) => {
|
|
42
58
|
if (!url || url.trim() === "") return false;
|
|
43
|
-
if (
|
|
59
|
+
if (opensAuthority(url)) return false;
|
|
60
|
+
if (url.startsWith("/")) return true;
|
|
44
61
|
try {
|
|
45
62
|
const parsed = new URL(url);
|
|
46
63
|
return [
|
|
@@ -53,7 +70,8 @@ const SafeUrlSchema = Schema.String.check(Schema.makeFilter((url) => {
|
|
|
53
70
|
}
|
|
54
71
|
}, { message: "Invalid or unsafe URL" }));
|
|
55
72
|
/**
|
|
56
|
-
* Schema for sanitized HTML-safe string
|
|
73
|
+
* Schema for a sanitized HTML-safe string — validates the value is a string; the
|
|
74
|
+
* actual escaping is applied at runtime by the sanitization helpers.
|
|
57
75
|
* @compliance NIST 800-53 SI-10 (Information Input Validation)
|
|
58
76
|
*/
|
|
59
77
|
const SanitizedStringSchema = Schema.String;
|
|
@@ -67,22 +85,22 @@ const SanitizedStringSchema = Schema.String;
|
|
|
67
85
|
*/
|
|
68
86
|
const EmailSchema = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.(?:[A-Za-z]{2,}|xn--[A-Za-z0-9-]+)$/));
|
|
69
87
|
/**
|
|
70
|
-
* Schema for phone number validation (US format)
|
|
88
|
+
* Schema for phone number validation (US format).
|
|
71
89
|
* @compliance NIST 800-53 SI-10 (Information Input Validation)
|
|
72
90
|
*/
|
|
73
91
|
const PhoneNumberSchema = Schema.String.check(Schema.isPattern(/^(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$/));
|
|
74
92
|
/**
|
|
75
|
-
* Schema for SSN validation (US format)
|
|
93
|
+
* Schema for SSN validation (US format).
|
|
76
94
|
* @compliance NIST 800-53 SI-10 (Information Input Validation)
|
|
77
95
|
*/
|
|
78
96
|
const SSNSchema = Schema.String.check(Schema.isPattern(/^\d{3}[-\s]?\d{2}[-\s]?\d{4}$/));
|
|
79
97
|
/**
|
|
80
|
-
* Schema for credit card number validation
|
|
98
|
+
* Schema for credit card number validation.
|
|
81
99
|
* @compliance NIST 800-53 SI-10 (Information Input Validation)
|
|
82
100
|
*/
|
|
83
101
|
const CreditCardSchema = Schema.String.check(Schema.isPattern(/^(?:\d{4}[-\s]?){3}\d{4}$|^\d{15,16}$/));
|
|
84
102
|
/**
|
|
85
|
-
* Schema for IPv4 address validation
|
|
103
|
+
* Schema for IPv4 address validation.
|
|
86
104
|
*/
|
|
87
105
|
const IPv4Schema = Schema.String.check(Schema.isPattern(/^(?:\d{1,3}\.){3}\d{1,3}$/));
|
|
88
106
|
/**
|
|
@@ -107,9 +125,16 @@ const escapeHtml = (text) => {
|
|
|
107
125
|
* Validates and sanitizes a user-supplied URL using Effect Schema.
|
|
108
126
|
* Returns an Exit with the sanitized URL or an error.
|
|
109
127
|
*
|
|
128
|
+
* Pure and total — failure is encoded as a resolved {@link Exit.Exit} failure
|
|
129
|
+
* (an `Exit.fail` carrying a {@link S.SchemaError}), never a thrown exception.
|
|
130
|
+
*
|
|
110
131
|
* @param url - The URL to be validated and sanitized.
|
|
111
|
-
* @param allowedProtocols -
|
|
112
|
-
*
|
|
132
|
+
* @param allowedProtocols - Allowed URL protocols; a root-relative path (`/foo`) is
|
|
133
|
+
* always accepted regardless of this list. An authority-opening reference — `//host`,
|
|
134
|
+
* `///host`, or `/\host` — is always **rejected**, because it names a different host
|
|
135
|
+
* once resolved and would otherwise bypass this list entirely.
|
|
136
|
+
* @returns An {@link Exit.Exit}: success carries the accepted URL string,
|
|
137
|
+
* failure carries a {@link S.SchemaError}.
|
|
113
138
|
* @compliance NIST 800-53 SI-10 (Information Input Validation)
|
|
114
139
|
*
|
|
115
140
|
* @example
|
|
@@ -129,7 +154,8 @@ const sanitizeUrlEffect = (url, allowedProtocols = [
|
|
|
129
154
|
const CustomSafeUrlSchema = Schema.String.check(Schema.makeFilter((u) => {
|
|
130
155
|
if (!u || u.trim() === "") return false;
|
|
131
156
|
const trimmed = u.trim();
|
|
132
|
-
if (
|
|
157
|
+
if (opensAuthority(trimmed)) return false;
|
|
158
|
+
if (trimmed.startsWith("/")) return true;
|
|
133
159
|
try {
|
|
134
160
|
const parsed = new URL(trimmed);
|
|
135
161
|
if (!allowedProtocols.includes(parsed.protocol)) return false;
|
|
@@ -165,9 +191,42 @@ const sanitizeUrl = (url, allowedProtocols = [
|
|
|
165
191
|
return Exit.isSuccess(result) ? result.value : "";
|
|
166
192
|
};
|
|
167
193
|
let purifyInstance;
|
|
194
|
+
/**
|
|
195
|
+
* Add `rel="noopener noreferrer"` to any link that opens a new browsing context.
|
|
196
|
+
*
|
|
197
|
+
* Without it the opened page receives a live `window.opener` handle and can navigate
|
|
198
|
+
* the original tab to a phishing page — reverse tabnabbing, WSTG-CLNT-14. DOMPurify
|
|
199
|
+
* does not add this by default, and unlike most of CLNT-14 the fix lives inside code
|
|
200
|
+
* this package already owns, so a weakness no signature can detect becomes one that is
|
|
201
|
+
* simply prevented.
|
|
202
|
+
*
|
|
203
|
+
* Modern browsers imply `noopener` for `target="_blank"`; this covers older engines and
|
|
204
|
+
* the named-target case (`target="win1"`), which remains exploitable everywhere.
|
|
205
|
+
*
|
|
206
|
+
* Note that DOMPurify's default configuration strips `target` outright, so this hook is
|
|
207
|
+
* a no-op unless the caller opts back in with `ADD_ATTR: ["target"]` or a custom
|
|
208
|
+
* `ALLOWED_ATTR` — which is precisely the configuration that reintroduces the risk.
|
|
209
|
+
*
|
|
210
|
+
* @internal
|
|
211
|
+
*/
|
|
212
|
+
const addNoopenerToTargetedLinks = (node) => {
|
|
213
|
+
if (typeof node.hasAttribute !== "function" || !node.hasAttribute("target")) return;
|
|
214
|
+
const target = node.getAttribute("target");
|
|
215
|
+
if (target === null || target === "_self" || target === "_parent" || target === "_top") return;
|
|
216
|
+
const existing = (node.getAttribute("rel") ?? "").split(/\s+/).filter(Boolean);
|
|
217
|
+
for (const required of ["noopener", "noreferrer"]) if (!existing.includes(required)) existing.push(required);
|
|
218
|
+
node.setAttribute("rel", existing.join(" "));
|
|
219
|
+
};
|
|
220
|
+
/** Register the reverse-tabnabbing hook on a DOMPurify instance. */
|
|
221
|
+
const withHooks = (purify) => {
|
|
222
|
+
purify.addHook("afterSanitizeAttributes", (node) => {
|
|
223
|
+
addNoopenerToTargetedLinks(node);
|
|
224
|
+
});
|
|
225
|
+
return purify;
|
|
226
|
+
};
|
|
168
227
|
const getPurify = () => {
|
|
169
228
|
if (purifyInstance !== void 0) return purifyInstance;
|
|
170
|
-
if (typeof window !== "undefined") purifyInstance = DOMPurify;
|
|
229
|
+
if (typeof window !== "undefined") purifyInstance = withHooks(DOMPurify);
|
|
171
230
|
else try {
|
|
172
231
|
const nodeModule = globalThis.process?.getBuiltinModule?.("module");
|
|
173
232
|
if (!nodeModule) {
|
|
@@ -175,7 +234,8 @@ const getPurify = () => {
|
|
|
175
234
|
return purifyInstance;
|
|
176
235
|
}
|
|
177
236
|
const { JSDOM } = nodeModule.createRequire(import.meta.url)("jsdom");
|
|
178
|
-
|
|
237
|
+
const dom = new JSDOM("");
|
|
238
|
+
purifyInstance = withHooks(DOMPurify(dom.window));
|
|
179
239
|
} catch {
|
|
180
240
|
purifyInstance = null;
|
|
181
241
|
}
|
|
@@ -189,9 +249,16 @@ const getPurify = () => {
|
|
|
189
249
|
* NOTE: Server-side HTML sanitization requires `jsdom` to be installed in the consuming application
|
|
190
250
|
* environment; otherwise, it will fall back to escaping HTML characters.
|
|
191
251
|
*
|
|
252
|
+
* On the first server-side call this lazily resolves `node:module` and
|
|
253
|
+
* `require`s `jsdom` to build a DOMPurify instance; the instance is cached at
|
|
254
|
+
* module scope, so subsequent calls incur no further module loading. Returns
|
|
255
|
+
* `""` for non-string or empty input and never throws — any loader failure is
|
|
256
|
+
* swallowed and downgraded to {@link escapeHtml}.
|
|
257
|
+
*
|
|
192
258
|
* @param html - The HTML string to sanitize.
|
|
193
259
|
* @param options - Optional DOMPurify configuration.
|
|
194
|
-
* @returns The sanitized HTML string
|
|
260
|
+
* @returns The sanitized HTML string, or the escaped string when no DOM is
|
|
261
|
+
* available.
|
|
195
262
|
*/
|
|
196
263
|
const sanitizeHtml = (html, options) => {
|
|
197
264
|
if (!html || typeof html !== "string") return "";
|
|
@@ -202,9 +269,17 @@ const sanitizeHtml = (html, options) => {
|
|
|
202
269
|
/**
|
|
203
270
|
* Validates user input using Effect Schema and returns an Exit.
|
|
204
271
|
*
|
|
272
|
+
* Strips HTML (unless `allowHtml`), collapses whitespace, and repeatedly
|
|
273
|
+
* removes dangerous URI schemes and inline handlers until the string
|
|
274
|
+
* stabilizes — the fixed-point loop defeats nested-payload bypasses such as
|
|
275
|
+
* `javascrjavascript:ipt:`. Failure is a resolved {@link Exit.Exit} failure,
|
|
276
|
+
* never a throw; the only failure path is a non-string reaching `S.String`.
|
|
277
|
+
*
|
|
205
278
|
* @param input - User input to validate and sanitize.
|
|
206
|
-
* @param options - Validation options.
|
|
207
|
-
*
|
|
279
|
+
* @param options - Validation options. Defaults: `maxLength` 500, `allowHtml`
|
|
280
|
+
* false, `allowNewlines` false, `trimWhitespace` true.
|
|
281
|
+
* @returns An {@link Exit.Exit}: success carries the sanitized string truncated
|
|
282
|
+
* to `maxLength`; failure carries a {@link S.SchemaError}.
|
|
208
283
|
* @compliance NIST 800-53 SI-10 (Information Input Validation)
|
|
209
284
|
*/
|
|
210
285
|
const validateUserInputEffect = (input, options = {}) => {
|
|
@@ -253,32 +328,52 @@ const validateUserInput = (input, maxLength = 500, allowHtml = false) => {
|
|
|
253
328
|
});
|
|
254
329
|
return Exit.isSuccess(result) ? result.value : "";
|
|
255
330
|
};
|
|
331
|
+
/** Own keys removed at every level, because assigning them reaches the prototype. */
|
|
332
|
+
const DANGEROUS_KEYS = [
|
|
333
|
+
"__proto__",
|
|
334
|
+
"constructor",
|
|
335
|
+
"prototype"
|
|
336
|
+
];
|
|
256
337
|
/**
|
|
257
|
-
*
|
|
338
|
+
* Strip prototype-pollution keys (`__proto__`, `constructor`, `prototype`) from
|
|
339
|
+
* every node of a parsed JSON value.
|
|
340
|
+
*
|
|
341
|
+
* **Mutates `val` in place** (deletes offending keys) and returns nothing;
|
|
342
|
+
* callers pass a freshly `JSON.parse`d value they own. The walk is iterative and
|
|
343
|
+
* unbounded in depth: every node is visited, however deeply nested.
|
|
344
|
+
*
|
|
345
|
+
* @internal
|
|
258
346
|
*/
|
|
259
|
-
const sanitizeObject = (val
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
347
|
+
const sanitizeObject = (val) => {
|
|
348
|
+
const stack = [val];
|
|
349
|
+
while (stack.length > 0) {
|
|
350
|
+
const current = stack.pop();
|
|
351
|
+
if (typeof current !== "object" || current === null) continue;
|
|
352
|
+
if (Array.isArray(current)) {
|
|
353
|
+
for (const item of current) stack.push(item);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const obj = current;
|
|
357
|
+
for (const key of DANGEROUS_KEYS) if (key in obj) delete obj[key];
|
|
358
|
+
for (const key of Object.keys(obj)) stack.push(obj[key]);
|
|
265
359
|
}
|
|
266
|
-
const dangerous = [
|
|
267
|
-
"__proto__",
|
|
268
|
-
"constructor",
|
|
269
|
-
"prototype"
|
|
270
|
-
];
|
|
271
|
-
const obj = val;
|
|
272
|
-
for (const key of dangerous) if (key in obj) delete obj[key];
|
|
273
|
-
for (const key of Object.keys(obj)) sanitizeObject(obj[key], depth + 1);
|
|
274
360
|
};
|
|
275
361
|
/**
|
|
276
362
|
* Safely parses JSON with Effect Schema validation and prototype pollution protection.
|
|
277
363
|
*
|
|
278
|
-
*
|
|
364
|
+
* Never throws: malformed JSON, a non-string argument, and schema-validation
|
|
365
|
+
* failure all resolve to {@link Option.none} rather than a thrown error, so the
|
|
366
|
+
* failure channel is the `Option` itself. As a side effect the parsed value is
|
|
367
|
+
* stripped of prototype-pollution keys in place before validation (the value is
|
|
368
|
+
* freshly created by `JSON.parse`, so no caller state is mutated).
|
|
369
|
+
*
|
|
370
|
+
* @template A - The decoded value type the `schema` produces on success; the
|
|
371
|
+
* returned `Option` carries this type.
|
|
279
372
|
* @param jsonString - The JSON string to parse.
|
|
280
|
-
* @param schema - Effect Schema to validate against
|
|
281
|
-
* @
|
|
373
|
+
* @param schema - Effect Schema to validate against; its decode must require no
|
|
374
|
+
* services ({@link SyncSchema}) so parsing stays synchronous.
|
|
375
|
+
* @returns {@link Option.some} with the parsed, validated value, or
|
|
376
|
+
* {@link Option.none} on any parse or validation failure.
|
|
282
377
|
* @compliance NIST 800-53 SI-10 (Information Input Validation)
|
|
283
378
|
*
|
|
284
379
|
* @example
|
|
@@ -332,8 +427,17 @@ const sanitizeJson = (jsonString) => {
|
|
|
332
427
|
}
|
|
333
428
|
};
|
|
334
429
|
/**
|
|
335
|
-
* Strips ANSI escape
|
|
336
|
-
*
|
|
430
|
+
* Strips ANSI escape sequences from a string.
|
|
431
|
+
*
|
|
432
|
+
* Removes CSI sequences (colour, cursor movement, screen and line erasure, mode
|
|
433
|
+
* switches), OSC sequences (window title and similar), two-character escapes, and
|
|
434
|
+
* any bare ESC left over. This is the control named by `LOG-ANSI-ESCAPE-001`: a
|
|
435
|
+
* terminal-backed log sink treats these as commands, so an attacker who lands them
|
|
436
|
+
* in a log can scroll earlier entries away or overwrite them (CWE-117).
|
|
437
|
+
*
|
|
438
|
+
* Removal is destructive by design — the sequence goes, and any text it carried
|
|
439
|
+
* goes with it. Where the record matters more than the rendering, escape the
|
|
440
|
+
* characters instead of deleting them.
|
|
337
441
|
*
|
|
338
442
|
* @param text - The text potentially containing ANSI codes.
|
|
339
443
|
* @returns The text with ANSI codes removed.
|
|
@@ -345,10 +449,11 @@ const sanitizeJson = (jsonString) => {
|
|
|
345
449
|
*/
|
|
346
450
|
const stripAnsi = (text) => {
|
|
347
451
|
if (!text || typeof text !== "string") return "";
|
|
348
|
-
return text.replaceAll(/\
|
|
452
|
+
return text.replaceAll(/\u001b(?:\[[0-?]{0,32}[ -/]{0,8}[@-~]|\][^\u0007\u001b]{0,512}(?:\u0007|\u001b\\)|[ -/]{0,8}[0-~])?/g, "");
|
|
349
453
|
};
|
|
350
454
|
/**
|
|
351
|
-
* PII pattern
|
|
455
|
+
* PII pattern catalog: each entry pairs a global-match regex with the marker that
|
|
456
|
+
* replaces every hit during redaction.
|
|
352
457
|
* @compliance NIST 800-53 AU-3 (Content of Audit Records)
|
|
353
458
|
*/
|
|
354
459
|
const PII_PATTERNS = {
|
|
@@ -388,9 +493,15 @@ const PII_PATTERNS = {
|
|
|
388
493
|
/**
|
|
389
494
|
* Redacts PII from text using Effect Schema validated options.
|
|
390
495
|
*
|
|
496
|
+
* Total — never throws. Failure to decode `options` is returned as a resolved
|
|
497
|
+
* {@link Exit.Exit} failure carrying the {@link S.SchemaError}. By default
|
|
498
|
+
* dates are **not** redacted (`redactDates` defaults to `false`); every other
|
|
499
|
+
* category defaults to `true`.
|
|
500
|
+
*
|
|
391
501
|
* @param text - The text to redact PII from.
|
|
392
502
|
* @param options - Configuration options for redaction.
|
|
393
|
-
* @returns Exit
|
|
503
|
+
* @returns An {@link Exit.Exit}: success carries the redacted text, failure
|
|
504
|
+
* carries a {@link S.SchemaError} from invalid `options`.
|
|
394
505
|
* @compliance NIST 800-53 AU-3 (Content of Audit Records)
|
|
395
506
|
*/
|
|
396
507
|
const redactPIIEffect = (text, options = {}) => {
|
|
@@ -416,9 +527,15 @@ const redactPIIEffect = (text, options = {}) => {
|
|
|
416
527
|
* Redacts common PII patterns in a string for safe logging.
|
|
417
528
|
* Detects and masks SSNs, credit cards, emails, phone numbers, etc.
|
|
418
529
|
*
|
|
419
|
-
* @param text - The text to redact PII from.
|
|
420
|
-
* @param options - Configuration options for redaction
|
|
421
|
-
*
|
|
530
|
+
* @param text - The text to redact PII from. Non-string input yields `""`.
|
|
531
|
+
* @param options - Configuration options for redaction, plus optional
|
|
532
|
+
* `customPatterns` applied after the built-ins. Each `pattern` **must** be a
|
|
533
|
+
* global (`/g`) RegExp.
|
|
534
|
+
* @returns The text with PII patterns replaced with redaction markers, or `""`
|
|
535
|
+
* for non-string input. If the built-in options fail schema validation the
|
|
536
|
+
* original `text` is returned unredacted rather than throwing.
|
|
537
|
+
* @throws {TypeError} If any `customPatterns` entry uses a non-global RegExp —
|
|
538
|
+
* `String.prototype.replaceAll` rejects non-global patterns.
|
|
422
539
|
* @compliance NIST 800-53 AU-3 (Content of Audit Records)
|
|
423
540
|
*
|
|
424
541
|
* @example
|
|
@@ -449,10 +566,17 @@ const redactPII = (text, options = {}) => {
|
|
|
449
566
|
* Creates a safe string representation of an object for logging,
|
|
450
567
|
* automatically redacting sensitive fields.
|
|
451
568
|
*
|
|
569
|
+
* Never throws: any serialization failure — a circular reference, a `BigInt`
|
|
570
|
+
* value, a throwing `toJSON` — is caught and returned as the sentinel string
|
|
571
|
+
* `"[Unable to stringify object]"`. Key matching is case-insensitive and
|
|
572
|
+
* compares the full key name (not substrings), so `apiKey` matches only the
|
|
573
|
+
* literal `"apiKey"`, not `"apiKeyId"`.
|
|
574
|
+
*
|
|
452
575
|
* @param obj - The object to stringify.
|
|
453
|
-
* @param sensitiveKeys -
|
|
576
|
+
* @param sensitiveKeys - Key names to redact, compared case-insensitively.
|
|
454
577
|
* @param indent - JSON indentation (default: 2).
|
|
455
|
-
* @returns A JSON string with sensitive values redacted
|
|
578
|
+
* @returns A JSON string with sensitive values redacted, or the sentinel
|
|
579
|
+
* `"[Unable to stringify object]"` when serialization fails.
|
|
456
580
|
* @compliance NIST 800-53 AU-3 (Content of Audit Records)
|
|
457
581
|
*
|
|
458
582
|
* @example
|
package/lib/sanitize.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sanitize.mjs","names":["S"],"sources":["../src/sanitize.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @file Input sanitization utilities for XSS prevention and data validation\n * @module utils/sanitize\n * @author ResQ\n * @description Provides type-safe input sanitization using Effect Schema for validation.\n * Includes utilities for HTML escaping, URL validation, PII redaction, and more.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\n\nimport type { Brand } from \"@resq-systems/types\";\nimport { Exit, Option, Schema as S } from \"effect\";\nimport DOMPurify from \"dompurify\";\nimport type { Config, WindowLike } from \"dompurify\";\n\n/**\n * A Schema with DecodingServices constrained to `never`, allowing synchronous decoding.\n */\ntype SyncSchema<T> = S.Codec<T, unknown, never>;\n\n// ============================================\n// Effect Schema Definitions\n// ============================================\n\n/**\n * Schema for URL protocol validation\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const UrlProtocolSchema = S.Literals([\"http:\", \"https:\", \"mailto:\", \"tel:\", \"ftp:\"]);\nexport type UrlProtocol = typeof UrlProtocolSchema.Type;\n\n/**\n * Schema for PII redaction options\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n */\nexport const PIIRedactionOptionsSchema = S.Struct({\n\tredactEmails: S.optional(S.Boolean),\n\tredactPhones: S.optional(S.Boolean),\n\tredactSSN: S.optional(S.Boolean),\n\tredactCreditCards: S.optional(S.Boolean),\n\tredactIPs: S.optional(S.Boolean),\n\tredactDates: S.optional(S.Boolean),\n});\nexport type PIIRedactionOptions = typeof PIIRedactionOptionsSchema.Type;\n\n/**\n * Schema for user input validation options\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const UserInputOptionsSchema = S.Struct({\n\tmaxLength: S.optional(S.Int.check(S.isGreaterThan(0))),\n\tallowHtml: S.optional(S.Boolean),\n\tallowNewlines: S.optional(S.Boolean),\n\ttrimWhitespace: S.optional(S.Boolean),\n});\nexport type UserInputOptions = typeof UserInputOptionsSchema.Type;\n\n/**\n * Schema for safe URL - validates URL format and protocol\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const SafeUrlSchema = S.String.check(\n\tS.makeFilter(\n\t\t(url: string) => {\n\t\t\tif (!url || url.trim() === \"\") return false;\n\t\t\tif (url.startsWith(\"/\") && !url.startsWith(\"//\")) return true;\n\t\t\ttry {\n\t\t\t\tconst parsed = new URL(url);\n\t\t\t\tconst safeProtocols = [\"http:\", \"https:\", \"mailto:\"];\n\t\t\t\treturn safeProtocols.includes(parsed.protocol);\n\t\t\t} catch {\n\t\t\t\treturn /^[a-zA-Z0-9/_.-]+$/.test(url);\n\t\t\t}\n\t\t},\n\t\t{ message: \"Invalid or unsafe URL\" },\n\t),\n);\n/** A string that has passed {@link isValidUrl} — a validated, injection-safe URL. */\nexport type SafeUrl = Brand<string, \"SafeUrl\">;\n\n/**\n * Schema for sanitized HTML-safe string (validates as string; escaping done at runtime)\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const SanitizedStringSchema = S.String;\nexport type SanitizedString = typeof SanitizedStringSchema.Type;\n\n/**\n * Schema for email address validation.\n *\n * Accepts a 2+ character alphabetic TLD or a Punycode/IDN `xn--…` TLD (e.g.\n * `.xn--p1ai` for `.рф`) so internationalized domains are not rejected. Kept in\n * sync with `@resq-systems/email-templates`'s `EmailAddress` brand.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const EmailSchema = S.String.check(\n\tS.isPattern(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.(?:[A-Za-z]{2,}|xn--[A-Za-z0-9-]+)$/),\n);\n/** A string that has passed {@link isValidEmail}. */\nexport type Email = Brand<string, \"Email\">;\n\n/**\n * Schema for phone number validation (US format)\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const PhoneNumberSchema = S.String.check(\n\tS.isPattern(/^(?:\\+?1[-.\\s]?)?\\(?\\d{3}\\)?[-.\\s]?\\d{3}[-.\\s]?\\d{4}$/),\n);\n/** A string that has passed {@link isValidPhone} (US format). */\nexport type PhoneNumber = Brand<string, \"PhoneNumber\">;\n\n/**\n * Schema for SSN validation (US format)\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const SSNSchema = S.String.check(S.isPattern(/^\\d{3}[-\\s]?\\d{2}[-\\s]?\\d{4}$/));\n/** A string that has passed {@link isValidSSN} (US format). */\nexport type SSN = Brand<string, \"SSN\">;\n\n/**\n * Schema for credit card number validation\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const CreditCardSchema = S.String.check(\n\tS.isPattern(/^(?:\\d{4}[-\\s]?){3}\\d{4}$|^\\d{15,16}$/),\n);\n/** A string matching the {@link CreditCardSchema} pattern. */\nexport type CreditCard = Brand<string, \"CreditCard\">;\n\n/**\n * Schema for IPv4 address validation\n */\nexport const IPv4Schema = S.String.check(S.isPattern(/^(?:\\d{1,3}\\.){3}\\d{1,3}$/));\n/** A string matching the {@link IPv4Schema} dotted-quad pattern. */\nexport type IPv4 = Brand<string, \"IPv4\">;\n\n// ============================================\n// Sanitization Functions\n// ============================================\n\n/**\n * Escapes special HTML characters in a string to their corresponding HTML entities,\n * preventing direct injection of HTML and JavaScript when rendering untrusted content.\n *\n * @param text - The plain text to escape.\n * @returns The escaped string safe for HTML rendering.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * escapeHtml('<script>alert(\"xss\")</script>');\n * // \"<script>alert("xss")</script>\"\n * ```\n */\nexport const escapeHtml = (text: string): string => {\n\tif (!text || typeof text !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\treturn text\n\t\t.replaceAll(\"&\", \"&\")\n\t\t.replaceAll(\"<\", \"<\")\n\t\t.replaceAll(\">\", \">\")\n\t\t.replaceAll('\"', \""\")\n\t\t.replaceAll(\"'\", \"'\");\n};\n\n/**\n * Validates and sanitizes a user-supplied URL using Effect Schema.\n * Returns an Exit with the sanitized URL or an error.\n *\n * @param url - The URL to be validated and sanitized.\n * @param allowedProtocols - Array of allowed URL protocols.\n * @returns Exit containing the sanitized URL or an error.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * const result = sanitizeUrlEffect('https://example.com');\n * // Exit.succeed('https://example.com')\n *\n * const invalid = sanitizeUrlEffect('javascript:alert(1)');\n * // Exit.fail(...)\n * ```\n */\nexport const sanitizeUrlEffect = (\n\turl: string,\n\tallowedProtocols: readonly UrlProtocol[] = [\"http:\", \"https:\", \"mailto:\"],\n): Exit.Exit<string, S.SchemaError> => {\n\tconst CustomSafeUrlSchema = S.String.check(\n\t\tS.makeFilter(\n\t\t\t(u: string) => {\n\t\t\t\tif (!u || u.trim() === \"\") return false;\n\t\t\t\tconst trimmed = u.trim();\n\t\t\t\tif (trimmed.startsWith(\"/\") && !trimmed.startsWith(\"//\")) return true;\n\t\t\t\ttry {\n\t\t\t\t\tconst parsed = new URL(trimmed);\n\t\t\t\t\tif (!allowedProtocols.includes(parsed.protocol)) return false;\n\t\t\t\t\tif (parsed.hostname.includes(\"javascript:\") || parsed.hostname.includes(\"data:\")) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t} catch {\n\t\t\t\t\treturn (\n\t\t\t\t\t\t/^[a-zA-Z0-9/_.-]+$/.test(trimmed) &&\n\t\t\t\t\t\t!trimmed.includes(\"javascript:\") &&\n\t\t\t\t\t\t!trimmed.includes(\"data:\")\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t},\n\t\t\t{ message: \"Invalid or unsafe URL\" },\n\t\t),\n\t);\n\n\treturn S.decodeUnknownExit(CustomSafeUrlSchema)(url);\n};\n\n/**\n * Validates and sanitizes a user-supplied URL, ensuring it conforms to allowed protocols\n * and is not a vector for injection attacks like `javascript:` or `data:`.\n *\n * @param url - The URL to be validated and sanitized.\n * @param allowedProtocols - Array of allowed URL protocols.\n * @returns The sanitized URL if valid, or an empty string if unsafe.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * sanitizeUrl('https://example.com'); // 'https://example.com'\n * sanitizeUrl('javascript:alert(1)'); // ''\n * ```\n */\nexport const sanitizeUrl = (\n\turl: string,\n\tallowedProtocols: readonly UrlProtocol[] = [\"http:\", \"https:\", \"mailto:\"],\n): string => {\n\tconst result = sanitizeUrlEffect(url, allowedProtocols);\n\treturn Exit.isSuccess(result) ? result.value : \"\";\n};\n\nlet purifyInstance: typeof DOMPurify | null | undefined;\n\nconst getPurify = (): typeof DOMPurify | null => {\n\tif (purifyInstance !== undefined) return purifyInstance;\n\n\tif (typeof window !== \"undefined\") {\n\t\tpurifyInstance = DOMPurify;\n\t} else {\n\t\ttry {\n\t\t\t// Resolve `node:module` at runtime (server-side only) via\n\t\t\t// process.getBuiltinModule so browser bundlers never see a static\n\t\t\t// `node:module` import. Available on Node >=20.16 and Bun; absent in\n\t\t\t// browsers, where the `window` branch above is taken instead. jsdom is an\n\t\t\t// optional peer dependency — install it for server-side HTML sanitization.\n\t\t\tconst proc = (globalThis as { process?: { getBuiltinModule?: (m: string) => unknown } })\n\t\t\t\t.process;\n\t\t\tconst nodeModule = proc?.getBuiltinModule?.(\"module\") as\n\t\t\t\t| { createRequire(path: string | URL): (id: string) => unknown }\n\t\t\t\t| undefined;\n\t\t\tif (!nodeModule) {\n\t\t\t\tpurifyInstance = null;\n\t\t\t\treturn purifyInstance;\n\t\t\t}\n\t\t\tconst req = nodeModule.createRequire(import.meta.url);\n\t\t\tconst { JSDOM } = req(\"jsdom\") as {\n\t\t\t\tJSDOM: new (\n\t\t\t\t\thtml?: string,\n\t\t\t\t) => {\n\t\t\t\t\twindow: WindowLike;\n\t\t\t\t};\n\t\t\t};\n\t\t\tconst dom = new JSDOM(\"\");\n\t\t\tpurifyInstance = DOMPurify(dom.window);\n\t\t} catch {\n\t\t\tpurifyInstance = null;\n\t\t}\n\t}\n\treturn purifyInstance;\n};\n\n/**\n * Sanitizes HTML to prevent XSS attacks.\n * Uses DOMPurify under the hood. If DOM is not available (e.g. server-side without JSDOM),\n * it falls back to escaping all HTML characters for safety.\n *\n * NOTE: Server-side HTML sanitization requires `jsdom` to be installed in the consuming application\n * environment; otherwise, it will fall back to escaping HTML characters.\n *\n * @param html - The HTML string to sanitize.\n * @param options - Optional DOMPurify configuration.\n * @returns The sanitized HTML string.\n */\nexport const sanitizeHtml = (html: string, options?: Config): string => {\n\tif (!html || typeof html !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\tconst purify = getPurify();\n\tif (purify) {\n\t\treturn purify.sanitize(html, options) as string;\n\t}\n\n\treturn escapeHtml(html);\n};\n\n/**\n * Validates user input using Effect Schema and returns an Exit.\n *\n * @param input - User input to validate and sanitize.\n * @param options - Validation options.\n * @returns Exit containing sanitized input or error.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const validateUserInputEffect = (\n\tinput: string,\n\toptions: UserInputOptions = {},\n): Exit.Exit<string, S.SchemaError> => {\n\tconst {\n\t\tmaxLength = 500,\n\t\tallowHtml = false,\n\t\tallowNewlines = false,\n\t\ttrimWhitespace = true,\n\t} = options;\n\n\tconst parsed = S.decodeUnknownExit(S.String)(input);\n\tif (Exit.isFailure(parsed)) return parsed;\n\n\tlet result = parsed.value;\n\n\tif (trimWhitespace) {\n\t\tresult = result.trim();\n\t}\n\n\tif (!allowHtml) {\n\t\tlet prev: string;\n\t\tdo {\n\t\t\tprev = result;\n\t\t\tresult = result.replaceAll(/<[^>]*>/g, \"\");\n\t\t} while (result !== prev);\n\t} else {\n\t\tresult = sanitizeHtml(result);\n\t}\n\n\tif (!allowNewlines) {\n\t\tresult = result.replaceAll(/[\\r\\n]+/g, \" \");\n\t}\n\n\tresult = result.replaceAll(/\\s+/g, \" \");\n\n\t// Loop until stable to prevent bypass via nested patterns (e.g. \"javascrjavascript:ipt:\")\n\tlet prevScheme: string;\n\tdo {\n\t\tprevScheme = result;\n\t\tresult = result\n\t\t\t.replaceAll(/javascript:/gi, \"\")\n\t\t\t.replaceAll(/data:/gi, \"\")\n\t\t\t.replaceAll(/vbscript:/gi, \"\")\n\t\t\t.replaceAll(/on\\w+=/gi, \"\");\n\t} while (result !== prevScheme);\n\n\treturn Exit.succeed(result.slice(0, maxLength));\n};\n\n/**\n * Validates and sanitizes generic user input by trimming, removing HTML tags (unless allowed),\n * normalizing whitespace, and removing dangerous patterns to prevent XSS and basic injection flaws.\n *\n * @param input - User input to validate and sanitize.\n * @param maxLength - Maximum allowed input length. Excess will be truncated.\n * @param allowHtml - If true, HTML tags are preserved; otherwise, all tags are stripped.\n * @returns Sanitized input string with length at most `maxLength`.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * validateUserInput('<p>Hello!</p>', 50); // \"Hello!\"\n * validateUserInput('<script>alert(1)</script>test', 100); // \"test\"\n * ```\n */\nexport const validateUserInput = (input: string, maxLength = 500, allowHtml = false): string => {\n\tif (!input || typeof input !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\tconst result = validateUserInputEffect(input, { maxLength, allowHtml });\n\treturn Exit.isSuccess(result) ? result.value : \"\";\n};\n\n/**\n * Recursively removes dangerous prototype pollution keys from an object.\n */\nconst sanitizeObject = (val: unknown, depth = 0): void => {\n\tif (depth > 50) {\n\t\treturn;\n\t}\n\tif (typeof val !== \"object\" || val === null) {\n\t\treturn;\n\t}\n\tif (Array.isArray(val)) {\n\t\tfor (const item of val) {\n\t\t\tsanitizeObject(item, depth + 1);\n\t\t}\n\t\treturn;\n\t}\n\tconst dangerous = [\"__proto__\", \"constructor\", \"prototype\"];\n\tconst obj = val as Record<string, unknown>;\n\tfor (const key of dangerous) {\n\t\tif (key in obj) {\n\t\t\tdelete obj[key];\n\t\t}\n\t}\n\tfor (const key of Object.keys(obj)) {\n\t\tsanitizeObject(obj[key], depth + 1);\n\t}\n};\n\n/**\n * Safely parses JSON with Effect Schema validation and prototype pollution protection.\n *\n * @template A - The expected schema type\n * @param jsonString - The JSON string to parse.\n * @param schema - Effect Schema to validate against.\n * @returns Option containing the parsed and validated object.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * const UserSchema = S.Struct({ name: S.String, age: S.Number });\n * const result = parseJsonWithSchema('{\"name\":\"John\",\"age\":30}', UserSchema);\n * // Option.some({ name: 'John', age: 30 })\n * ```\n */\nexport const parseJsonWithSchema = <A>(\n\tjsonString: string,\n\tschema: SyncSchema<A>,\n): Option.Option<A> => {\n\tif (!jsonString || typeof jsonString !== \"string\") {\n\t\treturn Option.none();\n\t}\n\n\ttry {\n\t\tconst sanitized = jsonString\n\t\t\t.replaceAll(/\\)\\s*\\{/g, \") {}\")\n\t\t\t.replaceAll(/\\]\\s*\\{/g, \"] {}\")\n\t\t\t.replaceAll(/\\}\\s*\\{/g, \"} {}\");\n\n\t\tconst parsed = JSON.parse(sanitized);\n\n\t\tsanitizeObject(parsed);\n\n\t\tconst result = S.decodeUnknownExit(schema)(parsed);\n\t\treturn Exit.isSuccess(result) ? Option.some(result.value as A) : Option.none();\n\t} catch {\n\t\treturn Option.none();\n\t}\n};\n\n/**\n * Sanitizes and safely parses a JSON string, removing suspicious syntax elements that could\n * potentially result in JSON polyglot exploits or prototype pollution.\n *\n * The result is returned as `unknown` — this function performs **no** schema\n * validation, so it cannot honestly promise any concrete shape for\n * attacker-controlled input. Narrow the result yourself, or prefer\n * {@link parseJsonWithSchema}, which validates against an Effect Schema and\n * returns a typed `Option`.\n *\n * @param jsonString - The JSON string to sanitize and parse.\n * @returns The parsed value (as `unknown`) if valid, or `null` if invalid.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * const obj = sanitizeJson('{\"foo\":\"bar\"}');\n * // obj: unknown — narrow before use, or use parseJsonWithSchema\n * ```\n */\nexport const sanitizeJson = (jsonString: string): unknown => {\n\tif (!jsonString || typeof jsonString !== \"string\") {\n\t\treturn null;\n\t}\n\n\ttry {\n\t\tconst sanitized = jsonString\n\t\t\t.replaceAll(/\\)\\s*\\{/g, \") {}\")\n\t\t\t.replaceAll(/\\]\\s*\\{/g, \"] {}\")\n\t\t\t.replaceAll(/\\}\\s*\\{/g, \"} {}\");\n\n\t\tconst parsed: unknown = JSON.parse(sanitized);\n\n\t\tsanitizeObject(parsed);\n\n\t\treturn parsed;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\n/**\n * Strips ANSI escape codes from a string.\n * Useful for cleaning terminal output before logging to files.\n *\n * @param text - The text potentially containing ANSI codes.\n * @returns The text with ANSI codes removed.\n *\n * @example\n * ```typescript\n * stripAnsi('\\x1b[31mRed text\\x1b[0m'); // 'Red text'\n * ```\n */\nexport const stripAnsi = (text: string): string => {\n\tif (!text || typeof text !== \"string\") {\n\t\treturn \"\";\n\t}\n\t// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI codes require control characters\n\treturn text.replaceAll(/\\x1b\\[[0-9;]*m/g, \"\");\n};\n\n// ============================================\n// PII Redaction Functions\n// ============================================\n\n/**\n * PII pattern definitions with Effect Schema validation\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n */\nconst PII_PATTERNS = {\n\tssn: { pattern: /\\b\\d{3}[-\\s]?\\d{2}[-\\s]?\\d{4}\\b/g, marker: \"[SSN]\" },\n\tcreditCard: { pattern: /\\b(?:\\d{4}[-\\s]?){3}\\d{4}\\b/g, marker: \"[CREDIT_CARD]\" },\n\tcreditCardAlt: { pattern: /\\b\\d{15,16}\\b/g, marker: \"[CREDIT_CARD]\" },\n\t// TLD alternation mirrors `EmailSchema` so IDN/Punycode addresses\n\t// (e.g. `user@example.xn--p1ai`) are redacted, not leaked. The `xn--` branch\n\t// is tried first: unlike the anchored (`$`) validators, this pattern ends in\n\t// `\\b`, so `[A-Za-z]{2,}` would otherwise match just `xn` and stop at the\n\t// hyphen, leaving `--p1ai` unredacted. (Also drops the stray `|` from the\n\t// former `[A-Z|a-z]` class.)\n\temail: {\n\t\tpattern: /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.(?:xn--[A-Za-z0-9-]+|[A-Za-z]{2,})\\b/g,\n\t\tmarker: \"[EMAIL]\",\n\t},\n\tphone: { pattern: /\\b(?:\\+?1[-.\\s]?)?\\(?\\d{3}\\)?[-.\\s]?\\d{3}[-.\\s]?\\d{4}\\b/g, marker: \"[PHONE]\" },\n\tipv4: { pattern: /\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b/g, marker: \"[IP_ADDRESS]\" },\n\tipv6: { pattern: /\\b(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\b/g, marker: \"[IP_ADDRESS]\" },\n\tdate: {\n\t\tpattern: /\\b(?:\\d{1,2}[/-]\\d{1,2}[/-]\\d{2,4}|\\d{4}[/-]\\d{1,2}[/-]\\d{1,2})\\b/g,\n\t\tmarker: \"[DATE]\",\n\t},\n} as const;\n\n/**\n * Redacts PII from text using Effect Schema validated options.\n *\n * @param text - The text to redact PII from.\n * @param options - Configuration options for redaction.\n * @returns Exit containing redacted text or error.\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n */\nexport const redactPIIEffect = (\n\ttext: string,\n\toptions: PIIRedactionOptions = {},\n): Exit.Exit<string, S.SchemaError> => {\n\tconst parsed = S.decodeUnknownExit(PIIRedactionOptionsSchema)(options);\n\tif (Exit.isFailure(parsed)) return Exit.failCause(parsed.cause);\n\n\tconst {\n\t\tredactEmails = true,\n\t\tredactPhones = true,\n\t\tredactSSN = true,\n\t\tredactCreditCards = true,\n\t\tredactIPs = true,\n\t\tredactDates = false,\n\t} = parsed.value;\n\n\tlet result = text;\n\n\tif (redactSSN) {\n\t\tresult = result.replaceAll(PII_PATTERNS.ssn.pattern, PII_PATTERNS.ssn.marker);\n\t}\n\n\tif (redactCreditCards) {\n\t\tresult = result.replaceAll(PII_PATTERNS.creditCard.pattern, PII_PATTERNS.creditCard.marker);\n\t\tresult = result.replaceAll(\n\t\t\tPII_PATTERNS.creditCardAlt.pattern,\n\t\t\tPII_PATTERNS.creditCardAlt.marker,\n\t\t);\n\t}\n\n\tif (redactEmails) {\n\t\tresult = result.replaceAll(PII_PATTERNS.email.pattern, PII_PATTERNS.email.marker);\n\t}\n\n\tif (redactPhones) {\n\t\tresult = result.replaceAll(PII_PATTERNS.phone.pattern, PII_PATTERNS.phone.marker);\n\t}\n\n\tif (redactIPs) {\n\t\tresult = result.replaceAll(PII_PATTERNS.ipv4.pattern, PII_PATTERNS.ipv4.marker);\n\t\tresult = result.replaceAll(PII_PATTERNS.ipv6.pattern, PII_PATTERNS.ipv6.marker);\n\t}\n\n\tif (redactDates) {\n\t\tresult = result.replaceAll(PII_PATTERNS.date.pattern, PII_PATTERNS.date.marker);\n\t}\n\n\treturn Exit.succeed(result);\n};\n\n/**\n * Redacts common PII patterns in a string for safe logging.\n * Detects and masks SSNs, credit cards, emails, phone numbers, etc.\n *\n * @param text - The text to redact PII from.\n * @param options - Configuration options for redaction.\n * @returns The text with PII patterns replaced with redaction markers.\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n *\n * @example\n * ```typescript\n * redactPII('Contact john@example.com or call 555-123-4567');\n * // 'Contact [EMAIL] or call [PHONE]'\n *\n * redactPII('SSN: 123-45-6789');\n * // 'SSN: [SSN]'\n * ```\n */\nexport const redactPII = (\n\ttext: string,\n\toptions: PIIRedactionOptions & {\n\t\tcustomPatterns?: Array<{ pattern: RegExp; replacement: string }>;\n\t} = {},\n): string => {\n\tif (!text || typeof text !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\tconst {\n\t\tredactEmails = true,\n\t\tredactPhones = true,\n\t\tredactSSN = true,\n\t\tredactCreditCards = true,\n\t\tredactIPs = true,\n\t\tredactDates = false,\n\t\tcustomPatterns = [],\n\t} = options;\n\n\tconst result = redactPIIEffect(text, {\n\t\tredactEmails,\n\t\tredactPhones,\n\t\tredactSSN,\n\t\tredactCreditCards,\n\t\tredactIPs,\n\t\tredactDates,\n\t});\n\n\tlet output = Exit.isSuccess(result) ? result.value : text;\n\n\tfor (const { pattern, replacement } of customPatterns) {\n\t\toutput = output.replaceAll(pattern, replacement);\n\t}\n\n\treturn output;\n};\n\n/**\n * Creates a safe string representation of an object for logging,\n * automatically redacting sensitive fields.\n *\n * @param obj - The object to stringify.\n * @param sensitiveKeys - Array of key names to redact.\n * @param indent - JSON indentation (default: 2).\n * @returns A JSON string with sensitive values redacted.\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n *\n * @example\n * ```typescript\n * safeStringify({ user: 'john', password: 'secret123' }, ['password']);\n * // '{\\n \"user\": \"john\",\\n \"password\": \"[REDACTED]\"\\n}'\n * ```\n */\nexport const safeStringify = (\n\tobj: unknown,\n\tsensitiveKeys: string[] = [\n\t\t\"password\",\n\t\t\"token\",\n\t\t\"apiKey\",\n\t\t\"secret\",\n\t\t\"authorization\",\n\t\t\"cookie\",\n\t\t\"ssn\",\n\t\t\"creditCard\",\n\t],\n\tindent = 2,\n): string => {\n\tconst sensitiveKeysLower = new Set(sensitiveKeys.map((k) => k.toLowerCase()));\n\n\tconst replacer = (_key: string, value: unknown): unknown => {\n\t\tif (_key && sensitiveKeysLower.has(_key.toLowerCase())) {\n\t\t\treturn \"[REDACTED]\";\n\t\t}\n\t\treturn value;\n\t};\n\n\ttry {\n\t\treturn JSON.stringify(obj, replacer, indent);\n\t} catch {\n\t\treturn \"[Unable to stringify object]\";\n\t}\n};\n\n// ============================================\n// Validation Helpers\n// ============================================\n\n/**\n * Validates if a string is a valid email address using Effect Schema.\n *\n * Narrows the input to {@link Email} on success, so validated call sites\n * carry the brand into downstream code.\n *\n * @param email - The string to validate.\n * @returns true if valid email, false otherwise.\n */\nexport const isValidEmail = (email: string): email is Email => {\n\treturn Exit.isSuccess(S.decodeUnknownExit(EmailSchema)(email));\n};\n\n/**\n * Validates if a string is a valid phone number using Effect Schema.\n *\n * Narrows the input to {@link PhoneNumber} on success.\n *\n * @param phone - The string to validate.\n * @returns true if valid phone number, false otherwise.\n */\nexport const isValidPhone = (phone: string): phone is PhoneNumber => {\n\treturn Exit.isSuccess(S.decodeUnknownExit(PhoneNumberSchema)(phone));\n};\n\n/**\n * Validates if a string is a valid SSN using Effect Schema.\n *\n * Narrows the input to {@link SSN} on success.\n *\n * @param ssn - The string to validate.\n * @returns true if valid SSN, false otherwise.\n */\nexport const isValidSSN = (ssn: string): ssn is SSN => {\n\treturn Exit.isSuccess(S.decodeUnknownExit(SSNSchema)(ssn));\n};\n\n/**\n * Validates if a string is a safe URL using Effect Schema.\n *\n * Narrows the input to {@link SafeUrl} on success.\n *\n * @param url - The string to validate.\n * @returns true if valid and safe URL, false otherwise.\n */\nexport const isValidUrl = (url: string): url is SafeUrl => {\n\treturn Exit.isSuccess(S.decodeUnknownExit(SafeUrlSchema)(url));\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sanitize.mjs","names":["S"],"sources":["../src/sanitize.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview Type-safe input sanitization built on Effect Schema — HTML escaping\n * and DOMPurify-backed HTML sanitization, safe-URL validation, prototype-pollution-\n * hardened JSON parsing, PII redaction, and branded validators for email, phone, SSN,\n * and more. Supports NIST 800-53 SI-10 (input validation) and AU-3 (audit content).\n *\n * @module @resq-systems/security/sanitize\n */\n\nimport type { Brand } from \"@resq-systems/types\";\nimport { Exit, Option, Schema as S } from \"effect\";\nimport DOMPurify from \"dompurify\";\nimport type { Config, WindowLike } from \"dompurify\";\n\n//#region Types\n\n/**\n * A Schema whose decoding services are constrained to `never`, allowing synchronous\n * decoding without an Effect runtime.\n */\ntype SyncSchema<T> = S.Codec<T, unknown, never>;\n//#endregion\n\n//#region Schemas\n\n/**\n * Schema constraining a URL protocol to the recognized safe set.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const UrlProtocolSchema = S.Literals([\"http:\", \"https:\", \"mailto:\", \"tel:\", \"ftp:\"]);\n/** One of the protocols accepted by {@link UrlProtocolSchema}. */\nexport type UrlProtocol = typeof UrlProtocolSchema.Type;\n\n/**\n * Schema for the per-category toggles that drive {@link redactPII}.\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n */\nexport const PIIRedactionOptionsSchema = S.Struct({\n\tredactEmails: S.optional(S.Boolean),\n\tredactPhones: S.optional(S.Boolean),\n\tredactSSN: S.optional(S.Boolean),\n\tredactCreditCards: S.optional(S.Boolean),\n\tredactIPs: S.optional(S.Boolean),\n\tredactDates: S.optional(S.Boolean),\n});\n/** Decoded options accepted by {@link redactPIIEffect} / {@link redactPII}. */\nexport type PIIRedactionOptions = typeof PIIRedactionOptionsSchema.Type;\n\n/**\n * Schema for the options controlling {@link validateUserInputEffect}.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const UserInputOptionsSchema = S.Struct({\n\tmaxLength: S.optional(S.Int.check(S.isGreaterThan(0))),\n\tallowHtml: S.optional(S.Boolean),\n\tallowNewlines: S.optional(S.Boolean),\n\ttrimWhitespace: S.optional(S.Boolean),\n});\n/** Decoded options accepted by {@link validateUserInputEffect}. */\nexport type UserInputOptions = typeof UserInputOptionsSchema.Type;\n\n/**\n * Schema for a safe URL — validates URL format and restricts to safe protocols.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\n/**\n * Whether a reference opens a URL *authority* component — i.e. can name a different\n * host once resolved against a base.\n *\n * `//evil.example` resolved against `https://trusted.example/page` yields host\n * `evil.example`, and so do `///evil.example` and `/\\evil.example`: the WHATWG URL\n * parser treats a backslash as a slash in the relative-slash state.\n *\n * This must be checked **before** the root-relative fast path. A `startsWith(\"//\")`\n * guard alone is insufficient twice over: it misses the backslash forms entirely, and\n * `//evil.example` that falls through to `new URL()` throws without a base and lands in\n * the catch branch, whose `[a-zA-Z0-9/_.-]` character class happily accepts it.\n *\n * @internal\n */\nconst opensAuthority = (url: string): boolean => /^[/\\\\]{2}/.test(url.trim());\n\nexport const SafeUrlSchema = S.String.check(\n\tS.makeFilter(\n\t\t(url: string) => {\n\t\t\tif (!url || url.trim() === \"\") return false;\n\t\t\tif (opensAuthority(url)) return false;\n\t\t\tif (url.startsWith(\"/\")) return true;\n\t\t\ttry {\n\t\t\t\tconst parsed = new URL(url);\n\t\t\t\tconst safeProtocols = [\"http:\", \"https:\", \"mailto:\"];\n\t\t\t\treturn safeProtocols.includes(parsed.protocol);\n\t\t\t} catch {\n\t\t\t\treturn /^[a-zA-Z0-9/_.-]+$/.test(url);\n\t\t\t}\n\t\t},\n\t\t{ message: \"Invalid or unsafe URL\" },\n\t),\n);\n/**\n * A URL string vouched safe against scheme-based injection. Mint one by\n * narrowing through the {@link isValidUrl} type guard (backed by\n * {@link SafeUrlSchema}); the brand guarantees the value is either a\n * root-relative path or an absolute URL restricted to `http:`/`https:`/\n * `mailto:`. An authority-opening reference (`//host`, `///host`, `/\\host`) is\n * rejected, since resolving one against a base yields a different host.\n *\n * It does **not** guarantee the host is reachable or trusted — for that, validate the\n * resolved origin with `isAllowedOrigin` from `@resq-systems/security/controls`.\n */\nexport type SafeUrl = Brand<string, \"SafeUrl\">;\n\n/**\n * Schema for a sanitized HTML-safe string — validates the value is a string; the\n * actual escaping is applied at runtime by the sanitization helpers.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const SanitizedStringSchema = S.String;\n/**\n * A string carrying the {@link SanitizedStringSchema} contract. The schema is\n * `S.String` alone, so decoding asserts only that the value is a string —\n * the actual escaping is applied separately by the sanitization helpers\n * (e.g. {@link escapeHtml}). The type name signals intent, not a proof of\n * escaping.\n */\nexport type SanitizedString = typeof SanitizedStringSchema.Type;\n\n/**\n * Schema for email address validation.\n *\n * Accepts a 2+ character alphabetic TLD or a Punycode/IDN `xn--…` TLD (e.g.\n * `.xn--p1ai` for `.рф`) so internationalized domains are not rejected. Kept in\n * sync with `@resq-systems/email-templates`'s `EmailAddress` brand.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const EmailSchema = S.String.check(\n\tS.isPattern(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.(?:[A-Za-z]{2,}|xn--[A-Za-z0-9-]+)$/),\n);\n/**\n * An email address that matches {@link EmailSchema}. Mint one by narrowing\n * through the {@link isValidEmail} type guard. The brand guarantees only\n * syntactic well-formedness (including IDN/Punycode TLDs) — not that the\n * mailbox exists or is deliverable.\n */\nexport type Email = Brand<string, \"Email\">;\n\n/**\n * Schema for phone number validation (US format).\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const PhoneNumberSchema = S.String.check(\n\tS.isPattern(/^(?:\\+?1[-.\\s]?)?\\(?\\d{3}\\)?[-.\\s]?\\d{3}[-.\\s]?\\d{4}$/),\n);\n/**\n * A US-format phone number matching {@link PhoneNumberSchema}. Mint one by\n * narrowing through the {@link isValidPhone} type guard. The brand asserts\n * the digit/separator shape only; it neither normalizes formatting nor\n * confirms the number is assigned.\n */\nexport type PhoneNumber = Brand<string, \"PhoneNumber\">;\n\n/**\n * Schema for SSN validation (US format).\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const SSNSchema = S.String.check(S.isPattern(/^\\d{3}[-\\s]?\\d{2}[-\\s]?\\d{4}$/));\n/**\n * A US Social Security Number matching {@link SSNSchema}. Mint one by\n * narrowing through the {@link isValidSSN} type guard. The brand asserts\n * the `NNN-NN-NNNN` shape only — it does not validate area/group ranges or\n * confirm the number was ever issued. Treat any value as sensitive PII.\n */\nexport type SSN = Brand<string, \"SSN\">;\n\n/**\n * Schema for credit card number validation.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const CreditCardSchema = S.String.check(\n\tS.isPattern(/^(?:\\d{4}[-\\s]?){3}\\d{4}$|^\\d{15,16}$/),\n);\n/**\n * A card number matching the {@link CreditCardSchema} pattern (13–16 digits\n * with optional group separators). No exported type guard mints this brand;\n * decode {@link CreditCardSchema} directly at the boundary. The pattern is a\n * shape check only — it performs **no** Luhn checksum and does not identify\n * the issuer. Treat any value as sensitive PII.\n */\nexport type CreditCard = Brand<string, \"CreditCard\">;\n\n/**\n * Schema for IPv4 address validation.\n */\nexport const IPv4Schema = S.String.check(S.isPattern(/^(?:\\d{1,3}\\.){3}\\d{1,3}$/));\n/**\n * A dotted-quad string matching {@link IPv4Schema}. No exported type guard\n * mints this brand; decode {@link IPv4Schema} directly. The pattern checks\n * four dot-separated groups of 1–3 digits only — it does **not** bound each\n * octet to `0–255`, so `999.0.0.1` still matches.\n */\nexport type IPv4 = Brand<string, \"IPv4\">;\n//#endregion\n\n//#region Sanitization\n\n/**\n * Escapes special HTML characters in a string to their corresponding HTML entities,\n * preventing direct injection of HTML and JavaScript when rendering untrusted content.\n *\n * @param text - The plain text to escape.\n * @returns The escaped string safe for HTML rendering.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * escapeHtml('<script>alert(\"xss\")</script>');\n * // \"<script>alert("xss")</script>\"\n * ```\n */\nexport const escapeHtml = (text: string): string => {\n\tif (!text || typeof text !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\treturn text\n\t\t.replaceAll(\"&\", \"&\")\n\t\t.replaceAll(\"<\", \"<\")\n\t\t.replaceAll(\">\", \">\")\n\t\t.replaceAll('\"', \""\")\n\t\t.replaceAll(\"'\", \"'\");\n};\n\n/**\n * Validates and sanitizes a user-supplied URL using Effect Schema.\n * Returns an Exit with the sanitized URL or an error.\n *\n * Pure and total — failure is encoded as a resolved {@link Exit.Exit} failure\n * (an `Exit.fail` carrying a {@link S.SchemaError}), never a thrown exception.\n *\n * @param url - The URL to be validated and sanitized.\n * @param allowedProtocols - Allowed URL protocols; a root-relative path (`/foo`) is\n * always accepted regardless of this list. An authority-opening reference — `//host`,\n * `///host`, or `/\\host` — is always **rejected**, because it names a different host\n * once resolved and would otherwise bypass this list entirely.\n * @returns An {@link Exit.Exit}: success carries the accepted URL string,\n * failure carries a {@link S.SchemaError}.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * const result = sanitizeUrlEffect('https://example.com');\n * // Exit.succeed('https://example.com')\n *\n * const invalid = sanitizeUrlEffect('javascript:alert(1)');\n * // Exit.fail(...)\n * ```\n */\nexport const sanitizeUrlEffect = (\n\turl: string,\n\tallowedProtocols: readonly UrlProtocol[] = [\"http:\", \"https:\", \"mailto:\"],\n): Exit.Exit<string, S.SchemaError> => {\n\tconst CustomSafeUrlSchema = S.String.check(\n\t\tS.makeFilter(\n\t\t\t(u: string) => {\n\t\t\t\tif (!u || u.trim() === \"\") return false;\n\t\t\t\tconst trimmed = u.trim();\n\t\t\t\t// Before the root-relative fast path: an authority-opening reference can\n\t\t\t\t// name a different host and bypasses `allowedProtocols` entirely.\n\t\t\t\tif (opensAuthority(trimmed)) return false;\n\t\t\t\tif (trimmed.startsWith(\"/\")) return true;\n\t\t\t\ttry {\n\t\t\t\t\tconst parsed = new URL(trimmed);\n\t\t\t\t\tif (!allowedProtocols.includes(parsed.protocol)) return false;\n\t\t\t\t\tif (parsed.hostname.includes(\"javascript:\") || parsed.hostname.includes(\"data:\")) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t} catch {\n\t\t\t\t\treturn (\n\t\t\t\t\t\t/^[a-zA-Z0-9/_.-]+$/.test(trimmed) &&\n\t\t\t\t\t\t!trimmed.includes(\"javascript:\") &&\n\t\t\t\t\t\t!trimmed.includes(\"data:\")\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t},\n\t\t\t{ message: \"Invalid or unsafe URL\" },\n\t\t),\n\t);\n\n\treturn S.decodeUnknownExit(CustomSafeUrlSchema)(url);\n};\n\n/**\n * Validates and sanitizes a user-supplied URL, ensuring it conforms to allowed protocols\n * and is not a vector for injection attacks like `javascript:` or `data:`.\n *\n * @param url - The URL to be validated and sanitized.\n * @param allowedProtocols - Array of allowed URL protocols.\n * @returns The sanitized URL if valid, or an empty string if unsafe.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * sanitizeUrl('https://example.com'); // 'https://example.com'\n * sanitizeUrl('javascript:alert(1)'); // ''\n * ```\n */\nexport const sanitizeUrl = (\n\turl: string,\n\tallowedProtocols: readonly UrlProtocol[] = [\"http:\", \"https:\", \"mailto:\"],\n): string => {\n\tconst result = sanitizeUrlEffect(url, allowedProtocols);\n\treturn Exit.isSuccess(result) ? result.value : \"\";\n};\n\nlet purifyInstance: typeof DOMPurify | null | undefined;\n\n/**\n * Add `rel=\"noopener noreferrer\"` to any link that opens a new browsing context.\n *\n * Without it the opened page receives a live `window.opener` handle and can navigate\n * the original tab to a phishing page — reverse tabnabbing, WSTG-CLNT-14. DOMPurify\n * does not add this by default, and unlike most of CLNT-14 the fix lives inside code\n * this package already owns, so a weakness no signature can detect becomes one that is\n * simply prevented.\n *\n * Modern browsers imply `noopener` for `target=\"_blank\"`; this covers older engines and\n * the named-target case (`target=\"win1\"`), which remains exploitable everywhere.\n *\n * Note that DOMPurify's default configuration strips `target` outright, so this hook is\n * a no-op unless the caller opts back in with `ADD_ATTR: [\"target\"]` or a custom\n * `ALLOWED_ATTR` — which is precisely the configuration that reintroduces the risk.\n *\n * @internal\n */\nconst addNoopenerToTargetedLinks = (node: Element): void => {\n\tif (typeof node.hasAttribute !== \"function\" || !node.hasAttribute(\"target\")) return;\n\n\tconst target = node.getAttribute(\"target\");\n\t// `_self`, `_parent`, and `_top` stay in the current context and grant no handle.\n\tif (target === null || target === \"_self\" || target === \"_parent\" || target === \"_top\") return;\n\n\tconst existing = (node.getAttribute(\"rel\") ?? \"\").split(/\\s+/).filter(Boolean);\n\tfor (const required of [\"noopener\", \"noreferrer\"]) {\n\t\tif (!existing.includes(required)) existing.push(required);\n\t}\n\tnode.setAttribute(\"rel\", existing.join(\" \"));\n};\n\n/** Register the reverse-tabnabbing hook on a DOMPurify instance. */\nconst withHooks = (purify: typeof DOMPurify): typeof DOMPurify => {\n\tpurify.addHook(\"afterSanitizeAttributes\", (node) => {\n\t\taddNoopenerToTargetedLinks(node as unknown as Element);\n\t});\n\treturn purify;\n};\n\nconst getPurify = (): typeof DOMPurify | null => {\n\tif (purifyInstance !== undefined) return purifyInstance;\n\n\tif (typeof window !== \"undefined\") {\n\t\tpurifyInstance = withHooks(DOMPurify);\n\t} else {\n\t\ttry {\n\t\t\t// Resolve `node:module` at runtime (server-side only) via\n\t\t\t// process.getBuiltinModule so browser bundlers never see a static\n\t\t\t// `node:module` import. Available on Node >=20.16 and Bun; absent in\n\t\t\t// browsers, where the `window` branch above is taken instead. jsdom is an\n\t\t\t// optional peer dependency — install it for server-side HTML sanitization.\n\t\t\tconst proc = (globalThis as { process?: { getBuiltinModule?: (m: string) => unknown } })\n\t\t\t\t.process;\n\t\t\tconst nodeModule = proc?.getBuiltinModule?.(\"module\") as\n\t\t\t\t| { createRequire(path: string | URL): (id: string) => unknown }\n\t\t\t\t| undefined;\n\t\t\tif (!nodeModule) {\n\t\t\t\tpurifyInstance = null;\n\t\t\t\treturn purifyInstance;\n\t\t\t}\n\t\t\tconst req = nodeModule.createRequire(import.meta.url);\n\t\t\tconst { JSDOM } = req(\"jsdom\") as {\n\t\t\t\tJSDOM: new (\n\t\t\t\t\thtml?: string,\n\t\t\t\t) => {\n\t\t\t\t\twindow: WindowLike;\n\t\t\t\t};\n\t\t\t};\n\t\t\tconst dom = new JSDOM(\"\");\n\t\t\tpurifyInstance = withHooks(DOMPurify(dom.window));\n\t\t} catch {\n\t\t\tpurifyInstance = null;\n\t\t}\n\t}\n\treturn purifyInstance;\n};\n\n/**\n * Sanitizes HTML to prevent XSS attacks.\n * Uses DOMPurify under the hood. If DOM is not available (e.g. server-side without JSDOM),\n * it falls back to escaping all HTML characters for safety.\n *\n * NOTE: Server-side HTML sanitization requires `jsdom` to be installed in the consuming application\n * environment; otherwise, it will fall back to escaping HTML characters.\n *\n * On the first server-side call this lazily resolves `node:module` and\n * `require`s `jsdom` to build a DOMPurify instance; the instance is cached at\n * module scope, so subsequent calls incur no further module loading. Returns\n * `\"\"` for non-string or empty input and never throws — any loader failure is\n * swallowed and downgraded to {@link escapeHtml}.\n *\n * @param html - The HTML string to sanitize.\n * @param options - Optional DOMPurify configuration.\n * @returns The sanitized HTML string, or the escaped string when no DOM is\n * available.\n */\nexport const sanitizeHtml = (html: string, options?: Config): string => {\n\tif (!html || typeof html !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\tconst purify = getPurify();\n\tif (purify) {\n\t\treturn purify.sanitize(html, options) as string;\n\t}\n\n\treturn escapeHtml(html);\n};\n\n/**\n * Validates user input using Effect Schema and returns an Exit.\n *\n * Strips HTML (unless `allowHtml`), collapses whitespace, and repeatedly\n * removes dangerous URI schemes and inline handlers until the string\n * stabilizes — the fixed-point loop defeats nested-payload bypasses such as\n * `javascrjavascript:ipt:`. Failure is a resolved {@link Exit.Exit} failure,\n * never a throw; the only failure path is a non-string reaching `S.String`.\n *\n * @param input - User input to validate and sanitize.\n * @param options - Validation options. Defaults: `maxLength` 500, `allowHtml`\n * false, `allowNewlines` false, `trimWhitespace` true.\n * @returns An {@link Exit.Exit}: success carries the sanitized string truncated\n * to `maxLength`; failure carries a {@link S.SchemaError}.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const validateUserInputEffect = (\n\tinput: string,\n\toptions: UserInputOptions = {},\n): Exit.Exit<string, S.SchemaError> => {\n\tconst {\n\t\tmaxLength = 500,\n\t\tallowHtml = false,\n\t\tallowNewlines = false,\n\t\ttrimWhitespace = true,\n\t} = options;\n\n\tconst parsed = S.decodeUnknownExit(S.String)(input);\n\tif (Exit.isFailure(parsed)) return parsed;\n\n\tlet result = parsed.value;\n\n\tif (trimWhitespace) {\n\t\tresult = result.trim();\n\t}\n\n\tif (!allowHtml) {\n\t\tlet prev: string;\n\t\tdo {\n\t\t\tprev = result;\n\t\t\tresult = result.replaceAll(/<[^>]*>/g, \"\");\n\t\t} while (result !== prev);\n\t} else {\n\t\tresult = sanitizeHtml(result);\n\t}\n\n\tif (!allowNewlines) {\n\t\tresult = result.replaceAll(/[\\r\\n]+/g, \" \");\n\t}\n\n\tresult = result.replaceAll(/\\s+/g, \" \");\n\n\t// Loop until stable to prevent bypass via nested patterns (e.g. \"javascrjavascript:ipt:\")\n\tlet prevScheme: string;\n\tdo {\n\t\tprevScheme = result;\n\t\tresult = result\n\t\t\t.replaceAll(/javascript:/gi, \"\")\n\t\t\t.replaceAll(/data:/gi, \"\")\n\t\t\t.replaceAll(/vbscript:/gi, \"\")\n\t\t\t.replaceAll(/on\\w+=/gi, \"\");\n\t} while (result !== prevScheme);\n\n\treturn Exit.succeed(result.slice(0, maxLength));\n};\n\n/**\n * Validates and sanitizes generic user input by trimming, removing HTML tags (unless allowed),\n * normalizing whitespace, and removing dangerous patterns to prevent XSS and basic injection flaws.\n *\n * @param input - User input to validate and sanitize.\n * @param maxLength - Maximum allowed input length. Excess will be truncated.\n * @param allowHtml - If true, HTML tags are preserved; otherwise, all tags are stripped.\n * @returns Sanitized input string with length at most `maxLength`.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * validateUserInput('<p>Hello!</p>', 50); // \"Hello!\"\n * validateUserInput('<script>alert(1)</script>test', 100); // \"test\"\n * ```\n */\nexport const validateUserInput = (input: string, maxLength = 500, allowHtml = false): string => {\n\tif (!input || typeof input !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\tconst result = validateUserInputEffect(input, { maxLength, allowHtml });\n\treturn Exit.isSuccess(result) ? result.value : \"\";\n};\n\n/** Own keys removed at every level, because assigning them reaches the prototype. */\nconst DANGEROUS_KEYS = [\"__proto__\", \"constructor\", \"prototype\"] as const;\n\n/**\n * Strip prototype-pollution keys (`__proto__`, `constructor`, `prototype`) from\n * every node of a parsed JSON value.\n *\n * **Mutates `val` in place** (deletes offending keys) and returns nothing;\n * callers pass a freshly `JSON.parse`d value they own. The walk is iterative and\n * unbounded in depth: every node is visited, however deeply nested.\n *\n * @internal\n */\nconst sanitizeObject = (val: unknown): void => {\n\t// Walked with an explicit stack rather than by recursion. The previous version\n\t// bounded itself at depth 50 to avoid overflowing the call stack and returned\n\t// early past that, so wrapping the payload in 51 layers carried `__proto__`\n\t// through untouched — and feeding the resulting leaf to an ordinary recursive\n\t// deep-merge set `Object.prototype.isAdmin`. The cap was never cycle protection:\n\t// both callers parse JSON first, and JSON cannot express a cycle. Dropping the\n\t// recursion removes the reason for the cap, so every node is now visited.\n\tconst stack: unknown[] = [val];\n\n\twhile (stack.length > 0) {\n\t\tconst current = stack.pop();\n\n\t\tif (typeof current !== \"object\" || current === null) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (Array.isArray(current)) {\n\t\t\tfor (const item of current) {\n\t\t\t\tstack.push(item);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst obj = current as Record<string, unknown>;\n\t\tfor (const key of DANGEROUS_KEYS) {\n\t\t\tif (key in obj) {\n\t\t\t\tdelete obj[key];\n\t\t\t}\n\t\t}\n\t\tfor (const key of Object.keys(obj)) {\n\t\t\tstack.push(obj[key]);\n\t\t}\n\t}\n};\n\n/**\n * Safely parses JSON with Effect Schema validation and prototype pollution protection.\n *\n * Never throws: malformed JSON, a non-string argument, and schema-validation\n * failure all resolve to {@link Option.none} rather than a thrown error, so the\n * failure channel is the `Option` itself. As a side effect the parsed value is\n * stripped of prototype-pollution keys in place before validation (the value is\n * freshly created by `JSON.parse`, so no caller state is mutated).\n *\n * @template A - The decoded value type the `schema` produces on success; the\n * returned `Option` carries this type.\n * @param jsonString - The JSON string to parse.\n * @param schema - Effect Schema to validate against; its decode must require no\n * services ({@link SyncSchema}) so parsing stays synchronous.\n * @returns {@link Option.some} with the parsed, validated value, or\n * {@link Option.none} on any parse or validation failure.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * const UserSchema = S.Struct({ name: S.String, age: S.Number });\n * const result = parseJsonWithSchema('{\"name\":\"John\",\"age\":30}', UserSchema);\n * // Option.some({ name: 'John', age: 30 })\n * ```\n */\nexport const parseJsonWithSchema = <A>(\n\tjsonString: string,\n\tschema: SyncSchema<A>,\n): Option.Option<A> => {\n\tif (!jsonString || typeof jsonString !== \"string\") {\n\t\treturn Option.none();\n\t}\n\n\ttry {\n\t\tconst sanitized = jsonString\n\t\t\t.replaceAll(/\\)\\s*\\{/g, \") {}\")\n\t\t\t.replaceAll(/\\]\\s*\\{/g, \"] {}\")\n\t\t\t.replaceAll(/\\}\\s*\\{/g, \"} {}\");\n\n\t\tconst parsed = JSON.parse(sanitized);\n\n\t\tsanitizeObject(parsed);\n\n\t\tconst result = S.decodeUnknownExit(schema)(parsed);\n\t\treturn Exit.isSuccess(result) ? Option.some(result.value as A) : Option.none();\n\t} catch {\n\t\treturn Option.none();\n\t}\n};\n\n/**\n * Sanitizes and safely parses a JSON string, removing suspicious syntax elements that could\n * potentially result in JSON polyglot exploits or prototype pollution.\n *\n * The result is returned as `unknown` — this function performs **no** schema\n * validation, so it cannot honestly promise any concrete shape for\n * attacker-controlled input. Narrow the result yourself, or prefer\n * {@link parseJsonWithSchema}, which validates against an Effect Schema and\n * returns a typed `Option`.\n *\n * @param jsonString - The JSON string to sanitize and parse.\n * @returns The parsed value (as `unknown`) if valid, or `null` if invalid.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * const obj = sanitizeJson('{\"foo\":\"bar\"}');\n * // obj: unknown — narrow before use, or use parseJsonWithSchema\n * ```\n */\nexport const sanitizeJson = (jsonString: string): unknown => {\n\tif (!jsonString || typeof jsonString !== \"string\") {\n\t\treturn null;\n\t}\n\n\ttry {\n\t\tconst sanitized = jsonString\n\t\t\t.replaceAll(/\\)\\s*\\{/g, \") {}\")\n\t\t\t.replaceAll(/\\]\\s*\\{/g, \"] {}\")\n\t\t\t.replaceAll(/\\}\\s*\\{/g, \"} {}\");\n\n\t\tconst parsed: unknown = JSON.parse(sanitized);\n\n\t\tsanitizeObject(parsed);\n\n\t\treturn parsed;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\n/**\n * Strips ANSI escape sequences from a string.\n *\n * Removes CSI sequences (colour, cursor movement, screen and line erasure, mode\n * switches), OSC sequences (window title and similar), two-character escapes, and\n * any bare ESC left over. This is the control named by `LOG-ANSI-ESCAPE-001`: a\n * terminal-backed log sink treats these as commands, so an attacker who lands them\n * in a log can scroll earlier entries away or overwrite them (CWE-117).\n *\n * Removal is destructive by design — the sequence goes, and any text it carried\n * goes with it. Where the record matters more than the rendering, escape the\n * characters instead of deleting them.\n *\n * @param text - The text potentially containing ANSI codes.\n * @returns The text with ANSI codes removed.\n *\n * @example\n * ```typescript\n * stripAnsi('\\x1b[31mRed text\\x1b[0m'); // 'Red text'\n * ```\n */\nexport const stripAnsi = (text: string): string => {\n\tif (!text || typeof text !== \"string\") {\n\t\treturn \"\";\n\t}\n\t// Every escape sequence, not just the colour ones. The previous pattern was\n\t// `/\\\\x1b\\\\[[0-9;]*m/` — SGR only — so `ESC[2J` (erase display), `ESC[?1049h`\n\t// (alternate screen buffer), `ESC[5A` (cursor up, which overwrites the audit\n\t// lines already written) and `ESC]0;...` (set window title) all survived the\n\t// function that `LOG-ANSI-ESCAPE-001` names as its control. Colour was the one\n\t// case handled, and the only one that is merely cosmetic.\n\t//\n\t// Alternatives are ordered CSI, OSC, then the general ECMA-48 form (ESC,\n\t// intermediates 0x20-0x2F, final 0x30-0x7E), which covers Fs escapes such as\n\t// ESC c. Every quantifier is bounded and none is nested, so nothing backtracks.\n\t// A trailing `?` also removes a bare ESC that begins no valid sequence.\n\treturn text.replaceAll(\n\t\t// biome-ignore lint/suspicious/noControlCharactersInRegex: stripping ANSI requires matching control characters\n\t\t/\\u001b(?:\\[[0-?]{0,32}[ -/]{0,8}[@-~]|\\][^\\u0007\\u001b]{0,512}(?:\\u0007|\\u001b\\\\)|[ -/]{0,8}[0-~])?/g,\n\t\t\"\",\n\t);\n};\n\n//#endregion\n\n//#region PII Redaction\n\n/**\n * PII pattern catalog: each entry pairs a global-match regex with the marker that\n * replaces every hit during redaction.\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n */\nconst PII_PATTERNS = {\n\tssn: { pattern: /\\b\\d{3}[-\\s]?\\d{2}[-\\s]?\\d{4}\\b/g, marker: \"[SSN]\" },\n\tcreditCard: { pattern: /\\b(?:\\d{4}[-\\s]?){3}\\d{4}\\b/g, marker: \"[CREDIT_CARD]\" },\n\tcreditCardAlt: { pattern: /\\b\\d{15,16}\\b/g, marker: \"[CREDIT_CARD]\" },\n\t// TLD alternation mirrors `EmailSchema` so IDN/Punycode addresses\n\t// (e.g. `user@example.xn--p1ai`) are redacted, not leaked. The `xn--` branch\n\t// is tried first: unlike the anchored (`$`) validators, this pattern ends in\n\t// `\\b`, so `[A-Za-z]{2,}` would otherwise match just `xn` and stop at the\n\t// hyphen, leaving `--p1ai` unredacted. (Also drops the stray `|` from the\n\t// former `[A-Z|a-z]` class.)\n\temail: {\n\t\tpattern: /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.(?:xn--[A-Za-z0-9-]+|[A-Za-z]{2,})\\b/g,\n\t\tmarker: \"[EMAIL]\",\n\t},\n\tphone: { pattern: /\\b(?:\\+?1[-.\\s]?)?\\(?\\d{3}\\)?[-.\\s]?\\d{3}[-.\\s]?\\d{4}\\b/g, marker: \"[PHONE]\" },\n\tipv4: { pattern: /\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b/g, marker: \"[IP_ADDRESS]\" },\n\tipv6: { pattern: /\\b(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\b/g, marker: \"[IP_ADDRESS]\" },\n\tdate: {\n\t\tpattern: /\\b(?:\\d{1,2}[/-]\\d{1,2}[/-]\\d{2,4}|\\d{4}[/-]\\d{1,2}[/-]\\d{1,2})\\b/g,\n\t\tmarker: \"[DATE]\",\n\t},\n} as const;\n\n/**\n * Redacts PII from text using Effect Schema validated options.\n *\n * Total — never throws. Failure to decode `options` is returned as a resolved\n * {@link Exit.Exit} failure carrying the {@link S.SchemaError}. By default\n * dates are **not** redacted (`redactDates` defaults to `false`); every other\n * category defaults to `true`.\n *\n * @param text - The text to redact PII from.\n * @param options - Configuration options for redaction.\n * @returns An {@link Exit.Exit}: success carries the redacted text, failure\n * carries a {@link S.SchemaError} from invalid `options`.\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n */\nexport const redactPIIEffect = (\n\ttext: string,\n\toptions: PIIRedactionOptions = {},\n): Exit.Exit<string, S.SchemaError> => {\n\tconst parsed = S.decodeUnknownExit(PIIRedactionOptionsSchema)(options);\n\tif (Exit.isFailure(parsed)) return Exit.failCause(parsed.cause);\n\n\tconst {\n\t\tredactEmails = true,\n\t\tredactPhones = true,\n\t\tredactSSN = true,\n\t\tredactCreditCards = true,\n\t\tredactIPs = true,\n\t\tredactDates = false,\n\t} = parsed.value;\n\n\tlet result = text;\n\n\tif (redactSSN) {\n\t\tresult = result.replaceAll(PII_PATTERNS.ssn.pattern, PII_PATTERNS.ssn.marker);\n\t}\n\n\tif (redactCreditCards) {\n\t\tresult = result.replaceAll(PII_PATTERNS.creditCard.pattern, PII_PATTERNS.creditCard.marker);\n\t\tresult = result.replaceAll(\n\t\t\tPII_PATTERNS.creditCardAlt.pattern,\n\t\t\tPII_PATTERNS.creditCardAlt.marker,\n\t\t);\n\t}\n\n\tif (redactEmails) {\n\t\tresult = result.replaceAll(PII_PATTERNS.email.pattern, PII_PATTERNS.email.marker);\n\t}\n\n\tif (redactPhones) {\n\t\tresult = result.replaceAll(PII_PATTERNS.phone.pattern, PII_PATTERNS.phone.marker);\n\t}\n\n\tif (redactIPs) {\n\t\tresult = result.replaceAll(PII_PATTERNS.ipv4.pattern, PII_PATTERNS.ipv4.marker);\n\t\tresult = result.replaceAll(PII_PATTERNS.ipv6.pattern, PII_PATTERNS.ipv6.marker);\n\t}\n\n\tif (redactDates) {\n\t\tresult = result.replaceAll(PII_PATTERNS.date.pattern, PII_PATTERNS.date.marker);\n\t}\n\n\treturn Exit.succeed(result);\n};\n\n/**\n * Redacts common PII patterns in a string for safe logging.\n * Detects and masks SSNs, credit cards, emails, phone numbers, etc.\n *\n * @param text - The text to redact PII from. Non-string input yields `\"\"`.\n * @param options - Configuration options for redaction, plus optional\n * `customPatterns` applied after the built-ins. Each `pattern` **must** be a\n * global (`/g`) RegExp.\n * @returns The text with PII patterns replaced with redaction markers, or `\"\"`\n * for non-string input. If the built-in options fail schema validation the\n * original `text` is returned unredacted rather than throwing.\n * @throws {TypeError} If any `customPatterns` entry uses a non-global RegExp —\n * `String.prototype.replaceAll` rejects non-global patterns.\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n *\n * @example\n * ```typescript\n * redactPII('Contact john@example.com or call 555-123-4567');\n * // 'Contact [EMAIL] or call [PHONE]'\n *\n * redactPII('SSN: 123-45-6789');\n * // 'SSN: [SSN]'\n * ```\n */\nexport const redactPII = (\n\ttext: string,\n\toptions: PIIRedactionOptions & {\n\t\tcustomPatterns?: Array<{ pattern: RegExp; replacement: string }>;\n\t} = {},\n): string => {\n\tif (!text || typeof text !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\tconst {\n\t\tredactEmails = true,\n\t\tredactPhones = true,\n\t\tredactSSN = true,\n\t\tredactCreditCards = true,\n\t\tredactIPs = true,\n\t\tredactDates = false,\n\t\tcustomPatterns = [],\n\t} = options;\n\n\tconst result = redactPIIEffect(text, {\n\t\tredactEmails,\n\t\tredactPhones,\n\t\tredactSSN,\n\t\tredactCreditCards,\n\t\tredactIPs,\n\t\tredactDates,\n\t});\n\n\tlet output = Exit.isSuccess(result) ? result.value : text;\n\n\tfor (const { pattern, replacement } of customPatterns) {\n\t\toutput = output.replaceAll(pattern, replacement);\n\t}\n\n\treturn output;\n};\n\n/**\n * Creates a safe string representation of an object for logging,\n * automatically redacting sensitive fields.\n *\n * Never throws: any serialization failure — a circular reference, a `BigInt`\n * value, a throwing `toJSON` — is caught and returned as the sentinel string\n * `\"[Unable to stringify object]\"`. Key matching is case-insensitive and\n * compares the full key name (not substrings), so `apiKey` matches only the\n * literal `\"apiKey\"`, not `\"apiKeyId\"`.\n *\n * @param obj - The object to stringify.\n * @param sensitiveKeys - Key names to redact, compared case-insensitively.\n * @param indent - JSON indentation (default: 2).\n * @returns A JSON string with sensitive values redacted, or the sentinel\n * `\"[Unable to stringify object]\"` when serialization fails.\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n *\n * @example\n * ```typescript\n * safeStringify({ user: 'john', password: 'secret123' }, ['password']);\n * // '{\\n \"user\": \"john\",\\n \"password\": \"[REDACTED]\"\\n}'\n * ```\n */\nexport const safeStringify = (\n\tobj: unknown,\n\tsensitiveKeys: string[] = [\n\t\t\"password\",\n\t\t\"token\",\n\t\t\"apiKey\",\n\t\t\"secret\",\n\t\t\"authorization\",\n\t\t\"cookie\",\n\t\t\"ssn\",\n\t\t\"creditCard\",\n\t],\n\tindent = 2,\n): string => {\n\tconst sensitiveKeysLower = new Set(sensitiveKeys.map((k) => k.toLowerCase()));\n\n\tconst replacer = (_key: string, value: unknown): unknown => {\n\t\tif (_key && sensitiveKeysLower.has(_key.toLowerCase())) {\n\t\t\treturn \"[REDACTED]\";\n\t\t}\n\t\treturn value;\n\t};\n\n\ttry {\n\t\treturn JSON.stringify(obj, replacer, indent);\n\t} catch {\n\t\treturn \"[Unable to stringify object]\";\n\t}\n};\n\n//#endregion\n\n//#region Validation Helpers\n\n/**\n * Validates if a string is a valid email address using Effect Schema.\n *\n * Narrows the input to {@link Email} on success, so validated call sites\n * carry the brand into downstream code.\n *\n * @param email - The string to validate.\n * @returns true if valid email, false otherwise.\n */\nexport const isValidEmail = (email: string): email is Email => {\n\treturn Exit.isSuccess(S.decodeUnknownExit(EmailSchema)(email));\n};\n\n/**\n * Validates if a string is a valid phone number using Effect Schema.\n *\n * Narrows the input to {@link PhoneNumber} on success.\n *\n * @param phone - The string to validate.\n * @returns true if valid phone number, false otherwise.\n */\nexport const isValidPhone = (phone: string): phone is PhoneNumber => {\n\treturn Exit.isSuccess(S.decodeUnknownExit(PhoneNumberSchema)(phone));\n};\n\n/**\n * Validates if a string is a valid SSN using Effect Schema.\n *\n * Narrows the input to {@link SSN} on success.\n *\n * @param ssn - The string to validate.\n * @returns true if valid SSN, false otherwise.\n */\nexport const isValidSSN = (ssn: string): ssn is SSN => {\n\treturn Exit.isSuccess(S.decodeUnknownExit(SSNSchema)(ssn));\n};\n\n/**\n * Validates if a string is a safe URL using Effect Schema.\n *\n * Narrows the input to {@link SafeUrl} on success.\n *\n * @param url - The string to validate.\n * @returns true if valid and safe URL, false otherwise.\n */\nexport const isValidUrl = (url: string): url is SafeUrl => {\n\treturn Exit.isSuccess(S.decodeUnknownExit(SafeUrlSchema)(url));\n};\n//#endregion\n"],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//#region src/threats/capec.generated.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Copyright 2026 ResQ Systems, Inc.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* @fileoverview GENERATED FILE — do not edit by hand.
|
|
19
|
+
*
|
|
20
|
+
* MITRE CAPEC attack patterns whose Related Weaknesses intersect a CWE this catalog
|
|
21
|
+
* uses. Derived from CAPEC view 1000 (Mechanisms of Attack), filtered to Standard and
|
|
22
|
+
* Detailed abstractions because Meta patterns are too abstract to help anyone triaging
|
|
23
|
+
* a finding.
|
|
24
|
+
*
|
|
25
|
+
* **Attribution, not conformance.** A CAPEC id tells a reader which family of attack a
|
|
26
|
+
* finding belongs to and where to read more. It does not assert that the rule detects
|
|
27
|
+
* every technique in that pattern, and nothing here changes a score or a verdict.
|
|
28
|
+
*
|
|
29
|
+
* Regenerate with `bun scripts/generate-capec.ts <capec-1000.csv>`.
|
|
30
|
+
*
|
|
31
|
+
* @module @resq-systems/security/threats/capec
|
|
32
|
+
*/
|
|
33
|
+
/** One CAPEC attack pattern, reduced to what a consumer triaging a finding needs. */
|
|
34
|
+
interface AttackPattern {
|
|
35
|
+
/** CAPEC identifier. */
|
|
36
|
+
readonly capec: number;
|
|
37
|
+
/** Pattern name, as published by MITRE. */
|
|
38
|
+
readonly name: string;
|
|
39
|
+
/** MITRE abstraction level: "Standard" or "Detailed". */
|
|
40
|
+
readonly abstraction: string;
|
|
41
|
+
/** MITRE's typical severity for the pattern. */
|
|
42
|
+
readonly severity: string;
|
|
43
|
+
/** Catalog CWEs this pattern relates to. */
|
|
44
|
+
readonly cwes: readonly number[];
|
|
45
|
+
}
|
|
46
|
+
/** Every relevant pattern, ordered by CAPEC id. */
|
|
47
|
+
declare const ATTACK_PATTERNS: readonly AttackPattern[];
|
|
48
|
+
/**
|
|
49
|
+
* Attack patterns related to a weakness.
|
|
50
|
+
*
|
|
51
|
+
* @param cwe - CWE identifier, typically a rule's `cwe` field.
|
|
52
|
+
* @returns Related patterns, or an empty array when none is mapped. Empty means MITRE
|
|
53
|
+
* publishes no Standard or Detailed pattern for that weakness — not that the weakness
|
|
54
|
+
* is unimportant.
|
|
55
|
+
*/
|
|
56
|
+
declare function attackPatternsForCwe(cwe: number | undefined): readonly AttackPattern[];
|
|
57
|
+
//#endregion
|
|
58
|
+
export { ATTACK_PATTERNS, AttackPattern, attackPatternsForCwe };
|
|
59
|
+
//# sourceMappingURL=capec.generated.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capec.generated.d.mts","names":[],"sources":["../../src/threats/capec.generated.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAkCiB;;WAEP;;WAEA;;WAEA;;WAEA;;WAEA;;;cAIG,0BAA0B;;;;;;;;;iBAknBvB,qBAAqB,mCAAmC"}
|