@blamejs/exceptd-skills 0.19.33 → 0.19.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/bin/exceptd.js +895 -2828
- package/data/_indexes/_meta.json +2 -2
- package/lib/auto-discovery.js +56 -286
- package/lib/canonical-eq.js +7 -40
- package/lib/citation-resolve.js +22 -70
- package/lib/collectors/ai-api.js +170 -76
- package/lib/collectors/cicd-pipeline-compromise.js +113 -136
- package/lib/collectors/citation-hygiene.js +72 -210
- package/lib/collectors/containers.js +41 -130
- package/lib/collectors/cred-stores.js +31 -115
- package/lib/collectors/crypto-codebase.js +55 -138
- package/lib/collectors/crypto.js +24 -54
- package/lib/collectors/hardening.js +20 -78
- package/lib/collectors/kernel.js +16 -46
- package/lib/collectors/library-author.js +198 -211
- package/lib/collectors/mcp.js +24 -70
- package/lib/collectors/runtime.js +24 -86
- package/lib/collectors/sbom.js +130 -118
- package/lib/collectors/scan-excludes.js +33 -139
- package/lib/collectors/secrets.js +62 -178
- package/lib/cross-ref-api.js +39 -123
- package/lib/currency-severity.js +8 -27
- package/lib/cve-batch.js +13 -21
- package/lib/cve-cli.js +13 -20
- package/lib/cve-curation.js +72 -239
- package/lib/cve-regression-watcher.js +29 -155
- package/lib/cvss.js +13 -54
- package/lib/doctor-bucketing.js +3 -19
- package/lib/exit-codes.js +10 -42
- package/lib/flag-suggest.js +7 -25
- package/lib/framework-gap.js +39 -113
- package/lib/gap-detectors.js +37 -159
- package/lib/id-validation.js +9 -30
- package/lib/job-queue.js +13 -36
- package/lib/lint-skills.js +88 -236
- package/lib/playbook-runner.js +759 -2107
- package/lib/prefetch.js +101 -376
- package/lib/refresh-external.js +199 -633
- package/lib/refresh-network.js +78 -311
- package/lib/rfc-cli.js +23 -68
- package/lib/scoring.js +85 -146
- package/lib/sign.js +43 -229
- package/lib/source-advisories.js +43 -194
- package/lib/source-ghsa.js +37 -120
- package/lib/source-osv.js +94 -266
- package/lib/ttp-mapper.js +28 -27
- package/lib/upstream-check-cli.js +36 -29
- package/lib/upstream-check.js +19 -44
- package/lib/validate-catalog-meta.js +17 -61
- package/lib/validate-cve-catalog.js +52 -121
- package/lib/validate-indexes.js +25 -76
- package/lib/validate-package.js +16 -62
- package/lib/validate-playbooks.js +78 -286
- package/lib/validate-vendor.js +16 -49
- package/lib/verify.js +56 -286
- package/lib/version-pins.js +5 -34
- package/lib/worker-pool.js +11 -30
- package/lib/xml-tokenizer.js +47 -152
- package/manifest.json +53 -53
- package/orchestrator/dispatcher.js +17 -68
- package/orchestrator/event-bus.js +11 -74
- package/orchestrator/index.js +138 -413
- package/orchestrator/pipeline.js +28 -85
- package/orchestrator/scanner.js +34 -138
- package/orchestrator/scheduler.js +20 -84
- package/package.json +1 -1
- package/sbom.cdx.json +242 -242
- package/scripts/audit-catalog-gaps.js +9 -62
- package/scripts/audit-cross-skill.js +5 -31
- package/scripts/audit-perf.js +29 -28
- package/scripts/backfill-theater-test.js +7 -64
- package/scripts/bootstrap.js +12 -44
- package/scripts/build-indexes.js +40 -154
- package/scripts/builders/activity-feed.js +4 -14
- package/scripts/builders/catalog-summaries.js +3 -10
- package/scripts/builders/currency.js +7 -20
- package/scripts/builders/cwe-chains.js +7 -30
- package/scripts/builders/did-ladders.js +6 -13
- package/scripts/builders/frequency.js +5 -19
- package/scripts/builders/jurisdiction-clocks.js +6 -25
- package/scripts/builders/recipes.js +6 -14
- package/scripts/builders/section-offsets.js +13 -51
- package/scripts/builders/stale-content.js +7 -28
- package/scripts/builders/summary-cards.js +8 -29
- package/scripts/builders/theater-fingerprints.js +21 -31
- package/scripts/builders/token-budget.js +4 -31
- package/scripts/check-agents-md-collectors.js +26 -57
- package/scripts/check-catalog-gap-budget.js +15 -32
- package/scripts/check-changelog-extract.js +18 -48
- package/scripts/check-codebase-patterns-currency.js +6 -22
- package/scripts/check-codebase-patterns.js +63 -143
- package/scripts/check-epss-consistency.js +9 -64
- package/scripts/check-framework-gap-coverage.js +13 -31
- package/scripts/check-manifest-snapshot.js +62 -81
- package/scripts/check-sbom-currency.js +44 -142
- package/scripts/check-test-count.js +15 -52
- package/scripts/check-test-coverage.js +83 -198
- package/scripts/check-test-subjects.js +21 -62
- package/scripts/check-ttp-references.js +14 -38
- package/scripts/check-ttp-upstream.js +8 -40
- package/scripts/check-version-bump.js +9 -61
- package/scripts/check-version-tags.js +20 -121
- package/scripts/predeploy.js +38 -184
- package/scripts/refresh-manifest-snapshot.js +16 -38
- package/scripts/refresh-mitre-atlas.js +7 -8
- package/scripts/refresh-mitre-attack.js +1 -8
- package/scripts/refresh-mitre-d3fend.js +3 -9
- package/scripts/refresh-mitre-ics-attack.js +7 -8
- package/scripts/refresh-reverse-refs.js +27 -94
- package/scripts/refresh-rfc-index.js +7 -10
- package/scripts/refresh-sbom.js +31 -161
- package/scripts/refresh-upstream-catalogs.js +63 -148
- package/scripts/release.js +69 -234
- package/scripts/run-e2e-scenarios.js +26 -73
- package/scripts/sync-manifest-metadata.js +10 -34
- package/scripts/sync-package-description.js +8 -17
- package/scripts/validate-vendor-online.js +13 -44
- package/scripts/verify-shipped-tarball.js +35 -141
|
@@ -2,43 +2,11 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* operator-facing-surface rules includes string literals that ship to
|
|
11
|
-
* operators — CLI `--help` text, error messages, and test descriptions —
|
|
12
|
-
* not just `//` comments. The scan therefore tests the WHOLE line, so a
|
|
13
|
-
* `version: '0.18.7'` data literal or a `--flag (v0.18.7)` help string
|
|
14
|
-
* counts the same as a `// v0.18.7` comment. Genuinely-load-bearing
|
|
15
|
-
* version references (real test fixtures, deprecation timelines) get the
|
|
16
|
-
* file added to COMMENT_EXEMPT below. The authoritative version surfaces
|
|
17
|
-
* are:
|
|
18
|
-
*
|
|
19
|
-
* 1. package.json / manifest.json `"version"` field
|
|
20
|
-
* 2. CHANGELOG.md `## X.Y.Z` headings
|
|
21
|
-
* 3. git tags
|
|
22
|
-
* 4. CLI `version` verb output (reads from package.json)
|
|
23
|
-
*
|
|
24
|
-
* Anywhere else, `// v0.13.22` / `Pre-v0.13.22` / `*-v0_13_22.test.js`
|
|
25
|
-
* is phase residue — operators don't have the roadmap, version tags
|
|
26
|
-
* rot the moment the next release lands, and `git clone` ships every
|
|
27
|
-
* comment to operators along with the code.
|
|
28
|
-
*
|
|
29
|
-
* The check uses a baseline snapshot (`tests/.version-tag-baseline.
|
|
30
|
-
* json`) capturing current violation counts per file. Future scans
|
|
31
|
-
* compare against the baseline:
|
|
32
|
-
*
|
|
33
|
-
* - Filename violations beyond baseline → fail.
|
|
34
|
-
* - Line violations beyond baseline (in any file) → fail.
|
|
35
|
-
* - Violations strictly within baseline → ok.
|
|
36
|
-
* - Violations below baseline (drift reduced) → ok +
|
|
37
|
-
* suggestion to refresh the baseline.
|
|
38
|
-
*
|
|
39
|
-
* Refresh: `node scripts/check-version-tags.js --update-baseline`.
|
|
40
|
-
*
|
|
41
|
-
* Wired into `npm run predeploy` as a gate.
|
|
5
|
+
* Predeploy gate refusing new version-stamped lines and filenames in the tracked
|
|
6
|
+
* source tree. Whole lines, not just comments: a stamp in a data literal or a
|
|
7
|
+
* help string is residue too, and a load-bearing one is exempted by path in
|
|
8
|
+
* COMMENT_EXEMPT rather than by narrowing the scan. Counts must not rise above
|
|
9
|
+
* tests/.version-tag-baseline.json; refresh it with `--update-baseline`.
|
|
42
10
|
*/
|
|
43
11
|
|
|
44
12
|
const fs = require("node:fs");
|
|
@@ -48,24 +16,14 @@ const { execFileSync } = require("node:child_process");
|
|
|
48
16
|
const ROOT = path.join(__dirname, "..");
|
|
49
17
|
const BASELINE_PATH = path.join(ROOT, "tests", ".version-tag-baseline.json");
|
|
50
18
|
|
|
51
|
-
// Directories we do not walk at all.
|
|
52
19
|
const SKIP_DIRS = new Set([
|
|
53
20
|
"node_modules", ".git", ".keys", ".cache", ".scratch",
|
|
54
21
|
"data", "vendor", ".husky",
|
|
55
22
|
]);
|
|
56
23
|
|
|
57
|
-
// File extensions we scan for comment violations.
|
|
58
24
|
const SCAN_EXTS = new Set([".js", ".cjs", ".mjs", ".md"]);
|
|
59
25
|
|
|
60
|
-
// Paths
|
|
61
|
-
// - CHANGELOG headings are how operators navigate the file
|
|
62
|
-
// - package.json / manifest.json carry the canonical version field
|
|
63
|
-
// - manifest-snapshot.json + sbom.cdx.json contain version-pinned
|
|
64
|
-
// metadata (the SBOM IS a version-stamped manifest)
|
|
65
|
-
// - lib/version-pins.js is a version-constant lookup table
|
|
66
|
-
// - This checker itself documents what it forbids
|
|
67
|
-
// - .git-blame-ignore-revs carries commit hashes, not version tags,
|
|
68
|
-
// but is conventional config the user maintains
|
|
26
|
+
// Paths where a version reference is load-bearing.
|
|
69
27
|
const COMMENT_EXEMPT = new Set([
|
|
70
28
|
"package.json",
|
|
71
29
|
"manifest.json",
|
|
@@ -74,45 +32,20 @@ const COMMENT_EXEMPT = new Set([
|
|
|
74
32
|
"CHANGELOG.md",
|
|
75
33
|
"lib/version-pins.js",
|
|
76
34
|
"scripts/check-version-tags.js",
|
|
77
|
-
//
|
|
78
|
-
// extraction + the shorter-vs-longer prefix-collision guard, so its fixtures
|
|
79
|
-
// MUST embed real `## X.Y.Z` headings (e.g. 0.15.5 vs 0.15.50) — load-bearing
|
|
80
|
-
// test data, not sprinkled release tags.
|
|
35
|
+
// Fixtures embed real `## X.Y.Z` headings, including a prefix collision.
|
|
81
36
|
"tests/check-changelog-extract.test.js",
|
|
82
|
-
//
|
|
83
|
-
// tags that exist with no published release (outage-recovery bumps), so the
|
|
84
|
-
// heading-completeness check can skip them — load-bearing references to git
|
|
85
|
-
// tags, an authoritative version surface.
|
|
37
|
+
// Allowlists the exact versions of tags with no published release.
|
|
86
38
|
"scripts/check-changelog-extract.js",
|
|
87
|
-
//
|
|
88
|
-
// shows an example ack naming a target version, and its test compares real
|
|
89
|
-
// X.Y.Z transitions (patch vs minor vs major vs downgrade). Those version
|
|
90
|
-
// literals are load-bearing data, not sprinkled release tags.
|
|
39
|
+
// Version comparison is the subject: real X.Y.Z transitions under test.
|
|
91
40
|
"scripts/check-version-bump.js",
|
|
92
41
|
"tests/version-bump-cadence.test.js",
|
|
93
|
-
// The
|
|
94
|
-
// IPv4 / longer-run boundaries and the PHASE_RESIDUE_RES / FILENAME_VERSION_RE
|
|
95
|
-
// / countLineViolations exports, so it MUST embed literal stamps like
|
|
96
|
-
// `0.18.9.`, `0.18.99`, `Pre-0.13.22`, and `foo-v0_13_2.test.js` as the inputs
|
|
97
|
-
// under test — load-bearing data for the detector's boundary cases, not
|
|
98
|
-
// sprinkled release tags.
|
|
42
|
+
// The detector's own boundary cases appear literally as the inputs under test.
|
|
99
43
|
"tests/check-version-tags.test.js",
|
|
100
44
|
]);
|
|
101
45
|
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
// ARE still scanned: a new file a contributor is about to commit is exactly
|
|
106
|
-
// what the gate must catch. Computed via `git check-ignore` over the walked set.
|
|
107
|
-
// Returns the ignored subset, or NULL when git cannot answer.
|
|
108
|
-
//
|
|
109
|
-
// "No path matched" and "the question could not be asked" are different
|
|
110
|
-
// results and must not collapse into the same empty set. Without a repository
|
|
111
|
-
// — a build context that omits .git/, or git not installed — an empty set
|
|
112
|
-
// silently reclassifies every local-only file as part of the shipped surface,
|
|
113
|
-
// so the gate reports violations in files a clone never contains. Returning
|
|
114
|
-
// null lets the caller say it could not determine the surface instead of
|
|
115
|
-
// asserting a wrong one.
|
|
46
|
+
// The ignored subset of `relPaths`, or null when git cannot answer. "No path
|
|
47
|
+
// matched" and "the question could not be asked" must not collapse into the same
|
|
48
|
+
// empty set, which would reclassify every local-only file as shipped surface.
|
|
116
49
|
function gitIgnoredSet(relPaths) {
|
|
117
50
|
if (!relPaths.length) return new Set();
|
|
118
51
|
try {
|
|
@@ -122,9 +55,7 @@ function gitIgnoredSet(relPaths) {
|
|
|
122
55
|
});
|
|
123
56
|
return new Set(out.split(/\r?\n/).filter(Boolean));
|
|
124
57
|
} catch (e) {
|
|
125
|
-
// Exit 1 with no stderr is git's
|
|
126
|
-
// answer, and an empty set is correct. Anything else (git missing, not a
|
|
127
|
-
// repository, .git absent) means the question went unanswered.
|
|
58
|
+
// Exit 1 with no stderr is git's "no path matched" — a real answer, not a failure.
|
|
128
59
|
const status = e && typeof e.status === "number" ? e.status : null;
|
|
129
60
|
const stderr = e && e.stderr ? String(e.stderr).trim() : "";
|
|
130
61
|
const out = e && e.stdout ? String(e.stdout) : "";
|
|
@@ -133,20 +64,11 @@ function gitIgnoredSet(relPaths) {
|
|
|
133
64
|
}
|
|
134
65
|
}
|
|
135
66
|
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
// `
|
|
139
|
-
// The trailing lookahead rejects a longer minor/patch digit (so `0.18.99`
|
|
140
|
-
// still matches, but the stamp can't be part of a wider number) and a
|
|
141
|
-
// dot-followed-by-digit (an IPv4 next octet / longer dotted-numeric run, e.g.
|
|
142
|
-
// `127.0.0.1`, whose `0.0.1` tail would otherwise register). A sentence-ending
|
|
143
|
-
// period after the patch (dot followed by non-digit / end-of-line, e.g.
|
|
144
|
-
// `// fixed in 0.18.9.`) is NOT excluded — that is exactly the version residue
|
|
145
|
-
// the gate must catch. The leading `(?<![\d.])` lookbehind keeps the IPv4
|
|
146
|
-
// suppression on the other side.
|
|
67
|
+
// A pre-1.0 project version, `v0.13.22` or bare; a non-0.x external version such
|
|
68
|
+
// as CycloneDX `1.6` misses. The lookarounds keep the stamp out of a wider number
|
|
69
|
+
// or a dotted run like `127.0.0.1`, whose tail would otherwise register.
|
|
147
70
|
const VERSION_TAG_RE = /(?<![\d.])v?0\.\d+\.\d+(?!\d)(?!\.\d)/;
|
|
148
71
|
|
|
149
|
-
// Phase residue patterns — broader than just version tags.
|
|
150
72
|
const PHASE_RESIDUE_RES = [
|
|
151
73
|
/\bcycle\s+\d+\b/i, // "cycle 13 P3 F3"
|
|
152
74
|
/\bphase\s+\d+(\.\d+)+\b/i,// "phase 9.11k"
|
|
@@ -173,12 +95,6 @@ function walk(dir, results = []) {
|
|
|
173
95
|
return results;
|
|
174
96
|
}
|
|
175
97
|
|
|
176
|
-
// Counts version-stamp lines in a file. Intentionally WHOLE-LINE, not
|
|
177
|
-
// comment-only: a 0.x stamp inside a shipped string literal (CLI --help text,
|
|
178
|
-
// an error message, a test description) is operator-readable residue just like
|
|
179
|
-
// a `//` comment, so it counts the same. A file with a genuinely load-bearing
|
|
180
|
-
// version literal (real test fixture, deprecation timeline) is exempted by path
|
|
181
|
-
// in COMMENT_EXEMPT, not by narrowing the scan.
|
|
182
98
|
function countLineViolations(rel) {
|
|
183
99
|
if (COMMENT_EXEMPT.has(rel)) return 0;
|
|
184
100
|
const ext = path.extname(rel);
|
|
@@ -199,16 +115,11 @@ function countLineViolations(rel) {
|
|
|
199
115
|
function scanCurrent() {
|
|
200
116
|
const files = walk(ROOT);
|
|
201
117
|
const ignored = gitIgnoredSet(files);
|
|
202
|
-
// Without git
|
|
203
|
-
// indistinguishable from tracked ones, so any result would be a guess.
|
|
204
|
-
// Report that rather than emit findings the baseline cannot be compared to.
|
|
118
|
+
// Without git, local-only files are indistinguishable from tracked ones.
|
|
205
119
|
if (ignored === null) return { byFile: {}, filenameViolations: [], surfaceUnknown: true };
|
|
206
120
|
const byFile = {};
|
|
207
121
|
const filenameViolations = [];
|
|
208
122
|
for (const rel of files) {
|
|
209
|
-
// Skip git-ignored, local-only files that `git clone` never ships.
|
|
210
|
-
// Untracked-but-not-ignored files are still scanned — a new file about to
|
|
211
|
-
// be committed is exactly what the gate guards.
|
|
212
123
|
if (ignored.has(rel)) continue;
|
|
213
124
|
if (FILENAME_VERSION_RE.test(rel)) filenameViolations.push(rel);
|
|
214
125
|
const n = countLineViolations(rel);
|
|
@@ -248,15 +159,8 @@ function main() {
|
|
|
248
159
|
const current = scanCurrent();
|
|
249
160
|
|
|
250
161
|
if (current.surfaceUnknown) {
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
// Automation is the one place this must not degrade to a skip. This gate
|
|
255
|
-
// runs inside predeploy, and predeploy guards the publish job, so a
|
|
256
|
-
// silently-skipped run there stops enforcing on exactly the path that
|
|
257
|
-
// ships. Locally — a container built without .git, a tarball inspection —
|
|
258
|
-
// skipping is the honest answer, because the shipped surface genuinely is
|
|
259
|
-
// not knowable there and failing would only punish the harness.
|
|
162
|
+
// A baseline written from this scan would bake in the wrong surface. In
|
|
163
|
+
// automation this fails rather than skips: predeploy guards publishing.
|
|
260
164
|
const inAutomation = process.env.CI === "true" || !!process.env.GITHUB_ACTIONS;
|
|
261
165
|
if (inAutomation) {
|
|
262
166
|
console.error("[check-version-tags] FAIL — no git repository available, so the shipped");
|
|
@@ -291,8 +195,6 @@ function main() {
|
|
|
291
195
|
|
|
292
196
|
const regressions = [];
|
|
293
197
|
|
|
294
|
-
// Filename regressions: any new filename matching the pattern that
|
|
295
|
-
// wasn't in the baseline.
|
|
296
198
|
for (const rel of current.filenameViolations) {
|
|
297
199
|
if (!baseline.filenameViolations.includes(rel)) {
|
|
298
200
|
regressions.push({
|
|
@@ -303,7 +205,6 @@ function main() {
|
|
|
303
205
|
}
|
|
304
206
|
}
|
|
305
207
|
|
|
306
|
-
// Comment regressions: per-file count grew.
|
|
307
208
|
for (const [rel, n] of Object.entries(current.byFile)) {
|
|
308
209
|
const prior = baseline.byFile[rel] || 0;
|
|
309
210
|
if (n > prior) {
|
|
@@ -317,11 +218,9 @@ function main() {
|
|
|
317
218
|
}
|
|
318
219
|
}
|
|
319
220
|
|
|
320
|
-
// Files newly added to the violation set (not in baseline at all).
|
|
321
221
|
for (const rel of Object.keys(current.byFile)) {
|
|
322
222
|
if (!(rel in baseline.byFile)) {
|
|
323
223
|
const n = current.byFile[rel];
|
|
324
|
-
// Skip if already captured as a count regression above.
|
|
325
224
|
if (regressions.some(r => r.path === rel)) continue;
|
|
326
225
|
regressions.push({
|
|
327
226
|
kind: "comment",
|
package/scripts/predeploy.js
CHANGED
|
@@ -1,33 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
* when the manifest-snapshot gate fails, the fix is NOT to
|
|
24
|
-
* run `npm run refresh-snapshot` blindly. The refresh script now refuses
|
|
25
|
-
* unless the operator passes `--commit-only` or sets
|
|
26
|
-
* EXCEPTD_SNAPSHOT_AUDIT_ACK=1. This is intentional: a failing snapshot
|
|
27
|
-
* gate means a breaking change was detected, and an accidental refresh
|
|
28
|
-
* would silently rewrite the baseline. Read the breaking-change list
|
|
29
|
-
* first, then run `node scripts/refresh-manifest-snapshot.js --commit-only`
|
|
30
|
-
* if the change is intentional.
|
|
3
|
+
* Local mirror of the CI pre-deployment gate sequence. Gates are isolated, so one
|
|
4
|
+
* failure does not short-circuit the rest and a single run surfaces every problem.
|
|
5
|
+
* Exit 0 all passed, 1 one or more failed, 2 runner-level error.
|
|
31
6
|
*/
|
|
32
7
|
|
|
33
8
|
const { execFileSync } = require("child_process");
|
|
@@ -36,10 +11,7 @@ const fs = require("fs");
|
|
|
36
11
|
|
|
37
12
|
const ROOT = path.join(__dirname, "..");
|
|
38
13
|
|
|
39
|
-
// Ordered
|
|
40
|
-
// ciJobName matches the `name:` field of the corresponding job in
|
|
41
|
-
// .github/workflows/ci.yml (or scorecard.yml). Used by the workflow-sync
|
|
42
|
-
// test to assert the two never drift.
|
|
14
|
+
// Ordered CI gates; `ciJobName` matches the job `name:` in .github/workflows/ci.yml.
|
|
43
15
|
const GATES = [
|
|
44
16
|
{
|
|
45
17
|
name: "Verify skill signatures (Ed25519)",
|
|
@@ -51,39 +23,19 @@ const GATES = [
|
|
|
51
23
|
{
|
|
52
24
|
name: "Run tests (node:test)",
|
|
53
25
|
command: process.execPath,
|
|
54
|
-
// Glob
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
// sits inside a path containing parentheses (e.g. Dropbox).
|
|
58
|
-
//
|
|
59
|
-
// --test-concurrency=1 forces sequential file execution. Several
|
|
60
|
-
// test files (build-incremental, indexes-v070, refresh-*) touch
|
|
61
|
-
// shared filesystem state under data/_indexes/ + refresh-report.json
|
|
62
|
-
// + skill bodies; running in parallel produces flaky races. Sequential
|
|
63
|
-
// is ~1.5s slower locally but eliminates the false negative we hit
|
|
64
|
-
// on the Linux CI runner in the v0.9.0 release attempt.
|
|
26
|
+
// Glob, not a directory arg: on Windows a bare directory resolves through the
|
|
27
|
+
// module loader and fails under a path containing parentheses. --test-concurrency=1
|
|
28
|
+
// is required — build-incremental, indexes-v070 and refresh-* share state.
|
|
65
29
|
args: ["--test", "--test-concurrency=1", "tests/*.test.js"],
|
|
66
30
|
ciJobName: "Tests",
|
|
67
31
|
},
|
|
68
32
|
{
|
|
69
33
|
name: "Validate CVE catalog schema + zero-day learning coverage",
|
|
70
34
|
command: process.execPath,
|
|
71
|
-
// --strict promotes
|
|
72
|
-
// resolution, strict CVSS-vector prefix, KEV-date-required, Hard-Rule-#14
|
|
73
|
-
// IoCs) to hard failures so they block a release rather than scrolling
|
|
74
|
-
// past. Auto-imported drafts stay exempt.
|
|
35
|
+
// --strict promotes deferred warnings to hard failures; drafts stay exempt.
|
|
75
36
|
args: [path.join(ROOT, "lib", "validate-cve-catalog.js"), "--strict"],
|
|
76
37
|
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
77
38
|
},
|
|
78
|
-
// the "validate-cves --offline --no-fail" and
|
|
79
|
-
// "validate-rfcs --offline --no-fail" gates were enumeration-only sanity
|
|
80
|
-
// checks: `--no-fail` forced them to always exit 0, so they never blocked
|
|
81
|
-
// a release on a real catalog problem. The deep catalog validation is
|
|
82
|
-
// already performed by the gate above (`lib/validate-cve-catalog.js`),
|
|
83
|
-
// including cross-catalog reference resolution after this same audit.
|
|
84
|
-
// Keeping the no-op gates as predeploy steps inflated the gate count for
|
|
85
|
-
// no marginal value and risked false confidence ("X gates passed"). They
|
|
86
|
-
// are removed in v0.12.14; document the removal in CHANGELOG.
|
|
87
39
|
{
|
|
88
40
|
name: "Manifest snapshot gate (breaking-change detector)",
|
|
89
41
|
command: process.execPath,
|
|
@@ -97,13 +49,7 @@ const GATES = [
|
|
|
97
49
|
ciJobName: "Lint skill files",
|
|
98
50
|
},
|
|
99
51
|
{
|
|
100
|
-
//
|
|
101
|
-
// an exit code of 0 means "ok", 1 means "items present
|
|
102
|
-
// (informational)", 2+ means a runtime error in the gate itself.
|
|
103
|
-
// The runner now distinguishes the two: 0/1 stay informational, 2+
|
|
104
|
-
// surface as a real failure. Pre-fix, any non-zero exit was rolled up
|
|
105
|
-
// as informational, which hid crashes (a 137 OOM looked the same as
|
|
106
|
-
// "found 12 items to review").
|
|
52
|
+
// Exit 0 ok, 1 "items present"; 2+ is a gate runtime error and fails the run.
|
|
107
53
|
name: "Forward-watch aggregator (informational)",
|
|
108
54
|
command: process.execPath,
|
|
109
55
|
args: [
|
|
@@ -145,11 +91,7 @@ const GATES = [
|
|
|
145
91
|
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
146
92
|
},
|
|
147
93
|
{
|
|
148
|
-
//
|
|
149
|
-
// EXTRACTED tree. Catches the class of bug where verify-on-source-tree
|
|
150
|
-
// passes (38/38) but verify-on-shipped-tarball fails (0/38) because
|
|
151
|
-
// something between sign and pack swapped keys/public.pem. Every release
|
|
152
|
-
// v0.11.x through v0.12.2 shipped this regression invisibly.
|
|
94
|
+
// Verifies the EXTRACTED tarball: a step between sign and pack can swap keys/public.pem.
|
|
153
95
|
name: "Verify shipped tarball (sign + pack + extract + verify round-trip)",
|
|
154
96
|
command: process.execPath,
|
|
155
97
|
args: [path.join(ROOT, "scripts", "verify-shipped-tarball.js")],
|
|
@@ -157,170 +99,100 @@ const GATES = [
|
|
|
157
99
|
requiresKeys: true,
|
|
158
100
|
},
|
|
159
101
|
{
|
|
160
|
-
// AGENTS.md
|
|
161
|
-
// CLI verb, CLI flag, lib/orchestrator/scripts export, playbook
|
|
162
|
-
// indicator, or CVE iocs field must land with a covering test
|
|
163
|
-
// reference in the same PR. The analyzer parses git diff against
|
|
164
|
-
// origin/main, classifies each change shape, and fails if a covered
|
|
165
|
-
// surface lacks a test literal anywhere under tests/. Blocking — a
|
|
166
|
-
// covered surface change without a covering test fails the gate.
|
|
102
|
+
// AGENTS.md Hard Rule #15: a CLI/export/indicator/iocs diff lands with a covering test.
|
|
167
103
|
name: "Diff coverage (feature changes require test coverage)",
|
|
168
104
|
command: process.execPath,
|
|
169
105
|
args: [path.join(ROOT, "scripts", "check-test-coverage.js")],
|
|
170
106
|
ciJobName: "Diff coverage",
|
|
171
107
|
},
|
|
172
108
|
{
|
|
173
|
-
// Validate every playbook in data/playbooks/ against the JSON schema
|
|
174
|
-
// + cross-playbook + cross-catalog references. v0.12.12 first wired
|
|
175
|
-
// this as informational so the patch-class release could land without
|
|
176
|
-
// retroactively breaking schema-drift cases; v0.13.0 flips it to
|
|
177
|
-
// required because the 20-playbook canonical set (including the 4
|
|
178
|
-
// v0.13.0 additions) all validate cleanly.
|
|
179
109
|
name: "Validate playbooks (schema + cross-refs)",
|
|
180
110
|
command: process.execPath,
|
|
181
111
|
args: [path.join(ROOT, "lib", "validate-playbooks.js"), "--strict"],
|
|
182
112
|
ciJobName: "Validate playbooks",
|
|
183
113
|
},
|
|
184
114
|
{
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
// baseline in tests/.test-count-baseline.json. Catches the class
|
|
188
|
-
// of regression where a test file gets accidentally deleted, a
|
|
189
|
-
// skip-all lands without review, or a misnamed file slips through
|
|
190
|
-
// the glob. The baseline is operator-refreshed on releases that
|
|
191
|
-
// intentionally add many new tests; --update-baseline rewrites it.
|
|
115
|
+
// Refuses silent test-set shrinkage: a deleted file, a skip-all or a misnamed
|
|
116
|
+
// file cannot pass. --update-baseline rewrites the pinned baseline.
|
|
192
117
|
name: "Test-count baseline (no silent shrinkage)",
|
|
193
118
|
command: process.execPath,
|
|
194
119
|
args: [path.join(ROOT, "scripts", "check-test-count.js")],
|
|
195
|
-
// Folds under the existing Data integrity CI job rather than a
|
|
196
|
-
// dedicated job — the check is fast (~70ms) static analysis and
|
|
197
|
-
// shares the integrity-tier framing with manifest-snapshot etc.
|
|
198
120
|
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
199
121
|
},
|
|
200
122
|
{
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
// temporal-staleness, logical-consistency, cross-ref-completeness,
|
|
204
|
-
// schema-evolution, operator-action-sla, unused-orphan) against
|
|
205
|
-
// the shipped catalog and fails if any class regresses beyond its
|
|
206
|
-
// documented budget. Mirrors the budget enforced by
|
|
207
|
-
// tests/shipped-catalog-integrity.test.js so the regression
|
|
208
|
-
// surfaces in BOTH the gate-summary table AND the test output.
|
|
123
|
+
// Fails if a detection class regresses past its budget. The same budget is in
|
|
124
|
+
// tests/shipped-catalog-integrity.test.js; the two must not drift.
|
|
209
125
|
name: "Catalog-gap budget (v0.13.21 extended detection classes)",
|
|
210
126
|
command: process.execPath,
|
|
211
127
|
args: [path.join(ROOT, "scripts", "check-catalog-gap-budget.js")],
|
|
212
128
|
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
213
129
|
},
|
|
214
130
|
{
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
// all five jurisdiction buckets (NIST, EU, UK, AU, ISO). Drafts are
|
|
218
|
-
// exempt. Prevents a US-centric subset from shipping in the offline
|
|
219
|
-
// catalog's framework-gap output for multi-jurisdiction operators.
|
|
131
|
+
// AGENTS.md Hard Rule #5: every curated CVE declares framework_control_gaps for
|
|
132
|
+
// all five jurisdiction buckets (NIST, EU, UK, AU, ISO). Drafts exempt.
|
|
220
133
|
name: "Framework-gap jurisdiction coverage (Hard Rule #5)",
|
|
221
134
|
command: process.execPath,
|
|
222
135
|
args: [path.join(ROOT, "scripts", "check-framework-gap-coverage.js")],
|
|
223
136
|
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
224
137
|
},
|
|
225
138
|
{
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
// retires and renumbers between releases, and a reference living outside
|
|
229
|
-
// the source catalogs is never dereferenced at runtime — so a stale id
|
|
230
|
-
// keeps rendering in operator output, pointing at a page that no longer
|
|
231
|
-
// resolves and leaving any control mapped to it orphaned (Hard Rule #4).
|
|
232
|
-
// Resolves against the pin rather than the network, so it can block.
|
|
139
|
+
// MITRE retires and renumbers technique ids, and a reference outside the two
|
|
140
|
+
// pinned catalogs is never dereferenced at runtime, orphaning its control.
|
|
233
141
|
name: "TTP reference integrity (no orphaned technique ids)",
|
|
234
142
|
command: process.execPath,
|
|
235
143
|
args: [path.join(ROOT, "scripts", "check-ttp-references.js")],
|
|
236
144
|
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
237
145
|
},
|
|
238
146
|
{
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
// rank — so sorting a publication's entries by score must sort them by
|
|
242
|
-
// percentile too. Refreshing one field without the other leaves an entry
|
|
243
|
-
// that passes every range and type check while ranking a CVE on a score
|
|
244
|
-
// that no longer supports it, which is precisely the input operators
|
|
245
|
-
// prioritise by. The ordering check catches that offline.
|
|
147
|
+
// epss_percentile is the score's rank within one daily publication, so a
|
|
148
|
+
// publication's entries must sort the same way by both.
|
|
246
149
|
name: "EPSS score/percentile consistency",
|
|
247
150
|
command: process.execPath,
|
|
248
151
|
args: [path.join(ROOT, "scripts", "check-epss-consistency.js")],
|
|
249
152
|
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
250
153
|
},
|
|
251
154
|
{
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
// `*-vX_Y_Z.test.js` filenames. Fails on NEW additions outside
|
|
255
|
-
// the authoritative version surfaces (package.json /
|
|
256
|
-
// manifest.json / CHANGELOG headings / git tags). The full rule
|
|
257
|
-
// is documented at the top of check-version-tags.js; refresh the
|
|
258
|
-
// baseline after an organic cleanup via
|
|
259
|
-
// `node scripts/check-version-tags.js --update-baseline`.
|
|
155
|
+
// Fails on NEW `// vX.Y.Z` comments and `*-vX_Y_Z.test.js` filenames: versions
|
|
156
|
+
// live in package.json, manifest.json, CHANGELOG headings and git tags.
|
|
260
157
|
name: "Version-tag drift (no new phase residue)",
|
|
261
158
|
command: process.execPath,
|
|
262
159
|
args: [path.join(ROOT, "scripts", "check-version-tags.js")],
|
|
263
160
|
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
264
161
|
},
|
|
265
162
|
{
|
|
266
|
-
// AGENTS.md collector
|
|
267
|
-
// where lib/collectors/ gets a new module but AGENTS.md's
|
|
268
|
-
// "<N> reference collectors ship today (...)" paragraph isn't
|
|
269
|
-
// bumped (or vice versa). The paragraph is the canonical source
|
|
270
|
-
// for AI-agent consumers; drift produces stale enumeration.
|
|
163
|
+
// Keeps lib/collectors/ and AGENTS.md's collector-enumeration paragraph in step.
|
|
271
164
|
name: "AGENTS.md collector enumeration drift",
|
|
272
165
|
command: process.execPath,
|
|
273
166
|
args: [path.join(ROOT, "scripts", "check-agents-md-collectors.js")],
|
|
274
167
|
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
275
168
|
},
|
|
276
169
|
{
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
// stdout then calls process.exit() (truncates the buffered write when
|
|
280
|
-
// piped — the stdout-flush-truncation class), and a stale/typo'd `// allow:` marker.
|
|
281
|
-
// dynamic-RegExp construction is surfaced warn-only this release. The
|
|
282
|
-
// exception mechanism + the "owned elsewhere" boundary are documented in
|
|
283
|
-
// the script header.
|
|
170
|
+
// Blocks a library-callable function that writes to stdout then calls
|
|
171
|
+
// process.exit(), and an orphaned allow marker. Dynamic RegExp is warn-only.
|
|
284
172
|
name: "Codebase-pattern gates (stdout-flush, dynamic RegExp, bidi codepoints, orphan markers)",
|
|
285
173
|
command: process.execPath,
|
|
286
174
|
args: [path.join(ROOT, "scripts", "check-codebase-patterns.js")],
|
|
287
175
|
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
288
176
|
},
|
|
289
177
|
{
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
// CVE id / playbook / data primitive / repo artifact), and every such
|
|
293
|
-
// subject must have a test. Blocks the naming drift that lets a test be
|
|
294
|
-
// filed under a version/finding label (where downstream readers can't find
|
|
295
|
-
// it) and surfaces any module/playbook that ships without a test. Derived
|
|
296
|
-
// dynamically from the source tree, so the list is never hand-maintained.
|
|
178
|
+
// Bidirectional: every tests/<x>.test.js names a real subject, and every subject
|
|
179
|
+
// has a test. Derived from the source tree, never hand-maintained.
|
|
297
180
|
name: "Test-subject coverage (every test maps to a subject; every subject has a test)",
|
|
298
181
|
command: process.execPath,
|
|
299
182
|
args: [path.join(ROOT, "scripts", "check-test-subjects.js")],
|
|
300
183
|
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
301
184
|
},
|
|
302
185
|
{
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
// Release body, and lints it for operator-facing quality (no internal
|
|
306
|
-
// phase/pass/slice narrative, no agent-dispatch / conversation residue,
|
|
307
|
-
// no tautological green claims). A malformed or internal-narrative section
|
|
308
|
-
// fails here rather than shipping as the public release body / falling
|
|
309
|
-
// back to the generic "Release of v<version>." line.
|
|
186
|
+
// Runs the same `## <version>` CHANGELOG extraction the release workflow
|
|
187
|
+
// publishes, then lints it, so a bad section fails here rather than publicly.
|
|
310
188
|
name: "Release-notes extract + operator-facing lint (CHANGELOG section)",
|
|
311
189
|
command: process.execPath,
|
|
312
190
|
args: [path.join(ROOT, "scripts", "check-changelog-extract.js")],
|
|
313
191
|
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
314
192
|
},
|
|
315
193
|
{
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
// (tests/.version-bump-ack.json naming the exact target version).
|
|
319
|
-
// Compares the top two `## X.Y.Z` CHANGELOG headings — hermetic, so it
|
|
320
|
-
// enforces identically locally and in the release.yml validate job. A
|
|
321
|
-
// hand-bumped minor without the ack fails here rather than shipping a
|
|
322
|
-
// wrong version number (the class of error behind two mis-versioned
|
|
323
|
-
// releases). Full contract at the top of check-version-bump.js.
|
|
194
|
+
// Patch is the ONLY default bump; a minor or major needs a committed
|
|
195
|
+
// tests/.version-bump-ack.json naming the exact target version.
|
|
324
196
|
name: "Version-bump cadence (patch-only default)",
|
|
325
197
|
command: process.execPath,
|
|
326
198
|
args: [path.join(ROOT, "scripts", "check-version-bump.js")],
|
|
@@ -341,9 +213,7 @@ function runGate(gate) {
|
|
|
341
213
|
}
|
|
342
214
|
}
|
|
343
215
|
const t0 = Date.now();
|
|
344
|
-
//
|
|
345
|
-
// can count `WARN ` lines for the summary table. We still want the live
|
|
346
|
-
// output, so each chunk is forwarded as it arrives.
|
|
216
|
+
// Piped stdio, forwarded on: the summary needs a WARN count from the output.
|
|
347
217
|
const { spawnSync } = require("child_process");
|
|
348
218
|
const r = spawnSync(gate.command, gate.args, {
|
|
349
219
|
cwd: ROOT,
|
|
@@ -353,9 +223,7 @@ function runGate(gate) {
|
|
|
353
223
|
const durationMs = Date.now() - t0;
|
|
354
224
|
if (r.stdout) process.stdout.write(r.stdout);
|
|
355
225
|
if (r.stderr) process.stderr.write(r.stderr);
|
|
356
|
-
//
|
|
357
|
-
// can surface them. Lint / validate output uses "WARN " at line start;
|
|
358
|
-
// count both the table form and an inline "[warn]" form.
|
|
226
|
+
// Both forms count: "WARN" at line start, and an inline "[warn]".
|
|
359
227
|
const combined = (r.stdout || "") + (r.stderr || "");
|
|
360
228
|
const warnCount = (
|
|
361
229
|
combined.match(/^WARN\b/gm) || []
|
|
@@ -365,23 +233,13 @@ function runGate(gate) {
|
|
|
365
233
|
if (r.status === 0) {
|
|
366
234
|
return { status: "passed", durationMs, warnCount };
|
|
367
235
|
}
|
|
368
|
-
//
|
|
369
|
-
// "soft signal" (exit codes 0..N) from "crash" (> N). Default behaviour
|
|
370
|
-
// for an informational gate without that field stays the same.
|
|
236
|
+
// informationalMaxExitCode separates a soft signal (0..N) from a crash; absent = any.
|
|
371
237
|
if (gate.informational) {
|
|
372
238
|
const ceil = typeof gate.informationalMaxExitCode === "number"
|
|
373
239
|
? gate.informationalMaxExitCode
|
|
374
240
|
: Infinity;
|
|
375
|
-
// A
|
|
376
|
-
//
|
|
377
|
-
// informational soft-signal. So is a signal kill (status:null with r.signal
|
|
378
|
-
// set — e.g. a 137 OOM kill) and a status that exceeds the soft-signal
|
|
379
|
-
// ceiling. Without surfacing the spawn-error case, an informational gate
|
|
380
|
-
// that never even ran fell through to "informational" and the release
|
|
381
|
-
// proceeded as if the gate had merely produced advisory output. The
|
|
382
|
-
// status===null && !signal case (no error object, but the process never
|
|
383
|
-
// produced an exit code) is treated the same way — a gate that did not
|
|
384
|
-
// exit cleanly cannot be classified as a soft signal.
|
|
241
|
+
// A gate that never ran cleanly is a crash, not a soft signal: spawn failure,
|
|
242
|
+
// a signal kill (137 OOM), and a status above the ceiling all fail here.
|
|
385
243
|
const spawnFailed = !!r.error || (r.status === null && !r.signal);
|
|
386
244
|
if (r.error || r.signal || spawnFailed || (r.status !== null && r.status > ceil)) {
|
|
387
245
|
return {
|
|
@@ -442,7 +300,6 @@ function main() {
|
|
|
442
300
|
}
|
|
443
301
|
}
|
|
444
302
|
|
|
445
|
-
// Summary table.
|
|
446
303
|
process.stdout.write("\n=== Pre-deploy summary ===\n");
|
|
447
304
|
const widest = results.reduce(
|
|
448
305
|
(n, r) => Math.max(n, r.gate.name.length),
|
|
@@ -459,10 +316,7 @@ function main() {
|
|
|
459
316
|
: "✗";
|
|
460
317
|
const timing = fmtMs(outcome.durationMs);
|
|
461
318
|
const timingSuffix = timing ? ` (${timing})` : "";
|
|
462
|
-
//
|
|
463
|
-
// distinguishable from one that passed cleanly. Pre-fix, warnings
|
|
464
|
-
// printed by individual gates (validate-cve-catalog, lint-skills,
|
|
465
|
-
// validate-playbooks) scrolled past invisible in the summary.
|
|
319
|
+
// A gate that "passed (3 warnings)" must be distinguishable from a clean pass.
|
|
466
320
|
const warnSuffix =
|
|
467
321
|
outcome.warnCount && outcome.warnCount > 0
|
|
468
322
|
? ` (${outcome.warnCount} warning${outcome.warnCount === 1 ? "" : "s"})`
|