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.
- package/build/core/auto-installer.js +31 -31
- package/build/core/command-allowlist.js +1 -1
- package/build/core/dependency-validator.js +9 -9
- package/build/core/distro-adapter.js +0 -7
- package/build/core/distro.js +0 -48
- package/build/core/encrypted-state.js +0 -7
- package/build/core/logger.js +1 -1
- package/build/core/pam-utils.js +1 -1
- package/build/core/parsers.js +1 -1
- package/build/core/preflight.js +13 -13
- package/build/core/progress.js +20 -20
- package/build/core/run-command.js +46 -0
- package/build/core/sudo-guard.js +4 -4
- package/build/core/third-party-installer.js +4 -4
- package/build/core/tool-wrapper.js +3 -3
- package/build/tools/access-control.js +6 -6
- package/build/tools/api-security.js +5 -51
- package/build/tools/app-hardening.js +23 -25
- package/build/tools/cloud-security.js +5 -51
- package/build/tools/compliance.js +9 -13
- package/build/tools/container-security.js +51 -52
- package/build/tools/deception.js +8 -54
- package/build/tools/dns-security.js +2 -48
- package/build/tools/encryption.js +86 -87
- package/build/tools/firewall.js +324 -30
- package/build/tools/hardening.js +12 -13
- package/build/tools/incident-response.js +3 -3
- package/build/tools/logging.js +17 -59
- package/build/tools/malware.js +2 -2
- package/build/tools/meta.js +86 -165
- package/build/tools/network-defense.js +3 -3
- package/build/tools/patch-management.js +8 -8
- package/build/tools/process-security.js +38 -92
- package/build/tools/sudo-management.js +36 -36
- package/build/tools/threat-intel.js +2 -48
- package/build/tools/vulnerability-management.js +3 -49
- package/build/tools/waf.js +47 -93
- package/build/tools/wireless-security.js +9 -55
- package/package.json +5 -3
- package/build/core/auto-installer.d.ts +0 -102
- package/build/core/auto-installer.d.ts.map +0 -1
- package/build/core/backup-manager.d.ts +0 -63
- package/build/core/backup-manager.d.ts.map +0 -1
- package/build/core/changelog.d.ts +0 -119
- package/build/core/changelog.d.ts.map +0 -1
- package/build/core/command-allowlist.d.ts +0 -129
- package/build/core/command-allowlist.d.ts.map +0 -1
- package/build/core/config.d.ts +0 -107
- package/build/core/config.d.ts.map +0 -1
- package/build/core/dependency-validator.d.ts +0 -106
- package/build/core/dependency-validator.d.ts.map +0 -1
- package/build/core/distro-adapter.d.ts +0 -177
- package/build/core/distro-adapter.d.ts.map +0 -1
- package/build/core/distro.d.ts +0 -68
- package/build/core/distro.d.ts.map +0 -1
- package/build/core/encrypted-state.d.ts +0 -76
- package/build/core/encrypted-state.d.ts.map +0 -1
- package/build/core/executor.d.ts +0 -65
- package/build/core/executor.d.ts.map +0 -1
- package/build/core/installer.d.ts +0 -129
- package/build/core/installer.d.ts.map +0 -1
- package/build/core/logger.d.ts +0 -118
- package/build/core/logger.d.ts.map +0 -1
- package/build/core/metrics.d.ts +0 -74
- package/build/core/metrics.d.ts.map +0 -1
- package/build/core/metrics.js +0 -97
- package/build/core/output-redactor.d.ts +0 -26
- package/build/core/output-redactor.d.ts.map +0 -1
- package/build/core/pam-utils.d.ts +0 -356
- package/build/core/pam-utils.d.ts.map +0 -1
- package/build/core/parsers.d.ts +0 -191
- package/build/core/parsers.d.ts.map +0 -1
- package/build/core/policy-engine.d.ts +0 -170
- package/build/core/policy-engine.d.ts.map +0 -1
- package/build/core/preflight.d.ts +0 -157
- package/build/core/preflight.d.ts.map +0 -1
- package/build/core/privilege-manager.d.ts +0 -108
- package/build/core/privilege-manager.d.ts.map +0 -1
- package/build/core/progress.d.ts +0 -99
- package/build/core/progress.d.ts.map +0 -1
- package/build/core/rate-limiter.d.ts +0 -101
- package/build/core/rate-limiter.d.ts.map +0 -1
- package/build/core/rollback.d.ts +0 -73
- package/build/core/rollback.d.ts.map +0 -1
- package/build/core/safeguards.d.ts +0 -58
- package/build/core/safeguards.d.ts.map +0 -1
- package/build/core/sanitizer.d.ts +0 -118
- package/build/core/sanitizer.d.ts.map +0 -1
- package/build/core/secure-fs.d.ts +0 -67
- package/build/core/secure-fs.d.ts.map +0 -1
- package/build/core/spawn-safe.d.ts +0 -55
- package/build/core/spawn-safe.d.ts.map +0 -1
- package/build/core/sudo-guard.d.ts +0 -167
- package/build/core/sudo-guard.d.ts.map +0 -1
- package/build/core/sudo-session.d.ts +0 -143
- package/build/core/sudo-session.d.ts.map +0 -1
- package/build/core/third-party-installer.d.ts +0 -58
- package/build/core/third-party-installer.d.ts.map +0 -1
- package/build/core/third-party-manifest.d.ts +0 -48
- package/build/core/third-party-manifest.d.ts.map +0 -1
- package/build/core/tool-annotations.d.ts +0 -13
- package/build/core/tool-annotations.d.ts.map +0 -1
- package/build/core/tool-dependencies.d.ts +0 -60
- package/build/core/tool-dependencies.d.ts.map +0 -1
- package/build/core/tool-durations.d.ts +0 -71
- package/build/core/tool-durations.d.ts.map +0 -1
- package/build/core/tool-registry.d.ts +0 -112
- package/build/core/tool-registry.d.ts.map +0 -1
- package/build/core/tool-wrapper.d.ts +0 -73
- package/build/core/tool-wrapper.d.ts.map +0 -1
- package/build/index.d.ts +0 -3
- package/build/index.d.ts.map +0 -1
- package/build/tools/access-control.d.ts +0 -11
- package/build/tools/access-control.d.ts.map +0 -1
- package/build/tools/api-security.d.ts +0 -12
- package/build/tools/api-security.d.ts.map +0 -1
- package/build/tools/app-hardening.d.ts +0 -11
- package/build/tools/app-hardening.d.ts.map +0 -1
- package/build/tools/backup.d.ts +0 -8
- package/build/tools/backup.d.ts.map +0 -1
- package/build/tools/cloud-security.d.ts +0 -17
- package/build/tools/cloud-security.d.ts.map +0 -1
- package/build/tools/compliance.d.ts +0 -11
- package/build/tools/compliance.d.ts.map +0 -1
- package/build/tools/container-security.d.ts +0 -14
- package/build/tools/container-security.d.ts.map +0 -1
- package/build/tools/deception.d.ts +0 -13
- package/build/tools/deception.d.ts.map +0 -1
- package/build/tools/dns-security.d.ts +0 -93
- package/build/tools/dns-security.d.ts.map +0 -1
- package/build/tools/ebpf-security.d.ts +0 -15
- package/build/tools/ebpf-security.d.ts.map +0 -1
- package/build/tools/encryption.d.ts +0 -12
- package/build/tools/encryption.d.ts.map +0 -1
- package/build/tools/firewall.d.ts +0 -9
- package/build/tools/firewall.d.ts.map +0 -1
- package/build/tools/hardening.d.ts +0 -8
- package/build/tools/hardening.d.ts.map +0 -1
- package/build/tools/incident-response.d.ts +0 -11
- package/build/tools/incident-response.d.ts.map +0 -1
- package/build/tools/integrity.d.ts +0 -15
- package/build/tools/integrity.d.ts.map +0 -1
- package/build/tools/logging.d.ts +0 -21
- package/build/tools/logging.d.ts.map +0 -1
- package/build/tools/malware.d.ts +0 -10
- package/build/tools/malware.d.ts.map +0 -1
- package/build/tools/meta.d.ts +0 -13
- package/build/tools/meta.d.ts.map +0 -1
- package/build/tools/network-defense.d.ts +0 -11
- package/build/tools/network-defense.d.ts.map +0 -1
- package/build/tools/patch-management.d.ts +0 -3
- package/build/tools/patch-management.d.ts.map +0 -1
- package/build/tools/process-security.d.ts +0 -12
- package/build/tools/process-security.d.ts.map +0 -1
- package/build/tools/secrets.d.ts +0 -8
- package/build/tools/secrets.d.ts.map +0 -1
- package/build/tools/sudo-management.d.ts +0 -17
- package/build/tools/sudo-management.d.ts.map +0 -1
- package/build/tools/supply-chain-security.d.ts +0 -8
- package/build/tools/supply-chain-security.d.ts.map +0 -1
- package/build/tools/threat-intel.d.ts +0 -22
- package/build/tools/threat-intel.d.ts.map +0 -1
- package/build/tools/vulnerability-management.d.ts +0 -11
- package/build/tools/vulnerability-management.d.ts.map +0 -1
- package/build/tools/waf.d.ts +0 -12
- package/build/tools/waf.d.ts.map +0 -1
- package/build/tools/wireless-security.d.ts +0 -19
- package/build/tools/wireless-security.d.ts.map +0 -1
- package/build/tools/zero-trust-network.d.ts +0 -8
- package/build/tools/zero-trust-network.d.ts.map +0 -1
package/build/tools/firewall.js
CHANGED
|
@@ -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(
|
|
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(
|
|
704
|
+
injectedRules.push(`IPv6: Failed to add "${rule.description}": ${add6Result.stderr}`);
|
|
588
705
|
}
|
|
589
706
|
else {
|
|
590
|
-
injectedRules.push(
|
|
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
|
|
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
|
|
1596
|
+
// Check firewall backend — detect 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 (
|
|
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
|
-
//
|
|
1361
|
-
|
|
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
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
:
|
|
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 }
|
|
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 };
|
package/build/tools/hardening.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
}
|
|
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 }
|
|
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
|
-
}
|
|
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
|
-
}
|
|
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
|
-
}
|
|
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 }
|
|
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" }
|
|
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 }
|
|
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 += `\
|
|
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
|
|
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(`
|
|
207
|
+
lines.push(` OK ${step.name}: ${step.desc} (${size} bytes)`);
|
|
208
208
|
successCount++;
|
|
209
209
|
}
|
|
210
210
|
else {
|
|
211
|
-
lines.push(`
|
|
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 ? "
|
|
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
|
}
|