@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
package/lib/ttp-mapper.js
CHANGED
|
@@ -8,10 +8,8 @@
|
|
|
8
8
|
const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
9
9
|
|
|
10
10
|
function map(controlId, gapCatalog) {
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// 'constructor' returned found:true for a key that is not a real catalog
|
|
14
|
-
// entry. Only own enumerable keys are catalog controls.
|
|
11
|
+
// hasOwn, not a bare gapCatalog[controlId]: '__proto__' / 'toString' /
|
|
12
|
+
// 'constructor' otherwise resolve an inherited member and report found:true.
|
|
15
13
|
if (!gapCatalog || !hasOwn(gapCatalog, controlId)) {
|
|
16
14
|
return { control_id: controlId, found: false, message: 'Control not in gap catalog' };
|
|
17
15
|
}
|
|
@@ -45,43 +43,35 @@ function gapsFor(attackPattern, gapCatalog, atlasCatalog) {
|
|
|
45
43
|
}
|
|
46
44
|
|
|
47
45
|
function coverage(frameworkId, ttpId, gapCatalog, atlasCatalog) {
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
// partially_covered_by / not_covered_by as an explicit null (not absent)
|
|
53
|
-
// so the no-match outcome is observable rather than a silent universal
|
|
54
|
-
// match.
|
|
46
|
+
// Guard before any deref: an empty frameworkId yields a prefix of '' that
|
|
47
|
+
// matches every control, and a non-string throws on .split().
|
|
48
|
+
// partially_covered_by / not_covered_by come back explicitly null, so the
|
|
49
|
+
// no-match outcome is observable rather than a silent universal match.
|
|
55
50
|
if (typeof frameworkId !== 'string' || frameworkId.trim() === '') {
|
|
56
51
|
return { ttp_id: ttpId, found: false, error: 'frameworkId required', partially_covered_by: null, not_covered_by: null };
|
|
57
52
|
}
|
|
58
53
|
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
// treated as a real ATLAS technique. Only own keys are catalog techniques.
|
|
54
|
+
// Same guard for ttpId: an inherited Object.prototype member would otherwise
|
|
55
|
+
// be treated as a real ATLAS technique.
|
|
62
56
|
if (!atlasCatalog || !hasOwn(atlasCatalog, ttpId)) {
|
|
63
57
|
return { ttp_id: ttpId, found: false };
|
|
64
58
|
}
|
|
65
59
|
const ttp = atlasCatalog[ttpId];
|
|
66
60
|
if (!ttp) return { ttp_id: ttpId, found: false };
|
|
67
61
|
|
|
68
|
-
// atlas-ttps.json uses controls_that_partially_help / controls_that_dont_help / framework_gap_detail
|
|
69
62
|
const partialControls = ttp.controls_that_partially_help || [];
|
|
70
63
|
const noHelpControls = ttp.controls_that_dont_help || [];
|
|
71
64
|
const gapDetail = ttp.framework_gap_detail || '';
|
|
72
65
|
const hasFrameworkGap = ttp.framework_gap === true;
|
|
73
66
|
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
// begins with the prefix and the next char is a segment boundary (-, .) or
|
|
79
|
-
// end-of-string, so 'soc2' still matches 'soc2-z' but 'is' never matches
|
|
80
|
-
// 'nist-...'.
|
|
67
|
+
// Match the first hyphen-delimited segment of a control id, not bare
|
|
68
|
+
// containment: includes() lets 'IS' match 'NIST' and '' match everything. A
|
|
69
|
+
// control matches when it starts with the prefix and the next character ends
|
|
70
|
+
// the segment (-, . or end), so 'soc2' still matches 'soc2-z'.
|
|
81
71
|
const frameworkPrefix = frameworkId.split('-')[0].toLowerCase();
|
|
82
72
|
if (frameworkPrefix.length === 0) {
|
|
83
|
-
//
|
|
84
|
-
//
|
|
73
|
+
// A hyphen-led frameworkId ("-X") has an empty first segment — the same
|
|
74
|
+
// universal-match hazard, so the same fail-closed result.
|
|
85
75
|
return { ttp_id: ttpId, found: false, error: 'frameworkId required', partially_covered_by: null, not_covered_by: null };
|
|
86
76
|
}
|
|
87
77
|
const segMatch = (c) => {
|
|
@@ -2,23 +2,11 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* lib/upstream-check
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* - `exceptd run --upstream-check`
|
|
11
|
-
* - `exceptd refresh --network`
|
|
12
|
-
*
|
|
13
|
-
* Runs in a child process so the parent verb stays synchronous and the
|
|
14
|
-
* network timeout is bounded by the spawnSync timeout.
|
|
15
|
-
*
|
|
16
|
-
* Output: one JSON line to stdout. Exits 0 even when the registry is
|
|
17
|
-
* unreachable (offline ≠ error — the freshness signal degrades gracefully).
|
|
18
|
-
*
|
|
19
|
-
* Flags:
|
|
20
|
-
* --timeout <ms> override the default 5000 ms network timeout
|
|
21
|
-
* --raw emit raw registry response instead of freshness report
|
|
5
|
+
* Emits lib/upstream-check's freshness report as one JSON line on stdout.
|
|
6
|
+
* Spawned as a child process so the calling verb stays synchronous and the
|
|
7
|
+
* network timeout is bounded by the spawnSync timeout. Exits 0 even when the
|
|
8
|
+
* registry is unreachable: callers parse the envelope off stdout, and absent
|
|
9
|
+
* freshness data is a degraded signal rather than an error.
|
|
22
10
|
*/
|
|
23
11
|
|
|
24
12
|
const path = require("path");
|
|
@@ -54,11 +42,8 @@ function readManifest() {
|
|
|
54
42
|
|
|
55
43
|
(async () => {
|
|
56
44
|
const opts = parseArgs(process.argv);
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
// structured `skipped` envelope and exit 0. Exit 0 because, like the
|
|
60
|
-
// existing "offline" path, missing freshness data is a graceful degradation
|
|
61
|
-
// — it is NOT an error condition for downstream callers.
|
|
45
|
+
// The registry probe is a network call, so air-gap mode answers with a
|
|
46
|
+
// `skipped` envelope instead. `ok: null` is neither pass nor fail.
|
|
62
47
|
if (process.env.EXCEPTD_AIR_GAP === "1" || opts.airGap) {
|
|
63
48
|
process.stdout.write(JSON.stringify({
|
|
64
49
|
ok: null,
|
|
@@ -81,12 +66,9 @@ function readManifest() {
|
|
|
81
66
|
});
|
|
82
67
|
process.stdout.write(JSON.stringify(report) + "\n");
|
|
83
68
|
})().catch((err) => {
|
|
84
|
-
// Any
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// res.stdout for the envelope). String(...) coerces the error to a primitive,
|
|
88
|
-
// so this JSON.stringify itself cannot throw. Exit 0 is the default — no prior
|
|
89
|
-
// non-zero exitCode is set on this path.
|
|
69
|
+
// Any throw still yields one parseable JSON line and exit 0, per the
|
|
70
|
+
// degradation contract above. String(...) coerces the error to a primitive so
|
|
71
|
+
// this JSON.stringify cannot itself throw.
|
|
90
72
|
process.stdout.write(JSON.stringify({
|
|
91
73
|
ok: false,
|
|
92
74
|
error: String((err && err.message) || err),
|
package/lib/upstream-check.js
CHANGED
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* fired automatically on every CLI invocation.
|
|
10
|
-
*
|
|
11
|
-
* Trust model: the registry call is a freshness signal, not a trust
|
|
12
|
-
* anchor. The Ed25519-signed skill catalog shipped in the operator's
|
|
13
|
-
* installed package remains the source of truth. This helper only
|
|
14
|
-
* reports "you're N days behind" — does not auto-update anything.
|
|
15
|
-
*
|
|
16
|
-
* Zero npm deps. Node 24 stdlib only.
|
|
4
|
+
* Queries the npm registry for the package's latest published version and
|
|
5
|
+
* publish time, for `doctor --registry-check`, `run --upstream-check` and
|
|
6
|
+
* `refresh --network`. The registry call is a freshness signal, not a trust
|
|
7
|
+
* anchor: the Ed25519-signed local catalog stays the source of truth, and this
|
|
8
|
+
* reports how far behind it is without updating anything.
|
|
17
9
|
*/
|
|
18
10
|
|
|
19
11
|
const https = require("https");
|
|
@@ -23,21 +15,14 @@ const PKG_NAME = "@blamejs/exceptd-skills";
|
|
|
23
15
|
const REQUEST_TIMEOUT_MS = 5000;
|
|
24
16
|
|
|
25
17
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* Honors EXCEPTD_REGISTRY_FIXTURE env var for offline testing — value is
|
|
33
|
-
* a path to a JSON file with { version, time: { <ver>: ISO } } shape.
|
|
18
|
+
* Returns { ok: true, version, published_at, source: "npm-registry" }, or
|
|
19
|
+
* { ok: false, error, source: "offline" } — never throws, so a caller treats an
|
|
20
|
+
* absent freshness signal as absent rather than as an error.
|
|
21
|
+
* EXCEPTD_REGISTRY_FIXTURE points at a JSON file shaped
|
|
22
|
+
* { version, time: { <ver>: ISO } } for offline runs.
|
|
34
23
|
*/
|
|
35
24
|
async function fetchLatestPublished({ timeoutMs = REQUEST_TIMEOUT_MS, pkgName = PKG_NAME } = {}) {
|
|
36
|
-
//
|
|
37
|
-
// be issued when the operator has declared an air-gapped environment.
|
|
38
|
-
// Returning a structured refusal (instead of throwing) lets callers degrade
|
|
39
|
-
// gracefully the same way they handle `offline` — the freshness signal is
|
|
40
|
-
// intentionally absent, not in error.
|
|
25
|
+
// A network operation: air-gap gets a structured refusal, as `offline` does.
|
|
41
26
|
if (process.env.EXCEPTD_AIR_GAP === "1") {
|
|
42
27
|
return { ok: false, error: "air-gap-blocked", source: "fetchLatestPublished" };
|
|
43
28
|
}
|
|
@@ -47,11 +32,8 @@ async function fetchLatestPublished({ timeoutMs = REQUEST_TIMEOUT_MS, pkgName =
|
|
|
47
32
|
const fs = require("fs");
|
|
48
33
|
const fixture = JSON.parse(fs.readFileSync(process.env.EXCEPTD_REGISTRY_FIXTURE, "utf8"));
|
|
49
34
|
const version = fixture["dist-tags"]?.latest || fixture.version;
|
|
50
|
-
// Same guard the network branch
|
|
51
|
-
//
|
|
52
|
-
// Returning ok:true with version:undefined would let an undefined
|
|
53
|
-
// version leak into buildFreshnessReport's semver compare and the
|
|
54
|
-
// operator-facing hint. Degrade to an explicit offline refusal.
|
|
35
|
+
// Same guard as the network branch: ok:true with version undefined leaks
|
|
36
|
+
// into buildFreshnessReport's semver compare and its hint.
|
|
55
37
|
if (!version) {
|
|
56
38
|
return { ok: false, error: "fixture missing dist-tags.latest / version", source: "offline" };
|
|
57
39
|
}
|
|
@@ -99,9 +81,8 @@ async function fetchLatestPublished({ timeoutMs = REQUEST_TIMEOUT_MS, pkgName =
|
|
|
99
81
|
}
|
|
100
82
|
|
|
101
83
|
/**
|
|
102
|
-
* Semver compare
|
|
103
|
-
*
|
|
104
|
-
* a pre-release would explicitly opt in).
|
|
84
|
+
* Semver compare, returning -1, 0 or 1. Canonical N.N.N only; a pre-release tag
|
|
85
|
+
* is ignored rather than ordered.
|
|
105
86
|
*/
|
|
106
87
|
function semverCmp(a, b) {
|
|
107
88
|
const pa = String(a).split(".").map((n) => parseInt(n, 10) || 0);
|
|
@@ -113,10 +94,7 @@ function semverCmp(a, b) {
|
|
|
113
94
|
return 0;
|
|
114
95
|
}
|
|
115
96
|
|
|
116
|
-
/**
|
|
117
|
-
* Build the operator-facing freshness report. Pure function — takes the
|
|
118
|
-
* registry response and the local version/manifest, returns the report.
|
|
119
|
-
*/
|
|
97
|
+
/** Operator-facing freshness report. Pure — no I/O of its own. */
|
|
120
98
|
function buildFreshnessReport({ localVersion, registry, localManifest }) {
|
|
121
99
|
if (!registry || !registry.ok) {
|
|
122
100
|
return {
|
|
@@ -128,16 +106,13 @@ function buildFreshnessReport({ localVersion, registry, localManifest }) {
|
|
|
128
106
|
};
|
|
129
107
|
}
|
|
130
108
|
const cmp = semverCmp(localVersion, registry.version);
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
// `new Date(...).getTime()` return NaN, which would otherwise propagate
|
|
134
|
-
// NaN into days_since_latest_publish. Degrade an unparseable date to the
|
|
135
|
-
// explicit null (signal-absent) branch instead of emitting NaN.
|
|
109
|
+
// An unparseable registry `time` yields NaN from getTime(); it degrades to the
|
|
110
|
+
// explicit null signal-absent branch rather than reaching the report.
|
|
136
111
|
const publishedTs = registry.published_at ? new Date(registry.published_at).getTime() : NaN;
|
|
137
112
|
const daysBehind = Number.isFinite(publishedTs)
|
|
138
113
|
? Math.max(0, Math.floor((Date.now() - publishedTs) / (24 * 3600 * 1000)))
|
|
139
114
|
: null;
|
|
140
|
-
//
|
|
115
|
+
// last_threat_review is per-skill; the report surfaces the most stale.
|
|
141
116
|
let oldestReview = null;
|
|
142
117
|
if (localManifest && Array.isArray(localManifest.skills)) {
|
|
143
118
|
for (const s of localManifest.skills) {
|
|
@@ -1,29 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* default rating and per-entry override note
|
|
9
|
-
* _meta.freshness_policy — review cadence + decay thresholds
|
|
10
|
-
*
|
|
11
|
-
* Per AGENTS.md rule #10 (no placeholder language), this validator
|
|
12
|
-
* rejects empty strings and the usual placeholder tokens. Rule #12
|
|
13
|
-
* (external data version pinning) is enforced informally — every catalog
|
|
14
|
-
* still needs its existing schema_version / last_updated fields, but
|
|
15
|
-
* those are validated by the existing per-catalog validators.
|
|
16
|
-
*
|
|
17
|
-
* Usage:
|
|
18
|
-
* node lib/validate-catalog-meta.js
|
|
19
|
-
* node lib/validate-catalog-meta.js --quiet
|
|
20
|
-
*
|
|
21
|
-
* Exit code:
|
|
22
|
-
* 0 all catalogs have the required _meta fields
|
|
23
|
-
* 1 one or more catalogs missing a required field
|
|
24
|
-
* 2 argv error
|
|
25
|
-
*
|
|
26
|
-
* No external dependencies. Node 24 stdlib only.
|
|
3
|
+
* Asserts every data/*.json carries its source-trust and freshness fields:
|
|
4
|
+
* _meta.tlp (TLP marking), _meta.source_confidence (Admiralty scheme) and
|
|
5
|
+
* _meta.freshness_policy. Empty strings and placeholder tokens are rejected.
|
|
6
|
+
* Exits 0 when every catalog passes, 1 on a missing or malformed field, 2 on an
|
|
7
|
+
* argv error. --strict promotes freshness warnings to errors.
|
|
27
8
|
*/
|
|
28
9
|
|
|
29
10
|
'use strict';
|
|
@@ -85,14 +66,10 @@ function containsPlaceholder(s) {
|
|
|
85
66
|
return PLACEHOLDER_TOKENS.some((re) => re.test(s));
|
|
86
67
|
}
|
|
87
68
|
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
// over to March 2 with a valid getTime() — so the parsed Y-M-D must round-trip
|
|
93
|
-
// back to the input components. Deliberately carries NO year-floor business
|
|
94
|
-
// rule (a valid-but-old 1900-01-01 stays a valid date so the staleness branch,
|
|
95
|
-
// not the validity branch, reports it).
|
|
69
|
+
// A Date for a real YYYY-MM-DD calendar date, null for anything else. The
|
|
70
|
+
// round-trip is what rejects an impossible date: `new Date('2026-02-30')` does
|
|
71
|
+
// not throw, it rolls over to March 2. No year-floor rule here — a
|
|
72
|
+
// valid-but-ancient date stays valid so the staleness branch reports it.
|
|
96
73
|
function parseIsoDateStrict(v) {
|
|
97
74
|
if (typeof v !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(v)) return null;
|
|
98
75
|
const d = new Date(v + 'T00:00:00Z');
|
|
@@ -115,19 +92,12 @@ function validateMeta(catalogPath, opts) {
|
|
|
115
92
|
const meta = data._meta;
|
|
116
93
|
|
|
117
94
|
if (!meta || typeof meta !== 'object') {
|
|
118
|
-
//
|
|
119
|
-
// `meta.tlp` / `meta.source_confidence` / `meta.freshness_policy`, so it
|
|
120
|
-
// must NOT run when `meta` is absent or non-object. Return early in the
|
|
121
|
-
// SAME shape the caller asked for: includeWarnings callers (main()) get
|
|
122
|
-
// `{errors, warnings}` so `result.errors` is a real array and the loop
|
|
123
|
-
// reports a clean FAIL + continues; no-opts callers still get a non-empty
|
|
124
|
-
// `string[]`. This still FAILS — it only removes the uncaught TypeError.
|
|
95
|
+
// Must return here — everything below dereferences `meta.*`.
|
|
125
96
|
errors.push('missing _meta block');
|
|
126
97
|
if (opts && opts.includeWarnings) return { errors, warnings };
|
|
127
98
|
return errors;
|
|
128
99
|
}
|
|
129
100
|
|
|
130
|
-
/* tlp */
|
|
131
101
|
if (typeof meta.tlp !== 'string') {
|
|
132
102
|
errors.push('_meta.tlp is missing or not a string');
|
|
133
103
|
} else if (!REQUIRED_TLP_VALUES.has(meta.tlp)) {
|
|
@@ -136,7 +106,6 @@ function validateMeta(catalogPath, opts) {
|
|
|
136
106
|
);
|
|
137
107
|
}
|
|
138
108
|
|
|
139
|
-
/* source_confidence */
|
|
140
109
|
const sc = meta.source_confidence;
|
|
141
110
|
if (!sc || typeof sc !== 'object') {
|
|
142
111
|
errors.push('_meta.source_confidence is missing or not an object');
|
|
@@ -157,7 +126,6 @@ function validateMeta(catalogPath, opts) {
|
|
|
157
126
|
}
|
|
158
127
|
}
|
|
159
128
|
|
|
160
|
-
/* freshness_policy */
|
|
161
129
|
const fp = meta.freshness_policy;
|
|
162
130
|
if (!fp || typeof fp !== 'object') {
|
|
163
131
|
errors.push('_meta.freshness_policy is missing or not an object');
|
|
@@ -179,8 +147,6 @@ function validateMeta(catalogPath, opts) {
|
|
|
179
147
|
} else if (containsPlaceholder(fp.note)) {
|
|
180
148
|
errors.push('_meta.freshness_policy.note contains placeholder language');
|
|
181
149
|
}
|
|
182
|
-
/* Soft check: cadence < stale < rebuild. Catches an obvious copy-paste
|
|
183
|
-
* mistake without being a hard schema constraint. */
|
|
184
150
|
if (
|
|
185
151
|
typeof fp.default_review_cadence_days === 'number' &&
|
|
186
152
|
typeof fp.stale_after_days === 'number' &&
|
|
@@ -198,15 +164,8 @@ function validateMeta(catalogPath, opts) {
|
|
|
198
164
|
}
|
|
199
165
|
}
|
|
200
166
|
|
|
201
|
-
/*
|
|
202
|
-
*
|
|
203
|
-
* (now - last_updated) > stale_after_days. Emitted at WARN level by
|
|
204
|
-
* default (does not fail validation).
|
|
205
|
-
*
|
|
206
|
-
* Optional `opts.strict` (or `opts.errorOnStale`) promotes the warning
|
|
207
|
-
* to an error; the predeploy gate runs --strict, plain validation keeps
|
|
208
|
-
* the warning posture.
|
|
209
|
-
*/
|
|
167
|
+
/* Staleness is a warning by default; `opts.strict` or `opts.errorOnStale`
|
|
168
|
+
* promotes it to an error, and the predeploy gate runs --strict. */
|
|
210
169
|
if (
|
|
211
170
|
meta.last_updated !== undefined &&
|
|
212
171
|
typeof fp.stale_after_days === 'number' &&
|
|
@@ -214,10 +173,8 @@ function validateMeta(catalogPath, opts) {
|
|
|
214
173
|
) {
|
|
215
174
|
const lu = parseIsoDateStrict(meta.last_updated);
|
|
216
175
|
if (lu === null) {
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
// "invalid input" (error under --strict, warning by default), NOT
|
|
220
|
-
// "no opinion" — otherwise the staleness check fails open.
|
|
176
|
+
// A malformed last_updated is invalid input, not "no opinion":
|
|
177
|
+
// skipping it here would let the staleness check fail open.
|
|
221
178
|
const msg =
|
|
222
179
|
`_meta.last_updated ${JSON.stringify(meta.last_updated)} is not a valid ISO date ` +
|
|
223
180
|
`(YYYY-MM-DD calendar date) — cannot evaluate freshness. ` +
|
|
@@ -244,9 +201,8 @@ function validateMeta(catalogPath, opts) {
|
|
|
244
201
|
}
|
|
245
202
|
}
|
|
246
203
|
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
// public function signature is unchanged for existing callers.
|
|
204
|
+
// Two return shapes: `{errors, warnings}` under opts.includeWarnings,
|
|
205
|
+
// otherwise a bare string[] of errors.
|
|
250
206
|
if (opts && opts.includeWarnings) {
|
|
251
207
|
return { errors, warnings };
|
|
252
208
|
}
|
|
@@ -291,7 +247,7 @@ function main() {
|
|
|
291
247
|
console.log(
|
|
292
248
|
`\n${passed}/${total} catalogs validated${warnSuffix}${failSuffix}.`,
|
|
293
249
|
);
|
|
294
|
-
// process.exitCode
|
|
250
|
+
// process.exitCode, not process.exit() — the exit can truncate a piped write.
|
|
295
251
|
process.exitCode = failed === 0 ? 0 : 1;
|
|
296
252
|
}
|
|
297
253
|
|