1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3
4use super::Client;
5
6#[derive(Debug, Clone, Default, Serialize)]
8pub struct SecurityState {
9 pub dependabot_alerts: bool,
10 pub dependabot_security_updates: bool,
11 pub secret_scanning: bool,
12 pub secret_scanning_ai_detection: bool,
13 pub push_protection: bool,
14}
15
16#[derive(Debug, Deserialize)]
17struct RepoSecurityResponse {
18 security_and_analysis: Option<SecurityAndAnalysis>,
19}
20
21#[derive(Debug, Deserialize)]
22struct SecurityAndAnalysis {
23 secret_scanning: Option<FeatureStatus>,
24 secret_scanning_ai_detection: Option<FeatureStatus>,
25 secret_scanning_push_protection: Option<FeatureStatus>,
26}
27
28#[derive(Debug, Deserialize)]
29struct FeatureStatus {
30 status: String,
31}
32
33impl Client {
34 pub async fn get_security_state(&self, repo: &str) -> Result<SecurityState> {
36 let mut state = SecurityState::default();
37
38 let resp = self
40 .get(&format!("/repos/{}/{repo}/vulnerability-alerts", self.org))
41 .await?;
42 state.dependabot_alerts = resp.status().as_u16() == 204;
43
44 let resp = self
46 .get(&format!(
47 "/repos/{}/{repo}/automated-security-fixes",
48 self.org
49 ))
50 .await?;
51
52 if resp.status().is_success() {
53 #[derive(Deserialize)]
54 struct AutoSecFixes {
55 enabled: bool,
56 }
57 if let Ok(body) = resp.json::<AutoSecFixes>().await {
58 state.dependabot_security_updates = body.enabled;
59 }
60 }
61
62 let resp = self.get(&format!("/repos/{}/{repo}", self.org)).await?;
64
65 if resp.status().is_success()
66 && let Ok(body) = resp.json::<RepoSecurityResponse>().await
67 && let Some(sa) = body.security_and_analysis
68 {
69 state.secret_scanning = sa
70 .secret_scanning
71 .as_ref()
72 .is_some_and(|f| f.status == "enabled");
73 state.secret_scanning_ai_detection = sa
74 .secret_scanning_ai_detection
75 .as_ref()
76 .is_some_and(|f| f.status == "enabled");
77 state.push_protection = sa
78 .secret_scanning_push_protection
79 .as_ref()
80 .is_some_and(|f| f.status == "enabled");
81 }
82
83 Ok(state)
84 }
85
86 pub async fn enable_dependabot_alerts(&self, repo: &str) -> Result<()> {
88 let resp = self
89 .put(&format!("/repos/{}/{repo}/vulnerability-alerts", self.org))
90 .await?;
91
92 ensure_success(resp, "enable Dependabot alerts", repo).await
93 }
94
95 pub async fn enable_dependabot_security_updates(&self, repo: &str) -> Result<()> {
97 let resp = self
98 .put(&format!(
99 "/repos/{}/{repo}/automated-security-fixes",
100 self.org
101 ))
102 .await?;
103
104 ensure_success(resp, "enable Dependabot security updates", repo).await
105 }
106
107 pub async fn set_security_features(
109 &self,
110 repo: &str,
111 secret_scanning: bool,
112 ai_detection: bool,
113 push_protection: bool,
114 ) -> Result<()> {
115 let body = serde_json::json!({
116 "security_and_analysis": {
117 "secret_scanning": {
118 "status": if secret_scanning { "enabled" } else { "disabled" }
119 },
120 "secret_scanning_ai_detection": {
121 "status": if ai_detection { "enabled" } else { "disabled" }
122 },
123 "secret_scanning_push_protection": {
124 "status": if push_protection { "enabled" } else { "disabled" }
125 }
126 }
127 });
128
129 let resp = self
130 .patch_json(&format!("/repos/{}/{repo}", self.org), &body)
131 .await?;
132
133 ensure_success(resp, "set security features", repo).await
134 }
135}
136
137async fn ensure_success(resp: reqwest::Response, action: &str, repo: &str) -> Result<()> {
138 let status = resp.status();
139 if status.is_success() || status.as_u16() == 204 {
140 Ok(())
141 } else {
142 let body = resp.text().await.unwrap_or_default();
143 anyhow::bail!("Failed to {action} for {repo} (HTTP {status}): {body}")
144 }
145}