@blamejs/exceptd-skills 0.18.6 → 0.18.8
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 +42 -0
- package/bin/exceptd.js +364 -119
- package/data/_indexes/_meta.json +22 -3
- package/data/cve-catalog.json +25 -0
- package/data/playbooks/framework.json +2 -2
- package/data/playbooks/post-quantum-migration.json +1 -1
- package/lib/auto-discovery.js +30 -10
- package/lib/collectors/ai-api.js +9 -2
- package/lib/collectors/cicd-pipeline-compromise.js +24 -5
- package/lib/collectors/cred-stores.js +17 -4
- package/lib/collectors/crypto.js +9 -2
- package/lib/collectors/hardening.js +9 -2
- package/lib/collectors/library-author.js +29 -5
- package/lib/collectors/mcp.js +9 -2
- package/lib/collectors/runtime.js +9 -2
- package/lib/collectors/sbom.js +28 -15
- package/lib/collectors/scan-excludes.js +25 -0
- package/lib/collectors/secrets.js +40 -4
- package/lib/cve-curation.js +84 -8
- package/lib/lint-skills.js +75 -3
- package/lib/playbook-runner.js +443 -50
- package/lib/prefetch.js +22 -2
- package/lib/refresh-external.js +32 -1
- package/lib/refresh-network.js +235 -26
- package/lib/schemas/cve-catalog.schema.json +5 -0
- package/lib/scoring.js +141 -21
- package/lib/sign.js +107 -29
- package/lib/source-advisories.js +23 -5
- package/lib/source-ghsa.js +25 -1
- package/lib/source-osv.js +26 -1
- package/lib/upstream-check.js +1 -1
- package/lib/validate-cve-catalog.js +30 -4
- package/lib/validate-indexes.js +135 -29
- package/lib/validate-playbooks.js +19 -7
- package/lib/validate-vendor.js +69 -8
- package/lib/verify.js +23 -6
- package/manifest.json +53 -53
- package/orchestrator/dispatcher.js +14 -3
- package/orchestrator/index.js +100 -20
- package/orchestrator/scanner.js +8 -0
- package/package.json +1 -1
- package/sbom.cdx.json +130 -130
- package/scripts/audit-cross-skill.js +1 -1
- package/scripts/bootstrap.js +1 -0
- package/scripts/build-indexes.js +84 -11
- package/scripts/check-agents-md-collectors.js +41 -13
- package/scripts/check-changelog-extract.js +4 -4
- package/scripts/check-codebase-patterns.js +19 -5
- package/scripts/check-manifest-snapshot.js +74 -30
- package/scripts/check-sbom-currency.js +25 -5
- package/scripts/check-test-count.js +26 -7
- package/scripts/check-test-coverage.js +44 -4
- package/scripts/check-version-tags.js +27 -8
- package/scripts/predeploy.js +1 -1
- package/scripts/refresh-manifest-snapshot.js +14 -4
- package/scripts/refresh-reverse-refs.js +7 -1
- package/scripts/refresh-sbom.js +1 -1
- package/scripts/release.js +3 -3
- package/scripts/run-e2e-scenarios.js +18 -8
- package/scripts/validate-vendor-online.js +28 -2
- package/scripts/verify-shipped-tarball.js +65 -6
- package/sources/validators/cve-validator.js +17 -1
- package/vendor/blamejs/_PROVENANCE.json +4 -2
package/orchestrator/index.js
CHANGED
|
@@ -28,6 +28,7 @@ const { currencyCheck, initPipeline } = require('./pipeline');
|
|
|
28
28
|
const { bus, EVENT_TYPES } = require('./event-bus');
|
|
29
29
|
const { start: startScheduler, stop: stopScheduler, runCurrencyNow } = require('./scheduler');
|
|
30
30
|
const { EXIT_CODES, safeExit } = require('../lib/exit-codes');
|
|
31
|
+
const { withRetry } = require('../vendor/blamejs/retry.js');
|
|
31
32
|
|
|
32
33
|
const cmd = process.argv[2];
|
|
33
34
|
const args = process.argv.slice(3);
|
|
@@ -557,14 +558,34 @@ async function runReport(format) {
|
|
|
557
558
|
revision_history: [{ number: '1', date: new Date().toISOString(), summary: 'Initial report emission' }],
|
|
558
559
|
},
|
|
559
560
|
},
|
|
561
|
+
// CSAF vulnerabilities[] are CVE-scoped by spec; non-CVE findings (signal
|
|
562
|
+
// detections without a catalogued CVE) are preserved in
|
|
563
|
+
// exceptd_extension.scan_summary below, not dropped.
|
|
560
564
|
vulnerabilities: scanResult.findings
|
|
561
565
|
.filter(f => f.cve_id)
|
|
562
|
-
.map(f =>
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
566
|
+
.map(f => {
|
|
567
|
+
const vuln = {
|
|
568
|
+
cve: f.cve_id,
|
|
569
|
+
// Never emit a null description/threat detail — the CSAF schema
|
|
570
|
+
// requires non-empty strings, so fall back through signal to a
|
|
571
|
+
// generic label.
|
|
572
|
+
notes: [{ category: 'description', text: f.action_required || f.signal || 'Vulnerability detected' }],
|
|
573
|
+
threats: f.severity === 'critical'
|
|
574
|
+
? [{ category: 'exploit_status', details: f.action_required || f.signal || 'Critical vulnerability detected' }]
|
|
575
|
+
: [],
|
|
576
|
+
};
|
|
577
|
+
// Emit the REAL catalog CVSS, never a hardcoded base_score:0 (which
|
|
578
|
+
// reads as "no impact" and inverts the risk for a critical CVE).
|
|
579
|
+
// Mirror the playbook-runner CSAF emitter: a cvss_v3 block carries
|
|
580
|
+
// vectorString (CSAF §3.2.1.5), so emit it only when both the score
|
|
581
|
+
// and vector are known, and omit scores entirely otherwise rather
|
|
582
|
+
// than fabricate one.
|
|
583
|
+
if (typeof f.cvss_score === 'number' && Number.isFinite(f.cvss_score) &&
|
|
584
|
+
typeof f.cvss_vector === 'string' && f.cvss_vector) {
|
|
585
|
+
vuln.scores = [{ products: [], cvss_v3: { baseScore: f.cvss_score, vectorString: f.cvss_vector } }];
|
|
586
|
+
}
|
|
587
|
+
return vuln;
|
|
588
|
+
}),
|
|
568
589
|
exceptd_extension: {
|
|
569
590
|
scan_summary: scanResult.summary,
|
|
570
591
|
dispatch_plan: plan,
|
|
@@ -685,7 +706,11 @@ function _acquireWatchLock() {
|
|
|
685
706
|
const STALE_MS = 60_000;
|
|
686
707
|
|
|
687
708
|
function tryCreate() {
|
|
688
|
-
|
|
709
|
+
// O_EXCL create with an explicit owner-only (0o600) mode: the predictable
|
|
710
|
+
// lock-file name is the cross-process mutex, so safety is the exclusive
|
|
711
|
+
// create + restrictive perms, not an unguessable name. The explicit mode
|
|
712
|
+
// also clears the insecure-temporary-file static-analysis finding.
|
|
713
|
+
const fd = fsMod.openSync(lockPath, 'wx', 0o600);
|
|
689
714
|
fsMod.writeSync(fd, JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() }));
|
|
690
715
|
fsMod.closeSync(fd);
|
|
691
716
|
}
|
|
@@ -1697,18 +1722,26 @@ async function validateAllCvesPreferCache(catalog, cacheDir) {
|
|
|
1697
1722
|
function readCached(source, id) {
|
|
1698
1723
|
const safe = id.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
1699
1724
|
const p = path.join(cacheDir, source, `${safe}.json`);
|
|
1700
|
-
|
|
1725
|
+
let fd;
|
|
1726
|
+
// Open once and fstat the descriptor — no existsSync→statSync→read TOCTOU.
|
|
1727
|
+
try { fd = fs.openSync(p, 'r'); }
|
|
1728
|
+
catch { return null; } // absent / unreadable (matches the old existsSync:false skip)
|
|
1701
1729
|
try {
|
|
1702
|
-
const st = fs.
|
|
1730
|
+
const st = fs.fstatSync(fd);
|
|
1703
1731
|
if (st.size > CACHE_FILE_MAX_BYTES) {
|
|
1704
1732
|
console.error(`[validate-cves] cache file ${p} exceeds ${CACHE_FILE_MAX_BYTES} byte cap (${st.size}); refusing to read.`);
|
|
1705
1733
|
return null;
|
|
1706
1734
|
}
|
|
1707
|
-
|
|
1735
|
+
// readFileSync(fd) loops read() to EOF — a single readSync may return
|
|
1736
|
+
// fewer than st.size bytes on network/FUSE/sync-backed fds, leaving the
|
|
1737
|
+
// tail NUL-filled and truncating the JSON. Reading via the open fd keeps
|
|
1738
|
+
// the open→fstat ordering TOCTOU-free.
|
|
1739
|
+
const parsed = JSON.parse(fs.readFileSync(fd, 'utf8'));
|
|
1708
1740
|
if (!cacheEntryVerified(source, id, parsed)) return null;
|
|
1709
1741
|
return parsed;
|
|
1710
1742
|
}
|
|
1711
1743
|
catch { return null; }
|
|
1744
|
+
finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* non-fatal */ } } }
|
|
1712
1745
|
}
|
|
1713
1746
|
|
|
1714
1747
|
function extractNvd(payload) {
|
|
@@ -1990,21 +2023,49 @@ async function runWatchlistOrgScan(rawArgs = []) {
|
|
|
1990
2023
|
safeExit(EXIT_CODES.GENERIC_FAILURE);
|
|
1991
2024
|
return;
|
|
1992
2025
|
}
|
|
2026
|
+
// Patterns whose query exhausted retries on a transient failure. Surfaced
|
|
2027
|
+
// in the envelope so a dropped pattern is observable rather than reading as
|
|
2028
|
+
// a clean zero — the false-negative-on-transient class (a flaky 5xx /
|
|
2029
|
+
// reset / timeout silently yielding "no matches found" for a threat-actor
|
|
2030
|
+
// naming pattern) is exactly what NEW-CTRL-052 exists to defend against.
|
|
2031
|
+
const erroredPatterns = [];
|
|
2032
|
+
// A 5xx / timeout / reset is retried with backoff + jitter (mirrors
|
|
2033
|
+
// lib/source-advisories.js#fetchFeed); a permanent 4xx other than the
|
|
2034
|
+
// rate-limit codes is not. 403/429 short-circuit to the rateLimited flag.
|
|
2035
|
+
const retryable = (err) => {
|
|
2036
|
+
// 403/429 are the documented rate-limit signal — surface them at once
|
|
2037
|
+
// (preserving the prior continue-on-rate-limit semantics) rather than
|
|
2038
|
+
// burning the retry budget; the operator re-runs with GITHUB_TOKEN set.
|
|
2039
|
+
if (err && err._rateLimit) return false;
|
|
2040
|
+
if (err && typeof err.statusCode === 'number') return err.statusCode >= 500;
|
|
2041
|
+
if (err && (err.name === 'AbortError' || err.name === 'TimeoutError')) return true;
|
|
2042
|
+
const code = err && (err.code || (err.cause && err.cause.code));
|
|
2043
|
+
return !!code && /^(ECONNRESET|ECONNREFUSED|ECONNABORTED|ETIMEDOUT|EPIPE|EAGAIN|ENOTFOUND|ENETUNREACH|UND_ERR)/.test(String(code));
|
|
2044
|
+
};
|
|
1993
2045
|
for (const p of patterns) {
|
|
1994
2046
|
const url = `https://api.github.com/search/repositories?q=${encodeURIComponent(p.q + ' org:' + org)}&per_page=100`;
|
|
1995
2047
|
const headers = { 'User-Agent': 'exceptd-watchlist-org-scan/0.13.3 (+https://exceptd.com)', 'Accept': 'application/vnd.github+json' };
|
|
1996
2048
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
1997
|
-
|
|
2049
|
+
// Each attempt gets a fresh abort timer.
|
|
2050
|
+
const attempt = async () => {
|
|
1998
2051
|
const ac = new AbortController();
|
|
1999
2052
|
const timer = setTimeout(() => ac.abort(), 10000);
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2053
|
+
try {
|
|
2054
|
+
const r = await fetch(url, { headers, signal: ac.signal });
|
|
2055
|
+
if (r.status === 403 || r.status === 429) {
|
|
2056
|
+
const e = new Error(`HTTP ${r.status}`);
|
|
2057
|
+
e.statusCode = r.status;
|
|
2058
|
+
e._rateLimit = true; // surfaced as rateLimited, never retried
|
|
2059
|
+
throw e;
|
|
2060
|
+
}
|
|
2061
|
+
if (!r.ok) { const e = new Error(`HTTP ${r.status}`); e.statusCode = r.status; throw e; }
|
|
2062
|
+
return await r.json();
|
|
2063
|
+
} finally {
|
|
2064
|
+
clearTimeout(timer);
|
|
2005
2065
|
}
|
|
2006
|
-
|
|
2007
|
-
|
|
2066
|
+
};
|
|
2067
|
+
try {
|
|
2068
|
+
const body = await withRetry(attempt, { maxAttempts: 3, baseDelayMs: 100, maxDelayMs: 2000, jitterFactor: 0.5, isRetryable: retryable });
|
|
2008
2069
|
for (const item of body.items || []) {
|
|
2009
2070
|
matches.push({
|
|
2010
2071
|
pattern_id: p.id,
|
|
@@ -2018,12 +2079,23 @@ async function runWatchlistOrgScan(rawArgs = []) {
|
|
|
2018
2079
|
stars: item.stargazers_count || 0,
|
|
2019
2080
|
});
|
|
2020
2081
|
}
|
|
2021
|
-
} catch {
|
|
2082
|
+
} catch (err) {
|
|
2083
|
+
// 403/429 (incl. after retries) is the documented rate-limit signal.
|
|
2084
|
+
// Any other exhausted-retry failure marks the pattern errored so the
|
|
2085
|
+
// operator can distinguish a dropped query from a clean no-match.
|
|
2086
|
+
if (err && err._rateLimit) {
|
|
2087
|
+
rateLimited = true;
|
|
2088
|
+
} else {
|
|
2089
|
+
erroredPatterns.push({ pattern_id: p.id, error: (err && err.message) || String(err) });
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2022
2092
|
}
|
|
2023
2093
|
const generated_at = new Date().toISOString();
|
|
2024
2094
|
if (outputFormat === 'json') {
|
|
2025
2095
|
process.stdout.write(JSON.stringify({
|
|
2026
|
-
|
|
2096
|
+
// A rate-limited OR errored pattern means the scan is incomplete — a
|
|
2097
|
+
// zero match-count is no longer trustworthy as "clean".
|
|
2098
|
+
ok: !rateLimited && erroredPatterns.length === 0,
|
|
2027
2099
|
verb: 'watchlist',
|
|
2028
2100
|
mode: 'org-scan',
|
|
2029
2101
|
generated_at,
|
|
@@ -2032,6 +2104,7 @@ async function runWatchlistOrgScan(rawArgs = []) {
|
|
|
2032
2104
|
match_count: matches.length,
|
|
2033
2105
|
matches,
|
|
2034
2106
|
rate_limited: rateLimited,
|
|
2107
|
+
errored_patterns: erroredPatterns,
|
|
2035
2108
|
unauthenticated: unauth,
|
|
2036
2109
|
control_reference: 'NEW-CTRL-052 (MAL-2026-SHAI-HULUD-OSS lesson)',
|
|
2037
2110
|
}) + '\n');
|
|
@@ -2055,6 +2128,10 @@ async function runWatchlistOrgScan(rawArgs = []) {
|
|
|
2055
2128
|
lines.push('> **Warning:** GitHub rate limit hit on at least one pattern query. Some matches may be missing. Re-run with `GITHUB_TOKEN` set to lift the limit.');
|
|
2056
2129
|
lines.push('');
|
|
2057
2130
|
}
|
|
2131
|
+
if (erroredPatterns.length > 0) {
|
|
2132
|
+
lines.push(`> **Warning:** ${erroredPatterns.length} pattern quer${erroredPatterns.length === 1 ? 'y' : 'ies'} failed after retries (${erroredPatterns.map((e) => e.pattern_id).join(', ')}). A zero match-count for ${erroredPatterns.length === 1 ? 'that pattern' : 'those patterns'} is NOT a clean result — re-run to confirm.`);
|
|
2133
|
+
lines.push('');
|
|
2134
|
+
}
|
|
2058
2135
|
if (matches.length === 0) {
|
|
2059
2136
|
lines.push('_No repositories matching threat-actor patterns found._');
|
|
2060
2137
|
} else {
|
|
@@ -2076,6 +2153,9 @@ async function runWatchlistOrgScan(rawArgs = []) {
|
|
|
2076
2153
|
console.log(`Org: ${org} patterns: ${patterns.length} matches: ${matches.length}`);
|
|
2077
2154
|
if (unauth) console.log('(unauthenticated — set GITHUB_TOKEN for private-repo coverage + higher rate limit)');
|
|
2078
2155
|
if (rateLimited) console.log('WARNING: GitHub rate limit hit on at least one query. Re-run with GITHUB_TOKEN set.');
|
|
2156
|
+
if (erroredPatterns.length > 0) {
|
|
2157
|
+
console.log(`WARNING: ${erroredPatterns.length} pattern quer${erroredPatterns.length === 1 ? 'y' : 'ies'} failed after retries (${erroredPatterns.map((e) => e.pattern_id).join(', ')}). A zero match-count for those is NOT a clean result — re-run to confirm.`);
|
|
2158
|
+
}
|
|
2079
2159
|
if (matches.length === 0) {
|
|
2080
2160
|
console.log('No threat-actor-pattern repos found.');
|
|
2081
2161
|
return;
|
package/orchestrator/scanner.js
CHANGED
|
@@ -89,6 +89,10 @@ function kernelScan() {
|
|
|
89
89
|
value: kernel,
|
|
90
90
|
cve_id: cveId,
|
|
91
91
|
rwep_score: cve.rwep_score,
|
|
92
|
+
// Carry the catalog CVSS so the CSAF report emits a real cvss_v3 block
|
|
93
|
+
// (base score + vector) instead of a placeholder base_score:0.
|
|
94
|
+
cvss_score: cve.cvss_score,
|
|
95
|
+
cvss_vector: cve.cvss_vector,
|
|
92
96
|
cisa_kev: cve.cisa_kev,
|
|
93
97
|
action_required: 'Cross-reference kernel version against patched version for this CVE',
|
|
94
98
|
skill_hint: 'kernel-lpe-triage',
|
|
@@ -158,6 +162,10 @@ function mcpScan() {
|
|
|
158
162
|
tool,
|
|
159
163
|
config_path: p,
|
|
160
164
|
severity: 'low',
|
|
165
|
+
// Route directly like the successful mcp_server_detected finding, so a
|
|
166
|
+
// parse-error finding reaches mcp-agent-trust even if the domain
|
|
167
|
+
// routing table changes — domain fallback alone left it brittle.
|
|
168
|
+
skill_hint: 'mcp-agent-trust',
|
|
161
169
|
action_required: 'MCP config file exists but could not be parsed'
|
|
162
170
|
});
|
|
163
171
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blamejs/exceptd-skills",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.8",
|
|
4
4
|
"description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 51 skills, 11 catalogs (459 CVEs / 181 CWEs / 805 ATT&CK + ICS / 170 ATLAS / 468 D3FEND / 8888 RFCs), 35 jurisdictions, 10-class catalog gap detector + budget gate, real XML parser + canonical-form diff + content-pattern regression detection, Ed25519-signed.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-security",
|