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,833 @@
1
+ /**
2
+ * AutoInstaller — multi-package-manager automatic dependency resolver.
3
+ *
4
+ * Handles installation of missing dependencies across system package managers
5
+ * (apt, dnf, yum, pacman, apk, zypper, brew), pip, and npm. This module is
6
+ * part of the pre-flight validation pipeline and is invoked when
7
+ * `KALI_DEFENSE_AUTO_INSTALL=true`.
8
+ *
9
+ * Design constraints:
10
+ * - Uses `execFileSafe` from `spawn-safe.ts` (NOT the executor) to avoid
11
+ * circular dependencies with `sudo-session`. spawn-safe enforces the
12
+ * command allowlist and `shell: false` automatically.
13
+ * - Every `execFileSafe` call is wrapped in try/catch — install failures
14
+ * must NEVER crash the server.
15
+ * - Logs exclusively to stderr (`console.error`) because the MCP server
16
+ * uses stdio for JSON-RPC transport.
17
+ *
18
+ * @module auto-installer
19
+ */
20
+ import { execFileSafe } from "./spawn-safe.js";
21
+ import { getConfig } from "./config.js";
22
+ import { detectDistro } from "./distro.js";
23
+ import { DEFENSIVE_TOOLS } from "./installer.js";
24
+ import { SudoSession } from "./sudo-session.js";
25
+ import { resolveCommand } from "./command-allowlist.js";
26
+ import { logChange, createChangeEntry } from "./changelog.js";
27
+ // ── Python import name mapping ───────────────────────────────────────────────
28
+ /**
29
+ * Maps pip package names to their Python import names when they differ.
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-008): Package allowlists for pip/npm ──────────────────────
43
+ /**
44
+ * Allowed pip packages that may be auto-installed.
45
+ * Only packages required by this project's tool manifests are permitted.
46
+ * Any package not in this set will be rejected with a logged warning.
47
+ */
48
+ const ALLOWED_PIP_PACKAGES = new Set([
49
+ // Security/malware analysis
50
+ "yara-python",
51
+ "python-nmap",
52
+ // System interaction
53
+ "python-apt",
54
+ // Data formats used by security tools
55
+ "PyYAML",
56
+ "python-dateutil",
57
+ "attrs",
58
+ ]);
59
+ /**
60
+ * Allowed npm packages that may be auto-installed globally.
61
+ * Only packages required by this project's tool manifests are permitted.
62
+ * Any package not in this set will be rejected with a logged warning.
63
+ */
64
+ const ALLOWED_NPM_PACKAGES = new Set([
65
+ // Supply chain security / SBOM generation
66
+ "cdxgen",
67
+ // Container vulnerability scanning (when installed via npm)
68
+ "snyk",
69
+ ]);
70
+ // ── Library dev-package suffix mapping per distro family ──────────────────────
71
+ const LIB_DEV_PATTERNS = {
72
+ debian: (lib) => [`lib${lib}-dev`, `lib${lib}0-dev`, `lib${lib}1-dev`],
73
+ rhel: (lib) => [`${lib}-devel`, `lib${lib}-devel`],
74
+ suse: (lib) => [`${lib}-devel`, `lib${lib}-devel`],
75
+ arch: (lib) => [lib, `lib${lib}`],
76
+ alpine: (lib) => [`${lib}-dev`, `lib${lib}-dev`],
77
+ };
78
+ // ── Approved packages allowlist ──────────────────────────────────────────────
79
+ /**
80
+ * Build a Set of all approved package names derived from DEFENSIVE_TOOLS.
81
+ * Only packages in this set may be installed by the auto-installer.
82
+ */
83
+ function buildApprovedPackages() {
84
+ const approved = new Set();
85
+ for (const tool of DEFENSIVE_TOOLS) {
86
+ const pkgs = tool.packages;
87
+ for (const value of Object.values(pkgs)) {
88
+ if (value)
89
+ approved.add(value);
90
+ }
91
+ }
92
+ return approved;
93
+ }
94
+ let _approvedPackages = null;
95
+ /** Get the lazily-built approved packages allowlist. */
96
+ function getApprovedPackages() {
97
+ if (!_approvedPackages) {
98
+ _approvedPackages = buildApprovedPackages();
99
+ }
100
+ return _approvedPackages;
101
+ }
102
+ // ── Package name validation ──────────────────────────────────────────────────
103
+ /**
104
+ * Validate that a package name contains only safe characters.
105
+ * Allowed: alphanumeric, hyphens, dots, plus signs, colons (for arch qualifiers).
106
+ * No shell metacharacters, no path separators, no spaces.
107
+ * Max length: 128 characters.
108
+ */
109
+ export function validatePackageName(name) {
110
+ return /^[a-zA-Z0-9][a-zA-Z0-9.+\-:]{0,127}$/.test(name);
111
+ }
112
+ /**
113
+ * Validate a pip/npm module name for safe characters.
114
+ * Slightly more permissive than system package names — also allows underscores.
115
+ */
116
+ function validateModuleName(name) {
117
+ return /^[a-zA-Z0-9][a-zA-Z0-9._+\-]{0,127}$/.test(name);
118
+ }
119
+ // ── Helper: DEFENSIVE_TOOLS lookup by binary name ────────────────────────────
120
+ /** Build a lookup map from binary → ToolRequirement on first access. */
121
+ let _binaryLookup = null;
122
+ function getBinaryLookup() {
123
+ if (!_binaryLookup) {
124
+ _binaryLookup = new Map();
125
+ for (const tool of DEFENSIVE_TOOLS) {
126
+ _binaryLookup.set(tool.binary, tool);
127
+ }
128
+ }
129
+ return _binaryLookup;
130
+ }
131
+ // ── Helper: execute with sudo if needed ──────────────────────────────────────
132
+ function isRoot() {
133
+ return process.geteuid?.() === 0;
134
+ }
135
+ /**
136
+ * Run a command synchronously, optionally with sudo.
137
+ * Returns `{ stdout, success }`.
138
+ *
139
+ * Uses execFileSafe for allowlist enforcement and shell: false.
140
+ * The target command inside sudo args is still resolved manually
141
+ * since execFileSafe only resolves the top-level command.
142
+ */
143
+ function execWithSudo(args, options) {
144
+ const timeout = options?.timeoutMs ?? 300_000;
145
+ const needsSudo = (options?.useSudo ?? true) && !isRoot();
146
+ if (needsSudo) {
147
+ // Resolve the target command via allowlist (sudo itself is resolved by execFileSafe)
148
+ const resolvedTargetCmd = resolveCommand(args[0]);
149
+ const resolvedArgs = [resolvedTargetCmd, ...args.slice(1)];
150
+ const session = SudoSession.getInstance();
151
+ const passwordBuf = session.getPassword(); // Returns Buffer | null (a copy)
152
+ // Use -S to read password from stdin, -p '' to suppress prompt
153
+ const cmdArgs = ["-S", "-p", "", ...resolvedArgs];
154
+ let inputBuf;
155
+ if (passwordBuf) {
156
+ const newline = Buffer.from("\n");
157
+ inputBuf = Buffer.concat([passwordBuf, newline]);
158
+ passwordBuf.fill(0); // Zero the password copy immediately
159
+ }
160
+ try {
161
+ const stdout = execFileSafe("sudo", cmdArgs, {
162
+ timeout,
163
+ maxBuffer: 10 * 1024 * 1024,
164
+ encoding: "utf-8",
165
+ input: inputBuf,
166
+ stdio: inputBuf ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"],
167
+ });
168
+ return { stdout: (stdout ?? ""), success: true, stderr: "" };
169
+ }
170
+ catch (err) {
171
+ const execErr = err;
172
+ return {
173
+ stdout: execErr.stdout ?? "",
174
+ success: false,
175
+ stderr: execErr.stderr ?? String(err),
176
+ };
177
+ }
178
+ finally {
179
+ // Zero the input buffer after use regardless of success/failure
180
+ if (inputBuf)
181
+ inputBuf.fill(0);
182
+ }
183
+ }
184
+ else {
185
+ // Running as root — execute directly; execFileSafe resolves via allowlist
186
+ try {
187
+ const stdout = execFileSafe(args[0], args.slice(1), {
188
+ timeout,
189
+ maxBuffer: 10 * 1024 * 1024,
190
+ encoding: "utf-8",
191
+ stdio: ["pipe", "pipe", "pipe"],
192
+ });
193
+ return { stdout: (stdout ?? ""), success: true, stderr: "" };
194
+ }
195
+ catch (err) {
196
+ const execErr = err;
197
+ return {
198
+ stdout: execErr.stdout ?? "",
199
+ success: false,
200
+ stderr: execErr.stderr ?? String(err),
201
+ };
202
+ }
203
+ }
204
+ }
205
+ /**
206
+ * Run a command synchronously WITHOUT sudo.
207
+ * execFileSafe handles allowlist resolution and shell: false.
208
+ */
209
+ function execSimple(command, args, options) {
210
+ try {
211
+ const stdout = execFileSafe(command, args, {
212
+ timeout: options?.timeoutMs ?? 30_000,
213
+ maxBuffer: 10 * 1024 * 1024,
214
+ encoding: "utf-8",
215
+ input: options?.input,
216
+ stdio: ["pipe", "pipe", "pipe"],
217
+ });
218
+ return { stdout: (stdout ?? ""), success: true, stderr: "" };
219
+ }
220
+ catch (err) {
221
+ const execErr = err;
222
+ return {
223
+ stdout: execErr.stdout ?? "",
224
+ success: false,
225
+ stderr: execErr.stderr ?? String(err),
226
+ };
227
+ }
228
+ }
229
+ // ── Helper: check if a binary is available ───────────────────────────────────
230
+ function binaryAvailable(binary) {
231
+ try {
232
+ execFileSafe("which", [binary], {
233
+ timeout: 5_000,
234
+ encoding: "utf-8",
235
+ stdio: ["pipe", "pipe", "pipe"],
236
+ });
237
+ return true;
238
+ }
239
+ catch {
240
+ return false;
241
+ }
242
+ }
243
+ // ── Helper: resolve package install command args per distro ───────────────────
244
+ function getInstallArgs(pkgManager, packageName) {
245
+ switch (pkgManager) {
246
+ case "apt":
247
+ return ["apt-get", "install", "-y", packageName];
248
+ case "dnf":
249
+ return ["dnf", "install", "-y", packageName];
250
+ case "yum":
251
+ return ["yum", "install", "-y", packageName];
252
+ case "pacman":
253
+ return ["pacman", "-S", "--noconfirm", packageName];
254
+ case "apk":
255
+ return ["apk", "add", packageName];
256
+ case "zypper":
257
+ return ["zypper", "install", "-y", packageName];
258
+ case "brew":
259
+ // brew should never be run with sudo
260
+ return ["brew", "install", packageName];
261
+ default:
262
+ return [];
263
+ }
264
+ }
265
+ // ── AutoInstaller ────────────────────────────────────────────────────────────
266
+ export class AutoInstaller {
267
+ static _instance = null;
268
+ distroCache = null;
269
+ /** Get or create the singleton instance. */
270
+ static instance() {
271
+ if (!AutoInstaller._instance) {
272
+ AutoInstaller._instance = new AutoInstaller();
273
+ // Fix E: Warn when auto-install is enabled
274
+ if (AutoInstaller._instance.isEnabled()) {
275
+ console.error("[auto-install] ⚠ Auto-installation is ENABLED. Packages will be installed with sudo when missing dependencies are detected.");
276
+ }
277
+ }
278
+ return AutoInstaller._instance;
279
+ }
280
+ /**
281
+ * Reset the singleton (for testing).
282
+ * @internal
283
+ */
284
+ static resetInstance() {
285
+ AutoInstaller._instance = null;
286
+ }
287
+ /** Check if auto-install is enabled via config. */
288
+ isEnabled() {
289
+ return getConfig().autoInstall;
290
+ }
291
+ /**
292
+ * Resolve all missing dependencies for a tool manifest.
293
+ *
294
+ * If auto-install is disabled, returns all dependencies as unresolved
295
+ * with method `'skipped'`.
296
+ */
297
+ async resolveAll(manifest, missingBinaries, missingPython, missingNpm, missingLibraries) {
298
+ const attempted = [];
299
+ // Early return if auto-install is disabled
300
+ if (!this.isEnabled()) {
301
+ const allMissing = [
302
+ ...missingBinaries,
303
+ ...(missingPython ?? []),
304
+ ...(missingNpm ?? []),
305
+ ...(missingLibraries ?? []),
306
+ ];
307
+ for (const dep of missingBinaries) {
308
+ attempted.push({
309
+ dependency: dep,
310
+ type: "binary",
311
+ method: "skipped",
312
+ success: false,
313
+ message: "Auto-install is disabled (set KALI_DEFENSE_AUTO_INSTALL=true to enable)",
314
+ });
315
+ }
316
+ for (const dep of missingPython ?? []) {
317
+ attempted.push({
318
+ dependency: dep,
319
+ type: "python-module",
320
+ method: "skipped",
321
+ success: false,
322
+ message: "Auto-install is disabled (set KALI_DEFENSE_AUTO_INSTALL=true to enable)",
323
+ });
324
+ }
325
+ for (const dep of missingNpm ?? []) {
326
+ attempted.push({
327
+ dependency: dep,
328
+ type: "npm-package",
329
+ method: "skipped",
330
+ success: false,
331
+ message: "Auto-install is disabled (set KALI_DEFENSE_AUTO_INSTALL=true to enable)",
332
+ });
333
+ }
334
+ for (const dep of missingLibraries ?? []) {
335
+ attempted.push({
336
+ dependency: dep,
337
+ type: "library",
338
+ method: "skipped",
339
+ success: false,
340
+ message: "Auto-install is disabled (set KALI_DEFENSE_AUTO_INSTALL=true to enable)",
341
+ });
342
+ }
343
+ return {
344
+ attempted,
345
+ allResolved: false,
346
+ unresolvedDependencies: allMissing,
347
+ };
348
+ }
349
+ console.error(`[auto-installer] Resolving dependencies for '${manifest.toolName}': ` +
350
+ `${missingBinaries.length} binaries, ` +
351
+ `${missingPython?.length ?? 0} python, ` +
352
+ `${missingNpm?.length ?? 0} npm, ` +
353
+ `${missingLibraries?.length ?? 0} libraries`);
354
+ // Install binaries
355
+ for (const binary of missingBinaries) {
356
+ const result = await this.installBinary(binary);
357
+ attempted.push(result);
358
+ }
359
+ // Install Python modules
360
+ for (const mod of missingPython ?? []) {
361
+ const result = await this.installPythonModule(mod);
362
+ attempted.push(result);
363
+ }
364
+ // Install npm packages
365
+ for (const pkg of missingNpm ?? []) {
366
+ const result = await this.installNpmPackage(pkg);
367
+ attempted.push(result);
368
+ }
369
+ // Install libraries
370
+ for (const lib of missingLibraries ?? []) {
371
+ const result = await this.installLibrary(lib);
372
+ attempted.push(result);
373
+ }
374
+ // Collect results
375
+ const unresolved = attempted
376
+ .filter((a) => !a.success)
377
+ .map((a) => a.dependency);
378
+ const allResolved = unresolved.length === 0;
379
+ // Summary
380
+ const succeeded = attempted.filter((a) => a.success).length;
381
+ const failed = attempted.filter((a) => !a.success && a.method !== "skipped").length;
382
+ if (attempted.length > 0) {
383
+ console.error(`[auto-installer] Summary for '${manifest.toolName}': ` +
384
+ `${succeeded} installed, ${failed} failed, ${unresolved.length} unresolved`);
385
+ }
386
+ return { attempted, allResolved, unresolvedDependencies: unresolved };
387
+ }
388
+ /**
389
+ * Install a system binary via the detected package manager.
390
+ *
391
+ * 1. Look up binary in DEFENSIVE_TOOLS for distro-specific package name
392
+ * 2. If not found, try binary name directly as package name
393
+ * 3. Verify with `which <binary>` after install
394
+ */
395
+ async installBinary(binary) {
396
+ const start = Date.now();
397
+ const distro = await this.getDistro();
398
+ if (distro.packageManager === "unknown") {
399
+ return {
400
+ dependency: binary,
401
+ type: "binary",
402
+ method: "system-package",
403
+ success: false,
404
+ message: "Cannot install: unknown package manager",
405
+ duration: Date.now() - start,
406
+ };
407
+ }
408
+ // Step 1: Look up in DEFENSIVE_TOOLS — only approved packages can be installed
409
+ const lookup = getBinaryLookup();
410
+ const toolReq = lookup.get(binary);
411
+ if (!toolReq) {
412
+ // Binary not in approved package list — refuse to install
413
+ console.error(`[auto-install] ⚠ Binary "${binary}" not in approved package list — skipping auto-install`);
414
+ return {
415
+ dependency: binary,
416
+ type: "binary",
417
+ method: "system-package",
418
+ success: false,
419
+ message: `Binary "${binary}" is not in the approved DEFENSIVE_TOOLS list. Auto-install refused.`,
420
+ duration: Date.now() - start,
421
+ };
422
+ }
423
+ // Resolve distro-specific package name (no raw binary name fallback)
424
+ const packageName = toolReq.packages[distro.family] ??
425
+ toolReq.packages.fallback ??
426
+ "";
427
+ if (!packageName) {
428
+ console.error(`[auto-install] ⚠ No package mapping for binary "${binary}" on ${distro.family} — skipping`);
429
+ return {
430
+ dependency: binary,
431
+ type: "binary",
432
+ method: "system-package",
433
+ success: false,
434
+ message: `No package mapping for "${binary}" on distro family "${distro.family}"`,
435
+ duration: Date.now() - start,
436
+ };
437
+ }
438
+ // Validate package name for safe characters
439
+ if (!validatePackageName(packageName)) {
440
+ console.error(`[auto-install] ⚠ Invalid package name "${packageName}" for binary "${binary}" — skipping`);
441
+ return {
442
+ dependency: binary,
443
+ type: "binary",
444
+ method: "system-package",
445
+ success: false,
446
+ message: `Package name "${packageName}" contains invalid characters. Auto-install refused.`,
447
+ duration: Date.now() - start,
448
+ };
449
+ }
450
+ // Verify package is in the approved allowlist
451
+ if (!getApprovedPackages().has(packageName)) {
452
+ console.error(`[auto-install] ⚠ Package "${packageName}" not in approved allowlist — skipping`);
453
+ return {
454
+ dependency: binary,
455
+ type: "binary",
456
+ method: "system-package",
457
+ success: false,
458
+ message: `Package "${packageName}" is not in the approved packages allowlist.`,
459
+ duration: Date.now() - start,
460
+ };
461
+ }
462
+ console.error(`[auto-installer] Installing binary '${binary}' via ${distro.packageManager} (package: ${packageName})...`);
463
+ // Build install command args
464
+ const installArgs = getInstallArgs(distro.packageManager, packageName);
465
+ if (installArgs.length === 0) {
466
+ return {
467
+ dependency: binary,
468
+ type: "binary",
469
+ method: "system-package",
470
+ success: false,
471
+ message: `No install command available for package manager '${distro.packageManager}'`,
472
+ duration: Date.now() - start,
473
+ };
474
+ }
475
+ // Execute install (brew doesn't use sudo)
476
+ const useSudo = distro.packageManager !== "brew";
477
+ const result = execWithSudo(installArgs, { useSudo, timeoutMs: 300_000 });
478
+ if (!result.success) {
479
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
480
+ console.error(`[auto-installer] ✗ Failed to install '${binary}' (package: ${packageName}): ${result.stderr.slice(0, 200)}`);
481
+ return {
482
+ dependency: binary,
483
+ type: "binary",
484
+ method: "system-package",
485
+ success: false,
486
+ message: `Failed to install package '${packageName}': ${result.stderr.slice(0, 300)}`,
487
+ duration: Date.now() - start,
488
+ };
489
+ }
490
+ // Verify installation
491
+ const installed = binaryAvailable(binary);
492
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
493
+ if (installed) {
494
+ console.error(`[auto-installer] ✓ Installed '${binary}' via ${distro.packageManager} (${elapsed}s)`);
495
+ // Log successful installation to the audit changelog
496
+ logChange(createChangeEntry({
497
+ tool: "auto-installer",
498
+ action: `Installed system package: ${packageName}`,
499
+ target: packageName,
500
+ before: "not installed",
501
+ after: `installed via ${distro.packageManager}`,
502
+ dryRun: false,
503
+ success: true,
504
+ }));
505
+ }
506
+ else {
507
+ console.error(`[auto-installer] ⚠ Package '${packageName}' installed but binary '${binary}' not found in PATH`);
508
+ }
509
+ return {
510
+ dependency: binary,
511
+ type: "binary",
512
+ method: "system-package",
513
+ success: installed,
514
+ message: installed
515
+ ? `Installed '${binary}' via ${distro.packageManager} (${packageName})`
516
+ : `Package '${packageName}' installed but binary '${binary}' not found in PATH`,
517
+ duration: Date.now() - start,
518
+ };
519
+ }
520
+ /**
521
+ * Install a Python module via pip.
522
+ *
523
+ * 1. Check if pip3 or pip exists
524
+ * 2. Try user-site install first (no sudo)
525
+ * 3. If that fails, try with sudo
526
+ * 4. Verify with `python3 -c "import <module>"`
527
+ */
528
+ async installPythonModule(module) {
529
+ const start = Date.now();
530
+ // Determine pip command
531
+ const pip = binaryAvailable("pip3") ? "pip3" : binaryAvailable("pip") ? "pip" : null;
532
+ if (!pip) {
533
+ console.error(`[auto-installer] ✗ Cannot install Python module '${module}': pip not found`);
534
+ return {
535
+ dependency: module,
536
+ type: "python-module",
537
+ method: "pip",
538
+ success: false,
539
+ message: "pip/pip3 not found. Install python3-pip first.",
540
+ duration: Date.now() - start,
541
+ };
542
+ }
543
+ // Validate module name for safe characters
544
+ if (!validateModuleName(module)) {
545
+ console.error(`[auto-install] ⚠ Invalid Python module name "${module}" — skipping`);
546
+ return {
547
+ dependency: module,
548
+ type: "python-module",
549
+ method: "pip",
550
+ success: false,
551
+ message: `Python module name "${module}" contains invalid characters. Auto-install refused.`,
552
+ duration: Date.now() - start,
553
+ };
554
+ }
555
+ // SECURITY (CORE-008): Verify pip package is in the allowed packages list
556
+ if (!ALLOWED_PIP_PACKAGES.has(module)) {
557
+ console.error(`[auto-install] ⚠ REJECTED: pip package "${module}" is not in the allowed packages list`);
558
+ return {
559
+ dependency: module,
560
+ type: "python-module",
561
+ method: "pip",
562
+ success: false,
563
+ message: `pip package "${module}" is not in the allowed packages list. Auto-install refused.`,
564
+ duration: Date.now() - start,
565
+ };
566
+ }
567
+ console.error(`[auto-installer] Installing Python module '${module}' via ${pip}...`);
568
+ // Try user-site install first (no sudo needed)
569
+ let result = execSimple(pip, ["install", "--user", module], { timeoutMs: 120_000 });
570
+ if (!result.success) {
571
+ // Try with sudo if user-site failed
572
+ console.error(`[auto-installer] User-site install failed for '${module}', trying with sudo...`);
573
+ result = execWithSudo([pip, "install", module], { timeoutMs: 120_000 });
574
+ }
575
+ if (!result.success) {
576
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
577
+ console.error(`[auto-installer] ✗ Failed to install Python module '${module}': ${result.stderr.slice(0, 200)}`);
578
+ return {
579
+ dependency: module,
580
+ type: "python-module",
581
+ method: "pip",
582
+ success: false,
583
+ message: `Failed to install '${module}' via pip: ${result.stderr.slice(0, 300)}`,
584
+ duration: Date.now() - start,
585
+ };
586
+ }
587
+ // Verify: determine the import name
588
+ const importName = PYTHON_IMPORT_MAP[module] ?? module.replace(/-/g, "_");
589
+ const python = binaryAvailable("python3") ? "python3" : "python";
590
+ const verifyResult = execSimple(python, ["-c", `import ${importName}`], { timeoutMs: 10_000 });
591
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
592
+ if (verifyResult.success) {
593
+ console.error(`[auto-installer] ✓ Installed Python module '${module}' (${elapsed}s)`);
594
+ // Log successful installation to the audit changelog
595
+ logChange(createChangeEntry({
596
+ tool: "auto-installer",
597
+ action: `Installed Python module: ${module}`,
598
+ target: module,
599
+ before: "not installed",
600
+ after: `installed via ${pip}`,
601
+ dryRun: false,
602
+ success: true,
603
+ }));
604
+ }
605
+ else {
606
+ console.error(`[auto-installer] ⚠ pip install succeeded for '${module}' but import verification failed`);
607
+ }
608
+ return {
609
+ dependency: module,
610
+ type: "python-module",
611
+ method: "pip",
612
+ success: verifyResult.success,
613
+ message: verifyResult.success
614
+ ? `Installed '${module}' via ${pip}`
615
+ : `pip install succeeded but 'import ${importName}' failed`,
616
+ duration: Date.now() - start,
617
+ };
618
+ }
619
+ /**
620
+ * Install an npm package globally.
621
+ *
622
+ * 1. Check if npm exists
623
+ * 2. Run `npm install -g <package>` with sudo if needed
624
+ * 3. Verify by checking if the package provides an expected binary
625
+ */
626
+ async installNpmPackage(pkg) {
627
+ const start = Date.now();
628
+ if (!binaryAvailable("npm")) {
629
+ console.error(`[auto-installer] ✗ Cannot install npm package '${pkg}': npm not found`);
630
+ return {
631
+ dependency: pkg,
632
+ type: "npm-package",
633
+ method: "npm",
634
+ success: false,
635
+ message: "npm not found. Install Node.js/npm first.",
636
+ duration: Date.now() - start,
637
+ };
638
+ }
639
+ // Validate npm package name for safe characters
640
+ if (!validateModuleName(pkg)) {
641
+ console.error(`[auto-install] ⚠ Invalid npm package name "${pkg}" — skipping`);
642
+ return {
643
+ dependency: pkg,
644
+ type: "npm-package",
645
+ method: "npm",
646
+ success: false,
647
+ message: `npm package name "${pkg}" contains invalid characters. Auto-install refused.`,
648
+ duration: Date.now() - start,
649
+ };
650
+ }
651
+ // SECURITY (CORE-008): Verify npm package is in the allowed packages list
652
+ if (!ALLOWED_NPM_PACKAGES.has(pkg)) {
653
+ console.error(`[auto-install] ⚠ REJECTED: npm package "${pkg}" is not in the allowed packages list`);
654
+ return {
655
+ dependency: pkg,
656
+ type: "npm-package",
657
+ method: "npm",
658
+ success: false,
659
+ message: `npm package "${pkg}" is not in the allowed packages list. Auto-install refused.`,
660
+ duration: Date.now() - start,
661
+ };
662
+ }
663
+ console.error(`[auto-installer] Installing npm package '${pkg}' globally...`);
664
+ // Try without sudo first (in case npm is configured with a user-writable prefix)
665
+ let result = execSimple("npm", ["install", "-g", pkg], { timeoutMs: 120_000 });
666
+ if (!result.success) {
667
+ // Try with sudo
668
+ console.error(`[auto-installer] Non-sudo npm install failed for '${pkg}', trying with sudo...`);
669
+ result = execWithSudo(["npm", "install", "-g", pkg], { timeoutMs: 120_000 });
670
+ }
671
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
672
+ if (!result.success) {
673
+ console.error(`[auto-installer] ✗ Failed to install npm package '${pkg}': ${result.stderr.slice(0, 200)}`);
674
+ return {
675
+ dependency: pkg,
676
+ type: "npm-package",
677
+ method: "npm",
678
+ success: false,
679
+ message: `Failed to install '${pkg}' via npm: ${result.stderr.slice(0, 300)}`,
680
+ duration: Date.now() - start,
681
+ };
682
+ }
683
+ // Verify — many npm packages provide a binary with the same name
684
+ const installed = binaryAvailable(pkg);
685
+ if (installed) {
686
+ console.error(`[auto-installer] ✓ Installed npm package '${pkg}' (${elapsed}s)`);
687
+ }
688
+ else {
689
+ // Package installed but binary might have a different name
690
+ console.error(`[auto-installer] ✓ npm package '${pkg}' installed (binary may differ from package name)`);
691
+ }
692
+ // Log successful npm installation to the audit changelog
693
+ logChange(createChangeEntry({
694
+ tool: "auto-installer",
695
+ action: `Installed npm package: ${pkg}`,
696
+ target: pkg,
697
+ before: "not installed",
698
+ after: "installed via npm (global)",
699
+ dryRun: false,
700
+ success: true,
701
+ }));
702
+ return {
703
+ dependency: pkg,
704
+ type: "npm-package",
705
+ method: "npm",
706
+ // Consider success if npm install succeeded, even if binary name differs
707
+ success: true,
708
+ message: installed
709
+ ? `Installed '${pkg}' via npm (binary verified)`
710
+ : `Installed '${pkg}' via npm (binary name may differ)`,
711
+ duration: Date.now() - start,
712
+ };
713
+ }
714
+ /**
715
+ * Install a system library (development headers).
716
+ *
717
+ * 1. Determine dev package name based on distro family
718
+ * 2. Try installing the first candidate that works
719
+ * 3. Verify with `ldconfig -p | grep <lib>` or `pkg-config --exists <lib>`
720
+ */
721
+ async installLibrary(lib) {
722
+ const start = Date.now();
723
+ const distro = await this.getDistro();
724
+ if (distro.packageManager === "unknown") {
725
+ return {
726
+ dependency: lib,
727
+ type: "library",
728
+ method: "system-package",
729
+ success: false,
730
+ message: "Cannot install: unknown package manager",
731
+ duration: Date.now() - start,
732
+ };
733
+ }
734
+ console.error(`[auto-installer] Installing library '${lib}' via ${distro.packageManager}...`);
735
+ // Get candidate package names for this distro family
736
+ const patternFn = LIB_DEV_PATTERNS[distro.family];
737
+ const candidates = patternFn ? patternFn(lib) : [`lib${lib}-dev`, lib];
738
+ let installed = false;
739
+ let lastError = "";
740
+ for (const candidate of candidates) {
741
+ // Validate candidate package name for safe characters
742
+ if (!validatePackageName(candidate)) {
743
+ console.error(`[auto-install] ⚠ Invalid library package name "${candidate}" — skipping candidate`);
744
+ continue;
745
+ }
746
+ const installArgs = getInstallArgs(distro.packageManager, candidate);
747
+ if (installArgs.length === 0)
748
+ continue;
749
+ const useSudo = distro.packageManager !== "brew";
750
+ const result = execWithSudo(installArgs, { useSudo, timeoutMs: 120_000 });
751
+ if (result.success) {
752
+ installed = true;
753
+ console.error(`[auto-installer] ✓ Installed library '${lib}' (package: ${candidate})`);
754
+ // Log successful library installation to the audit changelog
755
+ logChange(createChangeEntry({
756
+ tool: "auto-installer",
757
+ action: `Installed library package: ${candidate}`,
758
+ target: candidate,
759
+ before: "not installed",
760
+ after: `installed via ${distro.packageManager}`,
761
+ dryRun: false,
762
+ success: true,
763
+ }));
764
+ break;
765
+ }
766
+ lastError = result.stderr.slice(0, 200);
767
+ }
768
+ if (!installed) {
769
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
770
+ console.error(`[auto-installer] ✗ Failed to install library '${lib}': ${lastError}`);
771
+ return {
772
+ dependency: lib,
773
+ type: "library",
774
+ method: "system-package",
775
+ success: false,
776
+ message: `Failed to install library '${lib}'. Tried: ${candidates.join(", ")}`,
777
+ duration: Date.now() - start,
778
+ };
779
+ }
780
+ // Verify with ldconfig or pkg-config
781
+ const verified = this.verifyLibrary(lib);
782
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
783
+ if (verified) {
784
+ console.error(`[auto-installer] ✓ Library '${lib}' verified (${elapsed}s)`);
785
+ }
786
+ else {
787
+ console.error(`[auto-installer] ⚠ Library package installed but '${lib}' not found via ldconfig/pkg-config`);
788
+ }
789
+ return {
790
+ dependency: lib,
791
+ type: "library",
792
+ method: "system-package",
793
+ // Consider success if package install succeeded even if ldconfig doesn't show it yet
794
+ success: true,
795
+ message: verified
796
+ ? `Installed and verified library '${lib}'`
797
+ : `Package installed for '${lib}' (ldconfig/pkg-config verification inconclusive)`,
798
+ duration: Date.now() - start,
799
+ };
800
+ }
801
+ // ── Private helpers ──────────────────────────────────────────────────────
802
+ /**
803
+ * Get (and cache) the detected distro info.
804
+ */
805
+ async getDistro() {
806
+ if (!this.distroCache) {
807
+ this.distroCache = await detectDistro();
808
+ }
809
+ return this.distroCache;
810
+ }
811
+ /**
812
+ * Verify a library is available via ldconfig or pkg-config.
813
+ */
814
+ verifyLibrary(lib) {
815
+ // Try pkg-config first
816
+ if (binaryAvailable("pkg-config")) {
817
+ const pkgResult = execSimple("pkg-config", ["--exists", lib], { timeoutMs: 5_000 });
818
+ if (pkgResult.success)
819
+ return true;
820
+ }
821
+ // Try ldconfig -p | grep
822
+ try {
823
+ const ldconfigResult = execSimple("ldconfig", ["-p"], { timeoutMs: 10_000 });
824
+ if (ldconfigResult.success && ldconfigResult.stdout.includes(lib)) {
825
+ return true;
826
+ }
827
+ }
828
+ catch {
829
+ // ldconfig might not be available or might need sudo
830
+ }
831
+ return false;
832
+ }
833
+ }