diff --git a/rust/models/src/result.rs b/rust/models/src/result.rs index 4c6db539c..5afa69202 100644 --- a/rust/models/src/result.rs +++ b/rust/models/src/result.rs @@ -109,7 +109,7 @@ pub struct VulnerablePackage { pub fixed_version: FixedVersion, } -#[cfg_attr(feature = "serde_support", derive(serde::Serialize))] +#[cfg_attr(feature = "serde_support", derive(serde::Serialize), serde(untagged))] #[derive(Debug)] pub enum FixedVersion { Single { diff --git a/rust/notus/data/debian_10_advisory_parse_err.notus b/rust/notus/data/debian_10_advisory_parse_err.notus new file mode 100644 index 000000000..944463f20 --- /dev/null +++ b/rust/notus/data/debian_10_advisory_parse_err.notus @@ -0,0 +1,87 @@ +{ + "version": "1.3", + "package_type": "deb", + "advisories": [ + { + "oid": "1.3.6.1.4.1.25623.1.1.7.2.2023.10089729899100", + "fixed_packages": [ + { + "name": "gitlab-ce", + "range": { + "start": "?", + "end": "=" + } + }, + { + "name": "gitlab-ce", + "range": { + "start": "16.0.0", + "end": "16.0.7" + } + }, + { + "name": "gitlab-ce", + "range": { + "start": "16.1.0", + "end": "16.1.2" + } + } + ] + }, + { + "oid": "1.3.6.1.4.1.25623.1.1.7.2.2023.0988598199100", + "fixed_packages": [ + { + "name": "grafana", + "full_version": "8.5.24", + "specifier": ">=" + }, + { + "name": "grafana", + "range": { + "start": "9.0.0", + "end": "9.2.17" + } + }, + { + "name": "grafana", + "range": { + "start": "9.3.0", + "end": "9.3.13" + } + }, + { + "name": "grafana", + "range": { + "start": "9.4.0", + "end": "9.4.9" + } + }, + { + "name": "grafana8", + "full_version": "8.5.24", + "specifier": ">=" + }, + { + "name": "grafana9", + "full_version": "9.2.17", + "specifier": ">=" + }, + { + "name": "grafana9", + "range": { + "start": "9.3.0", + "end": "9.3.13" + } + }, + { + "name": "grafana9", + "range": { + "start": "9.4.0", + "end": "9.4.9" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/rust/notus/data/debian_10_json_parse_err.notus b/rust/notus/data/debian_10_json_parse_err.notus new file mode 100644 index 000000000..30061fa9c --- /dev/null +++ b/rust/notus/data/debian_10_json_parse_err.notus @@ -0,0 +1,87 @@ +{ + "version": "1.3", + "package_type": "deb", + "advisories": [ + { + "oid": "1.3.6.1.4.1.25623.1.1.7.2.2023.10089729899100", + "fixed_packages": [ + { + "name": "gitlab-ce", + "range": { + "start": "15.11.0", + "en": "15.11.11" + } + }, + { + "name": "gitlab-ce", + "range": { + "start": "16.0.0", + "end": "16.0.7" + } + }, + { + "name": "gitlab-ce", + "range": { + "start": "16.1.0", + "end": "16.1.2" + } + } + ] + }, + { + "oid": "1.3.6.1.4.1.25623.1.1.7.2.2023.0988598199100", + "fixed_packages": [ + { + "name": "grafana", + "full_version": "8.5.24", + "specifier": ">=" + }, + { + "name": "grafana", + "range": { + "start": "9.0.0", + "end": "9.2.17" + } + }, + { + "name": "grafana", + "range": { + "start": "9.3.0", + "end": "9.3.13" + } + }, + { + "name": "grafana", + "range": { + "start": "9.4.0", + "end": "9.4.9" + } + }, + { + "name": "grafana8", + "full_version": "8.5.24", + "specifier": ">=" + }, + { + "name": "grafana9", + "full_version": "9.2.17", + "specifier": ">=" + }, + { + "name": "grafana9", + "range": { + "start": "9.3.0", + "end": "9.3.13" + } + }, + { + "name": "grafana9", + "range": { + "start": "9.4.0", + "end": "9.4.9" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/rust/notus/src/advisory.rs b/rust/notus/src/advisory.rs index a20f89609..8a5853dfd 100644 --- a/rust/notus/src/advisory.rs +++ b/rust/notus/src/advisory.rs @@ -2,7 +2,10 @@ use std::collections::HashMap; use models::{FixedPackage, FixedVersion, Specifier}; -use crate::packages::{deb::Deb, ebuild::EBuild, rpm::Rpm, slack::Slack, Package}; +use crate::{ + error::Error, + packages::{deb::Deb, ebuild::EBuild, rpm::Rpm, slack::Slack, Package}, +}; pub type Advisories

= HashMap>>; @@ -17,7 +20,7 @@ impl PackageAdvisories { fn fill_advisory_map( advisory_map: &mut Advisories

, advisories: Vec, - ) { + ) -> Result<(), Error> { // Iterate through advisories of parse file for advisory in advisories { // Iterate through fixed_packages of single advisories @@ -26,7 +29,7 @@ impl PackageAdvisories { let (pkg_name, adv) = match Advisory::create(advisory.oid.clone(), &fixed_package) { Some(adv) => adv, // Notus data on system are wrong! - None => continue, // TODO: Some handling, at least logging + None => return Err(Error::AdvisoryParseError("".to_string(), fixed_package)), }; // Add advisory to map match advisory_map.get_mut(&pkg_name) { @@ -39,34 +42,37 @@ impl PackageAdvisories { }; } } + Ok(()) } } -impl From for PackageAdvisories { - fn from(value: models::Advisories) -> Self { +impl TryFrom for PackageAdvisories { + fn try_from(value: models::Advisories) -> Result { match value.package_type { models::PackageType::DEB => { let mut advisory_map: Advisories = HashMap::new(); - Self::fill_advisory_map(&mut advisory_map, value.advisories); - Self::Deb(advisory_map) + Self::fill_advisory_map(&mut advisory_map, value.advisories)?; + Ok(Self::Deb(advisory_map)) } models::PackageType::EBUILD => { let mut advisory_map: Advisories = HashMap::new(); - Self::fill_advisory_map(&mut advisory_map, value.advisories); - Self::EBuild(advisory_map) + Self::fill_advisory_map(&mut advisory_map, value.advisories)?; + Ok(Self::EBuild(advisory_map)) } models::PackageType::RPM => { let mut advisory_map: Advisories = HashMap::new(); - Self::fill_advisory_map(&mut advisory_map, value.advisories); - Self::Rpm(advisory_map) + Self::fill_advisory_map(&mut advisory_map, value.advisories)?; + Ok(Self::Rpm(advisory_map)) } models::PackageType::SLACK => { let mut advisory_map: Advisories = HashMap::new(); - Self::fill_advisory_map(&mut advisory_map, value.advisories); - Self::Slack(advisory_map) + Self::fill_advisory_map(&mut advisory_map, value.advisories)?; + Ok(Self::Slack(advisory_map)) } } } + + type Error = Error; } pub struct Advisory

diff --git a/rust/notus/src/error.rs b/rust/notus/src/error.rs index 0e173a82e..885ca0125 100644 --- a/rust/notus/src/error.rs +++ b/rust/notus/src/error.rs @@ -1,20 +1,38 @@ -use std::fmt::Display; +use std::{fmt::Display, io}; -#[derive(PartialEq, PartialOrd, Debug)] -pub enum NotusError { - InvalidOS, - JSONParseError, - UnsupportedVersion(String), - NoLoader, +use models::FixedPackage; + +#[derive(Debug)] +pub enum Error { + // The directory containing the notus advisories does not exist + MissingAdvisoryDir(String), + // The given notus advisory directory is a file + AdvisoryDirIsFile(String), + // There are no corresponding notus files for the given Operating System + UnknownOs(String), + // General error while loading notus advisories + LoadAdvisoryError(String, io::Error), + // Unable to parse notus advisory file due to a JSON error + JSONParseError(String, serde_json::Error), + // The version of the notus advisory file is not supported + UnsupportedVersion(String, String, String), + // Unable to parse a given package + PackageParseError(String), + // Unable to parse a package in the notus advisory file + AdvisoryParseError(String, FixedPackage), } -impl Display for NotusError { - fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - NotusError::InvalidOS => todo!(), - NotusError::JSONParseError => todo!(), - NotusError::UnsupportedVersion(_) => todo!(), - NotusError::NoLoader => todo!(), + Error::UnknownOs(path) => write!(f, "the File {path} was not found, that is either due to a typo or missing notus advisories for the corresponding OS"), + Error::JSONParseError(path, json_err) => write!(f, "unable to parse Notus file {path}. The corresponding parse error was: {json_err}"), + Error::UnsupportedVersion(path, version1, version2) => write!(f, "the version of the parsed advisory file {path} is {version1}. This version is currently not supported, the version {version2} is required"), + Error::MissingAdvisoryDir(path) => write!(f, "The directory {path}, which should contain the notus advisories does not exist"), + Error::AdvisoryDirIsFile(path) => write!(f, "The given notus advisory directory {path} is a file"), + Error::LoadAdvisoryError(path, err) => write!(f, "Unable to load advisories from {path}: {err}"), + Error::PackageParseError(pkg) => write!(f, "Unable to parse the given package {pkg}"), + Error::AdvisoryParseError(path, pkg) => write!(f, "Unable to parse fixed package information {:?} in the advisories {path}", pkg), } } } diff --git a/rust/notus/src/lib.rs b/rust/notus/src/lib.rs index 2ce655d65..f44143ba7 100644 --- a/rust/notus/src/lib.rs +++ b/rust/notus/src/lib.rs @@ -6,4 +6,5 @@ pub mod loader; pub mod packages; pub mod advisory; +pub mod error; pub mod notus; diff --git a/rust/notus/src/loader/json.rs b/rust/notus/src/loader/json.rs index 84c6ce908..c072d1ae2 100644 --- a/rust/notus/src/loader/json.rs +++ b/rust/notus/src/loader/json.rs @@ -4,14 +4,17 @@ use std::{ fs::File, - io::{Error, ErrorKind, Read}, + io::{self, Read}, path::Path, }; use models::Advisories; +use crate::error::Error; + use super::AdvisoriesLoader; +#[derive(Debug)] pub struct JSONAdvisoriesLoader

where P: AsRef, @@ -24,8 +27,16 @@ where P: AsRef, { pub fn new(path: P) -> Result { - if !path.as_ref().exists() || !path.as_ref().is_dir() { - return Err(ErrorKind::NotFound.into()); + if !path.as_ref().exists() { + return Err(Error::MissingAdvisoryDir( + path.as_ref().to_string_lossy().to_string(), + )); + } + + if !path.as_ref().is_dir() { + return Err(Error::AdvisoryDirIsFile( + path.as_ref().to_string_lossy().to_string(), + )); } Ok(Self { path }) @@ -38,12 +49,23 @@ where { fn load_package_advisories(&self, os: &str) -> Result { let notus_file = self.path.as_ref().join(format!("{os}.notus")); - let mut file = File::open(notus_file)?; + let notus_file_str = notus_file.to_string_lossy().to_string(); + let mut file = match File::open(notus_file) { + Ok(file) => file, + Err(err) => { + if matches!(err.kind(), io::ErrorKind::NotFound) { + return Err(Error::UnknownOs(os.to_string())); + } + return Err(Error::LoadAdvisoryError(notus_file_str, err)); + } + }; let mut buf = String::new(); - file.read_to_string(&mut buf)?; + if let Err(err) = file.read_to_string(&mut buf) { + return Err(Error::LoadAdvisoryError(notus_file_str, err)); + } match serde_json::from_str(&buf) { - Ok(json) => Ok(json), - Err(e) => Err(e.into()), + Ok(adv) => Ok(adv), + Err(err) => Err(Error::JSONParseError(notus_file_str, err)), } } } @@ -51,7 +73,7 @@ where #[cfg(test)] mod tests { - use crate::loader::AdvisoriesLoader; + use crate::{error::Error, loader::AdvisoriesLoader}; use super::JSONAdvisoriesLoader; @@ -62,4 +84,46 @@ mod tests { let loader = JSONAdvisoriesLoader::new(path).unwrap(); let _ = loader.load_package_advisories("debian_10").unwrap(); } + + #[test] + fn test_err_missing_advisory_dir() { + let mut path = env!("CARGO_MANIFEST_DIR").to_string(); + path.push_str("/data_foo"); + assert!( + matches!(JSONAdvisoriesLoader::new(path.clone()).expect_err("Should fail"), Error::MissingAdvisoryDir(p) if p == path) + ); + } + + #[test] + fn test_err_advisory_dir_is_file() { + let mut path = env!("CARGO_MANIFEST_DIR").to_string(); + path.push_str("/data/debian_10.notus"); + assert!( + matches!(JSONAdvisoriesLoader::new(path.clone()).expect_err("Should fail"), Error::AdvisoryDirIsFile(p) if p == path) + ); + } + + #[test] + fn test_err_unknown_os() { + let mut path = env!("CARGO_MANIFEST_DIR").to_string(); + path.push_str("/data"); + let loader = JSONAdvisoriesLoader::new(path).unwrap(); + + let os = "foo"; + assert!( + matches!(loader.load_package_advisories(os).expect_err("Should fail"), Error::UnknownOs(o) if o == os) + ); + } + + #[test] + fn test_err_json_parse() { + let mut path = env!("CARGO_MANIFEST_DIR").to_string(); + path.push_str("/data"); + let loader = JSONAdvisoriesLoader::new(path.clone()).unwrap(); + + let os = "debian_10_json_parse_err"; + assert!( + matches!(loader.load_package_advisories(os).expect_err("Should fail"), Error::JSONParseError(p, _) if p == format!("{path}/{os}.notus")) + ); + } } diff --git a/rust/notus/src/loader/mod.rs b/rust/notus/src/loader/mod.rs index 9e4ae0cf8..6168d2f95 100644 --- a/rust/notus/src/loader/mod.rs +++ b/rust/notus/src/loader/mod.rs @@ -2,10 +2,10 @@ // // SPDX-License-Identifier: GPL-2.0-or-later -use std::io::Error; - use models::Advisories; +use crate::error::Error; + pub mod json; /// Trait for and AdvisoryLoader diff --git a/rust/notus/src/notus.rs b/rust/notus/src/notus.rs index af8da39a7..158ab3a48 100644 --- a/rust/notus/src/notus.rs +++ b/rust/notus/src/notus.rs @@ -1,31 +1,14 @@ -use std::{collections::HashMap, fmt::Display}; +use std::collections::HashMap; use models::{NotusResults, VulnerablePackage}; use crate::{ advisory::{Advisories, PackageAdvisories}, + error::Error, loader::AdvisoriesLoader, packages::Package, }; -#[derive(PartialEq, PartialOrd, Debug)] - -pub enum NotusError { - InvalidOS, - JSONParseError, - UnsupportedVersion(String), -} - -impl Display for NotusError { - fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - NotusError::InvalidOS => todo!(), - NotusError::JSONParseError => todo!(), - NotusError::UnsupportedVersion(_) => todo!(), - } - } -} - pub struct Notus where L: AdvisoriesLoader, @@ -45,52 +28,72 @@ where } } - fn load_new_advisories(&self, os: &str) -> Result { - // TODO: Error handling - let advisories = self.loader.load_package_advisories(os).unwrap(); + fn load_new_advisories(&self, os: &str) -> Result { + let advisories = self.loader.load_package_advisories(os)?; - Ok(PackageAdvisories::from(advisories)) + match PackageAdvisories::try_from(advisories) { + Ok(adv) => Ok(adv), + Err(Error::AdvisoryParseError(_, pkg)) => { + Err(Error::AdvisoryParseError(os.to_string(), pkg)) + } + Err(err) => Err(err), + } } - fn parse_and_compare( - packages: Vec, - advisories: &Advisories

, - ) -> NotusResults { + fn parse(packages: &Vec) -> Result, Error> { + // Parse all packages + let mut parsed_packages = vec![]; + for package in packages { + match P::from_full_name(package) { + Some(package) => parsed_packages.push(package), + // Unable to parse user input + None => return Err(Error::PackageParseError(package.clone())), + } + } + + Ok(parsed_packages) + } + + fn compare(packages: &Vec

, advisories: &Advisories

) -> NotusResults { let mut results: NotusResults = HashMap::new(); for package in packages { - match P::from_full_name(&package) { - Some(package) => match advisories.get(&package.get_name()) { - Some(advisories) => { - for advisory in advisories { - if advisory.is_vulnerable(&package) { - let vul_pkg = VulnerablePackage { - name: package.get_name(), - installed_version: package.get_version(), - fixed_version: advisory.get_fixed_version(), - }; - match results.get_mut(&advisory.get_oid()) { - Some(vul_pkgs) => { - vul_pkgs.push(vul_pkg); - } - None => { - results.insert(advisory.get_oid(), vec![vul_pkg]); - } + match advisories.get(&package.get_name()) { + Some(advisories) => { + for advisory in advisories { + if advisory.is_vulnerable(package) { + let vul_pkg = VulnerablePackage { + name: package.get_name(), + installed_version: package.get_version(), + fixed_version: advisory.get_fixed_version(), + }; + match results.get_mut(&advisory.get_oid()) { + Some(vul_pkgs) => { + vul_pkgs.push(vul_pkg); + } + None => { + results.insert(advisory.get_oid(), vec![vul_pkg]); } } } } - // No advisory for package - None => continue, - }, - // Unable to parse user input - None => continue, // TODO: Some Error handling, at least Logging + } + // No advisory for package + None => continue, } } results } - pub fn scan(&mut self, os: &str, packages: Vec) -> Result { + fn parse_and_compare( + packages: &Vec, + advisories: &Advisories

, + ) -> Result { + let packages = Self::parse(packages)?; + Ok(Self::compare(&packages, advisories)) + } + + pub fn scan(&mut self, os: &str, packages: &Vec) -> Result { // Load advisories if not loaded let advisories = match self.loaded_advisories.get(os) { Some(adv) => adv, @@ -103,10 +106,10 @@ where // Parse and compare package list depending on package type of loaded advisories let results = match advisories { - PackageAdvisories::Deb(adv) => Self::parse_and_compare(packages, adv), - PackageAdvisories::EBuild(adv) => Self::parse_and_compare(packages, adv), - PackageAdvisories::Rpm(adv) => Self::parse_and_compare(packages, adv), - PackageAdvisories::Slack(adv) => Self::parse_and_compare(packages, adv), + PackageAdvisories::Deb(adv) => Self::parse_and_compare(packages, adv)?, + PackageAdvisories::EBuild(adv) => Self::parse_and_compare(packages, adv)?, + PackageAdvisories::Rpm(adv) => Self::parse_and_compare(packages, adv)?, + PackageAdvisories::Slack(adv) => Self::parse_and_compare(packages, adv)?, }; Ok(results) diff --git a/rust/notus/tests/notus.rs b/rust/notus/tests/notus.rs index 282bbf9d9..5830bbe6a 100644 --- a/rust/notus/tests/notus.rs +++ b/rust/notus/tests/notus.rs @@ -4,14 +4,15 @@ #[cfg(test)] mod tests { - use models::{FixedVersion, Specifier}; - use notus::{loader::json::JSONAdvisoriesLoader, notus::Notus}; + + use models::{FixedPackage, FixedVersion, Specifier}; + use notus::{error::Error, loader::json::JSONAdvisoriesLoader, notus::Notus}; #[test] fn test_notus() { let mut path = env!("CARGO_MANIFEST_DIR").to_string(); path.push_str("/data"); - let loader = JSONAdvisoriesLoader::new(path).unwrap(); + let loader = JSONAdvisoriesLoader::new(path.clone()).unwrap(); let mut notus = Notus::new(loader); let packages = vec![ @@ -23,7 +24,7 @@ mod tests { "foo-1.2.3".to_string(), // no vul ]; - let results = notus.scan("debian_10", packages).unwrap(); + let results = notus.scan("debian_10", &packages).unwrap(); assert_eq!(results.len(), 2); let result1 = &results["1.3.6.1.4.1.25623.1.1.7.2.2023.10089729899100"]; @@ -59,4 +60,36 @@ mod tests { } } } + + #[test] + fn test_err_package_parse_error() { + let mut path = env!("CARGO_MANIFEST_DIR").to_string(); + path.push_str("/data"); + let loader = JSONAdvisoriesLoader::new(path.clone()).unwrap(); + let mut notus = Notus::new(loader); + + let pkg_name = "wepofkewf~.124.sdefpo3-_~s#"; + + let packages = vec![pkg_name.to_string()]; + + let os = "debian_10"; + assert!( + matches!(notus.scan(os, &packages).expect_err("Should fail"), Error::PackageParseError(p) if p == pkg_name) + ); + } + + #[test] + fn test_err_advisory_parse_error() { + let mut path = env!("CARGO_MANIFEST_DIR").to_string(); + path.push_str("/data"); + let loader = JSONAdvisoriesLoader::new(path.clone()).unwrap(); + let mut notus = Notus::new(loader); + + let packages = vec![]; + + let os = "debian_10_advisory_parse_err"; + assert!( + matches!(notus.scan(os, &packages).expect_err("Should fail"), Error::AdvisoryParseError(p, FixedPackage::ByRange { name, range }) if p == os && name == "gitlab-ce" && range.start == "?" && range.end == "=" ) + ); + } }