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,849 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Allowlist — security control that restricts which binaries the
|
|
3
|
+
* MCP server may execute.
|
|
4
|
+
*
|
|
5
|
+
* Every command passed to `executeCommand()` (and the bypass modules that
|
|
6
|
+
* use `execFileSync` / `spawn` directly) MUST be present in this allowlist.
|
|
7
|
+
* Bare command names are resolved to absolute paths at startup, eliminating
|
|
8
|
+
* PATH-manipulation attacks when running under sudo.
|
|
9
|
+
*
|
|
10
|
+
* Design constraints:
|
|
11
|
+
* - **No circular dependencies**: only imports from `node:fs` (no executor,
|
|
12
|
+
* no sudo-session, no tool-registry).
|
|
13
|
+
* - Uses `fs.existsSync` for path resolution — never shells out to `which`.
|
|
14
|
+
* - Candidate paths are checked in order; the first match wins.
|
|
15
|
+
* - Unresolvable binaries are logged as warnings but don't block startup
|
|
16
|
+
* (not every system has every tool installed).
|
|
17
|
+
*
|
|
18
|
+
* @module command-allowlist
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, lstatSync } from "node:fs";
|
|
21
|
+
// INTENTIONAL EXCEPTION: This module uses execFileSync/execFile directly from node:child_process
|
|
22
|
+
// because the command allowlist must be initialized before spawn-safe.ts can function.
|
|
23
|
+
// spawn-safe.ts depends on this module for allowlist resolution, so routing through
|
|
24
|
+
// spawn-safe here would create a circular dependency. This is the only module (besides
|
|
25
|
+
// executor.ts) permitted to import child_process directly.
|
|
26
|
+
import { execFileSync, execFile } from "node:child_process";
|
|
27
|
+
import { promisify } from "node:util";
|
|
28
|
+
const execFileAsync = promisify(execFile);
|
|
29
|
+
// ── Critical Binary Package Mappings ─────────────────────────────────────────
|
|
30
|
+
/**
|
|
31
|
+
* Maps critical security binaries to their expected distro packages.
|
|
32
|
+
* These are the binaries where integrity matters most — a trojanized binary
|
|
33
|
+
* in any of these would undermine the entire defensive posture.
|
|
34
|
+
*/
|
|
35
|
+
const CRITICAL_BINARY_PACKAGES = {
|
|
36
|
+
"iptables": ["iptables"],
|
|
37
|
+
"nft": ["nftables"],
|
|
38
|
+
"sysctl": ["procps"],
|
|
39
|
+
"lynis": ["lynis"],
|
|
40
|
+
"rkhunter": ["rkhunter"],
|
|
41
|
+
"chkrootkit": ["chkrootkit"],
|
|
42
|
+
"clamscan": ["clamav"],
|
|
43
|
+
"aide": ["aide", "aide-common"],
|
|
44
|
+
"auditctl": ["auditd"],
|
|
45
|
+
"fail2ban-client": ["fail2ban"],
|
|
46
|
+
"sshd": ["openssh-server"],
|
|
47
|
+
"openssl": ["openssl"],
|
|
48
|
+
"gpg": ["gnupg", "gnupg2", "gpg"],
|
|
49
|
+
"sudo": ["sudo"],
|
|
50
|
+
};
|
|
51
|
+
// ── The Allowlist ────────────────────────────────────────────────────────────
|
|
52
|
+
/**
|
|
53
|
+
* Comprehensive mapping of every binary the MCP server may execute.
|
|
54
|
+
*
|
|
55
|
+
* Derived by scanning:
|
|
56
|
+
* - All `src/tools/*.ts` files for `command:` values passed to `executeCommand()`
|
|
57
|
+
* - `src/core/tool-dependencies.ts` for required/optional binaries
|
|
58
|
+
* - `src/core/auto-installer.ts` for direct `execFileSync` calls
|
|
59
|
+
* - `src/core/sudo-session.ts` and `src/core/privilege-manager.ts` for
|
|
60
|
+
* direct `spawn` / `execFileSync` calls
|
|
61
|
+
*/
|
|
62
|
+
const ALLOWLIST_DEFINITIONS = [
|
|
63
|
+
// ── Privilege / session management ──────────────────────────────────────
|
|
64
|
+
{ binary: "sudo", candidates: ["/usr/bin/sudo", "/bin/sudo"] },
|
|
65
|
+
{ binary: "whoami", candidates: ["/usr/bin/whoami", "/bin/whoami"] },
|
|
66
|
+
{ binary: "id", candidates: ["/usr/bin/id", "/bin/id"] },
|
|
67
|
+
{ binary: "env", candidates: ["/usr/bin/env", "/bin/env"] },
|
|
68
|
+
// ── Firewall ───────────────────────────────────────────────────────────
|
|
69
|
+
{ binary: "iptables", candidates: ["/usr/sbin/iptables", "/sbin/iptables"] },
|
|
70
|
+
{ binary: "ip6tables", candidates: ["/usr/sbin/ip6tables", "/sbin/ip6tables"] },
|
|
71
|
+
{ binary: "iptables-save", candidates: ["/usr/sbin/iptables-save", "/sbin/iptables-save"] },
|
|
72
|
+
{ binary: "ip6tables-save", candidates: ["/usr/sbin/ip6tables-save", "/sbin/ip6tables-save"] },
|
|
73
|
+
{ binary: "iptables-restore", candidates: ["/usr/sbin/iptables-restore", "/sbin/iptables-restore"] },
|
|
74
|
+
{ binary: "ip6tables-restore", candidates: ["/usr/sbin/ip6tables-restore", "/sbin/ip6tables-restore"] },
|
|
75
|
+
{ binary: "nft", candidates: ["/usr/sbin/nft", "/sbin/nft"] },
|
|
76
|
+
{ binary: "ufw", candidates: ["/usr/sbin/ufw", "/sbin/ufw"] },
|
|
77
|
+
{ binary: "netfilter-persistent", candidates: ["/usr/sbin/netfilter-persistent", "/sbin/netfilter-persistent"] },
|
|
78
|
+
// ── Kernel / sysctl ────────────────────────────────────────────────────
|
|
79
|
+
{ binary: "sysctl", candidates: ["/usr/sbin/sysctl", "/sbin/sysctl", "/usr/bin/sysctl"] },
|
|
80
|
+
{ binary: "lsmod", candidates: ["/usr/sbin/lsmod", "/sbin/lsmod", "/usr/bin/lsmod", "/bin/lsmod"] },
|
|
81
|
+
{ binary: "modprobe", candidates: ["/usr/sbin/modprobe", "/sbin/modprobe"] },
|
|
82
|
+
// ── Systemd / services ─────────────────────────────────────────────────
|
|
83
|
+
{ binary: "systemctl", candidates: ["/usr/bin/systemctl", "/bin/systemctl"] },
|
|
84
|
+
{ binary: "systemd-analyze", candidates: ["/usr/bin/systemd-analyze", "/bin/systemd-analyze"] },
|
|
85
|
+
// ── Networking ─────────────────────────────────────────────────────────
|
|
86
|
+
{ binary: "ss", candidates: ["/usr/bin/ss", "/bin/ss", "/usr/sbin/ss", "/sbin/ss"] },
|
|
87
|
+
{ binary: "ip", candidates: ["/usr/sbin/ip", "/sbin/ip", "/usr/bin/ip", "/bin/ip"] },
|
|
88
|
+
{ binary: "nmap", candidates: ["/usr/bin/nmap", "/usr/local/bin/nmap"] },
|
|
89
|
+
{ binary: "tcpdump", candidates: ["/usr/bin/tcpdump", "/usr/sbin/tcpdump", "/sbin/tcpdump"] },
|
|
90
|
+
{ binary: "hostname", candidates: ["/usr/bin/hostname", "/bin/hostname"] },
|
|
91
|
+
{ binary: "curl", candidates: ["/usr/bin/curl", "/bin/curl"] },
|
|
92
|
+
{ binary: "wget", candidates: ["/usr/bin/wget", "/bin/wget"] },
|
|
93
|
+
// ── Logging / audit ────────────────────────────────────────────────────
|
|
94
|
+
{ binary: "journalctl", candidates: ["/usr/bin/journalctl", "/bin/journalctl"] },
|
|
95
|
+
{ binary: "dmesg", candidates: ["/usr/bin/dmesg", "/bin/dmesg"] },
|
|
96
|
+
{ binary: "auditctl", candidates: ["/usr/sbin/auditctl", "/sbin/auditctl"] },
|
|
97
|
+
{ binary: "ausearch", candidates: ["/usr/sbin/ausearch", "/sbin/ausearch"] },
|
|
98
|
+
{ binary: "aureport", candidates: ["/usr/sbin/aureport", "/sbin/aureport"] },
|
|
99
|
+
{ binary: "fail2ban-client", candidates: ["/usr/bin/fail2ban-client", "/usr/local/bin/fail2ban-client"] },
|
|
100
|
+
{ binary: "logrotate", candidates: ["/usr/sbin/logrotate", "/sbin/logrotate"] },
|
|
101
|
+
// ── IDS / rootkit detection ────────────────────────────────────────────
|
|
102
|
+
{ binary: "aide", candidates: ["/usr/bin/aide", "/usr/sbin/aide"] },
|
|
103
|
+
{ binary: "rkhunter", candidates: ["/usr/bin/rkhunter", "/usr/local/bin/rkhunter"] },
|
|
104
|
+
{ binary: "chkrootkit", candidates: ["/usr/bin/chkrootkit", "/usr/sbin/chkrootkit", "/usr/local/bin/chkrootkit"] },
|
|
105
|
+
// ── Malware scanning ──────────────────────────────────────────────────
|
|
106
|
+
{ binary: "clamscan", candidates: ["/usr/bin/clamscan", "/usr/local/bin/clamscan"] },
|
|
107
|
+
{ binary: "freshclam", candidates: ["/usr/bin/freshclam", "/usr/local/bin/freshclam"] },
|
|
108
|
+
{ binary: "yara", candidates: ["/usr/bin/yara", "/usr/local/bin/yara"] },
|
|
109
|
+
// ── Compliance / audit frameworks ──────────────────────────────────────
|
|
110
|
+
{ binary: "lynis", candidates: ["/usr/bin/lynis", "/usr/sbin/lynis", "/usr/local/bin/lynis"] },
|
|
111
|
+
{ binary: "oscap", candidates: ["/usr/bin/oscap", "/usr/local/bin/oscap"] },
|
|
112
|
+
{ binary: "tiger", candidates: ["/usr/bin/tiger", "/usr/sbin/tiger", "/usr/local/bin/tiger"] },
|
|
113
|
+
// ── Container / sandboxing ─────────────────────────────────────────────
|
|
114
|
+
{ binary: "docker", candidates: ["/usr/bin/docker", "/usr/local/bin/docker"] },
|
|
115
|
+
{ binary: "docker-bench-security", candidates: ["/usr/bin/docker-bench-security", "/usr/local/bin/docker-bench-security"] },
|
|
116
|
+
{ binary: "trivy", candidates: ["/usr/bin/trivy", "/usr/local/bin/trivy"] },
|
|
117
|
+
{ binary: "grype", candidates: ["/usr/bin/grype", "/usr/local/bin/grype"] },
|
|
118
|
+
{ binary: "apparmor_status", candidates: ["/usr/sbin/apparmor_status", "/sbin/apparmor_status"] },
|
|
119
|
+
{ binary: "aa-status", candidates: ["/usr/sbin/aa-status", "/sbin/aa-status"] },
|
|
120
|
+
{ binary: "aa-enabled", candidates: ["/usr/sbin/aa-enabled", "/sbin/aa-enabled"] },
|
|
121
|
+
{ binary: "apparmor_parser", candidates: ["/usr/sbin/apparmor_parser", "/sbin/apparmor_parser"] },
|
|
122
|
+
{ binary: "getenforce", candidates: ["/usr/sbin/getenforce", "/sbin/getenforce"] },
|
|
123
|
+
{ binary: "setenforce", candidates: ["/usr/sbin/setenforce", "/sbin/setenforce"] },
|
|
124
|
+
{ binary: "sestatus", candidates: ["/usr/sbin/sestatus", "/sbin/sestatus"] },
|
|
125
|
+
{ binary: "getsebool", candidates: ["/usr/sbin/getsebool", "/sbin/getsebool"] },
|
|
126
|
+
{ binary: "lsns", candidates: ["/usr/bin/lsns", "/bin/lsns"] },
|
|
127
|
+
{ binary: "newuidmap", candidates: ["/usr/bin/newuidmap"] },
|
|
128
|
+
{ binary: "newgidmap", candidates: ["/usr/bin/newgidmap"] },
|
|
129
|
+
// ── Encryption / crypto ────────────────────────────────────────────────
|
|
130
|
+
{ binary: "openssl", candidates: ["/usr/bin/openssl", "/bin/openssl"] },
|
|
131
|
+
{ binary: "gpg", candidates: ["/usr/bin/gpg", "/bin/gpg"] },
|
|
132
|
+
{ binary: "gpg2", candidates: ["/usr/bin/gpg2", "/bin/gpg2"] },
|
|
133
|
+
{ binary: "cryptsetup", candidates: ["/usr/sbin/cryptsetup", "/sbin/cryptsetup"] },
|
|
134
|
+
// ── WireGuard / VPN ────────────────────────────────────────────────────
|
|
135
|
+
{ binary: "wg", candidates: ["/usr/bin/wg", "/usr/local/bin/wg"] },
|
|
136
|
+
{ binary: "wireguard", candidates: ["/usr/bin/wireguard", "/usr/local/bin/wireguard"] },
|
|
137
|
+
// ── SSH ────────────────────────────────────────────────────────────────
|
|
138
|
+
{ binary: "sshd", candidates: ["/usr/sbin/sshd", "/sbin/sshd"] },
|
|
139
|
+
{ binary: "ssh", candidates: ["/usr/bin/ssh", "/bin/ssh"] },
|
|
140
|
+
{ binary: "ssh-keygen", candidates: ["/usr/bin/ssh-keygen", "/bin/ssh-keygen"] },
|
|
141
|
+
{ binary: "ssh-askpass", candidates: ["/usr/bin/ssh-askpass", "/usr/lib/ssh/x11-ssh-askpass"] },
|
|
142
|
+
// ── User / access management ───────────────────────────────────────────
|
|
143
|
+
{ binary: "passwd", candidates: ["/usr/bin/passwd", "/bin/passwd"] },
|
|
144
|
+
{ binary: "usermod", candidates: ["/usr/sbin/usermod", "/sbin/usermod"] },
|
|
145
|
+
{ binary: "useradd", candidates: ["/usr/sbin/useradd", "/sbin/useradd"] },
|
|
146
|
+
{ binary: "visudo", candidates: ["/usr/sbin/visudo", "/sbin/visudo"] },
|
|
147
|
+
{ binary: "getent", candidates: ["/usr/bin/getent", "/bin/getent"] },
|
|
148
|
+
{ binary: "chage", candidates: ["/usr/bin/chage", "/bin/chage"] },
|
|
149
|
+
{ binary: "lastlog", candidates: ["/usr/bin/lastlog", "/bin/lastlog"] },
|
|
150
|
+
// ── Package managers ───────────────────────────────────────────────────
|
|
151
|
+
{ binary: "apt", candidates: ["/usr/bin/apt"] },
|
|
152
|
+
{ binary: "apt-get", candidates: ["/usr/bin/apt-get"] },
|
|
153
|
+
{ binary: "apt-cache", candidates: ["/usr/bin/apt-cache"] },
|
|
154
|
+
{ binary: "dpkg", candidates: ["/usr/bin/dpkg", "/bin/dpkg"] },
|
|
155
|
+
{ binary: "dpkg-query", candidates: ["/usr/bin/dpkg-query"] },
|
|
156
|
+
{ binary: "debsums", candidates: ["/usr/bin/debsums"] },
|
|
157
|
+
{ binary: "debsecan", candidates: ["/usr/bin/debsecan"] },
|
|
158
|
+
{ binary: "rpm", candidates: ["/usr/bin/rpm", "/bin/rpm"] },
|
|
159
|
+
{ binary: "dnf", candidates: ["/usr/bin/dnf"] },
|
|
160
|
+
{ binary: "yum", candidates: ["/usr/bin/yum"] },
|
|
161
|
+
{ binary: "pacman", candidates: ["/usr/bin/pacman"] },
|
|
162
|
+
{ binary: "apk", candidates: ["/sbin/apk", "/usr/sbin/apk"] },
|
|
163
|
+
{ binary: "zypper", candidates: ["/usr/bin/zypper"] },
|
|
164
|
+
{ binary: "brew", candidates: ["/usr/local/bin/brew", "/opt/homebrew/bin/brew", "/home/linuxbrew/.linuxbrew/bin/brew"] },
|
|
165
|
+
{ binary: "pip3", candidates: ["/usr/bin/pip3", "/usr/local/bin/pip3"] },
|
|
166
|
+
{ binary: "pip", candidates: ["/usr/bin/pip", "/usr/local/bin/pip"] },
|
|
167
|
+
{ binary: "npm", candidates: ["/usr/bin/npm", "/usr/local/bin/npm"] },
|
|
168
|
+
// ── Coreutils / standard POSIX ─────────────────────────────────────────
|
|
169
|
+
{ binary: "cat", candidates: ["/usr/bin/cat", "/bin/cat"] },
|
|
170
|
+
{ binary: "ls", candidates: ["/usr/bin/ls", "/bin/ls"] },
|
|
171
|
+
{ binary: "cp", candidates: ["/usr/bin/cp", "/bin/cp"] },
|
|
172
|
+
{ binary: "rm", candidates: ["/usr/bin/rm", "/bin/rm"] },
|
|
173
|
+
{ binary: "mv", candidates: ["/usr/bin/mv", "/bin/mv"] },
|
|
174
|
+
{ binary: "mkdir", candidates: ["/usr/bin/mkdir", "/bin/mkdir"] },
|
|
175
|
+
{ binary: "chmod", candidates: ["/usr/bin/chmod", "/bin/chmod"] },
|
|
176
|
+
{ binary: "chown", candidates: ["/usr/bin/chown", "/bin/chown"] },
|
|
177
|
+
{ binary: "chgrp", candidates: ["/usr/bin/chgrp", "/bin/chgrp"] },
|
|
178
|
+
{ binary: "stat", candidates: ["/usr/bin/stat", "/bin/stat"] },
|
|
179
|
+
{ binary: "head", candidates: ["/usr/bin/head", "/bin/head"] },
|
|
180
|
+
{ binary: "tail", candidates: ["/usr/bin/tail", "/bin/tail"] },
|
|
181
|
+
{ binary: "wc", candidates: ["/usr/bin/wc", "/bin/wc"] },
|
|
182
|
+
{ binary: "tee", candidates: ["/usr/bin/tee", "/bin/tee"] },
|
|
183
|
+
{ binary: "find", candidates: ["/usr/bin/find", "/bin/find"] },
|
|
184
|
+
{ binary: "grep", candidates: ["/usr/bin/grep", "/bin/grep"] },
|
|
185
|
+
{ binary: "zgrep", candidates: ["/usr/bin/zgrep", "/bin/zgrep"] },
|
|
186
|
+
{ binary: "awk", candidates: ["/usr/bin/awk", "/bin/awk", "/usr/bin/gawk", "/bin/gawk"] },
|
|
187
|
+
{ binary: "sed", candidates: ["/usr/bin/sed", "/bin/sed"] },
|
|
188
|
+
{ binary: "test", candidates: ["/usr/bin/test", "/bin/test"] },
|
|
189
|
+
{ binary: "df", candidates: ["/usr/bin/df", "/bin/df"] },
|
|
190
|
+
{ binary: "mount", candidates: ["/usr/bin/mount", "/bin/mount", "/sbin/mount"] },
|
|
191
|
+
{ binary: "findmnt", candidates: ["/usr/bin/findmnt", "/bin/findmnt"] },
|
|
192
|
+
{ binary: "lsblk", candidates: ["/usr/bin/lsblk", "/bin/lsblk"] },
|
|
193
|
+
{ binary: "file", candidates: ["/usr/bin/file", "/bin/file"] },
|
|
194
|
+
{ binary: "uptime", candidates: ["/usr/bin/uptime", "/bin/uptime"] },
|
|
195
|
+
// ── Hashing / integrity ────────────────────────────────────────────────
|
|
196
|
+
{ binary: "sha256sum", candidates: ["/usr/bin/sha256sum", "/bin/sha256sum"] },
|
|
197
|
+
{ binary: "sha512sum", candidates: ["/usr/bin/sha512sum", "/bin/sha512sum"] },
|
|
198
|
+
{ binary: "md5sum", candidates: ["/usr/bin/md5sum", "/bin/md5sum"] },
|
|
199
|
+
// ── Process inspection ─────────────────────────────────────────────────
|
|
200
|
+
{ binary: "ps", candidates: ["/usr/bin/ps", "/bin/ps"] },
|
|
201
|
+
{ binary: "pgrep", candidates: ["/usr/bin/pgrep", "/bin/pgrep"] },
|
|
202
|
+
{ binary: "lsof", candidates: ["/usr/bin/lsof", "/usr/sbin/lsof"] },
|
|
203
|
+
// ── Boot / secure boot ─────────────────────────────────────────────────
|
|
204
|
+
{ binary: "mokutil", candidates: ["/usr/bin/mokutil"] },
|
|
205
|
+
{ binary: "update-grub", candidates: ["/usr/sbin/update-grub", "/sbin/update-grub"] },
|
|
206
|
+
// ── Python (for auto-installer verification) ───────────────────────────
|
|
207
|
+
{ binary: "python3", candidates: ["/usr/bin/python3", "/usr/local/bin/python3"] },
|
|
208
|
+
{ binary: "python", candidates: ["/usr/bin/python", "/usr/local/bin/python"] },
|
|
209
|
+
// ── Library verification (auto-installer) ──────────────────────────────
|
|
210
|
+
{ binary: "pkg-config", candidates: ["/usr/bin/pkg-config", "/usr/local/bin/pkg-config"] },
|
|
211
|
+
{ binary: "ldconfig", candidates: ["/usr/sbin/ldconfig", "/sbin/ldconfig"] },
|
|
212
|
+
{ binary: "which", candidates: ["/usr/bin/which", "/bin/which"] },
|
|
213
|
+
// ── Binary analysis / memory protections ───────────────────────────────
|
|
214
|
+
{ binary: "readelf", candidates: ["/usr/bin/readelf", "/bin/readelf"] },
|
|
215
|
+
{ binary: "checksec", candidates: ["/usr/bin/checksec", "/usr/local/bin/checksec"] },
|
|
216
|
+
// ── Cron / scheduling ──────────────────────────────────────────────────
|
|
217
|
+
{ binary: "crontab", candidates: ["/usr/bin/crontab", "/bin/crontab"] },
|
|
218
|
+
// ── Kernel live-patching ───────────────────────────────────────────────
|
|
219
|
+
{ binary: "uname", candidates: ["/usr/bin/uname", "/bin/uname"] },
|
|
220
|
+
{ binary: "canonical-livepatch", candidates: ["/usr/bin/canonical-livepatch", "/snap/bin/canonical-livepatch"] },
|
|
221
|
+
{ binary: "kpatch", candidates: ["/usr/sbin/kpatch", "/usr/bin/kpatch"] },
|
|
222
|
+
{ binary: "klp", candidates: ["/usr/sbin/klp", "/usr/bin/klp"] },
|
|
223
|
+
// ── Supply chain security ──────────────────────────────────────────────
|
|
224
|
+
{ binary: "cosign", candidates: ["/usr/bin/cosign", "/usr/local/bin/cosign"] },
|
|
225
|
+
{ binary: "slsa-verifier", candidates: ["/usr/bin/slsa-verifier", "/usr/local/bin/slsa-verifier"] },
|
|
226
|
+
{ binary: "syft", candidates: ["/usr/bin/syft", "/usr/local/bin/syft"] },
|
|
227
|
+
{ binary: "cdxgen", candidates: ["/usr/bin/cdxgen", "/usr/local/bin/cdxgen"] },
|
|
228
|
+
// ── Secrets scanners ───────────────────────────────────────────────────
|
|
229
|
+
{ binary: "trufflehog", candidates: ["/usr/bin/trufflehog", "/usr/local/bin/trufflehog"] },
|
|
230
|
+
{ binary: "gitleaks", candidates: ["/usr/bin/gitleaks", "/usr/local/bin/gitleaks"] },
|
|
231
|
+
{ binary: "git", candidates: ["/usr/bin/git", "/bin/git"] },
|
|
232
|
+
// ── eBPF / runtime security ────────────────────────────────────────────
|
|
233
|
+
{ binary: "bpftool", candidates: ["/usr/sbin/bpftool", "/sbin/bpftool", "/usr/bin/bpftool"] },
|
|
234
|
+
{ binary: "falco", candidates: ["/usr/bin/falco", "/usr/local/bin/falco"] },
|
|
235
|
+
// ── IDS / network ──────────────────────────────────────────────────────
|
|
236
|
+
{ binary: "snort", candidates: ["/usr/bin/snort", "/usr/sbin/snort", "/usr/local/bin/snort"] },
|
|
237
|
+
{ binary: "suricata", candidates: ["/usr/bin/suricata", "/usr/sbin/suricata"] },
|
|
238
|
+
// ── macOS detection (distro.ts) ────────────────────────────────────────
|
|
239
|
+
{ binary: "sw_vers", candidates: ["/usr/bin/sw_vers"] },
|
|
240
|
+
{ binary: "lsb_release", candidates: ["/usr/bin/lsb_release"] },
|
|
241
|
+
// ── GUI askpass helpers (sudo-management.ts) ───────────────────────────
|
|
242
|
+
{ binary: "zenity", candidates: ["/usr/bin/zenity"] },
|
|
243
|
+
{ binary: "kdialog", candidates: ["/usr/bin/kdialog"] },
|
|
244
|
+
{ binary: "ksshaskpass", candidates: ["/usr/bin/ksshaskpass"] },
|
|
245
|
+
{ binary: "lxqt-sudo", candidates: ["/usr/bin/lxqt-sudo"] },
|
|
246
|
+
// ── DNS security ───────────────────────────────────────────────────────
|
|
247
|
+
{ binary: "dig", candidates: ["/usr/bin/dig", "/usr/local/bin/dig"] },
|
|
248
|
+
{ binary: "systemd-resolve", candidates: ["/usr/bin/systemd-resolve"] },
|
|
249
|
+
{ binary: "resolvectl", candidates: ["/usr/bin/resolvectl"] },
|
|
250
|
+
{ binary: "whois", candidates: ["/usr/bin/whois", "/bin/whois"] },
|
|
251
|
+
// ── Vulnerability scanning ─────────────────────────────────────────────
|
|
252
|
+
{ binary: "nikto", candidates: ["/usr/bin/nikto", "/usr/local/bin/nikto"] },
|
|
253
|
+
{ binary: "searchsploit", candidates: ["/usr/bin/searchsploit", "/usr/local/bin/searchsploit"] },
|
|
254
|
+
// ── Process security ───────────────────────────────────────────────────
|
|
255
|
+
{ binary: "getpcaps", candidates: ["/usr/sbin/getpcaps", "/sbin/getpcaps", "/usr/bin/getpcaps"] },
|
|
256
|
+
{ binary: "capsh", candidates: ["/usr/sbin/capsh", "/sbin/capsh", "/usr/bin/capsh"] },
|
|
257
|
+
{ binary: "systemd-cgls", candidates: ["/usr/bin/systemd-cgls", "/bin/systemd-cgls"] },
|
|
258
|
+
{ binary: "systemd-cgtop", candidates: ["/usr/bin/systemd-cgtop", "/bin/systemd-cgtop"] },
|
|
259
|
+
// ── WAF / web server ──────────────────────────────────────────────────
|
|
260
|
+
{ binary: "apache2ctl", candidates: ["/usr/sbin/apache2ctl", "/sbin/apache2ctl"] },
|
|
261
|
+
// ── Cloud security ────────────────────────────────────────────────────
|
|
262
|
+
{ binary: "cloud-init", candidates: ["/usr/bin/cloud-init", "/usr/local/bin/cloud-init"] },
|
|
263
|
+
{ binary: "aws", candidates: ["/usr/bin/aws", "/usr/local/bin/aws"] },
|
|
264
|
+
{ binary: "gsutil", candidates: ["/usr/bin/gsutil", "/usr/local/bin/gsutil", "/snap/bin/gsutil"] },
|
|
265
|
+
{ binary: "az", candidates: ["/usr/bin/az", "/usr/local/bin/az"] },
|
|
266
|
+
// ── Deception / honeypots ─────────────────────────────────────────────
|
|
267
|
+
{ binary: "ncat", candidates: ["/usr/bin/ncat", "/usr/local/bin/ncat"] },
|
|
268
|
+
{ binary: "inotifywait", candidates: ["/usr/bin/inotifywait", "/usr/local/bin/inotifywait"] },
|
|
269
|
+
// ── Wireless security ─────────────────────────────────────────────────
|
|
270
|
+
{ binary: "hciconfig", candidates: ["/usr/bin/hciconfig", "/usr/sbin/hciconfig", "/bin/hciconfig"] },
|
|
271
|
+
{ binary: "bluetoothctl", candidates: ["/usr/bin/bluetoothctl"] },
|
|
272
|
+
{ binary: "iwconfig", candidates: ["/usr/sbin/iwconfig", "/sbin/iwconfig"] },
|
|
273
|
+
{ binary: "iw", candidates: ["/usr/sbin/iw", "/sbin/iw", "/usr/bin/iw"] },
|
|
274
|
+
{ binary: "nmcli", candidates: ["/usr/bin/nmcli"] },
|
|
275
|
+
{ binary: "rfkill", candidates: ["/usr/sbin/rfkill", "/usr/bin/rfkill", "/sbin/rfkill"] },
|
|
276
|
+
// ── SIEM integration ──────────────────────────────────────────────────
|
|
277
|
+
{ binary: "nc", candidates: ["/usr/bin/nc", "/bin/nc", "/usr/bin/nc.openbsd", "/usr/bin/nc.traditional"] },
|
|
278
|
+
{ binary: "logger", candidates: ["/usr/bin/logger", "/bin/logger"] },
|
|
279
|
+
{ binary: "filebeat", candidates: ["/usr/bin/filebeat", "/usr/local/bin/filebeat"] },
|
|
280
|
+
// ── Certificate lifecycle ─────────────────────────────────────────────
|
|
281
|
+
{ binary: "certbot", candidates: ["/usr/bin/certbot", "/usr/local/bin/certbot", "/snap/bin/certbot"] },
|
|
282
|
+
// ── Forensics / incident response ─────────────────────────────────────
|
|
283
|
+
{ binary: "avml", candidates: ["/usr/bin/avml", "/usr/local/bin/avml"] },
|
|
284
|
+
{ binary: "dd", candidates: ["/usr/bin/dd", "/bin/dd"] },
|
|
285
|
+
{ binary: "fdisk", candidates: ["/usr/sbin/fdisk", "/sbin/fdisk"] },
|
|
286
|
+
// ── Network segmentation ──────────────────────────────────────────────
|
|
287
|
+
{ binary: "traceroute", candidates: ["/usr/bin/traceroute", "/usr/sbin/traceroute", "/bin/traceroute"] },
|
|
288
|
+
{ binary: "tracepath", candidates: ["/usr/bin/tracepath", "/bin/tracepath"] },
|
|
289
|
+
{ binary: "bridge", candidates: ["/usr/sbin/bridge", "/sbin/bridge"] },
|
|
290
|
+
// ── USB device control ────────────────────────────────────────────────
|
|
291
|
+
{ binary: "lsusb", candidates: ["/usr/bin/lsusb", "/bin/lsusb"] },
|
|
292
|
+
{ binary: "udevadm", candidates: ["/usr/bin/udevadm", "/bin/udevadm", "/sbin/udevadm"] },
|
|
293
|
+
// ── Reporting (optional conversion tools) ─────────────────────────────
|
|
294
|
+
{ binary: "pandoc", candidates: ["/usr/bin/pandoc", "/usr/local/bin/pandoc"] },
|
|
295
|
+
{ binary: "wkhtmltopdf", candidates: ["/usr/bin/wkhtmltopdf", "/usr/local/bin/wkhtmltopdf"] },
|
|
296
|
+
// ── General utilities ─────────────────────────────────────────────────
|
|
297
|
+
{ binary: "sh", candidates: ["/usr/bin/sh", "/bin/sh"] },
|
|
298
|
+
{ binary: "kill", candidates: ["/usr/bin/kill", "/bin/kill"] },
|
|
299
|
+
];
|
|
300
|
+
// ── Module-level constants for resolveSudoCommand() (Opt 6) ──────────────────
|
|
301
|
+
/** Sudo flags that take NO argument */
|
|
302
|
+
const SUDO_FLAGS_NO_ARG = new Set([
|
|
303
|
+
"-S", "-A", "-k", "-K", "-n", "-v", "-b", "-e", "-H", "-i", "-l", "-s",
|
|
304
|
+
"--stdin", "--askpass", "--reset-timestamp", "--remove-timestamp",
|
|
305
|
+
"--non-interactive", "--validate", "--background", "--edit",
|
|
306
|
+
"--set-home", "--login", "--list", "--shell",
|
|
307
|
+
]);
|
|
308
|
+
/** Sudo flags that take an argument (the next token) */
|
|
309
|
+
const SUDO_FLAGS_WITH_ARG = new Set([
|
|
310
|
+
"-p", "-u", "-g", "-C", "-T", "-r",
|
|
311
|
+
"--prompt", "--user", "--group", "--close-from", "--command-timeout", "--role",
|
|
312
|
+
]);
|
|
313
|
+
// ── Internal state ───────────────────────────────────────────────────────────
|
|
314
|
+
/** O(1) lookup by bare binary name. */
|
|
315
|
+
const allowlistMap = new Map();
|
|
316
|
+
/** O(1) reverse lookup: absolute path → AllowlistEntry */
|
|
317
|
+
const reversePathMap = new Map();
|
|
318
|
+
/** Whether `initializeAllowlist()` has been called. */
|
|
319
|
+
let initialized = false;
|
|
320
|
+
/**
|
|
321
|
+
* SECURITY (CORE-007): Whether to re-verify binary paths at execution time.
|
|
322
|
+
* When enabled, resolveCommand() confirms that the binary at the resolved path
|
|
323
|
+
* still exists as a regular file with the same inode as recorded at startup,
|
|
324
|
+
* mitigating TOCTOU (time-of-check-time-of-use) attacks where a binary is
|
|
325
|
+
* replaced between startup resolution and runtime execution.
|
|
326
|
+
*
|
|
327
|
+
* Configurable via KALI_DEFENSE_RUNTIME_PATH_VERIFY env var.
|
|
328
|
+
* Default: true (verify at runtime). Set to "false" to disable for performance.
|
|
329
|
+
*/
|
|
330
|
+
let runtimePathVerification = process.env.KALI_DEFENSE_RUNTIME_PATH_VERIFY !== "false";
|
|
331
|
+
// Populate the map immediately so `isAllowlisted()` works before init
|
|
332
|
+
for (const entry of ALLOWLIST_DEFINITIONS) {
|
|
333
|
+
allowlistMap.set(entry.binary, entry);
|
|
334
|
+
}
|
|
335
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
336
|
+
/**
|
|
337
|
+
* Initialize the allowlist by resolving candidate paths on the current system.
|
|
338
|
+
*
|
|
339
|
+
* For each allowlisted binary, checks which candidate paths actually exist
|
|
340
|
+
* on disk and caches the first match. This should be called once at server
|
|
341
|
+
* startup, before any tool registration.
|
|
342
|
+
*
|
|
343
|
+
* Binaries that cannot be found are logged as warnings but do not prevent
|
|
344
|
+
* startup — not every system has every tool installed.
|
|
345
|
+
*/
|
|
346
|
+
export function initializeAllowlist() {
|
|
347
|
+
let resolved = 0;
|
|
348
|
+
let unresolved = 0;
|
|
349
|
+
// Clear reverse map for re-initialization
|
|
350
|
+
reversePathMap.clear();
|
|
351
|
+
// Re-read runtime path verification setting on re-init
|
|
352
|
+
runtimePathVerification = process.env.KALI_DEFENSE_RUNTIME_PATH_VERIFY !== "false";
|
|
353
|
+
for (const entry of ALLOWLIST_DEFINITIONS) {
|
|
354
|
+
entry.resolvedPath = undefined;
|
|
355
|
+
entry.resolvedInode = undefined;
|
|
356
|
+
for (const candidate of entry.candidates) {
|
|
357
|
+
if (existsSync(candidate)) {
|
|
358
|
+
entry.resolvedPath = candidate;
|
|
359
|
+
// SECURITY (CORE-007): Record inode at startup for TOCTOU detection
|
|
360
|
+
try {
|
|
361
|
+
const stats = lstatSync(candidate);
|
|
362
|
+
entry.resolvedInode = stats.ino;
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// Best effort — inode recording is not critical
|
|
366
|
+
}
|
|
367
|
+
resolved++;
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (!entry.resolvedPath) {
|
|
372
|
+
unresolved++;
|
|
373
|
+
}
|
|
374
|
+
// Populate reverse path map for O(1) absolute-path lookups
|
|
375
|
+
if (entry.resolvedPath) {
|
|
376
|
+
reversePathMap.set(entry.resolvedPath, entry);
|
|
377
|
+
}
|
|
378
|
+
for (const candidate of entry.candidates) {
|
|
379
|
+
if (existsSync(candidate)) {
|
|
380
|
+
reversePathMap.set(candidate, entry);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
initialized = true;
|
|
385
|
+
console.error(`[command-allowlist] Initialized: ${resolved} binaries resolved, ` +
|
|
386
|
+
`${unresolved} not found on this system (${ALLOWLIST_DEFINITIONS.length} total allowlisted)` +
|
|
387
|
+
(runtimePathVerification ? " [runtime path verification ON]" : " [runtime path verification OFF]"));
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Resolve a bare command name to its absolute path via the allowlist.
|
|
391
|
+
*
|
|
392
|
+
* @param command - Bare binary name (e.g. `"iptables"`)
|
|
393
|
+
* @returns The absolute path to the binary (e.g. `"/usr/sbin/iptables"`)
|
|
394
|
+
* @throws {Error} If the command is not in the allowlist or cannot be found
|
|
395
|
+
*/
|
|
396
|
+
export function resolveCommand(command) {
|
|
397
|
+
// If the command is already an absolute path, use O(1) reverse lookup
|
|
398
|
+
if (command.startsWith("/")) {
|
|
399
|
+
if (reversePathMap.has(command)) {
|
|
400
|
+
// SECURITY (CORE-007): Re-verify at runtime if enabled
|
|
401
|
+
if (runtimePathVerification) {
|
|
402
|
+
verifyBinaryPathAtRuntime(command, reversePathMap.get(command));
|
|
403
|
+
}
|
|
404
|
+
return command;
|
|
405
|
+
}
|
|
406
|
+
// Fallback: linear scan for paths not yet in the reverse map
|
|
407
|
+
// (e.g., binaries installed after initializeAllowlist)
|
|
408
|
+
for (const entry of ALLOWLIST_DEFINITIONS) {
|
|
409
|
+
if (entry.candidates.includes(command) || entry.resolvedPath === command) {
|
|
410
|
+
reversePathMap.set(command, entry); // cache for next time
|
|
411
|
+
return command;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
throw new Error(`Command not in allowlist: ${command}. ` +
|
|
415
|
+
`Only pre-approved security binaries may be executed.`);
|
|
416
|
+
}
|
|
417
|
+
const entry = allowlistMap.get(command);
|
|
418
|
+
if (!entry) {
|
|
419
|
+
throw new Error(`Command not in allowlist: ${command}. ` +
|
|
420
|
+
`Only pre-approved security binaries may be executed.`);
|
|
421
|
+
}
|
|
422
|
+
// If already resolved (from initializeAllowlist), return cached path
|
|
423
|
+
if (entry.resolvedPath) {
|
|
424
|
+
// SECURITY (CORE-007): Re-verify at runtime if enabled
|
|
425
|
+
if (runtimePathVerification) {
|
|
426
|
+
verifyBinaryPathAtRuntime(entry.resolvedPath, entry);
|
|
427
|
+
}
|
|
428
|
+
return entry.resolvedPath;
|
|
429
|
+
}
|
|
430
|
+
// Lazy resolution: if initializeAllowlist() hasn't run or if the binary
|
|
431
|
+
// was installed after startup, try resolving now
|
|
432
|
+
for (const candidate of entry.candidates) {
|
|
433
|
+
if (existsSync(candidate)) {
|
|
434
|
+
entry.resolvedPath = candidate;
|
|
435
|
+
return candidate;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
throw new Error(`Allowlisted command '${command}' not found on this system. ` +
|
|
439
|
+
`Checked paths: ${entry.candidates.join(", ")}`);
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Check whether a bare command name is in the allowlist (without resolving).
|
|
443
|
+
*
|
|
444
|
+
* @param command - Bare binary name or absolute path
|
|
445
|
+
* @returns `true` if the command is allowlisted
|
|
446
|
+
*/
|
|
447
|
+
export function isAllowlisted(command) {
|
|
448
|
+
if (command.startsWith("/")) {
|
|
449
|
+
// O(1) reverse lookup first
|
|
450
|
+
if (reversePathMap.has(command))
|
|
451
|
+
return true;
|
|
452
|
+
// Fallback: linear scan for paths not in the reverse map
|
|
453
|
+
for (const entry of ALLOWLIST_DEFINITIONS) {
|
|
454
|
+
if (entry.candidates.includes(command) || entry.resolvedPath === command) {
|
|
455
|
+
reversePathMap.set(command, entry); // cache for next time
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
return allowlistMap.has(command);
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Resolve a sudo command and its target binary.
|
|
465
|
+
*
|
|
466
|
+
* When `command` is `"sudo"`, this function:
|
|
467
|
+
* 1. Resolves `sudo` itself to its absolute path
|
|
468
|
+
* 2. Finds the actual binary in the args array (skipping sudo flags like `-S`, `-p`, `-A`, `-k`, `-n`, `-v`)
|
|
469
|
+
* 3. Resolves that binary against the allowlist
|
|
470
|
+
* 4. Returns the resolved sudo path, the index of the target binary in args, and its resolved path
|
|
471
|
+
*
|
|
472
|
+
* @param args - The args array passed to sudo
|
|
473
|
+
* @returns Object with resolved paths and the index of the target command in args
|
|
474
|
+
* @throws {Error} If sudo or the target command is not allowlisted
|
|
475
|
+
*/
|
|
476
|
+
export function resolveSudoCommand(args) {
|
|
477
|
+
const sudoPath = resolveCommand("sudo");
|
|
478
|
+
// Find the target command in args by skipping sudo flags
|
|
479
|
+
// Uses module-level SUDO_FLAGS_NO_ARG / SUDO_FLAGS_WITH_ARG constants
|
|
480
|
+
let targetIndex = -1;
|
|
481
|
+
for (let i = 0; i < args.length; i++) {
|
|
482
|
+
const arg = args[i];
|
|
483
|
+
// Skip empty strings (like empty prompt "")
|
|
484
|
+
if (arg === "") {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
// Skip known flags
|
|
488
|
+
if (SUDO_FLAGS_NO_ARG.has(arg)) {
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (SUDO_FLAGS_WITH_ARG.has(arg)) {
|
|
492
|
+
// Skip the flag AND its argument
|
|
493
|
+
i++;
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
// If it starts with "-", it's an unknown flag — skip it
|
|
497
|
+
if (arg.startsWith("-")) {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
// This is the target command
|
|
501
|
+
targetIndex = i;
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
if (targetIndex === -1) {
|
|
505
|
+
// No target command found (e.g., `sudo -v` or `sudo -k`)
|
|
506
|
+
// These are sudo self-management commands, just return sudo
|
|
507
|
+
return { sudoPath, targetIndex: -1, targetPath: "" };
|
|
508
|
+
}
|
|
509
|
+
const targetCmd = args[targetIndex];
|
|
510
|
+
const targetPath = resolveCommand(targetCmd);
|
|
511
|
+
return { sudoPath, targetIndex, targetPath };
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Returns the full allowlist for inspection/debugging.
|
|
515
|
+
* Each entry includes resolution status.
|
|
516
|
+
*/
|
|
517
|
+
export function getAllowlistEntries() {
|
|
518
|
+
return ALLOWLIST_DEFINITIONS;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Returns whether the allowlist has been initialized.
|
|
522
|
+
*/
|
|
523
|
+
export function isInitialized() {
|
|
524
|
+
return initialized;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Returns whether runtime path verification is currently enabled.
|
|
528
|
+
*/
|
|
529
|
+
export function isRuntimePathVerificationEnabled() {
|
|
530
|
+
return runtimePathVerification;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Enable or disable runtime path verification.
|
|
534
|
+
* Useful for testing or performance-sensitive environments.
|
|
535
|
+
*/
|
|
536
|
+
export function setRuntimePathVerification(enabled) {
|
|
537
|
+
runtimePathVerification = enabled;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* SECURITY (CORE-007): Re-verify a binary path at execution time.
|
|
541
|
+
*
|
|
542
|
+
* Mitigates TOCTOU attacks where a binary resolved at startup could be
|
|
543
|
+
* replaced before runtime execution. Checks:
|
|
544
|
+
* 1. File still exists at the resolved path
|
|
545
|
+
* 2. File is a regular file (not a symlink to something else)
|
|
546
|
+
* 3. Inode matches what was recorded at startup (if available)
|
|
547
|
+
*
|
|
548
|
+
* @param binaryPath The absolute path to verify
|
|
549
|
+
* @param entry The allowlist entry with startup inode data
|
|
550
|
+
* @throws {Error} If the binary fails verification
|
|
551
|
+
*/
|
|
552
|
+
function verifyBinaryPathAtRuntime(binaryPath, entry) {
|
|
553
|
+
try {
|
|
554
|
+
const stats = lstatSync(binaryPath);
|
|
555
|
+
// Must still be a regular file (not replaced with a symlink)
|
|
556
|
+
if (!stats.isFile()) {
|
|
557
|
+
throw new Error(`SECURITY: Binary '${binaryPath}' is no longer a regular file ` +
|
|
558
|
+
`(may have been replaced with a symlink). Refusing to execute.`);
|
|
559
|
+
}
|
|
560
|
+
// If we recorded an inode at startup, verify it hasn't changed
|
|
561
|
+
if (entry.resolvedInode !== undefined && stats.ino !== entry.resolvedInode) {
|
|
562
|
+
throw new Error(`SECURITY: Binary '${binaryPath}' inode changed since startup ` +
|
|
563
|
+
`(was ${entry.resolvedInode}, now ${stats.ino}). Binary may have been replaced. Refusing to execute.`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
catch (err) {
|
|
567
|
+
if (err instanceof Error && err.message.startsWith("SECURITY:")) {
|
|
568
|
+
throw err; // Re-throw our own security errors
|
|
569
|
+
}
|
|
570
|
+
// File doesn't exist or can't be stat'd
|
|
571
|
+
throw new Error(`SECURITY: Binary '${binaryPath}' could not be verified at runtime: ` +
|
|
572
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Returns the critical binary package mappings for inspection/testing.
|
|
577
|
+
*/
|
|
578
|
+
export function getCriticalBinaryPackages() {
|
|
579
|
+
return CRITICAL_BINARY_PACKAGES;
|
|
580
|
+
}
|
|
581
|
+
// ── Binary Integrity Verification ────────────────────────────────────────────
|
|
582
|
+
/**
|
|
583
|
+
* Detect which package manager verification command is available.
|
|
584
|
+
* Returns the command prefix to use, or null if none found.
|
|
585
|
+
*/
|
|
586
|
+
function detectPackageVerifier() {
|
|
587
|
+
if (existsSync("/usr/bin/dpkg") || existsSync("/bin/dpkg"))
|
|
588
|
+
return "dpkg";
|
|
589
|
+
if (existsSync("/usr/bin/rpm") || existsSync("/bin/rpm"))
|
|
590
|
+
return "rpm";
|
|
591
|
+
if (existsSync("/usr/bin/pacman"))
|
|
592
|
+
return "pacman";
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Verify that a resolved binary is owned by its expected system package.
|
|
597
|
+
*
|
|
598
|
+
* Uses `dpkg -S` on Debian/Ubuntu, `rpm -qf` on RHEL/Fedora,
|
|
599
|
+
* or `pacman -Qo` on Arch to determine the owning package.
|
|
600
|
+
*
|
|
601
|
+
* @param binaryPath - Absolute path to the binary
|
|
602
|
+
* @param expectedPackage - Optional expected package name; if omitted, only ownership is checked
|
|
603
|
+
* @returns Verification result with owner info and status
|
|
604
|
+
*/
|
|
605
|
+
export function verifyBinaryOwnership(binaryPath, expectedPackage) {
|
|
606
|
+
const binary = binaryPath.split("/").pop() ?? binaryPath;
|
|
607
|
+
if (!existsSync(binaryPath)) {
|
|
608
|
+
return {
|
|
609
|
+
binary,
|
|
610
|
+
path: binaryPath,
|
|
611
|
+
verified: false,
|
|
612
|
+
message: `Binary not found at ${binaryPath}`,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
const verifier = detectPackageVerifier();
|
|
616
|
+
if (!verifier) {
|
|
617
|
+
return {
|
|
618
|
+
binary,
|
|
619
|
+
path: binaryPath,
|
|
620
|
+
verified: false,
|
|
621
|
+
message: "No package manager found for verification (need dpkg, rpm, or pacman)",
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
try {
|
|
625
|
+
let output;
|
|
626
|
+
let ownerPackage;
|
|
627
|
+
switch (verifier) {
|
|
628
|
+
case "dpkg": {
|
|
629
|
+
// dpkg -S /path/to/binary → "package-name: /path/to/binary"
|
|
630
|
+
const dpkgPath = existsSync("/usr/bin/dpkg") ? "/usr/bin/dpkg" : "/bin/dpkg";
|
|
631
|
+
output = execFileSync(dpkgPath, ["-S", binaryPath], {
|
|
632
|
+
encoding: "utf-8",
|
|
633
|
+
timeout: 10_000,
|
|
634
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
635
|
+
}).trim();
|
|
636
|
+
// Parse "package:arch: /path" or "package: /path"
|
|
637
|
+
const dpkgMatch = output.match(/^([^:]+?)(?::[^:]+)?:\s/);
|
|
638
|
+
ownerPackage = dpkgMatch ? dpkgMatch[1].trim() : output.split(":")[0].trim();
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
case "rpm": {
|
|
642
|
+
// rpm -qf /path/to/binary → "package-name-version-release.arch"
|
|
643
|
+
const rpmPath = existsSync("/usr/bin/rpm") ? "/usr/bin/rpm" : "/bin/rpm";
|
|
644
|
+
output = execFileSync(rpmPath, ["-qf", binaryPath], {
|
|
645
|
+
encoding: "utf-8",
|
|
646
|
+
timeout: 10_000,
|
|
647
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
648
|
+
}).trim();
|
|
649
|
+
// Extract package name (strip version-release.arch)
|
|
650
|
+
const rpmMatch = output.match(/^(.+?)-\d/);
|
|
651
|
+
ownerPackage = rpmMatch ? rpmMatch[1] : output;
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
case "pacman": {
|
|
655
|
+
// pacman -Qo /path/to/binary → "/path/to/binary is owned by package-name version"
|
|
656
|
+
output = execFileSync("/usr/bin/pacman", ["-Qo", binaryPath], {
|
|
657
|
+
encoding: "utf-8",
|
|
658
|
+
timeout: 10_000,
|
|
659
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
660
|
+
}).trim();
|
|
661
|
+
const pacmanMatch = output.match(/is owned by (\S+)/);
|
|
662
|
+
ownerPackage = pacmanMatch ? pacmanMatch[1] : output;
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
// If no expectedPackage specified, just report ownership
|
|
667
|
+
if (!expectedPackage) {
|
|
668
|
+
return {
|
|
669
|
+
binary,
|
|
670
|
+
path: binaryPath,
|
|
671
|
+
verified: true,
|
|
672
|
+
owner: ownerPackage,
|
|
673
|
+
message: `Owned by package: ${ownerPackage}`,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
// Check if the owner matches one of the expected packages
|
|
677
|
+
const expectedPackages = CRITICAL_BINARY_PACKAGES[binary] ?? [expectedPackage];
|
|
678
|
+
const isExpected = expectedPackages.some((pkg) => ownerPackage === pkg || ownerPackage.startsWith(`${pkg}:`));
|
|
679
|
+
if (isExpected) {
|
|
680
|
+
return {
|
|
681
|
+
binary,
|
|
682
|
+
path: binaryPath,
|
|
683
|
+
verified: true,
|
|
684
|
+
owner: ownerPackage,
|
|
685
|
+
message: `Verified: owned by expected package ${ownerPackage}`,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
return {
|
|
689
|
+
binary,
|
|
690
|
+
path: binaryPath,
|
|
691
|
+
verified: false,
|
|
692
|
+
owner: ownerPackage,
|
|
693
|
+
message: `WARNING: owned by '${ownerPackage}', expected one of [${expectedPackages.join(", ")}]`,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
catch (err) {
|
|
697
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
698
|
+
return {
|
|
699
|
+
binary,
|
|
700
|
+
path: binaryPath,
|
|
701
|
+
verified: false,
|
|
702
|
+
message: `Verification failed: ${msg}`,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Async version of binary ownership verification using execFile (non-blocking).
|
|
708
|
+
* Used by the parallelized `verifyAllBinaries()`.
|
|
709
|
+
*/
|
|
710
|
+
async function verifyBinaryOwnershipAsync(binaryPath, expectedPackage) {
|
|
711
|
+
const binary = binaryPath.split("/").pop() ?? binaryPath;
|
|
712
|
+
if (!existsSync(binaryPath)) {
|
|
713
|
+
return {
|
|
714
|
+
binary,
|
|
715
|
+
path: binaryPath,
|
|
716
|
+
verified: false,
|
|
717
|
+
message: `Binary not found at ${binaryPath}`,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
const verifier = detectPackageVerifier();
|
|
721
|
+
if (!verifier) {
|
|
722
|
+
return {
|
|
723
|
+
binary,
|
|
724
|
+
path: binaryPath,
|
|
725
|
+
verified: false,
|
|
726
|
+
message: "No package manager found for verification (need dpkg, rpm, or pacman)",
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
try {
|
|
730
|
+
let output;
|
|
731
|
+
let ownerPackage;
|
|
732
|
+
switch (verifier) {
|
|
733
|
+
case "dpkg": {
|
|
734
|
+
const dpkgPath = existsSync("/usr/bin/dpkg") ? "/usr/bin/dpkg" : "/bin/dpkg";
|
|
735
|
+
const { stdout } = await execFileAsync(dpkgPath, ["-S", binaryPath], {
|
|
736
|
+
encoding: "utf-8",
|
|
737
|
+
timeout: 10_000,
|
|
738
|
+
});
|
|
739
|
+
output = stdout.trim();
|
|
740
|
+
const dpkgMatch = output.match(/^([^:]+?)(?::[^:]+)?:\s/);
|
|
741
|
+
ownerPackage = dpkgMatch ? dpkgMatch[1].trim() : output.split(":")[0].trim();
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
case "rpm": {
|
|
745
|
+
const rpmPath = existsSync("/usr/bin/rpm") ? "/usr/bin/rpm" : "/bin/rpm";
|
|
746
|
+
const { stdout } = await execFileAsync(rpmPath, ["-qf", binaryPath], {
|
|
747
|
+
encoding: "utf-8",
|
|
748
|
+
timeout: 10_000,
|
|
749
|
+
});
|
|
750
|
+
output = stdout.trim();
|
|
751
|
+
const rpmMatch = output.match(/^(.+?)-\d/);
|
|
752
|
+
ownerPackage = rpmMatch ? rpmMatch[1] : output;
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
case "pacman": {
|
|
756
|
+
const { stdout } = await execFileAsync("/usr/bin/pacman", ["-Qo", binaryPath], {
|
|
757
|
+
encoding: "utf-8",
|
|
758
|
+
timeout: 10_000,
|
|
759
|
+
});
|
|
760
|
+
output = stdout.trim();
|
|
761
|
+
const pacmanMatch = output.match(/is owned by (\S+)/);
|
|
762
|
+
ownerPackage = pacmanMatch ? pacmanMatch[1] : output;
|
|
763
|
+
break;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (!expectedPackage) {
|
|
767
|
+
return {
|
|
768
|
+
binary,
|
|
769
|
+
path: binaryPath,
|
|
770
|
+
verified: true,
|
|
771
|
+
owner: ownerPackage,
|
|
772
|
+
message: `Owned by package: ${ownerPackage}`,
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
const expectedPackages = CRITICAL_BINARY_PACKAGES[binary] ?? [expectedPackage];
|
|
776
|
+
const isExpected = expectedPackages.some((pkg) => ownerPackage === pkg || ownerPackage.startsWith(`${pkg}:`));
|
|
777
|
+
if (isExpected) {
|
|
778
|
+
return {
|
|
779
|
+
binary,
|
|
780
|
+
path: binaryPath,
|
|
781
|
+
verified: true,
|
|
782
|
+
owner: ownerPackage,
|
|
783
|
+
message: `Verified: owned by expected package ${ownerPackage}`,
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
return {
|
|
787
|
+
binary,
|
|
788
|
+
path: binaryPath,
|
|
789
|
+
verified: false,
|
|
790
|
+
owner: ownerPackage,
|
|
791
|
+
message: `WARNING: owned by '${ownerPackage}', expected one of [${expectedPackages.join(", ")}]`,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
catch (err) {
|
|
795
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
796
|
+
return {
|
|
797
|
+
binary,
|
|
798
|
+
path: binaryPath,
|
|
799
|
+
verified: false,
|
|
800
|
+
message: `Verification failed: ${msg}`,
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Verify all resolved critical binaries against their expected packages.
|
|
806
|
+
*
|
|
807
|
+
* Runs after `initializeAllowlist()` and logs warnings for any binaries
|
|
808
|
+
* that can't be verified or are owned by unexpected packages.
|
|
809
|
+
*
|
|
810
|
+
* All verifications run in parallel for faster startup.
|
|
811
|
+
* This is best-effort — it never throws or blocks startup.
|
|
812
|
+
*
|
|
813
|
+
* @returns Array of verification results for all critical binaries that were resolved
|
|
814
|
+
*/
|
|
815
|
+
export async function verifyAllBinaries() {
|
|
816
|
+
let skipped = 0;
|
|
817
|
+
// Build list of verification tasks to run in parallel
|
|
818
|
+
const tasks = [];
|
|
819
|
+
for (const [binaryName, expectedPackages] of Object.entries(CRITICAL_BINARY_PACKAGES)) {
|
|
820
|
+
const entry = allowlistMap.get(binaryName);
|
|
821
|
+
if (!entry?.resolvedPath) {
|
|
822
|
+
skipped++;
|
|
823
|
+
continue; // Binary not found on system — nothing to verify
|
|
824
|
+
}
|
|
825
|
+
tasks.push(verifyBinaryOwnershipAsync(entry.resolvedPath, expectedPackages[0])
|
|
826
|
+
.catch(() => ({
|
|
827
|
+
binary: binaryName,
|
|
828
|
+
path: entry.resolvedPath,
|
|
829
|
+
verified: false,
|
|
830
|
+
message: `Verification failed unexpectedly`,
|
|
831
|
+
})));
|
|
832
|
+
}
|
|
833
|
+
// Run all verifications in parallel
|
|
834
|
+
const results = await Promise.all(tasks);
|
|
835
|
+
let verified = 0;
|
|
836
|
+
let warnings = 0;
|
|
837
|
+
for (const result of results) {
|
|
838
|
+
if (result.verified) {
|
|
839
|
+
verified++;
|
|
840
|
+
}
|
|
841
|
+
else {
|
|
842
|
+
warnings++;
|
|
843
|
+
console.error(`[binary-integrity] ⚠ ${result.message}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
console.error(`[binary-integrity] Checked ${results.length} critical binaries: ` +
|
|
847
|
+
`${verified} verified, ${warnings} warnings, ${skipped} skipped (not installed)`);
|
|
848
|
+
return results;
|
|
849
|
+
}
|