defense-mcp-server 0.6.0
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 +471 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/build/core/auto-installer.d.ts +102 -0
- package/build/core/auto-installer.d.ts.map +1 -0
- package/build/core/auto-installer.js +833 -0
- package/build/core/backup-manager.d.ts +63 -0
- package/build/core/backup-manager.d.ts.map +1 -0
- package/build/core/backup-manager.js +189 -0
- package/build/core/changelog.d.ts +75 -0
- package/build/core/changelog.d.ts.map +1 -0
- package/build/core/changelog.js +123 -0
- package/build/core/command-allowlist.d.ts +129 -0
- package/build/core/command-allowlist.d.ts.map +1 -0
- package/build/core/command-allowlist.js +849 -0
- package/build/core/config.d.ts +79 -0
- package/build/core/config.d.ts.map +1 -0
- package/build/core/config.js +193 -0
- package/build/core/dependency-validator.d.ts +106 -0
- package/build/core/dependency-validator.d.ts.map +1 -0
- package/build/core/dependency-validator.js +405 -0
- package/build/core/distro-adapter.d.ts +177 -0
- package/build/core/distro-adapter.d.ts.map +1 -0
- package/build/core/distro-adapter.js +481 -0
- package/build/core/distro.d.ts +68 -0
- package/build/core/distro.d.ts.map +1 -0
- package/build/core/distro.js +457 -0
- package/build/core/encrypted-state.d.ts +76 -0
- package/build/core/encrypted-state.d.ts.map +1 -0
- package/build/core/encrypted-state.js +209 -0
- package/build/core/executor.d.ts +56 -0
- package/build/core/executor.d.ts.map +1 -0
- package/build/core/executor.js +350 -0
- package/build/core/installer.d.ts +92 -0
- package/build/core/installer.d.ts.map +1 -0
- package/build/core/installer.js +1072 -0
- package/build/core/logger.d.ts +102 -0
- package/build/core/logger.d.ts.map +1 -0
- package/build/core/logger.js +132 -0
- package/build/core/parsers.d.ts +151 -0
- package/build/core/parsers.d.ts.map +1 -0
- package/build/core/parsers.js +479 -0
- package/build/core/policy-engine.d.ts +170 -0
- package/build/core/policy-engine.d.ts.map +1 -0
- package/build/core/policy-engine.js +656 -0
- package/build/core/preflight.d.ts +157 -0
- package/build/core/preflight.d.ts.map +1 -0
- package/build/core/preflight.js +638 -0
- package/build/core/privilege-manager.d.ts +108 -0
- package/build/core/privilege-manager.d.ts.map +1 -0
- package/build/core/privilege-manager.js +363 -0
- package/build/core/rate-limiter.d.ts +67 -0
- package/build/core/rate-limiter.d.ts.map +1 -0
- package/build/core/rate-limiter.js +129 -0
- package/build/core/rollback.d.ts +73 -0
- package/build/core/rollback.d.ts.map +1 -0
- package/build/core/rollback.js +278 -0
- package/build/core/safeguards.d.ts +58 -0
- package/build/core/safeguards.d.ts.map +1 -0
- package/build/core/safeguards.js +448 -0
- package/build/core/sanitizer.d.ts +118 -0
- package/build/core/sanitizer.d.ts.map +1 -0
- package/build/core/sanitizer.js +459 -0
- package/build/core/secure-fs.d.ts +67 -0
- package/build/core/secure-fs.d.ts.map +1 -0
- package/build/core/secure-fs.js +143 -0
- package/build/core/spawn-safe.d.ts +55 -0
- package/build/core/spawn-safe.d.ts.map +1 -0
- package/build/core/spawn-safe.js +146 -0
- package/build/core/sudo-guard.d.ts +145 -0
- package/build/core/sudo-guard.d.ts.map +1 -0
- package/build/core/sudo-guard.js +349 -0
- package/build/core/sudo-session.d.ts +100 -0
- package/build/core/sudo-session.d.ts.map +1 -0
- package/build/core/sudo-session.js +319 -0
- package/build/core/tool-dependencies.d.ts +61 -0
- package/build/core/tool-dependencies.d.ts.map +1 -0
- package/build/core/tool-dependencies.js +571 -0
- package/build/core/tool-registry.d.ts +111 -0
- package/build/core/tool-registry.d.ts.map +1 -0
- package/build/core/tool-registry.js +656 -0
- package/build/core/tool-wrapper.d.ts +73 -0
- package/build/core/tool-wrapper.d.ts.map +1 -0
- package/build/core/tool-wrapper.js +296 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +247 -0
- package/build/tools/access-control.d.ts +9 -0
- package/build/tools/access-control.d.ts.map +1 -0
- package/build/tools/access-control.js +1818 -0
- package/build/tools/api-security.d.ts +12 -0
- package/build/tools/api-security.d.ts.map +1 -0
- package/build/tools/api-security.js +901 -0
- package/build/tools/app-hardening.d.ts +11 -0
- package/build/tools/app-hardening.d.ts.map +1 -0
- package/build/tools/app-hardening.js +768 -0
- package/build/tools/backup.d.ts +8 -0
- package/build/tools/backup.d.ts.map +1 -0
- package/build/tools/backup.js +381 -0
- package/build/tools/cloud-security.d.ts +17 -0
- package/build/tools/cloud-security.d.ts.map +1 -0
- package/build/tools/cloud-security.js +739 -0
- package/build/tools/compliance.d.ts +10 -0
- package/build/tools/compliance.d.ts.map +1 -0
- package/build/tools/compliance.js +1225 -0
- package/build/tools/container-security.d.ts +14 -0
- package/build/tools/container-security.d.ts.map +1 -0
- package/build/tools/container-security.js +788 -0
- package/build/tools/deception.d.ts +13 -0
- package/build/tools/deception.d.ts.map +1 -0
- package/build/tools/deception.js +763 -0
- package/build/tools/dns-security.d.ts +93 -0
- package/build/tools/dns-security.d.ts.map +1 -0
- package/build/tools/dns-security.js +745 -0
- package/build/tools/drift-detection.d.ts +8 -0
- package/build/tools/drift-detection.d.ts.map +1 -0
- package/build/tools/drift-detection.js +326 -0
- package/build/tools/ebpf-security.d.ts +15 -0
- package/build/tools/ebpf-security.d.ts.map +1 -0
- package/build/tools/ebpf-security.js +294 -0
- package/build/tools/encryption.d.ts +9 -0
- package/build/tools/encryption.d.ts.map +1 -0
- package/build/tools/encryption.js +1667 -0
- package/build/tools/firewall.d.ts +9 -0
- package/build/tools/firewall.d.ts.map +1 -0
- package/build/tools/firewall.js +1398 -0
- package/build/tools/hardening.d.ts +10 -0
- package/build/tools/hardening.d.ts.map +1 -0
- package/build/tools/hardening.js +2654 -0
- package/build/tools/ids.d.ts +9 -0
- package/build/tools/ids.d.ts.map +1 -0
- package/build/tools/ids.js +624 -0
- package/build/tools/incident-response.d.ts +10 -0
- package/build/tools/incident-response.d.ts.map +1 -0
- package/build/tools/incident-response.js +1180 -0
- package/build/tools/logging.d.ts +12 -0
- package/build/tools/logging.d.ts.map +1 -0
- package/build/tools/logging.js +454 -0
- package/build/tools/malware.d.ts +10 -0
- package/build/tools/malware.d.ts.map +1 -0
- package/build/tools/malware.js +532 -0
- package/build/tools/meta.d.ts +11 -0
- package/build/tools/meta.d.ts.map +1 -0
- package/build/tools/meta.js +2278 -0
- package/build/tools/network-defense.d.ts +12 -0
- package/build/tools/network-defense.d.ts.map +1 -0
- package/build/tools/network-defense.js +760 -0
- package/build/tools/patch-management.d.ts +3 -0
- package/build/tools/patch-management.d.ts.map +1 -0
- package/build/tools/patch-management.js +708 -0
- package/build/tools/process-security.d.ts +12 -0
- package/build/tools/process-security.d.ts.map +1 -0
- package/build/tools/process-security.js +784 -0
- package/build/tools/reporting.d.ts +11 -0
- package/build/tools/reporting.d.ts.map +1 -0
- package/build/tools/reporting.js +559 -0
- package/build/tools/secrets.d.ts +9 -0
- package/build/tools/secrets.d.ts.map +1 -0
- package/build/tools/secrets.js +596 -0
- package/build/tools/siem-integration.d.ts +18 -0
- package/build/tools/siem-integration.d.ts.map +1 -0
- package/build/tools/siem-integration.js +754 -0
- package/build/tools/sudo-management.d.ts +18 -0
- package/build/tools/sudo-management.d.ts.map +1 -0
- package/build/tools/sudo-management.js +737 -0
- package/build/tools/supply-chain-security.d.ts +8 -0
- package/build/tools/supply-chain-security.d.ts.map +1 -0
- package/build/tools/supply-chain-security.js +256 -0
- package/build/tools/threat-intel.d.ts +22 -0
- package/build/tools/threat-intel.d.ts.map +1 -0
- package/build/tools/threat-intel.js +749 -0
- package/build/tools/vulnerability-management.d.ts +11 -0
- package/build/tools/vulnerability-management.d.ts.map +1 -0
- package/build/tools/vulnerability-management.js +667 -0
- package/build/tools/waf.d.ts +12 -0
- package/build/tools/waf.d.ts.map +1 -0
- package/build/tools/waf.js +843 -0
- package/build/tools/wireless-security.d.ts +19 -0
- package/build/tools/wireless-security.d.ts.map +1 -0
- package/build/tools/wireless-security.js +826 -0
- package/build/tools/zero-trust-network.d.ts +8 -0
- package/build/tools/zero-trust-network.d.ts.map +1 -0
- package/build/tools/zero-trust-network.js +367 -0
- package/docs/SAFEGUARDS.md +518 -0
- package/docs/TOOLS-REFERENCE.md +665 -0
- package/package.json +87 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-flight Validation Engine — orchestrates the complete pre-flight
|
|
3
|
+
* validation pipeline for MCP tools.
|
|
4
|
+
*
|
|
5
|
+
* Before each tool invocation this module:
|
|
6
|
+
* 1. Resolves the tool's manifest from the {@link ToolRegistry}
|
|
7
|
+
* 2. Checks binary, Python, npm, library, and file dependencies
|
|
8
|
+
* 3. Attempts auto-installation of missing deps when enabled
|
|
9
|
+
* 4. Validates privilege requirements via {@link PrivilegeManager}
|
|
10
|
+
* 5. Returns a structured {@link PreflightResult} with pass/fail, actionable
|
|
11
|
+
* messages, and a human-readable summary
|
|
12
|
+
*
|
|
13
|
+
* Results are cached for 60 seconds to avoid redundant checks when multiple
|
|
14
|
+
* tools from the same category are invoked in sequence.
|
|
15
|
+
*
|
|
16
|
+
* @module preflight
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync } from "node:fs";
|
|
19
|
+
import { execFileSafe } from "./spawn-safe.js";
|
|
20
|
+
import { initializeRegistry, } from "./tool-registry.js";
|
|
21
|
+
import { PrivilegeManager, } from "./privilege-manager.js";
|
|
22
|
+
import { AutoInstaller } from "./auto-installer.js";
|
|
23
|
+
import { isBinaryInstalled, clearDependencyCache, } from "./dependency-validator.js";
|
|
24
|
+
import { getToolRequirementForBinary } from "./tool-dependencies.js";
|
|
25
|
+
import { SafeguardRegistry, } from "./safeguards.js";
|
|
26
|
+
// ── Python import name mapping ───────────────────────────────────────────────
|
|
27
|
+
/**
|
|
28
|
+
* Maps pip package names to their Python import names when they differ.
|
|
29
|
+
* Mirrors the mapping in {@link AutoInstaller} (auto-installer.ts).
|
|
30
|
+
*/
|
|
31
|
+
const PYTHON_IMPORT_MAP = {
|
|
32
|
+
"yara-python": "yara",
|
|
33
|
+
"python-nmap": "nmap",
|
|
34
|
+
"python-apt": "apt",
|
|
35
|
+
PyYAML: "yaml",
|
|
36
|
+
Pillow: "PIL",
|
|
37
|
+
"scikit-learn": "sklearn",
|
|
38
|
+
beautifulsoup4: "bs4",
|
|
39
|
+
"python-dateutil": "dateutil",
|
|
40
|
+
attrs: "attr",
|
|
41
|
+
};
|
|
42
|
+
// ── SECURITY (CORE-017): Module name validation ────────────────────────────
|
|
43
|
+
/** Strict pattern for valid Python module names to prevent injection */
|
|
44
|
+
const PYTHON_MODULE_NAME_RE = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
45
|
+
// ── Dependency check helpers ─────────────────────────────────────────────────
|
|
46
|
+
/**
|
|
47
|
+
* SECURITY (CORE-017): Check if a Python module is installed WITHOUT executing code.
|
|
48
|
+
*
|
|
49
|
+
* Previous approach used `python3 -c "import <module>"` which executes
|
|
50
|
+
* `__init__.py`, potentially running arbitrary code. New approach:
|
|
51
|
+
* 1. Try `pip show <package_name>` which queries metadata without execution
|
|
52
|
+
* 2. Fall back to `python3 -c "import importlib; importlib.util.find_spec('module')"`
|
|
53
|
+
* which locates modules without executing them
|
|
54
|
+
* 3. Validate module name against strict pattern before use
|
|
55
|
+
*
|
|
56
|
+
* Uses {@link PYTHON_IMPORT_MAP} to translate pip names to import names.
|
|
57
|
+
*/
|
|
58
|
+
function isPythonModuleInstalled(moduleName) {
|
|
59
|
+
const importName = PYTHON_IMPORT_MAP[moduleName] ?? moduleName.replace(/-/g, "_");
|
|
60
|
+
// SECURITY (CORE-017): Validate module name against strict pattern
|
|
61
|
+
if (!PYTHON_MODULE_NAME_RE.test(importName)) {
|
|
62
|
+
console.error(`[preflight] Rejecting invalid Python module name: '${importName}'`);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
// Strategy 1: Try pip show (queries metadata, no code execution)
|
|
66
|
+
try {
|
|
67
|
+
execFileSafe("pip3", ["show", moduleName], {
|
|
68
|
+
timeout: 5_000,
|
|
69
|
+
stdio: "pipe",
|
|
70
|
+
});
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// pip3 not available or package not found by pip name; try pip
|
|
75
|
+
try {
|
|
76
|
+
execFileSafe("pip", ["show", moduleName], {
|
|
77
|
+
timeout: 5_000,
|
|
78
|
+
stdio: "pipe",
|
|
79
|
+
});
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Fall through to Strategy 2
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Strategy 2: Use importlib.util.find_spec() which finds without executing
|
|
87
|
+
try {
|
|
88
|
+
execFileSafe("python3", ["-c", `import importlib; exit(0 if importlib.util.find_spec('${importName}') else 1)`], {
|
|
89
|
+
timeout: 5_000,
|
|
90
|
+
stdio: "pipe",
|
|
91
|
+
});
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Check if an npm package is globally installed (binary on PATH).
|
|
100
|
+
*/
|
|
101
|
+
function isNpmPackageInstalled(packageName) {
|
|
102
|
+
try {
|
|
103
|
+
execFileSafe("which", [packageName], {
|
|
104
|
+
timeout: 5_000,
|
|
105
|
+
stdio: "pipe",
|
|
106
|
+
});
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Check if a system library is available via `pkg-config` or `ldconfig`.
|
|
115
|
+
*/
|
|
116
|
+
function isLibraryInstalled(libName) {
|
|
117
|
+
try {
|
|
118
|
+
// Try pkg-config first
|
|
119
|
+
execFileSafe("pkg-config", ["--exists", libName], {
|
|
120
|
+
timeout: 5_000,
|
|
121
|
+
stdio: "pipe",
|
|
122
|
+
});
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
try {
|
|
127
|
+
// Fallback to ldconfig
|
|
128
|
+
const result = execFileSafe("ldconfig", ["-p"], {
|
|
129
|
+
timeout: 5_000,
|
|
130
|
+
stdio: "pipe",
|
|
131
|
+
});
|
|
132
|
+
return result.toString().includes(libName);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Generate an install hint for a missing binary using the
|
|
141
|
+
* `DEFENSIVE_TOOLS` registry from tool-dependencies.
|
|
142
|
+
*/
|
|
143
|
+
function getInstallHint(binaryName) {
|
|
144
|
+
const toolReq = getToolRequirementForBinary(binaryName);
|
|
145
|
+
if (!toolReq)
|
|
146
|
+
return undefined;
|
|
147
|
+
const pkg = toolReq.packages.debian ?? toolReq.packages.fallback ?? binaryName;
|
|
148
|
+
return `sudo apt-get install -y ${pkg}`;
|
|
149
|
+
}
|
|
150
|
+
// ── PreflightEngine ──────────────────────────────────────────────────────────
|
|
151
|
+
/**
|
|
152
|
+
* Central orchestration engine for the pre-flight validation pipeline.
|
|
153
|
+
*
|
|
154
|
+
* Singleton — obtain via {@link PreflightEngine.instance}.
|
|
155
|
+
*
|
|
156
|
+
* The main entry point is {@link runPreflight}, which executes the full
|
|
157
|
+
* dependency → auto-install → privilege check pipeline and returns a
|
|
158
|
+
* structured {@link PreflightResult}.
|
|
159
|
+
*/
|
|
160
|
+
export class PreflightEngine {
|
|
161
|
+
registry;
|
|
162
|
+
privilegeManager;
|
|
163
|
+
autoInstaller;
|
|
164
|
+
/**
|
|
165
|
+
* Dependency cache — keyed by tool name only, 60s TTL.
|
|
166
|
+
* Covers: binary existence, privilege checks, auto-install results.
|
|
167
|
+
* Cached regardless of params (dependency results don't depend on runtime params).
|
|
168
|
+
*/
|
|
169
|
+
resultCache;
|
|
170
|
+
static CACHE_TTL = 60_000; // 60 seconds
|
|
171
|
+
static _instance = null;
|
|
172
|
+
constructor() {
|
|
173
|
+
this.registry = initializeRegistry();
|
|
174
|
+
this.privilegeManager = PrivilegeManager.instance();
|
|
175
|
+
this.autoInstaller = AutoInstaller.instance();
|
|
176
|
+
this.resultCache = new Map();
|
|
177
|
+
}
|
|
178
|
+
/** Get or create the singleton instance. */
|
|
179
|
+
static instance() {
|
|
180
|
+
if (!PreflightEngine._instance) {
|
|
181
|
+
PreflightEngine._instance = new PreflightEngine();
|
|
182
|
+
}
|
|
183
|
+
return PreflightEngine._instance;
|
|
184
|
+
}
|
|
185
|
+
// ── Main entry point ───────────────────────────────────────────────────
|
|
186
|
+
/**
|
|
187
|
+
* Run the full pre-flight validation pipeline for a tool.
|
|
188
|
+
*
|
|
189
|
+
* 1. Check cache — return early for valid passing results
|
|
190
|
+
* 2. Resolve the tool's manifest from the registry
|
|
191
|
+
* 3. Check all dependency types (binary, Python, npm, library, file)
|
|
192
|
+
* 4. Auto-install missing required deps when enabled
|
|
193
|
+
* 5. Validate privilege requirements (sudo, capabilities)
|
|
194
|
+
* 6. Determine overall pass/fail and generate summary
|
|
195
|
+
* 7. Cache and return the result
|
|
196
|
+
*/
|
|
197
|
+
async runPreflight(toolName, params) {
|
|
198
|
+
const startTime = Date.now();
|
|
199
|
+
// ── Step 1: Check dependency cache ──────────────────────────────────
|
|
200
|
+
// Dependency results (binary checks, privilege checks) are cached
|
|
201
|
+
// regardless of params — they don't depend on runtime parameters.
|
|
202
|
+
// Safeguard checks have their own 15s cache in SafeguardRegistry.
|
|
203
|
+
const depCached = this.resultCache.get(toolName);
|
|
204
|
+
const depCacheValid = depCached && depCached.expiry > Date.now() && depCached.result.passed;
|
|
205
|
+
// If we have a valid dep cache AND no params to safeguard-check, return early
|
|
206
|
+
if (depCacheValid && !params) {
|
|
207
|
+
return depCached.result;
|
|
208
|
+
}
|
|
209
|
+
// ── Step 2: Get manifest ───────────────────────────────────────────
|
|
210
|
+
const manifest = this.registry.getManifest(toolName);
|
|
211
|
+
if (!manifest) {
|
|
212
|
+
// No manifest found — pass with a warning
|
|
213
|
+
const result = {
|
|
214
|
+
toolName,
|
|
215
|
+
passed: true,
|
|
216
|
+
timestamp: Date.now(),
|
|
217
|
+
duration: Date.now() - startTime,
|
|
218
|
+
dependencies: {
|
|
219
|
+
checked: [],
|
|
220
|
+
missing: [],
|
|
221
|
+
installed: [],
|
|
222
|
+
warnings: [
|
|
223
|
+
"Tool not registered in manifest — skipping pre-flight",
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
privileges: {
|
|
227
|
+
satisfied: true,
|
|
228
|
+
issues: [],
|
|
229
|
+
recommendations: [],
|
|
230
|
+
},
|
|
231
|
+
summary: "",
|
|
232
|
+
errors: [],
|
|
233
|
+
warnings: [
|
|
234
|
+
"Tool not registered in manifest — skipping pre-flight",
|
|
235
|
+
],
|
|
236
|
+
};
|
|
237
|
+
result.summary = this.formatSummary(result);
|
|
238
|
+
this.cacheResult(toolName, result);
|
|
239
|
+
console.error(`[preflight] ⚠ No manifest for '${toolName}' — skipping (${Date.now() - startTime}ms)`);
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
// ── Step 3–4: Use cached dep results or run fresh checks ───────────
|
|
243
|
+
let dependencies;
|
|
244
|
+
let privileges;
|
|
245
|
+
if (depCacheValid) {
|
|
246
|
+
// Reuse cached dependency + privilege results
|
|
247
|
+
dependencies = depCached.result.dependencies;
|
|
248
|
+
privileges = depCached.result.privileges;
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
console.error(`[preflight] Running pre-flight for '${toolName}'...`);
|
|
252
|
+
dependencies = await this.checkDependencies(manifest);
|
|
253
|
+
const privResult = await this.checkPrivileges(manifest);
|
|
254
|
+
privileges = {
|
|
255
|
+
satisfied: privResult.satisfied,
|
|
256
|
+
issues: privResult.issues,
|
|
257
|
+
recommendations: privResult.recommendations,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
// ── Step 5: Determine overall pass/fail ────────────────────────────
|
|
261
|
+
const errors = [];
|
|
262
|
+
const warnings = [...dependencies.warnings];
|
|
263
|
+
// FAIL if any required dependency is still missing after install attempts
|
|
264
|
+
if (dependencies.missing.length > 0) {
|
|
265
|
+
for (const dep of dependencies.missing) {
|
|
266
|
+
const hint = dep.type === "binary" ? getInstallHint(dep.name) : undefined;
|
|
267
|
+
errors.push(`Missing required ${dep.type}: '${dep.name}'` +
|
|
268
|
+
(hint ? ` — Install with: ${hint}` : ""));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// FAIL if privilege issues of type 'sudo-required' or 'sudo-unavailable'
|
|
272
|
+
// (but NOT for 'conditional' sudo — those generate recommendations, not issues)
|
|
273
|
+
const blockingPrivilegeIssues = privileges.issues.filter((i) => i.type === "sudo-required" || i.type === "sudo-unavailable");
|
|
274
|
+
if (blockingPrivilegeIssues.length > 0) {
|
|
275
|
+
for (const issue of blockingPrivilegeIssues) {
|
|
276
|
+
errors.push(issue.description);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Add non-blocking privilege issues as warnings
|
|
280
|
+
const nonBlockingPrivilegeIssues = privileges.issues.filter((i) => i.type !== "sudo-required" && i.type !== "sudo-unavailable");
|
|
281
|
+
for (const issue of nonBlockingPrivilegeIssues) {
|
|
282
|
+
warnings.push(issue.description);
|
|
283
|
+
}
|
|
284
|
+
// Add privilege recommendations as warnings
|
|
285
|
+
for (const rec of privileges.recommendations) {
|
|
286
|
+
warnings.push(rec);
|
|
287
|
+
}
|
|
288
|
+
// ── Step 6: Safeguard checks (have own 15s cache via SafeguardRegistry) ─
|
|
289
|
+
let safeguardResult;
|
|
290
|
+
if (params) {
|
|
291
|
+
try {
|
|
292
|
+
const safeguardRegistry = SafeguardRegistry.getInstance();
|
|
293
|
+
safeguardResult = await safeguardRegistry.checkSafety(toolName, params);
|
|
294
|
+
// Add safeguard warnings to the overall warnings
|
|
295
|
+
if (safeguardResult.warnings.length > 0) {
|
|
296
|
+
warnings.push(...safeguardResult.warnings);
|
|
297
|
+
}
|
|
298
|
+
// Add safeguard blockers to the overall errors
|
|
299
|
+
if (!safeguardResult.safe) {
|
|
300
|
+
for (const blocker of safeguardResult.blockers) {
|
|
301
|
+
errors.push(blocker);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
console.error(`[preflight] Safeguards: ${safeguardResult.blockers.length} blocker(s), ` +
|
|
305
|
+
`${safeguardResult.warnings.length} warning(s)`);
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
console.error(`[preflight] ⚠ Safeguard check failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
309
|
+
// Safeguard failure is non-blocking — log and continue
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const passed = errors.length === 0;
|
|
313
|
+
const duration = Date.now() - startTime;
|
|
314
|
+
const result = {
|
|
315
|
+
toolName,
|
|
316
|
+
passed,
|
|
317
|
+
timestamp: Date.now(),
|
|
318
|
+
duration,
|
|
319
|
+
dependencies,
|
|
320
|
+
privileges,
|
|
321
|
+
safeguards: safeguardResult
|
|
322
|
+
? {
|
|
323
|
+
safe: safeguardResult.safe,
|
|
324
|
+
blockers: safeguardResult.blockers,
|
|
325
|
+
warnings: safeguardResult.warnings,
|
|
326
|
+
impactedApps: safeguardResult.impactedApps,
|
|
327
|
+
}
|
|
328
|
+
: undefined,
|
|
329
|
+
summary: "",
|
|
330
|
+
errors,
|
|
331
|
+
warnings,
|
|
332
|
+
};
|
|
333
|
+
// ── Step 7: Generate summary, cache, log ───────────────────────────
|
|
334
|
+
result.summary = this.formatSummary(result);
|
|
335
|
+
this.cacheResult(toolName, result);
|
|
336
|
+
// Log to stderr
|
|
337
|
+
const depCount = dependencies.checked.filter((c) => c.required).length;
|
|
338
|
+
const missingCount = dependencies.missing.length;
|
|
339
|
+
console.error(`[preflight] Dependencies: ${depCount} checked, ${missingCount} missing`);
|
|
340
|
+
if (manifest.sudo !== "never") {
|
|
341
|
+
const sudoStatus = privileges.satisfied
|
|
342
|
+
? "session active ✓"
|
|
343
|
+
: `${privileges.issues.length} issue(s)`;
|
|
344
|
+
console.error(`[preflight] Privileges: sudo ${manifest.sudo} — ${sudoStatus}`);
|
|
345
|
+
}
|
|
346
|
+
console.error(`[preflight] ${passed ? "✓" : "✗"} Pre-flight ${passed ? "passed" : "FAILED"} (${duration}ms)`);
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
// ── Individual check phases ────────────────────────────────────────────
|
|
350
|
+
/**
|
|
351
|
+
* Check all dependency types for a tool manifest.
|
|
352
|
+
*
|
|
353
|
+
* Checks binaries, Python modules, npm packages, system libraries,
|
|
354
|
+
* and required files. If any required dependency is missing and
|
|
355
|
+
* auto-install is enabled, attempts installation via {@link AutoInstaller}.
|
|
356
|
+
*/
|
|
357
|
+
async checkDependencies(manifest) {
|
|
358
|
+
const checked = [];
|
|
359
|
+
const warnings = [];
|
|
360
|
+
// 1. Required binaries — via isBinaryInstalled from dependency-validator
|
|
361
|
+
for (const bin of manifest.requiredBinaries) {
|
|
362
|
+
const found = await isBinaryInstalled(bin);
|
|
363
|
+
checked.push({ name: bin, type: "binary", required: true, found });
|
|
364
|
+
}
|
|
365
|
+
// 2. Optional binaries — same check, required=false
|
|
366
|
+
for (const bin of manifest.optionalBinaries ?? []) {
|
|
367
|
+
const found = await isBinaryInstalled(bin);
|
|
368
|
+
checked.push({ name: bin, type: "binary", required: false, found });
|
|
369
|
+
if (!found) {
|
|
370
|
+
warnings.push(`Optional dependency '${bin}' (binary) not found`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// 3. Required Python modules — via python3 -c "import <module>"
|
|
374
|
+
for (const mod of manifest.requiredPythonModules ?? []) {
|
|
375
|
+
const found = isPythonModuleInstalled(mod);
|
|
376
|
+
checked.push({
|
|
377
|
+
name: mod,
|
|
378
|
+
type: "python-module",
|
|
379
|
+
required: true,
|
|
380
|
+
found,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
// 4. Optional Python modules
|
|
384
|
+
for (const mod of manifest.optionalPythonModules ?? []) {
|
|
385
|
+
const found = isPythonModuleInstalled(mod);
|
|
386
|
+
checked.push({
|
|
387
|
+
name: mod,
|
|
388
|
+
type: "python-module",
|
|
389
|
+
required: false,
|
|
390
|
+
found,
|
|
391
|
+
});
|
|
392
|
+
if (!found) {
|
|
393
|
+
warnings.push(`Optional dependency '${mod}' (python-module) not found`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// 5. Required npm packages — via which <package>
|
|
397
|
+
for (const pkg of manifest.requiredNpmPackages ?? []) {
|
|
398
|
+
const found = isNpmPackageInstalled(pkg);
|
|
399
|
+
checked.push({
|
|
400
|
+
name: pkg,
|
|
401
|
+
type: "npm-package",
|
|
402
|
+
required: true,
|
|
403
|
+
found,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
// 6. Optional npm packages
|
|
407
|
+
for (const pkg of manifest.optionalNpmPackages ?? []) {
|
|
408
|
+
const found = isNpmPackageInstalled(pkg);
|
|
409
|
+
checked.push({
|
|
410
|
+
name: pkg,
|
|
411
|
+
type: "npm-package",
|
|
412
|
+
required: false,
|
|
413
|
+
found,
|
|
414
|
+
});
|
|
415
|
+
if (!found) {
|
|
416
|
+
warnings.push(`Optional dependency '${pkg}' (npm-package) not found`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// 7. Required libraries — via pkg-config --exists or ldconfig -p | grep
|
|
420
|
+
for (const lib of manifest.requiredLibraries ?? []) {
|
|
421
|
+
const found = isLibraryInstalled(lib);
|
|
422
|
+
checked.push({ name: lib, type: "library", required: true, found });
|
|
423
|
+
}
|
|
424
|
+
// 8. Required files — via fs.existsSync
|
|
425
|
+
for (const filePath of manifest.requiredFiles ?? []) {
|
|
426
|
+
const found = existsSync(filePath);
|
|
427
|
+
checked.push({ name: filePath, type: "file", required: true, found });
|
|
428
|
+
}
|
|
429
|
+
// ── Auto-install missing required deps ───────────────────────────────
|
|
430
|
+
const missingRequired = checked.filter((c) => c.required && !c.found);
|
|
431
|
+
if (missingRequired.length > 0 && this.autoInstaller.isEnabled()) {
|
|
432
|
+
const missingBinaries = missingRequired
|
|
433
|
+
.filter((c) => c.type === "binary")
|
|
434
|
+
.map((c) => c.name);
|
|
435
|
+
const missingPython = missingRequired
|
|
436
|
+
.filter((c) => c.type === "python-module")
|
|
437
|
+
.map((c) => c.name);
|
|
438
|
+
const missingNpm = missingRequired
|
|
439
|
+
.filter((c) => c.type === "npm-package")
|
|
440
|
+
.map((c) => c.name);
|
|
441
|
+
const missingLibraries = missingRequired
|
|
442
|
+
.filter((c) => c.type === "library")
|
|
443
|
+
.map((c) => c.name);
|
|
444
|
+
const installResult = await this.autoInstaller.resolveAll(manifest, missingBinaries, missingPython.length > 0 ? missingPython : undefined, missingNpm.length > 0 ? missingNpm : undefined, missingLibraries.length > 0 ? missingLibraries : undefined);
|
|
445
|
+
// Clear dependency cache so re-checks hit disk
|
|
446
|
+
clearDependencyCache();
|
|
447
|
+
// Re-check previously missing deps after install attempts
|
|
448
|
+
for (const check of checked) {
|
|
449
|
+
if (check.required && !check.found) {
|
|
450
|
+
let nowFound = false;
|
|
451
|
+
switch (check.type) {
|
|
452
|
+
case "binary":
|
|
453
|
+
nowFound = await isBinaryInstalled(check.name);
|
|
454
|
+
break;
|
|
455
|
+
case "python-module":
|
|
456
|
+
nowFound = isPythonModuleInstalled(check.name);
|
|
457
|
+
break;
|
|
458
|
+
case "npm-package":
|
|
459
|
+
nowFound = isNpmPackageInstalled(check.name);
|
|
460
|
+
break;
|
|
461
|
+
case "library":
|
|
462
|
+
nowFound = isLibraryInstalled(check.name);
|
|
463
|
+
break;
|
|
464
|
+
case "file":
|
|
465
|
+
nowFound = existsSync(check.name);
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
if (nowFound) {
|
|
469
|
+
check.found = true;
|
|
470
|
+
check.autoInstalled = true;
|
|
471
|
+
// Match to the install attempt for the user-facing message
|
|
472
|
+
const attempt = installResult.attempted.find((a) => a.dependency === check.name);
|
|
473
|
+
check.installMessage = attempt?.message;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const missing = checked.filter((c) => c.required && !c.found);
|
|
479
|
+
const installed = checked.filter((c) => c.autoInstalled === true);
|
|
480
|
+
return { checked, missing, installed, warnings };
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Check privilege requirements for a tool manifest.
|
|
484
|
+
* Delegates to {@link PrivilegeManager.checkForTool}.
|
|
485
|
+
*/
|
|
486
|
+
async checkPrivileges(manifest) {
|
|
487
|
+
const result = await this.privilegeManager.checkForTool(manifest);
|
|
488
|
+
return {
|
|
489
|
+
satisfied: result.satisfied,
|
|
490
|
+
issues: result.issues,
|
|
491
|
+
recommendations: result.recommendations,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
// ── Summary formatting ─────────────────────────────────────────────────
|
|
495
|
+
/**
|
|
496
|
+
* Generate a human-readable summary of the pre-flight result.
|
|
497
|
+
*
|
|
498
|
+
* @example Passing
|
|
499
|
+
* ```
|
|
500
|
+
* ✅ Pre-flight passed for 'firewall_iptables_list'
|
|
501
|
+
* Dependencies: 2/2 available (iptables, ip6tables)
|
|
502
|
+
* Privileges: sudo session active
|
|
503
|
+
* Ready to execute.
|
|
504
|
+
* ```
|
|
505
|
+
*
|
|
506
|
+
* @example Failing
|
|
507
|
+
* ```
|
|
508
|
+
* ❌ Pre-flight FAILED for 'compliance_oscap_scan'
|
|
509
|
+
* Missing dependencies:
|
|
510
|
+
* • oscap (binary) — Install with: sudo apt-get install -y libopenscap8
|
|
511
|
+
* Privilege issues:
|
|
512
|
+
* • Root access required for OpenSCAP scanning
|
|
513
|
+
* → Run 'sudo_elevate' tool first to provide credentials
|
|
514
|
+
* Cannot proceed until issues are resolved.
|
|
515
|
+
* ```
|
|
516
|
+
*/
|
|
517
|
+
formatSummary(result) {
|
|
518
|
+
const lines = [];
|
|
519
|
+
if (result.passed) {
|
|
520
|
+
// ── Passing summary ──────────────────────────────────────────────
|
|
521
|
+
const autoInstalledCount = result.dependencies.installed.length;
|
|
522
|
+
const autoNote = autoInstalledCount > 0
|
|
523
|
+
? ` (auto-installed ${autoInstalledCount} ${autoInstalledCount === 1 ? "dependency" : "dependencies"})`
|
|
524
|
+
: "";
|
|
525
|
+
lines.push(`✅ Pre-flight passed for '${result.toolName}'${autoNote}`);
|
|
526
|
+
// Dependencies line
|
|
527
|
+
const requiredChecks = result.dependencies.checked.filter((c) => c.required);
|
|
528
|
+
const requiredFound = requiredChecks.filter((c) => c.found);
|
|
529
|
+
if (requiredChecks.length > 0) {
|
|
530
|
+
const names = requiredFound.map((c) => {
|
|
531
|
+
if (c.autoInstalled && c.installMessage) {
|
|
532
|
+
return `${c.name} — ${c.installMessage}`;
|
|
533
|
+
}
|
|
534
|
+
if (c.autoInstalled) {
|
|
535
|
+
return `${c.name} — auto-installed`;
|
|
536
|
+
}
|
|
537
|
+
return c.name;
|
|
538
|
+
});
|
|
539
|
+
lines.push(` Dependencies: ${requiredFound.length}/${requiredChecks.length} available` +
|
|
540
|
+
(names.length > 0 ? ` (${names.join(", ")})` : ""));
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
lines.push(" Dependencies: none required");
|
|
544
|
+
}
|
|
545
|
+
// Privileges line
|
|
546
|
+
const manifest = this.registry.getManifest(result.toolName);
|
|
547
|
+
if (manifest && manifest.sudo !== "never") {
|
|
548
|
+
if (result.privileges.satisfied) {
|
|
549
|
+
lines.push(" Privileges: sudo session active");
|
|
550
|
+
}
|
|
551
|
+
else if (result.privileges.recommendations.length > 0) {
|
|
552
|
+
lines.push(` Privileges: ${result.privileges.recommendations[0]}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
lines.push(" Privileges: no elevation required");
|
|
557
|
+
}
|
|
558
|
+
lines.push(" Ready to execute.");
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
// ── Failing summary ──────────────────────────────────────────────
|
|
562
|
+
lines.push(`❌ Pre-flight FAILED for '${result.toolName}'`);
|
|
563
|
+
// Missing dependencies
|
|
564
|
+
if (result.dependencies.missing.length > 0) {
|
|
565
|
+
lines.push(" Missing dependencies:");
|
|
566
|
+
for (const dep of result.dependencies.missing) {
|
|
567
|
+
const hint = dep.type === "binary" ? getInstallHint(dep.name) : undefined;
|
|
568
|
+
lines.push(` • ${dep.name} (${dep.type})` +
|
|
569
|
+
(hint ? ` — Install with: ${hint}` : ""));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
// Privilege issues (blocking)
|
|
573
|
+
const blockingIssues = result.privileges.issues.filter((i) => i.type === "sudo-required" || i.type === "sudo-unavailable");
|
|
574
|
+
if (blockingIssues.length > 0) {
|
|
575
|
+
lines.push(" Privilege issues:");
|
|
576
|
+
for (const issue of blockingIssues) {
|
|
577
|
+
lines.push(` • ${issue.description}`);
|
|
578
|
+
lines.push(` → ${issue.resolution}`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
// Safeguard blockers
|
|
582
|
+
if (result.safeguards &&
|
|
583
|
+
result.safeguards.blockers.length > 0) {
|
|
584
|
+
lines.push(" Safety blockers:");
|
|
585
|
+
for (const blocker of result.safeguards.blockers) {
|
|
586
|
+
lines.push(` 🛑 ${blocker}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
lines.push(" Cannot proceed until issues are resolved.");
|
|
590
|
+
}
|
|
591
|
+
return lines.join("\n");
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Generate a shorter status message for prepending to tool output.
|
|
595
|
+
*
|
|
596
|
+
* - Passed (no issues): `"[pre-flight ✓] All checks passed (2 deps, sudo active)"`
|
|
597
|
+
* - Passed (warnings): `"[pre-flight ✓] Passed with warnings: optional dep 'nmap' not found"`
|
|
598
|
+
* - Failed: returns the full error summary from {@link formatSummary}
|
|
599
|
+
*/
|
|
600
|
+
formatStatusMessage(result) {
|
|
601
|
+
if (!result.passed) {
|
|
602
|
+
// Failed — return full summary
|
|
603
|
+
return this.formatSummary(result);
|
|
604
|
+
}
|
|
605
|
+
const requiredDeps = result.dependencies.checked.filter((c) => c.required);
|
|
606
|
+
const depCount = requiredDeps.length;
|
|
607
|
+
// Determine privilege status string
|
|
608
|
+
const manifest = this.registry.getManifest(result.toolName);
|
|
609
|
+
const needsSudo = manifest != null && manifest.sudo !== "never";
|
|
610
|
+
const privStatus = needsSudo ? ", sudo active" : "";
|
|
611
|
+
// Check for optional missing deps to include as warnings
|
|
612
|
+
const optionalMissing = result.dependencies.checked.filter((c) => !c.required && !c.found);
|
|
613
|
+
if (optionalMissing.length > 0) {
|
|
614
|
+
const missingNames = optionalMissing
|
|
615
|
+
.map((c) => c.name)
|
|
616
|
+
.join("', '");
|
|
617
|
+
return `[pre-flight ✓] Passed with warnings: optional dep '${missingNames}' not found`;
|
|
618
|
+
}
|
|
619
|
+
return `[pre-flight ✓] All checks passed (${depCount} deps${privStatus})`;
|
|
620
|
+
}
|
|
621
|
+
// ── Cache management ───────────────────────────────────────────────────
|
|
622
|
+
/**
|
|
623
|
+
* Clear the result cache.
|
|
624
|
+
* Call after installs, privilege changes, or any event that invalidates
|
|
625
|
+
* previous pre-flight results.
|
|
626
|
+
*/
|
|
627
|
+
clearCache() {
|
|
628
|
+
this.resultCache.clear();
|
|
629
|
+
}
|
|
630
|
+
// ── Private helpers ────────────────────────────────────────────────────
|
|
631
|
+
/** Store a result in the cache with TTL. */
|
|
632
|
+
cacheResult(toolName, result) {
|
|
633
|
+
this.resultCache.set(toolName, {
|
|
634
|
+
result,
|
|
635
|
+
expiry: Date.now() + PreflightEngine.CACHE_TTL,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|