Skip to main content

libverify_core/controls/
license_compliance.rs

1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::EvidenceBundle;
3
4/// Validates that no copyleft-licensed dependencies exist without explicit approval.
5///
6/// Maps to SOC2 CC7.1: monitor and evaluate system components.
7/// Copyleft licenses (GPL, AGPL, SSPL) impose distribution obligations
8/// that may conflict with proprietary licensing. This control flags
9/// copyleft dependencies for legal review.
10///
11/// Evaluation:
12/// - **Satisfied**: no copyleft dependencies detected
13/// - **Violated**: one or more copyleft dependencies found
14pub struct LicenseComplianceControl;
15
16impl Control for LicenseComplianceControl {
17    fn id(&self) -> ControlId {
18        builtin::id(builtin::LICENSE_COMPLIANCE)
19    }
20
21    fn description(&self) -> &'static str {
22        "Dependencies must not include copyleft licenses without explicit approval"
23    }
24
25    fn evaluate(&self, evidence: &EvidenceBundle) -> Vec<ControlFinding> {
26        let posture = match ControlFinding::extract_posture(self.id(), evidence) {
27            Ok(p) => p,
28            Err(findings) => return findings,
29        };
30
31        if posture.copyleft_dependencies.is_empty() {
32            return vec![ControlFinding::satisfied(
33                self.id(),
34                "No copyleft dependencies detected",
35                vec!["repository:license-compliance".to_string()],
36            )];
37        }
38
39        let subjects: Vec<String> = posture
40            .copyleft_dependencies
41            .iter()
42            .map(|dep| format!("{}:{}", dep.name, dep.license))
43            .collect();
44
45        let dep_list: Vec<String> = posture
46            .copyleft_dependencies
47            .iter()
48            .map(|dep| format!("{} ({})", dep.name, dep.license))
49            .collect();
50
51        vec![ControlFinding::violated(
52            self.id(),
53            format!("Copyleft dependencies detected: {}", dep_list.join(", ")),
54            subjects,
55        )]
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use crate::control::ControlStatus;
63    use crate::evidence::{CopyleftDependency, EvidenceGap, EvidenceState, RepositoryPosture};
64
65    fn bundle(state: EvidenceState<RepositoryPosture>) -> EvidenceBundle {
66        EvidenceBundle {
67            repository_posture: state,
68            ..Default::default()
69        }
70    }
71
72    #[test]
73    fn satisfied_when_no_copyleft_deps() {
74        let posture = RepositoryPosture::default();
75        let findings = LicenseComplianceControl.evaluate(&bundle(EvidenceState::complete(posture)));
76        assert_eq!(findings[0].status, ControlStatus::Satisfied);
77        assert!(findings[0].rationale.contains("No copyleft"));
78    }
79
80    #[test]
81    fn violated_when_copyleft_deps_exist() {
82        let posture = RepositoryPosture {
83            copyleft_dependencies: vec![
84                CopyleftDependency {
85                    name: "libfoo".to_string(),
86                    license: "GPL-3.0".to_string(),
87                },
88                CopyleftDependency {
89                    name: "libbar".to_string(),
90                    license: "AGPL-3.0".to_string(),
91                },
92            ],
93            ..Default::default()
94        };
95        let findings = LicenseComplianceControl.evaluate(&bundle(EvidenceState::complete(posture)));
96        assert_eq!(findings[0].status, ControlStatus::Violated);
97        assert!(findings[0].rationale.contains("libfoo"));
98        assert!(findings[0].rationale.contains("GPL-3.0"));
99        assert_eq!(findings[0].subjects.len(), 2);
100        assert!(findings[0].subjects[0].contains("libfoo:GPL-3.0"));
101    }
102
103    #[test]
104    fn indeterminate_when_posture_missing() {
105        let findings = LicenseComplianceControl.evaluate(&bundle(EvidenceState::missing(vec![
106            EvidenceGap::CollectionFailed {
107                source: "github".to_string(),
108                subject: "posture".to_string(),
109                detail: "API error".to_string(),
110            },
111        ])));
112        assert_eq!(findings[0].status, ControlStatus::Indeterminate);
113    }
114
115    #[test]
116    fn not_applicable_when_posture_not_applicable() {
117        let findings = LicenseComplianceControl.evaluate(&bundle(EvidenceState::not_applicable()));
118        assert_eq!(findings[0].status, ControlStatus::NotApplicable);
119    }
120}