ephemeris_forge_github/plan/
security.rs1use ephemeris_forge::{FieldDiff, Limitation, Report};
2
3use crate::api::model;
4use crate::api::request::{AnalysisPatch, CodeScanningPatch, ReviewersBody, StatusBody};
5use crate::live::SecurityView;
6use crate::operation::security::Operation;
7use crate::{CodeScanning, Language, Reviewer, Security, SetupState};
8
9macro_rules! status {
10 ($diff:ident, $patch:ident, $report:ident, $desired:ident, $live:ident, $($field:ident),+ $(,)?) => {$(
11 if let Some(wanted) = $desired.$field {
12 match $live.$field {
13 Some(actual) => {
14 if $diff.check(stringify!($field), actual == wanted, &actual, &wanted) {
15 $patch.$field = Some(StatusBody { status: wanted });
16 }
17 }
18 None => unreadable(stringify!($field), $report),
19 }
20 }
21 )+};
22}
23
24pub(super) fn plan(
25 desired: &Security,
26 live: &SecurityView<'_>,
27 report: &mut Report,
28) -> Vec<Operation> {
29 let mut diff = FieldDiff::default();
30 let mut operations = Vec::new();
31 let analysis = analysis(desired, live.analysis, &mut diff, report);
32 operations.extend((!analysis.is_empty()).then_some(Operation::PatchAnalysis(analysis)));
33 operations.extend(switch(
34 "vulnerability_alerts",
35 desired.vulnerability_alerts,
36 live.endpoints.vulnerability_alerts,
37 Operation::SetVulnerabilityAlerts,
38 &mut diff,
39 report,
40 ));
41 operations.extend(switch(
42 "automated_security_fixes",
43 desired.automated_security_fixes,
44 live.endpoints.automated_security_fixes,
45 Operation::SetAutomatedSecurityFixes,
46 &mut diff,
47 report,
48 ));
49 operations.extend(switch(
50 "private_vulnerability_reporting",
51 desired.private_vulnerability_reporting,
52 live.endpoints.private_vulnerability_reporting,
53 Operation::SetPrivateVulnerabilityReporting,
54 &mut diff,
55 report,
56 ));
57 if let Some(wanted) = &desired.code_scanning_default_setup {
58 match &live.endpoints.code_scanning_default_setup {
59 Some(actual) => {
60 operations.extend(code_scanning(wanted, actual, &mut diff));
61 }
62 None => unreadable("code_scanning_default_setup", report),
63 }
64 }
65 diff.record("security", report);
66 operations
67}
68
69fn unreadable(field: &'static str, report: &mut Report) {
70 report
71 .limitations
72 .push(Limitation::SecurityUnreadable { field });
73}
74
75fn analysis(
76 desired: &Security,
77 live: &model::Analysis,
78 diff: &mut FieldDiff,
79 report: &mut Report,
80) -> AnalysisPatch {
81 let mut patch = AnalysisPatch::default();
82 status!(
83 diff,
84 patch,
85 report,
86 desired,
87 live,
88 advanced_security,
89 code_security,
90 secret_scanning,
91 secret_scanning_push_protection,
92 secret_scanning_ai_detection,
93 secret_scanning_non_provider_patterns,
94 secret_scanning_delegated_alert_dismissal,
95 secret_scanning_delegated_bypass,
96 );
97 if let Some(wanted) = &desired.secret_scanning_delegated_bypass_options.reviewers {
98 if live.secret_scanning_delegated_bypass.is_none() {
99 unreadable("secret_scanning_delegated_bypass_options", report);
100 } else {
101 let wanted = sorted(wanted);
102 let actual = sorted(&live.secret_scanning_delegated_bypass_reviewers);
103 if diff.check(
104 "secret_scanning_delegated_bypass_options",
105 wanted == actual,
106 &actual,
107 &wanted,
108 ) {
109 patch.secret_scanning_delegated_bypass_options =
110 Some(ReviewersBody { reviewers: wanted });
111 }
112 }
113 }
114 patch
115}
116
117fn sorted(reviewers: &[Reviewer]) -> Vec<Reviewer> {
118 let mut reviewers = reviewers.to_vec();
119 reviewers.sort_unstable();
120 reviewers.dedup();
121 reviewers
122}
123
124fn switch(
125 field: &'static str,
126 desired: Option<bool>,
127 live: Option<bool>,
128 operation: fn(bool) -> Operation,
129 diff: &mut FieldDiff,
130 report: &mut Report,
131) -> Option<Operation> {
132 let wanted = desired?;
133 let Some(actual) = live else {
134 unreadable(field, report);
135 return None;
136 };
137 diff.check(field, actual == wanted, &actual, &wanted)
138 .then(|| operation(wanted))
139}
140
141fn code_scanning(
142 desired: &CodeScanning,
143 live: &model::CodeScanning,
144 diff: &mut FieldDiff,
145) -> Option<Operation> {
146 let mut changed = desired.state.is_some_and(|state| {
147 diff.check(
148 "code_scanning_default_setup.state",
149 live.state == state,
150 &live.state,
151 &state,
152 )
153 });
154 if desired.state != Some(SetupState::NotConfigured) {
155 if let Some(wanted) = &desired.languages {
156 let (actual, wanted) = (languages(&live.languages), languages(wanted));
157 changed |= diff.check(
158 "code_scanning_default_setup.languages",
159 actual == wanted,
160 &actual,
161 &wanted,
162 );
163 }
164 if let Some(runner_type) = desired.runner_type {
165 changed |= diff.check(
166 "code_scanning_default_setup.runner_type",
167 live.runner_type == Some(runner_type),
168 &live.runner_type,
169 &Some(runner_type),
170 );
171 }
172 if let Some(label) = &desired.runner_label {
173 changed |= diff.check(
174 "code_scanning_default_setup.runner_label",
175 live.runner_label.as_ref() == Some(label),
176 &live.runner_label,
177 &Some(label),
178 );
179 }
180 if let Some(query_suite) = desired.query_suite {
181 changed |= diff.check(
182 "code_scanning_default_setup.query_suite",
183 live.query_suite == Some(query_suite),
184 &live.query_suite,
185 &Some(query_suite),
186 );
187 }
188 if let Some(threat_model) = desired.threat_model {
189 changed |= diff.check(
190 "code_scanning_default_setup.threat_model",
191 live.threat_model == Some(threat_model),
192 &live.threat_model,
193 &Some(threat_model),
194 );
195 }
196 }
197 changed.then(|| Operation::PatchCodeScanning(code_scanning_patch(desired)))
198}
199
200fn code_scanning_patch(desired: &CodeScanning) -> CodeScanningPatch {
201 if desired.state == Some(SetupState::NotConfigured) {
202 return CodeScanningPatch {
203 state: desired.state,
204 ..CodeScanningPatch::default()
205 };
206 }
207 CodeScanningPatch {
208 state: desired.state,
209 languages: desired.languages.as_deref().map(languages),
210 runner_type: desired.runner_type,
211 runner_label: desired.runner_label.clone(),
212 query_suite: desired.query_suite,
213 threat_model: desired.threat_model,
214 }
215}
216
217fn languages(languages: &[Language]) -> Vec<Language> {
218 let mut languages = languages.to_vec();
219 languages.sort_unstable();
220 languages.dedup();
221 languages
222}