@blamejs/exceptd-skills 0.13.1 → 0.13.3
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 +73 -0
- package/bin/exceptd.js +140 -7
- package/data/_indexes/_meta.json +28 -28
- package/data/_indexes/activity-feed.json +3 -3
- package/data/_indexes/catalog-summaries.json +3 -3
- package/data/_indexes/chains.json +1897 -88
- package/data/_indexes/frequency.json +20 -0
- package/data/_indexes/section-offsets.json +574 -574
- package/data/_indexes/token-budget.json +97 -97
- package/data/atlas-ttps.json +2 -0
- package/data/attack-techniques.json +24 -3
- package/data/cve-catalog.json +96 -29
- package/data/cwe-catalog.json +20 -3
- package/data/framework-control-gaps.json +700 -1
- package/data/zeroday-lessons.json +889 -0
- package/lib/lint-skills.js +54 -1
- package/lib/source-advisories.js +26 -0
- package/manifest.json +62 -62
- package/orchestrator/index.js +155 -3
- package/package.json +1 -1
- package/sbom.cdx.json +50 -39
- package/scripts/check-test-count.js +146 -0
- package/scripts/predeploy.js +16 -0
- package/skills/age-gates-child-safety/skill.md +1 -0
- package/skills/ai-risk-management/skill.md +1 -0
- package/skills/api-security/skill.md +14 -4
- package/skills/cloud-iam-incident/skill.md +1 -1
- package/skills/defensive-countermeasure-mapping/skill.md +1 -0
- package/skills/email-security-anti-phishing/skill.md +15 -4
- package/skills/fuzz-testing-strategy/skill.md +1 -0
- package/skills/mlops-security/skill.md +1 -0
- package/skills/ot-ics-security/skill.md +1 -0
- package/skills/researcher/skill.md +1 -0
- package/skills/sector-energy/skill.md +1 -0
- package/skills/sector-federal-government/skill.md +1 -0
- package/skills/sector-telecom/skill.md +1 -0
- package/skills/skill-update-loop/skill.md +1 -0
- package/skills/threat-model-currency/skill.md +1 -0
- package/skills/threat-modeling-methodology/skill.md +1 -0
- package/skills/webapp-security/skill.md +1 -0
- package/skills/zeroday-gap-learn/skill.md +1 -0
package/orchestrator/index.js
CHANGED
|
@@ -379,11 +379,18 @@ async function runReport(format) {
|
|
|
379
379
|
// (GENERIC_FAILURE) not 2 (DETECTED_ESCALATE). Pre-v0.13 the
|
|
380
380
|
// body went to stderr and exit was 2; both broke CI consumers
|
|
381
381
|
// that expected the dispatch-error vs verb-finding distinction.
|
|
382
|
+
// v0.13.2: did-you-mean on the unknown format value (reuses
|
|
383
|
+
// lib/flag-suggest.js for Levenshtein-≤2 typo correction).
|
|
384
|
+
const { suggestFlag } = require('../lib/flag-suggest');
|
|
385
|
+
const dym = suggestFlag(String(format), VALID_REPORT_FORMATS);
|
|
386
|
+
const hint = dym ? ` Did you mean "${dym}"?` : '';
|
|
382
387
|
process.stdout.write(JSON.stringify({
|
|
383
388
|
ok: false,
|
|
384
389
|
verb: 'report',
|
|
385
|
-
error: `report: format "${format}" not in accepted set ${JSON.stringify(VALID_REPORT_FORMATS)}
|
|
390
|
+
error: `report: format "${format}" not in accepted set ${JSON.stringify(VALID_REPORT_FORMATS)}.${hint}`,
|
|
391
|
+
provided: format,
|
|
386
392
|
accepted_formats: VALID_REPORT_FORMATS,
|
|
393
|
+
did_you_mean: dym ? [dym] : [],
|
|
387
394
|
}) + '\n');
|
|
388
395
|
safeExit(EXIT_CODES.GENERIC_FAILURE);
|
|
389
396
|
return;
|
|
@@ -1086,6 +1093,7 @@ function runWatchlist(rawArgs = []) {
|
|
|
1086
1093
|
|
|
1087
1094
|
const byskill = rawArgs.includes('--by-skill');
|
|
1088
1095
|
const alertsMode = rawArgs.includes('--alerts');
|
|
1096
|
+
const orgScanMode = rawArgs.includes('--org-scan');
|
|
1089
1097
|
const manifestPath = path.join(__dirname, '..', 'manifest.json');
|
|
1090
1098
|
const repoRoot = path.join(__dirname, '..');
|
|
1091
1099
|
|
|
@@ -1093,12 +1101,22 @@ function runWatchlist(rawArgs = []) {
|
|
|
1093
1101
|
// "CVE-class alert patterns" — surfaces catalog entries matching
|
|
1094
1102
|
// high-priority shape rules (kernel-LPE-with-PoC, supply-chain-family,
|
|
1095
1103
|
// AI-discovered-KEV, recently-disclosed-with-active-exploitation).
|
|
1096
|
-
// The
|
|
1097
|
-
// forward-watch aggregation.
|
|
1104
|
+
// The modes are mutually exclusive; the first matching flag wins.
|
|
1098
1105
|
if (alertsMode) {
|
|
1099
1106
|
return runWatchlistAlerts(rawArgs);
|
|
1100
1107
|
}
|
|
1101
1108
|
|
|
1109
|
+
// v0.13.3: --org-scan probes GitHub for repository naming patterns
|
|
1110
|
+
// associated with known threat actors (per NEW-CTRL-052 from the
|
|
1111
|
+
// MAL-2026-SHAI-HULUD-OSS zeroday-lessons entry). The Shai-Hulud
|
|
1112
|
+
// worm uses GitHub itself as the exfil channel — repos named
|
|
1113
|
+
// "A Gift From TeamPCP", "Shai-Hulud-*", or with future variants
|
|
1114
|
+
// hold the stolen credentials. Operators can run this against
|
|
1115
|
+
// their own GitHub org to surface exfil staging in their tenant.
|
|
1116
|
+
if (orgScanMode) {
|
|
1117
|
+
return runWatchlistOrgScan(rawArgs);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1102
1120
|
let manifest;
|
|
1103
1121
|
try {
|
|
1104
1122
|
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
@@ -1582,6 +1600,140 @@ Examples:
|
|
|
1582
1600
|
`);
|
|
1583
1601
|
}
|
|
1584
1602
|
|
|
1603
|
+
/**
|
|
1604
|
+
* v0.13.3 — runWatchlistOrgScan: GitHub repo-pattern monitoring per
|
|
1605
|
+
* NEW-CTRL-052 (MAL-2026-SHAI-HULUD-OSS zeroday-lessons). The Shai-Hulud
|
|
1606
|
+
* worm uses GitHub itself as the exfil channel; the canonical naming
|
|
1607
|
+
* pattern is "A Gift From TeamPCP", commit-timestamps falsified to
|
|
1608
|
+
* 2099-01-01, and contributor accounts agwagwagwa / headdirt / tmechen.
|
|
1609
|
+
*
|
|
1610
|
+
* Operators run this against their GitHub org (--org <login> or
|
|
1611
|
+
* GITHUB_ORG env var) to surface exfil staging within their tenant.
|
|
1612
|
+
* Uses the GitHub Search API (unauthenticated for public-repo search;
|
|
1613
|
+
* GITHUB_TOKEN env var lifts the rate limit + enables private-repo
|
|
1614
|
+
* coverage). Report-only — never modifies repos.
|
|
1615
|
+
*
|
|
1616
|
+
* Flags / env:
|
|
1617
|
+
* --org <login> GitHub org / user to scope the search to (required)
|
|
1618
|
+
* --pattern <s> (repeatable) additional naming-pattern strings (defaults below)
|
|
1619
|
+
* --json structured JSON output
|
|
1620
|
+
* GITHUB_TOKEN env var lifts rate limit + private-repo search coverage
|
|
1621
|
+
*/
|
|
1622
|
+
async function runWatchlistOrgScan(rawArgs = []) {
|
|
1623
|
+
const jsonOut = rawArgs.includes('--json');
|
|
1624
|
+
// Extract --org <login>. Accept --org=foo too.
|
|
1625
|
+
let org = null;
|
|
1626
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
1627
|
+
if (rawArgs[i] === '--org' && rawArgs[i + 1]) { org = rawArgs[i + 1]; break; }
|
|
1628
|
+
if (rawArgs[i].startsWith('--org=')) { org = rawArgs[i].slice('--org='.length); break; }
|
|
1629
|
+
}
|
|
1630
|
+
if (!org) org = process.env.GITHUB_ORG || null;
|
|
1631
|
+
if (!org) {
|
|
1632
|
+
process.stdout.write(JSON.stringify({
|
|
1633
|
+
ok: false,
|
|
1634
|
+
verb: 'watchlist',
|
|
1635
|
+
mode: 'org-scan',
|
|
1636
|
+
error: 'watchlist --org-scan requires --org <login> (or GITHUB_ORG env var). Example: exceptd watchlist --org-scan --org blamejs',
|
|
1637
|
+
}) + '\n');
|
|
1638
|
+
safeExit(EXIT_CODES.GENERIC_FAILURE);
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
// Custom patterns via --pattern <s> (repeatable). Default set from
|
|
1642
|
+
// the MAL-2026-SHAI-HULUD-OSS catalog entry + NEW-CTRL-052 evidence.
|
|
1643
|
+
const customPatterns = [];
|
|
1644
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
1645
|
+
if (rawArgs[i] === '--pattern' && rawArgs[i + 1]) customPatterns.push(rawArgs[i + 1]);
|
|
1646
|
+
if (rawArgs[i].startsWith('--pattern=')) customPatterns.push(rawArgs[i].slice('--pattern='.length));
|
|
1647
|
+
}
|
|
1648
|
+
const DEFAULT_PATTERNS = [
|
|
1649
|
+
{ id: 'shai-hulud-classic', q: 'Shai-Hulud', severity: 'critical', source: 'MAL-2026-SHAI-HULUD-OSS (pre-source-release naming)' },
|
|
1650
|
+
{ id: 'teampcp-gift', q: 'A Gift From TeamPCP', severity: 'critical', source: 'MAL-2026-SHAI-HULUD-OSS (post-2026-05-12 release naming)' },
|
|
1651
|
+
{ id: 'teampcp-bare', q: 'TeamPCP', severity: 'high', source: 'MAL-2026-SHAI-HULUD-OSS threat actor reference' },
|
|
1652
|
+
];
|
|
1653
|
+
const patterns = [
|
|
1654
|
+
...DEFAULT_PATTERNS,
|
|
1655
|
+
...customPatterns.map((q, i) => ({ id: `custom-${i + 1}`, q, severity: 'medium', source: 'operator --pattern' })),
|
|
1656
|
+
];
|
|
1657
|
+
|
|
1658
|
+
// GitHub Search API: GET /search/code?q=<query>+org:<login>
|
|
1659
|
+
// and GET /search/repositories?q=<query>+org:<login>. Both return
|
|
1660
|
+
// the same shape (items[] with name, html_url, owner, created_at).
|
|
1661
|
+
const token = process.env.GITHUB_TOKEN || '';
|
|
1662
|
+
const matches = [];
|
|
1663
|
+
let rateLimited = false;
|
|
1664
|
+
let unauth = !token;
|
|
1665
|
+
if (typeof fetch !== 'function') {
|
|
1666
|
+
process.stdout.write(JSON.stringify({
|
|
1667
|
+
ok: false,
|
|
1668
|
+
verb: 'watchlist',
|
|
1669
|
+
mode: 'org-scan',
|
|
1670
|
+
error: 'fetch() not available — Node 18+ required.',
|
|
1671
|
+
}) + '\n');
|
|
1672
|
+
safeExit(EXIT_CODES.GENERIC_FAILURE);
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
for (const p of patterns) {
|
|
1676
|
+
const url = `https://api.github.com/search/repositories?q=${encodeURIComponent(p.q + ' org:' + org)}&per_page=100`;
|
|
1677
|
+
const headers = { 'User-Agent': 'exceptd-watchlist-org-scan/0.13.3 (+https://exceptd.com)', 'Accept': 'application/vnd.github+json' };
|
|
1678
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
1679
|
+
try {
|
|
1680
|
+
const ac = new AbortController();
|
|
1681
|
+
const timer = setTimeout(() => ac.abort(), 10000);
|
|
1682
|
+
const r = await fetch(url, { headers, signal: ac.signal });
|
|
1683
|
+
clearTimeout(timer);
|
|
1684
|
+
if (r.status === 403 || r.status === 429) {
|
|
1685
|
+
rateLimited = true;
|
|
1686
|
+
continue;
|
|
1687
|
+
}
|
|
1688
|
+
if (!r.ok) continue;
|
|
1689
|
+
const body = await r.json();
|
|
1690
|
+
for (const item of body.items || []) {
|
|
1691
|
+
matches.push({
|
|
1692
|
+
pattern_id: p.id,
|
|
1693
|
+
severity: p.severity,
|
|
1694
|
+
source: p.source,
|
|
1695
|
+
repo: item.full_name,
|
|
1696
|
+
url: item.html_url,
|
|
1697
|
+
private: item.private,
|
|
1698
|
+
created_at: item.created_at,
|
|
1699
|
+
updated_at: item.updated_at,
|
|
1700
|
+
stars: item.stargazers_count || 0,
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
} catch { /* network failure — best effort */ }
|
|
1704
|
+
}
|
|
1705
|
+
const generated_at = new Date().toISOString();
|
|
1706
|
+
if (jsonOut) {
|
|
1707
|
+
process.stdout.write(JSON.stringify({
|
|
1708
|
+
ok: !rateLimited,
|
|
1709
|
+
verb: 'watchlist',
|
|
1710
|
+
mode: 'org-scan',
|
|
1711
|
+
generated_at,
|
|
1712
|
+
org,
|
|
1713
|
+
patterns_evaluated: patterns.length,
|
|
1714
|
+
match_count: matches.length,
|
|
1715
|
+
matches,
|
|
1716
|
+
rate_limited: rateLimited,
|
|
1717
|
+
unauthenticated: unauth,
|
|
1718
|
+
control_reference: 'NEW-CTRL-052 (MAL-2026-SHAI-HULUD-OSS lesson)',
|
|
1719
|
+
}) + '\n');
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
console.log(`\nGitHub Org-Scan — ${generated_at}`);
|
|
1723
|
+
console.log(`Org: ${org} patterns: ${patterns.length} matches: ${matches.length}`);
|
|
1724
|
+
if (unauth) console.log('(unauthenticated — set GITHUB_TOKEN for private-repo coverage + higher rate limit)');
|
|
1725
|
+
if (rateLimited) console.log('WARNING: GitHub rate limit hit on at least one query. Re-run with GITHUB_TOKEN set.');
|
|
1726
|
+
if (matches.length === 0) {
|
|
1727
|
+
console.log('No threat-actor-pattern repos found.');
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
for (const m of matches) {
|
|
1731
|
+
console.log(`[${m.severity}] ${m.repo} ${m.private ? '(private)' : ''} created ${m.created_at}`);
|
|
1732
|
+
console.log(` pattern: ${m.pattern_id} (${m.source})`);
|
|
1733
|
+
console.log(` ${m.url}`);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1585
1737
|
// Only run the CLI when this file is executed directly. Earlier versions
|
|
1586
1738
|
// invoked main() at import time too, which meant `require('./orchestrator')`
|
|
1587
1739
|
// would trigger a full CLI dispatch (and printHelp) inside the importing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blamejs/exceptd-skills",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.3",
|
|
4
4
|
"description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 42 skills, 10 catalogs, 34 jurisdictions, pre-computed indexes, Ed25519-signed.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-security",
|
package/sbom.cdx.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.6",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:215c1846-8948-4275-8e79-ad2f8593225c",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "
|
|
7
|
+
"timestamp": "2043-09-26T19:40:54.000Z",
|
|
8
8
|
"tools": [
|
|
9
9
|
{
|
|
10
10
|
"vendor": "blamejs",
|
|
11
11
|
"name": "scripts/refresh-sbom.js",
|
|
12
|
-
"version": "0.13.
|
|
12
|
+
"version": "0.13.3"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"component": {
|
|
16
|
-
"bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.13.
|
|
16
|
+
"bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.13.3",
|
|
17
17
|
"type": "application",
|
|
18
18
|
"name": "@blamejs/exceptd-skills",
|
|
19
|
-
"version": "0.13.
|
|
19
|
+
"version": "0.13.3",
|
|
20
20
|
"description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 42 skills, 10 catalogs, 34 jurisdictions, pre-computed indexes, Ed25519-signed.",
|
|
21
21
|
"licenses": [
|
|
22
22
|
{
|
|
@@ -25,17 +25,17 @@
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
],
|
|
28
|
-
"purl": "pkg:npm/%40blamejs/exceptd-skills@0.13.
|
|
28
|
+
"purl": "pkg:npm/%40blamejs/exceptd-skills@0.13.3",
|
|
29
29
|
"hashes": [
|
|
30
30
|
{
|
|
31
31
|
"alg": "SHA-256",
|
|
32
|
-
"content": "
|
|
32
|
+
"content": "5f31c2d0207da9b36fe526a2913e975e27ae2442561e03b079695af7f5a8926b"
|
|
33
33
|
}
|
|
34
34
|
],
|
|
35
35
|
"externalReferences": [
|
|
36
36
|
{
|
|
37
37
|
"type": "distribution",
|
|
38
|
-
"url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.13.
|
|
38
|
+
"url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.13.3"
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
"type": "vcs",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"hashes": [
|
|
109
109
|
{
|
|
110
110
|
"alg": "SHA-256",
|
|
111
|
-
"content": "
|
|
111
|
+
"content": "1932f52ef4f3d1ba7963310436df82f5ef6269a3c77851e993e31885ebade1b2"
|
|
112
112
|
}
|
|
113
113
|
]
|
|
114
114
|
},
|
|
@@ -229,7 +229,7 @@
|
|
|
229
229
|
"hashes": [
|
|
230
230
|
{
|
|
231
231
|
"alg": "SHA-256",
|
|
232
|
-
"content": "
|
|
232
|
+
"content": "b3540a3296e5e901004d428351d40d3ac40b154da082071da2c00222c40b7b6e"
|
|
233
233
|
}
|
|
234
234
|
]
|
|
235
235
|
},
|
|
@@ -240,7 +240,7 @@
|
|
|
240
240
|
"hashes": [
|
|
241
241
|
{
|
|
242
242
|
"alg": "SHA-256",
|
|
243
|
-
"content": "
|
|
243
|
+
"content": "2b021f47355365d1ba59078dfa582397c7a64c2b4ebea4657ea260a66b76daf6"
|
|
244
244
|
}
|
|
245
245
|
]
|
|
246
246
|
},
|
|
@@ -251,7 +251,7 @@
|
|
|
251
251
|
"hashes": [
|
|
252
252
|
{
|
|
253
253
|
"alg": "SHA-256",
|
|
254
|
-
"content": "
|
|
254
|
+
"content": "76461dbec048c5e072435d57e3a04b780e3992dab9f316b1b52608e0a997e355"
|
|
255
255
|
}
|
|
256
256
|
]
|
|
257
257
|
},
|
|
@@ -262,7 +262,7 @@
|
|
|
262
262
|
"hashes": [
|
|
263
263
|
{
|
|
264
264
|
"alg": "SHA-256",
|
|
265
|
-
"content": "
|
|
265
|
+
"content": "b499cb431c1b71aab505db577a1e3b2fdcb5190afbcc6aaee0f6a237cfc16ca8"
|
|
266
266
|
}
|
|
267
267
|
]
|
|
268
268
|
},
|
|
@@ -273,7 +273,7 @@
|
|
|
273
273
|
"hashes": [
|
|
274
274
|
{
|
|
275
275
|
"alg": "SHA-256",
|
|
276
|
-
"content": "
|
|
276
|
+
"content": "4a0036f9ec17af29e0df111ac77b94f8be6a52742bfd89ff3583096d23b75e35"
|
|
277
277
|
}
|
|
278
278
|
]
|
|
279
279
|
},
|
|
@@ -317,7 +317,7 @@
|
|
|
317
317
|
"hashes": [
|
|
318
318
|
{
|
|
319
319
|
"alg": "SHA-256",
|
|
320
|
-
"content": "
|
|
320
|
+
"content": "ce1535f13d29ab90fac99b983f38a23dd685702b3f12ac9f2371294cb9859ecf"
|
|
321
321
|
}
|
|
322
322
|
]
|
|
323
323
|
},
|
|
@@ -570,7 +570,7 @@
|
|
|
570
570
|
"hashes": [
|
|
571
571
|
{
|
|
572
572
|
"alg": "SHA-256",
|
|
573
|
-
"content": "
|
|
573
|
+
"content": "1438620d2c8b0606eac4f63e620906b9ba079c57bfa7f737ceb6a50370cdc9a5"
|
|
574
574
|
}
|
|
575
575
|
]
|
|
576
576
|
},
|
|
@@ -691,7 +691,7 @@
|
|
|
691
691
|
"hashes": [
|
|
692
692
|
{
|
|
693
693
|
"alg": "SHA-256",
|
|
694
|
-
"content": "
|
|
694
|
+
"content": "48aa70089fe9fc3bee80e19042d28d91ceb996ed018b6131db970dba7cadb90e"
|
|
695
695
|
}
|
|
696
696
|
]
|
|
697
697
|
},
|
|
@@ -812,7 +812,7 @@
|
|
|
812
812
|
"hashes": [
|
|
813
813
|
{
|
|
814
814
|
"alg": "SHA-256",
|
|
815
|
-
"content": "
|
|
815
|
+
"content": "63702da0ef17b9dd32cff349473d5e1c32aae763cd769936a07570e34cb6b824"
|
|
816
816
|
}
|
|
817
817
|
]
|
|
818
818
|
},
|
|
@@ -988,7 +988,7 @@
|
|
|
988
988
|
"hashes": [
|
|
989
989
|
{
|
|
990
990
|
"alg": "SHA-256",
|
|
991
|
-
"content": "
|
|
991
|
+
"content": "fbc30b15d294d3bdfccfb3880781dae9e9a9624e3b6d6a64723cdc75b6b47d3b"
|
|
992
992
|
}
|
|
993
993
|
]
|
|
994
994
|
},
|
|
@@ -1032,7 +1032,7 @@
|
|
|
1032
1032
|
"hashes": [
|
|
1033
1033
|
{
|
|
1034
1034
|
"alg": "SHA-256",
|
|
1035
|
-
"content": "
|
|
1035
|
+
"content": "b827fb5d2a43409ba2c390b000e175c9357b86137d25d6647ff238b94922275b"
|
|
1036
1036
|
}
|
|
1037
1037
|
]
|
|
1038
1038
|
},
|
|
@@ -1289,6 +1289,17 @@
|
|
|
1289
1289
|
}
|
|
1290
1290
|
]
|
|
1291
1291
|
},
|
|
1292
|
+
{
|
|
1293
|
+
"bom-ref": "file:scripts/check-test-count.js",
|
|
1294
|
+
"type": "file",
|
|
1295
|
+
"name": "scripts/check-test-count.js",
|
|
1296
|
+
"hashes": [
|
|
1297
|
+
{
|
|
1298
|
+
"alg": "SHA-256",
|
|
1299
|
+
"content": "e8c7473d7a1f87d27aeab39cefa54c10c773831c3c3b0a786c81f9ac9a50d6e3"
|
|
1300
|
+
}
|
|
1301
|
+
]
|
|
1302
|
+
},
|
|
1292
1303
|
{
|
|
1293
1304
|
"bom-ref": "file:scripts/check-test-coverage.README.md",
|
|
1294
1305
|
"type": "file",
|
|
@@ -1329,7 +1340,7 @@
|
|
|
1329
1340
|
"hashes": [
|
|
1330
1341
|
{
|
|
1331
1342
|
"alg": "SHA-256",
|
|
1332
|
-
"content": "
|
|
1343
|
+
"content": "6a7766b986988fd14105d92f3488052333afff8b72eda17460e193ca58b2d60a"
|
|
1333
1344
|
}
|
|
1334
1345
|
]
|
|
1335
1346
|
},
|
|
@@ -1406,7 +1417,7 @@
|
|
|
1406
1417
|
"hashes": [
|
|
1407
1418
|
{
|
|
1408
1419
|
"alg": "SHA-256",
|
|
1409
|
-
"content": "
|
|
1420
|
+
"content": "51295c849bcced965b6448eb6b4bbd5caef5ba0b0cea7ce48abbacf47d331621"
|
|
1410
1421
|
}
|
|
1411
1422
|
]
|
|
1412
1423
|
},
|
|
@@ -1439,7 +1450,7 @@
|
|
|
1439
1450
|
"hashes": [
|
|
1440
1451
|
{
|
|
1441
1452
|
"alg": "SHA-256",
|
|
1442
|
-
"content": "
|
|
1453
|
+
"content": "2b611eb8fa4841fdfc3f1dd1ffd504a46c6ecdc654213a955efbabefb6b1db87"
|
|
1443
1454
|
}
|
|
1444
1455
|
]
|
|
1445
1456
|
},
|
|
@@ -1450,7 +1461,7 @@
|
|
|
1450
1461
|
"hashes": [
|
|
1451
1462
|
{
|
|
1452
1463
|
"alg": "SHA-256",
|
|
1453
|
-
"content": "
|
|
1464
|
+
"content": "9fc2252cbcf6162591e70d0bf5499a430b0584495ad584ce49fb7daf070d335f"
|
|
1454
1465
|
}
|
|
1455
1466
|
]
|
|
1456
1467
|
},
|
|
@@ -1472,7 +1483,7 @@
|
|
|
1472
1483
|
"hashes": [
|
|
1473
1484
|
{
|
|
1474
1485
|
"alg": "SHA-256",
|
|
1475
|
-
"content": "
|
|
1486
|
+
"content": "5ec3800a0049b2123aff67bfab4ff28491a86d2daeb712283e5e88b10c3d5d7b"
|
|
1476
1487
|
}
|
|
1477
1488
|
]
|
|
1478
1489
|
},
|
|
@@ -1527,7 +1538,7 @@
|
|
|
1527
1538
|
"hashes": [
|
|
1528
1539
|
{
|
|
1529
1540
|
"alg": "SHA-256",
|
|
1530
|
-
"content": "
|
|
1541
|
+
"content": "3d0c7ca85f32ee1fe74598889361ef2be16d099fe6e9e8d8c8184b7004306b30"
|
|
1531
1542
|
}
|
|
1532
1543
|
]
|
|
1533
1544
|
},
|
|
@@ -1549,7 +1560,7 @@
|
|
|
1549
1560
|
"hashes": [
|
|
1550
1561
|
{
|
|
1551
1562
|
"alg": "SHA-256",
|
|
1552
|
-
"content": "
|
|
1563
|
+
"content": "250f266908f51f99a4cb3aec0d5dacfcf91fac9f3d95e5a117429a40ed2ff45a"
|
|
1553
1564
|
}
|
|
1554
1565
|
]
|
|
1555
1566
|
},
|
|
@@ -1582,7 +1593,7 @@
|
|
|
1582
1593
|
"hashes": [
|
|
1583
1594
|
{
|
|
1584
1595
|
"alg": "SHA-256",
|
|
1585
|
-
"content": "
|
|
1596
|
+
"content": "fb8c261def9e3344b44fd219c209027029e1eddf0e6bee1ecffb2d2176e1585e"
|
|
1586
1597
|
}
|
|
1587
1598
|
]
|
|
1588
1599
|
},
|
|
@@ -1659,7 +1670,7 @@
|
|
|
1659
1670
|
"hashes": [
|
|
1660
1671
|
{
|
|
1661
1672
|
"alg": "SHA-256",
|
|
1662
|
-
"content": "
|
|
1673
|
+
"content": "72429f05010accbcb191cb1544f1b88493c2f5249362846e5713ec3226b83dc2"
|
|
1663
1674
|
}
|
|
1664
1675
|
]
|
|
1665
1676
|
},
|
|
@@ -1670,7 +1681,7 @@
|
|
|
1670
1681
|
"hashes": [
|
|
1671
1682
|
{
|
|
1672
1683
|
"alg": "SHA-256",
|
|
1673
|
-
"content": "
|
|
1684
|
+
"content": "7423cca19aab1026c07de63279137441018345731d3ee895c474316d432adaa2"
|
|
1674
1685
|
}
|
|
1675
1686
|
]
|
|
1676
1687
|
},
|
|
@@ -1725,7 +1736,7 @@
|
|
|
1725
1736
|
"hashes": [
|
|
1726
1737
|
{
|
|
1727
1738
|
"alg": "SHA-256",
|
|
1728
|
-
"content": "
|
|
1739
|
+
"content": "fd441131484dc5af4cd785ded0bac039123e6205483543752cb16fa508460c00"
|
|
1729
1740
|
}
|
|
1730
1741
|
]
|
|
1731
1742
|
},
|
|
@@ -1736,7 +1747,7 @@
|
|
|
1736
1747
|
"hashes": [
|
|
1737
1748
|
{
|
|
1738
1749
|
"alg": "SHA-256",
|
|
1739
|
-
"content": "
|
|
1750
|
+
"content": "91f00e7a9be2608393ec8cb6d5f0c9828f81b954a12a7c9fd04bd642b9091e09"
|
|
1740
1751
|
}
|
|
1741
1752
|
]
|
|
1742
1753
|
},
|
|
@@ -1747,7 +1758,7 @@
|
|
|
1747
1758
|
"hashes": [
|
|
1748
1759
|
{
|
|
1749
1760
|
"alg": "SHA-256",
|
|
1750
|
-
"content": "
|
|
1761
|
+
"content": "a73c3f36f23c12750d369931b7e3f884edae4a8aef35fc8690d15ef4500c4dd0"
|
|
1751
1762
|
}
|
|
1752
1763
|
]
|
|
1753
1764
|
},
|
|
@@ -1780,7 +1791,7 @@
|
|
|
1780
1791
|
"hashes": [
|
|
1781
1792
|
{
|
|
1782
1793
|
"alg": "SHA-256",
|
|
1783
|
-
"content": "
|
|
1794
|
+
"content": "59193e39c2fd73fdd7fede38a956bc730bbe4b712d7d6020788bb4d85f001ad8"
|
|
1784
1795
|
}
|
|
1785
1796
|
]
|
|
1786
1797
|
},
|
|
@@ -1802,7 +1813,7 @@
|
|
|
1802
1813
|
"hashes": [
|
|
1803
1814
|
{
|
|
1804
1815
|
"alg": "SHA-256",
|
|
1805
|
-
"content": "
|
|
1816
|
+
"content": "b6f3bee321833dc18f5624a9be4d28673d22e22018254b0bd1f3690b945073af"
|
|
1806
1817
|
}
|
|
1807
1818
|
]
|
|
1808
1819
|
},
|
|
@@ -1824,7 +1835,7 @@
|
|
|
1824
1835
|
"hashes": [
|
|
1825
1836
|
{
|
|
1826
1837
|
"alg": "SHA-256",
|
|
1827
|
-
"content": "
|
|
1838
|
+
"content": "cf1cc27ae5ae68d336c56d9f3afd950641e1d8d5b9f90b64c2daf00abe92bab0"
|
|
1828
1839
|
}
|
|
1829
1840
|
]
|
|
1830
1841
|
},
|
|
@@ -1835,7 +1846,7 @@
|
|
|
1835
1846
|
"hashes": [
|
|
1836
1847
|
{
|
|
1837
1848
|
"alg": "SHA-256",
|
|
1838
|
-
"content": "
|
|
1849
|
+
"content": "cebeba3940320ebc5b44ad2bb7b4cdcda412257c1a6319a1b7379c875ebe8d6a"
|
|
1839
1850
|
}
|
|
1840
1851
|
]
|
|
1841
1852
|
},
|
|
@@ -1846,7 +1857,7 @@
|
|
|
1846
1857
|
"hashes": [
|
|
1847
1858
|
{
|
|
1848
1859
|
"alg": "SHA-256",
|
|
1849
|
-
"content": "
|
|
1860
|
+
"content": "f2063eaea3f5ddf0f3d37b41985bf522b682a41f104796b3f0dff611cefd043c"
|
|
1850
1861
|
}
|
|
1851
1862
|
]
|
|
1852
1863
|
},
|
|
@@ -1857,7 +1868,7 @@
|
|
|
1857
1868
|
"hashes": [
|
|
1858
1869
|
{
|
|
1859
1870
|
"alg": "SHA-256",
|
|
1860
|
-
"content": "
|
|
1871
|
+
"content": "e26f194880cd6acf46abe31e9348d445e9222c7691e9b9b953662c4a472462f5"
|
|
1861
1872
|
}
|
|
1862
1873
|
]
|
|
1863
1874
|
},
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* scripts/check-test-count.js — v0.13.2 canonical-test-count predeploy gate.
|
|
6
|
+
*
|
|
7
|
+
* Why this exists. The v0.12 audit flagged that nothing in the suite asserts
|
|
8
|
+
* "we expect N tests today." A test file accidentally deleted, a skip-all
|
|
9
|
+
* mistakenly committed, or a misnamed file glob-excluded would all silently
|
|
10
|
+
* drop tests without anyone noticing. The lint + diff-coverage gates catch
|
|
11
|
+
* source changes; this gate catches test-set shrinkage.
|
|
12
|
+
*
|
|
13
|
+
* Mechanism: count `test(`, `test.only(`, and `test.skip(` declarations
|
|
14
|
+
* across `tests/*.test.js` via static analysis (faster than running). Compare
|
|
15
|
+
* to a baseline pinned in `tests/.test-count-baseline.json`. Fail if the
|
|
16
|
+
* observed count drops MORE than the configured tolerance (default 1) below
|
|
17
|
+
* the baseline. Growth above baseline is fine; if the count grows by more
|
|
18
|
+
* than `update_baseline_when_growth_exceeds`, surface a notice that the
|
|
19
|
+
* baseline file should be refreshed (operator commits the refresh as part
|
|
20
|
+
* of the release that added the tests).
|
|
21
|
+
*
|
|
22
|
+
* Output:
|
|
23
|
+
* stdout: structured JSON when --json, else a one-line summary
|
|
24
|
+
* exit 0: observed count is at or above baseline minus tolerance
|
|
25
|
+
* exit 1: observed count dropped beyond tolerance — fail predeploy
|
|
26
|
+
* exit 2: baseline file missing or malformed
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
|
|
32
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
33
|
+
const TESTS_DIR = path.join(ROOT, 'tests');
|
|
34
|
+
const BASELINE_PATH = path.join(TESTS_DIR, '.test-count-baseline.json');
|
|
35
|
+
|
|
36
|
+
function listTestFiles(dir) {
|
|
37
|
+
const out = [];
|
|
38
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
39
|
+
for (const e of entries) {
|
|
40
|
+
const p = path.join(dir, e.name);
|
|
41
|
+
if (e.isDirectory()) {
|
|
42
|
+
if (e.name === '_helpers' || e.name === 'fixtures' || e.name === 'e2e-scenarios') continue;
|
|
43
|
+
out.push(...listTestFiles(p));
|
|
44
|
+
} else if (e.isFile() && e.name.endsWith('.test.js')) {
|
|
45
|
+
out.push(p);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function countTests(filePath) {
|
|
52
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
53
|
+
const lines = text.split('\n');
|
|
54
|
+
let count = 0;
|
|
55
|
+
for (const line of lines) {
|
|
56
|
+
const stripped = line.replace(/^\s*\/\/.*$/, '').trim();
|
|
57
|
+
if (!stripped) continue;
|
|
58
|
+
if (/(?<![A-Za-z0-9_$.])test(?:\.only|\.skip)?\s*\(/.test(stripped)) count++;
|
|
59
|
+
}
|
|
60
|
+
return count;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function main() {
|
|
64
|
+
const wantJson = process.argv.includes('--json');
|
|
65
|
+
const wantUpdate = process.argv.includes('--update-baseline');
|
|
66
|
+
|
|
67
|
+
if (!fs.existsSync(BASELINE_PATH)) {
|
|
68
|
+
if (wantUpdate) {
|
|
69
|
+
const files = listTestFiles(TESTS_DIR);
|
|
70
|
+
const observed = files.reduce((n, f) => n + countTests(f), 0);
|
|
71
|
+
fs.writeFileSync(BASELINE_PATH, JSON.stringify({
|
|
72
|
+
baseline: observed,
|
|
73
|
+
tolerance: 1,
|
|
74
|
+
update_baseline_when_growth_exceeds: 20,
|
|
75
|
+
notes: 'Operator-pinned canonical test count. Bump when new test files land in a release. See scripts/check-test-count.js for the contract.',
|
|
76
|
+
recorded_at: new Date().toISOString().slice(0, 10),
|
|
77
|
+
}, null, 2) + '\n', 'utf8');
|
|
78
|
+
console.error(`[check-test-count] wrote initial baseline: ${observed}`);
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
console.error(`[check-test-count] baseline missing at ${path.relative(ROOT, BASELINE_PATH)}. Run with --update-baseline to create it.`);
|
|
82
|
+
process.exit(2);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let baselineFile;
|
|
86
|
+
try { baselineFile = JSON.parse(fs.readFileSync(BASELINE_PATH, 'utf8')); }
|
|
87
|
+
catch (e) {
|
|
88
|
+
console.error(`[check-test-count] cannot parse baseline: ${e.message}`);
|
|
89
|
+
process.exit(2);
|
|
90
|
+
}
|
|
91
|
+
const baseline = baselineFile.baseline;
|
|
92
|
+
const tolerance = baselineFile.tolerance || 1;
|
|
93
|
+
const updateThreshold = baselineFile.update_baseline_when_growth_exceeds || 20;
|
|
94
|
+
if (typeof baseline !== 'number' || baseline <= 0) {
|
|
95
|
+
console.error(`[check-test-count] baseline value invalid: ${baseline}`);
|
|
96
|
+
process.exit(2);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const files = listTestFiles(TESTS_DIR);
|
|
100
|
+
const observed = files.reduce((n, f) => n + countTests(f), 0);
|
|
101
|
+
|
|
102
|
+
if (wantUpdate) {
|
|
103
|
+
fs.writeFileSync(BASELINE_PATH, JSON.stringify({
|
|
104
|
+
...baselineFile,
|
|
105
|
+
baseline: observed,
|
|
106
|
+
recorded_at: new Date().toISOString().slice(0, 10),
|
|
107
|
+
}, null, 2) + '\n', 'utf8');
|
|
108
|
+
console.error(`[check-test-count] baseline updated: ${baseline} -> ${observed}`);
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const delta = observed - baseline;
|
|
113
|
+
const status = delta < -tolerance
|
|
114
|
+
? 'shrunk_beyond_tolerance'
|
|
115
|
+
: delta > updateThreshold
|
|
116
|
+
? 'grew_beyond_threshold_consider_bump'
|
|
117
|
+
: 'ok';
|
|
118
|
+
|
|
119
|
+
if (wantJson) {
|
|
120
|
+
process.stdout.write(JSON.stringify({
|
|
121
|
+
ok: status === 'ok' || status === 'grew_beyond_threshold_consider_bump',
|
|
122
|
+
verb: 'check-test-count',
|
|
123
|
+
observed,
|
|
124
|
+
baseline,
|
|
125
|
+
tolerance,
|
|
126
|
+
delta,
|
|
127
|
+
status,
|
|
128
|
+
files_scanned: files.length,
|
|
129
|
+
}) + '\n');
|
|
130
|
+
} else {
|
|
131
|
+
console.log(`[check-test-count] observed=${observed} baseline=${baseline} delta=${delta >= 0 ? '+' : ''}${delta} tolerance=${tolerance} files=${files.length} status=${status}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (status === 'shrunk_beyond_tolerance') {
|
|
135
|
+
console.error(`[check-test-count] FAIL - test count dropped from ${baseline} to ${observed} (delta ${delta}, tolerance -${tolerance}).`);
|
|
136
|
+
console.error('[check-test-count] Either a test file was accidentally removed, a test() invocation was deleted, OR the baseline is stale.');
|
|
137
|
+
console.error('[check-test-count] If the drop is intentional, run: node scripts/check-test-count.js --update-baseline');
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
if (status === 'grew_beyond_threshold_consider_bump') {
|
|
141
|
+
console.error(`[check-test-count] NOTICE - test count grew by ${delta} (above the ${updateThreshold} notice threshold). Consider refreshing the baseline: node scripts/check-test-count.js --update-baseline`);
|
|
142
|
+
}
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
main();
|