security.rs
1 //! Security command handlers. 2 3 use acdc_core::Result; 4 use acdc_security::{ 5 audit::FindingSeverity, 6 firewall::{self, FirewallManager, Protocol}, 7 hardening, 8 keys::{KeyRotation, KeyType}, 9 tls::CertificateManager, 10 }; 11 use console::style; 12 13 fn json_output<T: serde::Serialize>(value: &T) -> String { 14 serde_json::to_string_pretty(value).unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e)) 15 } 16 17 /// Run security audit. 18 pub async fn audit(json: bool) -> Result<()> { 19 let audit = acdc_security::run_security_audit().await?; 20 21 if json { 22 println!("{}", json_output(&audit)); 23 return Ok(()); 24 } 25 26 println!("{}", style("Security Audit Report").bold().cyan()); 27 println!("{}", style("─".repeat(50)).dim()); 28 println!( 29 "Timestamp: {}", 30 audit.timestamp.format("%Y-%m-%d %H:%M:%S UTC") 31 ); 32 println!("Risk Score: {}/100", risk_score_colored(audit.risk_score)); 33 println!(); 34 35 // Summary by severity 36 println!("{}", style("Findings by Severity:").bold()); 37 for severity in [ 38 FindingSeverity::Critical, 39 FindingSeverity::High, 40 FindingSeverity::Medium, 41 FindingSeverity::Low, 42 FindingSeverity::Info, 43 ] { 44 let count = audit 45 .severity_counts 46 .get(&severity.to_string()) 47 .unwrap_or(&0); 48 if *count > 0 { 49 let styled = match severity { 50 FindingSeverity::Critical => { 51 style(format!(" {}: {}", severity, count)).red().bold() 52 } 53 FindingSeverity::High => style(format!(" {}: {}", severity, count)).red(), 54 FindingSeverity::Medium => style(format!(" {}: {}", severity, count)).yellow(), 55 FindingSeverity::Low => style(format!(" {}: {}", severity, count)).blue(), 56 FindingSeverity::Info => style(format!(" {}: {}", severity, count)).dim(), 57 }; 58 println!("{}", styled); 59 } 60 } 61 println!(); 62 63 // List findings 64 if !audit.findings.is_empty() { 65 println!("{}", style("Findings:").bold()); 66 for finding in &audit.findings { 67 let severity_style = match finding.severity { 68 FindingSeverity::Critical => style(format!("[{}]", finding.severity)).red().bold(), 69 FindingSeverity::High => style(format!("[{}]", finding.severity)).red(), 70 FindingSeverity::Medium => style(format!("[{}]", finding.severity)).yellow(), 71 FindingSeverity::Low => style(format!("[{}]", finding.severity)).blue(), 72 FindingSeverity::Info => style(format!("[{}]", finding.severity)).dim(), 73 }; 74 println!( 75 "\n{} {} {}", 76 finding.id, 77 severity_style, 78 style(&finding.title).bold() 79 ); 80 println!(" {}", finding.description); 81 println!(" {}: {}", style("Fix").green(), finding.remediation); 82 if finding.auto_fixable { 83 println!(" {}", style("(auto-fixable)").dim()); 84 } 85 } 86 } else { 87 println!("{}", style("No security issues found!").green().bold()); 88 } 89 90 Ok(()) 91 } 92 93 /// Quick security check. 94 pub async fn check(json: bool) -> Result<()> { 95 let findings = acdc_security::quick_security_check().await?; 96 97 if json { 98 println!("{}", json_output(&findings)); 99 return Ok(()); 100 } 101 102 if findings.is_empty() { 103 println!("{}", style("Quick security check passed!").green().bold()); 104 } else { 105 println!( 106 "{}", 107 style(format!("Found {} issues:", findings.len())) 108 .yellow() 109 .bold() 110 ); 111 for finding in &findings { 112 println!(" {} [{}] {}", finding.id, finding.severity, finding.title); 113 } 114 } 115 116 Ok(()) 117 } 118 119 /// Show firewall status. 120 pub async fn firewall_status(json: bool) -> Result<()> { 121 let status = firewall::get_status().await?; 122 123 if json { 124 println!("{}", json_output(&status)); 125 return Ok(()); 126 } 127 128 println!("{}", style("Firewall Status").bold().cyan()); 129 println!("{}", style("─".repeat(40)).dim()); 130 println!( 131 "Installed: {}", 132 if status.installed { 133 style("Yes").green() 134 } else { 135 style("No").red() 136 } 137 ); 138 println!( 139 "Active: {}", 140 if status.active { 141 style("Yes").green() 142 } else { 143 style("No").red() 144 } 145 ); 146 println!("Default Incoming: {}", status.default_incoming); 147 println!("Default Outgoing: {}", status.default_outgoing); 148 println!(); 149 150 if !status.rules.is_empty() { 151 println!("{}", style("Rules:").bold()); 152 for rule in &status.rules { 153 println!( 154 " {} {} {} {}", 155 style(format!("[{}]", rule.number)).dim(), 156 rule.port, 157 style(format!("{:?}", rule.action)).green(), 158 rule.from.as_deref().unwrap_or("Anywhere") 159 ); 160 } 161 } 162 163 Ok(()) 164 } 165 166 /// Enable firewall. 167 pub async fn firewall_enable() -> Result<()> { 168 FirewallManager::enable().await?; 169 println!("{}", style("Firewall enabled").green().bold()); 170 Ok(()) 171 } 172 173 /// Disable firewall. 174 pub async fn firewall_disable() -> Result<()> { 175 FirewallManager::disable().await?; 176 println!("{}", style("Firewall disabled").yellow().bold()); 177 Ok(()) 178 } 179 180 /// Add firewall rule. 181 pub async fn firewall_allow(port: u16, protocol: &str, from: Option<String>) -> Result<()> { 182 let proto = match protocol.to_lowercase().as_str() { 183 "tcp" => Protocol::Tcp, 184 "udp" => Protocol::Udp, 185 _ => Protocol::Any, 186 }; 187 188 if let Some(src) = from { 189 FirewallManager::add_rule_from(port, proto, &src, None).await?; 190 println!( 191 "{}", 192 style(format!("Allowed {}/{} from {}", port, protocol, src)).green() 193 ); 194 } else { 195 FirewallManager::add_rule(port, proto, None).await?; 196 println!( 197 "{}", 198 style(format!("Allowed {}/{}", port, protocol)).green() 199 ); 200 } 201 202 Ok(()) 203 } 204 205 /// Delete firewall rule. 206 pub async fn firewall_delete(port: u16, protocol: &str) -> Result<()> { 207 let proto = match protocol.to_lowercase().as_str() { 208 "tcp" => Protocol::Tcp, 209 "udp" => Protocol::Udp, 210 _ => Protocol::Any, 211 }; 212 213 FirewallManager::delete_rule_by_port(port, proto).await?; 214 println!( 215 "{}", 216 style(format!("Deleted rule for {}/{}", port, protocol)).yellow() 217 ); 218 219 Ok(()) 220 } 221 222 /// Configure firewall for node operation. 223 pub async fn firewall_configure( 224 rest_alpha: u16, 225 rest_delta: u16, 226 p2p_alpha: u16, 227 p2p_delta: u16, 228 metrics: u16, 229 ) -> Result<()> { 230 println!( 231 "{}", 232 style("Configuring firewall for node operation...").cyan() 233 ); 234 235 FirewallManager::configure_for_node(rest_alpha, rest_delta, p2p_alpha, p2p_delta, metrics) 236 .await?; 237 238 println!( 239 "{}", 240 style("Firewall configured for node operation") 241 .green() 242 .bold() 243 ); 244 println!(" SSH: 22/tcp"); 245 println!(" ALPHA REST: {}/tcp", rest_alpha); 246 println!(" DELTA REST: {}/tcp", rest_delta); 247 println!(" ALPHA P2P: {}/tcp", p2p_alpha); 248 println!(" DELTA P2P: {}/tcp", p2p_delta); 249 println!(" Metrics: {}/tcp", metrics); 250 251 Ok(()) 252 } 253 254 /// List certificates. 255 pub async fn cert_list(json: bool) -> Result<()> { 256 let mgr = CertificateManager::new(CertificateManager::default_cert_dir()); 257 let certs = mgr.list_certificates()?; 258 259 if json { 260 println!("{}", json_output(&certs)); 261 return Ok(()); 262 } 263 264 if certs.is_empty() { 265 println!("{}", style("No certificates found").dim()); 266 return Ok(()); 267 } 268 269 println!("{}", style("Certificates").bold().cyan()); 270 println!("{}", style("─".repeat(60)).dim()); 271 272 for cert in &certs { 273 let expiry_style = if cert.is_expired { 274 style(format!("EXPIRED {} days ago", -cert.days_until_expiry)) 275 .red() 276 .bold() 277 } else if cert.days_until_expiry <= 7 { 278 style(format!("Expires in {} days", cert.days_until_expiry)).red() 279 } else if cert.days_until_expiry <= 30 { 280 style(format!("Expires in {} days", cert.days_until_expiry)).yellow() 281 } else { 282 style(format!("Expires in {} days", cert.days_until_expiry)).green() 283 }; 284 285 println!("\n{}", style(&cert.common_name).bold()); 286 println!(" Issuer: {}", cert.issuer); 287 println!(" SANs: {}", cert.san.join(", ")); 288 println!( 289 " Valid: {} to {}", 290 cert.not_before.format("%Y-%m-%d"), 291 cert.not_after.format("%Y-%m-%d") 292 ); 293 println!(" Status: {}", expiry_style); 294 if cert.is_self_signed { 295 println!(" {}", style("(self-signed)").dim()); 296 } 297 } 298 299 Ok(()) 300 } 301 302 /// Generate certificate. 303 pub async fn cert_generate(common_name: String, san: Vec<String>, days: u32) -> Result<()> { 304 let mgr = CertificateManager::new(CertificateManager::default_cert_dir()); 305 let info = mgr.generate_self_signed(&common_name, &san, days)?; 306 307 println!("{}", style("Certificate generated").green().bold()); 308 println!(" Common Name: {}", info.common_name); 309 println!(" SANs: {}", info.san.join(", ")); 310 println!(" Valid for: {} days", days); 311 println!(" Certificate: {:?}", info.cert_path); 312 println!(" Private Key: {:?}", info.key_path); 313 314 Ok(()) 315 } 316 317 /// Renew certificate. 318 pub async fn cert_renew(common_name: String, san: Vec<String>) -> Result<()> { 319 let mgr = CertificateManager::new(CertificateManager::default_cert_dir()); 320 let info = mgr.renew(&common_name, &san)?; 321 322 println!("{}", style("Certificate renewed").green().bold()); 323 println!(" Valid until: {}", info.not_after.format("%Y-%m-%d")); 324 325 Ok(()) 326 } 327 328 /// Delete certificate. 329 pub async fn cert_delete(common_name: String) -> Result<()> { 330 let mgr = CertificateManager::new(CertificateManager::default_cert_dir()); 331 mgr.delete(&common_name)?; 332 333 println!( 334 "{}", 335 style(format!("Certificate '{}' deleted", common_name)).yellow() 336 ); 337 338 Ok(()) 339 } 340 341 /// List keys for a node. 342 pub async fn keys_list(node_id: String, json: bool) -> Result<()> { 343 let rotation = KeyRotation::new(KeyRotation::default_data_dir()); 344 let keys = rotation.list_keys(&node_id)?; 345 346 if json { 347 println!("{}", json_output(&keys)); 348 return Ok(()); 349 } 350 351 println!( 352 "{}", 353 style(format!("Keys for node '{}'", node_id)).bold().cyan() 354 ); 355 println!("{}", style("─".repeat(50)).dim()); 356 357 for key in &keys { 358 let status_str = if !key.exists { 359 "not found".to_string() 360 } else if key.permissions_secure { 361 "secure".to_string() 362 } else { 363 format!("insecure ({:o})", key.permissions) 364 }; 365 366 let status_styled = if !key.exists { 367 style(status_str).dim() 368 } else if key.permissions_secure { 369 style(status_str).green() 370 } else { 371 style(status_str).red() 372 }; 373 374 println!( 375 " {} {}: {}", 376 if key.exists { "✓" } else { "✗" }, 377 style(key.key_type.to_string()).bold(), 378 status_styled 379 ); 380 } 381 382 Ok(()) 383 } 384 385 /// Backup keys. 386 pub async fn keys_backup(node_id: String) -> Result<()> { 387 let rotation = KeyRotation::new(KeyRotation::default_data_dir()); 388 let backup = rotation.backup_keys(&node_id)?; 389 390 println!("{}", style("Keys backed up").green().bold()); 391 println!(" Location: {:?}", backup.path); 392 println!( 393 " Keys: {}", 394 backup 395 .keys 396 .iter() 397 .map(|k| k.to_string()) 398 .collect::<Vec<_>>() 399 .join(", ") 400 ); 401 println!(" Size: {} bytes", backup.size); 402 403 Ok(()) 404 } 405 406 /// List key backups. 407 pub async fn keys_backups(node_id: String, json: bool) -> Result<()> { 408 let rotation = KeyRotation::new(KeyRotation::default_data_dir()); 409 let backups = rotation.list_backups(&node_id)?; 410 411 if json { 412 println!("{}", json_output(&backups)); 413 return Ok(()); 414 } 415 416 if backups.is_empty() { 417 println!("{}", style("No backups found").dim()); 418 return Ok(()); 419 } 420 421 println!( 422 "{}", 423 style(format!("Key backups for '{}'", node_id)) 424 .bold() 425 .cyan() 426 ); 427 println!("{}", style("─".repeat(50)).dim()); 428 429 for backup in &backups { 430 println!( 431 "\n{}", 432 style(backup.timestamp.format("%Y-%m-%d %H:%M:%S")).bold() 433 ); 434 println!(" Path: {:?}", backup.path); 435 println!( 436 " Keys: {}", 437 backup 438 .keys 439 .iter() 440 .map(|k| k.to_string()) 441 .collect::<Vec<_>>() 442 .join(", ") 443 ); 444 println!(" Size: {} bytes", backup.size); 445 } 446 447 Ok(()) 448 } 449 450 /// Rotate a key. 451 pub async fn keys_rotate(node_id: String, key_type: String) -> Result<()> { 452 let kt = match key_type.to_lowercase().as_str() { 453 "account" => KeyType::Account, 454 "network" => KeyType::Network, 455 "validator" => KeyType::Validator, 456 "prover" => KeyType::Prover, 457 _ => { 458 return Err(acdc_core::Error::Config(format!( 459 "Unknown key type: {}", 460 key_type 461 ))) 462 } 463 }; 464 465 let rotation = KeyRotation::new(KeyRotation::default_data_dir()); 466 let _info = rotation.rotate_key(&node_id, kt)?; 467 468 println!( 469 "{}", 470 style(format!("Key rotation initiated for {} key", kt)) 471 .yellow() 472 .bold() 473 ); 474 println!("{}", style("Restart the node to complete rotation.").dim()); 475 476 Ok(()) 477 } 478 479 /// Fix key permissions. 480 pub async fn keys_fix_permissions(node_id: String) -> Result<()> { 481 let rotation = KeyRotation::new(KeyRotation::default_data_dir()); 482 let fixed = rotation.fix_permissions(&node_id)?; 483 484 if fixed.is_empty() { 485 println!( 486 "{}", 487 style("All key permissions are already secure").green() 488 ); 489 } else { 490 println!("{}", style("Fixed permissions:").green().bold()); 491 for (kt, old, new) in &fixed { 492 println!(" {}: {:o} → {:o}", kt, old, new); 493 } 494 } 495 496 Ok(()) 497 } 498 499 /// Show hardening status. 500 pub async fn hardening_status(json: bool) -> Result<()> { 501 let status = hardening::get_status().await?; 502 503 if json { 504 println!("{}", json_output(&status)); 505 return Ok(()); 506 } 507 508 println!("{}", style("System Hardening Status").bold().cyan()); 509 println!("{}", style("─".repeat(50)).dim()); 510 println!( 511 "Score: {:.0}% ({}/{} checks passed)", 512 status.score(), 513 status.passed, 514 status.total_checks 515 ); 516 println!(); 517 518 for check in &status.checks { 519 let icon = if check.passed { 520 style("✓").green() 521 } else { 522 style("✗").red() 523 }; 524 525 let name_style = if check.passed { 526 style(&check.name).dim() 527 } else { 528 style(&check.name).bold() 529 }; 530 531 println!("{} {}", icon, name_style); 532 if !check.passed { 533 println!(" {}", style(&check.description).dim()); 534 println!(" Fix: {}", style(&check.remediation).yellow()); 535 } 536 } 537 538 Ok(()) 539 } 540 541 /// Apply auto-fixable hardening. 542 pub async fn hardening_apply() -> Result<()> { 543 let applied = hardening::apply_hardening().await?; 544 545 if applied.is_empty() { 546 println!("{}", style("No hardening changes needed").green()); 547 } else { 548 println!("{}", style("Applied hardening:").green().bold()); 549 for change in &applied { 550 println!(" ✓ {}", change); 551 } 552 println!(); 553 println!( 554 "{}", 555 style("Note: Changes may need to be persisted to sysctl.conf").dim() 556 ); 557 } 558 559 Ok(()) 560 } 561 562 fn risk_score_colored(score: u8) -> console::StyledObject<String> { 563 if score >= 75 { 564 style(score.to_string()).red().bold() 565 } else if score >= 50 { 566 style(score.to_string()).red() 567 } else if score >= 25 { 568 style(score.to_string()).yellow() 569 } else { 570 style(score.to_string()).green() 571 } 572 }