@blamejs/exceptd-skills 0.19.34 → 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 +18 -0
- package/bin/exceptd.js +0 -5
- package/data/_indexes/_meta.json +2 -2
- package/lib/collectors/ai-api.js +150 -22
- package/lib/collectors/cicd-pipeline-compromise.js +73 -28
- package/lib/collectors/library-author.js +141 -5
- package/lib/collectors/sbom.js +96 -12
- package/lib/collectors/scan-excludes.js +3 -2
- package/lib/cve-regression-watcher.js +0 -3
- package/lib/framework-gap.js +5 -0
- package/lib/lint-skills.js +24 -4
- package/lib/playbook-runner.js +68 -14
- package/lib/prefetch.js +3 -2
- package/lib/refresh-external.js +0 -6
- package/lib/refresh-network.js +3 -4
- package/lib/scoring.js +8 -1
- package/lib/ttp-mapper.js +14 -3
- package/lib/upstream-check-cli.js +26 -1
- package/lib/validate-cve-catalog.js +9 -2
- package/lib/validate-playbooks.js +9 -11
- package/manifest.json +53 -53
- package/orchestrator/index.js +0 -1
- package/package.json +1 -1
- package/sbom.cdx.json +78 -78
- package/scripts/audit-perf.js +24 -13
- package/scripts/builders/theater-fingerprints.js +9 -4
- package/scripts/check-agents-md-collectors.js +15 -3
- package/scripts/check-codebase-patterns.js +16 -3
- package/scripts/check-manifest-snapshot.js +49 -8
- package/scripts/check-test-coverage.js +17 -1
- package/scripts/refresh-mitre-atlas.js +5 -1
- package/scripts/refresh-mitre-ics-attack.js +5 -1
- package/scripts/refresh-rfc-index.js +6 -1
- package/scripts/refresh-upstream-catalogs.js +25 -13
- package/scripts/release.js +0 -2
- package/scripts/run-e2e-scenarios.js +2 -2
- package/scripts/verify-shipped-tarball.js +0 -1
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
* - file-level, in the first 50 lines: // codebase-patterns:allow-file <class> — <reason>
|
|
9
9
|
* - per-line, on the same line or up to 2 lines above: // allow:<class> — <reason>
|
|
10
10
|
*
|
|
11
|
-
* Owned elsewhere: phase/version vocabulary (check-version-tags.js),
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Owned elsewhere: phase/version vocabulary (check-version-tags.js), test
|
|
12
|
+
* assertions (check-test-coverage.js), and — both now in tests/cli.test.js,
|
|
13
|
+
* which absorbed the separate safe-exit-grep and operator-leak-grep files —
|
|
14
|
+
* the per-file CLI-dispatch process.exit ban and operator-output path leaks.
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
const fs = require("node:fs");
|
|
@@ -209,6 +210,18 @@ function inRanges(ranges, lineNo) {
|
|
|
209
210
|
// stopping there leaves a real exit-after-write unflagged.
|
|
210
211
|
const FUNCTION_START = /(^|[^.\w])function\b|=>\s*\{?\s*$|^\s*(async\s+)?(?!(?:if|for|while|switch|catch|do|else|with|finally|return)\b)[A-Za-z_$][\w$]*\s*\([^)]*\)\s*\{/;
|
|
211
212
|
|
|
213
|
+
// Scope, stated so it is not mistaken for full coverage of the class: the
|
|
214
|
+
// backward scan recognises a result-channel write only where it is written
|
|
215
|
+
// LITERALLY — `process.stdout.write(` or `console.log(`. A write reached
|
|
216
|
+
// INDIRECTLY, through a helper called from the exiting function (`printHelp()`,
|
|
217
|
+
// `renderSummary()`), is invisible to it, so an exit-after-write of that shape
|
|
218
|
+
// passes this gate and has to be caught by review or a per-file test.
|
|
219
|
+
//
|
|
220
|
+
// Not closed by matching call sites of same-file writer functions: measured over
|
|
221
|
+
// lib/, orchestrator/, scripts/ and bin/, that heuristic cannot tell a helper
|
|
222
|
+
// that writes to STDOUT from one that writes to stderr via console.error, and it
|
|
223
|
+
// fires on exits that are correct. Closing it properly needs call-graph
|
|
224
|
+
// resolution of the write target, not another regex.
|
|
212
225
|
function detectProcessExitAfterStdout(files) {
|
|
213
226
|
const hits = [];
|
|
214
227
|
for (const rel of (files || filesUnder(["bin/exceptd.js", "lib", "orchestrator", "scripts"]))) {
|
|
@@ -37,12 +37,47 @@ function captureSurface(manifest) {
|
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// Absent and corrupt are different states and must not collapse into one.
|
|
41
|
+
//
|
|
42
|
+
// ABSENT (key not in the object) is the stale baseline this gate exists to
|
|
43
|
+
// report on: a snapshot committed before a surface field existed carries no key
|
|
44
|
+
// for it, and an absent field is an empty surface, not a crash. Reading it
|
|
45
|
+
// unguarded threw a TypeError that the CLI's outer catch turned into exit 2 with
|
|
46
|
+
// a stack trace instead of an additive-change report.
|
|
47
|
+
//
|
|
48
|
+
// PRESENT-BUT-NOT-AN-ARRAY (a string, an object, null) is corruption. Coercing
|
|
49
|
+
// it to [] would report every live entry as additive and exit 0 — a gate that
|
|
50
|
+
// passes without checking anything. It raises instead, and the outer catch turns
|
|
51
|
+
// that into exit 2 naming the skill and the field.
|
|
52
|
+
function asArray(value, skillName, field) {
|
|
53
|
+
if (value === undefined) return [];
|
|
54
|
+
if (Array.isArray(value)) return value;
|
|
55
|
+
throw new Error(
|
|
56
|
+
`${skillName}: ${field} is ${value === null ? "null" : typeof value}, not an array. ` +
|
|
57
|
+
"The baseline or manifest is corrupt, not merely stale — regenerate the baseline " +
|
|
58
|
+
"with `node scripts/refresh-manifest-snapshot.js` and re-check the manifest."
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// captureSurface() always writes `skills`, so unlike the per-field case above
|
|
63
|
+
// there is no legitimate historical baseline without it: absent and non-array
|
|
64
|
+
// are both corruption here, and treating either as [] would report every skill
|
|
65
|
+
// as added and exit 0.
|
|
66
|
+
function skillList(surface, which) {
|
|
67
|
+
const skills = surface && surface.skills;
|
|
68
|
+
if (Array.isArray(skills)) return skills;
|
|
69
|
+
throw new Error(
|
|
70
|
+
`${which}.skills is ${skills === null ? "null" : typeof skills}, not an array. ` +
|
|
71
|
+
"Regenerate the baseline with `node scripts/refresh-manifest-snapshot.js`."
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
40
75
|
function diff(baseline, current) {
|
|
41
76
|
const breaking = [];
|
|
42
77
|
const additive = [];
|
|
43
78
|
|
|
44
|
-
const bSkills = new Map(baseline.
|
|
45
|
-
const cSkills = new Map(current.
|
|
79
|
+
const bSkills = new Map(skillList(baseline, "baseline").map(s => [s.name, s]));
|
|
80
|
+
const cSkills = new Map(skillList(current, "current").map(s => [s.name, s]));
|
|
46
81
|
|
|
47
82
|
for (const name of bSkills.keys()) {
|
|
48
83
|
if (!cSkills.has(name)) {
|
|
@@ -70,32 +105,38 @@ function diff(baseline, current) {
|
|
|
70
105
|
}
|
|
71
106
|
|
|
72
107
|
// Removed trigger keywords break downstream skill matchers.
|
|
73
|
-
const
|
|
108
|
+
const bTriggers = asArray(b.triggers, name, "baseline triggers");
|
|
109
|
+
const cTriggers = asArray(c.triggers, name, "triggers");
|
|
110
|
+
const removedTriggers = bTriggers.filter(t => !cTriggers.includes(t));
|
|
74
111
|
if (removedTriggers.length > 0) {
|
|
75
112
|
breaking.push(`${name}: removed trigger keywords: ${removedTriggers.join(", ")}`);
|
|
76
113
|
}
|
|
77
|
-
const addedTriggers =
|
|
114
|
+
const addedTriggers = cTriggers.filter(t => !bTriggers.includes(t));
|
|
78
115
|
if (addedTriggers.length > 0) {
|
|
79
116
|
additive.push(`${name}: added trigger keywords: ${addedTriggers.join(", ")}`);
|
|
80
117
|
}
|
|
81
118
|
|
|
82
119
|
// Removed data deps break the skill at load time.
|
|
83
|
-
const
|
|
120
|
+
const bDeps = asArray(b.data_deps, name, "baseline data_deps");
|
|
121
|
+
const cDeps = asArray(c.data_deps, name, "data_deps");
|
|
122
|
+
const removedDeps = bDeps.filter(d => !cDeps.includes(d));
|
|
84
123
|
if (removedDeps.length > 0) {
|
|
85
124
|
breaking.push(`${name}: removed data deps: ${removedDeps.join(", ")}`);
|
|
86
125
|
}
|
|
87
|
-
const addedDeps =
|
|
126
|
+
const addedDeps = cDeps.filter(d => !bDeps.includes(d));
|
|
88
127
|
if (addedDeps.length > 0) {
|
|
89
128
|
additive.push(`${name}: added data deps: ${addedDeps.join(", ")}`);
|
|
90
129
|
}
|
|
91
130
|
|
|
92
131
|
// Narrowing the cited surface is deliberate (AGENTS.md #4, #12), so removal is breaking.
|
|
93
132
|
for (const field of ["atlas_refs", "attack_refs", "framework_gaps", "rfc_refs", "cwe_refs", "d3fend_refs", "dlp_refs"]) {
|
|
94
|
-
const
|
|
133
|
+
const bRefs = asArray(b[field], name, `baseline ${field}`);
|
|
134
|
+
const cRefs = asArray(c[field], name, field);
|
|
135
|
+
const removed = bRefs.filter(r => !cRefs.includes(r));
|
|
95
136
|
if (removed.length > 0) {
|
|
96
137
|
breaking.push(`${name}: removed ${field}: ${removed.join(", ")}`);
|
|
97
138
|
}
|
|
98
|
-
const added =
|
|
139
|
+
const added = cRefs.filter(r => !bRefs.includes(r));
|
|
99
140
|
if (added.length > 0) {
|
|
100
141
|
additive.push(`${name}: added ${field}: ${added.join(", ")}`);
|
|
101
142
|
}
|
|
@@ -37,10 +37,26 @@ function parseArgs(argv) {
|
|
|
37
37
|
|
|
38
38
|
function printHelp() {
|
|
39
39
|
const banner =
|
|
40
|
+
"check-test-coverage — report changed surface that no test covers.\n" +
|
|
41
|
+
"\n" +
|
|
40
42
|
"Usage: node scripts/check-test-coverage.js [--base <ref>] [--staged]\n" +
|
|
41
43
|
" [--json] [--warn-only]\n" +
|
|
42
44
|
"\n" +
|
|
43
|
-
"
|
|
45
|
+
"Flags:\n" +
|
|
46
|
+
" --base <ref> compare against <ref>..HEAD (default: origin/main)\n" +
|
|
47
|
+
" --staged compare the staged set instead of a ref range\n" +
|
|
48
|
+
" --json emit the result as JSON instead of a text report\n" +
|
|
49
|
+
" --warn-only report findings but always exit 0\n" +
|
|
50
|
+
"\n" +
|
|
51
|
+
"Surfaces checked: CLI verbs and flags in bin/exceptd.js, exported functions\n" +
|
|
52
|
+
"in lib / orchestrator / scripts, playbook detect-indicator and look-artifact\n" +
|
|
53
|
+
"ids, and CVE entries whose iocs changed.\n" +
|
|
54
|
+
"\n" +
|
|
55
|
+
"Categorization: docs, tooling dotfiles, tests/ itself and derived indexes are\n" +
|
|
56
|
+
"allowlisted; workflows, manifests, schemas, SBOM and unclassified files are\n" +
|
|
57
|
+
"surfaced as manual-review, never auto-green.\n" +
|
|
58
|
+
"\n" +
|
|
59
|
+
"Exit codes: 0 clean or --warn-only, 1 uncovered surface, 2 runner error.\n";
|
|
44
60
|
process.stdout.write(banner);
|
|
45
61
|
}
|
|
46
62
|
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
* Thin per-type wrapper for the MITRE ATLAS refresher; the logic lives in
|
|
5
5
|
* scripts/refresh-upstream-catalogs.js#refreshAtlas. Wired as
|
|
6
6
|
* `npm run refresh-mitre-atlas`, and takes --dry-run.
|
|
7
|
+
*
|
|
8
|
+
* `CAP=<n>` bounds how many NEW techniques one run may add, matching every
|
|
9
|
+
* other per-type wrapper. Context backfill onto existing rows is never capped.
|
|
7
10
|
*/
|
|
8
11
|
const { refreshAtlas } = require("./refresh-upstream-catalogs.js");
|
|
9
12
|
const dry = process.argv.includes("--dry-run");
|
|
10
|
-
|
|
13
|
+
const cap = Number(process.env.CAP || Infinity);
|
|
14
|
+
refreshAtlas({ dry, cap }).catch((e) => { console.error("[err]", e); process.exit(1); });
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
* `npm run refresh-mitre-ics-attack [-- --dry-run]` — per-type wrapper for the
|
|
5
5
|
* MITRE ICS-attack STIX refresher; the logic lives in
|
|
6
6
|
* scripts/refresh-upstream-catalogs.js#refreshIcsAttack.
|
|
7
|
+
*
|
|
8
|
+
* `CAP=<n>` bounds how many NEW techniques one run may add, matching every
|
|
9
|
+
* other per-type wrapper. Context backfill onto existing rows is never capped.
|
|
7
10
|
*/
|
|
8
11
|
const { refreshIcsAttack } = require("./refresh-upstream-catalogs.js");
|
|
9
12
|
const dry = process.argv.includes("--dry-run");
|
|
10
|
-
|
|
13
|
+
const cap = Number(process.env.CAP || Infinity);
|
|
14
|
+
refreshIcsAttack({ dry, cap }).catch((e) => { console.error("[err]", e); process.exit(1); });
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Refreshes only the RFC catalog, leaving ATT&CK / ATLAS / D3FEND untouched.
|
|
5
5
|
* Logic lives in scripts/refresh-upstream-catalogs.js#refreshRfc.
|
|
6
|
+
*
|
|
7
|
+
* `CAP=<n>` bounds how many NEW rows one run may add — the upstream RFC index
|
|
8
|
+
* carries ~9000 entries, so an uncapped first run imports all of them at once.
|
|
9
|
+
* Context backfill onto rows already curated is never capped.
|
|
6
10
|
*/
|
|
7
11
|
const { refreshRfc } = require("./refresh-upstream-catalogs.js");
|
|
8
12
|
const dry = process.argv.includes("--dry-run");
|
|
9
|
-
|
|
13
|
+
const cap = Number(process.env.CAP || Infinity);
|
|
14
|
+
refreshRfc({ dry, cap }).catch((e) => { console.error("[err]", e); process.exit(1); });
|
|
@@ -14,15 +14,6 @@ const path = require("path");
|
|
|
14
14
|
const ROOT = path.join(__dirname, "..");
|
|
15
15
|
const TODAY = new Date().toISOString().slice(0, 10);
|
|
16
16
|
|
|
17
|
-
// Required-context field names come from the audit-catalog-gaps SPEC, never a
|
|
18
|
-
// second hardcoded list that would drift from it.
|
|
19
|
-
const AUDIT_SPEC = require("./audit-catalog-gaps.js").SPEC;
|
|
20
|
-
function specRequiredFields(catalogKey) {
|
|
21
|
-
const spec = AUDIT_SPEC[catalogKey];
|
|
22
|
-
if (!spec || !Array.isArray(spec.required_context)) return [];
|
|
23
|
-
return spec.required_context.map((r) => r.field);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
17
|
const MAX_REDIRECTS = 5;
|
|
27
18
|
|
|
28
19
|
// Rejects on anything but a 2xx, so an error body can never reach a consumer as
|
|
@@ -186,7 +177,7 @@ function parseRfcEntry(blk) {
|
|
|
186
177
|
};
|
|
187
178
|
}
|
|
188
179
|
|
|
189
|
-
async function refreshRfc({ dry = false, _deps = {} } = {}) {
|
|
180
|
+
async function refreshRfc({ dry = false, cap = Infinity, _deps = {} } = {}) {
|
|
190
181
|
const _fetchUrl = _deps.fetchUrl || fetchUrl;
|
|
191
182
|
const _loadCatalog = _deps.loadCatalog || loadCatalog;
|
|
192
183
|
const _writeCatalog = _deps.writeCatalog || writeCatalog;
|
|
@@ -254,6 +245,9 @@ async function refreshRfc({ dry = false, _deps = {} } = {}) {
|
|
|
254
245
|
// Existing rows handled in the first-pass backfill above.
|
|
255
246
|
if (existing.has(id)) continue;
|
|
256
247
|
if (e.status === "UNKNOWN") continue;
|
|
248
|
+
// The cap bounds new adds only; the backfill pass above is unbounded, so a
|
|
249
|
+
// capped run still completes context on every row already curated.
|
|
250
|
+
if (added >= cap) continue;
|
|
257
251
|
const obsoleted = !!e.obsoleted || e.status === "HISTORIC";
|
|
258
252
|
cat[id] = {
|
|
259
253
|
number: e.num,
|
|
@@ -479,6 +473,7 @@ async function refreshIcsAttack({ dry = false, cap = Infinity, _deps = {} } = {}
|
|
|
479
473
|
const local = _loadCatalog("attack-techniques.json");
|
|
480
474
|
const existing = new Set(Object.keys(local).filter((k) => k !== "_meta"));
|
|
481
475
|
let added = 0, backfilled = 0;
|
|
476
|
+
const skippedNoIcsTactic = [];
|
|
482
477
|
for (const t of techs) {
|
|
483
478
|
const extRef = (t.external_references || []).find((r) => r.source_name === "mitre-ics-attack" || r.source_name === "mitre-attack");
|
|
484
479
|
if (!extRef || !extRef.external_id) continue;
|
|
@@ -486,6 +481,17 @@ async function refreshIcsAttack({ dry = false, cap = Infinity, _deps = {} } = {}
|
|
|
486
481
|
const tactics = (t.kill_chain_phases || [])
|
|
487
482
|
.filter((p) => (p.kill_chain_name || "").includes("ics"))
|
|
488
483
|
.map((p) => ICS_TACTIC_NAME[p.phase_name] || `${p.phase_name} (ICS)`);
|
|
484
|
+
// The external-reference match above accepts a cross-listed enterprise
|
|
485
|
+
// reference, while the tactic map keeps ICS kill-chain phases only. An
|
|
486
|
+
// object matched by the first and not the second would land with
|
|
487
|
+
// `tactic: []`, which audit-catalog-gaps counts as a missing-context gap
|
|
488
|
+
// the moment it is written — the import spending gap budget on itself.
|
|
489
|
+
// Enterprise-only techniques belong to refreshAttack, so skip them here,
|
|
490
|
+
// and report the count so the omission is observable rather than silent.
|
|
491
|
+
if (tactics.length === 0) {
|
|
492
|
+
skippedNoIcsTactic.push(id);
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
489
495
|
const fullDesc = String(t.description || "").replace(/\s+/g, " ").trim();
|
|
490
496
|
let shortDesc = fullDesc.split(/\.\s/)[0];
|
|
491
497
|
if (shortDesc.length > 500) shortDesc = shortDesc.slice(0, 497) + "...";
|
|
@@ -514,7 +520,10 @@ async function refreshIcsAttack({ dry = false, cap = Infinity, _deps = {} } = {}
|
|
|
514
520
|
existing.add(id);
|
|
515
521
|
added++;
|
|
516
522
|
}
|
|
517
|
-
if (
|
|
523
|
+
if (skippedNoIcsTactic.length) {
|
|
524
|
+
console.log(`[refresh-upstream:ics-attack] skipped ${skippedNoIcsTactic.length} technique(s) with no ICS kill-chain phase (enterprise-only, handled by the ATT&CK refresher): ${skippedNoIcsTactic.slice(0, 10).join(", ")}${skippedNoIcsTactic.length > 10 ? ", ..." : ""}`);
|
|
525
|
+
}
|
|
526
|
+
if (dry) { console.log(`[refresh-upstream:ics-attack] DRY-RUN: +${added} new, ${backfilled} backfills`); return { added, backfilled, skipped_no_ics_tactic: skippedNoIcsTactic.length }; }
|
|
518
527
|
const changed = added > 0 || backfilled > 0;
|
|
519
528
|
if (changed) {
|
|
520
529
|
if (local._meta) { local._meta.last_updated = TODAY; local._meta.last_threat_review = TODAY; }
|
|
@@ -523,7 +532,7 @@ async function refreshIcsAttack({ dry = false, cap = Infinity, _deps = {} } = {}
|
|
|
523
532
|
} else {
|
|
524
533
|
console.log("[ok] attack-techniques.json: no upstream ICS changes — file unchanged");
|
|
525
534
|
}
|
|
526
|
-
return { added, backfilled };
|
|
535
|
+
return { added, backfilled, skipped_no_ics_tactic: skippedNoIcsTactic.length };
|
|
527
536
|
}
|
|
528
537
|
|
|
529
538
|
const ATLAS_SRC = "https://raw.githubusercontent.com/mitre-atlas/atlas-navigator-data/main/dist/stix-atlas.json";
|
|
@@ -595,7 +604,7 @@ function backfillAtlas(cur, fresh) {
|
|
|
595
604
|
return touched;
|
|
596
605
|
}
|
|
597
606
|
|
|
598
|
-
async function refreshAtlas({ dry = false, _deps = {} } = {}) {
|
|
607
|
+
async function refreshAtlas({ dry = false, cap = Infinity, _deps = {} } = {}) {
|
|
599
608
|
const _fetchUrl = _deps.fetchUrl || fetchUrl;
|
|
600
609
|
const _loadCatalog = _deps.loadCatalog || loadCatalog;
|
|
601
610
|
const _writeCatalog = _deps.writeCatalog || writeCatalog;
|
|
@@ -637,6 +646,9 @@ async function refreshAtlas({ dry = false, _deps = {} } = {}) {
|
|
|
637
646
|
if (backfillAtlas(cur, fresh)) { cur.last_verified = TODAY; backfilled++; }
|
|
638
647
|
continue;
|
|
639
648
|
}
|
|
649
|
+
// Bounds new adds only, matching every other refresher; backfill above runs
|
|
650
|
+
// on the full upstream set regardless of the cap.
|
|
651
|
+
if (added >= cap) continue;
|
|
640
652
|
local[id] = atlasEntryFromStix(t, ext);
|
|
641
653
|
existing.add(id);
|
|
642
654
|
added++;
|
package/scripts/release.js
CHANGED
|
@@ -14,8 +14,6 @@ var ROOT = path.resolve(__dirname, "..");
|
|
|
14
14
|
var REPO = "blamejs/exceptd-skills";
|
|
15
15
|
var PKG_NAME = "@blamejs/exceptd-skills";
|
|
16
16
|
|
|
17
|
-
var RERUN_LIMIT = 2;
|
|
18
|
-
|
|
19
17
|
// Windows resolves `npm` / `npx` as `.cmd` shims, which child_process can only
|
|
20
18
|
// invoke through a shell; `git`, `gh` and `node` are native exes that spawn directly.
|
|
21
19
|
function _needsShell(cmd) {
|
|
@@ -52,7 +52,7 @@ function stderrBanFailures(expect, stderr) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// Positive matchers against the parsed JSON body only; the stderr ban is separate.
|
|
55
|
-
function diffExpect(jsonBody, expect
|
|
55
|
+
function diffExpect(jsonBody, expect) {
|
|
56
56
|
const failures = [];
|
|
57
57
|
if (expect.json_path_equals) {
|
|
58
58
|
for (const [p, want] of Object.entries(expect.json_path_equals)) {
|
|
@@ -136,7 +136,7 @@ function evaluateScenario(scenario, expect, res) {
|
|
|
136
136
|
if (!body && hasJsonAssertion) {
|
|
137
137
|
failures.push(`stdout did not parse as JSON; first 200 chars: ${stdout.slice(0, 200)}`);
|
|
138
138
|
}
|
|
139
|
-
if (body) failures.push(...diffExpect(body, expect
|
|
139
|
+
if (body) failures.push(...diffExpect(body, expect));
|
|
140
140
|
|
|
141
141
|
// Unconditional, outside every `if (body)` branch above.
|
|
142
142
|
failures.push(...stderrBanFailures(expect, stderr));
|
|
@@ -39,7 +39,6 @@ function canonicalizeForTarball(value) {
|
|
|
39
39
|
function canonicalManifestBytesForTarball(manifest) {
|
|
40
40
|
const clone = Object.assign({}, manifest);
|
|
41
41
|
delete clone.manifest_signature;
|
|
42
|
-
const cryptoMod = require("crypto"); // eslint-disable-line no-unused-vars
|
|
43
42
|
const json = JSON.stringify(canonicalizeForTarball(clone), null, 2);
|
|
44
43
|
return normalizeSkillBytes(Buffer.from(json, "utf8"));
|
|
45
44
|
}
|