use libcvss::v2::CVSS2Vector; use libcvss::v3::CVSS3Vector; use libcvss::v31::CVSS31Vector; use std::io::{BufRead, BufReader}; use std::process::Command; use std::str::Split; struct NVDVulnerability { cve_id: String, cvss_v2_vector_string: Option, cvss_v2_score: Option, cvss_v3_version: Option, cvss_v3_vector_string: Option, cvss_v3_score: Option, } struct TestResults { cvss_v2_parsing_count: i64, cvss_v2_failed_strict_parsing_count: i64, cvss_v2_failed_non_strict_parsing_count: i64, cvss_v2_scoring_count: i64, cvss_v2_failed_scoring_count: i64, cvss_v3_parsing_count: i64, cvss_v3_failed_strict_parsing_count: i64, cvss_v3_failed_non_strict_parsing_count: i64, cvss_v3_scoring_count: i64, cvss_v3_failed_scoring_count: i64, } struct YearlySLA { cvss_v2_strict_parsing_sla: f64, cvss_v2_non_strict_parsing_sla: f64, cvss_v2_scoring_sla: f64, cvss_v3_strict_parsing_sla: f64, cvss_v3_non_strict_parsing_sla: f64, cvss_v3_scoring_sla: f64, } enum CVSS3MinorVersion { CVSS3, CVSS31, } #[test] #[ignore] pub fn nvd_parsing() { let yearly_sla = YearlySLA { cvss_v2_strict_parsing_sla: 0.99, cvss_v2_non_strict_parsing_sla: 0.999, cvss_v2_scoring_sla: 0.999, cvss_v3_strict_parsing_sla: 0.99, cvss_v3_non_strict_parsing_sla: 0.999, cvss_v3_scoring_sla: 0.999, }; evaluate_one_year_of_vulnerabilities(2007, &yearly_sla); evaluate_one_year_of_vulnerabilities(2008, &yearly_sla); evaluate_one_year_of_vulnerabilities(2009, &yearly_sla); evaluate_one_year_of_vulnerabilities(2010, &yearly_sla); evaluate_one_year_of_vulnerabilities(2011, &yearly_sla); evaluate_one_year_of_vulnerabilities(2012, &yearly_sla); evaluate_one_year_of_vulnerabilities(2013, &yearly_sla); evaluate_one_year_of_vulnerabilities(2014, &yearly_sla); evaluate_one_year_of_vulnerabilities(2015, &yearly_sla); evaluate_one_year_of_vulnerabilities(2016, &yearly_sla); evaluate_one_year_of_vulnerabilities(2017, &yearly_sla); evaluate_one_year_of_vulnerabilities(2018, &yearly_sla); evaluate_one_year_of_vulnerabilities(2019, &yearly_sla); evaluate_one_year_of_vulnerabilities(2020, &yearly_sla); } fn evaluate_one_year_of_vulnerabilities(year: i64, yearly_sla: &YearlySLA) { let mut test_results = TestResults { cvss_v2_parsing_count: 0, cvss_v2_failed_strict_parsing_count: 0, cvss_v2_failed_non_strict_parsing_count: 0, cvss_v2_scoring_count: 0, cvss_v2_failed_scoring_count: 0, cvss_v3_parsing_count: 0, cvss_v3_failed_strict_parsing_count: 0, cvss_v3_failed_non_strict_parsing_count: 0, cvss_v3_scoring_count: 0, cvss_v3_failed_scoring_count: 0, }; let vulnerabilities = get_nvd_vulnerabilities_from_year(year); for vulnerability in vulnerabilities { evaluate_vulnerability(&vulnerability, &mut test_results); } check_yearly_sla(&test_results, yearly_sla, year); } fn check_yearly_sla(test_results: &TestResults, yearly_sla: &YearlySLA, year: i64) { // CVSS V2 { let cvss_v2_strict_parsing_measurement = 1.0 - (test_results.cvss_v2_failed_strict_parsing_count as f64) / (test_results.cvss_v2_parsing_count as f64); println!( "Yearly error measurement for year {} on strict CVSS V2 parsing: {}. Evaluated vulnerabilities: {}. Errors: {}", year, cvss_v2_strict_parsing_measurement, test_results.cvss_v2_parsing_count, test_results.cvss_v2_failed_strict_parsing_count ); assert!(cvss_v2_strict_parsing_measurement > yearly_sla.cvss_v2_strict_parsing_sla, "Yearly SLA for year {} on strict CVSS V2 parsing was not respected! SLA: {:.3}. Actual: {:.3}", year, yearly_sla.cvss_v2_strict_parsing_sla, cvss_v2_strict_parsing_measurement); let cvss_v2_non_strict_parsing_measurement = 1.0 - (test_results.cvss_v2_failed_non_strict_parsing_count as f64) / (test_results.cvss_v2_parsing_count as f64); println!( "Yearly error measurement for year {} on non-strict CVSS V2 parsing: {}. Evaluated vulnerabilities: {}. Errors: {}", year, cvss_v2_non_strict_parsing_measurement, test_results.cvss_v2_parsing_count, test_results.cvss_v2_failed_non_strict_parsing_count ); assert!(cvss_v2_non_strict_parsing_measurement > yearly_sla.cvss_v2_non_strict_parsing_sla, "Yearly SLA for year {} on non-strict CVSS V2 parsing was not respected! SLA: {:.3}. Actual: {:.3}", year, yearly_sla.cvss_v2_non_strict_parsing_sla, cvss_v2_non_strict_parsing_measurement); let cvss_v2_scoring_measurement = 1.0 - (test_results.cvss_v2_failed_scoring_count as f64) / (test_results.cvss_v2_scoring_count as f64); println!( "Yearly error measurement for year {} on CVSS V2 scoring: {}. Evaluated vulnerabilities: {}. Errors: {}", year, cvss_v2_scoring_measurement, test_results.cvss_v2_scoring_count, test_results.cvss_v2_failed_scoring_count ); assert!(cvss_v2_scoring_measurement > yearly_sla.cvss_v2_scoring_sla, "Yearly SLA for year {} on non-strict CVSS V2 parsing was not respected! SLA: {:.3}. Actual: {:.3}", year, yearly_sla.cvss_v2_scoring_sla, cvss_v2_scoring_measurement); } // CVSS V3 { let cvss_v3_strict_parsing_measurement = 1.0 - (test_results.cvss_v3_failed_strict_parsing_count as f64) / (test_results.cvss_v3_parsing_count as f64); println!( "Yearly error measurement for year {} on strict CVSS V3 parsing: {}. Evaluated vulnerabilities: {}. Errors: {}", year, cvss_v3_strict_parsing_measurement, test_results.cvss_v3_parsing_count, test_results.cvss_v3_failed_strict_parsing_count ); assert!(cvss_v3_strict_parsing_measurement > yearly_sla.cvss_v3_strict_parsing_sla, "Yearly SLA for year {} on strict CVSS V3 parsing was not respected! SLA: {:.3}. Actual: {:.3}", year, yearly_sla.cvss_v3_strict_parsing_sla, cvss_v3_strict_parsing_measurement); let cvss_v3_non_strict_parsing_measurement = 1.0 - (test_results.cvss_v3_failed_non_strict_parsing_count as f64) / (test_results.cvss_v3_parsing_count as f64); println!( "Yearly error measurement for year {} on non-strict CVSS V3 parsing: {}. Evaluated vulnerabilities: {}. Errors: {}", year, cvss_v3_non_strict_parsing_measurement, test_results.cvss_v3_parsing_count, test_results.cvss_v3_failed_non_strict_parsing_count ); assert!(cvss_v3_non_strict_parsing_measurement > yearly_sla.cvss_v3_non_strict_parsing_sla, "Yearly SLA for year {} on non-strict CVSS V3 parsing was not respected! SLA: {:.3}. Actual: {:.3}", year, yearly_sla.cvss_v3_non_strict_parsing_sla, cvss_v3_non_strict_parsing_measurement); let cvss_v3_scoring_measurement = 1.0 - (test_results.cvss_v3_failed_scoring_count as f64) / (test_results.cvss_v3_scoring_count as f64); println!( "Yearly error measurement for year {} on CVSS V3 scoring: {}. Evaluated vulnerabilities: {}. Errors: {}", year, cvss_v3_scoring_measurement, test_results.cvss_v3_scoring_count, test_results.cvss_v3_failed_scoring_count ); assert!(cvss_v3_scoring_measurement > yearly_sla.cvss_v3_scoring_sla, "Yearly SLA for year {} on non-strict CVSS V3 parsing was not respected! SLA: {:.3}. Actual: {:.3}", year, yearly_sla.cvss_v3_scoring_sla, cvss_v3_scoring_measurement); } } fn evaluate_vulnerability(nvd_vulnerability: &NVDVulnerability, test_results: &mut TestResults) { match &nvd_vulnerability.cvss_v2_vector_string { None => {} Some(cvss_v2_vector_string) => { evaluate_cvss_v2(cvss_v2_vector_string, nvd_vulnerability, test_results) } } match &nvd_vulnerability.cvss_v3_vector_string { None => {} Some(cvss_v3_vector_string) => match &nvd_vulnerability.cvss_v3_version { None => {} Some(version) => match version { CVSS3MinorVersion::CVSS3 => { evaluate_cvss_v30(cvss_v3_vector_string, nvd_vulnerability, test_results) } CVSS3MinorVersion::CVSS31 => { evaluate_cvss_v31(cvss_v3_vector_string, nvd_vulnerability, test_results) } }, }, } } fn evaluate_cvss_v2( cvss_v2_vector_string: &String, nvd_vulnerability: &NVDVulnerability, test_results: &mut TestResults, ) { test_results.cvss_v2_parsing_count += 1; if CVSS2Vector::parse_strict(cvss_v2_vector_string.as_str()).is_err() { test_results.cvss_v2_failed_strict_parsing_count += 1; println!( "{} with CVSS V3 vector string {} could not be strictly parsed.", nvd_vulnerability.cve_id, cvss_v2_vector_string ); } if CVSS2Vector::parse_nonstrict(cvss_v2_vector_string.as_str()).is_err() { test_results.cvss_v2_failed_non_strict_parsing_count += 1; println!( "{} with CVSS V3 vector string {} could not be non-strictly parsed.", nvd_vulnerability.cve_id, cvss_v2_vector_string ); } match CVSS2Vector::parse_nonstrict(cvss_v2_vector_string.as_str()) { Err(_e) => {} Ok(cvss_v2_vector) => match nvd_vulnerability.cvss_v2_score { None => {} Some(score) => { test_results.cvss_v2_scoring_count += 1; if score != cvss_v2_vector.score() { test_results.cvss_v2_failed_scoring_count += 1; println!("{} with vector string {} had a computed score different from NVD score: {} vs {}", nvd_vulnerability.cve_id, cvss_v2_vector_string, score, cvss_v2_vector.score()); } } }, } } fn evaluate_cvss_v30( cvss_v3_vector_string: &String, nvd_vulnerability: &NVDVulnerability, test_results: &mut TestResults, ) { test_results.cvss_v3_parsing_count += 1; if CVSS3Vector::parse_strict(cvss_v3_vector_string.as_str()).is_err() { test_results.cvss_v3_failed_strict_parsing_count += 1; println!( "{} with CVSS V3 vector string {} could not be strictly parsed.", nvd_vulnerability.cve_id, cvss_v3_vector_string ); } if CVSS3Vector::parse_nonstrict(cvss_v3_vector_string.as_str()).is_err() { test_results.cvss_v3_failed_non_strict_parsing_count += 1; println!( "{} with CVSS V3 vector string {} could not be non-strictly parsed.", nvd_vulnerability.cve_id, cvss_v3_vector_string ); } match CVSS3Vector::parse_nonstrict(cvss_v3_vector_string.as_str()) { Err(_e) => {} Ok(cvss_v3_vector) => match nvd_vulnerability.cvss_v3_score { None => {} Some(score) => { test_results.cvss_v3_scoring_count += 1; if score != cvss_v3_vector.score() { test_results.cvss_v3_failed_scoring_count += 1; println!("{} with vector string {} had a computed score different from NVD score: {} vs {}", nvd_vulnerability.cve_id, cvss_v3_vector_string, score, cvss_v3_vector.score()); } } }, } } fn evaluate_cvss_v31( cvss_v3_vector_string: &String, nvd_vulnerability: &NVDVulnerability, test_results: &mut TestResults, ) { test_results.cvss_v3_parsing_count += 1; if CVSS31Vector::parse_strict(cvss_v3_vector_string.as_str()).is_err() { test_results.cvss_v3_failed_strict_parsing_count += 1; println!( "{} with CVSS V3 vector string {} could not be strictly parsed.", nvd_vulnerability.cve_id, cvss_v3_vector_string ); } if CVSS31Vector::parse_nonstrict(cvss_v3_vector_string.as_str()).is_err() { test_results.cvss_v3_failed_non_strict_parsing_count += 1; println!( "{} with CVSS V3 vector string {} could not be non-strictly parsed.", nvd_vulnerability.cve_id, cvss_v3_vector_string ); } match CVSS31Vector::parse_nonstrict(cvss_v3_vector_string.as_str()) { Err(_e) => {} Ok(cvss_v3_vector) => match nvd_vulnerability.cvss_v3_score { None => {} Some(score) => { test_results.cvss_v3_scoring_count += 1; if score != cvss_v3_vector.score() { test_results.cvss_v3_failed_scoring_count += 1; println!("{} with vector string {} had a computed score different from NVD score: {} vs {}", nvd_vulnerability.cve_id, cvss_v3_vector_string, score, cvss_v3_vector.score()); } } }, } } fn get_nvd_vulnerabilities_from_year(year: i64) -> Vec { check_all_binaries_available(); let mut nvd_vulnerabilities = Vec::new(); println!("Downloading NVD file for year {}...", year); let result = Command::new("sh").arg("-c").arg(format!("wget -qO - https://nvd.nist.gov/feeds/json/cve/1.1/nvdcve-1.1-{}.json.gz | gunzip | jq -r '.CVE_Items [] | [.cve.CVE_data_meta.ID, .impact.baseMetricV2.cvssV2.vectorString, .impact.baseMetricV2.cvssV2.baseScore, .impact.baseMetricV3.cvssV3.version, .impact.baseMetricV3.cvssV3.vectorString, .impact.baseMetricV3.cvssV3.baseScore] | @csv'", year)).output().unwrap(); println!("File is downloaded."); let errors = String::from_utf8(result.stderr).unwrap(); if !errors.trim().is_empty() { println!("{}", errors); } let br = BufReader::new(result.stdout.as_slice()); for res in br.lines() { let line = res.unwrap(); let mut split = line.split(','); let cve_id = extract_and_clean_field(&mut split) .expect("All vulnerabilities should have a CVE ID!") .to_string(); let cvss_v2_vector_string = extract_and_clean_field(&mut split); let cvss_v2_score = match extract_and_clean_field(&mut split) { None => None, Some(score_as_string) => match score_as_string.parse::() { Err(_e) => { println!( "For {} could not parse score {} as a f64 number.", cve_id, score_as_string ); None } Ok(score) => Some(score), }, }; let cvss_v3_version = extract_and_clean_field(&mut split); let cvss_v3_vector_string = extract_and_clean_field(&mut split); let cvss_v3_score = match extract_and_clean_field(&mut split) { None => None, Some(score_as_string) => match score_as_string.parse::() { Err(_e) => { println!( "For {} could not parse score {} as a f64 number.", cve_id, score_as_string ); None } Ok(score) => Some(score), }, }; let nvd_vulnerability = NVDVulnerability { cve_id, cvss_v2_vector_string, cvss_v2_score, cvss_v3_version: parse_cvss3_version(cvss_v3_version), cvss_v3_vector_string, cvss_v3_score, }; nvd_vulnerabilities.push(nvd_vulnerability); } nvd_vulnerabilities } fn parse_cvss3_version(version_as_string: Option) -> Option { match version_as_string { None => None, Some(version) => match version.as_str() { "3.0" => Some(CVSS3MinorVersion::CVSS3), "3.1" => Some(CVSS3MinorVersion::CVSS31), _ => panic!("CVSS3 version field ({}) was not recognized !", version), }, } } fn check_all_binaries_available() { // Check availability of sh check_is_binary_available("sh"); // Check availability of wget check_is_binary_available("wget"); // Check availability of gunzip check_is_binary_available("gunzip"); // Check availability of jq check_is_binary_available("jq"); } fn extract_and_clean_field(split: &mut Split) -> Option { match split.next() { None => None, Some(s) => { let removed_quotes = s.replace('"', ""); let cleaned = removed_quotes.trim(); if cleaned.is_empty() { None } else { Some(cleaned.to_string()) } } } } fn check_is_binary_available(binary_name: &'static str) { Command::new(binary_name).output().expect( format!( "Integration tests require binary {} to be installed on the system.", binary_name ) .as_str(), ); }