@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.
- package/ARCHITECTURE.md +2 -2
- package/CHANGELOG.md +42 -0
- package/CONTEXT.md +9 -9
- package/README.md +3 -3
- package/agents/report-generator.md +2 -2
- package/agents/skill-updater.md +1 -1
- package/agents/source-validator.md +3 -4
- package/agents/threat-researcher.md +1 -1
- package/bin/exceptd.js +91 -32
- package/data/_indexes/_meta.json +10 -10
- package/data/_indexes/activity-feed.json +12 -12
- package/data/_indexes/chains.json +70435 -4026
- package/data/_indexes/frequency.json +492 -163
- package/data/_indexes/section-offsets.json +51 -51
- package/data/_indexes/summary-cards.json +272 -106
- package/data/_indexes/token-budget.json +10 -10
- package/data/_indexes/trigger-table.json +15 -6
- package/data/_indexes/xref.json +218 -26
- package/data/cve-catalog.json +10 -10
- package/data/cwe-catalog.json +1 -0
- package/lib/auto-discovery.js +39 -1
- package/lib/collectors/ai-api.js +112 -7
- package/lib/collectors/citation-hygiene.js +27 -0
- package/lib/collectors/crypto-codebase.js +25 -0
- package/lib/collectors/kernel.js +32 -2
- package/lib/collectors/library-author.js +30 -0
- package/lib/collectors/runtime.js +38 -3
- package/lib/collectors/sbom.js +21 -2
- package/lib/collectors/scan-excludes.js +4 -1
- package/lib/collectors/secrets.js +125 -0
- package/lib/cve-cli.js +9 -1
- package/lib/cve-curation.js +8 -1
- package/lib/cve-regression-watcher.js +5 -2
- package/lib/exit-codes.js +2 -0
- package/lib/flag-suggest.js +1 -1
- package/lib/lint-skills.js +70 -0
- package/lib/playbook-runner.js +75 -14
- package/lib/prefetch.js +24 -1
- package/lib/refresh-external.js +32 -3
- package/lib/rfc-cli.js +8 -1
- package/lib/scoring.js +36 -8
- package/lib/validate-cve-catalog.js +36 -14
- package/lib/validate-package.js +8 -0
- package/lib/validate-playbooks.js +42 -0
- package/lib/verify.js +4 -3
- package/manifest-snapshot.json +4 -2
- package/manifest-snapshot.sha256 +1 -1
- package/manifest.json +57 -54
- package/orchestrator/README.md +1 -1
- package/orchestrator/index.js +65 -7
- package/orchestrator/scanner.js +53 -5
- package/package.json +1 -1
- package/sbom.cdx.json +110 -110
- package/scripts/build-indexes.js +42 -8
- package/scripts/builders/cwe-chains.js +1 -0
- package/scripts/builders/section-offsets.js +10 -2
- package/scripts/builders/token-budget.js +3 -3
- package/scripts/check-changelog-extract.js +38 -1
- package/scripts/check-sbom-currency.js +72 -0
- package/scripts/check-version-tags.js +5 -0
- package/scripts/release.js +22 -15
- package/skills/exploit-scoring/skill.md +8 -8
package/scripts/build-indexes.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
-
*
|
|
29
|
+
* Corpus totals live under the top-level `_meta` block:
|
|
30
30
|
* {
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
package/scripts/release.js
CHANGED
|
@@ -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
|
-
|
|
508
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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 " +
|
|
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 ×
|
|
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–
|
|
139
|
-
-
|
|
140
|
-
-
|
|
141
|
-
-
|
|
142
|
-
-
|
|
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–
|
|
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-
|
|
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 |
|