@blamejs/exceptd-skills 0.9.1
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/AGENTS.md +232 -0
- package/ARCHITECTURE.md +267 -0
- package/CHANGELOG.md +616 -0
- package/CONTEXT.md +203 -0
- package/LICENSE +200 -0
- package/NOTICE +82 -0
- package/README.md +307 -0
- package/SECURITY.md +73 -0
- package/agents/README.md +81 -0
- package/agents/report-generator.md +156 -0
- package/agents/skill-updater.md +102 -0
- package/agents/source-validator.md +119 -0
- package/agents/threat-researcher.md +149 -0
- package/bin/exceptd.js +183 -0
- package/data/_indexes/_meta.json +88 -0
- package/data/_indexes/activity-feed.json +362 -0
- package/data/_indexes/catalog-summaries.json +229 -0
- package/data/_indexes/chains.json +7135 -0
- package/data/_indexes/currency.json +359 -0
- package/data/_indexes/did-ladders.json +451 -0
- package/data/_indexes/frequency.json +2072 -0
- package/data/_indexes/handoff-dag.json +476 -0
- package/data/_indexes/jurisdiction-clocks.json +967 -0
- package/data/_indexes/jurisdiction-map.json +536 -0
- package/data/_indexes/recipes.json +319 -0
- package/data/_indexes/section-offsets.json +3656 -0
- package/data/_indexes/stale-content.json +14 -0
- package/data/_indexes/summary-cards.json +1736 -0
- package/data/_indexes/theater-fingerprints.json +381 -0
- package/data/_indexes/token-budget.json +2137 -0
- package/data/_indexes/trigger-table.json +1374 -0
- package/data/_indexes/xref.json +818 -0
- package/data/atlas-ttps.json +282 -0
- package/data/cve-catalog.json +496 -0
- package/data/cwe-catalog.json +1017 -0
- package/data/d3fend-catalog.json +738 -0
- package/data/dlp-controls.json +1039 -0
- package/data/exploit-availability.json +67 -0
- package/data/framework-control-gaps.json +1255 -0
- package/data/global-frameworks.json +2913 -0
- package/data/rfc-references.json +324 -0
- package/data/zeroday-lessons.json +377 -0
- package/keys/public.pem +3 -0
- package/lib/framework-gap.js +328 -0
- package/lib/job-queue.js +195 -0
- package/lib/lint-skills.js +536 -0
- package/lib/prefetch.js +372 -0
- package/lib/refresh-external.js +713 -0
- package/lib/schemas/cve-catalog.schema.json +151 -0
- package/lib/schemas/manifest.schema.json +106 -0
- package/lib/schemas/skill-frontmatter.schema.json +113 -0
- package/lib/scoring.js +149 -0
- package/lib/sign.js +197 -0
- package/lib/ttp-mapper.js +80 -0
- package/lib/validate-catalog-meta.js +198 -0
- package/lib/validate-cve-catalog.js +213 -0
- package/lib/validate-indexes.js +83 -0
- package/lib/validate-package.js +162 -0
- package/lib/validate-vendor.js +85 -0
- package/lib/verify.js +216 -0
- package/lib/worker-pool.js +84 -0
- package/manifest-snapshot.json +1833 -0
- package/manifest.json +2108 -0
- package/orchestrator/README.md +124 -0
- package/orchestrator/dispatcher.js +140 -0
- package/orchestrator/event-bus.js +146 -0
- package/orchestrator/index.js +874 -0
- package/orchestrator/pipeline.js +201 -0
- package/orchestrator/scanner.js +327 -0
- package/orchestrator/scheduler.js +137 -0
- package/package.json +113 -0
- package/sbom.cdx.json +158 -0
- package/scripts/audit-cross-skill.js +261 -0
- package/scripts/audit-perf.js +160 -0
- package/scripts/bootstrap.js +205 -0
- package/scripts/build-indexes.js +721 -0
- package/scripts/builders/activity-feed.js +79 -0
- package/scripts/builders/catalog-summaries.js +67 -0
- package/scripts/builders/currency.js +109 -0
- package/scripts/builders/cwe-chains.js +105 -0
- package/scripts/builders/did-ladders.js +149 -0
- package/scripts/builders/frequency.js +89 -0
- package/scripts/builders/jurisdiction-clocks.js +126 -0
- package/scripts/builders/recipes.js +159 -0
- package/scripts/builders/section-offsets.js +162 -0
- package/scripts/builders/stale-content.js +171 -0
- package/scripts/builders/summary-cards.js +166 -0
- package/scripts/builders/theater-fingerprints.js +198 -0
- package/scripts/builders/token-budget.js +96 -0
- package/scripts/check-manifest-snapshot.js +217 -0
- package/scripts/predeploy.js +267 -0
- package/scripts/refresh-manifest-snapshot.js +57 -0
- package/scripts/refresh-sbom.js +222 -0
- package/skills/age-gates-child-safety/skill.md +456 -0
- package/skills/ai-attack-surface/skill.md +282 -0
- package/skills/ai-c2-detection/skill.md +440 -0
- package/skills/ai-risk-management/skill.md +311 -0
- package/skills/api-security/skill.md +287 -0
- package/skills/attack-surface-pentest/skill.md +381 -0
- package/skills/cloud-security/skill.md +384 -0
- package/skills/compliance-theater/skill.md +365 -0
- package/skills/container-runtime-security/skill.md +379 -0
- package/skills/coordinated-vuln-disclosure/skill.md +473 -0
- package/skills/defensive-countermeasure-mapping/skill.md +300 -0
- package/skills/dlp-gap-analysis/skill.md +337 -0
- package/skills/email-security-anti-phishing/skill.md +206 -0
- package/skills/exploit-scoring/skill.md +331 -0
- package/skills/framework-gap-analysis/skill.md +374 -0
- package/skills/fuzz-testing-strategy/skill.md +313 -0
- package/skills/global-grc/skill.md +564 -0
- package/skills/identity-assurance/skill.md +272 -0
- package/skills/incident-response-playbook/skill.md +546 -0
- package/skills/kernel-lpe-triage/skill.md +303 -0
- package/skills/mcp-agent-trust/skill.md +326 -0
- package/skills/mlops-security/skill.md +325 -0
- package/skills/ot-ics-security/skill.md +340 -0
- package/skills/policy-exception-gen/skill.md +437 -0
- package/skills/pqc-first/skill.md +546 -0
- package/skills/rag-pipeline-security/skill.md +294 -0
- package/skills/researcher/skill.md +310 -0
- package/skills/sector-energy/skill.md +409 -0
- package/skills/sector-federal-government/skill.md +302 -0
- package/skills/sector-financial/skill.md +398 -0
- package/skills/sector-healthcare/skill.md +373 -0
- package/skills/security-maturity-tiers/skill.md +464 -0
- package/skills/skill-update-loop/skill.md +463 -0
- package/skills/supply-chain-integrity/skill.md +318 -0
- package/skills/threat-model-currency/skill.md +404 -0
- package/skills/threat-modeling-methodology/skill.md +312 -0
- package/skills/webapp-security/skill.md +281 -0
- package/skills/zeroday-gap-learn/skill.md +350 -0
- package/vendor/blamejs/LICENSE +201 -0
- package/vendor/blamejs/README.md +54 -0
- package/vendor/blamejs/_PROVENANCE.json +54 -0
- package/vendor/blamejs/retry.js +335 -0
- package/vendor/blamejs/worker-pool.js +418 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Multi-agent pipeline coordinator.
|
|
5
|
+
* Orchestrates: threat-researcher → source-validator → skill-updater → report-generator
|
|
6
|
+
*
|
|
7
|
+
* This module coordinates agent handoffs using a structured JSON protocol.
|
|
8
|
+
* Each stage produces a handoff package that the next stage consumes.
|
|
9
|
+
* Agents themselves are defined in agents/ and executed by AI assistants — not by this code.
|
|
10
|
+
* This module tracks state, validates handoffs, and routes between stages.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const crypto = require('crypto');
|
|
16
|
+
|
|
17
|
+
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
|
|
18
|
+
const DATA_DIR = process.env.EXCEPTD_DATA_DIR || path.join(__dirname, '..', 'data');
|
|
19
|
+
const REPORTS_DIR = path.join(__dirname, '..', 'reports');
|
|
20
|
+
|
|
21
|
+
const PIPELINE_STAGES = ['threat-researcher', 'source-validator', 'skill-updater', 'report-generator'];
|
|
22
|
+
|
|
23
|
+
// --- public API ---
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Initialize a new pipeline run for a given trigger.
|
|
27
|
+
*
|
|
28
|
+
* @param {'new_cve'|'atlas_update'|'framework_amendment'|'manual'} triggerType
|
|
29
|
+
* @param {object} triggerPayload - CVE ID, ATLAS version, etc.
|
|
30
|
+
* @returns {{ pipeline_id: string, trigger: object, stages: object[], status: string }}
|
|
31
|
+
*/
|
|
32
|
+
function initPipeline(triggerType, triggerPayload) {
|
|
33
|
+
const pipelineId = crypto.randomUUID();
|
|
34
|
+
const run = {
|
|
35
|
+
pipeline_id: pipelineId,
|
|
36
|
+
trigger: { type: triggerType, payload: triggerPayload, timestamp: new Date().toISOString() },
|
|
37
|
+
stages: PIPELINE_STAGES.map(name => ({
|
|
38
|
+
name,
|
|
39
|
+
status: 'pending',
|
|
40
|
+
agent_path: path.join(AGENTS_DIR, `${name}.md`),
|
|
41
|
+
started_at: null,
|
|
42
|
+
completed_at: null,
|
|
43
|
+
handoff: null,
|
|
44
|
+
errors: []
|
|
45
|
+
})),
|
|
46
|
+
status: 'initialized',
|
|
47
|
+
created_at: new Date().toISOString()
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
run.stages[0].status = 'ready';
|
|
51
|
+
return run;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build the handoff package for a specific pipeline stage.
|
|
56
|
+
* This is what an AI assistant reads to understand what to do and what to pass forward.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} run - Pipeline run object from initPipeline()
|
|
59
|
+
* @param {number} stageIndex - 0-based stage index
|
|
60
|
+
* @param {object} stageOutput - Output from the current stage
|
|
61
|
+
* @returns {object} Handoff package for the next stage
|
|
62
|
+
*/
|
|
63
|
+
function buildHandoff(run, stageIndex, stageOutput) {
|
|
64
|
+
const currentStage = run.stages[stageIndex];
|
|
65
|
+
const nextStage = run.stages[stageIndex + 1];
|
|
66
|
+
|
|
67
|
+
validateHandoff(currentStage.name, stageOutput);
|
|
68
|
+
|
|
69
|
+
const handoff = {
|
|
70
|
+
handoff_id: crypto.randomUUID(),
|
|
71
|
+
pipeline_id: run.pipeline_id,
|
|
72
|
+
from_stage: currentStage.name,
|
|
73
|
+
to_stage: nextStage?.name || 'complete',
|
|
74
|
+
timestamp: new Date().toISOString(),
|
|
75
|
+
trigger: run.trigger,
|
|
76
|
+
payload: stageOutput,
|
|
77
|
+
instructions: nextStage ? getStageInstructions(nextStage.name, stageOutput) : null
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
currentStage.handoff = handoff;
|
|
81
|
+
currentStage.status = 'completed';
|
|
82
|
+
currentStage.completed_at = new Date().toISOString();
|
|
83
|
+
|
|
84
|
+
if (nextStage) {
|
|
85
|
+
nextStage.status = 'ready';
|
|
86
|
+
} else {
|
|
87
|
+
run.status = 'completed';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return handoff;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check the currency of all skills and return a report.
|
|
95
|
+
* Used by the scheduler for weekly currency checks.
|
|
96
|
+
*
|
|
97
|
+
* @returns {{ currency_report: object[], action_required: boolean }}
|
|
98
|
+
*/
|
|
99
|
+
function currencyCheck() {
|
|
100
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'manifest.json'), 'utf8'));
|
|
101
|
+
const now = new Date();
|
|
102
|
+
const report = [];
|
|
103
|
+
|
|
104
|
+
for (const skill of manifest.skills) {
|
|
105
|
+
const reviewDate = new Date(skill.last_threat_review || '2020-01-01');
|
|
106
|
+
const daysSinceReview = Math.floor((now - reviewDate) / (1000 * 60 * 60 * 24));
|
|
107
|
+
|
|
108
|
+
const currencyScore = _currencyScore(daysSinceReview, skill.forward_watch?.length || 0);
|
|
109
|
+
|
|
110
|
+
report.push({
|
|
111
|
+
skill: skill.name,
|
|
112
|
+
last_threat_review: skill.last_threat_review,
|
|
113
|
+
days_since_review: daysSinceReview,
|
|
114
|
+
currency_score: currencyScore,
|
|
115
|
+
currency_label: _currencyLabel(currencyScore),
|
|
116
|
+
forward_watch_count: skill.forward_watch?.length || 0,
|
|
117
|
+
action_required: currencyScore < 70
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
report.sort((a, b) => a.currency_score - b.currency_score);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
currency_report: report,
|
|
125
|
+
action_required: report.some(r => r.action_required),
|
|
126
|
+
critical_count: report.filter(r => r.currency_score < 50).length,
|
|
127
|
+
check_timestamp: now.toISOString()
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get the agent definition for a stage.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} stageName - Agent name
|
|
135
|
+
* @returns {string|null} Agent instruction content
|
|
136
|
+
*/
|
|
137
|
+
function getAgentDefinition(stageName) {
|
|
138
|
+
const agentPath = path.join(AGENTS_DIR, `${stageName}.md`);
|
|
139
|
+
try {
|
|
140
|
+
return fs.readFileSync(agentPath, 'utf8');
|
|
141
|
+
} catch (_) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- private helpers ---
|
|
147
|
+
|
|
148
|
+
function validateHandoff(stageName, output) {
|
|
149
|
+
const required = {
|
|
150
|
+
'threat-researcher': ['cve_id_or_ttp', 'findings', 'primary_sources', 'confidence'],
|
|
151
|
+
'source-validator': ['verdict', 'verified_claims', 'rejected_claims'],
|
|
152
|
+
'skill-updater': ['updated_skills', 'updated_data_files', 'change_summary'],
|
|
153
|
+
'report-generator': ['report_format', 'report_content', 'audience']
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const req = required[stageName] || [];
|
|
157
|
+
const missing = req.filter(k => !(k in output));
|
|
158
|
+
if (missing.length > 0) {
|
|
159
|
+
throw new Error(`Handoff from ${stageName} missing required fields: ${missing.join(', ')}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getStageInstructions(stageName, previousOutput) {
|
|
164
|
+
const instructions = {
|
|
165
|
+
'source-validator': `Validate the threat research findings. Check each claimed primary source.
|
|
166
|
+
Verify: CVE exists in NVD, CISA KEV status is accurate, RWEP factor breakdown is justified.
|
|
167
|
+
Return verdict: approved | approved_with_corrections | rejected.
|
|
168
|
+
Input: ${JSON.stringify(previousOutput, null, 2).substring(0, 500)}...`,
|
|
169
|
+
|
|
170
|
+
'skill-updater': `Apply validated research to skill files and data catalogs.
|
|
171
|
+
For each approved finding: update data/cve-catalog.json, data/zeroday-lessons.json,
|
|
172
|
+
data/framework-control-gaps.json as appropriate. Bump last_threat_review in affected skills.
|
|
173
|
+
Input: ${JSON.stringify(previousOutput, null, 2).substring(0, 500)}...`,
|
|
174
|
+
|
|
175
|
+
'report-generator': `Generate structured reports from the completed pipeline run.
|
|
176
|
+
Produce: executive-summary.md, compliance-gap-report.md as applicable.
|
|
177
|
+
Focus on RWEP scores >= 80 and compliance theater findings.
|
|
178
|
+
Input: ${JSON.stringify(previousOutput, null, 2).substring(0, 500)}...`
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
return instructions[stageName] || null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function _currencyScore(daysSinceReview, forwardWatchCount) {
|
|
185
|
+
let score = 100;
|
|
186
|
+
if (daysSinceReview > 180) score -= 30;
|
|
187
|
+
else if (daysSinceReview > 90) score -= 20;
|
|
188
|
+
else if (daysSinceReview > 60) score -= 10;
|
|
189
|
+
else if (daysSinceReview > 30) score -= 5;
|
|
190
|
+
score -= forwardWatchCount * 5;
|
|
191
|
+
return Math.max(0, score);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function _currencyLabel(score) {
|
|
195
|
+
if (score >= 90) return 'current';
|
|
196
|
+
if (score >= 70) return 'acceptable';
|
|
197
|
+
if (score >= 50) return 'stale';
|
|
198
|
+
return 'critical_stale';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = { initPipeline, buildHandoff, currencyCheck, getAgentDefinition };
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Environment scanner. Discovers security posture signals from the current host.
|
|
5
|
+
* Produces structured findings that dispatcher.js routes to relevant skills.
|
|
6
|
+
*
|
|
7
|
+
* Designed to be run by a human operator or an AI assistant — not a background daemon.
|
|
8
|
+
* All discovery is read-only. No writes, no network calls beyond local probes.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
const DATA_DIR = process.env.EXCEPTD_DATA_DIR || path.join(__dirname, '..', 'data');
|
|
17
|
+
|
|
18
|
+
// --- public API ---
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Run all scanners and return consolidated findings.
|
|
22
|
+
* @returns {{ timestamp: string, host: object, findings: object[], summary: object }}
|
|
23
|
+
*/
|
|
24
|
+
async function scan() {
|
|
25
|
+
const timestamp = new Date().toISOString();
|
|
26
|
+
const findings = [];
|
|
27
|
+
|
|
28
|
+
const host = hostInfo();
|
|
29
|
+
findings.push(...kernelScan());
|
|
30
|
+
findings.push(...mcpScan());
|
|
31
|
+
findings.push(...cryptoScan());
|
|
32
|
+
findings.push(...aiApiScan());
|
|
33
|
+
findings.push(...frameworkScan());
|
|
34
|
+
|
|
35
|
+
const summary = summarize(findings);
|
|
36
|
+
return { timestamp, host, findings, summary };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Run a targeted scan for a specific domain.
|
|
41
|
+
* @param {'kernel'|'mcp'|'crypto'|'ai_api'|'framework'} domain
|
|
42
|
+
*/
|
|
43
|
+
async function scanDomain(domain) {
|
|
44
|
+
const scanners = { kernel: kernelScan, mcp: mcpScan, crypto: cryptoScan, ai_api: aiApiScan, framework: frameworkScan };
|
|
45
|
+
const fn = scanners[domain];
|
|
46
|
+
if (!fn) throw new Error(`Unknown scan domain: ${domain}. Valid: ${Object.keys(scanners).join(', ')}`);
|
|
47
|
+
return fn();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --- domain scanners ---
|
|
51
|
+
|
|
52
|
+
function kernelScan() {
|
|
53
|
+
const findings = [];
|
|
54
|
+
if (os.platform() !== 'linux') return findings;
|
|
55
|
+
|
|
56
|
+
const kernel = safeExecFile('uname', ['-r']) || 'unknown';
|
|
57
|
+
const catalog = loadJson('cve-catalog.json');
|
|
58
|
+
|
|
59
|
+
for (const [cveId, cve] of Object.entries(catalog)) {
|
|
60
|
+
if (cve.type !== 'LPE' && cve.type !== 'kernel') continue;
|
|
61
|
+
findings.push({
|
|
62
|
+
domain: 'kernel',
|
|
63
|
+
signal: 'kernel_version_detected',
|
|
64
|
+
value: kernel,
|
|
65
|
+
cve_id: cveId,
|
|
66
|
+
rwep_score: cve.rwep_score,
|
|
67
|
+
cisa_kev: cve.cisa_kev,
|
|
68
|
+
action_required: 'Cross-reference kernel version against patched version for this CVE',
|
|
69
|
+
skill_hint: 'kernel-lpe-triage',
|
|
70
|
+
severity: cve.rwep_score >= 90 ? 'critical' : cve.rwep_score >= 70 ? 'high' : 'medium'
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return findings;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function mcpScan() {
|
|
78
|
+
const findings = [];
|
|
79
|
+
const homeDir = os.homedir();
|
|
80
|
+
const platform = os.platform();
|
|
81
|
+
|
|
82
|
+
const mcpLocations = [
|
|
83
|
+
{ tool: 'claude-code', paths: [path.join(homeDir, '.claude', 'settings.json')] },
|
|
84
|
+
{ tool: 'cursor', paths: [path.join(homeDir, '.cursor', 'mcp.json')] },
|
|
85
|
+
{ tool: 'vscode', paths: [
|
|
86
|
+
path.join(homeDir, '.vscode', 'settings.json'),
|
|
87
|
+
...(platform === 'darwin' ? [path.join(homeDir, 'Library', 'Application Support', 'Code', 'User', 'settings.json')] : []),
|
|
88
|
+
...(platform === 'win32' ? [path.join(homeDir, 'AppData', 'Roaming', 'Code', 'User', 'settings.json')] : []),
|
|
89
|
+
...(platform === 'linux' ? [path.join(homeDir, '.config', 'Code', 'User', 'settings.json')] : [])
|
|
90
|
+
]},
|
|
91
|
+
{ tool: 'windsurf', paths: [path.join(homeDir, '.windsurf', 'mcp.json')] },
|
|
92
|
+
{ tool: 'gemini-cli', paths: [path.join(homeDir, '.gemini', 'settings.json')] }
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
for (const { tool, paths } of mcpLocations) {
|
|
96
|
+
for (const p of paths) {
|
|
97
|
+
if (!fs.existsSync(p)) continue;
|
|
98
|
+
try {
|
|
99
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
100
|
+
const config = JSON.parse(raw);
|
|
101
|
+
const mcpServers = config.mcpServers || config.mcp?.servers || {};
|
|
102
|
+
const serverList = Object.keys(mcpServers);
|
|
103
|
+
if (serverList.length === 0) continue;
|
|
104
|
+
|
|
105
|
+
for (const serverName of serverList) {
|
|
106
|
+
const server = mcpServers[serverName];
|
|
107
|
+
const isSigned = server.signature !== undefined;
|
|
108
|
+
const serverJson = JSON.stringify(server);
|
|
109
|
+
const hasPinnedVersion = /[@#]\d+\.\d+\.\d+/.test(serverJson);
|
|
110
|
+
|
|
111
|
+
findings.push({
|
|
112
|
+
domain: 'mcp',
|
|
113
|
+
signal: 'mcp_server_detected',
|
|
114
|
+
tool,
|
|
115
|
+
config_path: p,
|
|
116
|
+
server_name: serverName,
|
|
117
|
+
server_config: sanitizeConfig(server),
|
|
118
|
+
signed: isSigned,
|
|
119
|
+
version_pinned: hasPinnedVersion,
|
|
120
|
+
severity: !isSigned ? 'high' : !hasPinnedVersion ? 'medium' : 'low',
|
|
121
|
+
skill_hint: 'mcp-agent-trust',
|
|
122
|
+
action_required: !isSigned
|
|
123
|
+
? 'Unsigned MCP server — verify provenance immediately'
|
|
124
|
+
: !hasPinnedVersion
|
|
125
|
+
? 'MCP server version not pinned — pin to a specific version'
|
|
126
|
+
: 'MCP server detected — include in security review'
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
} catch (_) {
|
|
130
|
+
findings.push({
|
|
131
|
+
domain: 'mcp',
|
|
132
|
+
signal: 'mcp_config_parse_error',
|
|
133
|
+
tool,
|
|
134
|
+
config_path: p,
|
|
135
|
+
severity: 'low',
|
|
136
|
+
action_required: 'MCP config file exists but could not be parsed'
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return findings;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function cryptoScan() {
|
|
146
|
+
const findings = [];
|
|
147
|
+
|
|
148
|
+
const opensslRaw = safeExecFile('openssl', ['version']);
|
|
149
|
+
if (opensslRaw) {
|
|
150
|
+
const match = opensslRaw.match(/OpenSSL (\d+\.\d+\.\d+)/);
|
|
151
|
+
const version = match ? match[1] : 'unknown';
|
|
152
|
+
const [major, minor] = version.split('.').map(Number);
|
|
153
|
+
const isPqcReady = major > 3 || (major === 3 && minor >= 5);
|
|
154
|
+
|
|
155
|
+
findings.push({
|
|
156
|
+
domain: 'crypto',
|
|
157
|
+
signal: 'openssl_version',
|
|
158
|
+
value: version,
|
|
159
|
+
pqc_ready: isPqcReady,
|
|
160
|
+
severity: isPqcReady ? 'info' : 'high',
|
|
161
|
+
skill_hint: 'pqc-first',
|
|
162
|
+
action_required: isPqcReady
|
|
163
|
+
? `OpenSSL ${version} — PQC-capable. Verify ML-KEM/ML-DSA algorithm availability.`
|
|
164
|
+
: `OpenSSL ${version} — below 3.5. Upgrade required for PQC support (ML-KEM, ML-DSA, SLH-DSA).`
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const tlsProbe = probeTls();
|
|
169
|
+
if (tlsProbe) {
|
|
170
|
+
findings.push({
|
|
171
|
+
domain: 'crypto',
|
|
172
|
+
signal: 'tls_probe',
|
|
173
|
+
value: tlsProbe,
|
|
174
|
+
severity: tlsProbe.includes('TLSv1.3') ? 'info' : 'high',
|
|
175
|
+
skill_hint: 'pqc-first',
|
|
176
|
+
action_required: tlsProbe.includes('TLSv1.3')
|
|
177
|
+
? 'TLS 1.3 detected — verify X25519+ML-KEM-768 hybrid for HNDL-exposed connections'
|
|
178
|
+
: 'TLS below 1.3 — upgrade to TLS 1.3 minimum'
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return findings;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function aiApiScan() {
|
|
186
|
+
const findings = [];
|
|
187
|
+
|
|
188
|
+
const aiApiIndicators = [
|
|
189
|
+
{ name: 'openai', envVars: ['OPENAI_API_KEY'] },
|
|
190
|
+
{ name: 'anthropic', envVars: ['ANTHROPIC_API_KEY'] },
|
|
191
|
+
{ name: 'google-ai', envVars: ['GOOGLE_AI_API_KEY', 'GEMINI_API_KEY'] },
|
|
192
|
+
{ name: 'azure-openai', envVars: ['AZURE_OPENAI_API_KEY'] },
|
|
193
|
+
{ name: 'cohere', envVars: ['COHERE_API_KEY'] },
|
|
194
|
+
{ name: 'mistral', envVars: ['MISTRAL_API_KEY'] },
|
|
195
|
+
{ name: 'groq', envVars: ['GROQ_API_KEY'] },
|
|
196
|
+
{ name: 'together-ai', envVars: ['TOGETHER_API_KEY'] }
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
for (const api of aiApiIndicators) {
|
|
200
|
+
const detected = api.envVars.some(v => process.env[v]);
|
|
201
|
+
if (!detected) continue;
|
|
202
|
+
|
|
203
|
+
findings.push({
|
|
204
|
+
domain: 'ai_api',
|
|
205
|
+
signal: 'ai_api_dependency_detected',
|
|
206
|
+
api_name: api.name,
|
|
207
|
+
severity: 'info',
|
|
208
|
+
skill_hint: 'ai-c2-detection',
|
|
209
|
+
action_required: `${api.name} AI API detected — verify process-level monitoring and query anomaly alerting`
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const apiCount = findings.filter(f => f.signal === 'ai_api_dependency_detected').length;
|
|
214
|
+
if (apiCount > 0) {
|
|
215
|
+
findings.push({
|
|
216
|
+
domain: 'ai_api',
|
|
217
|
+
signal: 'ai_api_c2_risk_summary',
|
|
218
|
+
count: apiCount,
|
|
219
|
+
severity: 'medium',
|
|
220
|
+
skill_hint: 'ai-c2-detection',
|
|
221
|
+
action_required: `${apiCount} AI API(s) detected. Run ai-c2-detection skill to assess SesameOp/PROMPTFLUX exposure.`
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return findings;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function frameworkScan() {
|
|
229
|
+
const findings = [];
|
|
230
|
+
const gaps = loadJson('framework-control-gaps.json');
|
|
231
|
+
const openGaps = Object.entries(gaps).filter(([, g]) => g.status === 'open');
|
|
232
|
+
|
|
233
|
+
if (openGaps.length > 0) {
|
|
234
|
+
findings.push({
|
|
235
|
+
domain: 'framework',
|
|
236
|
+
signal: 'open_framework_gaps',
|
|
237
|
+
count: openGaps.length,
|
|
238
|
+
universal_gaps: openGaps.filter(([, g]) => g.framework === 'ALL').length,
|
|
239
|
+
severity: 'high',
|
|
240
|
+
skill_hint: 'framework-gap-analysis',
|
|
241
|
+
action_required: `${openGaps.length} open control gaps — run framework-gap-analysis for your compliance scope`
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const kev = loadJson('cve-catalog.json');
|
|
246
|
+
const kevHigh = Object.entries(kev).filter(([, c]) => c.cisa_kev && c.rwep_score >= 90);
|
|
247
|
+
if (kevHigh.length > 0) {
|
|
248
|
+
findings.push({
|
|
249
|
+
domain: 'framework',
|
|
250
|
+
signal: 'cisa_kev_high_rwep',
|
|
251
|
+
items: kevHigh.map(([id, c]) => ({ id, rwep: c.rwep_score, name: c.name })),
|
|
252
|
+
severity: 'critical',
|
|
253
|
+
skill_hint: 'compliance-theater',
|
|
254
|
+
action_required: `${kevHigh.length} CISA KEV CVEs with RWEP >= 90. Standard 30-day patch SLAs are theater for these.`
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return findings;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// --- helpers ---
|
|
262
|
+
|
|
263
|
+
function hostInfo() {
|
|
264
|
+
return {
|
|
265
|
+
platform: os.platform(),
|
|
266
|
+
arch: os.arch(),
|
|
267
|
+
release: os.release(),
|
|
268
|
+
hostname: os.hostname(),
|
|
269
|
+
scan_user: os.userInfo().username
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function summarize(findings) {
|
|
274
|
+
const byDomain = {};
|
|
275
|
+
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
|
|
276
|
+
for (const f of findings) {
|
|
277
|
+
byDomain[f.domain] = (byDomain[f.domain] || 0) + 1;
|
|
278
|
+
if (bySeverity[f.severity] !== undefined) bySeverity[f.severity]++;
|
|
279
|
+
}
|
|
280
|
+
const skills = [...new Set(findings.map(f => f.skill_hint).filter(Boolean))];
|
|
281
|
+
return {
|
|
282
|
+
total_findings: findings.length,
|
|
283
|
+
by_domain: byDomain,
|
|
284
|
+
by_severity: bySeverity,
|
|
285
|
+
recommended_skills: skills,
|
|
286
|
+
action_required: bySeverity.critical > 0 || bySeverity.high > 0
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function safeExecFile(cmd, args) {
|
|
291
|
+
try {
|
|
292
|
+
return execFileSync(cmd, args, { timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
293
|
+
.toString().trim();
|
|
294
|
+
} catch (_) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function probeTls() {
|
|
300
|
+
const result = spawnSync('openssl', ['s_client', '-connect', 'google.com:443', '-brief'], {
|
|
301
|
+
input: '',
|
|
302
|
+
timeout: 5000,
|
|
303
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
304
|
+
});
|
|
305
|
+
if (result.error || result.status !== 0) return null;
|
|
306
|
+
const output = (result.stdout || '').toString() + (result.stderr || '').toString();
|
|
307
|
+
const match = output.match(/Protocol\s*:\s*(TLSv\d+\.\d+)/);
|
|
308
|
+
return match ? match[1] : null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function sanitizeConfig(obj) {
|
|
312
|
+
const safe = { ...obj };
|
|
313
|
+
for (const key of Object.keys(safe)) {
|
|
314
|
+
if (/token|key|secret|password|credential/i.test(key)) safe[key] = '[REDACTED]';
|
|
315
|
+
}
|
|
316
|
+
return safe;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function loadJson(filename) {
|
|
320
|
+
try {
|
|
321
|
+
return JSON.parse(fs.readFileSync(path.join(DATA_DIR, filename), 'utf8'));
|
|
322
|
+
} catch (_) {
|
|
323
|
+
return {};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
module.exports = { scan, scanDomain };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Scheduled task coordinator for skill currency maintenance.
|
|
5
|
+
*
|
|
6
|
+
* Schedules: weekly currency check, monthly CVE validation, annual full audit.
|
|
7
|
+
* Emits events via event-bus.js when currency thresholds are breached.
|
|
8
|
+
*
|
|
9
|
+
* This is a simple interval-based scheduler. For production use, swap for a
|
|
10
|
+
* proper cron daemon or cloud scheduler without changing the task definitions.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { bus, EVENT_TYPES } = require('./event-bus');
|
|
14
|
+
const { currencyCheck } = require('./pipeline');
|
|
15
|
+
|
|
16
|
+
const INTERVALS = {
|
|
17
|
+
WEEKLY_CURRENCY: 7 * 24 * 60 * 60 * 1000,
|
|
18
|
+
MONTHLY_CVE_VALIDATION: 30 * 24 * 60 * 60 * 1000,
|
|
19
|
+
ANNUAL_AUDIT: 365 * 24 * 60 * 60 * 1000
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const CURRENCY_THRESHOLDS = {
|
|
23
|
+
critical: 50,
|
|
24
|
+
warning: 70
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
let timers = [];
|
|
28
|
+
let running = false;
|
|
29
|
+
|
|
30
|
+
// --- public API ---
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Start the scheduler. Runs all tasks immediately on start, then on schedule.
|
|
34
|
+
*/
|
|
35
|
+
function start() {
|
|
36
|
+
if (running) return;
|
|
37
|
+
running = true;
|
|
38
|
+
|
|
39
|
+
runWeeklyCurrencyCheck();
|
|
40
|
+
timers.push(setInterval(runWeeklyCurrencyCheck, INTERVALS.WEEKLY_CURRENCY));
|
|
41
|
+
timers.push(setInterval(runMonthlyCveValidation, INTERVALS.MONTHLY_CVE_VALIDATION));
|
|
42
|
+
timers.push(setInterval(runAnnualAudit, INTERVALS.ANNUAL_AUDIT));
|
|
43
|
+
|
|
44
|
+
console.log('[scheduler] Started. Weekly currency check, monthly CVE validation, annual audit scheduled.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Stop the scheduler and clear all timers.
|
|
49
|
+
*/
|
|
50
|
+
function stop() {
|
|
51
|
+
for (const t of timers) clearInterval(t);
|
|
52
|
+
timers = [];
|
|
53
|
+
running = false;
|
|
54
|
+
console.log('[scheduler] Stopped.');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Run just the currency check immediately (for CLI use).
|
|
59
|
+
* @returns {object} Currency report
|
|
60
|
+
*/
|
|
61
|
+
function runCurrencyNow() {
|
|
62
|
+
return runWeeklyCurrencyCheck();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// --- task implementations ---
|
|
66
|
+
|
|
67
|
+
function runWeeklyCurrencyCheck() {
|
|
68
|
+
const timestamp = new Date().toISOString();
|
|
69
|
+
console.log(`[scheduler] Running weekly currency check — ${timestamp}`);
|
|
70
|
+
|
|
71
|
+
const { currency_report, action_required, critical_count } = currencyCheck();
|
|
72
|
+
|
|
73
|
+
for (const skill of currency_report) {
|
|
74
|
+
if (skill.currency_score < CURRENCY_THRESHOLDS.critical) {
|
|
75
|
+
bus.skillCurrencyLow({
|
|
76
|
+
skill_name: skill.skill,
|
|
77
|
+
currency_score: skill.currency_score,
|
|
78
|
+
days_since_review: skill.days_since_review
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const result = {
|
|
84
|
+
task: 'weekly_currency_check',
|
|
85
|
+
timestamp,
|
|
86
|
+
skills_checked: currency_report.length,
|
|
87
|
+
action_required,
|
|
88
|
+
critical_count,
|
|
89
|
+
critical_skills: currency_report.filter(s => s.currency_score < CURRENCY_THRESHOLDS.critical).map(s => s.skill),
|
|
90
|
+
warning_skills: currency_report.filter(s =>
|
|
91
|
+
s.currency_score >= CURRENCY_THRESHOLDS.critical && s.currency_score < CURRENCY_THRESHOLDS.warning
|
|
92
|
+
).map(s => s.skill)
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (action_required) {
|
|
96
|
+
console.log(`[scheduler] Currency action required — ${critical_count} critical skills`);
|
|
97
|
+
console.log('[scheduler] Critical skills:', result.critical_skills.join(', ') || 'none');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function runMonthlyCveValidation() {
|
|
104
|
+
const timestamp = new Date().toISOString();
|
|
105
|
+
console.log(`[scheduler] Monthly CVE validation reminder — ${timestamp}`);
|
|
106
|
+
console.log('[scheduler] Action: Verify all data/cve-catalog.json entries against NVD and CISA KEV.');
|
|
107
|
+
console.log('[scheduler] Action: Update last_verified dates in data/exploit-availability.json.');
|
|
108
|
+
console.log('[scheduler] Run: node orchestrator/index.js validate-cves');
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
task: 'monthly_cve_validation',
|
|
112
|
+
timestamp,
|
|
113
|
+
action: 'Run node orchestrator/index.js validate-cves to check all CVE entries'
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function runAnnualAudit() {
|
|
118
|
+
const timestamp = new Date().toISOString();
|
|
119
|
+
console.log(`[scheduler] Annual full skill audit — ${timestamp}`);
|
|
120
|
+
console.log('[scheduler] All skills require full threat review against current landscape.');
|
|
121
|
+
console.log('[scheduler] See skill-update-loop for the full annual audit procedure.');
|
|
122
|
+
|
|
123
|
+
bus.emit(EVENT_TYPES.SKILL_CURRENCY_LOW, {
|
|
124
|
+
skill_name: 'ALL',
|
|
125
|
+
currency_score: 0,
|
|
126
|
+
days_since_review: 365,
|
|
127
|
+
note: 'Annual audit — all skills require review'
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
task: 'annual_full_audit',
|
|
132
|
+
timestamp,
|
|
133
|
+
action: 'Invoke skill-update-loop for all skills — annual currency review required'
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = { start, stop, runCurrencyNow };
|