@blamejs/exceptd-skills 0.16.22 → 0.16.24

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.
Files changed (62) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/CHANGELOG.md +42 -0
  3. package/CONTEXT.md +9 -9
  4. package/README.md +3 -3
  5. package/agents/report-generator.md +2 -2
  6. package/agents/skill-updater.md +1 -1
  7. package/agents/source-validator.md +3 -4
  8. package/agents/threat-researcher.md +1 -1
  9. package/bin/exceptd.js +91 -32
  10. package/data/_indexes/_meta.json +10 -10
  11. package/data/_indexes/activity-feed.json +12 -12
  12. package/data/_indexes/chains.json +70435 -4026
  13. package/data/_indexes/frequency.json +492 -163
  14. package/data/_indexes/section-offsets.json +51 -51
  15. package/data/_indexes/summary-cards.json +272 -106
  16. package/data/_indexes/token-budget.json +10 -10
  17. package/data/_indexes/trigger-table.json +15 -6
  18. package/data/_indexes/xref.json +218 -26
  19. package/data/cve-catalog.json +10 -10
  20. package/data/cwe-catalog.json +1 -0
  21. package/lib/auto-discovery.js +39 -1
  22. package/lib/collectors/ai-api.js +112 -7
  23. package/lib/collectors/citation-hygiene.js +27 -0
  24. package/lib/collectors/crypto-codebase.js +25 -0
  25. package/lib/collectors/kernel.js +32 -2
  26. package/lib/collectors/library-author.js +30 -0
  27. package/lib/collectors/runtime.js +38 -3
  28. package/lib/collectors/sbom.js +21 -2
  29. package/lib/collectors/scan-excludes.js +4 -1
  30. package/lib/collectors/secrets.js +125 -0
  31. package/lib/cve-cli.js +9 -1
  32. package/lib/cve-curation.js +8 -1
  33. package/lib/cve-regression-watcher.js +5 -2
  34. package/lib/exit-codes.js +2 -0
  35. package/lib/flag-suggest.js +1 -1
  36. package/lib/lint-skills.js +70 -0
  37. package/lib/playbook-runner.js +75 -14
  38. package/lib/prefetch.js +24 -1
  39. package/lib/refresh-external.js +32 -3
  40. package/lib/rfc-cli.js +8 -1
  41. package/lib/scoring.js +36 -8
  42. package/lib/validate-cve-catalog.js +36 -14
  43. package/lib/validate-package.js +8 -0
  44. package/lib/validate-playbooks.js +42 -0
  45. package/lib/verify.js +4 -3
  46. package/manifest-snapshot.json +4 -2
  47. package/manifest-snapshot.sha256 +1 -1
  48. package/manifest.json +57 -54
  49. package/orchestrator/README.md +1 -1
  50. package/orchestrator/index.js +65 -7
  51. package/orchestrator/scanner.js +53 -5
  52. package/package.json +1 -1
  53. package/sbom.cdx.json +110 -110
  54. package/scripts/build-indexes.js +42 -8
  55. package/scripts/builders/cwe-chains.js +1 -0
  56. package/scripts/builders/section-offsets.js +10 -2
  57. package/scripts/builders/token-budget.js +3 -3
  58. package/scripts/check-changelog-extract.js +38 -1
  59. package/scripts/check-sbom-currency.js +72 -0
  60. package/scripts/check-version-tags.js +5 -0
  61. package/scripts/release.js +22 -15
  62. package/skills/exploit-scoring/skill.md +8 -8
@@ -3,7 +3,7 @@
3
3
  * scripts/build-indexes.js
4
4
  *
5
5
  * Produces pre-computed indexes under `data/_indexes/` so AI consumers
6
- * and downstream tooling don't have to scan all 38 skills + 10 catalogs
6
+ * and downstream tooling don't have to scan every skill + catalog
7
7
  * to answer routine cross-reference questions.
8
8
  *
9
9
  * Outputs (17 total):
@@ -56,6 +56,7 @@
56
56
  const fs = require("fs");
57
57
  const path = require("path");
58
58
  const crypto = require("crypto");
59
+ const lint = require("../lib/lint-skills.js");
59
60
 
60
61
  const ROOT = path.join(__dirname, "..");
61
62
  const ABS = (p) => path.join(ROOT, p);
@@ -109,15 +110,47 @@ Examples:
109
110
 
110
111
  // --- Source loading (shared in-memory snapshot) -------------------------
111
112
 
113
+ // Cross-reference fields the derived indexes key on. The manifest carries a
114
+ // cache of these, but the skill frontmatter is the authoritative source — the
115
+ // linter and staleness gate read frontmatter. Overlaying the parsed
116
+ // frontmatter onto each skill record here means the indexes reflect the skill
117
+ // bodies even when the manifest cache has drifted (e.g. dropping UK-CAF / AU
118
+ // control mappings from framework_gaps). Array fields are overlaid only when
119
+ // present in frontmatter; description is a scalar.
120
+ const FRONTMATTER_ARRAY_FIELDS = [
121
+ "framework_gaps", "d3fend_refs", "cwe_refs", "atlas_refs",
122
+ "attack_refs", "rfc_refs", "triggers", "data_deps",
123
+ ];
124
+ const FRONTMATTER_SCALAR_FIELDS = ["description"];
125
+
126
+ function authoritativeSkill(entry, body) {
127
+ const { frontmatter } = lint.extractFrontmatterBlock(body);
128
+ const fm = lint.parseFrontmatter(frontmatter);
129
+ const merged = { ...entry };
130
+ for (const field of FRONTMATTER_ARRAY_FIELDS) {
131
+ if (Array.isArray(fm[field])) merged[field] = fm[field];
132
+ }
133
+ for (const field of FRONTMATTER_SCALAR_FIELDS) {
134
+ if (typeof fm[field] === "string") merged[field] = fm[field];
135
+ }
136
+ return merged;
137
+ }
138
+
112
139
  function loadSources() {
113
140
  const manifest = readJson(ABS("manifest.json"));
114
- const skills = manifest.skills;
115
- const skillNames = new Set(skills.map((s) => s.name));
116
141
  const catalogFiles = fs.readdirSync(ABS("data")).filter((f) => f.endsWith(".json")).map((f) => "data/" + f);
117
142
 
118
143
  // Per-skill body cache so multiple builders don't re-read the same file.
119
144
  const skillBodies = {};
120
- for (const s of skills) skillBodies[s.name] = fs.readFileSync(ABS(s.path), "utf8");
145
+ for (const s of manifest.skills) skillBodies[s.name] = fs.readFileSync(ABS(s.path), "utf8");
146
+
147
+ // Build the skill records from the authoritative frontmatter, falling back
148
+ // to the manifest cache for fields frontmatter doesn't carry (signatures,
149
+ // dlp_refs, etc.). Downstream builders read cross-reference arrays from
150
+ // these records, so this is the single point that keeps the indexes aligned
151
+ // with the skill bodies.
152
+ const skills = manifest.skills.map((s) => authoritativeSkill(s, skillBodies[s.name]));
153
+ const skillNames = new Set(skills.map((s) => s.name));
121
154
 
122
155
  const ctx = {
123
156
  root: ROOT,
@@ -157,7 +190,7 @@ const OUTPUTS = [
157
190
  {
158
191
  name: "xref",
159
192
  file: "xref.json",
160
- deps: [isManifest],
193
+ deps: [isManifest, isAnySkillBody],
161
194
  build: (ctx) => {
162
195
  const xref = {
163
196
  cwe_refs: {}, d3fend_refs: {}, framework_gaps: {},
@@ -181,7 +214,7 @@ const OUTPUTS = [
181
214
  {
182
215
  name: "trigger-table",
183
216
  file: "trigger-table.json",
184
- deps: [isManifest],
217
+ deps: [isManifest, isAnySkillBody],
185
218
  build: (ctx) => {
186
219
  const t = {};
187
220
  for (const s of ctx.skills) {
@@ -279,6 +312,7 @@ const OUTPUTS = [
279
312
  file: "chains.json",
280
313
  deps: [
281
314
  isManifest,
315
+ isAnySkillBody,
282
316
  isCatalog("cve-catalog"),
283
317
  isCatalog("cwe-catalog"),
284
318
  isCatalog("framework-control-gaps"),
@@ -429,7 +463,7 @@ const OUTPUTS = [
429
463
  {
430
464
  name: "frequency",
431
465
  file: "frequency.json",
432
- deps: [isManifest, isAnyCatalog],
466
+ deps: [isManifest, isAnySkillBody, isAnyCatalog],
433
467
  build: (ctx) => {
434
468
  const { buildFrequency } = require("./builders/frequency");
435
469
  return buildFrequency({
@@ -445,7 +479,7 @@ const OUTPUTS = [
445
479
  {
446
480
  name: "activity-feed",
447
481
  file: "activity-feed.json",
448
- deps: [isManifest, isAnyCatalog],
482
+ deps: [isManifest, isAnySkillBody, isAnyCatalog],
449
483
  build: (ctx) => {
450
484
  const { buildActivityFeed } = require("./builders/activity-feed");
451
485
  return buildActivityFeed({ root: ctx.root, manifest: ctx.manifest, skills: ctx.skills, catalogFiles: ctx.catalogFiles });
@@ -77,6 +77,7 @@ function buildCweChains({ skills, cweCatalog, atlasTtps, cveCatalog, frameworkGa
77
77
  title: rfcCatalog[r]?.title,
78
78
  status: rfcCatalog[r]?.status,
79
79
  })),
80
+ dlp_refs: [...accum.dlp_refs].sort(),
80
81
  };
81
82
 
82
83
  // Related CVEs: walk evidence_cves on the framework_gaps that the
@@ -118,11 +118,19 @@ function buildOne(absPath, relPath) {
118
118
  const next = h2[j + 1];
119
119
  const startByte = lineByteOffsets[cur.idx];
120
120
  const endByte = next ? lineByteOffsets[next.idx] : totalBytes;
121
- // Count H3 within this section.
121
+ // Count H3 within this section — fence-aware, the same way the H2 loop
122
+ // above is. A section starts and ends on an H2 header, both of which are
123
+ // outside any fence, so fence state always begins false here. "### Foo"
124
+ // lines inside ```...``` output templates are not real sub-sections.
122
125
  const endIdx = next ? next.idx : lines.length;
123
126
  let h3Count = 0;
127
+ let h3InFence = false;
124
128
  for (let k = cur.idx + 1; k < endIdx; k++) {
125
- if (/^### /.test(lines[k])) h3Count++;
129
+ if (/^```/.test(lines[k])) {
130
+ h3InFence = !h3InFence;
131
+ continue;
132
+ }
133
+ if (!h3InFence && /^### /.test(lines[k])) h3Count++;
126
134
  }
127
135
  sections.push({
128
136
  name: cur.raw.replace(/^##\s+/, ""),
@@ -26,10 +26,10 @@
26
26
  * }
27
27
  * }
28
28
  *
29
- * Plus a totals block:
29
+ * Corpus totals live under the top-level `_meta` block:
30
30
  * {
31
- * total_chars, total_approx_tokens,
32
- * by_recipe: { … } — placeholder consumers can use to estimate bundles
31
+ * schema_version, tokenizer_note, approx_chars_per_token,
32
+ * total_chars, total_approx_tokens, skill_count
33
33
  * }
34
34
  */
35
35
 
@@ -100,6 +100,35 @@ function readPackageVersion() {
100
100
  return JSON.parse(fs.readFileSync(PACKAGE_JSON, 'utf8')).version;
101
101
  }
102
102
 
103
+ // Every previously released version must keep its own `## <version> ` heading.
104
+ // The release flow edits the TOP of the file; an edit that replaces the prior
105
+ // release's heading instead of inserting above it silently merges that
106
+ // release's notes into the new section — the extract then spans multiple
107
+ // releases and the public release body republishes old notes under the new
108
+ // version. Tags are the authoritative record of what was released.
109
+ // Tags whose release never published: the tag-push event was dropped (e.g.
110
+ // a GitHub Actions outage) and — because the v* ruleset forbids re-pushing a
111
+ // tag — the recovery is a version bump re-released with the same notes under
112
+ // the NEW heading. The orphan tag therefore legitimately has no CHANGELOG
113
+ // entry of its own. Tag exists, npm/GitHub Release do not.
114
+ const ORPHAN_RELEASE_TAGS = new Set(['0.13.111', '0.15.25']);
115
+
116
+ function releasedVersionsFromTags() {
117
+ try {
118
+ const out = require('node:child_process').execFileSync('git', ['tag', '-l', 'v*'], { cwd: ROOT, encoding: 'utf8' });
119
+ return out.split(/\r?\n/)
120
+ .map((t) => (t.match(/^v(\d+\.\d+\.\d+)$/) || [])[1])
121
+ .filter((v) => v && !ORPHAN_RELEASE_TAGS.has(v));
122
+ } catch {
123
+ // git absent or tags not fetched (shallow checkout) — nothing to check.
124
+ return [];
125
+ }
126
+ }
127
+
128
+ function missingReleasedHeadings(text, versions) {
129
+ return versions.filter((v) => !headingLine(text, v));
130
+ }
131
+
103
132
  function main() {
104
133
  const version = process.argv[2] || readPackageVersion();
105
134
  if (!/^\d+\.\d+\.\d+$/.test(version)) {
@@ -130,6 +159,14 @@ function main() {
130
159
  return;
131
160
  }
132
161
 
162
+ const missing = missingReleasedHeadings(text, releasedVersionsFromTags());
163
+ if (missing.length > 0) {
164
+ console.error('[check-changelog-extract] FAIL: released version(s) lost their CHANGELOG heading: ' + missing.map((v) => '## ' + v).join(', '));
165
+ console.error('[check-changelog-extract] A new entry must be INSERTED ABOVE the previous release heading, never replace it — otherwise the prior release\'s notes merge into the new section and republish in the new release body.');
166
+ process.exitCode = 1;
167
+ return;
168
+ }
169
+
133
170
  const section = extractSection(text, version);
134
171
  if (section.length === 0) {
135
172
  console.error('[check-changelog-extract] FAIL: v' + version + ' section is empty — the release body would fall back to the generic "Release of v' + version + '." line.');
@@ -153,6 +190,6 @@ function main() {
153
190
  process.exitCode = 0;
154
191
  }
155
192
 
156
- module.exports = { extractSection, headingLine, lintOperatorClean, FORBIDDEN };
193
+ module.exports = { extractSection, headingLine, lintOperatorClean, FORBIDDEN, missingReleasedHeadings, releasedVersionsFromTags };
157
194
 
158
195
  if (require.main === module) main();
@@ -31,6 +31,39 @@ function resolveRoot(argv) {
31
31
  return path.join(__dirname, "..");
32
32
  }
33
33
 
34
+ // Entry count for a data/*.json catalog: keys minus the _meta sentinel. The
35
+ // catalogs are objects keyed by entry id (CVE-…, CWE-…, T…, AML.T…, D3-…,
36
+ // RFC-…) with a single _meta block, so the live entry total is the key count
37
+ // excluding _meta.
38
+ function catalogEntryCount(dataDir, file) {
39
+ const p = path.join(dataDir, file);
40
+ // A --root pointed at a partial tree (no such catalog file) skips that
41
+ // token's check rather than crashing — catalog PRESENCE is asserted by
42
+ // the cardinality check above and the per-component hash check below,
43
+ // not by the description parser.
44
+ if (!fs.existsSync(p)) return null;
45
+ const j = JSON.parse(fs.readFileSync(p, "utf8"));
46
+ if (Array.isArray(j)) return j.length;
47
+ if (j && typeof j === "object") {
48
+ return Object.keys(j).filter((k) => k !== "_meta").length;
49
+ }
50
+ return 0;
51
+ }
52
+
53
+ // The description string embeds per-catalog ENTRY counts as free text, e.g.
54
+ // "11 catalogs (439 CVEs / 177 CWEs / 805 ATT&CK + ICS / 170 ATLAS /
55
+ // 468 D3FEND / 8888 RFCs)". Each token maps to one data/*.json catalog whose
56
+ // live entry count must match. `label` is the regex-escaped text that follows
57
+ // the number in the description.
58
+ const DESCRIPTION_ENTRY_TOKENS = [
59
+ { file: "cve-catalog.json", label: "CVEs" },
60
+ { file: "cwe-catalog.json", label: "CWEs" },
61
+ { file: "attack-techniques.json", label: "ATT&CK \\+ ICS" },
62
+ { file: "atlas-ttps.json", label: "ATLAS" },
63
+ { file: "d3fend-catalog.json", label: "D3FEND" },
64
+ { file: "rfc-references.json", label: "RFCs" },
65
+ ];
66
+
34
67
  function checkSbomCurrency(root) {
35
68
  const sbomPath = path.join(root, "sbom.cdx.json");
36
69
  const manifestPath = path.join(root, "manifest.json");
@@ -64,6 +97,45 @@ function checkSbomCurrency(root) {
64
97
  errors.push("SBOM is not CycloneDX 1.6");
65
98
  }
66
99
 
100
+ // The SBOM ships per-catalog entry counts and a skill count embedded as free
101
+ // text in metadata.component.description (propagated verbatim from
102
+ // package.json). The numeric properties above only cover catalog/skill
103
+ // CARDINALITY (file count + skill count), so a catalog's entry total can
104
+ // drift past the count baked into the description while the dedicated SBOM
105
+ // gate still passes. Parse each token out of the description and assert it
106
+ // against the live entry count so a stale published-SBOM description fails
107
+ // the gate.
108
+ const description =
109
+ (sbom.metadata && sbom.metadata.component && sbom.metadata.component.description) || "";
110
+ for (const { file, label } of DESCRIPTION_ENTRY_TOKENS) {
111
+ const live = catalogEntryCount(dataDir, file);
112
+ if (live === null) continue;
113
+ const m = description.match(new RegExp("(\\d+)\\s+" + label + "\\b"));
114
+ if (!m) {
115
+ errors.push(
116
+ `SBOM description is missing the "${file.replace(/\.json$/, "")}" entry-count token (${label}) — regenerate via \`npm run refresh-sbom\``
117
+ );
118
+ continue;
119
+ }
120
+ const stated = Number(m[1]);
121
+ if (stated !== live) {
122
+ errors.push(
123
+ `SBOM description entry count for ${label} is ${stated} but live ${file} has ${live} — description is stale; update package.json.description and \`npm run refresh-sbom\``
124
+ );
125
+ }
126
+ }
127
+ // The skill count is embedded in the same description string ("N skills").
128
+ const skillMatch = description.match(/(\d+)\s+skills\b/);
129
+ if (!skillMatch) {
130
+ errors.push(
131
+ "SBOM description is missing the skill-count token (N skills) — regenerate via `npm run refresh-sbom`"
132
+ );
133
+ } else if (Number(skillMatch[1]) !== liveSkills) {
134
+ errors.push(
135
+ `SBOM description skill count is ${Number(skillMatch[1])} but live manifest has ${liveSkills} skills — description is stale; update package.json.description and \`npm run refresh-sbom\``
136
+ );
137
+ }
138
+
67
139
  // component-level cross-check. A renamed or version-bumped
68
140
  // skill that never made it into the SBOM refresh will pass the count
69
141
  // check (the cardinality is unchanged) but the per-component name +
@@ -70,6 +70,11 @@ const COMMENT_EXEMPT = new Set([
70
70
  // MUST embed real `## X.Y.Z` headings (e.g. 0.15.5 vs 0.15.50) — load-bearing
71
71
  // test data, not sprinkled release tags.
72
72
  "tests/check-changelog-extract.test.js",
73
+ // The extract gate's orphan-tag allowlist must name the exact versions of
74
+ // tags that exist with no published release (outage-recovery bumps), so the
75
+ // heading-completeness check can skip them — load-bearing references to git
76
+ // tags, an authoritative version surface.
77
+ "scripts/check-changelog-extract.js",
73
78
  ]);
74
79
 
75
80
  // Git-ignored files (a contributor's local-only working docs, scratch) are
@@ -503,21 +503,27 @@ function cmdRelease() {
503
503
  if (runId) {
504
504
  _run("gh", ["run", "watch", runId, "--exit-status"], { allowFail: true });
505
505
  var concl = _capture("gh", ["run", "view", runId, "--json", "conclusion", "--jq", ".conclusion"]).stdout;
506
+ // A non-success conclusion is a hard failure: the publish either failed or
507
+ // is unconfirmable, and either way the release is not done. Warning-and-
508
+ // continuing let a stalled publish read as a clean release.
506
509
  if (concl !== "success") {
507
- console.error("warning: release.yml conclusion=" + concl + " — re-check before trusting npm");
508
- } else {
509
- _ok("release.yml: success");
510
+ throw new Error("release: release.yml conclusion=" + (concl || "(unknown)") +
511
+ " the publish workflow did not finish successfully; re-check release.yml before treating the release as done");
510
512
  }
513
+ _ok("release.yml: success");
511
514
  } else {
512
- console.log("no release.yml run found yet (tag push may still be propagating)");
515
+ throw new Error("release: no release.yml run found for the tag the publish workflow has not started; " +
516
+ "confirm the tag was pushed and the workflow fired before treating the release as done");
513
517
  }
514
518
 
515
519
  _section("verify npm");
516
520
  var npmVersion = _capture("npm", ["view", PKG_NAME, "version"]).stdout;
517
521
  console.log("npm " + PKG_NAME + ": " + (npmVersion || "(unable to query)") + " (expected " + next + ")");
522
+ // Require a POSITIVE confirmation: the queried npm version must equal `next`.
523
+ // The hard failure is asserted at the end of the phase (after the tarball
524
+ // verify). An empty stdout (registry/auth/network failure) is treated as a
525
+ // mismatch — an unconfirmable publish is a failure, not a success.
518
526
  if (npmVersion === next) _ok("npm matches " + next);
519
- // A mismatch is asserted as a hard failure at the end of the phase (after
520
- // the tarball verify), so a stalled publish can't read as a clean release.
521
527
 
522
528
  _section("fresh-tarball signature verify");
523
529
  // Verify against the EXACT bytes a downstream consumer installs — the
@@ -535,18 +541,19 @@ function cmdRelease() {
535
541
  throw new Error("release: scripts/verify-shipped-tarball.js missing — cannot verify the shipped artifact");
536
542
  }
537
543
 
538
- // An npm-version mismatch after the workflow finished is not mere
539
- // propagation lag fail so a stalled/failed publish can't read as a
540
- // completed release. (A genuinely in-flight publish is caught by the
541
- // workflow-conclusion check above; by the time we query npm post-watch the
542
- // version should be live.)
543
- if (npmVersion && npmVersion !== next) {
544
- throw new Error("release: npm shows " + npmVersion + " but expected " + next +
545
- " publish did not complete; re-check release.yml before treating the release as done");
544
+ // Require a positive npm confirmation after the workflow finished. A version
545
+ // that is empty (query failed) OR != next is not mere propagation lag — fail
546
+ // so a stalled/failed/unconfirmable publish can't read as a completed
547
+ // release. (A genuinely in-flight publish is caught by the workflow-
548
+ // conclusion check above; by the time we query npm post-watch the version
549
+ // should be live.) The message reports the value actually queried.
550
+ if (npmVersion !== next) {
551
+ throw new Error("release: npm shows " + (npmVersion || "(unable to query)") + " but expected " + next +
552
+ " — publish did not complete or could not be confirmed; re-check release.yml before treating the release as done");
546
553
  }
547
554
 
548
555
  console.log("\nThe landing site auto-injects the version from jsDelivr @latest — no manual deploy.");
549
- console.log("Release complete: npm shows " + next + " and the shipped tarball verifies.");
556
+ console.log("Release complete: npm shows " + npmVersion + " and the shipped tarball verifies.");
550
557
  }
551
558
 
552
559
  function cmdAll(opts) {
@@ -109,7 +109,7 @@ RWEP = min(100, max(0,
109
109
  (poc_public × 20) +
110
110
  (ai_assisted × 15) +
111
111
  (active_expl × 20) +
112
- (blast_radius × 15) -
112
+ (blast_radius × 30) -
113
113
  (patch_avail × 15) -
114
114
  (live_patch × 10) +
115
115
  (reboot_req × 5)
@@ -135,11 +135,11 @@ RWEP = min(100, max(0,
135
135
  - Score contribution: +20 points if confirmed
136
136
  - Rationale: Confirmed exploitation means the threat is not theoretical. Treat as an incident-level response trigger.
137
137
 
138
- **blast_radius** (0.0 to 1.0 scaled to 0–15): How broad is the affected population?
139
- - 15 points: Affects all Linux systems since a specific kernel version (e.g., Copy Fail: all 4.14+)
140
- - 10 points: Affects a major distribution's default configuration
141
- - 7 points: Affects a specific distribution or configuration
142
- - 3 points: Affects a narrow software version range
138
+ **blast_radius** (0.0 to 1.0 scaled to 0–30): How broad is the affected population?
139
+ - 30 points: Affects all Linux systems since a specific kernel version (e.g., Copy Fail: all 4.14+)
140
+ - 20 points: Affects a major distribution's default configuration
141
+ - 14 points: Affects a specific distribution or configuration
142
+ - 6 points: Affects a narrow software version range
143
143
  - 0 points: Affects only highly specific configurations
144
144
 
145
145
  **patch_avail** (0 or 1): Is a patch available?
@@ -290,7 +290,7 @@ For a CVE not in the pre-calculated catalog, collect:
290
290
 
291
291
  ### Step 2: Apply RWEP formula
292
292
 
293
- Calculate factor values (binary 0/1 or scaled 0–1 for blast radius) and apply formula.
293
+ Calculate factor values (binary 0/1, or scaled 0–30 for blast radius) and apply formula.
294
294
 
295
295
  ### Step 3: Generate remediation timeline
296
296
 
@@ -336,7 +336,7 @@ The skill produces a per-CVE Exploit Priority Assessment showing the RWEP score,
336
336
  | PoC Public | Yes/No | +20/0 |
337
337
  | AI-Assisted | Yes/No | +15/0 |
338
338
  | Active Exploitation | Confirmed/Suspected/No | +20/+10/0 |
339
- | Blast Radius | [description] | [0-15] |
339
+ | Blast Radius | [description] | [0-30] |
340
340
  | Patch Available | Yes/No | -15/0 |
341
341
  | Live Patch Available | Yes/No | -10/0 |
342
342
  | Reboot Required | Yes/No | +5/0 |