@blamejs/core 0.9.49 → 0.10.2
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 +952 -908
- package/index.js +25 -0
- package/lib/_test/crypto-fixtures.js +67 -0
- package/lib/agent-event-bus.js +52 -6
- package/lib/agent-idempotency.js +169 -16
- package/lib/agent-orchestrator.js +263 -9
- package/lib/agent-posture-chain.js +163 -5
- package/lib/agent-saga.js +146 -16
- package/lib/agent-snapshot.js +349 -19
- package/lib/agent-stream.js +34 -2
- package/lib/agent-tenant.js +179 -23
- package/lib/agent-trace.js +84 -21
- package/lib/auth/aal.js +8 -1
- package/lib/auth/ciba.js +6 -1
- package/lib/auth/dpop.js +7 -2
- package/lib/auth/fal.js +17 -8
- package/lib/auth/jwt-external.js +128 -4
- package/lib/auth/oauth.js +232 -10
- package/lib/auth/oid4vci.js +67 -7
- package/lib/auth/openid-federation.js +71 -25
- package/lib/auth/passkey.js +140 -6
- package/lib/auth/sd-jwt-vc.js +78 -5
- package/lib/circuit-breaker.js +10 -2
- package/lib/cli.js +13 -0
- package/lib/compliance.js +176 -8
- package/lib/crypto-field.js +114 -14
- package/lib/crypto.js +216 -20
- package/lib/db.js +1 -0
- package/lib/guard-graphql.js +37 -0
- package/lib/guard-jmap.js +321 -0
- package/lib/guard-managesieve-command.js +566 -0
- package/lib/guard-pop3-command.js +317 -0
- package/lib/guard-regex.js +138 -1
- package/lib/guard-smtp-command.js +58 -3
- package/lib/guard-xml.js +39 -1
- package/lib/mail-agent.js +20 -7
- package/lib/mail-arc-sign.js +12 -8
- package/lib/mail-auth.js +323 -34
- package/lib/mail-crypto-pgp.js +934 -0
- package/lib/mail-crypto-smime.js +340 -0
- package/lib/mail-crypto.js +108 -0
- package/lib/mail-dav.js +1224 -0
- package/lib/mail-deploy.js +492 -0
- package/lib/mail-dkim.js +431 -26
- package/lib/mail-journal.js +435 -0
- package/lib/mail-scan.js +502 -0
- package/lib/mail-server-imap.js +64 -26
- package/lib/mail-server-jmap.js +488 -0
- package/lib/mail-server-managesieve.js +853 -0
- package/lib/mail-server-mx.js +40 -30
- package/lib/mail-server-pop3.js +836 -0
- package/lib/mail-server-rate-limit.js +13 -0
- package/lib/mail-server-submission.js +70 -24
- package/lib/mail-server-tls.js +445 -0
- package/lib/mail-sieve.js +557 -0
- package/lib/mail-spam-score.js +284 -0
- package/lib/mail.js +99 -0
- package/lib/metrics.js +80 -3
- package/lib/middleware/dpop.js +58 -3
- package/lib/middleware/idempotency-key.js +255 -42
- package/lib/middleware/protected-resource-metadata.js +114 -2
- package/lib/network-dns-resolver.js +33 -0
- package/lib/network-tls.js +46 -0
- package/lib/otel-export.js +13 -4
- package/lib/outbox.js +62 -12
- package/lib/pqc-agent.js +13 -5
- package/lib/retry.js +23 -9
- package/lib/router.js +23 -1
- package/lib/safe-ical.js +634 -0
- package/lib/safe-icap.js +502 -0
- package/lib/safe-mime.js +15 -0
- package/lib/safe-sieve.js +684 -0
- package/lib/safe-smtp.js +57 -0
- package/lib/safe-url.js +37 -0
- package/lib/safe-vcard.js +473 -0
- package/lib/self-update-standalone-verifier.js +32 -3
- package/lib/self-update.js +153 -33
- package/lib/vendor/MANIFEST.json +161 -156
- package/lib/vendor-data.js +127 -9
- package/lib/vex.js +324 -59
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/vex.js
CHANGED
|
@@ -7,35 +7,38 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @intro
|
|
9
9
|
* VEX (Vulnerability Exploitability eXchange) statement builder per
|
|
10
|
-
* OASIS CSAF 2.1 §4.4. Operators ship a
|
|
11
|
-
* `sbom.cdx.json` declaring per-vulnerability
|
|
12
|
-
* for the framework's component set. Status
|
|
10
|
+
* OASIS CSAF 2.1 §4.4 (CSAF VEX profile). Operators ship a
|
|
11
|
+
* `vex.cdx.json` alongside `sbom.cdx.json` declaring per-vulnerability
|
|
12
|
+
* exploitability state for the framework's component set. Status
|
|
13
|
+
* vocabulary follows the CSAF VEX profile §4.4 restriction (a strict
|
|
14
|
+
* subset of the full CSAF 2.1 §3.2.3.13 product_status vocabulary):
|
|
13
15
|
*
|
|
14
|
-
* "not_affected" — framework does not include / does not use
|
|
15
|
-
* the vulnerable component
|
|
16
|
-
* "affected" — framework includes and uses the vulnerable
|
|
17
|
-
* component; remediation required
|
|
18
16
|
* "fixed" — framework included the vulnerable component
|
|
19
17
|
* previously; the cited version ships the fix
|
|
18
|
+
* "known_affected" — framework includes and uses the vulnerable
|
|
19
|
+
* component; remediation required
|
|
20
|
+
* "known_not_affected" — framework does not include / does not use
|
|
21
|
+
* the vulnerable component
|
|
20
22
|
* "under_investigation" — disclosure is being evaluated
|
|
21
23
|
*
|
|
22
|
-
* Justifications (when status=
|
|
24
|
+
* Justifications (when status=known_not_affected): `component_not_present`,
|
|
23
25
|
* `vulnerable_code_not_present`, `vulnerable_code_not_in_execute_path`,
|
|
24
26
|
* `vulnerable_code_cannot_be_controlled_by_adversary`,
|
|
25
|
-
* `inline_mitigations_already_exist
|
|
27
|
+
* `inline_mitigations_already_exist` (CSAF 2.1 §3.2.2.7).
|
|
26
28
|
*
|
|
27
29
|
* `b.vex.statement({...})` produces a single VEX vulnerability
|
|
28
30
|
* record. `b.vex.document({...})` assembles a complete CSAF 2.1
|
|
29
|
-
* document with the framework's distributor metadata
|
|
30
|
-
*
|
|
31
|
-
*
|
|
31
|
+
* document with the framework's distributor metadata + an
|
|
32
|
+
* auto-emitted `product_tree.full_product_names` resolving every
|
|
33
|
+
* `product_ids` reference used by the statements (CSAF 2.1 §3.1).
|
|
34
|
+
* `b.vex.serialize` round-trips to canonical JSON (RFC 8785 / sorted
|
|
35
|
+
* keys) for signing.
|
|
32
36
|
*
|
|
33
|
-
* Why the framework ships VEX:
|
|
34
|
-
*
|
|
35
|
-
* framework
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* organisational VEX without re-auditing each framework dependency.
|
|
37
|
+
* Why the framework ships VEX: operators consuming the framework's
|
|
38
|
+
* VEX populate their own organisational VEX without re-auditing
|
|
39
|
+
* each framework dependency. Downstream consumers (Dependency-Track,
|
|
40
|
+
* csaf-validator-service, FIRST.org CSAF) reject malformed docs;
|
|
41
|
+
* shipping a spec-conformant doc is the cost of admission.
|
|
39
42
|
*
|
|
40
43
|
* @card
|
|
41
44
|
* OASIS CSAF 2.1 VEX statement + document builder. Operators ship
|
|
@@ -54,11 +57,15 @@ var VexError = defineClass("VexError", { alwaysPermanent: true });
|
|
|
54
57
|
var CSAF_VERSION = "2.1";
|
|
55
58
|
var DOCUMENT_CATEGORY_VEX = "csaf_vex";
|
|
56
59
|
|
|
57
|
-
// CSAF
|
|
60
|
+
// CSAF VEX profile §4.4 — product_status vocabulary restricted subset.
|
|
61
|
+
// The full CSAF 2.1 §3.2.3.13 vocabulary includes 8 values
|
|
62
|
+
// (first_affected, first_fixed, last_affected, recommended, etc.); the
|
|
63
|
+
// VEX profile restricts to these four. Spec-conformant VEX validators
|
|
64
|
+
// (csaf-validator-service, FIRST.org CSAF) reject documents whose
|
|
65
|
+
// product_status keys are outside this subset. The framework emits
|
|
66
|
+
// `csaf_vex` documents, so STATUS_VALUES tracks the VEX profile.
|
|
58
67
|
var STATUS_VALUES = Object.freeze([
|
|
59
|
-
"
|
|
60
|
-
"known_not_affected", "last_affected", "recommended",
|
|
61
|
-
"under_investigation",
|
|
68
|
+
"fixed", "known_affected", "known_not_affected", "under_investigation",
|
|
62
69
|
]);
|
|
63
70
|
|
|
64
71
|
// CSAF 2.1 §3.2.2.7 — justifications for known_not_affected.
|
|
@@ -77,6 +84,38 @@ var JUSTIFICATION_VALUES = Object.freeze([
|
|
|
77
84
|
// downstream validation failures against spec-conformant tooling.
|
|
78
85
|
var TLP_LABELS = Object.freeze(["CLEAR", "GREEN", "AMBER", "AMBER+STRICT", "RED"]);
|
|
79
86
|
|
|
87
|
+
// CSAF 2.1 §3.2.1.6 — document.tracking.status enumeration.
|
|
88
|
+
var TRACKING_STATUS_VALUES = Object.freeze(["draft", "interim", "final"]);
|
|
89
|
+
|
|
90
|
+
// CSAF 2.1 §3.2.1.10 — references[].category enumeration. `external`
|
|
91
|
+
// for third-party advisory URLs (NVD, GHSA, vendor pages OTHER than
|
|
92
|
+
// the document publisher); `self` for the publisher's own URLs
|
|
93
|
+
// (advisory hosted on the publisher's domain).
|
|
94
|
+
var REFERENCE_CATEGORY_VALUES = Object.freeze(["external", "self"]);
|
|
95
|
+
|
|
96
|
+
// CSAF 2.1 §3.2.3.7 — notes[].category enumeration. The framework
|
|
97
|
+
// accepts the full enumeration; operators pick per note. Default for
|
|
98
|
+
// `impactStatement` shorthand stays `details` (operator-readable
|
|
99
|
+
// impact / mitigation summary).
|
|
100
|
+
var NOTE_CATEGORY_VALUES = Object.freeze([
|
|
101
|
+
"description", "details", "faq", "general", "legal_disclaimer",
|
|
102
|
+
"other", "summary",
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
// CSAF 2.1 §3.2.1.12.1.1.1 — TLP 2.0 boilerplate text required when
|
|
106
|
+
// distribution.tlp.label is RED or AMBER+STRICT (FIRST TLP 2.0). The
|
|
107
|
+
// document MUST carry the canonical distribution prose so consumers
|
|
108
|
+
// know the redistribution constraint. Operators may override via
|
|
109
|
+
// opts.distributionText; the framework's defaults match FIRST TLP 2.0
|
|
110
|
+
// verbatim so a spec-conformant doc is the no-effort default.
|
|
111
|
+
var TLP_DEFAULT_TEXTS = Object.freeze({
|
|
112
|
+
"CLEAR": "TLP:CLEAR information may be distributed without restriction.",
|
|
113
|
+
"GREEN": "TLP:GREEN information may be shared with peers and partner organizations within the community, but not via publicly accessible channels.",
|
|
114
|
+
"AMBER": "TLP:AMBER information may be shared with members of the recipient's organization, and clients or customers who need to know, but only on a need-to-know basis.",
|
|
115
|
+
"AMBER+STRICT": "TLP:AMBER+STRICT information is restricted to the recipient organization only; no onward sharing without explicit permission.",
|
|
116
|
+
"RED": "TLP:RED information is restricted to named individual recipients. No further sharing is permitted.",
|
|
117
|
+
});
|
|
118
|
+
|
|
80
119
|
/**
|
|
81
120
|
* @primitive b.vex.statement
|
|
82
121
|
* @signature b.vex.statement(opts)
|
|
@@ -95,24 +134,27 @@ var TLP_LABELS = Object.freeze(["CLEAR", "GREEN", "AMBER", "AMBER+STRICT", "RED"
|
|
|
95
134
|
* CVE). A `cweId` alone is NOT a valid CSAF vulnerability identity
|
|
96
135
|
* (CWE is a weakness classification, not a per-vulnerability id);
|
|
97
136
|
* supply `ids` alongside `cweId` when issuing a non-CVE statement.
|
|
98
|
-
* Also required: `status` (one of STATUS_VALUES
|
|
99
|
-
* (array of product identifiers the
|
|
137
|
+
* Also required: `status` (one of STATUS_VALUES — CSAF VEX profile
|
|
138
|
+
* §4.4 subset), `productIds` (array of product identifiers the
|
|
139
|
+
* statement applies to).
|
|
100
140
|
*
|
|
101
141
|
* When `status === "known_not_affected"`, `justification` is required
|
|
102
142
|
* per CSAF 2.1 §3.2.3.13.
|
|
103
143
|
*
|
|
104
144
|
* @opts
|
|
105
|
-
* cveId:
|
|
106
|
-
* cweId:
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
145
|
+
* cveId: string, // CVE-YYYY-NNNN
|
|
146
|
+
* cweId: string, // CWE-NNN (emitted as cwes[0] per CSAF §3.2.3.4)
|
|
147
|
+
* cweName: string, // human-readable CWE name (e.g. "Cross-site Scripting"); when omitted, cwes[].name is omitted (avoids CWE-ID-as-name antipattern flagged by csaf-validator)
|
|
148
|
+
* ids: object[], // [{ systemName, text }] non-CVE tracking ids
|
|
149
|
+
* title: string, // human-readable vulnerability title
|
|
150
|
+
* status: string, // one of STATUS_VALUES (VEX profile subset)
|
|
151
|
+
* productIds: string[], // CSAF product identifiers
|
|
152
|
+
* justification: string, // required when status=known_not_affected
|
|
153
|
+
* impactStatement: string, // operator-readable impact / mitigation note (shorthand for notes[{category:"details",...}])
|
|
154
|
+
* notes: object[], // [{ category, text, title? }] full CSAF notes channel (CSAF §3.2.3.7)
|
|
155
|
+
* references: array, // [string] or [{ url, summary?, category? }] — CSAF §3.2.3.10
|
|
156
|
+
* firstReleased: string, // ISO 8601 timestamp
|
|
157
|
+
* lastUpdated: string, // ISO 8601 timestamp
|
|
116
158
|
*
|
|
117
159
|
* @example
|
|
118
160
|
* b.vex.statement({
|
|
@@ -150,6 +192,7 @@ function statement(opts) {
|
|
|
150
192
|
"statement: cweId must match `CWE-NNN` (got '" + opts.cweId + "')");
|
|
151
193
|
}
|
|
152
194
|
}
|
|
195
|
+
validateOpts.optionalNonEmptyString(opts.cweName, "statement.cweName", VexError, "vex/bad-cwe-name");
|
|
153
196
|
if (opts.ids !== undefined) {
|
|
154
197
|
if (!Array.isArray(opts.ids)) {
|
|
155
198
|
throw new VexError("vex/bad-ids",
|
|
@@ -167,7 +210,8 @@ function statement(opts) {
|
|
|
167
210
|
}
|
|
168
211
|
if (STATUS_VALUES.indexOf(opts.status) === -1) {
|
|
169
212
|
throw new VexError("vex/bad-status",
|
|
170
|
-
"statement: status must be one of " + STATUS_VALUES.join(" / ")
|
|
213
|
+
"statement: status must be one of " + STATUS_VALUES.join(" / ") +
|
|
214
|
+
" (CSAF VEX profile §4.4 subset)");
|
|
171
215
|
}
|
|
172
216
|
if (opts.productIds === undefined || opts.productIds === null) {
|
|
173
217
|
throw new VexError("vex/missing-product-ids",
|
|
@@ -187,16 +231,105 @@ function statement(opts) {
|
|
|
187
231
|
}
|
|
188
232
|
}
|
|
189
233
|
|
|
234
|
+
// CSAF 2.1 §3.2.3.7 — notes channel. Operators supplying `notes`
|
|
235
|
+
// directly get the full CSAF notes vocabulary (description,
|
|
236
|
+
// details, faq, general, legal_disclaimer, other, summary). The
|
|
237
|
+
// `impactStatement` shorthand stays for the common "single details
|
|
238
|
+
// note" case (back-compat with v0.9.6 callers).
|
|
239
|
+
var compiledNotes = null;
|
|
240
|
+
if (opts.notes !== undefined) {
|
|
241
|
+
if (!Array.isArray(opts.notes)) {
|
|
242
|
+
throw new VexError("vex/bad-notes",
|
|
243
|
+
"statement: notes must be an array of { category, text, title? }");
|
|
244
|
+
}
|
|
245
|
+
compiledNotes = [];
|
|
246
|
+
for (var ni = 0; ni < opts.notes.length; ni++) {
|
|
247
|
+
var n = opts.notes[ni];
|
|
248
|
+
if (!n || typeof n !== "object" ||
|
|
249
|
+
typeof n.text !== "string" || n.text.length === 0 ||
|
|
250
|
+
typeof n.category !== "string" ||
|
|
251
|
+
NOTE_CATEGORY_VALUES.indexOf(n.category) === -1) {
|
|
252
|
+
throw new VexError("vex/bad-notes",
|
|
253
|
+
"statement: notes[" + ni + "] must be { category: one of " +
|
|
254
|
+
NOTE_CATEGORY_VALUES.join("/") + ", text: string, title?: string }");
|
|
255
|
+
}
|
|
256
|
+
var noteOut = { category: n.category, text: n.text };
|
|
257
|
+
if (n.title !== undefined) {
|
|
258
|
+
if (typeof n.title !== "string" || n.title.length === 0) {
|
|
259
|
+
throw new VexError("vex/bad-notes",
|
|
260
|
+
"statement: notes[" + ni + "].title must be a non-empty string when supplied");
|
|
261
|
+
}
|
|
262
|
+
noteOut.title = n.title;
|
|
263
|
+
}
|
|
264
|
+
compiledNotes.push(noteOut);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// CSAF 2.1 §3.2.3.10 — references channel. Two operator shapes:
|
|
269
|
+
// - bare string url → { category: "external", url, summary: url }
|
|
270
|
+
// - { url, summary?, category? } → preserved verbatim after validation
|
|
271
|
+
// Operator-supplied category defaults to "external" only when the
|
|
272
|
+
// caller doesn't disambiguate; legitimate "self" references (URL on
|
|
273
|
+
// the publisher's own domain) require operator opt-in to avoid the
|
|
274
|
+
// framework auto-classifying.
|
|
275
|
+
var compiledRefs = null;
|
|
276
|
+
if (opts.references !== undefined) {
|
|
277
|
+
if (!Array.isArray(opts.references)) {
|
|
278
|
+
throw new VexError("vex/bad-references",
|
|
279
|
+
"statement: references must be an array of strings or { url, summary?, category? } objects");
|
|
280
|
+
}
|
|
281
|
+
compiledRefs = [];
|
|
282
|
+
for (var ri = 0; ri < opts.references.length; ri++) {
|
|
283
|
+
var r = opts.references[ri];
|
|
284
|
+
var refUrl;
|
|
285
|
+
var refSummary;
|
|
286
|
+
var refCategory = "external";
|
|
287
|
+
if (typeof r === "string") {
|
|
288
|
+
if (r.length === 0) {
|
|
289
|
+
throw new VexError("vex/bad-references",
|
|
290
|
+
"statement: references[" + ri + "] empty string");
|
|
291
|
+
}
|
|
292
|
+
refUrl = r;
|
|
293
|
+
refSummary = r;
|
|
294
|
+
} else if (r && typeof r === "object" && !Array.isArray(r)) {
|
|
295
|
+
if (typeof r.url !== "string" || r.url.length === 0) {
|
|
296
|
+
throw new VexError("vex/bad-references",
|
|
297
|
+
"statement: references[" + ri + "].url must be a non-empty string");
|
|
298
|
+
}
|
|
299
|
+
refUrl = r.url;
|
|
300
|
+
refSummary = typeof r.summary === "string" && r.summary.length > 0 ? r.summary : refUrl;
|
|
301
|
+
if (r.category !== undefined) {
|
|
302
|
+
if (typeof r.category !== "string" ||
|
|
303
|
+
REFERENCE_CATEGORY_VALUES.indexOf(r.category) === -1) {
|
|
304
|
+
throw new VexError("vex/bad-references",
|
|
305
|
+
"statement: references[" + ri + "].category must be one of " +
|
|
306
|
+
REFERENCE_CATEGORY_VALUES.join(" / "));
|
|
307
|
+
}
|
|
308
|
+
refCategory = r.category;
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
throw new VexError("vex/bad-references",
|
|
312
|
+
"statement: references[" + ri + "] must be a string url or { url, summary?, category? }");
|
|
313
|
+
}
|
|
314
|
+
compiledRefs.push({ category: refCategory, summary: refSummary, url: refUrl });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
190
318
|
var vuln = {};
|
|
191
319
|
if (opts.cveId) vuln.cve = opts.cveId;
|
|
192
|
-
// CSAF 2.1 §3.2.3.4 — cwes is a LIST of { id, name }
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
|
|
320
|
+
// CSAF 2.1 §3.2.3.4 — cwes is a LIST of { id, name }. The `name`
|
|
321
|
+
// field is the human-readable weakness title (e.g. "Cross-site
|
|
322
|
+
// Scripting" for CWE-79). Using the CWE-ID as the name is the
|
|
323
|
+
// antipattern csaf-validator-service flags; we now omit `name`
|
|
324
|
+
// when the operator hasn't supplied `cweName`. CSAF schema treats
|
|
325
|
+
// `name` as optional within the cwes[] array entry.
|
|
326
|
+
if (opts.cweId) {
|
|
327
|
+
var cweEntry = { id: opts.cweId };
|
|
328
|
+
if (opts.cweName) cweEntry.name = opts.cweName;
|
|
329
|
+
vuln.cwes = [cweEntry];
|
|
330
|
+
}
|
|
196
331
|
if (hasIds) {
|
|
197
|
-
vuln.ids = opts.ids.map(
|
|
198
|
-
return { system_name: entry.systemName, text: entry.text };
|
|
199
|
-
});
|
|
332
|
+
vuln.ids = opts.ids.map(_toCsafId);
|
|
200
333
|
}
|
|
201
334
|
if (opts.title) vuln.title = opts.title;
|
|
202
335
|
vuln.product_status = {};
|
|
@@ -208,23 +341,89 @@ function statement(opts) {
|
|
|
208
341
|
product_ids: opts.productIds.slice(),
|
|
209
342
|
}];
|
|
210
343
|
}
|
|
344
|
+
// Merge impactStatement shorthand + explicit notes[]. The shorthand
|
|
345
|
+
// prepends a single `{category:"details",title:"Impact",text:...}`
|
|
346
|
+
// entry to preserve v0.9.6 caller ordering; operators wanting full
|
|
347
|
+
// control supply `notes` directly.
|
|
348
|
+
var allNotes = [];
|
|
211
349
|
if (opts.impactStatement) {
|
|
212
|
-
|
|
350
|
+
allNotes.push({
|
|
213
351
|
category: "details",
|
|
214
352
|
text: opts.impactStatement,
|
|
215
353
|
title: "Impact",
|
|
216
|
-
}];
|
|
217
|
-
}
|
|
218
|
-
if (Array.isArray(opts.references) && opts.references.length > 0) {
|
|
219
|
-
vuln.references = opts.references.map(function (url) {
|
|
220
|
-
return { summary: "Advisory reference", url: url, category: "external" };
|
|
221
354
|
});
|
|
222
355
|
}
|
|
356
|
+
if (compiledNotes) allNotes = allNotes.concat(compiledNotes);
|
|
357
|
+
if (allNotes.length > 0) vuln.notes = allNotes;
|
|
358
|
+
if (compiledRefs) vuln.references = compiledRefs;
|
|
223
359
|
if (opts.firstReleased) vuln.first_released = opts.firstReleased;
|
|
224
360
|
if (opts.lastUpdated) vuln.last_updated = opts.lastUpdated;
|
|
225
361
|
return vuln;
|
|
226
362
|
}
|
|
227
363
|
|
|
364
|
+
// _toCsafId — converts the operator-facing camelCase ids[] entries
|
|
365
|
+
// (`{ systemName, text }`) to the snake_case shape CSAF 2.1 §3.2.3.5
|
|
366
|
+
// requires (`{ system_name, text }`). One-liner, but lifted to a
|
|
367
|
+
// named helper so the camelCase→snake_case conversion is grepable and
|
|
368
|
+
// documented in one place (no other field on the statement output
|
|
369
|
+
// switches case — this is the only one).
|
|
370
|
+
function _toCsafId(entry) {
|
|
371
|
+
return { system_name: entry.systemName, text: entry.text };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// _collectProductIds — walks every supplied statement and harvests
|
|
375
|
+
// the union of product_ids referenced under product_status[*] and
|
|
376
|
+
// flags[].product_ids. Emitted under document.product_tree.full_product_names
|
|
377
|
+
// (CSAF 2.1 §3.1) so flags[].product_ids and product_status keys
|
|
378
|
+
// resolve against the document's own product tree — the spec
|
|
379
|
+
// requires every referenced product_id to be defined in the
|
|
380
|
+
// product_tree. Without this, Dependency-Track + csaf-validator-service
|
|
381
|
+
// reject the document as "unresolved product reference."
|
|
382
|
+
function _collectProductIds(statements, productTreeNames) {
|
|
383
|
+
var seen = Object.create(null);
|
|
384
|
+
var ordered = [];
|
|
385
|
+
function _add(id) {
|
|
386
|
+
if (typeof id !== "string" || id.length === 0) return;
|
|
387
|
+
if (seen[id]) return;
|
|
388
|
+
seen[id] = true;
|
|
389
|
+
ordered.push(id);
|
|
390
|
+
}
|
|
391
|
+
for (var si = 0; si < statements.length; si++) {
|
|
392
|
+
var s = statements[si] || {};
|
|
393
|
+
if (s.product_status && typeof s.product_status === "object") {
|
|
394
|
+
var statusKeys = Object.keys(s.product_status);
|
|
395
|
+
for (var ki = 0; ki < statusKeys.length; ki++) {
|
|
396
|
+
var arr = s.product_status[statusKeys[ki]];
|
|
397
|
+
if (Array.isArray(arr)) {
|
|
398
|
+
for (var ai = 0; ai < arr.length; ai++) _add(arr[ai]);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (Array.isArray(s.flags)) {
|
|
403
|
+
for (var fi = 0; fi < s.flags.length; fi++) {
|
|
404
|
+
var fl = s.flags[fi];
|
|
405
|
+
if (fl && Array.isArray(fl.product_ids)) {
|
|
406
|
+
for (var fai = 0; fai < fl.product_ids.length; fai++) _add(fl.product_ids[fai]);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Operator may supply product display-names via productTreeNames
|
|
412
|
+
// ({ "<productId>": "<display name>" }). When omitted, the
|
|
413
|
+
// product_id doubles as the display name so the emitted
|
|
414
|
+
// full_product_names entry resolves consistently.
|
|
415
|
+
var fpn = [];
|
|
416
|
+
for (var oi = 0; oi < ordered.length; oi++) {
|
|
417
|
+
var pid = ordered[oi];
|
|
418
|
+
var displayName = (productTreeNames && typeof productTreeNames[pid] === "string" &&
|
|
419
|
+
productTreeNames[pid].length > 0)
|
|
420
|
+
? productTreeNames[pid]
|
|
421
|
+
: pid;
|
|
422
|
+
fpn.push({ product_id: pid, name: displayName });
|
|
423
|
+
}
|
|
424
|
+
return fpn;
|
|
425
|
+
}
|
|
426
|
+
|
|
228
427
|
/**
|
|
229
428
|
* @primitive b.vex.document
|
|
230
429
|
* @signature b.vex.document(opts)
|
|
@@ -233,13 +432,23 @@ function statement(opts) {
|
|
|
233
432
|
* @related b.vex.statement, b.vex.serialize
|
|
234
433
|
*
|
|
235
434
|
* Assemble a complete CSAF 2.1 VEX document with the supplied
|
|
236
|
-
* vulnerability statements + framework distributor metadata.
|
|
435
|
+
* vulnerability statements + framework distributor metadata. The
|
|
436
|
+
* document auto-emits `product_tree.full_product_names` resolving
|
|
437
|
+
* every `product_ids` reference used by the statements (CSAF 2.1
|
|
438
|
+
* §3.1) so the document is self-contained — spec-conformant VEX
|
|
439
|
+
* validators (csaf-validator-service, Dependency-Track) require
|
|
440
|
+
* every `product_ids` reference to resolve against the document's
|
|
441
|
+
* own product_tree.
|
|
237
442
|
*
|
|
238
443
|
* @opts
|
|
239
444
|
* documentId: string, // unique per-publication id (e.g. "blamejs-vex-2026-05-12")
|
|
240
445
|
* title: string, // document title
|
|
241
446
|
* publisher: { name, namespace, contactDetails? },
|
|
242
447
|
* tlp: string, // one of TLP_LABELS; default "CLEAR"
|
|
448
|
+
* distributionText: string, // overrides TLP_DEFAULT_TEXTS[tlp]; required when TLP RED or AMBER+STRICT and operator wants non-default prose
|
|
449
|
+
* lang: string, // BCP 47 language tag (CSAF §3.2.1.13); default "en"
|
|
450
|
+
* trackingStatus: string, // CSAF §3.2.1.6 tracking.status — "draft" | "interim" | "final"; default "final"
|
|
451
|
+
* productTreeNames: object, // optional { "<productId>": "<display name>" } — when omitted, productId doubles as display name
|
|
243
452
|
* statements: object[], // array of b.vex.statement output
|
|
244
453
|
* distributor: { ... }, // optional CSAF distributor block
|
|
245
454
|
* trackingId: string, // CSAF tracking id (e.g. version-pinned)
|
|
@@ -287,15 +496,63 @@ function document(opts) {
|
|
|
287
496
|
throw new VexError("vex/bad-tlp",
|
|
288
497
|
"document: tlp must be one of " + TLP_LABELS.join(" / "));
|
|
289
498
|
}
|
|
499
|
+
// CSAF 2.1 §3.2.1.13 — document.lang is a BCP 47 (RFC 5646)
|
|
500
|
+
// language tag. The spec marks it optional but downstream
|
|
501
|
+
// localisation pipelines + csaf-validator-service emit a warning
|
|
502
|
+
// when absent; default to "en" so framework-issued docs always
|
|
503
|
+
// carry the field. Operators publishing in other languages set
|
|
504
|
+
// explicitly.
|
|
505
|
+
var lang = "en";
|
|
506
|
+
if (opts.lang !== undefined) {
|
|
507
|
+
if (typeof opts.lang !== "string" || !/^[A-Za-z]{2,3}(-[A-Za-z0-9-]+)?$/.test(opts.lang)) {
|
|
508
|
+
throw new VexError("vex/bad-lang",
|
|
509
|
+
"document: lang must be a BCP 47 language tag (RFC 5646; e.g. 'en' / 'en-US' / 'de-DE')");
|
|
510
|
+
}
|
|
511
|
+
lang = opts.lang;
|
|
512
|
+
}
|
|
513
|
+
// CSAF 2.1 §3.2.1.6 — document.tracking.status enumeration. Default
|
|
514
|
+
// "final" matches the previously-hardcoded value; operators
|
|
515
|
+
// shipping in-progress disclosures set explicitly to "draft" /
|
|
516
|
+
// "interim". csaf-validator rejects values outside this enum.
|
|
517
|
+
var trackingStatus = "final";
|
|
518
|
+
if (opts.trackingStatus !== undefined) {
|
|
519
|
+
if (typeof opts.trackingStatus !== "string" ||
|
|
520
|
+
TRACKING_STATUS_VALUES.indexOf(opts.trackingStatus) === -1) {
|
|
521
|
+
throw new VexError("vex/bad-tracking-status",
|
|
522
|
+
"document: trackingStatus must be one of " +
|
|
523
|
+
TRACKING_STATUS_VALUES.join(" / ") + " (CSAF §3.2.1.6)");
|
|
524
|
+
}
|
|
525
|
+
trackingStatus = opts.trackingStatus;
|
|
526
|
+
}
|
|
527
|
+
if (opts.productTreeNames !== undefined &&
|
|
528
|
+
(typeof opts.productTreeNames !== "object" || opts.productTreeNames === null ||
|
|
529
|
+
Array.isArray(opts.productTreeNames))) {
|
|
530
|
+
throw new VexError("vex/bad-product-tree-names",
|
|
531
|
+
"document: productTreeNames must be a { <productId>: <displayName> } object");
|
|
532
|
+
}
|
|
533
|
+
validateOpts.optionalNonEmptyString(opts.distributionText, "document.distributionText", VexError, "vex/bad-distribution-text");
|
|
534
|
+
// FIRST TLP 2.0 + CSAF §3.2.1.12.1.1.1 — distribution.text is
|
|
535
|
+
// REQUIRED when TLP label is RED or AMBER+STRICT (the
|
|
536
|
+
// recipient-restricted tiers carry mandatory boilerplate prose).
|
|
537
|
+
// Other tiers benefit from the prose for downstream consumers;
|
|
538
|
+
// emit unconditionally with the operator-overridable default.
|
|
539
|
+
var distributionText = opts.distributionText || TLP_DEFAULT_TEXTS[tlp];
|
|
540
|
+
|
|
541
|
+
// CSAF 2.1 §3.1 — product_tree.full_product_names. Auto-derive
|
|
542
|
+
// from every productId referenced in statements so flags[] +
|
|
543
|
+
// product_status[] entries resolve against a real product node.
|
|
544
|
+
var fullProductNames = _collectProductIds(opts.statements, opts.productTreeNames || null);
|
|
545
|
+
|
|
290
546
|
var doc = {
|
|
291
547
|
document: {
|
|
292
548
|
category: DOCUMENT_CATEGORY_VEX,
|
|
293
549
|
csaf_version: CSAF_VERSION,
|
|
550
|
+
lang: lang,
|
|
294
551
|
title: opts.title,
|
|
295
552
|
tracking: {
|
|
296
553
|
id: opts.trackingId,
|
|
297
554
|
version: opts.trackingVersion,
|
|
298
|
-
status:
|
|
555
|
+
status: trackingStatus,
|
|
299
556
|
initial_release_date: opts.initialReleaseDate,
|
|
300
557
|
current_release_date: opts.currentReleaseDate,
|
|
301
558
|
revision_history: [
|
|
@@ -304,6 +561,7 @@ function document(opts) {
|
|
|
304
561
|
},
|
|
305
562
|
distribution: {
|
|
306
563
|
tlp: { label: tlp },
|
|
564
|
+
text: distributionText,
|
|
307
565
|
},
|
|
308
566
|
publisher: {
|
|
309
567
|
name: opts.publisher.name,
|
|
@@ -311,6 +569,9 @@ function document(opts) {
|
|
|
311
569
|
category: "vendor",
|
|
312
570
|
},
|
|
313
571
|
},
|
|
572
|
+
product_tree: {
|
|
573
|
+
full_product_names: fullProductNames,
|
|
574
|
+
},
|
|
314
575
|
vulnerabilities: opts.statements,
|
|
315
576
|
};
|
|
316
577
|
if (opts.publisher.contactDetails) {
|
|
@@ -354,12 +615,16 @@ function serialize(doc) {
|
|
|
354
615
|
}
|
|
355
616
|
|
|
356
617
|
module.exports = {
|
|
357
|
-
statement:
|
|
358
|
-
document:
|
|
359
|
-
serialize:
|
|
360
|
-
STATUS_VALUES:
|
|
361
|
-
JUSTIFICATION_VALUES:
|
|
362
|
-
TLP_LABELS:
|
|
363
|
-
|
|
364
|
-
|
|
618
|
+
statement: statement,
|
|
619
|
+
document: document,
|
|
620
|
+
serialize: serialize,
|
|
621
|
+
STATUS_VALUES: STATUS_VALUES,
|
|
622
|
+
JUSTIFICATION_VALUES: JUSTIFICATION_VALUES,
|
|
623
|
+
TLP_LABELS: TLP_LABELS,
|
|
624
|
+
TRACKING_STATUS_VALUES: TRACKING_STATUS_VALUES,
|
|
625
|
+
REFERENCE_CATEGORY_VALUES: REFERENCE_CATEGORY_VALUES,
|
|
626
|
+
NOTE_CATEGORY_VALUES: NOTE_CATEGORY_VALUES,
|
|
627
|
+
TLP_DEFAULT_TEXTS: TLP_DEFAULT_TEXTS,
|
|
628
|
+
CSAF_VERSION: CSAF_VERSION,
|
|
629
|
+
VexError: VexError,
|
|
365
630
|
};
|
package/package.json
CHANGED
package/sbom.cdx.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.6",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:c1165275-044f-4a5a-b7dc-061727bfd076",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-16T23:28:39.659Z",
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.10.2",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.
|
|
25
|
+
"version": "0.10.2",
|
|
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.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.10.2",
|
|
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.
|
|
57
|
+
"ref": "@blamejs/core@0.10.2",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|