Skip to content

Commit 79c8155

Browse files
authored
feat(domain): Use String for Package Versions to Support Non-SemVer Formats (#27)
The `semver` crate is strict and cannot parse many package versions found in the wild, such as those from Debian (e.g., `1.1.35-1.2+deb13u2`) or other non-standard formats (e.g., `31.0-api`, `32.0.0-android`). This caused panics when the application encountered these versions. This PR resolves the issue by: 1. Replacing the `semver::Version` type with a simple `String` for storing package versions. 2. Introducing the `version-compare` crate, which is more flexible and capable of comparing different versioning schemes. 3. Expanding the test suite to validate the correct behavior for a wide range of version formats, ensuring the suggested fix logic is robust.
1 parent 5b21dd2 commit 79c8155

File tree

9 files changed

+77
-48
lines changed

9 files changed

+77
-48
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ repos:
44
hooks:
55
- id: fmt
66
- id: cargo-check
7+
always_run: true
78
- id: clippy
89

910
- repo: https://github.com/pre-commit/pre-commit-hooks

Cargo.lock

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sysdig-lsp"
3-
version = "0.7.3"
3+
version = "0.7.4"
44
edition = "2024"
55
authors = [ "Sysdig Inc." ]
66
readme = "README.md"
@@ -33,6 +33,7 @@ tokio = { version = "1.43.0", features = ["full"] }
3333
tower-lsp = "0.20.0"
3434
tracing = "0.1.41"
3535
tracing-subscriber = "0.3.19"
36+
version-compare = "0.2.0"
3637

3738
[dev-dependencies]
3839
rstest = "0.21.0"

Justfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ test:
44

55
fix:
66
cargo fix --allow-staged --allow-dirty
7+
cargo machete --fix
78

89
fmt:
910
cargo fmt

src/domain/scanresult/package.rs

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::domain::scanresult::package_type::PackageType;
44
use crate::domain::scanresult::severity::Severity;
55
use crate::domain::scanresult::vulnerability::Vulnerability;
66
use crate::domain::scanresult::weak_hash::WeakHash;
7-
use semver::Version;
7+
use std::cmp::Ordering;
88
use std::collections::{HashMap, HashSet};
99
use std::fmt::Debug;
1010
use std::hash::{Hash, Hasher};
@@ -13,7 +13,7 @@ use std::sync::{Arc, RwLock};
1313
pub struct Package {
1414
package_type: PackageType,
1515
name: String,
16-
version: Version,
16+
version: String,
1717
path: String,
1818
found_in_layer: Arc<Layer>,
1919
vulnerabilities: RwLock<HashSet<WeakHash<Vulnerability>>>,
@@ -36,7 +36,7 @@ impl Package {
3636
pub(in crate::domain::scanresult) fn new(
3737
package_type: PackageType,
3838
name: String,
39-
version: Version,
39+
version: String,
4040
path: String,
4141
found_in_layer: Arc<Layer>,
4242
) -> Self {
@@ -59,7 +59,7 @@ impl Package {
5959
&self.name
6060
}
6161

62-
pub fn version(&self) -> &Version {
62+
pub fn version(&self) -> &String {
6363
&self.version
6464
}
6565

@@ -111,13 +111,13 @@ impl Package {
111111
.collect()
112112
}
113113

114-
pub fn suggested_fix_version(&self) -> Option<Version> {
114+
pub fn suggested_fix_version(&self) -> Option<String> {
115115
let vulnerabilities = self.vulnerabilities();
116116
if vulnerabilities.is_empty() {
117117
return None;
118118
}
119119

120-
let candidate_versions: Vec<Version> = vulnerabilities
120+
let candidate_versions: Vec<String> = vulnerabilities
121121
.iter()
122122
.filter_map(|vuln| vuln.fix_version().cloned())
123123
.collect::<HashSet<_>>()
@@ -137,7 +137,7 @@ impl Package {
137137
Severity::Unknown,
138138
];
139139

140-
let mut scores: HashMap<Version, HashMap<Severity, usize>> = HashMap::new();
140+
let mut scores: HashMap<String, HashMap<Severity, usize>> = HashMap::new();
141141

142142
for candidate in &candidate_versions {
143143
let mut score: HashMap<Severity, usize> = HashMap::new();
@@ -168,7 +168,16 @@ impl Package {
168168
}
169169

170170
// If scores are identical, lower version is better
171-
a.cmp(b)
171+
if version_compare::compare_to(a, b, version_compare::Cmp::Eq).unwrap_or(false) {
172+
return Ordering::Equal;
173+
}
174+
if version_compare::compare_to(a, b, version_compare::Cmp::Le).unwrap_or(false) {
175+
return Ordering::Less;
176+
}
177+
if version_compare::compare_to(a, b, version_compare::Cmp::Ge).unwrap_or(false) {
178+
return Ordering::Greater;
179+
}
180+
Ordering::Less
172181
});
173182

174183
sorted_candidates.first().cloned()
@@ -218,7 +227,6 @@ mod tests {
218227
use crate::domain::scanresult::vulnerability::Vulnerability;
219228
use chrono::NaiveDate;
220229
use rstest::{fixture, rstest};
221-
use semver::Version;
222230
use std::sync::Arc;
223231

224232
#[fixture]
@@ -236,7 +244,7 @@ mod tests {
236244
Arc::new(Package::new(
237245
PackageType::Os,
238246
"a_name".to_string(),
239-
Version::parse(version).unwrap(),
247+
version.to_string(),
240248
"a_path".to_string(),
241249
layer,
242250
))
@@ -253,7 +261,7 @@ mod tests {
253261
NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(),
254262
None,
255263
false,
256-
fix_version.map(|v| Version::parse(v).unwrap()),
264+
fix_version.map(|v| v.to_string()),
257265
))
258266
}
259267

@@ -293,20 +301,41 @@ mod tests {
293301
a_vulnerability("CVE-2022-41724", Severity::Medium, Some("2.9.0")),
294302
a_vulnerability("CVE-2022-41725", Severity::Medium, Some("2.9.0")),
295303
], Some("2.9.0"))]
304+
#[case("handles_debian_version", "1.1.35-1.2+deb13u2", vec![
305+
a_vulnerability("CVE-1", Severity::High, Some("1.1.35-1.2+deb13u3")),
306+
a_vulnerability("CVE-2", Severity::High, Some("1.1.35-1.3")),
307+
], Some("1.1.35-1.2+deb13u3"))]
308+
#[case("chooses_lower_version_with_debian_tilde", "257.8-1~deb13u1", vec![
309+
a_vulnerability("CVE-1", Severity::High, Some("257.8-1~deb13u2")),
310+
a_vulnerability("CVE-2", Severity::High, Some("257.8-1~deb13u3")),
311+
], Some("257.8-1~deb13u2"))]
312+
#[case("handles_jre_and_android_versions", "31.1-jre", vec![
313+
a_vulnerability("CVE-1", Severity::High, Some("32.0.0-android")),
314+
], Some("32.0.0-android"))]
315+
#[case("handles_api_version", "31.0-api", vec![a_vulnerability("CVE-1", Severity::High, Some("31.1-api"))], Some("31.1-api"))]
316+
#[case("handles_build_metadata_version", "1.0.15-1+b3", vec![a_vulnerability("CVE-1", Severity::High, Some("1.0.15-2"))], Some("1.0.15-2"))]
317+
#[case("handles_simple_float_version", "2.6", vec![a_vulnerability("CVE-1", Severity::High, Some("2.7"))], Some("2.7"))]
318+
#[case("handles_revision_version", "1.7.0-5", vec![a_vulnerability("CVE-1", Severity::High, Some("1.7.0-6"))], Some("1.7.0-6"))]
319+
#[case("handles_date_based_version", "6.5+20250216-2", vec![a_vulnerability("CVE-1", Severity::High, Some("6.5+20250216-3"))], Some("6.5+20250216-3"))]
320+
#[case("handles_jenkins_version", "3107.v665000b_51092", vec![a_vulnerability("CVE-1", Severity::High, Some("3107.v665000b_51093"))], Some("3107.v665000b_51093"))]
321+
#[case("handles_dot_separated_version", "3206.3208", vec![a_vulnerability("CVE-1", Severity::High, Some("3206.3209"))], Some("3206.3209"))]
322+
#[case("handles_complex_debian_version", "2.12.7+dfsg+really2.9.14-2.1+deb13u1", vec![a_vulnerability("CVE-1", Severity::High, Some("2.12.7+dfsg+really2.9.14-2.1+deb13u2"))], Some("2.12.7+dfsg+really2.9.14-2.1+deb13u2"))]
296323
fn test_suggested_fix_version(
297324
#[case] _description: &str,
298325
#[case] version: &str,
299326
#[with(version)] package: Arc<Package>,
300327
#[case] vulnerabilities: Vec<Arc<Vulnerability>>,
301328
#[case] expected_fix: Option<&str>,
302329
) {
303-
assert_eq!(package.version(), &Version::parse(version).unwrap());
330+
assert_eq!(package.version(), &version);
304331

305332
for vuln in &vulnerabilities {
306333
package.add_vulnerability_found(vuln.clone());
307334
}
308335

309-
let expected = expected_fix.map(|v| Version::parse(v).unwrap());
310-
assert_eq!(package.suggested_fix_version(), expected);
336+
assert_eq!(
337+
package.suggested_fix_version(),
338+
expected_fix.map(|x| x.to_string())
339+
);
311340
}
312341
}

src/domain/scanresult/scan_result.rs

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ use crate::domain::scanresult::severity::Severity;
1414
use crate::domain::scanresult::vulnerability::Vulnerability;
1515
use chrono::{DateTime, NaiveDate, Utc};
1616
use itertools::Itertools;
17-
use semver::Version;
1817
use std::collections::HashMap;
1918
use std::sync::Arc;
2019

@@ -110,7 +109,7 @@ impl ScanResult {
110109
&mut self,
111110
package_type: PackageType,
112111
name: String,
113-
version: Version,
112+
version: String,
114113
path: String,
115114
found_in_layer: Arc<Layer>,
116115
) -> Arc<Package> {
@@ -141,7 +140,7 @@ impl ScanResult {
141140
disclosure_date: NaiveDate,
142141
solution_date: Option<NaiveDate>,
143142
exploitable: bool,
144-
fix_version: Option<Version>,
143+
fix_version: Option<String>,
145144
) -> Arc<Vulnerability> {
146145
self.vulnerabilities
147146
.entry(cve.clone())
@@ -314,7 +313,7 @@ mod tests {
314313
let package = scan_result.add_package(
315314
PackageType::Os,
316315
"musl".to_string(),
317-
Version::parse("1.2.3").unwrap(),
316+
"1.2.3".to_string(),
318317
"/lib/ld-musl-x86_64.so.1".to_string(),
319318
layer.clone(),
320319
);
@@ -337,7 +336,7 @@ mod tests {
337336
Utc::now().naive_utc().date(),
338337
None,
339338
false,
340-
Some(Version::parse("1.2.4").unwrap()),
339+
Some("1.2.4".to_string()),
341340
);
342341

343342
assert_eq!(scan_result.vulnerabilities().len(), 1);
@@ -358,7 +357,7 @@ mod tests {
358357
let package = scan_result.add_package(
359358
PackageType::Os,
360359
"musl".to_string(),
361-
Version::parse("1.2.3").unwrap(),
360+
"1.2.3".to_string(),
362361
"/lib/ld-musl-x86_64.so.1".to_string(),
363362
layer.clone(),
364363
);
@@ -368,7 +367,7 @@ mod tests {
368367
Utc::now().naive_utc().date(),
369368
None,
370369
false,
371-
Some(Version::parse("1.2.4").unwrap()),
370+
Some("1.2.4".to_string()),
372371
);
373372

374373
package.add_vulnerability_found(vuln.clone());
@@ -463,7 +462,7 @@ mod tests {
463462
Utc::now().naive_utc().date(),
464463
None,
465464
false,
466-
Some(Version::parse("1.2.4").unwrap()),
465+
Some("1.2.4".to_string()),
467466
);
468467

469468
vuln.add_accepted_risk(risk.clone());
@@ -489,7 +488,7 @@ mod tests {
489488
let package = scan_result.add_package(
490489
PackageType::Os,
491490
"musl".to_string(),
492-
Version::parse("1.2.3").unwrap(),
491+
"1.2.3".to_string(),
493492
"/lib/ld-musl-x86_64.so.1".to_string(),
494493
layer.clone(),
495494
);
@@ -572,13 +571,13 @@ mod tests {
572571
let package = scan_result.add_package(
573572
PackageType::Os,
574573
"musl".to_string(),
575-
Version::parse("1.2.3").unwrap(),
574+
"1.2.3".to_string(),
576575
"/path".to_string(),
577576
layer.clone(),
578577
);
579578
assert_eq!(package.package_type(), &PackageType::Os);
580579
assert_eq!(package.name(), "musl");
581-
assert_eq!(package.version(), &Version::parse("1.2.3").unwrap());
580+
assert_eq!(package.version(), "1.2.3");
582581
assert_eq!(package.path(), "/path");
583582
assert!(format!("{:?}", package).contains("musl"));
584583
assert_eq!(package.clone(), package);
@@ -590,15 +589,15 @@ mod tests {
590589
now.naive_utc().date(),
591590
Some(now.naive_utc().date()),
592591
true,
593-
Some(Version::parse("1.2.4").unwrap()),
592+
Some("1.2.4".to_string()),
594593
);
595594
assert_eq!(vuln.cve(), "CVE-1");
596595
assert_eq!(vuln.severity(), Severity::High);
597596
assert_eq!(vuln.disclosure_date(), now.naive_utc().date());
598597
assert_eq!(vuln.solution_date(), Some(now.naive_utc().date()));
599598
assert!(vuln.exploitable());
600599
assert!(vuln.fixable());
601-
assert_eq!(vuln.fix_version(), Some(&Version::parse("1.2.4").unwrap()));
600+
assert_eq!(vuln.fix_version(), Some(&"1.2.4".to_string()));
602601
assert!(format!("{:?}", vuln).contains("CVE-1"));
603602

604603
// AcceptedRisk
@@ -673,14 +672,14 @@ mod tests {
673672
let pkg = scan_result.add_package(
674673
PackageType::Os,
675674
"pkg".to_string(),
676-
Version::parse("1.0.0").unwrap(),
675+
"1.0.0".to_string(),
677676
"/path".to_string(),
678677
layer.clone(),
679678
);
680679
let pkg2 = scan_result.add_package(
681680
PackageType::Os,
682681
"pkg".to_string(),
683-
Version::parse("1.0.0").unwrap(),
682+
"1.0.0".to_string(),
684683
"/path".to_string(),
685684
layer.clone(),
686685
);

src/domain/scanresult/vulnerability.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ use crate::domain::scanresult::package::Package;
44
use crate::domain::scanresult::severity::Severity;
55
use crate::domain::scanresult::weak_hash::WeakHash;
66
use chrono::NaiveDate;
7-
use semver::Version;
87
use std::collections::HashSet;
98
use std::fmt::Debug;
109
use std::hash::{Hash, Hasher};
@@ -16,7 +15,7 @@ pub struct Vulnerability {
1615
disclosure_date: NaiveDate,
1716
solution_date: Option<NaiveDate>,
1817
exploitable: bool,
19-
fix_version: Option<Version>,
18+
fix_version: Option<String>,
2019
found_in_packages: RwLock<HashSet<WeakHash<Package>>>,
2120
accepted_risks: RwLock<HashSet<WeakHash<AcceptedRisk>>>,
2221
}
@@ -41,7 +40,7 @@ impl Vulnerability {
4140
disclosure_date: NaiveDate,
4241
solution_date: Option<NaiveDate>,
4342
exploitable: bool,
44-
fix_version: Option<Version>,
43+
fix_version: Option<String>,
4544
) -> Self {
4645
Self {
4746
cve,
@@ -79,7 +78,7 @@ impl Vulnerability {
7978
self.fix_version.is_some()
8079
}
8180

82-
pub fn fix_version(&self) -> Option<&Version> {
81+
pub fn fix_version(&self) -> Option<&String> {
8382
self.fix_version.as_ref()
8483
}
8584

0 commit comments

Comments
 (0)