@blamejs/core 0.15.13 → 0.15.15
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 +4 -0
- package/index.js +2 -0
- package/lib/a2a-tasks.js +38 -6
- package/lib/agent-event-bus.js +13 -0
- package/lib/agent-idempotency.js +5 -1
- package/lib/agent-snapshot.js +32 -2
- package/lib/ai-aedt-bias-audit.js +2 -1
- package/lib/ai-content-detect.js +1 -3
- package/lib/ai-frontier-protocol.js +1 -1
- package/lib/ai-model-manifest.js +1 -1
- package/lib/ai-output.js +16 -7
- package/lib/api-snapshot.js +4 -1
- package/lib/app-shutdown.js +7 -1
- package/lib/archive-gz.js +9 -0
- package/lib/archive-read.js +9 -7
- package/lib/archive-tar-read.js +51 -8
- package/lib/archive.js +4 -2
- package/lib/asn1-der.js +70 -22
- package/lib/atomic-file.js +204 -2
- package/lib/audit-chain.js +54 -5
- package/lib/audit-daily-review.js +12 -2
- package/lib/audit-sign.js +2 -1
- package/lib/audit-tools.js +108 -23
- package/lib/audit.js +7 -2
- package/lib/auth/access-lock.js +2 -2
- package/lib/auth/bot-challenge.js +1 -1
- package/lib/auth/ciba.js +71 -8
- package/lib/auth/dpop.js +15 -1
- package/lib/auth/fido-mds3.js +30 -13
- package/lib/auth/jwt.js +21 -5
- package/lib/auth/lockout.js +18 -2
- package/lib/auth/oauth.js +8 -2
- package/lib/auth/passkey.js +1 -1
- package/lib/auth/password.js +1 -1
- package/lib/auth/saml.js +42 -12
- package/lib/auth/sd-jwt-vc.js +24 -4
- package/lib/auth/status-list.js +14 -2
- package/lib/auth/step-up.js +9 -1
- package/lib/auth-bot-challenge.js +21 -2
- package/lib/backup/bundle.js +7 -2
- package/lib/backup/crypto.js +23 -8
- package/lib/backup/index.js +41 -20
- package/lib/backup/manifest.js +7 -1
- package/lib/break-glass.js +41 -22
- package/lib/cbor.js +34 -11
- package/lib/cdn-cache-control.js +7 -3
- package/lib/cert.js +5 -3
- package/lib/cli.js +5 -1
- package/lib/cloud-events.js +3 -2
- package/lib/cluster-storage.js +7 -3
- package/lib/codepoint-class.js +17 -0
- package/lib/compliance-eaa.js +1 -1
- package/lib/compliance-sanctions.js +9 -7
- package/lib/compliance.js +1 -1
- package/lib/config-drift.js +22 -8
- package/lib/content-credentials.js +11 -8
- package/lib/content-digest.js +11 -4
- package/lib/cookies.js +10 -2
- package/lib/cose.js +20 -0
- package/lib/crdt.js +2 -1
- package/lib/crypto-field.js +48 -24
- package/lib/crypto.js +18 -4
- package/lib/csp.js +13 -0
- package/lib/daemon.js +4 -1
- package/lib/data-act.js +27 -4
- package/lib/db-file-lifecycle.js +14 -6
- package/lib/db-query.js +102 -11
- package/lib/db.js +32 -15
- package/lib/dora.js +43 -13
- package/lib/dr-runbook.js +1 -1
- package/lib/dsa.js +2 -1
- package/lib/dsr.js +22 -8
- package/lib/early-hints.js +19 -0
- package/lib/eat.js +5 -1
- package/lib/external-db.js +60 -4
- package/lib/fda-21cfr11.js +30 -5
- package/lib/flag-providers.js +6 -2
- package/lib/forms.js +1 -1
- package/lib/gate-contract.js +46 -5
- package/lib/gdpr-ropa.js +18 -9
- package/lib/graphql-federation.js +17 -4
- package/lib/guard-all.js +2 -2
- package/lib/guard-dsn.js +1 -1
- package/lib/guard-envelope.js +1 -1
- package/lib/guard-html.js +9 -11
- package/lib/guard-imap-command.js +1 -1
- package/lib/guard-jmap.js +1 -1
- package/lib/guard-json.js +14 -6
- package/lib/guard-mail-move.js +1 -1
- package/lib/guard-managesieve-command.js +1 -1
- package/lib/guard-pop3-command.js +1 -1
- package/lib/guard-smtp-command.js +1 -1
- package/lib/guard-svg.js +8 -9
- package/lib/html-balance.js +7 -3
- package/lib/http-client-cookie-jar.js +33 -12
- package/lib/http-client.js +225 -53
- package/lib/iab-tcf.js +3 -2
- package/lib/importmap-integrity.js +41 -1
- package/lib/incident-report.js +9 -6
- package/lib/json-patch.js +1 -1
- package/lib/json-path.js +24 -3
- package/lib/jtd.js +2 -2
- package/lib/legal-hold.js +24 -8
- package/lib/log.js +2 -2
- package/lib/mail-agent.js +2 -2
- package/lib/mail-arf.js +1 -1
- package/lib/mail-auth.js +27 -4
- package/lib/mail-bimi.js +16 -16
- package/lib/mail-bounce.js +3 -3
- package/lib/mail-crypto-smime.js +71 -6
- package/lib/mail-deploy.js +9 -5
- package/lib/mail-dkim.js +20 -7
- package/lib/mail-greylist.js +2 -4
- package/lib/mail-helo.js +2 -4
- package/lib/mail-journal.js +11 -8
- package/lib/mail-mdn.js +8 -4
- package/lib/mail-rbl.js +2 -4
- package/lib/mail-scan.js +3 -5
- package/lib/mail-server-jmap.js +4 -1
- package/lib/mail-server-registry.js +1 -1
- package/lib/mail-server-tls.js +9 -2
- package/lib/mail-spam-score.js +2 -4
- package/lib/mail-store-fts.js +1 -1
- package/lib/mail.js +22 -2
- package/lib/markup-tokenizer.js +24 -0
- package/lib/mcp.js +6 -4
- package/lib/mdoc.js +26 -3
- package/lib/metrics.js +14 -3
- package/lib/middleware/api-encrypt.js +2 -2
- package/lib/middleware/body-parser.js +10 -4
- package/lib/middleware/bot-guard.js +26 -18
- package/lib/middleware/clear-site-data.js +5 -1
- package/lib/middleware/compose-pipeline.js +39 -5
- package/lib/middleware/compression.js +9 -0
- package/lib/middleware/cors.js +32 -23
- package/lib/middleware/csrf-protect.js +60 -21
- package/lib/middleware/daily-byte-quota.js +6 -4
- package/lib/middleware/fetch-metadata.js +28 -4
- package/lib/middleware/network-allowlist.js +61 -30
- package/lib/middleware/rate-limit.js +25 -16
- package/lib/middleware/scim-server.js +2 -1
- package/lib/middleware/security-headers.js +24 -6
- package/lib/middleware/speculation-rules.js +6 -3
- package/lib/middleware/tus-upload.js +2 -2
- package/lib/money.js +1 -1
- package/lib/mtls-ca.js +10 -6
- package/lib/network-dns-resolver.js +1 -1
- package/lib/network-dns.js +9 -2
- package/lib/network-dnssec.js +2 -1
- package/lib/network-smtp-policy.js +23 -5
- package/lib/network-tls.js +27 -5
- package/lib/network-tsig.js +2 -2
- package/lib/nis2-report.js +1 -1
- package/lib/nist-crosswalk.js +1 -1
- package/lib/ntp-check.js +28 -0
- package/lib/numeric-bounds.js +9 -0
- package/lib/object-store/azure-blob.js +1 -2
- package/lib/object-store/gcs-bucket-ops.js +4 -2
- package/lib/object-store/gcs.js +6 -4
- package/lib/object-store/http-put.js +1 -2
- package/lib/object-store/http-request.js +30 -1
- package/lib/object-store/local.js +37 -17
- package/lib/object-store/sigv4.js +1 -2
- package/lib/observability-otlp-exporter.js +20 -4
- package/lib/outbox.js +11 -4
- package/lib/parsers/safe-xml.js +1 -1
- package/lib/parsers/safe-yaml.js +21 -3
- package/lib/pipl-cn.js +11 -8
- package/lib/queue-local.js +10 -3
- package/lib/redact.js +7 -3
- package/lib/request-helpers.js +347 -36
- package/lib/resource-access-lock.js +3 -3
- package/lib/restore-bundle.js +46 -18
- package/lib/restore-rollback.js +10 -4
- package/lib/restore.js +19 -0
- package/lib/retention.js +20 -4
- package/lib/router.js +17 -4
- package/lib/safe-ical.js +2 -2
- package/lib/safe-icap.js +1 -1
- package/lib/safe-json.js +70 -0
- package/lib/safe-sieve.js +1 -1
- package/lib/safe-vcard.js +1 -1
- package/lib/sandbox-worker.js +6 -0
- package/lib/sandbox.js +1 -1
- package/lib/scheduler.js +17 -1
- package/lib/self-update-standalone-verifier.js +16 -0
- package/lib/session.js +62 -120
- package/lib/sql.js +25 -3
- package/lib/static.js +65 -13
- package/lib/template.js +7 -5
- package/lib/tenant-quota.js +52 -19
- package/lib/tsa.js +5 -2
- package/lib/vault/index.js +5 -0
- package/lib/vault/passphrase-ops.js +22 -26
- package/lib/vault/passphrase-source.js +8 -3
- package/lib/vault/rotate.js +13 -18
- package/lib/vault/seal-pem-file.js +4 -1
- package/lib/vc.js +1 -1
- package/lib/vendor/MANIFEST.json +10 -10
- package/lib/vendor/public-suffix-list.dat +23 -10
- package/lib/vendor/public-suffix-list.data.js +5498 -5494
- package/lib/webhook.js +16 -1
- package/lib/websocket.js +1 -1
- package/lib/worm.js +1 -1
- package/lib/ws-client.js +83 -46
- package/lib/x509-chain.js +91 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/dora.js
CHANGED
|
@@ -62,12 +62,19 @@ var SIGNIFICANT_INCIDENT_THRESHOLDS = Object.freeze({
|
|
|
62
62
|
durationCriticalProcessMs: C.TIME.hours(2), // 2h
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
// DORA major-incident reporting timeline (Art. 19 + RTS (EU) 2024/1772 Art. 5):
|
|
66
|
+
// - Initial notification: as early as possible and WITHIN 4 HOURS of
|
|
67
|
+
// classifying the incident as major (the outer bound is 24h from first
|
|
68
|
+
// awareness). classify() returns mustReportInitialByMs as a duration FROM
|
|
69
|
+
// classification, so the operative window encoded here is 4h — the
|
|
70
|
+
// 24h-from-awareness outer bound is enforced separately via
|
|
71
|
+
// INITIAL_REPORT_OUTER_DEADLINE_MS below.
|
|
72
|
+
// - Intermediate update: within 72h of the INITIAL notification's submission.
|
|
73
|
+
// - Final report: within 1 month of the intermediate report's submission.
|
|
74
|
+
var INITIAL_REPORT_DEADLINE_MS = C.TIME.hours(4);
|
|
75
|
+
var INITIAL_REPORT_OUTER_DEADLINE_MS = C.TIME.hours(24);
|
|
76
|
+
var INTERMEDIATE_REPORT_DEADLINE_MS = C.TIME.hours(72);
|
|
77
|
+
var FINAL_REPORT_DEADLINE_MS = C.TIME.days(30);
|
|
71
78
|
|
|
72
79
|
// Adjacent-regulation incident-reporting deadlines — operators wiring
|
|
73
80
|
// NIS2 / CRA / HIPAA breach notification reach for these constants
|
|
@@ -124,14 +131,26 @@ function _classifyImpl(input) {
|
|
|
124
131
|
reasons.push("severity-high");
|
|
125
132
|
}
|
|
126
133
|
|
|
127
|
-
// 2. Affected clients
|
|
134
|
+
// 2. Affected clients — absolute count OR share of the client base. The
|
|
135
|
+
// percentile thresholds (RTS 2024/1772 Art. 1(1)(a): >10% major, ESA-
|
|
136
|
+
// guideline 1% significant) were declared but never evaluated, so a
|
|
137
|
+
// large-share-but-small-absolute incident (e.g. 16% of a 50k client base)
|
|
138
|
+
// under-classified. Operators pass clientBase (total clients) to enable
|
|
139
|
+
// the percentage criterion; absolute-only behavior is unchanged when it
|
|
140
|
+
// is omitted.
|
|
128
141
|
if (typeof input.affectedClients === "number" && input.affectedClients > 0) {
|
|
129
|
-
|
|
142
|
+
var clientBase = (typeof input.clientBase === "number" && input.clientBase > 0) ? input.clientBase : null;
|
|
143
|
+
var clientPct = clientBase ? (input.affectedClients / clientBase) : 0;
|
|
144
|
+
var majorByClients = input.affectedClients >= MAJOR_INCIDENT_THRESHOLDS.affectedClientsAbsolute ||
|
|
145
|
+
(clientBase !== null && clientPct >= MAJOR_INCIDENT_THRESHOLDS.affectedClientsPercentile);
|
|
146
|
+
var significantByClients = input.affectedClients >= SIGNIFICANT_INCIDENT_THRESHOLDS.affectedClientsAbsolute ||
|
|
147
|
+
(clientBase !== null && clientPct >= SIGNIFICANT_INCIDENT_THRESHOLDS.affectedClientsPercentile);
|
|
148
|
+
if (majorByClients) {
|
|
130
149
|
hitsMajor += 1;
|
|
131
|
-
reasons.push("clients-major
|
|
132
|
-
} else if (
|
|
150
|
+
reasons.push("clients-major");
|
|
151
|
+
} else if (significantByClients) {
|
|
133
152
|
hitsSignificant += 1;
|
|
134
|
-
reasons.push("clients-significant
|
|
153
|
+
reasons.push("clients-significant");
|
|
135
154
|
}
|
|
136
155
|
}
|
|
137
156
|
|
|
@@ -188,7 +207,11 @@ function _classifyImpl(input) {
|
|
|
188
207
|
return {
|
|
189
208
|
classification: classification,
|
|
190
209
|
mustReport: mustReport,
|
|
210
|
+
// Operative initial-notification window: 4h from this classification.
|
|
191
211
|
mustReportInitialByMs: mustReport ? INITIAL_REPORT_DEADLINE_MS : null,
|
|
212
|
+
// Outer bound: no later than 24h from first awareness — surfaced so an
|
|
213
|
+
// operator scheduling from `detectedAt` can enforce min(class+4h, aware+24h).
|
|
214
|
+
mustReportInitialOuterByMs: mustReport ? INITIAL_REPORT_OUTER_DEADLINE_MS : null,
|
|
192
215
|
reasons: reasons,
|
|
193
216
|
};
|
|
194
217
|
}
|
|
@@ -343,10 +366,16 @@ function create(opts) {
|
|
|
343
366
|
// Article 19 deadline — operator-side scheduler uses this.
|
|
344
367
|
nextStageDueAt: null,
|
|
345
368
|
};
|
|
369
|
+
// Anchor each downstream deadline on the SUBMISSION time of the report
|
|
370
|
+
// being filed (record.reportedAt), not on detectedAt: the intermediate is
|
|
371
|
+
// due 72h after the initial is *submitted*, and the final one month after
|
|
372
|
+
// the intermediate is *submitted* (DORA RTS 2024/1772 reporting timeline).
|
|
373
|
+
// Anchoring on detectedAt understates the deadline whenever a report is
|
|
374
|
+
// filed later than detection.
|
|
346
375
|
if (input.stage === "initial") {
|
|
347
|
-
record.nextStageDueAt =
|
|
376
|
+
record.nextStageDueAt = record.reportedAt + INTERMEDIATE_REPORT_DEADLINE_MS;
|
|
348
377
|
} else if (input.stage === "intermediate") {
|
|
349
|
-
record.nextStageDueAt =
|
|
378
|
+
record.nextStageDueAt = record.reportedAt + FINAL_REPORT_DEADLINE_MS;
|
|
350
379
|
}
|
|
351
380
|
_emit("dora.incident.reported", {
|
|
352
381
|
metadata: {
|
|
@@ -396,6 +425,7 @@ module.exports = {
|
|
|
396
425
|
MAJOR_INCIDENT_THRESHOLDS: MAJOR_INCIDENT_THRESHOLDS,
|
|
397
426
|
SIGNIFICANT_INCIDENT_THRESHOLDS: SIGNIFICANT_INCIDENT_THRESHOLDS,
|
|
398
427
|
INITIAL_REPORT_DEADLINE_MS: INITIAL_REPORT_DEADLINE_MS,
|
|
428
|
+
INITIAL_REPORT_OUTER_DEADLINE_MS: INITIAL_REPORT_OUTER_DEADLINE_MS,
|
|
399
429
|
INTERMEDIATE_REPORT_DEADLINE_MS: INTERMEDIATE_REPORT_DEADLINE_MS,
|
|
400
430
|
FINAL_REPORT_DEADLINE_MS: FINAL_REPORT_DEADLINE_MS,
|
|
401
431
|
DEADLINES_NIS2: DEADLINES_NIS2,
|
package/lib/dr-runbook.js
CHANGED
|
@@ -275,7 +275,7 @@ async function emit(opts) {
|
|
|
275
275
|
"drRunbook.emit: outDir", DrRunbookError, "dr-runbook/no-outdir");
|
|
276
276
|
validateOpts.requireNonEmptyString(opts.posture,
|
|
277
277
|
"drRunbook.emit: posture", DrRunbookError, "dr-runbook/no-posture");
|
|
278
|
-
if (!POSTURE_BLOCKS
|
|
278
|
+
if (!Object.prototype.hasOwnProperty.call(POSTURE_BLOCKS, opts.posture)) {
|
|
279
279
|
throw new DrRunbookError("dr-runbook/unknown-posture",
|
|
280
280
|
"drRunbook.emit: posture '" + opts.posture + "' not in supported list (" +
|
|
281
281
|
Object.keys(POSTURE_BLOCKS).join(", ") + ")");
|
package/lib/dsa.js
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
var validateOpts = require("./validate-opts");
|
|
38
38
|
var lazyRequire = require("./lazy-require");
|
|
39
|
+
var numericBounds = require("./numeric-bounds");
|
|
39
40
|
var C = require("./constants");
|
|
40
41
|
var { DsaError } = require("./framework-error");
|
|
41
42
|
|
|
@@ -414,7 +415,7 @@ function transparencyReport(opts) {
|
|
|
414
415
|
METRIC_FIELDS.forEach(function (field) {
|
|
415
416
|
var v = supplied[field];
|
|
416
417
|
if (v === undefined || v === null) { metrics[field] = 0; return; }
|
|
417
|
-
if (
|
|
418
|
+
if (!numericBounds.isNonNegativeFiniteInt(v)) {
|
|
418
419
|
throw new DsaError("dsa/bad-metric-value",
|
|
419
420
|
"b.dsa.transparencyReport: metrics." + field +
|
|
420
421
|
" must be a non-negative integer, got " +
|
package/lib/dsr.js
CHANGED
|
@@ -210,7 +210,7 @@ var POSTURE_DEADLINE_MS = Object.freeze({
|
|
|
210
210
|
// + extension). Read-only at module scope.
|
|
211
211
|
function _deadlineForPosture(posture) {
|
|
212
212
|
if (typeof posture !== "string") return POSTURE_DEADLINE_MS["default"];
|
|
213
|
-
return POSTURE_DEADLINE_MS[posture]
|
|
213
|
+
return Object.prototype.hasOwnProperty.call(POSTURE_DEADLINE_MS, posture) ? POSTURE_DEADLINE_MS[posture] : POSTURE_DEADLINE_MS["default"];
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
function _now() { return Date.now(); }
|
|
@@ -1198,12 +1198,26 @@ function dbTicketStore(opts) {
|
|
|
1198
1198
|
SUBJECT_FILTER_SPEC.forEach(function (spec) {
|
|
1199
1199
|
var supplied = filter.subject[spec.key];
|
|
1200
1200
|
if (!supplied) return;
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1201
|
+
if (!vaulted) {
|
|
1202
|
+
conds.push(spec.plainCol + " = " + spec.param);
|
|
1203
|
+
params[spec.param] = supplied;
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
// Vaulted: match BOTH the active keyed-MAC digest AND the legacy
|
|
1207
|
+
// salted-sha3 digest via the dual-read, so a ticket written before the
|
|
1208
|
+
// keyed-MAC default flip is still FOUND. A single-value equality on the
|
|
1209
|
+
// keyed-MAC digest silently skips those pre-flip rows, so an Art.17
|
|
1210
|
+
// erasure purge (list-by-subject → delete) would leave the subject's
|
|
1211
|
+
// legacy-hashed PII-bearing tickets behind. Mirrors subject.eraseHard.
|
|
1212
|
+
var cand = cryptoField().lookupHashCandidates(DSR_SEAL_TABLE, spec.sealField, supplied);
|
|
1213
|
+
var values = cand && cand.values ? cand.values : [];
|
|
1214
|
+
if (values.length === 0) return;
|
|
1215
|
+
var placeholders = values.map(function (v, i) {
|
|
1216
|
+
var p = spec.param + "_" + i;
|
|
1217
|
+
params[p] = v;
|
|
1218
|
+
return p;
|
|
1219
|
+
});
|
|
1220
|
+
conds.push(spec.hashCol + " IN (" + placeholders.join(", ") + ")");
|
|
1207
1221
|
});
|
|
1208
1222
|
}
|
|
1209
1223
|
|
|
@@ -1455,7 +1469,7 @@ var STATE_RULES = Object.freeze({
|
|
|
1455
1469
|
function stateRules(state) {
|
|
1456
1470
|
if (typeof state !== "string" || state.length === 0) return null;
|
|
1457
1471
|
// Direct posture-name lookup first
|
|
1458
|
-
if (STATE_RULES
|
|
1472
|
+
if (Object.prototype.hasOwnProperty.call(STATE_RULES, state)) return Object.assign({}, STATE_RULES[state]);
|
|
1459
1473
|
// 2-letter state abbreviation lookup (case-insensitive)
|
|
1460
1474
|
var u = state.toUpperCase();
|
|
1461
1475
|
var keys = Object.keys(STATE_RULES);
|
package/lib/early-hints.js
CHANGED
|
@@ -170,6 +170,13 @@ function send(res, opts) {
|
|
|
170
170
|
throw new EarlyHintsError("early-hints/bad-header-value",
|
|
171
171
|
"earlyHints.send: header '" + name + "' must be a string or string[]", true);
|
|
172
172
|
}
|
|
173
|
+
var vals = Array.isArray(canonical[name]) ? canonical[name] : [canonical[name]];
|
|
174
|
+
for (var vi = 0; vi < vals.length; vi += 1) {
|
|
175
|
+
if (typeof vals[vi] === "string" && _hasHeaderInjection(vals[vi])) {
|
|
176
|
+
throw new EarlyHintsError("early-hints/bad-header-value",
|
|
177
|
+
"earlyHints.send: header '" + name + "' value contains a CR/LF/NUL header-injection character", true);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
173
180
|
headers[name] = canonical[name];
|
|
174
181
|
}
|
|
175
182
|
|
|
@@ -182,9 +189,21 @@ function send(res, opts) {
|
|
|
182
189
|
}
|
|
183
190
|
}
|
|
184
191
|
|
|
192
|
+
// A header value reaching res.writeEarlyHints must carry no CR/LF/NUL — those
|
|
193
|
+
// split the interim response into attacker-chosen headers. Node rejects them
|
|
194
|
+
// too, but screen defensively (matching the cookies / security-headers
|
|
195
|
+
// boundary) so the framework surfaces a typed error, not a raw Node throw.
|
|
196
|
+
function _hasHeaderInjection(s) {
|
|
197
|
+
return s.indexOf("\r") !== -1 || s.indexOf("\n") !== -1 || s.indexOf("\0") !== -1;
|
|
198
|
+
}
|
|
199
|
+
|
|
185
200
|
function _validateLink(linkValue, idx) {
|
|
186
201
|
validateOpts.requireNonEmptyString(linkValue, "earlyHints.send.link[" + idx + "]",
|
|
187
202
|
EarlyHintsError, "early-hints/bad-link");
|
|
203
|
+
if (_hasHeaderInjection(linkValue)) {
|
|
204
|
+
throw new EarlyHintsError("early-hints/bad-link",
|
|
205
|
+
"link[" + idx + "] contains a CR/LF/NUL header-injection character");
|
|
206
|
+
}
|
|
188
207
|
if (linkValue.length > LINK_MAX_BYTES) {
|
|
189
208
|
throw new EarlyHintsError("early-hints/bad-link",
|
|
190
209
|
"link[" + idx + "] exceeds " + LINK_MAX_BYTES + " bytes");
|
package/lib/eat.js
CHANGED
|
@@ -205,7 +205,11 @@ async function verify(eat, opts) {
|
|
|
205
205
|
// Debug status — refuse a token that can't prove debug is disabled.
|
|
206
206
|
if (opts.requireDebugDisabled === true) {
|
|
207
207
|
var ds = raw.get(EAT.dbgstat);
|
|
208
|
-
|
|
208
|
+
// Accept ONLY a defined disabled enum value (1..4). `ds < disabled` alone
|
|
209
|
+
// let any out-of-range value >= 1 (5, 99, non-integers) pass as "disabled"
|
|
210
|
+
// — a fail-open on an undefined dbgstat (RFC 9711 §3.3 defines 0..4 only).
|
|
211
|
+
if (typeof ds !== "number" || !Number.isInteger(ds) ||
|
|
212
|
+
ds < DBGSTAT.disabled || ds > DBGSTAT["disabled-fully-and-permanently"]) {
|
|
209
213
|
throw new EatError("eat/debug-not-disabled",
|
|
210
214
|
"eat.verify: requireDebugDisabled — dbgstat is " +
|
|
211
215
|
(ds === undefined ? "absent" : (DBGSTAT_BY_VALUE[ds] || ds)) + ", not a disabled state");
|
package/lib/external-db.js
CHANGED
|
@@ -339,6 +339,50 @@ function _classifyStatement(sql) {
|
|
|
339
339
|
return _STATEMENT_CLASS_MAP[kw] || "OTHER";
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
// True when a top-level (paren-depth 0, outside any string / comment / quoted-
|
|
343
|
+
// identifier / dollar-quoted span) RETURNING keyword is present — a DML
|
|
344
|
+
// statement with RETURNING yields rows to the caller. Quote-aware so the literal
|
|
345
|
+
// word inside a value ('see RETURNING docs') is not mistaken for a clause, and
|
|
346
|
+
// depth-aware so a RETURNING buried in a CTE / subquery paren does not count for
|
|
347
|
+
// the OUTER statement. Reuses the shared opaque-span scanner; single linear pass.
|
|
348
|
+
function _hasTopLevelReturning(sql) {
|
|
349
|
+
var n = sql.length, i = 0, depth = 0;
|
|
350
|
+
while (i < n) {
|
|
351
|
+
var skipped = _skipOpaqueSpan(sql, i);
|
|
352
|
+
if (skipped === -1) return false; // unterminated span — nothing top-level beyond
|
|
353
|
+
if (skipped !== i) { i = skipped; continue; }
|
|
354
|
+
var ch = sql.charAt(i);
|
|
355
|
+
if (ch === "(") { depth += 1; i += 1; continue; }
|
|
356
|
+
if (ch === ")") { depth -= 1; i += 1; continue; }
|
|
357
|
+
if (_isIdentStart(ch)) {
|
|
358
|
+
var we = i + 1;
|
|
359
|
+
while (we < n && _isIdentChar(sql.charAt(we))) we += 1;
|
|
360
|
+
if (depth === 0 && sql.slice(i, we).toUpperCase() === "RETURNING") return true;
|
|
361
|
+
i = we;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
i += 1;
|
|
365
|
+
}
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Does this statement return a row set to the caller? Distinct from the
|
|
370
|
+
// residency gate's read/write split: an `EXPLAIN ANALYZE INSERT` is a WRITE for
|
|
371
|
+
// the gate yet returns plan rows; a DML `... RETURNING` is a write that yields
|
|
372
|
+
// rows. Consumers (e.g. cluster-storage's local exec choosing .all() vs .run())
|
|
373
|
+
// must use .all() for any row-returning statement, including a WITH (CTE) read
|
|
374
|
+
// that the leading keyword alone would mis-route to .run() and SILENTLY DROP.
|
|
375
|
+
// Statement classes that produce a row set for the caller (SELECT and the
|
|
376
|
+
// READ_INFO family — SHOW / DESCRIBE / PRAGMA). Keyed by class name, mirroring
|
|
377
|
+
// _STATEMENT_CLASS_MAP / _RESIDENCY_READ_CLASS.
|
|
378
|
+
var _ROW_RETURNING_CLASS = Object.freeze({ SELECT: true, READ_INFO: true });
|
|
379
|
+
|
|
380
|
+
function statementReturnsRows(sql) {
|
|
381
|
+
if (typeof sql !== "string" || sql.length === 0) return false;
|
|
382
|
+
if (_ROW_RETURNING_CLASS[_classifyStatement(sql)] === true) return true;
|
|
383
|
+
return _hasTopLevelReturning(sql);
|
|
384
|
+
}
|
|
385
|
+
|
|
342
386
|
// Statement classes that place no rows on the backend, so the cross-
|
|
343
387
|
// border residency write gate lets them pass without a row tag. Every
|
|
344
388
|
// other class — DML, ROUTINE (CALL / EXECUTE / DO), a COPY ... FROM
|
|
@@ -1810,12 +1854,19 @@ async function _readQuery(sql, params, opts) {
|
|
|
1810
1854
|
// was explicitly configured allowCrossBorder (which is audited).
|
|
1811
1855
|
var _readPosture = _activePosture();
|
|
1812
1856
|
var _tagPresent = opts.rowResidencyTag && typeof opts.rowResidencyTag === "string";
|
|
1857
|
+
// A replica only carries a real residency CONSTRAINT when its tag names a
|
|
1858
|
+
// specific region. "unrestricted" / "global" mean "no constraint" (the tag
|
|
1859
|
+
// defaults to "unrestricted" when none is configured), so such a replica may
|
|
1860
|
+
// serve any region's rows and must NOT require opts.rowResidencyTag — gating
|
|
1861
|
+
// it would over-reject every regulated read to an untagged replica.
|
|
1862
|
+
var _replicaConstrained = replica.residencyTag &&
|
|
1863
|
+
replica.residencyTag !== "unrestricted" && replica.residencyTag !== "global";
|
|
1813
1864
|
// Fail CLOSED when the row's region is not identified: a regulated read to a
|
|
1814
|
-
// residency-
|
|
1815
|
-
// residency-restricted rows to an arbitrary-region replica with no check
|
|
1816
|
-
// all (symmetric with the write gate's RESIDENCY_GATE_REQUIRED).
|
|
1865
|
+
// residency-CONSTRAINED replica without opts.rowResidencyTag would otherwise
|
|
1866
|
+
// route residency-restricted rows to an arbitrary-region replica with no check
|
|
1867
|
+
// at all (symmetric with the write gate's RESIDENCY_GATE_REQUIRED).
|
|
1817
1868
|
if (!_tagPresent && _crossBorderRegulated(_readPosture) &&
|
|
1818
|
-
|
|
1869
|
+
_replicaConstrained && !replica.allowCrossBorder) {
|
|
1819
1870
|
_emit("db.residency.replica.tag_required", "denied", {
|
|
1820
1871
|
backend: b.name, replicaIdx: replica.index,
|
|
1821
1872
|
replicaTag: replica.residencyTag, posture: _readPosture,
|
|
@@ -2574,6 +2625,11 @@ module.exports = {
|
|
|
2574
2625
|
// lock tables live on the externalDb side. See lib/external-db-migrate.js.
|
|
2575
2626
|
migrate: externalDbMigrate,
|
|
2576
2627
|
Pool: Pool,
|
|
2628
|
+
// Shared SQL statement classifier (CTE / EXPLAIN aware): does the statement
|
|
2629
|
+
// return a row set? Internal — consumed by cluster-storage's local exec to
|
|
2630
|
+
// choose .all() vs .run() so a WITH (CTE) read is not mis-routed and its rows
|
|
2631
|
+
// lost. Underscore-prefixed: framework-internal, not an operator API surface.
|
|
2632
|
+
_statementReturnsRows: statementReturnsRows,
|
|
2577
2633
|
_resetForTest: _resetForTest,
|
|
2578
2634
|
_extractTargetRelation: _extractTargetRelation,
|
|
2579
2635
|
};
|
package/lib/fda-21cfr11.js
CHANGED
|
@@ -91,6 +91,18 @@ var DEFAULT_SIGNATURE_MEANINGS = Object.freeze([
|
|
|
91
91
|
// must carry §11.10(e) shape. Operators add more via opts.gxpNamespaces.
|
|
92
92
|
var DEFAULT_GXP_NAMESPACES = Object.freeze(["subject", "consent", "db", "breakglass"]);
|
|
93
93
|
|
|
94
|
+
// Non-mutating verb segments. A GxP-namespace event whose final action segment
|
|
95
|
+
// is NOT one of these is treated as a record MODIFICATION and must carry the
|
|
96
|
+
// §11.10(e) before/after/reason shape (fail-closed — an unknown verb is assumed
|
|
97
|
+
// to mutate). hasOwnProperty-guarded lookup (untrusted action key).
|
|
98
|
+
var READ_VERBS = Object.freeze({
|
|
99
|
+
read: 1, viewed: 1, view: 1, get: 1, got: 1, list: 1, listed: 1, query: 1,
|
|
100
|
+
queried: 1, access: 1, accessed: 1, export: 1, exported: 1, history: 1,
|
|
101
|
+
isgranted: 1, check: 1, checked: 1, verify: 1, verified: 1, lookup: 1,
|
|
102
|
+
search: 1, searched: 1, fetch: 1, fetched: 1, render: 1, rendered: 1,
|
|
103
|
+
download: 1, downloaded: 1,
|
|
104
|
+
});
|
|
105
|
+
|
|
94
106
|
// §11.10(e) — every modification audit must carry timestamp, actor,
|
|
95
107
|
// before/after pair, and a reason. The framework's audit row shape
|
|
96
108
|
// already carries `recordedAt` (timestamp) + `actorUserId` + `reason`;
|
|
@@ -114,11 +126,15 @@ function _hasRequiredAuditShape(row) {
|
|
|
114
126
|
if (!row.action || typeof row.action !== "string") {
|
|
115
127
|
return { ok: false, reason: "row missing action verb (§11.10(e))" };
|
|
116
128
|
}
|
|
117
|
-
//
|
|
118
|
-
// "
|
|
129
|
+
// §11.10(e) requires before/after on every record MODIFICATION. Inferring
|
|
130
|
+
// "is this a modification" from a denylist of mutating verbs silently let
|
|
131
|
+
// off-list verbs (anonymize / revoke / overwrite / replace / merge /
|
|
132
|
+
// withdraw / restrict / objection …) bypass the requirement. Fail CLOSED
|
|
133
|
+
// instead: a GxP-namespace event is treated as a modification UNLESS its
|
|
134
|
+
// final verb segment is on an explicit non-mutating READ allowlist.
|
|
119
135
|
var verb = row.action.toLowerCase();
|
|
120
|
-
var
|
|
121
|
-
|
|
136
|
+
var lastSeg = verb.indexOf(".") === -1 ? verb : verb.slice(verb.lastIndexOf(".") + 1);
|
|
137
|
+
var modShape = !Object.prototype.hasOwnProperty.call(READ_VERBS, lastSeg);
|
|
122
138
|
if (modShape) {
|
|
123
139
|
var meta = row.metadata;
|
|
124
140
|
// Audit chain stores metadata as a JSON string when read back —
|
|
@@ -263,7 +279,16 @@ function posture(opts) {
|
|
|
263
279
|
}, "denied");
|
|
264
280
|
return { ok: false, reason: "record-hash-mismatch" };
|
|
265
281
|
}
|
|
266
|
-
if (verifyWith
|
|
282
|
+
if (verifyWith) {
|
|
283
|
+
// A configured verifier means a signature is REQUIRED — a null/empty
|
|
284
|
+
// signature must fail closed (recordHash alone is self-consistency, not
|
|
285
|
+
// authentication; accepting it is alg:none-style signature stripping).
|
|
286
|
+
if (!signed.signature) {
|
|
287
|
+
_emit("fda21cfr11.signature.verified", {
|
|
288
|
+
printedName: signed.printedName, ok: false, reason: "signature-required",
|
|
289
|
+
}, "denied");
|
|
290
|
+
return { ok: false, reason: "signature-required" };
|
|
291
|
+
}
|
|
267
292
|
var sigBuf = Buffer.from(signed.signature, "base64");
|
|
268
293
|
var payload = JSON.stringify({
|
|
269
294
|
printedName: signed.printedName,
|
package/lib/flag-providers.js
CHANGED
|
@@ -41,6 +41,7 @@ var validateOpts = require("./validate-opts");
|
|
|
41
41
|
var lazyRequire = require("./lazy-require");
|
|
42
42
|
var safeJson = require("./safe-json");
|
|
43
43
|
var C = require("./constants");
|
|
44
|
+
var atomicFile = require("./atomic-file");
|
|
44
45
|
var { defineClass } = require("./framework-error");
|
|
45
46
|
var FlagError = defineClass("FlagError", { alwaysPermanent: true });
|
|
46
47
|
|
|
@@ -128,7 +129,10 @@ function localFile(opts) {
|
|
|
128
129
|
validateOpts.requireNonEmptyString(opts.path,
|
|
129
130
|
"providers.localFile: path", FlagError, "flag/bad-provider");
|
|
130
131
|
var raw;
|
|
131
|
-
|
|
132
|
+
// Capped fd-bound read; the cap precedes the alloc so an oversized flag file
|
|
133
|
+
// can't OOM boot. refuseSymlink stays OFF: a flag file is commonly a k8s
|
|
134
|
+
// ConfigMap mount, which is a symlink chain — refusing it would break that.
|
|
135
|
+
try { raw = atomicFile.fdSafeReadSync(opts.path, { maxBytes: C.BYTES.mib(1), encoding: "utf8" }); }
|
|
132
136
|
catch (e) {
|
|
133
137
|
throw new FlagError("flag/bad-provider",
|
|
134
138
|
"providers.localFile: cannot read file " + JSON.stringify(opts.path) +
|
|
@@ -154,7 +158,7 @@ function localFile(opts) {
|
|
|
154
158
|
try {
|
|
155
159
|
nodeFs.watch(opts.path, { persistent: false }, function () {
|
|
156
160
|
try {
|
|
157
|
-
var nextRaw =
|
|
161
|
+
var nextRaw = atomicFile.fdSafeReadSync(opts.path, { maxBytes: C.BYTES.mib(1), encoding: "utf8" });
|
|
158
162
|
var nextParsed = safeJson.parse(nextRaw, { maxBytes: C.BYTES.mib(1) });
|
|
159
163
|
if (nextParsed && nextParsed.flags) {
|
|
160
164
|
for (var k in nextParsed.flags) {
|
package/lib/forms.js
CHANGED
|
@@ -252,7 +252,7 @@ function _renderField(field) {
|
|
|
252
252
|
var control;
|
|
253
253
|
if (type === "textarea") control = _renderTextarea(field);
|
|
254
254
|
else if (type === "select") control = _renderSelect(field);
|
|
255
|
-
else if (INPUT_TYPES
|
|
255
|
+
else if (Object.prototype.hasOwnProperty.call(INPUT_TYPES, type)) control = _renderInput(field);
|
|
256
256
|
else throw new Error("forms.render: unsupported field type: " + type);
|
|
257
257
|
|
|
258
258
|
// Hidden + submit fields don't need a label wrapper
|
package/lib/gate-contract.js
CHANGED
|
@@ -1095,13 +1095,49 @@ function makeProfileBuilder(profiles) {
|
|
|
1095
1095
|
* posture.piiPolicy; // → "redact"
|
|
1096
1096
|
*/
|
|
1097
1097
|
function lookupCompliancePosture(name, postures, errorFactory, codePrefix) {
|
|
1098
|
-
|
|
1098
|
+
// hasOwnProperty: `name` is caller input; a bracket lookup would let a
|
|
1099
|
+
// prototype key like "constructor" pass the presence check and reach
|
|
1100
|
+
// Object.assign (proto shadowing).
|
|
1101
|
+
if (!postures || !Object.prototype.hasOwnProperty.call(postures, name)) {
|
|
1099
1102
|
throw errorFactory(codePrefix + ".bad-posture",
|
|
1100
1103
|
"unknown compliancePosture " + JSON.stringify(name));
|
|
1101
1104
|
}
|
|
1102
1105
|
return Object.assign({}, postures[name]);
|
|
1103
1106
|
}
|
|
1104
1107
|
|
|
1108
|
+
/**
|
|
1109
|
+
* @primitive b.gateContract.makePostureAccessor
|
|
1110
|
+
* @signature b.gateContract.makePostureAccessor(postures, opts?)
|
|
1111
|
+
* @since 0.15.14
|
|
1112
|
+
* @status stable
|
|
1113
|
+
* @related b.gateContract.lookupCompliancePosture, b.gateContract.resolveProfileName
|
|
1114
|
+
*
|
|
1115
|
+
* Build the public `compliancePosture(name)` accessor a guard exposes — maps a
|
|
1116
|
+
* compliance-posture name through the guard's own `postures` table to the
|
|
1117
|
+
* profile it selects, returning `opts.fallback` (default `null`) for an
|
|
1118
|
+
* unknown name. Folds the one-line lookup the mail-scanner / content-detect
|
|
1119
|
+
* factories each redefined verbatim (`return POSTURES[name] || null`) into one
|
|
1120
|
+
* proto-shadow-safe helper: the membership test is `hasOwnProperty.call` so a
|
|
1121
|
+
* prototype key (constructor / __proto__ / toString) resolves to the fallback
|
|
1122
|
+
* rather than an inherited Function. Unlike `lookupCompliancePosture` it does
|
|
1123
|
+
* NOT throw and returns the raw mapped value (a profile name string), not an
|
|
1124
|
+
* object copy.
|
|
1125
|
+
*
|
|
1126
|
+
* @opts
|
|
1127
|
+
* fallback: any, // value returned for an unknown / proto-key name (default null)
|
|
1128
|
+
*
|
|
1129
|
+
* @example
|
|
1130
|
+
* var compliancePosture = b.gateContract.makePostureAccessor(COMPLIANCE_POSTURES);
|
|
1131
|
+
* compliancePosture("hipaa"); // → "strict"
|
|
1132
|
+
* compliancePosture("constructor"); // → null (proto key, not an own posture)
|
|
1133
|
+
*/
|
|
1134
|
+
function makePostureAccessor(postures, opts) {
|
|
1135
|
+
var fallback = (opts && Object.prototype.hasOwnProperty.call(opts, "fallback")) ? opts.fallback : null;
|
|
1136
|
+
return function (name) {
|
|
1137
|
+
return Object.prototype.hasOwnProperty.call(postures, name) ? postures[name] : fallback;
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1105
1141
|
// "GuardCidrError" -> "guardCidr" — the guard's audit/message identity, derived
|
|
1106
1142
|
// once from its error class name. Used for the default gate's audit/metric
|
|
1107
1143
|
// prefix AND the profile resolver's error message, so neither re-cases the name.
|
|
@@ -1199,9 +1235,13 @@ function makeProfileResolver(cfg) {
|
|
|
1199
1235
|
*/
|
|
1200
1236
|
function resolveProfileName(opts, postures, defaultProfile) {
|
|
1201
1237
|
opts = opts || {};
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1238
|
+
// hasOwnProperty: opts.posture is caller/operator input; a bracket lookup
|
|
1239
|
+
// would let a prototype key like "constructor" resolve to an inherited
|
|
1240
|
+
// truthy value and become the profile name (proto shadowing).
|
|
1241
|
+
var postureProfile = (opts.posture && postures &&
|
|
1242
|
+
Object.prototype.hasOwnProperty.call(postures, opts.posture))
|
|
1243
|
+
? postures[opts.posture] : null;
|
|
1244
|
+
return opts.profile || postureProfile || defaultProfile;
|
|
1205
1245
|
}
|
|
1206
1246
|
|
|
1207
1247
|
/**
|
|
@@ -2072,7 +2112,7 @@ Object.freeze(INPUT_CONTRACTS);
|
|
|
2072
2112
|
|
|
2073
2113
|
function resolveInputContract(contract) {
|
|
2074
2114
|
if (typeof contract === "function") return contract;
|
|
2075
|
-
return INPUT_CONTRACTS[contract]
|
|
2115
|
+
return Object.prototype.hasOwnProperty.call(INPUT_CONTRACTS, contract) ? INPUT_CONTRACTS[contract] : INPUT_CONTRACTS.text;
|
|
2076
2116
|
}
|
|
2077
2117
|
|
|
2078
2118
|
/**
|
|
@@ -3125,6 +3165,7 @@ module.exports = {
|
|
|
3125
3165
|
charThreatDisposition: charThreatDisposition,
|
|
3126
3166
|
extractBytesAsText: extractBytesAsText,
|
|
3127
3167
|
lookupCompliancePosture: lookupCompliancePosture,
|
|
3168
|
+
makePostureAccessor: makePostureAccessor,
|
|
3128
3169
|
ALL_STRICT_POSTURES: ALL_STRICT_POSTURES,
|
|
3129
3170
|
CHAR_THREATS_REJECT_ALL: CHAR_THREATS_REJECT_ALL,
|
|
3130
3171
|
DANGEROUS_URL_SCHEMES: DANGEROUS_URL_SCHEMES,
|
package/lib/gdpr-ropa.js
CHANGED
|
@@ -91,7 +91,7 @@ function create(opts) {
|
|
|
91
91
|
throw new GdprRopaError("gdpr-ropa/bad-id",
|
|
92
92
|
"gdpr.ropa." + op + ": activity.id must be a non-empty string");
|
|
93
93
|
}
|
|
94
|
-
if (!VALID_LEGAL_BASES
|
|
94
|
+
if (!Object.prototype.hasOwnProperty.call(VALID_LEGAL_BASES, activity.legalBasis)) {
|
|
95
95
|
throw new GdprRopaError("gdpr-ropa/bad-legal-basis",
|
|
96
96
|
"gdpr.ropa." + op + ": activity.legalBasis must be one of " + Object.keys(VALID_LEGAL_BASES).join(", "));
|
|
97
97
|
}
|
|
@@ -127,7 +127,7 @@ function create(opts) {
|
|
|
127
127
|
registeredAt: existing.registeredAt,
|
|
128
128
|
lastUpdatedAt: now(),
|
|
129
129
|
});
|
|
130
|
-
if (patch.legalBasis && !VALID_LEGAL_BASES
|
|
130
|
+
if (patch.legalBasis && !Object.prototype.hasOwnProperty.call(VALID_LEGAL_BASES, merged.legalBasis)) {
|
|
131
131
|
throw new GdprRopaError("gdpr-ropa/bad-legal-basis",
|
|
132
132
|
"gdpr.ropa.update: legalBasis must be one of " + Object.keys(VALID_LEGAL_BASES).join(", "));
|
|
133
133
|
}
|
|
@@ -173,6 +173,21 @@ function create(opts) {
|
|
|
173
173
|
activities: list(),
|
|
174
174
|
};
|
|
175
175
|
}
|
|
176
|
+
// Quote a CSV cell AND neutralize spreadsheet formula injection (CWE-1236):
|
|
177
|
+
// a value beginning with = + - @ TAB or CR is evaluated as a formula by
|
|
178
|
+
// Excel / Google Sheets when the export is opened, so a RoPA field like
|
|
179
|
+
// "=HYPERLINK(...)" or "=cmd|..." would execute. Prefix such a value with a
|
|
180
|
+
// single quote so the spreadsheet renders it as literal text; then RFC-4180
|
|
181
|
+
// quote (double internal quotes).
|
|
182
|
+
function _csvCell(v) {
|
|
183
|
+
var s = (v === undefined || v === null) ? ""
|
|
184
|
+
: (Array.isArray(v) ? JSON.stringify(v) : String(v));
|
|
185
|
+
var c0 = s.charCodeAt(0);
|
|
186
|
+
if (c0 === 0x3d || c0 === 0x2b || c0 === 0x2d || c0 === 0x40 || c0 === 0x09 || c0 === 0x0d) {
|
|
187
|
+
s = "'" + s;
|
|
188
|
+
}
|
|
189
|
+
return '"' + s.replace(/"/g, '""') + '"';
|
|
190
|
+
}
|
|
176
191
|
function _exportCsv() {
|
|
177
192
|
var headers = [
|
|
178
193
|
"id", "name", "purposes", "legalBasis", "dataCategories",
|
|
@@ -183,13 +198,7 @@ function create(opts) {
|
|
|
183
198
|
var entries = list();
|
|
184
199
|
for (var i = 0; i < entries.length; i++) {
|
|
185
200
|
var e = entries[i];
|
|
186
|
-
|
|
187
|
-
var v = e[h];
|
|
188
|
-
if (v === undefined || v === null) return "";
|
|
189
|
-
if (Array.isArray(v)) return JSON.stringify(v).replace(/"/g, "\"\"");
|
|
190
|
-
return String(v).replace(/"/g, "\"\"");
|
|
191
|
-
});
|
|
192
|
-
rows.push(row.map(function (c) { return '"' + c + '"'; }).join(","));
|
|
201
|
+
rows.push(headers.map(function (h) { return _csvCell(e[h]); }).join(","));
|
|
193
202
|
}
|
|
194
203
|
return rows.join("\n");
|
|
195
204
|
}
|
|
@@ -35,7 +35,13 @@ var ROUTER_TOKEN_MIN_LEN = 32;
|
|
|
35
35
|
var NONCE_MIN_LEN = 16; // string-length floor for nonce entropy, not bytes
|
|
36
36
|
var NONCE_MAX_LEN = 256; // string-length cap, not bytes
|
|
37
37
|
var NONCE_PREVIEW_LEN = 8; // log-preview slice length, not bytes
|
|
38
|
-
|
|
38
|
+
// Word-boundary anchored on BOTH probes. A prefix char-class ([\s,{]) on
|
|
39
|
+
// `_service` was bypassable by an alias with no leading space — `query{x:_service{sdl}}`
|
|
40
|
+
// has `:` before `_service`, which is not in the class, so the SDL trust gate
|
|
41
|
+
// was skipped and the subgraph leaked its full SDL. `\b` already prevents
|
|
42
|
+
// matching substrings like `my_service` (no boundary between `y` and `_`), so
|
|
43
|
+
// the prefix class only added the bypass.
|
|
44
|
+
var SDL_PROBE_RE = /\b_service\b|\b_entities\b/;
|
|
39
45
|
|
|
40
46
|
/**
|
|
41
47
|
* @primitive b.graphqlFederation.queryProbesSdl
|
|
@@ -173,14 +179,21 @@ function guardSdl(opts) {
|
|
|
173
179
|
return function graphqlFedGuard(req, res, next) {
|
|
174
180
|
Promise.resolve().then(function () {
|
|
175
181
|
return _readBody(req, errorClass).then(function (rawBody) {
|
|
176
|
-
var
|
|
182
|
+
var probesSdl = false;
|
|
177
183
|
try {
|
|
178
184
|
var parsed = typeof rawBody === "string" ? safeJson.parse(rawBody, { maxBytes: C.BYTES.mib(1) }) : rawBody; // routed via safeJson.parse
|
|
179
|
-
|
|
185
|
+
// A GraphQL HTTP batch is a JSON ARRAY of operations; probe EVERY
|
|
186
|
+
// element's query, not just `parsed.query` (which is undefined for an
|
|
187
|
+
// array → the SDL gate was skipped and a batched `_service{sdl}`
|
|
188
|
+
// operation leaked the SDL on batching-enabled subgraphs).
|
|
189
|
+
var candidates = Array.isArray(parsed)
|
|
190
|
+
? parsed.map(function (o) { return o && typeof o === "object" ? o.query : null; })
|
|
191
|
+
: [parsed && typeof parsed === "object" ? parsed.query : null];
|
|
192
|
+
probesSdl = candidates.some(queryProbesSdl);
|
|
180
193
|
} catch (_e) { /* not JSON; pass through */ }
|
|
181
194
|
if (req.body === undefined) req.body = rawBody;
|
|
182
195
|
|
|
183
|
-
if (!
|
|
196
|
+
if (!probesSdl) {
|
|
184
197
|
if (typeof next === "function") next();
|
|
185
198
|
return;
|
|
186
199
|
}
|
package/lib/guard-all.js
CHANGED
|
@@ -128,12 +128,12 @@ function _verifyParity() {
|
|
|
128
128
|
failures.push(g.NAME + ": missing gate(opts) function");
|
|
129
129
|
}
|
|
130
130
|
SHARED_PROFILES.forEach(function (p) {
|
|
131
|
-
if (!g.PROFILES || !g.PROFILES
|
|
131
|
+
if (!g.PROFILES || !Object.prototype.hasOwnProperty.call(g.PROFILES, p)) {
|
|
132
132
|
failures.push(g.NAME + ": does not declare shared profile " + JSON.stringify(p));
|
|
133
133
|
}
|
|
134
134
|
});
|
|
135
135
|
SHARED_POSTURES.forEach(function (p) {
|
|
136
|
-
if (!g.COMPLIANCE_POSTURES || !g.COMPLIANCE_POSTURES
|
|
136
|
+
if (!g.COMPLIANCE_POSTURES || !Object.prototype.hasOwnProperty.call(g.COMPLIANCE_POSTURES, p)) {
|
|
137
137
|
failures.push(g.NAME + ": does not declare shared compliance posture " + JSON.stringify(p));
|
|
138
138
|
}
|
|
139
139
|
});
|
package/lib/guard-dsn.js
CHANGED
|
@@ -210,7 +210,7 @@ function parse(deliveryStatusBody, opts) {
|
|
|
210
210
|
"parse: per-recipient block missing Action (RFC 3464 §2.3.3)");
|
|
211
211
|
}
|
|
212
212
|
var action = fields["action"].toLowerCase();
|
|
213
|
-
if (!KNOWN_ACTIONS
|
|
213
|
+
if (!Object.prototype.hasOwnProperty.call(KNOWN_ACTIONS, action)) {
|
|
214
214
|
throw new GuardDsnError("guard-dsn/bad-action",
|
|
215
215
|
"parse: Action '" + action + "' not in RFC 3464 §2.3.3 vocabulary");
|
|
216
216
|
}
|
package/lib/guard-envelope.js
CHANGED
|
@@ -148,7 +148,7 @@ var COMPLIANCE_POSTURES = gateContract.ALL_STRICT_POSTURES;
|
|
|
148
148
|
function check(ctx, opts) {
|
|
149
149
|
opts = opts || {};
|
|
150
150
|
var profile = gateContract.resolveProfileName(opts, COMPLIANCE_POSTURES, DEFAULT_PROFILE);
|
|
151
|
-
if (!PROFILES
|
|
151
|
+
if (!Object.prototype.hasOwnProperty.call(PROFILES, profile)) {
|
|
152
152
|
throw new GuardEnvelopeError("guard-envelope/bad-profile",
|
|
153
153
|
"check: unknown profile '" + profile + "'");
|
|
154
154
|
}
|