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,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
|
+
}
|