@blamejs/exceptd-skills 0.9.1
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/AGENTS.md +232 -0
- package/ARCHITECTURE.md +267 -0
- package/CHANGELOG.md +616 -0
- package/CONTEXT.md +203 -0
- package/LICENSE +200 -0
- package/NOTICE +82 -0
- package/README.md +307 -0
- package/SECURITY.md +73 -0
- package/agents/README.md +81 -0
- package/agents/report-generator.md +156 -0
- package/agents/skill-updater.md +102 -0
- package/agents/source-validator.md +119 -0
- package/agents/threat-researcher.md +149 -0
- package/bin/exceptd.js +183 -0
- package/data/_indexes/_meta.json +88 -0
- package/data/_indexes/activity-feed.json +362 -0
- package/data/_indexes/catalog-summaries.json +229 -0
- package/data/_indexes/chains.json +7135 -0
- package/data/_indexes/currency.json +359 -0
- package/data/_indexes/did-ladders.json +451 -0
- package/data/_indexes/frequency.json +2072 -0
- package/data/_indexes/handoff-dag.json +476 -0
- package/data/_indexes/jurisdiction-clocks.json +967 -0
- package/data/_indexes/jurisdiction-map.json +536 -0
- package/data/_indexes/recipes.json +319 -0
- package/data/_indexes/section-offsets.json +3656 -0
- package/data/_indexes/stale-content.json +14 -0
- package/data/_indexes/summary-cards.json +1736 -0
- package/data/_indexes/theater-fingerprints.json +381 -0
- package/data/_indexes/token-budget.json +2137 -0
- package/data/_indexes/trigger-table.json +1374 -0
- package/data/_indexes/xref.json +818 -0
- package/data/atlas-ttps.json +282 -0
- package/data/cve-catalog.json +496 -0
- package/data/cwe-catalog.json +1017 -0
- package/data/d3fend-catalog.json +738 -0
- package/data/dlp-controls.json +1039 -0
- package/data/exploit-availability.json +67 -0
- package/data/framework-control-gaps.json +1255 -0
- package/data/global-frameworks.json +2913 -0
- package/data/rfc-references.json +324 -0
- package/data/zeroday-lessons.json +377 -0
- package/keys/public.pem +3 -0
- package/lib/framework-gap.js +328 -0
- package/lib/job-queue.js +195 -0
- package/lib/lint-skills.js +536 -0
- package/lib/prefetch.js +372 -0
- package/lib/refresh-external.js +713 -0
- package/lib/schemas/cve-catalog.schema.json +151 -0
- package/lib/schemas/manifest.schema.json +106 -0
- package/lib/schemas/skill-frontmatter.schema.json +113 -0
- package/lib/scoring.js +149 -0
- package/lib/sign.js +197 -0
- package/lib/ttp-mapper.js +80 -0
- package/lib/validate-catalog-meta.js +198 -0
- package/lib/validate-cve-catalog.js +213 -0
- package/lib/validate-indexes.js +83 -0
- package/lib/validate-package.js +162 -0
- package/lib/validate-vendor.js +85 -0
- package/lib/verify.js +216 -0
- package/lib/worker-pool.js +84 -0
- package/manifest-snapshot.json +1833 -0
- package/manifest.json +2108 -0
- package/orchestrator/README.md +124 -0
- package/orchestrator/dispatcher.js +140 -0
- package/orchestrator/event-bus.js +146 -0
- package/orchestrator/index.js +874 -0
- package/orchestrator/pipeline.js +201 -0
- package/orchestrator/scanner.js +327 -0
- package/orchestrator/scheduler.js +137 -0
- package/package.json +113 -0
- package/sbom.cdx.json +158 -0
- package/scripts/audit-cross-skill.js +261 -0
- package/scripts/audit-perf.js +160 -0
- package/scripts/bootstrap.js +205 -0
- package/scripts/build-indexes.js +721 -0
- package/scripts/builders/activity-feed.js +79 -0
- package/scripts/builders/catalog-summaries.js +67 -0
- package/scripts/builders/currency.js +109 -0
- package/scripts/builders/cwe-chains.js +105 -0
- package/scripts/builders/did-ladders.js +149 -0
- package/scripts/builders/frequency.js +89 -0
- package/scripts/builders/jurisdiction-clocks.js +126 -0
- package/scripts/builders/recipes.js +159 -0
- package/scripts/builders/section-offsets.js +162 -0
- package/scripts/builders/stale-content.js +171 -0
- package/scripts/builders/summary-cards.js +166 -0
- package/scripts/builders/theater-fingerprints.js +198 -0
- package/scripts/builders/token-budget.js +96 -0
- package/scripts/check-manifest-snapshot.js +217 -0
- package/scripts/predeploy.js +267 -0
- package/scripts/refresh-manifest-snapshot.js +57 -0
- package/scripts/refresh-sbom.js +222 -0
- package/skills/age-gates-child-safety/skill.md +456 -0
- package/skills/ai-attack-surface/skill.md +282 -0
- package/skills/ai-c2-detection/skill.md +440 -0
- package/skills/ai-risk-management/skill.md +311 -0
- package/skills/api-security/skill.md +287 -0
- package/skills/attack-surface-pentest/skill.md +381 -0
- package/skills/cloud-security/skill.md +384 -0
- package/skills/compliance-theater/skill.md +365 -0
- package/skills/container-runtime-security/skill.md +379 -0
- package/skills/coordinated-vuln-disclosure/skill.md +473 -0
- package/skills/defensive-countermeasure-mapping/skill.md +300 -0
- package/skills/dlp-gap-analysis/skill.md +337 -0
- package/skills/email-security-anti-phishing/skill.md +206 -0
- package/skills/exploit-scoring/skill.md +331 -0
- package/skills/framework-gap-analysis/skill.md +374 -0
- package/skills/fuzz-testing-strategy/skill.md +313 -0
- package/skills/global-grc/skill.md +564 -0
- package/skills/identity-assurance/skill.md +272 -0
- package/skills/incident-response-playbook/skill.md +546 -0
- package/skills/kernel-lpe-triage/skill.md +303 -0
- package/skills/mcp-agent-trust/skill.md +326 -0
- package/skills/mlops-security/skill.md +325 -0
- package/skills/ot-ics-security/skill.md +340 -0
- package/skills/policy-exception-gen/skill.md +437 -0
- package/skills/pqc-first/skill.md +546 -0
- package/skills/rag-pipeline-security/skill.md +294 -0
- package/skills/researcher/skill.md +310 -0
- package/skills/sector-energy/skill.md +409 -0
- package/skills/sector-federal-government/skill.md +302 -0
- package/skills/sector-financial/skill.md +398 -0
- package/skills/sector-healthcare/skill.md +373 -0
- package/skills/security-maturity-tiers/skill.md +464 -0
- package/skills/skill-update-loop/skill.md +463 -0
- package/skills/supply-chain-integrity/skill.md +318 -0
- package/skills/threat-model-currency/skill.md +404 -0
- package/skills/threat-modeling-methodology/skill.md +312 -0
- package/skills/webapp-security/skill.md +281 -0
- package/skills/zeroday-gap-learn/skill.md +350 -0
- package/vendor/blamejs/LICENSE +201 -0
- package/vendor/blamejs/README.md +54 -0
- package/vendor/blamejs/_PROVENANCE.json +54 -0
- package/vendor/blamejs/retry.js +335 -0
- package/vendor/blamejs/worker-pool.js +418 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* scripts/predeploy.js
|
|
4
|
+
*
|
|
5
|
+
* Local mirror of the CI pre-deployment gate sequence. Runs every gate
|
|
6
|
+
* the `.github/workflows/ci.yml` workflow runs, in order. Each gate is
|
|
7
|
+
* isolated — a failure does not short-circuit the rest, so a single run
|
|
8
|
+
* surfaces all problems instead of just the first one (matches the CI
|
|
9
|
+
* shape where each job runs independently).
|
|
10
|
+
*
|
|
11
|
+
* Run before pushing to main or opening a PR:
|
|
12
|
+
* npm run predeploy
|
|
13
|
+
*
|
|
14
|
+
* Exit code:
|
|
15
|
+
* 0 — all gates passed
|
|
16
|
+
* 1 — one or more gates failed (per-gate output already printed)
|
|
17
|
+
* 2 — runner-level error (missing script, fork failure, etc.)
|
|
18
|
+
*
|
|
19
|
+
* Single-source-of-truth: the GATES list below mirrors the job sequence
|
|
20
|
+
* in .github/workflows/ci.yml. Test coverage in tests/predeploy.test.js
|
|
21
|
+
* asserts the two stay in sync.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const { execFileSync } = require("child_process");
|
|
25
|
+
const path = require("path");
|
|
26
|
+
const fs = require("fs");
|
|
27
|
+
|
|
28
|
+
const ROOT = path.join(__dirname, "..");
|
|
29
|
+
|
|
30
|
+
// Ordered list of CI gates. Each entry: { name, command, args, ciJobName }.
|
|
31
|
+
// ciJobName matches the `name:` field of the corresponding job in
|
|
32
|
+
// .github/workflows/ci.yml (or scorecard.yml). Used by the workflow-sync
|
|
33
|
+
// test to assert the two never drift.
|
|
34
|
+
const GATES = [
|
|
35
|
+
{
|
|
36
|
+
name: "Verify skill signatures (Ed25519)",
|
|
37
|
+
command: process.execPath,
|
|
38
|
+
args: [path.join(ROOT, "lib", "verify.js")],
|
|
39
|
+
ciJobName: "Verify skill signatures (Ed25519)",
|
|
40
|
+
requiresKeys: true,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "Run tests (node:test)",
|
|
44
|
+
command: process.execPath,
|
|
45
|
+
// Glob form rather than a directory arg: Node 25.x on Windows
|
|
46
|
+
// resolves a bare directory path through the module loader before
|
|
47
|
+
// the test runner sees it, which fails for a working dir that
|
|
48
|
+
// sits inside a path containing parentheses (e.g. Dropbox).
|
|
49
|
+
//
|
|
50
|
+
// --test-concurrency=1 forces sequential file execution. Several
|
|
51
|
+
// test files (build-incremental, indexes-v070, refresh-*) touch
|
|
52
|
+
// shared filesystem state under data/_indexes/ + refresh-report.json
|
|
53
|
+
// + skill bodies; running in parallel produces flaky races. Sequential
|
|
54
|
+
// is ~1.5s slower locally but eliminates the false negative we hit
|
|
55
|
+
// on the Linux CI runner in the v0.9.0 release attempt.
|
|
56
|
+
args: ["--test", "--test-concurrency=1", "tests/*.test.js"],
|
|
57
|
+
ciJobName: "Tests",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "Validate CVE catalog schema + zero-day learning coverage",
|
|
61
|
+
command: process.execPath,
|
|
62
|
+
args: [path.join(ROOT, "lib", "validate-cve-catalog.js")],
|
|
63
|
+
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "Validate offline CVE catalog state",
|
|
67
|
+
command: process.execPath,
|
|
68
|
+
args: [
|
|
69
|
+
path.join(ROOT, "orchestrator", "index.js"),
|
|
70
|
+
"validate-cves",
|
|
71
|
+
"--offline",
|
|
72
|
+
"--no-fail",
|
|
73
|
+
],
|
|
74
|
+
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "Validate offline RFC catalog state",
|
|
78
|
+
command: process.execPath,
|
|
79
|
+
args: [
|
|
80
|
+
path.join(ROOT, "orchestrator", "index.js"),
|
|
81
|
+
"validate-rfcs",
|
|
82
|
+
"--offline",
|
|
83
|
+
"--no-fail",
|
|
84
|
+
],
|
|
85
|
+
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "Manifest snapshot gate (breaking-change detector)",
|
|
89
|
+
command: process.execPath,
|
|
90
|
+
args: [path.join(ROOT, "scripts", "check-manifest-snapshot.js")],
|
|
91
|
+
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "Lint skill files",
|
|
95
|
+
command: process.execPath,
|
|
96
|
+
args: [path.join(ROOT, "lib", "lint-skills.js")],
|
|
97
|
+
ciJobName: "Lint skill files",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
// Informational only — surfaces the forward_watch horizon across all
|
|
101
|
+
// skills as a sanity signal. Emits the count but never fails the run;
|
|
102
|
+
// a parse problem is reported, not blocking.
|
|
103
|
+
name: "Forward-watch aggregator (informational)",
|
|
104
|
+
command: process.execPath,
|
|
105
|
+
args: [
|
|
106
|
+
path.join(ROOT, "orchestrator", "index.js"),
|
|
107
|
+
"watchlist",
|
|
108
|
+
],
|
|
109
|
+
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
110
|
+
informational: true,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "Validate catalog _meta (tlp + source_confidence + freshness_policy)",
|
|
114
|
+
command: process.execPath,
|
|
115
|
+
args: [path.join(ROOT, "lib", "validate-catalog-meta.js")],
|
|
116
|
+
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "SBOM currency check (sbom.cdx.json vs. live surface)",
|
|
120
|
+
command: process.execPath,
|
|
121
|
+
args: ["-e", sbomCurrencyChecker()],
|
|
122
|
+
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "Pre-computed indexes freshness (data/_indexes/ vs. live sources)",
|
|
126
|
+
command: process.execPath,
|
|
127
|
+
args: [path.join(ROOT, "lib", "validate-indexes.js")],
|
|
128
|
+
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "Vendor tree integrity (vendor/blamejs/ vs. _PROVENANCE.json)",
|
|
132
|
+
command: process.execPath,
|
|
133
|
+
args: [path.join(ROOT, "lib", "validate-vendor.js")],
|
|
134
|
+
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "Publish tarball shape (npm pack --dry-run + file allowlist)",
|
|
138
|
+
command: process.execPath,
|
|
139
|
+
args: [path.join(ROOT, "lib", "validate-package.js")],
|
|
140
|
+
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
/* Inline checker, run as `node -e`, so the predeploy gate stays one
|
|
145
|
+
* file and the SBOM regen logic stays in scripts/refresh-sbom.js
|
|
146
|
+
* (single source of truth). Compares the persisted sbom.cdx.json
|
|
147
|
+
* against the live skill_count + catalog_count derived from
|
|
148
|
+
* manifest.json + data/. Exits nonzero on drift, with a hint to run
|
|
149
|
+
* `npm run refresh-sbom`. */
|
|
150
|
+
function sbomCurrencyChecker() {
|
|
151
|
+
return [
|
|
152
|
+
"const fs=require('fs');const path=require('path');",
|
|
153
|
+
"const root=" + JSON.stringify(ROOT) + ";",
|
|
154
|
+
"const sbomPath=path.join(root,'sbom.cdx.json');",
|
|
155
|
+
"if(!fs.existsSync(sbomPath)){console.error('sbom.cdx.json not found — run `npm run refresh-sbom`.');process.exit(1);}",
|
|
156
|
+
"const sbom=JSON.parse(fs.readFileSync(sbomPath,'utf8'));",
|
|
157
|
+
"const manifest=JSON.parse(fs.readFileSync(path.join(root,'manifest.json'),'utf8'));",
|
|
158
|
+
"const dataDir=path.join(root,'data');",
|
|
159
|
+
"const liveCatalogs=fs.readdirSync(dataDir).filter(f=>f.endsWith('.json')).length;",
|
|
160
|
+
"const liveSkills=Array.isArray(manifest.skills)?manifest.skills.length:0;",
|
|
161
|
+
"const props=Object.fromEntries((sbom.metadata&&sbom.metadata.properties||[]).map(p=>[p.name,p.value]));",
|
|
162
|
+
"const sbomCatalogs=Number(props['exceptd:catalog:count']);",
|
|
163
|
+
"const sbomSkills=Number(props['exceptd:skill:count']);",
|
|
164
|
+
"let drift=false;",
|
|
165
|
+
"if(sbomCatalogs!==liveCatalogs){console.error(`SBOM catalog count ${sbomCatalogs} != live ${liveCatalogs}`);drift=true;}",
|
|
166
|
+
"if(sbomSkills!==liveSkills){console.error(`SBOM skill count ${sbomSkills} != live ${liveSkills}`);drift=true;}",
|
|
167
|
+
"if(sbom.bomFormat!=='CycloneDX'||sbom.specVersion!=='1.6'){console.error('SBOM is not CycloneDX 1.6');drift=true;}",
|
|
168
|
+
"if(drift){console.error('Run `npm run refresh-sbom` to regenerate sbom.cdx.json.');process.exit(1);}",
|
|
169
|
+
"console.log(`SBOM current — ${sbomSkills} skills, ${sbomCatalogs} catalogs.`);",
|
|
170
|
+
].join("");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function runGate(gate) {
|
|
174
|
+
if (gate.requiresKeys) {
|
|
175
|
+
const pubKey = path.join(ROOT, "keys", "public.pem");
|
|
176
|
+
if (!fs.existsSync(pubKey)) {
|
|
177
|
+
return {
|
|
178
|
+
status: "skipped",
|
|
179
|
+
reason:
|
|
180
|
+
"keys/public.pem missing — run `npm run bootstrap` to generate keys + sign skills.",
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
execFileSync(gate.command, gate.args, { stdio: "inherit", cwd: ROOT });
|
|
186
|
+
return { status: "passed" };
|
|
187
|
+
} catch (e) {
|
|
188
|
+
if (gate.informational) {
|
|
189
|
+
return {
|
|
190
|
+
status: "informational",
|
|
191
|
+
exitCode: e.status ?? null,
|
|
192
|
+
message: e.message,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
status: "failed",
|
|
197
|
+
exitCode: e.status ?? null,
|
|
198
|
+
message: e.message,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function main() {
|
|
204
|
+
const results = [];
|
|
205
|
+
for (const gate of GATES) {
|
|
206
|
+
process.stdout.write(`\n=== ${gate.name} ===\n`);
|
|
207
|
+
const outcome = runGate(gate);
|
|
208
|
+
results.push({ gate, outcome });
|
|
209
|
+
if (outcome.status === "skipped") {
|
|
210
|
+
process.stdout.write(` ⊘ skipped — ${outcome.reason}\n`);
|
|
211
|
+
} else if (outcome.status === "passed") {
|
|
212
|
+
process.stdout.write(` ✓ passed\n`);
|
|
213
|
+
} else if (outcome.status === "informational") {
|
|
214
|
+
process.stdout.write(
|
|
215
|
+
` ℹ informational (exit ${outcome.exitCode ?? "?"}) — not failing the run\n`
|
|
216
|
+
);
|
|
217
|
+
} else {
|
|
218
|
+
process.stdout.write(
|
|
219
|
+
` ✗ failed (exit ${outcome.exitCode ?? "?"}): ${outcome.message}\n`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Summary table.
|
|
225
|
+
process.stdout.write("\n=== Pre-deploy summary ===\n");
|
|
226
|
+
const widest = results.reduce(
|
|
227
|
+
(n, r) => Math.max(n, r.gate.name.length),
|
|
228
|
+
0
|
|
229
|
+
);
|
|
230
|
+
for (const { gate, outcome } of results) {
|
|
231
|
+
const icon =
|
|
232
|
+
outcome.status === "passed"
|
|
233
|
+
? "✓"
|
|
234
|
+
: outcome.status === "skipped"
|
|
235
|
+
? "⊘"
|
|
236
|
+
: outcome.status === "informational"
|
|
237
|
+
? "ℹ"
|
|
238
|
+
: "✗";
|
|
239
|
+
process.stdout.write(
|
|
240
|
+
` ${icon} ${gate.name.padEnd(widest)} ${outcome.status}\n`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const failures = results.filter((r) => r.outcome.status === "failed");
|
|
245
|
+
const skipped = results.filter((r) => r.outcome.status === "skipped");
|
|
246
|
+
const info = results.filter((r) => r.outcome.status === "informational");
|
|
247
|
+
process.stdout.write(
|
|
248
|
+
`\n${results.length - failures.length - skipped.length - info.length}/${results.length} gates passed` +
|
|
249
|
+
(skipped.length ? ` (${skipped.length} skipped)` : "") +
|
|
250
|
+
(info.length ? ` (${info.length} informational)` : "") +
|
|
251
|
+
(failures.length ? `, ${failures.length} failed` : "") +
|
|
252
|
+
".\n"
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
process.exit(failures.length > 0 ? 1 : 0);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
module.exports = { GATES };
|
|
259
|
+
|
|
260
|
+
if (require.main === module) {
|
|
261
|
+
try {
|
|
262
|
+
main();
|
|
263
|
+
} catch (e) {
|
|
264
|
+
console.error("[predeploy] runner error: " + ((e && e.stack) || e));
|
|
265
|
+
process.exit(2);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* scripts/refresh-manifest-snapshot.js
|
|
4
|
+
*
|
|
5
|
+
* Captures the current public skill surface from manifest.json and
|
|
6
|
+
* writes it to manifest-snapshot.json. Run this AFTER an intentional
|
|
7
|
+
* surface change (added skill, renamed trigger, refreshed framework
|
|
8
|
+
* refs) and commit the new snapshot alongside the change.
|
|
9
|
+
*
|
|
10
|
+
* Do NOT run this to "fix" a failing check-manifest-snapshot.js gate
|
|
11
|
+
* blindly — read the breaking-change list first. A breaking change is
|
|
12
|
+
* a surface narrowing every downstream consumer needs to know about.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* node scripts/refresh-manifest-snapshot.js
|
|
16
|
+
* git add manifest-snapshot.json
|
|
17
|
+
* git commit -m "refresh manifest snapshot: <what changed>"
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require("fs");
|
|
21
|
+
const path = require("path");
|
|
22
|
+
|
|
23
|
+
const ROOT = path.join(__dirname, "..");
|
|
24
|
+
const MANIFEST_PATH = path.join(ROOT, "manifest.json");
|
|
25
|
+
const SNAPSHOT_PATH = path.join(ROOT, "manifest-snapshot.json");
|
|
26
|
+
|
|
27
|
+
function captureSurface(manifest) {
|
|
28
|
+
const skills = (manifest.skills || []).map(s => ({
|
|
29
|
+
name: s.name,
|
|
30
|
+
version: s.version || null,
|
|
31
|
+
triggers: [...(s.triggers || [])].sort(),
|
|
32
|
+
data_deps: [...(s.data_deps || [])].sort(),
|
|
33
|
+
atlas_refs: [...(s.atlas_refs || [])].sort(),
|
|
34
|
+
attack_refs: [...(s.attack_refs || [])].sort(),
|
|
35
|
+
framework_gaps: [...(s.framework_gaps || [])].sort(),
|
|
36
|
+
rfc_refs: [...(s.rfc_refs || [])].sort(),
|
|
37
|
+
cwe_refs: [...(s.cwe_refs || [])].sort(),
|
|
38
|
+
d3fend_refs: [...(s.d3fend_refs || [])].sort(),
|
|
39
|
+
dlp_refs: [...(s.dlp_refs || [])].sort(),
|
|
40
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
_comment: "Auto-generated by scripts/refresh-manifest-snapshot.js — do not hand-edit. Public skill surface used by check-manifest-snapshot.js to detect breaking removals.",
|
|
44
|
+
_generated_at: new Date().toISOString(),
|
|
45
|
+
atlas_version: manifest.atlas_version || null,
|
|
46
|
+
skill_count: skills.length,
|
|
47
|
+
skills,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf8"));
|
|
52
|
+
const snapshot = captureSurface(manifest);
|
|
53
|
+
|
|
54
|
+
fs.writeFileSync(SNAPSHOT_PATH, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
|
|
55
|
+
|
|
56
|
+
console.log(`[refresh-manifest-snapshot] wrote ${snapshot.skill_count} skills to manifest-snapshot.json`);
|
|
57
|
+
console.log("[refresh-manifest-snapshot] commit this file alongside the surface change.");
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* scripts/refresh-sbom.js — regenerate sbom.cdx.json.
|
|
4
|
+
*
|
|
5
|
+
* The exceptd repository is zero-runtime-dependency by design (see
|
|
6
|
+
* package.json `dependencies: {}`). The SBOM therefore documents the
|
|
7
|
+
* project as an application component with an empty `components` array
|
|
8
|
+
* and pulls live surface counts from manifest.json + data/*.json so the
|
|
9
|
+
* artifact never silently drifts when the surface changes.
|
|
10
|
+
*
|
|
11
|
+
* Generated fields:
|
|
12
|
+
* - bomFormat / specVersion CycloneDX 1.6
|
|
13
|
+
* - serialNumber urn:uuid v4 derived from a stable
|
|
14
|
+
* hash of (project name + version +
|
|
15
|
+
* timestamp) so reruns produce a new
|
|
16
|
+
* UUID per refresh.
|
|
17
|
+
* - metadata.timestamp ISO 8601 of generation
|
|
18
|
+
* - metadata.tools hand-written generator
|
|
19
|
+
* - metadata.component application entry for exceptd-skills
|
|
20
|
+
* - metadata.properties catalog count, skill count, dataflow
|
|
21
|
+
* inputs, and the per-skill Ed25519
|
|
22
|
+
* integrity claim (lib/sign.js)
|
|
23
|
+
* - components [] — zero npm runtime deps
|
|
24
|
+
* - dependencies [] — nothing to depend on
|
|
25
|
+
*
|
|
26
|
+
* Run: node scripts/refresh-sbom.js
|
|
27
|
+
* npm run refresh-sbom
|
|
28
|
+
*
|
|
29
|
+
* No external dependencies. Node 24 stdlib only.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
'use strict';
|
|
33
|
+
|
|
34
|
+
const fs = require('node:fs');
|
|
35
|
+
const path = require('node:path');
|
|
36
|
+
const crypto = require('node:crypto');
|
|
37
|
+
const process = require('node:process');
|
|
38
|
+
|
|
39
|
+
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
40
|
+
const PACKAGE_PATH = path.join(REPO_ROOT, 'package.json');
|
|
41
|
+
const MANIFEST_PATH = path.join(REPO_ROOT, 'manifest.json');
|
|
42
|
+
const DATA_DIR = path.join(REPO_ROOT, 'data');
|
|
43
|
+
const SBOM_PATH = path.join(REPO_ROOT, 'sbom.cdx.json');
|
|
44
|
+
|
|
45
|
+
function readJson(p) {
|
|
46
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function countDataCatalogs(dir) {
|
|
50
|
+
return fs
|
|
51
|
+
.readdirSync(dir)
|
|
52
|
+
.filter((f) => f.endsWith('.json'))
|
|
53
|
+
.sort();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* RFC 4122 v4 UUID derived deterministically from a seed string so a
|
|
57
|
+
* given (project, version, timestamp) triple maps to a stable UUID
|
|
58
|
+
* across observers. Uses crypto.randomUUID() fallback if no seed. */
|
|
59
|
+
function uuidV4FromSeed(seed) {
|
|
60
|
+
const hash = crypto.createHash('sha256').update(seed).digest();
|
|
61
|
+
const b = Buffer.from(hash.subarray(0, 16));
|
|
62
|
+
b[6] = (b[6] & 0x0f) | 0x40; // version 4
|
|
63
|
+
b[8] = (b[8] & 0x3f) | 0x80; // RFC 4122 variant
|
|
64
|
+
const hex = b.toString('hex');
|
|
65
|
+
return (
|
|
66
|
+
hex.slice(0, 8) +
|
|
67
|
+
'-' +
|
|
68
|
+
hex.slice(8, 12) +
|
|
69
|
+
'-' +
|
|
70
|
+
hex.slice(12, 16) +
|
|
71
|
+
'-' +
|
|
72
|
+
hex.slice(16, 20) +
|
|
73
|
+
'-' +
|
|
74
|
+
hex.slice(20, 32)
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function loadVendorProvenance() {
|
|
79
|
+
const p = path.join(REPO_ROOT, 'vendor', 'blamejs', '_PROVENANCE.json');
|
|
80
|
+
if (!fs.existsSync(p)) return null;
|
|
81
|
+
try {
|
|
82
|
+
return readJson(p);
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function vendorComponents(prov) {
|
|
89
|
+
if (!prov || !prov.files) return [];
|
|
90
|
+
const out = [];
|
|
91
|
+
for (const [name, info] of Object.entries(prov.files)) {
|
|
92
|
+
out.push({
|
|
93
|
+
'bom-ref': `vendor:blamejs:${name}`,
|
|
94
|
+
type: 'library',
|
|
95
|
+
name: `blamejs/${name}`,
|
|
96
|
+
version: prov.pinned_commit ? prov.pinned_commit.slice(0, 12) : 'unknown',
|
|
97
|
+
description: `Vendored from blamejs/lib/${name} (flattened + stripped). See vendor/blamejs/README.md.`,
|
|
98
|
+
licenses: [{ license: { id: prov.license || 'Apache-2.0' } }],
|
|
99
|
+
hashes: [{ alg: 'SHA-256', content: info.vendored_sha256 }],
|
|
100
|
+
externalReferences: [
|
|
101
|
+
{ type: 'vcs', url: prov.source_repo || 'https://github.com/blamejs/blamejs' },
|
|
102
|
+
{ type: 'distribution', url: `${prov.source_repo || 'https://github.com/blamejs/blamejs'}/blob/${prov.pinned_commit}/${info.upstream_path}` },
|
|
103
|
+
],
|
|
104
|
+
properties: [
|
|
105
|
+
{ name: 'exceptd:vendor:upstream_sha256_at_pin', value: info.upstream_sha256_at_pin || '' },
|
|
106
|
+
{ name: 'exceptd:vendor:strip_summary', value: (info.stripped || []).join('; ') },
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildSbom() {
|
|
114
|
+
const pkg = readJson(PACKAGE_PATH);
|
|
115
|
+
const manifest = readJson(MANIFEST_PATH);
|
|
116
|
+
const catalogs = countDataCatalogs(DATA_DIR);
|
|
117
|
+
const timestamp = new Date().toISOString();
|
|
118
|
+
const skillCount = Array.isArray(manifest.skills) ? manifest.skills.length : 0;
|
|
119
|
+
const catalogCount = catalogs.length;
|
|
120
|
+
const vendorProv = loadVendorProvenance();
|
|
121
|
+
const vendoredComponents = vendorComponents(vendorProv);
|
|
122
|
+
|
|
123
|
+
const serialNumber =
|
|
124
|
+
'urn:uuid:' +
|
|
125
|
+
uuidV4FromSeed(`${pkg.name}@${pkg.version}@${timestamp}`);
|
|
126
|
+
|
|
127
|
+
const dataflowInput = catalogs
|
|
128
|
+
.map((c) => `data/${c}`)
|
|
129
|
+
.join(',');
|
|
130
|
+
|
|
131
|
+
const sbom = {
|
|
132
|
+
bomFormat: 'CycloneDX',
|
|
133
|
+
specVersion: '1.6',
|
|
134
|
+
serialNumber: serialNumber,
|
|
135
|
+
version: 1,
|
|
136
|
+
metadata: {
|
|
137
|
+
timestamp: timestamp,
|
|
138
|
+
tools: [
|
|
139
|
+
{
|
|
140
|
+
name: 'hand-written',
|
|
141
|
+
version: '0.1.0',
|
|
142
|
+
description:
|
|
143
|
+
'SBOM generated from package.json + manual review (scripts/refresh-sbom.js).',
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
component: {
|
|
147
|
+
// Switch from project: scheme to pkg:npm scheme post-v0.9.0 — the
|
|
148
|
+
// package is now published on npm with provenance attestation, so
|
|
149
|
+
// the CycloneDX bom-ref should reflect the canonical PURL.
|
|
150
|
+
'bom-ref': `pkg:npm/${pkg.name}@${pkg.version}`,
|
|
151
|
+
type: 'application',
|
|
152
|
+
name: pkg.name,
|
|
153
|
+
version: pkg.version,
|
|
154
|
+
description: pkg.description,
|
|
155
|
+
licenses: [{ license: { id: 'Apache-2.0' } }],
|
|
156
|
+
purl: `pkg:npm/${pkg.name.replace('@', '%40')}@${pkg.version}`,
|
|
157
|
+
externalReferences: [
|
|
158
|
+
{ type: 'distribution', url: `https://www.npmjs.com/package/${pkg.name}/v/${pkg.version}` },
|
|
159
|
+
{ type: 'vcs', url: (pkg.repository && pkg.repository.url) || 'https://github.com/blamejs/exceptd-skills' },
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
properties: [
|
|
163
|
+
{
|
|
164
|
+
name: 'cyclonedx:dataflow:input',
|
|
165
|
+
value: dataflowInput,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: 'exceptd:catalog:count',
|
|
169
|
+
value: String(catalogCount),
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: 'exceptd:skill:count',
|
|
173
|
+
value: String(skillCount),
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'exceptd:integrity:method',
|
|
177
|
+
value: 'Ed25519 per-skill (lib/sign.js)',
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'exceptd:runtime:dependency:count',
|
|
181
|
+
value: String(Object.keys(pkg.dependencies || {}).length),
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'exceptd:devDependency:count',
|
|
185
|
+
value: String(Object.keys(pkg.devDependencies || {}).length),
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: 'exceptd:vendor:count',
|
|
189
|
+
value: String(vendoredComponents.length),
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'exceptd:vendor:pin',
|
|
193
|
+
value: vendorProv?.pinned_commit
|
|
194
|
+
? `${vendorProv.source_repo}@${vendorProv.pinned_commit}`
|
|
195
|
+
: 'none',
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
},
|
|
199
|
+
components: vendoredComponents,
|
|
200
|
+
dependencies: [],
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return sbom;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function main() {
|
|
207
|
+
const sbom = buildSbom();
|
|
208
|
+
const json = JSON.stringify(sbom, null, 2) + '\n';
|
|
209
|
+
fs.writeFileSync(SBOM_PATH, json, 'utf8');
|
|
210
|
+
const lines = json.split(/\r?\n/).length;
|
|
211
|
+
process.stdout.write(
|
|
212
|
+
`wrote sbom.cdx.json — CycloneDX 1.6, ${lines} lines, ` +
|
|
213
|
+
`${sbom.metadata.properties.length} metadata.properties, ` +
|
|
214
|
+
`${sbom.components.length} components, serial ${sbom.serialNumber}\n`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (require.main === module) {
|
|
219
|
+
main();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = { buildSbom };
|