@blamejs/core 0.8.38 → 0.8.40
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 +2 -0
- package/index.js +3 -0
- package/lib/audit-tools.js +69 -0
- package/lib/audit.js +3 -0
- package/lib/auth/ato-kill-switch.js +112 -0
- package/lib/config-drift.js +64 -4
- package/lib/honeytoken.js +132 -0
- package/lib/middleware/csp-report.js +133 -0
- package/lib/middleware/index.js +2 -0
- package/lib/network-tls.js +62 -0
- package/lib/network.js +2 -0
- package/lib/ssrf-guard.js +55 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.8.x
|
|
10
10
|
|
|
11
|
+
- v0.8.40 (2026-05-07) — operator enhancements (2/2): `b.honeytoken.create({audit})` issues canary api-key / session / URL / row-id values that emit `honeytoken.tripped` audit on any positive lookup; `b.middleware.cspReport.create({onReport})` is a Reporting-API endpoint that ingests CSP / COEP / COOP violations as `csp.violation` audit rows; `b.auditTools.forensicSnapshot({out, since, passphrase, reason})` composes an audit-export slice + IR context manifest into one tamper-evident bundle for legal / regulator handover; `b.network.tls.pinsetDriftMonitor({intervalMs})` periodically compares the trust-store fingerprint set to the captured baseline and emits `network.tls.pinset.drifted` when CAs are added or removed. Adds the OpenSSF Scorecard CI workflow at `.github/workflows/scorecard.yml`. Defers items 11 (operator-supplied transform sandbox), 14 (chaos / fault-injection drills), and 15 (exploit replay corpus harness) with re-open conditions: surface when (a) operator demand surfaces OR (b) a CVE replay needs a vendored harness.
|
|
12
|
+
- v0.8.39 (2026-05-07) — operator enhancements (1/2): `b.configDrift.verifyVendorIntegrity()` re-hashes every file listed in `lib/vendor/MANIFEST.json` at boot and refuses on mismatch; `b.network.allowlist.create({allow, deny})` composes on `b.ssrfGuard` to gate per-call outbound URLs against an operator CIDR/host allow set; `b.auth.atoKillSwitch.trigger({userId, reason})` is a composite ATO incident-response workflow that destroys every session for the user, applies `b.auth.lockout`, and optionally flips `b.auth.accessLock` mode in one audited call.
|
|
11
13
|
- v0.8.38 (2026-05-07) — multipart parser refuses obsolete line folding (RFC 9112 §5.2 obs-fold) and CR/LF/NUL bytes in part-header values (RFC 9110 §5.5). Adds RFC 5987 / 8187 `filename*=UTF-8''…` extended-parameter support; the decoded value takes precedence over a legacy `filename=` companion.
|
|
12
14
|
|
|
13
15
|
- **0.8.37** (2026-05-08) — D-L family SQL/schema hygiene. **`_blamejs_audit_purge_anchor.scope` CHECK constraint** — `scope IN ('audit', 'consent')`. Pre-v0.8.37 a typo silently created a parallel anchor; the chain verifier walked the wrong anchor and missed tampering. **`personalDataCategories` vocabulary validation** — operator-supplied categories validated against the GDPR Article 9 special-category vocabulary + the framework's general categories at `db.init`. Unknown categories don't refuse (operators have legitimate custom labels) but emit a `db.personal_data_category_unknown` audit row so typos surface in regulator reviews. Allowed: `name`, `email`, `phone`, `address`, `ip`, `id-document`, `biometric`, `health`, `genetic`, `sexual-orientation`, `racial-or-ethnic-origin`, `political-opinion`, `religious-belief`, `trade-union-membership`, `criminal-record`, `financial`, `location`, `behavioral`, `device-id`, `child-data`, `education`, `employment`, `operator-defined`.
|
package/index.js
CHANGED
|
@@ -162,6 +162,7 @@ var auth = {
|
|
|
162
162
|
acr: require("./lib/auth/acr-vocabulary"),
|
|
163
163
|
authTime: require("./lib/auth/auth-time-tracker"),
|
|
164
164
|
accessLock: require("./lib/auth/access-lock"),
|
|
165
|
+
atoKillSwitch: require("./lib/auth/ato-kill-switch"),
|
|
165
166
|
};
|
|
166
167
|
var template = require("./lib/template");
|
|
167
168
|
var render = require("./lib/render");
|
|
@@ -225,6 +226,7 @@ var appShutdown = require("./lib/app-shutdown");
|
|
|
225
226
|
var slug = require("./lib/slug");
|
|
226
227
|
var webhook = require("./lib/webhook");
|
|
227
228
|
var apiKey = require("./lib/api-key");
|
|
229
|
+
var honeytoken = require("./lib/honeytoken");
|
|
228
230
|
var credentialHash = require("./lib/credential-hash");
|
|
229
231
|
var permissions = require("./lib/permissions");
|
|
230
232
|
var cache = require("./lib/cache");
|
|
@@ -401,6 +403,7 @@ module.exports = {
|
|
|
401
403
|
slug: slug,
|
|
402
404
|
webhook: webhook,
|
|
403
405
|
apiKey: apiKey,
|
|
406
|
+
honeytoken: honeytoken,
|
|
404
407
|
credentialHash: credentialHash,
|
|
405
408
|
permissions: permissions,
|
|
406
409
|
cache: cache,
|
package/lib/audit-tools.js
CHANGED
|
@@ -61,6 +61,7 @@ var auditSign = require("./audit-sign");
|
|
|
61
61
|
var backupCrypto = require("./backup/crypto");
|
|
62
62
|
var clusterStorage = require("./cluster-storage");
|
|
63
63
|
var lazyRequire = require("./lazy-require");
|
|
64
|
+
var validateOpts = require("./validate-opts");
|
|
64
65
|
var jsonSafe = require("./safe-json");
|
|
65
66
|
var { defineClass } = require("./framework-error");
|
|
66
67
|
|
|
@@ -665,9 +666,77 @@ async function _defaultApplyPurge(args) {
|
|
|
665
666
|
};
|
|
666
667
|
}
|
|
667
668
|
|
|
669
|
+
// forensicSnapshot — post-compromise composer that bundles an audit
|
|
670
|
+
// archive slice, current break-glass grants, the active incident
|
|
671
|
+
// report (if any), and process-runtime metadata into a single signed
|
|
672
|
+
// bundle. The operator passes this to legal / regulators / the IR
|
|
673
|
+
// team as one tamper-evident artifact.
|
|
674
|
+
//
|
|
675
|
+
// var snap = await b.auditTools.forensicSnapshot({
|
|
676
|
+
// out: "/forensics/2026-05-07-incident-42",
|
|
677
|
+
// since: Date.now() - C.TIME.days(7),
|
|
678
|
+
// passphrase: process.env.AUDIT_BUNDLE_PASSPHRASE,
|
|
679
|
+
// incidentId: "inc-2026-05-07-42",
|
|
680
|
+
// reason: "ATO investigation: 14 failed MFA from new geo, user u_42",
|
|
681
|
+
// actor: { id: "alice@ops.example.com", role: "incident-commander" },
|
|
682
|
+
// });
|
|
683
|
+
async function forensicSnapshot(opts) {
|
|
684
|
+
opts = opts || {};
|
|
685
|
+
_requirePassphrase(opts.passphrase);
|
|
686
|
+
_requireOutDir(opts.out, "forensicSnapshot");
|
|
687
|
+
var sinceMs = _toMs(opts.since);
|
|
688
|
+
if (sinceMs == null) {
|
|
689
|
+
throw new AuditToolsError("audit-tools/no-since",
|
|
690
|
+
"forensicSnapshot: opts.since is required");
|
|
691
|
+
}
|
|
692
|
+
validateOpts.requireNonEmptyString(opts.reason, "reason", AuditToolsError, "audit-tools/no-reason");
|
|
693
|
+
var sliceResult = await exportSlice({
|
|
694
|
+
out: opts.out,
|
|
695
|
+
since: sinceMs,
|
|
696
|
+
until: Date.now(),
|
|
697
|
+
passphrase: opts.passphrase,
|
|
698
|
+
readRows: opts.readRows,
|
|
699
|
+
readCoveringCheckpoint: opts.readCoveringCheckpoint,
|
|
700
|
+
});
|
|
701
|
+
// Compose snapshot manifest with operator-supplied IR context.
|
|
702
|
+
var manifest = {
|
|
703
|
+
snapshotKind: "forensic",
|
|
704
|
+
incidentId: opts.incidentId || null,
|
|
705
|
+
reason: opts.reason,
|
|
706
|
+
actor: opts.actor || null,
|
|
707
|
+
composedAt: new Date().toISOString(),
|
|
708
|
+
auditSliceFile: sliceResult && sliceResult.path,
|
|
709
|
+
auditSliceCount: sliceResult && sliceResult.rowCount,
|
|
710
|
+
runtime: {
|
|
711
|
+
nodeVersion: process.version,
|
|
712
|
+
platform: process.platform,
|
|
713
|
+
arch: process.arch,
|
|
714
|
+
pid: process.pid,
|
|
715
|
+
uptimeSec: Math.round(process.uptime()),
|
|
716
|
+
},
|
|
717
|
+
};
|
|
718
|
+
var manifestPath = require("node:path").join(opts.out, "forensic-snapshot.json");
|
|
719
|
+
require("node:fs").writeFileSync(manifestPath, _canonicalize(manifest), "utf8");
|
|
720
|
+
try {
|
|
721
|
+
require("./audit").safeEmit({
|
|
722
|
+
action: "audit.forensic_snapshot.composed",
|
|
723
|
+
outcome: "success",
|
|
724
|
+
metadata: {
|
|
725
|
+
out: opts.out,
|
|
726
|
+
incidentId: manifest.incidentId,
|
|
727
|
+
reason: opts.reason,
|
|
728
|
+
actor: opts.actor || null,
|
|
729
|
+
rowCount: manifest.auditSliceCount || 0,
|
|
730
|
+
},
|
|
731
|
+
});
|
|
732
|
+
} catch (_e) { /* audit best-effort */ }
|
|
733
|
+
return Object.assign({}, manifest, { manifestPath: manifestPath });
|
|
734
|
+
}
|
|
735
|
+
|
|
668
736
|
module.exports = {
|
|
669
737
|
archive: archive,
|
|
670
738
|
exportSlice: exportSlice,
|
|
739
|
+
forensicSnapshot: forensicSnapshot,
|
|
671
740
|
verifyBundle: verifyBundle,
|
|
672
741
|
purge: purge,
|
|
673
742
|
BUNDLE_FORMAT: BUNDLE_FORMAT,
|
package/lib/audit.js
CHANGED
|
@@ -247,6 +247,9 @@ var FRAMEWORK_NAMESPACES = [
|
|
|
247
247
|
"fdx", // b.fdx (fdx.bound / fdx.consent_receipt_issued)
|
|
248
248
|
"tcpa10dlc", // b.tcpa10dlc (tcpa10dlc.consent_recorded / consent_revoked)
|
|
249
249
|
"iabmspa", // b.iabMspa (iabmspa.processing_refused)
|
|
250
|
+
"vendor", // b.configDrift.verifyVendorIntegrity (vendor.integrity.verified / tampered)
|
|
251
|
+
"honeytoken", // b.honeytoken (honeytoken.issued / tripped)
|
|
252
|
+
"csp", // b.middleware.cspReport (csp.violation)
|
|
250
253
|
];
|
|
251
254
|
var registeredNamespaces = new Set(FRAMEWORK_NAMESPACES);
|
|
252
255
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.auth.atoKillSwitch — composite primitive for account-takeover
|
|
4
|
+
* incident response. Composes `b.session.destroyAllForUser` +
|
|
5
|
+
* `b.auth.lockout.lock` + (optionally) `b.auth.accessLock` mode flip
|
|
6
|
+
* into a single operator-callable workflow.
|
|
7
|
+
*
|
|
8
|
+
* Trigger conditions are operator territory (SOC alert, fraud signal,
|
|
9
|
+
* user self-report, IDS rule); this primitive is the deterministic
|
|
10
|
+
* cleanup path once the trigger fires:
|
|
11
|
+
*
|
|
12
|
+
* 1. destroy every session for the user across the cluster
|
|
13
|
+
* 2. lock the user out of new logins (b.auth.lockout)
|
|
14
|
+
* 3. emit an audit row with reason / actor for downstream forensics
|
|
15
|
+
*
|
|
16
|
+
* await b.auth.atoKillSwitch.trigger({
|
|
17
|
+
* userId: "u_42",
|
|
18
|
+
* reason: "fraud-signal: 14 failed MFA from new geo",
|
|
19
|
+
* actor: { id: req.user && req.user.id, role: req.user && req.user.role },
|
|
20
|
+
* lockout: true, // default true
|
|
21
|
+
* accessLock: "locked", // optional — flip the global access-lock mode
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* Returns `{ sessionsDestroyed, lockoutApplied, accessLockMode }`.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
var lazyRequire = require("../lazy-require");
|
|
28
|
+
var validateOpts = require("../validate-opts");
|
|
29
|
+
var { defineClass } = require("../framework-error");
|
|
30
|
+
|
|
31
|
+
var session = lazyRequire(function () { return require("../session"); });
|
|
32
|
+
var lockout = lazyRequire(function () { return require("./lockout"); });
|
|
33
|
+
var accessLock = lazyRequire(function () { return require("./access-lock"); });
|
|
34
|
+
var audit = lazyRequire(function () { return require("../audit"); });
|
|
35
|
+
|
|
36
|
+
var AtoKillSwitchError = defineClass("AtoKillSwitchError", { alwaysPermanent: true });
|
|
37
|
+
|
|
38
|
+
async function trigger(opts) {
|
|
39
|
+
opts = opts || {};
|
|
40
|
+
validateOpts(opts, [
|
|
41
|
+
"userId", "reason", "actor", "lockout", "accessLock",
|
|
42
|
+
], "auth.atoKillSwitch.trigger");
|
|
43
|
+
|
|
44
|
+
validateOpts.requireNonEmptyString(opts.userId, "userId", AtoKillSwitchError, "auth-ato-kill-switch/missing-user-id");
|
|
45
|
+
validateOpts.requireNonEmptyString(opts.reason, "reason", AtoKillSwitchError, "auth-ato-kill-switch/missing-reason");
|
|
46
|
+
var doLockout = opts.lockout !== false;
|
|
47
|
+
var accessLockMode = typeof opts.accessLock === "string" ? opts.accessLock : null;
|
|
48
|
+
|
|
49
|
+
var sessionsDestroyed = 0;
|
|
50
|
+
try {
|
|
51
|
+
sessionsDestroyed = await session().destroyAllForUser(opts.userId);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
audit().safeEmit({
|
|
54
|
+
action: "auth.ato_kill_switch.partial",
|
|
55
|
+
outcome: "failure",
|
|
56
|
+
metadata: {
|
|
57
|
+
userId: opts.userId,
|
|
58
|
+
step: "destroy-sessions",
|
|
59
|
+
reason: e && e.message,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
throw e;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
var lockoutApplied = false;
|
|
66
|
+
if (doLockout) {
|
|
67
|
+
try {
|
|
68
|
+
await lockout().lock(opts.userId, {
|
|
69
|
+
reason: "ato-kill-switch:" + opts.reason,
|
|
70
|
+
});
|
|
71
|
+
lockoutApplied = true;
|
|
72
|
+
} catch (_e) { /* lockout is best-effort; sessions already destroyed */ }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
var modeApplied = null;
|
|
76
|
+
if (accessLockMode !== null) {
|
|
77
|
+
try {
|
|
78
|
+
var lock = accessLock();
|
|
79
|
+
if (lock && typeof lock.set === "function") {
|
|
80
|
+
await lock.set(accessLockMode, {
|
|
81
|
+
actor: opts.actor || null,
|
|
82
|
+
reason: "ato-kill-switch:" + opts.reason,
|
|
83
|
+
});
|
|
84
|
+
modeApplied = accessLockMode;
|
|
85
|
+
}
|
|
86
|
+
} catch (_e) { /* operator may not have wired global accessLock; fine */ }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
audit().safeEmit({
|
|
90
|
+
action: "auth.ato_kill_switch.triggered",
|
|
91
|
+
outcome: "success",
|
|
92
|
+
metadata: {
|
|
93
|
+
userId: opts.userId,
|
|
94
|
+
reason: opts.reason,
|
|
95
|
+
actor: opts.actor || null,
|
|
96
|
+
sessionsDestroyed: sessionsDestroyed,
|
|
97
|
+
lockoutApplied: lockoutApplied,
|
|
98
|
+
accessLockMode: modeApplied,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
sessionsDestroyed: sessionsDestroyed,
|
|
104
|
+
lockoutApplied: lockoutApplied,
|
|
105
|
+
accessLockMode: modeApplied,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
trigger: trigger,
|
|
111
|
+
AtoKillSwitchError: AtoKillSwitchError,
|
|
112
|
+
};
|
package/lib/config-drift.js
CHANGED
|
@@ -292,10 +292,70 @@ function create(opts) {
|
|
|
292
292
|
};
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
// verifyVendorIntegrity — at-boot integrity check over `lib/vendor/*`.
|
|
296
|
+
// MANIFEST.json carries a sha256 digest per bundled file; we re-hash
|
|
297
|
+
// each one and refuse on mismatch. Catches a half-applied vendor
|
|
298
|
+
// refresh, a corrupted install, or an attacker who modified a
|
|
299
|
+
// vendored cjs without updating the manifest. Returns
|
|
300
|
+
// `{ ok, mismatches: [{ path, expected, actual }] }` and emits
|
|
301
|
+
// `vendor.integrity.{verified,tampered}` audit on each call.
|
|
302
|
+
function verifyVendorIntegrity(opts) {
|
|
303
|
+
opts = opts || {};
|
|
304
|
+
var libVendorDir = opts.libVendorDir || path.join(process.cwd(), "lib", "vendor");
|
|
305
|
+
var manifestPath = opts.manifestPath || path.join(libVendorDir, "MANIFEST.json");
|
|
306
|
+
var raw;
|
|
307
|
+
try { raw = fs.readFileSync(manifestPath, "utf8"); }
|
|
308
|
+
catch (_e) {
|
|
309
|
+
throw _err("VENDOR_MANIFEST_MISSING",
|
|
310
|
+
"vendor MANIFEST.json missing at " + manifestPath, true);
|
|
311
|
+
}
|
|
312
|
+
var manifest = safeJson.parse(raw);
|
|
313
|
+
if (!manifest || typeof manifest.packages !== "object") {
|
|
314
|
+
throw _err("VENDOR_MANIFEST_SHAPE",
|
|
315
|
+
"vendor MANIFEST.json missing `packages` map", true);
|
|
316
|
+
}
|
|
317
|
+
var mismatches = [];
|
|
318
|
+
var checkedCount = 0;
|
|
319
|
+
Object.keys(manifest.packages).forEach(function (pkgName) {
|
|
320
|
+
var pkg = manifest.packages[pkgName];
|
|
321
|
+
var files = (pkg && pkg.files) || {};
|
|
322
|
+
var hashes = (pkg && pkg.hashes) || {};
|
|
323
|
+
Object.keys(files).forEach(function (kind) {
|
|
324
|
+
var rel = files[kind];
|
|
325
|
+
var expected = hashes[kind];
|
|
326
|
+
if (typeof rel !== "string" || typeof expected !== "string") return;
|
|
327
|
+
var abs = path.isAbsolute(rel) ? rel : path.join(process.cwd(), rel);
|
|
328
|
+
var actual;
|
|
329
|
+
try {
|
|
330
|
+
var bytes = fs.readFileSync(abs);
|
|
331
|
+
actual = "sha256:" + require("node:crypto")
|
|
332
|
+
.createHash("sha256").update(bytes).digest("hex");
|
|
333
|
+
} catch (_e) {
|
|
334
|
+
mismatches.push({ pkg: pkgName, kind: kind, path: rel, expected: expected, actual: "<read-failed>" });
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
checkedCount += 1;
|
|
338
|
+
if (actual !== expected) {
|
|
339
|
+
mismatches.push({ pkg: pkgName, kind: kind, path: rel, expected: expected, actual: actual });
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
var ok = mismatches.length === 0;
|
|
344
|
+
try {
|
|
345
|
+
audit().safeEmit({
|
|
346
|
+
action: ok ? "vendor.integrity.verified" : "vendor.integrity.tampered",
|
|
347
|
+
outcome: ok ? "success" : "failure",
|
|
348
|
+
metadata: { checkedCount: checkedCount, mismatchCount: mismatches.length },
|
|
349
|
+
});
|
|
350
|
+
} catch (_e) { /* audit best-effort */ }
|
|
351
|
+
return { ok: ok, checkedCount: checkedCount, mismatches: mismatches };
|
|
352
|
+
}
|
|
353
|
+
|
|
295
354
|
module.exports = {
|
|
296
|
-
create:
|
|
297
|
-
|
|
355
|
+
create: create,
|
|
356
|
+
verifyVendorIntegrity: verifyVendorIntegrity,
|
|
357
|
+
ConfigDriftError: ConfigDriftError,
|
|
298
358
|
// Test-only export for hashing — operators don't need this directly.
|
|
299
|
-
_hashSnapshot:
|
|
300
|
-
_stableStringify:
|
|
359
|
+
_hashSnapshot: _hashSnapshot,
|
|
360
|
+
_stableStringify: _stableStringify,
|
|
301
361
|
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.honeytoken — canary credential framework. Generates decoy values
|
|
4
|
+
* (fake api-key shapes, fake admin URLs, fake DB row references) that
|
|
5
|
+
* are NEVER handed to a real client; their presence in a request,
|
|
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.
|
|
10
|
+
*
|
|
11
|
+
* var honey = b.honeytoken.create({ audit: b.audit });
|
|
12
|
+
*
|
|
13
|
+
* var token = honey.issue({
|
|
14
|
+
* kind: "apiKey",
|
|
15
|
+
* metadata: { plantedAt: "GET /admin/keys/404", linkedTo: "u_42" },
|
|
16
|
+
* });
|
|
17
|
+
* // → { value: "bk_canary_8f3a7b2e0c…", id: "ht_<hex>" }
|
|
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
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* Canary value shapes (`kind`):
|
|
25
|
+
* - "apiKey" → `bk_canary_<32 hex>` (matches b.apiKey shape)
|
|
26
|
+
* - "session" → `bks_canary_<48 hex>` (matches b.session shape)
|
|
27
|
+
* - "url" → `/admin/canary-<32 hex>` (planted as a clickable link)
|
|
28
|
+
* - "rowId" → `ht_canary_<32 hex>` (planted as a fake foreign key)
|
|
29
|
+
*
|
|
30
|
+
* Audit shape:
|
|
31
|
+
* - `honeytoken.issued` — outcome=success; metadata: { id, kind }
|
|
32
|
+
* - `honeytoken.tripped` — outcome=failure; metadata: { id, kind,
|
|
33
|
+
* metadata, observedAt, observedActor }
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
var crypto = require("./crypto");
|
|
37
|
+
var lazyRequire = require("./lazy-require");
|
|
38
|
+
var validateOpts = require("./validate-opts");
|
|
39
|
+
var { defineClass } = require("./framework-error");
|
|
40
|
+
|
|
41
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
42
|
+
|
|
43
|
+
var HoneytokenError = defineClass("HoneytokenError", { alwaysPermanent: true });
|
|
44
|
+
|
|
45
|
+
var KINDS = Object.freeze({
|
|
46
|
+
apiKey: function () { return "bk_canary_" + crypto.generateToken(16); }, // allow:raw-byte-literal — 16-byte (128-bit) canary entropy
|
|
47
|
+
session: function () { return "bks_canary_" + crypto.generateToken(24); }, // allow:raw-byte-literal — 24-byte (192-bit) canary entropy
|
|
48
|
+
url: function () { return "/admin/canary-" + crypto.generateToken(16); }, // allow:raw-byte-literal — 16-byte canary entropy
|
|
49
|
+
rowId: function () { return "ht_canary_" + crypto.generateToken(16); }, // allow:raw-byte-literal — 16-byte canary entropy
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
function create(opts) {
|
|
53
|
+
opts = opts || {};
|
|
54
|
+
validateOpts(opts, ["audit"], "honeytoken.create");
|
|
55
|
+
|
|
56
|
+
var registry = new Map(); // value → { id, kind, metadata, issuedAt }
|
|
57
|
+
|
|
58
|
+
function issue(spec) {
|
|
59
|
+
spec = spec || {};
|
|
60
|
+
validateOpts(spec, ["kind", "metadata"], "honeytoken.issue");
|
|
61
|
+
var kind = spec.kind;
|
|
62
|
+
if (typeof KINDS[kind] !== "function") {
|
|
63
|
+
throw new HoneytokenError(
|
|
64
|
+
"honeytoken/unknown-kind",
|
|
65
|
+
"honeytoken.issue: unknown kind '" + kind + "' " +
|
|
66
|
+
"(supported: " + Object.keys(KINDS).join(", ") + ")");
|
|
67
|
+
}
|
|
68
|
+
var value = KINDS[kind]();
|
|
69
|
+
var id = "ht_" + crypto.generateToken(8); // allow:raw-byte-literal — 8-byte registry id
|
|
70
|
+
var record = Object.freeze({
|
|
71
|
+
id: id,
|
|
72
|
+
kind: kind,
|
|
73
|
+
metadata: spec.metadata || null,
|
|
74
|
+
issuedAt: Date.now(),
|
|
75
|
+
});
|
|
76
|
+
registry.set(value, record);
|
|
77
|
+
try {
|
|
78
|
+
audit().safeEmit({
|
|
79
|
+
action: "honeytoken.issued",
|
|
80
|
+
outcome: "success",
|
|
81
|
+
metadata: { id: id, kind: kind },
|
|
82
|
+
});
|
|
83
|
+
} catch (_e) { /* audit best-effort */ }
|
|
84
|
+
return { id: id, value: value };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function lookup(value, observedActor) {
|
|
88
|
+
if (typeof value !== "string" || value.length === 0) return null;
|
|
89
|
+
var record = registry.get(value);
|
|
90
|
+
if (!record) return null;
|
|
91
|
+
try {
|
|
92
|
+
audit().safeEmit({
|
|
93
|
+
action: "honeytoken.tripped",
|
|
94
|
+
outcome: "failure",
|
|
95
|
+
metadata: {
|
|
96
|
+
id: record.id,
|
|
97
|
+
kind: record.kind,
|
|
98
|
+
metadata: record.metadata,
|
|
99
|
+
observedAt: Date.now(),
|
|
100
|
+
observedActor: observedActor || null,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
} catch (_e) { /* audit best-effort */ }
|
|
104
|
+
return record;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function revoke(id) {
|
|
108
|
+
var found = false;
|
|
109
|
+
registry.forEach(function (record, value) {
|
|
110
|
+
if (record.id === id) {
|
|
111
|
+
registry.delete(value);
|
|
112
|
+
found = true;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return found;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function size() { return registry.size; }
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
issue: issue,
|
|
122
|
+
lookup: lookup,
|
|
123
|
+
revoke: revoke,
|
|
124
|
+
size: size,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = {
|
|
129
|
+
create: create,
|
|
130
|
+
KINDS: Object.freeze(Object.keys(KINDS)),
|
|
131
|
+
HoneytokenError: HoneytokenError,
|
|
132
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.middleware.cspReport — Reporting-API endpoint for CSP / COEP /
|
|
4
|
+
* COOP / Permissions-Policy violations.
|
|
5
|
+
*
|
|
6
|
+
* The framework's default CSP appends `report-to default;` (see
|
|
7
|
+
* lib/middleware/security-headers.js); operators wire the matching
|
|
8
|
+
* `Reporting-Endpoints: default="https://app.example.com/csp-report"`
|
|
9
|
+
* header — and mount this middleware at the configured path. Browsers
|
|
10
|
+
* POST batches of violations as `application/reports+json`.
|
|
11
|
+
*
|
|
12
|
+
* var cspReport = b.middleware.cspReport.create({
|
|
13
|
+
* audit: b.audit,
|
|
14
|
+
* onReport: function (report) { metrics.count("csp.violation", 1, { directive: report.body.effectiveDirective }); },
|
|
15
|
+
* maxBytes: C.BYTES.kib(64),
|
|
16
|
+
* });
|
|
17
|
+
* router.post("/csp-report", cspReport);
|
|
18
|
+
*
|
|
19
|
+
* Audit shape: `csp.violation` (failure) per report; metadata carries
|
|
20
|
+
* the report.body fields (blockedURL, documentURL, effectiveDirective,
|
|
21
|
+
* sample, statusCode). Sample is truncated to 200 chars.
|
|
22
|
+
*
|
|
23
|
+
* Validation:
|
|
24
|
+
* - Refuses non-POST methods with 405
|
|
25
|
+
* - Refuses bodies > maxBytes (default 64 KiB) with 413
|
|
26
|
+
* - Refuses non-JSON bodies with 400
|
|
27
|
+
* - Accepts `application/reports+json` AND legacy `application/csp-report`
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
var C = require("../constants");
|
|
31
|
+
var lazyRequire = require("../lazy-require");
|
|
32
|
+
var safeBuffer = require("../safe-buffer");
|
|
33
|
+
var safeJson = require("../safe-json");
|
|
34
|
+
var validateOpts = require("../validate-opts");
|
|
35
|
+
|
|
36
|
+
var audit = lazyRequire(function () { return require("../audit"); });
|
|
37
|
+
|
|
38
|
+
var DEFAULT_MAX_BYTES = C.BYTES.kib(64);
|
|
39
|
+
var SAMPLE_TRUNCATE = 200;
|
|
40
|
+
|
|
41
|
+
function _truncate(value) {
|
|
42
|
+
if (typeof value !== "string") return value;
|
|
43
|
+
if (value.length <= SAMPLE_TRUNCATE) return value;
|
|
44
|
+
return value.slice(0, SAMPLE_TRUNCATE) + "…";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function _normalizeOne(reportLike) {
|
|
48
|
+
// Reporting API shape: { type, age, url, user_agent, body: {...} }
|
|
49
|
+
// Legacy CSP shape: { "csp-report": { ... } }
|
|
50
|
+
if (!reportLike || typeof reportLike !== "object") return null;
|
|
51
|
+
if (reportLike["csp-report"] && typeof reportLike["csp-report"] === "object") {
|
|
52
|
+
var legacy = reportLike["csp-report"];
|
|
53
|
+
return {
|
|
54
|
+
type: "csp-violation",
|
|
55
|
+
url: legacy["document-uri"] || null,
|
|
56
|
+
body: {
|
|
57
|
+
documentURL: legacy["document-uri"] || null,
|
|
58
|
+
blockedURL: legacy["blocked-uri"] || null,
|
|
59
|
+
effectiveDirective: legacy["effective-directive"] || legacy["violated-directive"] || null,
|
|
60
|
+
statusCode: legacy["status-code"] || null,
|
|
61
|
+
sample: _truncate(legacy["script-sample"] || ""),
|
|
62
|
+
sourceFile: legacy["source-file"] || null,
|
|
63
|
+
lineNumber: legacy["line-number"] || null,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (reportLike.type && reportLike.body && typeof reportLike.body === "object") {
|
|
68
|
+
return {
|
|
69
|
+
type: reportLike.type,
|
|
70
|
+
url: reportLike.url || null,
|
|
71
|
+
body: {
|
|
72
|
+
documentURL: reportLike.body.documentURL || null,
|
|
73
|
+
blockedURL: reportLike.body.blockedURL || null,
|
|
74
|
+
effectiveDirective: reportLike.body.effectiveDirective || null,
|
|
75
|
+
statusCode: reportLike.body.statusCode || null,
|
|
76
|
+
sample: _truncate(reportLike.body.sample || ""),
|
|
77
|
+
sourceFile: reportLike.body.sourceFile || null,
|
|
78
|
+
lineNumber: reportLike.body.lineNumber || null,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function create(opts) {
|
|
86
|
+
opts = opts || {};
|
|
87
|
+
validateOpts(opts, ["audit", "onReport", "maxBytes"], "middleware.cspReport");
|
|
88
|
+
var maxBytes = (typeof opts.maxBytes === "number" && isFinite(opts.maxBytes) && opts.maxBytes > 0)
|
|
89
|
+
? opts.maxBytes : DEFAULT_MAX_BYTES;
|
|
90
|
+
var onReport = (typeof opts.onReport === "function") ? opts.onReport : null;
|
|
91
|
+
|
|
92
|
+
return async function cspReport(req, res, _next) {
|
|
93
|
+
if (req.method !== "POST") {
|
|
94
|
+
res.writeHead(405, { "Allow": "POST" }); // allow:raw-byte-literal — HTTP 405 status
|
|
95
|
+
res.end();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
var body;
|
|
99
|
+
try {
|
|
100
|
+
body = await safeBuffer.boundedChunkCollector(req, { maxBytes: maxBytes });
|
|
101
|
+
} catch (_e) {
|
|
102
|
+
res.writeHead(413); // allow:raw-byte-literal — HTTP 413 status
|
|
103
|
+
res.end();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
var parsed;
|
|
107
|
+
try { parsed = safeJson.parse(body.toString("utf8")); }
|
|
108
|
+
catch (_e) {
|
|
109
|
+
res.writeHead(400); // allow:raw-byte-literal — HTTP 400 status
|
|
110
|
+
res.end();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
var reports = Array.isArray(parsed) ? parsed : [parsed];
|
|
114
|
+
for (var i = 0; i < reports.length; i++) {
|
|
115
|
+
var normalized = _normalizeOne(reports[i]);
|
|
116
|
+
if (!normalized) continue;
|
|
117
|
+
try {
|
|
118
|
+
audit().safeEmit({
|
|
119
|
+
action: "csp.violation",
|
|
120
|
+
outcome: "failure",
|
|
121
|
+
metadata: Object.assign({ type: normalized.type, url: normalized.url }, normalized.body),
|
|
122
|
+
});
|
|
123
|
+
} catch (_e) { /* audit best-effort */ }
|
|
124
|
+
if (onReport) {
|
|
125
|
+
try { onReport(normalized); } catch (_e) { /* hook best-effort */ }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
res.writeHead(204); // allow:raw-byte-literal — HTTP 204 status
|
|
129
|
+
res.end();
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = { create: create };
|
package/lib/middleware/index.js
CHANGED
|
@@ -32,6 +32,7 @@ var cookies = require("./cookies");
|
|
|
32
32
|
var cors = require("./cors");
|
|
33
33
|
var dailyByteQuota = require("./daily-byte-quota");
|
|
34
34
|
var cspNonce = require("./csp-nonce");
|
|
35
|
+
var cspReport = require("./csp-report");
|
|
35
36
|
var csrfProtect = require("./csrf-protect");
|
|
36
37
|
var dbRoleFor = require("./db-role-for");
|
|
37
38
|
var dpop = require("./dpop");
|
|
@@ -88,6 +89,7 @@ module.exports = {
|
|
|
88
89
|
compression: compression.create,
|
|
89
90
|
cookies: cookies.create,
|
|
90
91
|
cspNonce: cspNonce.create,
|
|
92
|
+
cspReport: cspReport.create,
|
|
91
93
|
securityTxt: securityTxt.create,
|
|
92
94
|
sse: sse.create,
|
|
93
95
|
requestLog: requestLog.create,
|
package/lib/network-tls.js
CHANGED
|
@@ -329,6 +329,67 @@ function captureBaselineFingerprints() {
|
|
|
329
329
|
STATE.baselineFingerprints = STATE.cas.map(function (e) { return e.meta.fingerprint256; });
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
// pinsetDriftMonitor — periodic check that emits audit + observability
|
|
333
|
+
// events when the trust-store fingerprint set drifts from the captured
|
|
334
|
+
// baseline. Different intent from expiryMonitor: this fires when a
|
|
335
|
+
// CA is added or removed (by operator config-flip OR by a tampered
|
|
336
|
+
// MANIFEST / vendor refresh), not when an existing one approaches
|
|
337
|
+
// validity expiry.
|
|
338
|
+
//
|
|
339
|
+
// b.network.tls.captureBaselineFingerprints(); // at boot
|
|
340
|
+
// var mon = b.network.tls.pinsetDriftMonitor({
|
|
341
|
+
// intervalMs: C.TIME.minutes(15),
|
|
342
|
+
// onDrift: function (drift) { /* operator hook */ },
|
|
343
|
+
// });
|
|
344
|
+
//
|
|
345
|
+
// Audit emissions:
|
|
346
|
+
// network.tls.pinset.drift_check — every check, ok / warn
|
|
347
|
+
// network.tls.pinset.drifted — when added.length || removed.length
|
|
348
|
+
function pinsetDriftMonitor(opts) {
|
|
349
|
+
opts = opts || {};
|
|
350
|
+
var intervalMs = opts.intervalMs;
|
|
351
|
+
var auditOn = opts.audit !== false;
|
|
352
|
+
if (typeof intervalMs !== "number" || !isFinite(intervalMs) || intervalMs <= 0) {
|
|
353
|
+
throw new TlsTrustError("tls/bad-interval",
|
|
354
|
+
"tls.pinsetDriftMonitor: intervalMs must be a positive finite number");
|
|
355
|
+
}
|
|
356
|
+
function _tick() {
|
|
357
|
+
var drift;
|
|
358
|
+
try { drift = detectBaselineDrift(); }
|
|
359
|
+
catch (_e) { return; }
|
|
360
|
+
if (drift === null) return; // baseline not captured; nothing to compare
|
|
361
|
+
if (auditOn) {
|
|
362
|
+
try {
|
|
363
|
+
audit().safeEmit({
|
|
364
|
+
action: "network.tls.pinset.drift_check",
|
|
365
|
+
outcome: drift.drifted ? "warn" : "ok",
|
|
366
|
+
metadata: { added: drift.added.length, removed: drift.removed.length },
|
|
367
|
+
});
|
|
368
|
+
} catch (_e) { /* drop-silent */ }
|
|
369
|
+
}
|
|
370
|
+
if (drift.drifted) {
|
|
371
|
+
try { observability().safeEvent("network.tls.pinset.drifted", 1, {}); }
|
|
372
|
+
catch (_e) { /* drop-silent */ }
|
|
373
|
+
if (auditOn) {
|
|
374
|
+
try {
|
|
375
|
+
audit().safeEmit({
|
|
376
|
+
action: "network.tls.pinset.drifted",
|
|
377
|
+
outcome: "failure",
|
|
378
|
+
metadata: { added: drift.added, removed: drift.removed },
|
|
379
|
+
});
|
|
380
|
+
} catch (_e) { /* drop-silent */ }
|
|
381
|
+
}
|
|
382
|
+
if (typeof opts.onDrift === "function") {
|
|
383
|
+
try { opts.onDrift(drift); } catch (_e) { /* operator hook */ }
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
var handle = safeAsync.repeating(_tick, intervalMs, { name: "tls-pinset-drift-monitor" });
|
|
388
|
+
return {
|
|
389
|
+
stop: function () { if (handle) { handle.stop(); handle = null; } },
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
332
393
|
function detectBaselineDrift() {
|
|
333
394
|
if (!STATE.baselineFingerprints) return null;
|
|
334
395
|
var current = STATE.cas.map(function (e) { return e.meta.fingerprint256; });
|
|
@@ -1683,6 +1744,7 @@ module.exports = {
|
|
|
1683
1744
|
purgeExpired: purgeExpired,
|
|
1684
1745
|
expiringSoon: expiringSoon,
|
|
1685
1746
|
expiryMonitor: expiryMonitor,
|
|
1747
|
+
pinsetDriftMonitor: pinsetDriftMonitor,
|
|
1686
1748
|
useSystemTrust: useSystemTrust,
|
|
1687
1749
|
isSystemTrustEnabled: isSystemTrustEnabled,
|
|
1688
1750
|
getTrustStore: getTrustStore,
|
package/lib/network.js
CHANGED
|
@@ -7,6 +7,7 @@ var proxy = require("./network-proxy");
|
|
|
7
7
|
var trust = require("./network-tls");
|
|
8
8
|
var heartbeat = require("./network-heartbeat");
|
|
9
9
|
var smtpPolicy = require("./network-smtp-policy");
|
|
10
|
+
var ssrfGuard = require("./ssrf-guard");
|
|
10
11
|
|
|
11
12
|
var validateOpts = require("./validate-opts");
|
|
12
13
|
var lazyRequire = require("./lazy-require");
|
|
@@ -226,6 +227,7 @@ module.exports = {
|
|
|
226
227
|
dane: smtpPolicy.dane,
|
|
227
228
|
tlsRpt: smtpPolicy.tlsRpt,
|
|
228
229
|
},
|
|
230
|
+
allowlist: { create: ssrfGuard.createAllowlist },
|
|
229
231
|
socket: {
|
|
230
232
|
setDefaultNoDelay: _setSocketNoDelay,
|
|
231
233
|
setDefaultKeepAlive: _setSocketKeepAlive,
|
package/lib/ssrf-guard.js
CHANGED
|
@@ -403,10 +403,65 @@ async function checkUrl(url, opts) {
|
|
|
403
403
|
return { url: parsed, ips: ips };
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
+
// b.network.allowlist — contextual per-call egress allowlist composing
|
|
407
|
+
// on ssrfGuard. Operators describe an allowed CIDR set + denylist;
|
|
408
|
+
// the resulting `assert(url)` either resolves to the validated IP set
|
|
409
|
+
// or throws SsrfError. Distinct from `ssrfGuard.checkUrl` (which uses
|
|
410
|
+
// the framework's hard-coded private/cloud-metadata ban list) — this
|
|
411
|
+
// is for cases where the operator's deployment has SPECIFIC outbound
|
|
412
|
+
// targets and everything else should be refused.
|
|
413
|
+
//
|
|
414
|
+
// var egress = b.network.allowlist.create({
|
|
415
|
+
// allow: ["api.partner.example.com", "192.0.2.0/24"],
|
|
416
|
+
// deny: ["api.partner.example.com/admin"],
|
|
417
|
+
// });
|
|
418
|
+
// await egress.assert("https://api.partner.example.com/v1/x");
|
|
419
|
+
function createAllowlist(opts) {
|
|
420
|
+
opts = opts || {};
|
|
421
|
+
var allowList = Array.isArray(opts.allow) ? opts.allow.slice() : [];
|
|
422
|
+
var denyList = Array.isArray(opts.deny) ? opts.deny.slice() : [];
|
|
423
|
+
if (allowList.length === 0) {
|
|
424
|
+
throw new SsrfError(
|
|
425
|
+
"network.allowlist.create requires at least one entry in `allow`",
|
|
426
|
+
"ssrf-guard/empty-allowlist", {});
|
|
427
|
+
}
|
|
428
|
+
function _matches(list, hostOrIp) {
|
|
429
|
+
for (var i = 0; i < list.length; i++) {
|
|
430
|
+
var entry = list[i];
|
|
431
|
+
if (entry === hostOrIp) return true;
|
|
432
|
+
if (entry.indexOf("/") !== -1) {
|
|
433
|
+
try { if (cidrContains(entry, hostOrIp)) return true; } catch (_e) { /* ignore */ }
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
async function assertUrl(url) {
|
|
439
|
+
var parsed;
|
|
440
|
+
try { parsed = new URL(url); } // allow:raw-new-url — local URL parse for hostname extraction
|
|
441
|
+
catch (_e) {
|
|
442
|
+
throw new SsrfError("invalid URL", "ssrf-guard/bad-url", { url: url });
|
|
443
|
+
}
|
|
444
|
+
var host = parsed.hostname;
|
|
445
|
+
if (!_matches(allowList, host)) {
|
|
446
|
+
throw new SsrfError(
|
|
447
|
+
"URL host '" + host + "' not on the operator allowlist",
|
|
448
|
+
"ssrf-guard/not-on-allowlist", { url: url, host: host });
|
|
449
|
+
}
|
|
450
|
+
if (_matches(denyList, host)) {
|
|
451
|
+
throw new SsrfError(
|
|
452
|
+
"URL host '" + host + "' on the operator denylist",
|
|
453
|
+
"ssrf-guard/on-denylist", { url: url, host: host });
|
|
454
|
+
}
|
|
455
|
+
return checkUrl(parsed.toString(), { allowInternal: true });
|
|
456
|
+
}
|
|
457
|
+
return { assert: assertUrl };
|
|
458
|
+
}
|
|
459
|
+
|
|
406
460
|
module.exports = {
|
|
407
461
|
classify: classify,
|
|
408
462
|
cidrContains: cidrContains,
|
|
409
463
|
checkUrl: checkUrl,
|
|
464
|
+
createAllowlist: createAllowlist,
|
|
410
465
|
isPrivate: function (ip) { return classify(ip) === "private"; },
|
|
411
466
|
isLoopback: function (ip) { return classify(ip) === "loopback"; },
|
|
412
467
|
isLinkLocal: function (ip) { return classify(ip) === "link-local"; },
|
package/package.json
CHANGED
package/sbom.cyclonedx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:7c951f5a-156e-49be-91c0-8a5a420faf2c",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-07T16:
|
|
8
|
+
"timestamp": "2026-05-07T16:35:12.500Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.8.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.8.40",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.40",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.8.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.8.40",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.8.
|
|
57
|
+
"ref": "@blamejs/core@0.8.40",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|