Skip to content

Commit

Permalink
feat(cli): recognize known exploited CVEs
Browse files Browse the repository at this point in the history
  • Loading branch information
mrl5 committed Feb 27, 2022
1 parent 96bc9a3 commit c452ad9
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 25 deletions.
13 changes: 12 additions & 1 deletion crates/cli/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ pub async fn execute(cmd: Command) -> Result<(), Box<dyn Error>> {
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,
Expand Down Expand Up @@ -52,6 +56,13 @@ pub enum Command {
cpe_batch: Option<String>,
#[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(
Expand Down
29 changes: 24 additions & 5 deletions crates/cli/src/command/cve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn Error>> {
pub async fn execute(
batch: String,
show_summary: bool,
check_known_exploited: bool,
) -> Result<(), Box<dyn Error>> {
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,
}
Expand All @@ -32,9 +51,9 @@ pub async fn execute(batch: String, show_summary: bool) -> Result<(), Box<dyn Er
Ok(())
}

fn print_cves(cves: Value, show_summary: bool) {
fn print_cves(cves: Value, show_summary: bool, known_exploited_cves: Option<&[String]>) {
if show_summary {
for cve in get_cves_summary(&cves) {
for cve in get_cves_summary(&cves, known_exploited_cves) {
println!("{}", cve);
}
} else {
Expand Down
14 changes: 10 additions & 4 deletions crates/cli/src/command/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand All @@ -58,6 +62,7 @@ async fn handle_pkgs(
cwd: &Path,
category: &str,
pkgs: &[Package],
known_exploited_cves: &[String],
) -> Result<(), Box<dyn Error>> {
let pattern = get_grep_patterns(pkgs)?;
let matches = query(pattern, feed)?;
Expand All @@ -74,19 +79,20 @@ 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(
client: &Client,
cwd: &Path,
category: &str,
matches: &[String],
known_exploited_cves: &[String],
) -> Result<(), Box<dyn Error>> {
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![]
Expand Down
2 changes: 2 additions & 0 deletions crates/security-advisories/src/cve_summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use std::fmt;
#[derive(Serialize, Debug)]
pub struct CveSummary {
pub id: String,
pub is_known_exploited_vuln: Option<bool>,
pub description: String,
pub urls: Vec<String>,
}
Expand All @@ -20,6 +21,7 @@ impl CveSummary {
pub fn new(id: String, description: String, urls: Vec<String>) -> Self {
Self {
id,
is_known_exploited_vuln: None,
description,
urls,
}
Expand Down
12 changes: 10 additions & 2 deletions crates/security-advisories/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ pub async fn fetch_feed_checksum(client: &Client) -> Result<String, Box<dyn Erro
nvd::fetch_feed_checksum(client).await
}

pub fn get_cves_summary(full_cve_resp: &Value) -> Vec<CveSummary> {
nvd::get_cves_summary(full_cve_resp)
pub fn get_cves_summary(
full_cve_resp: &Value,
known_exploitable_cves: Option<&[String]>,
) -> Vec<CveSummary> {
nvd::get_cves_summary(full_cve_resp, known_exploitable_cves)
}

pub async fn download_cpe_match_feed(
Expand All @@ -42,3 +45,8 @@ pub async fn fetch_known_exploited_vulns(client: &Client) -> Result<Value, Box<d
log::info!("fetching known exploited vulnerabilities ...");
cisa::fetch_known_exploited_vulns(client).await
}

pub async fn fetch_known_exploited_cves(client: &Client) -> Result<Vec<String>, Box<dyn Error>> {
log::info!("fetching known exploited CVEs ...");
cisa::fetch_known_exploited_cves(client).await
}
23 changes: 23 additions & 0 deletions crates/security-advisories/src/service/cisa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,31 @@ pub async fn fetch_known_exploited_vulns(client: &Client) -> Result<Value, Box<d
let url = format!("{}/{}/{}", home_url, feed_path, known_exploited_vulns);
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(reqwest::header::ACCEPT, "application/json".parse()?);

log::info!("source: {}", home_url);
let res = client.get(&url).headers(headers).send().await?;

let json: Value = res.json().await?;
Ok(json)
}

pub async fn fetch_known_exploited_cves(client: &Client) -> Result<Vec<String>, Box<dyn Error>> {
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<String> {
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
}
53 changes: 42 additions & 11 deletions crates/security-advisories/src/service/nvd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,22 @@ pub async fn fetch_cves_by_cpe(client: &Client, cpe: &str) -> Result<Value, Box<
Ok(json)
}

pub fn get_cves_summary(full_cve_resp: &Value) -> Vec<CveSummary> {
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<CveSummary> {
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<String, Box<dyn Error>> {
Expand Down Expand Up @@ -109,13 +113,22 @@ fn get_checksum(meta: String) -> Result<String, io::Error> {
}
}

fn get_cve_summary(cve_data: &Value) -> Option<CveSummary> {
fn get_cve_summary(
cve_data: &Value,
known_exploitable_cves: Option<&[String]>,
) -> Option<CveSummary> {
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
}
Expand Down Expand Up @@ -147,11 +160,29 @@ fn get_cve_urls(id: &str, cve_data: &Value) -> Vec<String> {
#[cfg(test)]
mod tests {
use super::*;
use serde_json::from_str;

#[test]
fn it_should_filter_checksum() {
let checksum = "f283d332a8a66ecb23d49ee385ce42fca691e598da29475beb0b3556ab1fe02e";
let test_data = "lastModifiedDate:2022-01-22T00:10:15-05:00\r\nsize:601269818\r\nzipSize:21871360\r\ngzSize:21871224\r\nsha256:F283D332A8A66ECB23D49EE385CE42FCA691E598DA29475BEB0B3556AB1FE02E\r\n";

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));
}
}
5 changes: 3 additions & 2 deletions docs/COOKBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -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:
```
Expand All @@ -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}}
```


Expand Down

0 comments on commit c452ad9

Please sign in to comment.