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