gem_audit/fixer/
version_resolver.rs1use crate::scanner::Remediation;
2use crate::version::Version;
3
4#[derive(Debug, Clone)]
6pub struct FixSuggestion {
7 pub name: String,
8 pub current_version: String,
9 pub resolved_version: Version,
10 pub advisory_ids: Vec<String>,
11}
12
13#[derive(Debug)]
15pub enum FixResult {
16 Fixed(FixSuggestion),
18 Unresolvable {
20 name: String,
21 current_version: String,
22 advisory_ids: Vec<String>,
23 },
24}
25
26impl FixResult {
27 pub fn name(&self) -> &str {
28 match self {
29 FixResult::Fixed(f) => &f.name,
30 FixResult::Unresolvable { name, .. } => name,
31 }
32 }
33}
34
35pub fn resolve_fixes(remediations: &[Remediation]) -> Vec<FixResult> {
43 remediations.iter().map(resolve_single).collect()
44}
45
46fn resolve_single(remediation: &Remediation) -> FixResult {
47 let advisory_ids: Vec<String> = remediation
48 .advisories
49 .iter()
50 .flat_map(|a| a.identifiers())
51 .collect();
52
53 let mut candidates: Vec<Version> = Vec::new();
56 for adv in &remediation.advisories {
57 for req in &adv.patched_versions {
58 if let Some(v) = req.minimum_version() {
59 candidates.push(v);
60 }
61 }
62 }
63
64 if candidates.is_empty() {
65 return FixResult::Unresolvable {
66 name: remediation.name.clone(),
67 current_version: remediation.version.clone(),
68 advisory_ids,
69 };
70 }
71
72 candidates.sort();
74 candidates.dedup();
75
76 for candidate in &candidates {
78 if all_advisories_patched(remediation, candidate) {
79 return FixResult::Fixed(FixSuggestion {
80 name: remediation.name.clone(),
81 current_version: remediation.version.clone(),
82 resolved_version: candidate.clone(),
83 advisory_ids,
84 });
85 }
86 }
87
88 FixResult::Unresolvable {
91 name: remediation.name.clone(),
92 current_version: remediation.version.clone(),
93 advisory_ids,
94 }
95}
96
97fn all_advisories_patched(remediation: &Remediation, version: &Version) -> bool {
99 remediation
100 .advisories
101 .iter()
102 .all(|adv| adv.patched(version))
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::advisory::Advisory;
109 use std::path::Path;
110
111 fn make_advisory(yaml: &str) -> Advisory {
112 Advisory::from_yaml(yaml, Path::new("test.yml")).unwrap()
113 }
114
115 fn make_remediation(name: &str, version: &str, advisories: Vec<Advisory>) -> Remediation {
116 Remediation {
117 name: name.to_string(),
118 version: version.to_string(),
119 advisories,
120 }
121 }
122
123 #[test]
124 fn single_advisory_gte() {
125 let adv =
126 make_advisory("---\ngem: test\ncve: 2020-0001\npatched_versions:\n - \">= 1.0.0\"\n");
127 let rem = make_remediation("test", "0.5.0", vec![adv]);
128 let results = resolve_fixes(&[rem]);
129 assert_eq!(results.len(), 1);
130 match &results[0] {
131 FixResult::Fixed(f) => {
132 assert_eq!(f.resolved_version, Version::parse("1.0.0").unwrap());
133 }
134 _ => panic!("expected Fixed"),
135 }
136 }
137
138 #[test]
139 fn single_advisory_pessimistic() {
140 let adv =
141 make_advisory("---\ngem: test\ncve: 2020-0001\npatched_versions:\n - \"~> 1.18.7\"\n");
142 let rem = make_remediation("test", "1.18.0", vec![adv]);
143 let results = resolve_fixes(&[rem]);
144 match &results[0] {
145 FixResult::Fixed(f) => {
146 assert_eq!(f.resolved_version, Version::parse("1.18.7").unwrap());
147 }
148 _ => panic!("expected Fixed"),
149 }
150 }
151
152 #[test]
153 fn single_advisory_gt() {
154 let adv =
155 make_advisory("---\ngem: test\ncve: 2020-0001\npatched_versions:\n - \"> 1.0.0\"\n");
156 let rem = make_remediation("test", "0.5.0", vec![adv]);
157 let results = resolve_fixes(&[rem]);
158 match &results[0] {
159 FixResult::Fixed(f) => {
160 assert_eq!(f.resolved_version, Version::parse("1.0.1").unwrap());
161 }
162 _ => panic!("expected Fixed"),
163 }
164 }
165
166 #[test]
167 fn multiple_patched_versions_or_picks_smallest() {
168 let adv = make_advisory(
170 "---\ngem: test\ncve: 2020-0001\npatched_versions:\n - \"~> 1.18.7\"\n - \">= 1.19.1\"\n",
171 );
172 let rem = make_remediation("test", "1.18.0", vec![adv]);
173 let results = resolve_fixes(&[rem]);
174 match &results[0] {
175 FixResult::Fixed(f) => {
176 assert_eq!(f.resolved_version, Version::parse("1.18.7").unwrap());
177 }
178 _ => panic!("expected Fixed"),
179 }
180 }
181
182 #[test]
183 fn two_advisories_and_takes_intersection() {
184 let adv1 =
188 make_advisory("---\ngem: test\ncve: 2020-0001\npatched_versions:\n - \">= 1.5\"\n");
189 let adv2 =
190 make_advisory("---\ngem: test\ncve: 2020-0002\npatched_versions:\n - \">= 2.0\"\n");
191 let rem = make_remediation("test", "1.0.0", vec![adv1, adv2]);
192 let results = resolve_fixes(&[rem]);
193 match &results[0] {
194 FixResult::Fixed(f) => {
195 assert_eq!(f.resolved_version, Version::parse("2.0").unwrap());
196 }
197 _ => panic!("expected Fixed"),
198 }
199 }
200
201 #[test]
202 fn no_patched_versions_is_unresolvable() {
203 let adv = make_advisory("---\ngem: test\ncve: 2020-0001\npatched_versions: []\n");
204 let rem = make_remediation("test", "1.0.0", vec![adv]);
205 let results = resolve_fixes(&[rem]);
206 assert!(matches!(&results[0], FixResult::Unresolvable { .. }));
207 }
208
209 #[test]
210 fn empty_remediations() {
211 let results = resolve_fixes(&[]);
212 assert!(results.is_empty());
213 }
214
215 #[test]
216 fn complex_multi_advisory_multi_patched() {
217 let adv1 = make_advisory(
223 "---\ngem: test\ncve: 2020-0001\npatched_versions:\n - \"~> 4.2.5\"\n - \">= 5.0.0\"\n",
224 );
225 let adv2 =
226 make_advisory("---\ngem: test\ncve: 2020-0002\npatched_versions:\n - \">= 5.0.1\"\n");
227 let rem = make_remediation("test", "4.2.0", vec![adv1, adv2]);
228 let results = resolve_fixes(&[rem]);
229 match &results[0] {
230 FixResult::Fixed(f) => {
231 assert_eq!(f.resolved_version, Version::parse("5.0.1").unwrap());
232 }
233 _ => panic!("expected Fixed"),
234 }
235 }
236}