Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion modules/fundamental/src/vulnerability/service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,38 @@ impl VulnerabilityService {
connection: &C,
) -> Result<PaginatedResults<VulnerabilitySummary>, Error> {
let limiter = vulnerability::Entity::find()
.filtering_with(search, Columns::from_entity::<vulnerability::Entity>())?
.filtering_with(
search,
Columns::from_entity::<vulnerability::Entity>().translator(|field, order, _value| {
// When sorting by 'id', translate to use a normalized sort key
// This ensures proper numeric sorting within CVE IDs while maintaining
// alphabetical ordering between different prefixes (ABC-, CVE-, GHSA-, etc.)
if field == "id" && (order == "asc" || order == "desc") {
Some(format!("id_sort_key:{}", order))
} else {
None
}
})
.add_expr(
"id_sort_key",
// Pad numberic segments with zeros to achieve the expected numeric sorting.
// The padding is done into two steps. First add 19 zeros to each number
// segment. Second, keep only the 19 right-most digits for each number segment.
// This behaves like LPAD which cannot be used here because that would be
// evaluated before the REGEXP matching.
// The number 19 is used as that is the largest segment defined, coming from the
// CVE ID spec.
Expr::cust(
"REGEXP_REPLACE( \
REGEXP_REPLACE(id, '\\y([0-9]+)\\y', '0000000000000000000\\1', 'g'), \
'\\y([0-9]+)([0-9]{19})\\y', \
'\\2', \
'g' \
)"
),
sea_orm::ColumnType::Text,
),
)?
.limiting(connection, paginated.offset, paginated.limit);

let total = limiter.total().await?;
Expand Down
93 changes: 93 additions & 0 deletions modules/fundamental/src/vulnerability/service/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,99 @@ async fn vulnerability_queries(ctx: &TrustifyContext) -> Result<(), anyhow::Erro
Ok(())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider adding tests for edge cases such as malformed or non-standard CVE IDs.

Including malformed CVE IDs in tests will help verify that the sort key logic handles unexpected formats correctly.

}

#[test_context(TrustifyContext)]
#[test(tokio::test)]
async fn vulnerability_numeric_sorting(ctx: &TrustifyContext) -> Result<(), anyhow::Error> {
let service = VulnerabilityService::new();

// Test various OSV ID formats to ensure generic numeric sorting works
// CVE format
ctx.graph.ingest_vulnerability("CVE-2024-40000", (), &ctx.db).await?;
ctx.graph.ingest_vulnerability("CVE-2024-10288000", (), &ctx.db).await?;

// GHSA format (alphanumeric)
ctx.graph.ingest_vulnerability("GHSA-r9p9-mrjm-926w", (), &ctx.db).await?;
ctx.graph.ingest_vulnerability("GHSA-vp9c-fpxx-744v", (), &ctx.db).await?;

// Go format
ctx.graph.ingest_vulnerability("GO-2024-268", (), &ctx.db).await?;
ctx.graph.ingest_vulnerability("GO-2024-1234", (), &ctx.db).await?;

// RustSec format
ctx.graph.ingest_vulnerability("RUSTSEC-2019-0033", (), &ctx.db).await?;
ctx.graph.ingest_vulnerability("RUSTSEC-2024-0001", (), &ctx.db).await?;

// Alpine format
ctx.graph.ingest_vulnerability("ALPINE-12345", (), &ctx.db).await?;
ctx.graph.ingest_vulnerability("ALPINE-6789", (), &ctx.db).await?;

// PyPI format
ctx.graph.ingest_vulnerability("PYSEC-2021-1234", (), &ctx.db).await?;
ctx.graph.ingest_vulnerability("PYSEC-2024-5678", (), &ctx.db).await?;

// OSV format
ctx.graph.ingest_vulnerability("OSV-2020-111", (), &ctx.db).await?;
ctx.graph.ingest_vulnerability("OSV-2020-58", (), &ctx.db).await?;

// Generic test prefix
ctx.graph.ingest_vulnerability("ABC-xxxx-yyyy", (), &ctx.db).await?;

// Test ascending sort
let vulns = service
.fetch_vulnerabilities(
q("").sort("id:asc"),
Paginated::default(),
Default::default(),
&ctx.db,
)
.await?;
assert_eq!(15, vulns.items.len());
// Alphabetical by prefix, then numeric within each prefix
assert_eq!(vulns.items[0].head.identifier, "ABC-xxxx-yyyy");
assert_eq!(vulns.items[1].head.identifier, "ALPINE-6789");
assert_eq!(vulns.items[2].head.identifier, "ALPINE-12345");
assert_eq!(vulns.items[3].head.identifier, "CVE-2024-40000");
assert_eq!(vulns.items[4].head.identifier, "CVE-2024-10288000");
assert_eq!(vulns.items[5].head.identifier, "GHSA-r9p9-mrjm-926w");
assert_eq!(vulns.items[6].head.identifier, "GHSA-vp9c-fpxx-744v");
assert_eq!(vulns.items[7].head.identifier, "GO-2024-268");
assert_eq!(vulns.items[8].head.identifier, "GO-2024-1234");
assert_eq!(vulns.items[9].head.identifier, "OSV-2020-58");
assert_eq!(vulns.items[10].head.identifier, "OSV-2020-111");
assert_eq!(vulns.items[11].head.identifier, "PYSEC-2021-1234");
assert_eq!(vulns.items[12].head.identifier, "PYSEC-2024-5678");
assert_eq!(vulns.items[13].head.identifier, "RUSTSEC-2019-0033");
assert_eq!(vulns.items[14].head.identifier, "RUSTSEC-2024-0001");

// Test descending sort
let vulns = service
.fetch_vulnerabilities(
q("").sort("id:desc"),
Paginated::default(),
Default::default(),
&ctx.db,
)
.await?;
assert_eq!(15, vulns.items.len());
assert_eq!(vulns.items[0].head.identifier, "RUSTSEC-2024-0001");
assert_eq!(vulns.items[1].head.identifier, "RUSTSEC-2019-0033");
assert_eq!(vulns.items[2].head.identifier, "PYSEC-2024-5678");
assert_eq!(vulns.items[3].head.identifier, "PYSEC-2021-1234");
assert_eq!(vulns.items[4].head.identifier, "OSV-2020-111");
assert_eq!(vulns.items[5].head.identifier, "OSV-2020-58");
assert_eq!(vulns.items[6].head.identifier, "GO-2024-1234");
assert_eq!(vulns.items[7].head.identifier, "GO-2024-268");
assert_eq!(vulns.items[8].head.identifier, "GHSA-vp9c-fpxx-744v");
assert_eq!(vulns.items[9].head.identifier, "GHSA-r9p9-mrjm-926w");
assert_eq!(vulns.items[10].head.identifier, "CVE-2024-10288000");
assert_eq!(vulns.items[11].head.identifier, "CVE-2024-40000");
assert_eq!(vulns.items[12].head.identifier, "ALPINE-12345");
assert_eq!(vulns.items[13].head.identifier, "ALPINE-6789");
assert_eq!(vulns.items[14].head.identifier, "ABC-xxxx-yyyy");

Ok(())
}

#[test_context(TrustifyContext)]
#[test(tokio::test)]
async fn analyze_purls(ctx: &TrustifyContext) -> Result<(), anyhow::Error> {
Expand Down