1use serde::Serialize;
2
3use crate::config::SecurityConfig;
4use crate::github::security::SecurityState;
5
6#[derive(Debug, Clone, PartialEq, Serialize)]
8pub struct SecurityChange {
9 pub feature: String,
10 pub current: bool,
11 pub desired: bool,
12}
13
14#[derive(Debug, Clone, PartialEq, Serialize)]
16pub struct RepoPlan {
17 pub repo: String,
18 pub changes: Vec<SecurityChange>,
19}
20
21impl RepoPlan {
22 pub fn has_changes(&self) -> bool {
23 !self.changes.is_empty()
24 }
25}
26
27pub fn plan_security(repo: &str, current: &SecurityState, desired: &SecurityConfig) -> RepoPlan {
29 let mut changes = Vec::new();
30
31 let checks = [
32 (
33 "dependabot_alerts",
34 current.dependabot_alerts,
35 desired.dependabot_alerts,
36 ),
37 (
38 "dependabot_security_updates",
39 current.dependabot_security_updates,
40 desired.dependabot_security_updates,
41 ),
42 (
43 "secret_scanning",
44 current.secret_scanning,
45 desired.secret_scanning,
46 ),
47 (
48 "secret_scanning_ai_detection",
49 current.secret_scanning_ai_detection,
50 desired.secret_scanning_ai_detection,
51 ),
52 (
53 "push_protection",
54 current.push_protection,
55 desired.push_protection,
56 ),
57 ];
58
59 for (feature, current_val, desired_val) in checks {
60 if current_val != desired_val {
61 changes.push(SecurityChange {
62 feature: feature.to_string(),
63 current: current_val,
64 desired: desired_val,
65 });
66 }
67 }
68
69 RepoPlan {
70 repo: repo.to_string(),
71 changes,
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 fn state(da: bool, dsu: bool, ss: bool, ai: bool, pp: bool) -> SecurityState {
80 SecurityState {
81 dependabot_alerts: da,
82 dependabot_security_updates: dsu,
83 secret_scanning: ss,
84 secret_scanning_ai_detection: ai,
85 push_protection: pp,
86 }
87 }
88
89 fn config(da: bool, dsu: bool, ss: bool, ai: bool, pp: bool) -> SecurityConfig {
90 SecurityConfig {
91 dependabot_alerts: da,
92 dependabot_security_updates: dsu,
93 secret_scanning: ss,
94 secret_scanning_ai_detection: ai,
95 push_protection: pp,
96 codeql_advanced_setup: false,
97 }
98 }
99
100 #[test]
101 fn no_changes_when_state_matches_config() {
102 let plan = plan_security(
103 "repo",
104 &state(true, true, true, true, true),
105 &config(true, true, true, true, true),
106 );
107 assert!(!plan.has_changes());
108 assert!(plan.changes.is_empty());
109 }
110
111 #[test]
112 fn all_changes_when_nothing_enabled() {
113 let plan = plan_security(
114 "repo",
115 &state(false, false, false, false, false),
116 &config(true, true, true, true, true),
117 );
118 assert!(plan.has_changes());
119 assert_eq!(plan.changes.len(), 5);
120 assert!(plan.changes.iter().all(|c| !c.current && c.desired));
121 }
122
123 #[test]
124 fn partial_changes() {
125 let plan = plan_security(
126 "repo",
127 &state(true, false, true, false, false),
128 &config(true, true, true, true, true),
129 );
130 assert_eq!(plan.changes.len(), 3);
131 let features: Vec<&str> = plan.changes.iter().map(|c| c.feature.as_str()).collect();
132 assert!(features.contains(&"dependabot_security_updates"));
133 assert!(features.contains(&"secret_scanning_ai_detection"));
134 assert!(features.contains(&"push_protection"));
135 }
136
137 #[test]
138 fn plan_to_disable_features() {
139 let plan = plan_security(
140 "repo",
141 &state(true, true, true, true, true),
142 &config(false, false, false, false, false),
143 );
144 assert_eq!(plan.changes.len(), 5);
145 assert!(plan.changes.iter().all(|c| c.current && !c.desired));
146 }
147
148 #[test]
149 fn repo_name_preserved() {
150 let plan = plan_security(
151 "my-cool-repo",
152 &state(false, false, false, false, false),
153 &config(true, true, true, true, true),
154 );
155 assert_eq!(plan.repo, "my-cool-repo");
156 }
157}