diff --git a/graph/src/graph/vulnerability.rs b/graph/src/graph/vulnerability.rs index 22c583ee7..b76322142 100644 --- a/graph/src/graph/vulnerability.rs +++ b/graph/src/graph/vulnerability.rs @@ -19,7 +19,7 @@ impl Graph { identifier: &str, tx: Transactional<'_>, ) -> Result { - if let Some(found) = self.get_cve(identifier, tx).await? { + if let Some(found) = self.get_vulnerability(identifier, tx).await? { Ok(found) } else { let entity = vulnerability::ActiveModel { @@ -31,7 +31,7 @@ impl Graph { } } - pub async fn get_cve( + pub async fn get_vulnerability( &self, identifier: &str, tx: Transactional<'_>, @@ -105,7 +105,9 @@ mod tests { assert_eq!(cve1.cve.id, cve2.cve.id); assert_ne!(cve1.cve.id, cve3.cve.id); - let not_found = system.get_cve("CVE-NOT_FOUND", Transactional::None).await?; + let not_found = system + .get_vulnerability("CVE-NOT_FOUND", Transactional::None) + .await?; assert!(not_found.is_none()); @@ -136,7 +138,9 @@ mod tests { .ingest_vulnerability("CVE-8675309", Transactional::None) .await?; - let cve = system.get_cve("CVE-8675309", Transactional::None).await?; + let cve = system + .get_vulnerability("CVE-8675309", Transactional::None) + .await?; assert!(cve.is_some()); diff --git a/importer/src/sbom/mod.rs b/importer/src/sbom/mod.rs index cfa6ef691..d622b431f 100644 --- a/importer/src/sbom/mod.rs +++ b/importer/src/sbom/mod.rs @@ -8,8 +8,8 @@ use sbom_walker::{ use std::process::ExitCode; use std::time::SystemTime; use time::{Date, Month, UtcOffset}; -use trustify_graph::graph::Graph; use trustify_common::config::Database; +use trustify_graph::graph::Graph; use url::Url; use walker_common::{fetcher::Fetcher, validate::ValidationOptions}; diff --git a/ingestors/Cargo.toml b/ingestors/Cargo.toml index f477d6978..9ac8576fa 100644 --- a/ingestors/Cargo.toml +++ b/ingestors/Cargo.toml @@ -10,10 +10,13 @@ serde = { version = "1.0.183", features = ["derive"] } serde_json = "1.0.114" chrono = { version = "0.4.35", features = ["serde"] } tokio = { version = "1.30.0", features = ["full"] } +thiserror = "1.0.58" anyhow = "1.0.72" log = "0.4.19" env_logger = "0.10.0" reqwest = "0.11" +ring = "0.17.8" +hex = "0.4.3" [dev-dependencies] test-log = { version = "0.2.15", features = ["env_logger", "trace"] } diff --git a/ingestors/src/cve/cve_record/mod.rs b/ingestors/src/cve/cve_record/mod.rs index edff5663b..e4b607d11 100644 --- a/ingestors/src/cve/cve_record/mod.rs +++ b/ingestors/src/cve/cve_record/mod.rs @@ -1,2 +1 @@ - -pub mod v5; \ No newline at end of file +pub mod v5; diff --git a/ingestors/src/cve/cve_record/v5.rs b/ingestors/src/cve/cve_record/v5.rs index 1e7c95ac0..9714bb49b 100644 --- a/ingestors/src/cve/cve_record/v5.rs +++ b/ingestors/src/cve/cve_record/v5.rs @@ -1,8 +1,8 @@ -use serde::{Deserialize, Serialize}; use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all="camelCase")] +#[serde(rename_all = "camelCase")] pub struct CveRecord { pub data_type: String, pub data_version: String, @@ -11,14 +11,23 @@ pub struct CveRecord { } #[derive(Serialize, Deserialize, Debug)] -#[serde(tag = "state", rename_all="UPPERCASE")] +#[serde(tag = "state", rename_all = "UPPERCASE")] pub enum CveMetadata { Published(MetadataPublished), Rejected(MetadataRejected), } +impl CveMetadata { + pub fn cve_id(&self) -> &str { + match self { + CveMetadata::Published(inner) => &inner.cve_id, + CveMetadata::Rejected(inner) => &inner.cve_id, + } + } +} + #[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all="camelCase")] +#[serde(rename_all = "camelCase")] pub struct MetadataPublished { pub cve_id: String, pub assigner_org_id: String, @@ -31,7 +40,7 @@ pub struct MetadataPublished { } #[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all="camelCase")] +#[serde(rename_all = "camelCase")] pub struct MetadataRejected { pub cve_id: String, pub assigner_org_id: String, @@ -45,40 +54,40 @@ pub struct MetadataRejected { } #[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all="camelCase")] +#[serde(rename_all = "camelCase")] pub struct Containers { pub cna: CnaContainer, pub adp: Option, } #[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all="camelCase")] +#[serde(rename_all = "camelCase")] pub struct CnaContainer { pub descriptions: Vec, pub problem_types: Vec, } #[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all="camelCase")] +#[serde(rename_all = "camelCase")] pub struct AdpContainer { pub descriptions: Vec, } #[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all="camelCase")] +#[serde(rename_all = "camelCase")] pub struct Description { pub lang: String, pub value: String, } #[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all="camelCase")] +#[serde(rename_all = "camelCase")] pub struct ProblemType { pub descriptions: Vec, } #[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all="camelCase")] +#[serde(rename_all = "camelCase")] pub struct ProblemTypeDescription { pub lang: String, pub description: String, @@ -88,11 +97,11 @@ pub struct ProblemTypeDescription { #[cfg(test)] mod test { + use crate::cve::cve_record::v5::CveRecord; use std::fs::File; use std::path::PathBuf; use std::str::FromStr; use test_log::test; - use crate::cve::cve_record::v5::CveRecord; #[test(tokio::test)] async fn serde() -> Result<(), anyhow::Error> { @@ -103,16 +112,10 @@ mod test { let cve_file = File::open(cve_json)?; - let cve: CveRecord = serde_json::from_reader( - cve_file - )?; + let cve: CveRecord = serde_json::from_reader(cve_file)?; println!("{:#?}", cve); Ok(()) - - } - - -} \ No newline at end of file +} diff --git a/ingestors/src/cve/loader.rs b/ingestors/src/cve/loader.rs new file mode 100644 index 000000000..87b331b92 --- /dev/null +++ b/ingestors/src/cve/loader.rs @@ -0,0 +1,112 @@ +use crate::cve::cve_record::v5::CveRecord; +use crate::hashing::HashingRead; +use crate::Error; +use std::io::Read; +use trustify_graph::db::Transactional; +use trustify_graph::graph::Graph; + +/// Loader capable of parsing a CVE Record JSON file +/// and manipulating the Graph to integrate it into +/// the knowledge base. +/// +/// Should result in ensuring that a *vulnerability* +/// related to the CVE Record exists in the graph, _along with_ +/// also ensuring that the CVE *advisory* ends up also +/// in the graph. +pub struct CveLoader<'g> { + graph: &'g Graph, +} + +impl<'g> CveLoader<'g> { + pub fn new(graph: &'g Graph) -> Self { + Self { graph } + } + + pub async fn load, R: Read>( + &self, + location: L, + record: R, + ) -> Result<(), Error> { + let mut reader = HashingRead::new(record); + let cve: CveRecord = serde_json::from_reader(&mut reader)?; + + self.graph + .ingest_vulnerability(cve.cve_metadata.cve_id(), Transactional::None) + .await?; + + let hashes = reader.hashes(); + + let sha256 = hex::encode(hashes.sha256.as_ref()); + + self.graph + .ingest_advisory( + cve.cve_metadata.cve_id(), + location, + sha256, + Transactional::None, + ) + .await?; + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use crate::cve::loader::CveLoader; + use std::fs::File; + use std::path::PathBuf; + use std::str::FromStr; + use test_log::test; + use trustify_graph::db::Transactional; + use trustify_graph::graph::Graph; + + #[test(tokio::test)] + async fn cve_loader() -> Result<(), anyhow::Error> { + let graph = Graph::for_test("ingestors_cve_loader").await?; + + let pwd = PathBuf::from_str(env!("CARGO_MANIFEST_DIR"))?; + let test_data = pwd.join("../etc/test-data/mitre"); + + let cve_json = test_data.join("CVE-2024-28111.json"); + let cve_file = File::open(cve_json)?; + + let loaded_vulnerability = graph + .get_vulnerability("CVE-2024-28111", Transactional::None) + .await?; + + assert!(loaded_vulnerability.is_none()); + + let loaded_advisory = graph + .get_advisory( + "CVE-2024-28111", + "CVE-2024-28111.json", + "06908108e8097f2a56e628e7814a7bd54a5fc95f645b7c9fab02c1eb8dd9cc0c", + ) + .await?; + + assert!(loaded_advisory.is_none()); + + let loader = CveLoader::new(&graph); + + loader.load("CVE-2024-28111.json", cve_file).await?; + + let loaded_vulnerability = graph + .get_vulnerability("CVE-2024-28111", Transactional::None) + .await?; + + assert!(loaded_vulnerability.is_some()); + + let loaded_advisory = graph + .get_advisory( + "CVE-2024-28111", + "CVE-2024-28111.json", + "06908108e8097f2a56e628e7814a7bd54a5fc95f645b7c9fab02c1eb8dd9cc0c", + ) + .await?; + + assert!(loaded_advisory.is_some()); + + Ok(()) + } +} diff --git a/ingestors/src/cve/mod.rs b/ingestors/src/cve/mod.rs index 360185129..f705707b2 100644 --- a/ingestors/src/cve/mod.rs +++ b/ingestors/src/cve/mod.rs @@ -1,12 +1,4 @@ -use trustify_graph::graph::Graph; pub mod cve_record; +pub mod loader; -pub struct CveIngestor<'g> { - graph: &'g Graph, - -} - -impl<'g> CveIngestor<'g> { - -} \ No newline at end of file diff --git a/ingestors/src/hashing.rs b/ingestors/src/hashing.rs new file mode 100644 index 000000000..b4d4d0003 --- /dev/null +++ b/ingestors/src/hashing.rs @@ -0,0 +1,36 @@ +use ring::digest::Context; +use ring::digest::{Digest, SHA256}; +use std::io::Read; + +pub struct HashingRead { + inner: R, + sha256: Context, +} + +#[derive(Debug)] +pub struct Hashes { + pub sha256: Digest, +} + +impl HashingRead { + pub fn new(inner: R) -> Self { + Self { + inner, + sha256: Context::new(&SHA256), + } + } + + pub fn hashes(&self) -> Hashes { + Hashes { + sha256: self.sha256.clone().finish(), + } + } +} + +impl Read for &mut HashingRead { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let len = self.inner.read(buf)?; + self.sha256.update(&buf[0..len]); + Ok(len) + } +} diff --git a/ingestors/src/lib.rs b/ingestors/src/lib.rs index 88e63335b..ce523d9c0 100644 --- a/ingestors/src/lib.rs +++ b/ingestors/src/lib.rs @@ -1,3 +1,23 @@ +pub mod cve; +pub mod hashing; -pub mod cve; \ No newline at end of file +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Json(serde_json::Error), + #[error(transparent)] + Graph(trustify_graph::graph::error::Error), +} + +impl From for Error { + fn from(value: serde_json::Error) -> Self { + Self::Json(value) + } +} + +impl From for Error { + fn from(value: trustify_graph::graph::error::Error) -> Self { + Self::Graph(value) + } +} diff --git a/server/src/lib.rs b/server/src/lib.rs index 1644123dd..305bb5fb8 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -4,11 +4,11 @@ use actix_web::web; use std::process::ExitCode; use std::sync::Arc; use std::time::Duration; -use trustify_graph::graph::{DbStrategy, Graph}; use trustify_auth::auth::AuthConfigArguments; use trustify_auth::authenticator::Authenticator; use trustify_auth::authorizer::Authorizer; use trustify_common::config::Database; +use trustify_graph::graph::{DbStrategy, Graph}; use trustify_infrastructure::app::http::{HttpServerBuilder, HttpServerConfig}; use trustify_infrastructure::endpoint::Huevos; use trustify_infrastructure::health::checks::{Local, Probe}; @@ -143,7 +143,7 @@ pub fn configure(config: &mut web::ServiceConfig) { #[cfg(test)] mod test_util { use std::sync::Arc; - use trustify_api::graph::{DbStrategy, Graph}; + use trustify_graph::graph::{DbStrategy, Graph}; pub async fn bootstrap_system(name: &str) -> Result, anyhow::Error> { Graph::bootstrap( diff --git a/server/src/server/importer/mod.rs b/server/src/server/importer/mod.rs index b9bf3bc38..04179a8ac 100644 --- a/server/src/server/importer/mod.rs +++ b/server/src/server/importer/mod.rs @@ -6,7 +6,7 @@ mod test; use crate::server::importer::service::ImporterService; use actix_web::{delete, get, post, put, web, Responder}; use endpoints::*; -use trustify_api::graph::Graph; +use trustify_graph::graph::Graph; /// mount the "importer" module pub fn configure(svc: &mut web::ServiceConfig, graph: Graph) { diff --git a/server/src/server/importer/service.rs b/server/src/server/importer/service.rs index fc24032b5..22808e9c0 100644 --- a/server/src/server/importer/service.rs +++ b/server/src/server/importer/service.rs @@ -6,7 +6,7 @@ use parking_lot::RwLock; use serde_json::Value; use std::collections::btree_map::Entry; use std::collections::BTreeMap; -use trustify_api::graph::Graph; +use trustify_graph::graph::Graph; use trustify_common::error::ErrorInformation; #[derive(Debug, thiserror::Error)]