@blamejs/exceptd-skills 0.19.33 → 0.19.34
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 +10 -0
- package/bin/exceptd.js +896 -2824
- 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 +20 -54
- package/lib/collectors/cicd-pipeline-compromise.js +40 -108
- 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 +57 -206
- package/lib/collectors/mcp.js +24 -70
- package/lib/collectors/runtime.js +24 -86
- package/lib/collectors/sbom.js +34 -106
- package/lib/collectors/scan-excludes.js +31 -138
- 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 -152
- 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 +35 -114
- 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 +64 -232
- package/lib/playbook-runner.js +693 -2095
- package/lib/prefetch.js +100 -376
- package/lib/refresh-external.js +199 -627
- package/lib/refresh-network.js +75 -307
- package/lib/rfc-cli.js +23 -68
- package/lib/scoring.js +77 -145
- 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 +14 -24
- package/lib/upstream-check-cli.js +10 -28
- package/lib/upstream-check.js +19 -44
- package/lib/validate-catalog-meta.js +17 -61
- package/lib/validate-cve-catalog.js +43 -119
- package/lib/validate-indexes.js +25 -76
- package/lib/validate-package.js +16 -62
- package/lib/validate-playbooks.js +69 -275
- 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 -412
- 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 +241 -241
- package/scripts/audit-catalog-gaps.js +9 -62
- package/scripts/audit-cross-skill.js +5 -31
- package/scripts/audit-perf.js +6 -16
- 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 +12 -27
- package/scripts/builders/token-budget.js +4 -31
- package/scripts/check-agents-md-collectors.js +11 -54
- 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 +50 -143
- package/scripts/check-epss-consistency.js +9 -64
- package/scripts/check-framework-gap-coverage.js +13 -31
- package/scripts/check-manifest-snapshot.js +13 -73
- package/scripts/check-sbom-currency.js +44 -142
- package/scripts/check-test-count.js +15 -52
- package/scripts/check-test-coverage.js +66 -197
- 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 +3 -8
- package/scripts/refresh-mitre-attack.js +1 -8
- package/scripts/refresh-mitre-d3fend.js +3 -9
- package/scripts/refresh-mitre-ics-attack.js +3 -8
- package/scripts/refresh-reverse-refs.js +27 -94
- package/scripts/refresh-rfc-index.js +2 -10
- package/scripts/refresh-sbom.js +31 -161
- package/scripts/refresh-upstream-catalogs.js +40 -137
- package/scripts/release.js +69 -232
- package/scripts/run-e2e-scenarios.js +24 -71
- 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 -140
|
@@ -2,40 +2,10 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* EPSS score/percentile consistency gate.
|
|
5
|
+
* EPSS score/percentile consistency gate. Exit 0 consistent, 1 inconsistent.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* higher than B on a given day, A's percentile is at least B's. That makes the
|
|
10
|
-
* pair self-checking with no network access — sort a day's entries by score and
|
|
11
|
-
* the percentiles must come out sorted too.
|
|
12
|
-
*
|
|
13
|
-
* The failure this catches is updating one field without the other. A refresh
|
|
14
|
-
* that writes a new score but keeps yesterday's percentile leaves a plausible
|
|
15
|
-
* entry: both numbers are in range, both look like EPSS values, and nothing
|
|
16
|
-
* downstream can tell they describe different days. The result is a CVE ranked
|
|
17
|
-
* as top-decile urgency on a score that no longer supports it, which is exactly
|
|
18
|
-
* the input the prioritisation model trusts most.
|
|
19
|
-
*
|
|
20
|
-
* What the ordering check does and does not establish. Monotonicity is a
|
|
21
|
-
* necessary condition, not a sufficient one: it catches a stale percentile that
|
|
22
|
-
* contradicts the new score's rank, which is what a partial refresh usually
|
|
23
|
-
* produces, but a stale value that happens to preserve the ordering passes.
|
|
24
|
-
* Proving two fields share a publication would require the fetched row, and
|
|
25
|
-
* that is not recoverable from the catalog offline. So this is a corruption
|
|
26
|
-
* detector, not a provenance proof — the provenance guarantee has to come from
|
|
27
|
-
* the write side, by fetching and writing score, percentile and date together.
|
|
28
|
-
*
|
|
29
|
-
* Three checks run:
|
|
30
|
-
* - within each epss_date, sorting by score must sort by percentile;
|
|
31
|
-
* - an entry carries both numeric fields or neither;
|
|
32
|
-
* - epss_note, which is derived text, must restate the current fields.
|
|
33
|
-
*
|
|
34
|
-
* Entries are grouped by epss_date so each publication is checked against
|
|
35
|
-
* itself; comparing across days is meaningless because the whole distribution
|
|
36
|
-
* shifts.
|
|
37
|
-
*
|
|
38
|
-
* Exit codes: 0 consistent, 1 inconsistent.
|
|
7
|
+
* The percentile is the score's rank within one publication, so a cohort must
|
|
8
|
+
* sort the same way by both. Monotonicity is necessary, not sufficient.
|
|
39
9
|
*/
|
|
40
10
|
|
|
41
11
|
const fs = require("node:fs");
|
|
@@ -44,13 +14,8 @@ const path = require("node:path");
|
|
|
44
14
|
const ROOT = path.resolve(__dirname, "..");
|
|
45
15
|
|
|
46
16
|
/**
|
|
47
|
-
* Percentiles
|
|
48
|
-
*
|
|
49
|
-
* is arithmetic, not drift. Real mismatches are pairs pulled from different
|
|
50
|
-
* days, which land orders of magnitude above this: the widest rounding artifact
|
|
51
|
-
* observed in the catalog is 8e-5, while a stale-field mismatch runs 0.05-0.9.
|
|
52
|
-
* The threshold sits between the two, far enough from each that neither
|
|
53
|
-
* classification is a close call.
|
|
17
|
+
* Percentiles publish rounded to five decimals, so adjacent scores can invert by
|
|
18
|
+
* a hair. Rounding artifacts reach 8e-5; a stale-field mismatch runs 0.05-0.9.
|
|
54
19
|
*/
|
|
55
20
|
const ROUNDING_TOLERANCE = 1e-3;
|
|
56
21
|
|
|
@@ -58,24 +23,10 @@ function loadCatalog(file) {
|
|
|
58
23
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
59
24
|
}
|
|
60
25
|
|
|
61
|
-
/**
|
|
62
|
-
* The operator-readable restatement of an entry's EPSS fields. Entries carry it
|
|
63
|
-
* as `epss_note`, and it is derived, not authored — so it must be rebuilt
|
|
64
|
-
* whenever the numbers move.
|
|
65
|
-
*
|
|
66
|
-
* The renderer lives in lib/cve-enrich.js because the refresh writer rebuilds
|
|
67
|
-
* the note from the same definition. A second copy here would let the gate and
|
|
68
|
-
* the writer disagree about the exact wording, which shows up as this gate
|
|
69
|
-
* failing on data that was refreshed correctly.
|
|
70
|
-
*/
|
|
26
|
+
/** Shared with the refresh writer so gate and writer cannot disagree on wording. */
|
|
71
27
|
const { renderEpssNote: renderNote } = require("../lib/cve-enrich.js");
|
|
72
28
|
|
|
73
|
-
/**
|
|
74
|
-
* Group entries by publication date. Entries missing either field are reported
|
|
75
|
-
* separately rather than skipped — a half-populated pair is its own defect, and
|
|
76
|
-
* silently ignoring it would let the gate pass on the very shape it exists to
|
|
77
|
-
* catch.
|
|
78
|
-
*/
|
|
29
|
+
/** Group entries by publication date; a half-populated pair is reported, not skipped. */
|
|
79
30
|
function cohorts(catalog) {
|
|
80
31
|
const groups = new Map();
|
|
81
32
|
const incomplete = [];
|
|
@@ -101,10 +52,7 @@ function cohorts(catalog) {
|
|
|
101
52
|
return { groups, incomplete };
|
|
102
53
|
}
|
|
103
54
|
|
|
104
|
-
/**
|
|
105
|
-
* Sort a cohort by score and report every place the percentile goes backwards
|
|
106
|
-
* by more than rounding can explain.
|
|
107
|
-
*/
|
|
55
|
+
/** Sort by score; report every percentile step backwards that rounding cannot explain. */
|
|
108
56
|
function inversions(rows) {
|
|
109
57
|
const sorted = [...rows].sort((a, b) => a.score - b.score);
|
|
110
58
|
const found = [];
|
|
@@ -141,10 +89,7 @@ function check(catalogFile) {
|
|
|
141
89
|
failures.push(`${row.id}: has one EPSS field but not ${row.missing} — write both together or neither`);
|
|
142
90
|
}
|
|
143
91
|
|
|
144
|
-
//
|
|
145
|
-
// moves the numbers and leaves the sentence describing the previous
|
|
146
|
-
// publication, so the entry states two different scores as of two different
|
|
147
|
-
// dates. It is derived text, so it can be checked exactly.
|
|
92
|
+
// epss_note is derived text, so it can be checked exactly against the fields.
|
|
148
93
|
for (const [id, entry] of Object.entries(catalog)) {
|
|
149
94
|
if (typeof entry.epss_note !== "string") continue;
|
|
150
95
|
if (typeof entry.epss_score !== "number" || typeof entry.epss_percentile !== "number") continue;
|
|
@@ -1,31 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/*
|
|
3
|
-
*
|
|
3
|
+
* Enforces global-first coverage (AGENTS.md Hard Rule #5): every curated CVE in
|
|
4
|
+
* data/cve-catalog.json must declare a framework_control_gaps statement for all
|
|
5
|
+
* five jurisdiction buckets, so a multi-jurisdiction operator reading the
|
|
6
|
+
* offline catalog gets more than a US-centric subset.
|
|
4
7
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* for all five jurisdiction buckets:
|
|
8
|
+
* Draft entries (_auto_imported) are exempt — they carry raw NVD data, and the
|
|
9
|
+
* curation bar requires promotion before shipping, at which point this applies.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* AU — AU-Essential-8-* / AU-ISM-*
|
|
13
|
-
* ISO — ISO-27001-2022-*
|
|
14
|
-
*
|
|
15
|
-
* A multi-jurisdiction operator reading the offline catalog's framework-gap
|
|
16
|
-
* output must get coverage for every required jurisdiction on every CVE, not
|
|
17
|
-
* a US-centric subset. Before this gate, two-thirds of the corpus mapped only
|
|
18
|
-
* a subset; a codex review surfaced it, the corpus was completed, and this
|
|
19
|
-
* gate keeps it from regressing when new CVEs are curated.
|
|
20
|
-
*
|
|
21
|
-
* Draft entries (_auto_imported === true) are exempt — they carry raw NVD
|
|
22
|
-
* data that has not been through curation yet. The curation bar (elsewhere)
|
|
23
|
-
* requires drafts to be promoted before shipping, at which point this gate
|
|
24
|
-
* applies.
|
|
25
|
-
*
|
|
26
|
-
* Exit code: 0 when every curated entry is complete, 1 when any entry omits a
|
|
27
|
-
* required bucket (the list is printed). Uses process.exitCode (not
|
|
28
|
-
* process.exit) so buffered stdout drains before the process ends.
|
|
11
|
+
* Exits 0 when every curated entry is complete, 1 when any omits a bucket.
|
|
12
|
+
* `process.exitCode`, never `process.exit()`: the failure list is a buffered
|
|
13
|
+
* stdout write that the exit can truncate.
|
|
29
14
|
*/
|
|
30
15
|
|
|
31
16
|
'use strict';
|
|
@@ -34,19 +19,16 @@ const fs = require('node:fs');
|
|
|
34
19
|
const path = require('node:path');
|
|
35
20
|
|
|
36
21
|
const ROOT = path.resolve(__dirname, '..');
|
|
37
|
-
//
|
|
38
|
-
// a fixture; defaults to the shipped catalog for the real predeploy run.
|
|
22
|
+
// argv[2] overrides the catalog so the gate's own test can point at a fixture.
|
|
39
23
|
const CATALOG = process.argv[2] || path.join(ROOT, 'data', 'cve-catalog.json');
|
|
40
24
|
|
|
41
25
|
const REQUIRED = ['NIST', 'EU', 'UK', 'AU', 'ISO'];
|
|
42
26
|
|
|
43
27
|
function bucketOf(key) {
|
|
44
28
|
if (/^NIST/i.test(key)) return 'NIST';
|
|
45
|
-
// EU bucket is the security-regulation family
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
// GDPR still owes NIS2/DORA/EU-AI coverage, so GDPR does not satisfy this
|
|
49
|
-
// bucket. Note EU-AI / EU-CRA match here; EU-GDPR deliberately does not.
|
|
29
|
+
// The EU bucket is the security-regulation family Hard Rule #5 names. GDPR is
|
|
30
|
+
// data protection and does NOT satisfy it: an entry mapped only to GDPR still
|
|
31
|
+
// owes NIS2/DORA/EU-AI coverage, so EU-GDPR must not match here.
|
|
50
32
|
if (/^(NIS2|DORA|EU-AI|EU-CRA)/i.test(key)) return 'EU';
|
|
51
33
|
if (/^UK-CAF/i.test(key)) return 'UK';
|
|
52
34
|
if (/^(AU-Essential-?8|AU-ISM|Essential-?8)/i.test(key)) return 'AU';
|
|
@@ -1,33 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* framework_gaps) from manifest.json and compares it to the committed
|
|
8
|
-
* manifest-snapshot.json baseline.
|
|
9
|
-
*
|
|
10
|
-
* The skill surface is the public contract this repo offers downstream
|
|
11
|
-
* AI assistants: skill names that downstream prompts may reference,
|
|
12
|
-
* trigger keywords that downstream skill-matchers index on, and the
|
|
13
|
-
* data files that skills rely on. Removing a skill or trigger keyword
|
|
14
|
-
* silently breaks every consumer that pinned that surface.
|
|
15
|
-
*
|
|
16
|
-
* Exit codes:
|
|
17
|
-
* 0 — no breaking changes (additive changes printed but not failing)
|
|
18
|
-
* 1 — breaking changes detected
|
|
19
|
-
* 2 — script-level error (missing baseline, IO failure, etc.)
|
|
20
|
-
*
|
|
21
|
-
* Operators see this gate in SECURITY.md / CONTRIBUTING.md as a CI
|
|
22
|
-
* promise: "removed skills, removed triggers, or removed data deps fail
|
|
23
|
-
* the build before they reach main."
|
|
24
|
-
*
|
|
25
|
-
* Usage:
|
|
26
|
-
* node scripts/check-manifest-snapshot.js
|
|
27
|
-
*
|
|
28
|
-
* Regenerate the baseline after an intentional removal:
|
|
29
|
-
* node scripts/refresh-manifest-snapshot.js
|
|
30
|
-
* git add manifest-snapshot.json && git commit
|
|
3
|
+
* CI gate: diffs the public skill surface in manifest.json — name, version,
|
|
4
|
+
* triggers, data_deps, ref arrays — against the committed manifest-snapshot.json.
|
|
5
|
+
* Exit 0 when nothing broke (additive changes print and still pass), 1 on a
|
|
6
|
+
* breaking change, 2 on a script-level error such as a missing baseline.
|
|
31
7
|
*/
|
|
32
8
|
|
|
33
9
|
const fs = require("fs");
|
|
@@ -39,9 +15,7 @@ const MANIFEST_PATH = path.join(ROOT, "manifest.json");
|
|
|
39
15
|
const SNAPSHOT_PATH = path.join(ROOT, "manifest-snapshot.json");
|
|
40
16
|
|
|
41
17
|
function captureSurface(manifest) {
|
|
42
|
-
//
|
|
43
|
-
// pinned against. NOT included: sha256 / signature / signed_at —
|
|
44
|
-
// those change every commit and are not a public contract.
|
|
18
|
+
// Only what a consumer pins against: sha256, signature and signed_at change every commit.
|
|
45
19
|
const skills = (manifest.skills || []).map(s => ({
|
|
46
20
|
name: s.name,
|
|
47
21
|
version: s.version || null,
|
|
@@ -70,30 +44,24 @@ function diff(baseline, current) {
|
|
|
70
44
|
const bSkills = new Map(baseline.skills.map(s => [s.name, s]));
|
|
71
45
|
const cSkills = new Map(current.skills.map(s => [s.name, s]));
|
|
72
46
|
|
|
73
|
-
// Removed skills are breaking.
|
|
74
47
|
for (const name of bSkills.keys()) {
|
|
75
48
|
if (!cSkills.has(name)) {
|
|
76
49
|
breaking.push(`removed skill: ${name}`);
|
|
77
50
|
}
|
|
78
51
|
}
|
|
79
52
|
|
|
80
|
-
// Added skills are additive.
|
|
81
53
|
for (const name of cSkills.keys()) {
|
|
82
54
|
if (!bSkills.has(name)) {
|
|
83
55
|
additive.push(`added skill: ${name}`);
|
|
84
56
|
}
|
|
85
57
|
}
|
|
86
58
|
|
|
87
|
-
// For each skill present in both, diff the pinned facts.
|
|
88
59
|
for (const [name, b] of bSkills) {
|
|
89
60
|
const c = cSkills.get(name);
|
|
90
61
|
if (!c) continue;
|
|
91
62
|
|
|
92
|
-
// version downgrades are breaking; bumps are additive.
|
|
93
63
|
if (b.version && c.version && b.version !== c.version) {
|
|
94
|
-
//
|
|
95
|
-
// upstream and the manifest version field is informational. The
|
|
96
|
-
// operator should bump, not unbump.
|
|
64
|
+
// Lexicographic, not semver: nothing upstream enforces a semver shape.
|
|
97
65
|
if (c.version < b.version) {
|
|
98
66
|
breaking.push(`${name}: version downgraded ${b.version} -> ${c.version}`);
|
|
99
67
|
} else {
|
|
@@ -111,7 +79,7 @@ function diff(baseline, current) {
|
|
|
111
79
|
additive.push(`${name}: added trigger keywords: ${addedTriggers.join(", ")}`);
|
|
112
80
|
}
|
|
113
81
|
|
|
114
|
-
// Removed data deps break the skill at load time.
|
|
82
|
+
// Removed data deps break the skill at load time.
|
|
115
83
|
const removedDeps = b.data_deps.filter(d => !c.data_deps.includes(d));
|
|
116
84
|
if (removedDeps.length > 0) {
|
|
117
85
|
breaking.push(`${name}: removed data deps: ${removedDeps.join(", ")}`);
|
|
@@ -121,11 +89,7 @@ function diff(baseline, current) {
|
|
|
121
89
|
additive.push(`${name}: added data deps: ${addedDeps.join(", ")}`);
|
|
122
90
|
}
|
|
123
91
|
|
|
124
|
-
//
|
|
125
|
-
// Per AGENTS.md rule #4 (no orphaned controls) and #12 (external
|
|
126
|
-
// data version pinning), narrowing the cited surface is a
|
|
127
|
-
// deliberate decision worth surfacing in CI. Treat as breaking;
|
|
128
|
-
// the operator can refresh the baseline alongside the intent.
|
|
92
|
+
// Narrowing the cited surface is deliberate (AGENTS.md #4, #12), so removal is breaking.
|
|
129
93
|
for (const field of ["atlas_refs", "attack_refs", "framework_gaps", "rfc_refs", "cwe_refs", "d3fend_refs", "dlp_refs"]) {
|
|
130
94
|
const removed = b[field].filter(r => !c[field].includes(r));
|
|
131
95
|
if (removed.length > 0) {
|
|
@@ -138,9 +102,6 @@ function diff(baseline, current) {
|
|
|
138
102
|
}
|
|
139
103
|
}
|
|
140
104
|
|
|
141
|
-
// ATLAS pinned-version change is breaking per AGENTS.md rule #12
|
|
142
|
-
// (never silently inherit version changes). The operator must update
|
|
143
|
-
// the baseline alongside the audit of TTP ID changes.
|
|
144
105
|
if (baseline.atlas_version && current.atlas_version &&
|
|
145
106
|
baseline.atlas_version !== current.atlas_version) {
|
|
146
107
|
breaking.push(
|
|
@@ -171,33 +132,16 @@ function formatDiff(result) {
|
|
|
171
132
|
}
|
|
172
133
|
|
|
173
134
|
/**
|
|
174
|
-
* Verify the
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* (the surface diff alone is defeated by editing manifest.json AND the
|
|
178
|
-
* baseline in lockstep — e.g. to hide a removed skill/trigger). The
|
|
179
|
-
* sidecar pins the baseline's exact bytes so that lockstep edit no longer
|
|
180
|
-
* produces a matching pair.
|
|
181
|
-
*
|
|
182
|
-
* Pairing invariant: refresh-manifest-snapshot.js always writes BOTH the
|
|
183
|
-
* snapshot and its sidecar, and package.json `files` ships them together.
|
|
184
|
-
* So a snapshot present WITHOUT its sidecar is not a benign legacy state
|
|
185
|
-
* for any tree the current gate runs against (CI/predeploy run on the
|
|
186
|
-
* committed tree, where both are present; the shipped tarball ships both).
|
|
187
|
-
* An absent sidecar next to a present snapshot is the integrity-evasion
|
|
188
|
-
* shape — treat it as a failure, symmetric with the present-but-mismatch
|
|
189
|
-
* failure. Returns { ok, error } so callers can surface a hard exit.
|
|
190
|
-
*
|
|
191
|
-
* @param {string} root repo root containing the snapshot + sidecar
|
|
192
|
-
* @returns {{ok: boolean, error: (string|null)}}
|
|
135
|
+
* Verify the snapshot still hashes to its .sha256 sidecar. The surface diff
|
|
136
|
+
* alone is defeated by editing manifest.json and the baseline in lockstep; the
|
|
137
|
+
* sidecar pins the baseline's exact bytes so that pair no longer matches.
|
|
193
138
|
*/
|
|
194
139
|
function checkSnapshotIntegrity(root) {
|
|
195
140
|
const snapshotPath = path.join(root, "manifest-snapshot.json");
|
|
196
141
|
const shaPath = path.join(root, "manifest-snapshot.sha256");
|
|
197
142
|
|
|
198
143
|
if (!fs.existsSync(snapshotPath)) {
|
|
199
|
-
//
|
|
200
|
-
// distinct error. Nothing for the integrity check to anchor against.
|
|
144
|
+
// Nothing to anchor against; the caller's baseline read reports this.
|
|
201
145
|
return { ok: true, error: null };
|
|
202
146
|
}
|
|
203
147
|
|
|
@@ -253,11 +197,7 @@ if (require.main === module) {
|
|
|
253
197
|
process.exit(2);
|
|
254
198
|
}
|
|
255
199
|
|
|
256
|
-
//
|
|
257
|
-
// and the sidecar must be present whenever the snapshot is. A missing
|
|
258
|
-
// sidecar next to a present snapshot is itself a failure — it is the
|
|
259
|
-
// only thing that would have caught a lockstep hand-edit of manifest.json
|
|
260
|
-
// + the baseline, so allowing it to be deleted re-opens that bypass.
|
|
200
|
+
// Before the diff: a tampered baseline must not be diffed as though trustworthy.
|
|
261
201
|
const integrity = checkSnapshotIntegrity(ROOT);
|
|
262
202
|
if (!integrity.ok) {
|
|
263
203
|
console.error("[check-manifest-snapshot] " + integrity.error);
|