use crate::diagnostic::Severity;
use super::types::{OsvAffected, OsvEcosystem, OsvPackageName, VulnSeverity};
fn parse_severity_label(s: &str) -> Option<VulnSeverity> {
match s.to_ascii_uppercase().as_str() {
"CRITICAL" => Some(VulnSeverity::Critical),
"HIGH" => Some(VulnSeverity::High),
"MODERATE" | "MEDIUM" => Some(VulnSeverity::Medium),
"LOW" => Some(VulnSeverity::Low),
_ => None,
}
}
fn severity_from_json(value: &serde_json::Value) -> Option<VulnSeverity> {
value
.get("severity")?
.as_str()
.and_then(parse_severity_label)
}
fn is_informational_value(value: &serde_json::Value) -> bool {
value
.get("informational")
.and_then(serde_json::Value::as_str)
.is_some_and(|s| s.trim() == "unmaintained")
}
pub(super) fn classify(
id: &str,
aliases: &[String],
database_specific: Option<&serde_json::Value>,
relevant_affected: &[&OsvAffected],
osv_name: &OsvPackageName,
osv_eco: OsvEcosystem,
) -> VulnSeverity {
if id.starts_with("MAL-") || aliases.iter().any(|alias| alias.starts_with("MAL-")) {
return VulnSeverity::Malicious;
}
if let Some(v) = database_specific.and_then(severity_from_json) {
return v;
}
for affected in relevant_affected {
if let Some(v) = affected
.ecosystem_specific
.as_ref()
.and_then(severity_from_json)
{
return v;
}
}
for affected in relevant_affected {
let genuinely_this_package = affected
.package
.as_ref()
.is_some_and(|p| osv_name == p.name.as_str() && p.ecosystem == osv_eco.as_str());
if genuinely_this_package
&& affected
.database_specific
.as_ref()
.is_some_and(is_informational_value)
{
return VulnSeverity::Informational;
}
}
VulnSeverity::Unknown
}
#[must_use]
pub const fn to_diagnostic_severity(severity: VulnSeverity) -> Severity {
match severity {
VulnSeverity::Critical
| VulnSeverity::High
| VulnSeverity::Unknown
| VulnSeverity::Malicious => Severity::Warning,
VulnSeverity::Medium | VulnSeverity::Low | VulnSeverity::Informational => {
Severity::Information
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::osv::types::{OsvAffected, OsvPackage};
const PKG_NAME: &str = "yaml-rust";
fn pkg_name() -> OsvPackageName {
OsvPackageName::new(PKG_NAME).unwrap()
}
const PKG_ECO: &str = "crates.io";
const PKG_OSV_ECO: OsvEcosystem = OsvEcosystem::CratesIo;
#[test]
fn database_specific_severity_wins() {
let json = serde_json::json!({ "severity": "CRITICAL" });
assert_eq!(
classify(
"RUSTSEC-2020-0071",
&[],
Some(&json),
&[],
&pkg_name(),
PKG_OSV_ECO
),
VulnSeverity::Critical
);
}
#[test]
fn database_specific_moderate_maps_to_medium() {
let json = serde_json::json!({ "severity": "MODERATE" });
assert_eq!(
classify(
"RUSTSEC-2020-0071",
&[],
Some(&json),
&[],
&pkg_name(),
PKG_OSV_ECO
),
VulnSeverity::Medium
);
}
#[test]
fn falls_back_to_ecosystem_specific_severity() {
let affected = OsvAffected {
versions: vec![],
package: None,
ecosystem_specific: Some(serde_json::json!({ "severity": "LOW" })),
database_specific: None,
ranges: vec![],
};
assert_eq!(
classify(
"RUSTSEC-2020-0071",
&[],
None,
&[&affected],
&pkg_name(),
PKG_OSV_ECO
),
VulnSeverity::Low
);
}
#[test]
fn cvss_vector_only_record_is_unknown() {
assert_eq!(
classify(
"RUSTSEC-2020-0071",
&[],
None,
&[],
&pkg_name(),
PKG_OSV_ECO
),
VulnSeverity::Unknown
);
}
#[test]
fn no_severity_at_all_is_unknown() {
assert_eq!(
classify(
"RUSTSEC-2020-0071",
&[],
None,
&[],
&pkg_name(),
PKG_OSV_ECO
),
VulnSeverity::Unknown
);
}
#[test]
fn non_mal_aliases_do_not_trigger_malicious_classification() {
let aliases = ["GHSA-xxxx-xxxx-xxxx".to_string(), "CVE-2020-1".to_string()];
assert_eq!(
classify(
"RUSTSEC-2020-0071",
&aliases,
None,
&[],
&pkg_name(),
PKG_OSV_ECO
),
VulnSeverity::Unknown
);
}
#[test]
fn mal_prefix_with_no_severity_fields_is_malicious() {
assert_eq!(
classify("MAL-2025-47141", &[], None, &[], &pkg_name(), PKG_OSV_ECO),
VulnSeverity::Malicious
);
}
#[test]
fn mal_prefix_wins_over_a_graded_severity_present_on_the_same_record() {
let json = serde_json::json!({ "severity": "CRITICAL" });
assert_eq!(
classify(
"MAL-2025-47141",
&[],
Some(&json),
&[],
&pkg_name(),
PKG_OSV_ECO
),
VulnSeverity::Malicious
);
}
#[test]
fn mal_alias_without_mal_prefixed_id_is_still_malicious() {
let json = serde_json::json!({ "severity": "HIGH" });
let aliases = ["MAL-2022-1".to_string(), "RUSTSEC-2022-0042".to_string()];
assert_eq!(
classify(
"GHSA-7pwq-f4pq-78gm",
&aliases,
Some(&json),
&[],
&pkg_name(),
PKG_OSV_ECO
),
VulnSeverity::Malicious
);
}
#[test]
fn rustdecimal_three_alias_group_all_classify_as_malicious() {
let ghsa_aliases = ["MAL-2022-1".to_string(), "RUSTSEC-2022-0042".to_string()];
let mal_aliases = [
"GHSA-7pwq-f4pq-78gm".to_string(),
"RUSTSEC-2022-0042".to_string(),
];
let rustsec_aliases = ["GHSA-7pwq-f4pq-78gm".to_string(), "MAL-2022-1".to_string()];
let ghsa_severity = serde_json::json!({ "severity": "HIGH" });
assert_eq!(
classify(
"GHSA-7pwq-f4pq-78gm",
&ghsa_aliases,
Some(&ghsa_severity),
&[],
&pkg_name(),
PKG_OSV_ECO
),
VulnSeverity::Malicious
);
assert_eq!(
classify(
"MAL-2022-1",
&mal_aliases,
None,
&[],
&pkg_name(),
PKG_OSV_ECO
),
VulnSeverity::Malicious
);
assert_eq!(
classify(
"RUSTSEC-2022-0042",
&rustsec_aliases,
None,
&[],
&pkg_name(),
PKG_OSV_ECO
),
VulnSeverity::Malicious
);
}
fn informational_affected(value: &str) -> OsvAffected {
OsvAffected {
versions: vec![],
package: Some(OsvPackage {
name: PKG_NAME.to_string(),
ecosystem: PKG_ECO.to_string(),
}),
ecosystem_specific: None,
database_specific: Some(serde_json::json!({ "informational": value })),
ranges: vec![],
}
}
#[test]
fn plain_unmaintained_is_classified_informational() {
let affected = informational_affected("unmaintained");
assert_eq!(
classify(
"RUSTSEC-2024-0320",
&[],
None,
&[&affected],
&pkg_name(),
PKG_OSV_ECO
),
VulnSeverity::Informational
);
}
#[test]
fn notice_value_is_not_classified_informational() {
let affected = informational_affected("notice");
assert_eq!(
classify(
"RUSTSEC-2026-0174",
&[],
None,
&[&affected],
&pkg_name(),
PKG_OSV_ECO
),
VulnSeverity::Unknown
);
}
#[test]
fn unsound_value_is_not_classified_informational() {
let affected = informational_affected("unsound");
assert_eq!(
classify(
"RUSTSEC-2021-0145",
&[],
None,
&[&affected],
&pkg_name(),
PKG_OSV_ECO
),
VulnSeverity::Unknown
);
}
#[test]
fn osv_unknown_informational_value_is_not_classified_informational() {
let affected = informational_affected("unknown");
assert_eq!(
classify(
"RUSTSEC-2020-0071",
&[],
None,
&[&affected],
&pkg_name(),
PKG_OSV_ECO
),
VulnSeverity::Unknown
);
}
#[test]
fn unrecognized_future_value_is_not_classified_informational() {
let affected = informational_affected("something-new-osv-invented");
assert_eq!(
classify(
"RUSTSEC-2020-0071",
&[],
None,
&[&affected],
&pkg_name(),
PKG_OSV_ECO
),
VulnSeverity::Unknown
);
}
#[test]
fn graded_severity_on_another_entry_wins_over_informational_regardless_of_order() {
let graded = OsvAffected {
versions: vec![],
package: None,
ecosystem_specific: Some(serde_json::json!({ "severity": "HIGH" })),
database_specific: None,
ranges: vec![],
};
let informational = informational_affected("unmaintained");
assert_eq!(
classify(
"RUSTSEC-2020-0071",
&[],
None,
&[&informational, &graded],
&pkg_name(),
PKG_OSV_ECO
),
VulnSeverity::High,
"informational entry listed first must not win over a later graded entry"
);
assert_eq!(
classify(
"RUSTSEC-2020-0071",
&[],
None,
&[&graded, &informational],
&pkg_name(),
PKG_OSV_ECO
),
VulnSeverity::High
);
}
#[test]
fn mal_prefix_wins_over_informational() {
let affected = informational_affected("unmaintained");
assert_eq!(
classify(
"MAL-2025-47141",
&[],
None,
&[&affected],
&pkg_name(),
PKG_OSV_ECO
),
VulnSeverity::Malicious
);
}
#[test]
fn informational_on_fallback_all_entries_is_not_classified_informational() {
let stranger = OsvAffected {
versions: vec![],
package: Some(OsvPackage {
name: "some-other-crate".to_string(),
ecosystem: PKG_ECO.to_string(),
}),
ecosystem_specific: None,
database_specific: Some(serde_json::json!({ "informational": "unmaintained" })),
ranges: vec![],
};
assert_eq!(
classify(
"RUSTSEC-2020-0071",
&[],
None,
&[&stranger],
&pkg_name(),
PKG_OSV_ECO
),
VulnSeverity::Unknown
);
}
#[test]
fn informational_on_package_less_entry_is_not_classified_informational() {
let affected = OsvAffected {
versions: vec![],
package: None,
ecosystem_specific: None,
database_specific: Some(serde_json::json!({ "informational": "unmaintained" })),
ranges: vec![],
};
assert_eq!(
classify(
"RUSTSEC-2020-0071",
&[],
None,
&[&affected],
&pkg_name(),
PKG_OSV_ECO
),
VulnSeverity::Unknown
);
}
#[test]
fn empty_whitespace_and_null_informational_values_are_not_classified_informational() {
for value in [
serde_json::json!({ "informational": "" }),
serde_json::json!({ "informational": " " }),
serde_json::json!({ "informational": null }),
serde_json::json!({}),
] {
let affected = OsvAffected {
versions: vec![],
package: Some(OsvPackage {
name: PKG_NAME.to_string(),
ecosystem: PKG_ECO.to_string(),
}),
ecosystem_specific: None,
database_specific: Some(value.clone()),
ranges: vec![],
};
assert_eq!(
classify(
"RUSTSEC-2020-0071",
&[],
None,
&[&affected],
&pkg_name(),
PKG_OSV_ECO
),
VulnSeverity::Unknown,
"expected {value:?} to not trigger Informational classification"
);
}
}
#[test]
fn to_diagnostic_severity_mapping() {
assert_eq!(
to_diagnostic_severity(VulnSeverity::Critical),
Severity::Warning
);
assert_eq!(
to_diagnostic_severity(VulnSeverity::High),
Severity::Warning
);
assert_eq!(
to_diagnostic_severity(VulnSeverity::Medium),
Severity::Information
);
assert_eq!(
to_diagnostic_severity(VulnSeverity::Low),
Severity::Information
);
assert_eq!(
to_diagnostic_severity(VulnSeverity::Unknown),
Severity::Warning
);
assert_eq!(
to_diagnostic_severity(VulnSeverity::Malicious),
Severity::Warning
);
assert_eq!(
to_diagnostic_severity(VulnSeverity::Informational),
Severity::Information
);
}
}