@blamejs/core 0.8.43 → 0.8.49
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 +92 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/guard-yaml.js
CHANGED
|
@@ -1,59 +1,68 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* @module b.guardYaml
|
|
4
|
+
* @nav Guards
|
|
5
|
+
* @title Guard Yaml
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
* - CVE-2022-1471 SnakeYAML constructor RCE
|
|
13
|
-
* - CVE-2020-1747 / CVE-2020-14343 PyYAML FullLoader RCE chain
|
|
14
|
-
* - CVE-2017-18342 PyYAML python/object/apply RCE
|
|
7
|
+
* @intro
|
|
8
|
+
* YAML content-safety guard — defends against the type-coercion,
|
|
9
|
+
* deserialization, and DoS catalog operators face when accepting
|
|
10
|
+
* YAML sourced from user input. All detection runs at the SOURCE
|
|
11
|
+
* level: the operator's downstream parser may be PyYAML, SnakeYAML,
|
|
12
|
+
* js-yaml, libyaml, or another implementation, and the guard
|
|
13
|
+
* refuses hostile inputs before any parser sees them.
|
|
15
14
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* Tag-injection RCE defense: language-specific deserialization tag
|
|
16
|
+
* prefixes (`!!python/` / `!!java.` / `!!ruby/` / `!!perl/` /
|
|
17
|
+
* `!!js/` / `!!cs/` / `!!net/` / `!!system.`) plus the `!!apply` /
|
|
18
|
+
* `!!new` / `!!eval` / `!!exec` family are refused regardless of
|
|
19
|
+
* profile under strict. CVE coverage: CVE-2026-24009 Docling/PyYAML
|
|
20
|
+
* unsafe load, CVE-2025-68664 LangChain deserialization, CVE-2022-
|
|
21
|
+
* 1471 SnakeYAML constructor RCE, CVE-2020-1747 / CVE-2020-14343
|
|
22
|
+
* PyYAML FullLoader, CVE-2017-18342 python/object/apply.
|
|
19
23
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
24
|
+
* YAML 1.1 vs 1.2 type-coercion attacks: PyYAML and libyaml still
|
|
25
|
+
* default to YAML 1.1 in 2026, which treats unquoted `no` / `yes`
|
|
26
|
+
* / `y` / `n` / `on` / `off` as booleans (the "Norway problem" —
|
|
27
|
+
* country code "NO" parses as false), and `0777`-shaped numerics
|
|
28
|
+
* parse as octal. These shapes are flagged at the source so
|
|
29
|
+
* operators can refuse silently coerced values.
|
|
23
30
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
31
|
+
* Anchor-bomb (billion laughs) detection: `&anchor` declares,
|
|
32
|
+
* `*alias` references, recursive aliasing amplifies a small input
|
|
33
|
+
* into GiB on parse. Caps via `maxAnchors` + `maxAliasDepth` +
|
|
34
|
+
* `maxNodes`, plus an explicit alias-amplification ratio
|
|
35
|
+
* (aliases / anchors >= 8 fires `alias-explosion`) catches the
|
|
36
|
+
* exponential expansion shape independent of absolute counts.
|
|
37
|
+
* CVE-2026-27807 MarkUs / CVE-2025-61301 / CVE-2025-61303 ("Laughter
|
|
38
|
+
* in the Wild" — 14 libraries / 10 languages) exemplify the family.
|
|
28
39
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
40
|
+
* Custom-tag exec surface: local `!Foo` and global `!!Bar` user
|
|
41
|
+
* tags suggest a non-safe parser is downstream even when the tag
|
|
42
|
+
* isn't on the language-specific deserialization denylist. Flagged
|
|
43
|
+
* per profile.
|
|
33
44
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
45
|
+
* Merge-key chain DoS: `<<: *anchor` invokes the YAML 1.1 merge-
|
|
46
|
+
* key spec; chains of merge keys against deeply nested anchors are
|
|
47
|
+
* an additional anchor-chain expansion vector.
|
|
37
48
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
49
|
+
* Multi-document streams: operators expecting a single doc silently
|
|
50
|
+
* receive only the first one and ignore the rest — hostile content
|
|
51
|
+
* in subsequent docs slips past validation that ran on the first.
|
|
52
|
+
* The guard refuses `multiDocPolicy === "reject"` and caps via
|
|
53
|
+
* `maxDocuments`.
|
|
41
54
|
*
|
|
42
|
-
*
|
|
55
|
+
* Duplicate-key smuggling, BOM placement, and bidi / null / control
|
|
56
|
+
* / zero-width character threats route through the same shared
|
|
57
|
+
* detector backing the guard-json / guard-csv families.
|
|
43
58
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
59
|
+
* Profiles: `strict` / `balanced` / `permissive`. Compliance
|
|
60
|
+
* postures: `hipaa` / `pci-dss` / `gdpr` / `soc2`. Operators select
|
|
61
|
+
* via `{ profile: "strict" }` or `{ compliance: "hipaa" }`;
|
|
62
|
+
* postures overlay on top of the profile baseline.
|
|
46
63
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* 8. Merge-key chain depth (<<: *anchor) — anchor-chain DoS.
|
|
51
|
-
*
|
|
52
|
-
* 9. Bidi / null / control / zero-width chars in scalar values.
|
|
53
|
-
*
|
|
54
|
-
* 10. Anti-DoS caps — total document size, total node count, max
|
|
55
|
-
* anchors, max alias depth, max document count, max scalar
|
|
56
|
-
* length, max depth.
|
|
64
|
+
* @card
|
|
65
|
+
* YAML content-safety guard — defends against the type-coercion, deserialization, and DoS catalog operators face when accepting YAML sourced from user input.
|
|
57
66
|
*/
|
|
58
67
|
|
|
59
68
|
var codepointClass = require("./codepoint-class");
|
|
@@ -433,6 +442,60 @@ function _detectDuplicateKeysYaml(text) {
|
|
|
433
442
|
|
|
434
443
|
// ---- Public surface ----
|
|
435
444
|
|
|
445
|
+
/**
|
|
446
|
+
* @primitive b.guardYaml.validate
|
|
447
|
+
* @signature b.guardYaml.validate(input, opts?)
|
|
448
|
+
* @since 0.7.14
|
|
449
|
+
* @status stable
|
|
450
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
451
|
+
* @related b.guardYaml.parse, b.guardYaml.gate
|
|
452
|
+
*
|
|
453
|
+
* Inspect `input` (string of YAML source) for the full guard-yaml
|
|
454
|
+
* threat catalog without committing to a parsed value. Returns
|
|
455
|
+
* `{ ok, issues, severities }` where `issues` is the aggregated
|
|
456
|
+
* detector output — every dangerous-tag prefix, custom-tag use,
|
|
457
|
+
* anchor / alias amplification, multi-document split, Norway-
|
|
458
|
+
* problem implicit boolean, leading-zero octal, merge-key chain,
|
|
459
|
+
* duplicate-key smuggle, codepoint-class threat, and parse failure
|
|
460
|
+
* is reported with `kind` / `severity` / `ruleId` / `snippet`.
|
|
461
|
+
* Profile-driven (`strict` / `balanced` / `permissive`) and posture-
|
|
462
|
+
* driven (`hipaa` / `pci-dss` / `gdpr` / `soc2`).
|
|
463
|
+
*
|
|
464
|
+
* Detection runs at the source level so the operator's downstream
|
|
465
|
+
* parser (PyYAML / SnakeYAML / js-yaml / libyaml) need not be
|
|
466
|
+
* consulted to identify hostile shapes. A final pass tries the safe-
|
|
467
|
+
* yaml parser and surfaces parse failure as a critical issue.
|
|
468
|
+
*
|
|
469
|
+
* @opts
|
|
470
|
+
* profile: "strict"|"balanced"|"permissive",
|
|
471
|
+
* compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
|
|
472
|
+
* tagPolicy: "reject"|"audit"|"allow",
|
|
473
|
+
* aliasPolicy: "reject"|"audit"|"allow",
|
|
474
|
+
* multiDocPolicy: "reject"|"audit"|"allow",
|
|
475
|
+
* norwayPolicy: "reject"|"audit"|"allow",
|
|
476
|
+
* leadingZeroPolicy: "reject"|"audit"|"allow",
|
|
477
|
+
* duplicateKeyPolicy: "reject"|"audit"|"allow",
|
|
478
|
+
* mergeKeyPolicy: "reject"|"audit"|"allow",
|
|
479
|
+
* bidiPolicy: "reject"|"strip"|"audit"|"allow",
|
|
480
|
+
* controlPolicy: "reject"|"strip"|"allow",
|
|
481
|
+
* nullBytePolicy: "reject"|"strip"|"allow",
|
|
482
|
+
* zeroWidthPolicy: "reject"|"strip"|"audit"|"allow",
|
|
483
|
+
* safeCoreTagsAllowed: boolean,
|
|
484
|
+
* maxBytes: number, // total source byte cap
|
|
485
|
+
* maxDepth: number, // recursion depth cap
|
|
486
|
+
* maxAnchors: number, // anchor declaration cap
|
|
487
|
+
* maxAliasDepth: number, // alias-chain depth cap
|
|
488
|
+
* maxDocuments: number, // multi-document doc count cap
|
|
489
|
+
* maxNodes: number, // total node count cap
|
|
490
|
+
* maxScalarLength: number, // per-scalar length cap
|
|
491
|
+
*
|
|
492
|
+
* @example
|
|
493
|
+
* var rv = b.guardYaml.validate("!!python/object/new:cls\nargs: [x]\n", {
|
|
494
|
+
* profile: "strict",
|
|
495
|
+
* });
|
|
496
|
+
* rv.ok; // → false
|
|
497
|
+
* rv.issues.some(function (i) { return i.kind === "dangerous-tag"; }); // → true
|
|
498
|
+
*/
|
|
436
499
|
function validate(input, opts) {
|
|
437
500
|
opts = _resolveOpts(opts);
|
|
438
501
|
numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
|
|
@@ -449,6 +512,41 @@ function validate(input, opts) {
|
|
|
449
512
|
return gateContract.aggregateIssues(_detectIssues(input, opts));
|
|
450
513
|
}
|
|
451
514
|
|
|
515
|
+
/**
|
|
516
|
+
* @primitive b.guardYaml.parse
|
|
517
|
+
* @signature b.guardYaml.parse(input, opts?)
|
|
518
|
+
* @since 0.7.14
|
|
519
|
+
* @status stable
|
|
520
|
+
* @related b.guardYaml.validate, b.guardYaml.gate
|
|
521
|
+
*
|
|
522
|
+
* Parse `input` (string of YAML source) into a JavaScript value
|
|
523
|
+
* after the guard-yaml threat catalog clears. Runs the full
|
|
524
|
+
* validate-shape detector, throws `GuardYamlError` on the first
|
|
525
|
+
* critical issue (dangerous tag, alias-explosion, multi-document
|
|
526
|
+
* under reject, parse failure, etc.), then routes through the safe-
|
|
527
|
+
* yaml parser with the configured `maxBytes` / `maxDepth` /
|
|
528
|
+
* `maxNodes` caps.
|
|
529
|
+
*
|
|
530
|
+
* The throw-on-critical pre-flight is what distinguishes guarded
|
|
531
|
+
* parse from a raw yaml-library `load()`: the operator's downstream
|
|
532
|
+
* code never sees deserialization-tag instantiation, billion-laughs
|
|
533
|
+
* expansion, or duplicate-key smuggling because the source is
|
|
534
|
+
* refused before the parser runs.
|
|
535
|
+
*
|
|
536
|
+
* @opts
|
|
537
|
+
* profile: "strict"|"balanced"|"permissive",
|
|
538
|
+
* compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
|
|
539
|
+
* tagPolicy: "reject"|"audit"|"allow",
|
|
540
|
+
* aliasPolicy: "reject"|"audit"|"allow",
|
|
541
|
+
* maxBytes: number, maxDepth: number, maxNodes: number,
|
|
542
|
+
*
|
|
543
|
+
* @example
|
|
544
|
+
* var safe = b.guardYaml.parse("name: alice\nage: 30\n", {
|
|
545
|
+
* profile: "strict",
|
|
546
|
+
* });
|
|
547
|
+
* safe.name; // → "alice"
|
|
548
|
+
* safe.age; // → 30
|
|
549
|
+
*/
|
|
452
550
|
function parse(input, opts) {
|
|
453
551
|
opts = _resolveOpts(opts);
|
|
454
552
|
if (typeof input !== "string") {
|
|
@@ -468,6 +566,35 @@ function parse(input, opts) {
|
|
|
468
566
|
});
|
|
469
567
|
}
|
|
470
568
|
|
|
569
|
+
/**
|
|
570
|
+
* @primitive b.guardYaml.gate
|
|
571
|
+
* @signature b.guardYaml.gate(opts?)
|
|
572
|
+
* @since 0.7.14
|
|
573
|
+
* @status stable
|
|
574
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
575
|
+
* @related b.guardYaml.validate, b.guardYaml.parse, b.staticServe.create, b.fileUpload.create
|
|
576
|
+
*
|
|
577
|
+
* Build a `b.gateContract` gate suitable for plugging into
|
|
578
|
+
* `b.staticServe({ contentSafety: { ".yaml": gate } })`,
|
|
579
|
+
* `b.fileUpload({ contentSafety: { "application/yaml": gate } })`,
|
|
580
|
+
* or any host primitive that consumes the gate-contract shape.
|
|
581
|
+
* Action chain on validation: `serve` (no issues) → `audit-only`
|
|
582
|
+
* (warn-only issues) → `refuse` (any high/critical issue). YAML
|
|
583
|
+
* sanitize is intentionally not offered — there's no safe re-emit
|
|
584
|
+
* for tag-injection / alias-explosion shapes; the only correct
|
|
585
|
+
* response is refusal.
|
|
586
|
+
*
|
|
587
|
+
* @opts
|
|
588
|
+
* profile: "strict"|"balanced"|"permissive",
|
|
589
|
+
* compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
|
|
590
|
+
* name: string, // gate identity for audit / observability
|
|
591
|
+
*
|
|
592
|
+
* @example
|
|
593
|
+
* var yamlGate = b.guardYaml.gate({ profile: "strict" });
|
|
594
|
+
* var hostile = Buffer.from("!!python/object/new:cls\nargs: [x]\n", "utf8");
|
|
595
|
+
* var verdict = await yamlGate.check({ bytes: hostile });
|
|
596
|
+
* verdict.action; // → "refuse"
|
|
597
|
+
*/
|
|
471
598
|
function gate(opts) {
|
|
472
599
|
opts = _resolveOpts(opts);
|
|
473
600
|
return gateContract.buildGuardGate(
|
|
@@ -486,13 +613,83 @@ function gate(opts) {
|
|
|
486
613
|
});
|
|
487
614
|
}
|
|
488
615
|
|
|
616
|
+
/**
|
|
617
|
+
* @primitive b.guardYaml.buildProfile
|
|
618
|
+
* @signature b.guardYaml.buildProfile(opts)
|
|
619
|
+
* @since 0.7.14
|
|
620
|
+
* @status stable
|
|
621
|
+
* @related b.guardYaml.gate, b.guardYaml.compliancePosture
|
|
622
|
+
*
|
|
623
|
+
* Compose a derived profile from one or more named bases plus
|
|
624
|
+
* inline overrides. `opts.extends` is a profile name (`"strict"` /
|
|
625
|
+
* `"balanced"` / `"permissive"`) or an array of names; later entries
|
|
626
|
+
* shadow earlier ones. Inline `opts` keys win last. Used to keep
|
|
627
|
+
* operator-defined profiles traceable to a baseline rather than re-
|
|
628
|
+
* typing every key.
|
|
629
|
+
*
|
|
630
|
+
* @opts
|
|
631
|
+
* extends: string|string[], // base profile name(s) to compose
|
|
632
|
+
* ...: any guard-yaml key, // inline override of resolved keys
|
|
633
|
+
*
|
|
634
|
+
* @example
|
|
635
|
+
* var custom = b.guardYaml.buildProfile({
|
|
636
|
+
* extends: "balanced",
|
|
637
|
+
* tagPolicy: "reject",
|
|
638
|
+
* maxAnchors: 8,
|
|
639
|
+
* });
|
|
640
|
+
* custom.tagPolicy; // → "reject"
|
|
641
|
+
* custom.maxAnchors; // → 8
|
|
642
|
+
*/
|
|
489
643
|
var buildProfile = gateContract.makeProfileBuilder(PROFILES);
|
|
490
644
|
|
|
645
|
+
/**
|
|
646
|
+
* @primitive b.guardYaml.compliancePosture
|
|
647
|
+
* @signature b.guardYaml.compliancePosture(name)
|
|
648
|
+
* @since 0.7.14
|
|
649
|
+
* @status stable
|
|
650
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
651
|
+
* @related b.guardYaml.gate, b.guardYaml.buildProfile
|
|
652
|
+
*
|
|
653
|
+
* Look up a compliance-posture overlay by name (`"hipaa"` /
|
|
654
|
+
* `"pci-dss"` / `"gdpr"` / `"soc2"`). Returns a shallow clone of the
|
|
655
|
+
* posture object — the caller may mutate freely. Throws
|
|
656
|
+
* `GuardYamlError("yaml.bad-posture")` on unknown name.
|
|
657
|
+
*
|
|
658
|
+
* @example
|
|
659
|
+
* var posture = b.guardYaml.compliancePosture("hipaa");
|
|
660
|
+
* posture.tagPolicy; // → "reject"
|
|
661
|
+
* posture.forensicSnippetBytes; // → 256
|
|
662
|
+
*/
|
|
491
663
|
function compliancePosture(name) {
|
|
492
664
|
return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES, _err, "yaml");
|
|
493
665
|
}
|
|
494
666
|
|
|
495
667
|
var _yamlRulePacks = gateContract.makeRulePackLoader(GuardYamlError, "yaml");
|
|
668
|
+
/**
|
|
669
|
+
* @primitive b.guardYaml.loadRulePack
|
|
670
|
+
* @signature b.guardYaml.loadRulePack(pack)
|
|
671
|
+
* @since 0.7.14
|
|
672
|
+
* @status stable
|
|
673
|
+
* @related b.guardYaml.gate
|
|
674
|
+
*
|
|
675
|
+
* Register an operator-supplied rule pack with the guard-yaml
|
|
676
|
+
* registry. The pack is identified by `pack.id` (non-empty string)
|
|
677
|
+
* and stored for later inspection / dispatch by gates that opt in
|
|
678
|
+
* via `opts.rulePackId`. Returns the pack object unchanged on
|
|
679
|
+
* success; throws `GuardYamlError("yaml.bad-opt")` when `pack` is
|
|
680
|
+
* missing or `pack.id` is not a non-empty string.
|
|
681
|
+
*
|
|
682
|
+
* @example
|
|
683
|
+
* var pack = b.guardYaml.loadRulePack({
|
|
684
|
+
* id: "deploy-keys",
|
|
685
|
+
* rules: [
|
|
686
|
+
* { id: "no-image-latest", severity: "high",
|
|
687
|
+
* detect: function (text) { return /image:\s*\S+:latest\b/.test(text); },
|
|
688
|
+
* reason: "deployment YAML must pin image tag (no :latest)" },
|
|
689
|
+
* ],
|
|
690
|
+
* });
|
|
691
|
+
* pack.id; // → "deploy-keys"
|
|
692
|
+
*/
|
|
496
693
|
var loadRulePack = _yamlRulePacks.load;
|
|
497
694
|
|
|
498
695
|
module.exports = {
|
package/lib/honeytoken.js
CHANGED
|
@@ -1,36 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* b.honeytoken
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* log, or DB lookup means an attacker found something they shouldn't
|
|
7
|
-
* have. The framework registers each token at issuance and refuses
|
|
8
|
-
* silently in production but always emits a `honeytoken.tripped`
|
|
9
|
-
* audit row on any positive lookup.
|
|
3
|
+
* @module b.honeytoken
|
|
4
|
+
* @nav Identity
|
|
5
|
+
* @title Honeytoken
|
|
10
6
|
*
|
|
11
|
-
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Framework-seeded canary records that trigger an audit alert on
|
|
9
|
+
* read; integrates with sealed columns. The framework generates
|
|
10
|
+
* decoy values (fake api-key shapes, fake admin URLs, fake DB row
|
|
11
|
+
* references) that are NEVER handed to a real client. Their
|
|
12
|
+
* presence in a request, log, or DB lookup means an attacker
|
|
13
|
+
* reached something they shouldn't have. Every positive lookup
|
|
14
|
+
* emits a `honeytoken.tripped` audit row with the observing
|
|
15
|
+
* actor's 5 W's so a SOC operator can pivot directly to the
|
|
16
|
+
* compromise.
|
|
12
17
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* if (honey.lookup(req.headers["x-api-key"])) {
|
|
20
|
-
* // attacker is using the canary; tripped event already audited
|
|
21
|
-
* return res.status(403).end();
|
|
22
|
-
* }
|
|
18
|
+
* Canary value shapes (`kind`):
|
|
19
|
+
* - `"apiKey"` — `bk_canary_<hex>` (mirrors b.apiKey shape)
|
|
20
|
+
* - `"session"` — `bks_canary_<hex>` (mirrors b.session shape)
|
|
21
|
+
* - `"url"` — `/admin/canary-<hex>` (planted as a clickable link)
|
|
22
|
+
* - `"rowId"` — `ht_canary_<hex>` (planted as a fake foreign key)
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* - "rowId" → `ht_canary_<32 hex>` (planted as a fake foreign key)
|
|
24
|
+
* Audit shape:
|
|
25
|
+
* - `honeytoken.issued` — outcome=success; metadata { id, kind }
|
|
26
|
+
* - `honeytoken.tripped` — outcome=failure; metadata { id, kind,
|
|
27
|
+
* metadata, observedAt, observedActor }
|
|
29
28
|
*
|
|
30
|
-
*
|
|
31
|
-
* -
|
|
32
|
-
* - `honeytoken.tripped` — outcome=failure; metadata: { id, kind,
|
|
33
|
-
* metadata, observedAt, observedActor }
|
|
29
|
+
* @card
|
|
30
|
+
* Framework-seeded canary records that trigger an audit alert on read; integrates with sealed columns.
|
|
34
31
|
*/
|
|
35
32
|
|
|
36
33
|
var crypto = require("./crypto");
|
|
@@ -49,6 +46,45 @@ var KINDS = Object.freeze({
|
|
|
49
46
|
rowId: function () { return "ht_canary_" + crypto.generateToken(16); }, // allow:raw-byte-literal — 16-byte canary entropy
|
|
50
47
|
});
|
|
51
48
|
|
|
49
|
+
/**
|
|
50
|
+
* @primitive b.honeytoken.create
|
|
51
|
+
* @signature b.honeytoken.create(opts)
|
|
52
|
+
* @since 0.8.40
|
|
53
|
+
* @status stable
|
|
54
|
+
* @compliance soc2, nis2, dora
|
|
55
|
+
* @related b.audit, b.apiKey.create, b.session
|
|
56
|
+
*
|
|
57
|
+
* Build an in-process honeytoken registry. Returns a handle exposing
|
|
58
|
+
* `issue(spec)` to mint a canary, `lookup(value, observedActor?)` to
|
|
59
|
+
* test an incoming value (audit-emits `honeytoken.tripped` on hit),
|
|
60
|
+
* `revoke(id)` to retire a canary, and `size()` for diagnostics. The
|
|
61
|
+
* registry is per-process — operators running multiple workers wire
|
|
62
|
+
* a shared b.audit sink and reconcile alerts at the audit layer
|
|
63
|
+
* rather than sharing the registry across nodes (a canary's value
|
|
64
|
+
* is what's planted in the trap, not what's known to the framework).
|
|
65
|
+
*
|
|
66
|
+
* @opts
|
|
67
|
+
* audit: b.audit, // audit sink for issued / tripped events (optional, recommended)
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* var honey = b.honeytoken.create({ audit: b.audit });
|
|
71
|
+
*
|
|
72
|
+
* var canary = honey.issue({
|
|
73
|
+
* kind: "apiKey",
|
|
74
|
+
* metadata: { plantedAt: "GET /admin/keys/list", linkedTo: "user-42" },
|
|
75
|
+
* });
|
|
76
|
+
* // → { id: "ht_<hex>", value: "bk_canary_<hex>" }
|
|
77
|
+
*
|
|
78
|
+
* // Plant the canary value somewhere an attacker who's escalated
|
|
79
|
+
* // privileges might find it (a fake row in an admin listing, a
|
|
80
|
+
* // dummy env-var leaked into a traceback page, etc.).
|
|
81
|
+
*
|
|
82
|
+
* // On every incoming credential, check for canary use:
|
|
83
|
+
* if (honey.lookup(req.headers["x-api-key"], { ip: req.ip })) {
|
|
84
|
+
* // tripped — audit already emitted; respond as if invalid.
|
|
85
|
+
* return res.writeHead(403).end();
|
|
86
|
+
* }
|
|
87
|
+
*/
|
|
52
88
|
function create(opts) {
|
|
53
89
|
opts = opts || {};
|
|
54
90
|
validateOpts(opts, ["audit"], "honeytoken.create");
|
package/lib/html-balance.js
CHANGED
|
@@ -1,4 +1,35 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.htmlBalance
|
|
4
|
+
* @nav Tools
|
|
5
|
+
* @title HTML Balance
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* HTML tag-balance verification. Walks an HTML fragment and refuses
|
|
9
|
+
* unbalanced container tags, orphan close tags, mismatched-name
|
|
10
|
+
* close tags, void-element close tags, unterminated comments, and
|
|
11
|
+
* unterminated raw-text elements (`<script>` / `<style>` / `<title>`
|
|
12
|
+
* / `<textarea>`). Returns `null` when balanced; otherwise an issue
|
|
13
|
+
* object with `code`, `message`, `line`, and `column` so operators
|
|
14
|
+
* can surface "you forgot to close this tag at line N" feedback at
|
|
15
|
+
* save time.
|
|
16
|
+
*
|
|
17
|
+
* Intent is operator-side structural feedback, not security
|
|
18
|
+
* validation. Attribute syntax, custom-element / shadow-DOM
|
|
19
|
+
* namespaces, script/style content semantics, and XSS-class checks
|
|
20
|
+
* are all out of scope here — the security pass is `b.guardHtml`,
|
|
21
|
+
* composed by `b.htmlBalance.checkSafe` for operators wanting both
|
|
22
|
+
* gates in one call.
|
|
23
|
+
*
|
|
24
|
+
* Void elements (`<br>`, `<img>`, `<input>`, …) are recognised and
|
|
25
|
+
* never expected to close. Self-closing slash forms (`<foo />`)
|
|
26
|
+
* are honoured. Raw-text elements skip their content entirely so
|
|
27
|
+
* `<script>` bodies containing literal `<` / `</` aren't mis-read
|
|
28
|
+
* as markup.
|
|
29
|
+
*
|
|
30
|
+
* @card
|
|
31
|
+
* HTML tag-balance verification.
|
|
32
|
+
*/
|
|
2
33
|
/**
|
|
3
34
|
* html-balance — minimal HTML structural sanity check.
|
|
4
35
|
*
|
|
@@ -45,6 +76,28 @@ function _posToLineColumn(src, pos) {
|
|
|
45
76
|
return { line: line, column: col };
|
|
46
77
|
}
|
|
47
78
|
|
|
79
|
+
/**
|
|
80
|
+
* @primitive b.htmlBalance.check
|
|
81
|
+
* @signature b.htmlBalance.check(html)
|
|
82
|
+
* @since 0.1.0
|
|
83
|
+
* @related b.htmlBalance.checkSafe, b.guardHtml
|
|
84
|
+
*
|
|
85
|
+
* Returns `null` when `html` is balanced; otherwise an issue object
|
|
86
|
+
* `{ code, message, line, column }`. Issue codes: `html/unterminated-comment`,
|
|
87
|
+
* `html/unterminated-tag`, `html/orphan-close`, `html/mismatched-close`,
|
|
88
|
+
* `html/void-close`, `html/unclosed-raw-text`, `html/unclosed-tag`.
|
|
89
|
+
* Non-string inputs and the empty string return `null` (nothing to
|
|
90
|
+
* balance). Self-closing slash forms and HTML5 void elements are
|
|
91
|
+
* recognised; raw-text elements skip their bodies so `<script>` JS
|
|
92
|
+
* containing `<` characters doesn't mis-balance.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* b.htmlBalance.check("<div><p>hello</p></div>");
|
|
96
|
+
* // → null
|
|
97
|
+
*
|
|
98
|
+
* var issue = b.htmlBalance.check("<div><p>hello</div>");
|
|
99
|
+
* // → { code: "html/mismatched-close", message: "</div> at line 1 col 14 does not match open <p> at line 1 col 6", line: 1, column: 14 }
|
|
100
|
+
*/
|
|
48
101
|
function check(html) {
|
|
49
102
|
if (typeof html !== "string" || html.length === 0) return null;
|
|
50
103
|
var stack = []; // [{ tag, openPos }]
|
|
@@ -242,6 +295,36 @@ function check(html) {
|
|
|
242
295
|
var lazyRequire = require("./lazy-require");
|
|
243
296
|
var _guardHtml = lazyRequire(function () { return require("./guard-html"); });
|
|
244
297
|
|
|
298
|
+
/**
|
|
299
|
+
* @primitive b.htmlBalance.checkSafe
|
|
300
|
+
* @signature b.htmlBalance.checkSafe(html, opts?)
|
|
301
|
+
* @since 0.7.7
|
|
302
|
+
* @related b.htmlBalance.check, b.guardHtml
|
|
303
|
+
*
|
|
304
|
+
* Composes `check()` (cheap structural well-formedness) with
|
|
305
|
+
* `b.guardHtml.gate({ profile })` (security-class checks against the
|
|
306
|
+
* strict / balanced / permissive vocabulary plus an optional
|
|
307
|
+
* compliance posture). Returns `{ balanceIssue, guardIssues, ok }`
|
|
308
|
+
* so callers can distinguish a structural problem from a content-
|
|
309
|
+
* safety reject and decide which path to surface to the operator.
|
|
310
|
+
*
|
|
311
|
+
* `opts.contentSafety` mirrors the same shape as `b.fileUpload({
|
|
312
|
+
* contentSafety })` and `b.staticServe({ contentSafety })` so a
|
|
313
|
+
* single `{ profile, posture }` value flows across the stack
|
|
314
|
+
* unchanged.
|
|
315
|
+
*
|
|
316
|
+
* @opts
|
|
317
|
+
* profile: "strict" | "balanced" | "permissive",
|
|
318
|
+
* posture: string, // compliance posture name; e.g. "hipaa", "pci-dss"
|
|
319
|
+
* contentSafety: { profile, posture }, // shared shape with b.fileUpload / b.staticServe
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* var rv = b.htmlBalance.checkSafe("<div onclick=\"x()\">hi</div>", { profile: "strict" });
|
|
323
|
+
* // → { balanceIssue: null, guardIssues: [{ kind: "event-handler-attribute", ... }], ok: false }
|
|
324
|
+
*
|
|
325
|
+
* b.htmlBalance.checkSafe("<p>hello</p>", { profile: "strict" });
|
|
326
|
+
* // → { balanceIssue: null, guardIssues: [], ok: true }
|
|
327
|
+
*/
|
|
245
328
|
function checkSafe(html, opts) {
|
|
246
329
|
opts = opts || {};
|
|
247
330
|
var balanceIssue = check(html);
|