Skip to main content

ephemeris_forge_github/plan/
security.rs

1use 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}