libverify_core/controls/
dependency_license_compliance.rs1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::EvidenceBundle;
3
4pub 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}