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,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* encrypted-state.ts — Encrypted storage for sensitive state data.
|
|
3
|
+
*
|
|
4
|
+
* Provides AES-256-GCM encrypted at-rest storage for rollback data,
|
|
5
|
+
* policy files, sudo session tokens, and other sensitive state.
|
|
6
|
+
*
|
|
7
|
+
* Key derivation uses PBKDF2 from a configurable secret via the
|
|
8
|
+
* `KALI_DEFENSE_STATE_KEY` environment variable. If no key is
|
|
9
|
+
* configured, falls back to unencrypted mode with a warning.
|
|
10
|
+
*
|
|
11
|
+
* @module encrypted-state
|
|
12
|
+
*/
|
|
13
|
+
import { randomBytes, createCipheriv, createDecipheriv, pbkdf2Sync, } from "node:crypto";
|
|
14
|
+
import { readFileSync, unlinkSync, existsSync, mkdirSync, chmodSync, } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { logger } from "./logger.js";
|
|
17
|
+
import { atomicWriteFileSync } from "./secure-fs.js";
|
|
18
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
19
|
+
/** AES-256-GCM algorithm identifier. */
|
|
20
|
+
const ALGORITHM = "aes-256-gcm";
|
|
21
|
+
/** Key length in bytes (256 bits). */
|
|
22
|
+
const KEY_LENGTH = 32;
|
|
23
|
+
/** IV length in bytes (96 bits recommended for GCM). */
|
|
24
|
+
const IV_LENGTH = 12;
|
|
25
|
+
/** Auth tag length in bytes. */
|
|
26
|
+
const AUTH_TAG_LENGTH = 16;
|
|
27
|
+
/** PBKDF2 iteration count. */
|
|
28
|
+
const PBKDF2_ITERATIONS = 100_000;
|
|
29
|
+
/** PBKDF2 digest algorithm. */
|
|
30
|
+
const PBKDF2_DIGEST = "sha512";
|
|
31
|
+
/** Salt length in bytes. */
|
|
32
|
+
const SALT_LENGTH = 16;
|
|
33
|
+
/** Default state directory. */
|
|
34
|
+
const DEFAULT_STATE_DIR = "/tmp/kali-defense/state/";
|
|
35
|
+
/** File permission: owner read/write only. */
|
|
36
|
+
const SECURE_FILE_MODE = 0o600;
|
|
37
|
+
/** Directory permission: owner read/write/execute only. */
|
|
38
|
+
const SECURE_DIR_MODE = 0o700;
|
|
39
|
+
/** Environment variable name for the encryption key. */
|
|
40
|
+
const ENV_KEY_NAME = "KALI_DEFENSE_STATE_KEY";
|
|
41
|
+
// ── Encrypted file format ────────────────────────────────────────────────────
|
|
42
|
+
// Binary layout: [salt (16)] [iv (12)] [authTag (16)] [ciphertext (...)]
|
|
43
|
+
// JSON fallback (unencrypted): plain JSON text
|
|
44
|
+
// ── SecureStateStore ─────────────────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* Encrypted state storage for sensitive data at rest.
|
|
47
|
+
*
|
|
48
|
+
* Uses AES-256-GCM with PBKDF2-derived keys when `KALI_DEFENSE_STATE_KEY`
|
|
49
|
+
* is set. Falls back to plaintext JSON when no key is configured.
|
|
50
|
+
*/
|
|
51
|
+
export class SecureStateStore {
|
|
52
|
+
stateDir;
|
|
53
|
+
secret;
|
|
54
|
+
/**
|
|
55
|
+
* @param stateDir - Directory for state files (default: `/tmp/kali-defense/state/`)
|
|
56
|
+
* @param secret - Encryption secret. If omitted, reads from `KALI_DEFENSE_STATE_KEY` env var.
|
|
57
|
+
* Pass empty string or omit to use unencrypted fallback.
|
|
58
|
+
*/
|
|
59
|
+
constructor(stateDir, secret) {
|
|
60
|
+
this.stateDir = stateDir ?? DEFAULT_STATE_DIR;
|
|
61
|
+
// Determine secret: explicit parameter > env var > null (unencrypted)
|
|
62
|
+
if (secret !== undefined) {
|
|
63
|
+
this.secret = secret.length > 0 ? secret : null;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const envKey = process.env[ENV_KEY_NAME];
|
|
67
|
+
this.secret = envKey && envKey.length > 0 ? envKey : null;
|
|
68
|
+
}
|
|
69
|
+
if (this.secret === null) {
|
|
70
|
+
logger.warn("encrypted-state", "init", "No encryption key configured — state files will be stored unencrypted. " +
|
|
71
|
+
`Set ${ENV_KEY_NAME} environment variable for encrypted storage.`);
|
|
72
|
+
}
|
|
73
|
+
// Ensure state directory exists with secure permissions
|
|
74
|
+
this.ensureStateDir();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Whether the store is operating in encrypted mode.
|
|
78
|
+
*/
|
|
79
|
+
get encrypted() {
|
|
80
|
+
return this.secret !== null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Save a state object to disk.
|
|
84
|
+
*
|
|
85
|
+
* @param id - Unique identifier for the state (used as filename stem)
|
|
86
|
+
* @param data - JSON-serializable object to persist
|
|
87
|
+
*/
|
|
88
|
+
save(id, data) {
|
|
89
|
+
const filePath = this.filePath(id);
|
|
90
|
+
const json = JSON.stringify(data);
|
|
91
|
+
if (this.secret !== null) {
|
|
92
|
+
const encrypted = this.encrypt(json);
|
|
93
|
+
atomicWriteFileSync(filePath, encrypted, { mode: SECURE_FILE_MODE });
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
atomicWriteFileSync(filePath, json, { mode: SECURE_FILE_MODE });
|
|
97
|
+
}
|
|
98
|
+
logger.debug("encrypted-state", "save", `State saved: ${id}`, { encrypted: this.encrypted });
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Load a state object from disk.
|
|
102
|
+
*
|
|
103
|
+
* @param id - Unique identifier for the state
|
|
104
|
+
* @returns The deserialized object, or `null` if the state file doesn't exist
|
|
105
|
+
*/
|
|
106
|
+
load(id) {
|
|
107
|
+
const filePath = this.filePath(id);
|
|
108
|
+
if (!existsSync(filePath)) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const raw = readFileSync(filePath);
|
|
112
|
+
if (this.secret !== null) {
|
|
113
|
+
const json = this.decrypt(raw);
|
|
114
|
+
return JSON.parse(json);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
return JSON.parse(raw.toString("utf-8"));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Delete a state file from disk.
|
|
122
|
+
*
|
|
123
|
+
* @param id - Unique identifier for the state to delete
|
|
124
|
+
*/
|
|
125
|
+
delete(id) {
|
|
126
|
+
const filePath = this.filePath(id);
|
|
127
|
+
if (existsSync(filePath)) {
|
|
128
|
+
unlinkSync(filePath);
|
|
129
|
+
logger.debug("encrypted-state", "delete", `State deleted: ${id}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// ── Private helpers ──────────────────────────────────────────────────────
|
|
133
|
+
/** Build the full file path for a state ID. */
|
|
134
|
+
filePath(id) {
|
|
135
|
+
// Sanitize id to prevent path traversal
|
|
136
|
+
const safeId = id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
137
|
+
return join(this.stateDir, `${safeId}.state`);
|
|
138
|
+
}
|
|
139
|
+
/** Ensure the state directory exists with secure permissions. */
|
|
140
|
+
ensureStateDir() {
|
|
141
|
+
if (!existsSync(this.stateDir)) {
|
|
142
|
+
mkdirSync(this.stateDir, { recursive: true, mode: SECURE_DIR_MODE });
|
|
143
|
+
}
|
|
144
|
+
chmodSync(this.stateDir, SECURE_DIR_MODE);
|
|
145
|
+
}
|
|
146
|
+
/** Derive an AES-256 key from the secret and a salt. */
|
|
147
|
+
deriveKey(salt) {
|
|
148
|
+
if (!this.secret) {
|
|
149
|
+
throw new Error("Cannot derive key: no secret configured");
|
|
150
|
+
}
|
|
151
|
+
return pbkdf2Sync(this.secret, salt, PBKDF2_ITERATIONS, KEY_LENGTH, PBKDF2_DIGEST);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Encrypt plaintext JSON using AES-256-GCM.
|
|
155
|
+
* Returns a Buffer: [salt (16)] [iv (12)] [authTag (16)] [ciphertext]
|
|
156
|
+
*/
|
|
157
|
+
encrypt(plaintext) {
|
|
158
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
159
|
+
const key = this.deriveKey(salt);
|
|
160
|
+
const iv = randomBytes(IV_LENGTH);
|
|
161
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
162
|
+
const encrypted = Buffer.concat([
|
|
163
|
+
cipher.update(plaintext, "utf-8"),
|
|
164
|
+
cipher.final(),
|
|
165
|
+
]);
|
|
166
|
+
const authTag = cipher.getAuthTag();
|
|
167
|
+
// Combine: salt + iv + authTag + ciphertext
|
|
168
|
+
return Buffer.concat([salt, iv, authTag, encrypted]);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Decrypt an AES-256-GCM encrypted buffer.
|
|
172
|
+
* Expects format: [salt (16)] [iv (12)] [authTag (16)] [ciphertext]
|
|
173
|
+
*/
|
|
174
|
+
decrypt(data) {
|
|
175
|
+
const minLength = SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH;
|
|
176
|
+
if (data.length < minLength) {
|
|
177
|
+
throw new Error("Corrupted state file: data too short to contain encryption headers");
|
|
178
|
+
}
|
|
179
|
+
let offset = 0;
|
|
180
|
+
const salt = data.subarray(offset, offset + SALT_LENGTH);
|
|
181
|
+
offset += SALT_LENGTH;
|
|
182
|
+
const iv = data.subarray(offset, offset + IV_LENGTH);
|
|
183
|
+
offset += IV_LENGTH;
|
|
184
|
+
const authTag = data.subarray(offset, offset + AUTH_TAG_LENGTH);
|
|
185
|
+
offset += AUTH_TAG_LENGTH;
|
|
186
|
+
const ciphertext = data.subarray(offset);
|
|
187
|
+
const key = this.deriveKey(salt);
|
|
188
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
189
|
+
decipher.setAuthTag(authTag);
|
|
190
|
+
try {
|
|
191
|
+
const decrypted = Buffer.concat([
|
|
192
|
+
decipher.update(ciphertext),
|
|
193
|
+
decipher.final(),
|
|
194
|
+
]);
|
|
195
|
+
return decrypted.toString("utf-8");
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
throw new Error("Failed to decrypt state file: invalid key or corrupted data");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// ── Singleton Export ─────────────────────────────────────────────────────────
|
|
203
|
+
/**
|
|
204
|
+
* Default singleton SecureStateStore instance.
|
|
205
|
+
*
|
|
206
|
+
* Uses the default state directory and reads the encryption key from
|
|
207
|
+
* the `KALI_DEFENSE_STATE_KEY` environment variable.
|
|
208
|
+
*/
|
|
209
|
+
export const secureState = new SecureStateStore();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for executing a command.
|
|
3
|
+
*/
|
|
4
|
+
export interface ExecuteOptions {
|
|
5
|
+
/** The command binary to execute */
|
|
6
|
+
command: string;
|
|
7
|
+
/** Arguments to pass to the command */
|
|
8
|
+
args: string[];
|
|
9
|
+
/** Timeout in milliseconds (overrides default) */
|
|
10
|
+
timeout?: number;
|
|
11
|
+
/** Working directory for the command */
|
|
12
|
+
cwd?: string;
|
|
13
|
+
/** Additional environment variables */
|
|
14
|
+
env?: Record<string, string>;
|
|
15
|
+
/** Data to pipe to stdin (Buffer preferred for sensitive data like passwords) */
|
|
16
|
+
stdin?: string | Buffer;
|
|
17
|
+
/** Maximum output buffer size in bytes */
|
|
18
|
+
maxBuffer?: number;
|
|
19
|
+
/** Tool name for timeout lookup */
|
|
20
|
+
toolName?: string;
|
|
21
|
+
/** Skip automatic sudo credential injection (used internally) */
|
|
22
|
+
skipSudoInjection?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Result of a command execution.
|
|
26
|
+
*/
|
|
27
|
+
export interface CommandResult {
|
|
28
|
+
/** Standard output content */
|
|
29
|
+
stdout: string;
|
|
30
|
+
/** Standard error content */
|
|
31
|
+
stderr: string;
|
|
32
|
+
/** Process exit code (124 on timeout) */
|
|
33
|
+
exitCode: number;
|
|
34
|
+
/** Whether the command was killed due to timeout */
|
|
35
|
+
timedOut: boolean;
|
|
36
|
+
/** Wall-clock duration in milliseconds */
|
|
37
|
+
duration: number;
|
|
38
|
+
/**
|
|
39
|
+
* Whether the command failed due to insufficient privileges.
|
|
40
|
+
* Detected by analyzing stderr/stdout against known permission error patterns.
|
|
41
|
+
* When `true`, the caller should prompt the user to call `sudo_elevate`.
|
|
42
|
+
*/
|
|
43
|
+
permissionDenied: boolean;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Executes a command safely using spawn with shell: false.
|
|
47
|
+
*
|
|
48
|
+
* - Transparently injects sudo credentials from SudoSession when available
|
|
49
|
+
* - Uses AbortController for timeout enforcement
|
|
50
|
+
* - Caps stdout/stderr buffers to maxBuffer
|
|
51
|
+
* - Tracks execution duration
|
|
52
|
+
* - Handles stdin piping
|
|
53
|
+
* - Catches spawn errors gracefully
|
|
54
|
+
*/
|
|
55
|
+
export declare function executeCommand(options: ExecuteOptions): Promise<CommandResult>;
|
|
56
|
+
//# sourceMappingURL=executor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/core/executor.ts"],"names":[],"mappings":"AAqEA;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,oCAAoC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,uCAAuC;IACvC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wCAAwC;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,iFAAiF;IACjF,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mCAAmC;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,oDAAoD;IACpD,QAAQ,EAAE,OAAO,CAAC;IAClB,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAgFD;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CAmNxB"}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { getConfig, getToolTimeout } from "./config.js";
|
|
4
|
+
import { SudoSession } from "./sudo-session.js";
|
|
5
|
+
import { SudoGuard } from "./sudo-guard.js";
|
|
6
|
+
import { resolveCommand, resolveSudoCommand } from "./command-allowlist.js";
|
|
7
|
+
// ── Askpass helper detection ─────────────────────────────────────────────────
|
|
8
|
+
/**
|
|
9
|
+
* Ordered list of known graphical sudo/SSH askpass helpers.
|
|
10
|
+
* The first one found on the system will be used as the SUDO_ASKPASS program.
|
|
11
|
+
*/
|
|
12
|
+
const ASKPASS_CANDIDATES = [
|
|
13
|
+
"/usr/bin/ssh-askpass", // Generic (often symlinked to ksshaskpass/gnome)
|
|
14
|
+
"/usr/bin/ksshaskpass", // KDE Plasma
|
|
15
|
+
"/usr/lib/ssh/x11-ssh-askpass", // X11 classic
|
|
16
|
+
"/usr/libexec/openssh/gnome-ssh-askpass", // GNOME
|
|
17
|
+
"/usr/bin/lxqt-sudo", // LXQt
|
|
18
|
+
];
|
|
19
|
+
/** Cached result of askpass detection (null = not yet checked, undefined = not found) */
|
|
20
|
+
let cachedAskpass = null;
|
|
21
|
+
/**
|
|
22
|
+
* Find the first available graphical askpass helper on the system.
|
|
23
|
+
* Returns the absolute path or `undefined` if none found.
|
|
24
|
+
* Result is cached for the process lifetime.
|
|
25
|
+
*/
|
|
26
|
+
function findAskpassHelper() {
|
|
27
|
+
if (cachedAskpass !== null)
|
|
28
|
+
return cachedAskpass;
|
|
29
|
+
// Check environment variable first — user may have set it explicitly
|
|
30
|
+
const envAskpass = process.env.SUDO_ASKPASS;
|
|
31
|
+
if (envAskpass && existsSync(envAskpass)) {
|
|
32
|
+
// SECURITY (CORE-016): Validate the env-specified askpass helper
|
|
33
|
+
const envValidation = SudoGuard.validateAskpassPath(envAskpass);
|
|
34
|
+
if (envValidation.valid) {
|
|
35
|
+
cachedAskpass = envAskpass;
|
|
36
|
+
return cachedAskpass;
|
|
37
|
+
}
|
|
38
|
+
console.error(`[executor] Rejecting SUDO_ASKPASS: ${envValidation.reason}`);
|
|
39
|
+
}
|
|
40
|
+
// Must have a display server to show a GUI dialog
|
|
41
|
+
const hasDisplay = !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
|
|
42
|
+
if (!hasDisplay) {
|
|
43
|
+
cachedAskpass = undefined;
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
for (const candidate of ASKPASS_CANDIDATES) {
|
|
47
|
+
if (existsSync(candidate)) {
|
|
48
|
+
// SECURITY (CORE-016): Validate each candidate's ownership, permissions, and integrity
|
|
49
|
+
const validation = SudoGuard.validateAskpassPath(candidate);
|
|
50
|
+
if (validation.valid) {
|
|
51
|
+
cachedAskpass = candidate;
|
|
52
|
+
console.error(`[executor] Found verified askpass helper: ${candidate}`);
|
|
53
|
+
return cachedAskpass;
|
|
54
|
+
}
|
|
55
|
+
console.error(`[executor] Skipping askpass candidate ${candidate}: ${validation.reason}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
console.error(`[executor] No verified askpass helper found`);
|
|
59
|
+
cachedAskpass = undefined;
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Prepares sudo command options by injecting credentials so that sudo
|
|
64
|
+
* works in non-interactive (no-TTY) environments like MCP servers.
|
|
65
|
+
*
|
|
66
|
+
* **Strategy (in priority order):**
|
|
67
|
+
* 1. If a {@link SudoSession} is active with a cached password, inject
|
|
68
|
+
* `-S -p ""` and pipe the password via stdin (fastest, no UI).
|
|
69
|
+
* 2. If no session but a graphical askpass helper is available (e.g.
|
|
70
|
+
* `ssh-askpass`, `ksshaskpass`), inject `-A` and set `SUDO_ASKPASS`
|
|
71
|
+
* env var. This pops a **secure native GUI dialog** for the password
|
|
72
|
+
* — the password never enters the chat or any log.
|
|
73
|
+
* 3. If neither is available, let sudo run as-is (will fail without TTY,
|
|
74
|
+
* but the error will be caught and an elevation prompt returned).
|
|
75
|
+
*
|
|
76
|
+
* This is transparent to callers — tool code continues to use
|
|
77
|
+
* `command: "sudo", args: ["iptables", ...]`.
|
|
78
|
+
*/
|
|
79
|
+
function prepareSudoOptions(options) {
|
|
80
|
+
// Only transform calls where the command is "sudo"
|
|
81
|
+
if (options.command !== "sudo")
|
|
82
|
+
return options;
|
|
83
|
+
// Skip if the caller explicitly opted out (e.g. sudo-session.ts itself)
|
|
84
|
+
if (options.skipSudoInjection)
|
|
85
|
+
return options;
|
|
86
|
+
// Skip if the caller already supplied `-S` or `-A` (manual control)
|
|
87
|
+
if (options.args.includes("-S") || options.args.includes("-A"))
|
|
88
|
+
return options;
|
|
89
|
+
const session = SudoSession.getInstance();
|
|
90
|
+
const passwordBuf = session.getPassword(); // Returns Buffer | null (a copy)
|
|
91
|
+
if (passwordBuf) {
|
|
92
|
+
// ── Strategy 1: SudoSession has a cached password → pipe via stdin ──
|
|
93
|
+
const newArgs = ["-S", "-p", "", ...options.args];
|
|
94
|
+
const newline = Buffer.from("\n");
|
|
95
|
+
const stdinPayload = options.stdin
|
|
96
|
+
? Buffer.concat([
|
|
97
|
+
passwordBuf,
|
|
98
|
+
newline,
|
|
99
|
+
Buffer.isBuffer(options.stdin) ? options.stdin : Buffer.from(options.stdin),
|
|
100
|
+
])
|
|
101
|
+
: Buffer.concat([passwordBuf, newline]);
|
|
102
|
+
// Zero the password copy immediately after concatenation
|
|
103
|
+
passwordBuf.fill(0);
|
|
104
|
+
return {
|
|
105
|
+
...options,
|
|
106
|
+
args: newArgs,
|
|
107
|
+
stdin: stdinPayload,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// ── Strategy 2: No cached password → try graphical askpass helper ──
|
|
111
|
+
const askpassPath = findAskpassHelper();
|
|
112
|
+
if (askpassPath) {
|
|
113
|
+
console.error(`[executor] No sudo session — falling back to askpass GUI: ${askpassPath}`);
|
|
114
|
+
const newArgs = ["-A", ...options.args];
|
|
115
|
+
return {
|
|
116
|
+
...options,
|
|
117
|
+
args: newArgs,
|
|
118
|
+
env: {
|
|
119
|
+
...options.env,
|
|
120
|
+
SUDO_ASKPASS: askpassPath,
|
|
121
|
+
// Ensure DISPLAY is forwarded for GUI dialog
|
|
122
|
+
...(process.env.DISPLAY ? { DISPLAY: process.env.DISPLAY } : {}),
|
|
123
|
+
...(process.env.WAYLAND_DISPLAY
|
|
124
|
+
? { WAYLAND_DISPLAY: process.env.WAYLAND_DISPLAY }
|
|
125
|
+
: {}),
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// ── Strategy 3: No session, no askpass → let sudo fail naturally ──
|
|
130
|
+
// The error will be caught by SudoGuard and an elevation prompt returned.
|
|
131
|
+
return options;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Executes a command safely using spawn with shell: false.
|
|
135
|
+
*
|
|
136
|
+
* - Transparently injects sudo credentials from SudoSession when available
|
|
137
|
+
* - Uses AbortController for timeout enforcement
|
|
138
|
+
* - Caps stdout/stderr buffers to maxBuffer
|
|
139
|
+
* - Tracks execution duration
|
|
140
|
+
* - Handles stdin piping
|
|
141
|
+
* - Catches spawn errors gracefully
|
|
142
|
+
*/
|
|
143
|
+
export async function executeCommand(options) {
|
|
144
|
+
const config = getConfig();
|
|
145
|
+
const timeout = options.timeout ??
|
|
146
|
+
(options.toolName
|
|
147
|
+
? getToolTimeout(options.toolName, config)
|
|
148
|
+
: config.commandTimeout);
|
|
149
|
+
const maxBuffer = options.maxBuffer ?? config.maxBuffer;
|
|
150
|
+
// ── Command allowlist validation ─────────────────────────────────────
|
|
151
|
+
// Resolve bare command names to absolute paths and reject anything
|
|
152
|
+
// not in the allowlist. For sudo, also validate the target binary.
|
|
153
|
+
try {
|
|
154
|
+
if (options.command === "sudo") {
|
|
155
|
+
const { sudoPath, targetIndex, targetPath } = resolveSudoCommand(options.args);
|
|
156
|
+
options = {
|
|
157
|
+
...options,
|
|
158
|
+
command: sudoPath,
|
|
159
|
+
args: targetIndex >= 0
|
|
160
|
+
? [
|
|
161
|
+
...options.args.slice(0, targetIndex),
|
|
162
|
+
targetPath,
|
|
163
|
+
...options.args.slice(targetIndex + 1),
|
|
164
|
+
]
|
|
165
|
+
: options.args,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
options = {
|
|
170
|
+
...options,
|
|
171
|
+
command: resolveCommand(options.command),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
177
|
+
return {
|
|
178
|
+
stdout: "",
|
|
179
|
+
stderr: `Allowlist validation failed: ${message}`,
|
|
180
|
+
exitCode: 1,
|
|
181
|
+
timedOut: false,
|
|
182
|
+
duration: 0,
|
|
183
|
+
permissionDenied: false,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
// ── Transparent sudo credential injection ────────────────────────────
|
|
187
|
+
const effectiveOptions = prepareSudoOptions(options);
|
|
188
|
+
return new Promise((resolve) => {
|
|
189
|
+
const startTime = Date.now();
|
|
190
|
+
let timedOut = false;
|
|
191
|
+
const controller = new AbortController();
|
|
192
|
+
const { signal } = controller;
|
|
193
|
+
let spawnEnv;
|
|
194
|
+
if (effectiveOptions.env) {
|
|
195
|
+
spawnEnv = { ...process.env, ...effectiveOptions.env };
|
|
196
|
+
}
|
|
197
|
+
let child;
|
|
198
|
+
try {
|
|
199
|
+
child = spawn(effectiveOptions.command, effectiveOptions.args, {
|
|
200
|
+
shell: false,
|
|
201
|
+
cwd: effectiveOptions.cwd,
|
|
202
|
+
env: spawnEnv,
|
|
203
|
+
signal,
|
|
204
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
const duration = Date.now() - startTime;
|
|
209
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
210
|
+
const stderrMsg = `Spawn error: ${message}`;
|
|
211
|
+
resolve({
|
|
212
|
+
stdout: "",
|
|
213
|
+
stderr: stderrMsg,
|
|
214
|
+
exitCode: 1,
|
|
215
|
+
timedOut: false,
|
|
216
|
+
duration,
|
|
217
|
+
permissionDenied: SudoGuard.isPermissionError(stderrMsg, 1),
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const stdoutChunks = [];
|
|
222
|
+
const stderrChunks = [];
|
|
223
|
+
let stdoutLen = 0;
|
|
224
|
+
let stderrLen = 0;
|
|
225
|
+
let stdoutCapped = false;
|
|
226
|
+
let stderrCapped = false;
|
|
227
|
+
const timeoutId = setTimeout(() => {
|
|
228
|
+
timedOut = true;
|
|
229
|
+
// Close stdin first to unblock any process waiting for input (e.g. sudo -S waiting for password)
|
|
230
|
+
try {
|
|
231
|
+
if (child.stdin && !child.stdin.destroyed)
|
|
232
|
+
child.stdin.end();
|
|
233
|
+
}
|
|
234
|
+
catch { /* ignore */ }
|
|
235
|
+
// Graceful SIGTERM first
|
|
236
|
+
try {
|
|
237
|
+
child.kill("SIGTERM");
|
|
238
|
+
}
|
|
239
|
+
catch { /* ignore */ }
|
|
240
|
+
// Escalate to SIGKILL after 5 seconds if still running
|
|
241
|
+
setTimeout(() => {
|
|
242
|
+
try {
|
|
243
|
+
if (!child.killed)
|
|
244
|
+
child.kill("SIGKILL");
|
|
245
|
+
}
|
|
246
|
+
catch { /* ignore */ }
|
|
247
|
+
}, 5000);
|
|
248
|
+
}, timeout);
|
|
249
|
+
child.stdout?.on("data", (chunk) => {
|
|
250
|
+
if (stdoutCapped)
|
|
251
|
+
return;
|
|
252
|
+
stdoutLen += chunk.length;
|
|
253
|
+
if (stdoutLen > maxBuffer) {
|
|
254
|
+
stdoutCapped = true;
|
|
255
|
+
const remaining = maxBuffer - (stdoutLen - chunk.length);
|
|
256
|
+
if (remaining > 0) {
|
|
257
|
+
stdoutChunks.push(chunk.subarray(0, remaining));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
stdoutChunks.push(chunk);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
child.stderr?.on("data", (chunk) => {
|
|
265
|
+
if (stderrCapped)
|
|
266
|
+
return;
|
|
267
|
+
stderrLen += chunk.length;
|
|
268
|
+
if (stderrLen > maxBuffer) {
|
|
269
|
+
stderrCapped = true;
|
|
270
|
+
const remaining = maxBuffer - (stderrLen - chunk.length);
|
|
271
|
+
if (remaining > 0) {
|
|
272
|
+
stderrChunks.push(chunk.subarray(0, remaining));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
stderrChunks.push(chunk);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
if (effectiveOptions.stdin && child.stdin) {
|
|
280
|
+
const stdinBuf = Buffer.isBuffer(effectiveOptions.stdin)
|
|
281
|
+
? effectiveOptions.stdin
|
|
282
|
+
: Buffer.from(effectiveOptions.stdin);
|
|
283
|
+
child.stdin.write(stdinBuf);
|
|
284
|
+
child.stdin.end();
|
|
285
|
+
// Zero the buffer after writing (may contain sudo password)
|
|
286
|
+
stdinBuf.fill(0);
|
|
287
|
+
}
|
|
288
|
+
child.on("close", (code) => {
|
|
289
|
+
clearTimeout(timeoutId);
|
|
290
|
+
const duration = Date.now() - startTime;
|
|
291
|
+
const exitCode = timedOut ? 124 : (code ?? 1);
|
|
292
|
+
let stdout = Buffer.concat(stdoutChunks).toString("utf-8");
|
|
293
|
+
let stderr = Buffer.concat(stderrChunks).toString("utf-8");
|
|
294
|
+
if (stdoutCapped) {
|
|
295
|
+
stdout += "\n[OUTPUT TRUNCATED - exceeded max buffer]";
|
|
296
|
+
}
|
|
297
|
+
if (stderrCapped) {
|
|
298
|
+
stderr += "\n[STDERR TRUNCATED - exceeded max buffer]";
|
|
299
|
+
}
|
|
300
|
+
if (timedOut) {
|
|
301
|
+
const timeoutSec = Math.round(timeout / 1000);
|
|
302
|
+
stderr += `\nCommand timed out after ${timeoutSec} seconds. ` +
|
|
303
|
+
`The target may be unreachable or the operation is taking too long. ` +
|
|
304
|
+
`Consider increasing KALI_DEFENSE_COMMAND_TIMEOUT (current: ${timeoutSec}s).`;
|
|
305
|
+
}
|
|
306
|
+
// Detect permission errors from combined output
|
|
307
|
+
const combinedOutput = stdout + "\n" + stderr;
|
|
308
|
+
const permissionDenied = !timedOut &&
|
|
309
|
+
exitCode !== 0 &&
|
|
310
|
+
SudoGuard.isPermissionError(combinedOutput, exitCode);
|
|
311
|
+
resolve({
|
|
312
|
+
stdout,
|
|
313
|
+
stderr,
|
|
314
|
+
exitCode,
|
|
315
|
+
timedOut,
|
|
316
|
+
duration,
|
|
317
|
+
permissionDenied,
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
child.on("error", (err) => {
|
|
321
|
+
clearTimeout(timeoutId);
|
|
322
|
+
const duration = Date.now() - startTime;
|
|
323
|
+
// If it was an abort error from our timeout, handle as timeout
|
|
324
|
+
if (timedOut) {
|
|
325
|
+
const timeoutSec = Math.round(timeout / 1000);
|
|
326
|
+
const timeoutMsg = `\nCommand timed out after ${timeoutSec} seconds. ` +
|
|
327
|
+
`The target may be unreachable or the operation is taking too long. ` +
|
|
328
|
+
`Consider increasing KALI_DEFENSE_COMMAND_TIMEOUT (current: ${timeoutSec}s).`;
|
|
329
|
+
resolve({
|
|
330
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
|
|
331
|
+
stderr: Buffer.concat(stderrChunks).toString("utf-8") + timeoutMsg,
|
|
332
|
+
exitCode: 124,
|
|
333
|
+
timedOut: true,
|
|
334
|
+
duration,
|
|
335
|
+
permissionDenied: false,
|
|
336
|
+
});
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const stderrMsg = `Process error: ${err.message}`;
|
|
340
|
+
resolve({
|
|
341
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
|
|
342
|
+
stderr: stderrMsg,
|
|
343
|
+
exitCode: 1,
|
|
344
|
+
timedOut: false,
|
|
345
|
+
duration,
|
|
346
|
+
permissionDenied: SudoGuard.isPermissionError(stderrMsg, 1),
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
}
|