defense-mcp-server 0.8.4 → 0.9.1

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 (56) hide show
  1. package/README.md +319 -55
  2. package/build/core/auto-installer.d.ts.map +1 -1
  3. package/build/core/auto-installer.js +0 -3
  4. package/build/core/backup-manager.d.ts.map +1 -1
  5. package/build/core/backup-manager.js +44 -33
  6. package/build/core/command-allowlist.d.ts.map +1 -1
  7. package/build/core/command-allowlist.js +14 -1
  8. package/build/core/dependency-validator.d.ts.map +1 -1
  9. package/build/core/distro-adapter.d.ts.map +1 -1
  10. package/build/core/distro.js +2 -2
  11. package/build/core/installer.d.ts.map +1 -1
  12. package/build/core/logger.d.ts.map +1 -1
  13. package/build/core/logger.js +7 -3
  14. package/build/core/pam-utils.d.ts.map +1 -1
  15. package/build/core/preflight.d.ts.map +1 -1
  16. package/build/core/safeguards.d.ts.map +1 -1
  17. package/build/core/safeguards.js +4 -3
  18. package/build/core/sanitizer.d.ts +1 -1
  19. package/build/core/sanitizer.d.ts.map +1 -1
  20. package/build/core/sanitizer.js +1 -1
  21. package/build/core/sudo-guard.d.ts +5 -0
  22. package/build/core/sudo-guard.d.ts.map +1 -1
  23. package/build/core/sudo-guard.js +0 -11
  24. package/build/core/third-party-installer.js +1 -1
  25. package/build/index.js +134 -7
  26. package/build/tools/access-control.d.ts.map +1 -1
  27. package/build/tools/access-control.js +22 -24
  28. package/build/tools/compliance.d.ts.map +1 -1
  29. package/build/tools/container-security.js +1 -1
  30. package/build/tools/deception.d.ts.map +1 -1
  31. package/build/tools/deception.js +3 -2
  32. package/build/tools/dns-security.d.ts.map +1 -1
  33. package/build/tools/ebpf-security.d.ts.map +1 -1
  34. package/build/tools/encryption.d.ts.map +1 -1
  35. package/build/tools/encryption.js +0 -18
  36. package/build/tools/firewall.d.ts.map +1 -1
  37. package/build/tools/firewall.js +0 -11
  38. package/build/tools/hardening.d.ts.map +1 -1
  39. package/build/tools/integrity.js +1 -1
  40. package/build/tools/logging.d.ts.map +1 -1
  41. package/build/tools/malware.d.ts.map +1 -1
  42. package/build/tools/malware.js +17 -7
  43. package/build/tools/meta.d.ts.map +1 -1
  44. package/build/tools/meta.js +9 -10
  45. package/build/tools/network-defense.d.ts.map +1 -1
  46. package/build/tools/network-defense.js +1 -30
  47. package/build/tools/sudo-management.js +12 -18
  48. package/build/tools/supply-chain-security.d.ts.map +1 -1
  49. package/build/tools/supply-chain-security.js +0 -20
  50. package/build/tools/threat-intel.d.ts.map +1 -1
  51. package/build/tools/threat-intel.js +0 -2
  52. package/build/tools/waf.js +1 -1
  53. package/build/tools/wireless-security.js +1 -1
  54. package/build/tools/zero-trust-network.d.ts.map +1 -1
  55. package/build/tools/zero-trust-network.js +0 -8
  56. package/package.json +4 -4
package/README.md CHANGED
@@ -1,13 +1,44 @@
1
1
  # Defense MCP Server
2
2
 
3
- A Model Context Protocol (MCP) server that gives AI assistants access to **31 defensive security tools** (with 150+ actions) on Linux. Connect it to Claude Desktop, Cursor, or any MCP-compatible client to harden systems, manage firewalls, scan for vulnerabilities, and enforce compliance — all through natural language conversation.
3
+ [![Smithery](https://smithery.ai/badge/@bottobot/defense-mcp-server)](https://smithery.ai/server/@bottobot/defense-mcp-server)
4
+ [![npm version](https://img.shields.io/npm/v/defense-mcp-server)](https://www.npmjs.com/package/defense-mcp-server)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+ [![Sponsor on GitHub](https://img.shields.io/badge/Sponsor-GitHub-ea4aaa?logo=github)](https://github.com/sponsors/bottobot)
7
+ [![Support on Ko-fi](https://img.shields.io/badge/Support-Ko--fi-FF5E5B?logo=ko-fi)](https://ko-fi.com/bottobot)
4
8
 
5
- ## Why I Made This
6
- Basically I'm a total noob when it comes to really serious system hardening so I thought I'd test the latest LLM models and see how far I could get. Turns out they're pretty helpful! I got tired of hardening my new systems by hand every time I spun up a new one so I made this MCP server to make it pretty easy. I jam packed as many security tools as I could into this thing so be prepared to burn tokens using it. Hopefully it helps you about half as much as its helped me.
9
+ **31 defensive security tools. 250+ actions. One MCP server.**
7
10
 
11
+ A Model Context Protocol (MCP) server that gives AI assistants access to **31 defensive security tools** (with 250+ actions) on Linux. Connect it to Claude Desktop, Cursor, Smithery, or any MCP-compatible client to harden systems, manage firewalls, scan for vulnerabilities, and enforce compliance — all through natural language conversation.
12
+
13
+ ### TLDR ###
14
+ It's an all-in-one easy to use MCP server that will help you secure your Linux OS and harden it to external attackers. It's not perfect by any stretch but if you need something that'll take the pain out of hardening a fresh OS install then give it a try. It's as simple as saying "give me a full security audit using the Defense MCP Server" to your favourite LLM agent.
15
+
16
+ ## **The Story Of This Thing** ##
17
+ I started experimenting with Kali and I had a weird thought that if someone ever gained control of that system they could do some damage in my network. I found it came with all sorts of interesting defensive tools besides the obvious offensive ones and so I started messing around with automating the hardening process. Eventually I left Kali to the hackers and just switched to a Debian 13 vanilla type setup on my daily driver. All those hacker tools just made me nervous! However I have spent an incredible amount of time learning about system security on Linux building this thing and I think its ready to see the light of day for others to use.
18
+
19
+ Please don't hesitate to leave feedback or if you think there's a good tool I should include.
20
+
21
+ ## Testing So Far ##
22
+ So my stack for this that I've tested is VS Codium on Kali and Debian 13 using Roo Code and Claude Code using Anthropic Models. I gotta say I'm pretty happy with how well it does. I've really tried to get it to be a simple all-in-one hardening tool that'll at least plug the major gaps in anyones system. If you're using API's then its really gonna chug down some tokens so be warned. It did go through a few rounds of token efficiency optimisation but it'll eat em up like their Girl Guides Thin Mint cookies.
23
+
24
+ ---
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ npm install -g defense-mcp-server
30
+ ```
31
+
32
+ Or via Smithery:
33
+
34
+ ```bash
35
+ npx -y @smithery/cli install @bottobot/defense-mcp-server --client claude
36
+ ```
37
+
38
+ ---
8
39
  ## So What It Does
9
40
 
10
- This server exposes Linux security tools as MCP tools that an AI assistant can invoke on your behalf. Instead of memorizing command syntax for dozens of security utilities, you describe what you want in plain English and the assistant calls the right tool with the right parameters. Sounds pretty good right!
41
+ This server exposes Linux security tools as MCP tools that an AI assistant can invoke on your behalf. Instead of memorizing command syntax for dozens of security utilities, you describe what you want in plain English and the assistant calls the right tool with the right parameters. Sounds pretty good right?
11
42
 
12
43
  Here are the tools:
13
44
 
@@ -46,7 +77,10 @@ Here are the tools:
46
77
  | **Deception/Honeypots** | Canary token deployment, honeyport listeners, trigger monitoring |
47
78
  | **Wireless Security** | Bluetooth/WiFi auditing, rogue AP detection, interface disabling |
48
79
 
49
- Every tool runs with safety guardrails:
80
+ ### Safety Guardrails
81
+
82
+ Every tool runs with safety guardrails — you won't blow up your box:
83
+
50
84
  - **Dry-run by default** — tools preview what they would do before making changes
51
85
  - **Command allowlist** — only pre-approved binaries can execute (no shell interpreters)
52
86
  - **Input sanitization** — all parameters validated against injection attacks
@@ -81,7 +115,7 @@ DEFENSE_MCP_AUTO_INSTALL=false node build/index.js
81
115
  ## Requirements
82
116
 
83
117
  - **Linux** (Kali, Debian, Ubuntu, RHEL, Arch, or any systemd-based distro)
84
- - **Node.js 18+**
118
+ - **Node.js 22+**
85
119
  - **npm 9+**
86
120
 
87
121
  ## System Dependencies
@@ -101,17 +135,105 @@ sudo apt-get install -y \
101
135
 
102
136
  ### Third-Party Tools
103
137
 
104
- These are **not available** in standard Debian/Ubuntu repos and require manual installation:
138
+ These are **not available** in standard Debian/Ubuntu repos. The instructions below avoid piping remote scripts into a shell (`curl | sh`) — each binary is downloaded, verified, and installed as a discrete step.
139
+
140
+ > **Note:** The MCP server can auto-install these tools for you when `DEFENSE_MCP_AUTO_INSTALL=true` (the default). It uses GPG fingerprint and SHA256 verification internally. The manual steps below are for pre-installation or air-gapped environments.
141
+
142
+ #### Falco (eBPF runtime security)
143
+
144
+ ```bash
145
+ # Import GPG key and verify fingerprint
146
+ curl -fsSL https://falco.org/repo/falcosecurity-packages.asc -o /tmp/falco.asc
147
+ gpg --show-keys /tmp/falco.asc # Verify: 478B 2FBB C75F 4237 B731 DA43 6510 6822 B35B 1B1F
148
+ sudo gpg --dearmor -o /usr/share/keyrings/falco-archive-keyring.gpg /tmp/falco.asc
149
+ rm /tmp/falco.asc
150
+
151
+ # Add signed repo and install
152
+ echo "deb [signed-by=/usr/share/keyrings/falco-archive-keyring.gpg] https://download.falco.org/packages/deb stable main" \
153
+ | sudo tee /etc/apt/sources.list.d/falcosecurity.list
154
+ sudo apt-get update && sudo apt-get install -y falco
155
+ ```
156
+
157
+ #### Trivy (container image scanning)
158
+
159
+ ```bash
160
+ # Import GPG key and verify fingerprint
161
+ curl -fsSL https://aquasecurity.github.io/trivy-repo/deb/public.key -o /tmp/trivy.asc
162
+ gpg --show-keys /tmp/trivy.asc # Verify: 2E2D 3567 4616 32C8 4BB6 CD6F E9D0 A361 6276 FA6C
163
+ sudo gpg --dearmor -o /usr/share/keyrings/trivy-archive-keyring.gpg /tmp/trivy.asc
164
+ rm /tmp/trivy.asc
165
+
166
+ # Add signed repo and install
167
+ echo "deb [signed-by=/usr/share/keyrings/trivy-archive-keyring.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" \
168
+ | sudo tee /etc/apt/sources.list.d/trivy.list
169
+ sudo apt-get update && sudo apt-get install -y trivy
170
+ ```
171
+
172
+ #### Grype (vulnerability scanning)
173
+
174
+ ```bash
175
+ VERSION=v0.110.0
176
+ curl -fsSL -o /tmp/grype.tar.gz \
177
+ "https://github.com/anchore/grype/releases/download/${VERSION}/grype_${VERSION#v}_linux_amd64.tar.gz"
178
+ curl -fsSL -o /tmp/grype.tar.gz.sha256 \
179
+ "https://github.com/anchore/grype/releases/download/${VERSION}/grype_${VERSION#v}_checksums.txt"
180
+
181
+ # Verify checksum
182
+ cd /tmp && grep "linux_amd64.tar.gz" grype.tar.gz.sha256 | sha256sum -c -
183
+ tar xzf grype.tar.gz grype && sudo install grype /usr/local/bin/grype
184
+ rm -f /tmp/grype /tmp/grype.tar.gz /tmp/grype.tar.gz.sha256
185
+ ```
186
+
187
+ #### Syft (SBOM generation)
188
+
189
+ ```bash
190
+ VERSION=v1.42.3
191
+ curl -fsSL -o /tmp/syft.tar.gz \
192
+ "https://github.com/anchore/syft/releases/download/${VERSION}/syft_${VERSION#v}_linux_amd64.tar.gz"
193
+ curl -fsSL -o /tmp/syft_checksums.txt \
194
+ "https://github.com/anchore/syft/releases/download/${VERSION}/syft_${VERSION#v}_checksums.txt"
195
+
196
+ # Verify checksum
197
+ cd /tmp && grep "linux_amd64.tar.gz" syft_checksums.txt | sha256sum -c -
198
+ tar xzf syft.tar.gz syft && sudo install syft /usr/local/bin/syft
199
+ rm -f /tmp/syft /tmp/syft.tar.gz /tmp/syft_checksums.txt
200
+ ```
201
+
202
+ #### TruffleHog (secret scanning)
203
+
204
+ ```bash
205
+ VERSION=v3.94.1
206
+ curl -fsSL -o /tmp/trufflehog.tar.gz \
207
+ "https://github.com/trufflesecurity/trufflehog/releases/download/${VERSION}/trufflehog_${VERSION#v}_linux_amd64.tar.gz"
208
+ curl -fsSL -o /tmp/trufflehog_checksums.txt \
209
+ "https://github.com/trufflesecurity/trufflehog/releases/download/${VERSION}/trufflehog_${VERSION#v}_checksums.txt"
210
+
211
+ # Verify checksum
212
+ cd /tmp && grep "linux_amd64.tar.gz" trufflehog_checksums.txt | sha256sum -c -
213
+ tar xzf trufflehog.tar.gz trufflehog && sudo install trufflehog /usr/local/bin/trufflehog
214
+ rm -f /tmp/trufflehog /tmp/trufflehog.tar.gz /tmp/trufflehog_checksums.txt
215
+ ```
216
+
217
+ #### slsa-verifier (supply chain verification)
105
218
 
106
- | Tool | Purpose | Install Command |
107
- |------|---------|----------------|
108
- | **Falco** | eBPF runtime security | `curl -fsSL https://falco.org/repo/falcosecurity-packages.asc \| sudo gpg --dearmor -o /usr/share/keyrings/falco-archive-keyring.gpg && echo "deb [signed-by=/usr/share/keyrings/falco-archive-keyring.gpg] https://download.falco.org/packages/deb stable main" \| sudo tee /etc/apt/sources.list.d/falcosecurity.list && sudo apt-get update && sudo apt-get install -y falco` |
109
- | **Trivy** | Container image scanning | `curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \| sh -s -- -b /usr/local/bin` |
110
- | **Grype** | Vulnerability scanning | `curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh \| sh -s -- -b /usr/local/bin` |
111
- | **Syft** | SBOM generation | `curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh \| sh -s -- -b /usr/local/bin` |
112
- | **TruffleHog** | Secret scanning | `curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh \| sh -s -- -b /usr/local/bin` |
113
- | **slsa-verifier** | Supply chain verification | Download from [GitHub releases](https://github.com/slsa-framework/slsa-verifier/releases) |
114
- | **cdxgen** | CycloneDX SBOM generation | `npm install -g @cyclonedx/cdxgen` |
219
+ ```bash
220
+ VERSION=v2.7.1
221
+ curl -fsSL -o /tmp/slsa-verifier \
222
+ "https://github.com/slsa-framework/slsa-verifier/releases/download/${VERSION}/slsa-verifier-linux-amd64"
223
+ curl -fsSL -o /tmp/slsa-verifier.sha256 \
224
+ "https://github.com/slsa-framework/slsa-verifier/releases/download/${VERSION}/slsa-verifier-linux-amd64.sha256"
225
+
226
+ # Verify checksum
227
+ cd /tmp && echo "$(cat slsa-verifier.sha256) slsa-verifier" | sha256sum -c -
228
+ sudo install slsa-verifier /usr/local/bin/slsa-verifier
229
+ rm -f /tmp/slsa-verifier /tmp/slsa-verifier.sha256
230
+ ```
231
+
232
+ #### cdxgen (CycloneDX SBOM generation)
233
+
234
+ ```bash
235
+ npm install -g @cyclonedx/cdxgen
236
+ ```
115
237
 
116
238
  ### Important Notes
117
239
 
@@ -120,37 +242,48 @@ These are **not available** in standard Debian/Ubuntu repos and require manual i
120
242
  - **`bpftool`**: On Debian Trixie, install the `bpftool` package directly (NOT `linux-tools-generic` which is Ubuntu-specific).
121
243
  - **`pam_pwquality`**: This is a PAM module (`libpam-pwquality`), not a standalone binary. Install via `apt-get install libpam-pwquality`.
122
244
 
123
- ## Installation
245
+ ## Quick Start
246
+
247
+ ### Step 1: Install the server
124
248
 
125
- ### Option A: npm (recommended)
249
+ Pick one method:
126
250
 
251
+ **npm (recommended):**
127
252
  ```bash
128
253
  npm install -g defense-mcp-server
129
254
  ```
130
255
 
131
- ### Option B: Clone and build
256
+ **Or clone and build from source:**
257
+ ```bash
258
+ git clone https://github.com/bottobot/defense-mcp-server.git
259
+ cd defense-mcp-server
260
+ npm install && npm run build
261
+ ```
132
262
 
133
- 1. Clone the repository:
134
- ```bash
135
- git clone https://github.com/bottobot/defense-mcp-server.git
136
- cd defense-mcp-server
137
- ```
263
+ ### Step 2: Connect to your AI client
138
264
 
139
- 2. Install dependencies:
140
- ```bash
141
- npm install
142
- ```
265
+ The server supports any MCP client. Pick your client below and add the configuration:
143
266
 
144
- 3. Build:
145
- ```bash
146
- npm run build
147
- ```
267
+ **Claude Code (CLI / VS Code / JetBrains):**
148
268
 
149
- ## Connecting to Claude Desktop
269
+ Create a `.mcp.json` file in your project root (or `~/.claude/` for global):
270
+ ```json
271
+ {
272
+ "mcpServers": {
273
+ "defense-mcp-server": {
274
+ "command": "defense-mcp-server",
275
+ "env": {
276
+ "DEFENSE_MCP_DRY_RUN": "true",
277
+ "DEFENSE_MCP_ALLOWED_DIRS": "/tmp,/home,/var/log"
278
+ }
279
+ }
280
+ }
281
+ }
282
+ ```
150
283
 
151
- Add this to your Claude Desktop configuration file (`~/.config/claude/claude_desktop_config.json` on Linux):
284
+ **Claude Desktop:**
152
285
 
153
- **If installed globally via npm:**
286
+ Edit `~/.config/claude/claude_desktop_config.json` (Linux) or `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
154
287
  ```json
155
288
  {
156
289
  "mcpServers": {
@@ -160,30 +293,50 @@ Add this to your Claude Desktop configuration file (`~/.config/claude/claude_des
160
293
  }
161
294
  }
162
295
  ```
296
+ Restart Claude Desktop. The server will appear in the MCP tools panel.
163
297
 
164
- **If cloned and built locally:**
298
+ **Cursor / Other MCP clients:**
165
299
  ```json
166
300
  {
167
301
  "mcpServers": {
168
302
  "defense-mcp-server": {
169
- "command": "node",
170
- "args": ["/path/to/defense-mcp-server/build/index.js"]
303
+ "command": "defense-mcp-server"
171
304
  }
172
305
  }
173
306
  }
174
307
  ```
175
308
 
176
- Replace `/path/to/` with the actual path where you cloned the repo.
309
+ **If you cloned and built from source**, replace `"defense-mcp-server"` in the command field with:
310
+ ```json
311
+ "command": "node",
312
+ "args": ["/path/to/defense-mcp-server/build/index.js"]
313
+ ```
177
314
 
178
- Restart Claude Desktop. The server will appear in the MCP tools panel.
315
+ ### Step 3: Verify it works
179
316
 
180
- ## Connecting to Other MCP Clients
317
+ Open your AI client and ask:
181
318
 
182
- Any MCP client that supports stdio transport can connect. The server communicates over stdin/stdout using the MCP protocol. Launch it with:
319
+ > "Check my firewall status"
183
320
 
184
- ```bash
185
- node build/index.js
186
- ```
321
+ The assistant should call the `firewall` tool and return your iptables/UFW rules. If it does, you're all set.
322
+
323
+ ### Step 4: Elevate when needed
324
+
325
+ Most audit tools (listing firewall rules, checking patches, reading logs) work without sudo. When a tool needs elevated privileges, the server will tell you. Elevate securely:
326
+
327
+ > "Elevate sudo with GUI dialog"
328
+
329
+ This opens a native password dialog (zenity/kdialog) — your password never passes through the AI conversation. The session auto-expires after 15 minutes, or you can drop it anytime:
330
+
331
+ > "Drop sudo privileges"
332
+
333
+ ### What happens on first run
334
+
335
+ 1. **Dry-run mode is on by default** — tools preview what they would do without changing anything
336
+ 2. **Missing tools are auto-installed** — if you ask for a malware scan but ClamAV isn't installed, the server installs it via apt/dnf automatically
337
+ 3. **All file access is restricted** — only `/tmp`, `/home`, and `/var/log` are accessible by default
338
+
339
+ To enable live changes (not just previews), set `DEFENSE_MCP_DRY_RUN=false` in the env config above.
187
340
 
188
341
  ## Usage Examples
189
342
 
@@ -227,6 +380,8 @@ Configuration is via environment variables. All have secure defaults:
227
380
  | `DEFENSE_MCP_AUTO_INSTALL` | `true` | Auto-install missing tool dependencies |
228
381
  | `DEFENSE_MCP_PREFLIGHT` | `true` | Enable pre-flight dependency checks |
229
382
  | `DEFENSE_MCP_PREFLIGHT_BANNERS` | `true` | Show pre-flight status in tool output |
383
+ | `MCP_TRANSPORT` | `stdio` | Transport mode: `stdio` or `http` |
384
+ | `MCP_PORT` | `3100` | HTTP server port (when `MCP_TRANSPORT=http`) |
230
385
 
231
386
  To apply changes for real (not just preview), set:
232
387
  ```bash
@@ -235,16 +390,125 @@ DEFENSE_MCP_DRY_RUN=false node build/index.js
235
390
 
236
391
  ## Security
237
392
 
238
- This server is designed to be safe by default:
393
+ A security tool that isn't secure itself is worse than useless. This server implements defense-in-depth across 10 layers, from configuration defaults down to cryptographic verification.
394
+
395
+ ### MCP Specification Compliance
396
+
397
+ The [MCP spec](https://modelcontextprotocol.io/) defines security requirements for servers. Here's how this project meets them:
398
+
399
+ | MCP Requirement | Implementation |
400
+ |----------------|---------------|
401
+ | **Validate all tool inputs** | Zod schemas on every parameter + 15 specialized validators (paths, IPs, ports, service names, etc.) |
402
+ | **Implement access controls** | 200+ entry command allowlist, `shell: false` enforced, sudo session management |
403
+ | **Rate limit tool invocations** | 30/tool/min, 100 global/min, auth failure throttling (5 attempts per 5 minutes) |
404
+ | **Sanitize tool outputs** | Error sanitization strips paths, stack traces, and truncates to 500 chars |
405
+
406
+ ### Layer 1: Safe Defaults
407
+
408
+ Everything is locked down out of the box. You have to explicitly opt in to making changes:
409
+
410
+ - **Dry-run mode on by default** — every tool previews what it would do before touching anything
411
+ - **Backups before changes** — system state is backed up automatically before any modification
412
+ - **Confirmation required** — destructive actions need explicit confirmation
413
+ - **Restricted directories** — the server can only access explicitly allowed paths; root `/`, `/etc`, `/usr`, `/bin`, `/sbin` are blocked by default
414
+ - **Protected paths** — system-critical files are blocked from modification regardless of directory config
415
+
416
+ ### Layer 2: Command Execution
417
+
418
+ No tool can run arbitrary commands. Every command goes through multiple gates:
419
+
420
+ - **Binary allowlist** — 200+ pre-approved binaries across 18 categories. If a binary isn't on the list, it doesn't run. Period.
421
+ - **Absolute path resolution** — binaries are resolved to absolute paths at startup via `fs.existsSync()`, never through `which` or PATH
422
+ - **`shell: false` enforced** — hardcoded, cannot be overridden. Shell metacharacters (`;`, `|`, `&`, `` ` ``, `$`, etc.) have no effect
423
+ - **TOCTOU detection** — binary inodes are recorded at startup and verified before execution to detect replacement
424
+ - **No fallback** — if a binary can't be resolved to a known path, execution is refused entirely
425
+
426
+ ### Layer 3: Input Validation
427
+
428
+ Every parameter is validated before it reaches any tool handler:
429
+
430
+ - **Zod schemas** — runtime type checking with string length limits, enum constraints, numeric ranges, array bounds
431
+ - **Path traversal protection** — `../` sequences rejected, null bytes blocked, symlinks resolved and re-validated
432
+ - **Shell metacharacter blocking** — `[;|&$\`(){}<>!\\\n\r]` stripped from all inputs
433
+ - **Control character rejection** — `[\x00-\x08\x0e-\x1f\x7f]` blocked
434
+ - **Specialized validators** for: targets (hostname/IP/CIDR), ports, service names, sysctl keys, package names, iptables chains, network interfaces, usernames, YARA rules, certificate paths, firewall zones, auditd keys
435
+ - **ReDoS protection** — regex patterns limited to 200 characters, nested quantifiers and excessive alternation rejected
436
+
437
+ ### Layer 4: Sudo & Privilege Management
438
+
439
+ The server never asks for your password in a way an AI can see:
440
+
441
+ - **GUI elevation** — `sudo_elevate_gui` opens a native zenity/kdialog dialog. The password goes directly to sudo, never through the AI conversation
442
+ - **Buffer storage** — passwords are stored as Node.js Buffers (not V8 strings), which can be explicitly zeroed from memory
443
+ - **Auto-zeroing** — password buffer is zeroed on session drop, timeout expiry, and process exit
444
+ - **Credential validation** — password is tested with `sudo -S -k -v` before being accepted
445
+ - **Auth rate limiting** — 5 failed attempts per 5 minutes, then locked out
446
+ - **Session UID guard** — session is dropped immediately if the OS user ID changes
447
+ - **NOPASSWD:ALL rejection** — the sudoers management tool explicitly refuses to write `NOPASSWD: ALL` rules
448
+ - **`sudo -S` stdin piping** — passwords are piped via stdin, never passed as command-line arguments (which would be visible in `ps`)
449
+ - **40+ permission error patterns** — detected and surfaced with clear elevation prompts instead of cryptic failures
450
+
451
+ ### Layer 5: Secure File Operations
452
+
453
+ Every file write is atomic and permission-hardened:
454
+
455
+ - **Atomic writes** — write to temp file, then rename. No partial writes, no corruption on crash
456
+ - **Owner-only permissions** — files created with `0o600`, directories with `0o700`
457
+ - **Explicit chmod** — permissions enforced independently of umask
458
+ - **Symlink protection** — real paths resolved and re-validated against allowed directories
459
+ - **Backup before modify** — timestamped backups with manifest tracking under `~/.defense-mcp/backups/`
460
+
461
+ ### Layer 6: Encrypted State Storage
462
+
463
+ Sensitive runtime data is encrypted at rest:
464
+
465
+ - **Algorithm**: AES-256-GCM (authenticated encryption)
466
+ - **Key derivation**: PBKDF2 with 100,000 iterations, SHA-512
467
+ - **IV**: 96-bit (GCM-recommended)
468
+ - **Auth tag**: 128-bit
469
+ - **Salt**: 128-bit per file
470
+ - **Fallback**: plaintext JSON with warning when no key is configured (`DEFENSE_MCP_STATE_KEY`)
471
+
472
+ ### Layer 7: Supply Chain Security
473
+
474
+ Auto-installed tools are verified, not blindly trusted:
475
+
476
+ - **System packages** — installed only via official package manager (apt/dnf/pacman)
477
+ - **pip allowlist** — only 9 pre-approved packages (yara-python, python-nmap, etc.)
478
+ - **npm allowlist** — only 2 pre-approved packages (cdxgen, snyk)
479
+ - **Third-party tools** (Falco, Trivy, Grype, Syft, TruffleHog, slsa-verifier):
480
+ - Never uses `curl | sh` — all downloads verified before execution
481
+ - SHA256 checksums hardcoded in manifest
482
+ - GPG fingerprints verified against known-good values
483
+ - Cosign verification where available
484
+ - Requires explicit `DEFENSE_MCP_THIRD_PARTY_INSTALL=true` to enable
485
+
486
+ ### Layer 8: Rate Limiting & Safeguards
487
+
488
+ Protection against runaway or abusive tool invocations:
489
+
490
+ - **Per-tool limit**: 30 invocations per 60 seconds
491
+ - **Global limit**: 100 invocations per 60 seconds
492
+ - **Running service detection** — detects VS Code, Docker, databases, web servers, MCP servers, SSH sessions before operations that could affect them
493
+ - **Pre-flight validation** — every tool checks dependencies, privileges, and safeguards before executing
494
+
495
+ ### Layer 9: Audit Trail & Rollback
496
+
497
+ Every change is recorded and reversible:
498
+
499
+ - **Structured changelog** — JSON entries with tool name, action, target, before/after values, timestamp
500
+ - **Rollback commands** — stored with each change, validated against command allowlist
501
+ - **Structured logging** — JSON-formatted security events to stderr with file rotation (10 MB, 5 files)
502
+ - **Security log level** — critical events always logged regardless of log level setting
503
+
504
+ ### Layer 10: Policy Engine
505
+
506
+ Hardening policies are enforced safely:
239
507
 
240
- - Commands execute with `shell: false`no shell interpretation
241
- - All binaries resolved against a 190-entry allowlist at startup
242
- - Input validated with Zod schemas before execution
243
- - Passwords handled as Buffers (zeroed after use, never logged)
244
- - Rate limited to prevent abuse (30/tool/min, 100 global/min)
245
- - All file writes go through secure-fs with audit trail
246
- - Encrypted state storage (AES-256-GCM) for sensitive runtime data
247
- - Atomic file writes (write-to-temp-then-rename) to prevent corruption
508
+ - **No shell interpreters**policy check and remediation commands use direct binary invocation only
509
+ - **Regex safety** pattern length limits (200 chars), nested quantifier rejection
510
+ - **Severity classification** critical, high, medium, low, info
511
+ - **Secure policy storage** policy files created with `0o700` directory permissions
248
512
 
249
513
  For the full security architecture, see [ARCHITECTURE.md](docs/ARCHITECTURE.md).
250
514
 
@@ -269,7 +533,7 @@ npm run audit:security
269
533
 
270
534
  ## Test Coverage
271
535
 
272
- - **1,801+ tests** across 60+ test files
536
+ - **2,048+ tests** across 62 test files
273
537
  - Every source module (core + tools) has a corresponding test file
274
538
  - Coverage enforced in CI pipeline
275
539
 
@@ -1 +1 @@
1
- {"version":3,"file":"auto-installer.d.ts","sourceRoot":"","sources":["../../src/core/auto-installer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AASH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AASvD,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,QAAQ,GAAG,eAAe,GAAG,aAAa,GAAG,SAAS,GAAG,MAAM,CAAC;IACtE,MAAM,EACF,gBAAgB,GAChB,KAAK,GACL,KAAK,GACL,OAAO,GACP,YAAY,GACZ,iBAAiB,GACjB,mBAAmB,GACnB,UAAU,GACV,SAAS,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,cAAc,EAAE,CAAC;IAC5B,WAAW,EAAE,OAAO,CAAC;IACrB,sBAAsB,EAAE,MAAM,EAAE,CAAC;CAClC;AA0FD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEzD;AAoOD,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAC,SAAS,CAA8B;IACtD,OAAO,CAAC,WAAW,CAA2B;IAE9C,4CAA4C;IAC5C,MAAM,CAAC,QAAQ,IAAI,aAAa;IAahC;;;OAGG;IACH,MAAM,CAAC,aAAa,IAAI,IAAI;IAI5B,mDAAmD;IACnD,SAAS,IAAI,OAAO;IAIpB;;;;;OAKG;IACG,UAAU,CACd,QAAQ,EAAE,YAAY,EACtB,eAAe,EAAE,MAAM,EAAE,EACzB,aAAa,CAAC,EAAE,MAAM,EAAE,EACxB,UAAU,CAAC,EAAE,MAAM,EAAE,EACrB,gBAAgB,CAAC,EAAE,MAAM,EAAE,GAC1B,OAAO,CAAC,iBAAiB,CAAC;IA6G7B;;;;;;OAMG;IACG,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAuP5D;;;;;;;OAOG;IACG,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAiHlE;;;;;;OAMG;IACG,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IA8G7D;;;;;;OAMG;IACG,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAyG1D;;OAEG;YACW,SAAS;IAOvB;;OAEG;IACH,OAAO,CAAC,aAAa;CAmBtB"}
1
+ {"version":3,"file":"auto-installer.d.ts","sourceRoot":"","sources":["../../src/core/auto-installer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AASH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AASvD,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,QAAQ,GAAG,eAAe,GAAG,aAAa,GAAG,SAAS,GAAG,MAAM,CAAC;IACtE,MAAM,EACF,gBAAgB,GAChB,KAAK,GACL,KAAK,GACL,OAAO,GACP,YAAY,GACZ,iBAAiB,GACjB,mBAAmB,GACnB,UAAU,GACV,SAAS,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,cAAc,EAAE,CAAC;IAC5B,WAAW,EAAE,OAAO,CAAC;IACrB,sBAAsB,EAAE,MAAM,EAAE,CAAC;CAClC;AA0FD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEzD;AAoOD,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAC,SAAS,CAA8B;IACtD,OAAO,CAAC,WAAW,CAA2B;IAE9C,4CAA4C;IAC5C,MAAM,CAAC,QAAQ,IAAI,aAAa;IAahC;;;OAGG;IACH,MAAM,CAAC,aAAa,IAAI,IAAI;IAI5B,mDAAmD;IACnD,SAAS,IAAI,OAAO;IAIpB;;;;;OAKG;IACG,UAAU,CACd,QAAQ,EAAE,YAAY,EACtB,eAAe,EAAE,MAAM,EAAE,EACzB,aAAa,CAAC,EAAE,MAAM,EAAE,EACxB,UAAU,CAAC,EAAE,MAAM,EAAE,EACrB,gBAAgB,CAAC,EAAE,MAAM,EAAE,GAC1B,OAAO,CAAC,iBAAiB,CAAC;IA6G7B;;;;;;OAMG;IACG,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAsP5D;;;;;;;OAOG;IACG,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAgHlE;;;;;;OAMG;IACG,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IA8G7D;;;;;;OAMG;IACG,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAwG1D;;OAEG;YACW,SAAS;IAOvB;;OAEG;IACH,OAAO,CAAC,aAAa;CAmBtB"}
@@ -589,7 +589,6 @@ export class AutoInstaller {
589
589
  const useSudo = distro.packageManager !== "brew";
590
590
  const result = execWithSudo(installArgs, { useSudo, timeoutMs: 300_000 });
591
591
  if (!result.success) {
592
- const elapsed = ((Date.now() - start) / 1000).toFixed(1);
593
592
  console.error(`[auto-installer] ✗ Failed to install '${binary}' (package: ${packageName}): ${result.stderr.slice(0, 200)}`);
594
593
  return {
595
594
  dependency: binary,
@@ -695,7 +694,6 @@ export class AutoInstaller {
695
694
  result = execWithSudo([pip, "install", module], { timeoutMs: 120_000 });
696
695
  }
697
696
  if (!result.success) {
698
- const elapsed = ((Date.now() - start) / 1000).toFixed(1);
699
697
  console.error(`[auto-installer] ✗ Failed to install Python module '${module}': ${result.stderr.slice(0, 200)}`);
700
698
  return {
701
699
  dependency: module,
@@ -888,7 +886,6 @@ export class AutoInstaller {
888
886
  lastError = result.stderr.slice(0, 200);
889
887
  }
890
888
  if (!installed) {
891
- const elapsed = ((Date.now() - start) / 1000).toFixed(1);
892
889
  console.error(`[auto-installer] ✗ Failed to install library '${lib}': ${lastError}`);
893
890
  return {
894
891
  dependency: lib,
@@ -1 +1 @@
1
- {"version":3,"file":"backup-manager.d.ts","sourceRoot":"","sources":["../../src/core/backup-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAkBH,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE,WAAW,EAAE,CAAC;CACxB;AAUD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAmC1E;AAID,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAE1B,SAAS,CAAC,EAAE,MAAM;IAK9B,sCAAsC;IACtC,OAAO,CAAC,SAAS;IAIjB,8DAA8D;IAC9D,OAAO,CAAC,YAAY;IAgBpB,8BAA8B;IAC9B,OAAO,CAAC,aAAa;IAKrB;;;OAGG;IACH,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW;IAqCzC;;;OAGG;IACG,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAK/C;;OAEG;IACG,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAqB9C;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAO3C;;OAEG;IACG,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CA+BtD"}
1
+ {"version":3,"file":"backup-manager.d.ts","sourceRoot":"","sources":["../../src/core/backup-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAgBH,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE,WAAW,EAAE,CAAC;CACxB;AAUD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAiC1E;AAID,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAE1B,SAAS,CAAC,EAAE,MAAM;IAK9B,sCAAsC;IACtC,OAAO,CAAC,SAAS;IAIjB,8DAA8D;IAC9D,OAAO,CAAC,YAAY;IAepB,8BAA8B;IAC9B,OAAO,CAAC,aAAa;IAKrB;;;OAGG;IACH,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW;IAyCzC;;;OAGG;IACG,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAK/C;;OAEG;IACG,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAyB9C;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAO3C;;OAEG;IACG,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAiCtD"}
@@ -4,7 +4,7 @@
4
4
  * Backups are stored under ~/.defense-mcp/backups/ with timestamped filenames.
5
5
  * A manifest.json tracks all backups for listing and restore operations.
6
6
  */
7
- import { existsSync, readFileSync, unlinkSync, statSync, lstatSync, } from "node:fs";
7
+ import { readFileSync, unlinkSync, statSync, lstatSync, } from "node:fs";
8
8
  import { join, basename, dirname, resolve as pathResolve } from "node:path";
9
9
  import { secureWriteFileSync, secureMkdirSync, secureCopyFileSync } from "./secure-fs.js";
10
10
  import { homedir } from "node:os";
@@ -38,20 +38,18 @@ export function validateBackupPath(filePath, baseDir) {
38
38
  if (!resolved.startsWith(resolvedBase + "/") && resolved !== resolvedBase) {
39
39
  throw new Error(`SECURITY: Backup path '${resolved}' escapes base directory '${resolvedBase}'`);
40
40
  }
41
- // 4. Reject symlinks (if the path exists)
42
- if (existsSync(filePath)) {
43
- try {
44
- const lstats = lstatSync(filePath);
45
- if (lstats.isSymbolicLink()) {
46
- throw new Error(`SECURITY: Backup path '${filePath}' is a symlink. Refusing to use.`);
47
- }
41
+ // 4. Reject symlinks use lstatSync directly to avoid TOCTOU
42
+ try {
43
+ const lstats = lstatSync(filePath);
44
+ if (lstats.isSymbolicLink()) {
45
+ throw new Error(`SECURITY: Backup path '${filePath}' is a symlink. Refusing to use.`);
48
46
  }
49
- catch (err) {
50
- if (err instanceof Error && err.message.startsWith("SECURITY:")) {
51
- throw err;
52
- }
53
- // lstat failure on existing path is suspicious but non-fatal for validation
47
+ }
48
+ catch (err) {
49
+ if (err instanceof Error && err.message.startsWith("SECURITY:")) {
50
+ throw err;
54
51
  }
52
+ // ENOENT (path doesn't exist yet) is fine for validation; other errors are non-fatal
55
53
  }
56
54
  }
57
55
  // ── BackupManager ────────────────────────────────────────────────────────────
@@ -69,17 +67,16 @@ export class BackupManager {
69
67
  /** Read manifest from disk with migration from old format. */
70
68
  readManifest() {
71
69
  try {
72
- if (existsSync(this.manifestPath)) {
73
- const raw = readFileSync(this.manifestPath, "utf-8");
74
- const parsed = JSON.parse(raw);
75
- if (parsed && Array.isArray(parsed.backups)) {
76
- // Migrate: ensure version field is present (old format may lack it)
77
- return { version: 1, backups: parsed.backups };
78
- }
70
+ // Read directly — avoids TOCTOU between existsSync and readFileSync
71
+ const raw = readFileSync(this.manifestPath, "utf-8");
72
+ const parsed = JSON.parse(raw);
73
+ if (parsed && Array.isArray(parsed.backups)) {
74
+ // Migrate: ensure version field is present (old format may lack it)
75
+ return { version: 1, backups: parsed.backups };
79
76
  }
80
77
  }
81
78
  catch {
82
- // Corrupt or missing manifest — start fresh
79
+ // Missing, unreadable, or corrupt manifest — start fresh
83
80
  }
84
81
  return { version: 1, backups: [] };
85
82
  }
@@ -95,9 +92,6 @@ export class BackupManager {
95
92
  backupSync(filePath) {
96
93
  const validated = FilePathSchema.parse(filePath);
97
94
  this.ensureDir();
98
- if (!existsSync(validated)) {
99
- throw new Error(`Source file does not exist: ${validated}`);
100
- }
101
95
  const id = randomUUID();
102
96
  const now = new Date();
103
97
  const ts = now.toISOString().replace(/[:.]/g, "-");
@@ -106,7 +100,16 @@ export class BackupManager {
106
100
  const backupPath = join(this.backupDir, backupName);
107
101
  // SECURITY (CORE-015): Validate the backup destination path
108
102
  validateBackupPath(backupPath, this.backupDir);
109
- secureCopyFileSync(validated, backupPath);
103
+ // Copy atomically — no prior existsSync to avoid TOCTOU
104
+ try {
105
+ secureCopyFileSync(validated, backupPath);
106
+ }
107
+ catch (err) {
108
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
109
+ throw new Error(`Source file does not exist: ${validated}`);
110
+ }
111
+ throw err;
112
+ }
110
113
  const stat = statSync(backupPath);
111
114
  const entry = {
112
115
  id,
@@ -139,13 +142,19 @@ export class BackupManager {
139
142
  if (!entry) {
140
143
  throw new Error(`Backup not found: ${validated}`);
141
144
  }
142
- if (!existsSync(entry.backupPath)) {
143
- throw new Error(`Backup file missing on disk: ${entry.backupPath}`);
144
- }
145
145
  // SECURITY (CORE-015): Validate the backup source path before restore
146
146
  validateBackupPath(entry.backupPath, this.backupDir);
147
147
  secureMkdirSync(dirname(entry.originalPath));
148
- secureCopyFileSync(entry.backupPath, entry.originalPath);
148
+ // Copy atomically — no prior existsSync to avoid TOCTOU
149
+ try {
150
+ secureCopyFileSync(entry.backupPath, entry.originalPath);
151
+ }
152
+ catch (err) {
153
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
154
+ throw new Error(`Backup file missing on disk: ${entry.backupPath}`);
155
+ }
156
+ throw err;
157
+ }
149
158
  console.error(`[backup-manager] Restored ${entry.backupPath} → ${entry.originalPath}`);
150
159
  }
151
160
  /**
@@ -174,12 +183,14 @@ export class BackupManager {
174
183
  }
175
184
  for (const entry of remove) {
176
185
  try {
177
- if (existsSync(entry.backupPath)) {
178
- unlinkSync(entry.backupPath);
179
- }
186
+ // Unlink directly — no prior existsSync to avoid TOCTOU
187
+ unlinkSync(entry.backupPath);
180
188
  }
181
189
  catch (err) {
182
- console.error(`[backup-manager] Failed to delete ${entry.backupPath}: ${err instanceof Error ? err.message : String(err)}`);
190
+ // ENOENT is fine (already gone); log other errors
191
+ if (!(err instanceof Error && "code" in err && err.code === "ENOENT")) {
192
+ console.error(`[backup-manager] Failed to delete ${entry.backupPath}: ${err instanceof Error ? err.message : String(err)}`);
193
+ }
183
194
  }
184
195
  }
185
196
  manifest.backups = keep;