@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,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* lib/validate-indexes.js
|
|
4
|
+
*
|
|
5
|
+
* Predeploy gate. Confirms that `data/_indexes/*.json` is current
|
|
6
|
+
* against the canonical sources (manifest.json + every data/*.json
|
|
7
|
+
* minus _indexes/* + every skill body).
|
|
8
|
+
*
|
|
9
|
+
* Strategy:
|
|
10
|
+
* 1. Load `data/_indexes/_meta.json` for the source SHA-256 table.
|
|
11
|
+
* 2. Re-hash every listed source file.
|
|
12
|
+
* 3. Fail if any hash diverges (the indexes are stale).
|
|
13
|
+
* 4. Fail if a new source file exists that's not in the index (the
|
|
14
|
+
* index doesn't reflect current state).
|
|
15
|
+
*
|
|
16
|
+
* Exit 0 on success, 1 on staleness.
|
|
17
|
+
*
|
|
18
|
+
* Run as: node lib/validate-indexes.js
|
|
19
|
+
* Or as predeploy gate via scripts/predeploy.js.
|
|
20
|
+
*
|
|
21
|
+
* Re-build with: npm run build-indexes
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require("fs");
|
|
25
|
+
const path = require("path");
|
|
26
|
+
const crypto = require("crypto");
|
|
27
|
+
|
|
28
|
+
const ROOT = path.join(__dirname, "..");
|
|
29
|
+
const ABS = (p) => path.join(ROOT, p);
|
|
30
|
+
const IDX_DIR = ABS("data/_indexes");
|
|
31
|
+
const META = path.join(IDX_DIR, "_meta.json");
|
|
32
|
+
|
|
33
|
+
function sha256(buf) {
|
|
34
|
+
return crypto.createHash("sha256").update(buf).digest("hex");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!fs.existsSync(META)) {
|
|
38
|
+
console.error("[validate-indexes] data/_indexes/_meta.json missing — run `npm run build-indexes`.");
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const meta = JSON.parse(fs.readFileSync(META, "utf8"));
|
|
43
|
+
const recorded = meta.source_hashes || {};
|
|
44
|
+
|
|
45
|
+
// Discover the current canonical source set.
|
|
46
|
+
const manifest = JSON.parse(fs.readFileSync(ABS("manifest.json"), "utf8"));
|
|
47
|
+
const liveSources = new Set();
|
|
48
|
+
liveSources.add("manifest.json");
|
|
49
|
+
for (const f of fs.readdirSync(ABS("data"))) {
|
|
50
|
+
if (f.endsWith(".json")) liveSources.add("data/" + f);
|
|
51
|
+
}
|
|
52
|
+
for (const s of manifest.skills) liveSources.add(s.path);
|
|
53
|
+
|
|
54
|
+
const drift = [];
|
|
55
|
+
const missing = [];
|
|
56
|
+
const recordedKeys = new Set(Object.keys(recorded));
|
|
57
|
+
|
|
58
|
+
for (const p of liveSources) {
|
|
59
|
+
if (!recordedKeys.has(p)) {
|
|
60
|
+
missing.push(`new source not in index: ${p}`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const live = sha256(fs.readFileSync(ABS(p)));
|
|
64
|
+
if (live !== recorded[p]) {
|
|
65
|
+
drift.push(`hash drift: ${p} (recorded ${recorded[p].slice(0, 12)}…, live ${live.slice(0, 12)}…)`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const p of recordedKeys) {
|
|
69
|
+
if (!liveSources.has(p)) {
|
|
70
|
+
missing.push(`stale source in index (file removed): ${p}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const issues = [...drift, ...missing];
|
|
75
|
+
if (issues.length === 0) {
|
|
76
|
+
console.log(`[validate-indexes] indexes current — ${recordedKeys.size} sources hashed at ${meta.generated_at}.`);
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.error("[validate-indexes] indexes STALE:");
|
|
81
|
+
for (const i of issues) console.error(" • " + i);
|
|
82
|
+
console.error("[validate-indexes] regenerate with: npm run build-indexes");
|
|
83
|
+
process.exit(1);
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* lib/validate-package.js
|
|
4
|
+
*
|
|
5
|
+
* Predeploy + prepublishOnly gate. Runs `npm pack --dry-run --json` and
|
|
6
|
+
* asserts the publish tarball is what we expect:
|
|
7
|
+
*
|
|
8
|
+
* - includes every required file from package.json `files`
|
|
9
|
+
* - excludes every forbidden file (secrets, tests, caches, dev artifacts)
|
|
10
|
+
* - is under the size budget (currently 5 MB)
|
|
11
|
+
* - `bin/exceptd.js` has the expected shebang
|
|
12
|
+
* - the bin target listed in package.json exists on disk
|
|
13
|
+
*
|
|
14
|
+
* Exit 0 on success, 1 on any violation.
|
|
15
|
+
*
|
|
16
|
+
* Zero npm deps. Node 24 stdlib.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require("fs");
|
|
20
|
+
const path = require("path");
|
|
21
|
+
const { spawnSync } = require("child_process");
|
|
22
|
+
|
|
23
|
+
const ROOT = path.join(__dirname, "..");
|
|
24
|
+
const ABS = (p) => path.join(ROOT, p);
|
|
25
|
+
const SIZE_BUDGET_BYTES = 5 * 1024 * 1024; // 5 MB published-tarball cap
|
|
26
|
+
|
|
27
|
+
const REQUIRED_PATHS = [
|
|
28
|
+
"package.json",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"NOTICE",
|
|
32
|
+
"AGENTS.md",
|
|
33
|
+
"manifest.json",
|
|
34
|
+
"manifest-snapshot.json",
|
|
35
|
+
"sbom.cdx.json",
|
|
36
|
+
"bin/exceptd.js",
|
|
37
|
+
"lib/refresh-external.js",
|
|
38
|
+
"lib/job-queue.js",
|
|
39
|
+
"lib/prefetch.js",
|
|
40
|
+
"lib/worker-pool.js",
|
|
41
|
+
"lib/verify.js",
|
|
42
|
+
"vendor/blamejs/retry.js",
|
|
43
|
+
"vendor/blamejs/worker-pool.js",
|
|
44
|
+
"vendor/blamejs/_PROVENANCE.json",
|
|
45
|
+
"vendor/blamejs/LICENSE",
|
|
46
|
+
"data/_indexes/_meta.json",
|
|
47
|
+
"keys/public.pem",
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// File / directory prefixes that MUST NOT appear in the publish tarball.
|
|
51
|
+
const FORBIDDEN_PATTERNS = [
|
|
52
|
+
/(^|\/)\.keys(\/|$)/, // private signing key
|
|
53
|
+
/(^|\/)\.cache(\/|$)/, // local upstream cache
|
|
54
|
+
/(^|\/)tests(\/|$)/, // test sources + fixtures
|
|
55
|
+
/(^|\/)refresh-report\.json$/, // runtime artifact
|
|
56
|
+
/(^|\/)\.env(\b|\.)/, // any env file
|
|
57
|
+
/(^|\/)\.git(\/|$|hub\/)/, // git internals — but allow .github/workflows in repo,
|
|
58
|
+
// it's already excluded by .npmignore semantics for files[]
|
|
59
|
+
/(^|\/)\.DS_Store$/,
|
|
60
|
+
/(^|\/)node_modules(\/|$)/,
|
|
61
|
+
/\.pem$/, // catches .keys/private.pem if it sneaks in;
|
|
62
|
+
// keys/public.pem is whitelisted below
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const PEM_ALLOWLIST = new Set(["keys/public.pem"]);
|
|
66
|
+
|
|
67
|
+
function runNpmPack() {
|
|
68
|
+
// `npm pack --dry-run --json` writes a JSON array to stdout describing
|
|
69
|
+
// what would be in the tarball without actually creating it.
|
|
70
|
+
const res = spawnSync("npm", ["pack", "--dry-run", "--json"], { cwd: ROOT, encoding: "utf8", shell: process.platform === "win32" });
|
|
71
|
+
if (res.status !== 0) {
|
|
72
|
+
process.stderr.write(`[validate-package] npm pack failed (exit ${res.status}): ${res.stderr || res.stdout}\n`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
let parsed;
|
|
76
|
+
try {
|
|
77
|
+
parsed = JSON.parse(res.stdout);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
process.stderr.write(`[validate-package] could not parse npm pack output: ${err.message}\n`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
const first = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
83
|
+
if (!first || !Array.isArray(first.files)) {
|
|
84
|
+
process.stderr.write(`[validate-package] unexpected npm pack output shape\n`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
return first;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function main() {
|
|
91
|
+
const issues = [];
|
|
92
|
+
|
|
93
|
+
const pkg = JSON.parse(fs.readFileSync(ABS("package.json"), "utf8"));
|
|
94
|
+
|
|
95
|
+
// package.json sanity
|
|
96
|
+
if (pkg.private === true) issues.push(`package.json "private" is true — npm publish will fail`);
|
|
97
|
+
if (!pkg.bin || !pkg.bin.exceptd) issues.push(`package.json missing bin.exceptd`);
|
|
98
|
+
if (!Array.isArray(pkg.files) || pkg.files.length === 0) issues.push(`package.json missing files[] whitelist`);
|
|
99
|
+
if (!pkg.publishConfig || pkg.publishConfig.access !== "public") {
|
|
100
|
+
issues.push(`package.json missing publishConfig.access: public (scoped package needs explicit access)`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// bin target exists + has a shebang
|
|
104
|
+
if (pkg.bin && pkg.bin.exceptd) {
|
|
105
|
+
const binPath = ABS(pkg.bin.exceptd);
|
|
106
|
+
if (!fs.existsSync(binPath)) {
|
|
107
|
+
issues.push(`bin target ${pkg.bin.exceptd} does not exist`);
|
|
108
|
+
} else {
|
|
109
|
+
const head = fs.readFileSync(binPath, "utf8").slice(0, 64);
|
|
110
|
+
if (!head.startsWith("#!/usr/bin/env node") && !head.startsWith("#!/usr/bin/node")) {
|
|
111
|
+
issues.push(`bin/${path.basename(binPath)} missing #!/usr/bin/env node shebang`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// npm pack dry-run
|
|
117
|
+
const packInfo = runNpmPack();
|
|
118
|
+
const filePaths = packInfo.files.map((f) => f.path.replace(/\\/g, "/"));
|
|
119
|
+
const fileSet = new Set(filePaths);
|
|
120
|
+
|
|
121
|
+
// Required files present
|
|
122
|
+
for (const r of REQUIRED_PATHS) {
|
|
123
|
+
if (!fileSet.has(r)) {
|
|
124
|
+
issues.push(`required file missing from publish tarball: ${r}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Forbidden files absent
|
|
129
|
+
for (const p of filePaths) {
|
|
130
|
+
if (PEM_ALLOWLIST.has(p)) continue;
|
|
131
|
+
for (const re of FORBIDDEN_PATTERNS) {
|
|
132
|
+
if (re.test(p)) {
|
|
133
|
+
issues.push(`forbidden file in publish tarball: ${p} (matched ${re})`);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Size budget
|
|
140
|
+
if (typeof packInfo.size === "number" && packInfo.size > SIZE_BUDGET_BYTES) {
|
|
141
|
+
issues.push(`tarball size ${(packInfo.size / 1024 / 1024).toFixed(2)} MB exceeds budget ${(SIZE_BUDGET_BYTES / 1024 / 1024).toFixed(0)} MB`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (issues.length === 0) {
|
|
145
|
+
const sizeMB = (packInfo.size / 1024 / 1024).toFixed(2);
|
|
146
|
+
const unpackedMB = (packInfo.unpackedSize / 1024 / 1024).toFixed(2);
|
|
147
|
+
process.stdout.write(
|
|
148
|
+
`[validate-package] OK — ${pkg.name}@${pkg.version}, ` +
|
|
149
|
+
`${packInfo.files.length} files, ` +
|
|
150
|
+
`${sizeMB} MB packed / ${unpackedMB} MB unpacked.\n`
|
|
151
|
+
);
|
|
152
|
+
process.exit(0);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
process.stderr.write(`[validate-package] FAILED — ${issues.length} issue(s):\n`);
|
|
156
|
+
for (const i of issues) process.stderr.write(` • ${i}\n`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (require.main === module) main();
|
|
161
|
+
|
|
162
|
+
module.exports = { main, REQUIRED_PATHS, FORBIDDEN_PATTERNS, SIZE_BUDGET_BYTES };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* lib/validate-vendor.js
|
|
4
|
+
*
|
|
5
|
+
* Predeploy gate. Confirms every file recorded in `vendor/blamejs/_PROVENANCE.json`
|
|
6
|
+
* has the same SHA-256 on disk as the manifest claims. Silent hand-edits
|
|
7
|
+
* to a vendored copy fail the build.
|
|
8
|
+
*
|
|
9
|
+
* Also confirms vendor/blamejs/LICENSE matches the recorded license hash so
|
|
10
|
+
* a license-text change is detected as a separate event from a code change.
|
|
11
|
+
*
|
|
12
|
+
* Exit codes:
|
|
13
|
+
* 0 — vendor tree in sync with provenance
|
|
14
|
+
* 1 — at least one file drift / missing
|
|
15
|
+
*
|
|
16
|
+
* Re-vendor with: copy upstream, apply strip rules, refresh hashes in
|
|
17
|
+
* _PROVENANCE.json, re-run this gate.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require("fs");
|
|
21
|
+
const path = require("path");
|
|
22
|
+
const crypto = require("crypto");
|
|
23
|
+
|
|
24
|
+
const ROOT = path.join(__dirname, "..");
|
|
25
|
+
const PROV = path.join(ROOT, "vendor", "blamejs", "_PROVENANCE.json");
|
|
26
|
+
|
|
27
|
+
function sha256(buf) {
|
|
28
|
+
return crypto.createHash("sha256").update(buf).digest("hex");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function main() {
|
|
32
|
+
if (!fs.existsSync(PROV)) {
|
|
33
|
+
console.error("[validate-vendor] vendor/blamejs/_PROVENANCE.json missing.");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
const prov = JSON.parse(fs.readFileSync(PROV, "utf8"));
|
|
37
|
+
const issues = [];
|
|
38
|
+
|
|
39
|
+
// License file.
|
|
40
|
+
if (prov.license_file && prov.license_sha256) {
|
|
41
|
+
const p = path.join(ROOT, "vendor", "blamejs", prov.license_file);
|
|
42
|
+
if (!fs.existsSync(p)) {
|
|
43
|
+
issues.push(`missing license file: ${prov.license_file}`);
|
|
44
|
+
} else {
|
|
45
|
+
const live = sha256(fs.readFileSync(p));
|
|
46
|
+
if (live !== prov.license_sha256) {
|
|
47
|
+
issues.push(`LICENSE drift: recorded ${prov.license_sha256.slice(0, 12)}…, live ${live.slice(0, 12)}…`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Each vendored file.
|
|
53
|
+
for (const [name, info] of Object.entries(prov.files || {})) {
|
|
54
|
+
const p = path.join(ROOT, info.vendored_path);
|
|
55
|
+
if (!fs.existsSync(p)) {
|
|
56
|
+
issues.push(`missing vendored file: ${info.vendored_path}`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const live = sha256(fs.readFileSync(p));
|
|
60
|
+
if (live !== info.vendored_sha256) {
|
|
61
|
+
issues.push(`drift in ${info.vendored_path}: recorded ${info.vendored_sha256.slice(0, 12)}…, live ${live.slice(0, 12)}…`);
|
|
62
|
+
}
|
|
63
|
+
// Smoke-check the vendored module loads.
|
|
64
|
+
try {
|
|
65
|
+
require(p);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
issues.push(`load error in ${info.vendored_path}: ${err.message}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (issues.length === 0) {
|
|
72
|
+
const fileCount = Object.keys(prov.files || {}).length;
|
|
73
|
+
console.log(`[validate-vendor] vendor tree current — ${fileCount} file(s) validated against pin ${prov.pinned_commit?.slice(0, 12) || "?"}.`);
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.error("[validate-vendor] vendor tree DRIFT:");
|
|
78
|
+
for (const i of issues) console.error(" • " + i);
|
|
79
|
+
console.error("[validate-vendor] re-vendor instructions: vendor/blamejs/README.md");
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (require.main === module) main();
|
|
84
|
+
|
|
85
|
+
module.exports = { main };
|
package/lib/verify.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Skill integrity verifier — Ed25519 cryptographic signatures.
|
|
5
|
+
*
|
|
6
|
+
* SHA-256 hashes alone protect against accidental corruption; anyone with repo
|
|
7
|
+
* write access can update the hash after tampering. Ed25519 signatures prove a
|
|
8
|
+
* specific keypair signed each skill. Even if the manifest is updated, a valid
|
|
9
|
+
* signature requires the private key, which never enters this repository.
|
|
10
|
+
*
|
|
11
|
+
* Signing ceremony: see lib/sign.js
|
|
12
|
+
* Public key: keys/public.pem (tracked in repo)
|
|
13
|
+
* Private key: .keys/private.pem (gitignored, kept off-repo)
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* node lib/verify.js — verify all skills
|
|
17
|
+
* node lib/verify.js <name> — verify one skill
|
|
18
|
+
* node lib/verify.js update — re-sign all skills (requires private key)
|
|
19
|
+
* node lib/verify.js check-key — verify the public key is present and valid
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const crypto = require('crypto');
|
|
25
|
+
|
|
26
|
+
const ROOT = path.join(__dirname, '..');
|
|
27
|
+
const MANIFEST_PATH = path.join(ROOT, 'manifest.json');
|
|
28
|
+
const SKILLS_DIR = path.join(ROOT, 'skills');
|
|
29
|
+
const PUBLIC_KEY_PATH = path.join(ROOT, 'keys', 'public.pem');
|
|
30
|
+
const PRIVATE_KEY_PATH = path.join(ROOT, '.keys', 'private.pem');
|
|
31
|
+
|
|
32
|
+
// --- public API ---
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Verify all skills in manifest.json against the Ed25519 public key.
|
|
36
|
+
* @returns {{ valid: string[], invalid: string[], missing_sig: string[], missing_file: string[], no_key: boolean }}
|
|
37
|
+
*/
|
|
38
|
+
function verifyAll() {
|
|
39
|
+
const publicKey = loadPublicKey();
|
|
40
|
+
if (!publicKey) {
|
|
41
|
+
console.error('[verify] No public key at keys/public.pem — run: node lib/sign.js generate-keypair');
|
|
42
|
+
return { valid: [], invalid: [], missing_sig: [], missing_file: [], no_key: true };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const manifest = loadManifest();
|
|
46
|
+
const result = { valid: [], invalid: [], missing_sig: [], missing_file: [], no_key: false };
|
|
47
|
+
|
|
48
|
+
for (const skill of manifest.skills) {
|
|
49
|
+
const outcome = verifySkill(skill, publicKey);
|
|
50
|
+
result[outcome.status].push(skill.name);
|
|
51
|
+
if (outcome.status !== 'valid') {
|
|
52
|
+
console.error(`[verify] FAIL ${skill.name}: ${outcome.reason}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Verify one skill by name.
|
|
61
|
+
* @param {string} skillName
|
|
62
|
+
* @returns {{ status: string, reason?: string }}
|
|
63
|
+
*/
|
|
64
|
+
function verifyOne(skillName) {
|
|
65
|
+
const publicKey = loadPublicKey();
|
|
66
|
+
if (!publicKey) throw new Error('No public key at keys/public.pem');
|
|
67
|
+
|
|
68
|
+
const manifest = loadManifest();
|
|
69
|
+
const skill = manifest.skills.find(s => s.name === skillName);
|
|
70
|
+
if (!skill) throw new Error(`Skill not in manifest: ${skillName}`);
|
|
71
|
+
|
|
72
|
+
return verifySkill(skill, publicKey);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Re-sign all skills using the private key and write signatures to manifest.json.
|
|
77
|
+
* Requires .keys/private.pem — never checked in.
|
|
78
|
+
* @returns {{ signed: string[], errors: string[] }}
|
|
79
|
+
*/
|
|
80
|
+
function signAll() {
|
|
81
|
+
const privateKey = loadPrivateKey();
|
|
82
|
+
if (!privateKey) throw new Error('No private key at .keys/private.pem — run: node lib/sign.js generate-keypair');
|
|
83
|
+
|
|
84
|
+
const manifest = loadManifest();
|
|
85
|
+
const result = { signed: [], errors: [] };
|
|
86
|
+
|
|
87
|
+
for (const skill of manifest.skills) {
|
|
88
|
+
const skillPath = path.join(ROOT, skill.path);
|
|
89
|
+
if (!fs.existsSync(skillPath)) {
|
|
90
|
+
result.errors.push(`${skill.name}: file not found at ${skill.path}`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const content = fs.readFileSync(skillPath, 'utf8');
|
|
94
|
+
skill.signature = sign(content, privateKey);
|
|
95
|
+
skill.signed_at = new Date().toISOString();
|
|
96
|
+
delete skill.sha256;
|
|
97
|
+
result.signed.push(skill.name);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
|
|
101
|
+
console.log(`[verify] Signed ${result.signed.length} skills with Ed25519 private key.`);
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- private helpers ---
|
|
106
|
+
|
|
107
|
+
function verifySkill(skill, publicKey) {
|
|
108
|
+
if (!skill.signature) {
|
|
109
|
+
return { status: 'missing_sig', reason: 'No Ed25519 signature in manifest — run: node lib/sign.js sign-all' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const skillPath = path.join(ROOT, skill.path);
|
|
113
|
+
if (!fs.existsSync(skillPath)) {
|
|
114
|
+
return { status: 'missing_file', reason: `File not found: ${skill.path}` };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const content = fs.readFileSync(skillPath, 'utf8');
|
|
118
|
+
const valid = verify(content, skill.signature, publicKey);
|
|
119
|
+
|
|
120
|
+
if (!valid) {
|
|
121
|
+
return {
|
|
122
|
+
status: 'invalid',
|
|
123
|
+
reason: `Ed25519 signature verification failed — skill content has been modified since last signing`
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { status: 'valid' };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function sign(content, privateKey) {
|
|
131
|
+
const signature = crypto.sign(null, Buffer.from(content, 'utf8'), {
|
|
132
|
+
key: privateKey,
|
|
133
|
+
dsaEncoding: 'ieee-p1363'
|
|
134
|
+
});
|
|
135
|
+
return signature.toString('base64');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function verify(content, signatureBase64, publicKey) {
|
|
139
|
+
try {
|
|
140
|
+
const signature = Buffer.from(signatureBase64, 'base64');
|
|
141
|
+
return crypto.verify(null, Buffer.from(content, 'utf8'), {
|
|
142
|
+
key: publicKey,
|
|
143
|
+
dsaEncoding: 'ieee-p1363'
|
|
144
|
+
}, signature);
|
|
145
|
+
} catch (_) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function loadPublicKey() {
|
|
151
|
+
if (!fs.existsSync(PUBLIC_KEY_PATH)) return null;
|
|
152
|
+
return fs.readFileSync(PUBLIC_KEY_PATH, 'utf8');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function loadPrivateKey() {
|
|
156
|
+
if (!fs.existsSync(PRIVATE_KEY_PATH)) return null;
|
|
157
|
+
return fs.readFileSync(PRIVATE_KEY_PATH, 'utf8');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function loadManifest() {
|
|
161
|
+
return JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8'));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- CLI ---
|
|
165
|
+
|
|
166
|
+
if (require.main === module) {
|
|
167
|
+
const arg = process.argv[2];
|
|
168
|
+
|
|
169
|
+
if (arg === 'update') {
|
|
170
|
+
const result = signAll();
|
|
171
|
+
if (result.errors.length > 0) {
|
|
172
|
+
console.error('[verify] Errors during signing:', result.errors);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
console.log('[verify] All skills signed. Run node lib/verify.js to confirm.');
|
|
176
|
+
process.exit(0);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (arg === 'check-key') {
|
|
180
|
+
const pub = loadPublicKey();
|
|
181
|
+
if (!pub) {
|
|
182
|
+
console.error('[verify] No public key — run: node lib/sign.js generate-keypair');
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
console.log('[verify] Public key present at keys/public.pem');
|
|
186
|
+
try {
|
|
187
|
+
crypto.createPublicKey(pub);
|
|
188
|
+
console.log('[verify] Public key is valid Ed25519.');
|
|
189
|
+
process.exit(0);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
console.error('[verify] Public key is malformed:', e.message);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (arg && arg !== 'verify') {
|
|
197
|
+
const outcome = verifyOne(arg);
|
|
198
|
+
console.log(`${arg}: ${outcome.status}${outcome.reason ? ' — ' + outcome.reason : ''}`);
|
|
199
|
+
process.exit(outcome.status === 'valid' ? 0 : 1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const result = verifyAll();
|
|
203
|
+
if (result.no_key) process.exit(1);
|
|
204
|
+
|
|
205
|
+
const total = Object.values(result).filter(Array.isArray).flat().length;
|
|
206
|
+
console.log(`\n[verify] ${result.valid.length}/${total} skills passed Ed25519 verification.`);
|
|
207
|
+
|
|
208
|
+
if (result.invalid.length > 0) { console.error('[verify] TAMPERED:', result.invalid.join(', ')); process.exit(1); }
|
|
209
|
+
if (result.missing_sig.length > 0) { console.warn('[verify] UNSIGNED:', result.missing_sig.join(', ')); process.exit(1); }
|
|
210
|
+
if (result.missing_file.length > 0) { console.error('[verify] MISSING:', result.missing_file.join(', ')); process.exit(1); }
|
|
211
|
+
|
|
212
|
+
console.log('[verify] All skills verified.');
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = { verifyAll, verifyOne, signAll };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* lib/worker-pool.js
|
|
4
|
+
*
|
|
5
|
+
* Thin convenience wrapper over the vendored blamejs worker-pool primitive.
|
|
6
|
+
* The vendored module (vendor/blamejs/worker-pool.js) provides:
|
|
7
|
+
*
|
|
8
|
+
* - bounded concurrency, defaults to max(2, cpus)
|
|
9
|
+
* - bounded in-memory queue (default 1024 depth)
|
|
10
|
+
* - per-task timeout (default 5min)
|
|
11
|
+
* - worker recycle on uncaught error / timeout / exit
|
|
12
|
+
*
|
|
13
|
+
* What this wrapper adds:
|
|
14
|
+
*
|
|
15
|
+
* - WorkerPool class around the function-style `create()` API for
|
|
16
|
+
* callers that prefer an instance
|
|
17
|
+
* - runAll(tasks, opts) helper that runs an array of tasks through a
|
|
18
|
+
* fresh pool and terminates it when done
|
|
19
|
+
* - DEFAULT_SIZE re-export for callers that want to size manually
|
|
20
|
+
*
|
|
21
|
+
* Honest framing: at v0.7.0 corpus size (38 skills, 10 catalogs, ~150ms
|
|
22
|
+
* total build time) worker-thread spawn cost is comparable to the work
|
|
23
|
+
* itself. The pool is here so the architecture scales as the corpus
|
|
24
|
+
* grows and so users can experiment with `--parallel`. Sequential builds
|
|
25
|
+
* remain the default.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const os = require("os");
|
|
29
|
+
const vendored = require("../vendor/blamejs/worker-pool");
|
|
30
|
+
|
|
31
|
+
const DEFAULT_SIZE = Math.max(1, Math.min(8, os.cpus()?.length || 4));
|
|
32
|
+
|
|
33
|
+
class WorkerPool {
|
|
34
|
+
/**
|
|
35
|
+
* @param {object} opts
|
|
36
|
+
* - runnerPath: absolute path to the worker script (required)
|
|
37
|
+
* - size, maxQueueDepth, taskTimeoutMs, onExit — forwarded to vendored.create
|
|
38
|
+
*/
|
|
39
|
+
constructor(opts = {}) {
|
|
40
|
+
if (!opts.runnerPath) throw new Error("WorkerPool: runnerPath is required");
|
|
41
|
+
const { runnerPath, ...rest } = opts;
|
|
42
|
+
if (rest.size === undefined) rest.size = DEFAULT_SIZE;
|
|
43
|
+
this._pool = vendored.create(runnerPath, rest);
|
|
44
|
+
}
|
|
45
|
+
run(message, transferList) {
|
|
46
|
+
return this._pool.run(message, transferList);
|
|
47
|
+
}
|
|
48
|
+
drain() {
|
|
49
|
+
return this._pool.drain();
|
|
50
|
+
}
|
|
51
|
+
terminate() {
|
|
52
|
+
return this._pool.terminate();
|
|
53
|
+
}
|
|
54
|
+
stats() {
|
|
55
|
+
return this._pool.stats();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Run a list of tasks against a fresh pool, await all results, terminate.
|
|
61
|
+
*/
|
|
62
|
+
async function runAll(tasks, opts = {}) {
|
|
63
|
+
const pool = new WorkerPool(opts);
|
|
64
|
+
try {
|
|
65
|
+
return await Promise.all(tasks.map((t) => pool.run(t)));
|
|
66
|
+
} finally {
|
|
67
|
+
await pool.terminate();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
WorkerPool,
|
|
73
|
+
runAll,
|
|
74
|
+
DEFAULT_SIZE,
|
|
75
|
+
// Re-export vendored primitives for callers that prefer the function-style API
|
|
76
|
+
// or need the size constants.
|
|
77
|
+
create: vendored.create,
|
|
78
|
+
MIN_SIZE: vendored.MIN_SIZE,
|
|
79
|
+
MAX_SIZE: vendored.MAX_SIZE,
|
|
80
|
+
DEFAULT_MAX_QUEUE_DEPTH: vendored.DEFAULT_MAX_QUEUE_DEPTH,
|
|
81
|
+
MAX_QUEUE_DEPTH_CAP: vendored.MAX_QUEUE_DEPTH_CAP,
|
|
82
|
+
DEFAULT_TASK_TIMEOUT_MS: vendored.DEFAULT_TASK_TIMEOUT_MS,
|
|
83
|
+
MAX_TASK_TIMEOUT_MS: vendored.MAX_TASK_TIMEOUT_MS,
|
|
84
|
+
};
|