Skip to main content

sbom_tools/tui/app_states/
compliance.rs

1//! Compliance state types.
2
3/// Available policy presets
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5pub enum PolicyPreset {
6    #[default]
7    Enterprise,
8    Strict,
9    Permissive,
10    /// EU Cyber Resilience Act compliance (delegates to `quality::ComplianceChecker`)
11    Cra,
12    /// NTIA Minimum Elements compliance (delegates to `quality::ComplianceChecker`)
13    Ntia,
14    /// FDA Medical Device compliance (delegates to `quality::ComplianceChecker`)
15    Fda,
16    /// NIST SP 800-218 Secure Software Development Framework
17    NistSsdf,
18    /// Executive Order 14028 Section 4
19    Eo14028,
20    /// NSA CNSA 2.0 — Post-Quantum Cryptography (strictest)
21    Cnsa2,
22    /// NIST PQC Readiness — IR 8547 + FIPS 203/204/205
23    NistPqc,
24    /// BSI TR-03183-2 — German national CRA-aligned SBOM guideline
25    BsiTr03183_2,
26    /// CRA Article 24 — Open-source software steward (lighter manufacturer profile)
27    CraOssSteward,
28    /// EUCC Substantial — Reg. (EU) 2024/482 reference profile (Annex IV)
29    EuccSubstantial,
30    /// EU AI Act — Reg. (EU) 2024/1689 Annex IV technical-documentation readiness
31    EuAiAct,
32    /// BSI/G7 SBOM-for-AI — Feb 2026 minimum-elements readiness
33    BsiSbomForAi,
34}
35
36impl PolicyPreset {
37    pub const fn next(self) -> Self {
38        match self {
39            Self::Enterprise => Self::Strict,
40            Self::Strict => Self::Permissive,
41            Self::Permissive => Self::Cra,
42            Self::Cra => Self::Ntia,
43            Self::Ntia => Self::Fda,
44            Self::Fda => Self::NistSsdf,
45            Self::NistSsdf => Self::Eo14028,
46            Self::Eo14028 => Self::Cnsa2,
47            Self::Cnsa2 => Self::NistPqc,
48            Self::NistPqc => Self::BsiTr03183_2,
49            Self::BsiTr03183_2 => Self::CraOssSteward,
50            Self::CraOssSteward => Self::EuccSubstantial,
51            Self::EuccSubstantial => Self::EuAiAct,
52            Self::EuAiAct => Self::BsiSbomForAi,
53            Self::BsiSbomForAi => Self::Enterprise,
54        }
55    }
56
57    pub const fn label(self) -> &'static str {
58        match self {
59            Self::Enterprise => "Enterprise",
60            Self::Strict => "Strict",
61            Self::Permissive => "Permissive",
62            Self::Cra => "EU CRA",
63            Self::Ntia => "NTIA",
64            Self::Fda => "FDA",
65            Self::NistSsdf => "NIST SSDF",
66            Self::Eo14028 => "EO 14028",
67            Self::Cnsa2 => "CNSA 2.0",
68            Self::NistPqc => "NIST PQC",
69            Self::BsiTr03183_2 => "BSI TR-03183-2",
70            Self::CraOssSteward => "CRA OSS Steward",
71            Self::EuccSubstantial => "EUCC",
72            Self::EuAiAct => "EU AI Act",
73            Self::BsiSbomForAi => "BSI/G7 SBOM-for-AI",
74        }
75    }
76
77    /// Whether this preset delegates to the standards-based compliance checker.
78    pub const fn is_standards_based(self) -> bool {
79        matches!(
80            self,
81            Self::Cra
82                | Self::Ntia
83                | Self::Fda
84                | Self::NistSsdf
85                | Self::Eo14028
86                | Self::Cnsa2
87                | Self::NistPqc
88                | Self::BsiTr03183_2
89                | Self::CraOssSteward
90                | Self::EuccSubstantial
91                | Self::EuAiAct
92                | Self::BsiSbomForAi
93        )
94    }
95
96    /// Get the corresponding `ComplianceLevel` for standards-based presets.
97    pub const fn compliance_level(self) -> Option<crate::quality::ComplianceLevel> {
98        match self {
99            Self::Cra => Some(crate::quality::ComplianceLevel::CraPhase2),
100            Self::Ntia => Some(crate::quality::ComplianceLevel::NtiaMinimum),
101            Self::Fda => Some(crate::quality::ComplianceLevel::FdaMedicalDevice),
102            Self::NistSsdf => Some(crate::quality::ComplianceLevel::NistSsdf),
103            Self::Eo14028 => Some(crate::quality::ComplianceLevel::Eo14028),
104            Self::Cnsa2 => Some(crate::quality::ComplianceLevel::Cnsa2),
105            Self::NistPqc => Some(crate::quality::ComplianceLevel::NistPqc),
106            Self::BsiTr03183_2 => Some(crate::quality::ComplianceLevel::BsiTr03183_2),
107            Self::CraOssSteward => Some(crate::quality::ComplianceLevel::CraOssSteward),
108            Self::EuccSubstantial => Some(crate::quality::ComplianceLevel::EuccSubstantial),
109            Self::EuAiAct => Some(crate::quality::ComplianceLevel::EuAiAct),
110            Self::BsiSbomForAi => Some(crate::quality::ComplianceLevel::BsiSbomForAi),
111            _ => None,
112        }
113    }
114}
115
116/// State for compliance/policy view (diff mode security policy)
117#[derive(Debug, Clone, Default)]
118pub struct PolicyComplianceState {
119    /// Currently selected policy preset
120    pub policy_preset: PolicyPreset,
121    /// Cached compliance result
122    pub result: Option<crate::tui::security::ComplianceResult>,
123    /// Selected violation index
124    pub selected_violation: usize,
125    /// Whether compliance check has been run
126    pub checked: bool,
127    /// Show detailed view of selected violation
128    pub show_details: bool,
129}
130
131impl PolicyComplianceState {
132    pub fn new() -> Self {
133        Self::default()
134    }
135
136    pub fn toggle_policy(&mut self) {
137        self.policy_preset = self.policy_preset.next();
138        self.result = None; // Clear cached result when policy changes
139        self.checked = false;
140    }
141
142    pub const fn toggle_details(&mut self) {
143        self.show_details = !self.show_details;
144    }
145
146    pub fn passes(&self) -> bool {
147        self.result.as_ref().is_none_or(|r| r.passes)
148    }
149}
150
151/// Severity filter for compliance violation display.
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
153pub enum ComplianceSeverityFilter {
154    /// Show all violations
155    #[default]
156    All,
157    /// Show only errors
158    ErrorsOnly,
159    /// Show warnings and above
160    WarningsAndAbove,
161}
162
163impl ComplianceSeverityFilter {
164    /// Cycle to the next filter option.
165    pub const fn next(self) -> Self {
166        match self {
167            Self::All => Self::ErrorsOnly,
168            Self::ErrorsOnly => Self::WarningsAndAbove,
169            Self::WarningsAndAbove => Self::All,
170        }
171    }
172
173    /// Human-readable label for display in the filter bar.
174    pub const fn label(self) -> &'static str {
175        match self {
176            Self::All => "All",
177            Self::ErrorsOnly => "Errors",
178            Self::WarningsAndAbove => "Warn+",
179        }
180    }
181
182    /// Whether a violation severity passes this filter.
183    pub fn matches(self, severity: crate::quality::ViolationSeverity) -> bool {
184        match self {
185            Self::All => true,
186            Self::ErrorsOnly => {
187                matches!(severity, crate::quality::ViolationSeverity::Error)
188            }
189            Self::WarningsAndAbove => {
190                matches!(
191                    severity,
192                    crate::quality::ViolationSeverity::Error
193                        | crate::quality::ViolationSeverity::Warning
194                )
195            }
196        }
197    }
198}
199
200/// State for compliance tab in diff mode (side-by-side multi-standard comparison)
201#[derive(Debug, Clone)]
202pub struct DiffComplianceState {
203    /// Currently selected compliance standard index
204    pub selected_standard: usize,
205    /// Currently selected violation in the list
206    pub selected_violation: usize,
207    /// Scroll offset for violations viewport
208    pub scroll_offset: usize,
209    /// Show old SBOM violations (left panel), new SBOM violations (right panel), or diff
210    pub view_mode: DiffComplianceViewMode,
211    /// Whether the detail overlay is shown for the selected violation
212    pub show_detail: bool,
213    /// Severity filter for violation display
214    pub severity_filter: ComplianceSeverityFilter,
215    /// Whether violations are grouped by component/element
216    pub group_by_element: bool,
217    /// Which element groups are currently expanded (by element name)
218    pub expanded_groups: std::collections::HashSet<String>,
219}
220
221/// What to show in the diff compliance tab
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub enum DiffComplianceViewMode {
224    /// Side-by-side violation counts per standard
225    Overview,
226    /// Show violations that are new in the new SBOM
227    NewViolations,
228    /// Show violations resolved between old and new
229    ResolvedViolations,
230    /// Show all violations for old SBOM
231    OldViolations,
232    /// Show all violations for new SBOM
233    NewSbomViolations,
234}
235
236impl Default for DiffComplianceState {
237    fn default() -> Self {
238        Self {
239            selected_standard: 3,
240            selected_violation: 0,
241            scroll_offset: 0,
242            show_detail: false,
243            view_mode: DiffComplianceViewMode::Overview,
244            severity_filter: ComplianceSeverityFilter::default(),
245            group_by_element: false,
246            expanded_groups: std::collections::HashSet::new(),
247        }
248    }
249}
250
251impl DiffComplianceState {
252    pub fn new() -> Self {
253        Self::default()
254    }
255
256    pub const fn next_standard(&mut self) {
257        let count = crate::quality::ComplianceLevel::all().len();
258        self.selected_standard = (self.selected_standard + 1) % count;
259        self.selected_violation = 0;
260        self.scroll_offset = 0;
261    }
262
263    pub const fn prev_standard(&mut self) {
264        let count = crate::quality::ComplianceLevel::all().len();
265        self.selected_standard = if self.selected_standard == 0 {
266            count - 1
267        } else {
268            self.selected_standard - 1
269        };
270        self.selected_violation = 0;
271        self.scroll_offset = 0;
272    }
273
274    pub const fn next_view_mode(&mut self) {
275        self.view_mode = match self.view_mode {
276            DiffComplianceViewMode::Overview => DiffComplianceViewMode::NewViolations,
277            DiffComplianceViewMode::NewViolations => DiffComplianceViewMode::ResolvedViolations,
278            DiffComplianceViewMode::ResolvedViolations => DiffComplianceViewMode::OldViolations,
279            DiffComplianceViewMode::OldViolations => DiffComplianceViewMode::NewSbomViolations,
280            DiffComplianceViewMode::NewSbomViolations => DiffComplianceViewMode::Overview,
281        };
282        self.selected_violation = 0;
283        self.scroll_offset = 0;
284    }
285
286    pub const fn select_next(&mut self, max: usize) {
287        if max > 0 && self.selected_violation + 1 < max {
288            self.selected_violation += 1;
289        }
290    }
291
292    pub const fn select_prev(&mut self) {
293        if self.selected_violation > 0 {
294            self.selected_violation -= 1;
295        }
296    }
297
298    /// Cycle the severity filter and reset selection.
299    pub const fn toggle_severity_filter(&mut self) {
300        self.severity_filter = self.severity_filter.next();
301        self.selected_violation = 0;
302        self.scroll_offset = 0;
303    }
304
305    /// Toggle grouping violations by component/element.
306    pub fn toggle_group_by_element(&mut self) {
307        self.group_by_element = !self.group_by_element;
308        self.selected_violation = 0;
309        self.scroll_offset = 0;
310    }
311}