Skip to main content

libverify_core/controls/
dependency_license_compliance.rs

1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::EvidenceBundle;
3
4/// Validates that no copyleft-licensed dependencies (GPL, AGPL) are present.
5///
6/// Maps to SOC2 CC7.1: ensure compliance with third-party license obligations.
7/// Copyleft licenses impose viral obligations that may conflict with proprietary
8/// licensing or organizational policy. This control flags dependencies with
9/// GPL, AGPL, or similar copyleft licenses for legal review.
10///
11/// Evaluation:
12/// - **Satisfied**: no copyleft-licensed dependencies detected
13/// - **Violated**: one or more copyleft-licensed dependencies found
14pub struct DependencyLicenseComplianceControl;
15
16impl Control for DependencyLicenseComplianceControl {
17    fn id(&self) -> ControlId {
18        builtin::id(builtin::DEPENDENCY_LICENSE_COMPLIANCE)
19    }
20
21    fn description(&self) -> &'static str {
22        "Dependencies must not include copyleft-licensed (GPL/AGPL) packages without review"
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-licensed dependencies detected in the dependency graph",
35                vec!["repository:licenses:compliant".to_string()],
36            )];
37        }
38
39        let subjects: Vec<String> = posture
40            .copyleft_dependencies
41            .iter()
42            .map(|d| format!("{}:{}", d.name, d.license))
43            .collect();
44
45        let count = posture.copyleft_dependencies.len();
46        vec![ControlFinding::violated(
47            self.id(),
48            format!(
49                "{count} copyleft-licensed dependency(ies) detected — \
50                 review for license compliance before distribution"
51            ),
52            subjects,
53        )]
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60    use crate::control::ControlStatus;
61    use crate::evidence::{
62        CopyleftDependency, EvidenceGap, EvidenceState, RepositoryPosture,
63    };
64
65    fn posture(deps: Vec<CopyleftDependency>) -> RepositoryPosture {
66        RepositoryPosture {
67            copyleft_dependencies: deps,
68            ..Default::default()
69        }
70    }
71
72    fn bundle(state: EvidenceState<RepositoryPosture>) -> EvidenceBundle {
73        EvidenceBundle {
74            repository_posture: state,
75            ..Default::default()
76        }
77    }
78
79    #[test]
80    fn not_applicable_when_posture_not_applicable() {
81        let findings = DependencyLicenseComplianceControl
82            .evaluate(&bundle(EvidenceState::not_applicable()));
83        assert_eq!(findings[0].status, ControlStatus::NotApplicable);
84    }
85
86    #[test]
87    fn indeterminate_when_posture_missing() {
88        let findings =
89            DependencyLicenseComplianceControl.evaluate(&bundle(EvidenceState::missing(vec![
90                EvidenceGap::CollectionFailed {
91                    source: "github".to_string(),
92                    subject: "posture".to_string(),
93                    detail: "API error".to_string(),
94                },
95            ])));
96        assert_eq!(findings[0].status, ControlStatus::Indeterminate);
97    }
98
99    #[test]
100    fn satisfied_when_no_copyleft_deps() {
101        let findings = DependencyLicenseComplianceControl
102            .evaluate(&bundle(EvidenceState::complete(posture(vec![]))));
103        assert_eq!(findings[0].status, ControlStatus::Satisfied);
104        assert!(findings[0].rationale.contains("No copyleft"));
105    }
106
107    #[test]
108    fn violated_when_copyleft_deps_exist() {
109        let deps = vec![
110            CopyleftDependency {
111                name: "libfoo".to_string(),
112                license: "GPL-3.0".to_string(),
113            },
114            CopyleftDependency {
115                name: "libbar".to_string(),
116                license: "AGPL-3.0".to_string(),
117            },
118        ];
119        let findings = DependencyLicenseComplianceControl
120            .evaluate(&bundle(EvidenceState::complete(posture(deps))));
121        assert_eq!(findings[0].status, ControlStatus::Violated);
122        assert!(findings[0].rationale.contains("2 copyleft"));
123        assert_eq!(findings[0].subjects.len(), 2);
124        assert!(findings[0].subjects[0].contains("libfoo:GPL-3.0"));
125        assert!(findings[0].subjects[1].contains("libbar:AGPL-3.0"));
126    }
127}