Skip to content

Commit 07293c7

Browse files
lcarvaclaude
andcommitted
fix: sort CVE records correctly
CVE records follow a specific format where the last segment represents a numerical sequence. To properly sort CVE records, we must treat this sequence segment differently than the rest of the record ID. fixes #1811 Co-Authored-By: Claude <[email protected]> Signed-off-by: Luiz Carvalho <[email protected]>
1 parent 2c357b0 commit 07293c7

File tree

2 files changed

+83
-1
lines changed

2 files changed

+83
-1
lines changed

modules/fundamental/src/vulnerability/service/mod.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use trustify_common::{
2828
model::{Paginated, PaginatedResults},
2929
purl::Purl,
3030
};
31+
use sea_query::Expr;
3132
use trustify_entity::{advisory, cvss3, vulnerability};
3233
use trustify_module_ingestor::common::Deprecation;
3334

@@ -47,7 +48,36 @@ impl VulnerabilityService {
4748
connection: &C,
4849
) -> Result<PaginatedResults<VulnerabilitySummary>, Error> {
4950
let limiter = vulnerability::Entity::find()
50-
.filtering_with(search, Columns::from_entity::<vulnerability::Entity>())?
51+
.filtering_with(
52+
search,
53+
Columns::from_entity::<vulnerability::Entity>().translator(|field, order, _value| {
54+
// When sorting by 'id', translate to use a normalized sort key
55+
// This ensures proper numeric sorting within CVE IDs while maintaining
56+
// alphabetical ordering between different prefixes (ABC-, CVE-, GHSA-, etc.)
57+
if field == "id" && (order == "asc" || order == "desc") {
58+
Some(format!("id_sort_key:{}", order))
59+
} else {
60+
None
61+
}
62+
})
63+
.add_expr(
64+
"id_sort_key",
65+
// Create a normalized sort key that preserves prefixes but sorts numbers numerically
66+
// For CVE IDs: converts "CVE-2024-288" to "CVE-2024-0000000000000000288"
67+
// - Year is always 4 digits, no padding needed
68+
// - Pad sequence to 19 digits (max length per CVE schema)
69+
// CVE schema: https://github.com/CVEProject/cve-schema/blob/main/schema/CVE_Record_Format.json
70+
// For other IDs: returns the ID as-is for alphabetical sorting
71+
// This ensures: ABC-123 < CVE-2023-9000 < CVE-2024-10000 < GHSA-xxx
72+
Expr::cust(
73+
"CASE WHEN id ~ '^CVE-[0-9]{4}-[0-9]{4,19}$' THEN \
74+
SUBSTRING(id FROM '^CVE-[0-9]{4}-') || \
75+
LPAD(SUBSTRING(id FROM '-([0-9]+)$'), 19, '0') \
76+
ELSE id END"
77+
),
78+
sea_orm::ColumnType::Text,
79+
),
80+
)?
5181
.limiting(connection, paginated.offset, paginated.limit);
5282

5383
let total = limiter.total().await?;

modules/fundamental/src/vulnerability/service/test.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,58 @@ async fn vulnerability_queries(ctx: &TrustifyContext) -> Result<(), anyhow::Erro
540540
Ok(())
541541
}
542542

543+
#[test_context(TrustifyContext)]
544+
#[test(tokio::test)]
545+
async fn vulnerability_numeric_sorting(ctx: &TrustifyContext) -> Result<(), anyhow::Error> {
546+
let service = VulnerabilityService::new();
547+
548+
ctx.graph.ingest_vulnerability("CVE-2024-40000", (), &ctx.db).await?;
549+
ctx.graph.ingest_vulnerability("CVE-2024-10288000", (), &ctx.db).await?;
550+
ctx.graph.ingest_vulnerability("CVE-2023-1234", (), &ctx.db).await?;
551+
ctx.graph.ingest_vulnerability("CVE-2024-9000", (), &ctx.db).await?;
552+
ctx.graph.ingest_vulnerability("CVE-2023-5100", (), &ctx.db).await?;
553+
ctx.graph.ingest_vulnerability("GHSA-xxxx-yyyy-zzzz", (), &ctx.db).await?;
554+
ctx.graph.ingest_vulnerability("ABC-xxxx-yyyy", (), &ctx.db).await?;
555+
556+
// Test ascending sort
557+
let vulns = service
558+
.fetch_vulnerabilities(
559+
q("").sort("id:asc"),
560+
Paginated::default(),
561+
Default::default(),
562+
&ctx.db,
563+
)
564+
.await?;
565+
assert_eq!(7, vulns.items.len());
566+
assert_eq!(vulns.items[0].head.identifier, "ABC-xxxx-yyyy");
567+
assert_eq!(vulns.items[1].head.identifier, "CVE-2023-1234");
568+
assert_eq!(vulns.items[2].head.identifier, "CVE-2023-5100");
569+
assert_eq!(vulns.items[3].head.identifier, "CVE-2024-9000");
570+
assert_eq!(vulns.items[4].head.identifier, "CVE-2024-40000");
571+
assert_eq!(vulns.items[5].head.identifier, "CVE-2024-10288000");
572+
assert_eq!(vulns.items[6].head.identifier, "GHSA-xxxx-yyyy-zzzz");
573+
574+
// Test descending sort
575+
let vulns = service
576+
.fetch_vulnerabilities(
577+
q("").sort("id:desc"),
578+
Paginated::default(),
579+
Default::default(),
580+
&ctx.db,
581+
)
582+
.await?;
583+
assert_eq!(7, vulns.items.len());
584+
assert_eq!(vulns.items[0].head.identifier, "GHSA-xxxx-yyyy-zzzz");
585+
assert_eq!(vulns.items[1].head.identifier, "CVE-2024-10288000");
586+
assert_eq!(vulns.items[2].head.identifier, "CVE-2024-40000");
587+
assert_eq!(vulns.items[3].head.identifier, "CVE-2024-9000");
588+
assert_eq!(vulns.items[4].head.identifier, "CVE-2023-5100");
589+
assert_eq!(vulns.items[5].head.identifier, "CVE-2023-1234");
590+
assert_eq!(vulns.items[6].head.identifier, "ABC-xxxx-yyyy");
591+
592+
Ok(())
593+
}
594+
543595
#[test_context(TrustifyContext)]
544596
#[test(tokio::test)]
545597
async fn analyze_purls(ctx: &TrustifyContext) -> Result<(), anyhow::Error> {

0 commit comments

Comments
 (0)