defense-mcp-server 0.6.0

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