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,459 @@
1
+ import { resolve, normalize } from "node:path";
2
+ import { getConfig } from "./config.js";
3
+ /**
4
+ * Regex matching dangerous shell metacharacters.
5
+ * These characters could enable command injection if passed unsanitized.
6
+ */
7
+ const SHELL_METACHAR_RE = /[;|&$`(){}<>!\\\n\r]/;
8
+ /**
9
+ * Regex matching control characters (excluding tab, newline, carriage return
10
+ * which are handled by SHELL_METACHAR_RE where dangerous).
11
+ */
12
+ const CONTROL_CHAR_RE = /[\x00-\x08\x0e-\x1f\x7f]/;
13
+ /**
14
+ * Regex matching path traversal components (`..` as a directory segment).
15
+ */
16
+ const PATH_TRAVERSAL_RE = /(^|[\/\\])\.\.([\/\\]|$)/;
17
+ /**
18
+ * Validates a target string as hostname, IPv4, IPv6, or CIDR notation.
19
+ * Throws on invalid input.
20
+ */
21
+ export function validateTarget(target, config) {
22
+ if (!target || typeof target !== "string") {
23
+ throw new Error("Target must be a non-empty string");
24
+ }
25
+ const trimmed = target.trim();
26
+ if (SHELL_METACHAR_RE.test(trimmed)) {
27
+ throw new Error(`Target contains forbidden shell metacharacters: ${trimmed}`);
28
+ }
29
+ if (CONTROL_CHAR_RE.test(trimmed)) {
30
+ throw new Error(`Target contains control characters: ${trimmed}`);
31
+ }
32
+ // IPv4 with optional CIDR
33
+ const ipv4Re = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
34
+ // IPv6 (simplified)
35
+ const ipv6Re = /^[0-9a-fA-F:]+(%[a-zA-Z0-9]+)?(\/\d{1,3})?$/;
36
+ // Hostname
37
+ const hostnameRe = /^[a-zA-Z0-9]([a-zA-Z0-9._-]{0,253}[a-zA-Z0-9])?$/;
38
+ if (!ipv4Re.test(trimmed) &&
39
+ !ipv6Re.test(trimmed) &&
40
+ !hostnameRe.test(trimmed)) {
41
+ throw new Error(`Invalid target format: ${trimmed}`);
42
+ }
43
+ // Validate IPv4 octets if it looks like IPv4
44
+ if (ipv4Re.test(trimmed)) {
45
+ const ipPart = trimmed.split("/")[0];
46
+ const octets = ipPart.split(".").map(Number);
47
+ if (octets.some((o) => o < 0 || o > 255)) {
48
+ throw new Error(`Invalid IPv4 address: ${trimmed}`);
49
+ }
50
+ // Validate CIDR prefix
51
+ if (trimmed.includes("/")) {
52
+ const prefix = parseInt(trimmed.split("/")[1], 10);
53
+ if (prefix < 0 || prefix > 32) {
54
+ throw new Error(`Invalid CIDR prefix: ${trimmed}`);
55
+ }
56
+ }
57
+ }
58
+ return trimmed;
59
+ }
60
+ /**
61
+ * Validates a single port number (1-65535).
62
+ * Throws on invalid input.
63
+ */
64
+ export function validatePort(port) {
65
+ const num = typeof port === "string" ? parseInt(port, 10) : port;
66
+ if (isNaN(num) || num < 1 || num > 65535 || !Number.isInteger(num)) {
67
+ throw new Error(`Invalid port number: ${port}. Must be 1-65535`);
68
+ }
69
+ return num;
70
+ }
71
+ /**
72
+ * Validates a port range specification (e.g., "80,443,1-1024").
73
+ * Throws on invalid input.
74
+ */
75
+ export function validatePortRange(range) {
76
+ if (!range || typeof range !== "string") {
77
+ throw new Error("Port range must be a non-empty string");
78
+ }
79
+ const trimmed = range.trim();
80
+ if (SHELL_METACHAR_RE.test(trimmed)) {
81
+ throw new Error(`Port range contains forbidden shell metacharacters: ${trimmed}`);
82
+ }
83
+ const portRangeRe = /^(\d{1,5}(-\d{1,5})?,)*\d{1,5}(-\d{1,5})?$/;
84
+ if (!portRangeRe.test(trimmed)) {
85
+ throw new Error(`Invalid port range format: ${trimmed}`);
86
+ }
87
+ // Validate individual port numbers and ranges
88
+ const parts = trimmed.split(",");
89
+ for (const part of parts) {
90
+ if (part.includes("-")) {
91
+ const [startStr, endStr] = part.split("-");
92
+ const start = parseInt(startStr, 10);
93
+ const end = parseInt(endStr, 10);
94
+ if (start < 1 || start > 65535 || end < 1 || end > 65535) {
95
+ throw new Error(`Port out of range in: ${part}`);
96
+ }
97
+ if (start > end) {
98
+ throw new Error(`Invalid port range (start > end): ${part}`);
99
+ }
100
+ }
101
+ else {
102
+ const p = parseInt(part, 10);
103
+ if (p < 1 || p > 65535) {
104
+ throw new Error(`Port out of range: ${part}`);
105
+ }
106
+ }
107
+ }
108
+ return trimmed;
109
+ }
110
+ /**
111
+ * Validates a file path is within allowed directories,
112
+ * contains no traversal attacks, no null bytes, no shell metacharacters.
113
+ * Throws on invalid input.
114
+ */
115
+ export function validateFilePath(filePath, config) {
116
+ if (!filePath || typeof filePath !== "string") {
117
+ throw new Error("File path must be a non-empty string");
118
+ }
119
+ // Check for null bytes
120
+ if (filePath.includes("\0")) {
121
+ throw new Error("File path contains null bytes");
122
+ }
123
+ // Check for path traversal
124
+ if (PATH_TRAVERSAL_RE.test(filePath)) {
125
+ throw new Error("Path contains forbidden directory traversal (..)");
126
+ }
127
+ // Check for shell metacharacters
128
+ if (SHELL_METACHAR_RE.test(filePath)) {
129
+ throw new Error(`File path contains forbidden shell metacharacters: ${filePath}`);
130
+ }
131
+ // Check for control characters
132
+ if (CONTROL_CHAR_RE.test(filePath)) {
133
+ throw new Error(`File path contains control characters: ${filePath}`);
134
+ }
135
+ const normalized = normalize(resolve(filePath));
136
+ const cfg = config ?? getConfig();
137
+ // Check path traversal - ensure resolved path doesn't escape allowed dirs
138
+ const isAllowed = cfg.allowedDirs.some((dir) => {
139
+ const normalizedDir = normalize(resolve(dir));
140
+ return (normalized === normalizedDir || normalized.startsWith(normalizedDir + "/"));
141
+ });
142
+ if (!isAllowed) {
143
+ throw new Error(`File path is not within allowed directories: ${filePath} (allowed: ${cfg.allowedDirs.join(", ")})`);
144
+ }
145
+ // Check against protected paths
146
+ const isProtected = cfg.protectedPaths.some((protectedPath) => {
147
+ const normalizedProtected = normalize(resolve(protectedPath));
148
+ return (normalized === normalizedProtected ||
149
+ normalized.startsWith(normalizedProtected + "/"));
150
+ });
151
+ if (isProtected) {
152
+ throw new Error(`File path is in a protected location: ${filePath}`);
153
+ }
154
+ return normalized;
155
+ }
156
+ /**
157
+ * Validates an array of arguments for shell metacharacters.
158
+ * Throws on invalid input.
159
+ */
160
+ export function sanitizeArgs(args) {
161
+ if (!Array.isArray(args)) {
162
+ throw new Error("Arguments must be an array of strings");
163
+ }
164
+ for (let i = 0; i < args.length; i++) {
165
+ const arg = args[i];
166
+ if (typeof arg !== "string") {
167
+ throw new Error(`Argument at index ${i} is not a string`);
168
+ }
169
+ if (SHELL_METACHAR_RE.test(arg)) {
170
+ throw new Error(`Argument at index ${i} contains forbidden shell metacharacters: ${arg}`);
171
+ }
172
+ if (CONTROL_CHAR_RE.test(arg)) {
173
+ throw new Error(`Argument at index ${i} contains control characters: ${arg}`);
174
+ }
175
+ }
176
+ return args;
177
+ }
178
+ /**
179
+ * Validates a systemd service name.
180
+ * Only allows `[a-zA-Z0-9._@-]+`.
181
+ * Throws on invalid input.
182
+ */
183
+ export function validateServiceName(name) {
184
+ if (!name || typeof name !== "string") {
185
+ throw new Error("Service name must be a non-empty string");
186
+ }
187
+ const trimmed = name.trim();
188
+ const serviceRe = /^[a-zA-Z0-9._@-]+$/;
189
+ if (!serviceRe.test(trimmed)) {
190
+ throw new Error(`Invalid service name: ${trimmed}. Only [a-zA-Z0-9._@-] allowed`);
191
+ }
192
+ return trimmed;
193
+ }
194
+ /**
195
+ * Validates a sysctl key (must be word.word.word... pattern).
196
+ * Throws on invalid input.
197
+ */
198
+ export function validateSysctlKey(key) {
199
+ if (!key || typeof key !== "string") {
200
+ throw new Error("Sysctl key must be a non-empty string");
201
+ }
202
+ const trimmed = key.trim();
203
+ const sysctlRe = /^[a-zA-Z0-9_]+(\.[a-zA-Z0-9_]+)+$/;
204
+ if (!sysctlRe.test(trimmed)) {
205
+ throw new Error(`Invalid sysctl key: ${trimmed}. Must match word.word.word pattern`);
206
+ }
207
+ return trimmed;
208
+ }
209
+ /**
210
+ * Validates a configuration key.
211
+ * Only allows `[a-zA-Z0-9._-]+`.
212
+ * Throws on invalid input.
213
+ */
214
+ export function validateConfigKey(key) {
215
+ if (!key || typeof key !== "string") {
216
+ throw new Error("Config key must be a non-empty string");
217
+ }
218
+ const trimmed = key.trim();
219
+ const configRe = /^[a-zA-Z0-9._-]+$/;
220
+ if (!configRe.test(trimmed)) {
221
+ throw new Error(`Invalid config key: ${trimmed}. Only [a-zA-Z0-9._-] allowed`);
222
+ }
223
+ return trimmed;
224
+ }
225
+ /**
226
+ * Validates a package name.
227
+ * Only allows `[a-zA-Z0-9._+:-]+`.
228
+ * Throws on invalid input.
229
+ */
230
+ export function validatePackageName(name) {
231
+ if (!name || typeof name !== "string") {
232
+ throw new Error("Package name must be a non-empty string");
233
+ }
234
+ const trimmed = name.trim();
235
+ const pkgRe = /^[a-zA-Z0-9._+:-]+$/;
236
+ if (!pkgRe.test(trimmed)) {
237
+ throw new Error(`Invalid package name: ${trimmed}. Only [a-zA-Z0-9._+:-] allowed`);
238
+ }
239
+ return trimmed;
240
+ }
241
+ /**
242
+ * Validates an iptables chain name.
243
+ * Allows built-in chains `[A-Z_]+` (e.g., INPUT, OUTPUT, FORWARD)
244
+ * and custom chains matching `[A-Za-z_][A-Za-z0-9_-]{0,28}`.
245
+ * Throws on invalid input.
246
+ */
247
+ export function validateIptablesChain(chain) {
248
+ if (!chain || typeof chain !== "string") {
249
+ throw new Error("Iptables chain must be a non-empty string");
250
+ }
251
+ const trimmed = chain.trim();
252
+ // Allow both built-in (e.g. INPUT) and custom chains (e.g. syn_flood)
253
+ const chainRe = /^[A-Za-z_][A-Za-z0-9_-]{0,28}$/;
254
+ if (!chainRe.test(trimmed)) {
255
+ throw new Error(`Invalid iptables chain: ${trimmed}. Must match [A-Za-z_][A-Za-z0-9_-]{0,28}`);
256
+ }
257
+ return trimmed;
258
+ }
259
+ /**
260
+ * Validates a network interface name.
261
+ * Only allows `[a-zA-Z0-9._-]+`, max 16 characters.
262
+ * Throws on invalid input.
263
+ */
264
+ export function validateInterface(iface) {
265
+ if (!iface || typeof iface !== "string") {
266
+ throw new Error("Interface name must be a non-empty string");
267
+ }
268
+ const trimmed = iface.trim();
269
+ if (trimmed.length > 16) {
270
+ throw new Error(`Interface name too long: ${trimmed}. Maximum 16 characters`);
271
+ }
272
+ const ifaceRe = /^[a-zA-Z0-9._-]+$/;
273
+ if (!ifaceRe.test(trimmed)) {
274
+ throw new Error(`Invalid interface name: ${trimmed}. Only [a-zA-Z0-9._-] allowed`);
275
+ }
276
+ return trimmed;
277
+ }
278
+ /**
279
+ * Validates a Unix username.
280
+ * Only allows `[a-zA-Z0-9._-]+`, max 32 characters.
281
+ * Throws on invalid input.
282
+ */
283
+ export function validateUsername(name) {
284
+ if (!name || typeof name !== "string") {
285
+ throw new Error("Username must be a non-empty string");
286
+ }
287
+ const trimmed = name.trim();
288
+ if (trimmed.length > 32) {
289
+ throw new Error(`Username too long: ${trimmed}. Maximum 32 characters`);
290
+ }
291
+ const usernameRe = /^[a-zA-Z0-9._-]+$/;
292
+ if (!usernameRe.test(trimmed)) {
293
+ throw new Error(`Invalid username: ${trimmed}. Only [a-zA-Z0-9._-] allowed`);
294
+ }
295
+ return trimmed;
296
+ }
297
+ /**
298
+ * Validates a YARA rule file path (must end in .yar or .yara).
299
+ * Throws on invalid input.
300
+ */
301
+ export function validateYaraRule(path) {
302
+ if (!path || typeof path !== "string") {
303
+ throw new Error("YARA rule path must be a non-empty string");
304
+ }
305
+ const trimmed = path.trim();
306
+ if (PATH_TRAVERSAL_RE.test(trimmed)) {
307
+ throw new Error("Path contains forbidden directory traversal (..)");
308
+ }
309
+ if (SHELL_METACHAR_RE.test(trimmed)) {
310
+ throw new Error(`YARA rule path contains forbidden shell metacharacters: ${trimmed}`);
311
+ }
312
+ if (CONTROL_CHAR_RE.test(trimmed)) {
313
+ throw new Error(`YARA rule path contains control characters: ${trimmed}`);
314
+ }
315
+ if (!trimmed.endsWith(".yar") && !trimmed.endsWith(".yara")) {
316
+ throw new Error(`Invalid YARA rule file: ${trimmed}. Must end in .yar or .yara`);
317
+ }
318
+ return trimmed;
319
+ }
320
+ /**
321
+ * Validates a certificate file path (must end in .pem, .crt, .key, .p12, or .pfx).
322
+ * Throws on invalid input.
323
+ */
324
+ export function validateCertPath(path) {
325
+ if (!path || typeof path !== "string") {
326
+ throw new Error("Certificate path must be a non-empty string");
327
+ }
328
+ const trimmed = path.trim();
329
+ if (PATH_TRAVERSAL_RE.test(trimmed)) {
330
+ throw new Error("Path contains forbidden directory traversal (..)");
331
+ }
332
+ if (SHELL_METACHAR_RE.test(trimmed)) {
333
+ throw new Error(`Certificate path contains forbidden shell metacharacters: ${trimmed}`);
334
+ }
335
+ if (CONTROL_CHAR_RE.test(trimmed)) {
336
+ throw new Error(`Certificate path contains control characters: ${trimmed}`);
337
+ }
338
+ const validExtensions = [".pem", ".crt", ".key", ".p12", ".pfx"];
339
+ const hasValidExt = validExtensions.some((ext) => trimmed.toLowerCase().endsWith(ext));
340
+ if (!hasValidExt) {
341
+ throw new Error(`Invalid certificate file: ${trimmed}. Must end in ${validExtensions.join(", ")}`);
342
+ }
343
+ return trimmed;
344
+ }
345
+ /**
346
+ * Validates a firewalld zone name.
347
+ * Only allows `[a-zA-Z0-9_-]+`.
348
+ * Throws on invalid input.
349
+ */
350
+ export function validateFirewallZone(zone) {
351
+ if (!zone || typeof zone !== "string") {
352
+ throw new Error("Firewall zone must be a non-empty string");
353
+ }
354
+ const trimmed = zone.trim();
355
+ const zoneRe = /^[a-zA-Z0-9_-]+$/;
356
+ if (!zoneRe.test(trimmed)) {
357
+ throw new Error(`Invalid firewall zone: ${trimmed}. Only [a-zA-Z0-9_-] allowed`);
358
+ }
359
+ return trimmed;
360
+ }
361
+ /**
362
+ * Validates an auditd key name.
363
+ * Only allows `[a-zA-Z0-9_-]+`.
364
+ * Throws on invalid input.
365
+ */
366
+ export function validateAuditdKey(key) {
367
+ if (!key || typeof key !== "string") {
368
+ throw new Error("Auditd key must be a non-empty string");
369
+ }
370
+ const trimmed = key.trim();
371
+ const keyRe = /^[a-zA-Z0-9_-]+$/;
372
+ if (!keyRe.test(trimmed)) {
373
+ throw new Error(`Invalid auditd key: ${trimmed}. Only [a-zA-Z0-9_-] allowed`);
374
+ }
375
+ return trimmed;
376
+ }
377
+ /**
378
+ * Validates a tool-supplied file path against traversal attacks and an explicit
379
+ * list of allowed root directories.
380
+ *
381
+ * 1. Rejects paths containing `..`
382
+ * 2. Uses `path.resolve()` to normalize
383
+ * 3. Verifies resolved path is within one of the allowed directories
384
+ *
385
+ * @param inputPath The user-supplied path
386
+ * @param allowedDirs Array of allowed root directories (e.g. ["/var/log", "/etc"])
387
+ * @param label Human-readable label for error messages (default: "Path")
388
+ * @returns The resolved, validated path
389
+ */
390
+ export function validateToolPath(inputPath, allowedDirs, label = "Path") {
391
+ if (!inputPath || typeof inputPath !== "string") {
392
+ throw new Error(`${label} must be a non-empty string`);
393
+ }
394
+ if (inputPath.includes("\0")) {
395
+ throw new Error(`${label} contains null bytes`);
396
+ }
397
+ // Defense-in-depth: reject any path containing `..` sequences
398
+ if (PATH_TRAVERSAL_RE.test(inputPath)) {
399
+ throw new Error(`${label} contains forbidden directory traversal (..): '${inputPath}'`);
400
+ }
401
+ if (SHELL_METACHAR_RE.test(inputPath)) {
402
+ throw new Error(`${label} contains forbidden shell metacharacters: '${inputPath}'`);
403
+ }
404
+ if (CONTROL_CHAR_RE.test(inputPath)) {
405
+ throw new Error(`${label} contains control characters: '${inputPath}'`);
406
+ }
407
+ const resolved = normalize(resolve(inputPath));
408
+ const isAllowed = allowedDirs.some((dir) => {
409
+ const normalizedDir = normalize(resolve(dir));
410
+ return (resolved === normalizedDir || resolved.startsWith(normalizedDir + "/"));
411
+ });
412
+ if (!isAllowed) {
413
+ throw new Error(`${label} '${resolved}' is not within allowed directories: ${allowedDirs.join(", ")}`);
414
+ }
415
+ return resolved;
416
+ }
417
+ // ── Error Sanitization ───────────────────────────────────────────────────────
418
+ /**
419
+ * SECURITY (TOOL-029): Regex to match absolute paths in error messages.
420
+ * Matches paths like /home/user/... or /var/lib/...
421
+ */
422
+ const ABS_PATH_RE = /\/(?:home|root|tmp|var|etc|usr|opt|srv|run|mnt|media)\/\S*/gi;
423
+ /**
424
+ * SECURITY (TOOL-029): Regex to match stack traces in error messages.
425
+ * Matches lines starting with " at " (Node.js stack trace format).
426
+ */
427
+ const STACK_TRACE_RE = /\n\s+at .+/g;
428
+ /**
429
+ * Sanitize error messages before returning them to MCP clients.
430
+ *
431
+ * Strips:
432
+ * 1. Absolute file paths (replaced with `[path]`)
433
+ * 2. Stack traces (removed entirely)
434
+ * 3. Overly long messages (truncated to 500 chars)
435
+ *
436
+ * @param error - The caught error (unknown type)
437
+ * @returns A sanitized error message string safe for external exposure
438
+ */
439
+ export function sanitizeToolError(error) {
440
+ let message;
441
+ if (error instanceof Error) {
442
+ message = error.message;
443
+ }
444
+ else if (typeof error === "string") {
445
+ message = error;
446
+ }
447
+ else {
448
+ message = String(error);
449
+ }
450
+ // Strip stack traces
451
+ message = message.replace(STACK_TRACE_RE, "");
452
+ // Strip absolute paths
453
+ message = message.replace(ABS_PATH_RE, "[path]");
454
+ // Truncate overly long messages
455
+ if (message.length > 500) {
456
+ message = message.substring(0, 497) + "...";
457
+ }
458
+ return message;
459
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Secure filesystem utilities for the defense-mcp-server.
3
+ * All state files (changelog, rollback, backups) must use these helpers
4
+ * to ensure restrictive permissions (owner-only read/write).
5
+ */
6
+ /** Options for secureWriteFileSync. */
7
+ export interface SecureWriteOptions {
8
+ /** Character encoding for string data. Defaults to `"utf-8"`. */
9
+ encoding?: BufferEncoding;
10
+ /** Use atomic write (write to temp file, then rename). Defaults to `false`. */
11
+ atomic?: boolean;
12
+ }
13
+ /**
14
+ * Write a file with owner-only permissions (0o600).
15
+ * Creates parent directories with 0o700 if they don't exist.
16
+ *
17
+ * @param filePath - Destination file path
18
+ * @param data - Content to write
19
+ * @param encodingOrOptions - Either a BufferEncoding string (legacy) or a SecureWriteOptions object
20
+ */
21
+ export declare function secureWriteFileSync(filePath: string, data: string | Buffer, encodingOrOptions?: BufferEncoding | SecureWriteOptions): void;
22
+ /**
23
+ * Create a directory with owner-only permissions (0o700).
24
+ */
25
+ export declare function secureMkdirSync(dirPath: string): void;
26
+ /**
27
+ * Copy a file and set owner-only permissions on the destination (0o600).
28
+ */
29
+ export declare function secureCopyFileSync(src: string, dest: string): void;
30
+ /**
31
+ * Verify that a state file has secure permissions.
32
+ * Returns true if the file is owner-only (no group/other read/write/execute).
33
+ * Returns false if permissions are too open or file doesn't exist.
34
+ */
35
+ export declare function verifySecurePermissions(filePath: string): boolean;
36
+ /**
37
+ * Fix permissions on an existing file to be owner-only.
38
+ */
39
+ export declare function hardenFilePermissions(filePath: string): void;
40
+ /**
41
+ * Fix permissions on an existing directory to be owner-only.
42
+ */
43
+ export declare function hardenDirPermissions(dirPath: string): void;
44
+ /** Options for atomicWriteFileSync. */
45
+ export interface AtomicWriteOptions {
46
+ /** File permissions mode. Defaults to `0o600`. */
47
+ mode?: number;
48
+ /** Character encoding for string data. Defaults to `"utf-8"`. */
49
+ encoding?: BufferEncoding;
50
+ }
51
+ /**
52
+ * Write a file atomically using a write-to-temp-then-rename strategy.
53
+ *
54
+ * Steps:
55
+ * 1. Write data to a temporary file in the same directory (`.tmp` suffix)
56
+ * 2. Set file permissions on the temp file
57
+ * 3. Rename (atomic on POSIX) from temp to target path
58
+ * 4. If rename fails, clean up the temp file
59
+ *
60
+ * This prevents file corruption from interrupted writes (crash, signal, etc.).
61
+ *
62
+ * @param filePath - Destination file path
63
+ * @param data - Content to write (string or Buffer)
64
+ * @param options - Write options (mode, encoding)
65
+ */
66
+ export declare function atomicWriteFileSync(filePath: string, data: string | Buffer, options?: AtomicWriteOptions): void;
67
+ //# sourceMappingURL=secure-fs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secure-fs.d.ts","sourceRoot":"","sources":["../../src/core/secure-fs.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAYH,uCAAuC;AACvC,MAAM,WAAW,kBAAkB;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,+EAA+E;IAC/E,MAAM,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAC/B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GAAG,MAAM,EACrB,iBAAiB,CAAC,EAAE,cAAc,GAAG,kBAAkB,GACxD,IAAI,CA0BN;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAMrD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAOlE;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAMjE;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAI5D;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAI1D;AAID,uCAAuC;AACvC,MAAM,WAAW,kBAAkB;IAC/B,kDAAkD;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC7B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAC/B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GAAG,MAAM,EACrB,OAAO,CAAC,EAAE,kBAAkB,GAC7B,IAAI,CAkCN"}
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Secure filesystem utilities for the defense-mcp-server.
3
+ * All state files (changelog, rollback, backups) must use these helpers
4
+ * to ensure restrictive permissions (owner-only read/write).
5
+ */
6
+ import { writeFileSync, mkdirSync, copyFileSync, chmodSync, existsSync, statSync, renameSync, unlinkSync } from "node:fs";
7
+ import { dirname, join } from "node:path";
8
+ import { randomBytes } from "node:crypto";
9
+ /** File permission: owner read/write only (0o600) */
10
+ const SECURE_FILE_MODE = 0o600;
11
+ /** Directory permission: owner read/write/execute only (0o700) */
12
+ const SECURE_DIR_MODE = 0o700;
13
+ /**
14
+ * Write a file with owner-only permissions (0o600).
15
+ * Creates parent directories with 0o700 if they don't exist.
16
+ *
17
+ * @param filePath - Destination file path
18
+ * @param data - Content to write
19
+ * @param encodingOrOptions - Either a BufferEncoding string (legacy) or a SecureWriteOptions object
20
+ */
21
+ export function secureWriteFileSync(filePath, data, encodingOrOptions) {
22
+ // Normalize options
23
+ let encoding = "utf-8";
24
+ let atomic = false;
25
+ if (typeof encodingOrOptions === "string") {
26
+ encoding = encodingOrOptions;
27
+ }
28
+ else if (encodingOrOptions !== undefined) {
29
+ encoding = encodingOrOptions.encoding ?? "utf-8";
30
+ atomic = encodingOrOptions.atomic ?? false;
31
+ }
32
+ // Ensure parent directory exists with secure permissions
33
+ const dir = dirname(filePath);
34
+ if (!existsSync(dir)) {
35
+ mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
36
+ }
37
+ if (atomic) {
38
+ atomicWriteFileSync(filePath, data, { mode: SECURE_FILE_MODE, encoding });
39
+ }
40
+ else {
41
+ // Write the file
42
+ writeFileSync(filePath, data, { encoding, mode: SECURE_FILE_MODE });
43
+ // Explicitly chmod in case umask interfered
44
+ chmodSync(filePath, SECURE_FILE_MODE);
45
+ }
46
+ }
47
+ /**
48
+ * Create a directory with owner-only permissions (0o700).
49
+ */
50
+ export function secureMkdirSync(dirPath) {
51
+ if (!existsSync(dirPath)) {
52
+ mkdirSync(dirPath, { recursive: true, mode: SECURE_DIR_MODE });
53
+ }
54
+ // Explicitly chmod in case umask interfered
55
+ chmodSync(dirPath, SECURE_DIR_MODE);
56
+ }
57
+ /**
58
+ * Copy a file and set owner-only permissions on the destination (0o600).
59
+ */
60
+ export function secureCopyFileSync(src, dest) {
61
+ const dir = dirname(dest);
62
+ if (!existsSync(dir)) {
63
+ mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
64
+ }
65
+ copyFileSync(src, dest);
66
+ chmodSync(dest, SECURE_FILE_MODE);
67
+ }
68
+ /**
69
+ * Verify that a state file has secure permissions.
70
+ * Returns true if the file is owner-only (no group/other read/write/execute).
71
+ * Returns false if permissions are too open or file doesn't exist.
72
+ */
73
+ export function verifySecurePermissions(filePath) {
74
+ if (!existsSync(filePath))
75
+ return false;
76
+ const stats = statSync(filePath);
77
+ // Check that group and other have no permissions
78
+ // mode & 0o077 should be 0 (no group/other bits set)
79
+ return (stats.mode & 0o077) === 0;
80
+ }
81
+ /**
82
+ * Fix permissions on an existing file to be owner-only.
83
+ */
84
+ export function hardenFilePermissions(filePath) {
85
+ if (existsSync(filePath)) {
86
+ chmodSync(filePath, SECURE_FILE_MODE);
87
+ }
88
+ }
89
+ /**
90
+ * Fix permissions on an existing directory to be owner-only.
91
+ */
92
+ export function hardenDirPermissions(dirPath) {
93
+ if (existsSync(dirPath)) {
94
+ chmodSync(dirPath, SECURE_DIR_MODE);
95
+ }
96
+ }
97
+ /**
98
+ * Write a file atomically using a write-to-temp-then-rename strategy.
99
+ *
100
+ * Steps:
101
+ * 1. Write data to a temporary file in the same directory (`.tmp` suffix)
102
+ * 2. Set file permissions on the temp file
103
+ * 3. Rename (atomic on POSIX) from temp to target path
104
+ * 4. If rename fails, clean up the temp file
105
+ *
106
+ * This prevents file corruption from interrupted writes (crash, signal, etc.).
107
+ *
108
+ * @param filePath - Destination file path
109
+ * @param data - Content to write (string or Buffer)
110
+ * @param options - Write options (mode, encoding)
111
+ */
112
+ export function atomicWriteFileSync(filePath, data, options) {
113
+ const mode = options?.mode ?? SECURE_FILE_MODE;
114
+ const encoding = options?.encoding ?? "utf-8";
115
+ // Ensure parent directory exists with secure permissions
116
+ const dir = dirname(filePath);
117
+ if (!existsSync(dir)) {
118
+ mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
119
+ }
120
+ // Generate a unique temp file path in the same directory
121
+ const tmpSuffix = `.tmp.${randomBytes(6).toString("hex")}`;
122
+ const tmpPath = join(dir, `${filePath.split("/").pop()}${tmpSuffix}`);
123
+ try {
124
+ // Step 1: Write to temp file
125
+ writeFileSync(tmpPath, data, { encoding, mode });
126
+ // Step 2: Explicitly set permissions (in case umask interfered)
127
+ chmodSync(tmpPath, mode);
128
+ // Step 3: Atomic rename
129
+ renameSync(tmpPath, filePath);
130
+ }
131
+ catch (error) {
132
+ // Step 4: Clean up temp file on failure
133
+ try {
134
+ if (existsSync(tmpPath)) {
135
+ unlinkSync(tmpPath);
136
+ }
137
+ }
138
+ catch {
139
+ // Best-effort cleanup — ignore errors
140
+ }
141
+ throw error;
142
+ }
143
+ }