fenir/lib.rs
1#![deny(clippy::mem_forget)]
2
3/// NVD link Rest API for CVE request
4pub const NVD_CVE_LINK: &str = "https://services.nvd.nist.gov/rest/json/cves/2.0";
5
6/// NVD api key name to use to by the underlying OS
7pub const NVD_KEY_NAME: &str = "NVD_API_KEY";
8
9/// Space substitution in URL
10const URL_SPACE: &str = "%20";
11
12/// The `package` module provides all the necessary components
13/// for managing and manipulating software packages.
14/// This module includes handling errors, defining package concepts,
15/// managing facilities, and other related utilities.
16pub mod package {
17 /// The `concepts` module defines core structures and traits for handling
18 /// package information, changelogs, and CVE (Common Vulnerabilities and Exposures) management
19 ///
20 pub mod concepts {
21 pub use crate::cve::Cve;
22 use crate::facilities::Uppercase;
23 use crate::package::errors::write_error_message_and_exit;
24 use crate::{section, section_level};
25 use regex::Regex;
26 use std::fmt::{Display, Formatter};
27 use std::ops::Add;
28 use std::process::Output;
29
30 /// Represents a package status that's representing the supported status.
31 ///
32 #[derive(Eq, PartialEq, Copy, Clone)]
33 pub enum PackageStatus {
34 /// The package is currently installed on the system.
35 Installed,
36
37 /// The package is not found on the system.
38 NotFound,
39
40 /// The package has been uninstalled from the system. In some system, this status can be
41 /// appearing after a package uninstallation.
42 Uninstalled,
43 }
44
45 /// The `Package` struct is used to encapsulate information about a software package,
46 /// such as the package's name, its version, and its current status in the system.
47 #[derive(PartialEq, Eq, Clone)]
48 pub struct Package {
49 /// The name of the package.
50 pub name: String,
51
52 /// The version of the package.
53 pub version: String,
54
55 /// The current status of the package (e.g., see corresponding enum).
56 pub status: PackageStatus,
57 }
58
59 impl Display for Package {
60 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
61 write!(formatter, "{} => {}", self.name, self.version)
62 }
63 }
64
65 /// Creates a default `Package` instance.
66 ///
67 /// This function initializes a `Package` with default values for its fields.
68 /// It can be used when a default or placeholder instance of `Package` is required.
69 ///
70 /// # Returns
71 ///
72 /// A `Package` instance with default values:
73 /// * name as an empty string,
74 /// * version as an empty string,
75 /// * status set to `PackageStatus::NotFound`.
76 pub fn default_package() -> Package {
77 Package {
78 name: String::new(),
79 version: String::new(),
80 status: PackageStatus::NotFound,
81 }
82 }
83
84 /// Represents a repository that can hold any type of repository type. The repository type
85 /// depends on the underlying operating system.
86 pub struct Repository<T> {
87 /// The effective repository
88 pub repos: T,
89 }
90
91 /// Trait defining standard requests related to packages.
92 pub trait Request {
93 /// Asks for a package given its name and version, returning a `Package` object.
94 ///
95 /// # Arguments
96 ///
97 /// * `package` - A string slice that holds the name of the package.
98 /// * `version` - A string slice that holds the version of the package.
99 ///
100 /// # Returns
101 ///
102 /// A Package object.
103 ///
104 fn ask_package(&self, package: &str, version: &str) -> Package;
105
106 /// Asks for the changelog of a package. The aim of this command is the access of a standard
107 /// way to the changelog's package. The standard way means the changelog content thanks to
108 /// the packaging standard feature of the current system.
109 ///
110 /// # Arguments
111 ///
112 /// * `package` - The `Package` object for which the changelog is requested.
113 ///
114 /// # Returns
115 ///
116 /// The changelog content.
117 ///
118 fn ask_changelog_package(&self, package: Package) -> String;
119
120 /// Reads the changelog local files of a package. As the opposite of `ask_changelog_package`
121 /// this method is an alternative way to access to local changelogs. Thanks to this method,
122 /// it's possible to access to local changelog files if the standard way give no data.
123 ///
124 /// # Arguments
125 ///
126 /// * `package` - The `Package` object for which the changelog files are to be read.
127 ///
128 /// # Returns
129 ///
130 /// The local changelog files content.
131 ///
132 fn read_changelog_files(&self, package: Package) -> String;
133 }
134
135 /// Displays a list of CVEs (Common Vulnerabilities and Exposures) for a given package.
136 ///
137 /// # Arguments
138 /// * `cve_list` - A vector containing CVE entries.
139 /// * `package` - The package for which the CVEs are being shown.
140 ///
141 /// # Examples
142 ///
143 /// The arguments are showing with the form like this (eg: with command `get-cve less`):
144 /// ```shell
145 /// [ less => 2.9+b1 ]
146 /// CVE-2023-1234
147 /// CVE-2024-2232
148 /// CVE-2024-5664
149 /// ```
150 ///
151 pub fn show_cve_list(cve_list: Vec<Cve>, package: Package) {
152 if cve_list.is_empty() {
153 println!("No cve for: {}", package);
154 } else {
155 section_level!(package);
156 cve_list.iter().for_each(print_cve);
157 }
158 }
159
160 /// Print the cve
161 ///
162 /// # Arguments
163 ///
164 /// * `cve`- The cve to display
165 ///
166 pub fn print_cve(cve: &Cve) {
167 println!(" {}", cve);
168 }
169
170 /// Filters a list of CVEs based on an optional filter string.
171 ///
172 /// # Arguments
173 ///
174 /// * `cve_list` - A vector of `Cve` objects to be filtered.
175 /// - Each `Cve` represents a Common Vulnerabilities and Exposures entry.
176 ///
177 /// * `filter` - An optional reference to a string to filter the `cve_list`.
178 /// - If `Some` filter is provided, it will be used for filtering the list of CVEs.
179 /// - If `None`, the original list will be returned with no filter applied.
180 ///
181 /// # Returns
182 ///
183 /// * A vector of filtered `Cve` objects based on the provided filter string or the original list if no filter is provided.
184 ///
185 /// # Examples
186 ///
187 /// ```shell
188 /// get-cve less --filter 2023
189 ///
190 /// [less => 2.9+b1]
191 /// CVE-2023-1234
192 /// ```
193 ///
194 pub fn filtering_cve(cve_list: Vec<Cve>, filter: Option<&String>) -> Vec<Cve> {
195 match filter {
196 Some(filter) => filter_cve_list(cve_list, &filter.to_uppercase()),
197 None => cve_list,
198 }
199 }
200
201 fn filter_cve_list(cve_list: Vec<Cve>, filter: &str) -> Vec<Cve> {
202 let filter_regex = Regex::new(filter).unwrap();
203 cve_list
204 .into_iter()
205 .filter(|cve| filter_regex.is_match(&cve.reference))
206 .collect()
207 }
208
209 /// Trait defining a contract for extracting CVEs from changelog content.
210 ///
211 pub trait Changelog {
212 /// Extracts a list of CVEs from the given changelog content.
213 ///
214 /// # Arguments
215 ///
216 /// * `changelog_content` - A string slice that holds the content of the changelog.
217 ///
218 /// # Returns
219 ///
220 /// A vector of `Cve` structs that were found in the changelog content.
221 fn cve_list(&self, changelog_content: &str) -> Vec<Cve>;
222 }
223
224 impl Changelog for Package {
225 /// Implementation of the `cve_list` method for the `Package` struct.
226 ///
227 /// This method parses the provided changelog content and returns a list of CVEs.
228 ///
229 /// # Note
230 ///
231 /// The actual implementation uses a regex to search for CVE entries in the provided
232 /// changelog content.
233 fn cve_list(&self, changelog_content: &str) -> Vec<Cve> {
234 const CVE_REGEX_PATTERN: &str = "CVE-[0-9]{4}-[0-9]+";
235 let cve_regex = Regex::new(CVE_REGEX_PATTERN).unwrap();
236
237 let mut cve_found: Vec<Cve> = extract_cves(&cve_regex, changelog_content);
238 simplify_cve_list_content(&mut cve_found);
239
240 cve_found
241 }
242 }
243
244 fn extract_cves(cve_regex: &Regex, content: &str) -> Vec<Cve> {
245 let mut cve_list = Vec::new();
246 if cve_regex.is_match(&content.to_uppercase()) {
247 for cve in cve_regex
248 .captures_iter(content)
249 .filter_map(|c| c.get(0).map(|m| m.as_str().to_string()))
250 {
251 cve_list.push(Cve::new(cve));
252 }
253 }
254 cve_list
255 }
256
257 /// Generates a changelog result based on the output of a changelog command. The alternative
258 /// changelog files content are using in complement of the standard command uses to ensure to collect
259 /// the maximum of cve data.
260 ///
261 /// # Arguments
262 ///
263 /// * `package` - A `Package` struct representing the package for which the changelog is searched.
264 /// * `changelog_command` - An `Output` struct representing the output of the changelog command.
265 /// * `local_changelog_content` - A `String` that represents the local changelog files content to use to
266 /// with the standard changelog command access.
267 ///
268 /// # Returns
269 ///
270 /// Returns a `String` containing the changelog result if the command was successful, or an empty string otherwise.
271 ///
272 pub fn changelog_result(
273 package: Package,
274 changelog_command: Output,
275 local_changelog_content: String,
276 ) -> String {
277 let is_success = changelog_command.status.success();
278 if is_success {
279 handle_success(
280 changelog_command.stdout,
281 package.name.as_str(),
282 &local_changelog_content,
283 )
284 } else {
285 String::new()
286 }
287 }
288
289 fn handle_success(stdout: Vec<u8>, package_name: &str, alternative: &str) -> String {
290 handle_stdout(stdout, package_name).add(alternative)
291 }
292
293 fn handle_stdout(stdout: Vec<u8>, package_name: &str) -> String {
294 String::from_utf8(stdout).unwrap_or_else(|_| {
295 handle_output_error(package_name);
296 String::new()
297 })
298 }
299
300 fn handle_output_error(package_name: &str) {
301 write_error_message_and_exit("Output error", Some(package_name));
302 }
303
304 /// Simplifies the content of a list of CVEs by sorting and removing duplicates.
305 ///
306 /// # Arguments
307 ///
308 /// * `cve_list` - A mutable reference to a vector of `Cve` objects to be simplified.
309 /// - This vector will be sorted in place and then deduplicated, removing any duplicate `Cve` entries.
310 ///
311 pub fn simplify_cve_list_content(cve_list: &mut Vec<Cve>) {
312 cve_list.sort();
313 cve_list.dedup();
314 }
315 }
316
317 /// The `errors` module contains utilities for handling errors related to packages.
318 ///
319 pub mod errors {
320 use crate::package::concepts::Package;
321 use std::process::exit;
322
323 /// Shows the standard error message and exit the current process with the system value `1` as error code.
324 ///
325 /// The standard error message is: `Missing argument. See help.`.
326 #[macro_export]
327 macro_rules! no_argument_and_exit {
328 () => {
329 write_error_message_and_exit("Missing argument. See help.", None);
330 };
331 }
332
333 /// Stops the execution if the package is invalid.
334 ///
335 /// # Arguments
336 /// * `package` - A reference to the `Package` to be validated.
337 ///
338 /// An invalid package is one where both name and version are empty.
339 /// In such cases, an error message is written and the process exits.
340 ///
341 pub fn stop_if_invalid_package(package: &Package) {
342 if package.name.is_empty() && package.version.is_empty() {
343 write_error_message_and_exit("Unsupported OS", None);
344 }
345 }
346
347 /// Writes an error message with an optional argument and exits the process with a status code of 1.
348 ///
349 /// # Arguments
350 /// * `message` - The error message to be written.
351 /// * `argument` - An optional argument to be included in the error message.
352 ///
353 pub fn write_error_message_and_exit(message: &str, argument: Option<&str>) {
354 write_error_message(message, argument);
355 exit(1);
356 }
357
358 /// Writes an error message to standard error with an optional argument. No exit value.
359 ///
360 /// # Arguments
361 /// * `message` - The error message to be written.
362 /// * `argument` - An optional argument to be included in the error message.
363 ///
364 pub fn write_error_message(message: &str, argument: Option<&str>) {
365 let final_message = match argument {
366 Some(a) => format!("{}: {}", message, a),
367 None => message.to_string(),
368 };
369
370 eprintln!("{final_message}");
371 }
372 }
373}
374
375/// The `facilities` module provides utility functions for handling
376/// temporary directories and processing command results.
377pub mod facilities {
378 use crate::package::errors::write_error_message_and_exit;
379 use chrono::{Datelike, Days, Local};
380 use csv::{Reader, ReaderBuilder};
381 use enable_ansi_support::enable_ansi_support;
382 use indicatif::{ProgressBar, ProgressStyle};
383 use regex::Regex;
384 use serde_json::Value;
385 use std::fs::File;
386 use std::ops::Add;
387 use std::path::{Path, PathBuf};
388 use std::process::Output;
389 use std::string::FromUtf8Error;
390 use std::{env, io};
391
392 /// Shows a flow of values
393 #[macro_export]
394 macro_rules! flow {
395 ($value:expr) => {
396 flow!(',', $value)
397 };
398
399 ($sep:expr, $value:expr) => {
400 $value.split($sep).for_each(|x| value!(x.trim()))
401 };
402 }
403
404 /// Formats a header for presentation. The text is formatted in upper cases with a green foreground.
405 ///
406 /// # Argument
407 ///
408 /// * `text`- The header text
409 ///
410 /// # Return
411 ///
412 /// A formatted String for this header.
413 #[macro_export]
414 macro_rules! header {
415 ($value:expr) => {
416 section!($value.to_string().to_ascii_uppercase()).fg(Color::Green)
417 };
418 }
419
420 /// Shows a section with a level for section positioning. If one value is given, this value will
421 /// be considering as the level 0.
422 ///
423 #[macro_export]
424 macro_rules! section_level {
425 ($value:expr) => {
426 section_level!(0, $value)
427 };
428
429 ($n:expr, $value:expr) => {{
430 let w: usize = $n;
431 println!("{:>w$} {}", "", section!($value))
432 }};
433 }
434
435 /// Formats a section for presentation.
436 ///
437 /// # Arguments
438 ///
439 /// * `title`- Title's section
440 /// * `fg`- Text's colour. If `None` uses the default colour.
441 /// * `bg`- Text background's colour. If `None` uses the default colour.
442 ///
443 /// # Returns
444 ///
445 /// A formatted String with foreground and background colours.
446 #[macro_export]
447 macro_rules! section {
448 ($value:expr) => {{
449 use termint::enums::Modifier;
450 use termint::widgets::ToSpan;
451
452 format!("[ {} ]", $value.to_string().first_uppercase())
453 .as_str()
454 .modifier(Modifier::BOLD)
455 }};
456
457 ($value:expr, $fg:expr) => {
458 section!($value).fg($fg)
459 };
460
461 ($value:expr, $fg:expr, $bg:expr) => {
462 section!($value, $fg).bg($bg)
463 };
464 }
465
466 /// Show a label and value with a standard formatted way.
467 ///
468 /// # Arguments
469 ///
470 /// * `label` - optional label to add to the output
471 /// * `value` - value to show on the output. The argument's name is using to build an automatic label
472 ///
473 #[macro_export]
474 macro_rules! stamp {
475 ($value:expr) => {{
476 let values = stringify!($value).split(".").collect::<Vec<&str>>();
477 if values.len() == 2 {
478 println!(
479 "{:>6} {}: {}",
480 "-",
481 values[1]
482 .to_string()
483 .wording()
484 .replace("_", " ")
485 .first_uppercase(),
486 $value
487 );
488 } else {
489 stamp!($value, $value);
490 }
491 }};
492
493 ($label:expr, $value:expr) => {
494 println!(
495 "{:>6} {}: {}",
496 "-",
497 $label.to_string().first_uppercase(),
498 $value
499 )
500 };
501 }
502
503 /// Shows the formatted value with a previous space of 6 positions
504 ///
505 /// # Argument
506 /// * `value` - The value to show
507 #[macro_export]
508 macro_rules! value {
509 ($value:expr) => {
510 println!("{:>6} {}", "-", $value)
511 };
512 }
513
514 /// Prints a slice of `Value` items, cleaning each value and separating them by commas.
515 ///
516 /// # Argument
517 ///
518 /// * `values` - A reference to a slice of `Value` objects that will be printed.
519 ///
520 pub fn print_values(values: &[Value]) {
521 values.iter().for_each(|value| {
522 match !value.is_object() {
523 true => println!("{:>7} {}", "-", String::cleaning(value.to_string())),
524 false => {
525 let map_value = value.as_object().unwrap();
526 map_value.iter().for_each(|(key, value)| {
527 println!("{:>6} {}:", "-", key.wording());
528 print_values(std::slice::from_ref(value));
529 });
530 }
531 };
532 })
533 }
534
535 /// Returns the current date as a formatted string.
536 ///
537 /// # Examples
538 /// ```
539 /// use fenir::facilities::get_current_date;
540 ///
541 /// let current_date = get_current_date();
542 /// println!("Today's date is: {}", current_date); // Output: "2023-10-05" (value may vary based on the current date)
543 /// ```
544 ///
545 /// # Returns
546 /// A `String` containing the current date in the format "YYYY-MM-DD".
547 ///
548 /// # Dependencies
549 /// - `chrono`: This function uses the `chrono` crate for date and time operations.
550 ///
551 /// # Panics
552 /// This function should not panic under normal circumstances.
553 pub fn get_current_date() -> String {
554 let local_date = Local::now();
555 format!(
556 "{}-{:02}-{:02}",
557 local_date.year(),
558 local_date.month(),
559 local_date.day()
560 )
561 }
562
563 /// Constructs a date range string from the provided arguments.
564 ///
565 /// This function extracts two dates from the `args` vector based on the
566 /// index provided by `option_new_date`, and returns them concatenated
567 /// with a semicolon (`;`) delimiter.
568 ///
569 /// # Arguments
570 ///
571 /// * `option_new_date` - An optional index pointing to the position
572 /// in `args` where the dates should be extracted.
573 /// * `args` - A vector of strings from which the dates are to be extracted.
574 ///
575 /// # Returns
576 ///
577 /// A `String` containing the concatenated date range, or an empty string
578 /// if the dates aren't found.
579 ///
580 /// # Examples
581 ///
582 /// ```
583 /// use fenir::facilities::build_dates_range;
584 ///
585 /// let dates = build_dates_range(Some(0), &["".to_string(), "2023-01-01".to_string(), "2023-12-31".to_string()]);
586 /// assert_eq!(dates, "2023-01-01;2023-12-31");
587 /// ```
588 pub fn build_dates_range(option_new_date: Option<usize>, args: &[String]) -> String {
589 let mut dates = String::new();
590 let regex = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap();
591 if let Some(date_first) = args.get(option_new_date.unwrap() + 1) {
592 if regex.is_match(date_first.as_str()) {
593 dates = dates.add(date_first);
594 } else {
595 dates = create_default_date();
596 }
597 } else {
598 dates = create_default_date();
599 }
600
601 if let Some(date_last) = args.get(option_new_date.unwrap() + 2) {
602 if regex.is_match(date_last.as_str()) {
603 dates = dates.add(";").add(date_last);
604 } else {
605 dates = create_next_date(dates);
606 }
607 } else {
608 dates = create_next_date(dates);
609 }
610
611 dates
612 }
613
614 fn create_next_date(dates: String) -> String {
615 let date_next = Local::now();
616 let mut date_result = String::new();
617 let result = date_next.checked_add_days(Days::new(1));
618 if let Some(date) = result {
619 date_result = dates
620 .add(";")
621 .add(date.format("%Y-%m-%d").to_string().as_str());
622 }
623
624 date_result
625 }
626
627 fn create_default_date() -> String {
628 let date_current = Local::now();
629 date_current.format("%Y-%m-%d").to_string()
630 }
631
632 /// Restores the current directory to its previous state and removes the specified temporary directory.
633 ///
634 /// # Arguments
635 /// * `current_dir` - A result containing the path to the current directory.
636 /// * `temp_dir_name` - The name of the temporary directory to be removed.
637 ///
638 /// This function sets the current directory to the one provided in `current_dir`,
639 /// and then removes the temporary directory specified by `temp_dir_name`
640 ///
641 pub fn restore_current_dir_and_remove_temp_dir(
642 current_dir: io::Result<PathBuf>,
643 temp_dir_name: &str,
644 ) {
645 env::set_current_dir(current_dir.unwrap()).expect(
646 "Can't restore initial \
647 directory",
648 );
649 let _ = std::fs::remove_dir_all(format!("/var/tmp/{}", temp_dir_name).as_str());
650 }
651
652 /// Displays the result of a command execution.
653 ///
654 /// # Arguments
655 /// * `command_result` - An `Output` struct representing the result of a command execution.
656 ///
657 /// # Returns
658 /// * `Result<String, FromUtf8Error>` - The output of the command as a `String` if successful,
659 /// or an error if the output cannot be converted from UTF-8.
660 ///
661 /// This function checks if the command was successful.
662 /// If the command failed, it writes an error message and exits.
663 /// If the output of the command cannot be converted to a string, it also writes an error message and exits.
664 ///
665 pub fn show_command_result(command_result: Output) -> Result<String, FromUtf8Error> {
666 if !command_result.status.success() {
667 write_error_message_and_exit("Package not found", None);
668 }
669
670 let command_output = String::from_utf8(command_result.stdout);
671 if command_output.is_err() {
672 write_error_message_and_exit("Error on output parsing", None);
673 }
674 command_output
675 }
676
677 /// Trait for cleaning string content. Used for remove the `"`, `FIELD_SEPARATOR_CHAR` characters
678 /// from value string.
679 pub trait Cleaning {
680 fn cleaning(value: String) -> String;
681 }
682
683 impl Cleaning for String {
684 fn cleaning(value: String) -> String {
685 let cleaning_char = ['"', FIELD_SEPARATOR_CHAR];
686
687 let mut final_result = value.clone();
688
689 if value.starts_with(cleaning_char) {
690 final_result.remove(0);
691 }
692
693 if value.ends_with(cleaning_char) {
694 final_result.remove(final_result.len() - 1);
695 }
696
697 final_result.replace("\\n", "\n")
698 }
699 }
700
701 /// This trait allows to split a string to words. The words are identifying with the first char as uppercase.
702 ///
703 /// # Example
704 ///
705 /// ```rust
706 /// use fenir::facilities::Wording;
707 ///
708 /// let result = "AStringForExample".to_string();
709 ///
710 /// assert_eq!("Astring for example".to_string(), result.wording())
711 /// ```
712 ///
713 pub trait Wording {
714 fn wording(&self) -> String;
715 }
716
717 impl Wording for String {
718 fn wording(&self) -> String {
719 let mut result = String::new();
720
721 let mut chars = self.chars();
722 let first = chars.nth(0);
723
724 if let Some(c) = first {
725 if c.is_ascii_lowercase() {
726 result.push(c.to_ascii_uppercase());
727 } else {
728 result.push(c);
729 }
730
731 let mut previous = ' ';
732
733 chars.for_each(|x| {
734 if x.is_ascii_lowercase() {
735 if previous.is_ascii_uppercase() {
736 result.push(' ');
737 }
738 result.push(x);
739 previous = x;
740 } else if x.is_ascii_uppercase() {
741 if previous.is_ascii_lowercase() {
742 result.push(' ');
743 previous = ' ';
744 }
745 result.push(x.to_ascii_lowercase());
746 } else {
747 result.push(x);
748 previous = x;
749 }
750 });
751 }
752
753 result
754 }
755 }
756
757 /// Categorizes the String content to verify if all char in the string is only in uppercases.
758 pub trait Categorize {
759 /// Verify if the string content is composing only of upper cases.
760 ///
761 /// # Arguments
762 ///
763 /// * `string` - The string for analysing.
764 ///
765 /// # Returns
766 ///
767 /// `true` if all string characters are uppercases, otherwise `false`.
768 ///
769 /// # Notes
770 ///
771 /// If the string is an empty string, will return `false`.
772 fn only_uppercases(string: String) -> bool;
773 }
774
775 impl Categorize for String {
776 fn only_uppercases(string: String) -> bool {
777 let mut status = false;
778 for chr in string.chars() {
779 if chr == ' ' {
780 continue;
781 }
782
783 status = chr.is_ascii_uppercase();
784
785 if !status {
786 break;
787 }
788 }
789
790 status
791 }
792 }
793
794 /// String standardization for data displaying. This trait prepares a string into the
795 /// format awaiting by `hierarchical_show()` function.
796 ///
797 pub trait Standardize {
798 /// Standardization is a process that allows to build a valid String format for hierarchical showing
799 ///
800 /// # Arguments
801 ///
802 /// * `self` - Self element on which the standardization will be applied.
803 /// * `keywords`- Keywords list to take account during the standardization.
804 ///
805 /// # Return
806 ///
807 /// The new String with the standardization
808 ///
809 /// # See
810 /// * `hierarchical_show()`.
811 ///
812 fn standardize(&self, keywords: Vec<&str>) -> String;
813 }
814
815 /// Char field separator for fields content long string from database
816 const FIELD_SEPARATOR_CHAR: char = ':';
817
818 /// String field separator for fields content long string from database
819 pub const FIELD_SEPARATOR_STRING: &str = ":";
820
821 pub const FIELD_SEPARATOR_STRING_LONG: &str = "::";
822
823 impl Standardize for String {
824 fn standardize(&self, keywords: Vec<&str>) -> String {
825 if self.is_empty() {
826 return String::new();
827 }
828
829 let elements = self.split(FIELD_SEPARATOR_CHAR).collect::<Vec<&str>>();
830 let ref_value = 2;
831 let mut counter = 0;
832 let mut result = Vec::<String>::new();
833
834 elements.iter().for_each(|s| {
835 if s.is_empty() {
836 if counter != ref_value {
837 counter += 1;
838 result.push(FIELD_SEPARATOR_STRING.to_string());
839 } else {
840 counter = 0;
841 }
842 } else {
843 if result.last().is_some() && result.last().unwrap() != FIELD_SEPARATOR_STRING {
844 result.push(FIELD_SEPARATOR_STRING.to_string());
845 }
846 if keywords.contains(s) {
847 result.push(s.to_string().to_uppercase());
848 } else if String::only_uppercases(s.to_string()) {
849 result.push(s.to_string().to_lowercase());
850 } else {
851 result.push(s.to_string());
852 }
853
854 counter = 0;
855 }
856 });
857
858 begin_complement(&mut result);
859
860 end_complement(&mut result);
861
862 String::from_iter(result)
863 }
864 }
865
866 fn begin_complement(result: &mut Vec<String>) {
867 result[0..2]
868 .iter_mut()
869 .filter(|x| *x != FIELD_SEPARATOR_STRING)
870 .for_each(|x| x.insert(0, FIELD_SEPARATOR_STRING.parse().unwrap()));
871 }
872
873 fn end_complement(result: &mut Vec<String>) {
874 let length = result.len();
875 result[length - 2..length]
876 .iter_mut()
877 .filter(|x| *x != FIELD_SEPARATOR_STRING)
878 .for_each(|x| x.push(FIELD_SEPARATOR_STRING_LONG.parse().unwrap()));
879 }
880
881 /// Transform the list of keywords tho a String value with some specifics separators.
882 /// With this trait, all keywords will be separated with specific separators.
883 ///
884 pub trait Transform {
885 fn transform(&self, keywords: Vec<&str>) -> String;
886 }
887
888 /// Transform the string value to another string that replace all separator `::` with `-` and end
889 /// of subline with the `\n`.
890 impl Transform for String {
891 fn transform(&self, keywords: Vec<&str>) -> String {
892 let mut result = String::new();
893 let tmp = format!("{}{}", FIELD_SEPARATOR_STRING, FIELD_SEPARATOR_STRING);
894 let mark = tmp.as_str();
895
896 let mut previous_key = "";
897 keywords.iter().for_each(|&keyword| {
898 let regex = Regex::new(keyword.to_string().as_str())
899 .unwrap_or_else(|_| panic!("Invalid regular expression pattern: {}", keyword));
900 regex
901 .split(self)
902 .filter(|&sentence| !sentence.is_empty())
903 .for_each(|sentence| {
904 if sentence == mark {
905 result.push_str(mark);
906 result.push_str(keyword);
907 previous_key = keyword;
908 } else if sentence.chars().nth(0).unwrap() == FIELD_SEPARATOR_CHAR {
909 if previous_key.is_empty() {
910 result.push_str(mark);
911 result.push_str(keyword);
912 }
913 result.push_str(sentence);
914 } else {
915 result = result.replace(keyword, "");
916 result.push_str(mark);
917 result.push_str(keyword);
918 result.push(FIELD_SEPARATOR_CHAR);
919
920 if !sentence.contains(previous_key) || previous_key.is_empty() {
921 result.push_str(sentence);
922 }
923
924 previous_key = keyword;
925 }
926 });
927 });
928
929 if !result.ends_with(mark) && result.ends_with(FIELD_SEPARATOR_STRING) {
930 result.push(FIELD_SEPARATOR_CHAR);
931 }
932
933 result
934 }
935 }
936
937 /// Reduce a string to the given size. If the string length is > to the size value, the end of the
938 /// string is replaced by the `...`.
939 ///
940 /// # Example
941 ///
942 /// ```rust
943 /// use fenir::facilities::Reduce;
944 ///
945 /// let original = String::from("This is an example string");
946 /// let result = original.reduce(16);
947 ///
948 /// assert_eq!(String::from("This is an ex..."), result);
949 /// ```
950 pub trait Reduce {
951 /// Reduce the string to the given size.
952 ///
953 /// # Argument
954 ///
955 /// * `size` - The size of the new string
956 fn reduce(&self, size: usize) -> String;
957 }
958
959 impl Reduce for String {
960 fn reduce(&self, size: usize) -> String {
961 if self.is_empty()
962 || size == 0
963 || size > self.len()
964 || self == Value::Null.to_string().as_str()
965 {
966 self.clone()
967 } else if size <= "...".len() {
968 String::from("...")
969 } else {
970 let reduced =
971 self.escape_default().to_string()[0..(size - "...".len())].to_string();
972 format!("{reduced}...")
973 }
974 }
975 }
976
977 /// Configures and returns a CSV reader with the specific settings.
978 ///
979 /// This function sets up a CSV reader with custom options such as delimiter,
980 /// headers, and flexible end-of-line handling.
981 ///
982 /// # Arguments
983 /// * `csv_path`- Path to the csv file.
984 ///
985 /// # Settings
986 ///
987 /// * `delimiter` - A `u8` byte set on `,` separator.
988 /// * `has_headers`- A bool that specifies the CSV contains some header, set to `true`.
989 /// * `flexible` - A `bool` that specifies whether to handle flexible end-of-line characters, set to `true`.
990 ///
991 /// # Returns
992 ///
993 /// * `csv::Reader<File>` - A `csv::Reader` configured based on the given settings.
994 pub fn configure_csv_reader(csv_path: &Path) -> csv::Result<Reader<File>> {
995 ReaderBuilder::new()
996 .delimiter(b',')
997 .flexible(true)
998 .has_headers(true)
999 .from_path(csv_path)
1000 }
1001
1002 /// Shows the element list as a hierarchical design.
1003 ///
1004 /// # Arguments
1005 /// * `section` - The section name for this hierarchy.
1006 /// * `elements` - List of elements separated by the ':' separator.
1007 ///
1008 pub fn hierarchical_show(section: &str, elements: String) {
1009 if elements.is_empty() {
1010 return;
1011 }
1012
1013 println!();
1014 let mut width: i32 = 0;
1015
1016 let str_cleaning = String::cleaning(elements);
1017
1018 section_level!(2, section);
1019 if !str_cleaning.contains(FIELD_SEPARATOR_STRING) {
1020 let w = (width + 7) as usize;
1021 println!("{:>w$} {}\n", "-", str_cleaning.trim());
1022 return;
1023 }
1024
1025 let parts: Vec<&str> = str_cleaning.split(FIELD_SEPARATOR_STRING).collect();
1026 let ref_section = parts.get(1).unwrap_or(&"");
1027
1028 section_show(ref_section, 0);
1029
1030 let mut old_value = vec![];
1031 let mut last_section = ref_section;
1032
1033 parts.iter().for_each(|part| {
1034 if !part.is_empty() && last_section != part {
1035 if String::only_uppercases(part.to_string()) {
1036 last_section = part;
1037 if last_section == ref_section {
1038 width = -1;
1039 }
1040 width += 1;
1041 section_show(part, width as usize);
1042 old_value.clear();
1043 } else if !part.is_empty() && !old_value.contains(part) {
1044 let w = (width + 7) as usize;
1045 println!("{:>w$} {}", "-", part.trim());
1046 old_value.push(*part);
1047 }
1048 }
1049 });
1050 }
1051
1052 fn section_show(value: &str, width: usize) {
1053 let lower = String::from(value).to_ascii_lowercase();
1054 let wording = lower.wording();
1055 let words: Vec<&str> = wording.as_str().split(' ').collect();
1056 match words.len() {
1057 1..3 => {
1058 if !value.is_empty() {
1059 let w = width + 3;
1060 section_level!(w, value);
1061 }
1062 }
1063 _ => {
1064 if !value.is_empty() {
1065 let w = width + 7;
1066 println!("{:>w$} {}", "-", value.trim());
1067 }
1068 }
1069 };
1070 }
1071
1072 /// Parse the current string for returning a string with the first character in uppercase.
1073 pub trait Uppercase {
1074 /// Transform the first character of a String to an uppercase
1075 ///
1076 /// # Returns
1077 /// The new string with the first character in uppercase
1078 fn first_uppercase(&self) -> String;
1079 }
1080
1081 impl Uppercase for String {
1082 fn first_uppercase(&self) -> String {
1083 let c = self.chars().nth(0).unwrap();
1084 format!("{}{}", c.to_ascii_uppercase(), &self[1..])
1085 }
1086 }
1087
1088 /// Configure the terminal for insure the ansi support colours
1089 /// for all platforms.
1090 pub fn configure_terminal() {
1091 let _ = enable_ansi_support();
1092 }
1093
1094 /// Progress bar for text console.
1095 pub struct ProgressText {
1096 progress: ProgressBar,
1097 }
1098
1099 impl ProgressText {
1100 /// Create a new occurrence of the ProgressText.
1101 ///
1102 /// # Arguments
1103 /// * `current` - Current value of the progress bar text.
1104 /// * `max`- Max value of the progress bar text.
1105 /// * `message`- Complement message adding at the end of the progress bar text.
1106 pub fn new(current: usize, max: usize, message: String) -> ProgressText {
1107 let progress = ProgressBar::new(max as u64)
1108 .with_message(message)
1109 .with_position(current as u64);
1110 progress.set_style(
1111 ProgressStyle::default_bar()
1112 .template(
1113 "[{elapsed_precise}] {bar:40.cyan/blue} {pos}/{len} records for '{msg}'",
1114 )
1115 .unwrap(),
1116 );
1117
1118 Self { progress }
1119 }
1120
1121 /// Increment the value of the progress bar and show the progression.
1122 ///
1123 /// # Note
1124 ///
1125 /// If the value of the progress bar is reach to the max value, this progress bar is considering as done.
1126 pub fn progress(&mut self) {
1127 if self.progress.position() == self.progress.length().unwrap() {
1128 self.progress.finish();
1129 } else {
1130 self.progress.inc(1);
1131 }
1132 }
1133
1134 /// # Return
1135 /// The current value of the progress bar.
1136 pub fn value(&self) -> usize {
1137 self.progress.position() as usize
1138 }
1139
1140 /// # Return
1141 /// The max value of the progress bar
1142 pub fn max(&self) -> usize {
1143 self.progress.length().unwrap() as usize
1144 }
1145
1146 /// Status of the progress bar.
1147 ///
1148 /// # Return
1149 /// `true` if the progress bar is done. That means if the progress bar is done, some internal values
1150 /// will be unavailable (like message).
1151 pub fn is_done(&self) -> bool {
1152 self.progress.is_finished()
1153 }
1154 }
1155}
1156
1157/// Several features for specific Os implementation. Provide several methods that can be used
1158/// for Os support implementations.
1159pub mod os {
1160 use std::path::{Path, PathBuf};
1161 use std::{env, fs};
1162
1163 /// The `OSFamily` enum represents different families of operating systems.
1164 ///
1165 /// This enum provides several variants:
1166 /// - `Debian`: Represents Debian-based operating systems.
1167 /// - `macOS`: Represents the macOS operating systems.
1168 /// - `RedHat`: Represents RedHat-based operating systems.
1169 /// - `Unsupported`: Represents unsupported or unrecognized operating systems.
1170 /// - `Windows`: Represents the Windows operating systems.
1171 ///
1172 #[derive(PartialEq, Eq)]
1173 pub enum OSFamily {
1174 /// Represents Debian-based operating systems.
1175 Debian,
1176
1177 /// Represents Macos-based operating systems.
1178 MacOS,
1179
1180 /// Represents RedHat-based operating systems.
1181 RedHat,
1182
1183 /// Represents unsupported or unrecognized operating systems.
1184 Unsupported,
1185
1186 ///Represents the Windows-based operating systems.
1187 Windows,
1188 }
1189
1190 // Constants for file paths
1191 const DEBIAN_PATH: &str = "/usr/bin/apt-get";
1192 const MACOS_PATH: &str = "/usr/bin/sw_vers";
1193 const REDHAT_PATH: &str = "/usr/bin/rpm";
1194 const WINDOWS_PATH: &str = "c:\\";
1195
1196 /// Control if the current Os is included into the supported Os.
1197 pub struct SupportedOs;
1198 impl SupportedOs {
1199 /// Detects the operating system by checking for the existence of certain files.
1200 ///
1201 /// # Returns
1202 ///
1203 /// * `OSFamily::Debian` - if the system has `/usr/bin/apt-get`.
1204 /// * `OSFamily::macOS` - if the system has `/usr/bin/sw_vers`.
1205 /// * `OSFamily::RedHat` - if the system has `/usr/bin/rpm`.
1206 /// * `OSFamily::Windows` - if the system has `c:\\`.
1207 /// * `OSFamily::Unsupported` - if none of those files are found.
1208 pub fn supported_os() -> OSFamily {
1209 Debian::detect_os()
1210 }
1211 }
1212
1213 /// Detect the current os family.
1214 trait OsDetector {
1215 /// Detects the current OSFamily.
1216 ///
1217 /// # Returns
1218 ///
1219 /// A value of type of `OSFamily`.
1220 fn detect_os() -> OSFamily;
1221 }
1222
1223 /// Debian Os detector
1224 struct Debian;
1225 impl OsDetector for Debian {
1226 fn detect_os() -> OSFamily {
1227 if file_exists(DEBIAN_PATH) {
1228 OSFamily::Debian
1229 } else {
1230 Redhat::detect_os()
1231 }
1232 }
1233 }
1234
1235 /// Redhat Os detector
1236 struct Redhat;
1237 impl OsDetector for Redhat {
1238 fn detect_os() -> OSFamily {
1239 if file_exists(REDHAT_PATH) {
1240 OSFamily::RedHat
1241 } else {
1242 MacOS::detect_os()
1243 }
1244 }
1245 }
1246
1247 /// MacOs detector
1248 struct MacOS;
1249 impl OsDetector for MacOS {
1250 fn detect_os() -> OSFamily {
1251 if file_exists(MACOS_PATH) {
1252 OSFamily::MacOS
1253 } else {
1254 Windows::detect_os()
1255 }
1256 }
1257 }
1258
1259 /// Windows Os detector.
1260 struct Windows;
1261 impl OsDetector for Windows {
1262 fn detect_os() -> OSFamily {
1263 if file_exists(WINDOWS_PATH) {
1264 OSFamily::Windows
1265 } else {
1266 OSFamily::Unsupported
1267 }
1268 }
1269 }
1270
1271 /// Checks if a file exists at the given path.
1272 fn file_exists(path: &str) -> bool {
1273 Path::new(path).exists()
1274 }
1275
1276 /// Find the tyr home's path directory according to current OS.
1277 ///
1278 /// # Returns
1279 ///
1280 /// The home path directory. If the OS is not supported, the home tyr path directory will be defined on the current dir.
1281 pub fn find_tyr_home_path() -> PathBuf {
1282 match SupportedOs::supported_os() {
1283 OSFamily::Windows => Path::new(Option::unwrap(env::var_os("HOMEDRIVE")).as_os_str())
1284 .join(Option::unwrap(env::var_os("HOMEPATH")).as_os_str())
1285 .join(".tyr"),
1286 OSFamily::Unsupported => Path::new(".").join(".tyr"),
1287 _ => Path::new(Option::unwrap(env::var_os("HOME")).as_os_str()).join(".tyr"),
1288 }
1289 }
1290
1291 /// Create the tyr home path directory.
1292 ///
1293 /// # Arguments
1294 ///
1295 /// * `tyr_path` - the tyr path that will be used as tyr home path.
1296 ///
1297 /// # Notes
1298 ///
1299 /// If the tyr home path directory exists yet, it will not be recreated.
1300 pub fn create_tyr_home(tyr_path: &Path) {
1301 if !tyr_path.exists() {
1302 fs::create_dir(tyr_path).expect("Can't create tyr home path");
1303 }
1304 }
1305
1306 /// Parse the raw id given as parameter and return the found id as u32
1307 ///
1308 /// # Arguments
1309 ///
1310 /// * `raw_id` - The raw cwe id of the form of `CWE-<num>` or `CAPEC-<num>`.
1311 ///
1312 /// # Results
1313 ///
1314 /// The id that is corresponding to the `<num>` part.
1315 pub fn parse_id(raw_id: &str, prefix: &str) -> Option<u32> {
1316 let parts: Vec<&str> = raw_id.split('-').collect();
1317 if parts.len() == 2 && parts[0].eq_ignore_ascii_case(prefix) {
1318 parts[1].parse().ok()
1319 } else {
1320 None
1321 }
1322 }
1323}
1324
1325/// Module for database features (based on SQLite)
1326pub mod database {
1327 use crate::capec::CapecRecord;
1328 use crate::cwe::CweRecord;
1329 use crate::download::download_mitre_archive;
1330 use crate::facilities::{FIELD_SEPARATOR_STRING, Uppercase};
1331 use crate::mitre::MitreRecord;
1332 use crate::network::{fetch_json_response, parse_response};
1333 use crate::os::{create_tyr_home, find_tyr_home_path};
1334 use crate::package::errors::{write_error_message, write_error_message_and_exit};
1335 use crate::section;
1336 use regex::Regex;
1337 use rusqlite::{Connection, Rows, Statement, params};
1338 use serde_json::Value;
1339 use std::error::Error;
1340 use std::path::{Path, PathBuf};
1341
1342 /// Mitre database name
1343 pub const MITRE_DB_NAME: &str = "mitre.db";
1344
1345 /// Open the database connexion
1346 ///
1347 /// # Example
1348 /// ```no_run
1349 /// match db_mitre!() {
1350 /// Ok(connection) => println!("Connected with {:?}", connection),
1351 /// Err(_) => eprintln!("Connection failed"),
1352 /// };
1353 /// ```
1354 #[macro_export]
1355 macro_rules! db_mitre {
1356 () => {
1357 Connection::open(db_mitre_path())
1358 };
1359 }
1360
1361 /// The `MitreData` struct represents the necessary information to Mitre Data to used to.
1362 ///
1363 /// # Fields
1364 ///
1365 /// * `url` - A `String` that holds the URL for querying MITRE data to be used.
1366 /// * `sources` - A `Vector<String>` that denotes the list of path to download to be processed.
1367 #[derive(Clone)]
1368 pub struct MitreData {
1369 /// Data's name
1370 pub name: &'static str,
1371
1372 /// The url to use for the mitre query
1373 pub url: &'static str,
1374
1375 /// List of sources files
1376 pub files: Vec<&'static str>,
1377 }
1378
1379 /// Defines a MitreData definition.
1380 ///
1381 pub trait MitreDefinition {
1382 /// Define the MitreData
1383 fn define() -> MitreData;
1384 }
1385
1386 /// Define the tables definition for the Mitre data
1387 pub trait MitreDatabase {
1388 /// Gets the list of table definitions
1389 fn table_definitions(&self) -> Vec<(&'static str, &'static str)>;
1390
1391 /// Gets the list of index definitions
1392 fn index_definitions(&self) -> Vec<(&'static str, &'static str)>;
1393 }
1394
1395 /// Manage the database according to the database model defined with `MitreDatabase`.
1396 pub struct Database<T: MitreDatabase> {
1397 pub database: T,
1398 }
1399
1400 impl<T: MitreDatabase> Database<T> {
1401 /// Checks if the database is valid or not.
1402 pub fn check_db(&self) {
1403 let conn = db_mitre!().unwrap();
1404
1405 self.database
1406 .table_definitions()
1407 .iter()
1408 .for_each(|(table, _)| {
1409 let query = format!("SELECT COUNT(*) FROM {}", table);
1410 let result = conn.query_row(query.as_str(), [], |row| row.get::<usize, u32>(0));
1411 if result.is_err() {
1412 write_error_message_and_exit(
1413 result.err().unwrap().to_string().as_str(),
1414 None,
1415 );
1416 } else {
1417 println!(
1418 "Successfully queried MITRE table '{}' ({} records)",
1419 table,
1420 result.ok().unwrap()
1421 );
1422 }
1423 });
1424 }
1425
1426 /// Create the table list.
1427 pub fn init_db(&self) {
1428 self.drop_tables();
1429
1430 match db_mitre!() {
1431 Ok(connection) => {
1432 self.database
1433 .table_definitions()
1434 .iter()
1435 .for_each(|(name, definition)| {
1436 match connection.execute(definition, params![]) {
1437 Ok(_) => println!("Successfully created MITRE '{}'", name),
1438 Err(e) => eprintln!("Error creating MITRE '{}' : {:?}", name, e),
1439 };
1440 });
1441
1442 self.drop_indexes();
1443
1444 self.database
1445 .index_definitions()
1446 .iter()
1447 .for_each(|(idx, definition)| {
1448 match connection.execute(definition, params![]) {
1449 Ok(_) => println!("Successfully created MITRE index '{}'", idx),
1450 Err(e) => eprintln!("Error creating MITRE index {}: {:?}", idx, e),
1451 }
1452 });
1453 }
1454 Err(e) => {
1455 write_error_message_and_exit(
1456 "Database connection error",
1457 Some(e.to_string().as_str()),
1458 );
1459 }
1460 };
1461 }
1462
1463 fn drop_tables(&self) {
1464 let connection = db_mitre!().unwrap();
1465 self.database
1466 .table_definitions()
1467 .iter()
1468 .for_each(|(table, _)| {
1469 let _ = connection
1470 .execute(
1471 format!("DROP TABLE IF EXISTS {}", table).as_str(),
1472 params![],
1473 )
1474 .unwrap_or_else(|_| panic!("Error on dropping table: {}", table));
1475 });
1476 }
1477
1478 fn drop_indexes(&self) {
1479 let connection = db_mitre!().unwrap();
1480 self.database
1481 .index_definitions()
1482 .iter()
1483 .for_each(|(idx, _)| {
1484 let _ = connection
1485 .execute(format!("DROP INDEX IF EXISTS {}", idx).as_str(), params![])
1486 .unwrap_or_else(|_| panic!("Error on index dropping: {}", idx));
1487 });
1488 }
1489 }
1490
1491 /// Executes a given query and processes the response.
1492 ///
1493 /// This function takes a `Result` containing a `String` query or an error message.
1494 /// It unwraps the query string, fetches the response from the URL specified by the query,
1495 /// and then parses this response into a `Value`.
1496 ///
1497 /// # Arguments
1498 ///
1499 /// * `query` - A `Result` wrapping a `String` query or an error message.
1500 ///
1501 /// # Returns
1502 ///
1503 /// * A `Value` obtained from parsing the response. If the response fetching or parsing
1504 /// fails, it returns a default `Value`.
1505 ///
1506 /// # Panics
1507 ///
1508 /// This function will panic if `query` is an `Err` variant.
1509 ///
1510 /// # Example
1511 ///
1512 /// ```rust
1513 /// use fenir::database::execute_query;
1514 ///
1515 /// let query = Ok(String::from("http://example.com/api"));
1516 /// let value = execute_query(query);
1517 /// ```
1518 pub fn execute_query(query: Result<String, &str>) -> Value {
1519 let query_str = query.unwrap();
1520 let response = fetch_json_response(&query_str);
1521
1522 parse_response(response)
1523 }
1524
1525 /// New type for injector function that will be used for injecting data into the local
1526 /// SQLite database.
1527 type InjectorFunction = fn(&Path) -> Result<(), Box<dyn Error>>;
1528
1529 /// Refresh the Mitre data that will be injected into the local database.
1530 ///
1531 /// # Arguments
1532 ///
1533 /// * `url` - The url to used to,
1534 /// * `file`- The file to download to.
1535 /// * `injector`- The method used for injecting the csv into the local database.
1536 ///
1537 /// # Results
1538 ///
1539 /// The final result of these operations.
1540 pub fn refresh_data_mitre_into_database(url: String, file: String, injector: InjectorFunction) {
1541 // Download and extract the archive
1542 let env_path = find_tyr_home_path();
1543 create_tyr_home(&env_path);
1544 let source_file = match download_mitre_archive(url, file.clone(), &env_path) {
1545 Err(e) => {
1546 write_error_message_and_exit("Error during download", Some(e.to_string().as_str()));
1547 String::new()
1548 }
1549 Ok(file) => file.clone(),
1550 };
1551
1552 if !source_file.is_empty() {
1553 println!(
1554 "Source file downloaded successfully: {}",
1555 source_file.clone()
1556 );
1557 }
1558
1559 match injector(&env_path.clone().join(source_file.clone())) {
1560 Err(e) => {
1561 write_error_message(
1562 "Error during archive injection",
1563 Some(e.to_string().as_str()),
1564 );
1565 }
1566 _ => {
1567 println!("Operation completed successfully: {}\n", file.clone());
1568 }
1569 }
1570
1571 let _ = std::fs::remove_file(env_path.join(file.clone()));
1572 let _ = std::fs::remove_file(env_path.join(source_file.clone()));
1573 }
1574
1575 /// find the cwe by its id.
1576 ///
1577 /// # Arguments
1578 ///
1579 /// * `cwe_id`- CWE's id
1580 ///
1581 /// # Returns
1582 ///
1583 /// A `CweRecord` if CWE found or `None`.
1584 ///
1585 pub fn find_cwe_by_id(cwe_id: u32) -> Option<CweRecord> {
1586 let connection = db_mitre!().unwrap();
1587 let mut statement = connection
1588 .prepare("SELECT * FROM cwe WHERE id= :id_cwe")
1589 .ok()?;
1590 let mut rows = statement.query(&[(":id_cwe", &cwe_id)]).ok()?;
1591 let row = rows.next().ok()?;
1592 if row.is_some() {
1593 Some(CweRecord {
1594 id: row?.get(0).unwrap(),
1595 name: row?.get(1).unwrap(),
1596 description: row?.get(2).unwrap(),
1597 extended: row?.get(3).unwrap_or("".to_string()),
1598 consequences: row?.get(4).unwrap(),
1599 detections: row?.get(5).unwrap_or("".to_string()),
1600 mitigations: row?.get(6).unwrap_or("".to_string()),
1601 attacks: row?.get(7).unwrap_or("".to_string()),
1602 })
1603 } else {
1604 None
1605 }
1606 }
1607
1608 /// find the capec by its id.
1609 ///
1610 /// # Arguments
1611 ///
1612 /// * `capec_id`- Capec's id
1613 ///
1614 /// # Returns
1615 ///
1616 /// A `CapecRecord` if CAPEC found or `None`.
1617 ///
1618 pub fn find_capec_by_id(capec_id: u32) -> Option<CapecRecord> {
1619 let connection = db_mitre!().unwrap();
1620 let mut statement = connection
1621 .prepare("SELECT * FROM capec WHERE id= :id_capec")
1622 .ok()?;
1623 let mut rows = statement.query(&[(":id_capec", &capec_id)]).ok()?;
1624 let row = rows.next().ok()?;
1625 if row.is_some() {
1626 Some(CapecRecord {
1627 id: row?.get(0).unwrap(),
1628 name: row?.get(1).unwrap(),
1629 abstraction: row?.get(2).unwrap(),
1630 status: row?.get(3).unwrap(),
1631 description: row?.get(4).unwrap(),
1632 alternates: row?.get(5).unwrap_or("".to_string()),
1633 likelihood: row?.get(6).unwrap_or("".to_string()),
1634 severity: row?.get(7).unwrap_or("".to_string()),
1635 attacks: row?.get(8).unwrap_or("".to_string()),
1636 flow: row?.get(9).unwrap_or("".to_string()),
1637 prerequisites: row?.get(10).unwrap_or("".to_string()),
1638 skills: row?.get(11).unwrap_or("".to_string()),
1639 resources: row?.get(12).unwrap_or("".to_string()),
1640 indicators: row?.get(13).unwrap_or("".to_string()),
1641 consequences: row?.get(14).unwrap_or("".to_string()),
1642 mitigations: row?.get(15).unwrap_or("".to_string()),
1643 instances: row?.get(16).unwrap_or("".to_string()),
1644 weaknesses: row?.get(17).unwrap_or("".to_string()),
1645 taxonomy: row?.get(18).unwrap_or("".to_string()),
1646 })
1647 } else {
1648 None
1649 }
1650 }
1651
1652 /// Analyse a string criteria and split each element by separating the words criteria from the operators.
1653 ///
1654 /// # Arguments
1655 ///
1656 /// * `criteria` - Criteria string for analysis
1657 ///
1658 /// # Results
1659 ///
1660 /// A couple of values where the first argument is the list of operators and the second the list of words of the criteria
1661 ///
1662 pub fn criteria_analysis(criteria: String) -> Option<(Vec<String>, Vec<String>)> {
1663 if criteria.is_empty() {
1664 None
1665 } else {
1666 let regex = Regex::new(r"\b(?:and|not|or)\b");
1667 let result = regex
1668 .clone()
1669 .unwrap()
1670 .captures_iter(&criteria)
1671 .collect::<Vec<_>>();
1672 let mut operators = Vec::<String>::new();
1673 result.iter().for_each(|cap| {
1674 operators.push(cap.get(0).unwrap().as_str().to_string());
1675 });
1676
1677 let mut crit = criteria.clone();
1678 ["not ", "and ", "or "].iter().for_each(|&o| {
1679 crit = crit.replace(o, "^");
1680 });
1681
1682 let words = words_collector(criteria);
1683 Some((words, operators))
1684 }
1685 }
1686
1687 fn words_collector(criteria: String) -> Vec<String> {
1688
1689 let mut crit = criteria.clone();
1690 ["not ", "and ", "or "].iter().for_each(|&o| {
1691 crit = crit.replace(o, "^");
1692 });
1693
1694 let mut words = Vec::<String>::new();
1695 let mut others = Vec::<char>::new();
1696 let mut keep_space = false;
1697 let chars = crit.chars();
1698 chars.for_each(|c| {
1699 if c != '\'' && c == '^' {
1700 if !others.is_empty() {
1701 words.push(others.iter().collect());
1702 others.clear();
1703 }
1704 words.push(String::new());
1705 } else if keep_space || c != ' ' {
1706 others.push(c);
1707 } else {
1708 keep_space = !keep_space;
1709 }
1710 });
1711
1712 if !others.is_empty() {
1713 words.push(others.iter().collect());
1714 }
1715
1716 words
1717 }
1718
1719 /// Split the arguments list to `domain` and `criteria`. `domain` is corresponding to
1720 /// the name of field into the concerning table's database.
1721 ///
1722 /// # Arguments
1723 ///
1724 /// * `arguments` - List of arguments that's composing the query
1725 ///
1726 /// # Results
1727 ///
1728 /// A couple of values with the domain for the first field, and left of criteria for the
1729 /// second field.
1730 pub fn find_domain_and_criteria(arguments: &[String]) -> Option<(String, String)> {
1731 let string_args = arguments.iter().last().unwrap();
1732 let mut pattern = "=";
1733 let is_sep = match string_args.find(pattern) {
1734 Some(s) => Some(s),
1735 None => {
1736 pattern = "not";
1737 string_args.find(pattern)
1738 }
1739 };
1740
1741 if let Some(pos_sep) = is_sep {
1742 let domain = String::from(string_args[0..pos_sep].trim());
1743 let criteria = match pattern {
1744 "not" => format!("not {}", string_args[pos_sep + pattern.len()..].trim()),
1745 _ => String::from(string_args[pos_sep + pattern.len()..].trim()),
1746 };
1747
1748 match domain.is_empty() || criteria.is_empty() {
1749 true => None,
1750 false => Some((domain, criteria)),
1751 }
1752 } else {
1753 None
1754 }
1755 }
1756
1757 /// Associate the list of arguments with the SQL query in construction
1758 ///
1759 /// # Arguments
1760 ///
1761 /// * `words` - list of arguments words
1762 /// * `statement`- statement SQL in construction
1763 ///
1764 /// # Results
1765 ///
1766 /// The statement completed by the criteria binding
1767 ///
1768 pub fn bind_criteria<'a>(
1769 words: &[String],
1770 statement: Result<Statement<'a>, rusqlite::Error>,
1771 ) -> Statement<'a> {
1772 let mut counter = 1;
1773 let mut statement = statement.unwrap();
1774 let nb = words.len();
1775 for word in words.iter().take(nb) {
1776 if !word.trim().is_empty() {
1777 statement
1778 .raw_bind_parameter(counter, format!("%{}%", word.to_lowercase().trim()))
1779 .unwrap_or_else(|_| {
1780 panic!(
1781 "{}",
1782 format!("Can't bind parameter: {}", word.to_lowercase()).trim()
1783 )
1784 });
1785 counter += 1;
1786 }
1787 }
1788
1789 statement
1790 }
1791
1792 /// Prepare the query model given as parameter with the elements of criteria
1793 ///
1794 /// # Arguments
1795 ///
1796 /// * `query_template`- Query model used to prepare the final query
1797 /// * `words`- list of words to use as criteria into the query template.
1798 /// * `operators`- list of operators.
1799 ///
1800 /// # Results
1801 ///
1802 /// The final query
1803 pub fn prepare_query(
1804 query_template: String,
1805 words: Vec<String>,
1806 operators: &[String],
1807 ) -> FinalQuery {
1808 let mut query = query_template.clone();
1809 let domain = query_template.rsplit_once(' ').unwrap().1;
1810
1811 let mut count_like = 1;
1812 let mut count_op = 0;
1813
1814 words.iter().for_each(|word| {
1815 if !word.trim().is_empty() {
1816 query.push_str(format!(" LIKE ?{}", count_like).as_str());
1817 count_like += 1;
1818 } else if count_op < operators.len() {
1819 let op = &operators[count_op];
1820 count_op += 1;
1821 let query_str = match op.as_str() {
1822 "not" => format!(" {} ", op.trim()),
1823 _ => format!(" {op} {domain} ")
1824 };
1825
1826 query.push_str(query_str.as_str());
1827 }
1828 });
1829
1830 (query, words)
1831 }
1832
1833 /// Return the path of mitre database
1834 ///
1835 /// # Results
1836 ///
1837 /// Mitre database path
1838 pub fn db_mitre_path() -> PathBuf {
1839 let tyr_home = find_tyr_home_path();
1840 create_tyr_home(&tyr_home);
1841 tyr_home.join(MITRE_DB_NAME)
1842 }
1843
1844 /// Record extractor from the search result
1845 pub type SearchResultExtractor<T> = fn(&mut Rows) -> Vec<T>;
1846
1847 type FinalQuery = (String, Vec<String>);
1848
1849 /// Search the list of words into the database.
1850 ///
1851 /// `<T>` is the type of record as the result of the extraction.
1852 ///
1853 /// # Arguments
1854 /// * `query_string` - SQL string query
1855 /// * `words` - list of words for this query
1856 /// * `extractor`- function use to extract the searching result
1857 ///
1858 /// # Result
1859 ///
1860 /// A list of records according to T type
1861 ///
1862 pub fn search<T>(final_query: FinalQuery, extractor: SearchResultExtractor<T>) -> Vec<T> {
1863 let connection = db_mitre!().unwrap();
1864 let statement = connection.prepare(final_query.0.as_str());
1865 let mut binding = bind_criteria(&final_query.1, statement);
1866 let mut rows = binding.raw_query();
1867
1868 extractor(&mut rows)
1869 }
1870
1871 /// Parse the string values to a list of unsigned 32 bits.
1872 /// The string must be formatted like this: `::<num>::<num>::...::`
1873 ///
1874 /// # Argument
1875 /// * `values` - raw string values to parse to
1876 ///
1877 /// # Result
1878 /// The list of unsigned 32 values.
1879 pub fn parse_id_to_u32(values: String) -> Vec<u32> {
1880 let mut id_list: Vec<u32> = values
1881 .split(FIELD_SEPARATOR_STRING)
1882 .map(|v| {
1883 if !v.is_empty() {
1884 let chars = v.chars();
1885 let length = chars.clone().count();
1886 let mut counter: usize = 0;
1887 chars.for_each(|c| {
1888 if c.is_ascii_digit() {
1889 counter += 1;
1890 }
1891 });
1892
1893 if counter == length {
1894 v.parse::<u32>().unwrap()
1895 } else {
1896 0
1897 }
1898 } else {
1899 0
1900 }
1901 })
1902 .collect();
1903
1904 id_list.sort();
1905 id_list.dedup();
1906 id_list.retain(|x| *x != 0);
1907 id_list
1908 }
1909
1910 /// Finds the list of columns for the given table name
1911 ///
1912 /// # Argument
1913 ///
1914 /// * `table_name` - The table's name to explore.
1915 ///
1916 /// # Return
1917 ///
1918 /// The list of column's names for this table.
1919 pub fn find_columns(table_name: &str) -> Vec<String> {
1920 let db = db_mitre!().unwrap();
1921 let sql_select = format!("SELECT * FROM {table_name}");
1922 let stmt = db.prepare(sql_select.as_str()).unwrap();
1923
1924 stmt.column_names()
1925 .iter()
1926 .map(|x| x.to_string())
1927 .collect::<Vec<String>>()
1928 }
1929
1930 /// Search fonction type. The args ar the criteria used for the searching function implementation.
1931 type Searcher = fn(args: &[String]);
1932
1933 /// Search the values into the list of columns.
1934 ///
1935 /// # Arguments
1936 ///
1937 /// * `args`- Arguments of the searching.
1938 /// * `columns`- The list of columns in which all arguments will be applied.
1939 pub fn searching(args: &[String], columns: &[String], searcher: Searcher) {
1940 let args_id = args.iter().position(|x| x == "search" || x == "s").unwrap();
1941 let criteria = &args[args_id + 1..];
1942 if criteria[0].contains(&"all".to_string()) {
1943 let args_left = criteria[0]["all".len()..].to_string();
1944 columns.iter().for_each(|x| {
1945 if *x != "id" {
1946 println!("{}", section!(format!("Searching for domain: {x}")));
1947 let args = format!("{} {}", x, &args_left);
1948 searcher(&[args]);
1949 }
1950 });
1951 } else {
1952 searcher(args);
1953 }
1954 }
1955
1956 /// Refresh the mitre database. the function takes a string that contains a list of csv files and the associated url.
1957 /// The method converts this string in an internal vector and create the query to download the CSV file from the
1958 /// associated url and inject these values into the local mitre database.
1959 ///
1960 /// # Arguments
1961 ///
1962 /// * `mitre_data` - MitreData description for the database
1963 /// * `injector` - the associated injector function that allows to store the downloaded data into the local database.
1964 ///
1965 pub fn refresh_mitre_database(mitre_data: MitreData, injector: InjectorFunction) {
1966 let csv_files = mitre_data.files;
1967 csv_files.iter().for_each(|&x| {
1968 refresh_data_mitre_into_database(mitre_data.url.to_string(), x.to_string(), injector);
1969 });
1970 }
1971
1972 /// Find the mitre record according to the mitre's identification.
1973 ///
1974 /// # Argument
1975 /// * `id` - Mitre identification. This id is a simple id value without the `T` of this id.
1976 ///
1977 /// # Return
1978 ///
1979 /// The Mitre record found or None.
1980 ///
1981 /// # Example
1982 ///
1983 /// ```rust
1984 /// use fenir::database::find_mitre_by_id;
1985 /// use fenir::mitre::MitreRecord;
1986 ///
1987 /// let result: Option<MitreRecord> = find_mitre_by_id("1531");
1988 ///
1989 /// if result.is_none() {
1990 /// eprintln!("Mitre record not found.");
1991 /// } else {
1992 /// println!("Mitre record found");
1993 /// }
1994 /// ```
1995 pub fn find_mitre_by_id(id: &str) -> Option<MitreRecord> {
1996 if id.is_empty() {
1997 return None;
1998 }
1999
2000 let conn = db_mitre!().unwrap();
2001 let query = format!("SELECT * FROM technique WHERE id = 'T{}'", id);
2002 let results = conn.prepare(query.as_str());
2003 if let Ok(mut results) = results {
2004 let value = results.query_row([], |row| {
2005 Ok(MitreRecord {
2006 id: row.get(0).unwrap(),
2007 name: row.get(1).unwrap(),
2008 description: row.get(2).unwrap(),
2009 url: row.get(3).unwrap(),
2010 created: row.get(4).unwrap(),
2011 modified: row.get(5).unwrap(),
2012 domain: row.get(6).unwrap(),
2013 version: row.get(7).unwrap(),
2014 tactics: row.get(8).unwrap(),
2015 platforms: row.get(9).unwrap(),
2016 sub_techniques: row.get(10).unwrap_or("Unknown".to_string()),
2017 contributors: row.get(11).unwrap_or("Unknown".to_string()),
2018 support_remote: row.get(12).unwrap_or_default(),
2019 impact_type: row.get(13).unwrap_or("Unknown".to_string()),
2020 citations: row.get(14).unwrap(),
2021 tactic_type: row.get(15).unwrap_or("Unknown".to_string()),
2022 mtc_id: row.get(16).unwrap_or("Unknown".to_string()),
2023 })
2024 });
2025 value.ok()
2026 } else {
2027 None
2028 }
2029 }
2030
2031 /// Return the `Value::Null` if the string argument is empty. Otherwise return the corresponding `Value`/
2032 ///
2033 /// # Argument
2034 ///
2035 /// * `final_result`- The string value to parse to `Value` type.
2036 ///
2037 /// # Return
2038 ///
2039 /// The corresponding `Value`.
2040 ///
2041 /// # Example
2042 ///
2043 /// ```rust
2044 /// use rusqlite::types::Value;
2045 /// use fenir::database::text_or_null;
2046 ///
2047 /// let result = text_or_null("".to_string());
2048 ///
2049 /// assert_eq!(Value::Null, result);
2050 ///
2051 /// let other = text_or_null("A string".to_string());
2052 ///
2053 /// assert_eq!(Value::Text("A string".to_string()), other);
2054 /// ```
2055 pub fn text_or_null(final_result: String) -> rusqlite::types::Value {
2056 if final_result.is_empty() {
2057 rusqlite::types::Value::Null
2058 } else {
2059 rusqlite::types::Value::Text(final_result)
2060 }
2061 }
2062}
2063
2064/// Tools for download support
2065pub mod download {
2066 use std::error::Error;
2067 use std::fs::File;
2068 use std::io::{Read, Write};
2069 use std::path::Path;
2070 use ureq::Body;
2071 use ureq::http::Response;
2072 use zip::ZipArchive;
2073
2074 /// Downloads a MITRE archive from the given query URL. If the source file is a ZIP file type,
2075 /// extracts the source file from the ZIP archive.
2076 ///
2077 /// This function performs the following steps:
2078 /// 1. Fetches the source file content from the `query` URL.
2079 /// 2. Saves the downloaded the file specified by `query.source_file`.
2080 /// 3. Extracts the required ZIP archive specified if the file is a ZIP type.
2081 ///
2082 /// # Arguments
2083 ///
2084 /// * `query` - An instance of `QueryMitre` struct containing the required URL,
2085 /// and source file.
2086 /// * `env_path`- The tyr home path.
2087 ///
2088 /// # Returns
2089 ///
2090 /// This function returns a `Result` type:
2091 /// * `Ok(String)` - If the process was successful. Contains the source file to used to.
2092 /// * `Err(Box<dyn std::error::Error>)` - If any error occurred during downloading, saving, or extracting.
2093 ///
2094 /// # Errors
2095 ///
2096 /// The function will return an error in cases such as:
2097 /// * Network issues while downloading the source file.
2098 /// * Problems during the unzipping file if it's a ZIP archive.
2099 ///
2100 /// # Example
2101 ///
2102 /// ```rust
2103 /// use fenir::download::{download_mitre_archive};
2104 /// use fenir::os::find_tyr_home_path;
2105 /// use std::path::Path;
2106 ///
2107 /// if let Err(e) = download_mitre_archive("https://example.com".to_string(), "data.csv.zip".to_string(), &find_tyr_home_path()) {
2108 /// eprintln!("Error: {}", e);
2109 /// }
2110 ///
2111 /// let tyr_home = find_tyr_home_path();
2112 /// std::fs::remove_file(tyr_home.join(Path::new("data.csv.zip")));
2113 /// ```
2114 pub fn download_mitre_archive(
2115 url: String,
2116 file: String,
2117 env_path: &Path,
2118 ) -> Result<String, Box<dyn Error>> {
2119 let full_url = format!("{}/{}", url, file);
2120 let response = fetch_url(full_url.as_str());
2121
2122 save_downloaded_archive(response, file.as_str(), env_path)?;
2123
2124 if file.ends_with(".zip") {
2125 let target_file = file.trim_end_matches(".zip");
2126 extract_zip_file(file.as_str(), target_file, env_path)?;
2127 Ok(target_file.to_string())
2128 } else {
2129 Ok(file)
2130 }
2131 }
2132
2133 /// Fetches a CSV file from a given URL.
2134 ///
2135 /// This function sends a GET request to the specified URL and attempts to fetch the CSV file.
2136 /// It checks if the request was successful by verifying the HTTP status code.
2137 ///
2138 /// # Arguments
2139 ///
2140 /// * `url` - A string slice that holds the URL of the CSV file to be fetched.
2141 ///
2142 /// # Returns
2143 ///
2144 /// This function returns a `Result` containing a `Response` object on success, or a `Box<dyn Error>` on failure.
2145 ///
2146 /// # Errors
2147 ///
2148 /// This function will return an error if:
2149 ///
2150 /// * The request fails (e.g., due to network issues).
2151 /// * The server responds with a non-200 HTTP status code.
2152 ///
2153 /// # Examples
2154 ///
2155 /// ```rust
2156 /// use fenir::download::fetch_url;
2157 ///
2158 /// let url = "https://www.google.com";
2159 /// let response = fetch_url(url);
2160 ///
2161 /// // Process the response...
2162 /// if response.is_err() {
2163 /// eprintln!("Url is not responding: {url}");
2164 /// } else {
2165 /// println!("Fetched response for: {url}");
2166 /// }
2167 /// ```
2168 ///
2169 /// # Dependencies
2170 ///
2171 /// This function relies on the `ureq` crate for HTTP requests.
2172 pub fn fetch_url(url: &str) -> Result<Response<Body>, Box<dyn Error>> {
2173 // Send GET request to the URL
2174 let response = ureq::get(url).call()?;
2175
2176 // Ensure the request was successful
2177 if response.status() != 200 {
2178 return Err(format!("Failed to download archive: HTTP {}", response.status()).into());
2179 }
2180
2181 Ok(response)
2182 }
2183
2184 /// Saves a downloaded archive to a specified file.
2185 ///
2186 /// # Arguments
2187 ///
2188 /// * `response` - A `Result` wrapping the HTTP `Response` object if the request was successful,
2189 /// or an error if the request failed.
2190 /// * `zip_file_name` - A string slice that holds the name of the file where the downloaded archive will be saved.
2191 /// * `env_path`- The tyr home path.
2192 ///
2193 /// # Returns
2194 ///
2195 /// This function returns a `Result`:
2196 ///
2197 /// * `Ok(())` if the file was successfully saved.
2198 /// * `Err(Box<dyn Error>)` if any error occurred during the process.
2199 /// * `env_path`- The tyr home path.
2200 ///
2201 /// # Errors
2202 ///
2203 /// This function will return an error if:
2204 ///
2205 /// * The file cannot be created.
2206 /// * Reading the response content fails.
2207 /// * Writing the content to the file fails.
2208 ///
2209 pub fn save_downloaded_archive(
2210 response: Result<Response<Body>, Box<dyn Error>>,
2211 zip_file_name: &str,
2212 env_path: &Path,
2213 ) -> Result<(), Box<dyn Error>> {
2214 let mut out = File::create(env_path.join(zip_file_name))?;
2215 let mut content = vec![];
2216 response?
2217 .into_body()
2218 .as_reader()
2219 .read_to_end(&mut content)?;
2220 out.write_all(&content)?;
2221 Ok(())
2222 }
2223
2224 /// Extracts a specified CSV file from a ZIP file and saves it to the filesystem.
2225 ///
2226 /// # Arguments
2227 ///
2228 /// * `from_zip_file_name` - A string slice that holds the name of the ZIP file to be read.
2229 /// * `to_csv_file_name` - A string slice that holds the name of the CSV file inside the ZIP to be extracted and saved.
2230 /// * `destination_path`- The path where to store the extracted zip.
2231 ///
2232 /// # Returns
2233 ///
2234 /// This function returns a `Result` type. On success, it returns an empty tuple `()`. On failure, it returns an error wrapped in a `Box<dyn Error>`.
2235 ///
2236 /// # Example
2237 ///
2238 /// ```
2239 /// use fenir::download::extract_zip_file;
2240 /// use fenir::os::find_tyr_home_path;
2241 ///
2242 /// let result = extract_zip_file("data.zip", "data.csv", &find_tyr_home_path());
2243 /// if let Err(e) = result {
2244 /// println!("Error extracting file: {}", e);
2245 /// }
2246 /// ```
2247 ///
2248 /// # Errors
2249 ///
2250 /// The function will return an error in the following cases:
2251 /// * If the ZIP file cannot be opened.
2252 /// * If the specified CSV file does not exist in the ZIP.
2253 /// * If the CSV file cannot be read or written to the filesystem.
2254 pub fn extract_zip_file(
2255 from_zip_file_name: &str,
2256 to_csv_file_name: &str,
2257 destination_path: &Path,
2258 ) -> Result<(), Box<dyn Error>> {
2259 // Extract the CSV file from the ZIP
2260 let file = File::open(destination_path.join(from_zip_file_name))?;
2261 let mut archive = ZipArchive::new(file)?;
2262 let mut csv_file = archive.by_name(to_csv_file_name)?;
2263 let mut csv_content = String::new();
2264 csv_file.read_to_string(&mut csv_content)?;
2265 // Save the CSV content to a file
2266 let mut extracted_csv = File::create(destination_path.join(to_csv_file_name))?;
2267 extracted_csv.write_all(csv_content.as_bytes())?;
2268 Ok(())
2269 }
2270}
2271
2272/// Module with helper function for NVD REST network query.
2273pub mod query {
2274 use crate::cpe::NVD_CPE_LINK;
2275 use crate::facilities::get_current_date;
2276 use crate::query::QueryType::{ByCpeSearchWords, ByCveNew, ByCveUpdated};
2277 use crate::{NVD_CVE_LINK, URL_SPACE};
2278
2279 /// `QueryType` is an enumeration representing the types of queries that can be made
2280 /// for Common Vulnerabilities and Exposures (CVE). It derives `Eq`, `PartialEq`, and `Clone`
2281 /// traits for equality comparisons and cloning capabilities.
2282 #[derive(Eq, PartialEq, Clone)]
2283 pub enum QueryType {
2284 /// Query by a specific CVE identifier.
2285 ByCve,
2286
2287 /// Query by a CVE for a CPE name (string description)
2288 ByCveCpeName,
2289
2290 /// Query for a known exploited CVE list
2291 ByCveExploited,
2292
2293 /// Query to retrieve new CVEs.
2294 ByCveNew,
2295
2296 /// Query by search strings
2297 ByCveSearchString,
2298
2299 /// Query by search strings with strict mode
2300 ByCveSearchStrict,
2301
2302 /// Query to retrieve updated CVEs.
2303 ByCveUpdated,
2304
2305 /// Query by a Common Platform Enumeration (CPE) identifier.
2306 ByCpe,
2307
2308 /// Query to perform CPE matching.
2309 ByCpeMatch,
2310
2311 /// Query to perform CPE searching by considering the word arguments as a single criteria.
2312 ByCpeSearchStrict,
2313
2314 /// Query to perform CPE searching according to a list of words.
2315 ByCpeSearchWords,
2316
2317 /// Query about vulnerable CPE
2318 ByCpeVulnerable,
2319
2320 /// Query by a Common Weakness Enumeration (CWE) identifier.
2321 ByCwe,
2322
2323 /// Cve http link test
2324 LinkTest,
2325 }
2326
2327 macro_rules! query_dates_for_new_cve {
2328 ($d1:expr) => {
2329 query_dates_for_new_cve!($d1, "")
2330 };
2331
2332 ($d1:expr, $d2:expr) => {
2333 query_dates!("pub", $d1, $d2)
2334 };
2335 }
2336
2337 macro_rules! query_dates_for_updated_cve {
2338 ($d1: expr) => {
2339 query_dates_for_updated_cve!($d1, "")
2340 };
2341
2342 ($d1: expr, $d2:expr) => {
2343 query_dates!("lastMod", $d1, $d2)
2344 };
2345 }
2346
2347 macro_rules! query_dates {
2348 ($prefix:expr, $d1:expr, $d2:expr) => {{
2349 let date1 = match $d1.to_string().is_empty() {
2350 true => get_current_date(),
2351 false => $d1.to_string(),
2352 };
2353
2354 let date2 = match $d2.to_string().is_empty() {
2355 true => get_current_date(),
2356 false => $d2.to_string(),
2357 };
2358
2359 format!(
2360 "{}StartDate={}T00:00:00.000&{}EndDate={}T00:00:00.000",
2361 $prefix, date1, $prefix, date2
2362 )
2363 }};
2364 }
2365
2366 /// Generates a query string for CVE (Common Vulnerabilities and Exposures) based on the provided query date type
2367 /// and date values.
2368 ///
2369 /// # Arguments
2370 ///
2371 /// * `query_date` - An instance of `CveQueryType` enum specifying the type of date query (new CVEs or updated CVEs).
2372 /// * `value` - A string containing date values separated by a semicolon (`;`). The values represent start and end dates
2373 /// respectively. If only one date is provided, it will be considered as both the start and end date.
2374 ///
2375 /// # Returns
2376 ///
2377 /// A `String` containing the constructed query with appropriate start and end dates.
2378 ///
2379 /// - For `CveNew`, it constructs `pubStartDate` and `pubEndDate` parameters.
2380 /// - For `CveUpdated`, it constructs `lastModStartDate` and `lastModEndDate` parameters.
2381 ///
2382 /// If a date is empty, the current date is used as a fallback.
2383 ///
2384 /// # Examples
2385 ///
2386 /// ```
2387 /// use fenir::query::{QueryType, create_query_dates};
2388 /// use fenir::facilities::get_current_date;
2389 ///
2390 /// let result = create_query_dates(QueryType::ByCveNew, "2023-01-01;2023-12-31");
2391 /// assert_eq!(result, "pubStartDate=2023-01-01T00:00:00.000&pubEndDate=2023-12-31T00:00:00.000");
2392 ///
2393 /// let result = create_query_dates(QueryType::ByCveUpdated, "2023-01-01;");
2394 /// let current_date = get_current_date();
2395 ///
2396 /// assert_eq!(result, format!("lastModStartDate=2023-01-01T00:00:00.000&lastModEndDate={}T00:00:00.000", current_date));
2397 /// ```
2398 pub fn create_query_dates(query_date: QueryType, value: &str) -> String {
2399 let dates: Vec<&str> = value.split(";").collect();
2400 match dates.len() {
2401 2 => match query_date {
2402 ByCveNew => {
2403 query_dates_for_new_cve!(dates[0], dates[1])
2404 }
2405
2406 ByCveUpdated => {
2407 query_dates_for_updated_cve!(dates[0], dates[1])
2408 }
2409 _ => String::new(),
2410 },
2411
2412 1 => match query_date {
2413 ByCveNew => {
2414 query_dates_for_new_cve!(dates[0])
2415 }
2416
2417 ByCveUpdated => {
2418 query_dates_for_updated_cve!(dates[0])
2419 }
2420
2421 _ => String::new(),
2422 },
2423 _ => String::new(),
2424 }
2425 }
2426
2427 /// Builds a query URL based on the type of query and a URL argument. Thanks to the `query_type`
2428 /// argument, the details for full URL constructing is taking in charge automatically.
2429 ///
2430 /// # Arguments
2431 ///
2432 /// * `query_type` - A variant of the `CveQueryType` enum specifying the type of CVE query.
2433 /// * `url_argument` - A string slice that holds the argument to be passed in the URL query.
2434 ///
2435 /// # Returns
2436 ///
2437 /// If successful, returns `Ok` containing the constructed query URL as a `String`.
2438 /// Otherwise, returns an `Err` with a static string slice indicating the error.
2439 ///
2440 /// # Examples
2441 /// ```
2442 /// use fenir::NVD_CVE_LINK;
2443 /// use fenir::query::{QueryType, build_query};
2444 ///
2445 /// let query_url = build_query(QueryType::ByCve, "CVE-2021-3456").unwrap();
2446 /// assert_eq!(query_url, format!("{}?cveId=CVE-2021-3456", NVD_CVE_LINK));
2447 /// ```
2448 pub fn build_query(query_type: QueryType, url_argument: &str) -> Result<String, &'static str> {
2449 match query_type {
2450 QueryType::ByCve => Ok(format!(
2451 "{NVD_CVE_LINK}?cveId={}",
2452 url_argument.to_uppercase()
2453 )),
2454
2455 QueryType::ByCveCpeName => Ok(format!(
2456 "{NVD_CVE_LINK}?virtualMatchString={}",
2457 url_argument
2458 )),
2459
2460 QueryType::ByCveExploited => Ok(format!("{NVD_CVE_LINK}?hasKev")),
2461
2462 ByCveNew | ByCveUpdated => Ok(format!(
2463 "{NVD_CVE_LINK}?{}",
2464 create_query_dates(query_type, url_argument)
2465 )),
2466
2467 QueryType::ByCwe => Ok(format!(
2468 "{NVD_CVE_LINK}?cweId={}",
2469 url_argument.to_uppercase()
2470 )),
2471
2472 QueryType::ByCpeVulnerable => {
2473 let query_str = build_query(QueryType::ByCveCpeName, url_argument)?;
2474 Ok(format!("{}&isVulnerable", query_str))
2475 }
2476
2477 QueryType::ByCveSearchString => {
2478 Ok(format!("{NVD_CVE_LINK}?keywordSearch={}", url_argument))
2479 }
2480
2481 QueryType::ByCveSearchStrict => {
2482 let query_str = build_query(QueryType::ByCveSearchString, url_argument)?;
2483 Ok(format!("{}&keywordExactMatch", query_str))
2484 }
2485
2486 QueryType::ByCpe => Ok(format!("{NVD_CPE_LINK}?cpeName={}", url_argument)),
2487
2488 QueryType::ByCpeMatch => Ok(format!(
2489 "{NVD_CPE_LINK}?cpeMatchString={}",
2490 url_argument.to_lowercase()
2491 )),
2492
2493 ByCpeSearchWords => Ok(format!(
2494 "{NVD_CPE_LINK}?keywordSearch={}",
2495 url_argument.replace(' ', URL_SPACE)
2496 )),
2497
2498 QueryType::ByCpeSearchStrict => {
2499 let first_part = build_query(ByCpeSearchWords, url_argument)?;
2500 Ok(format!("{first_part}&keywordExactMatch"))
2501 }
2502
2503 QueryType::LinkTest => Ok(url_argument.to_string()),
2504 }
2505 }
2506}
2507
2508/// Network module with functions used for HTTPS request and JSON exploration
2509pub mod network {
2510 use crate::NVD_KEY_NAME;
2511 use crate::database::MitreData;
2512 use crate::query::QueryType::LinkTest;
2513 use crate::query::build_query;
2514 use serde_json::Value;
2515 use std::env;
2516 use std::ffi::OsString;
2517 use termint::enums::Color;
2518 use termint::widgets::ToSpan;
2519 use ureq::Error;
2520
2521 /// Parses the given response from a `ureq` request.
2522 ///
2523 /// This function takes a `Result` containing either a `serde_json::Value` or a `ureq::Error`.
2524 /// If the result is an error, it returns an empty `Value`.
2525 /// If the result is a valid response, it returns a JSON `Value`.
2526 ///
2527 /// # Arguments
2528 ///
2529 /// * `response` - A `Result` containing either a `serde_json::Value` or `ureq::Error`.
2530 ///
2531 /// # Returns
2532 ///
2533 /// * A `serde_json::Value` parsed from the response, or an empty `Value::Null` if an error occurred.
2534 ///
2535 pub fn parse_response(response: Result<Value, Box<Error>>) -> Value {
2536 response.unwrap_or(Value::Null)
2537 }
2538
2539 /// Fetches a response from the given query URL. If the variable environment contains the
2540 /// `NVD_API_KEY`, this value will be used as NVD api key.
2541 ///
2542 /// This function uses the `ureq` crate to send an HTTP GET request to the specified URL.
2543 /// It returns the response if the request is successful, otherwise, it returns an error.
2544 /// The HTTP request is force to HTTPS only for secure reason.
2545 ///
2546 /// # Arguments
2547 ///
2548 /// * `query` - A string slice that holds the URL to which the GET request should be sent.
2549 ///
2550 /// # Returns
2551 ///
2552 /// This function returns a `Result` which is:
2553 /// - `Ok(serde_json::Value)` if the request is successful.
2554 /// - `Err(ureq::Error)` if there is an error during the request.
2555 ///
2556 /// # Examples
2557 ///
2558 /// ```
2559 /// use fenir::network::fetch_json_response;
2560 /// let url = "https://api.example.com/data";
2561 ///
2562 /// match fetch_json_response(url) {
2563 /// Ok(response) => println!("Success: {:?}", response),
2564 /// Err(error) => eprintln!("Error: {:?}", error),
2565 /// }
2566 /// ```
2567 ///
2568 /// # Errors
2569 ///
2570 /// This function will return an `Err` variant if the GET request fails for any reason,
2571 /// such as network issues or invalid URLs.
2572 ///
2573 pub fn fetch_json_response(query: &str) -> Result<Value, Box<Error>> {
2574 let apikey = check_nvd_api_key();
2575 if let Some(item) = apikey {
2576 let result: Value = ureq::get(query)
2577 .header("apiKey", item.to_str().unwrap())
2578 .call()?
2579 .body_mut()
2580 .with_config()
2581 .read_json()?;
2582 Ok(result)
2583 } else {
2584 let result = ureq::get(query)
2585 .call()?
2586 .body_mut()
2587 .with_config()
2588 .read_json()?;
2589
2590 Ok(result)
2591 }
2592 }
2593
2594 /// Check if the nvd api key is exiting
2595 ///
2596 /// # Returns
2597 ///
2598 /// - The result of checking. The presence of the NVD_API_KEY is optional
2599 pub fn check_nvd_api_key() -> Option<OsString> {
2600 env::var_os(NVD_KEY_NAME)
2601 }
2602
2603 /// Warning missing API key message
2604 pub fn check_nvd_api_key_warning() {
2605 eprintln!("{}", "WARNING: NVD api key not found. For a better performance, in your environment set NVD_API_KEY=<your_api_key_value>.".to_string().fg(Color::DarkYellow));
2606 }
2607
2608 /// Checks the validity of a connection by sending a GET request to the provided URL.
2609 ///
2610 /// # Arguments
2611 ///
2612 /// * `query` - A string slice that represents the URL to which the GET request is made.
2613 ///
2614 /// # Returns
2615 ///
2616 /// * `true` if the response for the GET request is successful (status code 200).
2617 /// * `false` if there is any error in making the request or the response indicates a failure.
2618 ///
2619 /// # Examples
2620 ///
2621 /// ```rust
2622 /// use fenir::NVD_CVE_LINK;
2623 /// use fenir::network::validate_connection;
2624 ///
2625 /// let is_valid = validate_connection(NVD_CVE_LINK);
2626 /// assert!(is_valid);
2627 /// ```
2628 pub fn validate_connection(query: &str) -> bool {
2629 // Simple URL validation
2630 if !query.starts_with("http://") && !query.starts_with("https://") {
2631 eprintln!("Invalid URL: {}", query);
2632 return false;
2633 }
2634
2635 let agent = ureq::Agent::new_with_defaults();
2636
2637 let query_test = build_query(LinkTest, query);
2638 let query_info_for_error = query_test.clone().unwrap();
2639
2640 match agent.get(query_test.unwrap().as_str()).call() {
2641 Ok(res) => {
2642 if res.status() == 200 {
2643 true
2644 } else {
2645 eprintln!(
2646 "Request failed with status: {} ({})",
2647 res.status(),
2648 query_info_for_error
2649 );
2650 false
2651 }
2652 }
2653 Err(Error::StatusCode(res)) => {
2654 eprintln!(
2655 "Request failed with response: {:?} ({})",
2656 res, query_info_for_error
2657 );
2658 false
2659 }
2660 Err(Error::Http(transport)) => {
2661 eprintln!(
2662 "Request http error: {} ({})",
2663 transport, query_info_for_error
2664 );
2665 false
2666 }
2667 _ => false,
2668 }
2669 }
2670
2671 /// Check if the Mitre data is valid for exploitation. This feature check if the URL and associated
2672 /// sources are available.
2673 ///
2674 /// # Argument
2675 /// * `data`- MitreData used for Mitre data checking
2676 ///
2677 /// If the connection is valid, it prints "valid".
2678 /// If the connection is invalid, it prints "invalid".
2679 ///
2680 /// # Example
2681 ///
2682 /// ```rust
2683 /// use fenir::network::check_mitre_data;
2684 /// use fenir::database::{MitreData, MitreDefinition};
2685 ///
2686 /// pub struct Link;
2687 ///
2688 /// impl MitreDefinition for Link {
2689 /// fn define() -> MitreData {
2690 /// MitreData {
2691 /// name: "Cpe",
2692 /// url: "https://services.nvd.nist.gov/rest/json/cpes/2.0",
2693 /// files: vec![],
2694 /// }
2695 /// }
2696 /// }
2697 ///
2698 /// check_mitre_data(Link::define());
2699 /// ```
2700 pub fn check_mitre_data(data: MitreData) {
2701 println!("Check mitre domain: {}", data.name);
2702 if data.files.is_empty() {
2703 print!("\tTest url: {}... ", data.url);
2704 if !validate_connection(data.url) {
2705 eprintln!("Failed");
2706 } else {
2707 println!("Valid");
2708 }
2709 }
2710
2711 data.files.iter().for_each(|&source| {
2712 let url_data = format!("{}/{}", data.url, source);
2713 print!("\tTest: {}... ", url_data);
2714 if !validate_connection(url_data.as_str()) {
2715 println!("Invalid");
2716 } else {
2717 println!("valid");
2718 }
2719 });
2720 }
2721}
2722
2723/// Module for CWE structure definition and its associated functions
2724pub mod cwe {
2725 use crate::database::{MitreData, MitreDefinition, find_cwe_by_id, parse_id_to_u32};
2726 use crate::facilities::{Cleaning, Uppercase};
2727 use crate::{header, section, section_level};
2728 use serde_json::Value;
2729 use std::fmt::{Display, Formatter};
2730 use termint::enums::Color;
2731 use termint::widgets::ToSpan;
2732
2733 /// Represents a CweRecord that's containing all content of an existent CWE.
2734 #[derive(Clone, Eq, PartialEq, Debug)]
2735 pub struct CweRecord {
2736 /// the CWE id (without CWE string prefix)
2737 pub id: u32,
2738
2739 /// the name of the CWE
2740 pub name: String,
2741
2742 /// The primary description of the CWE
2743 pub description: String,
2744
2745 /// The full description of the CWE
2746 pub extended: String,
2747
2748 /// The associated capec
2749 pub attacks: String,
2750
2751 /// The consequences of the CWE
2752 pub consequences: String,
2753
2754 /// The detections feature for this CWE
2755 pub detections: String,
2756
2757 /// The mitigations for this CWE
2758 pub mitigations: String,
2759 }
2760
2761 impl Display for CweRecord {
2762 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2763 write!(
2764 f,
2765 "{}\n{} {}",
2766 header!(format!("CWE-{}", self.id)),
2767 format!("{:>4} {}", "-", "Name:").fg(Color::DarkBlue),
2768 self.name
2769 )
2770 }
2771 }
2772
2773 impl MitreDefinition for CweRecord {
2774 fn define() -> MitreData {
2775 MitreData {
2776 name: "Cwe",
2777 url: "https://cwe.mitre.org/data/csv",
2778 files: vec!["699.csv.zip", "1000.csv.zip", "1194.csv.zip"],
2779 }
2780 }
2781 }
2782
2783 /// Shows the weaknesses whose the ids are corresponding to the `raw_weaknesses` string.
2784 ///
2785 /// # Argument
2786 ///
2787 /// * `section` - Section to show as section level/
2788 /// * `raw_weaknesses`- The string of weaknesses id. This string is the format: `::<id1>::<id2>::...::<idN>::`.
2789 ///
2790 pub fn show_weaknesses(section: &str, raw_weaknesses: String) {
2791 let cwe_list = parse_id_to_u32(raw_weaknesses);
2792
2793 section_level!(2, section);
2794 for cwe in cwe_list {
2795 let result = find_cwe_by_id(cwe);
2796 if let Some(record) = result {
2797 println!("{:>6} CWE-{} - {}", "-", cwe, record.name);
2798 } else {
2799 println!("{:>6} CWE-{} - ???", "-", cwe);
2800 }
2801 }
2802 }
2803
2804 /// Flat the cwe values to a string value
2805 ///
2806 /// # Arguments
2807 ///
2808 /// * `values` - List of cwe values
2809 ///
2810 /// # Returns
2811 ///
2812 /// A string with all cwe id separate by a `,`.
2813 pub fn join_cwe_list(values: &[Value]) -> String {
2814 let mut result = Vec::new();
2815 values
2816 .iter()
2817 .for_each(|v| result.push(String::cleaning(v["value"].to_string())));
2818
2819 result.join(", ")
2820 }
2821}
2822
2823/// Module for CPE structure definition and its associated functions
2824pub mod cpe {
2825 use crate::database::{MitreData, MitreDefinition};
2826 use crate::facilities::Uppercase;
2827 use crate::package::errors::write_error_message_and_exit;
2828 use crate::{NOT_ENOUGH_ELEMENTS_ERROR, stamp};
2829 use std::fmt::{Display, Formatter};
2830
2831 /// NVD link Rest API for CPE request
2832 pub const NVD_CPE_LINK: &str = "https://services.nvd.nist.gov/rest/json/cpes/2.0";
2833
2834 /// NVD link Rest API for CPE/CVE matching
2835 pub const NVD_CPE_MATCH_LINK: &str = "https://services.nvd.nist.gov/rest/json/cpematch/2.0";
2836
2837 /// Represents a Common Platform Enumeration (CPE) identifier.
2838 ///
2839 /// CPE is a standardized method used to describe and identify classes of applications,
2840 /// operating systems, and hardware devices present within an enterprise's computing assets.
2841 ///
2842 /// This struct includes various fields that together form a complete CPE identifier.
2843 ///
2844 /// # Note
2845 ///
2846 /// All fields that are not explicitly specified, are marked with the `*` character.
2847 ///
2848 #[derive(Eq, PartialEq, Clone, Debug, Default)]
2849 pub struct Cpe {
2850 /// Type CPE type
2851 pub cpe_type: CpeType,
2852
2853 /// The product's vendor.
2854 pub vendor: String,
2855
2856 /// The product's name.
2857 pub product: String,
2858
2859 /// The product's version.
2860 pub version: String,
2861
2862 /// The update indication related to this version.
2863 pub update: String,
2864
2865 /// The product's edition.
2866 pub edition: String,
2867
2868 /// The product's language.
2869 pub language: String,
2870
2871 /// The software edition.
2872 pub sw_edition: String,
2873
2874 /// The target software.
2875 pub target_sw: String,
2876
2877 /// The target hardware.
2878 pub target_hw: String,
2879
2880 /// The other field for free indication.
2881 pub other: String,
2882 }
2883
2884 impl Cpe {
2885 /// Creates a new instance of the `cpe` struct with the provided `CpeType`.
2886 ///
2887 /// Fields such as `vendor`, `product`, `version`, `update`, and `edition` are initialized with default values (`"*"`).
2888 ///
2889 /// # Arguments
2890 ///
2891 /// * `cpe_type` - The type of the Common Platform Enumeration (CPE). This can be either `CpeType::Application` or `CpeType::OperatingSystem`.
2892 ///
2893 /// # Returns
2894 ///
2895 /// A new `cpe` instance with the specified CPE type and default field values.
2896 pub fn new(cpe_type: CpeType) -> Self {
2897 Cpe {
2898 cpe_type,
2899 vendor: "*".to_string(),
2900 product: "*".to_string(),
2901 version: "*".to_string(),
2902 update: "*".to_string(),
2903 edition: "*".to_string(),
2904 language: "*".to_string(),
2905 sw_edition: "*".to_string(),
2906 target_hw: "*".to_string(),
2907 target_sw: "*".to_string(),
2908 other: "*".to_string(),
2909 }
2910 }
2911
2912 /// Returns a string representation of the instance. This is a convenience method for
2913 /// formats the instance into a string using the `Display` trait implementation
2914 /// of the type of `self`.
2915 ///
2916 /// # Returns
2917 ///
2918 /// A `String` containing the formatted representation of the instance.
2919 ///
2920 /// # Notes:
2921 ///
2922 /// - the output format is : cpe:2.3:<part>:<vendor>:<product>:<version>:<update>:<edition>
2923 /// - the output format is limited with elements only used with these tools:
2924 /// - <part>: support only o (for operating system, a for application),
2925 /// - the <sw_edition>:<target_sw>:<target_hw>:<other> of the CPE norm is not supported.
2926 ///
2927 pub fn name(&self) -> String {
2928 format!("{}", self)
2929 }
2930
2931 /// Sets the vendor field for this `cpe` instance.
2932 ///
2933 /// This method updates the vendor field with the provided name, converting it to lowercase.
2934 ///
2935 /// # Arguments
2936 ///
2937 /// * `name` - A string slice that holds the name of the vendor.
2938 ///
2939 /// # Example
2940 ///
2941 /// ```
2942 /// use fenir::cpe::{Cpe, CpeType};
2943 /// let mut cpe = Cpe::new(CpeType::Application);
2944 ///
2945 /// cpe.vendor("NewVendor");
2946 /// assert_eq!(cpe.vendor, "newvendor");
2947 /// ```
2948 pub fn vendor(&mut self, name: &str) {
2949 self.vendor = String::from(name).to_lowercase();
2950 }
2951
2952 /// Sets the product name for the CPE identifier.
2953 ///
2954 /// This method takes a string slice representing the product name,
2955 /// converts it to lowercase, and assigns it to the `product` field of the `cpe` struct.
2956 ///
2957 /// # Parameters
2958 ///
2959 /// * `name` - A string slice that holds the name of the product. This name will be converted to lowercase.
2960 ///
2961 /// # Example
2962 ///
2963 /// ```
2964 /// use fenir::cpe::{Cpe, CpeType};
2965 /// let mut cpe = Cpe::new(CpeType::Application);
2966 ///
2967 /// cpe.product("CoolProduct");
2968 /// assert_eq!(cpe.product, "coolproduct");
2969 /// ```
2970 pub fn product(&mut self, name: &str) {
2971 self.product = String::from(name).to_lowercase();
2972 }
2973
2974 /// Sets the version for the `cpe` instance.
2975 ///
2976 /// This method updates the `version` field of the `cpe` struct, converting the provided
2977 /// version string to lowercase to ensure consistency.
2978 ///
2979 /// # Parameters
2980 ///
2981 /// * `value` - A reference to a string slice representing the new version to be set.
2982 ///
2983 /// # Example
2984 ///
2985 /// ```
2986 /// use fenir::cpe::{Cpe, CpeType};
2987 /// let mut cpe = Cpe::new(CpeType::Application);
2988 ///
2989 /// cpe.version("2.0.0");
2990 /// assert_eq!(cpe.version, "2.0.0");
2991 /// ```
2992 ///
2993 /// # Note
2994 ///
2995 /// The input version string will be converted to lowercase.
2996 ///
2997 /// # Errors
2998 ///
2999 /// This method does not return an error but mutates the `cpe` instance in place.
3000 pub fn version(&mut self, value: &str) {
3001 self.version = String::from(value).to_lowercase();
3002 }
3003
3004 /// Updates the `update` field of the `cpe` struct with the given value, converting it to lowercase.
3005 ///
3006 /// # Arguments
3007 ///
3008 /// * `value` - A string slice representing the new update/version details of the product.
3009 ///
3010 /// # Examples
3011 ///
3012 /// ```
3013 /// use fenir::cpe::{Cpe, CpeType};
3014 /// let mut cpe = Cpe::new(CpeType::Application);
3015 ///
3016 /// cpe.update("Update2");
3017 /// assert_eq!(cpe.update, "update2");
3018 /// ```
3019 pub fn update(&mut self, value: &str) {
3020 self.update = String::from(value).to_lowercase();
3021 }
3022
3023 /// Sets the edition field of the `cpe` struct.
3024 ///
3025 /// This method takes a string slice, converts it to a `String`, transforms it to lowercase,
3026 /// and assigns it to the inner field.
3027 ///
3028 /// # Arguments
3029 ///
3030 /// * `value` - A string slice that holds the new value for this field.
3031 ///
3032 /// # Example
3033 ///
3034 /// ```rust
3035 /// use fenir::cpe::{Cpe, CpeType};
3036 /// let mut cpe = Cpe::new(CpeType::Application);
3037 ///
3038 /// cpe.edition("Premium");
3039 /// assert_eq!(cpe.edition, "premium");
3040 /// ```
3041 pub fn edition(&mut self, value: &str) {
3042 self.edition = String::from(value).to_lowercase();
3043 }
3044
3045 /// Sets the language of the `cpe` struct.
3046 ///
3047 /// This method takes a string slice, converts it to a `String`, transforms it to lowercase,
3048 /// and assigns it to the inner field.
3049 ///
3050 /// # Arguments
3051 ///
3052 /// * `value` - A string slice that holds the new value for this field.
3053 ///
3054 /// # Example
3055 ///
3056 /// ```rust
3057 /// use fenir::cpe::{Cpe, CpeType};
3058 /// let mut cpe = Cpe::new(CpeType::Application);
3059 ///
3060 /// cpe.language("EN");
3061 /// assert_eq!(cpe.language, "en");
3062 /// ```
3063 pub fn language(&mut self, value: &str) {
3064 self.language = String::from(value).to_lowercase();
3065 }
3066
3067 /// Sets the software edition of the `cpe` struct.
3068 ///
3069 /// This method takes a string slice, converts it to a `String`, transforms it to lowercase,
3070 /// and assigns it to the inner field.
3071 ///
3072 /// # Arguments
3073 ///
3074 /// * `value` - A string slice that holds the new value for this field.
3075 ///
3076 /// # Example
3077 ///
3078 /// ```rust
3079 /// use fenir::cpe::{Cpe, CpeType};
3080 /// let mut cpe = Cpe::new(CpeType::Application);
3081 ///
3082 /// cpe.sw_edition("SoftEdition");
3083 /// assert_eq!(cpe.sw_edition, "softedition");
3084 /// ```
3085 pub fn sw_edition(&mut self, value: &str) {
3086 self.sw_edition = String::from(value).to_lowercase();
3087 }
3088
3089 /// Sets the target software of the `cpe` struct.
3090 ///
3091 /// This method takes a string slice, converts it to a `String`, transforms it to lowercase,
3092 /// and assigns it to the inner field.
3093 ///
3094 /// # Arguments
3095 ///
3096 /// * `value` - A string slice that holds the new value for this field.
3097 ///
3098 /// # Example
3099 ///
3100 /// ```rust
3101 /// use fenir::cpe::{Cpe, CpeType};
3102 /// let mut cpe = Cpe::new(CpeType::Application);
3103 ///
3104 /// cpe.target_sw("TargetSoft");
3105 /// assert_eq!(cpe.target_sw, "targetsoft");
3106 /// ```
3107 pub fn target_sw(&mut self, value: &str) {
3108 self.target_sw = String::from(value).to_lowercase();
3109 }
3110
3111 /// Sets the target hardware of the `cpe` struct.
3112 ///
3113 /// This method takes a string slice, converts it to a `String`, transforms it to lowercase,
3114 /// and assigns it to the inner field.
3115 ///
3116 /// # Arguments
3117 ///
3118 /// * `value` - A string slice that holds the new value for this field.
3119 ///
3120 /// # Example
3121 ///
3122 /// ```rust
3123 /// use fenir::cpe::{Cpe, CpeType};
3124 /// let mut cpe = Cpe::new(CpeType::Application);
3125 ///
3126 /// cpe.target_hw("Raspberry");
3127 /// assert_eq!(cpe.target_hw, "raspberry");
3128 /// ```
3129 pub fn target_hw(&mut self, value: &str) {
3130 self.target_hw = String::from(value).to_lowercase();
3131 }
3132
3133 /// Sets the other value of the `cpe` struct.
3134 ///
3135 /// This method takes a string slice, converts it to a `String`, transforms it to lowercase,
3136 /// and assigns it to the inner field.
3137 ///
3138 /// # Arguments
3139 ///
3140 /// * `value` - A string slice that holds the new value for this field.
3141 ///
3142 /// # Example
3143 ///
3144 /// ```rust
3145 /// use fenir::cpe::{Cpe, CpeType};
3146 /// let mut cpe = Cpe::new(CpeType::Application);
3147 ///
3148 /// cpe.other("Other");
3149 /// assert_eq!(cpe.other, "other");
3150 /// ```
3151 pub fn other(&mut self, value: &str) {
3152 self.other = String::from(value).to_lowercase();
3153 }
3154
3155 /// Parses a CPE (Common Platform Enumeration) from a given string. Support only cpe version 2.3.
3156 ///
3157 /// # Arguments
3158 /// * `cpe_string` - A string in the CPE format to be parsed
3159 ///
3160 /// # Returns
3161 /// * `cpe` - A struct containing the parsed components of the CPE string
3162 ///
3163 /// # Notes:
3164 ///
3165 /// - the output format is : cpe:2.3:<part>:<vendor>:<product>:<version>:<update>:<edition>
3166 /// - the output format is limited with elements only used with these tools:
3167 /// - <part>: support only o (for operating system, a for application),
3168 /// - the <sw_edition>:<target_sw>:<target_hw>:<other> of the CPE norm is not supported.
3169 ///
3170 pub fn from(cpe_string: &str) -> Cpe {
3171 if !cpe_string.contains(CPE_HEADER) {
3172 write_error_message_and_exit("CPE string header not found: cpe:2.3", None);
3173 }
3174
3175 let elements = extract_elements(cpe_string);
3176 let mut cpe = match elements[2].as_str() {
3177 "o" => Cpe::new(CpeType::OperatingSystem),
3178 "a" => Cpe::new(CpeType::Application),
3179 "h" => Cpe::new(CpeType::Hardware),
3180 _ => Cpe::new(CpeType::All),
3181 };
3182
3183 cpe.vendor(&elements[3]);
3184 cpe.product(&elements[4]);
3185 cpe.version(&elements[5]);
3186 cpe.update(&elements[6]);
3187 cpe.edition(&elements[7]);
3188 cpe.language(&elements[8]);
3189 cpe.sw_edition(&elements[9]);
3190 cpe.target_sw(&elements[10]);
3191 cpe.target_hw(&elements[11]);
3192 cpe.other(&elements[12]);
3193 cpe
3194 }
3195
3196 /// Describes the CPE (Common Platform Enumeration) attributes.
3197 ///
3198 /// This method prints out a human-readable description of the CPE,
3199 /// detailing its type, vendor, product, version, edition, and update.
3200 ///
3201 /// The type is either "soft" for applications or "os" for operating systems,
3202 /// inferred from the `cpe_type` field.
3203 ///
3204 /// # Examples
3205 ///
3206 /// ```rust
3207 /// use fenir::cpe::{Cpe, CpeType};
3208 ///
3209 /// let cpe = Cpe::from("cpe:2.3:a:lostvip:ruoyi-go:*:*:*:*:*:*:*:*");
3210 ///
3211 /// cpe.explains();
3212 /// ```
3213 /// Will produce the output:
3214 /// ```shell
3215 /// - Id: cpe:2.3:a:lostvip:ruoyi-go:*:*:*:*:*:*:*:*
3216 /// - Type: soft
3217 /// - Vendor: lostvip
3218 /// - Product: ruoyi-go
3219 /// - Version: *
3220 /// - Edition: *
3221 /// - Update: *
3222 /// - Language: *
3223 /// - Sw_edition: *
3224 /// - Target sw : *
3225 /// - Target hw: *
3226 /// - Other: *
3227 /// ```
3228 pub fn explains(&self) {
3229 stamp!("id", &self.name());
3230 stamp!("type", self.cpe_type());
3231 stamp!("vendor", &self.vendor);
3232 stamp!("product", &self.product);
3233 stamp!("version", &self.version);
3234 stamp!("edition", &self.edition);
3235 stamp!("update", &self.update);
3236 stamp!("language", &self.language);
3237 stamp!("sw edition", &self.sw_edition);
3238 stamp!("target sw", &self.target_sw);
3239 stamp!("target hw", &self.target_hw);
3240 stamp!("other", &self.other);
3241 }
3242
3243 /// Returns the CPE type description
3244 ///
3245 /// # Returns
3246 ///
3247 /// * "soft" - if type is application (a)
3248 /// * "hardware" - if type is hardware (h)
3249 /// * "os" - if type is operating system (o)
3250 /// * "all" - if type is all (*)
3251 ///
3252 /// # Example
3253 ///
3254 /// ```rust
3255 /// use fenir::cpe::{Cpe, CpeType};
3256 ///
3257 /// let cpe = Cpe::new(CpeType::OperatingSystem);
3258 /// assert_eq!("os", cpe.cpe_type());
3259 /// ```
3260 pub fn cpe_type(&self) -> &str {
3261 match self.cpe_type {
3262 CpeType::Application => "soft",
3263 CpeType::OperatingSystem => "os",
3264 CpeType::Hardware => "hard",
3265 CpeType::All => "all",
3266 }
3267 }
3268 }
3269
3270 /// CpeLink used for CPE request.
3271 pub struct CpeLink;
3272 impl MitreDefinition for CpeLink {
3273 fn define() -> MitreData {
3274 MitreData {
3275 name: "Cpe",
3276 url: "https://services.nvd.nist.gov/rest/json/cpes/2.0",
3277 files: vec![],
3278 }
3279 }
3280 }
3281
3282 /// CpeMatchLink used for cpe link complement
3283 pub struct CpeMatchLink;
3284 impl MitreDefinition for CpeMatchLink {
3285 fn define() -> MitreData {
3286 MitreData {
3287 name: "Cpe match",
3288 url: "https://services.nvd.nist.gov/rest/json/cpematch/2.0",
3289 files: vec![],
3290 }
3291 }
3292 }
3293
3294 /// Operating system type
3295 pub const CPE_OS_LABEL: &str = "os";
3296 // Hardware type
3297 pub const CPE_HARDWARE_LABEL: &str = "h";
3298 /// Application/software type
3299 pub const CPE_SOFT_LABEL: &str = "soft";
3300 /// All type
3301 pub const CPE_ALL_LABEL: &str = "*";
3302
3303 /// Creates a `cpe` instance based on the provided application type.
3304 ///
3305 /// This function checks if the `apps_type` matches the constant value `OS_TYPE`.
3306 /// If it does, it creates a `cpe` instance of type `OperatingSystem`, otherwise it creates a
3307 /// `cpe` instance of type `Application`.
3308 ///
3309 /// # Arguments
3310 ///
3311 /// * `apps_type` - A string slice representing the type of the application. It can be either
3312 /// `"os"` for an operating system or any other value for an application.
3313 ///
3314 /// # Returns
3315 ///
3316 /// A `cpe` instance with the appropriate `CpeType`.
3317 ///
3318 /// # Examples
3319 ///
3320 /// ```
3321 /// use fenir::cpe::{create_cpe_from_type, Cpe, CpeType};
3322 ///
3323 /// let cpe_os = create_cpe_from_type("os");
3324 /// assert_eq!(cpe_os.cpe_type, CpeType::OperatingSystem);
3325 ///
3326 /// let cpe_app = create_cpe_from_type("soft");
3327 /// assert_eq!(cpe_app.cpe_type, CpeType::Application);
3328 /// ```
3329 pub fn create_cpe_from_type(apps_type: &str) -> Cpe {
3330 match apps_type {
3331 CPE_OS_LABEL => Cpe::new(CpeType::OperatingSystem),
3332 CPE_SOFT_LABEL => Cpe::new(CpeType::Application),
3333 CPE_HARDWARE_LABEL => Cpe::new(CpeType::Hardware),
3334 _ => Cpe::new(CpeType::All),
3335 }
3336 }
3337
3338 /// Returns a string representation of the instance.
3339 ///
3340 /// # Returns
3341 ///
3342 /// A `String` containing the formatted representation of the instance.
3343 ///
3344 /// # Notes:
3345 ///
3346 /// - the output format is : cpe:2.3:<part>:<vendor>:<product>:<version>:<update>:<edition>
3347 /// - the output format is limited with elements only used with these tools:
3348 /// - <part>: support only o (for operating system, a for application),
3349 /// - the <sw_edition>:<target_sw>:<target_hw>:<other> of the CPE norm is not supported.
3350 ///
3351 impl Display for Cpe {
3352 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
3353 let soft_type = match self.cpe_type {
3354 CpeType::Application => "a",
3355 CpeType::OperatingSystem => "o",
3356 CpeType::Hardware => "h",
3357 CpeType::All => "*",
3358 };
3359
3360 write!(
3361 f,
3362 "{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}",
3363 CPE_HEADER,
3364 soft_type,
3365 self.vendor,
3366 self.product,
3367 self.version,
3368 self.update,
3369 self.edition,
3370 self.language,
3371 self.sw_edition,
3372 self.target_sw,
3373 self.target_hw,
3374 self.other
3375 )
3376 }
3377 }
3378
3379 /// Represents different types of Common Platform Enumeration (CPE) identifiers.
3380 ///
3381 /// CPE is a standardized method of describing and identifying classes of
3382 /// applications, operating systems, and hardware devices present among an enterprise's computing assets.
3383 ///
3384 /// # Variants
3385 ///
3386 /// - `All`: Represents all type of products.
3387 /// - `Application`: Represents a software application.
3388 /// - `OperatingSystem`: Represents an operating system.
3389 ///
3390 #[derive(Eq, PartialEq, Clone, Debug, Default)]
3391 pub enum CpeType {
3392 #[default]
3393 All,
3394 Application,
3395 OperatingSystem,
3396 Hardware,
3397 }
3398
3399 impl Display for CpeType {
3400 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
3401 let value = match self {
3402 CpeType::All => "*",
3403 CpeType::Application => "application",
3404 CpeType::OperatingSystem => "Os",
3405 CpeType::Hardware => "hardware",
3406 };
3407
3408 f.write_str(value)
3409 }
3410 }
3411
3412 /// CPE request header that's identifying the release used (cpe v 2.3)
3413 const CPE_HEADER: &str = "cpe:2.3";
3414
3415 /// Extracts elements from a Common Platform Enumeration (CPE) string by splitting it on colons (`:`)
3416 /// and converting each element to lowercase.
3417 ///
3418 /// If the number of elements in the CPE string is less than or equal to 8, this function will write an
3419 /// error message and exit the process with a status code of 1.
3420 ///
3421 /// # Arguments
3422 ///
3423 /// * `cpe_string` - A string slice representing the CPE string to be parsed.
3424 ///
3425 /// # Returns
3426 ///
3427 /// A vector of strings, each representing an element of the CPE string.
3428 ///
3429 /// # Panics
3430 ///
3431 /// This function will exit the process if the number of elements is less than or equal to 8.
3432 ///
3433 /// # Examples
3434 ///
3435 /// ```
3436 /// use fenir::cpe::extract_elements;
3437 ///
3438 /// let cpe = "cpe:2.3:o:vendor:product:version:update:edition:language:*:*:*";
3439 /// let elements = extract_elements(cpe);
3440 /// assert!(elements.len() > 8);
3441 /// ```
3442 pub fn extract_elements(cpe_string: &str) -> Vec<String> {
3443 let elements: Vec<String> = cpe_string.split(':').map(|s| s.to_lowercase()).collect();
3444 if elements.len() <= 8 {
3445 write_error_message_and_exit(NOT_ENOUGH_ELEMENTS_ERROR, None);
3446 }
3447 elements
3448 }
3449}
3450
3451/// Shows the version given as argument
3452///
3453/// # Arguments
3454///
3455/// * `version` - A string that's describing the version.
3456///
3457pub fn show_version(version: &str) {
3458 println!("Version: {}", version);
3459 println!("(fenir lib version: {})", env!("CARGO_PKG_VERSION"));
3460}
3461
3462/// Module for CVE structure definition and its associated functions
3463pub mod cve {
3464 use crate::cwe::join_cwe_list;
3465 use crate::database::{MitreData, MitreDefinition};
3466 use crate::facilities::Cleaning;
3467 use serde_json::Value;
3468 use std::fmt::{Display, Formatter};
3469
3470 /// The CVE score value. The score value is corresponding to the standard CVE NVD score.
3471 #[derive(Default, PartialEq, Clone, PartialOrd, Ord, Eq)]
3472 pub struct Score {
3473 score: String,
3474 }
3475
3476 impl Score {
3477 /// Create a new score instance.
3478 ///
3479 /// # Argument:
3480 ///
3481 /// * `value` - string score value. This value must be a numeric forms that is conforming to the NVD standard.
3482 pub fn new(value: String) -> Score {
3483 Self { score: value }
3484 }
3485
3486 /// Float value of the CVE score.
3487 ///
3488 ///
3489 /// # Return
3490 ///
3491 /// Score float value.
3492 pub fn value(&self) -> f64 {
3493 self.score.parse::<f64>().unwrap()
3494 }
3495
3496 /// Score's label. This label shown is depending on the score value as the NVD standard definition.
3497 ///
3498 /// # Return
3499 ///
3500 /// The score label.
3501 ///
3502 /// # Example
3503 ///
3504 /// ```rust
3505 /// use fenir::cve::Score;
3506 ///
3507 /// let score = Score::new("5.2".to_string());
3508 ///
3509 /// assert_eq!("MEDIUM", score.label());
3510 /// ```
3511 pub fn label(&self) -> &str {
3512 match self.value() {
3513 0.0 => "None",
3514 value if value > 0.0 && value < 4.0 => "LOW",
3515 value if (4.0..7.0).contains(&value) => "MEDIUM",
3516 value if (7.0..9.0).contains(&value) => "HIGH",
3517 _ => "CRITICAL",
3518 }
3519 }
3520 }
3521
3522 /// Represents a CVE with a reference.
3523 #[derive(PartialEq, PartialOrd, Ord, Eq, Default)]
3524 pub struct Cve {
3525 /// The CVE reference like: CVE-2024-12021
3526 pub reference: String,
3527
3528 /// Optional description. Some new CVE can have no description.
3529 pub description: Option<String>,
3530
3531 /// Optional score CVSS3. Some new CVE can have no score.
3532 pub score_v3: Option<Score>,
3533
3534 /// Optional score CVSS4. Some new CVE can have no score.
3535 pub score_v4: Option<Score>,
3536
3537 /// Optional CPE identification links with this CVE. Some new CVE can have no cpe id.
3538 pub cpe_id: Option<String>,
3539
3540 /// Optional weaknesses links with this CVE. Some new CVE can have no weaknesses (cwe).
3541 pub weaknesses: Option<String>,
3542 }
3543
3544 impl Cve {
3545 /// Create a new CVE.
3546 ///
3547 /// # Argument
3548 ///
3549 /// * `reference`- The CVE reference
3550 ///
3551 /// # Return
3552 ///
3553 /// A new instance.
3554 ///
3555 /// # Example
3556 ///
3557 /// ```rust
3558 /// use fenir::cve::Cve;
3559 /// let cve = Cve::new("CVE-2025-12040".to_string());
3560 ///
3561 /// assert_eq!("CVE-2025-12040", cve.reference);
3562 /// ```
3563 pub fn new(reference: String) -> Self {
3564 Self {
3565 reference,
3566 ..Default::default()
3567 }
3568 }
3569 }
3570
3571 impl Display for Cve {
3572 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
3573 write!(formatter, "{}", String::cleaning(self.reference.clone()))
3574 }
3575 }
3576
3577 /// NVD url link Rest API for CVE queries.
3578 impl MitreDefinition for Cve {
3579 fn define() -> MitreData {
3580 MitreData {
3581 name: "Cve",
3582 url: "https://services.nvd.nist.gov/rest/json/cves/2.0",
3583 files: vec![],
3584 }
3585 }
3586 }
3587
3588 /// Create a CVE from the raw value
3589 ///
3590 /// # Arguments
3591 ///
3592 /// * `value`- Raw value
3593 ///
3594 /// # Returns
3595 ///
3596 /// The CVE
3597 pub fn create_cve_from(value: &Value) -> Cve {
3598 let mut cve = Cve::new(String::cleaning(value["cve"]["id"].to_string()));
3599 cve.description = Some(String::cleaning(
3600 value["cve"]["descriptions"][0]["value"].to_string(),
3601 ));
3602
3603 let score_value =
3604 value["cve"]["metrics"]["cvssMetricV31"][0]["cvssData"]["baseScore"].to_string();
3605 cve.score_v3 = match score_value.as_str() {
3606 "null" | "None" => Some(Score::new("0.0".to_string())),
3607 _ => Some(Score::new(score_value)),
3608 };
3609
3610 let score_value =
3611 value["cve"]["metrics"]["cvssMetricV40"][0]["cvssData"]["baseScore"].to_string();
3612 cve.score_v4 = match score_value.as_str() {
3613 "null" | "None" => Some(Score::new("0.0".to_string())),
3614 _ => Some(Score::new(score_value)),
3615 };
3616
3617 let cwe = value["cve"]["weaknesses"][0]["description"].as_array();
3618 cve.weaknesses = match cwe {
3619 Some(_) => Some(String::cleaning(join_cwe_list(cwe.unwrap()))),
3620 None => Some(String::from("None")),
3621 };
3622
3623 let optional_cpe = value["cve"]["configurations"][0]["nodes"][0]["cpeMatch"].as_array();
3624 if let Some(cpe_id) = optional_cpe {
3625 cve.cpe_id = Some(String::cleaning(
3626 cpe_id.first().unwrap()["criteria"].to_string(),
3627 ));
3628 } else {
3629 cve.cpe_id = Some(String::from("None"));
3630 }
3631
3632 cve
3633 }
3634
3635 /// Extracts a list of CVE (Common Vulnerabilities and Exposures) references
3636 /// from a given JSON value that contains CWE (Common Weakness Enumeration) data.
3637 ///
3638 /// # Arguments
3639 ///
3640 /// * `cwe_list` - A JSON value that contains a list of vulnerabilities under the key `"vulnerabilities"`.
3641 ///
3642 /// # Returns
3643 ///
3644 /// This function returns a vector of `Cve` structs, each containing the reference (`id`) of a CVE.
3645 ///
3646 /// # Example
3647 ///
3648 /// ```rust
3649 /// use serde_json::json;
3650 /// use fenir::cve::cve_from_cwe;
3651 ///
3652 /// let cwe_json = json!({
3653 /// "vulnerabilities": [
3654 /// { "cve": { "id": "CVE-2021-1234" }},
3655 /// { "cve": { "id": "CVE-2021-5678" }}
3656 /// ]
3657 /// });
3658 ///
3659 /// let cves = cve_from_cwe(&cwe_json);
3660 /// assert_eq!(cves.len(), 2);
3661 /// assert_eq!(cves[0].reference, "CVE-2021-1234");
3662 /// assert_eq!(cves[1].reference, "CVE-2021-5678");
3663 /// ```
3664 ///
3665 /// # Note
3666 ///
3667 /// It is assumed that the `Cve` struct is defined elsewhere in your codebase.
3668 /// Below is an example of how `Cve` might be defined:
3669 ///
3670 /// ```rust
3671 /// #[derive(PartialEq, PartialOrd, Ord, Eq)]
3672 /// pub struct Cve {
3673 /// pub reference: String,
3674 /// }
3675 /// ```
3676 pub fn cve_from_cwe(cwe_list: &Value) -> Vec<Cve> {
3677 let mut result: Vec<Cve> = vec![];
3678 let binding = cwe_list["vulnerabilities"].as_array();
3679 if let Some(_item) = binding {
3680 let lines = binding.unwrap();
3681 lines
3682 .iter()
3683 .for_each(|line| result.push(create_cve_from(line)))
3684 }
3685
3686 result
3687 }
3688}
3689
3690/// Module for CAPEC structure definition and its associated functions.
3691pub mod capec {
3692 use crate::database::{MitreData, MitreDefinition, find_capec_by_id, parse_id_to_u32};
3693 use crate::facilities::{
3694 FIELD_SEPARATOR_STRING, FIELD_SEPARATOR_STRING_LONG, Standardize, Uppercase,
3695 };
3696 use crate::{header, section};
3697 use std::fmt::{Display, Formatter};
3698 use termint::enums::Color;
3699 use termint::widgets::ToSpan;
3700
3701 /// Structure for a CAPEC record
3702 #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
3703 pub struct CapecRecord {
3704 /// Capec's id
3705 pub id: u32,
3706
3707 /// Capec's name
3708 pub name: String,
3709
3710 /// Capec's abstraction
3711 pub abstraction: String,
3712
3713 /// Capec's status
3714 pub status: String,
3715
3716 /// Capec's description
3717 pub description: String,
3718
3719 /// Capec's Likelihood
3720 pub likelihood: String,
3721
3722 /// Capec's alternate terms
3723 pub alternates: String,
3724
3725 /// Capec's severity
3726 pub severity: String,
3727
3728 /// Capec's flow
3729 pub flow: String,
3730
3731 /// Capec's related attack terms
3732 pub attacks: String,
3733
3734 /// Capec prerequisites
3735 pub prerequisites: String,
3736
3737 /// Capec's consequences
3738 pub consequences: String,
3739
3740 /// Capec's skills
3741 pub skills: String,
3742
3743 /// Capec's resources
3744 pub resources: String,
3745
3746 /// Capec's indicators
3747 pub indicators: String,
3748
3749 /// Capec mitigations
3750 pub mitigations: String,
3751
3752 /// Capec weaknesses
3753 pub weaknesses: String,
3754
3755 /// Capec's instances
3756 pub instances: String,
3757
3758 /// Capec's taxonomy
3759 pub taxonomy: String,
3760 }
3761
3762 impl Display for CapecRecord {
3763 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
3764 write!(
3765 f,
3766 "{}\n{} {} (Abstraction: {}. Status: {}.)",
3767 header!(format!("CAPEC-{}", self.id)),
3768 format!("{:>4} {}", "︎-", "Name:").fg(Color::DarkBlue),
3769 self.name,
3770 self.abstraction,
3771 self.status
3772 )
3773 }
3774 }
3775
3776 /// Capec's url to get all .csv.zip files to load into the internal database.
3777 impl MitreDefinition for CapecRecord {
3778 fn define() -> MitreData {
3779 MitreData {
3780 name: "Capec",
3781 url: "https://capec.mitre.org/data/csv",
3782 files: vec![
3783 "282.csv.zip",
3784 "283.csv.zip",
3785 "284.csv.zip",
3786 "483.csv.zip",
3787 "553.csv.zip",
3788 "683.csv.zip",
3789 "703.csv.zip",
3790 "1000.csv.zip",
3791 "2000.csv.zip",
3792 ],
3793 }
3794 }
3795 }
3796
3797 /// Shows the list of attacks describing into the string value. Each id value is separating from
3798 /// each value with a `:` characters, as: `:<id_1>:<id_2>:...:<id_n>:`. The `id` value is the
3799 /// CAPEC's id.
3800 ///
3801 /// # Arguments
3802 /// * `attacks` - List of CAPEC's ids.
3803 ///
3804 pub fn show_attacks(attacks: String) {
3805 let capec_list = parse_id_to_u32(attacks);
3806 if capec_list.is_empty() {
3807 println!("{:>4} None", "-");
3808 return;
3809 }
3810
3811 for capec_id in capec_list {
3812 let result = find_capec_by_id(capec_id);
3813 if let Some(record) = result {
3814 println!("{:>4} CAPEC-{} - {}", "-", capec_id, record.name);
3815 } else {
3816 println!("{:>4} CAPEC-{} - ???", "-", capec_id);
3817 }
3818 }
3819 }
3820
3821 /// Return the list of Mitre Att&ck' ids from the given taxonomy.
3822 ///
3823 /// # Arguments
3824 /// * `taxonomy`- CAPEC's taxonomy.
3825 ///
3826 /// # Returns
3827 /// The list of Mitre Att&ck's ids from the taxonomy.
3828 pub fn extract_mitre_attack_ids(taxonomy: String) -> Vec<String> {
3829 let normalized_taxonomy =
3830 taxonomy.standardize(vec!["TAXONOMY NAME", "ENTRY ID", "ENTRY NAME"]);
3831
3832 let mut result = Vec::new();
3833
3834 if !normalized_taxonomy.is_empty() {
3835 let sections = normalized_taxonomy.split(FIELD_SEPARATOR_STRING_LONG);
3836 sections.for_each(|section| {
3837 let cells = section
3838 .split(FIELD_SEPARATOR_STRING)
3839 .filter(|cell| !cell.is_empty())
3840 .collect::<Vec<&str>>();
3841
3842 let is_mitre = cells.contains(&"TAXONOMY NAME") && cells.contains(&"attack");
3843 if is_mitre {
3844 result.push(cells[3].to_owned());
3845 }
3846 });
3847 }
3848
3849 result
3850 }
3851}
3852
3853/// Error message for error on not enough element during parsing
3854const NOT_ENOUGH_ELEMENTS_ERROR: &str = "Not enough elements for parsing";
3855
3856/// Module for Mitre Att&ck structure definition and its associated functions.
3857pub mod mitre {
3858
3859 /// Describes a mitre record. If some values are not existing into the database, the corresponding field is empty.
3860 pub struct MitreRecord {
3861 /// The Mitre's id
3862 pub id: String,
3863
3864 /// The name of this mitre record
3865 pub name: String,
3866
3867 /// The description of this mitre record
3868 pub description: String,
3869
3870 /// The URL associated with this record. This URL allows to get more information for example.
3871 pub url: String,
3872
3873 /// The creation date.
3874 pub created: String,
3875
3876 /// The last updating date.
3877 pub modified: String,
3878
3879 /// The domain for this mitre record. If several domains are existing, this string contains all these separated by a comma.
3880 pub domain: String,
3881
3882 /// The version of this record.
3883 pub version: String,
3884
3885 /// The associated tactics for this mitre record. If several tactics are existing, this string contains all these separated by a comma.
3886 pub tactics: String,
3887
3888 /// The list of platforms. If several platforms are existing, this string contains all these separated by a comma.
3889 pub platforms: String,
3890
3891 /// Associated sub-techniques.
3892 pub sub_techniques: String,
3893
3894 /// The list of contributors. If several contributors are existing, this string contains all these separated by a comma.
3895 pub contributors: String,
3896
3897 /// Status that indicate if this Mitre record support the remote mode or not.
3898 pub support_remote: String,
3899
3900 /// Type of impact of this attack
3901 pub impact_type: String,
3902
3903 /// The list of citations that are using to document this Mitre record. If several contributors are existing, this string contains all these separated by a comma.
3904 pub citations: String,
3905
3906 /// Type of tactic used with this Mitre record
3907 pub tactic_type: String,
3908
3909 /// MTC's id used for the `Mobile` attack domain.
3910 pub mtc_id: String,
3911 }
3912}
3913
3914#[cfg(test)]
3915mod tests {
3916 use std::thread::sleep;
3917 use std::time::Duration;
3918
3919 fn setup() {
3920 let waiting = Duration::from_millis(30);
3921 sleep(waiting);
3922 }
3923
3924 mod query {
3925 use crate::network::validate_connection;
3926 use crate::tests::setup;
3927 use crate::{NVD_CVE_LINK, NVD_KEY_NAME};
3928 use std::env;
3929
3930 #[test]
3931 fn nvd_api_key_valid() {
3932 setup();
3933
3934 let env_var = env::var_os(NVD_KEY_NAME);
3935 if env_var.is_some() {
3936 let result = validate_connection(NVD_CVE_LINK);
3937 assert_eq!(result, true);
3938 } else {
3939 panic!("No NVD_API_KEY found");
3940 }
3941 }
3942
3943 #[test]
3944 fn is_connection_valid_url_validate() {
3945 setup();
3946
3947 assert!(validate_connection(NVD_CVE_LINK));
3948 }
3949
3950 #[test]
3951 fn is_connection_invalid_url_invalidate() {
3952 assert!(!validate_connection("http://foobar.local"));
3953 }
3954
3955 mod build_query {
3956 use crate::NVD_CVE_LINK;
3957 use crate::facilities::get_current_date;
3958 use crate::query::QueryType::{ByCve, ByCveNew, ByCveUpdated, ByCwe};
3959 use crate::query::build_query;
3960 use crate::tests::setup;
3961
3962 #[test]
3963 fn build_query_cve_valid() {
3964 setup();
3965
3966 let result = format!("{NVD_CVE_LINK}?cveId=CVE-2020-2102");
3967
3968 assert_eq!(result, build_query(ByCve, "CVE-2020-2102").unwrap());
3969 }
3970
3971 #[test]
3972 fn build_query_cve_lowercase_valid() {
3973 setup();
3974
3975 let result = format!("{NVD_CVE_LINK}?cveId=CVE-2020-2102");
3976
3977 assert_eq!(result, build_query(ByCve, "cve-2020-2102").unwrap());
3978 }
3979
3980 #[test]
3981 fn build_query_cwe_valid() {
3982 setup();
3983
3984 let result = format!("{NVD_CVE_LINK}?cweId=CWE-287");
3985
3986 assert_eq!(result, build_query(ByCwe, "CWE-287").unwrap());
3987 }
3988
3989 #[test]
3990 fn build_query_cwe_lowercase_valid() {
3991 setup();
3992
3993 let result = format!("{NVD_CVE_LINK}?cweId=CWE-287");
3994
3995 assert_eq!(result, build_query(ByCwe, "cwe-287").unwrap());
3996 }
3997
3998 #[test]
3999 fn build_query_new_dates_valid() {
4000 let result = build_query(ByCveNew, "2024-02-10;2024-03-10");
4001
4002 assert_eq!(
4003 "https://services.nvd.nist.gov/rest/json/cves/2.0?pubStartDate=2024-02-10T00:00:00.000&pubEndDate=2024-03-10T00:00:00.000",
4004 result.unwrap().as_str()
4005 );
4006 }
4007
4008 #[test]
4009 fn build_query_new_dates_default() {
4010 setup();
4011
4012 let result = build_query(ByCveNew, "");
4013 let ref_date = format!(
4014 "{NVD_CVE_LINK}?pubStartDate={}T00:00:00.000&pubEndDate={}T00:00:00.000",
4015 get_current_date(),
4016 get_current_date()
4017 );
4018
4019 assert_eq!(ref_date, result.unwrap());
4020 }
4021
4022 #[test]
4023 fn build_query_partial_date_first_default() {
4024 setup();
4025
4026 let result = build_query(ByCveNew, ";2024-01-01");
4027 let ref_date = format!(
4028 "{NVD_CVE_LINK}?pubStartDate={}T00:00:00.000&pubEndDate=2024-01-01T00:00:00.000",
4029 get_current_date()
4030 );
4031
4032 assert_eq!(ref_date, result.unwrap());
4033 }
4034
4035 #[test]
4036 fn build_query_partial_date_last_default() {
4037 setup();
4038
4039 let result = build_query(ByCveNew, "2024-01-01;");
4040 let ref_date = format!(
4041 "{NVD_CVE_LINK}?pubStartDate=2024-01-01T00:00:00.000&pubEndDate={}T00:00:00.000",
4042 get_current_date()
4043 );
4044
4045 assert_eq!(ref_date, result.unwrap());
4046 }
4047
4048 #[test]
4049 fn build_query_updated_valid() {
4050 setup();
4051
4052 let result = build_query(ByCveUpdated, "2024-01-01;2024-02-01");
4053
4054 assert_eq!(
4055 format!(
4056 "{NVD_CVE_LINK}?lastModStartDate=2024-01-01T00:00:00.000&lastModEndDate=2024-02-01T00:00:00.000"
4057 ),
4058 result.unwrap()
4059 );
4060 }
4061
4062 #[test]
4063 fn build_query_updated_default_valid() {
4064 setup();
4065
4066 let result = build_query(ByCveUpdated, "");
4067
4068 assert_eq!(
4069 format!(
4070 "{NVD_CVE_LINK}?lastModStartDate={}T00:00:00.000&lastModEndDate={}T00:00:00.000",
4071 get_current_date(),
4072 get_current_date()
4073 ),
4074 result.unwrap()
4075 );
4076 }
4077
4078 #[test]
4079 fn build_query_updated_first_valid() {
4080 setup();
4081
4082 let result = build_query(ByCveUpdated, "2023-01-02;");
4083
4084 assert_eq!(
4085 format!(
4086 "{NVD_CVE_LINK}?lastModStartDate=2023-01-02T00:00:00.000&lastModEndDate={}T00:00:00.000",
4087 get_current_date()
4088 ),
4089 result.unwrap()
4090 );
4091 }
4092
4093 #[test]
4094 fn build_query_updated_last_valid() {
4095 setup();
4096
4097 let result = build_query(ByCveUpdated, ";2032-12-12");
4098
4099 assert_eq!(
4100 format!(
4101 "{NVD_CVE_LINK}?lastModStartDate={}T00:00:00.000&lastModEndDate=2032-12-12T00:00:00.000",
4102 get_current_date()
4103 ),
4104 result.unwrap()
4105 );
4106 }
4107 }
4108
4109 mod execute_query {
4110 use crate::database::execute_query;
4111 use crate::query::QueryType::{ByCve, ByCveNew, ByCveUpdated};
4112 use crate::query::build_query;
4113 use crate::tests::setup;
4114 use serde_json::Value::Null;
4115
4116 #[test]
4117 fn execute_query_valid_cve_found() {
4118 setup();
4119
4120 let result = execute_query(build_query(ByCve, "CVE-2019-1010218"));
4121
4122 assert_ne!(Null, result["resultsPerPage"]);
4123 }
4124
4125 #[test]
4126 fn execute_query_invalid_cve_not_found() {
4127 setup();
4128
4129 let result = execute_query(build_query(ByCve, "CVE-1980-0000"));
4130
4131 assert_eq!(0, result["resultsPerPage"]);
4132 }
4133
4134 #[test]
4135 fn execute_query_new_dates_valid() {
4136 setup();
4137
4138 let result = execute_query(build_query(ByCveNew, "2024-01-01;2024-02-01"));
4139
4140 assert_ne!(0, result["resultsPerPage"]);
4141 }
4142
4143 #[test]
4144 fn execute_query_new_dates_swapped_not_valid() {
4145 setup();
4146
4147 let result = execute_query(build_query(ByCveNew, "2024-02-01;2024-01-01"));
4148
4149 assert_eq!(Null, result["resultsPerPage"]);
4150 }
4151
4152 #[test]
4153 fn execute_query_new_dates_malformed_no_valid() {
4154 setup();
4155
4156 let result = execute_query(build_query(ByCveNew, "202402-01;2024-03-01"));
4157
4158 assert_eq!(Null, result["resultPerPage"]);
4159 }
4160
4161 #[test]
4162 fn execute_query_update_dates_valid() {
4163 setup();
4164
4165 let result = execute_query(build_query(ByCveUpdated, "2024-01-01;2024-02-01"));
4166
4167 assert_ne!(0, result["resultsPerPage"]);
4168 }
4169
4170 #[test]
4171 fn execute_query_updated_dates_swapped_not_valid() {
4172 setup();
4173
4174 let result = execute_query(build_query(ByCveUpdated, "2024-02-01;2024-01-01"));
4175
4176 assert_eq!(Null, result["resultsPerPage"]);
4177 }
4178
4179 #[test]
4180 fn execute_query_updated_dates_malformed_no_valid() {
4181 setup();
4182
4183 let result = execute_query(build_query(ByCveUpdated, "202402-01;2024-03-01"));
4184
4185 assert_eq!(Null, result["resultPerPage"]);
4186 }
4187 }
4188 }
4189
4190 mod string {
4191 use crate::facilities::{Categorize, Cleaning, Standardize, Transform, Uppercase, Wording};
4192
4193 #[test]
4194 fn cleaning_ok() {
4195 let result = String::cleaning(String::from("\"value\""));
4196
4197 assert_eq!("value", result);
4198 }
4199
4200 #[test]
4201 fn cleaning_dots_ok() {
4202 let result = String::cleaning(String::from(":value:"));
4203
4204 assert_eq!("value", result);
4205 }
4206
4207 #[test]
4208 fn cleaning_no_cleaning() {
4209 let result = String::cleaning(String::from("value"));
4210
4211 assert_eq!("value", result);
4212 }
4213
4214 #[test]
4215 fn cleaning_empty_string_not_updated() {
4216 let result = String::cleaning(String::from(""));
4217
4218 assert_eq!("", result);
4219 }
4220
4221 #[test]
4222 fn only_uppercases_valid() {
4223 assert!(String::only_uppercases(String::from("BJDKE")));
4224 }
4225
4226 #[test]
4227 fn only_uppercases_invalid() {
4228 assert_eq!(false, String::only_uppercases(String::from("ABDdD")));
4229 }
4230
4231 #[test]
4232 fn only_uppercases_empty_invalid() {
4233 assert!(!String::only_uppercases(String::from("")));
4234 }
4235
4236 #[test]
4237 fn only_uppercases_with_space_valid() {
4238 assert!(String::only_uppercases(String::from("TECHNICAL PART")));
4239 }
4240
4241 #[test]
4242 fn smoothing_valid() {
4243 let original = String::from("::KKKK:fgte::::DFDF:fff::FFF:E::");
4244 let result = original.standardize(vec!["KKKK", "DFDF"]);
4245
4246 assert_eq!("::KKKK:fgte::DFDF:fff:fff:e::", result);
4247 }
4248
4249 #[test]
4250 fn smoothing_not_updated() {
4251 let original = String::from("::DKDKDKD::");
4252 let result = original.standardize(vec![""]);
4253
4254 assert_eq!("::dkdkdkd::", result);
4255 }
4256
4257 #[test]
4258 fn transform_valid() {
4259 let original = String::from("KKKKK fgte. DFDF");
4260 let result = original.transform(vec!["KKKKK", "DFDF"]);
4261
4262 assert_eq!("::KKKKK: fgte. ::DFDF::", result);
4263 }
4264
4265 #[test]
4266 fn wording_empty_string_valid() {
4267 let original = String::from("");
4268 let result = original.wording();
4269
4270 assert_eq!("", result);
4271 }
4272
4273 #[test]
4274 fn wording_associated_valid() {
4275 let original = String::from("wordAssociated");
4276 let result = original.wording();
4277
4278 assert_eq!("Word associated", result);
4279 }
4280
4281 #[test]
4282 fn wording_associated_inverse_valid() {
4283 let original = String::from("Wordassociation");
4284 let result = original.wording();
4285
4286 assert_eq!("Wordassociation", result);
4287 }
4288
4289 #[test]
4290 fn wording_not_associated_no_transformation() {
4291 let original = String::from("wordassociated");
4292 let result = original.wording();
4293
4294 assert_eq!("Wordassociated", result);
4295 }
4296
4297 #[test]
4298 fn wording_with_multi_words() {
4299 let original = String::from("OneWordAssociatedWithAnother");
4300 let result = original.wording();
4301
4302 assert_eq!("One word associated with another", result);
4303 }
4304
4305 #[test]
4306 fn wording_uppercases_lowercases_valid() {
4307 let original = String::from("WORDassociated");
4308 let result = original.wording();
4309
4310 assert_eq!("Wordassociated", result);
4311 }
4312
4313 #[test]
4314 fn wording_right_form_valid() {
4315 let original = String::from("Space");
4316 let result = original.wording();
4317
4318 assert_eq!("Space", result);
4319 }
4320
4321 #[test]
4322 fn wording_with_spaces_valid() {
4323 let original = String::from("Space From another World");
4324 let result = original.wording();
4325
4326 assert_eq!("Space from another world", result);
4327 }
4328
4329 #[test]
4330 fn first_uppercase_valid() {
4331 let original = String::from("first");
4332 let result = original.first_uppercase();
4333
4334 assert_eq!("First", result);
4335 }
4336
4337 #[test]
4338 fn first_already_uppercase_valid() {
4339 let original = String::from("First");
4340 let result = original.first_uppercase();
4341
4342 assert_eq!("First", result);
4343 }
4344 }
4345
4346 mod parse_id {
4347 use crate::os::parse_id;
4348
4349 #[test]
4350 fn cwe_id_valid_id_parsed() {
4351 assert_eq!(Some(15), parse_id("CWE-15", "cwe"));
4352 }
4353
4354 #[test]
4355 fn cwe_id_valid_lowercase_id_parsed() {
4356 assert_eq!(Some(118), parse_id("cwe-118", "cwe"));
4357 }
4358
4359 #[test]
4360 fn cwe_id_invalid_id_not_parsed() {
4361 assert_eq!(None, parse_id("cwe116", "cwe"));
4362 }
4363
4364 #[test]
4365 fn cwe_id_invalid_id_num_not_parsed() {
4366 assert_eq!(None, parse_id("CWE-A1", "cwe"));
4367 }
4368
4369 #[test]
4370 fn cwe_id_invalid_id_prefix_not_parsed() {
4371 assert_eq!(None, parse_id("DFDF-12", "cwe"));
4372 }
4373
4374 #[test]
4375 fn cwe_id_incomplete_id_not_parsed() {
4376 assert_eq!(None, parse_id("CWE-", "cwe"));
4377 }
4378
4379 #[test]
4380 fn cwe_id_no_separator_not_parsed() {
4381 assert_eq!(None, parse_id("CWE", "cwe"));
4382 }
4383 }
4384
4385 mod criteria {
4386 use crate::database::criteria_analysis;
4387 use utmt::{assert_match_option, assert_not_match_option};
4388
4389 #[test]
4390 fn simple_no_error() -> Result<(), &'static str> {
4391 let args = String::from("Masquerading");
4392 let result = criteria_analysis(args);
4393
4394 assert_match_option!(result)
4395 }
4396
4397 #[test]
4398 fn simple_right_content() -> Result<(), &'static str> {
4399 let args = String::from("Masquerading");
4400 let result = criteria_analysis(args);
4401
4402 match result.unwrap().0.contains(&String::from("Masquerading")) {
4403 true => Ok(()),
4404 false => Err("Simple criteria analysis failed: content not found"),
4405 }
4406 }
4407
4408 #[test]
4409 fn analysis_empty_string() -> Result<(), &'static str> {
4410 let result = criteria_analysis(String::from(""));
4411
4412 assert_not_match_option!(result)
4413 }
4414
4415 #[test]
4416 fn analysis_only_or_operator() -> Result<(), &'static str> {
4417 let args = String::from("Masquerading or Hijack");
4418 let result = criteria_analysis(args);
4419
4420 assert_match_option!(result)
4421 }
4422
4423 #[test]
4424 fn analysis_or_operator_right_content() -> Result<(), &'static str> {
4425 let args = String::from("Masquerading or Hijack");
4426 let result = criteria_analysis(args);
4427
4428 let mut count = 0;
4429 result.clone().unwrap().0.iter().for_each(|x| {
4430 if !x.trim().is_empty() {
4431 if x.trim() == "Masquerading" || x.trim() == "Hijack" {
4432 count += 1;
4433 } else {
4434 count -= 1;
4435 }
4436 }
4437 });
4438
4439 result.unwrap().1.iter().for_each(|x| {
4440 if x.trim() == "or" {
4441 count += 1;
4442 } else {
4443 count -= 1;
4444 }
4445 });
4446
4447 match count {
4448 3 => Ok(()),
4449 _ => Err("Error with or operator"),
4450 }
4451 }
4452
4453 #[test]
4454 fn analysis_only_and_operator() -> Result<(), &'static str> {
4455 let args = String::from("Process and Hijacking");
4456 let result = criteria_analysis(args);
4457
4458 assert_match_option!(result)
4459 }
4460
4461 #[test]
4462 fn analysis_and_operator_right_content() -> Result<(), &'static str> {
4463 let args = String::from("Process and Hijacking");
4464 let result = criteria_analysis(args);
4465
4466 let mut count = 0;
4467 result.clone().unwrap().0.iter().for_each(|x| {
4468 if !x.is_empty() {
4469 if x.trim() == "Process" || x.trim() == "Hijacking" {
4470 count += 1;
4471 } else {
4472 count -= 1;
4473 }
4474 }
4475 });
4476
4477 result.unwrap().1.iter().for_each(|x| {
4478 if x.trim() == "and" {
4479 count += 1;
4480 } else {
4481 count -= 1;
4482 }
4483 });
4484
4485 match count {
4486 3 => Ok(()),
4487 _ => Err("Error with and operator"),
4488 }
4489 }
4490
4491 #[test]
4492 fn analysis_only_not_operator() -> Result<(), &'static str> {
4493 let args = String::from("not Masquerading");
4494 let result = criteria_analysis(args);
4495
4496 assert_match_option!(result)
4497 }
4498
4499 #[test]
4500 fn analysis_not_operator_right_content() -> Result<(), &'static str> {
4501 let args = String::from("not Masquerading");
4502 let result = criteria_analysis(args);
4503
4504 let mut count = 0;
4505 result.clone().unwrap().0.iter().for_each(|x| {
4506 if x.trim() == "Masquerading" || x.trim() == "" {
4507 count += 1;
4508 } else {
4509 count -= 1;
4510 }
4511 });
4512
4513 result.unwrap().1.iter().for_each(|x| {
4514 if x.trim() == "not" {
4515 count += 1;
4516 } else {
4517 count -= 1;
4518 }
4519 });
4520
4521 match count {
4522 3 => Ok(()),
4523 _ => Err("Error with not operator"),
4524 }
4525 }
4526
4527 #[test]
4528 fn analysis_with_and_or_operators() -> Result<(), &'static str> {
4529 let args = String::from("Phishing or Process and Hijacking");
4530 let result = criteria_analysis(args);
4531
4532 assert_match_option!(result)
4533 }
4534
4535 #[test]
4536 fn analysis_and_operators_right_content() -> Result<(), &'static str> {
4537 let args = String::from("Phishing or Process and Hijacking");
4538 let result = criteria_analysis(args);
4539
4540 let mut count = 0;
4541 result.clone().unwrap().0.iter().for_each(|x| {
4542 if !x.is_empty() {
4543 if x.trim() == "Phishing" || x.trim() == "Process" || x.trim() == "Hijacking" {
4544 count += 1;
4545 } else {
4546 count -= 1;
4547 }
4548 }
4549 });
4550
4551 result.unwrap().1.iter().for_each(|x| {
4552 if x.trim() == "and" || x.trim() == "or" {
4553 count += 1;
4554 } else {
4555 count -= 1;
4556 }
4557 });
4558
4559 match count {
4560 5 => Ok(()),
4561 _ => Err("Error with or/and operators"),
4562 }
4563 }
4564
4565 #[test]
4566 fn analysis_and_or_not_operators() -> Result<(), &'static str> {
4567 let args = String::from("Phishing or Process and Hijacking and not Masquerading");
4568 let result = criteria_analysis(args);
4569
4570 assert_match_option!(result)
4571 }
4572
4573 #[test]
4574 fn analysis_and_or_not_operators_right_content() -> Result<(), &'static str> {
4575 let args = String::from("Phishing or Process and Hijacking and not Masquerading");
4576 let result = criteria_analysis(args);
4577
4578 let mut count = 0;
4579 result.clone().unwrap().0.iter().for_each(|x| {
4580 if !x.trim().is_empty() {
4581 if x.trim() == "Phishing"
4582 || x.trim() == "Process"
4583 || x.trim() == "Hijacking"
4584 || x.trim() == "Masquerading"
4585 {
4586 count += 1;
4587 } else {
4588 count -= 1;
4589 }
4590 }
4591 });
4592
4593 result.unwrap().1.iter().for_each(|x| {
4594 if x.trim() == "and" || x.trim() == "or" || x.trim() == "not" {
4595 count += 1;
4596 } else {
4597 count -= 1;
4598 }
4599 });
4600
4601 match count {
4602 8 => Ok(()),
4603 _ => Err("Error with and/or/not operators"),
4604 }
4605 }
4606 }
4607
4608 mod find_domain {
4609 use crate::database::find_domain_and_criteria;
4610 use utmt::{assert_all, assert_match_option, assert_not_match_option};
4611
4612 #[test]
4613 fn criteria_simple_no_error() -> Result<(), &'static str> {
4614 let args = &[String::from("taxonomy = Hijack")];
4615 let values = find_domain_and_criteria(args);
4616
4617 assert_match_option!(values)
4618 }
4619
4620 #[test]
4621 fn criteria_simple_with_error() -> Result<(), &'static str> {
4622 let args = &[String::from("taxonomy")];
4623 let values = find_domain_and_criteria(args);
4624
4625 assert_not_match_option!(values)
4626 }
4627
4628 #[test]
4629 fn criteria_simple_valid() {
4630 let args = &[String::from("taxonomy = Masquerading")];
4631 let values = find_domain_and_criteria(args);
4632
4633 let (domain, argument) = values.unwrap();
4634
4635 assert_all!(domain == "taxonomy", argument == "Masquerading");
4636 }
4637
4638 #[test]
4639 fn criteria_complex() {
4640 let args = &[String::from(
4641 "taxonomy = Masquerading and Hijack or impair defense",
4642 )];
4643 let values = find_domain_and_criteria(args);
4644 let (domain, argument) = values.unwrap();
4645
4646 assert_all!(
4647 domain == "taxonomy",
4648 argument == "Masquerading and Hijack or impair defense"
4649 );
4650 }
4651
4652 #[test]
4653 fn criteria_malformed_error() -> Result<(), &'static str> {
4654 let args = &[String::from("taxonomy = ")];
4655 let values = find_domain_and_criteria(args);
4656
4657 assert_not_match_option!(values)
4658 }
4659
4660 #[test]
4661 fn criteria_malformed_domain_error() -> Result<(), &'static str> {
4662 let args = &[String::from("= Masquerading ")];
4663 let values = find_domain_and_criteria(args);
4664
4665 assert_not_match_option!(
4666 values,
4667 "The malformed expression ' = Masquerading ' would be invalid"
4668 )
4669 }
4670
4671 #[test]
4672 fn criteria_malformed_equal_only_error() -> Result<(), &'static str> {
4673 let args = &[String::from("=")];
4674 let values = find_domain_and_criteria(args);
4675
4676 assert_not_match_option!(values, "'=' alone would not be supported")
4677 }
4678
4679 #[test]
4680 fn criteria_malformed_triple_equals_error() -> Result<(), &'static str> {
4681 let args = &[String::from("===")];
4682 let values = find_domain_and_criteria(args);
4683
4684 assert_not_match_option!(values, "'===' would not be supported")
4685 }
4686 }
4687
4688 mod mitre {
4689 use crate::database::find_mitre_by_id;
4690
4691 #[test]
4692 fn find_mitre_id() -> Result<(), &'static str> {
4693 if let Some(_) = find_mitre_by_id("1562.003") {
4694 Ok(())
4695 } else {
4696 Err("Mitre data not found")
4697 }
4698 }
4699
4700 #[test]
4701 fn find_invalid_mitre_id() -> Result<(), &'static str> {
4702 if let Some(_) = find_mitre_by_id("") {
4703 Err("Mitre data would be not found")
4704 } else {
4705 Ok(())
4706 }
4707 }
4708 }
4709}