@blamejs/core 0.9.49 → 0.10.2
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 +952 -908
- package/index.js +25 -0
- package/lib/_test/crypto-fixtures.js +67 -0
- package/lib/agent-event-bus.js +52 -6
- package/lib/agent-idempotency.js +169 -16
- package/lib/agent-orchestrator.js +263 -9
- package/lib/agent-posture-chain.js +163 -5
- package/lib/agent-saga.js +146 -16
- package/lib/agent-snapshot.js +349 -19
- package/lib/agent-stream.js +34 -2
- package/lib/agent-tenant.js +179 -23
- package/lib/agent-trace.js +84 -21
- package/lib/auth/aal.js +8 -1
- package/lib/auth/ciba.js +6 -1
- package/lib/auth/dpop.js +7 -2
- package/lib/auth/fal.js +17 -8
- package/lib/auth/jwt-external.js +128 -4
- package/lib/auth/oauth.js +232 -10
- package/lib/auth/oid4vci.js +67 -7
- package/lib/auth/openid-federation.js +71 -25
- package/lib/auth/passkey.js +140 -6
- package/lib/auth/sd-jwt-vc.js +78 -5
- package/lib/circuit-breaker.js +10 -2
- package/lib/cli.js +13 -0
- package/lib/compliance.js +176 -8
- package/lib/crypto-field.js +114 -14
- package/lib/crypto.js +216 -20
- package/lib/db.js +1 -0
- package/lib/guard-graphql.js +37 -0
- package/lib/guard-jmap.js +321 -0
- package/lib/guard-managesieve-command.js +566 -0
- package/lib/guard-pop3-command.js +317 -0
- package/lib/guard-regex.js +138 -1
- package/lib/guard-smtp-command.js +58 -3
- package/lib/guard-xml.js +39 -1
- package/lib/mail-agent.js +20 -7
- package/lib/mail-arc-sign.js +12 -8
- package/lib/mail-auth.js +323 -34
- package/lib/mail-crypto-pgp.js +934 -0
- package/lib/mail-crypto-smime.js +340 -0
- package/lib/mail-crypto.js +108 -0
- package/lib/mail-dav.js +1224 -0
- package/lib/mail-deploy.js +492 -0
- package/lib/mail-dkim.js +431 -26
- package/lib/mail-journal.js +435 -0
- package/lib/mail-scan.js +502 -0
- package/lib/mail-server-imap.js +64 -26
- package/lib/mail-server-jmap.js +488 -0
- package/lib/mail-server-managesieve.js +853 -0
- package/lib/mail-server-mx.js +40 -30
- package/lib/mail-server-pop3.js +836 -0
- package/lib/mail-server-rate-limit.js +13 -0
- package/lib/mail-server-submission.js +70 -24
- package/lib/mail-server-tls.js +445 -0
- package/lib/mail-sieve.js +557 -0
- package/lib/mail-spam-score.js +284 -0
- package/lib/mail.js +99 -0
- package/lib/metrics.js +80 -3
- package/lib/middleware/dpop.js +58 -3
- package/lib/middleware/idempotency-key.js +255 -42
- package/lib/middleware/protected-resource-metadata.js +114 -2
- package/lib/network-dns-resolver.js +33 -0
- package/lib/network-tls.js +46 -0
- package/lib/otel-export.js +13 -4
- package/lib/outbox.js +62 -12
- package/lib/pqc-agent.js +13 -5
- package/lib/retry.js +23 -9
- package/lib/router.js +23 -1
- package/lib/safe-ical.js +634 -0
- package/lib/safe-icap.js +502 -0
- package/lib/safe-mime.js +15 -0
- package/lib/safe-sieve.js +684 -0
- package/lib/safe-smtp.js +57 -0
- package/lib/safe-url.js +37 -0
- package/lib/safe-vcard.js +473 -0
- package/lib/self-update-standalone-verifier.js +32 -3
- package/lib/self-update.js +153 -33
- package/lib/vendor/MANIFEST.json +161 -156
- package/lib/vendor-data.js +127 -9
- package/lib/vex.js +324 -59
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.guardManageSieveCommand
|
|
4
|
+
* @nav Guards
|
|
5
|
+
* @title Guard ManageSieve Command
|
|
6
|
+
* @order 454
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* ManageSieve command-line validator (RFC 5804 — "A Protocol for
|
|
10
|
+
* Remotely Managing Sieve Scripts"). Gates every verb the framework's
|
|
11
|
+
* ManageSieve listener accepts from peers — `AUTHENTICATE` /
|
|
12
|
+
* `STARTTLS` / `LOGOUT` / `CAPABILITY` / `HAVESPACE` / `PUTSCRIPT` /
|
|
13
|
+
* `LISTSCRIPTS` / `SETACTIVE` / `GETSCRIPT` / `DELETESCRIPT` /
|
|
14
|
+
* `RENAMESCRIPT` / `NOOP`.
|
|
15
|
+
*
|
|
16
|
+
* ManageSieve is a line-oriented text protocol that mixes simple
|
|
17
|
+
* single-line commands with literal-syntax payloads (`{N}` / `{N+}`)
|
|
18
|
+
* carrying script bytes verbatim. Responses are `OK ...` / `NO ...` /
|
|
19
|
+
* `BYE ...` per RFC 5804 §1.2.
|
|
20
|
+
*
|
|
21
|
+
* ## Smuggling defense — bare-CR / bare-LF refusal
|
|
22
|
+
*
|
|
23
|
+
* Same wire-protocol concern as SMTP / IMAP / POP3. Command lines
|
|
24
|
+
* MUST be canonical CRLF-terminated; bare-CR or bare-LF inside a
|
|
25
|
+
* command (outside the literal-payload window) is refused. The
|
|
26
|
+
* listener clears the receive buffer at STARTTLS upgrade to defend
|
|
27
|
+
* the same pre-handshake injection class that affected STARTTLS in
|
|
28
|
+
* Exim (CVE-2021-38371) / Dovecot (CVE-2021-33515) / Postfix
|
|
29
|
+
* (CVE-2011-0411).
|
|
30
|
+
*
|
|
31
|
+
* ## Cleartext-AUTH refusal under strict
|
|
32
|
+
*
|
|
33
|
+
* RFC 5804 §1.1 + RFC 4954 §4 — `AUTHENTICATE` with credential-
|
|
34
|
+
* bearing mechanisms (PLAIN / LOGIN) over a cleartext channel
|
|
35
|
+
* exposes the password to passive observation. Strict + balanced
|
|
36
|
+
* refuse `AUTHENTICATE PLAIN` / `AUTHENTICATE LOGIN` pre-TLS; the
|
|
37
|
+
* listener composes this gate at the validate boundary AND
|
|
38
|
+
* re-checks at the dispatch boundary as defense-in-depth.
|
|
39
|
+
*
|
|
40
|
+
* `AUTHENTICATE EXTERNAL` (RFC 4422 §4) is exempt — the credential
|
|
41
|
+
* is the TLS client certificate already presented, so cleartext is
|
|
42
|
+
* not the concern. `AUTHENTICATE SCRAM-SHA-256` (RFC 7677) and
|
|
43
|
+
* `AUTHENTICATE OAUTHBEARER` (RFC 7628) are mechanism-side
|
|
44
|
+
* credential-protected and may run pre-TLS under permissive; strict
|
|
45
|
+
* still requires TLS (defense-in-depth + active-MITM resistance).
|
|
46
|
+
*
|
|
47
|
+
* ## Script-name shape (RFC 5804 §2.1)
|
|
48
|
+
*
|
|
49
|
+
* Script names are UTF-8 strings of 1-512 octets containing no NUL
|
|
50
|
+
* (0x00), CR (0x0D), LF (0x0A), forward-slash (0x2F), backslash
|
|
51
|
+
* (0x5C), or double-quote (0x22). The forward-slash + backslash
|
|
52
|
+
* refusal blocks path-traversal-style storage-backend collisions;
|
|
53
|
+
* the NUL/CR/LF refusal blocks wire-protocol smuggling.
|
|
54
|
+
*
|
|
55
|
+
* ## Literal syntax (RFC 5804 §2.3 + RFC 7888 LITERAL+)
|
|
56
|
+
*
|
|
57
|
+
* `PUTSCRIPT name {N}` / `PUTSCRIPT name {N+}` introduces an N-byte
|
|
58
|
+
* script payload. The bare `{N}` form is synchronizing (server
|
|
59
|
+
* replies with a continuation request before the client sends the
|
|
60
|
+
* payload); `{N+}` (RFC 7888) is non-synchronizing. The validator
|
|
61
|
+
* refuses N values above the per-profile script-byte cap (matching
|
|
62
|
+
* `b.safeSieve`'s `maxScriptBytes`: 64 KiB strict / 256 KiB balanced
|
|
63
|
+
* / 1 MiB permissive).
|
|
64
|
+
*
|
|
65
|
+
* ## Per-verb shape
|
|
66
|
+
*
|
|
67
|
+
* RFC 5804 §2.1-§2.10:
|
|
68
|
+
*
|
|
69
|
+
* - `AUTHENTICATE` "<mech>" [<literal-initial-response>]
|
|
70
|
+
* - `STARTTLS` — no args
|
|
71
|
+
* - `LOGOUT` — no args
|
|
72
|
+
* - `CAPABILITY` — no args
|
|
73
|
+
* - `NOOP` [string] — optional echo-tag arg
|
|
74
|
+
* - `HAVESPACE` "<name>" <N> — name + non-negative integer
|
|
75
|
+
* - `PUTSCRIPT` "<name>" <literal-script>
|
|
76
|
+
* - `LISTSCRIPTS` — no args
|
|
77
|
+
* - `SETACTIVE` "<name>" — single script-name arg (empty
|
|
78
|
+
* string deactivates all per §2.8)
|
|
79
|
+
* - `GETSCRIPT` "<name>" — single script-name arg
|
|
80
|
+
* - `DELETESCRIPT` "<name>" — single script-name arg
|
|
81
|
+
* - `RENAMESCRIPT` "<old>" "<new>" — two script-name args
|
|
82
|
+
*
|
|
83
|
+
* ## Caps
|
|
84
|
+
*
|
|
85
|
+
* - Per-line cap (excluding the literal payload itself): 8 KiB
|
|
86
|
+
* strict / 16 KiB balanced / 64 KiB permissive. ManageSieve's
|
|
87
|
+
* command lines are LONGER than POP3/IMAP because script names
|
|
88
|
+
* may carry UTF-8 + the literal-payload announcement.
|
|
89
|
+
* - Script-byte cap (literal `{N}` value): same as
|
|
90
|
+
* `b.safeSieve.PROFILES.<profile>.maxScriptBytes` — 64 KiB /
|
|
91
|
+
* 256 KiB / 1 MiB.
|
|
92
|
+
* - Script name: RFC 5804 §2.1 1-512 octets.
|
|
93
|
+
*
|
|
94
|
+
* Throws `GuardManageSieveCommandError` on every refusal.
|
|
95
|
+
*
|
|
96
|
+
* @card
|
|
97
|
+
* ManageSieve command-line validator (RFC 5804 + RFC 7888 LITERAL+).
|
|
98
|
+
* Refuses bare-CR / bare-LF (smuggling defense), caps per-line +
|
|
99
|
+
* script-name + literal-script bytes, refuses cleartext AUTHENTICATE
|
|
100
|
+
* under strict (RFC 4954 §4 class), validates per-verb shape.
|
|
101
|
+
*/
|
|
102
|
+
|
|
103
|
+
var { defineClass } = require("./framework-error");
|
|
104
|
+
|
|
105
|
+
var GuardManageSieveCommandError = defineClass("GuardManageSieveCommandError",
|
|
106
|
+
{ alwaysPermanent: true });
|
|
107
|
+
|
|
108
|
+
var DEFAULT_PROFILE = "strict";
|
|
109
|
+
|
|
110
|
+
var PROFILES = Object.freeze({
|
|
111
|
+
strict: {
|
|
112
|
+
maxLineBytes: 8192, // allow:raw-byte-literal — 8 KiB per-line cap (strict)
|
|
113
|
+
maxScriptBytes: 65536, // allow:raw-byte-literal — 64 KiB script cap (matches safeSieve strict)
|
|
114
|
+
maxScriptNameBytes: 512, // allow:raw-byte-literal — RFC 5804 §2.1 script-name cap
|
|
115
|
+
allowBareLf: false,
|
|
116
|
+
allowCleartextAuth: false,
|
|
117
|
+
allowLiteralPlus: true, // RFC 7888 LITERAL+ accepted under strict (operator MAY refuse via opts.allowLiteralPlus=false) // allow:raw-byte-literal — RFC number
|
|
118
|
+
},
|
|
119
|
+
balanced: {
|
|
120
|
+
maxLineBytes: 16384, // allow:raw-byte-literal — 16 KiB per-line cap (balanced)
|
|
121
|
+
maxScriptBytes: 262144, // allow:raw-byte-literal — 256 KiB script cap (matches safeSieve balanced)
|
|
122
|
+
maxScriptNameBytes: 512, // allow:raw-byte-literal — RFC 5804 §2.1 script-name cap
|
|
123
|
+
allowBareLf: false,
|
|
124
|
+
allowCleartextAuth: false,
|
|
125
|
+
allowLiteralPlus: true,
|
|
126
|
+
},
|
|
127
|
+
permissive: {
|
|
128
|
+
maxLineBytes: 65536, // allow:raw-byte-literal — 64 KiB per-line cap (permissive)
|
|
129
|
+
maxScriptBytes: 1048576, // allow:raw-byte-literal — 1 MiB script cap (matches safeSieve permissive)
|
|
130
|
+
maxScriptNameBytes: 512, // allow:raw-byte-literal — RFC 5804 §2.1 script-name cap
|
|
131
|
+
allowBareLf: true,
|
|
132
|
+
allowCleartextAuth: true,
|
|
133
|
+
allowLiteralPlus: true,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
var COMPLIANCE_POSTURES = Object.freeze({
|
|
138
|
+
hipaa: "strict",
|
|
139
|
+
"pci-dss": "strict",
|
|
140
|
+
gdpr: "strict",
|
|
141
|
+
soc2: "strict",
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ManageSieve verbs per RFC 5804 §2.
|
|
145
|
+
var KNOWN_VERBS = Object.freeze({
|
|
146
|
+
AUTHENTICATE: true, STARTTLS: true, LOGOUT: true,
|
|
147
|
+
CAPABILITY: true, NOOP: true, HAVESPACE: true,
|
|
148
|
+
PUTSCRIPT: true, LISTSCRIPTS: true, SETACTIVE: true,
|
|
149
|
+
GETSCRIPT: true, DELETESCRIPT: true, RENAMESCRIPT: true,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
var ZERO_ARG_VERBS = Object.freeze({
|
|
153
|
+
STARTTLS: true,
|
|
154
|
+
LOGOUT: true,
|
|
155
|
+
CAPABILITY: true,
|
|
156
|
+
LISTSCRIPTS: true,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// SASL mechanisms that carry credentials in cleartext — refused
|
|
160
|
+
// pre-TLS under strict + balanced per RFC 4954 §4.
|
|
161
|
+
var CLEARTEXT_VULNERABLE_MECHS = Object.freeze({
|
|
162
|
+
PLAIN: true,
|
|
163
|
+
LOGIN: true,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Numeric arg for HAVESPACE / literal length — anchored bounded
|
|
167
|
+
// decimal, up to 10 digits (10^10 - 1 = 9999999999, well below the
|
|
168
|
+
// 1 MiB permissive script cap).
|
|
169
|
+
var NUM_RE = /^[0-9]{1,10}$/; // allow:regex-no-length-cap — anchored + bounded repeat
|
|
170
|
+
|
|
171
|
+
// Literal-length suffix: `{N}` (synchronizing) or `{N+}` (LITERAL+).
|
|
172
|
+
var LITERAL_RE = /^\{([0-9]{1,10})(\+?)\}$/; // allow:regex-no-length-cap — anchored + bounded digits
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @primitive b.guardManageSieveCommand.validate
|
|
176
|
+
* @signature b.guardManageSieveCommand.validate(line, opts?)
|
|
177
|
+
* @since 0.9.57
|
|
178
|
+
* @status stable
|
|
179
|
+
* @related b.guardPop3Command.validate, b.guardImapCommand.validate, b.safeSieve.parse
|
|
180
|
+
*
|
|
181
|
+
* Validate a single ManageSieve command line (without its CRLF
|
|
182
|
+
* terminator, and without the literal-script payload that may follow).
|
|
183
|
+
* Returns a shape describing the parsed verb + arguments + (when
|
|
184
|
+
* applicable) the trailing literal-byte count the listener must read
|
|
185
|
+
* from the wire. Throws `GuardManageSieveCommandError` on refusal.
|
|
186
|
+
*
|
|
187
|
+
* @opts
|
|
188
|
+
* profile: "strict" | "balanced" | "permissive",
|
|
189
|
+
* posture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
|
|
190
|
+
* tls: boolean, // when false + AUTHENTICATE PLAIN/LOGIN
|
|
191
|
+
* under strict, refuse with
|
|
192
|
+
* `guard-managesieve-command/cleartext-auth`
|
|
193
|
+
* (RFC 4954 §4 + RFC 5804 §1.1)
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* var p = b.guardManageSieveCommand.validate('PUTSCRIPT "myscript" {52+}', { tls: true });
|
|
197
|
+
* // → { verb: "PUTSCRIPT", args: ["myscript"], literalBytes: 52, literalPlus: true }
|
|
198
|
+
*
|
|
199
|
+
* var c = b.guardManageSieveCommand.validate("CAPABILITY", { tls: true });
|
|
200
|
+
* // → { verb: "CAPABILITY", args: [] }
|
|
201
|
+
*/
|
|
202
|
+
function validate(line, opts) {
|
|
203
|
+
opts = opts || {};
|
|
204
|
+
var profileName = typeof opts.profile === "string" ? opts.profile : DEFAULT_PROFILE;
|
|
205
|
+
if (opts.posture && COMPLIANCE_POSTURES[opts.posture]) {
|
|
206
|
+
profileName = COMPLIANCE_POSTURES[opts.posture];
|
|
207
|
+
}
|
|
208
|
+
var caps = PROFILES[profileName];
|
|
209
|
+
if (!caps) {
|
|
210
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/bad-profile",
|
|
211
|
+
"guardManageSieveCommand.validate: unknown profile '" + profileName + "'");
|
|
212
|
+
}
|
|
213
|
+
if (typeof line !== "string") {
|
|
214
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/bad-input",
|
|
215
|
+
"guardManageSieveCommand.validate: line must be a string");
|
|
216
|
+
}
|
|
217
|
+
if (line.length === 0) {
|
|
218
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/empty-line",
|
|
219
|
+
"guardManageSieveCommand.validate: empty command line");
|
|
220
|
+
}
|
|
221
|
+
if (line.length > caps.maxLineBytes) {
|
|
222
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/line-too-long",
|
|
223
|
+
"guardManageSieveCommand.validate: line " + line.length +
|
|
224
|
+
" bytes exceeds cap " + caps.maxLineBytes);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Control-byte refusal outside string literals — quoted-string
|
|
228
|
+
// payload (the script-name argument) is scanned separately in
|
|
229
|
+
// `_parseQuotedString`; here we walk the line and skip over the
|
|
230
|
+
// quoted-string regions.
|
|
231
|
+
var inQuote = false;
|
|
232
|
+
for (var i = 0; i < line.length; i += 1) {
|
|
233
|
+
var c = line.charCodeAt(i);
|
|
234
|
+
if (c === 0x22 && !_isEscaped(line, i)) { // allow:raw-byte-literal — DQUOTE
|
|
235
|
+
inQuote = !inQuote;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (inQuote) continue;
|
|
239
|
+
if (c === 0x00 || c === 0x7F || (c < 0x20 && c !== 0x09)) { // allow:raw-byte-literal — control-byte refusal
|
|
240
|
+
if (c === 0x0A && caps.allowBareLf) continue;
|
|
241
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/bad-byte",
|
|
242
|
+
"guardManageSieveCommand.validate: control byte 0x" +
|
|
243
|
+
c.toString(16) + " at offset " + i); // allow:raw-byte-literal — base-16 toString radix
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (inQuote) {
|
|
247
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/unterminated-string",
|
|
248
|
+
"guardManageSieveCommand.validate: unterminated quoted string");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
var firstSpace = line.indexOf(" ");
|
|
252
|
+
var verb = (firstSpace === -1 ? line : line.slice(0, firstSpace)).toUpperCase();
|
|
253
|
+
var rest = firstSpace === -1 ? "" : line.slice(firstSpace + 1);
|
|
254
|
+
|
|
255
|
+
if (!KNOWN_VERBS[verb]) {
|
|
256
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/unknown-verb",
|
|
257
|
+
"guardManageSieveCommand.validate: unknown verb '" + verb + "' (RFC 5804 §2)");
|
|
258
|
+
}
|
|
259
|
+
if (ZERO_ARG_VERBS[verb] && rest.length > 0) {
|
|
260
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/unexpected-args",
|
|
261
|
+
"guardManageSieveCommand.validate: verb '" + verb + "' takes no arguments");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
switch (verb) {
|
|
265
|
+
case "AUTHENTICATE": return _validateAuthenticate(rest, caps, profileName, opts);
|
|
266
|
+
case "NOOP": return _validateNoop(rest, caps);
|
|
267
|
+
case "HAVESPACE": return _validateHavespace(rest, caps);
|
|
268
|
+
case "PUTSCRIPT": return _validatePutscript(rest, caps);
|
|
269
|
+
case "SETACTIVE": return _validateSingleName(verb, rest, caps, { allowEmpty: true });
|
|
270
|
+
case "GETSCRIPT": return _validateSingleName(verb, rest, caps, { allowEmpty: false });
|
|
271
|
+
case "DELETESCRIPT": return _validateSingleName(verb, rest, caps, { allowEmpty: false });
|
|
272
|
+
case "RENAMESCRIPT": return _validateRenamescript(rest, caps);
|
|
273
|
+
default:
|
|
274
|
+
// STARTTLS / LOGOUT / CAPABILITY / LISTSCRIPTS — ZERO_ARG_VERBS
|
|
275
|
+
// guard above already enforced no-args.
|
|
276
|
+
return { verb: verb, args: [] };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// AUTHENTICATE "<mech>" [<literal-initial-response>]
|
|
281
|
+
// Initial-response is either a literal `{N}` / `{N+}` or a quoted
|
|
282
|
+
// base64 string per RFC 5804 §2.1 + RFC 4422.
|
|
283
|
+
function _validateAuthenticate(rest, caps, profileName, opts) {
|
|
284
|
+
if (!rest) {
|
|
285
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/missing-mechanism",
|
|
286
|
+
"guardManageSieveCommand.validate: AUTHENTICATE requires a mechanism");
|
|
287
|
+
}
|
|
288
|
+
var parsed = _parseQuotedString(rest);
|
|
289
|
+
if (parsed === null) {
|
|
290
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/bad-mechanism",
|
|
291
|
+
"guardManageSieveCommand.validate: AUTHENTICATE mechanism must be a quoted string");
|
|
292
|
+
}
|
|
293
|
+
var mech = parsed.value.toUpperCase();
|
|
294
|
+
var trailing = parsed.rest;
|
|
295
|
+
// Cleartext-AUTH refusal — RFC 4954 §4 + RFC 5804 §1.1. Strict +
|
|
296
|
+
// balanced refuse credential-bearing mechanisms (PLAIN / LOGIN)
|
|
297
|
+
// pre-TLS. EXTERNAL is exempt (credential is the TLS client cert,
|
|
298
|
+
// not a password). SCRAM* / OAUTHBEARER are still refused under
|
|
299
|
+
// strict (defense-in-depth + active-MITM resistance).
|
|
300
|
+
if (opts.tls === false && !caps.allowCleartextAuth) {
|
|
301
|
+
if (CLEARTEXT_VULNERABLE_MECHS[mech] || profileName === "strict") {
|
|
302
|
+
if (mech !== "EXTERNAL") {
|
|
303
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/cleartext-auth",
|
|
304
|
+
"guardManageSieveCommand.validate: AUTHENTICATE " + mech +
|
|
305
|
+
" refused over cleartext (use STARTTLS first; RFC 5804 §1.1 + RFC 4954 §4)");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
var literalBytes = null;
|
|
310
|
+
var literalPlus = false;
|
|
311
|
+
if (trailing) {
|
|
312
|
+
// Optional initial-response — either `{N+?}` literal or a quoted
|
|
313
|
+
// base64 string.
|
|
314
|
+
var lit = LITERAL_RE.exec(trailing); // allow:regex-no-length-cap — LITERAL_RE anchored + bounded digits
|
|
315
|
+
if (lit) {
|
|
316
|
+
var n = parseInt(lit[1], 10);
|
|
317
|
+
var isPlus = lit[2] === "+";
|
|
318
|
+
if (isPlus && !caps.allowLiteralPlus) {
|
|
319
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/literal-plus-refused",
|
|
320
|
+
"guardManageSieveCommand.validate: LITERAL+ refused under profile '" + profileName + "'");
|
|
321
|
+
}
|
|
322
|
+
// Base64-initial-response cap: bound by the script-name cap
|
|
323
|
+
// (initial-response is a SASL token, not a script body; 4 KiB
|
|
324
|
+
// is generous).
|
|
325
|
+
if (n > 4096) { // allow:raw-byte-literal — 4 KiB SASL initial-response cap
|
|
326
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/literal-too-large",
|
|
327
|
+
"guardManageSieveCommand.validate: AUTHENTICATE initial-response " +
|
|
328
|
+
n + " bytes exceeds 4096-byte cap");
|
|
329
|
+
}
|
|
330
|
+
literalBytes = n;
|
|
331
|
+
literalPlus = isPlus;
|
|
332
|
+
} else {
|
|
333
|
+
var inner = _parseQuotedString(trailing);
|
|
334
|
+
if (inner === null || inner.rest.length > 0) {
|
|
335
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/bad-initial-response",
|
|
336
|
+
"guardManageSieveCommand.validate: AUTHENTICATE initial-response must be a " +
|
|
337
|
+
"literal `{N}` / `{N+}` or quoted base64 string");
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return { verb: "AUTHENTICATE", args: [mech], literalBytes: literalBytes, literalPlus: literalPlus };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function _validateNoop(rest, caps) {
|
|
345
|
+
if (!rest) return { verb: "NOOP", args: [] };
|
|
346
|
+
var parsed = _parseQuotedString(rest);
|
|
347
|
+
if (parsed === null || parsed.rest.length > 0) {
|
|
348
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/bad-noop-arg",
|
|
349
|
+
"guardManageSieveCommand.validate: NOOP optional arg must be a quoted string");
|
|
350
|
+
}
|
|
351
|
+
_checkScriptNameBytes(parsed.value, caps);
|
|
352
|
+
return { verb: "NOOP", args: [parsed.value] };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function _validateHavespace(rest, caps) {
|
|
356
|
+
if (!rest) {
|
|
357
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/bad-havespace",
|
|
358
|
+
"guardManageSieveCommand.validate: HAVESPACE requires `\"name\" size`");
|
|
359
|
+
}
|
|
360
|
+
var parsed = _parseQuotedString(rest);
|
|
361
|
+
if (parsed === null) {
|
|
362
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/bad-havespace",
|
|
363
|
+
"guardManageSieveCommand.validate: HAVESPACE script-name must be a quoted string");
|
|
364
|
+
}
|
|
365
|
+
_checkScriptName(parsed.value, caps);
|
|
366
|
+
var sizeStr = parsed.rest;
|
|
367
|
+
if (!sizeStr || !NUM_RE.test(sizeStr)) { // allow:regex-no-length-cap — NUM_RE anchored + bounded
|
|
368
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/bad-havespace",
|
|
369
|
+
"guardManageSieveCommand.validate: HAVESPACE size must be a positive decimal integer");
|
|
370
|
+
}
|
|
371
|
+
var size = parseInt(sizeStr, 10);
|
|
372
|
+
if (size > caps.maxScriptBytes) {
|
|
373
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/script-too-large",
|
|
374
|
+
"guardManageSieveCommand.validate: HAVESPACE size " + size +
|
|
375
|
+
" bytes exceeds cap " + caps.maxScriptBytes);
|
|
376
|
+
}
|
|
377
|
+
return { verb: "HAVESPACE", args: [parsed.value, size] };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function _validatePutscript(rest, caps) {
|
|
381
|
+
if (!rest) {
|
|
382
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/bad-putscript",
|
|
383
|
+
"guardManageSieveCommand.validate: PUTSCRIPT requires `\"name\" {N[+]}`");
|
|
384
|
+
}
|
|
385
|
+
var parsed = _parseQuotedString(rest);
|
|
386
|
+
if (parsed === null) {
|
|
387
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/bad-putscript",
|
|
388
|
+
"guardManageSieveCommand.validate: PUTSCRIPT script-name must be a quoted string");
|
|
389
|
+
}
|
|
390
|
+
_checkScriptName(parsed.value, caps);
|
|
391
|
+
var litStr = parsed.rest;
|
|
392
|
+
if (!litStr) {
|
|
393
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/bad-putscript",
|
|
394
|
+
"guardManageSieveCommand.validate: PUTSCRIPT requires a literal `{N}` or `{N+}` payload announcement");
|
|
395
|
+
}
|
|
396
|
+
var m = LITERAL_RE.exec(litStr); // allow:regex-no-length-cap — LITERAL_RE anchored + bounded digits
|
|
397
|
+
if (!m) {
|
|
398
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/bad-literal",
|
|
399
|
+
"guardManageSieveCommand.validate: PUTSCRIPT literal must match `{N}` or `{N+}` (RFC 5804 §2.3 + RFC 7888)");
|
|
400
|
+
}
|
|
401
|
+
var n = parseInt(m[1], 10);
|
|
402
|
+
var isPlus = m[2] === "+";
|
|
403
|
+
if (isPlus && !caps.allowLiteralPlus) {
|
|
404
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/literal-plus-refused",
|
|
405
|
+
"guardManageSieveCommand.validate: LITERAL+ refused under current profile");
|
|
406
|
+
}
|
|
407
|
+
if (n > caps.maxScriptBytes) {
|
|
408
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/script-too-large",
|
|
409
|
+
"guardManageSieveCommand.validate: PUTSCRIPT script " + n +
|
|
410
|
+
" bytes exceeds cap " + caps.maxScriptBytes);
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
verb: "PUTSCRIPT",
|
|
414
|
+
args: [parsed.value],
|
|
415
|
+
literalBytes: n,
|
|
416
|
+
literalPlus: isPlus,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function _validateSingleName(verb, rest, caps, nameOpts) {
|
|
421
|
+
if (!rest) {
|
|
422
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/missing-name",
|
|
423
|
+
"guardManageSieveCommand.validate: " + verb + " requires a quoted script-name argument");
|
|
424
|
+
}
|
|
425
|
+
var parsed = _parseQuotedString(rest);
|
|
426
|
+
if (parsed === null || parsed.rest.length > 0) {
|
|
427
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/bad-name",
|
|
428
|
+
"guardManageSieveCommand.validate: " + verb + " script-name must be a quoted string");
|
|
429
|
+
}
|
|
430
|
+
if (parsed.value.length === 0) {
|
|
431
|
+
if (!nameOpts.allowEmpty) {
|
|
432
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/empty-name",
|
|
433
|
+
"guardManageSieveCommand.validate: " + verb + " script-name must be non-empty");
|
|
434
|
+
}
|
|
435
|
+
return { verb: verb, args: [parsed.value] };
|
|
436
|
+
}
|
|
437
|
+
_checkScriptName(parsed.value, caps);
|
|
438
|
+
return { verb: verb, args: [parsed.value] };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function _validateRenamescript(rest, caps) {
|
|
442
|
+
if (!rest) {
|
|
443
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/bad-rename",
|
|
444
|
+
"guardManageSieveCommand.validate: RENAMESCRIPT requires `\"old\" \"new\"`");
|
|
445
|
+
}
|
|
446
|
+
var first = _parseQuotedString(rest);
|
|
447
|
+
if (first === null || !first.rest) {
|
|
448
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/bad-rename",
|
|
449
|
+
"guardManageSieveCommand.validate: RENAMESCRIPT requires two quoted script-name arguments");
|
|
450
|
+
}
|
|
451
|
+
_checkScriptName(first.value, caps);
|
|
452
|
+
var second = _parseQuotedString(first.rest);
|
|
453
|
+
if (second === null || second.rest.length > 0) {
|
|
454
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/bad-rename",
|
|
455
|
+
"guardManageSieveCommand.validate: RENAMESCRIPT second arg must be a quoted script-name");
|
|
456
|
+
}
|
|
457
|
+
_checkScriptName(second.value, caps);
|
|
458
|
+
return { verb: "RENAMESCRIPT", args: [first.value, second.value] };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// _parseQuotedString — extract a leading `"..."` quoted string from
|
|
462
|
+
// `s` and return `{ value, rest }`, where `rest` is whitespace-trimmed.
|
|
463
|
+
// Returns null if `s` does not begin with a DQUOTE. RFC 5804 §1.2
|
|
464
|
+
// quoted strings allow UTF-8 content and `\"` / `\\` escape sequences.
|
|
465
|
+
function _parseQuotedString(s) {
|
|
466
|
+
if (s.length === 0 || s.charCodeAt(0) !== 0x22) return null; // allow:raw-byte-literal — DQUOTE
|
|
467
|
+
var out = "";
|
|
468
|
+
var i = 1;
|
|
469
|
+
while (i < s.length) {
|
|
470
|
+
var c = s.charCodeAt(i);
|
|
471
|
+
if (c === 0x5C) { // allow:raw-byte-literal — backslash escape
|
|
472
|
+
if (i + 1 >= s.length) return null;
|
|
473
|
+
var esc = s.charCodeAt(i + 1);
|
|
474
|
+
if (esc === 0x22) { out += '"'; i += 2; continue; } // allow:raw-byte-literal — DQUOTE
|
|
475
|
+
if (esc === 0x5C) { out += "\\"; i += 2; continue; } // allow:raw-byte-literal — backslash
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
if (c === 0x22) { // allow:raw-byte-literal — closing DQUOTE
|
|
479
|
+
var rest = s.slice(i + 1);
|
|
480
|
+
// Trim leading whitespace from rest.
|
|
481
|
+
var k = 0;
|
|
482
|
+
while (k < rest.length && (rest.charCodeAt(k) === 0x20 || rest.charCodeAt(k) === 0x09)) k += 1; // allow:raw-byte-literal — SP / HTAB
|
|
483
|
+
return { value: out, rest: rest.slice(k) };
|
|
484
|
+
}
|
|
485
|
+
if (c === 0x00 || c === 0x0D || c === 0x0A) return null; // allow:raw-byte-literal — NUL/CR/LF refused in quoted strings
|
|
486
|
+
out += s[i];
|
|
487
|
+
i += 1;
|
|
488
|
+
}
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// _isEscaped — DQUOTE at position i is escaped if preceded by an odd
|
|
493
|
+
// number of backslashes. Used by the outer control-byte walker so the
|
|
494
|
+
// in-quote flag doesn't flip on `\"` sequences.
|
|
495
|
+
function _isEscaped(line, i) {
|
|
496
|
+
var n = 0;
|
|
497
|
+
var j = i - 1;
|
|
498
|
+
while (j >= 0 && line.charCodeAt(j) === 0x5C) { n += 1; j -= 1; } // allow:raw-byte-literal — backslash count
|
|
499
|
+
return (n & 1) === 1;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// _checkScriptName — RFC 5804 §2.1: 1-512 octets, no NUL/CR/LF/slash/
|
|
503
|
+
// backslash/DQUOTE. The quoted-string parser already refuses NUL/CR/LF
|
|
504
|
+
// + the unescaped DQUOTE that would close the literal; here we
|
|
505
|
+
// additionally refuse the forward-slash + backslash that the
|
|
506
|
+
// quoted-string layer is happy with but RFC 5804 §2.1 explicitly
|
|
507
|
+
// forbids in the script-name production.
|
|
508
|
+
function _checkScriptName(name, caps) {
|
|
509
|
+
if (name.length === 0) {
|
|
510
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/empty-name",
|
|
511
|
+
"guardManageSieveCommand.validate: script-name must be non-empty (RFC 5804 §2.1)");
|
|
512
|
+
}
|
|
513
|
+
_checkScriptNameBytes(name, caps);
|
|
514
|
+
for (var i = 0; i < name.length; i += 1) {
|
|
515
|
+
var c = name.charCodeAt(i);
|
|
516
|
+
if (c === 0x2F || c === 0x5C) { // allow:raw-byte-literal — forward-slash + backslash refused
|
|
517
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/bad-name-byte",
|
|
518
|
+
"guardManageSieveCommand.validate: script-name byte 0x" +
|
|
519
|
+
c.toString(16) + " refused (RFC 5804 §2.1)"); // allow:raw-byte-literal — base-16 toString radix
|
|
520
|
+
}
|
|
521
|
+
if (c === 0x00) { // allow:raw-byte-literal — NUL refused
|
|
522
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/bad-name-byte",
|
|
523
|
+
"guardManageSieveCommand.validate: NUL byte refused in script-name (RFC 5804 §2.1)");
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function _checkScriptNameBytes(name, caps) {
|
|
529
|
+
// RFC 5804 §2.1 script-name limits are octet-based, not UTF-16
|
|
530
|
+
// code-unit based. Use Buffer.byteLength so non-ASCII script names
|
|
531
|
+
// (multibyte UTF-8) honor the byte cap and downstream
|
|
532
|
+
// filesystem/storage backends never see a name longer than the
|
|
533
|
+
// advertised limit.
|
|
534
|
+
var byteLen = Buffer.byteLength(name, "utf8");
|
|
535
|
+
if (byteLen > caps.maxScriptNameBytes) {
|
|
536
|
+
throw new GuardManageSieveCommandError("guard-managesieve-command/name-too-long",
|
|
537
|
+
"guardManageSieveCommand.validate: script-name " + byteLen +
|
|
538
|
+
" UTF-8 bytes exceeds cap " + caps.maxScriptNameBytes + " (RFC 5804 §2.1)");
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* @primitive b.guardManageSieveCommand.compliancePosture
|
|
544
|
+
* @signature b.guardManageSieveCommand.compliancePosture(posture)
|
|
545
|
+
* @since 0.9.57
|
|
546
|
+
* @status stable
|
|
547
|
+
*
|
|
548
|
+
* Return the effective profile for a compliance posture, or `null`
|
|
549
|
+
* for unknown names.
|
|
550
|
+
*
|
|
551
|
+
* @example
|
|
552
|
+
* b.guardManageSieveCommand.compliancePosture("hipaa"); // → "strict"
|
|
553
|
+
*/
|
|
554
|
+
function compliancePosture(posture) {
|
|
555
|
+
return COMPLIANCE_POSTURES[posture] || null;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
module.exports = {
|
|
559
|
+
validate: validate,
|
|
560
|
+
compliancePosture: compliancePosture,
|
|
561
|
+
PROFILES: PROFILES,
|
|
562
|
+
COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
|
|
563
|
+
KNOWN_VERBS: KNOWN_VERBS,
|
|
564
|
+
ZERO_ARG_VERBS: ZERO_ARG_VERBS,
|
|
565
|
+
GuardManageSieveCommandError: GuardManageSieveCommandError,
|
|
566
|
+
};
|