1#![deny(clippy::mem_forget)]
2
3use ascii_table_rs::{AsciiTable, CellValue};
4use fenir::cpe::Cpe;
5use fenir::cve::{create_cve_from, cve_from_cwe, Cve, Score};
6use fenir::cwe::show_weaknesses;
7use fenir::database::{
8 execute_query, find_capec_by_id, find_cwe_by_id, parse_id_to_u32, MitreDefinition,
9};
10use fenir::facilities::{
11 build_dates_range, print_values, Reduce, FIELD_SEPARATOR_STRING, FIELD_SEPARATOR_STRING_LONG,
12};
13use fenir::facilities::{Cleaning, Uppercase, Wording};
14use fenir::network::{check_mitre_data, check_nvd_api_key, check_nvd_api_key_warning};
15use fenir::package::concepts::{filtering_cve, print_cve, simplify_cve_list_content};
16use fenir::package::errors::{write_error_message, write_error_message_and_exit};
17use fenir::query::QueryType::{
18 ByCpeVulnerable, ByCve, ByCveCpeName, ByCveExploited, ByCveNew, ByCveUpdated, ByCwe,
19};
20use fenir::query::{build_query, create_query_dates, QueryType};
21use fenir::{header, section, section_level, show_version};
22use serde_json::Value;
23use serde_json::Value::Null;
24use std::io::stdout;
25use std::process::exit;
26use termint::enums::Color;
27use termint::widgets::ToSpan;
28use treelog::builder::TreeBuilder;
29
30#[cfg(target_os = "linux")]
31use crate::package::{get_changelog_for, get_package};
32
33#[cfg(target_os = "linux")]
34use fenir::os::{OSFamily, SupportedOs};
35
36#[cfg(target_os = "linux")]
37use fenir::package::concepts::show_cve_list;
38
39#[cfg(target_os = "linux")]
40use fenir::package::concepts::Changelog;
41
42#[cfg(target_os = "linux")]
43use fenir::package::errors::stop_if_invalid_package;
44
45#[cfg(target_os = "linux")]
47mod package {
48 use crate::package::os::{Debian, RedHat};
49 use core::str;
50 use fenir::facilities::{restore_current_dir_and_remove_temp_dir, show_command_result};
51 use fenir::os::{OSFamily, SupportedOs};
52 use fenir::package::concepts::{
53 changelog_result, default_package, Package, PackageStatus, Repository, Request,
54 };
55 use fenir::package::errors::{write_error_message, write_error_message_and_exit};
56 use std::env;
57 use std::fs::File;
58 use std::io::{BufReader, Read};
59 use std::path::Path;
60 use std::process::{Child, Output, Stdio};
61 use std::string::FromUtf8Error;
62
63 pub mod os {
65 pub struct Debian;
67
68 pub struct RedHat;
70 }
71
72 impl Request for Debian {
73 fn ask_package(&self, package: &str, version: &str) -> Package {
74 let command_output: Result<String, FromUtf8Error> = if version.is_empty() {
75 let command_result = std::process::Command::new("/usr/bin/dpkg-query")
76 .arg("-W")
77 .arg(package)
78 .stderr(Stdio::null())
79 .output()
80 .expect("Fail to run dpkg-query command");
81
82 show_command_result(command_result)
83 } else if version.eq("next") {
84 let command_result = std::process::Command::new("/usr/bin/apt")
85 .arg("list")
86 .arg("--upgradable")
87 .arg(package)
88 .arg("-qq")
89 .stderr(Stdio::null())
90 .output()
91 .expect("Fail to run apt-get list");
92
93 show_command_result(command_result)
94 } else {
95 let cmd_package = std::process::Command::new("/usr/bin/dpkg-query")
96 .arg("-W")
97 .arg(package)
98 .stderr(Stdio::null())
99 .stdout(Stdio::piped())
100 .spawn()
101 .unwrap();
102
103 let cmd_version = grep_version(version, cmd_package);
104
105 show_command_result(cmd_version)
106 };
107
108 extract_package_result(command_output)
109 }
110
111 fn ask_changelog_package(&self, package: Package) -> String {
112 let changelog_command = std::process::Command::new("/usr/bin/apt")
113 .args([
114 "changelog",
115 format!("{}={}", package.name, package.version).as_str(),
116 ])
117 .stderr(Stdio::null())
118 .output()
119 .expect("Fail to run apt command");
120
121 changelog_result(
122 package.clone(),
123 changelog_command,
124 self.read_changelog_files(package.clone()),
125 )
126 }
127
128 fn read_changelog_files(&self, package: Package) -> String {
129 let changelog_file = match get_debian_changelog_for("/usr/share/doc", &package) {
130 Ok(file) => file,
131 Err(err) => {
132 write_error_message(package.name.as_str(), Some(err));
133 return String::new();
134 }
135 };
136
137 let result = decode_changelog(changelog_file);
138 result.unwrap_or_else(|_| String::new())
139 }
140 }
141
142 fn decode_changelog(file: File) -> Result<String, &'static str> {
166 let gz_decoder = flate2::read::GzDecoder::new(BufReader::new(file));
167 match debian_changelog::ChangeLog::read(BufReader::new(gz_decoder)) {
168 Ok(changelog) => {
169 let _ = std::fs::remove_dir_all("/var/tmp/get-cve");
170 Ok(changelog.to_string())
171 }
172 _ => Err("Failed for changelog reading"),
173 }
174 }
175
176 impl Request for RedHat {
177 fn ask_package(&self, package: &str, version: &str) -> Package {
178 let command_output: Result<String, FromUtf8Error> = if version.is_empty() {
179 let command_result = std::process::Command::new("/usr/bin/rpm")
180 .arg("-qa")
181 .arg("--qf")
182 .arg("%{NAME}.%{ARCH}\t%{VERSION}-%{RELEASE}")
183 .arg(package)
184 .stderr(Stdio::null())
185 .output()
186 .expect("Error on rpm command running");
187
188 show_command_result(command_result)
189 } else {
190 let command_result = std::process::Command::new("/usr/bin/rpm")
191 .arg("-qa")
192 .arg("--qf")
193 .arg("%{NAME}.%{ARCH}\t%{VERSION}-%{RELEASE}")
194 .arg(package)
195 .stderr(Stdio::null())
196 .stdout(Stdio::piped())
197 .spawn()
198 .expect("Error on rpm command running");
199
200 let grep_result = grep_version(version, command_result);
201 show_command_result(grep_result)
202 };
203
204 extract_package_result(command_output)
205 }
206
207 fn ask_changelog_package(&self, package: Package) -> String {
208 let changelog_command = std::process::Command::new("/usr/bin/rpm")
209 .arg("-q")
210 .arg("--changelog")
211 .arg(package.name.as_str())
212 .stderr(Stdio::null())
213 .output()
214 .expect("Impossible to run rpm command for changelog");
215
216 changelog_result(
217 package.clone(),
218 changelog_command,
219 self.read_changelog_files(package.clone()),
220 )
221 }
222
223 fn read_changelog_files(&self, package: Package) -> String {
224 let package_name = extract_package_name(&package.name);
225 let changelog_path = format!("/usr/share/doc/{}", package_name);
226
227 match get_changelog_files(changelog_path) {
228 Some(changelog_files) => merge_files_content(changelog_files),
229 _ => String::new(),
230 }
231 }
232 }
233
234 fn extract_package_name(pkg_name: &str) -> String {
250 if pkg_name.contains('.') {
251 pkg_name.split('.').next().unwrap().to_string()
252 } else {
253 pkg_name.to_string()
254 }
255 }
256
257 fn get_changelog_files<P: AsRef<Path>>(path: P) -> Option<Vec<std::fs::DirEntry>> {
270 let dir = std::fs::read_dir(path).ok()?;
271 Some(dir.flatten().collect())
272 }
273
274 fn merge_files_content(files: Vec<std::fs::DirEntry>) -> String {
291 files
292 .iter()
293 .filter_map(|file| {
294 let path = file.path();
295 if path.is_file() {
296 let file = File::open(&path).ok()?;
297 let mut buffer = BufReader::new(file);
298 let mut content = String::new();
299 buffer.read_to_string(&mut content).ok()?;
300 Some(content)
301 } else {
302 None
303 }
304 })
305 .collect::<String>()
306 }
307
308 pub fn get_package(argument: String, version: Option<String>) -> Package {
319 if SupportedOs::supported_os() == OSFamily::Debian {
320 let repository = Repository { repos: Debian };
321 Request::ask_package(
322 &repository.repos,
323 argument.as_str(),
324 version.unwrap_or_default().as_str(),
325 )
326 } else if SupportedOs::supported_os() == OSFamily::RedHat {
327 let repository = Repository { repos: RedHat };
328 Request::ask_package(
329 &repository.repos,
330 argument.as_str(),
331 version.unwrap_or_default().as_str(),
332 )
333 } else {
334 default_package()
335 }
336 }
337
338 pub fn get_changelog_for(package: &Package) -> String {
348 if SupportedOs::supported_os() == OSFamily::Debian {
349 let repository = Repository { repos: Debian };
350 Request::ask_changelog_package(&repository.repos, package.clone())
351 } else {
352 let repository = Repository { repos: RedHat };
353 Request::ask_changelog_package(&repository.repos, package.clone())
354 }
355 }
356
357 fn get_debian_changelog_for(path: &str, package: &Package) -> Result<File, &'static str> {
378 match package.status {
379 PackageStatus::Uninstalled => get_next_changelog_file(&mut package.clone()),
380 _ => {
381 let changelogs = [
382 format!("{}/{}/changelog.Debian.gz", path, package.name),
383 format!("{}/{}/changelog.gz", path, package.name),
384 ];
385
386 let mut idx = 0usize;
387 let mut result = Err("No alternative changelog");
388 while idx < changelogs.len() && result.is_err() {
389 let changelog = changelogs[idx].clone();
390 idx += 1;
391 result = match File::open(changelog) {
392 Ok(file) => Ok(file),
393 _ => Err("No alternative changelog"),
394 };
395 }
396
397 result
398 }
399 }
400 }
401
402 fn get_next_changelog_file(package: &mut Package) -> Result<File, &'static str> {
429 let current_dir = env::current_dir();
430 let current_dir_clone = env::current_dir();
431 let _ = std::fs::create_dir("/var/tmp/get-cve");
432 env::set_current_dir("/var/tmp/get-cve").expect("Error setting tmp directory");
433
434 println!("Download {package}");
435
436 let command_download = std::process::Command::new("/usr/bin/apt-get")
437 .arg("download")
438 .arg(package.name.as_str())
439 .stderr(Stdio::null())
440 .current_dir("/var/tmp/get-cve")
441 .output()
442 .expect("Error on apt-get command running");
443
444 if command_download.status.success() {
445 println!("Decompress {package}");
446 let package_deb = glob::glob(format!("{}*.deb", package.name).as_str());
447 if let Err(_item) = package_deb {
448 restore_current_dir_and_remove_temp_dir(current_dir, "get-cve");
449 write_error_message_and_exit(
450 "Error, problem on packages into temp \
451 directory",
452 None,
453 );
454 } else {
455 let package_name = package_deb.unwrap().next().unwrap();
456 let command_decompress = std::process::Command::new("/usr/bin/dpkg")
457 .current_dir("/var/tmp/get-cve/")
458 .arg("-x")
459 .arg(package_name.unwrap())
460 .arg(".")
461 .output()
462 .expect("Error on decompress command running");
463
464 if !command_decompress.status.success() {
465 restore_current_dir_and_remove_temp_dir(current_dir_clone, "get-cve");
466 write_error_message_and_exit(
467 "Decompression error",
468 Option::from(
469 String::from_utf8(command_decompress.stderr)
470 .unwrap()
471 .as_str(),
472 ),
473 );
474 } else {
475 println!("Search changelog for {package}");
476 package.status = PackageStatus::Installed;
477 return get_debian_changelog_for("/var/tmp/get-cve/usr/share/doc", package);
478 }
479 }
480 }
481 Err("Error on package download")
482 }
483
484 fn extract_package_result(command_output: Result<String, FromUtf8Error>) -> Package {
501 let output_string = command_output.unwrap();
502 if output_string.contains("upgradable") {
503 let elements = output_string.split("/").collect::<Vec<&str>>();
504 let version_elements = elements[1].split(' ').collect::<Vec<&str>>();
505
506 Package {
507 name: String::from(elements[0]),
508 version: String::from(version_elements[1].trim()),
509 status: PackageStatus::Uninstalled,
510 }
511 } else {
512 let elements: Vec<&str> = output_string.as_str().split('\t').collect();
513 if elements.len() < 2 {
514 write_error_message_and_exit("Package not found", None);
515 default_package()
516 } else {
517 Package {
518 name: String::from(elements[0]),
519 version: String::from(elements[1].trim()),
520 status: PackageStatus::Installed,
521 }
522 }
523 }
524 }
525
526 fn grep_version(version: &str, pipe_package: Child) -> Output {
549 std::process::Command::new("/usr/bin/grep")
550 .arg("-w")
551 .arg(version)
552 .stdin(Stdio::from(pipe_package.stdout.unwrap()))
553 .stderr(Stdio::null())
554 .output()
555 .expect("Error on grep on version")
556 }
557}
558
559trait CvssColor {
560 fn cvss_color(&self) -> (Color, Color);
561}
562
563impl CvssColor for f64 {
564 fn cvss_color(&self) -> (Color, Color) {
565 match *self {
566 0.0 => (Color::Default, Color::Default),
567 value if value > 0.0 && value < 4.0 => {
568 (Color::Rgb(255, 255, 255), Color::Rgb(95, 177, 88))
569 }
570 value if (4.0..7.0).contains(&value) => (Color::Black, Color::Rgb(249, 238, 86)),
571 value if (7.0..9.0).contains(&value) => {
572 (Color::Rgb(255, 255, 255), Color::Rgb(232, 152, 63))
573 }
574 _ => (Color::Rgb(255, 255, 255), Color::Rgb(176, 54, 52)),
575 }
576 }
577}
578
579#[cfg(target_os = "linux")]
580pub fn special_options(args: &[String]) {
581 let mut filter: Option<&String> = None;
582 if let Some(idx_filter) = args.iter().position(|a| a == "--filter") {
583 filter = args.get(idx_filter + 1);
584 }
585
586 let (package_name, mut package_version) = extract_argument(args[0].clone());
587
588 if SupportedOs::supported_os() == OSFamily::Debian && args.iter().any(|a| a == "--next") {
589 package_version = Option::from(String::from("next"));
590 }
591
592 let package = get_package(package_name, package_version);
593 stop_if_invalid_package(&package);
594
595 let changelog_content = get_changelog_for(&package);
596 let changelog_cve_list = filtering_cve(package.cve_list(&changelog_content), filter);
597 if args.iter().any(|a| a == "--long" || a == "-L") {
598 if check_nvd_api_key().is_none() {
599 check_nvd_api_key_warning();
600 }
601
602 let mut cve_list = Vec::new();
603 for cve_value in changelog_cve_list {
604 eprint!(
605 "Search CVE list for package: {package}. It will take a while... {}\r",
606 cve_value.reference
607 );
608 let result = execute_query(build_query(ByCve, cve_value.reference.as_str()));
609 if result != Null {
610 let cve = create_cve_from(&result["vulnerabilities"][0]);
611 cve_list.push(cve);
612 }
613 }
614 show_cves(args, &mut cve_list);
615 } else {
616 show_cve_list(changelog_cve_list, package);
617 }
618}
619
620pub fn common_options(args: &mut [String]) {
621 if args.iter().any(|a| a == "version" || a == "v") {
622 show_version(env!("CARGO_PKG_VERSION"));
623 exit(0);
624 }
625
626 #[cfg(not(target_os = "linux"))]
627 if check_nvd_api_key().is_none() {
628 check_nvd_api_key_warning();
629 }
630
631 if args.iter().any(|a| a == "check" || a == "c") {
632 check_mitre_data(Cve::define());
633 exit(0);
634 }
635
636 if args.iter().any(|a| a == "e" || a == "exploited") {
637 let mut cve_list = standard_or_filtered_list(args, extract_cve_list("", ByCveExploited));
638 show_cves(args, &mut cve_list);
639 exit(0);
640 }
641
642 if let Some(cpe_string) = args
643 .iter()
644 .position(|a| a == "for-cpe" || a == "f")
645 .and_then(|i| args.get(i + 1))
646 {
647 let cpe = Cpe::from(cpe_string);
648 let mut result = run_search_cpe(cpe, args);
649 show_cves(args, &mut result);
650
651 exit(0);
652 }
653
654 if let Some(new_option) = args.iter().position(|a| a == "new" || a == "n") {
655 let dates_range = build_dates_range(Some(new_option), args);
656 let mut cve_list =
657 standard_or_filtered_list(args, extract_cve_list(dates_range.as_str(), ByCveNew));
658 show_cves(args, &mut cve_list);
659 exit(0);
660 }
661
662 if let Some(updated_option) = args.iter().position(|a| a == "updated" || a == "u") {
663 let dates_range = build_dates_range(Some(updated_option), args);
664 let mut cve_list =
665 standard_or_filtered_list(args, extract_cve_list(dates_range.as_str(), ByCveUpdated));
666 show_cves(args, &mut cve_list);
667 exit(0);
668 }
669
670 if let Some(cwe_option) = args.iter().position(|a| a == "cwe" || a == "w") {
671 run_search_cwe(args, Some(cwe_option));
672 exit(0);
673 }
674
675 if let Some(search_option) = args.iter().position(|a| a == "search" || a == "s") {
676 let result = run_search_cve_string(args, Some(search_option));
677 let mut values = standard_or_filtered_list(args, result);
678 show_cves(args, &mut values);
679 exit(0);
680 }
681
682 let key_cve = args
683 .iter()
684 .position(|a| a.to_ascii_lowercase().contains("cve"));
685 if let Some(pos) = key_cve
686 && pos == 0
687 {
688 run_search_cve(args, key_cve);
689 exit(0);
690 }
691}
692
693fn standard_or_filtered_list(args: &mut [String], cve_list: Vec<Cve>) -> Vec<Cve> {
694 match args.iter().position(|a| a == "--filter") {
695 Some(pos) => filtering_cve(cve_list, args.get(pos + 1)),
696 _ => cve_list,
697 }
698}
699
700fn show_cves(args: &[String], cve_list: &mut [Cve]) {
701 println!();
702 if !args.iter().any(|a| a == "--long" || a == "-L") {
703 show_cve_values(cve_list);
704 } else {
705 if let Some(pos) = args.iter().position(|a| a == "--sort_by")
706 && let Some(criteria) = args.get(pos + 1)
707 {
708 let mut sort_score_by = String::new();
709 if let Some(sorting_criteria) = args.get(pos + 2) {
710 sort_score_by = sorting_criteria.clone();
711 }
712
713 match criteria.as_str() {
714 "score" => match sort_score_by.as_str() {
715 "v4" => cve_list.sort_by(|a, b| {
716 b.score_v4
717 .clone()
718 .unwrap()
719 .value()
720 .total_cmp(&a.score_v4.clone().unwrap().value())
721 }),
722
723 _ => cve_list.sort_by(|a, b| {
724 b.score_v3
725 .clone()
726 .unwrap()
727 .value()
728 .total_cmp(&a.score_v3.clone().unwrap().value())
729 }),
730 },
731
732 "description" => cve_list.sort_by(|a, b| {
733 a.description
734 .clone()
735 .unwrap()
736 .cmp(&b.description.clone().unwrap())
737 }),
738
739 "cpe" => cve_list
740 .sort_by(|a, b| a.cpe_id.clone().unwrap().cmp(&b.cpe_id.clone().unwrap())),
741 "weaknesses" => cve_list.sort_by(|a, b| {
742 b.weaknesses
743 .clone()
744 .unwrap()
745 .cmp(&a.weaknesses.clone().unwrap())
746 }),
747
748 _ => (),
749 };
750 }
751
752 if args.iter().any(|a| a == "--to_csv") {
753 show_as_csv(cve_list, args.iter().any(|a| a == "with_headers"));
754 } else {
755 show_as_table(cve_list);
756 }
757 }
758}
759
760fn show_as_csv(cve_list: &mut [Cve], with_headers: bool) {
761 if !cve_list.is_empty() {
762 let mut writer = csv::Writer::from_writer(stdout());
763 if with_headers {
764 writer
765 .write_record([
766 "cve",
767 "description",
768 "score v3",
769 "level v3",
770 "score v4",
771 "level v4",
772 "cpe",
773 "weaknesses",
774 ])
775 .expect("Failed to write CSV headers");
776 }
777
778 cve_list.iter_mut().for_each(|cve| {
779 writer
780 .write_record([
781 cve.reference.clone(),
782 cve.description.clone().unwrap(),
783 cve.score_v3.clone().unwrap().value().to_string(),
784 cve.score_v3.clone().unwrap().label().to_string(),
785 cve.score_v4.clone().unwrap().value().to_string(),
786 cve.score_v4.clone().unwrap().label().to_string(),
787 cve.cpe_id.clone().unwrap(),
788 cve.weaknesses.clone().unwrap(),
789 ])
790 .expect("Failed to write CSV content");
791 });
792
793 writer.flush().expect("Failed to flush CSV writer");
794 }
795}
796
797fn show_cve_values(cve_list: &[Cve]) {
798 cve_list.iter().for_each(|c| println!("{c}"));
799}
800
801fn run_search_cve_string(args: &mut [String], position: Option<usize>) -> Vec<Cve> {
802 let result = match args.iter().position(|a| a == "--strict" || a == "-S") {
803 Some(pos) => {
804 let values = args.get(pos + 1);
805 if let Some(item) = values {
806 execute_query(build_query(QueryType::ByCveSearchStrict, item.as_str()))
807 } else {
808 write_error_message_and_exit("Missing criteria", None);
809 Null
810 }
811 }
812
813 _ => match args.get(position.unwrap() + 1) {
814 Some(values) => {
815 execute_query(build_query(QueryType::ByCveSearchString, values.as_str()))
816 }
817 _ => {
818 write_error_message_and_exit("Missing criteria", None);
819 Null
820 }
821 },
822 };
823 let mut cves = cve_from_new_query(&result);
824 simplify_cve_list_content(&mut cves);
825 cves
826}
827
828#[cfg(target_os = "linux")]
846fn extract_argument(argument: String) -> (String, Option<String>) {
847 let package_name: String;
848 let mut package_version: Option<String> = None;
849
850 if argument.contains("=") {
851 let component: Vec<&str> = argument.split("=").collect();
852 package_name = String::from(component[0]);
853 package_version = Some(String::from(component[1]));
854 } else {
855 package_name = argument.clone();
856 }
857
858 (package_name, package_version)
859}
860
861fn extract_cve_list(criteria: &str, cve_type: QueryType) -> Vec<Cve> {
880 let result = execute_query(build_query(cve_type.clone(), criteria));
881 if !result.is_null() && result["resultsPerPage"] != 0 {
882 let mut start_index = result["startIndex"].clone().as_u64().unwrap();
883 let total_results = result["totalResults"].clone().as_u64().unwrap();
884
885 let mut cve_list = cve_from_new_query(&result);
886 start_index += cve_list.len() as u64;
887 while cve_list.len() < (total_results as usize) {
888 let query = format!(
889 "{}&startIndex={}",
890 build_query(cve_type.clone(), criteria).unwrap(),
891 start_index
892 );
893 let result = execute_query(Ok(query));
894 let mut cves = cve_from_new_query(&result);
895 start_index += cves.len() as u64;
896 cve_list.append(&mut cves);
897 }
898
899 simplify_cve_list_content(&mut cve_list);
900 return cve_list;
901 } else {
902 write_error_message_and_exit("No result found", None)
903 }
904
905 Vec::new()
906}
907
908fn show_as_table(cve_list: &[Cve]) {
909 let mut table = AsciiTable::new("CVE list");
910
911 table.set_headers(vec![
912 "CVE",
913 "Description",
914 "Score v3",
915 "Score v4",
916 "CPE",
917 "Weaknesses",
918 ]);
919
920 cve_list.iter().for_each(|cve| {
921 let description = cve.description.clone().unwrap_or(String::from("None"));
922 let cpe_id = cve.cpe_id.clone().unwrap_or(String::from("None"));
923 let weaknesses = cve.weaknesses.clone().unwrap_or(String::from("None"));
924
925 let score_v3 = format_score(cve.score_v3.clone().unwrap());
926 let score_v4 = format_score(cve.score_v4.clone().unwrap());
927
928 table.add_row(vec![
929 CellValue::Str(cve.reference.clone()),
930 CellValue::Str(description.replace("\n", " ").reduce(70)),
931 CellValue::Str(score_v3.to_string()),
932 CellValue::Str(score_v4.to_string()),
933 CellValue::Str(cpe_id.reduce(40)),
934 CellValue::Str(weaknesses.reduce(15)),
935 ])
936 });
937
938 table.render()
939}
940
941fn format_score(cve_score: Score) -> String {
942 let (color_fg, color_bg) = cve_score.value().cvss_color();
943 format!(
944 "{:^18}",
945 format!(
946 " {} - {} ",
947 format!("{0:.1}", cve_score.value()),
948 cve_score.label()
949 )
950 )
951 .fg(color_fg)
952 .bg(color_bg)
953 .to_string()
954}
955
956fn run_search_cpe(cpe: Cpe, args: &mut [String]) -> Vec<Cve> {
972 if let Some(new_date_option) = args.iter().position(|a| a == "--new") {
973 let dates = build_dates_range(Some(new_date_option), args);
974 let query_dates = create_query_dates(ByCveNew, dates.as_str());
975 let cpe_query = build_query(ByCveCpeName, cpe.name().as_str());
976 standard_or_filtered_list(
977 args,
978 extract_cve_list(
979 format!("{}&{}", cpe_query.unwrap(), query_dates).as_str(),
980 ByCveCpeName,
981 ),
982 )
983 } else if let Some(updated_date_option) = args.iter().position(|a| a == "--updated") {
984 let dates = build_dates_range(Some(updated_date_option), args);
985 let query_dates = create_query_dates(ByCveUpdated, dates.as_str());
986 let cpe_query = build_query(ByCveCpeName, cpe.name().as_str());
987 standard_or_filtered_list(
988 args,
989 extract_cve_list(
990 format!("{}&{}", cpe_query.unwrap(), query_dates).as_str(),
991 ByCveCpeName,
992 ),
993 )
994 } else if args.iter().any(|a| a == "--vul" || a == "-v") {
995 standard_or_filtered_list(args, extract_cve_list(cpe.name().as_str(), ByCpeVulnerable))
996 } else {
997 standard_or_filtered_list(args, extract_cve_list(cpe.name().as_str(), ByCveCpeName))
998 }
999}
1000
1001fn run_search_cve(args: &mut [String], key_cve: Option<usize>) {
1025 if let Some(cve_value) = args.get(key_cve.unwrap()) {
1026 let result = if cve_value == &String::from("--cpe") {
1027 println!("Searching...");
1028 execute_query(build_query(ByCveCpeName, args[2].as_str()))
1029 } else {
1030 execute_query(build_query(ByCve, cve_value.as_str()))
1031 };
1032
1033 if args.iter().any(|a| a == "--long" || a == "-L") {
1034 let mut cve = vec![create_cve_from(&result["vulnerabilities"][0])];
1035 if args.iter().any(|a| a == "--to_csv") {
1036 show_as_csv(&mut cve, args.iter().any(|a| a == "with_headers"));
1037 } else {
1038 show_as_table(&cve);
1039 }
1040 return;
1041 }
1042
1043 if result["resultsPerPage"] == 0 || result == Null {
1044 write_error_message_and_exit("CVE not found", None);
1045 }
1046
1047 if args.iter().any(|a| a == "--schema" || a == "-S") {
1048 show_schema(&result);
1049 return;
1050 }
1051
1052 println!("{}", header!(cve_value));
1053 if cve_value == &String::from("--cpe") {
1054 cve_from_new_query(&result).iter().for_each(print_cve);
1055 return;
1056 }
1057
1058 let mut cumulative_args = 0;
1059
1060 if args.iter().any(|a| a == "--score" || a == "-s") {
1061 let include_data = args.iter().any(|a| a == "--data" || a == "-D");
1062 if include_data {
1063 cumulative_args += 1;
1064 }
1065 if args.iter().any(|a| a == "v3") {
1066 CveDisplayType::ByScores.show(&result, Some("v3"), Some(include_data));
1067 cumulative_args += 1;
1068 } else if args.iter().any(|a| a == "v4") {
1069 CveDisplayType::ByScores.show(&result, Some("v4"), Some(include_data));
1070 cumulative_args += 1;
1071 } else {
1072 CveDisplayType::ByScores.show(&result, Some("all"), Some(include_data));
1073 cumulative_args += 1;
1074 }
1075 }
1076
1077 if args.iter().any(|a| a == "--cwe" || a == "-w") {
1078 CveDisplayType::ByCwe.show(&result, None, None);
1079 cumulative_args += 1;
1080 }
1081
1082 if args.iter().any(|a| a == "--lang" || a == "-l") {
1083 CveDisplayType::ByLanguages.show(&result, None, None);
1084 cumulative_args += 1;
1085 }
1086
1087 if let Some(key_description) = args.iter().position(|a| a == "--desc" || a == "-d") {
1088 let key_lang = args.get(key_description + 1).map(|lang| lang.as_str());
1089
1090 CveDisplayType::ByDescription.show(&result, key_lang, None);
1091 cumulative_args += 1;
1092 }
1093
1094 if args.iter().any(|a| a == "--doc" || a == "-K") {
1095 CveDisplayType::ByDocumentations.show(&result, None, None);
1096 cumulative_args += 1;
1097 }
1098
1099 if args.iter().any(|a| a == "--cpe" || a == "-f") {
1100 CveDisplayType::ByCpe.show(&result, None, None);
1101 cumulative_args += 1;
1102 }
1103
1104 if cumulative_args == 0 {
1105 CveDisplayType::All.show(&result, Some("all"), Some(true));
1106 }
1107 }
1108}
1109
1110fn show_schema(result: &Value) {
1111 let cve = create_cve_from(&result["vulnerabilities"][0]);
1112
1113 let mut builder = TreeBuilder::new();
1114 let cve_node = builder.node(section!(cve.reference, Color::Green).to_string());
1115
1116 if let Some(cwe_list) = cve.weaknesses {
1117 let cwe_id = parse_id_to_u32(
1118 cwe_list
1119 .replace("CWE-", "")
1120 .replace(", ", FIELD_SEPARATOR_STRING),
1121 );
1122 cwe_id.iter().for_each(|id| {
1123 if let Some(cwe) = find_cwe_by_id(*id) {
1124 let cwe_node = cve_node.node(
1125 format!("CWE-{} - {}", cwe.id, cwe.name)
1126 .fg(Color::Red)
1127 .to_string(),
1128 );
1129
1130 let capec_id = parse_id_to_u32(
1131 cwe.clone()
1132 .attacks
1133 .replace("CAPEC-", "")
1134 .replace(", ", FIELD_SEPARATOR_STRING),
1135 );
1136 if !capec_id.is_empty() {
1137 capec_id.iter().for_each(|ca_id| {
1138 if let Some(capec) = find_capec_by_id(*ca_id) {
1139 cwe_node.leaf(
1140 format!("CAPEC-{} - {}", capec.id, capec.name)
1141 .fg(Color::DarkCyan)
1142 .to_string(),
1143 );
1144 }
1145 });
1146 }
1147 cwe_node.end();
1148 }
1149 });
1150 } else {
1151 write_error_message_and_exit("No schemas found", None);
1152 }
1153
1154 let tree = builder.build();
1155 println!("{}", tree.render_to_string());
1156}
1157
1158fn show_data(result: &Value, level: &str) {
1159 let cvss_data = match level {
1160 "v3" => get_cvss3_data(result),
1161 "v4" => get_cvss4_data(result),
1162 _ => Null,
1163 };
1164
1165 if !cvss_data.is_null() {
1166 let map = cvss_data.as_object().unwrap();
1167 let filtered = map
1168 .iter()
1169 .filter(|(k, _)| **k != "baseScore" && **k != "baseSeverity" && **k != "version");
1170 filtered.for_each(|(k, v)| {
1171 let str_value = match k.to_string().contains("vectorString") {
1172 false => v.to_string().wording().replace("_", " "),
1173 true => v.to_string(),
1174 };
1175 println!(
1176 "{:>7} {}: {}",
1177 "-",
1178 k.wording(),
1179 String::cleaning(str_value)
1180 );
1181 });
1182 } else {
1183 write_error_message("CVSS data not found", Some(level));
1184 }
1185}
1186
1187pub fn show_languages(result: &Value) {
1221 if let Some(descriptions) = get_descriptions_section(result) {
1222 section_level!(2, "Languages");
1223 for l in descriptions {
1224 print_values(&[l["lang"].clone()]);
1225 }
1226 }
1227}
1228
1229pub fn show_description(result: &Value, key_lang: Option<&String>) {
1268 section_level!(2, "Description");
1269
1270 let description = if let Some(lang) = key_lang {
1271 extract_description(result, Some(lang.as_str()))
1272 } else {
1273 extract_description(result, None)
1274 };
1275 print_values(&[description]);
1276 println!();
1277}
1278
1279fn show_documentations(result: &Value) {
1280 if let Some(documentations) = get_documentation_section(result) {
1281 section_level!(2, "Documentations");
1282 documentations.iter().for_each(|doc| {
1283 print_values(std::slice::from_ref(doc));
1284 if doc != documentations.last().unwrap() {
1285 println!();
1286 }
1287 });
1288 } else {
1289 write_error_message_and_exit("Documentation not found", None);
1290 }
1291}
1292
1293fn extract_description(value: &Value, lang: Option<&str>) -> Value {
1308 if let Some(lang) = lang
1309 && let Some(languages) = get_descriptions_section(value)
1310 {
1311 for l in languages {
1312 if l["lang"].as_str() == Some(lang) {
1313 return l["value"].clone();
1314 }
1315 }
1316 }
1317 extract_default_description(value)
1318}
1319
1320fn extract_default_description(value: &Value) -> Value {
1332 value["vulnerabilities"][0]["cve"]["descriptions"][0]["value"].clone()
1333}
1334
1335fn get_descriptions_section(value: &Value) -> Option<&Vec<Value>> {
1347 value["vulnerabilities"][0]["cve"]["descriptions"].as_array()
1348}
1349
1350fn get_documentation_section(value: &Value) -> Option<&Vec<Value>> {
1359 value["vulnerabilities"][0]["cve"]["references"].as_array()
1360}
1361
1362pub fn run_search_cwe(args: &mut [String], cwe_option: Option<usize>) {
1404 if let Some(cwe_value) = args.get(cwe_option.unwrap() + 1) {
1405 let result = execute_query(build_query(ByCwe, cwe_value.as_str()));
1406 let mut cve_list = standard_or_filtered_list(args, cve_from_cwe(&result));
1407 simplify_cve_list_content(&mut cve_list);
1408 if cve_list.is_empty() {
1409 write_error_message_and_exit("CVE not found", None);
1410 }
1411 show_cves(args, &mut cve_list);
1412 }
1413}
1414
1415enum CveDisplayType {
1416 All,
1417 ByCpe,
1418 ByCwe,
1419 ByData,
1420 ByDescription,
1421 ByDocumentations,
1422 ByLanguages,
1423 ByScores,
1424}
1425
1426impl CveDisplayType {
1427 fn show(&self, value: &Value, argument: Option<&str>, flag: Option<bool>) {
1428 match self {
1429 CveDisplayType::All => {
1430 show_description(value, None);
1431 show_scores(value, argument.unwrap().trim(), flag.unwrap_or(false));
1432 show_documentations(value);
1433 println!();
1434 show_cwe_values(value);
1435 println!();
1436 show_cpe_values(value);
1437 }
1438
1439 CveDisplayType::ByCpe => {
1440 show_cpe_values(value);
1441 }
1442
1443 CveDisplayType::ByCwe => {
1444 show_cwe_values(value);
1445 }
1446
1447 CveDisplayType::ByData => {
1448 show_data(value, argument.unwrap().trim());
1449 }
1450
1451 CveDisplayType::ByDescription => {
1452 show_description(value, None);
1453 }
1454
1455 CveDisplayType::ByDocumentations => {
1456 show_documentations(value);
1457 }
1458
1459 CveDisplayType::ByLanguages => {
1460 show_languages(value);
1461 }
1462
1463 CveDisplayType::ByScores => {
1464 show_scores(value, argument.unwrap().trim(), flag.unwrap_or(false));
1465 }
1466 }
1467 }
1468}
1469
1470fn show_cpe_values(value: &Value) {
1471 section_level!(2, "CPE");
1472 let values = extract_cpe_id_list(value);
1473 if let Some(values) = values {
1474 values.iter().for_each(|v| {
1475 let mut result = v.as_object().unwrap().clone();
1476 result.remove("matchCriteriaId");
1477 print_values(&[Value::Object(result)]);
1478 println!();
1479 })
1480 } else {
1481 println!("None");
1482 }
1483}
1484
1485fn extract_cpe_id_list(value: &Value) -> Option<&Vec<Value>> {
1486 value["vulnerabilities"][0]["cve"]["configurations"][0]["nodes"][0]["cpeMatch"].as_array()
1487}
1488
1489pub fn show_cwe_values(result: &Value) {
1526 let values = extract_cwe(result);
1527 if !values.is_empty() {
1528 let slice_values: Vec<_> = values
1529 .iter()
1530 .map(|v| String::cleaning(v.to_string()))
1531 .collect();
1532 let mut str_values = slice_values.join(FIELD_SEPARATOR_STRING_LONG);
1533 str_values = str_values.replace("CWE-", FIELD_SEPARATOR_STRING_LONG);
1534 show_weaknesses("weaknesses", str_values);
1535 } else {
1536 println!("None");
1537 }
1538}
1539
1540fn extract_cwe(value: &Value) -> Vec<Value> {
1555 let mut result: Vec<Value> = vec![];
1556 let binding = value["vulnerabilities"][0]["cve"]["weaknesses"].clone();
1557 let elements_option = binding.as_array();
1558 if let Some(elements) = elements_option {
1559 elements.iter().for_each(|value| {
1560 let vals = value["description"].as_array().unwrap();
1561 vals.iter()
1562 .for_each(|val| result.push(val["value"].clone()))
1563 });
1564 }
1565
1566 result.dedup();
1567
1568 result
1569}
1570
1571pub fn show_scores(result: &Value, level: &str, include_data: bool) {
1625 if level != "all" {
1626 let cvss_data = match level {
1627 "v3" => get_cvss3_data(result),
1628 "v4" => get_cvss4_data(result),
1629 _ => Null,
1630 };
1631
1632 section_level!(2, "Scores");
1633 if !cvss_data.is_null() {
1634 let score = cvss_data["baseScore"].as_f64().unwrap();
1635 let (color_fg, color_bg) = score.cvss_color();
1636 println!(
1637 "{:>6} Score {}: {:.1} - {}",
1638 "-",
1639 level,
1640 score,
1641 format!(
1642 " {} ",
1643 String::cleaning(cvss_data["baseSeverity"].to_string())
1644 )
1645 .fg(color_fg)
1646 .bg(color_bg),
1647 );
1648 println!();
1649
1650 if include_data {
1651 section_level!(3, "Data");
1652 CveDisplayType::ByData.show(result, Some(level), None);
1653 println!();
1654 }
1655 } else {
1656 write_error_message("Score not found", Some(level));
1657 println!();
1658 }
1659 } else if level == "all" {
1660 show_scores(result, "v3", include_data);
1661 show_scores(result, "v4", include_data);
1662 } else {
1663 write_error_message("Score type not found", Some(level));
1664 }
1665}
1666
1667fn get_cvss4_data(result: &Value) -> Value {
1687 get_cvss_scoring(result, "V40")
1688}
1689
1690fn get_cvss3_data(result: &Value) -> Value {
1731 get_cvss_scoring(result, "V31")
1732}
1733
1734fn get_cvss_scoring(result: &Value, level: &str) -> Value {
1744 result["vulnerabilities"][0]["cve"]["metrics"][format!("cvssMetric{}", level)][0]["cvssData"]
1745 .clone()
1746}
1747
1748pub fn cve_from_new_query(values: &Value) -> Vec<Cve> {
1785 if let Some(vulnerabilities) = values["vulnerabilities"].as_array() {
1786 let mut results: Vec<Cve> = vec![];
1787 vulnerabilities
1788 .iter()
1789 .for_each(|v| results.push(create_cve_from(v)));
1790
1791 results
1792 } else {
1793 vec![]
1794 }
1795}
1796
1797#[cfg(test)]
1798mod tests {
1799 use crate::{extract_cwe, extract_description};
1800 use fenir::cve::{create_cve_from, cve_from_cwe};
1801 use fenir::database::execute_query;
1802 use fenir::query::build_query;
1803 use fenir::query::QueryType::{ByCve, ByCwe};
1804 use serde_json::Value;
1805 use std::thread::sleep;
1806 use std::time::Duration;
1807 use utmt::assert_all;
1808
1809 fn setup() {
1810 let waiting = Duration::from_millis(30);
1811 sleep(waiting);
1812 }
1813
1814 #[test]
1815 fn extract_description_default_lang_valid() {
1816 setup();
1817
1818 let result = execute_query(build_query(ByCve, "CVE-2024-6387"));
1819
1820 assert_default_description(&result, None);
1821 }
1822
1823 fn assert_default_description(result: &Value, option: Option<&str>) {
1824 assert!(
1825 extract_description(&result, option)
1826 .to_string()
1827 .contains("A security regression (CVE-2006-5051) was discovered ")
1828 );
1829 }
1830
1831 #[test]
1832 fn extract_description_invalid_default() {
1833 setup();
1834
1835 let result = execute_query(build_query(ByCve, "CVE-2024-6387"));
1836
1837 assert_default_description(&result, Option::from("ff"));
1838 }
1839
1840 #[test]
1841 fn extract_description_es_lang_valid() {
1842 setup();
1843
1844 let result = execute_query(build_query(ByCve, "CVE-2024-6387"));
1845
1846 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"));
1847 }
1848
1849 #[test]
1850 fn extract_cwe_valid_cve_non_empty() {
1851 setup();
1852
1853 let result = execute_query(build_query(ByCve, "CVE-2024-6387"));
1854
1855 assert!(!extract_cwe(&result).is_empty());
1856 }
1857
1858 #[test]
1859 fn create_cve_from_value() {
1860 setup();
1861
1862 let cve = create_cve_from(&execute_query(build_query(ByCve, "CVE-2024-6387")));
1863
1864 assert_all!(
1865 cve.cpe_id.is_some(),
1866 cve.description.is_some(),
1867 cve.score_v3.is_some(),
1868 !cve.reference.is_empty(),
1869 cve.weaknesses.is_some()
1870 );
1871 }
1872
1873 #[test]
1874 fn extract_cwe_invalid_cve_empty() {
1875 setup();
1876
1877 let result = execute_query(build_query(ByCve, "CVE-1970-0000"));
1878
1879 assert!(extract_cwe(&result).is_empty());
1880 }
1881
1882 #[test]
1883 fn cve_from_cwe_result_valid_cwe_not_empty() {
1884 setup();
1885
1886 let result = execute_query(build_query(ByCwe, "CWE-287"));
1887
1888 assert!(!cve_from_cwe(&result).is_empty());
1889 }
1890}