use super::response::{OsvAffected, OsvSeverity, OsvVulnerability};
use crate::model::{
CvssScore, CvssVersion, Remediation, RemediationType, Severity, VulnerabilityRef,
VulnerabilitySource,
};
use chrono::{DateTime, Utc};
pub fn map_osv_to_vulnerability_ref(osv: &OsvVulnerability) -> VulnerabilityRef {
VulnerabilityRef {
id: osv.id.clone(),
source: VulnerabilitySource::Osv,
severity: extract_severity(&osv.severity),
cvss: extract_cvss_scores(&osv.severity),
affected_versions: extract_affected_versions(&osv.affected),
remediation: extract_remediation(&osv.affected),
description: osv.details.clone().or_else(|| osv.summary.clone()),
cwes: extract_cwes(osv.database_specific.as_ref()),
published: parse_datetime(osv.published.as_ref()),
modified: parse_datetime(osv.modified.as_ref()),
is_kev: false, kev_info: None,
vex_status: None,
}
}
fn extract_severity(severities: &[OsvSeverity]) -> Option<Severity> {
for sev in severities {
if let Some(score) = parse_cvss_score(&sev.score) {
return Some(Severity::from_cvss(score));
}
}
None
}
fn extract_cvss_scores(severities: &[OsvSeverity]) -> Vec<CvssScore> {
severities
.iter()
.filter_map(|sev| {
let version = match sev.severity_type.as_str() {
"CVSS_V2" => Some(CvssVersion::V2),
"CVSS_V3" => Some(CvssVersion::V3),
"CVSS_V31" => Some(CvssVersion::V31),
"CVSS_V4" => Some(CvssVersion::V4),
_ => None,
}?;
let base_score = parse_cvss_score(&sev.score)?;
Some(CvssScore {
version,
base_score,
vector: if sev.score.contains(':') {
Some(sev.score.clone())
} else {
None
},
exploitability_score: None,
impact_score: None,
})
})
.collect()
}
fn parse_cvss_score(score_str: &str) -> Option<f32> {
if let Ok(score) = score_str.parse::<f32>() {
return Some(score);
}
if score_str.contains('/') {
for part in score_str.split('/') {
if part.to_lowercase().starts_with("score:")
&& let Ok(score) = part[6..].parse::<f32>()
{
return Some(score);
}
}
}
None
}
fn extract_affected_versions(affected: &[OsvAffected]) -> Vec<String> {
let mut versions = Vec::new();
for aff in affected {
versions.extend(aff.versions.iter().cloned());
for range in &aff.ranges {
for event in &range.events {
if let Some(ref introduced) = event.introduced
&& introduced != "0"
{
versions.push(format!(">= {introduced}"));
}
if let Some(ref fixed) = event.fixed {
versions.push(format!("< {fixed} (fixed)"));
}
if let Some(ref last) = event.last_affected {
versions.push(format!("<= {last}"));
}
}
}
}
versions
}
fn extract_remediation(affected: &[OsvAffected]) -> Option<Remediation> {
for aff in affected {
for range in &aff.ranges {
for event in &range.events {
if let Some(ref fixed) = event.fixed {
return Some(Remediation {
remediation_type: RemediationType::Upgrade,
description: Some(format!("Upgrade to version {fixed} or later")),
fixed_version: Some(fixed.clone()),
});
}
}
}
}
None
}
fn extract_cwes(database_specific: Option<&serde_json::Value>) -> Vec<String> {
let mut cwes = Vec::new();
if let Some(db_specific) = database_specific {
if let Some(cwe_ids) = db_specific.get("cwe_ids").and_then(|v| v.as_array()) {
for cwe in cwe_ids {
if let Some(cwe_str) = cwe.as_str() {
cwes.push(cwe_str.to_string());
}
}
}
if let Some(cwes_arr) = db_specific.get("cwes").and_then(|v| v.as_array()) {
for cwe in cwes_arr {
if let Some(cwe_id) = cwe.get("cweId").and_then(|v| v.as_str()) {
cwes.push(cwe_id.to_string());
}
}
}
}
cwes
}
fn parse_datetime(dt_str: Option<&String>) -> Option<DateTime<Utc>> {
dt_str.map(String::as_str).and_then(|s| {
DateTime::parse_from_rfc3339(s)
.map(|dt| dt.with_timezone(&Utc))
.ok()
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_cvss_score() {
assert_eq!(parse_cvss_score("7.5"), Some(7.5));
assert_eq!(parse_cvss_score("10.0"), Some(10.0));
assert_eq!(parse_cvss_score("invalid"), None);
}
#[test]
fn test_severity_from_score() {
assert_eq!(Severity::from_cvss(9.5), Severity::Critical);
assert_eq!(Severity::from_cvss(7.5), Severity::High);
assert_eq!(Severity::from_cvss(5.0), Severity::Medium);
assert_eq!(Severity::from_cvss(2.0), Severity::Low);
}
}