Skip to main content

tatara_core/domain/
compliance_binding.rs

1//! Type-level compliance bindings.
2//!
3//! Controls bind to convergence point TYPES, not instances. "All Security
4//! substrate points must satisfy NIST AC-6" is a type-level constraint
5//! verified at the phase specified by the binding.
6
7use serde::{Deserialize, Serialize};
8
9use super::convergence_state::{ConvergencePointType, SubstrateType};
10use super::point_id::PointId;
11
12/// A compliance control bound to convergence point types.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ComplianceBinding {
15    /// What convergence points this control applies to.
16    pub selector: PointSelector,
17    /// The compliance control to verify.
18    pub control: ComplianceControl,
19    /// When this control is verified.
20    pub phase: VerificationPhase,
21}
22
23/// Selects which convergence points a compliance control applies to.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum PointSelector {
27    /// All points of a specific structural type.
28    ByType(ConvergencePointType),
29    /// All points on a specific substrate.
30    BySubstrate(SubstrateType),
31    /// Points matching both substrate and type.
32    BySubstrateAndType(SubstrateType, ConvergencePointType),
33    /// All points in a specific environment.
34    ByEnvironment(String),
35    /// All points handling data of a specific classification.
36    ByDataClassification(DataClassification),
37    /// A specific point by ID.
38    ById(PointId),
39    /// All convergence points (universal control).
40    All,
41}
42
43/// When compliance is verified relative to convergence execution.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum VerificationPhase {
47    /// Verified at plan time by static analysis (zero cost).
48    PlanTime,
49    /// Verified inline during the convergence boundary.
50    AtBoundary,
51    /// Verified after convergence via live probes.
52    PostConvergence,
53}
54
55/// A specific compliance control from a framework.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ComplianceControl {
58    /// Framework name (e.g., "nist-800-53", "soc2", "fedramp").
59    pub framework: String,
60    /// Control identifier (e.g., "AC-6", "CC6.1", "3.4").
61    pub control_id: String,
62    /// Human-readable description.
63    pub description: String,
64}
65
66/// Data classification for compliance purposes.
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(rename_all = "snake_case")]
69pub enum DataClassification {
70    /// Personally Identifiable Information.
71    Pii,
72    /// Protected Health Information.
73    Phi,
74    /// Payment Card Industry data.
75    Pci,
76    /// Public data.
77    Public,
78    /// Internal data.
79    Internal,
80    /// Confidential data.
81    Confidential,
82}
83
84/// The complete set of compliance controls bound to a convergence DAG.
85/// Computed at plan time before any execution.
86#[derive(Debug, Clone, Serialize, Deserialize, Default)]
87pub struct ComplianceClosure {
88    /// All bindings that apply to this DAG.
89    pub bindings: Vec<ComplianceBinding>,
90    /// Resolved: which specific points each control applies to.
91    pub resolved: Vec<ResolvedControl>,
92    /// Count of controls verifiable at plan time (zero cost).
93    pub plan_time_count: usize,
94    /// Count of controls verifiable at boundary (inline).
95    pub at_boundary_count: usize,
96    /// Count of controls verifiable post-convergence (live probes).
97    pub post_convergence_count: usize,
98}
99
100/// A compliance control resolved to specific convergence points.
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct ResolvedControl {
103    /// The control being verified.
104    pub control: ComplianceControl,
105    /// The points this control applies to.
106    pub point_ids: Vec<PointId>,
107    /// When it's verified.
108    pub phase: VerificationPhase,
109}
110
111impl PointSelector {
112    /// Check if this selector matches a point with the given attributes.
113    pub fn matches(
114        &self,
115        point_type: &ConvergencePointType,
116        substrate: &SubstrateType,
117        point_id: &PointId,
118        environment: Option<&str>,
119        data_class: Option<&DataClassification>,
120    ) -> bool {
121        match self {
122            Self::ByType(t) => t == point_type,
123            Self::BySubstrate(s) => s == substrate,
124            Self::BySubstrateAndType(s, t) => s == substrate && t == point_type,
125            Self::ByEnvironment(env) => environment == Some(env.as_str()),
126            Self::ByDataClassification(dc) => data_class == Some(dc),
127            Self::ById(id) => id == point_id,
128            Self::All => true,
129        }
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    fn sample_point_id() -> PointId {
138        PointId::compute(b"test", &[], b"state")
139    }
140
141    #[test]
142    fn test_selector_all_matches_everything() {
143        let sel = PointSelector::All;
144        assert!(sel.matches(
145            &ConvergencePointType::Transform,
146            &SubstrateType::Compute,
147            &sample_point_id(),
148            None,
149            None,
150        ));
151    }
152
153    #[test]
154    fn test_selector_by_substrate() {
155        let sel = PointSelector::BySubstrate(SubstrateType::Security);
156        assert!(sel.matches(
157            &ConvergencePointType::Gate,
158            &SubstrateType::Security,
159            &sample_point_id(),
160            None,
161            None,
162        ));
163        assert!(!sel.matches(
164            &ConvergencePointType::Gate,
165            &SubstrateType::Compute,
166            &sample_point_id(),
167            None,
168            None,
169        ));
170    }
171
172    #[test]
173    fn test_selector_by_type() {
174        let sel = PointSelector::ByType(ConvergencePointType::Gate);
175        assert!(sel.matches(
176            &ConvergencePointType::Gate,
177            &SubstrateType::Compute,
178            &sample_point_id(),
179            None,
180            None,
181        ));
182        assert!(!sel.matches(
183            &ConvergencePointType::Transform,
184            &SubstrateType::Compute,
185            &sample_point_id(),
186            None,
187            None,
188        ));
189    }
190
191    #[test]
192    fn test_selector_by_data_classification() {
193        let sel = PointSelector::ByDataClassification(DataClassification::Pii);
194        assert!(sel.matches(
195            &ConvergencePointType::Transform,
196            &SubstrateType::Storage,
197            &sample_point_id(),
198            None,
199            Some(&DataClassification::Pii),
200        ));
201        assert!(!sel.matches(
202            &ConvergencePointType::Transform,
203            &SubstrateType::Storage,
204            &sample_point_id(),
205            None,
206            Some(&DataClassification::Public),
207        ));
208    }
209
210    #[test]
211    fn test_verification_phase_serde() {
212        for phase in [
213            VerificationPhase::PlanTime,
214            VerificationPhase::AtBoundary,
215            VerificationPhase::PostConvergence,
216        ] {
217            let json = serde_json::to_string(&phase).unwrap();
218            let parsed: VerificationPhase = serde_json::from_str(&json).unwrap();
219            assert_eq!(phase, parsed);
220        }
221    }
222
223    #[test]
224    fn test_compliance_binding_serde() {
225        let binding = ComplianceBinding {
226            selector: PointSelector::BySubstrate(SubstrateType::Security),
227            control: ComplianceControl {
228                framework: "nist-800-53".into(),
229                control_id: "AC-6".into(),
230                description: "Least privilege".into(),
231            },
232            phase: VerificationPhase::PlanTime,
233        };
234        let json = serde_json::to_string(&binding).unwrap();
235        let _: ComplianceBinding = serde_json::from_str(&json).unwrap();
236    }
237}