Skip to main content

libverify_core/
control.rs

1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5use crate::evidence::{EvidenceBundle, EvidenceGap, EvidenceState, RepositoryPosture};
6
7/// A string-based control identifier, enabling open extensibility.
8///
9/// Built-in controls use kebab-case IDs (e.g. "review-independence").
10/// Platform-specific verifiers can register controls with their own IDs
11/// (e.g. "jira-linkage", "bitbucket-pipeline-status").
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(transparent)]
14pub struct ControlId(String);
15
16impl ControlId {
17    pub fn new(id: impl Into<String>) -> Self {
18        Self(id.into())
19    }
20
21    pub fn as_str(&self) -> &str {
22        &self.0
23    }
24}
25
26impl fmt::Display for ControlId {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        f.write_str(&self.0)
29    }
30}
31
32impl From<&str> for ControlId {
33    fn from(s: &str) -> Self {
34        Self(s.to_string())
35    }
36}
37
38impl From<String> for ControlId {
39    fn from(s: String) -> Self {
40        Self(s)
41    }
42}
43
44// --- Built-in control IDs (constants for compile-time safety) ---
45
46pub mod builtin {
47    use super::ControlId;
48
49    // Source Track
50    pub const SOURCE_AUTHENTICITY: &str = "source-authenticity";
51    pub const REVIEW_INDEPENDENCE: &str = "review-independence";
52    pub const BRANCH_HISTORY_INTEGRITY: &str = "branch-history-integrity";
53    pub const BRANCH_PROTECTION_ENFORCEMENT: &str = "branch-protection-enforcement";
54    pub const TWO_PARTY_REVIEW: &str = "two-party-review";
55
56    // Build Track
57    pub const BUILD_PROVENANCE: &str = "build-provenance";
58    pub const REQUIRED_STATUS_CHECKS: &str = "required-status-checks";
59    pub const HOSTED_BUILD_PLATFORM: &str = "hosted-build-platform";
60    pub const PROVENANCE_AUTHENTICITY: &str = "provenance-authenticity";
61    pub const BUILD_ISOLATION: &str = "build-isolation";
62
63    // Dependencies Track
64    pub const DEPENDENCY_SIGNATURE: &str = "dependency-signature";
65    pub const DEPENDENCY_PROVENANCE_CHECK: &str = "dependency-provenance";
66    pub const DEPENDENCY_SIGNER_VERIFIED: &str = "dependency-signer-verified";
67    pub const DEPENDENCY_COMPLETENESS: &str = "dependency-completeness";
68
69    // Compliance (platform-neutral naming)
70    pub const CHANGE_REQUEST_SIZE: &str = "change-request-size";
71    pub const TEST_COVERAGE: &str = "test-coverage";
72    pub const SCOPED_CHANGE: &str = "scoped-change";
73    pub const ISSUE_LINKAGE: &str = "issue-linkage";
74    pub const STALE_REVIEW: &str = "stale-review";
75    pub const DESCRIPTION_QUALITY: &str = "description-quality";
76    pub const MERGE_COMMIT_POLICY: &str = "merge-commit-policy";
77    pub const CONVENTIONAL_TITLE: &str = "conventional-title";
78    pub const SECURITY_FILE_CHANGE: &str = "security-file-change";
79    pub const RELEASE_TRACEABILITY: &str = "release-traceability";
80
81    // ASPM / Repository Posture
82    pub const CODEOWNERS_COVERAGE: &str = "codeowners-coverage";
83    pub const SECRET_SCANNING: &str = "secret-scanning";
84    pub const VULNERABILITY_SCANNING: &str = "vulnerability-scanning";
85    pub const SECURITY_POLICY: &str = "security-policy";
86
87    // Enterprise Posture
88    pub const SECRET_SCANNING_PUSH_PROTECTION: &str = "secret-scanning-push-protection";
89    pub const BRANCH_PROTECTION_ADMIN_ENFORCEMENT: &str = "branch-protection-admin-enforcement";
90    pub const DISMISS_STALE_REVIEWS_ON_PUSH: &str = "dismiss-stale-reviews-on-push";
91    pub const ACTIONS_PINNED_DEPENDENCIES: &str = "actions-pinned-dependencies";
92    pub const ENVIRONMENT_PROTECTION_RULES: &str = "environment-protection-rules";
93    pub const CODE_SCANNING_ALERTS_RESOLVED: &str = "code-scanning-alerts-resolved";
94    pub const DEPENDENCY_LICENSE_COMPLIANCE: &str = "dependency-license-compliance";
95    pub const SBOM_ATTESTATION: &str = "sbom-attestation";
96    pub const RELEASE_ASSET_ATTESTATION: &str = "release-asset-attestation";
97    pub const PRIVILEGED_WORKFLOW_DETECTION: &str = "privileged-workflow-detection";
98    pub const WORKFLOW_PERMISSIONS_RESTRICTED: &str = "workflow-permissions-restricted";
99    pub const DEPENDENCY_UPDATE_TOOL: &str = "dependency-update-tool";
100    pub const REPOSITORY_PERMISSIONS_AUDIT: &str = "repository-permissions-audit";
101    pub const DEFAULT_BRANCH_SETTINGS_BASELINE: &str = "default-branch-settings-baseline";
102    pub const SECURITY_TEST_IN_CI: &str = "security-test-in-ci";
103
104    /// All 43 built-in control IDs.
105    pub const ALL: &[&str] = &[
106        SOURCE_AUTHENTICITY,
107        REVIEW_INDEPENDENCE,
108        BRANCH_HISTORY_INTEGRITY,
109        BRANCH_PROTECTION_ENFORCEMENT,
110        TWO_PARTY_REVIEW,
111        BUILD_PROVENANCE,
112        REQUIRED_STATUS_CHECKS,
113        HOSTED_BUILD_PLATFORM,
114        PROVENANCE_AUTHENTICITY,
115        BUILD_ISOLATION,
116        DEPENDENCY_SIGNATURE,
117        DEPENDENCY_PROVENANCE_CHECK,
118        DEPENDENCY_SIGNER_VERIFIED,
119        DEPENDENCY_COMPLETENESS,
120        CHANGE_REQUEST_SIZE,
121        TEST_COVERAGE,
122        SCOPED_CHANGE,
123        ISSUE_LINKAGE,
124        STALE_REVIEW,
125        DESCRIPTION_QUALITY,
126        MERGE_COMMIT_POLICY,
127        CONVENTIONAL_TITLE,
128        SECURITY_FILE_CHANGE,
129        RELEASE_TRACEABILITY,
130        CODEOWNERS_COVERAGE,
131        SECRET_SCANNING,
132        VULNERABILITY_SCANNING,
133        SECURITY_POLICY,
134        SECRET_SCANNING_PUSH_PROTECTION,
135        BRANCH_PROTECTION_ADMIN_ENFORCEMENT,
136        DISMISS_STALE_REVIEWS_ON_PUSH,
137        ACTIONS_PINNED_DEPENDENCIES,
138        ENVIRONMENT_PROTECTION_RULES,
139        CODE_SCANNING_ALERTS_RESOLVED,
140        DEPENDENCY_LICENSE_COMPLIANCE,
141        SBOM_ATTESTATION,
142        RELEASE_ASSET_ATTESTATION,
143        PRIVILEGED_WORKFLOW_DETECTION,
144        WORKFLOW_PERMISSIONS_RESTRICTED,
145        DEPENDENCY_UPDATE_TOOL,
146        REPOSITORY_PERMISSIONS_AUDIT,
147        DEFAULT_BRANCH_SETTINGS_BASELINE,
148        SECURITY_TEST_IN_CI,
149    ];
150
151    /// Returns a ControlId for a built-in constant.
152    pub fn id(s: &str) -> ControlId {
153        ControlId::new(s)
154    }
155}
156
157/// Outcome of evaluating a single control against evidence.
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
159#[serde(rename_all = "snake_case")]
160pub enum ControlStatus {
161    Satisfied,
162    Violated,
163    Indeterminate,
164    NotApplicable,
165}
166
167impl ControlStatus {
168    pub fn as_str(&self) -> &'static str {
169        match self {
170            Self::Satisfied => "satisfied",
171            Self::Violated => "violated",
172            Self::Indeterminate => "indeterminate",
173            Self::NotApplicable => "not_applicable",
174        }
175    }
176}
177
178impl fmt::Display for ControlStatus {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        f.write_str(self.as_str())
181    }
182}
183
184/// Result of a single control evaluation.
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct ControlFinding {
187    pub control_id: ControlId,
188    pub status: ControlStatus,
189    pub rationale: String,
190    pub subjects: Vec<String>,
191    pub evidence_gaps: Vec<EvidenceGap>,
192}
193
194impl ControlFinding {
195    pub fn satisfied(
196        control_id: ControlId,
197        rationale: impl Into<String>,
198        subjects: Vec<String>,
199    ) -> Self {
200        Self {
201            control_id,
202            status: ControlStatus::Satisfied,
203            rationale: rationale.into(),
204            subjects,
205            evidence_gaps: Vec::new(),
206        }
207    }
208
209    pub fn violated(
210        control_id: ControlId,
211        rationale: impl Into<String>,
212        subjects: Vec<String>,
213    ) -> Self {
214        Self {
215            control_id,
216            status: ControlStatus::Violated,
217            rationale: rationale.into(),
218            subjects,
219            evidence_gaps: Vec::new(),
220        }
221    }
222
223    pub fn indeterminate(
224        control_id: ControlId,
225        rationale: impl Into<String>,
226        subjects: Vec<String>,
227        evidence_gaps: Vec<EvidenceGap>,
228    ) -> Self {
229        Self {
230            control_id,
231            status: ControlStatus::Indeterminate,
232            rationale: rationale.into(),
233            subjects,
234            evidence_gaps,
235        }
236    }
237
238    pub fn not_applicable(control_id: ControlId, rationale: impl Into<String>) -> Self {
239        Self {
240            control_id,
241            status: ControlStatus::NotApplicable,
242            rationale: rationale.into(),
243            subjects: Vec::new(),
244            evidence_gaps: Vec::new(),
245        }
246    }
247
248    /// Extracts `RepositoryPosture` from evidence, returning appropriate
249    /// `Indeterminate` or `NotApplicable` findings for non-complete states.
250    ///
251    /// Use in posture controls to eliminate repeated `match` boilerplate:
252    /// ```ignore
253    /// let posture = match ControlFinding::extract_posture(self.id(), evidence) {
254    ///     Ok(p) => p,
255    ///     Err(findings) => return findings,
256    /// };
257    /// ```
258    pub fn extract_posture(
259        id: ControlId,
260        evidence: &EvidenceBundle,
261    ) -> Result<&RepositoryPosture, Vec<ControlFinding>> {
262        match &evidence.repository_posture {
263            EvidenceState::Complete { value } | EvidenceState::Partial { value, .. } => Ok(value),
264            EvidenceState::Missing { gaps } => Err(vec![ControlFinding::indeterminate(
265                id,
266                "Repository posture evidence could not be collected",
267                vec![],
268                gaps.clone(),
269            )]),
270            EvidenceState::NotApplicable => Err(vec![ControlFinding::not_applicable(
271                id,
272                "Repository posture not applicable",
273            )]),
274        }
275    }
276}
277
278/// A verifiable SDLC control that produces findings from evidence.
279pub trait Control: Send + Sync {
280    /// Returns the unique identifier for this control.
281    fn id(&self) -> ControlId;
282
283    /// Human-readable description for SARIF rule output.
284    fn description(&self) -> &'static str {
285        "Custom control"
286    }
287
288    /// SOC2 Trust Services Criteria this control maps to (e.g., &["CC6.1", "CC8.1"]).
289    /// Returns empty slice for controls not mapped to SOC2.
290    fn tsc_criteria(&self) -> &'static [&'static str] {
291        builtin_tsc_mapping(self.id().as_str())
292    }
293
294    /// Evaluates the evidence bundle and returns one finding per subject.
295    fn evaluate(&self, evidence: &EvidenceBundle) -> Vec<ControlFinding>;
296}
297
298/// Returns SOC2 Trust Services Criteria for a built-in control ID.
299pub fn builtin_tsc_mapping(id: &str) -> &'static [&'static str] {
300    match id {
301        // CC6: Logical and Physical Access Controls
302        builtin::SOURCE_AUTHENTICITY => &["CC6.1"],
303        builtin::BRANCH_PROTECTION_ENFORCEMENT => &["CC6.1", "CC8.1"],
304        builtin::CODEOWNERS_COVERAGE => &["CC6.1"],
305        builtin::SECRET_SCANNING => &["CC6.1", "CC6.6"],
306        // CC7: System Operations
307        builtin::ISSUE_LINKAGE => &["CC7.2"],
308        builtin::STALE_REVIEW => &["CC7.2"],
309        builtin::SECURITY_FILE_CHANGE => &["CC7.2"],
310        builtin::RELEASE_TRACEABILITY => &["CC7.2"],
311        builtin::REQUIRED_STATUS_CHECKS => &["CC7.1"],
312        builtin::VULNERABILITY_SCANNING => &["CC7.1"],
313        builtin::SECURITY_POLICY => &["CC7.3", "CC7.4"],
314        // CC8: Change Management
315        builtin::REVIEW_INDEPENDENCE => &["CC8.1"],
316        builtin::TWO_PARTY_REVIEW => &["CC8.1"],
317        builtin::CHANGE_REQUEST_SIZE => &["CC8.1"],
318        builtin::TEST_COVERAGE => &["CC8.1"],
319        builtin::SCOPED_CHANGE => &["CC8.1"],
320        builtin::DESCRIPTION_QUALITY => &["CC8.1"],
321        builtin::MERGE_COMMIT_POLICY => &["CC8.1"],
322        builtin::CONVENTIONAL_TITLE => &["CC8.1"],
323        builtin::BRANCH_HISTORY_INTEGRITY => &["CC8.1"],
324        // PI: Processing Integrity
325        builtin::BUILD_PROVENANCE => &["PI1.4"],
326        builtin::HOSTED_BUILD_PLATFORM => &["PI1.4"],
327        builtin::PROVENANCE_AUTHENTICITY => &["PI1.4"],
328        builtin::BUILD_ISOLATION => &["PI1.4"],
329        // Dependencies (CC7.1 + PI)
330        builtin::DEPENDENCY_SIGNATURE => &["CC7.1", "PI1.4"],
331        builtin::DEPENDENCY_PROVENANCE_CHECK => &["CC7.1", "PI1.4"],
332        builtin::DEPENDENCY_SIGNER_VERIFIED => &["CC7.1", "PI1.4"],
333        builtin::DEPENDENCY_COMPLETENESS => &["CC7.1", "PI1.4"],
334        // Enterprise Posture
335        builtin::SECRET_SCANNING_PUSH_PROTECTION => &["CC6.1", "CC6.6"],
336        builtin::BRANCH_PROTECTION_ADMIN_ENFORCEMENT => &["CC6.1", "CC8.1"],
337        builtin::DISMISS_STALE_REVIEWS_ON_PUSH => &["CC8.1"],
338        builtin::ACTIONS_PINNED_DEPENDENCIES => &["CC7.1", "PI1.4"],
339        builtin::ENVIRONMENT_PROTECTION_RULES => &["CC6.1", "CC8.1"],
340        builtin::CODE_SCANNING_ALERTS_RESOLVED => &["CC7.1"],
341        builtin::DEPENDENCY_LICENSE_COMPLIANCE => &["CC7.1"],
342        builtin::SBOM_ATTESTATION => &["CC7.1"],
343        builtin::RELEASE_ASSET_ATTESTATION => &["PI1.4"],
344        builtin::PRIVILEGED_WORKFLOW_DETECTION => &["CC6.1", "CC8.1"],
345        _ => &[],
346    }
347}
348
349/// Runs every control against the evidence bundle and collects all findings.
350pub fn evaluate_all(
351    controls: &[Box<dyn Control>],
352    evidence: &EvidenceBundle,
353) -> Vec<ControlFinding> {
354    let mut findings = Vec::new();
355    for control in controls {
356        findings.extend(control.evaluate(evidence));
357    }
358    findings
359}
360
361#[cfg(test)]
362mod tests {
363    use super::*;
364
365    #[test]
366    fn control_id_display() {
367        let id = ControlId::new("review-independence");
368        assert_eq!(id.to_string(), "review-independence");
369        assert_eq!(id.as_str(), "review-independence");
370    }
371
372    #[test]
373    fn control_id_from_str() {
374        let id: ControlId = "source-authenticity".into();
375        assert_eq!(id.as_str(), "source-authenticity");
376    }
377
378    #[test]
379    fn builtin_ids_are_unique() {
380        let mut seen = std::collections::HashSet::new();
381        for id in builtin::ALL {
382            assert!(seen.insert(id), "duplicate built-in ID: {id}");
383        }
384    }
385}