From c452ad947678604051d41f5732256ad70cac6641 Mon Sep 17 00:00:00 2001 From: mrl5 <31549762+mrl5@users.noreply.github.com> Date: Sun, 27 Feb 2022 17:26:01 +0100 Subject: [PATCH] feat(cli): recognize known exploited CVEs --- crates/cli/src/command.rs | 13 ++++- crates/cli/src/command/cve.rs | 29 ++++++++-- crates/cli/src/command/scan.rs | 14 +++-- crates/security-advisories/src/cve_summary.rs | 2 + crates/security-advisories/src/service.rs | 12 ++++- .../security-advisories/src/service/cisa.rs | 23 ++++++++ crates/security-advisories/src/service/nvd.rs | 53 +++++++++++++++---- docs/COOKBOOK.md | 5 +- 8 files changed, 126 insertions(+), 25 deletions(-) diff --git a/crates/cli/src/command.rs b/crates/cli/src/command.rs index 7246577..4a4c337 100644 --- a/crates/cli/src/command.rs +++ b/crates/cli/src/command.rs @@ -22,7 +22,11 @@ pub async fn execute(cmd: Command) -> Result<(), Box> { packages_batch, cpe_feed, } => cpe::execute(get_input(packages_batch)?, cpe_feed.feed_dir).await, - Command::Cve { cpe_batch, summary } => cve::execute(get_input(cpe_batch)?, summary).await, + Command::Cve { + cpe_batch, + summary, + check_known_exploited, + } => cve::execute(get_input(cpe_batch)?, summary, check_known_exploited).await, Command::Scan { cpe_feed, out_dir, @@ -52,6 +56,13 @@ pub enum Command { cpe_batch: Option, #[structopt(short, long, help = "Prints CVE summary instead of full response")] summary: bool, + + #[structopt( + short, + long, + help = "Additonal check agains known exploited vulnerabilities catalog" + )] + check_known_exploited: bool, }, #[structopt( diff --git a/crates/cli/src/command/cve.rs b/crates/cli/src/command/cve.rs index 1178f13..7eccff8 100644 --- a/crates/cli/src/command/cve.rs +++ b/crates/cli/src/command/cve.rs @@ -7,23 +7,42 @@ use cpe_tag::validators::validate_cpe_batch; use security_advisories::http::get_client; -use security_advisories::service::{fetch_cves_by_cpe, get_cves_summary}; +use security_advisories::service::{ + fetch_cves_by_cpe, fetch_known_exploited_cves, get_cves_summary, +}; use serde_json::from_str; use serde_json::Value; use std::error::Error; -pub async fn execute(batch: String, show_summary: bool) -> Result<(), Box> { +pub async fn execute( + batch: String, + show_summary: bool, + check_known_exploited: bool, +) -> Result<(), Box> { log::info!("validating input ..."); let json = from_str(&batch)?; validate_cpe_batch(&json)?; let client = get_client()?; + let mut feed = vec![]; + if check_known_exploited { + feed = fetch_known_exploited_cves(&client).await?; + } + for v in json.as_array().unwrap_or(&vec![]) { match v.as_str() { Some(cpe) => { log::info!("fetching CVEs by CPE {} ...", cpe); let cves = fetch_cves_by_cpe(&client, cpe).await?; - print_cves(cves, show_summary); + print_cves( + cves, + show_summary, + if check_known_exploited { + Some(&feed) + } else { + None + }, + ); } None => continue, } @@ -32,9 +51,9 @@ pub async fn execute(batch: String, show_summary: bool) -> Result<(), Box) { if show_summary { - for cve in get_cves_summary(&cves) { + for cve in get_cves_summary(&cves, known_exploited_cves) { println!("{}", cve); } } else { diff --git a/crates/cli/src/command/scan.rs b/crates/cli/src/command/scan.rs index babf429..a54da23 100644 --- a/crates/cli/src/command/scan.rs +++ b/crates/cli/src/command/scan.rs @@ -12,7 +12,9 @@ use os_adapter::adapter::get_adapter; use reqwest::Client; use security_advisories::cve_summary::CveSummary; use security_advisories::http::get_client; -use security_advisories::service::{fetch_cves_by_cpe, get_cves_summary, CPE_MATCH_FEED}; +use security_advisories::service::{ + fetch_cves_by_cpe, fetch_known_exploited_cves, get_cves_summary, CPE_MATCH_FEED, +}; use std::error::Error; use std::fs::create_dir_all; use std::fs::File; @@ -42,10 +44,12 @@ pub async fn execute( log::info!("listing all catpkgs ..."); let catpkgs = os.get_all_catpkgs()?; + let known_exploited_cves = fetch_known_exploited_cves(&client).await?; + for (ctg, pkgs) in catpkgs { let cwd = out_dir.join(&ctg); log::debug!("processing {} ...", ctg); - handle_pkgs(&client, &feed, &cwd, &ctg, &pkgs).await?; + handle_pkgs(&client, &feed, &cwd, &ctg, &pkgs, &known_exploited_cves).await?; } println!("Done. You can find results in {:?}", out_dir.as_os_str()); @@ -58,6 +62,7 @@ async fn handle_pkgs( cwd: &Path, category: &str, pkgs: &[Package], + known_exploited_cves: &[String], ) -> Result<(), Box> { let pattern = get_grep_patterns(pkgs)?; let matches = query(pattern, feed)?; @@ -74,7 +79,7 @@ async fn handle_pkgs( "found CPEs for packages in {}. Searching for CVEs ...", category ); - handle_cves(client, cwd, category, &matches).await + handle_cves(client, cwd, category, &matches, known_exploited_cves).await } async fn handle_cves( @@ -82,11 +87,12 @@ async fn handle_cves( cwd: &Path, category: &str, matches: &[String], + known_exploited_cves: &[String], ) -> Result<(), Box> { let mut already_notified = false; for cpe in matches { let cves = match fetch_cves_by_cpe(client, cpe).await { - Ok(res) => get_cves_summary(&res), + Ok(res) => get_cves_summary(&res, Some(known_exploited_cves)), Err(e) => { log::error!("{}", e); vec![] diff --git a/crates/security-advisories/src/cve_summary.rs b/crates/security-advisories/src/cve_summary.rs index 609d592..61ebd88 100644 --- a/crates/security-advisories/src/cve_summary.rs +++ b/crates/security-advisories/src/cve_summary.rs @@ -12,6 +12,7 @@ use std::fmt; #[derive(Serialize, Debug)] pub struct CveSummary { pub id: String, + pub is_known_exploited_vuln: Option, pub description: String, pub urls: Vec, } @@ -20,6 +21,7 @@ impl CveSummary { pub fn new(id: String, description: String, urls: Vec) -> Self { Self { id, + is_known_exploited_vuln: None, description, urls, } diff --git a/crates/security-advisories/src/service.rs b/crates/security-advisories/src/service.rs index 974dcb0..4c5c26b 100644 --- a/crates/security-advisories/src/service.rs +++ b/crates/security-advisories/src/service.rs @@ -26,8 +26,11 @@ pub async fn fetch_feed_checksum(client: &Client) -> Result Vec { - nvd::get_cves_summary(full_cve_resp) +pub fn get_cves_summary( + full_cve_resp: &Value, + known_exploitable_cves: Option<&[String]>, +) -> Vec { + nvd::get_cves_summary(full_cve_resp, known_exploitable_cves) } pub async fn download_cpe_match_feed( @@ -42,3 +45,8 @@ pub async fn fetch_known_exploited_vulns(client: &Client) -> Result Result, Box> { + log::info!("fetching known exploited CVEs ..."); + cisa::fetch_known_exploited_cves(client).await +} diff --git a/crates/security-advisories/src/service/cisa.rs b/crates/security-advisories/src/service/cisa.rs index 5f46096..ed9922c 100644 --- a/crates/security-advisories/src/service/cisa.rs +++ b/crates/security-advisories/src/service/cisa.rs @@ -16,8 +16,31 @@ pub async fn fetch_known_exploited_vulns(client: &Client) -> Result Result, Box> { + let known_exploited_vulns = fetch_known_exploited_vulns(client).await?; + + Ok(get_known_exploited_cves(&known_exploited_vulns)) +} + +fn get_known_exploited_cves(known_exploited_vulns: &Value) -> Vec { + let mut cves = vec![]; + if let Some(kev) = known_exploited_vulns["vulnerabilities"].as_array() { + for item in kev { + if let Some(cve) = item["cveID"].as_str() { + cves.push(cve.to_owned()); + } + } + } else { + log::error!("couldn't get known exploited CVEs"); + } + + cves +} diff --git a/crates/security-advisories/src/service/nvd.rs b/crates/security-advisories/src/service/nvd.rs index 0824584..4839b38 100644 --- a/crates/security-advisories/src/service/nvd.rs +++ b/crates/security-advisories/src/service/nvd.rs @@ -35,18 +35,22 @@ pub async fn fetch_cves_by_cpe(client: &Client, cpe: &str) -> Result Vec { - let mut ids = vec![]; - if let Some(items) = full_cve_resp["result"]["CVE_Items"].as_array() { - for item in items { - let cve_data = &item["cve"]; - if let Some(summary) = get_cve_summary(cve_data) { - ids.push(summary); +pub fn get_cves_summary( + full_cve_resp: &Value, + known_exploitable_cves: Option<&[String]>, +) -> Vec { + let mut summary_items = vec![]; + + if let Some(resp_items) = full_cve_resp["result"]["CVE_Items"].as_array() { + for resp_item in resp_items { + let cve_data = &resp_item["cve"]; + if let Some(summary) = get_cve_summary(cve_data, known_exploitable_cves) { + summary_items.push(summary); } } } - ids + summary_items } pub async fn fetch_feed_checksum(client: &Client) -> Result> { @@ -109,13 +113,22 @@ fn get_checksum(meta: String) -> Result { } } -fn get_cve_summary(cve_data: &Value) -> Option { +fn get_cve_summary( + cve_data: &Value, + known_exploitable_cves: Option<&[String]>, +) -> Option { if let Some(id) = cve_data["CVE_data_meta"]["ID"].as_str() { - return Some(CveSummary::new( + let mut summary = CveSummary::new( id.to_owned(), get_cve_desc(cve_data), get_cve_urls(id, cve_data), - )); + ); + + if let Some(kec) = known_exploitable_cves { + summary.is_known_exploited_vuln = Some(kec.contains(&id.to_owned())); + } + + return Some(summary); } None } @@ -147,6 +160,8 @@ fn get_cve_urls(id: &str, cve_data: &Value) -> Vec { #[cfg(test)] mod tests { use super::*; + use serde_json::from_str; + #[test] fn it_should_filter_checksum() { let checksum = "f283d332a8a66ecb23d49ee385ce42fca691e598da29475beb0b3556ab1fe02e"; @@ -154,4 +169,20 @@ mod tests { assert_eq!(get_checksum(test_data.to_owned()).unwrap(), checksum); } + + #[test] + fn it_should_recognize_known_exploitable_cves() { + let known_exploitable_cves = vec!["CVE-2021-22204".to_owned()]; + let listed_cve = from_str(r#"{"CVE_data_meta":{"ASSIGNER":"cve@gitlab.com","ID":"CVE-2021-22204"},"data_format":"MITRE","data_type":"CVE","data_version":"4.0","description":{"description_data":[{"lang":"en","value":"Improper neutralization of user data in the DjVu file format in ExifTool versions 7.44 and up allows arbitrary code execution when parsing the malicious image"}]},"problemtype":{"problemtype_data":[{"description":[{"lang":"en","value":"CWE-74"}]}]},"references":{"reference_data":[]}}"#).unwrap(); + let unlisted_cve = from_str(r#"{"CVE_data_meta":{"ASSIGNER":"cve@mitre.org","ID":"CVE-2022-23935"},"data_format":"MITRE","data_type":"CVE","data_version":"4.0","description":{"description_data":[{"lang":"en","value":"lib/Image/ExifTool.pm in ExifTool before 12.38 mishandles a $file =~ /\\|$/ check."}]},"problemtype":{"problemtype_data":[{"description":[{"lang":"en","value":"NVD-CWE-noinfo"}]}]},"references":{"reference_data":[]}}"#).unwrap(); + + let summary = get_cve_summary(&listed_cve, None).unwrap(); + assert_eq!(summary.is_known_exploited_vuln, None); + + let summary = get_cve_summary(&listed_cve, Some(&known_exploitable_cves)).unwrap(); + assert_eq!(summary.is_known_exploited_vuln, Some(true)); + + let summary = get_cve_summary(&unlisted_cve, Some(&known_exploitable_cves)).unwrap(); + assert_eq!(summary.is_known_exploited_vuln, Some(false)); + } } diff --git a/docs/COOKBOOK.md b/docs/COOKBOOK.md index 5113fee..d35927c 100644 --- a/docs/COOKBOOK.md +++ b/docs/COOKBOOK.md @@ -42,6 +42,7 @@ $ cat ~/vulner/scan-results/2022-01-30UTC/*/app-emulation/*containerd*.txt | jq ``` { "id": "CVE-2021-41103", + "is_known_exploited_vuln": false, "description": "A bug was found in containerd where container root directories and some plugins had insufficiently restricted permissions, allowing otherwise unprivileged Linux users to traverse directory contents and execute programs.", , "urls": [ @@ -91,7 +92,7 @@ RUST_LOG=debug vulner sync && echo ' ] } ] -' | jq -c '.' | vulner cpe | vulner cve --summary +' | jq -c '.' | vulner cpe | vulner cve --summary --check-known-exploited ``` example produces: ``` @@ -101,7 +102,7 @@ example produces: [2022-01-29T14:22:27Z INFO vulner::utils] computing checksum of "/tmp/vulner/feeds/json/nvdcpematch-1.0.json" ... [2022-01-29T14:22:28Z DEBUG reqwest::async_impl::client] response '200 OK' for https://nvd.nist.gov/feeds/json/cpematch/1.0/nvdcpematch-1.0.meta CPE match feed is up to date, available in "/tmp/vulner/feeds/json/nvdcpematch-1.0.json" -{"id":"CVE-2020-7595","desc":{"description_data":[{"lang":"en","value":"xmlStringLenDecodeEntities in parser.c in libxml2 2.9.10 has an infinite loop in a certain end-of-file situation."}]},"impact":{"acInsufInfo":false,"cvssV2":{"accessComplexity":"LOW","accessVector":"NETWORK","authentication":"NONE","availabilityImpact":"PARTIAL","baseScore":5,"confidentialityImpact":"NONE","integrityImpact":"NONE","vectorString":"AV:N/AC:L/Au:N/C:N/I:N/A:P","version":"2.0"},"exploitabilityScore":10,"impactScore":2.9,"obtainAllPrivilege":false,"obtainOtherPrivilege":false,"obtainUserPrivilege":false,"severity":"MEDIUM","userInteractionRequired":false}} +{"id":"CVE-2020-7595","is_known_exploited_vuln":false,"desc":{"description_data":[{"lang":"en","value":"xmlStringLenDecodeEntities in parser.c in libxml2 2.9.10 has an infinite loop in a certain end-of-file situation."}]},"impact":{"acInsufInfo":false,"cvssV2":{"accessComplexity":"LOW","accessVector":"NETWORK","authentication":"NONE","availabilityImpact":"PARTIAL","baseScore":5,"confidentialityImpact":"NONE","integrityImpact":"NONE","vectorString":"AV:N/AC:L/Au:N/C:N/I:N/A:P","version":"2.0"},"exploitabilityScore":10,"impactScore":2.9,"obtainAllPrivilege":false,"obtainOtherPrivilege":false,"obtainUserPrivilege":false,"severity":"MEDIUM","userInteractionRequired":false}} ```