@blamejs/exceptd-skills 0.19.32 → 0.19.34
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 +22 -0
- package/bin/exceptd.js +896 -2824
- package/data/_indexes/_meta.json +8 -8
- package/data/_indexes/activity-feed.json +2 -2
- package/data/_indexes/catalog-summaries.json +7 -7
- package/data/_indexes/chains.json +60118 -0
- package/data/attack-techniques.json +267 -7
- package/data/cve-catalog.json +9991 -3
- package/data/cwe-catalog.json +109 -2
- package/data/framework-control-gaps.json +578 -3
- package/data/zeroday-lessons.json +8330 -1
- package/lib/auto-discovery.js +56 -286
- package/lib/canonical-eq.js +7 -40
- package/lib/citation-resolve.js +22 -70
- package/lib/collectors/ai-api.js +20 -54
- package/lib/collectors/cicd-pipeline-compromise.js +40 -108
- package/lib/collectors/citation-hygiene.js +72 -210
- package/lib/collectors/containers.js +41 -130
- package/lib/collectors/cred-stores.js +31 -115
- package/lib/collectors/crypto-codebase.js +55 -138
- package/lib/collectors/crypto.js +24 -54
- package/lib/collectors/hardening.js +20 -78
- package/lib/collectors/kernel.js +16 -46
- package/lib/collectors/library-author.js +57 -206
- package/lib/collectors/mcp.js +24 -70
- package/lib/collectors/runtime.js +24 -86
- package/lib/collectors/sbom.js +34 -106
- package/lib/collectors/scan-excludes.js +31 -138
- package/lib/collectors/secrets.js +62 -178
- package/lib/cross-ref-api.js +39 -123
- package/lib/currency-severity.js +8 -27
- package/lib/cve-batch.js +13 -21
- package/lib/cve-cli.js +13 -20
- package/lib/cve-curation.js +72 -239
- package/lib/cve-regression-watcher.js +29 -152
- package/lib/cvss.js +13 -54
- package/lib/doctor-bucketing.js +3 -19
- package/lib/exit-codes.js +10 -42
- package/lib/flag-suggest.js +7 -25
- package/lib/framework-gap.js +35 -114
- package/lib/gap-detectors.js +37 -159
- package/lib/id-validation.js +9 -30
- package/lib/job-queue.js +13 -36
- package/lib/lint-skills.js +64 -232
- package/lib/playbook-runner.js +693 -2095
- package/lib/prefetch.js +100 -376
- package/lib/refresh-external.js +199 -627
- package/lib/refresh-network.js +75 -307
- package/lib/rfc-cli.js +23 -68
- package/lib/scoring.js +77 -145
- package/lib/sign.js +43 -229
- package/lib/source-advisories.js +43 -194
- package/lib/source-ghsa.js +37 -120
- package/lib/source-osv.js +94 -266
- package/lib/ttp-mapper.js +14 -24
- package/lib/upstream-check-cli.js +10 -28
- package/lib/upstream-check.js +19 -44
- package/lib/validate-catalog-meta.js +17 -61
- package/lib/validate-cve-catalog.js +43 -119
- package/lib/validate-indexes.js +25 -76
- package/lib/validate-package.js +16 -62
- package/lib/validate-playbooks.js +69 -275
- package/lib/validate-vendor.js +16 -49
- package/lib/verify.js +56 -286
- package/lib/version-pins.js +5 -34
- package/lib/worker-pool.js +11 -30
- package/lib/xml-tokenizer.js +47 -152
- package/manifest.json +53 -53
- package/orchestrator/dispatcher.js +17 -68
- package/orchestrator/event-bus.js +11 -74
- package/orchestrator/index.js +138 -412
- package/orchestrator/pipeline.js +28 -85
- package/orchestrator/scanner.js +34 -138
- package/orchestrator/scheduler.js +20 -84
- package/package.json +2 -2
- package/sbom.cdx.json +253 -253
- package/scripts/audit-catalog-gaps.js +9 -62
- package/scripts/audit-cross-skill.js +5 -31
- package/scripts/audit-perf.js +6 -16
- package/scripts/backfill-theater-test.js +7 -64
- package/scripts/bootstrap.js +12 -44
- package/scripts/build-indexes.js +40 -154
- package/scripts/builders/activity-feed.js +4 -14
- package/scripts/builders/catalog-summaries.js +3 -10
- package/scripts/builders/currency.js +7 -20
- package/scripts/builders/cwe-chains.js +7 -30
- package/scripts/builders/did-ladders.js +6 -13
- package/scripts/builders/frequency.js +5 -19
- package/scripts/builders/jurisdiction-clocks.js +6 -25
- package/scripts/builders/recipes.js +6 -14
- package/scripts/builders/section-offsets.js +13 -51
- package/scripts/builders/stale-content.js +7 -28
- package/scripts/builders/summary-cards.js +8 -29
- package/scripts/builders/theater-fingerprints.js +12 -27
- package/scripts/builders/token-budget.js +4 -31
- package/scripts/check-agents-md-collectors.js +11 -54
- package/scripts/check-catalog-gap-budget.js +15 -32
- package/scripts/check-changelog-extract.js +18 -48
- package/scripts/check-codebase-patterns-currency.js +6 -22
- package/scripts/check-codebase-patterns.js +50 -143
- package/scripts/check-epss-consistency.js +9 -64
- package/scripts/check-framework-gap-coverage.js +13 -31
- package/scripts/check-manifest-snapshot.js +13 -73
- package/scripts/check-sbom-currency.js +44 -142
- package/scripts/check-test-count.js +15 -52
- package/scripts/check-test-coverage.js +66 -197
- package/scripts/check-test-subjects.js +21 -62
- package/scripts/check-ttp-references.js +14 -38
- package/scripts/check-ttp-upstream.js +8 -40
- package/scripts/check-version-bump.js +9 -61
- package/scripts/check-version-tags.js +20 -121
- package/scripts/predeploy.js +38 -184
- package/scripts/refresh-manifest-snapshot.js +16 -38
- package/scripts/refresh-mitre-atlas.js +3 -8
- package/scripts/refresh-mitre-attack.js +1 -8
- package/scripts/refresh-mitre-d3fend.js +3 -9
- package/scripts/refresh-mitre-ics-attack.js +3 -8
- package/scripts/refresh-reverse-refs.js +27 -94
- package/scripts/refresh-rfc-index.js +2 -10
- package/scripts/refresh-sbom.js +31 -161
- package/scripts/refresh-upstream-catalogs.js +40 -137
- package/scripts/release.js +69 -232
- package/scripts/run-e2e-scenarios.js +24 -71
- package/scripts/sync-manifest-metadata.js +10 -34
- package/scripts/sync-package-description.js +8 -17
- package/scripts/validate-vendor-online.js +13 -44
- package/scripts/verify-shipped-tarball.js +35 -140
package/lib/rfc-cli.js
CHANGED
|
@@ -2,21 +2,14 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* datatracker lookup to disambiguate obsoleted-vs-nonexistent. Resolves an RFC
|
|
9
|
-
* number to its title + status so an agent can confirm a citation (e.g. "is
|
|
10
|
-
* RFC 9404 the Sieve spec?") without the datatracker. Optional --check
|
|
11
|
-
* "<claimed title>" reports whether the claimed title matches.
|
|
5
|
+
* `exceptd rfc <number>` — resolves an RFC number to its title and status:
|
|
6
|
+
* local index, then resolved cache, then one datatracker lookup to separate
|
|
7
|
+
* obsoleted from nonexistent.
|
|
12
8
|
*/
|
|
13
9
|
|
|
14
10
|
const { resolveRfc } = require("./citation-resolve.js");
|
|
15
11
|
|
|
16
|
-
// Stopwords
|
|
17
|
-
// run preceded by one of these in the index title is still a clean match; a run
|
|
18
|
-
// preceded by a CONTENT word (e.g. "datagram" before "transport layer security")
|
|
19
|
-
// is the tail of a more-specific title and must NOT be accepted as a match.
|
|
12
|
+
// Stopwords don't disambiguate one RFC title from another.
|
|
20
13
|
const TITLE_STOPWORDS = new Set(["the", "a", "an", "of", "for", "to", "in", "on", "and", "or"]);
|
|
21
14
|
|
|
22
15
|
function normTitle(s) {
|
|
@@ -24,37 +17,19 @@ function normTitle(s) {
|
|
|
24
17
|
}
|
|
25
18
|
|
|
26
19
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* Replaces the old lenient bidirectional substring test (`a.includes(b) ||
|
|
30
|
-
* b.includes(a)`), which let "TLS" match the DTLS title (substring of "dtls")
|
|
31
|
-
* and let "Transport Layer Security" match the DTLS title (tail-of-phrase).
|
|
32
|
-
* The comparison is now whole-word and phrase-aware:
|
|
33
|
-
*
|
|
34
|
-
* 1. Every claimed token must appear as a WHOLE word in the index title
|
|
35
|
-
* (so "tls" never matches inside "dtls").
|
|
36
|
-
* 2. The claimed token sequence must appear as a CONTIGUOUS run in the index
|
|
37
|
-
* title, OR the claim must cover enough of the index title (containment
|
|
38
|
-
* ratio floor) to be unambiguous.
|
|
39
|
-
* 3. A contiguous run that is immediately preceded by a distinguishing
|
|
40
|
-
* CONTENT word in the index title is rejected — it is the tail of a
|
|
41
|
-
* more-specific title (the "datagram transport layer security" trap).
|
|
42
|
-
*
|
|
43
|
-
* Returns true / false. Only called when both a claim and an index title exist.
|
|
20
|
+
* Whole-word, phrase-aware comparison of a claimed RFC title against the
|
|
21
|
+
* authoritative index title. Called only when both exist.
|
|
44
22
|
*/
|
|
45
23
|
function titleMatches(claimed, indexTitle) {
|
|
46
24
|
const claimTokens = normTitle(claimed).split(" ").filter(Boolean);
|
|
47
25
|
const titleTokens = normTitle(indexTitle).split(" ").filter(Boolean);
|
|
48
26
|
if (claimTokens.length === 0 || titleTokens.length === 0) return false;
|
|
49
27
|
|
|
50
|
-
// (1) Whole-word containment: every claimed token must be a standalone token
|
|
51
|
-
// in the index title. Kills the tls-inside-dtls substring false positive.
|
|
52
28
|
const titleSet = new Set(titleTokens);
|
|
53
29
|
for (const t of claimTokens) {
|
|
54
30
|
if (!titleSet.has(t)) return false;
|
|
55
31
|
}
|
|
56
32
|
|
|
57
|
-
// Find every contiguous run of the claim inside the index title.
|
|
58
33
|
const runStarts = [];
|
|
59
34
|
for (let i = 0; i + claimTokens.length <= titleTokens.length; i++) {
|
|
60
35
|
let hit = true;
|
|
@@ -65,16 +40,12 @@ function titleMatches(claimed, indexTitle) {
|
|
|
65
40
|
}
|
|
66
41
|
|
|
67
42
|
if (runStarts.length > 0) {
|
|
68
|
-
// A single-token claim that
|
|
69
|
-
// its own —
|
|
70
|
-
// (e.g. "tls" is NOT a token inside "dtls"), so "TLS" correctly matches the
|
|
71
|
-
// 8446 title (standalone "tls" token) but not the 9147 DTLS title.
|
|
43
|
+
// A single-token claim that survived the whole-word check is unambiguous on
|
|
44
|
+
// its own — "tls" is not a token inside "dtls".
|
|
72
45
|
if (claimTokens.length === 1) return true;
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
// qualifier (e.g. "datagram" before "transport layer security") is the
|
|
77
|
-
// tail of a more-specific title and must not be accepted as a match.
|
|
46
|
+
// A multi-token run counts only where some occurrence begins the title or
|
|
47
|
+
// is preceded by a stopword. Preceded by a content qualifier ("datagram"
|
|
48
|
+
// before "transport layer security") it is the tail of a different title.
|
|
78
49
|
for (const start of runStarts) {
|
|
79
50
|
if (start === 0) return true;
|
|
80
51
|
const prev = titleTokens[start - 1];
|
|
@@ -83,13 +54,9 @@ function titleMatches(claimed, indexTitle) {
|
|
|
83
54
|
return false;
|
|
84
55
|
}
|
|
85
56
|
|
|
86
|
-
// No contiguous run,
|
|
87
|
-
// claim covers a strong majority of the
|
|
88
|
-
//
|
|
89
|
-
// not a match. Count DISTINCT claim tokens that appear in the title: counting
|
|
90
|
-
// non-distinct tokens lets a repeated-token claim (e.g. "security security
|
|
91
|
-
// security security") inflate the ratio past the floor and falsely match an
|
|
92
|
-
// unrelated title.
|
|
57
|
+
// No contiguous run, all tokens present out of order: accept only when the
|
|
58
|
+
// claim covers a strong majority of the title's tokens. DISTINCT claim tokens
|
|
59
|
+
// are counted, or a repeated-token claim inflates the ratio past the floor.
|
|
93
60
|
const distinct = new Set(claimTokens);
|
|
94
61
|
const present = [...distinct].filter((t) => titleSet.has(t)).length;
|
|
95
62
|
const ratio = present / titleTokens.length;
|
|
@@ -99,9 +66,8 @@ function titleMatches(claimed, indexTitle) {
|
|
|
99
66
|
async function main() {
|
|
100
67
|
const argv = process.argv.slice(2);
|
|
101
68
|
const flags = new Set(argv.filter((a) => a.startsWith("--")));
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
// a flag, so it isn't checked here.
|
|
69
|
+
// Unknown flags are rejected, as in the in-process verbs. `--check` consumes
|
|
70
|
+
// the following token, which is a positional and so isn't checked here.
|
|
105
71
|
const KNOWN = new Set(["--json", "--pretty", "--air-gap", "--no-network", "--check", "--help", "-h"]);
|
|
106
72
|
const unknown = [...flags].filter((f) => !KNOWN.has(f));
|
|
107
73
|
if (unknown.length > 0) {
|
|
@@ -112,10 +78,8 @@ async function main() {
|
|
|
112
78
|
process.exitCode = 1;
|
|
113
79
|
return;
|
|
114
80
|
}
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
// the RFC number resolves correctly regardless of flag order
|
|
118
|
-
// (`rfc --check "Some Title" 9404` reads id=9404, not id="Some Title").
|
|
81
|
+
// The `--check` value token is excluded from the positional pool by INDEX, so
|
|
82
|
+
// `rfc --check "Some Title" 9404` reads id=9404, not id="Some Title".
|
|
119
83
|
const checkIdx = argv.indexOf("--check");
|
|
120
84
|
const checkValueIdx = (checkIdx !== -1 && argv[checkIdx + 1] && !argv[checkIdx + 1].startsWith("--")) ? checkIdx + 1 : -1;
|
|
121
85
|
const positionals = argv.filter((a, i) => !a.startsWith("--") && i !== checkValueIdx);
|
|
@@ -123,9 +87,7 @@ async function main() {
|
|
|
123
87
|
const pretty = flags.has("--pretty");
|
|
124
88
|
const json = flags.has("--json") || pretty;
|
|
125
89
|
|
|
126
|
-
//
|
|
127
|
-
// checkValueIdx so the two never diverge); a trailing `--check` with no value
|
|
128
|
-
// leaves it null.
|
|
90
|
+
// Exactly the excluded value token; a trailing `--check` with no value is null.
|
|
129
91
|
let claimedTitle = null;
|
|
130
92
|
if (checkValueIdx !== -1) claimedTitle = argv[checkValueIdx];
|
|
131
93
|
|
|
@@ -143,10 +105,7 @@ async function main() {
|
|
|
143
105
|
if (claimedTitle && r.title) {
|
|
144
106
|
titleMatch = titleMatches(claimedTitle, r.title);
|
|
145
107
|
}
|
|
146
|
-
//
|
|
147
|
-
// code is derived below — a non-zero exit (status nonexistent OR an explicit
|
|
148
|
-
// title mismatch) must carry ok:false, not the inverted ok:true the envelope
|
|
149
|
-
// previously hardcoded.
|
|
108
|
+
// `ok` derives from the same condition as the exit code below.
|
|
150
109
|
const fails = r.status === "nonexistent" || titleMatch === false;
|
|
151
110
|
const body = { verb: "rfc", ...r, ...(claimedTitle ? { claimed_title: claimedTitle, title_match: titleMatch } : {}), ok: !fails };
|
|
152
111
|
|
|
@@ -171,15 +130,11 @@ async function main() {
|
|
|
171
130
|
if (fails) process.exitCode = 2;
|
|
172
131
|
}
|
|
173
132
|
|
|
174
|
-
//
|
|
175
|
-
// test the IIFE must not fire — it would read process.argv and write to stdout —
|
|
176
|
-
// so the pure title-match helper can be exercised in-process.
|
|
133
|
+
// Under require it would read process.argv and write stdout; keep it testable.
|
|
177
134
|
if (require.main === module) {
|
|
178
135
|
main().catch((err) => {
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
// envelope rather than crashing with a raw stack trace, and signal failure
|
|
182
|
-
// via exitCode so the event loop drains stderr before exit.
|
|
136
|
+
// Any unexpected throw becomes the {ok:false,error} envelope, not a raw
|
|
137
|
+
// stack; `process.exitCode`, not `process.exit()`, so stderr drains.
|
|
183
138
|
process.stderr.write(JSON.stringify({ ok: false, verb: "rfc", error: String((err && err.message) || err) }) + "\n");
|
|
184
139
|
process.exitCode = 1;
|
|
185
140
|
});
|
package/lib/scoring.js
CHANGED
|
@@ -4,17 +4,13 @@
|
|
|
4
4
|
* RWEP — Real-World Exploit Priority scoring engine. Supplements CVSS with
|
|
5
5
|
* exploit availability, active exploitation and operational constraints.
|
|
6
6
|
*
|
|
7
|
-
* `rwep_factors`
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* also 30. Do not feed booleans in here; `scoreCustom()` takes those, plus a
|
|
11
|
-
* numeric blast_radius and a ladder string. `deriveRwepFromFactors()` detects
|
|
12
|
-
* which shape it was given and routes accordingly.
|
|
7
|
+
* In a stored `rwep_factors` block every factor is its POST-WEIGHT contribution
|
|
8
|
+
* except `blast_radius`, a RAW 0..30 magnitude; the object sums to `rwep_score`
|
|
9
|
+
* only because the blast weight is also 30.
|
|
13
10
|
*/
|
|
14
11
|
|
|
15
12
|
// Loaded from the schema so the two cannot drift. live_patch_tools is
|
|
16
|
-
//
|
|
17
|
-
// live_patch_available => live_patch_tools implication is enforced below.
|
|
13
|
+
// schema-optional — validate() enforces live_patch_available => live_patch_tools.
|
|
18
14
|
const CVE_SCHEMA_REQUIRED = require('./schemas/cve-catalog.schema.json').required;
|
|
19
15
|
|
|
20
16
|
// reboot_required applies even when a live patch exists, because a live patch
|
|
@@ -30,12 +26,9 @@ const RWEP_WEIGHTS = {
|
|
|
30
26
|
reboot_required: 5
|
|
31
27
|
};
|
|
32
28
|
|
|
33
|
-
// Must stay aligned with playbook-runner's _activeExploitationLadder
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
// 'theoretical' is mapped explicitly at 0 — a published PoC carries its weight
|
|
37
|
-
// through poc_available, and an incidental `?? 0` fall-through here would be
|
|
38
|
-
// indistinguishable from an unrecognised value.
|
|
29
|
+
// Must stay aligned with playbook-runner's _activeExploitationLadder. 'unknown'
|
|
30
|
+
// scores a quarter — an untriaged CVE is not a clean one; 'theoretical' is
|
|
31
|
+
// mapped explicitly so it stays distinguishable from an unrecognised value.
|
|
39
32
|
const ACTIVE_EXPLOITATION_LADDER = {
|
|
40
33
|
confirmed: 1.0,
|
|
41
34
|
suspected: 0.5,
|
|
@@ -45,15 +38,10 @@ const ACTIVE_EXPLOITATION_LADDER = {
|
|
|
45
38
|
};
|
|
46
39
|
|
|
47
40
|
/**
|
|
48
|
-
* Ladder multiplier
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* null and undefined are the documented 'none' default and count as recognised.
|
|
53
|
-
* Anything else returns multiplier 0 AND emits a process warning: an
|
|
54
|
-
* out-of-vocabulary string would otherwise drop 20 points silently, and the
|
|
55
|
-
* no-match path has to be observable. validateFactors() carries the structured
|
|
56
|
-
* diagnostic for callers that collect warnings.
|
|
41
|
+
* Ladder multiplier as { multiplier, recognised, normalised }. Case- and
|
|
42
|
+
* whitespace-normalised; null and undefined are the 'none' default and count as
|
|
43
|
+
* recognised. Anything else returns multiplier 0 with recognised false, so an
|
|
44
|
+
* out-of-vocabulary string cannot drop 20 points silently.
|
|
57
45
|
*/
|
|
58
46
|
function resolveActiveExploitation(active_exploitation) {
|
|
59
47
|
if (active_exploitation === undefined || active_exploitation === null) {
|
|
@@ -72,11 +60,8 @@ function resolveActiveExploitation(active_exploitation) {
|
|
|
72
60
|
function activeExploitationMultiplier(active_exploitation) {
|
|
73
61
|
const r = resolveActiveExploitation(active_exploitation);
|
|
74
62
|
if (!r.recognised) {
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
// process.emitWarning so it lands on the standard Node diagnostic channel
|
|
78
|
-
// without changing the function's number return contract; deduped per
|
|
79
|
-
// distinct offending value so a batch curation run doesn't flood stderr.
|
|
63
|
+
// Routed through process.emitWarning so the bare-number return contract is
|
|
64
|
+
// unchanged; validateFactors() carries the structured form.
|
|
80
65
|
const detail = active_exploitation === undefined || active_exploitation === null
|
|
81
66
|
? String(active_exploitation)
|
|
82
67
|
: (typeof active_exploitation === 'string' ? JSON.stringify(active_exploitation) : `${typeof active_exploitation} ${JSON.stringify(active_exploitation)}`);
|
|
@@ -89,8 +74,7 @@ function activeExploitationMultiplier(active_exploitation) {
|
|
|
89
74
|
}
|
|
90
75
|
|
|
91
76
|
// Boolean-input (Shape-A) keys scoreCustom recognises; validateFactors flags
|
|
92
|
-
// anything else. The last two are
|
|
93
|
-
// aliases so a factor bag built straight from an entry validates.
|
|
77
|
+
// anything else. The last two are catalog field names accepted as aliases.
|
|
94
78
|
const RECOGNISED_FACTOR_KEYS = new Set([
|
|
95
79
|
'cisa_kev', 'poc_available', 'ai_assisted_weapon', 'ai_discovered',
|
|
96
80
|
'active_exploitation', 'blast_radius', 'patch_available',
|
|
@@ -99,11 +83,9 @@ const RECOGNISED_FACTOR_KEYS = new Set([
|
|
|
99
83
|
'patch_required_reboot',
|
|
100
84
|
]);
|
|
101
85
|
|
|
102
|
-
// Post-weight (Shape-B) keys deriveRwepFromFactors may sum. `ai_factor`
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
// it from every derivation. A key outside this set is excluded from the sum and
|
|
106
|
-
// surfaced, so a typo cannot quietly change a score.
|
|
86
|
+
// Post-weight (Shape-B) keys deriveRwepFromFactors may sum. `ai_factor` is added
|
|
87
|
+
// back because it is absent from the Shape-A set above. A key outside this set is
|
|
88
|
+
// excluded from the sum and surfaced, so a typo cannot quietly change a score.
|
|
107
89
|
const RECOGNISED_POST_WEIGHT_KEYS = new Set([...RECOGNISED_FACTOR_KEYS, 'ai_factor']);
|
|
108
90
|
|
|
109
91
|
function score(cveId, catalog) {
|
|
@@ -115,9 +97,7 @@ function score(cveId, catalog) {
|
|
|
115
97
|
/**
|
|
116
98
|
* Warnings for a factor bag: missing-but-defaultable fields and out-of-range
|
|
117
99
|
* values. Never throws — a caller wanting enforcement treats a non-empty return
|
|
118
|
-
* as a failure.
|
|
119
|
-
* active_exploitation must be a ladder value; blast_radius is an integer in
|
|
120
|
-
* [0, 30], flagged out of range because that usually means a unit error.
|
|
100
|
+
* as a failure.
|
|
121
101
|
*/
|
|
122
102
|
function validateFactors(factors) {
|
|
123
103
|
const warnings = [];
|
|
@@ -127,8 +107,8 @@ function validateFactors(factors) {
|
|
|
127
107
|
const boolFields = ['cisa_kev', 'poc_available', 'ai_assisted_weapon', 'ai_discovered',
|
|
128
108
|
'patch_available', 'live_patch_available', 'reboot_required'];
|
|
129
109
|
for (const f of boolFields) {
|
|
130
|
-
//
|
|
131
|
-
//
|
|
110
|
+
// Same aliasing as scoreCustom, so a bag supplying only the catalog field
|
|
111
|
+
// name is not flagged missing.
|
|
132
112
|
const present = (f === 'ai_assisted_weapon')
|
|
133
113
|
? (factors.ai_assisted_weapon ?? factors.ai_assisted_weaponization)
|
|
134
114
|
: (f === 'reboot_required')
|
|
@@ -146,20 +126,18 @@ function validateFactors(factors) {
|
|
|
146
126
|
warnings.push("active_exploitation: missing (treated as 'none')");
|
|
147
127
|
} else {
|
|
148
128
|
// Normalised before the vocabulary check so this accepts exactly what the
|
|
149
|
-
// scorer accepts
|
|
129
|
+
// scorer accepts — otherwise 'Confirmed' is flagged here and taken there.
|
|
150
130
|
const aeNorm = typeof aeRaw === 'string' ? aeRaw.trim().toLowerCase() : aeRaw;
|
|
151
131
|
if (!aeAllowed.includes(aeNorm)) {
|
|
152
132
|
warnings.push(`active_exploitation: expected one of ${aeAllowed.join(', ')}, got ${JSON.stringify(aeRaw)}`);
|
|
153
133
|
}
|
|
154
134
|
}
|
|
155
|
-
// Number.isFinite rather than
|
|
156
|
-
// `JSON.stringify(NaN) === 'null'
|
|
157
|
-
// "expected number, got number (null)".
|
|
135
|
+
// Number.isFinite rather than typeof: `typeof NaN === 'number'` and
|
|
136
|
+
// `JSON.stringify(NaN) === 'null'` produce "expected number, got number (null)".
|
|
158
137
|
if (factors.blast_radius === undefined || factors.blast_radius === null) {
|
|
159
138
|
warnings.push('blast_radius: missing (treated as 0)');
|
|
160
139
|
} else if (typeof factors.blast_radius !== 'number') {
|
|
161
|
-
// scoreCustom coerces a numeric string
|
|
162
|
-
// noted rather than rejected — the scorer will use it either way.
|
|
140
|
+
// scoreCustom coerces a numeric string, so a finite one is noted, not rejected.
|
|
163
141
|
if (typeof factors.blast_radius === 'string' && Number.isFinite(Number(factors.blast_radius)) && factors.blast_radius.trim() !== '') {
|
|
164
142
|
warnings.push(`blast_radius: numeric string "${factors.blast_radius}" accepted (coerced to ${Number(factors.blast_radius)}); prefer a JSON number`);
|
|
165
143
|
} else {
|
|
@@ -172,8 +150,8 @@ function validateFactors(factors) {
|
|
|
172
150
|
} else if (factors.blast_radius < 0 || factors.blast_radius > 30) {
|
|
173
151
|
warnings.push(`blast_radius: ${factors.blast_radius} out of expected range [0, 30] (clamped to weight ceiling, but the value usually indicates a unit-of-measure mistake)`);
|
|
174
152
|
}
|
|
175
|
-
// An unknown key is surfaced
|
|
176
|
-
//
|
|
153
|
+
// An unknown key is surfaced, not ignored: `patch_avilable` would otherwise
|
|
154
|
+
// default to false with no diagnostic.
|
|
177
155
|
for (const k of Object.keys(factors)) {
|
|
178
156
|
if (!RECOGNISED_FACTOR_KEYS.has(k)) {
|
|
179
157
|
warnings.push(`unknown factor: ${k} (ignored — not in the recognised key set)`);
|
|
@@ -183,19 +161,16 @@ function validateFactors(factors) {
|
|
|
183
161
|
}
|
|
184
162
|
|
|
185
163
|
/**
|
|
186
|
-
* RWEP for a factor bag, clamped to [0, 100].
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* `opts.collectWarnings` it returns `{ score, _scoring_warnings }` instead;
|
|
190
|
-
* for validation without a score, call `validateFactors()` directly.
|
|
164
|
+
* RWEP for a factor bag, clamped to [0, 100]. Returns a bare number, which
|
|
165
|
+
* callers depend on; with `opts.collectWarnings` it returns
|
|
166
|
+
* `{ score, _scoring_warnings }` instead.
|
|
191
167
|
*/
|
|
192
168
|
function scoreCustom(factors, opts) {
|
|
193
169
|
const {
|
|
194
170
|
cisa_kev = false,
|
|
195
171
|
poc_available = false,
|
|
196
172
|
ai_assisted_weapon = false,
|
|
197
|
-
// Catalog field name,
|
|
198
|
-
// counts the +15 AI factor.
|
|
173
|
+
// Catalog field name, aliased so an entry-derived bag still counts the +15.
|
|
199
174
|
ai_assisted_weaponization = false,
|
|
200
175
|
ai_discovered = false,
|
|
201
176
|
active_exploitation = 'none',
|
|
@@ -203,8 +178,7 @@ function scoreCustom(factors, opts) {
|
|
|
203
178
|
patch_available = false,
|
|
204
179
|
live_patch_available = false,
|
|
205
180
|
reboot_required = false,
|
|
206
|
-
// Likewise
|
|
207
|
-
// spellings are accepted and a direct caller cannot lose the factor.
|
|
181
|
+
// Likewise the catalog spelling; both are accepted so no caller loses it.
|
|
208
182
|
patch_required_reboot,
|
|
209
183
|
} = factors || {};
|
|
210
184
|
const rebootFactor = (reboot_required === true) || (patch_required_reboot === true);
|
|
@@ -217,11 +191,8 @@ function scoreCustom(factors, opts) {
|
|
|
217
191
|
// odd number would truncate under a `Math.floor(weight/2)` split.
|
|
218
192
|
const aeMultiplier = activeExploitationMultiplier(active_exploitation);
|
|
219
193
|
score += RWEP_WEIGHTS.active_exploitation * aeMultiplier;
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
// 1 and `[7]` into 7 — values the validator rejects — so the scorer would add
|
|
223
|
-
// a contribution the validator calls invalid. NaN also has
|
|
224
|
-
// `typeof === 'number'` and propagates through the final clamp.
|
|
194
|
+
// Only a finite number or a trimmed non-empty numeric string, matching
|
|
195
|
+
// validateFactors. Bare Number() turns `true` into 1 and `[7]` into 7.
|
|
225
196
|
let brRaw = 0;
|
|
226
197
|
if (typeof blast_radius === 'number' && Number.isFinite(blast_radius)) {
|
|
227
198
|
brRaw = blast_radius;
|
|
@@ -254,23 +225,17 @@ function scoreCustom(factors, opts) {
|
|
|
254
225
|
}
|
|
255
226
|
|
|
256
227
|
/**
|
|
257
|
-
* RWEP from a `rwep_factors` object of either shape,
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
* Shape A (booleans, a ladder string, a numeric blast_radius) routes through
|
|
261
|
-
* scoreCustom. Shape B (post-weight contributions, how the catalog stores them)
|
|
262
|
-
* is summed and clamped to [0, 100].
|
|
228
|
+
* RWEP from a `rwep_factors` object of either shape. Shape A (booleans, a ladder
|
|
229
|
+
* string, a numeric blast_radius) routes through scoreCustom; Shape B
|
|
230
|
+
* (post-weight contributions, how the catalog stores them) is summed and clamped.
|
|
263
231
|
*/
|
|
264
232
|
function deriveRwepFromFactors(factors) {
|
|
265
233
|
if (!factors || typeof factors !== 'object') return 0;
|
|
266
234
|
const entries = Object.entries(factors);
|
|
267
235
|
if (entries.length === 0) return 0;
|
|
268
|
-
// A boolean
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
// disambiguates, not excluding active_exploitation from this check. Excluding
|
|
272
|
-
// it here under-scores a ladder-only bag, which falls through to the sum and
|
|
273
|
-
// skips the string entirely.
|
|
236
|
+
// A boolean or a ladder string is Shape-A evidence. The ladder string is legal
|
|
237
|
+
// in both shapes, so hasPostWeightInt below disambiguates; excluding it here
|
|
238
|
+
// sends a ladder-only bag to the sum, which skips the string entirely.
|
|
274
239
|
const aeAllowed = new Set(['none', 'unknown', 'suspected', 'theoretical', 'confirmed']);
|
|
275
240
|
const hasBooleanOrLadder = entries.some(
|
|
276
241
|
([, v]) => (typeof v === 'boolean' || (typeof v === 'string' && aeAllowed.has(v.trim().toLowerCase()))),
|
|
@@ -283,18 +248,14 @@ function deriveRwepFromFactors(factors) {
|
|
|
283
248
|
if (hasBooleanOrLadder && !hasPostWeightInt) {
|
|
284
249
|
return scoreCustom(factors);
|
|
285
250
|
}
|
|
286
|
-
// Shape B: sum and clamp. blast_radius
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
// make the two scorers disagree, which would show up as a false divergence in
|
|
290
|
-
// validate()'s recompute-vs-stored gate.
|
|
251
|
+
// Shape B: sum and clamp. blast_radius needs a per-factor clamp first — a unit
|
|
252
|
+
// error like 300 would otherwise reach only the aggregate clamp and make the
|
|
253
|
+
// two scorers disagree in validate()'s recompute-vs-stored gate.
|
|
291
254
|
let sum = 0;
|
|
292
255
|
for (const [k, v] of Object.entries(factors)) {
|
|
293
256
|
if (typeof v !== 'number' || !Number.isFinite(v)) continue;
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
// typo means. Summing it blindly let a sub-5 typo inflate the breakdown
|
|
297
|
-
// with no diagnostic.
|
|
257
|
+
// Excluded AND surfaced, as in validateFactors, so a sub-5 typo cannot
|
|
258
|
+
// inflate the sum with no diagnostic.
|
|
298
259
|
if (!RECOGNISED_POST_WEIGHT_KEYS.has(k)) {
|
|
299
260
|
process.emitWarning(
|
|
300
261
|
`rwep_factors carries unrecognised key '${k}'; excluded from the derived sum`,
|
|
@@ -314,9 +275,8 @@ function deriveRwepFromFactors(factors) {
|
|
|
314
275
|
return Math.max(0, Math.min(100, sum));
|
|
315
276
|
}
|
|
316
277
|
|
|
317
|
-
// Post-weight (Shape-B) factor object required by cve-catalog.schema.json
|
|
318
|
-
//
|
|
319
|
-
// so Σ Object.values(...) === scoreCustom(inputs) (pre-clamp) by construction.
|
|
278
|
+
// Post-weight (Shape-B) factor object required by cve-catalog.schema.json:
|
|
279
|
+
// Σ Object.values(...) === scoreCustom(inputs) pre-clamp, by construction.
|
|
320
280
|
function postWeightFactors(inputs) {
|
|
321
281
|
const i = inputs || {};
|
|
322
282
|
const aeMultiplier = activeExploitationMultiplier(i.active_exploitation);
|
|
@@ -351,8 +311,7 @@ function compare(cveId, catalog, opts) {
|
|
|
351
311
|
if (!entry) throw new Error(`CVE not in catalog: ${cveId}`);
|
|
352
312
|
|
|
353
313
|
// `recompute` ignores the stored score and re-derives from rwep_factors,
|
|
354
|
-
// which is how catalog drift against current weights is caught.
|
|
355
|
-
// through the shape detector so hand-edited factors in either shape work.
|
|
314
|
+
// which is how catalog drift against the current weights is caught.
|
|
356
315
|
const recompute = !!(opts && opts.recompute);
|
|
357
316
|
let rwep;
|
|
358
317
|
if (recompute) {
|
|
@@ -362,8 +321,7 @@ function compare(cveId, catalog, opts) {
|
|
|
362
321
|
rwep = entry.rwep_score;
|
|
363
322
|
}
|
|
364
323
|
// Absent or non-finite CVSS must not reach `cvss * 10`: NaN fails every band
|
|
365
|
-
//
|
|
366
|
-
// computed. Absent CVSS is not-comparable, not zero.
|
|
324
|
+
// and falls through to "broadly aligned". Absent CVSS is not-comparable, not zero.
|
|
367
325
|
const cvss = (typeof entry.cvss_score === 'number' && Number.isFinite(entry.cvss_score))
|
|
368
326
|
? entry.cvss_score
|
|
369
327
|
: null;
|
|
@@ -375,9 +333,7 @@ function compare(cveId, catalog, opts) {
|
|
|
375
333
|
const delta = (cvssAbsent || !rwepValid) ? null : rwep - cvssEquivalent;
|
|
376
334
|
|
|
377
335
|
// The "broadly aligned" band is ±10, the tightest that still reads ordinary
|
|
378
|
-
// CVSS rounding as alignment
|
|
379
|
-
// exactly the case where the CVSS-calibrated SLA is the thing at issue.
|
|
380
|
-
// A zero-vs-zero entry reports no scoring signal rather than alignment.
|
|
336
|
+
// CVSS rounding as alignment; ±20 swallows a delta of 12.
|
|
381
337
|
let explanation = '';
|
|
382
338
|
if (!rwepValid) {
|
|
383
339
|
explanation = 'RWEP score absent or non-numeric for this CVE — no usable RWEP signal to compare. Backfill rwep_score / rwep_factors in the catalog.';
|
|
@@ -387,29 +343,23 @@ function compare(cveId, catalog, opts) {
|
|
|
387
343
|
explanation = 'CVSS absent — RWEP is the only usable score for this CVE; no CVSS comparison is possible. Backfill cvss_score in the catalog to enable the comparison.';
|
|
388
344
|
} else if (delta > 10) {
|
|
389
345
|
explanation = `RWEP significantly higher than CVSS equivalent. Factors driving delta: `;
|
|
390
|
-
//
|
|
391
|
-
// normalization
|
|
392
|
-
// stray-cased 'Confirmed' or the patch_required_reboot alias shows a raised
|
|
393
|
-
// RWEP with no stated reason.
|
|
346
|
+
// Every factor scoreCustom counts, through the same aliases and
|
|
347
|
+
// normalization, or a raised RWEP is shown with no stated reason.
|
|
394
348
|
const driving = [];
|
|
395
349
|
if (entry.cisa_kev) driving.push('CISA KEV (+25)');
|
|
396
350
|
if (entry.poc_available) driving.push('public PoC (+20)');
|
|
397
351
|
if (entry.ai_discovered || entry.ai_assisted_weaponization) driving.push('AI-discovered (+15 weaponization)');
|
|
398
|
-
// Through the same ladder, so suspected (+10) and unknown (+5) are listed
|
|
399
|
-
// with their real contribution. A confirmed-only test leaves the enumerated
|
|
400
|
-
// factors summing to less than the delta, or to nothing at all.
|
|
352
|
+
// Through the same ladder, so suspected (+10) and unknown (+5) are listed too.
|
|
401
353
|
const ae = resolveActiveExploitation(entry.active_exploitation);
|
|
402
354
|
if (ae.multiplier > 0) {
|
|
403
355
|
driving.push(`${ae.normalised} exploitation (+${Math.round(RWEP_WEIGHTS.active_exploitation * ae.multiplier)})`);
|
|
404
356
|
}
|
|
405
|
-
// blast_radius contributes its raw 0..30 value, so a blast-driven delta
|
|
406
|
-
// needs it listed or the raised RWEP has no stated cause.
|
|
357
|
+
// blast_radius contributes its raw 0..30 value, so a blast-driven delta needs it.
|
|
407
358
|
const blastRaw = Number((entry.rwep_factors || {}).blast_radius);
|
|
408
359
|
const blast = Number.isFinite(blastRaw) ? Math.max(0, Math.min(RWEP_WEIGHTS.blast_radius, blastRaw)) : 0;
|
|
409
360
|
if (blast > 0) driving.push(`blast radius (+${Math.round(blast)})`);
|
|
410
|
-
// Ungated by live_patch_available, mirroring scoreCustom's rebootFactor
|
|
411
|
-
//
|
|
412
|
-
// needs a reboot and has a live patch.
|
|
361
|
+
// Ungated by live_patch_available, mirroring scoreCustom's rebootFactor:
|
|
362
|
+
// gating here hides a driver the score counted.
|
|
413
363
|
if (entry.reboot_required || entry.patch_required_reboot) driving.push('reboot required (+5)');
|
|
414
364
|
// Names the structural cause rather than trailing off after "driving delta:".
|
|
415
365
|
explanation += driving.length ? driving.join(', ') : 'blast magnitude / structural RWEP factors';
|
|
@@ -443,17 +393,14 @@ function compare(cveId, catalog, opts) {
|
|
|
443
393
|
}
|
|
444
394
|
|
|
445
395
|
/**
|
|
446
|
-
* Which shape an `rwep_factors` block uses: 'A' raw, 'B' post-weight,
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
* `{ cisa_kev: true, blast_radius: 30 }` sums to 30 when the intended score is
|
|
451
|
-
* 55, because a raw boolean contributes nothing to a post-weight sum.
|
|
396
|
+
* Which shape an `rwep_factors` block uses: 'A' raw, 'B' post-weight, 'unknown'
|
|
397
|
+
* for empty or ambiguous blocks, 'mixed' for the violating case. Mixing them
|
|
398
|
+
* breaks the sum invariant silently: `{ cisa_kev: true, blast_radius: 30 }` sums
|
|
399
|
+
* to 30 where the intended score is 55.
|
|
452
400
|
*/
|
|
453
401
|
function detectFactorShape(factors) {
|
|
454
402
|
if (!factors || typeof factors !== 'object') return 'unknown';
|
|
455
|
-
// Both spellings per factor, so a post-weight integer on either
|
|
456
|
-
// registers as Shape-B evidence rather than slipping past the detector.
|
|
403
|
+
// Both spellings per factor, so a post-weight integer on either registers as Shape-B.
|
|
457
404
|
const boolFields = ['cisa_kev', 'poc_available', 'ai_assisted_weaponization', 'ai_discovered', 'ai_factor', 'active_exploitation', 'patch_available', 'live_patch_available', 'patch_required_reboot', 'reboot_required'];
|
|
458
405
|
let sawBool = false;
|
|
459
406
|
let sawWeightedInt = false;
|
|
@@ -461,8 +408,7 @@ function detectFactorShape(factors) {
|
|
|
461
408
|
if (k === 'blast_radius') continue; // always integer in both shapes
|
|
462
409
|
if (k === 'active_exploitation' && typeof v === 'string') {
|
|
463
410
|
// Valid in both shapes, so it is not Shape-A evidence: counting it would
|
|
464
|
-
// return 'mixed' on a clean Shape-B block.
|
|
465
|
-
// post-weight path, not here.
|
|
411
|
+
// return 'mixed' on a clean Shape-B block.
|
|
466
412
|
continue;
|
|
467
413
|
}
|
|
468
414
|
if (typeof v === 'boolean' || v === null) {
|
|
@@ -486,10 +432,8 @@ function validate(catalog) {
|
|
|
486
432
|
for (const [cveId, entry] of Object.entries(catalog)) {
|
|
487
433
|
if (cveId.startsWith('_')) continue;
|
|
488
434
|
// Drafts carry a conservative-default rwep_score beside null-until-curated
|
|
489
|
-
// factor fields, so the divergence check below
|
|
490
|
-
//
|
|
491
|
-
// the validator's draft tier instead; clearing `_auto_imported` at curation
|
|
492
|
-
// restores full validation.
|
|
435
|
+
// factor fields, so the divergence check below fires on every one. They are
|
|
436
|
+
// reviewed through `_auto_imported_meta.curation_needed` instead.
|
|
493
437
|
if (entry && entry._auto_imported === true) continue;
|
|
494
438
|
for (const field of CVE_SCHEMA_REQUIRED) {
|
|
495
439
|
if (!(field in entry)) {
|
|
@@ -506,10 +450,9 @@ function validate(catalog) {
|
|
|
506
450
|
if (shape === 'mixed') {
|
|
507
451
|
errors.push(`${cveId}: rwep_factors mixes Shape A (booleans) with Shape B (post-weight integers) — sum invariant cannot hold. Convert factors to a single shape.`);
|
|
508
452
|
}
|
|
509
|
-
// Per-factor coherence: every Shape-B contribution must equal the weight
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
// judgment-set factor with no deriving field.
|
|
453
|
+
// Per-factor coherence: every Shape-B contribution must equal the weight its
|
|
454
|
+
// source field implies, because two compensating errors cancel inside the ±5
|
|
455
|
+
// aggregate tolerance below. blast_radius is exempt — no deriving field.
|
|
513
456
|
if (shape === 'B') {
|
|
514
457
|
const f = entry.rwep_factors;
|
|
515
458
|
const aeMultiplier = resolveActiveExploitation(entry.active_exploitation).multiplier;
|
|
@@ -526,8 +469,8 @@ function validate(catalog) {
|
|
|
526
469
|
errors.push(`${cveId}: rwep_factors.${k} is ${f[k]} but the entry's source fields imply ${want}`);
|
|
527
470
|
}
|
|
528
471
|
}
|
|
529
|
-
// One implied weight, two accepted spellings
|
|
530
|
-
// contradictory value
|
|
472
|
+
// One implied weight, two accepted spellings: both are checked, or a
|
|
473
|
+
// contradictory value under the alias passes the coherence gate.
|
|
531
474
|
const rebootWant = entry.patch_required_reboot === true ? RWEP_WEIGHTS.reboot_required : 0;
|
|
532
475
|
for (const rebootKey of ['reboot_required', 'patch_required_reboot']) {
|
|
533
476
|
if (rebootKey in f && typeof f[rebootKey] === 'number' && f[rebootKey] !== rebootWant) {
|
|
@@ -544,8 +487,7 @@ function validate(catalog) {
|
|
|
544
487
|
blast_radius: entry.rwep_factors ? entry.rwep_factors.blast_radius : 0,
|
|
545
488
|
patch_available: entry.patch_available,
|
|
546
489
|
live_patch_available: entry.live_patch_available,
|
|
547
|
-
// Both spellings, mirroring scoreCustom
|
|
548
|
-
// and computes a divergent expected score.
|
|
490
|
+
// Both spellings, mirroring scoreCustom; passing one computes a divergent score.
|
|
549
491
|
reboot_required: entry.reboot_required || entry.patch_required_reboot
|
|
550
492
|
});
|
|
551
493
|
if (Math.abs(calculatedRwep - entry.rwep_score) > 5) {
|
|
@@ -556,15 +498,10 @@ function validate(catalog) {
|
|
|
556
498
|
}
|
|
557
499
|
|
|
558
500
|
/**
|
|
559
|
-
* Strict CVSS 3.x vector parse, as `{ ok, version, reason? }`.
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
* here. Mandatory metrics in order are AV/AC/PR/UI/S/C/I/A; E/RL/RC and the
|
|
564
|
-
* CR..MA environmental set are optional.
|
|
565
|
-
*
|
|
566
|
-
* 3.0 and 3.1 share one grammar and differ only in the prefix, and CSAF 2.0
|
|
567
|
-
* accepts both, so the version is recorded rather than rejected.
|
|
501
|
+
* Strict CVSS 3.x vector parse, as `{ ok, version, reason? }`. A permissive parse
|
|
502
|
+
* here fails downstream instead: strict CSAF validators reject a document whose
|
|
503
|
+
* cvss_v3 block is keyed off a malformed vector. CSAF 2.0 accepts 3.0 and 3.1
|
|
504
|
+
* alike, so the version is recorded rather than rejected.
|
|
568
505
|
*/
|
|
569
506
|
const CVSS_3X_RE = /^CVSS:3\.[01]\/AV:[NALP]\/AC:[LH]\/PR:[NLH]\/UI:[NR]\/S:[UC]\/C:[NLH]\/I:[NLH]\/A:[NLH](\/E:[XUPFH])?(\/RL:[XOTWU])?(\/RC:[XURC])?(\/CR:[XLMH])?(\/IR:[XLMH])?(\/AR:[XLMH])?(\/MAV:[XNALP])?(\/MAC:[XLH])?(\/MPR:[XNLH])?(\/MUI:[XNR])?(\/MS:[XUC])?(\/MC:[XNLH])?(\/MI:[XNLH])?(\/MA:[XNLH])?$/;
|
|
570
507
|
|
|
@@ -588,16 +525,11 @@ function parseCvss31Vector(v) {
|
|
|
588
525
|
|
|
589
526
|
/**
|
|
590
527
|
* Package-Confidence Score: a supplementary 0-100 supply-chain trust signal
|
|
591
|
-
* shown alongside RWEP, never instead of it.
|
|
592
|
-
*
|
|
593
|
-
* Its polarity is the INVERSE of RWEP — high means trustworthy provenance, low
|
|
594
|
-
* means it behaves like malware — so the two must never be summed or compared.
|
|
595
|
-
* It is deliberately outside the RWEP factor key set and is never called from
|
|
596
|
-
* validate(), scoreCustom() or deriveRwepFromFactors(), so it cannot perturb a
|
|
597
|
-
* stored rwep_score or trip the divergence gate.
|
|
528
|
+
* shown alongside RWEP, never instead of it. Null with no usable input.
|
|
598
529
|
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
530
|
+
* Its polarity is the INVERSE of RWEP — high means trustworthy provenance — so
|
|
531
|
+
* the two are never summed or compared. Equal-weight mean of whichever
|
|
532
|
+
* sub-signals are present; an absent dimension is skipped rather than scored 0.
|
|
601
533
|
*/
|
|
602
534
|
function packageConfidence(inputs) {
|
|
603
535
|
if (!inputs || typeof inputs !== 'object') return null;
|