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.
Files changed (186) hide show
  1. package/CHANGELOG.md +471 -0
  2. package/LICENSE +21 -0
  3. package/README.md +242 -0
  4. package/build/core/auto-installer.d.ts +102 -0
  5. package/build/core/auto-installer.d.ts.map +1 -0
  6. package/build/core/auto-installer.js +833 -0
  7. package/build/core/backup-manager.d.ts +63 -0
  8. package/build/core/backup-manager.d.ts.map +1 -0
  9. package/build/core/backup-manager.js +189 -0
  10. package/build/core/changelog.d.ts +75 -0
  11. package/build/core/changelog.d.ts.map +1 -0
  12. package/build/core/changelog.js +123 -0
  13. package/build/core/command-allowlist.d.ts +129 -0
  14. package/build/core/command-allowlist.d.ts.map +1 -0
  15. package/build/core/command-allowlist.js +849 -0
  16. package/build/core/config.d.ts +79 -0
  17. package/build/core/config.d.ts.map +1 -0
  18. package/build/core/config.js +193 -0
  19. package/build/core/dependency-validator.d.ts +106 -0
  20. package/build/core/dependency-validator.d.ts.map +1 -0
  21. package/build/core/dependency-validator.js +405 -0
  22. package/build/core/distro-adapter.d.ts +177 -0
  23. package/build/core/distro-adapter.d.ts.map +1 -0
  24. package/build/core/distro-adapter.js +481 -0
  25. package/build/core/distro.d.ts +68 -0
  26. package/build/core/distro.d.ts.map +1 -0
  27. package/build/core/distro.js +457 -0
  28. package/build/core/encrypted-state.d.ts +76 -0
  29. package/build/core/encrypted-state.d.ts.map +1 -0
  30. package/build/core/encrypted-state.js +209 -0
  31. package/build/core/executor.d.ts +56 -0
  32. package/build/core/executor.d.ts.map +1 -0
  33. package/build/core/executor.js +350 -0
  34. package/build/core/installer.d.ts +92 -0
  35. package/build/core/installer.d.ts.map +1 -0
  36. package/build/core/installer.js +1072 -0
  37. package/build/core/logger.d.ts +102 -0
  38. package/build/core/logger.d.ts.map +1 -0
  39. package/build/core/logger.js +132 -0
  40. package/build/core/parsers.d.ts +151 -0
  41. package/build/core/parsers.d.ts.map +1 -0
  42. package/build/core/parsers.js +479 -0
  43. package/build/core/policy-engine.d.ts +170 -0
  44. package/build/core/policy-engine.d.ts.map +1 -0
  45. package/build/core/policy-engine.js +656 -0
  46. package/build/core/preflight.d.ts +157 -0
  47. package/build/core/preflight.d.ts.map +1 -0
  48. package/build/core/preflight.js +638 -0
  49. package/build/core/privilege-manager.d.ts +108 -0
  50. package/build/core/privilege-manager.d.ts.map +1 -0
  51. package/build/core/privilege-manager.js +363 -0
  52. package/build/core/rate-limiter.d.ts +67 -0
  53. package/build/core/rate-limiter.d.ts.map +1 -0
  54. package/build/core/rate-limiter.js +129 -0
  55. package/build/core/rollback.d.ts +73 -0
  56. package/build/core/rollback.d.ts.map +1 -0
  57. package/build/core/rollback.js +278 -0
  58. package/build/core/safeguards.d.ts +58 -0
  59. package/build/core/safeguards.d.ts.map +1 -0
  60. package/build/core/safeguards.js +448 -0
  61. package/build/core/sanitizer.d.ts +118 -0
  62. package/build/core/sanitizer.d.ts.map +1 -0
  63. package/build/core/sanitizer.js +459 -0
  64. package/build/core/secure-fs.d.ts +67 -0
  65. package/build/core/secure-fs.d.ts.map +1 -0
  66. package/build/core/secure-fs.js +143 -0
  67. package/build/core/spawn-safe.d.ts +55 -0
  68. package/build/core/spawn-safe.d.ts.map +1 -0
  69. package/build/core/spawn-safe.js +146 -0
  70. package/build/core/sudo-guard.d.ts +145 -0
  71. package/build/core/sudo-guard.d.ts.map +1 -0
  72. package/build/core/sudo-guard.js +349 -0
  73. package/build/core/sudo-session.d.ts +100 -0
  74. package/build/core/sudo-session.d.ts.map +1 -0
  75. package/build/core/sudo-session.js +319 -0
  76. package/build/core/tool-dependencies.d.ts +61 -0
  77. package/build/core/tool-dependencies.d.ts.map +1 -0
  78. package/build/core/tool-dependencies.js +571 -0
  79. package/build/core/tool-registry.d.ts +111 -0
  80. package/build/core/tool-registry.d.ts.map +1 -0
  81. package/build/core/tool-registry.js +656 -0
  82. package/build/core/tool-wrapper.d.ts +73 -0
  83. package/build/core/tool-wrapper.d.ts.map +1 -0
  84. package/build/core/tool-wrapper.js +296 -0
  85. package/build/index.d.ts +3 -0
  86. package/build/index.d.ts.map +1 -0
  87. package/build/index.js +247 -0
  88. package/build/tools/access-control.d.ts +9 -0
  89. package/build/tools/access-control.d.ts.map +1 -0
  90. package/build/tools/access-control.js +1818 -0
  91. package/build/tools/api-security.d.ts +12 -0
  92. package/build/tools/api-security.d.ts.map +1 -0
  93. package/build/tools/api-security.js +901 -0
  94. package/build/tools/app-hardening.d.ts +11 -0
  95. package/build/tools/app-hardening.d.ts.map +1 -0
  96. package/build/tools/app-hardening.js +768 -0
  97. package/build/tools/backup.d.ts +8 -0
  98. package/build/tools/backup.d.ts.map +1 -0
  99. package/build/tools/backup.js +381 -0
  100. package/build/tools/cloud-security.d.ts +17 -0
  101. package/build/tools/cloud-security.d.ts.map +1 -0
  102. package/build/tools/cloud-security.js +739 -0
  103. package/build/tools/compliance.d.ts +10 -0
  104. package/build/tools/compliance.d.ts.map +1 -0
  105. package/build/tools/compliance.js +1225 -0
  106. package/build/tools/container-security.d.ts +14 -0
  107. package/build/tools/container-security.d.ts.map +1 -0
  108. package/build/tools/container-security.js +788 -0
  109. package/build/tools/deception.d.ts +13 -0
  110. package/build/tools/deception.d.ts.map +1 -0
  111. package/build/tools/deception.js +763 -0
  112. package/build/tools/dns-security.d.ts +93 -0
  113. package/build/tools/dns-security.d.ts.map +1 -0
  114. package/build/tools/dns-security.js +745 -0
  115. package/build/tools/drift-detection.d.ts +8 -0
  116. package/build/tools/drift-detection.d.ts.map +1 -0
  117. package/build/tools/drift-detection.js +326 -0
  118. package/build/tools/ebpf-security.d.ts +15 -0
  119. package/build/tools/ebpf-security.d.ts.map +1 -0
  120. package/build/tools/ebpf-security.js +294 -0
  121. package/build/tools/encryption.d.ts +9 -0
  122. package/build/tools/encryption.d.ts.map +1 -0
  123. package/build/tools/encryption.js +1667 -0
  124. package/build/tools/firewall.d.ts +9 -0
  125. package/build/tools/firewall.d.ts.map +1 -0
  126. package/build/tools/firewall.js +1398 -0
  127. package/build/tools/hardening.d.ts +10 -0
  128. package/build/tools/hardening.d.ts.map +1 -0
  129. package/build/tools/hardening.js +2654 -0
  130. package/build/tools/ids.d.ts +9 -0
  131. package/build/tools/ids.d.ts.map +1 -0
  132. package/build/tools/ids.js +624 -0
  133. package/build/tools/incident-response.d.ts +10 -0
  134. package/build/tools/incident-response.d.ts.map +1 -0
  135. package/build/tools/incident-response.js +1180 -0
  136. package/build/tools/logging.d.ts +12 -0
  137. package/build/tools/logging.d.ts.map +1 -0
  138. package/build/tools/logging.js +454 -0
  139. package/build/tools/malware.d.ts +10 -0
  140. package/build/tools/malware.d.ts.map +1 -0
  141. package/build/tools/malware.js +532 -0
  142. package/build/tools/meta.d.ts +11 -0
  143. package/build/tools/meta.d.ts.map +1 -0
  144. package/build/tools/meta.js +2278 -0
  145. package/build/tools/network-defense.d.ts +12 -0
  146. package/build/tools/network-defense.d.ts.map +1 -0
  147. package/build/tools/network-defense.js +760 -0
  148. package/build/tools/patch-management.d.ts +3 -0
  149. package/build/tools/patch-management.d.ts.map +1 -0
  150. package/build/tools/patch-management.js +708 -0
  151. package/build/tools/process-security.d.ts +12 -0
  152. package/build/tools/process-security.d.ts.map +1 -0
  153. package/build/tools/process-security.js +784 -0
  154. package/build/tools/reporting.d.ts +11 -0
  155. package/build/tools/reporting.d.ts.map +1 -0
  156. package/build/tools/reporting.js +559 -0
  157. package/build/tools/secrets.d.ts +9 -0
  158. package/build/tools/secrets.d.ts.map +1 -0
  159. package/build/tools/secrets.js +596 -0
  160. package/build/tools/siem-integration.d.ts +18 -0
  161. package/build/tools/siem-integration.d.ts.map +1 -0
  162. package/build/tools/siem-integration.js +754 -0
  163. package/build/tools/sudo-management.d.ts +18 -0
  164. package/build/tools/sudo-management.d.ts.map +1 -0
  165. package/build/tools/sudo-management.js +737 -0
  166. package/build/tools/supply-chain-security.d.ts +8 -0
  167. package/build/tools/supply-chain-security.d.ts.map +1 -0
  168. package/build/tools/supply-chain-security.js +256 -0
  169. package/build/tools/threat-intel.d.ts +22 -0
  170. package/build/tools/threat-intel.d.ts.map +1 -0
  171. package/build/tools/threat-intel.js +749 -0
  172. package/build/tools/vulnerability-management.d.ts +11 -0
  173. package/build/tools/vulnerability-management.d.ts.map +1 -0
  174. package/build/tools/vulnerability-management.js +667 -0
  175. package/build/tools/waf.d.ts +12 -0
  176. package/build/tools/waf.d.ts.map +1 -0
  177. package/build/tools/waf.js +843 -0
  178. package/build/tools/wireless-security.d.ts +19 -0
  179. package/build/tools/wireless-security.d.ts.map +1 -0
  180. package/build/tools/wireless-security.js +826 -0
  181. package/build/tools/zero-trust-network.d.ts +8 -0
  182. package/build/tools/zero-trust-network.d.ts.map +1 -0
  183. package/build/tools/zero-trust-network.js +367 -0
  184. package/docs/SAFEGUARDS.md +518 -0
  185. package/docs/TOOLS-REFERENCE.md +665 -0
  186. 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
+ }