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::{CopyleftDependency, EvidenceGap, EvidenceState, RepositoryPosture};
62
63    fn posture(deps: Vec<CopyleftDependency>) -> RepositoryPosture {
64        RepositoryPosture {
65            copyleft_dependencies: deps,
66            ..Default::default()
67        }
68    }
69
70    fn bundle(state: EvidenceState<RepositoryPosture>) -> EvidenceBundle {
71        EvidenceBundle {
72            repository_posture: state,
73            ..Default::default()
74        }
75    }
76
77    #[test]
78    fn not_applicable_when_posture_not_applicable() {
79        let findings =
80            DependencyLicenseComplianceControl.evaluate(&bundle(EvidenceState::not_applicable()));
81        assert_eq!(findings[0].status, ControlStatus::NotApplicable);
82    }
83
84    #[test]
85    fn indeterminate_when_posture_missing() {
86        let findings =
87            DependencyLicenseComplianceControl.evaluate(&bundle(EvidenceState::missing(vec![
88                EvidenceGap::CollectionFailed {
89                    source: "github".to_string(),
90                    subject: "posture".to_string(),
91                    detail: "API error".to_string(),
92                },
93            ])));
94        assert_eq!(findings[0].status, ControlStatus::Indeterminate);
95    }
96
97    #[test]
98    fn satisfied_when_no_copyleft_deps() {
99        let findings = DependencyLicenseComplianceControl
100            .evaluate(&bundle(EvidenceState::complete(posture(vec![]))));
101        assert_eq!(findings[0].status, ControlStatus::Satisfied);
102        assert!(findings[0].rationale.contains("No copyleft"));
103    }
104
105    #[test]
106    fn violated_when_copyleft_deps_exist() {
107        let deps = vec![
108            CopyleftDependency {
109                name: "libfoo".to_string(),
110                license: "GPL-3.0".to_string(),
111            },
112            CopyleftDependency {
113                name: "libbar".to_string(),
114                license: "AGPL-3.0".to_string(),
115            },
116        ];
117        let findings = DependencyLicenseComplianceControl
118            .evaluate(&bundle(EvidenceState::complete(posture(deps))));
119        assert_eq!(findings[0].status, ControlStatus::Violated);
120        assert!(findings[0].rationale.contains("2 copyleft"));
121        assert_eq!(findings[0].subjects.len(), 2);
122        assert!(findings[0].subjects[0].contains("libfoo:GPL-3.0"));
123        assert!(findings[0].subjects[1].contains("libbar:AGPL-3.0"));
124    }
125}