1use anyhow::Result;
2use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
3
4use crate::github::Client;
5
6use super::audit_log::AuditLog;
7use super::planner::RepoPlan;
8
9pub async fn execute_security_plan(
11 client: &Client,
12 plans: &[RepoPlan],
13 audit_log: &AuditLog,
14) -> Result<ExecutionReport> {
15 let multi = MultiProgress::new();
16 let style = ProgressStyle::with_template(" {spinner:.green} [{elapsed_precise}] {msg}")?;
17
18 let mut report = ExecutionReport::default();
19
20 for plan in plans.iter().filter(|p| p.has_changes()) {
21 let pb = multi.add(ProgressBar::new_spinner());
22 pb.set_style(style.clone());
23 pb.set_message(format!(
24 "{}: applying {} changes...",
25 plan.repo,
26 plan.changes.len()
27 ));
28
29 match apply_repo_security(client, plan, audit_log).await {
30 Ok(()) => {
31 pb.finish_with_message(format!("{}: ✅ done", plan.repo));
32 report.succeeded += 1;
33 }
34 Err(e) => {
35 pb.finish_with_message(format!("{}: ❌ {e}", plan.repo));
36 report.failed.push((plan.repo.clone(), e.to_string()));
37 }
38 }
39 }
40
41 Ok(report)
42}
43
44async fn apply_repo_security(client: &Client, plan: &RepoPlan, audit_log: &AuditLog) -> Result<()> {
45 for change in &plan.changes {
46 match change.feature.as_str() {
47 "dependabot_alerts" if change.desired => {
48 client.enable_dependabot_alerts(&plan.repo).await?;
49 }
50 "dependabot_security_updates" if change.desired => {
51 client
52 .enable_dependabot_security_updates(&plan.repo)
53 .await?;
54 }
55 "secret_scanning" | "secret_scanning_ai_detection" | "push_protection" => {
56 continue;
59 }
60 _ => {
61 tracing::warn!("Disabling {} is not yet supported", change.feature);
62 continue;
63 }
64 }
65
66 audit_log.log(
67 &plan.repo,
68 &format!("enable_{}", change.feature),
69 "success",
70 change.current,
71 change.desired,
72 )?;
73 }
74
75 let ss_changes: Vec<_> = plan
77 .changes
78 .iter()
79 .filter(|c| {
80 matches!(
81 c.feature.as_str(),
82 "secret_scanning" | "secret_scanning_ai_detection" | "push_protection"
83 )
84 })
85 .collect();
86
87 if !ss_changes.is_empty() {
88 let secret_scanning = ss_changes
90 .iter()
91 .find(|c| c.feature == "secret_scanning")
92 .map(|c| c.desired)
93 .unwrap_or(true);
94 let ai_detection = ss_changes
95 .iter()
96 .find(|c| c.feature == "secret_scanning_ai_detection")
97 .map(|c| c.desired)
98 .unwrap_or(true);
99 let push_protection = ss_changes
100 .iter()
101 .find(|c| c.feature == "push_protection")
102 .map(|c| c.desired)
103 .unwrap_or(true);
104
105 client
106 .set_security_features(&plan.repo, secret_scanning, ai_detection, push_protection)
107 .await?;
108
109 for change in &ss_changes {
110 audit_log.log(
111 &plan.repo,
112 &format!("set_{}", change.feature),
113 "success",
114 change.current,
115 change.desired,
116 )?;
117 }
118 }
119
120 Ok(())
121}
122
123#[derive(Debug, Default)]
124pub struct ExecutionReport {
125 pub succeeded: usize,
126 pub failed: Vec<(String, String)>,
127}
128
129impl ExecutionReport {
130 pub fn print_summary(&self) {
131 use console::style;
132
133 println!();
134 if self.failed.is_empty() {
135 println!(
136 " {} All {} repositories updated successfully.",
137 style("✅").green(),
138 self.succeeded
139 );
140 } else {
141 println!(
142 " {} {} succeeded, {} {} failed:",
143 style("⚠️").yellow(),
144 self.succeeded,
145 self.failed.len(),
146 if self.failed.len() == 1 {
147 "repo"
148 } else {
149 "repos"
150 }
151 );
152 for (repo, err) in &self.failed {
153 println!(" {} {}: {}", style("❌").red(), repo, err);
154 }
155 }
156 }
157}