@blamejs/core 0.7.4 → 0.7.19
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/CHANGELOG.md +30 -0
- package/README.md +1 -0
- package/index.js +27 -1
- package/lib/api-key.js +2 -5
- package/lib/auth/jwt-external.js +365 -0
- package/lib/auth/jwt.js +27 -1
- package/lib/auth/password.js +34 -0
- package/lib/codepoint-class.js +196 -0
- package/lib/csv.js +25 -36
- package/lib/db-declare-view.js +3 -4
- package/lib/file-upload.js +213 -10
- package/lib/framework-error.js +78 -0
- package/lib/gate-contract.js +971 -0
- package/lib/guard-all.js +405 -0
- package/lib/guard-archive.js +739 -0
- package/lib/guard-csv.js +816 -0
- package/lib/guard-email.js +744 -0
- package/lib/guard-filename.js +724 -0
- package/lib/guard-html.js +976 -0
- package/lib/guard-json.js +729 -0
- package/lib/guard-markdown.js +586 -0
- package/lib/guard-svg.js +976 -0
- package/lib/guard-xml.js +405 -0
- package/lib/guard-yaml.js +529 -0
- package/lib/mail-dkim.js +13 -6
- package/lib/mail.js +19 -0
- package/lib/middleware/bearer-auth.js +152 -0
- package/lib/middleware/body-parser.js +79 -0
- package/lib/middleware/index.js +3 -0
- package/lib/numeric-bounds.js +20 -0
- package/lib/session.js +61 -4
- package/lib/static.js +184 -4
- package/lib/validate-opts.js +21 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* guard-email — Email content-safety primitive (b.guardEmail).
|
|
4
|
+
*
|
|
5
|
+
* Threat catalog grounded in current research:
|
|
6
|
+
* - SMTP smuggling (CVE-2023-51764 Postfix; CVE-2023-51765 Sendmail;
|
|
7
|
+
* CVE-2023-51766 Exim; CVE-2026-32178 .NET System.Net.Mail) —
|
|
8
|
+
* embedded SMTP verbs after bare-CR / bare-LF / dot-stuffing
|
|
9
|
+
* manipulation lets an attacker inject a second message in the
|
|
10
|
+
* same SMTP session with a forged envelope.
|
|
11
|
+
* - CRLF header injection — `\r\n` inside any header field value
|
|
12
|
+
* splits the header section and lets the attacker forge `From:`,
|
|
13
|
+
* `Bcc:`, or smuggle a body.
|
|
14
|
+
* - IDN homograph spoofing — mixed-script Unicode in the domain part
|
|
15
|
+
* (Cyrillic а / Greek α / Armenian / Cherokee letters that look
|
|
16
|
+
* like Latin lowercase). Most filters miss confusables.
|
|
17
|
+
* - Display-name spoofing — `"support@apple.com" <attacker@evil>` —
|
|
18
|
+
* the rendered name impersonates a trusted address while the
|
|
19
|
+
* envelope routes elsewhere.
|
|
20
|
+
* - Bare IP literal addresses — `user@[1.2.3.4]` / `user@[IPv6:...]`.
|
|
21
|
+
* - Comment syntax in addresses — `(comment)` per RFC 5322 — most
|
|
22
|
+
* receivers reject; senders that accept it are a smuggling vector.
|
|
23
|
+
* - RFC 5321 / 5322 length caps — local-part 64; domain 255; total
|
|
24
|
+
* address 320; per-line 998.
|
|
25
|
+
* - Multiple @ characters / multiple addresses in a single field.
|
|
26
|
+
* - Bidi / null / control / zero-width chars in addresses + headers.
|
|
27
|
+
* - BOM injection at the start of a header.
|
|
28
|
+
*
|
|
29
|
+
* var rv = b.guardEmail.validateAddress(addr, { profile: "strict" });
|
|
30
|
+
* var rv = b.guardEmail.validateMessage(rfc822, { profile: "strict" });
|
|
31
|
+
* var safe = b.guardEmail.sanitize(input, { profile: "balanced" });
|
|
32
|
+
* var g = b.guardEmail.gate({ profile: "strict" });
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
var codepointClass = require("./codepoint-class");
|
|
36
|
+
var lazyRequire = require("./lazy-require");
|
|
37
|
+
var gateContract = require("./gate-contract");
|
|
38
|
+
var C = require("./constants");
|
|
39
|
+
var numericBounds = require("./numeric-bounds");
|
|
40
|
+
var { GuardEmailError } = require("./framework-error");
|
|
41
|
+
|
|
42
|
+
var observability = lazyRequire(function () { return require("./observability"); });
|
|
43
|
+
void observability;
|
|
44
|
+
|
|
45
|
+
var _err = GuardEmailError.factory;
|
|
46
|
+
|
|
47
|
+
// ---- RFC 5321 / 5322 limits ----
|
|
48
|
+
|
|
49
|
+
var LIMIT_LOCAL_PART = 64; // allow:raw-byte-literal — RFC 5321 §4.5.3.1.1
|
|
50
|
+
var LIMIT_DOMAIN = 255; // allow:raw-byte-literal — RFC 5321 §4.5.3.1.2
|
|
51
|
+
var LIMIT_ADDRESS = 320; // allow:raw-byte-literal — RFC 5321 sum (64 + 1 + 255)
|
|
52
|
+
var LIMIT_LINE = 998; // allow:raw-byte-literal — RFC 5322 §2.1.1 maximum line length
|
|
53
|
+
|
|
54
|
+
// ---- Source-level threat detectors ----
|
|
55
|
+
|
|
56
|
+
// Bare CR or bare LF outside CRLF pair — SMTP smuggling vector class.
|
|
57
|
+
// Detection MUST scan the full message bytes; a bare LF in the body
|
|
58
|
+
// can let an attacker smuggle a follow-on `\r\n.\r\n` sequence past
|
|
59
|
+
// the upstream MTA's end-of-data check.
|
|
60
|
+
function _scanBareLineEndings(input) {
|
|
61
|
+
var bareCr = false;
|
|
62
|
+
var bareLf = false;
|
|
63
|
+
for (var i = 0; i < input.length; i += 1) {
|
|
64
|
+
var c = input.charCodeAt(i);
|
|
65
|
+
if (c === 13) { // allow:raw-byte-literal — CR
|
|
66
|
+
var next = i + 1 < input.length ? input.charCodeAt(i + 1) : -1;
|
|
67
|
+
if (next !== 10) bareCr = true; // allow:raw-byte-literal — LF
|
|
68
|
+
} else if (c === 10) { // allow:raw-byte-literal — LF
|
|
69
|
+
var prev = i > 0 ? input.charCodeAt(i - 1) : -1;
|
|
70
|
+
if (prev !== 13) bareLf = true; // allow:raw-byte-literal — CR
|
|
71
|
+
}
|
|
72
|
+
if (bareCr && bareLf) break;
|
|
73
|
+
}
|
|
74
|
+
return { bareCr: bareCr, bareLf: bareLf };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Smuggled SMTP verbs after a bare line ending — the canonical
|
|
78
|
+
// SEC Consult / smtpsmuggling.com pattern.
|
|
79
|
+
var SMUGGLED_VERB_RE = /(?:\r(?!\n)|(?<!\r)\n)\.?\s*(?:MAIL FROM|RCPT TO|DATA|EHLO|HELO|RSET|QUIT)\b/i;
|
|
80
|
+
|
|
81
|
+
// CRLF in any single-line header value — header injection.
|
|
82
|
+
function _hasCrlfInHeaderValue(value) {
|
|
83
|
+
for (var i = 0; i < value.length; i += 1) {
|
|
84
|
+
var c = value.charCodeAt(i);
|
|
85
|
+
if (c === 13 || c === 10) return true; // allow:raw-byte-literal — CR or LF in header value
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Strict address regex — RFC 5321 plus a small, conservative subset of
|
|
91
|
+
// 5322 atext. Domains MUST be DNS-shaped (label syntax).
|
|
92
|
+
//
|
|
93
|
+
// allow:dynamic-regex — built once at module load from the static
|
|
94
|
+
// atext class; no runtime input.
|
|
95
|
+
// Local-part regex is permissive on length so the explicit cap check
|
|
96
|
+
// can produce a useful local-part-cap issue (instead of failing the
|
|
97
|
+
// regex first and surfacing address-syntax). RFC 5321 cap is enforced
|
|
98
|
+
// downstream via opts.maxLocalPartBytes.
|
|
99
|
+
var _LOCAL = "[A-Za-z0-9!#$%&'*+/=?^_`{|}~.-]+";
|
|
100
|
+
var _LABEL = "[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?";
|
|
101
|
+
var _DOMAIN = "(?:" + _LABEL + "(?:\\." + _LABEL + ")+)";
|
|
102
|
+
// allow:dynamic-regex — built once at module load from the static
|
|
103
|
+
// _LOCAL + _DOMAIN literal strings; no runtime input.
|
|
104
|
+
var ADDRESS_RE = new RegExp("^(" + _LOCAL + ")@(" + _DOMAIN + ")$");
|
|
105
|
+
|
|
106
|
+
// IP-literal (square-bracketed IPv4 / IPv6). Allowed-or-refused via opt.
|
|
107
|
+
var IP_LITERAL_RE = /^[^@]+@\[[^\]]+\]$/;
|
|
108
|
+
|
|
109
|
+
// Comment syntax in address — `(comment)` per RFC 5322. Most receivers
|
|
110
|
+
// reject; we flag.
|
|
111
|
+
var ADDRESS_COMMENT_RE = /[()]/;
|
|
112
|
+
|
|
113
|
+
// Punycode prefix on a domain label.
|
|
114
|
+
var PUNYCODE_LABEL_RE = /(?:^|\.)xn--/i;
|
|
115
|
+
|
|
116
|
+
// Mixed-script detection — flag any domain with characters from more
|
|
117
|
+
// than one of: Latin / Cyrillic / Greek / Armenian / Cherokee. The
|
|
118
|
+
// catalog is conservative; operators with legitimate non-Latin domains
|
|
119
|
+
// register the script via `allowedScripts: ["latin", "cyrillic"]`.
|
|
120
|
+
//
|
|
121
|
+
// Codepoints from Unicode 15.1 official script ranges (via codepoint-
|
|
122
|
+
// class.js conventions — keep numeric, no literal characters).
|
|
123
|
+
var SCRIPT_RANGES = {
|
|
124
|
+
latin: [[0x0041, 0x005a], [0x0061, 0x007a],
|
|
125
|
+
[0x00c0, 0x024f], [0x1e00, 0x1eff]], // allow:raw-byte-literal — Unicode script ranges
|
|
126
|
+
cyrillic: [[0x0400, 0x04ff], [0x0500, 0x052f]], // allow:raw-byte-literal — Unicode Cyrillic + Cyrillic Supplement
|
|
127
|
+
greek: [[0x0370, 0x03ff], [0x1f00, 0x1fff]], // allow:raw-byte-literal — Unicode Greek + Greek Extended
|
|
128
|
+
armenian: [[0x0530, 0x058f]], // allow:raw-byte-literal — Unicode Armenian
|
|
129
|
+
cherokee: [[0x13a0, 0x13ff], [0xab70, 0xabbf]], // allow:raw-byte-literal — Unicode Cherokee + Cherokee Supplement
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
function _scriptFor(cp) {
|
|
133
|
+
var keys = Object.keys(SCRIPT_RANGES);
|
|
134
|
+
for (var i = 0; i < keys.length; i += 1) {
|
|
135
|
+
var ranges = SCRIPT_RANGES[keys[i]];
|
|
136
|
+
for (var j = 0; j < ranges.length; j += 1) {
|
|
137
|
+
if (cp >= ranges[j][0] && cp <= ranges[j][1]) return keys[i];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return null; // unknown script — not a confusable
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function _detectMixedScripts(domain, allowedScripts) {
|
|
144
|
+
var seen = {};
|
|
145
|
+
for (var i = 0; i < domain.length; i += 1) {
|
|
146
|
+
var script = _scriptFor(domain.charCodeAt(i));
|
|
147
|
+
if (script === null) continue;
|
|
148
|
+
seen[script] = true;
|
|
149
|
+
}
|
|
150
|
+
var scripts = Object.keys(seen);
|
|
151
|
+
if (scripts.length <= 1) return null;
|
|
152
|
+
var disallowed = [];
|
|
153
|
+
for (var k = 0; k < scripts.length; k += 1) {
|
|
154
|
+
if (!allowedScripts || allowedScripts.indexOf(scripts[k]) === -1) {
|
|
155
|
+
disallowed.push(scripts[k]);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return scripts.length > 1 && disallowed.length > 0 ? scripts : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Header fields the spec treats as single-line (no folded CR/LF).
|
|
162
|
+
var SINGLE_LINE_HEADERS = ["from", "to", "cc", "bcc", "reply-to", "sender",
|
|
163
|
+
"subject", "message-id", "in-reply-to", "references",
|
|
164
|
+
"date", "return-path"];
|
|
165
|
+
|
|
166
|
+
// Display-name + envelope split.
|
|
167
|
+
var DISPLAY_PHRASE_ANGLE_RE = /^\s*(.*?)\s*<\s*([^>]+)\s*>\s*$/;
|
|
168
|
+
|
|
169
|
+
function _parseAddressLine(line) {
|
|
170
|
+
// Returns { display, envelope } — display may be empty.
|
|
171
|
+
var m = line.match(DISPLAY_PHRASE_ANGLE_RE);
|
|
172
|
+
if (m) return { display: m[1].replace(/^"|"$/g, ""), envelope: m[2] };
|
|
173
|
+
return { display: "", envelope: line.trim() };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---- Profile presets ----
|
|
177
|
+
|
|
178
|
+
var PROFILES = Object.freeze({
|
|
179
|
+
"strict": {
|
|
180
|
+
crlfHeaderInjectionPolicy: "reject",
|
|
181
|
+
smtpSmugglingPolicy: "reject",
|
|
182
|
+
bareCrPolicy: "reject",
|
|
183
|
+
bareLfPolicy: "reject",
|
|
184
|
+
multiAtPolicy: "reject",
|
|
185
|
+
ipLiteralPolicy: "reject",
|
|
186
|
+
addressCommentPolicy: "reject",
|
|
187
|
+
punycodePolicy: "reject", // strict refuses Punycode — operators with legit IDN traffic opt up
|
|
188
|
+
mixedScriptPolicy: "reject",
|
|
189
|
+
displayNameSpoofPolicy: "reject",
|
|
190
|
+
bomPolicy: "reject",
|
|
191
|
+
bidiPolicy: "reject",
|
|
192
|
+
controlPolicy: "reject",
|
|
193
|
+
nullBytePolicy: "reject",
|
|
194
|
+
zeroWidthPolicy: "reject",
|
|
195
|
+
allowedScripts: ["latin"],
|
|
196
|
+
maxLocalPartBytes: LIMIT_LOCAL_PART,
|
|
197
|
+
maxDomainBytes: LIMIT_DOMAIN,
|
|
198
|
+
maxAddressBytes: LIMIT_ADDRESS,
|
|
199
|
+
maxHeaderLineBytes: LIMIT_LINE,
|
|
200
|
+
maxHeaders: 128, // allow:raw-byte-literal — header count cap
|
|
201
|
+
maxBytes: C.BYTES.mib(8),
|
|
202
|
+
},
|
|
203
|
+
"balanced": {
|
|
204
|
+
crlfHeaderInjectionPolicy: "reject",
|
|
205
|
+
smtpSmugglingPolicy: "reject",
|
|
206
|
+
bareCrPolicy: "audit",
|
|
207
|
+
bareLfPolicy: "audit",
|
|
208
|
+
multiAtPolicy: "reject",
|
|
209
|
+
ipLiteralPolicy: "audit",
|
|
210
|
+
addressCommentPolicy: "audit",
|
|
211
|
+
punycodePolicy: "audit",
|
|
212
|
+
mixedScriptPolicy: "reject",
|
|
213
|
+
displayNameSpoofPolicy: "audit",
|
|
214
|
+
bomPolicy: "strip",
|
|
215
|
+
bidiPolicy: "strip",
|
|
216
|
+
controlPolicy: "strip",
|
|
217
|
+
nullBytePolicy: "strip",
|
|
218
|
+
zeroWidthPolicy: "strip",
|
|
219
|
+
allowedScripts: ["latin", "cyrillic", "greek"],
|
|
220
|
+
maxLocalPartBytes: LIMIT_LOCAL_PART,
|
|
221
|
+
maxDomainBytes: LIMIT_DOMAIN,
|
|
222
|
+
maxAddressBytes: LIMIT_ADDRESS,
|
|
223
|
+
maxHeaderLineBytes: LIMIT_LINE,
|
|
224
|
+
maxHeaders: 512, // allow:raw-byte-literal — header count cap
|
|
225
|
+
maxBytes: C.BYTES.mib(32),
|
|
226
|
+
},
|
|
227
|
+
"permissive": {
|
|
228
|
+
crlfHeaderInjectionPolicy: "reject", // header injection refused at every profile — universal smuggling vector
|
|
229
|
+
smtpSmugglingPolicy: "reject", // SMTP smuggling refused at every profile — universal vector
|
|
230
|
+
bareCrPolicy: "audit",
|
|
231
|
+
bareLfPolicy: "audit",
|
|
232
|
+
multiAtPolicy: "reject", // multi-@ refused at every profile — RFC 5322 violates
|
|
233
|
+
ipLiteralPolicy: "allow",
|
|
234
|
+
addressCommentPolicy: "audit",
|
|
235
|
+
punycodePolicy: "audit",
|
|
236
|
+
mixedScriptPolicy: "audit",
|
|
237
|
+
displayNameSpoofPolicy: "audit",
|
|
238
|
+
bomPolicy: "audit",
|
|
239
|
+
bidiPolicy: "audit",
|
|
240
|
+
controlPolicy: "strip",
|
|
241
|
+
nullBytePolicy: "reject", // null bytes refused at every profile
|
|
242
|
+
zeroWidthPolicy: "audit",
|
|
243
|
+
allowedScripts: null, // permissive — allow every Unicode script
|
|
244
|
+
maxLocalPartBytes: LIMIT_LOCAL_PART,
|
|
245
|
+
maxDomainBytes: LIMIT_DOMAIN,
|
|
246
|
+
maxAddressBytes: LIMIT_ADDRESS,
|
|
247
|
+
maxHeaderLineBytes: LIMIT_LINE,
|
|
248
|
+
maxHeaders: 2048, // allow:raw-byte-literal — header count cap
|
|
249
|
+
maxBytes: C.BYTES.mib(128),
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
var DEFAULTS = Object.freeze(Object.assign({}, PROFILES["strict"], {
|
|
254
|
+
mode: "enforce",
|
|
255
|
+
maxRuntimeMs: C.TIME.seconds(10),
|
|
256
|
+
}));
|
|
257
|
+
|
|
258
|
+
var COMPLIANCE_POSTURES = Object.freeze({
|
|
259
|
+
"hipaa": Object.assign({}, PROFILES["strict"], {
|
|
260
|
+
forensicSnippetBytes: C.BYTES.bytes(256),
|
|
261
|
+
}),
|
|
262
|
+
"pci-dss": Object.assign({}, PROFILES["strict"], {
|
|
263
|
+
forensicSnippetBytes: C.BYTES.bytes(256),
|
|
264
|
+
}),
|
|
265
|
+
"gdpr": Object.assign({}, PROFILES["balanced"], {
|
|
266
|
+
forensicSnippetBytes: C.BYTES.bytes(128),
|
|
267
|
+
}),
|
|
268
|
+
"soc2-cc7": Object.assign({}, PROFILES["strict"], {
|
|
269
|
+
forensicSnippetBytes: C.BYTES.bytes(512),
|
|
270
|
+
}),
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
function _resolveOpts(opts) {
|
|
274
|
+
return gateContract.resolveProfileAndPosture(opts, {
|
|
275
|
+
profiles: PROFILES,
|
|
276
|
+
compliancePostures: COMPLIANCE_POSTURES,
|
|
277
|
+
defaults: DEFAULTS,
|
|
278
|
+
errorClass: GuardEmailError,
|
|
279
|
+
errCodePrefix: "email",
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ---- Address validation ----
|
|
284
|
+
|
|
285
|
+
function _detectAddressIssues(input, opts) {
|
|
286
|
+
var issues = [];
|
|
287
|
+
if (typeof input !== "string") {
|
|
288
|
+
return [{ kind: "bad-input", severity: "high",
|
|
289
|
+
snippet: "address is not a string" }];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Total-address length cap.
|
|
293
|
+
if (input.length > opts.maxAddressBytes) {
|
|
294
|
+
issues.push({
|
|
295
|
+
kind: "address-cap", severity: "high", ruleId: "email.address-cap",
|
|
296
|
+
snippet: "address " + input.length + " bytes exceeds maxAddressBytes " +
|
|
297
|
+
opts.maxAddressBytes,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Multi-@ check — count any unquoted @ outside square brackets.
|
|
302
|
+
var atCount = 0;
|
|
303
|
+
var inQuote = false;
|
|
304
|
+
var inBrack = false;
|
|
305
|
+
for (var i = 0; i < input.length; i += 1) {
|
|
306
|
+
var c = input.charAt(i);
|
|
307
|
+
if (c === '"') inQuote = !inQuote;
|
|
308
|
+
else if (c === "[" && !inQuote) inBrack = true;
|
|
309
|
+
else if (c === "]" && !inQuote) inBrack = false;
|
|
310
|
+
else if (c === "@" && !inQuote && !inBrack) atCount += 1;
|
|
311
|
+
}
|
|
312
|
+
if (atCount !== 1 && opts.multiAtPolicy !== "allow") {
|
|
313
|
+
issues.push({
|
|
314
|
+
kind: "multi-at", severity: "critical",
|
|
315
|
+
ruleId: "email.multi-at",
|
|
316
|
+
snippet: "address has " + atCount + " '@' characters; expected exactly 1",
|
|
317
|
+
});
|
|
318
|
+
return issues; // can't continue without one envelope split
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Comment syntax — `(comment)` per RFC 5322; most receivers reject.
|
|
322
|
+
if (opts.addressCommentPolicy !== "allow" && ADDRESS_COMMENT_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxAddressBytes
|
|
323
|
+
issues.push({
|
|
324
|
+
kind: "address-comment",
|
|
325
|
+
severity: opts.addressCommentPolicy === "reject" ? "high" : "warn",
|
|
326
|
+
ruleId: "email.address-comment",
|
|
327
|
+
snippet: "address contains '(' or ')' — RFC 5322 comment syntax, " +
|
|
328
|
+
"smuggling-prone vs RFC 5321 receivers",
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// IP-literal check.
|
|
333
|
+
if (IP_LITERAL_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxAddressBytes
|
|
334
|
+
if (opts.ipLiteralPolicy !== "allow") {
|
|
335
|
+
issues.push({
|
|
336
|
+
kind: "ip-literal",
|
|
337
|
+
severity: opts.ipLiteralPolicy === "reject" ? "high" : "warn",
|
|
338
|
+
ruleId: "email.ip-literal",
|
|
339
|
+
snippet: "address uses IP literal `[...]` — bypasses DNS / DMARC alignment",
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
// Length-aware cap checks BEFORE syntax-regex. The regex enforces
|
|
344
|
+
// RFC 5321 label-length (63) and atext shape; oversized inputs would
|
|
345
|
+
// fail the regex first and bury the more useful cap diagnosis.
|
|
346
|
+
var atIdx = input.lastIndexOf("@");
|
|
347
|
+
var localPart = atIdx === -1 ? input : input.slice(0, atIdx);
|
|
348
|
+
var domain = atIdx === -1 ? "" : input.slice(atIdx + 1);
|
|
349
|
+
|
|
350
|
+
if (localPart.length > opts.maxLocalPartBytes) {
|
|
351
|
+
issues.push({
|
|
352
|
+
kind: "local-part-cap", severity: "high",
|
|
353
|
+
ruleId: "email.local-part-cap",
|
|
354
|
+
snippet: "local-part " + localPart.length + " bytes exceeds " +
|
|
355
|
+
opts.maxLocalPartBytes + " (RFC 5321 §4.5.3.1.1)",
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
if (domain.length > opts.maxDomainBytes) {
|
|
359
|
+
issues.push({
|
|
360
|
+
kind: "domain-cap", severity: "high",
|
|
361
|
+
ruleId: "email.domain-cap",
|
|
362
|
+
snippet: "domain " + domain.length + " bytes exceeds " +
|
|
363
|
+
opts.maxDomainBytes + " (RFC 5321 §4.5.3.1.2)",
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Punycode / IDN — flag operator's choice. Runs against the lexed
|
|
368
|
+
// domain so non-ASCII codepoints (which fail the strict ASCII regex)
|
|
369
|
+
// still surface this issue.
|
|
370
|
+
if (opts.punycodePolicy !== "allow" && PUNYCODE_LABEL_RE.test(domain)) { // allow:regex-no-length-cap — domain bounded by maxDomainBytes
|
|
371
|
+
issues.push({
|
|
372
|
+
kind: "punycode-domain",
|
|
373
|
+
severity: opts.punycodePolicy === "reject" ? "high" : "warn",
|
|
374
|
+
ruleId: "email.punycode-domain",
|
|
375
|
+
snippet: "domain uses IDN/Punycode (`xn--` label) — may be " +
|
|
376
|
+
"homograph-spoofing",
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Mixed-script confusable detection in domain. Runs on the lexed
|
|
381
|
+
// domain (post-@ split) so non-ASCII codepoints surface here even
|
|
382
|
+
// when they fail the strict ASCII syntax regex below.
|
|
383
|
+
var mixed = _detectMixedScripts(domain, opts.allowedScripts);
|
|
384
|
+
if (mixed && opts.mixedScriptPolicy !== "allow") {
|
|
385
|
+
issues.push({
|
|
386
|
+
kind: "mixed-script-domain",
|
|
387
|
+
severity: opts.mixedScriptPolicy === "reject" ? "critical" : "high",
|
|
388
|
+
ruleId: "email.mixed-script-domain",
|
|
389
|
+
snippet: "domain mixes scripts (" + mixed.join(", ") + ") — " +
|
|
390
|
+
"IDN homograph spoofing class",
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// If we found a cap issue, skip the strict-ASCII regex (over-cap
|
|
395
|
+
// input fails it anyway and the cap diagnosis is more actionable).
|
|
396
|
+
var hasCap = issues.some(function (i) {
|
|
397
|
+
return i.kind === "local-part-cap" || i.kind === "domain-cap";
|
|
398
|
+
});
|
|
399
|
+
if (!hasCap) {
|
|
400
|
+
var match = input.match(ADDRESS_RE); // allow:regex-no-length-cap — input bounded by maxAddressBytes
|
|
401
|
+
if (!match) {
|
|
402
|
+
// The strict regex caught a non-ASCII or shape issue. If we've
|
|
403
|
+
// already surfaced a punycode / mixed-script issue (the actual
|
|
404
|
+
// semantic threat), don't pile on with address-syntax — the
|
|
405
|
+
// operator gets the real diagnosis.
|
|
406
|
+
var hasIdnIssue = issues.some(function (i) {
|
|
407
|
+
return i.kind === "punycode-domain" || i.kind === "mixed-script-domain";
|
|
408
|
+
});
|
|
409
|
+
if (!hasIdnIssue) {
|
|
410
|
+
issues.push({
|
|
411
|
+
kind: "address-syntax", severity: "high",
|
|
412
|
+
ruleId: "email.address-syntax",
|
|
413
|
+
snippet: "address does not match RFC 5321 atext@DNS-domain shape",
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Codepoint-class threats inside the address.
|
|
421
|
+
issues.push.apply(issues, codepointClass.detectCharThreats(input, opts, "email"));
|
|
422
|
+
|
|
423
|
+
return issues;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function validateAddress(input, opts) {
|
|
427
|
+
opts = _resolveOpts(opts);
|
|
428
|
+
numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
|
|
429
|
+
["maxLocalPartBytes", "maxDomainBytes", "maxAddressBytes",
|
|
430
|
+
"maxHeaderLineBytes", "maxHeaders", "maxBytes"],
|
|
431
|
+
"guardEmail.validateAddress", GuardEmailError, "email.bad-opt");
|
|
432
|
+
if (typeof input !== "string") {
|
|
433
|
+
return {
|
|
434
|
+
ok: false,
|
|
435
|
+
issues: [{ kind: "bad-input", severity: "high",
|
|
436
|
+
snippet: "address is not a string" }],
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return gateContract.aggregateIssues(_detectAddressIssues(input, opts));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ---- Message validation (full RFC 822 / 5322) ----
|
|
443
|
+
|
|
444
|
+
function _detectMessageIssues(input, opts) {
|
|
445
|
+
var issues = [];
|
|
446
|
+
if (typeof input !== "string") {
|
|
447
|
+
return [{ kind: "bad-input", severity: "high",
|
|
448
|
+
snippet: "input is not a string" }];
|
|
449
|
+
}
|
|
450
|
+
if (input.length > opts.maxBytes) {
|
|
451
|
+
return [{ kind: "too-large", severity: "high",
|
|
452
|
+
ruleId: "email.too-large",
|
|
453
|
+
snippet: "input " + input.length +
|
|
454
|
+
" bytes exceeds maxBytes " + opts.maxBytes }];
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// BOM at start of message — header-injection prelude.
|
|
458
|
+
if (opts.bomPolicy !== "allow") {
|
|
459
|
+
if (input.charCodeAt(0) === 0xfeff) { // allow:raw-byte-literal — Unicode BOM
|
|
460
|
+
issues.push({
|
|
461
|
+
kind: "bom",
|
|
462
|
+
severity: opts.bomPolicy === "reject" ? "high" : "warn",
|
|
463
|
+
ruleId: "email.bom",
|
|
464
|
+
snippet: "message starts with BOM (U+FEFF) — header-parser confusion",
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// SMTP smuggling — bare CR / bare LF + smuggled-verb scan.
|
|
470
|
+
var bare = _scanBareLineEndings(input);
|
|
471
|
+
if (bare.bareCr && opts.bareCrPolicy !== "allow") {
|
|
472
|
+
issues.push({
|
|
473
|
+
kind: "bare-cr",
|
|
474
|
+
severity: opts.bareCrPolicy === "reject" ? "critical" : "warn",
|
|
475
|
+
ruleId: "email.bare-cr",
|
|
476
|
+
snippet: "message contains bare CR (not part of CRLF) — SMTP " +
|
|
477
|
+
"smuggling vector class (CVE-2023-51764)",
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
if (bare.bareLf && opts.bareLfPolicy !== "allow") {
|
|
481
|
+
issues.push({
|
|
482
|
+
kind: "bare-lf",
|
|
483
|
+
severity: opts.bareLfPolicy === "reject" ? "critical" : "warn",
|
|
484
|
+
ruleId: "email.bare-lf",
|
|
485
|
+
snippet: "message contains bare LF (not part of CRLF) — SMTP " +
|
|
486
|
+
"smuggling vector class (CVE-2023-51765 / CVE-2023-51766)",
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
if (opts.smtpSmugglingPolicy !== "allow" && SMUGGLED_VERB_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
|
|
490
|
+
issues.push({
|
|
491
|
+
kind: "smtp-smuggling", severity: "critical",
|
|
492
|
+
ruleId: "email.smtp-smuggling",
|
|
493
|
+
snippet: "embedded SMTP verb after bare CR/LF — smuggling vector " +
|
|
494
|
+
"(SEC Consult / smtpsmuggling.com class)",
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Split header section from body.
|
|
499
|
+
var headerEnd = input.indexOf("\r\n\r\n");
|
|
500
|
+
if (headerEnd === -1) headerEnd = input.indexOf("\n\n");
|
|
501
|
+
var headerSection = headerEnd === -1 ? input : input.slice(0, headerEnd);
|
|
502
|
+
|
|
503
|
+
// Per-line cap + count cap.
|
|
504
|
+
var lines = headerSection.split(/\r?\n/);
|
|
505
|
+
if (lines.length > opts.maxHeaders) {
|
|
506
|
+
issues.push({
|
|
507
|
+
kind: "header-count-cap", severity: "high",
|
|
508
|
+
ruleId: "email.header-count-cap",
|
|
509
|
+
snippet: "header count " + lines.length + " exceeds maxHeaders " +
|
|
510
|
+
opts.maxHeaders,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
for (var li = 0; li < lines.length; li += 1) {
|
|
514
|
+
if (lines[li].length > opts.maxHeaderLineBytes) {
|
|
515
|
+
issues.push({
|
|
516
|
+
kind: "header-line-cap", severity: "high",
|
|
517
|
+
ruleId: "email.header-line-cap",
|
|
518
|
+
snippet: "header line " + (li + 1) + " is " + lines[li].length +
|
|
519
|
+
" bytes (RFC 5322 §2.1.1 limit " + opts.maxHeaderLineBytes + ")",
|
|
520
|
+
});
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Walk single-line headers checking for embedded CRLF + display-name
|
|
526
|
+
// spoofing.
|
|
527
|
+
var unfolded = _unfoldHeaders(lines);
|
|
528
|
+
for (var hi = 0; hi < unfolded.length; hi += 1) {
|
|
529
|
+
var entry = unfolded[hi];
|
|
530
|
+
var name = entry.name.toLowerCase();
|
|
531
|
+
if (SINGLE_LINE_HEADERS.indexOf(name) === -1) continue;
|
|
532
|
+
|
|
533
|
+
// CRLF in single-line header value.
|
|
534
|
+
if (opts.crlfHeaderInjectionPolicy !== "allow" &&
|
|
535
|
+
_hasCrlfInHeaderValue(entry.value)) {
|
|
536
|
+
issues.push({
|
|
537
|
+
kind: "crlf-header-injection", severity: "critical",
|
|
538
|
+
ruleId: "email.crlf-header-injection",
|
|
539
|
+
snippet: "header `" + entry.name + "` contains CR/LF — header " +
|
|
540
|
+
"injection vector (smuggle From/Bcc/body)",
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Address-bearing headers — run address checks on each address.
|
|
545
|
+
if (name === "from" || name === "to" || name === "cc" ||
|
|
546
|
+
name === "bcc" || name === "reply-to" || name === "sender" ||
|
|
547
|
+
name === "return-path") {
|
|
548
|
+
var addrIssues = _checkAddressHeaderValue(entry.value, opts, entry.name);
|
|
549
|
+
for (var ai = 0; ai < addrIssues.length; ai += 1) {
|
|
550
|
+
issues.push(addrIssues[ai]);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Codepoint-class threats in the full message.
|
|
556
|
+
issues.push.apply(issues, codepointClass.detectCharThreats(input, opts, "email"));
|
|
557
|
+
|
|
558
|
+
return issues;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function _unfoldHeaders(lines) {
|
|
562
|
+
var out = [];
|
|
563
|
+
var current = null;
|
|
564
|
+
for (var i = 0; i < lines.length; i += 1) {
|
|
565
|
+
var line = lines[i];
|
|
566
|
+
if (line === "") { current = null; continue; }
|
|
567
|
+
if (current && (line.charAt(0) === " " || line.charAt(0) === "\t")) {
|
|
568
|
+
current.value += " " + line.replace(/^\s+/, "");
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
var colonAt = line.indexOf(":");
|
|
572
|
+
if (colonAt === -1) { current = null; continue; }
|
|
573
|
+
current = {
|
|
574
|
+
name: line.slice(0, colonAt).trim(),
|
|
575
|
+
value: line.slice(colonAt + 1).replace(/^\s+/, ""),
|
|
576
|
+
};
|
|
577
|
+
out.push(current);
|
|
578
|
+
}
|
|
579
|
+
return out;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function _splitAddressList(value) {
|
|
583
|
+
// Split on commas outside angle brackets and quoted strings.
|
|
584
|
+
var parts = [];
|
|
585
|
+
var depth = 0;
|
|
586
|
+
var inQuote = false;
|
|
587
|
+
var start = 0;
|
|
588
|
+
for (var i = 0; i < value.length; i += 1) {
|
|
589
|
+
var c = value.charAt(i);
|
|
590
|
+
if (c === '"' && (i === 0 || value.charAt(i - 1) !== "\\")) inQuote = !inQuote;
|
|
591
|
+
else if (!inQuote && c === "<") depth += 1;
|
|
592
|
+
else if (!inQuote && c === ">") depth -= 1;
|
|
593
|
+
else if (!inQuote && depth === 0 && c === ",") {
|
|
594
|
+
parts.push(value.slice(start, i).trim());
|
|
595
|
+
start = i + 1;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (start < value.length) parts.push(value.slice(start).trim());
|
|
599
|
+
return parts.filter(function (s) { return s.length > 0; });
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function _checkAddressHeaderValue(value, opts, headerName) {
|
|
603
|
+
var issues = [];
|
|
604
|
+
var parts = _splitAddressList(value);
|
|
605
|
+
for (var p = 0; p < parts.length; p += 1) {
|
|
606
|
+
var parsed = _parseAddressLine(parts[p]);
|
|
607
|
+
var addrIssues = _detectAddressIssues(parsed.envelope, opts);
|
|
608
|
+
for (var k = 0; k < addrIssues.length; k += 1) {
|
|
609
|
+
var iss = Object.assign({}, addrIssues[k], {
|
|
610
|
+
snippet: headerName + ": " + addrIssues[k].snippet,
|
|
611
|
+
});
|
|
612
|
+
issues.push(iss);
|
|
613
|
+
}
|
|
614
|
+
// Display-name spoofing: display contains an `@` that does NOT match
|
|
615
|
+
// the envelope domain.
|
|
616
|
+
if (parsed.display && parsed.display.indexOf("@") !== -1 &&
|
|
617
|
+
opts.displayNameSpoofPolicy !== "allow") {
|
|
618
|
+
var atIdx = parsed.envelope.lastIndexOf("@");
|
|
619
|
+
var envDomain = atIdx === -1 ? "" : parsed.envelope.slice(atIdx + 1);
|
|
620
|
+
var displayHasDomain = parsed.display.toLowerCase().indexOf(envDomain.toLowerCase()) !== -1;
|
|
621
|
+
if (!displayHasDomain) {
|
|
622
|
+
issues.push({
|
|
623
|
+
kind: "display-name-spoof",
|
|
624
|
+
severity: opts.displayNameSpoofPolicy === "reject" ? "critical" : "high",
|
|
625
|
+
ruleId: "email.display-name-spoof",
|
|
626
|
+
snippet: headerName + ": display name `" +
|
|
627
|
+
parsed.display.slice(0, 64) + "` includes an @-address that " + // allow:raw-byte-literal — snippet truncation
|
|
628
|
+
"doesn't match the envelope domain `" + envDomain + "`",
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return issues;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function validateMessage(input, opts) {
|
|
637
|
+
opts = _resolveOpts(opts);
|
|
638
|
+
numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
|
|
639
|
+
["maxLocalPartBytes", "maxDomainBytes", "maxAddressBytes",
|
|
640
|
+
"maxHeaderLineBytes", "maxHeaders", "maxBytes"],
|
|
641
|
+
"guardEmail.validateMessage", GuardEmailError, "email.bad-opt");
|
|
642
|
+
if (typeof input !== "string") {
|
|
643
|
+
return {
|
|
644
|
+
ok: false,
|
|
645
|
+
issues: [{ kind: "bad-input", severity: "high",
|
|
646
|
+
snippet: "input is not a string" }],
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
return gateContract.aggregateIssues(_detectMessageIssues(input, opts));
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// validate(input, opts) — auto-detect single address vs full message.
|
|
653
|
+
function validate(input, opts) {
|
|
654
|
+
if (typeof input === "string" && input.indexOf("\n") === -1 &&
|
|
655
|
+
input.indexOf(":") === -1) {
|
|
656
|
+
return validateAddress(input, opts);
|
|
657
|
+
}
|
|
658
|
+
return validateMessage(input, opts);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function sanitize(input, opts) {
|
|
662
|
+
opts = _resolveOpts(opts);
|
|
663
|
+
if (typeof input !== "string") {
|
|
664
|
+
throw _err("email.bad-input", "sanitize requires string input");
|
|
665
|
+
}
|
|
666
|
+
// Critical shapes have no safe sanitization in email — throw on
|
|
667
|
+
// smuggling / CRLF injection / multi-@ / mixed-script.
|
|
668
|
+
var issues = _detectMessageIssues(input, opts);
|
|
669
|
+
for (var i = 0; i < issues.length; i += 1) {
|
|
670
|
+
if (issues[i].severity === "critical") {
|
|
671
|
+
throw _err(issues[i].ruleId || "email.refused",
|
|
672
|
+
"guardEmail.sanitize: " + issues[i].snippet);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return codepointClass.applyCharStripPolicies(input, opts);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function gate(opts) {
|
|
679
|
+
opts = _resolveOpts(opts);
|
|
680
|
+
return gateContract.buildGuardGate(
|
|
681
|
+
opts.name || "guardEmail:" + (opts.profile || "default"),
|
|
682
|
+
opts,
|
|
683
|
+
async function (ctx) {
|
|
684
|
+
var text = gateContract.extractBytesAsText(ctx);
|
|
685
|
+
if (!text) return { ok: true, action: "serve" };
|
|
686
|
+
var rv = validateMessage(text, opts);
|
|
687
|
+
if (rv.issues.length === 0) return { ok: true, action: "serve" };
|
|
688
|
+
var hasCritical = rv.issues.some(function (i) {
|
|
689
|
+
return i.severity === "critical";
|
|
690
|
+
});
|
|
691
|
+
var hasHigh = rv.issues.some(function (i) {
|
|
692
|
+
return i.severity === "high";
|
|
693
|
+
});
|
|
694
|
+
if (!hasCritical && !hasHigh) {
|
|
695
|
+
return { ok: true, action: "audit-only", issues: rv.issues };
|
|
696
|
+
}
|
|
697
|
+
return { ok: false, action: "refuse", issues: rv.issues };
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
var buildProfile = gateContract.makeProfileBuilder(PROFILES);
|
|
702
|
+
|
|
703
|
+
function compliancePosture(name) {
|
|
704
|
+
return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES, _err, "email");
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
var _emailRulePacks = gateContract.makeRulePackLoader(GuardEmailError, "email");
|
|
708
|
+
var loadRulePack = _emailRulePacks.load;
|
|
709
|
+
|
|
710
|
+
module.exports = {
|
|
711
|
+
// ---- guard-* family registry exports ----
|
|
712
|
+
NAME: "email",
|
|
713
|
+
KIND: "content",
|
|
714
|
+
MIME_TYPES: Object.freeze(["message/rfc822", "message/global"]),
|
|
715
|
+
EXTENSIONS: Object.freeze([".eml", ".mbox", ".msg"]),
|
|
716
|
+
INTEGRATION_FIXTURES: Object.freeze({
|
|
717
|
+
kind: "content",
|
|
718
|
+
contentType: "message/rfc822",
|
|
719
|
+
extension: ".eml",
|
|
720
|
+
benignBytes: Buffer.from(
|
|
721
|
+
"From: alice@example.com\r\nTo: bob@example.com\r\n" +
|
|
722
|
+
"Subject: hello\r\nDate: Mon, 5 May 2026 10:00:00 +0000\r\n\r\n" +
|
|
723
|
+
"Hello.\r\n", "utf8"),
|
|
724
|
+
// Hostile: SMTP-smuggling pattern — bare LF followed by SMTP verb
|
|
725
|
+
// (CVE-2023-51764 / 51765 / 51766 class).
|
|
726
|
+
hostileBytes: Buffer.from(
|
|
727
|
+
"From: alice@example.com\r\nTo: bob@example.com\r\n" +
|
|
728
|
+
"Subject: hi\r\n\r\n" +
|
|
729
|
+
"body line 1\n.\nMAIL FROM: <evil@attacker>\r\n", "utf8"),
|
|
730
|
+
}),
|
|
731
|
+
// ---- primitive surface ----
|
|
732
|
+
validate: validate,
|
|
733
|
+
validateAddress: validateAddress,
|
|
734
|
+
validateMessage: validateMessage,
|
|
735
|
+
sanitize: sanitize,
|
|
736
|
+
gate: gate,
|
|
737
|
+
buildProfile: buildProfile,
|
|
738
|
+
compliancePosture: compliancePosture,
|
|
739
|
+
loadRulePack: loadRulePack,
|
|
740
|
+
PROFILES: PROFILES,
|
|
741
|
+
DEFAULTS: DEFAULTS,
|
|
742
|
+
COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
|
|
743
|
+
GuardEmailError: GuardEmailError,
|
|
744
|
+
};
|