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::{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}