Skip to main content

ward/github/
security.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3
4use super::Client;
5
6/// Current security state of a repository.
7#[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    /// Read the current security state of a repository.
35    pub async fn get_security_state(&self, repo: &str) -> Result<SecurityState> {
36        let mut state = SecurityState::default();
37
38        // Dependabot alerts (vulnerability-alerts)
39        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        // Dependabot security updates (automated-security-fixes)
45        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        // Secret scanning, AI detection, push protection (from repo settings)
63        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    /// Enable vulnerability alerts (Dependabot alerts) for a repo.
87    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    /// Enable automated security fixes (Dependabot security updates) for a repo.
96    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    /// Enable secret scanning, AI detection, and/or push protection.
108    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}