#![deny(clippy::mem_forget)]
use ascii_table_rs::{AsciiTable, CellValue};
use fenir::cpe::Cpe;
use fenir::cve::{create_cve_from, cve_from_cwe, Cve, Score};
use fenir::cwe::show_weaknesses;
use fenir::database::{
execute_query, find_capec_by_id, find_cwe_by_id, parse_id_to_u32, MitreDefinition,
};
use fenir::facilities::{
build_dates_range, print_values, Reduce, FIELD_SEPARATOR_STRING, FIELD_SEPARATOR_STRING_LONG,
};
use fenir::facilities::{Cleaning, Uppercase, Wording};
use fenir::network::{check_mitre_data, check_nvd_api_key, check_nvd_api_key_warning};
use fenir::package::concepts::{filtering_cve, print_cve, simplify_list_content};
use fenir::package::errors::{write_error_message, write_error_message_and_exit};
use fenir::query::QueryType::{
ByCpeVulnerable, ByCve, ByCveCpeName, ByCveExploited, ByCveNew, ByCveUpdated, ByCwe,
};
use fenir::query::{build_query, create_query_dates, QueryType};
use fenir::{header, section, section_level, show_version};
use serde_json::Value;
use serde_json::Value::Null;
use std::io::stdout;
use std::process::exit;
use termint::enums::Color;
use termint::widgets::ToSpan;
use treelog::builder::TreeBuilder;
#[cfg(target_os = "linux")]
use crate::package::{get_changelog_for, get_package};
#[cfg(target_os = "linux")]
use fenir::os::{OSFamily, SupportedOs};
#[cfg(target_os = "linux")]
use fenir::package::concepts::show_cve_list;
#[cfg(target_os = "linux")]
use fenir::package::concepts::Changelog;
#[cfg(target_os = "linux")]
use fenir::package::errors::stop_if_invalid_package;
#[cfg(target_os = "linux")]
mod package {
use crate::package::os::{Debian, RedHat};
use core::str;
use fenir::facilities::{restore_current_dir_and_remove_temp_dir, show_command_result};
use fenir::os::{OSFamily, SupportedOs};
use fenir::package::concepts::{
changelog_result, default_package, Package, PackageStatus, Repository, Request,
};
use fenir::package::errors::{write_error_message, write_error_message_and_exit};
use std::env;
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::Path;
use std::process::{Child, Output, Stdio};
use std::string::FromUtf8Error;
pub mod os {
pub struct Debian;
pub struct RedHat;
}
impl Request for Debian {
fn ask_package(&self, package: &str, version: &str) -> Package {
let command_output: Result<String, FromUtf8Error> = if version.is_empty() {
let command_result = std::process::Command::new("/usr/bin/dpkg-query")
.arg("-W")
.arg(package)
.stderr(Stdio::null())
.output()
.expect("Fail to run dpkg-query command");
show_command_result(command_result)
} else if version.eq("next") {
let command_result = std::process::Command::new("/usr/bin/apt")
.arg("list")
.arg("--upgradable")
.arg(package)
.arg("-qq")
.stderr(Stdio::null())
.output()
.expect("Fail to run apt-get list");
show_command_result(command_result)
} else {
let cmd_package = std::process::Command::new("/usr/bin/dpkg-query")
.arg("-W")
.arg(package)
.stderr(Stdio::null())
.stdout(Stdio::piped())
.spawn()
.unwrap();
let cmd_version = grep_version(version, cmd_package);
show_command_result(cmd_version)
};
extract_package_result(command_output)
}
fn ask_changelog_package(&self, package: Package) -> String {
let changelog_command = std::process::Command::new("/usr/bin/apt")
.args([
"changelog",
format!("{}={}", package.name, package.version).as_str(),
])
.stderr(Stdio::null())
.output()
.expect("Fail to run apt command");
changelog_result(
package.clone(),
changelog_command,
self.read_changelog_files(package.clone()),
)
}
fn read_changelog_files(&self, package: Package) -> String {
let changelog_file = match get_debian_changelog_for("/usr/share/doc", &package) {
Ok(file) => file,
Err(err) => {
write_error_message(package.name.as_str(), Some(err));
return String::new();
}
};
let result = decode_changelog(changelog_file);
result.unwrap_or_else(|_| String::new())
}
}
fn decode_changelog(file: File) -> Result<String, &'static str> {
let gz_decoder = flate2::read::GzDecoder::new(BufReader::new(file));
match debian_changelog::ChangeLog::read(BufReader::new(gz_decoder)) {
Ok(changelog) => {
let _ = std::fs::remove_dir_all("/var/tmp/get-cve");
Ok(changelog.to_string())
}
_ => Err("Failed for changelog reading"),
}
}
impl Request for RedHat {
fn ask_package(&self, package: &str, version: &str) -> Package {
let command_output: Result<String, FromUtf8Error> = if version.is_empty() {
let command_result = std::process::Command::new("/usr/bin/rpm")
.arg("-qa")
.arg("--qf")
.arg("%{NAME}.%{ARCH}\t%{VERSION}-%{RELEASE}")
.arg(package)
.stderr(Stdio::null())
.output()
.expect("Error on rpm command running");
show_command_result(command_result)
} else {
let command_result = std::process::Command::new("/usr/bin/rpm")
.arg("-qa")
.arg("--qf")
.arg("%{NAME}.%{ARCH}\t%{VERSION}-%{RELEASE}")
.arg(package)
.stderr(Stdio::null())
.stdout(Stdio::piped())
.spawn()
.expect("Error on rpm command running");
let grep_result = grep_version(version, command_result);
show_command_result(grep_result)
};
extract_package_result(command_output)
}
fn ask_changelog_package(&self, package: Package) -> String {
let changelog_command = std::process::Command::new("/usr/bin/rpm")
.arg("-q")
.arg("--changelog")
.arg(package.name.as_str())
.stderr(Stdio::null())
.output()
.expect("Impossible to run rpm command for changelog");
changelog_result(
package.clone(),
changelog_command,
self.read_changelog_files(package.clone()),
)
}
fn read_changelog_files(&self, package: Package) -> String {
let package_name = extract_package_name(&package.name);
let changelog_path = format!("/usr/share/doc/{}", package_name);
match get_changelog_files(changelog_path) {
Some(changelog_files) => merge_files_content(changelog_files),
_ => String::new(),
}
}
}
fn extract_package_name(pkg_name: &str) -> String {
if pkg_name.contains('.') {
pkg_name.split('.').next().unwrap().to_string()
} else {
pkg_name.to_string()
}
}
fn get_changelog_files<P: AsRef<Path>>(path: P) -> Option<Vec<std::fs::DirEntry>> {
let dir = std::fs::read_dir(path).ok()?;
Some(dir.flatten().collect())
}
fn merge_files_content(files: Vec<std::fs::DirEntry>) -> String {
files
.iter()
.filter_map(|file| {
let path = file.path();
if path.is_file() {
let file = File::open(&path).ok()?;
let mut buffer = BufReader::new(file);
let mut content = String::new();
buffer.read_to_string(&mut content).ok()?;
Some(content)
} else {
None
}
})
.collect::<String>()
}
pub fn get_package(argument: String, version: Option<String>) -> Package {
if SupportedOs::supported_os() == OSFamily::Debian {
let repository = Repository { repos: Debian };
Request::ask_package(
&repository.repos,
argument.as_str(),
version.unwrap_or_default().as_str(),
)
} else if SupportedOs::supported_os() == OSFamily::RedHat {
let repository = Repository { repos: RedHat };
Request::ask_package(
&repository.repos,
argument.as_str(),
version.unwrap_or_default().as_str(),
)
} else {
default_package()
}
}
pub fn get_changelog_for(package: &Package) -> String {
if SupportedOs::supported_os() == OSFamily::Debian {
let repository = Repository { repos: Debian };
Request::ask_changelog_package(&repository.repos, package.clone())
} else {
let repository = Repository { repos: RedHat };
Request::ask_changelog_package(&repository.repos, package.clone())
}
}
fn get_debian_changelog_for(path: &str, package: &Package) -> Result<File, &'static str> {
match package.status {
PackageStatus::Uninstalled => get_next_changelog_file(&mut package.clone()),
_ => {
let changelogs = [
format!("{}/{}/changelog.Debian.gz", path, package.name),
format!("{}/{}/changelog.gz", path, package.name),
];
let mut idx = 0usize;
let mut result = Err("No alternative changelog");
while idx < changelogs.len() && result.is_err() {
let changelog = changelogs[idx].clone();
idx += 1;
result = match File::open(changelog) {
Ok(file) => Ok(file),
_ => Err("No alternative changelog"),
};
}
result
}
}
}
fn get_next_changelog_file(package: &mut Package) -> Result<File, &'static str> {
let current_dir = env::current_dir();
let current_dir_clone = env::current_dir();
let _ = std::fs::create_dir("/var/tmp/get-cve");
env::set_current_dir("/var/tmp/get-cve").expect("Error setting tmp directory");
println!("Download {package}");
let command_download = std::process::Command::new("/usr/bin/apt-get")
.arg("download")
.arg(package.name.as_str())
.stderr(Stdio::null())
.current_dir("/var/tmp/get-cve")
.output()
.expect("Error on apt-get command running");
if command_download.status.success() {
println!("Decompress {package}");
let package_deb = glob::glob(format!("{}*.deb", package.name).as_str());
if let Err(_item) = package_deb {
restore_current_dir_and_remove_temp_dir(current_dir, "get-cve");
write_error_message_and_exit(
"Error, problem on packages into temp \
directory",
None,
);
} else {
let package_name = package_deb.unwrap().next().unwrap();
let command_decompress = std::process::Command::new("/usr/bin/dpkg")
.current_dir("/var/tmp/get-cve/")
.arg("-x")
.arg(package_name.unwrap())
.arg(".")
.output()
.expect("Error on decompress command running");
if !command_decompress.status.success() {
restore_current_dir_and_remove_temp_dir(current_dir_clone, "get-cve");
write_error_message_and_exit(
"Decompression error",
Option::from(
String::from_utf8(command_decompress.stderr)
.unwrap()
.as_str(),
),
);
} else {
println!("Search changelog for {package}");
package.status = PackageStatus::Installed;
return get_debian_changelog_for("/var/tmp/get-cve/usr/share/doc", package);
}
}
}
Err("Error on package download")
}
fn extract_package_result(command_output: Result<String, FromUtf8Error>) -> Package {
let output_string = command_output.unwrap();
if output_string.contains("upgradable") {
let elements = output_string.split("/").collect::<Vec<&str>>();
let version_elements = elements[1].split(' ').collect::<Vec<&str>>();
Package {
name: String::from(elements[0]),
version: String::from(version_elements[1].trim()),
status: PackageStatus::Uninstalled,
}
} else {
let elements: Vec<&str> = output_string.as_str().split('\t').collect();
if elements.len() < 2 {
write_error_message_and_exit("Package not found", None);
default_package()
} else {
Package {
name: String::from(elements[0]),
version: String::from(elements[1].trim()),
status: PackageStatus::Installed,
}
}
}
}
fn grep_version(version: &str, pipe_package: Child) -> Output {
std::process::Command::new("/usr/bin/grep")
.arg("-w")
.arg(version)
.stdin(Stdio::from(pipe_package.stdout.unwrap()))
.stderr(Stdio::null())
.output()
.expect("Error on grep on version")
}
}
trait CvssColor {
fn cvss_color(&self) -> (Color, Color);
}
impl CvssColor for f64 {
fn cvss_color(&self) -> (Color, Color) {
match *self {
0.0 => (Color::Default, Color::Default),
value if value > 0.0 && value < 4.0 => {
(Color::Rgb(255, 255, 255), Color::Rgb(95, 177, 88))
}
value if (4.0..7.0).contains(&value) => (Color::Black, Color::Rgb(249, 238, 86)),
value if (7.0..9.0).contains(&value) => {
(Color::Rgb(255, 255, 255), Color::Rgb(232, 152, 63))
}
_ => (Color::Rgb(255, 255, 255), Color::Rgb(176, 54, 52)),
}
}
}
#[cfg(target_os = "linux")]
pub fn special_options(args: &[String]) {
let mut filter: Option<&String> = None;
if let Some(idx_filter) = args.iter().position(|a| a == "--filter") {
filter = args.get(idx_filter + 1);
}
let (package_name, mut package_version) = extract_argument(args[0].clone());
if SupportedOs::supported_os() == OSFamily::Debian && args.iter().any(|a| a == "--next") {
package_version = Option::from(String::from("next"));
}
let package = get_package(package_name, package_version);
stop_if_invalid_package(&package);
let changelog_content = get_changelog_for(&package);
let changelog_cve_list = filtering_cve(package.cve_list(&changelog_content), filter);
if args.iter().any(|a| a == "--long" || a == "-L") {
if check_nvd_api_key().is_none() {
check_nvd_api_key_warning();
}
let mut cve_list = Vec::new();
for cve_value in changelog_cve_list {
eprint!(
"Search CVE list for package: {package}. It will take a while... {}\r",
cve_value.reference
);
let result = execute_query(build_query(ByCve, cve_value.reference.as_str()));
if result != Null {
let cve = create_cve_from(&result["vulnerabilities"][0]);
cve_list.push(cve);
}
}
show_cves(args, &mut cve_list);
} else {
show_cve_list(changelog_cve_list, package);
}
}
pub fn common_options(args: &mut [String]) {
if args.iter().any(|a| a == "version" || a == "v") {
show_version(env!("CARGO_PKG_VERSION"));
exit(0);
}
#[cfg(not(target_os = "linux"))]
if check_nvd_api_key().is_none() {
check_nvd_api_key_warning();
}
if args.iter().any(|a| a == "check" || a == "c") {
check_mitre_data(Cve::define());
exit(0);
}
if args.iter().any(|a| a == "e" || a == "exploited") {
let mut cve_list = standard_or_filtered_list(args, extract_cve_list("", ByCveExploited));
show_cves(args, &mut cve_list);
exit(0);
}
if let Some(cpe_string) = args
.iter()
.position(|a| a == "for-cpe" || a == "f")
.and_then(|i| args.get(i + 1))
{
let cpe = Cpe::from(cpe_string);
let mut result = run_search_cpe(cpe, args);
show_cves(args, &mut result);
exit(0);
}
if let Some(new_option) = args.iter().position(|a| a == "new" || a == "n") {
let dates_range = build_dates_range(Some(new_option), args);
let mut cve_list =
standard_or_filtered_list(args, extract_cve_list(dates_range.as_str(), ByCveNew));
show_cves(args, &mut cve_list);
exit(0);
}
if let Some(updated_option) = args.iter().position(|a| a == "updated" || a == "u") {
let dates_range = build_dates_range(Some(updated_option), args);
let mut cve_list =
standard_or_filtered_list(args, extract_cve_list(dates_range.as_str(), ByCveUpdated));
show_cves(args, &mut cve_list);
exit(0);
}
if let Some(cwe_option) = args.iter().position(|a| a == "cwe" || a == "w") {
run_search_cwe(args, Some(cwe_option));
exit(0);
}
if let Some(search_option) = args.iter().position(|a| a == "search" || a == "s") {
let result = run_search_cve_string(args, Some(search_option));
let mut values = standard_or_filtered_list(args, result);
show_cves(args, &mut values);
exit(0);
}
let key_cve = args
.iter()
.position(|a| a.to_ascii_lowercase().contains("cve"));
if let Some(pos) = key_cve
&& pos == 0
{
run_search_cve(args, key_cve);
exit(0);
}
}
fn standard_or_filtered_list(args: &mut [String], cve_list: Vec<Cve>) -> Vec<Cve> {
match args.iter().position(|a| a == "--filter") {
Some(pos) => filtering_cve(cve_list, args.get(pos + 1)),
_ => cve_list,
}
}
fn show_cves(args: &[String], cve_list: &mut [Cve]) {
println!();
if !args.iter().any(|a| a == "--long" || a == "-L") {
show_cve_values(cve_list);
} else {
if let Some(pos) = args.iter().position(|a| a == "--sort_by")
&& let Some(criteria) = args.get(pos + 1)
{
let mut sort_score_by = String::new();
if let Some(sorting_criteria) = args.get(pos + 2) {
sort_score_by = sorting_criteria.clone();
}
match criteria.as_str() {
"score" => match sort_score_by.as_str() {
"v4" => cve_list.sort_by(|a, b| {
b.score_v4
.clone()
.unwrap()
.value()
.total_cmp(&a.score_v4.clone().unwrap().value())
}),
_ => cve_list.sort_by(|a, b| {
b.score_v3
.clone()
.unwrap()
.value()
.total_cmp(&a.score_v3.clone().unwrap().value())
}),
},
"description" => cve_list.sort_by(|a, b| {
a.description
.clone()
.unwrap()
.cmp(&b.description.clone().unwrap())
}),
"cpe" => cve_list
.sort_by(|a, b| a.cpe_id.clone().unwrap().cmp(&b.cpe_id.clone().unwrap())),
"weaknesses" => cve_list.sort_by(|a, b| {
b.weaknesses
.clone()
.unwrap()
.cmp(&a.weaknesses.clone().unwrap())
}),
_ => (),
};
}
if args.iter().any(|a| a == "--to_csv") {
show_as_csv(cve_list, args.iter().any(|a| a == "with_headers"));
} else {
show_as_table(cve_list);
}
}
}
fn show_as_csv(cve_list: &mut [Cve], with_headers: bool) {
if !cve_list.is_empty() {
let mut writer = csv::Writer::from_writer(stdout());
if with_headers {
writer
.write_record([
"cve",
"description",
"score v3",
"level v3",
"score v4",
"level v4",
"cpe",
"weaknesses",
])
.expect("Failed to write CSV headers");
}
cve_list.iter_mut().for_each(|cve| {
writer
.write_record([
cve.reference.clone(),
cve.description.clone().unwrap(),
cve.score_v3.clone().unwrap().value().to_string(),
cve.score_v3.clone().unwrap().label().to_string(),
cve.score_v4.clone().unwrap().value().to_string(),
cve.score_v4.clone().unwrap().label().to_string(),
cve.cpe_id.clone().unwrap(),
cve.weaknesses.clone().unwrap(),
])
.expect("Failed to write CSV content");
});
writer.flush().expect("Failed to flush CSV writer");
}
}
fn show_cve_values(cve_list: &[Cve]) {
cve_list.iter().for_each(|c| println!("{c}"));
}
fn run_search_cve_string(args: &mut [String], position: Option<usize>) -> Vec<Cve> {
let result = match args.iter().position(|a| a == "--strict" || a == "-S") {
Some(pos) => {
let values = args.get(pos + 1);
if let Some(item) = values {
execute_query(build_query(QueryType::ByCveSearchStrict, item.as_str()))
} else {
write_error_message_and_exit("Missing criteria", None);
Null
}
}
_ => match args.get(position.unwrap() + 1) {
Some(values) => {
execute_query(build_query(QueryType::ByCveSearchString, values.as_str()))
}
_ => {
write_error_message_and_exit("Missing criteria", None);
Null
}
},
};
let mut cves = cve_from_new_query(&result);
simplify_list_content(&mut cves);
cves
}
#[cfg(target_os = "linux")]
fn extract_argument(argument: String) -> (String, Option<String>) {
let package_name: String;
let mut package_version: Option<String> = None;
if argument.contains("=") {
let component: Vec<&str> = argument.split("=").collect();
package_name = String::from(component[0]);
package_version = Some(String::from(component[1]));
} else {
package_name = argument.clone();
}
(package_name, package_version)
}
fn extract_cve_list(criteria: &str, cve_type: QueryType) -> Vec<Cve> {
let result = execute_query(build_query(cve_type.clone(), criteria));
if !result.is_null() && result["resultsPerPage"] != 0 {
let mut start_index = result["startIndex"].clone().as_u64().unwrap();
let total_results = result["totalResults"].clone().as_u64().unwrap();
let mut cve_list = cve_from_new_query(&result);
start_index += cve_list.len() as u64;
while cve_list.len() < (total_results as usize) {
let query = format!(
"{}&startIndex={}",
build_query(cve_type.clone(), criteria).unwrap(),
start_index
);
let result = execute_query(Ok(query));
let mut cves = cve_from_new_query(&result);
start_index += cves.len() as u64;
cve_list.append(&mut cves);
}
simplify_list_content(&mut cve_list);
return cve_list;
} else {
write_error_message_and_exit("No result found", None)
}
Vec::new()
}
fn show_as_table(cve_list: &[Cve]) {
let mut table = AsciiTable::new("CVE list");
table.set_headers(vec![
"CVE",
"Description",
"Score v3",
"Score v4",
"CPE",
"Weaknesses",
]);
cve_list.iter().for_each(|cve| {
let description = cve.description.clone().unwrap_or(String::from("None"));
let cpe_id = cve.cpe_id.clone().unwrap_or(String::from("None"));
let weaknesses = cve.weaknesses.clone().unwrap_or(String::from("None"));
let score_v3 = format_score(cve.score_v3.clone().unwrap());
let score_v4 = format_score(cve.score_v4.clone().unwrap());
table.add_row(vec![
CellValue::Str(cve.reference.clone()),
CellValue::Str(description.replace("\n", " ").reduce(70)),
CellValue::Str(score_v3.to_string()),
CellValue::Str(score_v4.to_string()),
CellValue::Str(cpe_id.reduce(40)),
CellValue::Str(weaknesses.reduce(15)),
])
});
table.render()
}
fn format_score(cve_score: Score) -> String {
let (color_fg, color_bg) = cve_score.value().cvss_color();
format!(
"{:^18}",
format!(
" {} - {} ",
format!("{0:.1}", cve_score.value()),
cve_score.label()
)
)
.fg(color_fg)
.bg(color_bg)
.to_string()
}
fn run_search_cpe(cpe: Cpe, args: &mut [String]) -> Vec<Cve> {
if let Some(new_date_option) = args.iter().position(|a| a == "--new") {
let dates = build_dates_range(Some(new_date_option), args);
let query_dates = create_query_dates(ByCveNew, dates.as_str());
let cpe_query = build_query(ByCveCpeName, cpe.name().as_str());
standard_or_filtered_list(
args,
extract_cve_list(
format!("{}&{}", cpe_query.unwrap(), query_dates).as_str(),
ByCveCpeName,
),
)
} else if let Some(updated_date_option) = args.iter().position(|a| a == "--updated") {
let dates = build_dates_range(Some(updated_date_option), args);
let query_dates = create_query_dates(ByCveUpdated, dates.as_str());
let cpe_query = build_query(ByCveCpeName, cpe.name().as_str());
standard_or_filtered_list(
args,
extract_cve_list(
format!("{}&{}", cpe_query.unwrap(), query_dates).as_str(),
ByCveCpeName,
),
)
} else if args.iter().any(|a| a == "--vul" || a == "-v") {
standard_or_filtered_list(args, extract_cve_list(cpe.name().as_str(), ByCpeVulnerable))
} else {
standard_or_filtered_list(args, extract_cve_list(cpe.name().as_str(), ByCveCpeName))
}
}
fn run_search_cve(args: &mut [String], key_cve: Option<usize>) {
if let Some(cve_value) = args.get(key_cve.unwrap()) {
let result = if cve_value == &String::from("--cpe") {
println!("Searching...");
execute_query(build_query(ByCveCpeName, args[2].as_str()))
} else {
execute_query(build_query(ByCve, cve_value.as_str()))
};
if args.iter().any(|a| a == "--long" || a == "-L") {
let mut cve = vec![create_cve_from(&result["vulnerabilities"][0])];
if args.iter().any(|a| a == "--to_csv") {
show_as_csv(&mut cve, args.iter().any(|a| a == "with_headers"));
} else {
show_as_table(&cve);
}
return;
}
if result["resultsPerPage"] == 0 || result == Null {
write_error_message_and_exit("CVE not found", None);
}
if args.iter().any(|a| a == "--schema" || a == "-S") {
show_schema(&result);
return;
}
println!("{}", header!(cve_value));
if cve_value == &String::from("--cpe") {
cve_from_new_query(&result).iter().for_each(print_cve);
return;
}
let mut cumulative_args = 0;
if args.iter().any(|a| a == "--score" || a == "-s") {
let include_data = args.iter().any(|a| a == "--data" || a == "-D");
if include_data {
cumulative_args += 1;
}
if args.iter().any(|a| a == "v3") {
CveDisplayType::ByScores.show(&result, Some("v3"), Some(include_data));
cumulative_args += 1;
} else if args.iter().any(|a| a == "v4") {
CveDisplayType::ByScores.show(&result, Some("v4"), Some(include_data));
cumulative_args += 1;
} else {
CveDisplayType::ByScores.show(&result, Some("all"), Some(include_data));
cumulative_args += 1;
}
}
if args.iter().any(|a| a == "--cwe" || a == "-w") {
CveDisplayType::ByCwe.show(&result, None, None);
cumulative_args += 1;
}
if args.iter().any(|a| a == "--lang" || a == "-l") {
CveDisplayType::ByLanguages.show(&result, None, None);
cumulative_args += 1;
}
if let Some(key_description) = args.iter().position(|a| a == "--desc" || a == "-d") {
let key_lang = args.get(key_description + 1).map(|lang| lang.as_str());
CveDisplayType::ByDescription.show(&result, key_lang, None);
cumulative_args += 1;
}
if args.iter().any(|a| a == "--doc" || a == "-K") {
CveDisplayType::ByDocumentations.show(&result, None, None);
cumulative_args += 1;
}
if args.iter().any(|a| a == "--cpe" || a == "-f") {
CveDisplayType::ByCpe.show(&result, None, None);
cumulative_args += 1;
}
if cumulative_args == 0 {
CveDisplayType::All.show(&result, Some("all"), Some(true));
}
}
}
fn show_schema(result: &Value) {
let cve = create_cve_from(&result["vulnerabilities"][0]);
let mut builder = TreeBuilder::new();
let cve_node = builder.node(section!(cve.reference, Color::Green).to_string());
if let Some(cwe_list) = cve.weaknesses {
let cwe_id = parse_id_to_u32(
cwe_list
.replace("CWE-", "")
.replace(", ", FIELD_SEPARATOR_STRING),
);
cwe_id.iter().for_each(|id| {
if let Some(cwe) = find_cwe_by_id(*id) {
let cwe_node = cve_node.node(
format!("CWE-{} - {}", cwe.id, cwe.name)
.fg(Color::Red)
.to_string(),
);
let capec_id = parse_id_to_u32(
cwe.clone()
.attacks
.replace("CAPEC-", "")
.replace(", ", FIELD_SEPARATOR_STRING),
);
if !capec_id.is_empty() {
capec_id.iter().for_each(|ca_id| {
if let Some(capec) = find_capec_by_id(*ca_id) {
cwe_node.leaf(
format!("CAPEC-{} - {}", capec.id, capec.name)
.fg(Color::DarkCyan)
.to_string(),
);
}
});
}
cwe_node.end();
}
});
} else {
write_error_message_and_exit("No schemas found", None);
}
let tree = builder.build();
println!("{}", tree.render_to_string());
}
fn show_data(result: &Value, level: &str) {
let cvss_data = match level {
"v3" => get_cvss3_data(result),
"v4" => get_cvss4_data(result),
_ => Null,
};
if !cvss_data.is_null() {
let map = cvss_data.as_object().unwrap();
let filtered = map
.iter()
.filter(|(k, _)| **k != "baseScore" && **k != "baseSeverity" && **k != "version");
filtered.for_each(|(k, v)| {
let str_value = match k.to_string().contains("vectorString") {
false => v.to_string().wording().replace("_", " "),
true => v.to_string(),
};
println!(
"{:>7} {}: {}",
"-",
k.wording(),
String::cleaning(str_value)
);
});
} else {
write_error_message("CVSS data not found", Some(level));
}
}
pub fn show_languages(result: &Value) {
if let Some(descriptions) = get_descriptions_section(result) {
section_level!(2, "Languages");
for l in descriptions {
print_values(&[l["lang"].clone()]);
}
}
}
pub fn show_description(result: &Value, key_lang: Option<&String>) {
section_level!(2, "Description");
let description = if let Some(lang) = key_lang {
extract_description(result, Some(lang.as_str()))
} else {
extract_description(result, None)
};
print_values(&[description]);
println!();
}
fn show_documentations(result: &Value) {
if let Some(documentations) = get_documentation_section(result) {
section_level!(2, "Documentations");
documentations.iter().for_each(|doc| {
print_values(std::slice::from_ref(doc));
if doc != documentations.last().unwrap() {
println!();
}
});
} else {
write_error_message_and_exit("Documentation not found", None);
}
}
fn extract_description(value: &Value, lang: Option<&str>) -> Value {
if let Some(lang) = lang
&& let Some(languages) = get_descriptions_section(value)
{
for l in languages {
if l["lang"].as_str() == Some(lang) {
return l["value"].clone();
}
}
}
extract_default_description(value)
}
fn extract_default_description(value: &Value) -> Value {
value["vulnerabilities"][0]["cve"]["descriptions"][0]["value"].clone()
}
fn get_descriptions_section(value: &Value) -> Option<&Vec<Value>> {
value["vulnerabilities"][0]["cve"]["descriptions"].as_array()
}
fn get_documentation_section(value: &Value) -> Option<&Vec<Value>> {
value["vulnerabilities"][0]["cve"]["references"].as_array()
}
pub fn run_search_cwe(args: &mut [String], cwe_option: Option<usize>) {
if let Some(cwe_value) = args.get(cwe_option.unwrap() + 1) {
let result = execute_query(build_query(ByCwe, cwe_value.as_str()));
let mut cve_list = standard_or_filtered_list(args, cve_from_cwe(&result));
simplify_list_content(&mut cve_list);
if cve_list.is_empty() {
write_error_message_and_exit("CVE not found", None);
}
show_cves(args, &mut cve_list);
}
}
enum CveDisplayType {
All,
ByCpe,
ByCwe,
ByData,
ByDescription,
ByDocumentations,
ByLanguages,
ByScores,
}
impl CveDisplayType {
fn show(&self, value: &Value, argument: Option<&str>, flag: Option<bool>) {
match self {
CveDisplayType::All => {
show_description(value, None);
show_scores(value, argument.unwrap().trim(), flag.unwrap_or(false));
show_documentations(value);
println!();
show_cwe_values(value);
println!();
show_cpe_values(value);
}
CveDisplayType::ByCpe => {
show_cpe_values(value);
}
CveDisplayType::ByCwe => {
show_cwe_values(value);
}
CveDisplayType::ByData => {
show_data(value, argument.unwrap().trim());
}
CveDisplayType::ByDescription => {
show_description(value, None);
}
CveDisplayType::ByDocumentations => {
show_documentations(value);
}
CveDisplayType::ByLanguages => {
show_languages(value);
}
CveDisplayType::ByScores => {
show_scores(value, argument.unwrap().trim(), flag.unwrap_or(false));
}
}
}
}
fn show_cpe_values(value: &Value) {
section_level!(2, "CPE");
let values = extract_cpe_id_list(value);
if let Some(values) = values {
values.iter().for_each(|v| {
let mut result = v.as_object().unwrap().clone();
result.remove("matchCriteriaId");
print_values(&[Value::Object(result)]);
println!();
})
} else {
println!("None");
}
}
fn extract_cpe_id_list(value: &Value) -> Option<&Vec<Value>> {
value["vulnerabilities"][0]["cve"]["configurations"][0]["nodes"][0]["cpeMatch"].as_array()
}
pub fn show_cwe_values(result: &Value) {
let values = extract_cwe(result);
if !values.is_empty() {
let slice_values: Vec<_> = values
.iter()
.map(|v| String::cleaning(v.to_string()))
.collect();
let mut str_values = slice_values.join(FIELD_SEPARATOR_STRING_LONG);
str_values = str_values.replace("CWE-", FIELD_SEPARATOR_STRING_LONG);
show_weaknesses("weaknesses", str_values);
} else {
println!("None");
}
}
fn extract_cwe(value: &Value) -> Vec<Value> {
let mut result: Vec<Value> = vec![];
let binding = value["vulnerabilities"][0]["cve"]["weaknesses"].clone();
let elements_option = binding.as_array();
if let Some(elements) = elements_option {
elements.iter().for_each(|value| {
let vals = value["description"].as_array().unwrap();
vals.iter()
.for_each(|val| result.push(val["value"].clone()))
});
}
result.dedup();
result
}
pub fn show_scores(result: &Value, level: &str, include_data: bool) {
if level != "all" {
let cvss_data = match level {
"v3" => get_cvss3_data(result),
"v4" => get_cvss4_data(result),
_ => Null,
};
section_level!(2, "Scores");
if !cvss_data.is_null() {
let score = cvss_data["baseScore"].as_f64().unwrap();
let (color_fg, color_bg) = score.cvss_color();
println!(
"{:>6} Score {}: {:.1} - {}",
"-",
level,
score,
format!(
" {} ",
String::cleaning(cvss_data["baseSeverity"].to_string())
)
.fg(color_fg)
.bg(color_bg),
);
println!();
if include_data {
section_level!(3, "Data");
CveDisplayType::ByData.show(result, Some(level), None);
println!();
}
} else {
write_error_message("Score not found", Some(level));
println!();
}
} else if level == "all" {
show_scores(result, "v3", include_data);
show_scores(result, "v4", include_data);
} else {
write_error_message("Score type not found", Some(level));
}
}
fn get_cvss4_data(result: &Value) -> Value {
get_cvss_scoring(result, "V40")
}
fn get_cvss3_data(result: &Value) -> Value {
get_cvss_scoring(result, "V31")
}
fn get_cvss_scoring(result: &Value, level: &str) -> Value {
result["vulnerabilities"][0]["cve"]["metrics"][format!("cvssMetric{}", level)][0]["cvssData"]
.clone()
}
pub fn cve_from_new_query(values: &Value) -> Vec<Cve> {
if let Some(vulnerabilities) = values["vulnerabilities"].as_array() {
let mut results: Vec<Cve> = vec![];
vulnerabilities
.iter()
.for_each(|v| results.push(create_cve_from(v)));
results
} else {
vec![]
}
}
#[cfg(test)]
mod tests {
use crate::{extract_cwe, extract_description};
use fenir::cve::{create_cve_from, cve_from_cwe};
use fenir::database::execute_query;
use fenir::query::build_query;
use fenir::query::QueryType::{ByCve, ByCwe};
use serde_json::Value;
use std::thread::sleep;
use std::time::Duration;
use utmt::assert_all;
fn setup() {
let waiting = Duration::from_millis(30);
sleep(waiting);
}
#[test]
fn extract_description_default_lang_valid() {
setup();
let result = execute_query(build_query(ByCve, "CVE-2024-6387"));
assert_default_description(&result, None);
}
fn assert_default_description(result: &Value, option: Option<&str>) {
assert!(
extract_description(&result, option)
.to_string()
.contains("A security regression (CVE-2006-5051) was discovered ")
);
}
#[test]
fn extract_description_invalid_default() {
setup();
let result = execute_query(build_query(ByCve, "CVE-2024-6387"));
assert_default_description(&result, Option::from("ff"));
}
#[test]
fn extract_description_es_lang_valid() {
setup();
let result = execute_query(build_query(ByCve, "CVE-2024-6387"));
assert!(extract_description(&result, Option::from("es")).to_string().contains("Se encontró una condición de ejecución del controlador de señales en el servidor de OpenSSH"));
}
#[test]
fn extract_cwe_valid_cve_non_empty() {
setup();
let result = execute_query(build_query(ByCve, "CVE-2024-6387"));
assert!(!extract_cwe(&result).is_empty());
}
#[test]
fn create_cve_from_value() {
setup();
let cve = create_cve_from(&execute_query(build_query(ByCve, "CVE-2024-6387")));
assert_all!(
cve.cpe_id.is_some(),
cve.description.is_some(),
cve.score_v3.is_some(),
!cve.reference.is_empty(),
cve.weaknesses.is_some()
);
}
#[test]
fn extract_cwe_invalid_cve_empty() {
setup();
let result = execute_query(build_query(ByCve, "CVE-1970-0000"));
assert!(extract_cwe(&result).is_empty());
}
#[test]
fn cve_from_cwe_result_valid_cwe_not_empty() {
setup();
let result = execute_query(build_query(ByCwe, "CWE-287"));
assert!(!cve_from_cwe(&result).is_empty());
}
}