defense-mcp-server 0.9.2 → 0.9.4

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 (170) hide show
  1. package/build/core/auto-installer.js +31 -31
  2. package/build/core/command-allowlist.js +1 -1
  3. package/build/core/dependency-validator.js +9 -9
  4. package/build/core/distro-adapter.js +0 -7
  5. package/build/core/distro.js +0 -48
  6. package/build/core/encrypted-state.js +0 -7
  7. package/build/core/logger.js +1 -1
  8. package/build/core/pam-utils.js +1 -1
  9. package/build/core/parsers.js +1 -1
  10. package/build/core/preflight.js +13 -13
  11. package/build/core/progress.js +20 -20
  12. package/build/core/run-command.js +46 -0
  13. package/build/core/sudo-guard.js +4 -4
  14. package/build/core/third-party-installer.js +4 -4
  15. package/build/core/tool-wrapper.js +3 -3
  16. package/build/tools/access-control.js +6 -6
  17. package/build/tools/api-security.js +5 -51
  18. package/build/tools/app-hardening.js +23 -25
  19. package/build/tools/cloud-security.js +5 -51
  20. package/build/tools/compliance.js +9 -13
  21. package/build/tools/container-security.js +51 -52
  22. package/build/tools/deception.js +8 -54
  23. package/build/tools/dns-security.js +2 -48
  24. package/build/tools/encryption.js +86 -87
  25. package/build/tools/firewall.js +324 -30
  26. package/build/tools/hardening.js +12 -13
  27. package/build/tools/incident-response.js +3 -3
  28. package/build/tools/logging.js +17 -59
  29. package/build/tools/malware.js +2 -2
  30. package/build/tools/meta.js +86 -165
  31. package/build/tools/network-defense.js +3 -3
  32. package/build/tools/patch-management.js +8 -8
  33. package/build/tools/process-security.js +38 -92
  34. package/build/tools/sudo-management.js +36 -36
  35. package/build/tools/threat-intel.js +2 -48
  36. package/build/tools/vulnerability-management.js +3 -49
  37. package/build/tools/waf.js +47 -93
  38. package/build/tools/wireless-security.js +9 -55
  39. package/package.json +5 -3
  40. package/build/core/auto-installer.d.ts +0 -102
  41. package/build/core/auto-installer.d.ts.map +0 -1
  42. package/build/core/backup-manager.d.ts +0 -63
  43. package/build/core/backup-manager.d.ts.map +0 -1
  44. package/build/core/changelog.d.ts +0 -119
  45. package/build/core/changelog.d.ts.map +0 -1
  46. package/build/core/command-allowlist.d.ts +0 -129
  47. package/build/core/command-allowlist.d.ts.map +0 -1
  48. package/build/core/config.d.ts +0 -107
  49. package/build/core/config.d.ts.map +0 -1
  50. package/build/core/dependency-validator.d.ts +0 -106
  51. package/build/core/dependency-validator.d.ts.map +0 -1
  52. package/build/core/distro-adapter.d.ts +0 -177
  53. package/build/core/distro-adapter.d.ts.map +0 -1
  54. package/build/core/distro.d.ts +0 -68
  55. package/build/core/distro.d.ts.map +0 -1
  56. package/build/core/encrypted-state.d.ts +0 -76
  57. package/build/core/encrypted-state.d.ts.map +0 -1
  58. package/build/core/executor.d.ts +0 -65
  59. package/build/core/executor.d.ts.map +0 -1
  60. package/build/core/installer.d.ts +0 -129
  61. package/build/core/installer.d.ts.map +0 -1
  62. package/build/core/logger.d.ts +0 -118
  63. package/build/core/logger.d.ts.map +0 -1
  64. package/build/core/metrics.d.ts +0 -74
  65. package/build/core/metrics.d.ts.map +0 -1
  66. package/build/core/metrics.js +0 -97
  67. package/build/core/output-redactor.d.ts +0 -26
  68. package/build/core/output-redactor.d.ts.map +0 -1
  69. package/build/core/pam-utils.d.ts +0 -356
  70. package/build/core/pam-utils.d.ts.map +0 -1
  71. package/build/core/parsers.d.ts +0 -191
  72. package/build/core/parsers.d.ts.map +0 -1
  73. package/build/core/policy-engine.d.ts +0 -170
  74. package/build/core/policy-engine.d.ts.map +0 -1
  75. package/build/core/preflight.d.ts +0 -157
  76. package/build/core/preflight.d.ts.map +0 -1
  77. package/build/core/privilege-manager.d.ts +0 -108
  78. package/build/core/privilege-manager.d.ts.map +0 -1
  79. package/build/core/progress.d.ts +0 -99
  80. package/build/core/progress.d.ts.map +0 -1
  81. package/build/core/rate-limiter.d.ts +0 -101
  82. package/build/core/rate-limiter.d.ts.map +0 -1
  83. package/build/core/rollback.d.ts +0 -73
  84. package/build/core/rollback.d.ts.map +0 -1
  85. package/build/core/safeguards.d.ts +0 -58
  86. package/build/core/safeguards.d.ts.map +0 -1
  87. package/build/core/sanitizer.d.ts +0 -118
  88. package/build/core/sanitizer.d.ts.map +0 -1
  89. package/build/core/secure-fs.d.ts +0 -67
  90. package/build/core/secure-fs.d.ts.map +0 -1
  91. package/build/core/spawn-safe.d.ts +0 -55
  92. package/build/core/spawn-safe.d.ts.map +0 -1
  93. package/build/core/sudo-guard.d.ts +0 -167
  94. package/build/core/sudo-guard.d.ts.map +0 -1
  95. package/build/core/sudo-session.d.ts +0 -143
  96. package/build/core/sudo-session.d.ts.map +0 -1
  97. package/build/core/third-party-installer.d.ts +0 -58
  98. package/build/core/third-party-installer.d.ts.map +0 -1
  99. package/build/core/third-party-manifest.d.ts +0 -48
  100. package/build/core/third-party-manifest.d.ts.map +0 -1
  101. package/build/core/tool-annotations.d.ts +0 -13
  102. package/build/core/tool-annotations.d.ts.map +0 -1
  103. package/build/core/tool-dependencies.d.ts +0 -60
  104. package/build/core/tool-dependencies.d.ts.map +0 -1
  105. package/build/core/tool-durations.d.ts +0 -71
  106. package/build/core/tool-durations.d.ts.map +0 -1
  107. package/build/core/tool-registry.d.ts +0 -112
  108. package/build/core/tool-registry.d.ts.map +0 -1
  109. package/build/core/tool-wrapper.d.ts +0 -73
  110. package/build/core/tool-wrapper.d.ts.map +0 -1
  111. package/build/index.d.ts +0 -3
  112. package/build/index.d.ts.map +0 -1
  113. package/build/tools/access-control.d.ts +0 -11
  114. package/build/tools/access-control.d.ts.map +0 -1
  115. package/build/tools/api-security.d.ts +0 -12
  116. package/build/tools/api-security.d.ts.map +0 -1
  117. package/build/tools/app-hardening.d.ts +0 -11
  118. package/build/tools/app-hardening.d.ts.map +0 -1
  119. package/build/tools/backup.d.ts +0 -8
  120. package/build/tools/backup.d.ts.map +0 -1
  121. package/build/tools/cloud-security.d.ts +0 -17
  122. package/build/tools/cloud-security.d.ts.map +0 -1
  123. package/build/tools/compliance.d.ts +0 -11
  124. package/build/tools/compliance.d.ts.map +0 -1
  125. package/build/tools/container-security.d.ts +0 -14
  126. package/build/tools/container-security.d.ts.map +0 -1
  127. package/build/tools/deception.d.ts +0 -13
  128. package/build/tools/deception.d.ts.map +0 -1
  129. package/build/tools/dns-security.d.ts +0 -93
  130. package/build/tools/dns-security.d.ts.map +0 -1
  131. package/build/tools/ebpf-security.d.ts +0 -15
  132. package/build/tools/ebpf-security.d.ts.map +0 -1
  133. package/build/tools/encryption.d.ts +0 -12
  134. package/build/tools/encryption.d.ts.map +0 -1
  135. package/build/tools/firewall.d.ts +0 -9
  136. package/build/tools/firewall.d.ts.map +0 -1
  137. package/build/tools/hardening.d.ts +0 -8
  138. package/build/tools/hardening.d.ts.map +0 -1
  139. package/build/tools/incident-response.d.ts +0 -11
  140. package/build/tools/incident-response.d.ts.map +0 -1
  141. package/build/tools/integrity.d.ts +0 -15
  142. package/build/tools/integrity.d.ts.map +0 -1
  143. package/build/tools/logging.d.ts +0 -21
  144. package/build/tools/logging.d.ts.map +0 -1
  145. package/build/tools/malware.d.ts +0 -10
  146. package/build/tools/malware.d.ts.map +0 -1
  147. package/build/tools/meta.d.ts +0 -13
  148. package/build/tools/meta.d.ts.map +0 -1
  149. package/build/tools/network-defense.d.ts +0 -11
  150. package/build/tools/network-defense.d.ts.map +0 -1
  151. package/build/tools/patch-management.d.ts +0 -3
  152. package/build/tools/patch-management.d.ts.map +0 -1
  153. package/build/tools/process-security.d.ts +0 -12
  154. package/build/tools/process-security.d.ts.map +0 -1
  155. package/build/tools/secrets.d.ts +0 -8
  156. package/build/tools/secrets.d.ts.map +0 -1
  157. package/build/tools/sudo-management.d.ts +0 -17
  158. package/build/tools/sudo-management.d.ts.map +0 -1
  159. package/build/tools/supply-chain-security.d.ts +0 -8
  160. package/build/tools/supply-chain-security.d.ts.map +0 -1
  161. package/build/tools/threat-intel.d.ts +0 -22
  162. package/build/tools/threat-intel.d.ts.map +0 -1
  163. package/build/tools/vulnerability-management.d.ts +0 -11
  164. package/build/tools/vulnerability-management.d.ts.map +0 -1
  165. package/build/tools/waf.d.ts +0 -12
  166. package/build/tools/waf.d.ts.map +0 -1
  167. package/build/tools/wireless-security.d.ts +0 -19
  168. package/build/tools/wireless-security.d.ts.map +0 -1
  169. package/build/tools/zero-trust-network.d.ts +0 -8
  170. package/build/tools/zero-trust-network.d.ts.map +0 -1
@@ -45,6 +45,123 @@ const CHAIN_NAME_REGEX = /^[A-Za-z_][A-Za-z0-9_-]{0,28}$/;
45
45
  // ── nftables table name regex ──────────────────────────────────────────────
46
46
  // TOOL-008: Added table name validation
47
47
  const NFTABLES_TABLE_NAME_RE = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
48
+ // ── Nftables detection helper ─────────────────────────────────────────────
49
+ /**
50
+ * Detect whether nftables is the active firewall backend on this system.
51
+ * Returns { active: true, reason } if nftables is managing the firewall,
52
+ * meaning iptables-persistent / UFW should NOT be installed on top of it.
53
+ */
54
+ async function detectNftablesActive() {
55
+ // 1. Check if nftables service is running
56
+ const svcResult = await executeCommand({
57
+ command: "systemctl",
58
+ args: ["is-active", "nftables"],
59
+ toolName: "firewall",
60
+ timeout: 5000,
61
+ });
62
+ const nftServiceActive = svcResult.stdout.trim() === "active";
63
+ // 2. Check if nft binary exists and has a non-trivial ruleset
64
+ const nftResult = await executeCommand({
65
+ command: "sudo",
66
+ args: ["nft", "list", "ruleset"],
67
+ toolName: "firewall",
68
+ timeout: 10000,
69
+ });
70
+ const hasNftRules = nftResult.exitCode === 0 &&
71
+ nftResult.stdout.trim().length > 50 &&
72
+ nftResult.stdout.includes("chain ");
73
+ // 3. Check if nftables is enabled at boot
74
+ const enabledResult = await executeCommand({
75
+ command: "systemctl",
76
+ args: ["is-enabled", "nftables"],
77
+ toolName: "firewall",
78
+ timeout: 5000,
79
+ });
80
+ const nftEnabled = enabledResult.stdout.trim() === "enabled";
81
+ if (nftServiceActive && hasNftRules) {
82
+ return { active: true, reason: "nftables service is running with active rules" };
83
+ }
84
+ if (hasNftRules && nftEnabled) {
85
+ return { active: true, reason: "nftables is enabled at boot with active rules" };
86
+ }
87
+ if (hasNftRules) {
88
+ return { active: true, reason: "nftables has active rules loaded" };
89
+ }
90
+ if (nftServiceActive) {
91
+ return { active: true, reason: "nftables service is running" };
92
+ }
93
+ return { active: false, reason: "nftables is not active" };
94
+ }
95
+ async function discoverActivePorts() {
96
+ const ports = [];
97
+ const seen = new Set();
98
+ // Discover listening TCP/UDP ports
99
+ const listenResult = await executeCommand({
100
+ command: "sudo",
101
+ args: ["ss", "-tlnpH"],
102
+ toolName: "firewall",
103
+ timeout: 10000,
104
+ });
105
+ if (listenResult.exitCode === 0) {
106
+ for (const line of listenResult.stdout.split("\n")) {
107
+ // Format: LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=...))
108
+ const match = line.match(/:(\d+)\s+[\d.*:]+\s+users:\(\("([^"]+)"/);
109
+ if (match) {
110
+ const port = parseInt(match[1], 10);
111
+ const process = match[2];
112
+ const key = `tcp:${port}:listen`;
113
+ if (!seen.has(key) && port >= 1 && port <= 65535) {
114
+ seen.add(key);
115
+ ports.push({ protocol: "tcp", port, process, direction: "listen" });
116
+ }
117
+ }
118
+ }
119
+ }
120
+ const listenUdpResult = await executeCommand({
121
+ command: "sudo",
122
+ args: ["ss", "-ulnpH"],
123
+ toolName: "firewall",
124
+ timeout: 10000,
125
+ });
126
+ if (listenUdpResult.exitCode === 0) {
127
+ for (const line of listenUdpResult.stdout.split("\n")) {
128
+ const match = line.match(/:(\d+)\s+[\d.*:]+\s+users:\(\("([^"]+)"/);
129
+ if (match) {
130
+ const port = parseInt(match[1], 10);
131
+ const process = match[2];
132
+ const key = `udp:${port}:listen`;
133
+ if (!seen.has(key) && port >= 1 && port <= 65535) {
134
+ seen.add(key);
135
+ ports.push({ protocol: "udp", port, process, direction: "listen" });
136
+ }
137
+ }
138
+ }
139
+ }
140
+ // Discover established outbound connections (remote ports programs are talking to)
141
+ const estResult = await executeCommand({
142
+ command: "sudo",
143
+ args: ["ss", "-tnpH", "state", "established"],
144
+ toolName: "firewall",
145
+ timeout: 10000,
146
+ });
147
+ if (estResult.exitCode === 0) {
148
+ for (const line of estResult.stdout.split("\n")) {
149
+ // Format: ESTAB 0 0 192.168.1.5:54321 1.2.3.4:443 users:(("curl",pid=...))
150
+ // We want the remote port (destination) to allow outbound traffic to it
151
+ const match = line.match(/\s+[\d.]+:\d+\s+[\d.]+:(\d+)\s+users:\(\("([^"]+)"/);
152
+ if (match) {
153
+ const port = parseInt(match[1], 10);
154
+ const process = match[2];
155
+ const key = `tcp:${port}:established-out`;
156
+ if (!seen.has(key) && port >= 1 && port <= 65535) {
157
+ seen.add(key);
158
+ ports.push({ protocol: "tcp", port, process, direction: "established-out" });
159
+ }
160
+ }
161
+ }
162
+ }
163
+ return ports;
164
+ }
48
165
  // ── Registration entry point ───────────────────────────────────────────────
49
166
  export function registerFirewallTools(server) {
50
167
  server.tool("firewall", "Firewall: iptables, UFW, nftables, persistence, policy audit", {
@@ -487,6 +604,7 @@ export function registerFirewallTools(server) {
487
604
  }
488
605
  const fullCmd = `sudo iptables -P ${chain} ${policy}`;
489
606
  const ipv6Cmd = `sudo ip6tables -P ${chain} ${policy}`;
607
+ const injectedRules = [];
490
608
  // ── SAFETY CHECK: Prevent DROP policy without essential allow rules ──
491
609
  if (policy === "DROP" && (chain === "INPUT" || chain === "FORWARD" || chain === "OUTPUT")) {
492
610
  const safetyRules = [];
@@ -547,7 +665,6 @@ export function registerFirewallTools(server) {
547
665
  addArgs6: ["-I", "OUTPUT", "6", "-p", "tcp", "--dport", "80", "-j", "ACCEPT"],
548
666
  });
549
667
  }
550
- const injectedRules = [];
551
668
  for (const rule of safetyRules) {
552
669
  const checkResult = await executeCommand({
553
670
  command: "sudo",
@@ -575,7 +692,7 @@ export function registerFirewallTools(server) {
575
692
  isError: true,
576
693
  };
577
694
  }
578
- injectedRules.push(`✅ Auto-added: ${rule.description}`);
695
+ injectedRules.push(`Auto-added: ${rule.description}`);
579
696
  if (ipv6 && rule.addArgs6) {
580
697
  const add6Result = await executeCommand({
581
698
  command: "sudo",
@@ -584,10 +701,10 @@ export function registerFirewallTools(server) {
584
701
  timeout: getToolTimeout("firewall"),
585
702
  });
586
703
  if (add6Result.exitCode !== 0) {
587
- injectedRules.push(`⚠️ IPv6: Failed to add "${rule.description}": ${add6Result.stderr}`);
704
+ injectedRules.push(`IPv6: Failed to add "${rule.description}": ${add6Result.stderr}`);
588
705
  }
589
706
  else {
590
- injectedRules.push(`✅ Auto-added (IPv6): ${rule.description}`);
707
+ injectedRules.push(`Auto-added (IPv6): ${rule.description}`);
591
708
  }
592
709
  }
593
710
  }
@@ -604,11 +721,97 @@ export function registerFirewallTools(server) {
604
721
  });
605
722
  logChange(safetyEntry);
606
723
  }
724
+ // ── AUTO-DISCOVER running services and allow their ports ──
725
+ // This prevents killing active programs (qbittorrent, apt, etc.)
726
+ const activePorts = await discoverActivePorts();
727
+ const serviceRules = [];
728
+ // Deduplicate: skip ports already covered by hardcoded safety rules
729
+ const hardcodedPorts = new Set(["tcp:53", "udp:53", "tcp:80", "tcp:443"]);
730
+ for (const ap of activePorts) {
731
+ const portKey = `${ap.protocol}:${ap.port}`;
732
+ if (hardcodedPorts.has(portKey))
733
+ continue;
734
+ if (chain === "INPUT" && ap.direction === "listen") {
735
+ // Allow inbound to listening ports
736
+ const checkArgs = ["-C", "INPUT", "-p", ap.protocol, "--dport", String(ap.port), "-j", "ACCEPT"];
737
+ const addArgs = ["-A", "INPUT", "-p", ap.protocol, "--dport", String(ap.port), "-j", "ACCEPT"];
738
+ const checkR = await executeCommand({
739
+ command: "sudo", args: ["iptables", ...checkArgs],
740
+ toolName: "firewall", timeout: getToolTimeout("firewall"),
741
+ });
742
+ if (checkR.exitCode !== 0) {
743
+ if (dry_run ?? getConfig().dryRun) {
744
+ serviceRules.push(`[DRY-RUN] Would allow INPUT ${ap.protocol}/${ap.port} (${ap.process})`);
745
+ }
746
+ else {
747
+ const addR = await executeCommand({
748
+ command: "sudo", args: ["iptables", ...addArgs],
749
+ toolName: "firewall", timeout: getToolTimeout("firewall"),
750
+ });
751
+ if (addR.exitCode === 0) {
752
+ serviceRules.push(`Auto-allowed INPUT ${ap.protocol}/${ap.port} (${ap.process})`);
753
+ if (ipv6) {
754
+ await executeCommand({
755
+ command: "sudo", args: ["ip6tables", ...addArgs],
756
+ toolName: "firewall", timeout: getToolTimeout("firewall"),
757
+ });
758
+ }
759
+ }
760
+ }
761
+ }
762
+ }
763
+ else if (chain === "OUTPUT" && (ap.direction === "listen" || ap.direction === "established-out")) {
764
+ // Allow outbound to remote ports that programs are actively using
765
+ const dportOrSport = ap.direction === "listen" ? "--sport" : "--dport";
766
+ const checkArgs = ["-C", "OUTPUT", "-p", ap.protocol, dportOrSport, String(ap.port), "-j", "ACCEPT"];
767
+ const addArgs = ["-A", "OUTPUT", "-p", ap.protocol, dportOrSport, String(ap.port), "-j", "ACCEPT"];
768
+ const checkR = await executeCommand({
769
+ command: "sudo", args: ["iptables", ...checkArgs],
770
+ toolName: "firewall", timeout: getToolTimeout("firewall"),
771
+ });
772
+ if (checkR.exitCode !== 0) {
773
+ if (dry_run ?? getConfig().dryRun) {
774
+ serviceRules.push(`[DRY-RUN] Would allow OUTPUT ${ap.protocol}/${dportOrSport.replace("--", "")} ${ap.port} (${ap.process})`);
775
+ }
776
+ else {
777
+ const addR = await executeCommand({
778
+ command: "sudo", args: ["iptables", ...addArgs],
779
+ toolName: "firewall", timeout: getToolTimeout("firewall"),
780
+ });
781
+ if (addR.exitCode === 0) {
782
+ serviceRules.push(`Auto-allowed OUTPUT ${ap.protocol}/${dportOrSport.replace("--", "")} ${ap.port} (${ap.process})`);
783
+ if (ipv6) {
784
+ await executeCommand({
785
+ command: "sudo", args: ["ip6tables", ...addArgs],
786
+ toolName: "firewall", timeout: getToolTimeout("firewall"),
787
+ });
788
+ }
789
+ }
790
+ }
791
+ }
792
+ }
793
+ }
794
+ if (serviceRules.length > 0) {
795
+ injectedRules.push(...serviceRules);
796
+ const serviceEntry = createChangeEntry({
797
+ tool: "firewall",
798
+ action: `Safety: auto-allowed ${serviceRules.length} active service ports before ${chain} DROP`,
799
+ target: chain,
800
+ after: serviceRules.join("; "),
801
+ dryRun: !!(dry_run ?? getConfig().dryRun),
802
+ success: true,
803
+ });
804
+ logChange(serviceEntry);
805
+ }
607
806
  }
608
807
  if (dry_run ?? getConfig().dryRun) {
609
808
  const cmds = [fullCmd];
610
809
  if (ipv6)
611
810
  cmds.push(ipv6Cmd);
811
+ // Include discovered service rules in dry-run output
812
+ const injectedSummary = injectedRules.length > 0
813
+ ? `\n\nSafety rules that would be added first:\n ${injectedRules.join("\n ")}`
814
+ : "";
612
815
  const entry = createChangeEntry({
613
816
  tool: "firewall",
614
817
  action: `[DRY-RUN] Set ${chain} policy to ${policy}`,
@@ -620,7 +823,7 @@ export function registerFirewallTools(server) {
620
823
  logChange(entry);
621
824
  return {
622
825
  content: [
623
- createTextContent(`[DRY-RUN] Would execute:\n ${cmds.join("\n ")}`),
826
+ createTextContent(`[DRY-RUN] Would execute:\n ${cmds.join("\n ")}${injectedSummary}`),
624
827
  ],
625
828
  };
626
829
  }
@@ -794,6 +997,26 @@ export function registerFirewallTools(server) {
794
997
  // ── ufw_status ────────────────────────────────────────────────
795
998
  case "ufw_status": {
796
999
  try {
1000
+ // Check if nftables is managing firewall directly (not via UFW backend)
1001
+ const nftCheckStatus = await detectNftablesActive();
1002
+ if (nftCheckStatus.active) {
1003
+ // Check if UFW is even installed
1004
+ const ufwCheck = await executeCommand({
1005
+ command: "which",
1006
+ args: ["ufw"],
1007
+ toolName: "firewall",
1008
+ timeout: 5000,
1009
+ });
1010
+ if (ufwCheck.exitCode !== 0) {
1011
+ return {
1012
+ content: [
1013
+ createTextContent(`UFW is not installed. This system is using nftables directly (${nftCheckStatus.reason}). ` +
1014
+ `Use firewall action=nftables_list to view current rules. ` +
1015
+ `Installing UFW on a system with active nftables rules is not recommended as it may conflict.`),
1016
+ ],
1017
+ };
1018
+ }
1019
+ }
797
1020
  const args = ["ufw", "status"];
798
1021
  if (params.verbose) {
799
1022
  args.push("verbose");
@@ -823,6 +1046,27 @@ export function registerFirewallTools(server) {
823
1046
  case "ufw_add":
824
1047
  case "ufw_delete": {
825
1048
  try {
1049
+ // Guard: refuse UFW rule changes if nftables is active and UFW is not installed
1050
+ const nftCheckUfw = await detectNftablesActive();
1051
+ if (nftCheckUfw.active) {
1052
+ const ufwInstalled = await executeCommand({
1053
+ command: "which",
1054
+ args: ["ufw"],
1055
+ toolName: "firewall",
1056
+ timeout: 5000,
1057
+ });
1058
+ if (ufwInstalled.exitCode !== 0) {
1059
+ return {
1060
+ content: [
1061
+ createErrorContent(`Cannot ${action === "ufw_add" ? "add" : "delete"} UFW rule: UFW is not installed and ` +
1062
+ `this system is using nftables directly (${nftCheckUfw.reason}). ` +
1063
+ `Installing UFW on a system with active nftables rules can conflict and break networking. ` +
1064
+ `Manage firewall rules via nftables instead.`),
1065
+ ],
1066
+ isError: true,
1067
+ };
1068
+ }
1069
+ }
826
1070
  if (!params.rule_action) {
827
1071
  return { content: [createErrorContent("Error: 'rule_action' is required for add/delete actions (allow, deny, reject, limit)")], isError: true };
828
1072
  }
@@ -1118,6 +1362,20 @@ export function registerFirewallTools(server) {
1118
1362
  case "persist_enable": {
1119
1363
  const dry_run = params.dry_run;
1120
1364
  try {
1365
+ // Guard: refuse to install iptables-persistent if nftables is active
1366
+ const nftCheck = await detectNftablesActive();
1367
+ if (nftCheck.active) {
1368
+ return {
1369
+ content: [
1370
+ createErrorContent(`Cannot enable iptables persistence: ${nftCheck.reason}. ` +
1371
+ `Installing iptables-persistent on a system using nftables can conflict ` +
1372
+ `and break networking. Use nftables native persistence instead ` +
1373
+ `(nft rules are typically persisted via /etc/nftables.conf and the nftables systemd service). ` +
1374
+ `To view current nftables rules, use: firewall action=nftables_list`),
1375
+ ],
1376
+ isError: true,
1377
+ };
1378
+ }
1121
1379
  const da = await getDistroAdapter();
1122
1380
  const fwp = da.fwPersistence;
1123
1381
  const installDesc = `sudo ${fwp.installCmd.join(" ")}`;
@@ -1249,7 +1507,7 @@ export function registerFirewallTools(server) {
1249
1507
  : "Persistence is properly configured",
1250
1508
  };
1251
1509
  return {
1252
- content: [createTextContent(JSON.stringify(status, null, 2))],
1510
+ content: [createTextContent(JSON.stringify(status))],
1253
1511
  };
1254
1512
  }
1255
1513
  catch (err) {
@@ -1335,9 +1593,34 @@ export function registerFirewallTools(server) {
1335
1593
  const ruleCount = (output.match(/^[A-Z]+\s/gm) || []).length;
1336
1594
  findings.push({ check: "iptables_rule_count", status: ruleCount > 0 ? "INFO" : "WARN", value: String(ruleCount), description: "Total iptables rules" });
1337
1595
  }
1338
- // Check UFW statusdistinguish "not installed" from "command failed"
1596
+ // Check firewall backenddetect nftables vs UFW vs neither
1597
+ const nftAuditCheck = await detectNftablesActive();
1339
1598
  const ufwWhich = await executeCommand({ command: "which", args: ["ufw"], timeout: 5000, toolName: "firewall" });
1340
- if (ufwWhich.exitCode === 0) {
1599
+ if (nftAuditCheck.active) {
1600
+ // nftables is the active backend
1601
+ findings.push({
1602
+ check: "nftables_active",
1603
+ status: "PASS",
1604
+ value: `active (${nftAuditCheck.reason})`,
1605
+ description: "Firewall active via nftables",
1606
+ });
1607
+ if (ufwWhich.exitCode === 0) {
1608
+ // UFW is also installed — check if it's using nftables backend
1609
+ const ufwResult = await executeCommand({ command: "sudo", args: ["ufw", "status"], timeout: 10000, toolName: "firewall" });
1610
+ if (ufwResult.exitCode === 0 && ufwResult.stdout.includes("Status: active")) {
1611
+ findings.push({ check: "ufw_active", status: "INFO", value: "active (alongside nftables)", description: "UFW is active — likely using nftables backend" });
1612
+ }
1613
+ }
1614
+ else {
1615
+ findings.push({
1616
+ check: "ufw_not_needed",
1617
+ status: "INFO",
1618
+ value: "not installed",
1619
+ description: "UFW is not installed — not needed since nftables is managing the firewall. Do NOT install UFW as it may conflict.",
1620
+ });
1621
+ }
1622
+ }
1623
+ else if (ufwWhich.exitCode === 0) {
1341
1624
  // UFW binary exists — try to get status
1342
1625
  const ufwResult = await executeCommand({ command: "sudo", args: ["ufw", "status"], timeout: 10000, toolName: "firewall" });
1343
1626
  if (ufwResult.exitCode === 0) {
@@ -1357,14 +1640,8 @@ export function registerFirewallTools(server) {
1357
1640
  }
1358
1641
  }
1359
1642
  else {
1360
- // UFW binary not found — check if nftables has rules directly
1361
- const nftDirect = await executeCommand({ command: "sudo", args: ["nft", "list", "ruleset"], timeout: 10000, toolName: "firewall" });
1362
- if (nftDirect.exitCode === 0 && nftDirect.stdout.trim().length > 50) {
1363
- findings.push({ check: "nftables_active", status: "PASS", value: "active (nftables native)", description: "Firewall active via nftables (no UFW)" });
1364
- }
1365
- else {
1366
- findings.push({ check: "ufw_installed", status: "FAIL", value: "not installed", description: "No firewall detected (UFW not found, nftables empty)" });
1367
- }
1643
+ // Neither nftables nor UFW active
1644
+ findings.push({ check: "firewall_missing", status: "FAIL", value: "not installed", description: "No firewall detected (UFW not found, nftables not active)", recommendation: "Install and configure a firewall (nftables recommended for modern systems)" });
1368
1645
  }
1369
1646
  // Check ip6tables
1370
1647
  const ip6Result = await executeCommand({ command: "sudo", args: ["ip6tables", "-L", "-n"], timeout: 10000, toolName: "firewall" });
@@ -1383,23 +1660,40 @@ export function registerFirewallTools(server) {
1383
1660
  });
1384
1661
  }
1385
1662
  }
1386
- // Check for firewall persistence (distro-aware)
1663
+ // Check for firewall persistence (distro-aware, nftables-aware)
1387
1664
  const daPolicy = await getDistroAdapter();
1388
- const fwpPolicy = daPolicy.fwPersistence;
1389
- const persistResult = await executeCommand({ command: fwpPolicy.checkInstalledCmd[0], args: fwpPolicy.checkInstalledCmd.slice(1), timeout: 5000, toolName: "firewall" });
1390
- const persistInstalled = daPolicy.isDebian ? persistResult.stdout.includes("ii") : persistResult.exitCode === 0;
1391
- findings.push({
1392
- check: "firewall_persistence",
1393
- status: persistInstalled ? "PASS" : "WARN",
1394
- value: persistInstalled ? "installed" : "not installed",
1395
- description: `${fwpPolicy.packageName} (rules survive reboot)`,
1396
- recommendation: persistInstalled
1397
- ? undefined
1398
- : "Use firewall with action='persist_enable' to install and activate persistence, then action='persist_save' to persist current rules",
1399
- });
1665
+ if (nftAuditCheck.active) {
1666
+ // nftables persistence is via nftables.conf + systemd service
1667
+ const nftConfExists = await executeCommand({ command: "test", args: ["-f", "/etc/nftables.conf"], timeout: 3000, toolName: "firewall" });
1668
+ const nftSvcEnabled = await executeCommand({ command: "systemctl", args: ["is-enabled", "nftables"], timeout: 5000, toolName: "firewall" });
1669
+ const nftPersistent = nftConfExists.exitCode === 0 && nftSvcEnabled.stdout.trim() === "enabled";
1670
+ findings.push({
1671
+ check: "firewall_persistence",
1672
+ status: nftPersistent ? "PASS" : "WARN",
1673
+ value: nftPersistent ? "nftables persistence configured" : "nftables persistence not configured",
1674
+ description: "nftables rules persistence (/etc/nftables.conf + systemd service)",
1675
+ recommendation: nftPersistent
1676
+ ? undefined
1677
+ : "Ensure /etc/nftables.conf contains your rules and run: sudo systemctl enable nftables",
1678
+ });
1679
+ }
1680
+ else {
1681
+ const fwpPolicy = daPolicy.fwPersistence;
1682
+ const persistResult = await executeCommand({ command: fwpPolicy.checkInstalledCmd[0], args: fwpPolicy.checkInstalledCmd.slice(1), timeout: 5000, toolName: "firewall" });
1683
+ const persistInstalled = daPolicy.isDebian ? persistResult.stdout.includes("ii") : persistResult.exitCode === 0;
1684
+ findings.push({
1685
+ check: "firewall_persistence",
1686
+ status: persistInstalled ? "PASS" : "WARN",
1687
+ value: persistInstalled ? "installed" : "not installed",
1688
+ description: `${fwpPolicy.packageName} (rules survive reboot)`,
1689
+ recommendation: persistInstalled
1690
+ ? undefined
1691
+ : "Use firewall with action='persist_enable' to install and activate persistence, then action='persist_save' to persist current rules",
1692
+ });
1693
+ }
1400
1694
  const passCount = findings.filter(f => f.status === "PASS").length;
1401
1695
  const failCount = findings.filter(f => f.status === "FAIL").length;
1402
- return { content: [createTextContent(JSON.stringify({ summary: { total: findings.length, pass: passCount, fail: failCount, warn: findings.filter(f => f.status === "WARN").length }, findings }, null, 2))] };
1696
+ return { content: [createTextContent(JSON.stringify({ summary: { total: findings.length, pass: passCount, fail: failCount, warn: findings.filter(f => f.status === "WARN").length }, findings }))] };
1403
1697
  }
1404
1698
  catch (error) {
1405
1699
  return { content: [createErrorContent(error instanceof Error ? error.message : String(error))], isError: true };
@@ -13,8 +13,7 @@ import { validateServiceName, sanitizeArgs, validateSysctlKey, } from "../core/s
13
13
  import { SafeguardRegistry } from "../core/safeguards.js";
14
14
  import { SudoSession } from "../core/sudo-session.js";
15
15
  import { readFileSync, existsSync } from "node:fs";
16
- import { execSync } from "node:child_process";
17
- import { spawnSafe } from "../core/spawn-safe.js";
16
+ import { spawnSafe, execFileSafe } from "../core/spawn-safe.js";
18
17
  import { secureWriteFileSync } from "../core/secure-fs.js";
19
18
  // ── TOOL-019 remediation: privilege check helper ───────────────────────────
20
19
  /**
@@ -38,7 +37,7 @@ function checkPrivileges() {
38
37
  }
39
38
  // Fallback: Check if sudo is available without password (NOPASSWD)
40
39
  try {
41
- execSync("sudo -n true 2>/dev/null", { timeout: 3000, stdio: "ignore" });
40
+ execFileSafe("sudo", ["-n", "true"], { timeout: 3000, stdio: "ignore" });
42
41
  return { ok: true };
43
42
  }
44
43
  catch {
@@ -681,7 +680,7 @@ export function registerHardeningTools(server) {
681
680
  info: findings.length - passCount - failCount - warnCount,
682
681
  },
683
682
  findings,
684
- }, null, 2))],
683
+ }))],
685
684
  };
686
685
  }
687
686
  catch (error) {
@@ -717,7 +716,7 @@ export function registerHardeningTools(server) {
717
716
  results.push({ module: mod.name, description: mod.description, cis: mod.cis, blacklisted, loaded, status: blacklisted && !loaded ? "PASS" : loaded ? "FAIL" : "WARN" });
718
717
  }
719
718
  const passCount = results.filter(r => r.status === "PASS").length;
720
- return { content: [createTextContent(JSON.stringify({ summary: { total: results.length, pass: passCount, fail: results.filter(r => r.status === "FAIL").length, warn: results.filter(r => r.status === "WARN").length }, results }, null, 2))] };
719
+ return { content: [createTextContent(JSON.stringify({ summary: { total: results.length, pass: passCount, fail: results.filter(r => r.status === "FAIL").length, warn: results.filter(r => r.status === "WARN").length }, results }))] };
721
720
  }
722
721
  catch (error) {
723
722
  return { content: [createErrorContent(error instanceof Error ? error.message : String(error))], isError: true };
@@ -913,7 +912,7 @@ export function registerHardeningTools(server) {
913
912
  },
914
913
  kernelCommandLine: cmd,
915
914
  findings,
916
- }, null, 2))],
915
+ }))],
917
916
  };
918
917
  }
919
918
  catch (error) {
@@ -1899,7 +1898,7 @@ export function registerHardeningTools(server) {
1899
1898
  totalFindings: findings.length,
1900
1899
  findings: findings.slice(0, 50),
1901
1900
  rawExposureLine: exposureLine || "Not found",
1902
- }, null, 2))],
1901
+ }))],
1903
1902
  };
1904
1903
  }
1905
1904
  else {
@@ -1937,7 +1936,7 @@ export function registerHardeningTools(server) {
1937
1936
  },
1938
1937
  flaggedServices: flagged,
1939
1938
  allServices: services,
1940
- }, null, 2))],
1939
+ }))],
1941
1940
  };
1942
1941
  }
1943
1942
  }
@@ -2120,7 +2119,7 @@ export function registerHardeningTools(server) {
2120
2119
  }
2121
2120
  }
2122
2121
  const passCount = findings.filter(f => f.status === "PASS").length;
2123
- return { content: [createTextContent(JSON.stringify({ summary: { total: findings.length, pass: passCount, fail: findings.filter(f => f.status === "FAIL").length, warn: findings.filter(f => f.status === "WARN").length }, findings }, null, 2))] };
2122
+ return { content: [createTextContent(JSON.stringify({ summary: { total: findings.length, pass: passCount, fail: findings.filter(f => f.status === "FAIL").length, warn: findings.filter(f => f.status === "WARN").length }, findings }))] };
2124
2123
  }
2125
2124
  catch (error) {
2126
2125
  return { content: [createErrorContent(error instanceof Error ? error.message : String(error))], isError: true };
@@ -2148,7 +2147,7 @@ export function registerHardeningTools(server) {
2148
2147
  findings.push({ check: `umask_${file.path.replace(/\//g, "_")}`, status: hasSecure ? "PASS" : "WARN", value: lines.join("; ").substring(0, 200) || "not explicitly set", description: `umask in ${file.path} (should be 027 or 077)` });
2149
2148
  }
2150
2149
  }
2151
- return { content: [createTextContent(JSON.stringify({ summary: { total: findings.length, pass: findings.filter(f => f.status === "PASS").length, warn: findings.filter(f => f.status === "WARN").length }, findings, recommendation: "Set umask 027 in /etc/profile and /etc/login.defs for CIS compliance" }, null, 2))] };
2150
+ return { content: [createTextContent(JSON.stringify({ summary: { total: findings.length, pass: findings.filter(f => f.status === "PASS").length, warn: findings.filter(f => f.status === "WARN").length }, findings, recommendation: "Set umask 027 in /etc/profile and /etc/login.defs for CIS compliance" }))] };
2152
2151
  }
2153
2152
  catch (error) {
2154
2153
  return { content: [createErrorContent(error instanceof Error ? error.message : String(error))], isError: true };
@@ -2278,7 +2277,7 @@ export function registerHardeningTools(server) {
2278
2277
  findings.push({ check: `${banner.path.replace(/\//g, "_")}_perms`, status: perms.stdout.trim().startsWith("644") ? "PASS" : "WARN", value: perms.stdout.trim(), description: `${banner.description} permissions (should be 644 root:root)` });
2279
2278
  }
2280
2279
  }
2281
- return { content: [createTextContent(JSON.stringify({ summary: { total: findings.length, pass: findings.filter(f => f.status === "PASS").length, fail: findings.filter(f => f.status === "FAIL").length, warn: findings.filter(f => f.status === "WARN").length }, findings }, null, 2))] };
2280
+ return { content: [createTextContent(JSON.stringify({ summary: { total: findings.length, pass: findings.filter(f => f.status === "PASS").length, fail: findings.filter(f => f.status === "FAIL").length, warn: findings.filter(f => f.status === "WARN").length }, findings }))] };
2282
2281
  }
2283
2282
  catch (error) {
2284
2283
  return { content: [createErrorContent(error instanceof Error ? error.message : String(error))], isError: true };
@@ -2433,7 +2432,7 @@ export function registerHardeningTools(server) {
2433
2432
  }
2434
2433
  }
2435
2434
  if (findings.lsusbNote) {
2436
- text += `\n⚠ ${findings.lsusbNote}\n`;
2435
+ text += `\nWARNING: ${findings.lsusbNote}\n`;
2437
2436
  }
2438
2437
  text += `\nUSB Storage Devices: ${findings.storageDeviceCount ?? 0}\n`;
2439
2438
  if (Array.isArray(findings.storageDevices) && findings.storageDevices.length > 0) {
@@ -2507,7 +2506,7 @@ export function registerHardeningTools(server) {
2507
2506
  for (const r of results) {
2508
2507
  text += ` • ${r.target}: ${r.action} [${r.status}]\n`;
2509
2508
  }
2510
- text += `\nUSB Storage Module Still Loaded: ${moduleStillLoaded ? "yes " : "no "}\n`;
2509
+ text += `\nUSB Storage Module Still Loaded: ${moduleStillLoaded ? "yes WARNING" : "no OK"}\n`;
2511
2510
  return { content: [createTextContent(text)] };
2512
2511
  }
2513
2512
  else {
@@ -204,11 +204,11 @@ export function registerIncidentResponseTools(server) {
204
204
  timeout: 5000,
205
205
  });
206
206
  const size = sizeResult.stdout.trim();
207
- lines.push(` ${step.name}: ${step.desc} (${size} bytes)`);
207
+ lines.push(` OK ${step.name}: ${step.desc} (${size} bytes)`);
208
208
  successCount++;
209
209
  }
210
210
  else {
211
- lines.push(` ${step.name}: ${step.desc} [FAILED: ${result.stderr.trim()}]`);
211
+ lines.push(` FAIL ${step.name}: ${step.desc} [FAILED: ${result.stderr.trim()}]`);
212
212
  failCount++;
213
213
  }
214
214
  }
@@ -1124,7 +1124,7 @@ export function registerIncidentResponseTools(server) {
1124
1124
  `Evidence Path: ${evidence_path}`,
1125
1125
  `Current SHA-256: ${currentHash}`,
1126
1126
  `Recorded SHA-256: ${recordedHash ?? "NOT FOUND"}`,
1127
- `Integrity: ${hashMatch ? " VERIFIED — hashes match" : " MISMATCH — evidence may be tampered"}`,
1127
+ `Integrity: ${hashMatch ? "OK VERIFIED — hashes match" : "FAIL MISMATCH — evidence may be tampered"}`,
1128
1128
  ];
1129
1129
  return { content: [createTextContent(lines.join("\n"))] };
1130
1130
  }