1use anyhow::Result;
9use clap::Subcommand;
10use std::process::Command;
11
12#[derive(Subcommand)]
13pub enum SecureCommands {
14 Scan,
16 Harden {
18 #[arg(long)]
20 dry_run: bool,
21 },
22 Firewall {
24 #[arg(long)]
26 dry_run: bool,
27 },
28 Audit,
30 Updates,
32}
33
34fn run_check(_name: &str, cmd_name: &str, args: &[&str]) -> (bool, String) {
35 match Command::new(cmd_name).args(args).output() {
36 Ok(o) if o.status.success() => {
37 let out = String::from_utf8_lossy(&o.stdout).trim().to_string();
38 (true, out)
39 }
40 _ => (false, "N/A".into()),
41 }
42}
43
44pub async fn run(cmd: SecureCommands) -> Result<()> {
45 match cmd {
46 SecureCommands::Scan => {
47 println!("Zernel Security Scan");
48 println!("{}", "=".repeat(60));
49 println!();
50
51 let mut issues = 0;
52
53 println!("[1/8] SSH Configuration");
55 #[cfg(target_os = "linux")]
56 {
57 if let Ok(config) = std::fs::read_to_string("/etc/ssh/sshd_config") {
58 let root_login = config
59 .lines()
60 .any(|l| l.trim().starts_with("PermitRootLogin") && l.contains("yes"));
61 let password_auth = config.lines().any(|l| {
62 l.trim().starts_with("PasswordAuthentication") && l.contains("yes")
63 });
64
65 if root_login {
66 println!(" WARN: Root login enabled — disable with PermitRootLogin no");
67 issues += 1;
68 } else {
69 println!(" OK: Root login disabled");
70 }
71
72 if password_auth {
73 println!(" WARN: Password auth enabled — use key-based auth only");
74 issues += 1;
75 } else {
76 println!(" OK: Key-based auth only");
77 }
78 } else {
79 println!(" SKIP: sshd_config not found");
80 }
81 }
82 #[cfg(not(target_os = "linux"))]
83 println!(" SKIP: Linux only");
84
85 println!();
87 println!("[2/8] Firewall");
88 let (fw_ok, _) = run_check("iptables", "iptables", &["-L", "-n"]);
89 if fw_ok {
90 println!(" OK: Firewall active");
91 } else {
92 println!(" WARN: No firewall rules — configure with: zernel secure firewall");
93 issues += 1;
94 }
95
96 println!();
98 println!("[3/8] Security Updates");
99 #[cfg(target_os = "linux")]
100 {
101 let auto_updates =
102 std::path::Path::new("/etc/apt/apt.conf.d/20auto-upgrades").exists();
103 if auto_updates {
104 println!(" OK: Unattended security updates enabled");
105 } else {
106 println!(" WARN: Auto security updates not configured");
107 println!(" Fix: apt install unattended-upgrades && dpkg-reconfigure unattended-upgrades");
108 issues += 1;
109 }
110 }
111 #[cfg(not(target_os = "linux"))]
112 println!(" SKIP: Linux only");
113
114 println!();
116 println!("[4/8] Swap (should be disabled for ML)");
117 #[cfg(target_os = "linux")]
118 {
119 if let Ok(swaps) = std::fs::read_to_string("/proc/swaps") {
120 let swap_active = swaps.lines().count() > 1;
121 if swap_active {
122 println!(" WARN: Swap is active — disable with: swapoff -a");
123 issues += 1;
124 } else {
125 println!(" OK: Swap disabled");
126 }
127 }
128 }
129 #[cfg(not(target_os = "linux"))]
130 println!(" SKIP: Linux only");
131
132 println!();
134 println!("[5/8] Unnecessary Services");
135 let unnecessary = ["cups", "avahi-daemon", "bluetooth", "ModemManager"];
136 for svc in &unnecessary {
137 let (running, _) = run_check(svc, "systemctl", &["is-active", svc]);
138 if running {
139 println!(" WARN: {svc} is running — not needed for ML servers");
140 issues += 1;
141 }
142 }
143 if issues == 0 {
144 println!(" OK: No unnecessary services detected");
145 }
146
147 println!();
149 println!("[6/8] GPU Persistence Mode");
150 let (_, gpu_pm) = run_check(
151 "nvidia-smi",
152 "nvidia-smi",
153 &["--query-gpu=persistence_mode", "--format=csv,noheader"],
154 );
155 if gpu_pm.contains("Enabled") {
156 println!(" OK: GPU persistence mode enabled");
157 } else {
158 println!(" WARN: GPU persistence mode disabled — enable with: nvidia-smi -pm 1");
159 issues += 1;
160 }
161
162 println!();
164 println!("[7/8] Post-Quantum Cryptography");
165 let pqc_dir = crate::experiments::tracker::zernel_dir().join("pqc");
166 let has_keys = pqc_dir.exists()
167 && std::fs::read_dir(&pqc_dir)
168 .map(|entries| entries.flatten().count() > 0)
169 .unwrap_or(false);
170 if has_keys {
171 println!(" OK: PQC keys configured");
172 } else {
173 println!(" INFO: No PQC keys — generate with: zernel pqc keygen");
174 }
175
176 println!();
178 println!("[8/8] Kernel Tuning");
179 #[cfg(target_os = "linux")]
180 {
181 let zernel_conf = std::path::Path::new("/etc/sysctl.d/99-zernel.conf");
182 if zernel_conf.exists() {
183 println!(" OK: Zernel sysctl tuning applied");
184 } else {
185 println!(" WARN: Zernel sysctl tuning not applied");
186 println!(" Fix: zernel tune apply");
187 issues += 1;
188 }
189 }
190 #[cfg(not(target_os = "linux"))]
191 println!(" SKIP: Linux only");
192
193 println!();
195 println!("{}", "=".repeat(60));
196 if issues == 0 {
197 println!("Security scan: PASS (0 issues)");
198 } else {
199 println!("Security scan: {issues} issue(s) found");
200 println!("Harden: zernel secure harden");
201 }
202 }
203
204 SecureCommands::Harden { dry_run } => {
205 println!(
206 "Zernel System Hardening{}",
207 if dry_run { " (dry run)" } else { "" }
208 );
209 println!("{}", "=".repeat(50));
210
211 let actions: Vec<(&str, Vec<&str>)> = vec![
212 ("Disable swap", vec!["swapoff", "-a"]),
213 ("Enable GPU persistence", vec!["nvidia-smi", "-pm", "1"]),
214 (
215 "Disable unnecessary services",
216 vec![
217 "systemctl",
218 "disable",
219 "--now",
220 "cups",
221 "avahi-daemon",
222 "bluetooth",
223 ],
224 ),
225 (
226 "Install unattended-upgrades",
227 vec!["apt-get", "install", "-y", "unattended-upgrades"],
228 ),
229 ];
230
231 for (name, args) in &actions {
232 if dry_run {
233 println!(" WOULD: {name} ({} {})", args[0], args[1..].join(" "));
234 } else {
235 print!(" {name}... ");
236 let status = Command::new(args[0]).args(&args[1..]).output();
237 match status {
238 Ok(o) if o.status.success() => println!("OK"),
239 _ => println!("SKIP (may need root)"),
240 }
241 }
242 }
243
244 if dry_run {
246 println!(" WOULD: Apply kernel tuning (zernel tune apply)");
247 } else {
248 println!(" Applying kernel tuning...");
249 let _ = Command::new("zernel").args(["tune", "apply"]).status();
250 }
251
252 println!();
253 if !dry_run {
254 println!("Hardening complete. Run: zernel secure scan — to verify");
255 }
256 }
257
258 SecureCommands::Firewall { dry_run } => {
259 println!(
260 "Zernel ML Firewall Configuration{}",
261 if dry_run { " (dry run)" } else { "" }
262 );
263 println!("{}", "=".repeat(50));
264 println!();
265 println!("Allows:");
266 println!(" - SSH (port 22)");
267 println!(" - NCCL (ports 29500-30000)");
268 println!(" - Zernel services (9091, 9092, 3000)");
269 println!(" - Prometheus (9090)");
270 println!(" - Ollama (11434)");
271 println!("Blocks: everything else inbound");
272 println!();
273
274 let rules = [
275 vec![
276 "-A",
277 "INPUT",
278 "-m",
279 "state",
280 "--state",
281 "ESTABLISHED,RELATED",
282 "-j",
283 "ACCEPT",
284 ],
285 vec!["-A", "INPUT", "-i", "lo", "-j", "ACCEPT"],
286 vec!["-A", "INPUT", "-p", "tcp", "--dport", "22", "-j", "ACCEPT"],
287 vec![
288 "-A",
289 "INPUT",
290 "-p",
291 "tcp",
292 "--dport",
293 "29500:30000",
294 "-j",
295 "ACCEPT",
296 ],
297 vec![
298 "-A", "INPUT", "-p", "tcp", "--dport", "9091", "-j", "ACCEPT",
299 ],
300 vec![
301 "-A", "INPUT", "-p", "tcp", "--dport", "9092", "-j", "ACCEPT",
302 ],
303 vec![
304 "-A", "INPUT", "-p", "tcp", "--dport", "3000", "-j", "ACCEPT",
305 ],
306 vec![
307 "-A", "INPUT", "-p", "tcp", "--dport", "9090", "-j", "ACCEPT",
308 ],
309 vec![
310 "-A", "INPUT", "-p", "tcp", "--dport", "11434", "-j", "ACCEPT",
311 ],
312 vec!["-A", "INPUT", "-p", "icmp", "-j", "ACCEPT"],
313 vec!["-P", "INPUT", "DROP"],
314 ];
315
316 for rule in &rules {
317 if dry_run {
318 println!(" iptables {}", rule.join(" "));
319 } else {
320 let _ = Command::new("iptables").args(rule).output();
321 println!(" Applied: iptables {}", rule.join(" "));
322 }
323 }
324
325 if !dry_run {
326 println!();
327 println!("Firewall configured. Save with: iptables-save > /etc/iptables.rules");
328 }
329 }
330
331 SecureCommands::Audit => {
332 println!("Enabling Audit Logging");
333 println!("{}", "=".repeat(50));
334
335 let status = Command::new("apt-get")
336 .args(["install", "-y", "auditd"])
337 .output();
338
339 match status {
340 Ok(o) if o.status.success() => {
341 println!(" auditd installed");
342
343 let rules = [
345 "-w /etc/zernel/ -p wa -k zernel_config",
346 "-w /usr/local/bin/zernel -p x -k zernel_exec",
347 "-w /usr/local/bin/zerneld -p x -k zerneld_exec",
348 ];
349
350 for rule in &rules {
351 let _ = Command::new("auditctl")
352 .args(rule.split_whitespace())
353 .output();
354 println!(" Rule: {rule}");
355 }
356
357 println!();
358 println!("Audit logging enabled. View: ausearch -k zernel_config");
359 }
360 _ => {
361 println!(" Failed to install auditd (requires root + apt)");
362 }
363 }
364 }
365
366 SecureCommands::Updates => {
367 println!("Security Updates Check");
368 println!("{}", "=".repeat(50));
369
370 let output = Command::new("apt-get")
371 .args(["--just-print", "upgrade"])
372 .output();
373
374 match output {
375 Ok(o) if o.status.success() => {
376 let stdout = String::from_utf8_lossy(&o.stdout);
377 let upgradable: Vec<&str> =
378 stdout.lines().filter(|l| l.starts_with("Inst")).collect();
379
380 if upgradable.is_empty() {
381 println!(" All packages up to date.");
382 } else {
383 println!(" {} packages need updating:", upgradable.len());
384 for pkg in upgradable.iter().take(20) {
385 println!(" {pkg}");
386 }
387 if upgradable.len() > 20 {
388 println!(" ... and {} more", upgradable.len() - 20);
389 }
390 println!();
391 println!(" Apply: sudo apt-get upgrade -y");
392 }
393 }
394 _ => {
395 println!(" Cannot check updates (requires apt)");
396 }
397 }
398 }
399 }
400 Ok(())
401}