@blamejs/core 0.7.51 → 0.7.61
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 +20 -0
- package/index.js +16 -0
- package/lib/crypto.js +125 -0
- package/lib/framework-error.js +62 -0
- package/lib/guard-all.js +8 -0
- package/lib/guard-auth.js +278 -0
- package/lib/guard-graphql.js +461 -0
- package/lib/guard-image.js +371 -0
- package/lib/guard-jsonpath.js +300 -0
- package/lib/guard-pdf.js +345 -0
- package/lib/guard-regex.js +287 -0
- package/lib/guard-shell.js +319 -0
- package/lib/guard-template.js +279 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,26 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.7.x
|
|
10
10
|
|
|
11
|
+
- **0.7.61** (2026-05-06) — eslint cleanup in `lib/guard-regex.js` and `lib/guard-shell.js`. The `no-useless-escape` rule (eslint v9+) flagged unnecessary backslashes inside regex character classes — `*`, `+`, `?`, `[` don't need escaping when they appear inside `[...]`. Behavior unchanged: regex semantics are identical with or without the escapes (the engine treats both forms as the literal character). The framework's CI gate runs eslint with `--max-warnings 0`; this slice unblocks the CI lint job that's been failing on tag pushes since v0.7.53. No operator-facing behavior change.
|
|
12
|
+
|
|
13
|
+
- **0.7.60** (2026-05-05) — `b.crypto.encryptEnvelopeAsCertPeer` + `b.crypto.decryptEnvelopeAsCertPeer` — cert-bound envelope primitives. The default `b.crypto.encrypt` / `b.crypto.decrypt` source the recipient from a published framework keypair (operator owns both halves); the new cert-peer variants source the recipient's ECDH P-384 half from a TLS peer cert plus a peer-supplied ML-KEM-1024 pubkey. Wire format unchanged — the envelope dispatches on the same version bytes and KEM ID; only the input keys differ. Use cases: sealed-storage records with peer recipients (operator A seals to operator B's TLS cert + KEM pubkey), cross-service messages between cert-identified peers without a shared framework keypair, audit log entries tagged with peer recipients. The encrypt path extracts the cert's SPKI as P-384 ECDH pubkey and refuses with `crypto/cert-key-not-ecdh-p384` if the cert isn't `id-ecPublicKey` over `secp384r1`; the decrypt path accepts either a `KeyObject` or a PEM string for `certPrivateKey` and applies the same curve check. Math is the existing hybrid ML-KEM-1024 + P-384 ECDH + SHAKE256 + XChaCha20-Poly1305 — these are convenience wrappers, not new crypto.
|
|
14
|
+
|
|
15
|
+
- **0.7.59** (2026-05-05) — `b.guardAuth` — composite auth-bundle safety primitive (KIND="auth-bundle"). Composes `guardJwt` + `guardOauth` + `b.cookies.parseSafe` + light header-smuggling detection into a single auth-flow gate. Consumes `ctx.authBundle` shape `{ jwtToken?, oauthFlow?, cookieHeader?, requestHeaders? }`. Each sub-validator runs independently; aggregated issues carry a `source` field (`"jwt"` / `"oauth"` / `"cookies"` / `"headers"` / `"auth"`) so operators see which sub-guard raised which issue. Sub-guards run at the operator's chosen `childProfile` (default tracks the auth profile). `requireAtLeastOne: true` (strict default) refuses empty bundles to defend against the operator-misuse class where the gate runs but no auth input is supplied. Profiles: `strict` (childProfile=strict, requireAtLeastOne), `balanced` (childProfile=balanced, allow empty), `permissive` (childProfile=permissive, allow empty). Postures: `hipaa` / `pci-dss` / `soc2` strict overlay; `gdpr` balanced overlay. Adaptive integration harness gains a KIND="auth-bundle" dispatcher reading `ctx.authBundle`. Auto-registers into `b.guardAll` as a STANDALONE_GUARD.
|
|
16
|
+
|
|
17
|
+
- **0.7.58** (2026-05-05) — `b.guardPdf` — PDF identifier-safety primitive (KIND="metadata"). Validates PDF inputs without vendoring a full parser; operators bring their own PDF library (pdf-lib, pdfjs-dist, vendored mupdf) and feed structural metadata to the guard for policy enforcement. Threat catalog: magic-byte missing (`%PDF-` header check); JavaScript action (`/JS` / `/JavaScript` — RCE class in vulnerable readers, Adobe / Foxit / nitro CVEs — universally refused); LaunchAction (`/Launch` invokes external program — universally refused); OpenAction trigger (drive-by class when paired with JavaScript / Launch); embedded files (`/EmbeddedFile` — may carry executable payloads); encrypted PDF refuse (many AV / sandbox tools can't scan); polyglot signal via operator-supplied `polyglotDetected: true`; oversized bytes / page count / embedded-file count caps. **`b.guardPdf.inspectMagic(bytes)`** is the operator helper that returns `true` when bytes start with `%PDF-`. Profiles: `strict` (max 500 pages, 0 embedded files, 64 MiB), `balanced` (max 5000 pages, 10 embedded files, 128 MiB; audit non-RCE classes), `permissive` (max 50000 pages, 100 embedded files, 512 MiB; allow encrypted). Postures: `hipaa` / `pci-dss` / `soc2` strict overlay; `gdpr` balanced overlay. Auto-registers into `b.guardAll` as a STANDALONE_GUARD. Group F of the original guard-family expansion plan complete.
|
|
18
|
+
|
|
19
|
+
- **0.7.57** (2026-05-05) — `b.guardImage` — image-format identifier-safety primitive (KIND="metadata"). Validates image-format inputs without vendoring a full decoder; the framework's stance is operators bring their own decoder (sharp, jimp, libvips wrappers, etc.) and `guardImage` closes the magic-byte vs declared-Content-Type mismatch class plus operator-supplied metadata bounds. Magic-byte detection covers PNG / JPEG / GIF (87a + 89a) / WebP (RIFF + WEBP marker) / BMP / ICO / TIFF (II + MM) / AVIF / HEIC + SVG-routing detection. Threat catalog: magic-byte vs declared-MIME mismatch (drive-by content-type confusion class — `Content-Type: image/png` with JPEG bytes); polyglot file (multiple format magic bytes detected — PHP-in-JPEG / JS-in-PNG class); unknown / no magic-byte match (refused at strict, audited at balanced+); SVG-routing-via-image bypass (operators MUST pass SVG through `b.guardSvg` directly — strict + balanced + permissive all refuse); oversized dimensions (operator passes `width` / `height`; refused against `maxWidth` / `maxHeight`); excessive frame count for animated formats; oversized bytes. **`b.guardImage.inspectMagic(bytes)`** is the operator helper that returns the detected MIME types without running the full validate / gate flow. Profiles: `strict` (8192×8192, 60 frames, 32 MiB), `balanced` (16384×16384, 200 frames, 64 MiB), `permissive` (65536×65536, 1000 frames, 256 MiB). Postures: `hipaa` / `pci-dss` / `soc2` strict overlay; `gdpr` balanced overlay. Adaptive integration harness gains a KIND="metadata" dispatcher reading `ctx.metadata`. Auto-registers into `b.guardAll` as a STANDALONE_GUARD.
|
|
20
|
+
|
|
21
|
+
- **0.7.56** (2026-05-05) — `b.guardTemplate` — Server-Side Template Injection (SSTI) identifier-safety primitive (KIND="identifier"). Detects template-engine syntax in user-input strings before they're rendered through any template engine; refused by default at every profile because operator-untrusted input rarely legitimately contains template syntax. Threat catalog: Jinja / Django / Twig / Liquid / Handlebars / AngularJS `{{...}}` + `{%...%}` (CVE-2024-22195 Jinja `xml_attr` filter, CVE-2024-26139 Bottle, CVE-2024-23348 Pyrogram); ERB / Tornado `<%...%>` and `<%=...%>`; Pug `#{...}` + `!{...}` interpolation; Mako / Velocity / Tornado `${...}` interpolation; Velocity directives (`#set` / `#if` / `#else` / `#elseif` / `#end` / `#foreach` / `#parse` / `#include` / `#stop`); BIDI / null / control / zero-width universal refuse. Profiles: `strict` (refuse everything), `balanced` (Jinja / ERB / Pug / Velocity directives still refused; audit `${...}` since it can also be a JS template-literal in some operator contexts), `permissive` (universal SSTI shapes still refused; audit `${...}` + Velocity directives). Postures: `hipaa` / `pci-dss` / `soc2` strict overlay; `gdpr` balanced overlay. Auto-registers into `b.guardAll` as a STANDALONE_GUARD. Group E of the original guard-family expansion plan complete.
|
|
22
|
+
|
|
23
|
+
- **0.7.55** (2026-05-05) — `b.guardJsonpath` — JSONPath identifier-safety primitive (KIND="identifier"). Validates user-supplied JSONPath strings (RFC 9535) before they're handed to a JSONPath evaluator. Many JSONPath libraries (notably the original Stefan Goessner implementation and several JS forks) route filter / script expressions through dynamic-code execution, turning a query path into an RCE primitive. Threat catalog: filter expression `?(...)` (dynamic-code-execution class — universally refused); script expression `(@.x)` style; JS-source hint detection (path containing tokens that only appear in code-injection attempts — dynamic-code-exec keyword, constructor invocation keyword, function-declaration keyword, arrow-function arrow, statement-separator semicolon — all built from explicit substrings to keep the source file free of the literal keywords); recursive-descent depth bombs (operator-tunable `maxRecursiveDescents`); excessive bracket nesting; oversized pattern; BIDI / null / control / zero-width universal refuse. Profiles: `strict` (refuse everything), `balanced` (RCE class refused; audit bracket nesting + recursive descent), `permissive` (RCE class still refused; allow recursive descent up to 16). Postures: `hipaa` / `pci-dss` / `soc2` strict overlay; `gdpr` balanced overlay. Auto-registers into `b.guardAll` as a STANDALONE_GUARD.
|
|
24
|
+
|
|
25
|
+
- **0.7.54** (2026-05-05) — `b.guardRegex` — regex pattern identifier-safety primitive (KIND="identifier"). Validates user-supplied regex pattern strings for catastrophic-backtracking (ReDoS) shapes BEFORE compilation. Threat catalog: nested quantifiers (canonical ReDoS class — `(a+)+`, `(a*)+`, `(.+)+` — CVE-2024-21538 cross-spawn / CVE-2022-25929 chartjs-adapter-luxon are recent prominent examples; universally refused at every profile); alternation with quantifier (`(a|b)+`); bounded-repeat upper-bound overflow (operator-tunable `maxBoundedRepeat` — strict 100 / balanced 1000 / permissive 10000); lookaround with internal quantifier; oversized pattern; BIDI / null / control / zero-width universal refuse. Profiles: `strict` (refuse everything), `balanced` (nested quants still refused; audit the rest), `permissive` (nested quants + codepoint class still refused; allow alternation-quant). Postures: `hipaa` / `pci-dss` / `soc2` strict overlay; `gdpr` balanced overlay. Auto-registers into `b.guardAll` as a STANDALONE_GUARD.
|
|
26
|
+
|
|
27
|
+
- **0.7.53** (2026-05-05) — `b.guardShell` — shell-argument identifier-safety primitive (KIND="identifier"). Validates user-input strings BEFORE they're handed to a child-process spawn. The canonical defense remains "use array args + `shell: false`", but operators still receive operator-untrusted strings that flow through path-arg or arg-list shapes — `guardShell` refuses obvious shell-injection shapes before the spawn call. Threat catalog: POSIX shell metacharacters (`;`, `&`, `|`, `<`, `>`, `(`, `)`, `{`, `}`, `[`, `]`, `*`, `?`, `~`, `!`, `#`, `\`, single+double quotes), cmd.exe metacharacters (`&`, `|`, `<`, `>`, `^`, `%`, `"`, `'`, `(`, `)`, `,`, `;`, `=`), `$(...)` + `${VAR}` command + parameter substitution, backtick command substitution, `<(...)` / `>(...)` Bash process substitution, `$VAR` parameter expansion, newline / NUL injection (line-splitting class), leading-hyphen option-flag injection (operator opt-in to refuse args starting with `-` — defends against `-rf` / `--exec` shapes interpreted as option flags), BIDI / null / control / zero-width universal refuse. Profiles: `strict` (refuse everything), `balanced` (universal-refuse class only — substitutions / newline / BIDI / null still refused; metacharacters audited), `permissive` (universal-refuse class still refused; metacharacters audited; leading hyphen allowed). Postures: `hipaa` / `pci-dss` / `soc2` strict overlay; `gdpr` balanced overlay. Auto-registers into `b.guardAll` as a STANDALONE_GUARD.
|
|
28
|
+
|
|
29
|
+
- **0.7.52** (2026-05-05) — `b.guardGraphql` — GraphQL request-shape safety primitive (KIND="graphql-request"). Validates user-supplied GraphQL request bundles against the canonical query-shape DoS catalog before the framework hands the query to a schema-aware executor. Threat catalog: query depth bombs (N² query-shape DoS — caps at strict 8 / balanced 12 / permissive 24); alias-bomb breadth DoS (caps at 8/16/32 aliases per selection-set); introspection in production (`__schema` / `__type` schema-leak); batch query DoS (caps at 1/10/50 entries per array); persisted-query enforcement (refuse free-form queries when `persistedQueryPolicy: "require"`); operation-name allowlist drift; variable type confusion (operator declares `variableShapes: { id: "string", limit: "number" }`); oversized query / variable / total bytes; BIDI / null / control / zero-width universal refuse on the query string. Brace-counting query-shape walker handles strings + comments correctly without requiring a full GraphQL parser. Profiles: `strict` (refuse introspection + batch + shape-DoS), `balanced` (audit most, allow batch up to 10), `permissive` (universal-refuse class still refused; rest allow / audit). Postures: `hipaa` / `pci-dss` / `soc2` strict overlay; `gdpr` balanced overlay. Adaptive integration harness gains a KIND="graphql-request" dispatcher reading `ctx.graphqlRequest`. Auto-registers into `b.guardAll` as a STANDALONE_GUARD.
|
|
30
|
+
|
|
11
31
|
- **0.7.51** (2026-05-05) — `b.guardOauth` — OAuth flow-shape safety primitive (KIND="oauth-flow"). Validates user-supplied OAuth 2.x / OIDC authorization-code-flow parameter bundles before the framework's `b.auth.oauth` client exchanges them. Threat catalog: PKCE missing or non-S256 (RFC 7636 / OAuth 2.1 mandate; `plain` is the downgrade-attack class); state missing (RFC 6749 §10.12 CSRF); redirect_uri not in operator allowlist (exact-match per OAuth 2.1 — no prefix / wildcard / scheme drift); response_type allowlist drift (refuse implicit `token` deprecated in OAuth 2.1, require operator-allowed types); scope-token shape per RFC 6749 §3.3 (refuse non-printable / control / whitespace-other-than-space); issuer missing on callback (RFC 9207 IdP-mix-up defense — set `flow._isCallback = true` to enforce); authorization-code reuse via operator-supplied `seenCodeStore.hasSeen(code)` (RFC 6749 §10.5 replay class); excessive parameter / total bytes; BIDI / null / control / zero-width universal refuse. Profiles: `strict` (PKCE S256, state required, redirect_uri exact-match, RFC 9207 iss required), `balanced` (PKCE any method, state + redirect_uri allowlist required, iss audited), `permissive` (universal-refuse class still refused; rest audit). Postures: `hipaa` / `pci-dss` / `soc2` strict overlay; `gdpr` balanced overlay. Adaptive integration harness gains a KIND="oauth-flow" dispatcher reading `ctx.oauthFlow`. Auto-registers into `b.guardAll` as a STANDALONE_GUARD.
|
|
12
32
|
|
|
13
33
|
- **0.7.50** (2026-05-05) — `b.guardJwt` — JWT identifier-safety primitive (KIND="identifier"). Validates user-supplied JWT compact-serialization strings against the canonical CVE-class refuse list before hand-off to a verifier — `b.guardJwt` never replaces signature verification, it reduces the input space the verifier sees. Threat catalog: shape malformation; `alg=none` (CVE-2015-9235 jsonwebtoken / CVE-2018-0114 java-jwt) — universally refused at every profile; alg-allowlist drift (PQC-first default: ML-DSA / SLH-DSA / EdDSA / ES* / RS* / PS*); `kid` path-traversal (operator `keyResolver` path-injection class — e.g. `kid: "../etc/passwd"` would escape a key-directory `fs.readFile(keyDir + kid)` resolver); `typ` confusion (non-JWT-shape media-type tokens coerced into the slot); oversized header / payload / signature segment defense (decompression bomb + parser DoS); `exp` / `nbf` / `iat` sanity (past `exp` = replay; far-future `nbf` / `iat` = clock-skew / attacker-shaped); required-claims enforcement (default `iss` / `exp` / `iat` at strict); unknown `crit` field refuse (RFC 7515 §4.1.11 — operator MUST refuse crit values it doesn't understand); BIDI / null / control / zero-width universal refuse. **`b.guardJwt.kidSafe(kid)`** is the documented contract for operator `keyResolver` implementations: throws on traversal indicators or control bytes, returns the validated kid on success. Profiles: `strict` (refuse everything), `balanced` (refuse alg=none / kid-traversal / unknown-crit; audit the rest), `permissive` (universal-refuse class still refused). Postures: `hipaa` / `pci-dss` / `soc2` strict overlay; `gdpr` balanced overlay. Auto-registers into `b.guardAll` as a STANDALONE_GUARD.
|
package/index.js
CHANGED
|
@@ -117,6 +117,14 @@ var guardTime = require("./lib/guard-time");
|
|
|
117
117
|
var guardMime = require("./lib/guard-mime");
|
|
118
118
|
var guardJwt = require("./lib/guard-jwt");
|
|
119
119
|
var guardOauth = require("./lib/guard-oauth");
|
|
120
|
+
var guardGraphql = require("./lib/guard-graphql");
|
|
121
|
+
var guardShell = require("./lib/guard-shell");
|
|
122
|
+
var guardRegex = require("./lib/guard-regex");
|
|
123
|
+
var guardJsonpath = require("./lib/guard-jsonpath");
|
|
124
|
+
var guardTemplate = require("./lib/guard-template");
|
|
125
|
+
var guardImage = require("./lib/guard-image");
|
|
126
|
+
var guardPdf = require("./lib/guard-pdf");
|
|
127
|
+
var guardAuth = require("./lib/guard-auth");
|
|
120
128
|
var guardAll = require("./lib/guard-all");
|
|
121
129
|
var ssrfGuard = require("./lib/ssrf-guard");
|
|
122
130
|
var authHeader = require("./lib/auth-header");
|
|
@@ -265,6 +273,14 @@ module.exports = {
|
|
|
265
273
|
guardMime: guardMime,
|
|
266
274
|
guardJwt: guardJwt,
|
|
267
275
|
guardOauth: guardOauth,
|
|
276
|
+
guardGraphql: guardGraphql,
|
|
277
|
+
guardShell: guardShell,
|
|
278
|
+
guardRegex: guardRegex,
|
|
279
|
+
guardJsonpath: guardJsonpath,
|
|
280
|
+
guardTemplate: guardTemplate,
|
|
281
|
+
guardImage: guardImage,
|
|
282
|
+
guardPdf: guardPdf,
|
|
283
|
+
guardAuth: guardAuth,
|
|
268
284
|
guardAll: guardAll,
|
|
269
285
|
ssrfGuard: ssrfGuard,
|
|
270
286
|
authHeader: authHeader,
|
package/lib/crypto.js
CHANGED
|
@@ -321,6 +321,129 @@ function encryptMlkem768X25519(plaintext, recipient) {
|
|
|
321
321
|
]).toString("base64");
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
// ---- Cert-peer envelope primitives ----
|
|
325
|
+
//
|
|
326
|
+
// The framework's default `encrypt` / `decrypt` source the recipient
|
|
327
|
+
// from a published framework keypair (operator owns both halves). The
|
|
328
|
+
// cert-peer variants source the recipient from a TLS peer cert (peer
|
|
329
|
+
// owns the ECDH P-384 half) plus a peer-supplied ML-KEM-1024 pubkey.
|
|
330
|
+
// Wire format is unchanged — the envelope dispatches on the same
|
|
331
|
+
// version bytes and KEM ID. Only the input keys differ.
|
|
332
|
+
//
|
|
333
|
+
// Use cases beyond the b.middleware.apiEncrypt strategy:
|
|
334
|
+
// - Sealed-storage records with peer recipients (operator A seals
|
|
335
|
+
// to operator B's TLS cert + KEM pubkey).
|
|
336
|
+
// - Cross-service messages between cert-identified peers without
|
|
337
|
+
// a shared framework keypair.
|
|
338
|
+
// - Audit log entries tagged with peer recipients.
|
|
339
|
+
|
|
340
|
+
function _extractEcdhP384FromCert(certDer) {
|
|
341
|
+
// The cert's SubjectPublicKeyInfo carries the ECDH P-384 pubkey when
|
|
342
|
+
// the cert is issued for that curve. node:crypto's X509Certificate
|
|
343
|
+
// exposes `publicKey` as a KeyObject; we only export the SPKI as PEM
|
|
344
|
+
// so the existing `encrypt` path consumes the same shape it accepts
|
|
345
|
+
// for `ecPublicKey`.
|
|
346
|
+
var cert = new nodeCrypto.X509Certificate(certDer);
|
|
347
|
+
var keyObj = cert.publicKey;
|
|
348
|
+
var details = keyObj.asymmetricKeyDetails || {};
|
|
349
|
+
if (keyObj.asymmetricKeyType !== "ec" ||
|
|
350
|
+
details.namedCurve !== "secp384r1") {
|
|
351
|
+
var err = new Error(
|
|
352
|
+
"cert public key is not ECDH P-384 (got asymmetricKeyType=" +
|
|
353
|
+
keyObj.asymmetricKeyType + ", namedCurve=" + details.namedCurve + ")");
|
|
354
|
+
err.code = "crypto/cert-key-not-ecdh-p384";
|
|
355
|
+
throw err;
|
|
356
|
+
}
|
|
357
|
+
return keyObj.export({ type: "spki", format: "pem" });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// encryptEnvelopeAsCertPeer — produce a cert-bound envelope for the
|
|
361
|
+
// peer identified by their TLS cert + ML-KEM-1024 pubkey.
|
|
362
|
+
//
|
|
363
|
+
// var envelope = b.crypto.encryptEnvelopeAsCertPeer(plaintext, {
|
|
364
|
+
// peerCertDer: Buffer | Uint8Array, // peer's TLS cert (DER)
|
|
365
|
+
// peerKemPubkey: string, // peer's ML-KEM-1024 pubkey PEM
|
|
366
|
+
// });
|
|
367
|
+
function encryptEnvelopeAsCertPeer(plaintext, opts) {
|
|
368
|
+
if (!opts || typeof opts !== "object") {
|
|
369
|
+
throw new Error("encryptEnvelopeAsCertPeer: opts object required");
|
|
370
|
+
}
|
|
371
|
+
if (!opts.peerCertDer) {
|
|
372
|
+
var e1 = new Error("peerCertDer required (peer's TLS cert as DER bytes)");
|
|
373
|
+
e1.code = "crypto/peer-cert-missing";
|
|
374
|
+
throw e1;
|
|
375
|
+
}
|
|
376
|
+
if (typeof opts.peerKemPubkey !== "string") { // allow:inline-require-non-empty-string-validation — crypto module avoids validateOpts dependency to stay minimal
|
|
377
|
+
var e2 = new Error("peerKemPubkey required (peer's ML-KEM-1024 pubkey PEM)");
|
|
378
|
+
e2.code = "crypto/peer-kem-pubkey-missing";
|
|
379
|
+
throw e2;
|
|
380
|
+
}
|
|
381
|
+
if (opts.peerKemPubkey.length === 0) {
|
|
382
|
+
var e2b = new Error("peerKemPubkey is empty");
|
|
383
|
+
e2b.code = "crypto/peer-kem-pubkey-missing";
|
|
384
|
+
throw e2b;
|
|
385
|
+
}
|
|
386
|
+
var ecPubPem = _extractEcdhP384FromCert(opts.peerCertDer);
|
|
387
|
+
return encrypt(plaintext, {
|
|
388
|
+
publicKey: opts.peerKemPubkey,
|
|
389
|
+
ecPublicKey: ecPubPem,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// decryptEnvelopeAsCertPeer — decrypt an envelope sealed to this
|
|
394
|
+
// operator's TLS cert ECDH-pubkey + ML-KEM-1024 pubkey.
|
|
395
|
+
//
|
|
396
|
+
// var plaintext = b.crypto.decryptEnvelopeAsCertPeer(envelope, {
|
|
397
|
+
// certPrivateKey: KeyObject | string, // this operator's cert P-384 priv
|
|
398
|
+
// kemSecret: string, // this operator's ML-KEM-1024 priv PEM
|
|
399
|
+
// });
|
|
400
|
+
function decryptEnvelopeAsCertPeer(envelope, opts) {
|
|
401
|
+
if (!opts || typeof opts !== "object") {
|
|
402
|
+
throw new Error("decryptEnvelopeAsCertPeer: opts object required");
|
|
403
|
+
}
|
|
404
|
+
if (!opts.certPrivateKey) {
|
|
405
|
+
var e1 = new Error("certPrivateKey required");
|
|
406
|
+
e1.code = "crypto/cert-private-key-missing";
|
|
407
|
+
throw e1;
|
|
408
|
+
}
|
|
409
|
+
if (typeof opts.kemSecret !== "string") { // allow:inline-require-non-empty-string-validation — crypto module avoids validateOpts dependency to stay minimal
|
|
410
|
+
var e2 = new Error("kemSecret required (operator's ML-KEM-1024 priv PEM)");
|
|
411
|
+
e2.code = "crypto/kem-secret-missing";
|
|
412
|
+
throw e2;
|
|
413
|
+
}
|
|
414
|
+
if (opts.kemSecret.length === 0) {
|
|
415
|
+
var e2b = new Error("kemSecret is empty");
|
|
416
|
+
e2b.code = "crypto/kem-secret-missing";
|
|
417
|
+
throw e2b;
|
|
418
|
+
}
|
|
419
|
+
// Normalize certPrivateKey to PEM string (existing decrypt accepts
|
|
420
|
+
// PEM string).
|
|
421
|
+
var ecPrivPem;
|
|
422
|
+
if (typeof opts.certPrivateKey === "string") {
|
|
423
|
+
ecPrivPem = opts.certPrivateKey;
|
|
424
|
+
} else if (typeof opts.certPrivateKey.export === "function") {
|
|
425
|
+
var details = opts.certPrivateKey.asymmetricKeyDetails || {};
|
|
426
|
+
if (opts.certPrivateKey.asymmetricKeyType !== "ec" ||
|
|
427
|
+
details.namedCurve !== "secp384r1") {
|
|
428
|
+
var e3 = new Error(
|
|
429
|
+
"certPrivateKey is not ECDH P-384 (got asymmetricKeyType=" +
|
|
430
|
+
opts.certPrivateKey.asymmetricKeyType + ", namedCurve=" +
|
|
431
|
+
details.namedCurve + ")");
|
|
432
|
+
e3.code = "crypto/cert-key-not-ecdh-p384";
|
|
433
|
+
throw e3;
|
|
434
|
+
}
|
|
435
|
+
ecPrivPem = opts.certPrivateKey.export({ type: "pkcs8", format: "pem" });
|
|
436
|
+
} else {
|
|
437
|
+
var e4 = new Error("certPrivateKey must be a KeyObject or PEM string");
|
|
438
|
+
e4.code = "crypto/cert-private-key-bad-shape";
|
|
439
|
+
throw e4;
|
|
440
|
+
}
|
|
441
|
+
return decrypt(envelope, {
|
|
442
|
+
privateKey: opts.kemSecret,
|
|
443
|
+
ecPrivateKey: ecPrivPem,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
324
447
|
// Operator-audit accessor — exposes every supported KEM hybrid for
|
|
325
448
|
// compliance audit visibility ("which envelopes does this deploy
|
|
326
449
|
// accept on decrypt?").
|
|
@@ -351,6 +474,8 @@ module.exports = {
|
|
|
351
474
|
encrypt: encrypt,
|
|
352
475
|
decrypt: decrypt,
|
|
353
476
|
encryptMlkem768X25519: encryptMlkem768X25519,
|
|
477
|
+
encryptEnvelopeAsCertPeer: encryptEnvelopeAsCertPeer,
|
|
478
|
+
decryptEnvelopeAsCertPeer: decryptEnvelopeAsCertPeer,
|
|
354
479
|
SUPPORTED_KEM_ALGORITHMS: SUPPORTED_KEM_ALGORITHMS,
|
|
355
480
|
// Symmetric buffer encrypt/decrypt
|
|
356
481
|
encryptPacked: encryptPacked,
|
package/lib/framework-error.js
CHANGED
|
@@ -306,6 +306,60 @@ var GuardJwtError = defineClass("GuardJwtError", { alwaysPermane
|
|
|
306
306
|
// BIDI / null / control / zero-width universal refuse.
|
|
307
307
|
// alwaysPermanent.
|
|
308
308
|
var GuardOauthError = defineClass("GuardOauthError", { alwaysPermanent: true });
|
|
309
|
+
// GuardGraphqlError covers GraphQL request-shape violations: query
|
|
310
|
+
// depth bombs (N² query-shape DoS), alias-bomb breadth DoS,
|
|
311
|
+
// introspection in production, batch-query DoS, persisted-query
|
|
312
|
+
// enforcement, operation-name allowlist drift, variable type
|
|
313
|
+
// confusion, oversized query / variable / total bytes, BIDI / null /
|
|
314
|
+
// control / zero-width universal refuse on the query string.
|
|
315
|
+
// alwaysPermanent.
|
|
316
|
+
var GuardGraphqlError = defineClass("GuardGraphqlError", { alwaysPermanent: true });
|
|
317
|
+
// GuardShellError covers shell-arg identifier violations: POSIX +
|
|
318
|
+
// cmd.exe metacharacters, $(...) / ${...} command + parameter
|
|
319
|
+
// substitution, backtick substitution, process substitution
|
|
320
|
+
// (`<(...)` / `>(...)`), `$VAR` parameter expansion, newline
|
|
321
|
+
// injection, leading-hyphen option-flag injection (`-rf` / `--exec`
|
|
322
|
+
// class), BIDI / null / control / zero-width universal refuse.
|
|
323
|
+
// alwaysPermanent.
|
|
324
|
+
var GuardShellError = defineClass("GuardShellError", { alwaysPermanent: true });
|
|
325
|
+
// GuardRegexError covers regex-pattern identifier violations: nested
|
|
326
|
+
// quantifier ReDoS class (CVE-2024-21538 / CVE-2022-25929), alternation
|
|
327
|
+
// with quantifier, bounded-repeat upper-bound overflow, lookaround
|
|
328
|
+
// with internal quantifier, oversized pattern, BIDI / null / control /
|
|
329
|
+
// zero-width universal refuse. alwaysPermanent.
|
|
330
|
+
var GuardRegexError = defineClass("GuardRegexError", { alwaysPermanent: true });
|
|
331
|
+
// GuardJsonpathError covers JSONPath identifier violations: filter
|
|
332
|
+
// expression (`?(...)` — RCE class in eval-based implementations),
|
|
333
|
+
// script expression, JS-source hints (`eval` / `new` / `function` /
|
|
334
|
+
// `=>` / `;`), excessive bracket nesting, recursive-descent depth
|
|
335
|
+
// bombs, oversized pattern, BIDI / null / control / zero-width
|
|
336
|
+
// universal refuse. alwaysPermanent.
|
|
337
|
+
var GuardJsonpathError = defineClass("GuardJsonpathError", { alwaysPermanent: true });
|
|
338
|
+
// GuardTemplateError covers Server-Side Template Injection (SSTI)
|
|
339
|
+
// identifier violations: Jinja / Django / Twig / Liquid / Handlebars /
|
|
340
|
+
// AngularJS `{{...}}` + `{%...%}` shapes (CVE-2024-22195 / 26139 /
|
|
341
|
+
// 23348 class), ERB / Tornado `<%...%>`, Pug `#{...}` / `!{...}`
|
|
342
|
+
// interpolation, Mako / Velocity / Tornado `${...}`, Velocity
|
|
343
|
+
// directives (#set / #if / #foreach), BIDI / null / control / zero-
|
|
344
|
+
// width universal refuse. alwaysPermanent.
|
|
345
|
+
var GuardTemplateError = defineClass("GuardTemplateError", { alwaysPermanent: true });
|
|
346
|
+
// GuardImageError covers image-metadata violations: magic-byte vs
|
|
347
|
+
// declared-MIME mismatch (drive-by content-type confusion class),
|
|
348
|
+
// polyglot (multiple format magic bytes — PHP-in-JPEG / JS-in-PNG
|
|
349
|
+
// class), unknown magic-byte, SVG-routing-via-image bypass, oversized
|
|
350
|
+
// dimensions / frame count, oversized total bytes. alwaysPermanent.
|
|
351
|
+
var GuardImageError = defineClass("GuardImageError", { alwaysPermanent: true });
|
|
352
|
+
// GuardPdfError covers PDF-metadata violations: magic-byte missing,
|
|
353
|
+
// JavaScript action (`/JS` / `/JavaScript` — RCE class), Launch
|
|
354
|
+
// action, OpenAction trigger, embedded-file presence + count cap,
|
|
355
|
+
// encrypted PDF refuse, polyglot signal, oversized bytes / page
|
|
356
|
+
// count. alwaysPermanent.
|
|
357
|
+
var GuardPdfError = defineClass("GuardPdfError", { alwaysPermanent: true });
|
|
358
|
+
// GuardAuthError covers composite auth-bundle violations: aggregates
|
|
359
|
+
// guardJwt + guardOauth + b.cookies.parseSafe + light header-smuggling
|
|
360
|
+
// detection into a single gate with `source` tagging on each issue.
|
|
361
|
+
// alwaysPermanent.
|
|
362
|
+
var GuardAuthError = defineClass("GuardAuthError", { alwaysPermanent: true });
|
|
309
363
|
// DoraError covers DORA Article 17 incident-reporting workflow errors
|
|
310
364
|
// (classification refusal, report-shape validation, ESA-template
|
|
311
365
|
// generation, audit-chain integration). Permanent — these are
|
|
@@ -372,6 +426,14 @@ module.exports = {
|
|
|
372
426
|
GuardMimeError: GuardMimeError,
|
|
373
427
|
GuardJwtError: GuardJwtError,
|
|
374
428
|
GuardOauthError: GuardOauthError,
|
|
429
|
+
GuardGraphqlError: GuardGraphqlError,
|
|
430
|
+
GuardShellError: GuardShellError,
|
|
431
|
+
GuardRegexError: GuardRegexError,
|
|
432
|
+
GuardJsonpathError: GuardJsonpathError,
|
|
433
|
+
GuardTemplateError: GuardTemplateError,
|
|
434
|
+
GuardImageError: GuardImageError,
|
|
435
|
+
GuardPdfError: GuardPdfError,
|
|
436
|
+
GuardAuthError: GuardAuthError,
|
|
375
437
|
DoraError: DoraError,
|
|
376
438
|
ComplianceError: ComplianceError,
|
|
377
439
|
SmtpPolicyError: SmtpPolicyError,
|
package/lib/guard-all.js
CHANGED
|
@@ -96,6 +96,14 @@ var STANDALONE_GUARDS = [
|
|
|
96
96
|
require("./guard-mime"),
|
|
97
97
|
require("./guard-jwt"),
|
|
98
98
|
require("./guard-oauth"),
|
|
99
|
+
require("./guard-graphql"),
|
|
100
|
+
require("./guard-shell"),
|
|
101
|
+
require("./guard-regex"),
|
|
102
|
+
require("./guard-jsonpath"),
|
|
103
|
+
require("./guard-template"),
|
|
104
|
+
require("./guard-image"),
|
|
105
|
+
require("./guard-pdf"),
|
|
106
|
+
require("./guard-auth"),
|
|
99
107
|
];
|
|
100
108
|
|
|
101
109
|
// Framework-wide profile + posture vocabulary that every guard MUST
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* guard-auth — Composite auth-bundle safety primitive (b.guardAuth).
|
|
4
|
+
*
|
|
5
|
+
* Composes guardJwt + guardOauth + the cookie/header middleware
|
|
6
|
+
* validators into a single auth-flow check. KIND="auth-bundle" —
|
|
7
|
+
* consumes `ctx.authBundle` shape:
|
|
8
|
+
* {
|
|
9
|
+
* jwtToken?: string, // routed to guardJwt
|
|
10
|
+
* oauthFlow?: object, // routed to guardOauth
|
|
11
|
+
* cookieHeader?: string, // routed to b.cookies.parseSafe
|
|
12
|
+
* requestHeaders?: object, // routed through threat-detection
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* Each sub-validator runs independently; aggregated issues are
|
|
16
|
+
* returned with a `source` field tagging which sub-guard raised them.
|
|
17
|
+
* Operators get one gate to drop into a request lifecycle that covers
|
|
18
|
+
* the full canonical-auth threat surface.
|
|
19
|
+
*
|
|
20
|
+
* var rv = b.guardAuth.validate({
|
|
21
|
+
* jwtToken: bearerToken,
|
|
22
|
+
* oauthFlow: req.query,
|
|
23
|
+
* cookieHeader: req.headers.cookie,
|
|
24
|
+
* requestHeaders: req.headers,
|
|
25
|
+
* }, { profile: "strict" });
|
|
26
|
+
*
|
|
27
|
+
* var g = b.guardAuth.gate({ profile: "strict" });
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
var lazyRequire = require("./lazy-require");
|
|
31
|
+
var gateContract = require("./gate-contract");
|
|
32
|
+
var C = require("./constants");
|
|
33
|
+
var numericBounds = require("./numeric-bounds");
|
|
34
|
+
var guardJwt = require("./guard-jwt");
|
|
35
|
+
var guardOauth = require("./guard-oauth");
|
|
36
|
+
var cookies = require("./cookies");
|
|
37
|
+
var { GuardAuthError } = require("./framework-error");
|
|
38
|
+
|
|
39
|
+
var observability = lazyRequire(function () { return require("./observability"); });
|
|
40
|
+
void observability;
|
|
41
|
+
|
|
42
|
+
var _err = GuardAuthError.factory;
|
|
43
|
+
|
|
44
|
+
// ---- Profile presets ----
|
|
45
|
+
|
|
46
|
+
var PROFILES = Object.freeze({
|
|
47
|
+
"strict": {
|
|
48
|
+
bidiPolicy: "reject",
|
|
49
|
+
controlPolicy: "reject",
|
|
50
|
+
nullBytePolicy: "reject",
|
|
51
|
+
zeroWidthPolicy: "reject",
|
|
52
|
+
childProfile: "strict",
|
|
53
|
+
requireAtLeastOne: true,
|
|
54
|
+
maxBytes: C.BYTES.kib(64),
|
|
55
|
+
maxRuntimeMs: C.TIME.seconds(2),
|
|
56
|
+
},
|
|
57
|
+
"balanced": {
|
|
58
|
+
bidiPolicy: "reject",
|
|
59
|
+
controlPolicy: "reject",
|
|
60
|
+
nullBytePolicy: "reject",
|
|
61
|
+
zeroWidthPolicy: "reject",
|
|
62
|
+
childProfile: "balanced",
|
|
63
|
+
requireAtLeastOne: false,
|
|
64
|
+
maxBytes: C.BYTES.kib(128),
|
|
65
|
+
maxRuntimeMs: C.TIME.seconds(2),
|
|
66
|
+
},
|
|
67
|
+
"permissive": {
|
|
68
|
+
bidiPolicy: "reject", // BIDI refused at every profile
|
|
69
|
+
controlPolicy: "reject", // controls refused at every profile
|
|
70
|
+
nullBytePolicy: "reject", // null refused at every profile
|
|
71
|
+
zeroWidthPolicy: "reject", // zero-width refused at every profile
|
|
72
|
+
childProfile: "permissive",
|
|
73
|
+
requireAtLeastOne: false,
|
|
74
|
+
maxBytes: C.BYTES.kib(512),
|
|
75
|
+
maxRuntimeMs: C.TIME.seconds(2),
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
var DEFAULTS = Object.freeze(Object.assign({}, PROFILES["strict"], {
|
|
80
|
+
mode: "enforce",
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
var COMPLIANCE_POSTURES = Object.freeze({
|
|
84
|
+
"hipaa": Object.assign({}, PROFILES["strict"], {
|
|
85
|
+
forensicSnippetBytes: C.BYTES.bytes(512),
|
|
86
|
+
}),
|
|
87
|
+
"pci-dss": Object.assign({}, PROFILES["strict"], {
|
|
88
|
+
forensicSnippetBytes: C.BYTES.bytes(512),
|
|
89
|
+
}),
|
|
90
|
+
"gdpr": Object.assign({}, PROFILES["balanced"], {
|
|
91
|
+
forensicSnippetBytes: C.BYTES.bytes(256),
|
|
92
|
+
}),
|
|
93
|
+
"soc2": Object.assign({}, PROFILES["strict"], {
|
|
94
|
+
forensicSnippetBytes: C.BYTES.bytes(1024),
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
function _resolveOpts(opts) {
|
|
99
|
+
return gateContract.resolveProfileAndPosture(opts, {
|
|
100
|
+
profiles: PROFILES,
|
|
101
|
+
compliancePostures: COMPLIANCE_POSTURES,
|
|
102
|
+
defaults: DEFAULTS,
|
|
103
|
+
errorClass: GuardAuthError,
|
|
104
|
+
errCodePrefix: "auth",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function _detectIssues(bundle, opts) {
|
|
109
|
+
var issues = [];
|
|
110
|
+
if (!bundle || typeof bundle !== "object") {
|
|
111
|
+
return [{ kind: "bad-input", severity: "high",
|
|
112
|
+
ruleId: "auth.bad-input", source: "auth",
|
|
113
|
+
snippet: "auth bundle is not an object" }];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
var sawAny = false;
|
|
117
|
+
|
|
118
|
+
// JWT routing.
|
|
119
|
+
if (typeof bundle.jwtToken === "string" && bundle.jwtToken.length > 0) {
|
|
120
|
+
sawAny = true;
|
|
121
|
+
var jwtRv = guardJwt.validate(bundle.jwtToken,
|
|
122
|
+
{ profile: opts.childProfile });
|
|
123
|
+
for (var ji = 0; ji < jwtRv.issues.length; ji += 1) {
|
|
124
|
+
issues.push(Object.assign({}, jwtRv.issues[ji], { source: "jwt" }));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// OAuth routing.
|
|
129
|
+
if (bundle.oauthFlow && typeof bundle.oauthFlow === "object") {
|
|
130
|
+
sawAny = true;
|
|
131
|
+
var oauthRv = guardOauth.validate(bundle.oauthFlow,
|
|
132
|
+
Object.assign({ profile: opts.childProfile },
|
|
133
|
+
opts.allowedRedirectUris ?
|
|
134
|
+
{ allowedRedirectUris: opts.allowedRedirectUris } : {}));
|
|
135
|
+
for (var oi = 0; oi < oauthRv.issues.length; oi += 1) {
|
|
136
|
+
issues.push(Object.assign({}, oauthRv.issues[oi], { source: "oauth" }));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Cookie-header threat detection.
|
|
141
|
+
if (typeof bundle.cookieHeader === "string" && bundle.cookieHeader.length > 0) {
|
|
142
|
+
sawAny = true;
|
|
143
|
+
var ckRv = cookies.parseSafe(bundle.cookieHeader, {});
|
|
144
|
+
for (var ci = 0; ci < ckRv.issues.length; ci += 1) {
|
|
145
|
+
var iss = ckRv.issues[ci];
|
|
146
|
+
issues.push({
|
|
147
|
+
kind: "cookie-" + iss.kind,
|
|
148
|
+
severity: iss.severity,
|
|
149
|
+
source: "cookies",
|
|
150
|
+
ruleId: "auth.cookie-" + iss.kind,
|
|
151
|
+
snippet: iss.snippet,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Request-header threat detection — light shape (CRLF / token-grammar
|
|
157
|
+
// would otherwise be done by b.middleware.headers; here we sample
|
|
158
|
+
// for the high-severity classes only).
|
|
159
|
+
if (bundle.requestHeaders && typeof bundle.requestHeaders === "object") {
|
|
160
|
+
sawAny = true;
|
|
161
|
+
var rh = bundle.requestHeaders;
|
|
162
|
+
if (rh["content-length"] !== undefined &&
|
|
163
|
+
rh["transfer-encoding"] !== undefined) {
|
|
164
|
+
issues.push({
|
|
165
|
+
kind: "header-smuggling-cl-te", severity: "high",
|
|
166
|
+
source: "headers", ruleId: "auth.header-smuggling-cl-te",
|
|
167
|
+
snippet: "request carries both Content-Length and Transfer-" +
|
|
168
|
+
"Encoding (RFC 9112 §6.1 — CL.TE / TE.CL smuggling " +
|
|
169
|
+
"vector)",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (opts.requireAtLeastOne && !sawAny) {
|
|
175
|
+
issues.push({
|
|
176
|
+
kind: "no-auth-input", severity: "high",
|
|
177
|
+
source: "auth", ruleId: "auth.no-auth-input",
|
|
178
|
+
snippet: "auth bundle has no jwtToken / oauthFlow / cookieHeader / " +
|
|
179
|
+
"requestHeaders — strict requires at least one input",
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return issues;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function validate(input, opts) {
|
|
187
|
+
opts = _resolveOpts(opts);
|
|
188
|
+
numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
|
|
189
|
+
["maxBytes"],
|
|
190
|
+
"guardAuth.validate", GuardAuthError, "auth.bad-opt");
|
|
191
|
+
return gateContract.aggregateIssues(_detectIssues(input, opts));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function sanitize(input, opts) {
|
|
195
|
+
opts = _resolveOpts(opts);
|
|
196
|
+
if (!input || typeof input !== "object") {
|
|
197
|
+
throw _err("auth.bad-input", "sanitize requires bundle object");
|
|
198
|
+
}
|
|
199
|
+
var issues = _detectIssues(input, opts);
|
|
200
|
+
for (var i = 0; i < issues.length; i += 1) {
|
|
201
|
+
if (issues[i].severity === "critical" || issues[i].severity === "high") {
|
|
202
|
+
throw _err(issues[i].ruleId || "auth.refused",
|
|
203
|
+
"guardAuth.sanitize [" + issues[i].source + "]: " +
|
|
204
|
+
issues[i].snippet);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return input;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function gate(opts) {
|
|
211
|
+
opts = _resolveOpts(opts);
|
|
212
|
+
return gateContract.buildGuardGate(
|
|
213
|
+
opts.name || "guardAuth:" + (opts.profile || "default"),
|
|
214
|
+
opts,
|
|
215
|
+
async function (ctx) {
|
|
216
|
+
var bundle = ctx && (ctx.authBundle || ctx.auth);
|
|
217
|
+
if (!bundle) return { ok: true, action: "serve" };
|
|
218
|
+
var rv = validate(bundle, opts);
|
|
219
|
+
if (rv.issues.length === 0) return { ok: true, action: "serve" };
|
|
220
|
+
var hasCritical = rv.issues.some(function (i) {
|
|
221
|
+
return i.severity === "critical";
|
|
222
|
+
});
|
|
223
|
+
var hasHigh = rv.issues.some(function (i) {
|
|
224
|
+
return i.severity === "high";
|
|
225
|
+
});
|
|
226
|
+
if (!hasCritical && !hasHigh) {
|
|
227
|
+
return { ok: true, action: "audit-only", issues: rv.issues };
|
|
228
|
+
}
|
|
229
|
+
return { ok: false, action: "refuse", issues: rv.issues };
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
var buildProfile = gateContract.makeProfileBuilder(PROFILES);
|
|
234
|
+
|
|
235
|
+
function compliancePosture(name) {
|
|
236
|
+
return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES,
|
|
237
|
+
_err, "auth");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
var _authRulePacks = gateContract.makeRulePackLoader(GuardAuthError, "auth");
|
|
241
|
+
var loadRulePack = _authRulePacks.load;
|
|
242
|
+
|
|
243
|
+
module.exports = {
|
|
244
|
+
// ---- guard-* family registry exports ----
|
|
245
|
+
NAME: "auth",
|
|
246
|
+
KIND: "auth-bundle",
|
|
247
|
+
INTEGRATION_FIXTURES: Object.freeze({
|
|
248
|
+
kind: "auth-bundle",
|
|
249
|
+
benignBytes: Buffer.from(JSON.stringify({
|
|
250
|
+
jwtToken: "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9." +
|
|
251
|
+
"eyJpc3MiOiJleGFtcGxlIiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjE3MDAwMDAwMDB9.sig",
|
|
252
|
+
cookieHeader: "sid=abc123; theme=dark",
|
|
253
|
+
}), "utf8"),
|
|
254
|
+
hostileBytes: Buffer.from(JSON.stringify({
|
|
255
|
+
jwtToken: "eyJhbGciOiJub25lIn0.eyJzdWIiOiJ4In0.",
|
|
256
|
+
}), "utf8"),
|
|
257
|
+
benignAuthBundle: {
|
|
258
|
+
jwtToken: "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9." +
|
|
259
|
+
"eyJpc3MiOiJleGFtcGxlIiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjE3MDAwMDAwMDB9.sig",
|
|
260
|
+
cookieHeader: "sid=abc123; theme=dark",
|
|
261
|
+
},
|
|
262
|
+
// Hostile: alg=none JWT — universal refuse routed through guardJwt.
|
|
263
|
+
hostileAuthBundle: {
|
|
264
|
+
jwtToken: "eyJhbGciOiJub25lIn0.eyJzdWIiOiJ4In0.",
|
|
265
|
+
},
|
|
266
|
+
}),
|
|
267
|
+
// ---- primitive surface ----
|
|
268
|
+
validate: validate,
|
|
269
|
+
sanitize: sanitize,
|
|
270
|
+
gate: gate,
|
|
271
|
+
buildProfile: buildProfile,
|
|
272
|
+
compliancePosture: compliancePosture,
|
|
273
|
+
loadRulePack: loadRulePack,
|
|
274
|
+
PROFILES: PROFILES,
|
|
275
|
+
DEFAULTS: DEFAULTS,
|
|
276
|
+
COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
|
|
277
|
+
GuardAuthError: GuardAuthError,
|
|
278
|
+
};
|