diff --git a/rust/doc/openapi.yml b/rust/doc/openapi.yml index 5b815c7f7..6e9b481a3 100644 --- a/rust/doc/openapi.yml +++ b/rust/doc/openapi.yml @@ -346,6 +346,19 @@ paths: description: "Schema of a list of results response" get results 0-3: $ref: "#/components/examples/scan_results" + "206": + description: "Partial results, most likely an error occurred while fetching the results" + content: + application/json: + schema: + type: "array" + items: + $ref: "#/components/schemas/Result" + examples: + schema: + description: "Schema of a list of results response" + get results 0-3: + $ref: "#/components/examples/scan_results" "400": description: "Bad range format" diff --git a/rust/src/openvasd/controller/entry.rs b/rust/src/openvasd/controller/entry.rs index fa81d1350..41b07c770 100644 --- a/rust/src/openvasd/controller/entry.rs +++ b/rust/src/openvasd/controller/entry.rs @@ -429,7 +429,23 @@ where }; match ctx.scheduler.get_results(&id, begin, end).await { - Ok(results) => Ok(ctx.response.ok_byte_stream(results).await), + Ok(results) => { + match ctx.scheduler.get_scan(&id).await { + Ok((_, status)) => { + let status_code = match status.status { + Phase::Failed => hyper::StatusCode::PARTIAL_CONTENT, + _ => hyper::StatusCode::OK, + }; + + Ok(ctx.response.byte_stream(status_code, results).await) + } + // ignore this error as it isn't directly related to the results + Err(e) => { + tracing::warn!("Error while fetching scan status: {:?}", e); + Ok(ctx.response.ok_byte_stream(results).await) + } + } + } Err(crate::storage::Error::NotFound) => { Ok(ctx.response.not_found("scans/results", &id)) } @@ -470,6 +486,7 @@ where pub mod client { use std::sync::Arc; + use http::StatusCode; use http_body_util::{BodyExt, Empty, Full}; use hyper::{ body::Bytes, header::HeaderValue, service::HttpService, HeaderMap, Method, Request, @@ -482,6 +499,7 @@ pub mod client { }; use serde::Deserialize; + use crate::storage::inmemory; use crate::{ controller::{ClientIdentifier, Context}, storage::{file::Storage, NVTStorer, UserNASLStorageForKBandVT}, @@ -506,11 +524,7 @@ pub mod client { >, FSPluginLoader, )>, - Arc< - UserNASLStorageForKBandVT< - crate::storage::inmemory::Storage, - >, - >, + Arc>>, > { use crate::file::tests::{example_feeds, nasl_root}; let storage = crate::storage::inmemory::Storage::default(); @@ -552,6 +566,25 @@ pub mod client { Client::authenticated(scanner, storage) } + pub async fn fails_to_fetch_results() -> Client< + scannerlib::scanner::fake::LambdaScanner, + Arc>>, + > { + // TODO: do we need to bother with the storage? + use crate::file::tests::example_feeds; + let storage = crate::storage::inmemory::Storage::default(); + let storage = Arc::new(UserNASLStorageForKBandVT::new(storage)); + storage + .synchronize_feeds(example_feeds().await) + .await + .unwrap(); + + let scanner = scannerlib::scanner::fake::LambdaScannerBuilder::new() + .with_fetch_results(|_| Err(scanner::Error::Unexpected("no results".to_string()))) + .build(); + Client::authenticated(scanner, storage) + } + pub async fn file_based_example_feed( prefix: &str, ) -> Client< @@ -648,7 +681,7 @@ pub mod client { let result = self .request_empty(Method::GET, KnownPaths::ScanStatus(id.to_string())) .await; - self.parsed(result).await + self.parsed(result, StatusCode::OK).await } pub async fn header(&self) -> TypeResult> { @@ -662,14 +695,18 @@ pub mod client { let result = self .request_empty(Method::GET, KnownPaths::Scans(Some(id.to_string()))) .await; - self.parsed(result).await + self.parsed(result, StatusCode::OK).await } - pub async fn scan_results(&self, id: &str) -> TypeResult> { + pub async fn scan_results( + &self, + id: &str, + status: StatusCode, + ) -> TypeResult> { let result = self .request_empty(Method::GET, KnownPaths::ScanResults(id.to_string(), None)) .await; - self.parsed(result).await + self.parsed(result, status).await } pub async fn scan_delete(&self, id: &str) -> TypeResult<()> { let result = self @@ -705,7 +742,7 @@ pub mod client { let result = self .request_empty(Method::GET, KnownPaths::Scans(None)) .await; - self.parsed(result).await + self.parsed(result, StatusCode::OK).await } // TODO: deal with that static stuff that prevents deserializiation based on Bytes @@ -731,12 +768,12 @@ pub mod client { let result = self .request_json(Method::POST, KnownPaths::Scans(None), scan) .await; - self.parsed(result).await + self.parsed(result, StatusCode::CREATED).await } pub async fn vts(&self) -> TypeResult> { let result = self.request_empty(Method::GET, KnownPaths::Vts(None)).await; - self.parsed(result).await + self.parsed(result, StatusCode::OK).await } /// Starts a scan and wait until is finished and returns it status and results @@ -770,14 +807,19 @@ pub mod client { } } - pub async fn parsed<'a, T>(&self, result: HttpResult) -> TypeResult + pub async fn parsed<'a, T>( + &self, + result: HttpResult, + expected_status: StatusCode, + ) -> TypeResult where T: for<'de> Deserialize<'de>, { let resp = result?; - if resp.status() != 200 && resp.status() != 201 { + if resp.status() != expected_status { return Err(scanner::Error::Unexpected(format!( - "Expected 200 for a body response but got {}", + "Expected {} for a body response but got {}", + expected_status, resp.status() ))); } @@ -792,6 +834,7 @@ pub mod client { #[cfg(test)] pub(super) mod tests { + use http::StatusCode; use scannerlib::models::{Scan, VT}; #[tokio::test] @@ -820,8 +863,25 @@ pub(super) mod tests { assert!(vts.len() > 2); let (id, status) = client.scan_finish(&scan).await.unwrap(); assert_eq!(status.status, scannerlib::models::Phase::Succeeded); - let results = client.scan_results(&id).await.unwrap(); + let results = client.scan_results(&id, StatusCode::OK).await.unwrap(); assert_eq!(3, results.len()); client.scan_delete(&id).await.unwrap(); } + + #[tokio::test] + #[tracing_test::traced_test] + async fn status_of_internal_error_should_be_reflects() { + let client = super::client::fails_to_fetch_results().await; + + let mut scan: Scan = Scan::default(); + scan.target.hosts.push("localhost".to_string()); + let (id, status) = client.scan_finish(&scan).await.unwrap(); + assert_eq!(status.status, scannerlib::models::Phase::Failed); + let results = client + .scan_results(&id, StatusCode::PARTIAL_CONTENT) + .await + .unwrap(); + assert_eq!(0, results.len()); + client.scan_delete(&id).await.unwrap(); + } } diff --git a/rust/src/openvasd/response.rs b/rust/src/openvasd/response.rs index b79d7e28c..26691be55 100644 --- a/rust/src/openvasd/response.rs +++ b/rust/src/openvasd/response.rs @@ -171,11 +171,11 @@ impl Response { } #[inline] - fn ok_json_response(&self, body: BodyKind) -> Result { + fn json_response(&self, status: hyper::StatusCode, body: BodyKind) -> Result { match self .default_response_builder() .header("Content-Type", "application/json") - .status(hyper::StatusCode::OK) + .status(status) .body(body) { Ok(resp) => resp, @@ -188,8 +188,9 @@ impl Response { } } } + #[inline] - pub async fn ok_byte_stream(&self, mut value: T) -> Result + pub async fn byte_stream(&self, status: hyper::StatusCode, mut value: T) -> Result where T: Iterator> + Send + 'static, { @@ -227,7 +228,15 @@ impl Response { tracing::debug!("end send values"); drop(tx); }); - self.ok_json_response(BodyKind::BinaryStream(rx)) + self.json_response(status, BodyKind::BinaryStream(rx)) + } + + #[inline] + pub async fn ok_byte_stream(&self, value: T) -> Result + where + T: Iterator> + Send + 'static, + { + self.byte_stream(hyper::StatusCode::OK, value).await } #[inline] @@ -281,7 +290,10 @@ impl Response { } pub fn ok_static(&self, value: &[u8]) -> Result { - self.ok_json_response(BodyKind::Binary(value.to_vec().into())) + self.json_response( + hyper::StatusCode::OK, + BodyKind::Binary(value.to_vec().into()), + ) } pub fn created(&self, value: &T) -> Result diff --git a/rust/src/openvasd/scheduling.rs b/rust/src/openvasd/scheduling.rs index d4357e083..2a1611b0b 100644 --- a/rust/src/openvasd/scheduling.rs +++ b/rust/src/openvasd/scheduling.rs @@ -265,7 +265,12 @@ where }; } Err(e) => { - tracing::warn!(%scan_id, %e, "unable to fetch results"); + // TODO: set scan to failed and inform entry to return 500 instead of 200 + // Also may remove from running + tracing::warn!(%scan_id, %e, "unable to fetch results, setting scan to failed"); + let mut status = self.db.get_status(&scan_id).await?; + status.status = Phase::Failed; + self.db.update_status(&scan_id, status).await?; } }; } diff --git a/rust/src/scanner/mod.rs b/rust/src/scanner/mod.rs index 087cb9362..29093dbf3 100644 --- a/rust/src/scanner/mod.rs +++ b/rust/src/scanner/mod.rs @@ -39,6 +39,165 @@ use crate::storage::{ContextKey, DefaultDispatcher}; use running_scan::{RunningScan, RunningScanHandle}; use scanner_stack::DefaultScannerStack; +// This is a fake implementation of the ScannerStack trait and is only used for testing purposes. +#[cfg(debug_assertions)] +pub mod fake { + use super::*; + + type StartScan = Arc Result<(), Error> + Send + Sync + 'static>>; + type CanStartScan = Arc bool + Send + Sync + 'static>>; + type StopScan = Arc Result<(), Error> + Send + Sync + 'static>>; + type DeleteScan = Arc Result<(), Error> + Send + Sync + 'static>>; + type FetchResults = Arc Result + Send + Sync + 'static>>; + + /// A fake implementation of the ScannerStack trait. + /// + /// This is useful for testing the Scanner implementation. + pub struct LambdaScannerBuilder { + start_scan: StartScan, + can_start_scan: CanStartScan, + stop_scan: StopScan, + delete_scan: DeleteScan, + fetch_results: FetchResults, + } + + impl Default for LambdaScannerBuilder { + fn default() -> Self { + Self::new() + } + } + + impl LambdaScannerBuilder { + pub fn new() -> Self { + Self { + start_scan: Arc::new(Box::new(|_| Ok(()))), + can_start_scan: Arc::new(Box::new(|_| true)), + stop_scan: Arc::new(Box::new(|_| Ok(()))), + delete_scan: Arc::new(Box::new(|_| Ok(()))), + fetch_results: Arc::new(Box::new(|_| Ok(ScanResults::default()))), + } + } + + pub fn with_start_scan(mut self, f: F) -> Self + where + F: Fn(Scan) -> Result<(), Error> + Send + Sync + 'static, + { + self.start_scan = Arc::new(Box::new(f)); + self + } + + pub fn with_can_start_scan(mut self, f: F) -> Self + where + F: Fn(&Scan) -> bool + Send + Sync + 'static, + { + self.can_start_scan = Arc::new(Box::new(f)); + self + } + + pub fn with_stop_scan(mut self, f: F) -> Self + where + F: Fn(&str) -> Result<(), Error> + Send + Sync + 'static, + { + self.stop_scan = Arc::new(Box::new(f)); + self + } + + pub fn with_delete_scan(mut self, f: F) -> Self + where + F: Fn(&str) -> Result<(), Error> + Send + Sync + 'static, + { + self.delete_scan = Arc::new(Box::new(f)); + self + } + + pub fn with_fetch_results(mut self, f: F) -> Self + where + F: Fn(&str) -> Result + Send + Sync + 'static, + { + self.fetch_results = Arc::new(Box::new(f)); + self + } + + pub fn build(self) -> LambdaScanner { + LambdaScanner { + start_scan: self.start_scan, + can_start_scan: self.can_start_scan, + stop_scan: self.stop_scan, + delete_scan: self.delete_scan, + fetch_results: self.fetch_results, + } + } + } + + pub struct LambdaScanner { + start_scan: StartScan, + can_start_scan: CanStartScan, + stop_scan: StopScan, + delete_scan: DeleteScan, + fetch_results: FetchResults, + } + + #[async_trait] + impl ScanStarter for LambdaScanner { + async fn start_scan(&self, scan: Scan) -> Result<(), Error> { + let start_scan = self.start_scan.clone(); + tokio::task::spawn_blocking(move || (start_scan)(scan)) + .await + .unwrap() + } + + async fn can_start_scan(&self, scan: &Scan) -> bool { + let can_start_scan = self.can_start_scan.clone(); + let scan = scan.clone(); + tokio::task::spawn_blocking(move || (can_start_scan)(&scan)) + .await + .unwrap() + } + } + + #[async_trait] + impl ScanStopper for LambdaScanner { + async fn stop_scan(&self, id: I) -> Result<(), Error> + where + I: AsRef + Send + 'static, + { + let stop_scan = self.stop_scan.clone(); + let id = id.as_ref().to_string(); + tokio::task::spawn_blocking(move || (stop_scan)(&id)) + .await + .unwrap() + } + } + + #[async_trait] + impl ScanDeleter for LambdaScanner { + async fn delete_scan(&self, id: I) -> Result<(), Error> + where + I: AsRef + Send + 'static, + { + let delete_scan = self.delete_scan.clone(); + let id = id.as_ref().to_string(); + tokio::task::spawn_blocking(move || (delete_scan)(&id)) + .await + .unwrap() + } + } + + #[async_trait] + impl ScanResultFetcher for LambdaScanner { + async fn fetch_results(&self, id: I) -> Result + where + I: AsRef + Send + 'static, + { + let fetch_results = self.fetch_results.clone(); + let id = id.as_ref().to_string(); + tokio::task::spawn_blocking(move || (fetch_results)(&id)) + .await + .unwrap() + } + } +} + /// Allows starting, stopping and managing the results of new scans. pub struct Scanner { running: Arc>>,