@blamejs/blamejs-shop 0.3.12 → 0.3.13
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/lib/admin.js +155 -113
- package/lib/asset-manifest.json +1 -1
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/README.md +3 -2
- package/lib/vendor/blamejs/SECURITY.md +3 -0
- package/lib/vendor/blamejs/api-snapshot.json +14 -2
- package/lib/vendor/blamejs/lib/agent-event-bus.js +4 -4
- package/lib/vendor/blamejs/lib/agent-idempotency.js +6 -6
- package/lib/vendor/blamejs/lib/agent-orchestrator.js +9 -9
- package/lib/vendor/blamejs/lib/agent-posture-chain.js +10 -10
- package/lib/vendor/blamejs/lib/agent-saga.js +6 -7
- package/lib/vendor/blamejs/lib/agent-snapshot.js +8 -8
- package/lib/vendor/blamejs/lib/agent-stream.js +3 -3
- package/lib/vendor/blamejs/lib/agent-tenant.js +4 -4
- package/lib/vendor/blamejs/lib/agent-trace.js +5 -5
- package/lib/vendor/blamejs/lib/ai-disclosure.js +3 -3
- package/lib/vendor/blamejs/lib/app.js +2 -2
- package/lib/vendor/blamejs/lib/archive-read.js +1 -1
- package/lib/vendor/blamejs/lib/archive-tar-read.js +1 -1
- package/lib/vendor/blamejs/lib/archive-wrap.js +5 -5
- package/lib/vendor/blamejs/lib/audit-tools.js +65 -5
- package/lib/vendor/blamejs/lib/audit.js +2 -2
- package/lib/vendor/blamejs/lib/auth/ciba.js +1 -1
- package/lib/vendor/blamejs/lib/auth/dpop.js +1 -1
- package/lib/vendor/blamejs/lib/auth/fal.js +1 -1
- package/lib/vendor/blamejs/lib/auth/fido-mds3.js +2 -3
- package/lib/vendor/blamejs/lib/auth/jwt-external.js +2 -2
- package/lib/vendor/blamejs/lib/auth/oauth.js +9 -9
- package/lib/vendor/blamejs/lib/auth/oid4vci.js +7 -7
- package/lib/vendor/blamejs/lib/auth/oid4vp.js +1 -1
- package/lib/vendor/blamejs/lib/auth/openid-federation.js +5 -5
- package/lib/vendor/blamejs/lib/auth/passkey.js +6 -6
- package/lib/vendor/blamejs/lib/auth/saml.js +1 -1
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +3 -6
- package/lib/vendor/blamejs/lib/backup/index.js +18 -18
- package/lib/vendor/blamejs/lib/cache.js +4 -4
- package/lib/vendor/blamejs/lib/calendar.js +5 -5
- package/lib/vendor/blamejs/lib/circuit-breaker.js +1 -1
- package/lib/vendor/blamejs/lib/cms-codec.js +2 -2
- package/lib/vendor/blamejs/lib/compliance.js +14 -14
- package/lib/vendor/blamejs/lib/crypto-field.js +58 -21
- package/lib/vendor/blamejs/lib/crypto.js +5 -6
- package/lib/vendor/blamejs/lib/db-query.js +131 -9
- package/lib/vendor/blamejs/lib/db.js +106 -22
- package/lib/vendor/blamejs/lib/external-db.js +64 -16
- package/lib/vendor/blamejs/lib/framework-schema.js +4 -4
- package/lib/vendor/blamejs/lib/guard-list-id.js +2 -2
- package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +1 -2
- package/lib/vendor/blamejs/lib/incident-report.js +150 -0
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +1 -1
- package/lib/vendor/blamejs/lib/mail-deploy.js +3 -3
- package/lib/vendor/blamejs/lib/mail-server-managesieve.js +2 -2
- package/lib/vendor/blamejs/lib/mail-server-pop3.js +2 -2
- package/lib/vendor/blamejs/lib/mail-store.js +1 -1
- package/lib/vendor/blamejs/lib/metrics.js +8 -8
- package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +1 -1
- package/lib/vendor/blamejs/lib/middleware/dpop.js +5 -5
- package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +21 -22
- package/lib/vendor/blamejs/lib/middleware/protected-resource-metadata.js +2 -2
- package/lib/vendor/blamejs/lib/network-dns-resolver.js +2 -2
- package/lib/vendor/blamejs/lib/network-dns.js +1 -2
- package/lib/vendor/blamejs/lib/network-tls.js +0 -1
- package/lib/vendor/blamejs/lib/outbox.js +1 -1
- package/lib/vendor/blamejs/lib/pqc-agent.js +1 -1
- package/lib/vendor/blamejs/lib/retention.js +1 -1
- package/lib/vendor/blamejs/lib/retry.js +1 -1
- package/lib/vendor/blamejs/lib/safe-archive.js +2 -2
- package/lib/vendor/blamejs/lib/safe-ical.js +2 -2
- package/lib/vendor/blamejs/lib/safe-mime.js +1 -1
- package/lib/vendor/blamejs/lib/self-update-standalone-verifier.js +1 -1
- package/lib/vendor/blamejs/lib/self-update.js +2 -2
- package/lib/vendor/blamejs/lib/static.js +1 -1
- package/lib/vendor/blamejs/lib/subject.js +2 -2
- package/lib/vendor/blamejs/lib/vault/index.js +64 -1
- package/lib/vendor/blamejs/lib/vault/rotate.js +19 -0
- package/lib/vendor/blamejs/lib/vendor-data.js +1 -1
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.7.json +77 -0
- package/lib/vendor/blamejs/scripts/release.js +28 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-tools-dual-control.test.js +115 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +163 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +102 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-column-gate.test.js +150 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-key-aad.test.js +109 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +53 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/incident-report.test.js +65 -0
- package/package.json +1 -1
|
@@ -88,16 +88,41 @@ function _bumpMinor(version) {
|
|
|
88
88
|
return parts[0] + "." + (parts[1] + 1) + ".0";
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
// Quote a single argument for a Windows cmd.exe command line. Tokens
|
|
92
|
+
// made only of safe characters pass through unquoted; anything else is
|
|
93
|
+
// double-quoted with embedded quotes doubled.
|
|
94
|
+
function _quoteWinArg(a) {
|
|
95
|
+
a = String(a);
|
|
96
|
+
if (/^[A-Za-z0-9_@.\-/:=]+$/.test(a)) return a;
|
|
97
|
+
return '"' + a.replace(/"/g, '""') + '"';
|
|
98
|
+
}
|
|
99
|
+
|
|
91
100
|
function _run(cmd, args, opts) {
|
|
92
101
|
opts = opts || {};
|
|
93
|
-
|
|
102
|
+
args = args || [];
|
|
103
|
+
var spawnCmd = cmd;
|
|
104
|
+
var spawnArgs = args;
|
|
105
|
+
var useShell = false;
|
|
106
|
+
if (_needsShell(cmd)) {
|
|
107
|
+
// Windows resolves npm / npx through .cmd shims that can only be
|
|
108
|
+
// launched via a shell (the CVE-2024-27980 mitigation refuses to
|
|
109
|
+
// spawn .cmd files without one). Node 26's DEP0190 deprecates
|
|
110
|
+
// pairing an args ARRAY with shell:true because the args would be
|
|
111
|
+
// concatenated onto the command line without escaping — a quoting /
|
|
112
|
+
// injection hazard. Build a single, explicitly-quoted command
|
|
113
|
+
// string and pass NO args array, which is the supported shape.
|
|
114
|
+
spawnCmd = [cmd].concat(args.map(_quoteWinArg)).join(" ");
|
|
115
|
+
spawnArgs = undefined;
|
|
116
|
+
useShell = true;
|
|
117
|
+
}
|
|
118
|
+
var rv = childProcess.spawnSync(spawnCmd, spawnArgs, {
|
|
94
119
|
cwd: opts.cwd || ROOT,
|
|
95
120
|
stdio: opts.stdio || "inherit",
|
|
96
121
|
env: Object.assign({}, process.env, opts.env || {}),
|
|
97
|
-
shell:
|
|
122
|
+
shell: useShell,
|
|
98
123
|
});
|
|
99
124
|
if (rv.status !== 0 && !opts.allowFail) {
|
|
100
|
-
throw new Error("release: " + cmd + " " +
|
|
125
|
+
throw new Error("release: " + cmd + " " + args.join(" ") +
|
|
101
126
|
" failed with status " + rv.status);
|
|
102
127
|
}
|
|
103
128
|
return rv;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.auditTools.purge dual-control gate.
|
|
4
|
+
*
|
|
5
|
+
* When audit_log is placed under dual control, physically purging the
|
|
6
|
+
* audit chain requires a consumed m-of-n grant in addition to a
|
|
7
|
+
* verified archive and confirm:true — one operator must not be able to
|
|
8
|
+
* erase the tamper-evident chain alone. The grant's action is also
|
|
9
|
+
* bound, so a grant minted for a different operation can't be replayed
|
|
10
|
+
* against a purge.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
var helpers = require("../helpers");
|
|
14
|
+
var b = helpers.b;
|
|
15
|
+
var check = helpers.check;
|
|
16
|
+
var fs = helpers.fs;
|
|
17
|
+
var os = helpers.os;
|
|
18
|
+
var path = helpers.path;
|
|
19
|
+
var setupTestDb = helpers.setupTestDb;
|
|
20
|
+
var teardownTestDb = helpers.teardownTestDb;
|
|
21
|
+
|
|
22
|
+
var PASS = Buffer.from("operator-passphrase");
|
|
23
|
+
var PURGE_ACTION = "auditTools.purge";
|
|
24
|
+
|
|
25
|
+
async function _seedAuditRows(count) {
|
|
26
|
+
b.audit.registerNamespace("test");
|
|
27
|
+
for (var i = 0; i < count; i++) {
|
|
28
|
+
await b.audit.record({
|
|
29
|
+
actor: { userId: "u-" + i }, action: "test.seeded", outcome: "success", metadata: { i: i },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function _expectCode(fn, code) {
|
|
35
|
+
var threw = null;
|
|
36
|
+
try { await fn(); } catch (e) { threw = e; }
|
|
37
|
+
return threw && threw.code === code;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function run() {
|
|
41
|
+
var dir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-at-dc-"));
|
|
42
|
+
try {
|
|
43
|
+
await setupTestDb(dir);
|
|
44
|
+
await _seedAuditRows(5);
|
|
45
|
+
await b.audit.checkpoint();
|
|
46
|
+
|
|
47
|
+
var out = path.join(dir, "bundles", "archive-dc");
|
|
48
|
+
await b.auditTools.archive({ before: Date.now() + 1000, out: out, passphrase: PASS });
|
|
49
|
+
|
|
50
|
+
// Simulate audit_log being under dual control (m-of-n). The gate
|
|
51
|
+
// override returns a gate descriptor for the audit_log table; the
|
|
52
|
+
// denial branches all fire before any chain mutation, so the same
|
|
53
|
+
// verified bundle is reused across them.
|
|
54
|
+
var sawTable = null;
|
|
55
|
+
var gate = function (table) { sawTable = table; return { m: 2, n: 3 }; };
|
|
56
|
+
|
|
57
|
+
// No grant → refused.
|
|
58
|
+
check("purge under dual control refuses without a grant",
|
|
59
|
+
await _expectCode(function () {
|
|
60
|
+
return b.auditTools.purge({ archive: out, passphrase: PASS, confirm: true, checkDualControlGate: gate });
|
|
61
|
+
}, "audit-tools/dual-control-required"));
|
|
62
|
+
check("gate was consulted for the audit_log table", sawTable === "audit_log");
|
|
63
|
+
|
|
64
|
+
// Grant not ready (not a consumed m-of-n grant) → refused.
|
|
65
|
+
check("purge refuses a not-ready grant",
|
|
66
|
+
await _expectCode(function () {
|
|
67
|
+
return b.auditTools.purge({
|
|
68
|
+
archive: out, passphrase: PASS, confirm: true, checkDualControlGate: gate,
|
|
69
|
+
dualControlGrant: { ready: false, action: PURGE_ACTION },
|
|
70
|
+
});
|
|
71
|
+
}, "audit-tools/dual-control-grant-not-ready"));
|
|
72
|
+
|
|
73
|
+
// Grant minted for a different action → refused (action binding).
|
|
74
|
+
check("purge refuses a grant bound to a different action",
|
|
75
|
+
await _expectCode(function () {
|
|
76
|
+
return b.auditTools.purge({
|
|
77
|
+
archive: out, passphrase: PASS, confirm: true, checkDualControlGate: gate,
|
|
78
|
+
dualControlGrant: { ready: true, action: "db.eraseHard" },
|
|
79
|
+
});
|
|
80
|
+
}, "audit-tools/dual-control-grant-mismatch"));
|
|
81
|
+
|
|
82
|
+
// Confirm the chain is still intact — no denial mutated it.
|
|
83
|
+
var beforeRows = await b.clusterStorage.executeAll("SELECT COUNT(*) as c FROM audit_log");
|
|
84
|
+
check("audit_log untouched by denied purges", Number(beforeRows[0].c) >= 5);
|
|
85
|
+
|
|
86
|
+
// Valid consumed grant for the purge action → proceeds.
|
|
87
|
+
var pres = await b.auditTools.purge({
|
|
88
|
+
archive: out, passphrase: PASS, confirm: true, checkDualControlGate: gate,
|
|
89
|
+
dualControlGrant: { ready: true, action: PURGE_ACTION },
|
|
90
|
+
});
|
|
91
|
+
check("purge with a valid grant succeeds", pres.purged === true);
|
|
92
|
+
check("purge reports dual-control was consumed", pres.dualControlConsumed === true);
|
|
93
|
+
check("purge deleted rows", pres.rowsDeleted > 0);
|
|
94
|
+
|
|
95
|
+
// No gate (audit_log not under dual control) → confirm + archive
|
|
96
|
+
// alone is sufficient (no grant required) — the gate is opt-in.
|
|
97
|
+
var noGateResult = b.auditTools.purge;
|
|
98
|
+
check("purge surface present", typeof noGateResult === "function");
|
|
99
|
+
} finally {
|
|
100
|
+
await teardownTestDb(dir);
|
|
101
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_e) {}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log("OK — audit-tools dual-control tests");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { run: run };
|
|
108
|
+
if (require.main === module) {
|
|
109
|
+
// Rethrow on failure so Node surfaces the error and exits non-zero,
|
|
110
|
+
// instead of logging the caught error object — a taint analyzer traces
|
|
111
|
+
// a logged error back to the test passphrase fixture (a non-secret
|
|
112
|
+
// constant) and raises a false clear-text-logging alert.
|
|
113
|
+
run().then(function () { process.exit(0); })
|
|
114
|
+
.catch(function (err) { process.exitCode = 1; throw err; });
|
|
115
|
+
}
|
|
@@ -309,6 +309,7 @@ var VALID_ALLOW_CLASSES = {
|
|
|
309
309
|
"inline-require": 1,
|
|
310
310
|
"inline-require-non-empty-string-validation": 1,
|
|
311
311
|
"internal-binding-in-prose": 1,
|
|
312
|
+
"internal-narrative-comment": 1,
|
|
312
313
|
"list-without-pagination": 1,
|
|
313
314
|
"math-random-noncrypto": 1,
|
|
314
315
|
"no-number-money-arithmetic": 1,
|
|
@@ -7951,6 +7952,101 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
7951
7952
|
reason: "Codex P1 on v0.11.4 PR #109 — b.audit.useStore({ record }) inlined await on operator-supplied callback. A stalled network call neither resolves nor rejects ⇒ b.audit.record() never returns, emit/safeEmit drains stall behind it; hot-path observability emission is supposed to be drop-silent on hangs. Defense: wrap every external-callback await in safeAsync.withTimeout. Detector catches future operator-supplied async hooks on the same shape (_externalStore / _userStore / _externalSink / _externalCb / _externalCallback / _externalHook).",
|
|
7952
7953
|
},
|
|
7953
7954
|
|
|
7955
|
+
{
|
|
7956
|
+
// v0.14.7 — physically deleting audit-chain rows is a
|
|
7957
|
+
// single-operator-can-erase-evidence risk. `auditTools.purge`
|
|
7958
|
+
// (and the low-level `db.purgeAuditChain` it composes) MUST
|
|
7959
|
+
// consult a dual-control gate — two distinct authorizers — before
|
|
7960
|
+
// the destructive delete runs, so one compromised credential can't
|
|
7961
|
+
// silently truncate the tamper-evident chain. The gate primitive
|
|
7962
|
+
// is `db._checkDualControlGate(table)` (resolved at runtime); the
|
|
7963
|
+
// purge path threads it via `_resolveDualControlGate(opts)` and
|
|
7964
|
+
// refuses with `dual-control-required` when no grant is present.
|
|
7965
|
+
//
|
|
7966
|
+
// Detector: any lib/ call to `purgeAuditChain(` MUST appear in a
|
|
7967
|
+
// file that also names the gate (`_checkDualControlGate` /
|
|
7968
|
+
// `_resolveDualControlGate` / `dualControlGrant`). A new caller
|
|
7969
|
+
// that physically deletes chain rows without routing through the
|
|
7970
|
+
// gate trips here.
|
|
7971
|
+
id: "audit-purge-without-dual-control-gate",
|
|
7972
|
+
primitive: "auditTools.purge({ dualControlGrant }) / db.purgeAuditChain — physical audit-chain deletion MUST consult the dual-control gate (_checkDualControlGate / _resolveDualControlGate); one operator must not be able to erase the tamper-evident chain alone",
|
|
7973
|
+
regex: /\bpurgeAuditChain\s*\(/,
|
|
7974
|
+
requires: /_checkDualControlGate|_resolveDualControlGate|dualControlGrant/,
|
|
7975
|
+
skipCommentLines: true,
|
|
7976
|
+
allowlist: [],
|
|
7977
|
+
reason: "v0.14.7 — audit-chain purge is irreversible and tamper-evidence-destroying. The discipline: the chain may only be truncated under a two-authorizer dual-control grant. db.js defines the gate (_checkDualControlGate) and audit-tools.js purge() resolves+enforces it (_resolveDualControlGate) before calling db().purgeAuditChain; both files satisfy the companion check. A future file that calls purgeAuditChain without naming the gate would let a single operator delete evidence — exactly the shape this blocks.",
|
|
7978
|
+
},
|
|
7979
|
+
|
|
7980
|
+
{
|
|
7981
|
+
// v0.14.7 — raw-SQL fragments passed to `whereRaw` / the
|
|
7982
|
+
// WhereBuilder `.raw(sql, params)` escape hatch must parameterize
|
|
7983
|
+
// values via the `params` array, NOT interpolate or concatenate
|
|
7984
|
+
// them into the SQL string. `_assertRawNoStringLiteral` refuses an
|
|
7985
|
+
// embedded `'...'` literal at runtime (unless the caller opts in
|
|
7986
|
+
// with `allowLiterals`), but the static discipline is stronger:
|
|
7987
|
+
// lib/ must never BUILD the raw SQL by `${...}` template
|
|
7988
|
+
// interpolation or `"..." + value` string concatenation — that is
|
|
7989
|
+
// the injection shape the bound-params API exists to prevent.
|
|
7990
|
+
//
|
|
7991
|
+
// Detector fires on a whereRaw/.raw call whose first argument is a
|
|
7992
|
+
// template literal containing `${` or a string-concat expression.
|
|
7993
|
+
// A no-arg `bodyParser.raw()` (different `.raw`) does not match —
|
|
7994
|
+
// the regex requires an interpolated/concatenated string argument.
|
|
7995
|
+
id: "whereraw-interpolated-or-concatenated-sql",
|
|
7996
|
+
primitive: "whereRaw(sql, params) / qb.raw(sql, params) — pass values through the bound `params` array; never `${...}`-interpolate or `+`-concatenate them into the SQL string (that is the injection shape the params API prevents)",
|
|
7997
|
+
regex: /(?:whereRaw|\.raw)\s*\(\s*(?:`[^`]*\$\{|["'][^"']*["']\s*\+|[A-Za-z_$][\w$]*\s*\+\s*["'`])/,
|
|
7998
|
+
skipCommentLines: true,
|
|
7999
|
+
allowlist: [],
|
|
8000
|
+
reason: "v0.14.7 — b.db whereRaw / WhereBuilder.raw accept an operator SQL fragment plus a bound-params array. Concatenating or template-interpolating a value into the fragment defeats the placeholder binding and reintroduces SQL injection. lib/ has zero such call sites today; the detector keeps it that way (the runtime _assertRawNoStringLiteral gate is the operator-facing backstop, this is the framework-internal one). No-argument `.raw()` mountings (e.g. bodyParser.raw()) don't match — an interpolated/concatenated string argument is required.",
|
|
8001
|
+
},
|
|
8002
|
+
|
|
8003
|
+
{
|
|
8004
|
+
// v0.14.7 — equality-lookup ("derived") hashes for sealed columns
|
|
8005
|
+
// must be computed through cryptoField._computeDerivedHash, which
|
|
8006
|
+
// honours the per-table / per-column mode policy (salted-sha3 by
|
|
8007
|
+
// default, hmac-shake256 opt-in keyed off vault.getDerivedHashMacKey).
|
|
8008
|
+
// Hand-rolling `sha3Hash(vault.getDerivedHashSalt() + ns + value)`
|
|
8009
|
+
// at a new site bypasses the keyed-MAC option AND the mode policy,
|
|
8010
|
+
// and risks a static-salt regression if the salt source is changed
|
|
8011
|
+
// for one caller but not the canonical helper.
|
|
8012
|
+
//
|
|
8013
|
+
// Detector: direct use of `getDerivedHashSalt().toString("hex") +`
|
|
8014
|
+
// outside crypto-field.js (the canonical helper). The helper itself
|
|
8015
|
+
// is allowlisted.
|
|
8016
|
+
id: "derived-hash-handrolled-outside-crypto-field",
|
|
8017
|
+
primitive: "cryptoField derived/lookup hashes — compute via _computeDerivedHash (honours salted-sha3 vs hmac-shake256 mode + vault.getDerivedHashMacKey); do not hand-roll sha3Hash(getDerivedHashSalt() + ns + value) at call sites",
|
|
8018
|
+
regex: /getDerivedHashSalt\s*\(\s*\)\s*\.toString\s*\(\s*["']hex["']\s*\)\s*\+/,
|
|
8019
|
+
skipCommentLines: true,
|
|
8020
|
+
allowlist: [
|
|
8021
|
+
// The canonical helper — _computeDerivedHash branches on mode here.
|
|
8022
|
+
"lib/crypto-field.js",
|
|
8023
|
+
],
|
|
8024
|
+
reason: "v0.14.7 — derived-hash equality lookups gained a keyed mode (hmac-shake256 off vault.getDerivedHashMacKey) alongside the salted-sha3 default. The mode decision lives in cryptoField._computeDerivedHash. A site that hand-derives the hash with getDerivedHashSalt() bypasses the keyed option and the per-column mode policy; only the canonical helper (crypto-field.js) may name the salt directly.",
|
|
8025
|
+
},
|
|
8026
|
+
|
|
8027
|
+
{
|
|
8028
|
+
// v0.14.7 — a `db.auth.failed` audit row must record WHICH
|
|
8029
|
+
// relation the rejected credential attempted to reach
|
|
8030
|
+
// (`attemptedTable`), so an operator triaging a credential-abuse
|
|
8031
|
+
// event can scope blast radius without correlating to the raw SQL
|
|
8032
|
+
// log. external-db extracts the target relation defensively
|
|
8033
|
+
// (_extractTargetRelation) and stamps both resource.attemptedTable
|
|
8034
|
+
// and the audit metadata. Emitting the auth-failure audit without
|
|
8035
|
+
// it loses the forensic signal.
|
|
8036
|
+
//
|
|
8037
|
+
// Detector: an `action: "db.auth.failed"` audit emit MUST appear in
|
|
8038
|
+
// a file that also names `attemptedTable`. The metric emitter
|
|
8039
|
+
// (_emitMetric("db.auth.failed", ...)) uses a positional shape, not
|
|
8040
|
+
// `action:`, so it doesn't match.
|
|
8041
|
+
id: "db-auth-failed-audit-without-attempted-relation",
|
|
8042
|
+
primitive: "db.auth.failed audit — stamp the attempted relation (attemptedTable, via _extractTargetRelation) on credential-rejection audit rows so blast radius is triageable without the raw SQL log",
|
|
8043
|
+
regex: /action\s*:\s*["']db\.auth\.failed["']/,
|
|
8044
|
+
requires: /attemptedTable/,
|
|
8045
|
+
skipCommentLines: true,
|
|
8046
|
+
allowlist: [],
|
|
8047
|
+
reason: "v0.14.7 — external-db credential-rejection audits (SQLSTATE 28000 / 28P01 / 42501) now carry attemptedTable, the relation the rejected identity tried to touch, extracted defensively from the SQL. The detector requires any file emitting an action:'db.auth.failed' audit to also name attemptedTable so a future emitter can't drop the forensic field.",
|
|
8048
|
+
},
|
|
8049
|
+
|
|
7954
8050
|
];
|
|
7955
8051
|
|
|
7956
8052
|
// @example placeholder detection lives in
|
|
@@ -10565,9 +10661,9 @@ function testPrimitiveReachability() {
|
|
|
10565
10661
|
// onDeny / problemDetails opts. A new deny-path
|
|
10566
10662
|
// middleware that hardcodes
|
|
10567
10663
|
// `res.writeHead(<4xx/5xx>, { "Content-Type": ... })`
|
|
10568
|
-
// locks consumers out of the response shape —
|
|
10569
|
-
//
|
|
10570
|
-
//
|
|
10664
|
+
// locks consumers out of the response shape — which is
|
|
10665
|
+
// what pinned rate-limit's 429 to text/plain before this
|
|
10666
|
+
// convention existed. ----
|
|
10571
10667
|
function testDenyPathComposesDenyResponse() {
|
|
10572
10668
|
// class: deny-path-hardcoded-response
|
|
10573
10669
|
var MW_ROOT = path.resolve(LIB_ROOT, "middleware");
|
|
@@ -10610,9 +10706,73 @@ function testDenyPathComposesDenyResponse() {
|
|
|
10610
10706
|
violations);
|
|
10611
10707
|
}
|
|
10612
10708
|
|
|
10709
|
+
// ---- Pattern: operator-facing source comments must describe the code,
|
|
10710
|
+
// not the internal authoring process. These shapes are
|
|
10711
|
+
// internal slice / bug / plan IDs, code-review-process
|
|
10712
|
+
// residue (Codex P-levels, PR numbers), and dated
|
|
10713
|
+
// decision parentheticals — none of which an operator
|
|
10714
|
+
// reading the shipped source can map to anything in their
|
|
10715
|
+
// own checkout. Genuinely operator-meaningful references
|
|
10716
|
+
// (RFC / CVE / NIST / CWE, "since vX.Y.Z", established
|
|
10717
|
+
// terse markers like D-M4 / AUTH-32) are NOT matched.
|
|
10718
|
+
// Allowlist a false positive with `// allow:internal-
|
|
10719
|
+
// narrative-comment`. ----
|
|
10720
|
+
function testNoInternalNarrativeComments() {
|
|
10721
|
+
// class: internal-narrative-comment
|
|
10722
|
+
var NARRATIVE = [
|
|
10723
|
+
{ re: /\b(?:SUBSTRATE|BUG|MAIL)-\d+\b/, what: "internal slice/bug ID" },
|
|
10724
|
+
{ re: /\b(?:D-[MLH]\d+|AUTH-\d+|CRYPTO-\d+|SUPPLY-\d+)\b/, what: "internal domain/slice ID" },
|
|
10725
|
+
{ re: /\bCodex\s+P\d/, what: "code-review-process reference (Codex P#)" },
|
|
10726
|
+
{ re: /\bF-[A-Z]{2,}-\d+\b/, what: "internal feature/plan item ID" },
|
|
10727
|
+
{ re: /\bPR\s+#\d+\b/, what: "pull-request number (process residue)" },
|
|
10728
|
+
{ re: /\b[Aa]udit\s+\d{4}-\d{2}-\d{2}/, what: "dated audit/decision residue" },
|
|
10729
|
+
{ re: /\bReported\s+\d{4}-\d{2}-\d{2}/, what: "dated report residue" },
|
|
10730
|
+
{ re: /\bCore Rule\s+§\d/, what: "internal CLAUDE.md rule-number citation" },
|
|
10731
|
+
];
|
|
10732
|
+
var files = _libFiles();
|
|
10733
|
+
var bad = [];
|
|
10734
|
+
var jsdocLineRe = /^\s*\*/;
|
|
10735
|
+
for (var fi = 0; fi < files.length; fi++) {
|
|
10736
|
+
var rel = _relPath(files[fi]);
|
|
10737
|
+
var src = fs.readFileSync(files[fi], "utf8");
|
|
10738
|
+
var lines = src.split("\n");
|
|
10739
|
+
for (var li = 0; li < lines.length; li++) {
|
|
10740
|
+
var line = lines[li];
|
|
10741
|
+
// Scan the comment portion only: a ` * `-prefixed JSDoc line in
|
|
10742
|
+
// full, or the text after a line's first `//`.
|
|
10743
|
+
var comment = null;
|
|
10744
|
+
if (jsdocLineRe.test(line)) {
|
|
10745
|
+
comment = line;
|
|
10746
|
+
} else {
|
|
10747
|
+
var idx = line.indexOf("//");
|
|
10748
|
+
if (idx !== -1) comment = line.slice(idx);
|
|
10749
|
+
}
|
|
10750
|
+
if (comment === null) continue;
|
|
10751
|
+
for (var p = 0; p < NARRATIVE.length; p++) {
|
|
10752
|
+
var m = comment.match(NARRATIVE[p].re);
|
|
10753
|
+
if (m) {
|
|
10754
|
+
bad.push({
|
|
10755
|
+
file: rel,
|
|
10756
|
+
line: li + 1,
|
|
10757
|
+
content: NARRATIVE[p].what + ": `" + m[0] + "` in an operator-facing comment — " +
|
|
10758
|
+
"describe the change, not the internal process (strip the label / drop the " +
|
|
10759
|
+
"dated parenthetical / cite the public reason instead)",
|
|
10760
|
+
});
|
|
10761
|
+
break;
|
|
10762
|
+
}
|
|
10763
|
+
}
|
|
10764
|
+
}
|
|
10765
|
+
}
|
|
10766
|
+
bad = _filterMarkers(bad, "internal-narrative-comment");
|
|
10767
|
+
_report("operator-facing source comments must not carry internal-process narrative " +
|
|
10768
|
+
"(slice / bug / plan IDs, Codex / PR references, dated decision parentheticals)",
|
|
10769
|
+
bad);
|
|
10770
|
+
}
|
|
10771
|
+
|
|
10613
10772
|
async function run() {
|
|
10614
10773
|
testPrimitiveReachability();
|
|
10615
10774
|
testDenyPathComposesDenyResponse();
|
|
10775
|
+
testNoInternalNarrativeComments();
|
|
10616
10776
|
testNoOrphanAllowClass();
|
|
10617
10777
|
testNoRawByteLiterals();
|
|
10618
10778
|
testNoRawTimeLiterals();
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.cryptoField derived-hash modes.
|
|
4
|
+
*
|
|
5
|
+
* Equality-lookup ("derived") hashes for sealed columns can be computed
|
|
6
|
+
* two ways:
|
|
7
|
+
* - salted-sha3 (default) — SHA3-512 over a per-deployment salt.
|
|
8
|
+
* - hmac-shake256 (opt-in) — keyed MAC off vault.getDerivedHashMacKey,
|
|
9
|
+
* so the hash is unforgeable without the
|
|
10
|
+
* per-deployment MAC key.
|
|
11
|
+
*
|
|
12
|
+
* The mode is chosen per-table (derivedHashMode) or per-column
|
|
13
|
+
* (spec.mode). The decision lives in _computeDerivedHash; call sites
|
|
14
|
+
* must never hand-roll the hash.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
var helpers = require("../helpers");
|
|
18
|
+
var b = helpers.b;
|
|
19
|
+
var check = helpers.check;
|
|
20
|
+
var fs = require("fs");
|
|
21
|
+
var os = require("os");
|
|
22
|
+
var path = require("path");
|
|
23
|
+
|
|
24
|
+
async function run() {
|
|
25
|
+
var dir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-cf-dh-"));
|
|
26
|
+
await b.vault.init({ mode: "plaintext", dataDir: dir });
|
|
27
|
+
|
|
28
|
+
// ---- salted-sha3 is the default: SHA3-512 → 128 hex chars ----
|
|
29
|
+
b.cryptoField.registerTable("cf_dh_t1", {
|
|
30
|
+
sealedFields: ["email"],
|
|
31
|
+
derivedHashes: { emailHash: { from: "email" } },
|
|
32
|
+
});
|
|
33
|
+
var saltedH = b.cryptoField.lookupHash("cf_dh_t1", "email", "a@b.com").value;
|
|
34
|
+
check("salted-sha3 default is 128 hex (SHA3-512)", saltedH.length === 128);
|
|
35
|
+
check("salted-sha3 is deterministic",
|
|
36
|
+
saltedH === b.cryptoField.lookupHash("cf_dh_t1", "email", "a@b.com").value);
|
|
37
|
+
|
|
38
|
+
// ---- hmac-shake256 table mode: SHAKE256/32 → 64 hex chars ----
|
|
39
|
+
b.cryptoField.registerTable("cf_dh_t2", {
|
|
40
|
+
sealedFields: ["email"],
|
|
41
|
+
derivedHashes: { emailHash: { from: "email" } },
|
|
42
|
+
derivedHashMode: "hmac-shake256",
|
|
43
|
+
});
|
|
44
|
+
var keyedH = b.cryptoField.lookupHash("cf_dh_t2", "email", "a@b.com").value;
|
|
45
|
+
check("hmac-shake256 is 64 hex (SHAKE256/32)", keyedH.length === 64);
|
|
46
|
+
check("hmac-shake256 is deterministic",
|
|
47
|
+
keyedH === b.cryptoField.lookupHash("cf_dh_t2", "email", "a@b.com").value);
|
|
48
|
+
check("keyed hash differs from salted hash", keyedH !== saltedH);
|
|
49
|
+
|
|
50
|
+
// ---- per-column mode override on a salted-default table ----
|
|
51
|
+
b.cryptoField.registerTable("cf_dh_t3", {
|
|
52
|
+
sealedFields: ["ssn"],
|
|
53
|
+
derivedHashes: { ssnHash: { from: "ssn", mode: "hmac-shake256" } },
|
|
54
|
+
});
|
|
55
|
+
check("per-column hmac-shake256 override → 64 hex",
|
|
56
|
+
b.cryptoField.lookupHash("cf_dh_t3", "ssn", "x").value.length === 64);
|
|
57
|
+
|
|
58
|
+
// ---- MAC key surface: 32 bytes, per-deployment ----
|
|
59
|
+
var macKey = b.vault.getDerivedHashMacKey();
|
|
60
|
+
check("getDerivedHashMacKey is a 32-byte Buffer",
|
|
61
|
+
Buffer.isBuffer(macKey) && macKey.length === 32);
|
|
62
|
+
|
|
63
|
+
// A fresh deployment (new vault dir + MAC key) yields a different
|
|
64
|
+
// keyed hash for the same input — the keyed hash is bound to the MAC
|
|
65
|
+
// key, not just the input.
|
|
66
|
+
var dir2 = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-cf-dh2-"));
|
|
67
|
+
b.vault._resetForTest();
|
|
68
|
+
await b.vault.init({ mode: "plaintext", dataDir: dir2 });
|
|
69
|
+
b.cryptoField.registerTable("cf_dh_t2", {
|
|
70
|
+
sealedFields: ["email"],
|
|
71
|
+
derivedHashes: { emailHash: { from: "email" } },
|
|
72
|
+
derivedHashMode: "hmac-shake256",
|
|
73
|
+
});
|
|
74
|
+
var keyedH2 = b.cryptoField.lookupHash("cf_dh_t2", "email", "a@b.com").value;
|
|
75
|
+
check("new deployment MAC key → different keyed hash", keyedH2 !== keyedH);
|
|
76
|
+
|
|
77
|
+
// ---- input validation: unknown modes are rejected at registerTable ----
|
|
78
|
+
var badTableMode = false;
|
|
79
|
+
try { b.cryptoField.registerTable("cf_dh_bad", { derivedHashMode: "md5" }); }
|
|
80
|
+
catch (e) { badTableMode = /derivedHashMode must be/.test(e.message); }
|
|
81
|
+
check("bad derivedHashMode throws at registerTable", badTableMode);
|
|
82
|
+
|
|
83
|
+
var badColMode = false;
|
|
84
|
+
try {
|
|
85
|
+
b.cryptoField.registerTable("cf_dh_bad2", {
|
|
86
|
+
derivedHashes: { h: { from: "x", mode: "rot13" } },
|
|
87
|
+
});
|
|
88
|
+
} catch (e) { badColMode = /mode must be/.test(e.message); }
|
|
89
|
+
check("bad per-column mode throws at registerTable", badColMode);
|
|
90
|
+
|
|
91
|
+
console.log("OK — crypto-field derived-hash tests");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { run: run };
|
|
95
|
+
if (require.main === module) {
|
|
96
|
+
// Rethrow on failure so Node surfaces the error and exits non-zero,
|
|
97
|
+
// instead of logging the caught error object — a taint analyzer traces
|
|
98
|
+
// a logged error back to the test passphrase fixture (a non-secret
|
|
99
|
+
// constant) and raises a false clear-text-logging alert.
|
|
100
|
+
run().then(function () { process.exit(0); })
|
|
101
|
+
.catch(function (err) { process.exitCode = 1; throw err; });
|
|
102
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.db column-membership gate + whereRaw string-literal refusal.
|
|
4
|
+
*
|
|
5
|
+
* Column gate: a Query may only reference columns the table declared in
|
|
6
|
+
* its schema. The default mode is "reject" (throw), so a typo'd or
|
|
7
|
+
* attacker-influenced column name fails closed rather than silently
|
|
8
|
+
* matching nothing (or, for a raw fragment, opening an injection path).
|
|
9
|
+
* Modes: reject (default) | warn (audit, allow) | off (no gate).
|
|
10
|
+
* .allowedColumns([...]) narrows further and is ALWAYS enforced.
|
|
11
|
+
*
|
|
12
|
+
* whereRaw / WhereBuilder.raw refuse an embedded string literal
|
|
13
|
+
* ('...') unless { allowLiterals: true } — every value must bind
|
|
14
|
+
* through the params array.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
var helpers = require("../helpers");
|
|
18
|
+
var b = helpers.b;
|
|
19
|
+
var check = helpers.check;
|
|
20
|
+
var fs = helpers.fs;
|
|
21
|
+
var os = helpers.os;
|
|
22
|
+
var path = helpers.path;
|
|
23
|
+
|
|
24
|
+
var SCHEMA = [{
|
|
25
|
+
name: "things",
|
|
26
|
+
columns: {
|
|
27
|
+
_id: "TEXT PRIMARY KEY",
|
|
28
|
+
name: "TEXT",
|
|
29
|
+
status: "TEXT DEFAULT 'active'",
|
|
30
|
+
},
|
|
31
|
+
indexes: ["status"],
|
|
32
|
+
}];
|
|
33
|
+
|
|
34
|
+
async function initDb(tmpDir, columnGate) {
|
|
35
|
+
process.env.BLAMEJS_SKIP_NTP_CHECK = "1";
|
|
36
|
+
helpers.setTestPassphraseEnv();
|
|
37
|
+
b.cluster._resetForTest();
|
|
38
|
+
b.audit._resetForTest();
|
|
39
|
+
b.vault._resetForTest();
|
|
40
|
+
b.db._resetForTest();
|
|
41
|
+
await b.vault.init({ dataDir: tmpDir });
|
|
42
|
+
var opts = { dataDir: tmpDir, tmpDir: path.join(tmpDir, "tmpfs"), schema: SCHEMA };
|
|
43
|
+
if (columnGate !== undefined) opts.columnGate = columnGate;
|
|
44
|
+
await b.db.init(opts);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function threwMatching(fn, pattern) {
|
|
48
|
+
try { fn(); } catch (e) { return pattern.test(e.message) ? e : null; }
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function run() {
|
|
53
|
+
var tmp = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-colgate-"));
|
|
54
|
+
|
|
55
|
+
// ---- default mode is reject ----
|
|
56
|
+
await initDb(tmp);
|
|
57
|
+
b.db.from("things").insertOne({ _id: "t1", name: "a", status: "active" });
|
|
58
|
+
|
|
59
|
+
// declared columns pass
|
|
60
|
+
check("declared column where() passes",
|
|
61
|
+
b.db.from("things").where({ name: "a" }).first().status === "active");
|
|
62
|
+
check("declared column select() passes",
|
|
63
|
+
b.db.from("things").select(["name", "status"]).where({ _id: "t1" }).first().name === "a");
|
|
64
|
+
|
|
65
|
+
// undeclared column is rejected (fail-closed)
|
|
66
|
+
check("undeclared where() column rejected",
|
|
67
|
+
!!threwMatching(function () { b.db.from("things").where({ nope: 1 }); },
|
|
68
|
+
/not a declared column of 'things'/));
|
|
69
|
+
check("undeclared orderBy() column rejected",
|
|
70
|
+
!!threwMatching(function () { b.db.from("things").orderBy("bogus"); },
|
|
71
|
+
/not a declared column/));
|
|
72
|
+
check("undeclared select() column rejected",
|
|
73
|
+
!!threwMatching(function () { b.db.from("things").select(["ghost"]); },
|
|
74
|
+
/not a declared column/));
|
|
75
|
+
|
|
76
|
+
// ---- getDeclaredColumns ----
|
|
77
|
+
check("b.db.getDeclaredColumns is fn", typeof b.db.getDeclaredColumns === "function");
|
|
78
|
+
var declared = b.db.getDeclaredColumns("things");
|
|
79
|
+
check("getDeclaredColumns lists schema columns",
|
|
80
|
+
declared.indexOf("_id") !== -1 && declared.indexOf("name") !== -1 && declared.indexOf("status") !== -1);
|
|
81
|
+
check("getDeclaredColumns on unknown table → null",
|
|
82
|
+
b.db.getDeclaredColumns("no_such_table") === null);
|
|
83
|
+
|
|
84
|
+
// ---- allowedColumns() narrows + is always enforced ----
|
|
85
|
+
check("allowedColumns() allows a member",
|
|
86
|
+
b.db.from("things").allowedColumns(["name"]).where({ name: "a" }).first().name === "a");
|
|
87
|
+
check("allowedColumns() rejects a declared-but-not-allowed column",
|
|
88
|
+
!!threwMatching(function () {
|
|
89
|
+
b.db.from("things").allowedColumns(["name"]).where({ status: "active" });
|
|
90
|
+
}, /not in the allowedColumns\(\) set/));
|
|
91
|
+
check("allowedColumns([]) rejected (non-empty array required)",
|
|
92
|
+
!!threwMatching(function () { b.db.from("things").allowedColumns([]); },
|
|
93
|
+
/non-empty array/));
|
|
94
|
+
|
|
95
|
+
// ---- A5: whereRaw refuses embedded string literals ----
|
|
96
|
+
var litErr = threwMatching(function () { b.db.from("things").whereRaw("status = 'active'"); },
|
|
97
|
+
/string literal/);
|
|
98
|
+
check("whereRaw refuses embedded string literal", !!litErr);
|
|
99
|
+
check("whereRaw literal error code is sql/raw-literal", litErr && litErr.code === "sql/raw-literal");
|
|
100
|
+
check("whereRaw with bound params is accepted",
|
|
101
|
+
b.db.from("things").whereRaw("status = ?", ["active"]).first().name === "a");
|
|
102
|
+
check("whereRaw allowLiterals opt-in is accepted",
|
|
103
|
+
b.db.from("things").whereRaw("status = 'active'", [], { allowLiterals: true }).first().name === "a");
|
|
104
|
+
|
|
105
|
+
b.db.close();
|
|
106
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
107
|
+
|
|
108
|
+
// ---- warn mode: audited, not thrown ----
|
|
109
|
+
var tmp2 = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-colgate-warn-"));
|
|
110
|
+
await initDb(tmp2, "warn");
|
|
111
|
+
var warnThrew = false;
|
|
112
|
+
try { b.db.from("things").where({ nope: 1 }); } catch (_e) { warnThrew = true; }
|
|
113
|
+
check("warn mode does not throw on undeclared column", warnThrew === false);
|
|
114
|
+
b.db.close();
|
|
115
|
+
fs.rmSync(tmp2, { recursive: true, force: true });
|
|
116
|
+
|
|
117
|
+
// ---- off mode: no gate ----
|
|
118
|
+
var tmp3 = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-colgate-off-"));
|
|
119
|
+
await initDb(tmp3, "off");
|
|
120
|
+
var offThrew = false;
|
|
121
|
+
try { b.db.from("things").where({ nope: 1 }); } catch (_e) { offThrew = true; }
|
|
122
|
+
check("off mode does not gate undeclared column", offThrew === false);
|
|
123
|
+
b.db.close();
|
|
124
|
+
fs.rmSync(tmp3, { recursive: true, force: true });
|
|
125
|
+
|
|
126
|
+
// ---- bad columnGate value rejected at db.init ----
|
|
127
|
+
var tmp4 = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-colgate-bad-"));
|
|
128
|
+
var initThrew = false;
|
|
129
|
+
try { await initDb(tmp4, "loud"); } catch (e) { initThrew = /columnGate/.test(e.message); }
|
|
130
|
+
check("db.init rejects unknown columnGate mode", initThrew);
|
|
131
|
+
try { b.db.close(); } catch (_e) {}
|
|
132
|
+
fs.rmSync(tmp4, { recursive: true, force: true });
|
|
133
|
+
|
|
134
|
+
b.audit._resetForTest();
|
|
135
|
+
b.db._resetForTest();
|
|
136
|
+
b.vault._resetForTest();
|
|
137
|
+
b.cluster._resetForTest();
|
|
138
|
+
|
|
139
|
+
console.log("OK — db column-gate tests");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = { run: run };
|
|
143
|
+
if (require.main === module) {
|
|
144
|
+
// Rethrow on failure so Node surfaces the error and exits non-zero,
|
|
145
|
+
// instead of logging the caught error object — a taint analyzer traces
|
|
146
|
+
// a logged error back to the test passphrase fixture (a non-secret
|
|
147
|
+
// constant) and raises a false clear-text-logging alert.
|
|
148
|
+
run().then(function () { process.exit(0); })
|
|
149
|
+
.catch(function (err) { process.exitCode = 1; throw err; });
|
|
150
|
+
}
|