@blamejs/exceptd-skills 0.15.48 → 0.15.50
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 +15 -4
- package/data/_indexes/_meta.json +2 -2
- package/lib/collectors/cicd-pipeline-compromise.js +4 -4
- package/lib/collectors/scan-excludes.js +1 -1
- package/lib/lint-skills.js +3 -1
- package/lib/playbook-runner.js +16 -4
- package/lib/sign.js +2 -1
- package/lib/upstream-check-cli.js +3 -1
- package/lib/validate-catalog-meta.js +6 -2
- package/lib/validate-cve-catalog.js +7 -3
- package/lib/validate-package.js +5 -2
- package/lib/validate-playbooks.js +9 -4
- package/lib/validate-vendor.js +5 -2
- package/lib/verify.js +2 -1
- package/manifest.json +44 -44
- package/package.json +3 -1
- package/sbom.cdx.json +132 -47
- package/scripts/check-codebase-patterns-currency.js +142 -0
- package/scripts/check-codebase-patterns.js +337 -0
- package/scripts/predeploy.js +13 -0
- package/scripts/release.js +23 -0
- package/vendor/blamejs/README.md +1 -0
- package/vendor/blamejs/_PROVENANCE.json +16 -0
- package/vendor/blamejs/codepoint-class.js +262 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.50 — 2026-05-30
|
|
4
|
+
|
|
5
|
+
Hardening: `--operator` validation and the operator-text sanitizer now classify and strip Unicode threat codepoints — Trojan-Source bidirectional overrides (CVE-2021-42574), zero-width/invisible marks, C0 controls, and null — through a shared vendored codepoint-threat table, and the `--operator` rejection now names the specific codepoint family (for example "bidirectional-override codepoint") instead of a generic message. Unicode General Category C remains the reject/strip backstop, so the broader control / private-use / unassigned set is still refused.
|
|
6
|
+
|
|
7
|
+
Internal: a new codebase-pattern gate class, `bidi-codepoint-literal`, blocks raw bidi-override / zero-width / null codepoints embedded literally in source (invisible-in-review code reordering — the Trojan-Source class); source must escape them or route through the shared table.
|
|
8
|
+
|
|
9
|
+
## 0.15.49 — 2026-05-30
|
|
10
|
+
|
|
11
|
+
Internal: a new predeploy gate, `scripts/check-codebase-patterns.js`, enforces code-shape bug classes that recurred across releases. It blocks a library-callable function that writes to stdout and then calls `process.exit()` (which truncates buffered output when the stream is piped — the class the v0.15.47 validate-cves fix addressed) and a stale or reason-less `// allow:` suppression marker, and warns on dynamic `RegExp` construction. The flagged `process.exit` sites across the catalog, playbook, package, and vendor validators were converted to the flush-safe `safeExit` form, and the dynamic-`RegExp` sites carry inline justification markers. A companion advisory, wired into the release `prepare` step, flags when the upstream pattern catalog grows a class exceptd hasn't triaged. No change to the shipped CLI surface, catalogs, or skills.
|
|
12
|
+
|
|
3
13
|
## 0.15.48 — 2026-05-30
|
|
4
14
|
|
|
5
15
|
Internal: the release flow is now driven by a phased orchestrator, `scripts/release.js`. Each subcommand (prepare, gates, commit, push, watch, merge, tag, release) runs one idempotent, resumable phase and exits with a script-safe code; the tag phase enforces a GUARD against tag-on-stale-HEAD and version skew between `package.json`, `manifest.json`, and the CHANGELOG heading. No change to the shipped CLI, catalogs, or skills.
|
package/bin/exceptd.js
CHANGED
|
@@ -63,6 +63,7 @@ const PKG_ROOT = path.resolve(__dirname, "..");
|
|
|
63
63
|
const { EXIT_CODES, listExitCodes } = require(path.join(PKG_ROOT, "lib", "exit-codes.js"));
|
|
64
64
|
const { validateIdComponent } = require(path.join(PKG_ROOT, "lib", "id-validation.js"));
|
|
65
65
|
const { suggestFlag, flagsFor, VERB_FLAG_ALLOWLIST } = require(path.join(PKG_ROOT, "lib", "flag-suggest.js"));
|
|
66
|
+
const codepointClass = require(path.join(PKG_ROOT, "vendor", "blamejs", "codepoint-class.js"));
|
|
66
67
|
|
|
67
68
|
// Union of every flag known to ANY verb. A flag that is valid somewhere but
|
|
68
69
|
// not on the active verb (e.g. `--csaf-status` on `brief`) is cross-verb
|
|
@@ -1524,6 +1525,7 @@ function dispatchPlaybook(cmd, argv) {
|
|
|
1524
1525
|
// Cc / Cf / Co / Cn — bidi overrides (U+202E "RTL OVERRIDE"),
|
|
1525
1526
|
// zero-width joiners (U+200B-D), invisible format chars, private-use
|
|
1526
1527
|
// codepoints, unassigned codepoints. An operator string like
|
|
1528
|
+
// allow:bidi-codepoint-literal — illustrative bidi-forgery example in the --operator reject-path doc comment
|
|
1527
1529
|
// "aliceevilbob" renders as "alicebobevila" in any UI that respects
|
|
1528
1530
|
// bidi — a forgery surface where the attested name looks like Bob but the
|
|
1529
1531
|
// bytes are Alice. Reject anything outside a positive allowlist of
|
|
@@ -1552,18 +1554,27 @@ function dispatchPlaybook(cmd, argv) {
|
|
|
1552
1554
|
);
|
|
1553
1555
|
}
|
|
1554
1556
|
if (/\p{C}/u.test(normalized)) {
|
|
1555
|
-
//
|
|
1556
|
-
//
|
|
1557
|
+
// \p{C} (Cc/Cf/Cs/Co/Cn) is the reject gate — it is strictly broader
|
|
1558
|
+
// than the named family regexes (bidi / C0-control / zero-width / null),
|
|
1559
|
+
// so it stays the backstop and catches the divergent remainder the
|
|
1560
|
+
// family tables miss (U+007F, U+0080-009F, private-use, unassigned).
|
|
1561
|
+
// The vendored codepoint tables only CLASSIFY the first offending
|
|
1562
|
+
// codepoint into a human family name for the hint.
|
|
1557
1563
|
let offending = "";
|
|
1564
|
+
let family = "control / format / private-use / unassigned codepoint";
|
|
1558
1565
|
for (const cp of normalized) {
|
|
1559
1566
|
if (/\p{C}/u.test(cp)) {
|
|
1560
1567
|
offending = "U+" + cp.codePointAt(0).toString(16).toUpperCase().padStart(4, "0");
|
|
1568
|
+
if (codepointClass.BIDI_RE.test(cp)) family = "bidirectional-override codepoint";
|
|
1569
|
+
else if (codepointClass.ZERO_WIDTH_RE.test(cp)) family = "zero-width / invisible codepoint";
|
|
1570
|
+
else if (cp === codepointClass.NULL_BYTE) family = "null byte";
|
|
1571
|
+
else if (codepointClass.C0_CTRL_RE.test(cp)) family = "C0 control character";
|
|
1561
1572
|
break;
|
|
1562
1573
|
}
|
|
1563
1574
|
}
|
|
1564
1575
|
return emitError(
|
|
1565
|
-
`${cmd}: --operator contains a Unicode
|
|
1566
|
-
{ verb: cmd, provided_length: args.operator.length, offending_codepoint: offending },
|
|
1576
|
+
`${cmd}: --operator contains a Unicode ${family} (${offending}). Bidi overrides, zero-width joiners, and format marks corrupt attestation rendering and enable name-forgery. Use printable identifiers only.`,
|
|
1577
|
+
{ verb: cmd, provided_length: args.operator.length, offending_codepoint: offending, offending_family: family },
|
|
1567
1578
|
pretty
|
|
1568
1579
|
);
|
|
1569
1580
|
}
|
package/data/_indexes/_meta.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "1.1.0",
|
|
3
|
-
"generated_at": "2026-05-
|
|
3
|
+
"generated_at": "2026-05-30T22:44:21.522Z",
|
|
4
4
|
"generator": "scripts/build-indexes.js",
|
|
5
5
|
"source_count": 54,
|
|
6
6
|
"source_hashes": {
|
|
7
|
-
"manifest.json": "
|
|
7
|
+
"manifest.json": "33da1072778152239ab47e8b4ef930f702678299bfa641e297a233dc9022dbfa",
|
|
8
8
|
"data/atlas-ttps.json": "878b4a08bb73c8d20396d85cf433a88f2bc5e7a8cbf7f6ab773ce7ede0a11251",
|
|
9
9
|
"data/attack-techniques.json": "84fad74c8497cab922ed64b814752f54aa4620c2a938cb06642ff1510e1c5cb3",
|
|
10
10
|
"data/cve-catalog.json": "7a5f4e31401505e53330cdc4b54b39f8a8b04459d6b9411676d291c583ae535f",
|
|
@@ -73,16 +73,16 @@ function walkWorkflows(root) {
|
|
|
73
73
|
// - mapping: `on:\n push:\n pull_request_target:`
|
|
74
74
|
// Heuristic accepts all four.
|
|
75
75
|
function workflowHasTrigger(content, name) {
|
|
76
|
-
if (new RegExp(`^\\s*on:\\s*['"]?${name}['"]?\\s*(?:#.*)?$`, "m").test(content)) return true;
|
|
76
|
+
if (new RegExp(`^\\s*on:\\s*['"]?${name}['"]?\\s*(?:#.*)?$`, "m").test(content)) return true; // allow:dynamic-regex — `name` is a hardcoded trigger literal (pull_request_target / issue_comment / pull_request), never operator/file input
|
|
77
77
|
const listMatch = content.match(/^\s*on:\s*\[([^\]]*)\]/m);
|
|
78
|
-
if (listMatch && new RegExp(`(?:^|,)\\s*['"]?${name}['"]?\\s*(?:,|$)`).test(listMatch[1])) return true;
|
|
78
|
+
if (listMatch && new RegExp(`(?:^|,)\\s*['"]?${name}['"]?\\s*(?:,|$)`).test(listMatch[1])) return true; // allow:dynamic-regex — `name` is a hardcoded trigger literal, never operator/file input
|
|
79
79
|
// block list AND mapping forms both follow `on:\n` with indented
|
|
80
80
|
// continuation lines. Capture the block and inspect for either
|
|
81
81
|
// `- <name>` (list) or `<name>:` (mapping) within it.
|
|
82
82
|
const blockMatch = content.match(/^\s*on:\s*\n((?:[ \t]+[^\n]+\n?)+)/m);
|
|
83
83
|
if (blockMatch) {
|
|
84
|
-
if (new RegExp(`^[ \\t]+-\\s+['"]?${name}['"]?\\s*(?:#.*)?\\s*$`, "m").test(blockMatch[1])) return true;
|
|
85
|
-
if (new RegExp(`^[ \\t]+${name}:`, "m").test(blockMatch[1])) return true;
|
|
84
|
+
if (new RegExp(`^[ \\t]+-\\s+['"]?${name}['"]?\\s*(?:#.*)?\\s*$`, "m").test(blockMatch[1])) return true; // allow:dynamic-regex — `name` is a hardcoded trigger literal, never operator/file input
|
|
85
|
+
if (new RegExp(`^[ \\t]+${name}:`, "m").test(blockMatch[1])) return true; // allow:dynamic-regex — `name` is a hardcoded trigger literal, never operator/file input
|
|
86
86
|
}
|
|
87
87
|
return false;
|
|
88
88
|
}
|
|
@@ -189,7 +189,7 @@ function buildEvidenceLocations(hits) {
|
|
|
189
189
|
const uri = raw.replace(/\\/g, "/");
|
|
190
190
|
const line = Number(h.line);
|
|
191
191
|
const hasLine = Number.isInteger(line) && line > 0;
|
|
192
|
-
const key = hasLine ? `${uri}
|
|
192
|
+
const key = hasLine ? `${uri}\u0000${line}` : uri;
|
|
193
193
|
if (seen.has(key)) continue;
|
|
194
194
|
seen.add(key);
|
|
195
195
|
out.push(hasLine ? { uri, startLine: line } : { uri });
|
package/lib/lint-skills.js
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
const fs = require('node:fs');
|
|
41
41
|
const path = require('node:path');
|
|
42
42
|
const process = require('node:process');
|
|
43
|
+
const { safeExit } = require('./exit-codes');
|
|
43
44
|
|
|
44
45
|
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
45
46
|
const MANIFEST_PATH = path.join(REPO_ROOT, 'manifest.json');
|
|
@@ -866,7 +867,8 @@ function main() {
|
|
|
866
867
|
if (strictFail) {
|
|
867
868
|
console.log(`[lint-skills] --strict: ${warned + (airGapWarnings ? airGapWarnings.length : 0)} warning(s) treated as failures.`);
|
|
868
869
|
}
|
|
869
|
-
|
|
870
|
+
safeExit(failed === 0 && orphans.length === 0 && !strictFail ? 0 : 1);
|
|
871
|
+
return;
|
|
870
872
|
}
|
|
871
873
|
|
|
872
874
|
// Export the minimal frontmatter parser for downstream consumers
|
package/lib/playbook-runner.js
CHANGED
|
@@ -48,6 +48,7 @@ const path = require('path');
|
|
|
48
48
|
const os = require('os');
|
|
49
49
|
const crypto = require('crypto');
|
|
50
50
|
const scoring = require('./scoring');
|
|
51
|
+
const codepointClass = require('../vendor/blamejs/codepoint-class.js');
|
|
51
52
|
|
|
52
53
|
// cross-ref-api wraps catalog reads. If cve-catalog.json is corrupt
|
|
53
54
|
// JSON, cross-ref-api's loadCatalog (post-v0.12.14) catches the parse
|
|
@@ -2184,9 +2185,20 @@ function sanitizeOperatorText(s) {
|
|
|
2184
2185
|
let normalised;
|
|
2185
2186
|
try { normalised = s.normalize('NFC'); }
|
|
2186
2187
|
catch { return null; }
|
|
2187
|
-
//
|
|
2188
|
-
//
|
|
2189
|
-
|
|
2188
|
+
// Two-pass strip. First remove the named threat families (bidi-override /
|
|
2189
|
+
// C0-control / zero-width / null) via the shared vendored codepoint tables,
|
|
2190
|
+
// so the family vocabulary has a single source of truth. Then strip any
|
|
2191
|
+
// remaining General Category C codepoint: \p{C} (Cc/Cf/Cs/Co/Cn) is the
|
|
2192
|
+
// backstop — it is strictly broader than the family union (also catches
|
|
2193
|
+
// U+007F, U+0080-009F, private-use, unassigned), so the family pass is a
|
|
2194
|
+
// documented-intent superset removal and the result is identical to the
|
|
2195
|
+
// single \p{C} strip.
|
|
2196
|
+
const familyStripped = normalised
|
|
2197
|
+
.replace(codepointClass.BIDI_RE_G, '')
|
|
2198
|
+
.replace(codepointClass.C0_CTRL_RE_G, '')
|
|
2199
|
+
.replace(codepointClass.ZW_RE_G, '')
|
|
2200
|
+
.replace(codepointClass.NULL_RE_G, '');
|
|
2201
|
+
const stripped = familyStripped.replace(/\p{C}/gu, '');
|
|
2190
2202
|
const trimmed = stripped.trim();
|
|
2191
2203
|
if (trimmed.length === 0) return null;
|
|
2192
2204
|
// Cap at 256 codepoints (Array.from counts codepoints, not UTF-16 code
|
|
@@ -3632,7 +3644,7 @@ function evalCondition(expr, ctx, playbook) {
|
|
|
3632
3644
|
// analyze() can surface analyze.runtime_errors[] without losing the
|
|
3633
3645
|
// diagnostic.
|
|
3634
3646
|
try {
|
|
3635
|
-
return new RegExp(m[2], 'i').test(val);
|
|
3647
|
+
return new RegExp(m[2], 'i').test(val); // allow:dynamic-regex — m[2] is the pattern from a signed-catalog playbook condition (/…/), and construction + .test() are already wrapped in this try/catch to neutralize a malformed/pathological pattern
|
|
3636
3648
|
} catch (e) {
|
|
3637
3649
|
const errorRec = { _regex_eval_error: { source: m[1], expr: m[2], message: e && e.message ? String(e.message) : String(e) } };
|
|
3638
3650
|
// Two sites where ctx may carry an accumulator: runOpts._runErrors
|
package/lib/sign.js
CHANGED
|
@@ -77,6 +77,7 @@ const fs = require('fs');
|
|
|
77
77
|
const path = require('path');
|
|
78
78
|
const crypto = require('crypto');
|
|
79
79
|
const { execFileSync } = require('child_process');
|
|
80
|
+
const { safeExit } = require('./exit-codes');
|
|
80
81
|
|
|
81
82
|
const ROOT = path.join(__dirname, '..');
|
|
82
83
|
const MANIFEST_PATH = path.join(ROOT, 'manifest.json');
|
|
@@ -219,7 +220,7 @@ function signAll() {
|
|
|
219
220
|
}
|
|
220
221
|
printFingerprintBanner();
|
|
221
222
|
|
|
222
|
-
if (errors > 0)
|
|
223
|
+
if (errors > 0) { safeExit(1); return; }
|
|
223
224
|
}
|
|
224
225
|
|
|
225
226
|
/**
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
const path = require("path");
|
|
25
25
|
const fs = require("fs");
|
|
26
|
+
const { safeExit } = require("./exit-codes");
|
|
26
27
|
|
|
27
28
|
const ROOT = path.resolve(__dirname, "..");
|
|
28
29
|
const { fetchLatestPublished, buildFreshnessReport } = require("./upstream-check.js");
|
|
@@ -65,7 +66,8 @@ function readManifest() {
|
|
|
65
66
|
reason: "registry probe disabled in air-gap mode",
|
|
66
67
|
source: "upstream-check",
|
|
67
68
|
}) + "\n");
|
|
68
|
-
|
|
69
|
+
safeExit(0);
|
|
70
|
+
return;
|
|
69
71
|
}
|
|
70
72
|
const registry = await fetchLatestPublished({ timeoutMs: opts.timeoutMs });
|
|
71
73
|
if (opts.raw) {
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
const fs = require('node:fs');
|
|
32
32
|
const path = require('node:path');
|
|
33
33
|
const process = require('node:process');
|
|
34
|
+
const { safeExit } = require('./exit-codes');
|
|
34
35
|
|
|
35
36
|
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
36
37
|
const DATA_DIR = path.join(REPO_ROOT, 'data');
|
|
@@ -64,10 +65,12 @@ function parseArgs(argv) {
|
|
|
64
65
|
' --quiet Suppress per-catalog PASS output; show failures only.\n' +
|
|
65
66
|
' --strict Promote freshness warnings to errors (used by the predeploy gate).\n',
|
|
66
67
|
);
|
|
67
|
-
|
|
68
|
+
safeExit(0);
|
|
69
|
+
return null;
|
|
68
70
|
} else {
|
|
69
71
|
console.error(`Unknown argument: ${a}`);
|
|
70
|
-
|
|
72
|
+
safeExit(2);
|
|
73
|
+
return null;
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
return opts;
|
|
@@ -208,6 +211,7 @@ function validateMeta(catalogPath, opts) {
|
|
|
208
211
|
|
|
209
212
|
function main() {
|
|
210
213
|
const opts = parseArgs(process.argv);
|
|
214
|
+
if (opts === null) return; // parseArgs handled --help / bad-arg and set the exit code
|
|
211
215
|
const files = fs
|
|
212
216
|
.readdirSync(DATA_DIR)
|
|
213
217
|
.filter((f) => f.endsWith('.json'))
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
const fs = require('node:fs');
|
|
27
27
|
const path = require('node:path');
|
|
28
28
|
const process = require('node:process');
|
|
29
|
+
const { safeExit } = require('./exit-codes');
|
|
29
30
|
|
|
30
31
|
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
31
32
|
const SCHEMA_PATH = path.join(REPO_ROOT, 'lib', 'schemas', 'cve-catalog.schema.json');
|
|
@@ -82,10 +83,12 @@ function parseArgs(argv) {
|
|
|
82
83
|
' --quiet Suppress per-CVE PASS output; show failures only.\n' +
|
|
83
84
|
' --strict Promote advisory warnings to errors (used by the predeploy gate). Off by default.\n',
|
|
84
85
|
);
|
|
85
|
-
|
|
86
|
+
safeExit(0);
|
|
87
|
+
return null;
|
|
86
88
|
} else {
|
|
87
89
|
console.error(`Unknown argument: ${a}`);
|
|
88
|
-
|
|
90
|
+
safeExit(2);
|
|
91
|
+
return null;
|
|
89
92
|
}
|
|
90
93
|
}
|
|
91
94
|
return opts;
|
|
@@ -138,7 +141,7 @@ function validate(value, schema, schemaName, pathStr) {
|
|
|
138
141
|
errors.push(`${here}: string shorter than minLength ${schema.minLength}`);
|
|
139
142
|
}
|
|
140
143
|
if (schema.pattern !== undefined) {
|
|
141
|
-
const re = new RegExp(schema.pattern);
|
|
144
|
+
const re = new RegExp(schema.pattern); // allow:dynamic-regex — bundled schema.pattern, not operator input
|
|
142
145
|
if (!re.test(value)) {
|
|
143
146
|
errors.push(`${here}: string ${JSON.stringify(value)} does not match pattern /${schema.pattern}/`);
|
|
144
147
|
}
|
|
@@ -312,6 +315,7 @@ function additionalChecks(key, entry, ctx) {
|
|
|
312
315
|
|
|
313
316
|
function main() {
|
|
314
317
|
const opts = parseArgs(process.argv);
|
|
318
|
+
if (opts === null) return; // parseArgs handled --help / bad-arg and set the exit code
|
|
315
319
|
const schema = readJson(SCHEMA_PATH);
|
|
316
320
|
const catalog = readJson(CATALOG_PATH);
|
|
317
321
|
const lessons = readJson(LESSONS_PATH);
|
package/lib/validate-package.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
const fs = require("fs");
|
|
20
20
|
const path = require("path");
|
|
21
21
|
const { spawnSync } = require("child_process");
|
|
22
|
+
const { safeExit } = require("./exit-codes");
|
|
22
23
|
|
|
23
24
|
const ROOT = path.join(__dirname, "..");
|
|
24
25
|
const ABS = (p) => path.join(ROOT, p);
|
|
@@ -154,12 +155,14 @@ function main() {
|
|
|
154
155
|
`${packInfo.files.length} files, ` +
|
|
155
156
|
`${sizeMB} MB packed / ${unpackedMB} MB unpacked.\n`
|
|
156
157
|
);
|
|
157
|
-
|
|
158
|
+
safeExit(0);
|
|
159
|
+
return;
|
|
158
160
|
}
|
|
159
161
|
|
|
160
162
|
process.stderr.write(`[validate-package] FAILED — ${issues.length} issue(s):\n`);
|
|
161
163
|
for (const i of issues) process.stderr.write(` • ${i}\n`);
|
|
162
|
-
|
|
164
|
+
safeExit(1);
|
|
165
|
+
return;
|
|
163
166
|
}
|
|
164
167
|
|
|
165
168
|
if (require.main === module) main();
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
const fs = require('node:fs');
|
|
71
71
|
const path = require('node:path');
|
|
72
72
|
const process = require('node:process');
|
|
73
|
+
const { safeExit } = require('./exit-codes');
|
|
73
74
|
|
|
74
75
|
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
75
76
|
const SCHEMA_PATH = path.join(REPO_ROOT, 'lib', 'schemas', 'playbook.schema.json');
|
|
@@ -94,10 +95,12 @@ function parseArgs(argv) {
|
|
|
94
95
|
' --quiet Suppress per-playbook PASS output; show failures only.\n' +
|
|
95
96
|
' --strict Treat warnings as errors (used by the predeploy gate).\n',
|
|
96
97
|
);
|
|
97
|
-
|
|
98
|
+
safeExit(0);
|
|
99
|
+
return null;
|
|
98
100
|
} else {
|
|
99
101
|
console.error(`Unknown argument: ${a}`);
|
|
100
|
-
|
|
102
|
+
safeExit(2);
|
|
103
|
+
return null;
|
|
101
104
|
}
|
|
102
105
|
}
|
|
103
106
|
return opts;
|
|
@@ -161,7 +164,7 @@ function validate(value, schema, schemaName, pathStr) {
|
|
|
161
164
|
err(`${here}: string shorter than minLength ${schema.minLength}`);
|
|
162
165
|
}
|
|
163
166
|
if (schema.pattern !== undefined) {
|
|
164
|
-
const re = new RegExp(schema.pattern);
|
|
167
|
+
const re = new RegExp(schema.pattern); // allow:dynamic-regex — bundled schema.pattern, not operator input
|
|
165
168
|
if (!re.test(value)) {
|
|
166
169
|
err(`${here}: string ${JSON.stringify(value)} does not match pattern /${schema.pattern}/`);
|
|
167
170
|
}
|
|
@@ -559,6 +562,7 @@ function checkMutexReciprocity(playbooks) {
|
|
|
559
562
|
|
|
560
563
|
function main() {
|
|
561
564
|
const opts = parseArgs(process.argv);
|
|
565
|
+
if (opts === null) return; // parseArgs handled --help / bad-arg and set the exit code
|
|
562
566
|
const schema = readJson(SCHEMA_PATH);
|
|
563
567
|
const ctx = loadContext();
|
|
564
568
|
const playbooks = loadPlaybooks();
|
|
@@ -617,7 +621,8 @@ function main() {
|
|
|
617
621
|
(warned ? `, ${warned} with warnings` : '') +
|
|
618
622
|
(errored ? `, ${errored} failed` : '') + '.',
|
|
619
623
|
);
|
|
620
|
-
|
|
624
|
+
safeExit(errored === 0 ? 0 : 1);
|
|
625
|
+
return;
|
|
621
626
|
}
|
|
622
627
|
|
|
623
628
|
module.exports = {
|
package/lib/validate-vendor.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
const fs = require("fs");
|
|
21
21
|
const path = require("path");
|
|
22
22
|
const crypto = require("crypto");
|
|
23
|
+
const { safeExit } = require("./exit-codes");
|
|
23
24
|
|
|
24
25
|
const ROOT = path.join(__dirname, "..");
|
|
25
26
|
const PROV = path.join(ROOT, "vendor", "blamejs", "_PROVENANCE.json");
|
|
@@ -71,13 +72,15 @@ function main() {
|
|
|
71
72
|
if (issues.length === 0) {
|
|
72
73
|
const fileCount = Object.keys(prov.files || {}).length;
|
|
73
74
|
console.log(`[validate-vendor] vendor tree current — ${fileCount} file(s) validated against pin ${prov.pinned_commit?.slice(0, 12) || "?"}.`);
|
|
74
|
-
|
|
75
|
+
safeExit(0);
|
|
76
|
+
return;
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
console.error("[validate-vendor] vendor tree DRIFT:");
|
|
78
80
|
for (const i of issues) console.error(" • " + i);
|
|
79
81
|
console.error("[validate-vendor] re-vendor instructions: vendor/blamejs/README.md");
|
|
80
|
-
|
|
82
|
+
safeExit(1);
|
|
83
|
+
return;
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
if (require.main === module) main();
|
package/lib/verify.js
CHANGED
|
@@ -520,7 +520,7 @@ function validateAgainstSchema(value, schema, here, root) {
|
|
|
520
520
|
errors.push(`${here}: string shorter than minLength ${effectiveSchema.minLength}`);
|
|
521
521
|
}
|
|
522
522
|
if (effectiveSchema.pattern !== undefined) {
|
|
523
|
-
const re = new RegExp(effectiveSchema.pattern);
|
|
523
|
+
const re = new RegExp(effectiveSchema.pattern); // allow:dynamic-regex — bundled schema.pattern, not operator input
|
|
524
524
|
if (!re.test(value)) {
|
|
525
525
|
errors.push(`${here}: string ${JSON.stringify(value)} does not match pattern /${effectiveSchema.pattern}/`);
|
|
526
526
|
}
|
|
@@ -648,6 +648,7 @@ function checkExpectedFingerprint(liveFp, pinPath) {
|
|
|
648
648
|
// Route through the shared loader so a BOM-prefixed pin file
|
|
649
649
|
// (Notepad with files.encoding=utf8bom) is tolerated identically across
|
|
650
650
|
// every verify site. Pre-fix the verbatim split-trim-find produced a
|
|
651
|
+
// allow:bidi-codepoint-literal — illustrative BOM-prefixed first-line in the pin-loader doc comment
|
|
651
652
|
// first-line of "SHA256:..." (with leading BOM) that would never equal
|
|
652
653
|
// a live fingerprint.
|
|
653
654
|
const firstLine = loadExpectedFingerprintFirstLine(p) || '';
|