/ crates / acdc / src / commands / security.rs
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  }