diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 738f10a311..5e3de229b7 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -193,6 +193,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + [[package]] name = "arcstr" version = "1.2.0" @@ -385,6 +394,92 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "axum-macros", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "axum-server" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1df331683d982a0b9492b38127151e6453639cd34926eb9c07d4cd8c6d22bfc" +dependencies = [ + "arc-swap", + "bytes", + "either", + "fs-err", + "http", + "http-body", + "hyper", + "hyper-util", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "base16ct" version = "1.0.0" @@ -1891,6 +1986,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a" +dependencies = [ + "autocfg", + "tokio", +] + [[package]] name = "fs2" version = "0.4.3" @@ -3031,6 +3136,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "md-5" version = "0.10.6" @@ -4870,6 +4981,8 @@ dependencies = [ "aes-gcm 0.10.3", "anyhow", "async-trait", + "axum", + "axum-server", "base64", "blowfish 0.9.1", "bzip2", @@ -4902,8 +5015,6 @@ dependencies = [ "http", "http-body", "http-body-util", - "hyper", - "hyper-util", "insta", "itertools 0.15.0", "lazy-regex", @@ -4949,8 +5060,6 @@ dependencies = [ "serde_json", "sha1 0.11.0", "sha2 0.11.0", - "signal-hook", - "signal-hook-tokio", "snmp2", "socket2", "sqlx", @@ -4962,6 +5071,8 @@ dependencies = [ "tokio", "tokio-rustls", "toml", + "tower", + "tower-service", "tracing", "tracing-appender", "tracing-subscriber", @@ -5168,6 +5279,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_spanned" version = "1.1.1" @@ -5300,16 +5422,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" -[[package]] -name = "signal-hook" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d" -dependencies = [ - "libc", - "signal-hook-registry", -] - [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -5320,18 +5432,6 @@ dependencies = [ "libc", ] -[[package]] -name = "signal-hook-tokio" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e513e435a8898a0002270f29d0a708b7879708fb5c4d00e46983ca2d2d378cf0" -dependencies = [ - "futures-core", - "libc", - "signal-hook", - "tokio", -] - [[package]] name = "signature" version = "2.2.0" @@ -6152,8 +6252,10 @@ dependencies = [ "pin-project-lite", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index e8398e9544..d1e7a89890 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -13,8 +13,6 @@ clap = { version = "4.6.1", features = ["derive", "env"] } futures = "0.3.32" http-body = "1" http-body-util = "0.1.3" -hyper = { version = "1", features = ["full"] } -hyper-util = { version = "0", features = ["tokio"] } insta = { version = "1.48.0", features = ["redactions", "ron", "toml"] } rustls = "0.23.41" serde = { version = "1.0", features = ["derive"] } @@ -34,6 +32,10 @@ once_cell = "1" aes = "0.8.4" aes-gcm = { version = "0.10.3" } async-trait = "0.1.89" +axum = { version = "0.8", features = ["json", "macros"] } +axum-server = { version = "0.8", features = ["tls-rustls"] } +tower = { version = "0.5.3", features = ["limit", "util"] } +tower-service = { version = "0.3.3" } base64 = "0.22.1" blowfish = "0.9.1" cbc = { version = "0.1.2", features = ["alloc"] } @@ -91,8 +93,6 @@ ripemd = "0.2.0" rsa = { version = "0.9.10", features = ["hazmat"] } russh = "0.61" rustls-native-certs = "0.8.4" -signal-hook-tokio = { version = "0.4.0", features = ["futures-v0_3"] } -signal-hook = "0.4.4" search_path = "0.1.4" sequoia-ipc = "0.36.1" sequoia-openpgp = { version = "2.3.0", default-features = false, features = [ @@ -116,8 +116,6 @@ clap = { workspace = true } futures = { workspace = true } http-body = { workspace = true } http-body-util = { workspace = true } -hyper = { workspace = true } -hyper-util = { workspace = true } rustls = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/rust/data/tests/scanner/config/basic_feed.toml b/rust/data/tests/scanner/config/basic_feed.toml index df27c8dc65..17793caafd 100644 --- a/rust/data/tests/scanner/config/basic_feed.toml +++ b/rust/data/tests/scanner/config/basic_feed.toml @@ -10,3 +10,6 @@ lock_file_dir = "/tmp/" [notus] products_path = 'examples/feed/notus/products' advisories_path = 'examples/feed/notus/advisories' + +[endpoints] +enable_get_scans = true diff --git a/rust/data/tests/scanner/config/cis_single_db_connection.toml b/rust/data/tests/scanner/config/cis_single_db_connection.toml index 7ed7f4b5a2..fa09bc76bd 100644 --- a/rust/data/tests/scanner/config/cis_single_db_connection.toml +++ b/rust/data/tests/scanner/config/cis_single_db_connection.toml @@ -7,3 +7,6 @@ max_connections = 1 [container_image_scanner.image] batch_size = 1 + +[endpoints] +enable_get_scans = true diff --git a/rust/src/openvasd/README.md b/rust/src/openvasd/README.md index 2489ebe04a..c1d64af17c 100644 --- a/rust/src/openvasd/README.md +++ b/rust/src/openvasd/README.md @@ -173,7 +173,7 @@ Options: --tls-client-certs path to client tls certs. Enables mtls. [env: TLS_CLIENT_CERTS=] --enable-get-scans [] - enable get scans endpoint. Default 'true'. [env: ENABLE_GET_SCANS=] [possible values: true, false] + enable get scans endpoint. Default 'false'. [env: ENABLE_GET_SCANS=] [possible values: true, false] --enable-get-performance [] enable get performance endpoint. Default 'false'. [env: ENABLE_GET_PERFORMANCE=] [possible values: true, false] --api-key diff --git a/rust/src/openvasd/api/auth.rs b/rust/src/openvasd/api/auth.rs new file mode 100644 index 0000000000..44c4a89210 --- /dev/null +++ b/rust/src/openvasd/api/auth.rs @@ -0,0 +1,246 @@ +//! [`axum`] [`Layer`] and [`Service`] for route access authorization. +//! +//! Performs authentication and authorization for a given authentication method, either `Mtls`, `ApiKey` or `Disabled`. +//! +//! # Example +//! ```rust +//! Router::new() +//! .nest( +//! "/unprotected", +//! Router::new() +//! .route("/foo", get("foo")) +//! .route("/bar", get("bar"))) +//! .nest( +//! "/unprotected", +//! Router::new() +//! .route("/", get("lorem")) +//! .route("/", get("ipsum")) +//! // enable authentication for all routes in this router: /unprotected/lorem and /unprotected/ipsum +//! .layer(crate::auth::AuthLayer::new(Authentication::ApiKey, Arc::new(vec!["api-keys"])))) +//! ``` +use axum::body::Body; +use axum::http::{Request, Response, StatusCode}; +use axum::response::IntoResponse; +use std::fmt::Display; +use std::pin::Pin; +use std::sync::Arc; +use std::task; +use tower::{Layer, Service}; + +/// Type for [`axum`] extension extraction +pub type ClientId = String; + +/// Enabled authentication method. +#[derive(Copy, Clone, Debug)] +#[allow(unused)] +pub enum Authentication { + Mtls, + ApiKey, + Disabled, +} + +impl Display for Authentication { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Mtls => write!(f, "mTLS"), + Self::ApiKey => write!(f, "apikey"), + Self::Disabled => write!(f, "disabled"), + } + } +} + +/// Enforces authentication for the affected routes. +#[derive(Clone)] +pub struct AuthLayer { + /// authentication method to use + auth_method: Authentication, + /// valid api-keys if the ApiKey method is set + api_keys: Arc>, +} + +impl AuthLayer { + /// Creates a new authentication layer for a given authentication method and key set. + pub fn new(auth_method: Authentication, api_keys: Arc>) -> AuthLayer { + AuthLayer { + auth_method, + api_keys, + } + } +} + +impl Layer for AuthLayer { + type Service = AuthService; + + fn layer(&self, inner: S) -> Self::Service { + AuthService { + inner, + auth_method: self.auth_method, + api_keys: self.api_keys.clone(), + } + } +} + +/// Route authentication [`Service`]. +/// +/// Supports authentication via either `mTLS` or `api keys`. In case of the former the actual authentication +/// occurs within the [`MtlsAcceptor`](super::mtls::MtlsAcceptor). According to the `mTLS` [rfc](https://datatracker.ietf.org/doc/html/rfc8705) +/// a connection has to be terminated if the given client certificates is *invalid*. This happens on the `session layer` +/// before any `HTTP` communication can happen, meaning that it would never reach the [`AuthService`]. +/// +/// An otherwise failed authentication results in a 401 Unauthorized response with an empty body. +/// +/// The service can only react to two `mTLS` cases: +/// * The given client certificates were valid +/// * No client certificates were given (which doesn't require a connection termination) +#[derive(Clone)] +pub struct AuthService { + inner: S, + /// authentication method to use + auth_method: Authentication, + /// valid api-keys if the ApiKey method is set + api_keys: Arc>, +} + +impl Service> for AuthService +where + S: Service, Response = Response> + Clone, + S::Response: IntoResponse, + S::Future: Send + 'static, +{ + type Response = S::Response; + type Error = S::Error; + type Future = + Pin> + Send + 'static>>; + + fn poll_ready(&mut self, ctx: &mut task::Context<'_>) -> task::Poll> { + self.inner.poll_ready(ctx) + } + + fn call(&mut self, mut req: Request) -> Self::Future { + let mut inner = self.inner.clone(); + + let client_id: Option = match self.auth_method { + // the actual authentication happens in the MtlsAcceptor + Authentication::Mtls => req + .extensions() + .get::() + .map(|x| x.to_string()), + // fetch the given api-key from the HTTP headers and compare against all valid keys + Authentication::ApiKey => req.headers().get("X-API-KEY").and_then(|key| { + key.to_str() + .ok() + .and_then(|key| self.api_keys.iter().find(|x| x == &key)) + .cloned() + }), + Authentication::Disabled => Some("unknown".to_string()), + }; + + if let Some(client_id) = client_id { + // insert the client_id which can be a hexadecimal string of the client certificate hash, + // the used api-key or "unknown" if authentication is disabled + req.extensions_mut().insert(client_id); + + Box::pin(inner.call(req)) + } else { + let response = Response::builder() + .status(StatusCode::UNAUTHORIZED) + .body(Body::from("")) + .expect("static unauthorized response"); + Box::pin(async { Ok(response) }) + } + } +} + +#[cfg(test)] +mod tests { + use axum::{ + body::Body, + http::{HeaderValue, Request, StatusCode}, + response::Response, + }; + use std::{convert::Infallible, sync::Arc}; + use tower::service_fn; + + use super::{AuthService, Authentication}; + use crate::api::{auth::ClientId, tests::send_request}; + + /// Tests that "unknown" is set as the client id if authentication is set to disabled. + #[tokio::test] + async fn auth_disabled_anon_id() -> anyhow::Result<()> { + // simulates a route handler + let inner = service_fn(|req: Request| async move { + let client_id = req.extensions().get::(); + assert_eq!(client_id, Some(&"unknown".to_string())); + Ok::(Response::new(Body::empty())) + }); + + let mut service = AuthService { + inner, + auth_method: Authentication::Disabled, + api_keys: Arc::new(vec![]), + }; + + let req = Request::new(Body::empty()); + let resp = send_request(&mut service, req).await; + assert!(resp.status().is_success()); + + Ok(()) + } + + /// Tests that the used api-key is set as the client id. + #[tokio::test] + async fn auth_api_key_id() -> anyhow::Result<()> { + // simulates a route handler + let inner = service_fn(|req: Request| async move { + let client_id = req.extensions().get::(); + assert_eq!(client_id, Some(&"secret2".to_string())); + Ok::(Response::new(Body::empty())) + }); + + let mut service = AuthService { + inner, + auth_method: Authentication::ApiKey, + api_keys: Arc::new(vec![ + "secret1".to_string(), + "secret2".to_string(), + "secret3".to_string(), + ]), + }; + + let req = Request::builder().body(Body::empty())?; + let resp = send_request(&mut service, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + + let req = Request::builder() + .header("X-Api-Key", HeaderValue::from_static("secret2")) + .body(Body::empty()) + .expect("valid HTTP request"); + let resp = send_request(&mut service, req).await; + assert!(resp.status().is_success()); + + Ok(()) + } + + /// Tests that only MTLS is enabled if both mtls and api keys are configured and no fallback occurs. + #[tokio::test] + async fn auth_no_fallback() -> anyhow::Result<()> { + // simulates a route handler + let inner = service_fn(|_: Request| async move { + Ok::(Response::new(Body::empty())) + }); + + let mut service = AuthService { + inner, + auth_method: Authentication::Mtls, + api_keys: Arc::new(vec!["secret1".to_string()]), + }; + + let req = Request::builder() + .header("X-Api-Key", HeaderValue::from_static("secret1")) + .body(Body::empty())?; + let resp = send_request(&mut service, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + + Ok(()) + } +} diff --git a/rust/src/openvasd/api/error.rs b/rust/src/openvasd/api/error.rs new file mode 100644 index 0000000000..874341df75 --- /dev/null +++ b/rust/src/openvasd/api/error.rs @@ -0,0 +1,156 @@ +//! Unified error handling within the API. +use std::error::Error; + +use crate::{ + api::response::{BadRequest, BodyKind}, + database::dao::{DAOError, DBViolation}, + scans::scheduling, + vts::orchestrator::WorkerError, +}; + +use axum::{ + body::Body, + extract::rejection::JsonRejection, + http::{StatusCode, header::CONTENT_TYPE}, + response::{IntoResponse, Response}, +}; +use scannerlib::{models, notus::NotusError}; +use serde::Serialize; +use thiserror::Error; +use tokio::sync::mpsc::error::SendError; + +/// Unified API error type. +#[derive(Error, Debug)] +pub enum ApiError { + #[error("database error: {0}")] + Database(#[from] DAOError), + #[error("notus error: {0}")] + Notus(#[from] NotusError), + #[error("scheduling error: {0}")] + Scheduling(#[from] SendError), + #[error("duplicate credentials")] + DuplicateCredentials(models::Service), + #[error("feed is not synced")] + FeedNotSynced, + #[error("scan still running")] + ScanRunning, + #[error("json error: {0}")] + Json(#[from] JsonRejection), + #[error("invalid user input: {0}")] + InvalidInput(String), + #[error("failed to fetch VT: {0}")] + VtsError(WorkerError), +} + +impl ApiError { + /// Transforms an ApiError into a HTTP StatusCode reflecting it's meaning. + pub fn status_code(&self) -> StatusCode { + match self { + ApiError::Database(e) => match e { + DAOError::DBViolation(_) => StatusCode::CONFLICT, + DAOError::NotFound => StatusCode::NOT_FOUND, + DAOError::Corrupt | DAOError::Infrastructure(_) => { + StatusCode::INTERNAL_SERVER_ERROR + } + }, + ApiError::Notus(e) => match e { + NotusError::UnknownProduct(_) => StatusCode::NOT_FOUND, + _ => StatusCode::INTERNAL_SERVER_ERROR, + }, + ApiError::Scheduling(_) => StatusCode::INTERNAL_SERVER_ERROR, + ApiError::DuplicateCredentials(_) => StatusCode::BAD_REQUEST, + ApiError::FeedNotSynced => StatusCode::SERVICE_UNAVAILABLE, + ApiError::ScanRunning => StatusCode::CONFLICT, + ApiError::Json(_) => StatusCode::BAD_REQUEST, + ApiError::InvalidInput(_) => StatusCode::BAD_REQUEST, + ApiError::VtsError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +/// JSON encoded error representation +#[derive(Serialize)] +pub struct ErrorJson { + status: u16, + message: String, + details: Option, +} + +impl From for ErrorJson { + fn from(value: ApiError) -> Self { + let status = value.status_code().as_u16(); + + let (message, details) = match value { + ApiError::Database(e) => match e { + DAOError::DBViolation(DBViolation::UniqueViolation) => { + ("Scan ID already in use".to_string(), None) + } + DAOError::NotFound => ("Entry not found".to_string(), None), + _ => ("Internal server error".to_string(), None), + }, + ApiError::Notus(NotusError::UnknownProduct(product)) => { + ("Unknown notus product".to_string(), Some(product)) + } + ApiError::Scheduling(_) | ApiError::VtsError(_) | ApiError::Notus(_) => { + ("Internal server error".to_string(), None) + } + ApiError::DuplicateCredentials(service) => ( + "Duplicate service credentials".to_string(), + Some(service.to_string()), + ), + ApiError::FeedNotSynced => ("Feed is not synced".to_string(), None), + ApiError::ScanRunning => ( + "Unable to perform action".to_string(), + Some("Scan is already running".to_string()), + ), + ApiError::Json(err) => ("Failed to process JSON".to_string(), Some(err.to_string())), + ApiError::InvalidInput(err) => ("Invalid query".to_string(), Some(err)), + }; + + Self { + status, + message, + details, + } + } +} + +/// Transforms an [`ApiError`] into an `HTTP` response. +impl IntoResponse for ApiError { + fn into_response(self) -> axum::response::Response { + tracing::warn!("{self}"); + + let resp = Response::builder() + .status(self.status_code()) + .header(CONTENT_TYPE, "application/json"); + + let err_json: ErrorJson = self.into(); + resp.body(Body::from( + serde_json::to_string(&err_json).expect("valid json"), + )) + .expect("valid response") + } +} + +impl From for BodyKind { + fn from(value: ApiError) -> Self { + match value { + ApiError::Json(JsonRejection::JsonSyntaxError(err)) => { + if let Some(e) = err.source() + && let Some(e) = e.downcast_ref::() + { + let br = BadRequest { + line: e.line(), + column: e.column(), + message: e.to_string(), + }; + + BodyKind::json_content(StatusCode::BAD_REQUEST, &br) + } else { + BodyKind::no_content(StatusCode::BAD_REQUEST) + } + } + _ => BodyKind::no_content(value.status_code()), + } + } +} diff --git a/rust/src/openvasd/api/mod.rs b/rust/src/openvasd/api/mod.rs new file mode 100644 index 0000000000..4b2e42837b --- /dev/null +++ b/rust/src/openvasd/api/mod.rs @@ -0,0 +1,257 @@ +//! Scanner HTTP REST API. +//! +//! This module contains all logic required to run an `axum` based HTTP API and communicate with the +//! `Scanner` and `Notus` to perform various scan operations. +use std::{fs, net::SocketAddr, path::PathBuf, sync::Arc}; + +use axum_server::{ + Handle, + tls_rustls::{RustlsAcceptor, RustlsConfig}, +}; +use libc::{SIGINT, SIGQUIT, SIGTERM}; +use mtls::MtlsAcceptor; +use rustls::{ + crypto::CryptoProvider, + pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject}, +}; +use tokio::{ + signal, + sync::{RwLock, oneshot}, +}; + +pub mod response; +pub use response::StreamResult; +pub mod states; +pub use auth::Authentication; + +mod auth; +pub mod error; +mod mtls; +mod router; +pub mod routes; + +/// The current API version. +pub const API_VERSION: &str = "1"; +pub type InternalIdentifier = String; + +/// Main API configuration. +pub struct ApiConfig { + /// Address to listen on. + pub address: SocketAddr, + /// Enabled authentication method, either `mTLS`, `api-key` or `disabled`. + pub auth_method: Authentication, + /// Server TLS certificate filepath. + pub server_cert_path: Option, + /// Server TLS key filepath. + pub server_key_file: Option, + /// Client mTLS certs or intermediary ca directory. + pub client_certs_path: Option, + /// Max concurrent requests. + pub max_requests: usize, + /// List of valid api keys. + pub api_keys: Arc>, + /// Feed database fetcher, redis or sqlite. + pub feed: states::Feed, + /// Scanner access point. + pub scanner: states::ScannerBridge, + /// Container image scanner access point. + pub image_scanner: states::ScannerBridge, + /// Notus products. + pub notus: Arc>, + /// Enables the `GET /scans` and `GET /container-image-scanner` routes. + pub enable_additional_routes: bool, +} + +/// Main API entry point. +/// +/// Configures the underlying TCP/TLS sockets and serves the REST API via [`axum`]. +pub async fn run(cfg: &ApiConfig) -> anyhow::Result { + // Spawns a task to listen for unix signals and perform graceful shutdowns + let (signal_tx, mut signal_rx) = oneshot::channel::(); + let shutdown_handle = Handle::default(); + let handle = shutdown_handle.clone(); + tokio::spawn(async move { + shutdown_signals(signal_tx).await; + handle.graceful_shutdown(None); + }); + + let app = router::create_router(cfg); + + match (cfg.server_cert_path.as_ref(), cfg.server_key_file.as_ref()) { + // Enable TLS if a certificate and key is present + (Some(cert_path), Some(key_path)) => { + let server_cert = CertificateDer::from_pem_file(cert_path)?; + let server_key = PrivateKeyDer::from_pem_file(key_path)?; + + if CryptoProvider::get_default().is_none() { + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + } + + let mut server_config = if let Some(client_certs_path) = cfg.client_certs_path.as_ref() + { + // Enable mTLS client authentication + let mut roots = rustls::RootCertStore::empty(); + + // Depending on how the client certificate has been created the directory has to + // contain either the used intermediary CA certificate or the client certificates. + let dir = fs::read_dir(client_certs_path)?; + for cert_file in dir.filter_map(|x| x.ok()) { + if let Ok(certs) = CertificateDer::pem_file_iter(cert_file.path()) { + roots.add_parsable_certificates(certs.collect::, _>>()?); + } + } + + let verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(roots)) + // Allow connections without client certificates. + // This pushes the authentication to the AuthService. + .allow_unauthenticated() + .build()?; + + rustls::ServerConfig::builder().with_client_cert_verifier(verifier) + } else { + // No mTLS + rustls::ServerConfig::builder().with_no_client_auth() + } + .with_single_cert(vec![server_cert], server_key)?; + + // Enable HTTP2 negotiation + server_config.alpn_protocols = vec![b"h2".to_vec()]; + + let rustls_config = RustlsConfig::from_config(Arc::new(server_config)); + + // Whether or not mTLS authentication is actually performed is configured in the + // rustls_config earlier. + let acceptor = MtlsAcceptor::new(RustlsAcceptor::new(rustls_config)); + + tracing::info!("listening on https://{}", cfg.address); + + // Start with tls and HTTP2 + axum_server::bind(cfg.address) + .acceptor(acceptor) + .handle(shutdown_handle) + .serve(app.into_make_service()) + .await?; + } + (None, None) => { + tracing::info!("listening on http://{}", cfg.address); + + // Start without tls and HTTP1.1 + axum_server::bind(cfg.address) + .handle(shutdown_handle) + .serve(app.into_make_service()) + .await?; + } + _ => { + panic!("both a server cert and key file are required"); + } + }; + + println!("waiting for signal"); + + // A non-error termination of axum_serve::serve can only be triggered by a signal + signal_rx + .try_recv() + // Signal based linux exit codes are defined as 128 + signal number + .map(|x| 128 + x) + .map_err(|e| e.into()) +} + +/// Intercepts and processes shutdown signals. +/// +/// Used in combination with a [`Handle`] to perform a graceful shutdown for the signals +/// `SIGINT`, `SIGTERM` and `SIGQUIT`. +/// +/// A special case is `SIGHUP`, which is triggered when the controlling terminal is closed. +/// Instead of causing a shutdown as well it is being ignored forcing the scanner to keep running. +async fn shutdown_signals(tx: oneshot::Sender) { + loop { + let sigint = async { + signal::unix::signal(signal::unix::SignalKind::interrupt()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + let sigterm = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + let sigquit = async { + signal::unix::signal(signal::unix::SignalKind::quit()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + let sighup = async { + signal::unix::signal(signal::unix::SignalKind::hangup()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + let signal = tokio::select! { + _ = sigint => { + tracing::info!(signal=SIGINT, "Exit based on signal."); + SIGINT + }, + _ = sigterm => { + tracing::info!(signal=SIGTERM, "Exit based on signal."); + SIGTERM + }, + _ = sigquit => { + tracing::info!(signal=SIGQUIT, "Exit based on signal."); + SIGQUIT + }, + // Ignore sighup and keep running + _ = sighup => { + tracing::info!("Ignoring SIGHUP signal."); + continue + } + }; + + // Send the triggered signal back to the main thread to set the correct exit code + tx.send(signal).expect("shutdown signal oneshot channel"); + + break; + } +} + +#[cfg(test)] +pub(super) mod tests { + use axum::{ + body::Body, + http::{Request, header::*}, + }; + + use std::fmt::Debug; + + use axum::response::Response; + use tower::{Service, ServiceExt}; + + pub async fn send_request(router: &mut S, request: Request) -> Response + where + S: Service, Response = Response>, + S::Error: Debug, + { + router.ready().await.unwrap().call(request).await.unwrap() + } + + pub fn json_request( + method: &str, + uri: &str, + payload: &T, + ) -> Request { + let body_bytes = serde_json::to_vec(payload).expect("Failed to serialize json payload"); + + Request::builder() + .method(method) + .uri(uri) + .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) + .body(Body::from(body_bytes)) + .expect("valid HTTP request") + } +} diff --git a/rust/src/openvasd/api/mtls.rs b/rust/src/openvasd/api/mtls.rs new file mode 100644 index 0000000000..ba5df86590 --- /dev/null +++ b/rust/src/openvasd/api/mtls.rs @@ -0,0 +1,130 @@ +//! `mTLS` [`Acceptor`](MtlsAcceptor) and [`Service`](MtlsService). +//! +//! Various parts required to communicate information about the `mTLS` handshake into the +//! application layer of axum. +use axum::http::{self, Request}; +use axum_server::{ + accept::{Accept, DefaultAcceptor}, + tls_rustls::RustlsAcceptor, +}; +use rustls::pki_types::CertificateDer; +use sha2::{Digest, Sha256}; +use std::{fmt::Display, pin::Pin}; +use std::{io, task}; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio_rustls::server::TlsStream; +use tower::Service; + +/// Sha256 hash for the clients `mTLS` certificate. +#[derive(Clone, Debug)] +pub struct MtlsCertHash(Option<[u8; 32]>); + +impl Display for MtlsCertHash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.0 { + Some(hash) => f.write_str(&hash.iter().map(|b| format!("{b:02x}")).collect::()), + None => f.write_str("unknown"), + } + } +} + +/// Wrapper around the default [`RustlsAcceptor`]. +/// +/// Extracts the clients `mTLS` peer certificate and injects a sha256 hash of it back into the +/// axum extensions via the [`MtlsService`]. +#[derive(Clone)] +pub struct MtlsAcceptor { + inner: RustlsAcceptor, +} + +impl MtlsAcceptor { + pub fn new(inner: RustlsAcceptor) -> MtlsAcceptor { + Self { inner } + } + + pub fn inner(&self) -> &RustlsAcceptor { + &self.inner + } + + /// Hashes a given peer certificate + fn hash(cert: &CertificateDer) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(cert); + hasher.finalize().into() + } +} + +impl Accept for MtlsAcceptor +where + A: Accept + Clone + Send + 'static, + A::Stream: AsyncRead + AsyncWrite + Unpin + Send, + A::Service: Send, + A::Future: Send, + I: Send + 'static, + S: Send + 'static, +{ + type Stream = TlsStream; + type Service = MtlsService; + + type Future = Pin> + Send>>; + + fn accept(&self, stream: I, service: S) -> Self::Future { + let inner = self.inner().clone(); + + Box::pin(async move { + // call the default RustlsAcceptor which also performs the mTLS handshake + let (stream, service) = inner.accept(stream, service).await?; + + // hash the clients peer certificate if one was given + let hash = match stream.get_ref().1.peer_certificates() { + Some(certs) if !certs.is_empty() => { + MtlsCertHash(Some(Self::hash(certs.first().expect("peer certificate")))) + } + _ => MtlsCertHash(None), + }; + + // pass the hash to the MtlsService which injects it into the axum extensions + Ok((stream, MtlsService::new(service, hash))) + }) + } +} + +/// Simple [`Service`] that injects the clients peer certificate hash into the axum extensions +/// exposing it for the [`AuthService`](super::auth::AuthService). +#[derive(Clone)] +pub struct MtlsService { + inner: S, + // sha256 hash of a clients valid mTLS peer certificate if provided + cert_hash: MtlsCertHash, +} + +impl MtlsService { + /// Creates a new MtlsService called by [`MtlsAcceptor`] + fn new(inner: S, cert_hash: MtlsCertHash) -> MtlsService { + MtlsService { inner, cert_hash } + } +} + +impl Service> for MtlsService +where + S: Service>, +{ + type Response = S::Response; + type Error = S::Error; + type Future = S::Future; + + fn poll_ready(&mut self, ctx: &mut task::Context<'_>) -> task::Poll> { + self.inner.poll_ready(ctx) + } + + fn call(&mut self, mut req: http::Request) -> Self::Future { + if self.cert_hash.0.is_some() { + tracing::debug!("client cert: {}", self.cert_hash); + } + + // insert the client hash + // used by the AuthService + req.extensions_mut().insert(self.cert_hash.clone()); + self.inner.call(req) + } +} diff --git a/rust/src/openvasd/greenbone_scanner_framework/entry/response/json_stream.rs b/rust/src/openvasd/api/response/json_stream.rs similarity index 96% rename from rust/src/openvasd/greenbone_scanner_framework/entry/response/json_stream.rs rename to rust/src/openvasd/api/response/json_stream.rs index a2680a2420..0aab7bf4af 100644 --- a/rust/src/openvasd/greenbone_scanner_framework/entry/response/json_stream.rs +++ b/rust/src/openvasd/api/response/json_stream.rs @@ -3,11 +3,10 @@ use std::{ task::{Context, Poll}, }; +use axum::body::Bytes; use futures::Stream; use futures_util::stream::StreamExt; -use crate::greenbone_scanner_framework::entry::Bytes; - #[derive(Copy, Clone)] enum JsonFramingState { Start, @@ -96,7 +95,7 @@ where mod tests { use futures::{StreamExt, stream}; - use crate::greenbone_scanner_framework::entry::Bytes; + use axum::body::Bytes; #[tokio::test] async fn json_serialization() { diff --git a/rust/src/openvasd/greenbone_scanner_framework/entry/response/mod.rs b/rust/src/openvasd/api/response/mod.rs similarity index 85% rename from rust/src/openvasd/greenbone_scanner_framework/entry/response/mod.rs rename to rust/src/openvasd/api/response/mod.rs index 2db8caa050..69fcb70d08 100644 --- a/rust/src/openvasd/greenbone_scanner_framework/entry/response/mod.rs +++ b/rust/src/openvasd/api/response/mod.rs @@ -1,3 +1,4 @@ +//! Streamed HTTP response types. mod json_stream; use std::{ convert::Infallible, @@ -5,16 +6,20 @@ use std::{ task::{Context, Poll}, }; +use axum::{ + body::Bytes, + http::{StatusCode, header}, + response::{IntoResponse, Response}, +}; use futures::{Stream, StreamExt, stream}; use http_body::{Body, Frame, SizeHint}; -use hyper::{StatusCode, body::Bytes}; pub struct BodyKind { - pub status_code: StatusCode, + status_code: StatusCode, pub content: BodyKindContent, } -/// Implements the hyper http body types +/// Implements the http body types pub enum BodyKindContent { /// Empty body Empty, @@ -35,10 +40,10 @@ pub struct BadRequest { macro_rules! internal_server_error { ($e:expr) => {{ tracing::warn!(error = %$e, "Unexpected error occurred"); - $crate::greenbone_scanner_framework::entry::response::BodyKind::no_content(hyper::StatusCode::INTERNAL_SERVER_ERROR) + $crate::api::response::BodyKind::no_content(axum::http::StatusCode::INTERNAL_SERVER_ERROR) }}; () => {{ - $crate::greenbone_scanner_framework::entry::response::BodyKind::no_content(hyper::StatusCode::INTERNAL_SERVER_ERROR) + $crate::api::response::BodyKind::no_content(axum::http::StatusCode::INTERNAL_SERVER_ERROR) }}; } @@ -178,3 +183,18 @@ impl Body for BodyKindContent { } } } + +impl IntoResponse for BodyKind { + fn into_response(self) -> Response { + let mut resp = Response::new(axum::body::Body::new(self.content)); + *resp.status_mut() = self.status_code; + + // this struct is only used for streamed results anymore which are all JSON encoded + resp.headers_mut().insert( + header::CONTENT_TYPE, + "application/json".parse().expect("valid HTTP header value"), + ); + + resp + } +} diff --git a/rust/src/openvasd/api/router.rs b/rust/src/openvasd/api/router.rs new file mode 100644 index 0000000000..18f99a3ba7 --- /dev/null +++ b/rust/src/openvasd/api/router.rs @@ -0,0 +1,194 @@ +//! [`axum`] based Scanner API router. +use axum::{ + Router, + body::Body, + extract::{DefaultBodyLimit, State}, + http::{Request, Response}, + middleware::{self, Next}, + routing::{get, head}, +}; +use tower::limit::ConcurrencyLimitLayer; + +use crate::api::states::Feed; + +use super::{auth::Authentication, routes::*}; +use scannerlib::models::FeedState; + +use super::ApiConfig; + +/// Injects default headers to all responses. +/// +/// This middleware adds three default headers to *every* HTTP response. This includes the 401 unauthorized +/// responses generated by the [`AuthService`](super::auth::AuthService) on failed authentications and +/// 404 not found for unregistered routes. +async fn default_headers( + State((authentication, feed)): State<(Authentication, Feed)>, + request: Request, + next: Next, +) -> Response { + let mut response = next.run(request).await; + + let headers = response.headers_mut(); + headers.insert( + "api-version", + crate::api::API_VERSION + .parse() + .expect("valid HTTP header value"), + ); + + // Authentication is the currently enabled authentication method + headers.insert( + "authentication", + authentication + .to_string() + .parse() + .unwrap_or_else(|_| panic!("'{}' is not a valid header value", authentication)), + ); + + let feed_state_str = match &*feed.state() { + FeedState::Unknown | FeedState::Syncing => "unavailable".to_string(), + FeedState::Synced(vt, adv) => format!("{vt}{adv}"), + }; + headers.insert( + "feed-version", + feed_state_str.parse().expect("valid HTTP header value"), + ); + + response +} + +/// Creates the api router. +/// +/// This is the main entry point for most axum functionalities, configuring all routes, +/// authentication, shared states and more based on the given [`ApiConfig`]. +/// +/// All routes with the `/scans` and `/container-image-scanner` prefix require authorization via either +/// mtls or api keys, with the former being preferred. The actual verification of the client certificates +/// is handled by [`MtlsAcceptor`](super::mtls::MtlsAcceptor) which, if a certificate is valid, +/// injects it's sh256 hash into the extensions of the [`Request`]. +/// +/// Unless specified otherwise the `HEAD` routes are auto generated by [`axum`] from their `GET` counterpart. +/// +/// | Method | Route | Authenticated | +/// |----------|---------------------------------------------------|---------------| +/// | `HEAD` | /health | | +/// | `GET` | /health/alive | | +/// | `GET` | /health/ready | | +/// | `GET` | /health/started | | +/// | `HEAD` | /scans | ✓ | +/// | `GET` | /scans* | ✓ | +/// | `POST` | /scans | ✓ | +/// | `GET` | /scans/preferences | ✓ | +/// | `GET` | /scans/{id} | ✓ | +/// | `POST` | /scans/{id} | ✓ | +/// | `DELETE` | /scans/{id} | ✓ | +/// | `GET` | /scans/{id}/results | ✓ | +/// | `GET` | /scans/{id}/results/{rid} | ✓ | +/// | `GET` | /scans/{id}/status | ✓ | +/// | `HEAD` | /container-image-scanner/scans | ✓ | +/// | `GET` | /container-image-scanner/scans* | ✓ | +/// | `POST` | /container-image-scanner/scans | ✓ | +/// | `GET` | /container-image-scanner/scans/preferences | ✓ | +/// | `GET` | /container-image-scanner/scans/{id} | ✓ | +/// | `POST` | /container-image-scanner/scans/{id} | ✓ | +/// | `DELETE` | /container-image-scanner/scans/{id} | ✓ | +/// | `GET` | /container-image-scanner/scans/{id}/results | ✓ | +/// | `GET` | /container-image-scanner/scans/{id}/results/{rid} | ✓ | +/// | `GET` | /container-image-scanner/scans/{id}/status | ✓ | +/// | `HEAD` | /vts | | +/// | `GET` | /vts | | +/// | `HEAD` | /notus | | +/// | `GET` | /notus | | +/// | `POST` | /notus/{os} | | +/// +/// \*: Only present if the `enable-get-scans` option is set to true. Default: true. +pub fn create_router(cfg: &ApiConfig) -> Router { + Router::new() + .nest( + // All health routes are currently NOPs + "/health", + Router::new() + .route("/", head(())) + .route("/alive", get(())) + .route("/ready", get(())) + .route("/started", get(())), + ) + .nest( + "/scans", + scans::router( + cfg.scanner.clone(), + cfg.auth_method, + cfg.api_keys.clone(), + cfg.enable_additional_routes, + ), + ) + .nest("/vts", vts::router(cfg.feed.clone())) + .nest("/notus", notus::router(cfg.notus.clone())) + .nest( + "/container-image-scanner/scans", + container_image_scanner::router( + cfg.image_scanner.clone(), + cfg.auth_method, + cfg.api_keys.clone(), + cfg.enable_additional_routes, + ), + ) + // Inject the api-version, feed-version and authentication headers into all responses + .layer(middleware::from_fn_with_state( + (cfg.auth_method, cfg.feed.clone()), + default_headers, + )) + // Limit of concurrent requests + .layer(ConcurrencyLimitLayer::new(cfg.max_requests)) + // Set the body payload limit to 32MiB (forms, json, etc). + // The default is 2MB + .layer(DefaultBodyLimit::max(32 * 1024 * 1024)) +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use crate::{ + Config, + api::{ApiConfig, tests::send_request}, + }; + use axum::{ + body::Body, + http::{HeaderValue, Request, StatusCode}, + }; + + async fn config() -> anyhow::Result { + let cfg = Config::from_file( + Path::new(env!("CARGO_MANIFEST_DIR")).join("data/tests/scanner/config/basic.toml"), + ); + crate::init_api(cfg).await + } + + /// Tests whether the three default headers are added to responses + #[tokio::test] + async fn default_headers() -> anyhow::Result<()> { + let cfg = config().await?; + let mut router = super::create_router(&cfg).into_service(); + + let req = Request::head("/health").body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let headers = resp.headers(); + assert_eq!( + headers.get("api-version"), + Some(&HeaderValue::from_str(crate::api::API_VERSION)?) + ); + assert_eq!( + headers.get("authentication"), + Some(&HeaderValue::from_str("disabled")?) + ); + assert_eq!( + headers.get("feed-version"), + Some(&HeaderValue::from_str("unavailable")?) + ); + + Ok(()) + } +} diff --git a/rust/src/openvasd/api/routes/container_image_scanner.rs b/rust/src/openvasd/api/routes/container_image_scanner.rs new file mode 100644 index 0000000000..aa7aa7f5ab --- /dev/null +++ b/rust/src/openvasd/api/routes/container_image_scanner.rs @@ -0,0 +1,509 @@ +//! `/container-image-scanner` routes. +#![allow(clippy::result_large_err)] + +use axum::{ + Extension, Json, Router, + extract::{Path, Query, State, rejection::JsonRejection}, + http::status::StatusCode, + response::IntoResponse, + routing::{delete, get, head, post}, +}; +use serde::Deserialize; + +use std::sync::Arc; + +use crate::api::{ + Authentication, + auth::{AuthLayer, ClientId}, + error::ApiError, + response::BodyKind, + states::ScannerBridge, +}; +use scannerlib::models::{self, PreferenceValue, ScanPreferenceInformation}; + +const PREFERENCES: [ScanPreferenceInformation; 2] = [ + ScanPreferenceInformation { + id: "accept_invalid_certs", + name: "Accepts certificates without trust chain verification", + default: PreferenceValue::Bool(true), + description: "This disables the CA chain verification for TLS certificates when connecting to a registry. \ + This is useful for self-signed certificates.", + }, + ScanPreferenceInformation { + id: "registry_allow_insecure", + name: "Use HTTP instead of HTTPS", + default: PreferenceValue::Bool(false), + description: "This allows unencrypted communication with an registry (HTTP instead of HTTPS).", + }, +]; + +/// Access point for all `/container-image-scanner` prefixed routes. +/// +/// Used by [crate::api::router::create_router] to build the HTTP API. +pub fn router( + scanner: ScannerBridge, + auth_method: Authentication, + api_keys: Arc>, + enable_additional_routes: bool, +) -> Router { + Router::new() + .route( + "/", + if enable_additional_routes { + // HEAD routes are auto generated from their GET counterpart + get(get_scans) + } else { + // add an explicit HEAD route if the GET route is disabled + head(()) + }, + ) + .route("/", post(post_scans)) + .route("/preferences", get(get_scans_preferences)) + .route("/{id}", get(get_scans_id)) + .route("/{id}", post(post_scans_id)) + .route("/{id}", delete(delete_scans_id)) + .route("/{id}/results", get(get_scans_id_results)) + .route("/{id}/results/{rid}", get(get_scans_id_results_rid)) + .route("/{id}/status", get(get_scans_id_status)) + // enable authentication for all routes + .layer(AuthLayer::new(auth_method, api_keys)) + .with_state(scanner) +} + +/// `GET /container-image-scanner/scans/preferences` route handler +/// +/// Authenticated: yes +/// +/// Returns `JSON` encoded list of all available scan preferences, which can be set within the scan +/// configuration via the POST /scans endpoint. +/// +/// ## Errors +/// * 401: unauthorized access +async fn get_scans_preferences() -> impl IntoResponse { + axum::Json::from(PREFERENCES) +} + +/// `GET /container-image-scanner/scans` route handler +/// +/// Authenticated: yes +/// +/// Returns a `JSON` encoded list of all scans belonging to the users client ID. +/// All passwords are redacted and it contains no status or result information. +/// +/// ## Errors +/// * 401: unauthorized access +/// * 405: `enable_get_scans` is set to false +/// * 500: internal error +async fn get_scans( + client_id: Extension, + State(scanner): State, +) -> impl IntoResponse { + BodyKind::from_result_stream( + StatusCode::OK, + scanner.get_scans(client_id.to_string()).await, + ) + .await +} + +/// `POST /container-image-scanner/scans` route handler +/// +/// Authenticated: yes +/// +/// Creates a new `scan` with a given `JSON` encoded configuration without starting it. On success +/// the `id` of the newly created scan is returned with a `201 CREATED` status code. +/// +/// ## Errors +/// * 400: invalid scan configuration +/// * 401: unauthorized access +/// * 409: a scan with the same `id` already exists +async fn post_scans( + client_id: Extension, + State(scanner): State, + scan: Result, JsonRejection>, +) -> Result { + let mut scan = scan?; + + // generate a random scan id if none was given + if scan.scan_id.is_empty() { + scan.scan_id = uuid::Uuid::new_v4().into(); + } + + scanner.post_scan(&client_id, &scan).await?; + Ok((StatusCode::CREATED, Json(scan.scan_id.clone())).into_response()) +} + +/// `GET /container-image-scanner/scans/{id}` route handler +/// +/// Authenticated: yes +/// +/// Provides `JSON` encoded information about a scan with a given `id` previously added by `POST /container-image-scanner/scans`. +/// All passwords are redacted and it contains no status or result information. +/// +/// ## Errors +/// * 401: unauthorized access +/// * 404: scan not found +async fn get_scans_id( + client_id: Extension, + Path(scan_id): Path, + State(scanner): State, +) -> Result { + let mut scan = scanner.get_scan(&client_id, &scan_id).await?; + + // hide passwords from the credentials + scan.target.credentials = scan + .clone() + .target + .credentials + .into_iter() + .map(|c| c.hide_pass()) + .collect(); + + Ok(Json(scan)) +} + +/// `POST /container-image-scanner/scans/{id}` route handler +/// +/// Authenticated: yes +/// +/// Performs an action on a scan, either starting or stopping it, that was previously stored by the user. +/// Returns an empty response with the status code `204 NO CONTENT`. +/// +/// ## Schema +/// ```json +///{ +/// "action": "stop|start" +///} +/// ``` +/// +/// ## Errors +/// * 400: invalid action user input +/// * 401: unauthorized access +/// * 500: internal error +async fn post_scans_id( + client_id: Extension, + Path(scan_id): Path, + State(scanner): State, + action: Result, JsonRejection>, +) -> Result { + scanner + .schedule_scan(&client_id, &scan_id, action?.action) + .await?; + + Ok(StatusCode::NO_CONTENT) +} + +/// `DELETE /container-image-scanner/scans/{id}` route handler +/// +/// Authenticated: yes +/// +/// Deletes a scan with a given ID. The scan cannot be deleted when it is currently running. +/// The current status can be retrieved via `GET /container-image-scanner/scans/{id}/status`. +/// +/// On success an empty response with the status code `204 NO CONTENT` is returned. +/// +/// ## Errors +/// * 401: unauthorized access +/// * 404: scan not found +/// * 409: scan is currently running +/// * 500: internal error +async fn delete_scans_id( + client_id: Extension, + Path(scan_id): Path, + State(scanner): State, +) -> Result { + let status = scanner.get_scan_status(&client_id, &scan_id).await?; + + // scan is not running + if !status.is_running() { + scanner.delete_scan(&client_id, &scan_id).await?; + Ok(StatusCode::NO_CONTENT) + } else { + Err(ApiError::ScanRunning) + } +} + +/// URL query parameter for /scans/{id}/results +#[derive(Deserialize)] +pub struct Params { + range: Option, +} + +/// `GET /container-image-scanner/scans/{id}/results` route handler +/// +/// Authenticated: yes +/// +/// Retrieves the results of a scan. If the scan is still running partial results are returned as +/// they are being generated. +/// +/// The results can be filtered using the `range` query by specifying a range or just a single +/// result index, for example range=3 or range=1-3 +/// +/// ## Errors +/// * 400: invalid range format +/// * 401: unauthorized access +/// * 404: scan not found +async fn get_scans_id_results( + client_id: Extension, + Path(scan_id): Path, + Query(params): Query, + State(scanner): State, +) -> Result { + let range = params + .range + .map(|x| super::scans::parse_results_range(&x)) + .transpose()?; + + let results = scanner + .get_scan_results( + &client_id, + &scan_id, + range.map(|(x, _)| x), + range.map(|(_, x)| x), + ) + .await?; + + Ok(BodyKind::from_result_stream(StatusCode::OK, results).await) +} + +/// `GET /container-image-scanner/scans/{id}/results/{rid}` route handler +/// +/// Authenticated: yes +/// +/// Retrieves a single specified `JSON` encoded result of a given scan. +/// +/// ## Errors +/// * 400: invalid ID +/// * 401: unauthorized access +/// * 404: scan not found +pub async fn get_scans_id_results_rid( + client_id: Extension, + Path((scan_id, result_id)): Path<(String, usize)>, + State(scanner): State, +) -> Result { + let result = scanner + .get_scan_result(&client_id, &scan_id, result_id) + .await?; + Ok(Json(result)) +} + +/// `GET /container-image-scanner/scans/{id}/status` route handler +/// +/// Authenticated: yes +/// +/// Provides `JSON` encoded status information about a scan with a given `id` previously added by +/// `POST /container-image/scanner/scans`. +/// +/// ## Schema +/// ```json +/// { +/// "start_time": 0, +/// "end_time": 0, +/// "status": "stored", +/// "host_info": { +/// "all": 0, +/// "excluded": 0, +/// "dead": 0, +/// "alive": 0, +/// "queued": 0, +/// "finished": 0 +/// } +/// } +/// ``` +/// +/// ## Errors +/// * 401: unauthorized access +/// * 404: scan couldn't be found +async fn get_scans_id_status( + client_id: Extension, + Path(scan_id): Path, + State(scanner): State, +) -> Result { + Ok(Json(scanner.get_scan_status(&client_id, &scan_id).await?)) +} + +#[cfg(test)] +mod tests { + use std::{path::Path, sync::Arc}; + + use crate::{ + Config, + api::{ + ApiConfig, Authentication, + tests::{json_request, send_request}, + }, + }; + use axum::{ + body::Body, + http::{Request, StatusCode}, + }; + use http::HeaderValue; + use http_body_util::BodyExt; + use scannerlib::models::Scan; + + async fn config() -> anyhow::Result { + let cfg = Config::from_file( + Path::new(env!("CARGO_MANIFEST_DIR")).join("data/tests/scanner/config/basic.toml"), + ); + crate::init_api(cfg).await + } + + /// Tests that all /container-image-scanner prefixed routes require authentication. + /// All requests are relative from the /container-image-scanner prefix. + #[tokio::test] + async fn scan_routes_unauth() -> anyhow::Result<()> { + let cfg = config().await?; + let mut router = super::router( + cfg.scanner, + Authentication::ApiKey, + Arc::new(vec!["secret".to_string()]), + true, + ) + .into_service(); + + // Authentication precedes the validation of the individual route input data, making them + // irrelevant for the sake of this test. + // + // The current implementation applies the authentication layer to the whole router which + // enforces authentication to ALL routes, but since this is security related and the + // implementation might change in the future each route is tested explicitly. + let req = Request::get("/").body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + + let req = Request::post("/").body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + + let req = Request::get("/preferences").body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + + let req = Request::get("/s1").body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + + let req = Request::post("/s1").body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + + let req = Request::delete("/s1").body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + + let req = Request::get("/s1/results").body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + + let req = Request::get("/s1/results/r1").body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + + let req = Request::get("/s1/status").body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + + Ok(()) + } + + /// Tests api-key authentication + #[tokio::test] + async fn scan_routes_api_key_authentication() -> anyhow::Result<()> { + let cfg = config().await?; + let mut router = super::router( + cfg.scanner, + Authentication::ApiKey, + Arc::new(vec!["secret1".to_string(), "secret2".to_string()]), + true, + ) + .into_service(); + + let req = Request::get("/") + .header("X-Api-Key", "secret1") + .body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = Request::get("/") + .header("X-Api-Key", "secret2") + .body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = Request::get("/") + .header("X-Api-Key", "invalid") + .body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + + Ok(()) + } + + /// Tests client data separation for the GET /container-image-scanner/scan route + #[tokio::test] + async fn get_scans_client_separation() -> anyhow::Result<()> { + let cfg = config().await?; + let mut router = super::router( + cfg.scanner, + Authentication::ApiKey, + Arc::new(vec!["client1".to_string(), "client2".to_string()]), + true, + ) + .into_service(); + + let scan1 = Scan { + scan_id: "scan1".to_string(), + ..Default::default() + }; + + // Since the request is router relative the real API url is POST /container-image-scanner/scans + let mut req = json_request("POST", "/", &scan1); + req.headers_mut() + .insert("X-Api-Key", HeaderValue::from_static("client1")); + + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::CREATED); + + let scan2 = Scan { + scan_id: "scan2".to_string(), + ..Default::default() + }; + + // Since the request is router relative the real API url is POST /container-image-scanner/scans + let mut req = json_request("POST", "/", &scan2); + req.headers_mut() + .insert("X-Api-Key", HeaderValue::from_static("client2")); + + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::CREATED); + + // Get client1s scans, again route relative + let req = Request::get("/") + .header("X-Api-Key", "client1") + .body(Body::empty())?; + + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let bytes = resp.into_body().collect().await?.to_bytes(); + let client1_scans: Vec<&str> = serde_json::from_slice(&bytes)?; + + // Must only return the scan added by client1 + assert_eq!(&client1_scans, &["scan1"]); + + // Get client2s scans, again route relative + let req = Request::get("/") + .header("X-Api-Key", "client2") + .body(Body::empty())?; + + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let bytes = resp.into_body().collect().await?.to_bytes(); + let client1_scans: Vec<&str> = serde_json::from_slice(&bytes)?; + + // Must only return the scan added by client2 + assert_eq!(&client1_scans, &["scan2"]); + + Ok(()) + } +} diff --git a/rust/src/openvasd/api/routes/mod.rs b/rust/src/openvasd/api/routes/mod.rs new file mode 100644 index 0000000000..01ed28d01a --- /dev/null +++ b/rust/src/openvasd/api/routes/mod.rs @@ -0,0 +1,7 @@ +//! Collection of HTTP sub-router for the API. +//! +//! Each module represents a path prefix within the REST API. +pub mod container_image_scanner; +pub mod notus; +pub mod scans; +pub mod vts; diff --git a/rust/src/openvasd/api/routes/notus.rs b/rust/src/openvasd/api/routes/notus.rs new file mode 100644 index 0000000000..fda76abf29 --- /dev/null +++ b/rust/src/openvasd/api/routes/notus.rs @@ -0,0 +1,60 @@ +//! `/notus` routes. +use axum::{ + Json, Router, + extract::{Path, State, rejection::JsonRejection}, + response::IntoResponse, + routing::{get, post}, +}; +use scannerlib::notus::Notus; +use tokio::sync::RwLock; + +use std::sync::Arc; + +use crate::api::error::ApiError; + +/// Access point for all `/notus` prefixed routes. +/// +/// Used by [crate::api::router::create_router] to build the HTTP API. +pub fn router(notus: Arc>) -> Router { + Router::new() + .route("/", get(get_notus)) + .route("/{os}", post(post_notus)) + .with_state(notus) +} + +/// GET /notus route handler +/// +/// Authenticated: no +/// +/// Returns a `JSON` encoded array of all supported operating systems. +/// +/// ## Errors +/// * 500: internal error +async fn get_notus( + State(notus): State>>, +) -> Result { + let result = notus.read_owned().await.get_available_os()?; + Ok(Json(result)) +} + +/// POST /notus/{os} route handler +/// +/// Authenticated: no +/// +/// Compares a given `JSON` encoded array of packages on a given OS against a list of known vulnerabilities. +/// On success a `JSON` encoded list of all vulnerable packages along with their fixed versions is returned. +/// +/// ## Errors +/// * 400: invalid `JSON` input data +/// * 404: unknown OS +/// * 500: internal error +async fn post_notus( + Path(os): Path, + State(notus): State>>, + packages: Result>, JsonRejection>, +) -> Result { + let mut notus = notus.write_owned().await; + + let result = notus.scan(&os, &packages?)?; + Ok(Json(result)) +} diff --git a/rust/src/openvasd/api/routes/scans.rs b/rust/src/openvasd/api/routes/scans.rs new file mode 100644 index 0000000000..79b6062908 --- /dev/null +++ b/rust/src/openvasd/api/routes/scans.rs @@ -0,0 +1,537 @@ +//! `/scans` routes. +#![allow(clippy::result_large_err)] + +use std::{collections::HashSet, sync::Arc}; + +use axum::{ + Extension, Json, Router, + extract::{Path, Query, State, rejection::JsonRejection}, + http::status::StatusCode, + response::IntoResponse, + routing::{delete, get, head, post}, +}; +use serde::Deserialize; + +use crate::api::{ + Authentication, + auth::{AuthLayer, ClientId}, + error::ApiError, + response::BodyKind, + states::ScannerBridge, +}; +use scannerlib::models; + +/// Access point for all `/scans` prefixed routes. +/// +/// Used by [crate::api::router::create_router] to build the HTTP API. +pub fn router( + scanner: ScannerBridge, + auth_method: Authentication, + api_keys: Arc>, + enable_additional_routes: bool, +) -> Router { + // All routes are relative to `/scans/` + Router::new() + .route( + "/", + if enable_additional_routes { + // HEAD routes are auto generated from their GET counterpart + get(get_scans) + } else { + // add an explicit HEAD route if the GET route is disabled + head(()) + }, + ) + .route("/", post(post_scans)) + .route("/preferences", get(get_scans_preferences)) + .route("/{id}", get(get_scans_id)) + .route("/{id}", post(post_scans_id)) + .route("/{id}", delete(delete_scans_id)) + .route("/{id}/results", get(get_scans_id_results)) + .route("/{id}/results/{rid}", get(get_scans_id_results_rid)) + .route("/{id}/status", get(get_scans_id_status)) + // enable authentication for all routes + .layer(AuthLayer::new(auth_method, api_keys)) + .with_state(scanner) +} + +/// `GET /scans/preferences` route handler. +/// +/// Authenticated: yes +/// +/// Returns `JSON` encoded list of all available scan preferences, which can be set within the scan +/// configuration via the POST /scans endpoint. +/// +/// ## Errors +/// * 401: unauthorized access +async fn get_scans_preferences() -> impl IntoResponse { + axum::Json::from(scannerlib::scanner::preferences::preference::PREFERENCES) +} + +/// `GET /scans` route handler. +/// +/// Authenticated: yes +/// +/// Returns a `JSON` encoded list of all scans belonging to the users client ID. +/// All passwords are redacted and it contains no status or result information. +/// +/// ## Errors +/// * 401: unauthorized access +/// * 405: `enable_get_scans` is set to false +/// * 500: internal error +async fn get_scans( + client_id: Extension, + State(scanner): State, +) -> impl IntoResponse { + BodyKind::from_result_stream( + StatusCode::OK, + scanner.get_scans(client_id.to_string()).await, + ) + .await +} + +/// `POST /scans` route handler. +/// +/// Authenticated: yes +/// +/// Creates a new `scan` with a given `JSON` encoded configuration without starting it. On success +/// the `id` of the newly created scan is returned with a `201 CREATED` status code. +/// +/// ## Errors +/// * 400: invalid scan configuration +/// * 401: unauthorized access +/// * 409: a scan with the same `id` already exists +async fn post_scans( + client_id: Extension, + State(scanner): State, + scan: Result, JsonRejection>, +) -> Result { + let mut scan = scan?; + + if let Some(dup) = get_duplicate_credential_service(&scan.target.credentials) { + return Err(ApiError::DuplicateCredentials(dup)); + } + + // generate a random scan id if none was given + if scan.scan_id.is_empty() { + scan.scan_id = uuid::Uuid::new_v4().into(); + } + + scanner.post_scan(&client_id, &scan).await?; + Ok((StatusCode::CREATED, Json(scan.scan_id.clone())).into_response()) +} + +/// `GET /scans/{id}` route handler. +/// +/// Authenticated: yes +/// +/// Provides `JSON` encoded information about a scan with a given `id` previously added by `POST /scans`. +/// All passwords are redacted and it contains no status or result information. +/// +/// ## Errors +/// * 401: unauthorized access +/// * 404: scan not be found +async fn get_scans_id( + client_id: Extension, + Path(scan_id): Path, + State(scanner): State, +) -> Result { + let mut scan = scanner.get_scan(&client_id, &scan_id).await?; + + // hide passwords from the credentials + scan.target.credentials = scan + .clone() + .target + .credentials + .into_iter() + .map(|c| c.hide_pass()) + .collect(); + + Ok(Json(scan)) +} + +/// `POST /scans/{id}` route handler. +/// +/// Authenticated: yes +/// +/// Performs an action on a scan, either starting or stopping it, that was previously stored by the user. +/// Returns an empty response with the status code `204 NO CONTENT`. +/// +/// ## Schema +/// ```json +///{ +/// "action": "stop|start" +///} +/// ``` +/// +/// ## Errors +/// * 400: invalid action user input +/// * 401: unauthorized access +/// * 500: internal error +async fn post_scans_id( + client_id: Extension, + Path(scan_id): Path, + State(scanner): State, + action: Result, JsonRejection>, +) -> Result { + scanner + .schedule_scan(&client_id, &scan_id, action?.action) + .await?; + + // maybe change to 202 Accepted instead + Ok(StatusCode::NO_CONTENT) +} + +/// `DELETE /scans/{id}` route handler. +/// +/// Authenticated: yes +/// +/// Deletes a scan with a given ID. The scan cannot be deleted when it is currently running. +/// The current status can be retrieved via `GET /scans/{id}/status`. +/// +/// On success an empty response with the status code `204 NO CONTENT` is returned. +/// +/// ## Errors +/// * 401: unauthorized access +/// * 404: scan not found +/// * 409: scan is currently running +/// * 500: internal error +async fn delete_scans_id( + client_id: Extension, + Path(scan_id): Path, + State(scanner): State, +) -> Result { + let status = scanner.get_scan_status(&client_id, &scan_id).await?; + + // scan is not running + if !status.is_running() { + scanner.delete_scan(&client_id, &scan_id).await?; + Ok(StatusCode::NO_CONTENT) + } else { + Err(ApiError::ScanRunning) + } +} + +/// URL query parameter for /scans/{id}/results. +#[derive(Deserialize)] +struct Params { + range: Option, +} + +/// `GET /scans/{id}/results` route handler. +/// +/// Authenticated: yes +/// +/// Retrieves the results of a scan. If the scan is still running partial results are returned as +/// they are being generated. +/// +/// The results can be filtered using the `range` query by specifying a range or just a single +/// result index, for example `range=3` or `range=1-3`. +/// +/// ## Errors +/// * 400: invalid range format +/// * 401: unauthorized access +/// * 404: scan not found +async fn get_scans_id_results( + client_id: Extension, + Path(scan_id): Path, + Query(params): Query, + State(scanner): State, +) -> Result { + let range = params.range.map(|x| parse_results_range(&x)).transpose()?; + + let results = scanner + .get_scan_results( + &client_id, + &scan_id, + range.map(|(x, _)| x), + range.map(|(_, x)| x), + ) + .await?; + + Ok(BodyKind::from_result_stream(StatusCode::OK, results).await) +} + +/// `GET /scans/{id}/results/{rid}` route handler. +/// +/// Authenticated: yes +/// +/// Retrieves a single specified `JSON` encoded result of a given scan. +/// +/// ## Errors +/// * 400: invalid ID +/// * 401: unauthorized access +/// * 404: scan not found +async fn get_scans_id_results_rid( + client_id: Extension, + Path((scan_id, result_id)): Path<(String, usize)>, + State(scanner): State, +) -> Result { + let result = scanner + .get_scan_result(&client_id, &scan_id, result_id) + .await?; + + Ok(Json(result)) +} + +/// `GET /scans/{id}/status` route handler. +/// +/// Authenticated: yes +/// +/// Provides `JSON` encoded status information about a scan with a given `id` previously added by `POST /scans`. +/// +/// ## Schema +/// ```json +///{ +/// "start_time": 0, +/// "end_time": 0, +/// "status": "stored", +/// "host_info": { +/// "all": 0, +/// "excluded": 0, +/// "dead": 0, +/// "alive": 0, +/// "queued": 0, +/// "finished": 0 +/// } +/// } +/// ``` +/// +/// ## Errors +/// * 401: unauthorized access +/// * 404: scan couldn't be found +async fn get_scans_id_status( + client_id: Extension, + Path(scan_id): Path, + State(scanner): State, +) -> Result { + Ok(Json(scanner.get_scan_status(&client_id, &scan_id).await?)) +} + +/// Finds and returns duplicate credentials for a single service. +pub(super) fn get_duplicate_credential_service( + credentials: &[models::Credential], +) -> Option { + let mut map: HashSet<_> = HashSet::default(); + for c in credentials.iter() { + if !map.insert(c.service.to_string()) { + return Some(c.service.clone()); + } + } + None +} + +/// Parses a string either containing either a single number or into an inclusive numerical range. +/// +/// ## Examples +/// * `3` becomes the start and end values `3 and 3` +/// * `1 2` becomes `1 and 2` respectively +pub(super) fn parse_results_range(range: &str) -> Result<(usize, usize), ApiError> { + if let Some((start, end)) = range.split_once('-') { + if let Ok(start) = start.parse::() + && let Ok(end) = end.parse::() + { + Ok((start, end)) + } else { + Err(ApiError::InvalidInput( + "\"{range}\" is not a valid range".to_string(), + )) + } + } else if let Ok(idx) = range.parse::() { + Ok((idx, idx)) + } else { + Err(ApiError::InvalidInput( + "\"{range}\" is not a valid range".to_string(), + )) + } +} + +#[cfg(test)] +mod tests { + use std::{path::Path, sync::Arc}; + + use crate::{ + Config, + api::{ + ApiConfig, Authentication, routes, + tests::{json_request, send_request}, + }, + }; + use axum::{ + body::Body, + http::{Request, StatusCode}, + }; + use http::HeaderValue; + use http_body_util::BodyExt; + use scannerlib::models::Scan; + + async fn config() -> anyhow::Result { + let cfg = Config::from_file( + Path::new(env!("CARGO_MANIFEST_DIR")).join("data/tests/scanner/config/basic.toml"), + ); + crate::init_api(cfg).await + } + + /// Tests that all /scans prefixed routes require authentication. + /// All requests are relative from the /scans prefix. + #[tokio::test] + async fn scan_routes_unauth() -> anyhow::Result<()> { + let cfg = config().await?; + let mut router = super::router( + cfg.scanner, + Authentication::ApiKey, + Arc::new(vec!["secret".to_string()]), + true, + ) + .into_service(); + + // Authentication precedes the validation of the individual route input data, making them + // irrelevant for the sake of this test. + // + // The current implementation applies the authentication layer to the whole router which + // enforces authentication to ALL routes, but since this is security related and the + // implementation might change in the future each route is tested explicitly. + let req = Request::get("/").body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + + let req = Request::post("/").body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + + let req = Request::get("/preferences").body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + + let req = Request::get("/s1").body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + + let req = Request::post("/s1").body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + + let req = Request::delete("/s1").body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + + let req = Request::get("/s1/results").body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + + let req = Request::get("/s1/results/r1").body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + + let req = Request::get("/s1/status").body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + + Ok(()) + } + + /// Tests api-key authentication. + /// + /// While this test covers the usage of one out of multiple possible valid `api-keys`, it is + /// currently not possible to set more than a single `api-key` via the config or cmdline arguments. + #[tokio::test] + async fn scan_routes_api_key_authentication() -> anyhow::Result<()> { + let cfg = config().await?; + let mut router = super::router( + cfg.scanner, + Authentication::ApiKey, + Arc::new(vec!["secret1".to_string(), "secret2".to_string()]), + true, + ) + .into_service(); + + let req = Request::get("/") + .header("X-Api-Key", "secret1") + .body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = Request::get("/") + .header("X-Api-Key", "secret2") + .body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = Request::get("/") + .header("X-Api-Key", "invalid") + .body(Body::empty())?; + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + + Ok(()) + } + + /// Tests client data separation for the GET /scan route + #[tokio::test] + async fn get_scans_client_separation() -> anyhow::Result<()> { + let cfg = config().await?; + let mut router = routes::scans::router( + cfg.scanner, + Authentication::ApiKey, + Arc::new(vec!["client1".to_string(), "client2".to_string()]), + true, + ) + .into_service(); + + let scan1 = Scan { + scan_id: "scan1".to_string(), + ..Default::default() + }; + + // Since the request is router relative the real API url is POST /scans + let mut req = json_request("POST", "/", &scan1); + req.headers_mut() + .insert("X-Api-Key", HeaderValue::from_static("client1")); + + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::CREATED); + + let scan2 = Scan { + scan_id: "scan2".to_string(), + ..Default::default() + }; + + // Since the request is router relative the real API url is POST /scans + let mut req = json_request("POST", "/", &scan2); + req.headers_mut() + .insert("X-Api-Key", HeaderValue::from_static("client2")); + + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::CREATED); + + // Get client1s scans, again route relative + let req = Request::get("/") + .header("X-Api-Key", "client1") + .body(Body::empty())?; + + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let bytes = resp.into_body().collect().await?.to_bytes(); + let client1_scans: Vec<&str> = serde_json::from_slice(&bytes)?; + + // Must only return the scan added by client1 + assert_eq!(&client1_scans, &["scan1"]); + + // Get client2s scans, again route relative + let req = Request::get("/") + .header("X-Api-Key", "client2") + .body(Body::empty())?; + + let resp = send_request(&mut router, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let bytes = resp.into_body().collect().await?.to_bytes(); + let client1_scans: Vec<&str> = serde_json::from_slice(&bytes)?; + + // Must only return the scan added by client2 + assert_eq!(&client1_scans, &["scan2"]); + + Ok(()) + } +} diff --git a/rust/src/openvasd/api/routes/vts.rs b/rust/src/openvasd/api/routes/vts.rs new file mode 100644 index 0000000000..72648bfe8f --- /dev/null +++ b/rust/src/openvasd/api/routes/vts.rs @@ -0,0 +1,25 @@ +//! `/vts` routes. +use axum::{ + Router, extract::State, http::status::StatusCode, response::IntoResponse, routing::get, +}; + +use crate::api::{error::ApiError, response::BodyKind, states::Feed}; + +/// Access point for all `/vts` prefixed routes. +/// +/// Used by [crate::api::router::create_router] to build the HTTP API. +pub fn router(feed: Feed) -> Router { + Router::new().route("/", get(get_vts)).with_state(feed) +} + +/// `GET /vts` route handler. +/// +/// Authenticated: no +/// +/// Returns a streamed response containing a `JSON` encoded array of `OIDs`. +/// +/// ## Errors +/// * 503: feed state is unknown or unsynced +async fn get_vts(State(feed): State) -> Result { + Ok(BodyKind::from_result_stream(StatusCode::OK, feed.get_oids()?).await) +} diff --git a/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans__tests__get_scans.snap b/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans__tests__get_scans.snap new file mode 100644 index 0000000000..47dd7859d4 --- /dev/null +++ b/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans__tests__get_scans.snap @@ -0,0 +1,6 @@ +--- +source: src/openvasd/api/get_scans.rs +assertion_line: 180 +expression: resp +--- +[""] diff --git a/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans_id__tests__get_scans.snap b/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans_id__tests__get_scans.snap similarity index 82% rename from rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans_id__tests__get_scans.snap rename to rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans_id__tests__get_scans.snap index d7bc0877db..4a72bbd9e5 100644 --- a/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans_id__tests__get_scans.snap +++ b/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans_id__tests__get_scans.snap @@ -1,5 +1,5 @@ --- -source: src/openvasd/greenbone-scanner-framework/get_scans_id.rs +source: src/openvasd/api/get_scans_id.rs expression: resp --- {"scan_id":"2689367b205c16ce32ed4200942b8b8b1e262dfc70d9bc9fbc77c49699a4f1df","target":{"hosts":[],"ports":[],"excluded_hosts":[],"credentials":[],"alive_test_ports":[],"alive_test_methods":[],"reverse_lookup_unify":null,"reverse_lookup_only":null},"scan_preferences":[],"vts":[]} diff --git a/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results__tests__scan_results.snap b/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results__tests__scan_results.snap new file mode 100644 index 0000000000..f427319709 --- /dev/null +++ b/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results__tests__scan_results.snap @@ -0,0 +1,5 @@ +--- +source: src/openvasd/api/get_scans_id_results.rs +expression: resp +--- +[] diff --git a/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results__tests__scan_results_from.snap b/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results__tests__scan_results_from.snap similarity index 60% rename from rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results__tests__scan_results_from.snap rename to rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results__tests__scan_results_from.snap index b773eaa7e3..436684c6c7 100644 --- a/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results__tests__scan_results_from.snap +++ b/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results__tests__scan_results_from.snap @@ -1,5 +1,5 @@ --- -source: src/openvasd/greenbone-scanner-framework/get_scans_id_results.rs +source: src/openvasd/api/get_scans_id_results.rs expression: resp --- [ diff --git a/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results__tests__scan_results_from_to.snap b/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results__tests__scan_results_from_to.snap similarity index 98% rename from rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results__tests__scan_results_from_to.snap rename to rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results__tests__scan_results_from_to.snap index 0db1de6bfd..850e24f8d3 100644 --- a/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results__tests__scan_results_from_to.snap +++ b/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results__tests__scan_results_from_to.snap @@ -1,5 +1,5 @@ --- -source: src/openvasd/greenbone-scanner-framework/get_scans_id_results.rs +source: src/openvasd/api/get_scans_id_results.rs expression: resp --- [ diff --git a/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results_id__tests__get_status.snap b/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results_id__tests__get_status.snap new file mode 100644 index 0000000000..e2e814ba0a --- /dev/null +++ b/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results_id__tests__get_status.snap @@ -0,0 +1,5 @@ +--- +source: src/openvasd/api/get_scans_id_results_id.rs +expression: resp +--- +{"id":42,"type":"log"} diff --git a/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_status__tests__get_status.snap b/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_status__tests__get_status.snap similarity index 57% rename from rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_status__tests__get_status.snap rename to rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_status__tests__get_status.snap index f66884e77f..376662e63d 100644 --- a/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_status__tests__get_status.snap +++ b/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_status__tests__get_status.snap @@ -1,5 +1,5 @@ --- -source: src/openvasd/greenbone-scanner-framework/get_scans_id_status.rs +source: src/openvasd/api/get_scans_id_status.rs expression: resp --- {"start_time":null,"end_time":null,"status":"stored","host_info":null} diff --git a/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans_preferences__tests__get_scans_preferences.snap b/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans_preferences__tests__get_scans_preferences.snap new file mode 100644 index 0000000000..afbcf24f4f --- /dev/null +++ b/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_scans_preferences__tests__get_scans_preferences.snap @@ -0,0 +1,5 @@ +--- +source: src/openvasd/api/get_scans_preferences.rs +expression: resp +--- +[] diff --git a/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_vts__tests__get_vts_authenticated.snap b/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_vts__tests__get_vts_authenticated.snap similarity index 61% rename from rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_vts__tests__get_vts_authenticated.snap rename to rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_vts__tests__get_vts_authenticated.snap index fbf8dac957..b82ca66c96 100644 --- a/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_vts__tests__get_vts_authenticated.snap +++ b/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_vts__tests__get_vts_authenticated.snap @@ -1,5 +1,5 @@ --- -source: src/openvasd/greenbone-scanner-framework/get_vts.rs +source: src/openvasd/api/get_vts.rs expression: resp --- ["2689367b205c16ce32ed4200942b8b8b1e262dfc70d9bc9fbc77c49699a4f1df"] diff --git a/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_vts__tests__get_vts_unauthenticated.snap b/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_vts__tests__get_vts_unauthenticated.snap new file mode 100644 index 0000000000..58e264e8ca --- /dev/null +++ b/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__get_vts__tests__get_vts_unauthenticated.snap @@ -0,0 +1,5 @@ +--- +source: src/openvasd/api/get_vts.rs +expression: resp +--- +["unknown"] diff --git a/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__post_scans__tests__post_scans.snap b/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__post_scans__tests__post_scans.snap new file mode 100644 index 0000000000..c0f29bc0ca --- /dev/null +++ b/rust/src/openvasd/api/snapshots/openvasd__greenbone_scanner_framework__post_scans__tests__post_scans.snap @@ -0,0 +1,5 @@ +--- +source: src/openvasd/api/post_scans.rs +expression: resp +--- +"response_id" diff --git a/rust/src/openvasd/api/states.rs b/rust/src/openvasd/api/states.rs new file mode 100644 index 0000000000..d45bd96f07 --- /dev/null +++ b/rust/src/openvasd/api/states.rs @@ -0,0 +1,255 @@ +//! Application states held by the API. +//! +//! This module contains access points for all stateful operations that either require the +//! [`Feed`] or the [`Scanner`](ScannerBridge) exposed to the API endpoints. +#![allow(clippy::result_large_err)] +use std::sync::{Arc, RwLock, RwLockReadGuard}; + +use crate::api::error::ApiError; +use crate::container_image_scanner::DBScan; +use crate::database::dao::{DAOError, Execute, Fetch, RetryExec, StreamFetch}; +use crate::database::sqlite::results::DBResults; +use crate::database::sqlite::scans::ScanDB; +use crate::scans::scheduling::{self, Message}; +use crate::vts::PluginFetcher; +use crate::{crypt::ChaCha20Crypt, database::sqlite::DataBase}; + +use super::StreamResult; +use futures::{StreamExt, TryStreamExt}; +use scannerlib::models::{self, FeedState, Phase, VTData}; +use tokio::sync::mpsc::Sender; + +/// Feed access point. +#[derive(Clone)] +pub struct Feed { + /// Storage independent plugin fetcher. + fetcher: Arc>, + /// Current feed state. + feed_state: Arc>, +} + +impl Feed { + pub fn new(fetcher: F, feed_state: Arc>) -> Self + where + F: PluginFetcher + Send + Sync + 'static, + { + Self { + fetcher: Arc::new(Box::new(fetcher)), + feed_state, + } + } + + /// Fetches all oids present in the feed and returns it as a JsonStream. + pub fn get_oids(&self) -> Result, ApiError> { + if matches!(*self.state(), FeedState::Synced(_, _)) { + Ok(Box::pin( + self.fetcher + .get_oids() + .map(|x| x.map_err(ApiError::VtsError)), + )) + } else { + Err(ApiError::FeedNotSynced) + } + } + + /// Fetches all vts present in the feed and returns it as a JsonStream. + pub fn get_vts(&self) -> Result, ApiError> { + if matches!(*self.state(), FeedState::Synced(_, _)) { + Ok(Box::pin( + self.fetcher + .get_vts() + .map(|x| x.map_err(ApiError::VtsError)), + )) + } else { + Err(ApiError::FeedNotSynced) + } + } + + /// Reads the current feed state. + pub fn state(&self) -> RwLockReadGuard<'_, FeedState> { + self.feed_state.read().expect("failed to read feed state") + } +} + +/// Unified bridge between the API and either the container image scanner or normal scanner. +#[derive(Clone)] +pub struct ScannerBridge { + /// Crypter used to encrypt credentials. Not used by the container image scanner. + pub crypter: Option>, + /// Database to save scan data into. + pub pool: DataBase, + /// Scheduler to start or stop scans. Not used by the container image scanner. + pub scheduler: Option>, +} + +impl ScannerBridge { + pub fn new( + pool: DataBase, + crypter: Option>, + scheduler: Option>, + ) -> Self { + Self { + crypter, + pool, + scheduler, + } + } + + /// Posts a given scan without starting it. + pub async fn post_scan(&self, client_id: &str, scan: &models::Scan) -> Result<(), DAOError> { + if let Some(ref crypter) = self.crypter { + ScanDB::new(&self.pool, (crypter.as_ref(), client_id, scan)) + .exec() + .await + .map(|_| ()) + } else { + DBScan::new(&self.pool, (client_id, scan)).exec().await + } + } + + /// Gets all scans owned by the `client_id` and returns a streamed list of their ids. + pub async fn get_scans(&self, client_id: String) -> StreamResult { + Box::pin( + if self.crypter.is_some() { + ScanDB::new(&self.pool, client_id).stream_fetch() + } else { + DBScan::new(&self.pool, client_id).stream_fetch() + } + .map_err(|e| e.into()), + ) + } + + /// Gets a scan with a given `scan_id` owned by the client with the given `client_id`. + pub async fn get_scan(&self, client_id: &str, scan_id: &str) -> Result { + let id = self.get_scan_id(client_id, scan_id).await?; + + if let Some(ref crypter) = self.crypter { + ScanDB::new(&self.pool, (crypter.as_ref(), id)) + .fetch() + .await + } else { + DBScan::new(&self.pool, id.to_string()).fetch().await + } + .map_err(|e| e.into()) + } + + /// Gets the current status of a scan with a given `scan_id`. + pub async fn get_scan_status( + &self, + client_id: &str, + scan_id: &str, + ) -> Result { + let id = self.get_scan_id(client_id, scan_id).await?; + + if self.crypter.is_some() { + ScanDB::new(&self.pool, id).fetch().await + } else { + DBScan::new(&self.pool, id.to_string()).fetch().await + } + .map_err(|e| e.into()) + } + + /// Deletes the scan with the given `scan_id` if it is not currently running. + pub async fn delete_scan(&self, client_id: &str, scan_id: &str) -> Result<(), ApiError> { + let id = self.get_scan_id(client_id, scan_id).await?.to_string(); + let status = self.get_scan_status(client_id, scan_id).await?; + + if !status.is_running() { + if self.crypter.is_some() { + ScanDB::new(&self.pool, id).exec().await?; + } else { + DBScan::new(&self.pool, id).retry_exec().await?; + } + Ok(()) + } else { + Err(ApiError::ScanRunning) + } + } + + /// Schedules a scan to either stop or start. + pub async fn schedule_scan( + &self, + client_id: &str, + scan_id: &str, + action: models::Action, + ) -> Result<(), ApiError> { + let id = self.get_scan_id(client_id, scan_id).await?.to_string(); + let status = self.get_scan_status(client_id, scan_id).await?; + + let msg = match action { + models::Action::Start => match status.status { + Phase::Succeeded => Err("scan is already completed"), + Phase::Requested => Err("scan is already requested"), + Phase::Running => Err("scan is already running"), + Phase::Failed => Err("scan is already failed"), + Phase::Stored | Phase::Stopped => Ok(scheduling::Message::Start(id.clone())), + }, + models::Action::Stop => match status.status { + Phase::Succeeded => Err("scan is already completed"), + Phase::Failed => Err("scan is already failed"), + Phase::Stored | Phase::Stopped => Err("scan is not running"), + Phase::Requested | Phase::Running => Ok(scheduling::Message::Stop(id.clone())), + }, + }; + + match msg { + Ok(msg) => { + if let Some(ref scheduler) = self.scheduler { + scheduler.send(msg).await.map_err(|e| e.into()) + } else { + DBScan::new(&self.pool, (id, action)) + .retry_exec() + .await + .map_err(|e| e.into()) + } + } + Err(e) => { + tracing::warn!(id = scan_id, "{e}"); + Ok(()) + } + } + } + + /// Gets a single result from a given scans list of results. + pub async fn get_scan_result( + &self, + client_id: &str, + scan_id: &str, + result_id: usize, + ) -> Result { + let id = self.get_scan_id(client_id, scan_id).await?.to_string(); + + DBResults::new(&self.pool, (id, result_id)) + .fetch() + .await + .map_err(|e| e.into()) + } + + /// Gets all results of a given scan. + /// + /// The results are progressively filled as the scan is running. + pub async fn get_scan_results( + &self, + client_id: &str, + scan_id: &str, + start: Option, + end: Option, + ) -> Result, ApiError> { + let id = self.get_scan_id(client_id, scan_id).await?.to_string(); + + let results = DBResults::new(&self.pool, (id, start, end)) + .stream_fetch() + .map_err(|e| e.into()); + Ok(Box::pin(results)) + } + + /// Helper function to resolve a `client_id` and `scan_id` to the scans internal id. + pub async fn get_scan_id(&self, client_id: &str, scan_id: &str) -> Result { + Ok(ScanDB::new(&self.pool, (client_id, scan_id)) + .fetch() + .await? + .ok_or(DAOError::NotFound)? + .parse::() + .expect("numeric ID")) + } +} diff --git a/rust/src/openvasd/api_tests/mod.rs b/rust/src/openvasd/api_tests/mod.rs index b8c74bedb0..4fb84fa819 100644 --- a/rust/src/openvasd/api_tests/mod.rs +++ b/rust/src/openvasd/api_tests/mod.rs @@ -47,7 +47,7 @@ async fn check_head_endpoints(t: &OpenvasdInstance, write_snapshots: bool) { #[tokio::test] async fn head_endpoints() { - let t = Test::new("head_endpoints").config("basic").await; + let t = Test::new("head_endpoints").config("basic_feed").await; check_head_endpoints(&t, true).await } @@ -70,6 +70,141 @@ async fn get_scans_preferences() { body.snapshot("body"); } +/// Tests that the endpoints enable-get-scans works +#[tokio::test] +async fn get_scans_route_config() { + let t = Test::new("get_scans_disabled").config("basic").await; + t.request(GET, "/scans") + .await + .assert_status(StatusCode::METHOD_NOT_ALLOWED); + t.request(GET, "/container-image-scanner/scans") + .await + .assert_status(StatusCode::METHOD_NOT_ALLOWED); + + let t = Test::new("get_scans_enabled").config("basic_feed").await; + t.request(GET, "/scans").await.assert_status(StatusCode::OK); + t.request(GET, "/container-image-scanner/scans") + .await + .assert_status(StatusCode::OK); +} + +#[tokio::test] +async fn scans_error_handling() { + let t = Test::new("scans_error_handling").config("basic").await; + + t.request(POST, "/scans") + .json(Scan { + scan_id: "scan1".to_string(), + ..Default::default() + }) + .await + .assert_status(StatusCode::CREATED); + + // duplicate scan id + t.request(POST, "/scans") + .json(Scan { + scan_id: "scan1".to_string(), + ..Default::default() + }) + .await + .assert_status(StatusCode::CONFLICT) + .snapshot_with_name("duplicate_id"); + + // valid JSON but invalid scan struct + t.request(POST, "/scans") + .json("{}") + .await + .assert_status(StatusCode::BAD_REQUEST) + .snapshot_with_name("invalid_scan"); + + // invalid JSON + t.request(POST, "/scans") + .json("{") + .await + .assert_status(StatusCode::BAD_REQUEST) + .snapshot_with_name("invalid_json"); + + // unknown scan id + t.request(GET, "/scans/doesnt-exist") + .await + .assert_status(StatusCode::NOT_FOUND) + .snapshot(); + + // unknown result id + t.request(GET, "/scans/scan1/results/9999") + .await + .assert_status(StatusCode::NOT_FOUND) + .snapshot(); +} + +#[tokio::test] +async fn cis_scans_error_handling() { + let t = Test::new("scans_error_handling").config("basic").await; + + t.request(POST, "/container-image-scanner/scans") + .json(Scan { + scan_id: "scan1".to_string(), + ..Default::default() + }) + .await + .assert_status(StatusCode::CREATED); + + // duplicate scan id + t.request(POST, "/container-image-scanner/scans") + .json(Scan { + scan_id: "scan1".to_string(), + ..Default::default() + }) + .await + .assert_status(StatusCode::CONFLICT) + .snapshot_with_name("duplicate_id"); + + // valid JSON but invalid scan struct + t.request(POST, "/container-image-scanner/scans") + .json("{}") + .await + .assert_status(StatusCode::BAD_REQUEST) + .snapshot_with_name("invalid_scan"); + + // invalid JSON + t.request(POST, "/container-image-scanner/scans") + .json("{") + .await + .assert_status(StatusCode::BAD_REQUEST) + .snapshot_with_name("invalid_json"); + + // unknown scan id + t.request(GET, "/container-image-scanner/scans/doesnt-exist") + .await + .assert_status(StatusCode::NOT_FOUND) + .snapshot(); + + // unknown result id + t.request(GET, "/container-image-scanner/scans/scan1/results/9999") + .await + .assert_status(StatusCode::NOT_FOUND) + .snapshot(); +} + +#[tokio::test] +async fn get_container_image_scanner_scans_preferences() { + let t = Test::new("get_scans_preferences") + .config("cis_single_db_connection") + .await; + + let mut body = t + .request(GET, "/container-image-scanner/scans/preferences") + .await + .assert_status(StatusCode::OK) + .body::>>(); + + // The full response body looks ugly, so we extract + // it as a map to make the snapshot more readable + // Then we sort by id to have some sort of order + body.sort_by_key(|entry| entry["id"].to_string()); + body.snapshot("body"); +} + async fn notus_test( t: &OpenvasdInstance, package_name: &str, @@ -285,8 +420,10 @@ async fn container_image_scanner_deadlock() { }; let scan = t.create_container_image_scan(scan).await; + scan.start().await; - scan.wait_for(Phase::Failed.with_timeout(Duration::from_secs(5))) + + scan.wait_for(Phase::Failed.with_timeout(Duration::from_secs(10))) .await .body::() .snapshot("status"); diff --git a/rust/src/openvasd/api_tests/snapshots/get_scans_preferences GET __container-image-scanner__scans__preferences_body.snap b/rust/src/openvasd/api_tests/snapshots/get_scans_preferences GET __container-image-scanner__scans__preferences_body.snap new file mode 100644 index 0000000000..5f3ddd35c0 --- /dev/null +++ b/rust/src/openvasd/api_tests/snapshots/get_scans_preferences GET __container-image-scanner__scans__preferences_body.snap @@ -0,0 +1,18 @@ +--- +source: src/openvasd/api_tests/test_builder.rs +expression: inner +--- +[ + { + "default": true, + "description": "This disables the CA chain verification for TLS certificates when connecting to a registry. This is useful for self-signed certificates.", + "id": "accept_invalid_certs", + "name": "Accepts certificates without trust chain verification", + }, + { + "default": false, + "description": "This allows unencrypted communication with an registry (HTTP instead of HTTPS).", + "id": "registry_allow_insecure", + "name": "Use HTTP instead of HTTPS", + }, +] diff --git a/rust/src/openvasd/api_tests/snapshots/head_endpoints HEAD __health__alive.snap b/rust/src/openvasd/api_tests/snapshots/head_endpoints HEAD __health__alive.snap index 0be4f3ea9f..86ddc049aa 100644 --- a/rust/src/openvasd/api_tests/snapshots/head_endpoints HEAD __health__alive.snap +++ b/rust/src/openvasd/api_tests/snapshots/head_endpoints HEAD __health__alive.snap @@ -1,5 +1,5 @@ --- -source: src/openvasd/tests/test_builder.rs +source: src/openvasd/api_tests/test_builder.rs expression: inner --- { @@ -7,6 +7,7 @@ expression: inner "headers": { "api-version": "1", "authentication": "disabled", + "content-length": "0", "date": "[redacted]", "feed-version": "[redacted]", }, diff --git a/rust/src/openvasd/api_tests/snapshots/head_endpoints HEAD __health__ready.snap b/rust/src/openvasd/api_tests/snapshots/head_endpoints HEAD __health__ready.snap index 0be4f3ea9f..86ddc049aa 100644 --- a/rust/src/openvasd/api_tests/snapshots/head_endpoints HEAD __health__ready.snap +++ b/rust/src/openvasd/api_tests/snapshots/head_endpoints HEAD __health__ready.snap @@ -1,5 +1,5 @@ --- -source: src/openvasd/tests/test_builder.rs +source: src/openvasd/api_tests/test_builder.rs expression: inner --- { @@ -7,6 +7,7 @@ expression: inner "headers": { "api-version": "1", "authentication": "disabled", + "content-length": "0", "date": "[redacted]", "feed-version": "[redacted]", }, diff --git a/rust/src/openvasd/api_tests/snapshots/head_endpoints HEAD __health__started.snap b/rust/src/openvasd/api_tests/snapshots/head_endpoints HEAD __health__started.snap index 0be4f3ea9f..86ddc049aa 100644 --- a/rust/src/openvasd/api_tests/snapshots/head_endpoints HEAD __health__started.snap +++ b/rust/src/openvasd/api_tests/snapshots/head_endpoints HEAD __health__started.snap @@ -1,5 +1,5 @@ --- -source: src/openvasd/tests/test_builder.rs +source: src/openvasd/api_tests/test_builder.rs expression: inner --- { @@ -7,6 +7,7 @@ expression: inner "headers": { "api-version": "1", "authentication": "disabled", + "content-length": "0", "date": "[redacted]", "feed-version": "[redacted]", }, diff --git a/rust/src/openvasd/api_tests/snapshots/head_endpoints HEAD __notus.snap b/rust/src/openvasd/api_tests/snapshots/head_endpoints HEAD __notus.snap index 0be4f3ea9f..1d6c3607b3 100644 --- a/rust/src/openvasd/api_tests/snapshots/head_endpoints HEAD __notus.snap +++ b/rust/src/openvasd/api_tests/snapshots/head_endpoints HEAD __notus.snap @@ -1,5 +1,5 @@ --- -source: src/openvasd/tests/test_builder.rs +source: src/openvasd/api_tests/test_builder.rs expression: inner --- { @@ -7,6 +7,8 @@ expression: inner "headers": { "api-version": "1", "authentication": "disabled", + "content-length": "41", + "content-type": "application/json", "date": "[redacted]", "feed-version": "[redacted]", }, diff --git a/rust/src/openvasd/api_tests/snapshots/head_endpoints HEAD __scans.snap b/rust/src/openvasd/api_tests/snapshots/head_endpoints HEAD __scans.snap index 0be4f3ea9f..3ba3d79077 100644 --- a/rust/src/openvasd/api_tests/snapshots/head_endpoints HEAD __scans.snap +++ b/rust/src/openvasd/api_tests/snapshots/head_endpoints HEAD __scans.snap @@ -1,5 +1,5 @@ --- -source: src/openvasd/tests/test_builder.rs +source: src/openvasd/api_tests/test_builder.rs expression: inner --- { @@ -7,6 +7,7 @@ expression: inner "headers": { "api-version": "1", "authentication": "disabled", + "content-type": "application/json", "date": "[redacted]", "feed-version": "[redacted]", }, diff --git a/rust/src/openvasd/api_tests/snapshots/notus POST __notus__not_a_system.snap b/rust/src/openvasd/api_tests/snapshots/notus POST __notus__not_a_system.snap index e664b20e87..7cd889b719 100644 --- a/rust/src/openvasd/api_tests/snapshots/notus POST __notus__not_a_system.snap +++ b/rust/src/openvasd/api_tests/snapshots/notus POST __notus__not_a_system.snap @@ -1,5 +1,5 @@ --- -source: src/openvasd/tests/test_builder.rs +source: src/openvasd/api_tests/test_builder.rs expression: inner --- { @@ -7,9 +7,10 @@ expression: inner "headers": { "api-version": "1", "authentication": "disabled", - "content-length": "0", + "content-length": "73", + "content-type": "application/json", "date": "[redacted]", "feed-version": "[redacted]", }, - "body": "", + "body": "{\"status\":404,\"message\":\"Unknown notus product\",\"details\":\"not_a_system\"}", } diff --git a/rust/src/openvasd/api_tests/snapshots/notus_compose POST __notus__not_a_system.snap b/rust/src/openvasd/api_tests/snapshots/notus_compose POST __notus__not_a_system.snap index dd814013b0..1c21be20cb 100644 --- a/rust/src/openvasd/api_tests/snapshots/notus_compose POST __notus__not_a_system.snap +++ b/rust/src/openvasd/api_tests/snapshots/notus_compose POST __notus__not_a_system.snap @@ -7,8 +7,10 @@ expression: inner "headers": { "api-version": "1", "authentication": "mTLS", + "content-length": "73", + "content-type": "application/json", "date": "[redacted]", "feed-version": "[redacted]", }, - "body": "", + "body": "{\"status\":404,\"message\":\"Unknown notus product\",\"details\":\"not_a_system\"}", } diff --git a/rust/src/openvasd/api_tests/snapshots/scans_error_handling GET __container-image-scanner__scans__doesnt-exist.snap b/rust/src/openvasd/api_tests/snapshots/scans_error_handling GET __container-image-scanner__scans__doesnt-exist.snap new file mode 100644 index 0000000000..a53bd2064b --- /dev/null +++ b/rust/src/openvasd/api_tests/snapshots/scans_error_handling GET __container-image-scanner__scans__doesnt-exist.snap @@ -0,0 +1,16 @@ +--- +source: src/openvasd/api_tests/test_builder.rs +expression: inner +--- +{ + "status_code": 404, + "headers": { + "api-version": "1", + "authentication": "disabled", + "content-length": "57", + "content-type": "application/json", + "date": "[redacted]", + "feed-version": "[redacted]", + }, + "body": "{\"status\":404,\"message\":\"Entry not found\",\"details\":null}", +} diff --git a/rust/src/openvasd/api_tests/snapshots/scans_error_handling GET __container-image-scanner__scans__scan1__results__9999.snap b/rust/src/openvasd/api_tests/snapshots/scans_error_handling GET __container-image-scanner__scans__scan1__results__9999.snap new file mode 100644 index 0000000000..a53bd2064b --- /dev/null +++ b/rust/src/openvasd/api_tests/snapshots/scans_error_handling GET __container-image-scanner__scans__scan1__results__9999.snap @@ -0,0 +1,16 @@ +--- +source: src/openvasd/api_tests/test_builder.rs +expression: inner +--- +{ + "status_code": 404, + "headers": { + "api-version": "1", + "authentication": "disabled", + "content-length": "57", + "content-type": "application/json", + "date": "[redacted]", + "feed-version": "[redacted]", + }, + "body": "{\"status\":404,\"message\":\"Entry not found\",\"details\":null}", +} diff --git a/rust/src/openvasd/api_tests/snapshots/scans_error_handling GET __scans__doesnt-exist.snap b/rust/src/openvasd/api_tests/snapshots/scans_error_handling GET __scans__doesnt-exist.snap new file mode 100644 index 0000000000..a53bd2064b --- /dev/null +++ b/rust/src/openvasd/api_tests/snapshots/scans_error_handling GET __scans__doesnt-exist.snap @@ -0,0 +1,16 @@ +--- +source: src/openvasd/api_tests/test_builder.rs +expression: inner +--- +{ + "status_code": 404, + "headers": { + "api-version": "1", + "authentication": "disabled", + "content-length": "57", + "content-type": "application/json", + "date": "[redacted]", + "feed-version": "[redacted]", + }, + "body": "{\"status\":404,\"message\":\"Entry not found\",\"details\":null}", +} diff --git a/rust/src/openvasd/api_tests/snapshots/scans_error_handling GET __scans__scan1__results__9999.snap b/rust/src/openvasd/api_tests/snapshots/scans_error_handling GET __scans__scan1__results__9999.snap new file mode 100644 index 0000000000..a53bd2064b --- /dev/null +++ b/rust/src/openvasd/api_tests/snapshots/scans_error_handling GET __scans__scan1__results__9999.snap @@ -0,0 +1,16 @@ +--- +source: src/openvasd/api_tests/test_builder.rs +expression: inner +--- +{ + "status_code": 404, + "headers": { + "api-version": "1", + "authentication": "disabled", + "content-length": "57", + "content-type": "application/json", + "date": "[redacted]", + "feed-version": "[redacted]", + }, + "body": "{\"status\":404,\"message\":\"Entry not found\",\"details\":null}", +} diff --git a/rust/src/openvasd/api_tests/snapshots/scans_error_handling POST __container-image-scanner__scans_duplicate_id.snap b/rust/src/openvasd/api_tests/snapshots/scans_error_handling POST __container-image-scanner__scans_duplicate_id.snap new file mode 100644 index 0000000000..09ac11a628 --- /dev/null +++ b/rust/src/openvasd/api_tests/snapshots/scans_error_handling POST __container-image-scanner__scans_duplicate_id.snap @@ -0,0 +1,16 @@ +--- +source: src/openvasd/api_tests/test_builder.rs +expression: inner +--- +{ + "status_code": 409, + "headers": { + "api-version": "1", + "authentication": "disabled", + "content-length": "64", + "content-type": "application/json", + "date": "[redacted]", + "feed-version": "[redacted]", + }, + "body": "{\"status\":409,\"message\":\"Scan ID already in use\",\"details\":null}", +} diff --git a/rust/src/openvasd/api_tests/snapshots/scans_error_handling POST __container-image-scanner__scans_invalid_json.snap b/rust/src/openvasd/api_tests/snapshots/scans_error_handling POST __container-image-scanner__scans_invalid_json.snap new file mode 100644 index 0000000000..93a502177a --- /dev/null +++ b/rust/src/openvasd/api_tests/snapshots/scans_error_handling POST __container-image-scanner__scans_invalid_json.snap @@ -0,0 +1,16 @@ +--- +source: src/openvasd/api_tests/test_builder.rs +expression: inner +--- +{ + "status_code": 400, + "headers": { + "api-version": "1", + "authentication": "disabled", + "content-length": "187", + "content-type": "application/json", + "date": "[redacted]", + "feed-version": "[redacted]", + }, + "body": "{\"status\":400,\"message\":\"Failed to process JSON\",\"details\":\"Failed to deserialize the JSON body into the target type: invalid type: string \\\"{\\\", expected struct Scan at line 1 column 3\"}", +} diff --git a/rust/src/openvasd/api_tests/snapshots/scans_error_handling POST __container-image-scanner__scans_invalid_scan.snap b/rust/src/openvasd/api_tests/snapshots/scans_error_handling POST __container-image-scanner__scans_invalid_scan.snap new file mode 100644 index 0000000000..9c39fa8e9b --- /dev/null +++ b/rust/src/openvasd/api_tests/snapshots/scans_error_handling POST __container-image-scanner__scans_invalid_scan.snap @@ -0,0 +1,16 @@ +--- +source: src/openvasd/api_tests/test_builder.rs +expression: inner +--- +{ + "status_code": 400, + "headers": { + "api-version": "1", + "authentication": "disabled", + "content-length": "188", + "content-type": "application/json", + "date": "[redacted]", + "feed-version": "[redacted]", + }, + "body": "{\"status\":400,\"message\":\"Failed to process JSON\",\"details\":\"Failed to deserialize the JSON body into the target type: invalid type: string \\\"{}\\\", expected struct Scan at line 1 column 4\"}", +} diff --git a/rust/src/openvasd/api_tests/snapshots/scans_error_handling POST __scans_duplicate_id.snap b/rust/src/openvasd/api_tests/snapshots/scans_error_handling POST __scans_duplicate_id.snap new file mode 100644 index 0000000000..09ac11a628 --- /dev/null +++ b/rust/src/openvasd/api_tests/snapshots/scans_error_handling POST __scans_duplicate_id.snap @@ -0,0 +1,16 @@ +--- +source: src/openvasd/api_tests/test_builder.rs +expression: inner +--- +{ + "status_code": 409, + "headers": { + "api-version": "1", + "authentication": "disabled", + "content-length": "64", + "content-type": "application/json", + "date": "[redacted]", + "feed-version": "[redacted]", + }, + "body": "{\"status\":409,\"message\":\"Scan ID already in use\",\"details\":null}", +} diff --git a/rust/src/openvasd/api_tests/snapshots/scans_error_handling POST __scans_invalid_json.snap b/rust/src/openvasd/api_tests/snapshots/scans_error_handling POST __scans_invalid_json.snap new file mode 100644 index 0000000000..93a502177a --- /dev/null +++ b/rust/src/openvasd/api_tests/snapshots/scans_error_handling POST __scans_invalid_json.snap @@ -0,0 +1,16 @@ +--- +source: src/openvasd/api_tests/test_builder.rs +expression: inner +--- +{ + "status_code": 400, + "headers": { + "api-version": "1", + "authentication": "disabled", + "content-length": "187", + "content-type": "application/json", + "date": "[redacted]", + "feed-version": "[redacted]", + }, + "body": "{\"status\":400,\"message\":\"Failed to process JSON\",\"details\":\"Failed to deserialize the JSON body into the target type: invalid type: string \\\"{\\\", expected struct Scan at line 1 column 3\"}", +} diff --git a/rust/src/openvasd/api_tests/snapshots/scans_error_handling POST __scans_invalid_scan.snap b/rust/src/openvasd/api_tests/snapshots/scans_error_handling POST __scans_invalid_scan.snap new file mode 100644 index 0000000000..9c39fa8e9b --- /dev/null +++ b/rust/src/openvasd/api_tests/snapshots/scans_error_handling POST __scans_invalid_scan.snap @@ -0,0 +1,16 @@ +--- +source: src/openvasd/api_tests/test_builder.rs +expression: inner +--- +{ + "status_code": 400, + "headers": { + "api-version": "1", + "authentication": "disabled", + "content-length": "188", + "content-type": "application/json", + "date": "[redacted]", + "feed-version": "[redacted]", + }, + "body": "{\"status\":400,\"message\":\"Failed to process JSON\",\"details\":\"Failed to deserialize the JSON body into the target type: invalid type: string \\\"{}\\\", expected struct Scan at line 1 column 4\"}", +} diff --git a/rust/src/openvasd/api_tests/test_builder.rs b/rust/src/openvasd/api_tests/test_builder.rs index 2a015718ca..8cdb2df700 100644 --- a/rust/src/openvasd/api_tests/test_builder.rs +++ b/rust/src/openvasd/api_tests/test_builder.rs @@ -4,7 +4,6 @@ use std::{ collections::BTreeMap, - error::Error, fs, net::{Ipv4Addr, SocketAddr, TcpListener}, ops::{Deref, DerefMut}, @@ -23,7 +22,7 @@ use serde::{Serialize, de::DeserializeOwned, ser::SerializeMap}; use tokio::sync::{Mutex, OwnedMutexGuard}; use tokio::time::Instant; -use crate::{build_runtime, config::Config}; +use crate::config::Config; const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5); const DEFAULT_SLEEP_INTERVAL: Duration = Duration::from_millis(100); @@ -171,6 +170,15 @@ impl Response { self } + pub fn snapshot_with_name(&self, name: &str) -> &Self { + Snapshot { + inner: self.snapshot.clone(), + name_prefix: self.name.clone(), + } + .snapshot(name); + self + } + pub(crate) fn snapshot_if(&self, write_snapshots: bool) -> &Self { if write_snapshots { self.snapshot(); @@ -272,11 +280,11 @@ impl Test { unused_local_address().expect("allocate openvasd test listener") }; config.listener.address = address; - let runtime = build_runtime(config) + let api_cfg = crate::init_api(config) .await .map_err(|error| anyhow::anyhow!("{error}"))?; - let mut task = tokio::spawn(async move { runtime.run_blocking().await }); + let mut task = tokio::spawn(async move { crate::api::run(&api_cfg).await }); tokio::select! { result = wait_for_listener(address) => result?, result = &mut task => { @@ -317,7 +325,7 @@ pub struct OpenvasdInstance { pub address: SocketAddr, test_name: String, request_client: RequestClient, - task: tokio::task::JoinHandle>>, + task: tokio::task::JoinHandle>, _openvas_guard: Option>, } diff --git a/rust/src/openvasd/config/mod.rs b/rust/src/openvasd/config/mod.rs index f3cfc0774c..04c0c69291 100644 --- a/rust/src/openvasd/config/mod.rs +++ b/rust/src/openvasd/config/mod.rs @@ -186,9 +186,8 @@ impl TypedValueParser for Mode { #[derive(Deserialize, Serialize, Debug, Clone, Default)] pub struct Endpoints { - pub enable_get_scans: bool, #[serde(default)] - pub enable_get_performance: Option, + pub enable_get_scans: bool, #[serde(default)] pub key: Option, } @@ -497,16 +496,7 @@ impl Config { .num_args(0..=1) .value_parser(clap::builder::BoolValueParser::new()) .default_missing_value("true") - .help("enable get scans endpoint. Default 'true'."), - ) - .arg( - clap::Arg::new("enable-get-performance") - .env("ENABLE_GET_PERFORMANCE") - .long("enable-get-performance") - .num_args(0..=1) - .value_parser(clap::builder::BoolValueParser::new()) - .default_missing_value("false") - .help("enable get performance endpoint. Default 'false'."), + .help("enable get scans endpoint. Default 'false'."), ) .arg( clap::Arg::new("api-key") @@ -721,9 +711,6 @@ impl Config { if let Some(version) = cmds.get_one::("version") { config.version = *version; } - if let Some(enable) = cmds.get_one::("enable-get-performance") { - config.endpoints.enable_get_performance = Some(*enable); - } if let Some(api_key) = cmds.get_one::("api-key") { config.endpoints.key = Some(api_key.clone()); } diff --git a/rust/src/openvasd/container_image_scanner/endpoints/mod.rs b/rust/src/openvasd/container_image_scanner/endpoints/mod.rs deleted file mode 100644 index 156a315e86..0000000000 --- a/rust/src/openvasd/container_image_scanner/endpoints/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod scans; -pub mod vts; diff --git a/rust/src/openvasd/container_image_scanner/endpoints/scans.rs b/rust/src/openvasd/container_image_scanner/endpoints/scans.rs deleted file mode 100644 index be14c2f73d..0000000000 --- a/rust/src/openvasd/container_image_scanner/endpoints/scans.rs +++ /dev/null @@ -1,871 +0,0 @@ -use std::pin::Pin; - -use crate::greenbone_scanner_framework::{MapScanID, StreamResult, entry::Prefixed, prelude::*}; -use futures::TryStreamExt; -use scannerlib::models::{self, PreferenceValue, Scan, ScanPreferenceInformation}; -use tracing::instrument; - -use crate::{ - container_image_scanner::scheduling::db::{DBResults, DataBase, scan::DBScan}, - database::dao::{DAOError, DBViolation, Execute, Fetch, RetryExec, StreamFetch}, -}; - -pub struct Scans { - pub pool: DataBase, -} - -impl Prefixed for Scans { - fn prefix(&self) -> &'static str { - "container-image-scanner" - } -} - -impl PostScans for Scans { - #[instrument(skip_all, fields(client_id=client_id, scan_id=scan.scan_id))] - fn post_scans( - &self, - client_id: String, - scan: Scan, - ) -> Pin> + Send + '_>> { - Box::pin(async move { - // maybe get rid of clone - let scan_id = scan.scan_id.clone(); - match DBScan::new(&self.pool, (client_id.as_str(), &scan)) - .exec() - .await - { - Ok(_) => Ok(scan_id), - Err(DAOError::DBViolation(DBViolation::UniqueViolation)) => { - Err(PostScansError::DuplicateId(scan_id)) - } - Err(error) => Err(PostScansError::External(Box::new(error))), - } - }) - } -} - -impl MapScanID for Scans { - fn contains_scan_id<'a>( - &'a self, - client_id: &'a str, - scan_id: &'a str, - ) -> Pin< - Box< - dyn Future> - + Send - + 'a, - >, - > { - Box::pin(async move { - match DBScan::new(&self.pool, (client_id, scan_id)).fetch().await { - Ok(x) => x, - Err(error) => { - tracing::warn!(%error, "Unable to fetch id from client_scan_map. Returning no id found."); - None - } - } - }) - } -} - -impl GetScans for Scans { - fn get_scans(&self, client_id: String) -> StreamResult { - let result = DBScan::new(&self.pool, client_id) - .stream_fetch() - .map_err(GetScansError::from_external); - - Box::pin(result) - } -} - -impl GetScansPreferences for Scans { - fn get_scans_preferences( - &self, - ) -> Pin> + Send>> { - Box::pin(async move { - vec![ - ScanPreferenceInformation { - id: "accept_invalid_certs", - name: "Accepts certificates without trust chain verification", - default: PreferenceValue::Bool(true), - description: "This disables the CA chain verification for TLS certificates when connecting to a registry. \ - This is useful for self-signed certificates.", - }, - ScanPreferenceInformation { - id: "registry_allow_insecure", - name: "Use HTTP instead of HTTPS", - default: PreferenceValue::Bool(false), - description: "This allows unencrypted communication with an registry (HTTP instead of HTTPS).", - }, - ] - }) - } -} - -impl GetScansId for Scans { - fn get_scans_id<'a>( - &'a self, - id: String, - ) -> Pin> + Send + 'a>> { - Box::pin(async move { - DBScan::new(&self.pool, id) - .fetch() - .await - .map_err(GetScansError::from_external) - }) - } -} - -impl GetScansIdResults for Scans { - fn get_scans_id_results( - &self, - id: String, - from: Option, - to: Option, - ) -> StreamResult { - let result = DBResults::new(&self.pool, (id, from, to)) - .stream_fetch() - .map_err(GetScansError::from_external); - - Box::pin(result) - } -} - -impl GetScansIdResultsId for Scans { - fn get_scans_id_results_id( - &self, - id: String, - result_id: usize, - ) -> Pin> + Send + '_>> - { - Box::pin(async move { - DBResults::new(&self.pool, (id, result_id)) - .fetch() - .await - .map_err(|e| match e { - DAOError::NotFound => GetScansIDResultsIDError::NotFound, - e => e.into(), - }) - }) - } -} - -impl GetScansIdStatus for Scans { - fn get_scans_id_status( - &self, - id: String, - ) -> Pin> + Send + '_>> - { - Box::pin(async move { - DBScan::new(&self.pool, id) - .fetch() - .await - .map_err(GetScansIDStatusError::from_external) - }) - } -} - -impl PostScansId for Scans { - fn post_scans_id( - &self, - id: String, - action: models::Action, - ) -> Pin> + Send + '_>> { - Box::pin(async move { - DBScan::new(&self.pool, (id, action)) - .retry_exec() - .await - .map_err(PostScansIDError::from_external) - }) - } -} - -impl DeleteScansId for Scans { - fn delete_scans_id( - &self, - id: String, - ) -> Pin> + Send + '_>> { - Box::pin(async move { - let db = DBScan::new(&self.pool, id); - let phase: models::Phase = db - .fetch() - .await - .map_err(DeleteScansIDError::from_external)?; - if phase.is_running() { - return Err(DeleteScansIDError::Running); - } - db.retry_exec() - .await - .map_err(DeleteScansIDError::from_external) - }) - } -} - -#[cfg(test)] -pub mod scans_utils { - - use std::{sync::Arc, time::Duration}; - - use crate::greenbone_scanner_framework::entry::ClientHash; - use crate::greenbone_scanner_framework::prelude::*; - use tokio::sync::Mutex; - - use super::Scans; - use crate::container_image_scanner::{ - Config, MIGRATOR, - config::DBLocation, - image::{DockerRegistryV2Mock, RegistryPreference}, - scheduling::{Scheduler, db::DataBase}, - }; - use scannerlib::models; - use scannerlib::notus::products_loader; - - pub fn client_id() -> String { - ClientHash::default().to_string() - } - - pub fn second_client_id() -> String { - ClientHash::from("second").to_string() - } - - async fn in_memory_scheduler_and_scan( - config: crate::container_image_scanner::Config, - ) -> (Scheduler, Scans) { - let pool = DataBase::connect(&DBLocation::InMemory.sqlite_address("test")) - .await - .expect("inmemory database must be available"); - - MIGRATOR - .run(&pool) - .await - .expect("need migrated database scheme"); - let products_path: &str = - concat!(env!("CARGO_MANIFEST_DIR"), "/examples/feed/notus/products"); - - let scheduler = Scheduler::init( - config.into(), - pool.clone(), - products_loader(products_path, false), - ); - let scans = super::Scans { pool: pool.clone() }; - (scheduler, scans) - } - - pub struct Fakes { - registry: DockerRegistryV2Mock, - pub entry: super::Scans, - pub scheduler: Scheduler, - } - - impl Fakes { - pub async fn internal_id(&self, client_id: &str, scan_id: &str) -> String { - self.entry - .contains_scan_id(client_id, scan_id) - .await - .unwrap() - } - - pub async fn simulate_stop_scan( - &mut self, - client_id: &str, - scan_id: &str, - ) -> models::Status { - let id = self.internal_id(client_id, scan_id).await; - - self.entry - .post_scans_id(id.clone(), models::Action::Stop) - .await - .unwrap(); - - self.entry.get_scans_id_status(id).await.unwrap() - } - - pub async fn simulate_start_scan( - &mut self, - client_id: &str, - scan: models::Scan, - ) -> (String, models::Status) { - let scan_id = self - .entry - .post_scans(client_id.to_owned(), scan) - .await - .unwrap(); - - let id = self.internal_id(client_id, &scan_id).await; - - self.entry - .post_scans_id(id.clone(), models::Action::Start) - .await - .unwrap(); - - (scan_id, self.entry.get_scans_id_status(id).await.unwrap()) - } - - pub async fn init() -> Self { - Self::init_with_config(Config::default()).await - } - - pub async fn init_with_config(config: Config) -> Self { - let registry = DockerRegistryV2Mock::serve_default().await; - let (scheduler, entry) = in_memory_scheduler_and_scan(config).await; - - Self { - registry, - entry, - scheduler, - } - } - - async fn create_start_scan( - &mut self, - client_id: &ClientID, - scan: models::Scan, - ) -> String - where - ClientID: Fn() -> String, - { - let (scan_id, _) = self.simulate_start_scan(&client_id(), scan).await; - scan_id - } - - pub async fn run_scheduler_rounds(&self, rounds: usize) { - let conn = Arc::new(Mutex::new(self.scheduler.pool())); - for _ in 0..rounds { - Scheduler::start_scans( - self.scheduler.config(), - conn.clone(), - self.scheduler.products(), - ) - .await; - } - } - - pub async fn status_for_scan_id(&self, client_id: &str, scan_id: &str) -> models::Status { - let id = self.internal_id(client_id, scan_id).await; - self.entry - .get_scans_id_status(id) - .await - .expect("get_scans_id_status must function") - } - - pub async fn start_scan_and_run( - &mut self, - client_id: &str, - scan: models::Scan, - rounds: usize, - ) -> (String, models::Status) { - let (scan_id, _) = self.simulate_start_scan(client_id, scan).await; - self.run_scheduler_rounds(rounds).await; - let status = self.status_for_scan_id(client_id, &scan_id).await; - (scan_id, status) - } - - pub async fn create_start_results( - &mut self, - client_id: &ClientID, - scan: models::Scan, - ) -> (String, models::Status) - where - ClientID: Fn() -> String, - { - let scans = scan.target.hosts.len(); - let scan_id = self.create_start_scan(&client_id, scan).await; - self.run_scheduler_rounds(scans).await; - let id = self - .entry - .contains_scan_id(&client_id(), &scan_id) - .await - .unwrap(); - let result = self - .entry - .get_scans_id_status(id.clone()) - .await - .expect("get_scans_id_status must function"); - - (id, result) - } - - pub fn insecure_scan(&self, scan_id: impl Into, hosts: I) -> models::Scan - where - I: IntoIterator, - H: Into, - { - models::Scan { - scan_id: scan_id.into(), - target: models::Target { - hosts: hosts.into_iter().map(Into::into).collect(), - ..Default::default() - }, - scan_preferences: vec![(RegistryPreference::Insecure.key(), "true").into()], - ..Default::default() - } - } - - pub fn success_scan(&self) -> models::Scan { - let credentials = vec![]; - let hosts = DockerRegistryV2Mock::supported_images() - .clone() - .into_iter() - .map(|mut x| { - x.registry = self.registry.address().into(); - x.to_string() - }) - .collect(); - - let target = models::Target { - hosts, - credentials, - ..Default::default() - }; - let scan_preferences = vec![(RegistryPreference::Insecure.key(), "true").into()]; - - models::Scan { - scan_id: uuid::Uuid::new_v4().to_string(), - target, - scan_preferences, - ..Default::default() - } - } - pub fn pool(&self) -> DataBase { - self.scheduler.pool() - } - - pub fn config_without_retries() -> Config { - let mut config = Config::default(); - config.image.scanning_retries = 0; - config.image.retry_timeout = Duration::from_millis(1); - config - } - - #[allow(dead_code)] - /// This is just a toggle to temporally use logging - fn init_logging() { - let filter = tracing_subscriber::filter::Targets::new() - .with_default(tracing::Level::WARN) - .with_target("greenbone_scanner_framework", tracing::Level::INFO) - .with_target("container_scanning", tracing::Level::TRACE); - let layer = tracing_subscriber::fmt::layer() - .with_span_events(tracing_subscriber::fmt::format::FmtSpan::FULL); - tracing_subscriber::util::SubscriberInitExt::init( - tracing_subscriber::layer::SubscriberExt::with( - tracing_subscriber::layer::SubscriberExt::with( - tracing_subscriber::registry(), - layer, - ), - filter, - ), - ); - } - } -} - -#[cfg(test)] -mod test { - use crate::greenbone_scanner_framework::entry::ClientHash; - use crate::greenbone_scanner_framework::prelude::*; - use futures::StreamExt; - use scannerlib::models::{self, Phase}; - use sqlx::query_scalar; - - use super::scans_utils::second_client_id; - use crate::container_image_scanner::{ - endpoints::scans::scans_utils::{Fakes, client_id}, - image::DockerRegistryV2Mock, - }; - - fn auth_header(registry: &mockito::ServerGuard) -> String { - format!( - r#"Bearer realm="http://{}/token""#, - registry.host_with_port() - ) - } - - fn mock_registry_bearer_auth( - registry: &mut mockito::ServerGuard, - scope: &str, - token_status: usize, - ) -> (mockito::Mock, mockito::Mock) { - let v2 = registry - .mock("GET", "/v2/") - .with_status(401) - .with_header("WWW-Authenticate", &auth_header(registry)) - .expect_at_least(1) - .create(); - let token = registry - .mock("GET", "/token") - .match_query(mockito::Matcher::UrlEncoded( - "scope".into(), - scope.to_owned(), - )) - .with_status(token_status) - .with_header("Content-Type", "application/json") - .with_body(r#"{"token": "waldfee"}"#) - .create(); - (v2, token) - } - - #[tokio::test] - async fn post_scan_double_id() { - let entry = Fakes::init().await.entry; - let scan = models::Scan { - scan_id: "test".to_owned(), - ..Default::default() - }; - let result = entry - .post_scans(client_id(), scan.clone()) - .await - .expect("post scans should succeed"); - assert_eq!(result, "test".to_owned()); - let result = entry.post_scans(client_id(), scan.clone()).await; - assert!( - matches!(result, Err(PostScansError::DuplicateId(_))), - "expected duplicate id result" - ); - } - - #[tokio::test] - async fn post_scan() { - let entry = Fakes::init().await.entry; - let hosts = vec!["oci://localhost/test/myimage".to_owned()]; - let credentials = vec![models::Credential { - credential_type: models::CredentialType::UP { - username: "me".to_owned(), - password: "password".to_owned(), - privilege: None, - }, - ..Default::default() - }]; - - let target = models::Target { - hosts, - credentials, - ..Default::default() - }; - let scan = models::Scan { - scan_id: "test".to_owned(), - target, - ..Default::default() - }; - let result = entry - .post_scans(client_id(), scan.clone()) - .await - .expect("post scans should succeed"); - assert_eq!(result, "test".to_owned()); - let id = entry - .contains_scan_id(&client_id(), &scan.scan_id) - .await - .unwrap(); - let result = entry.get_scans_id(id).await.unwrap(); - assert_eq!(scan.scan_id, result.scan_id); - assert_eq!(scan.target.hosts, result.target.hosts); - } - - #[tokio::test] - async fn start_scan() -> Result<(), Box> { - let mut fakes = Fakes::init().await; - let scan = fakes.success_scan(); - - let (_, status) = fakes.simulate_start_scan(&client_id(), scan).await; - assert_eq!(status.status, Phase::Requested); - Ok(()) - } - - #[tokio::test] - async fn stop_scan() -> Result<(), Box> { - let mut fakes = Fakes::init().await; - let scan = fakes.success_scan(); - let client_id = client_id(); - - let (id, status) = fakes.simulate_start_scan(&client_id, scan).await; - - assert_eq!(status.status, Phase::Requested); - - let status = fakes.simulate_stop_scan(&client_id, &id).await; - - assert_eq!(status.status, Phase::Stopped); - Ok(()) - } - - #[tokio::test] - async fn delete_scan_running() -> Result<(), Box> { - let mut fakes = Fakes::init().await; - let scan = fakes.success_scan(); - let client_id = client_id(); - - let (scan_id, _) = fakes.simulate_start_scan(&client_id, scan).await; - - let result = fakes - .entry - .delete_scans_id(fakes.internal_id(&client_id, &scan_id).await) - .await; - assert!(matches!(result, Err(DeleteScansIDError::Running))); - Ok(()) - } - - #[tokio::test] - async fn delete_scan() { - let mut fakes = Fakes::init().await; - let scan = fakes.success_scan(); - let (scan_id, _) = fakes.create_start_results(&client_id, scan).await; - let result = fakes - .entry - .get_scans_id_results(scan_id.clone(), None, None); - let result: Vec<_> = result.collect().await; - - let result: Vec<_> = result - .into_iter() - .filter_map(|x| x.ok()) - .map(|x| x.id) - .collect(); - assert!(!result.is_empty(), "expected results"); - fakes.entry.delete_scans_id(scan_id.clone()).await.unwrap(); - let result = fakes - .entry - .get_scans_id_results(scan_id.clone(), None, None); - let result: Vec<_> = result.collect().await; - - let result = result.len(); - assert_eq!(result, 0); - let count: i64 = query_scalar("SELECT count(id) FROM client_scan_map WHERE id = ?") - .bind(scan_id) - .fetch_one(&fakes.entry.pool) - .await - .unwrap(); - assert_eq!(count, 0); - } - - #[tokio::test] - async fn start_scan_succeeded() { - let mut fakes = Fakes::init().await; - let scan = fakes.success_scan(); - let (_, status) = fakes.create_start_results(&client_id, scan).await; - - let result = status.status; - assert_eq!(result, Phase::Succeeded); - } - - #[tokio::test] - // Regression for when no images got found and the scan sticked to requested. - async fn start_scan_failed_when_tag_resolution_returns_no_images() { - let mut fakes = Fakes::init().await; - let client_id = client_id(); - let mut registry = mockito::Server::new_async().await; - let _auth = - mock_registry_bearer_auth(&mut registry, "repository:nichtsfrei/victim:pull", 200); - let _tags = registry - .mock("GET", "/v2/nichtsfrei/victim/tags/list") - .with_status(200) - .with_header("Content-Type", "application/json") - .with_body(r#"{"name": "nichtsfrei/victim", "tags": []}"#) - .expect(1) - .create(); - - let scan = fakes.insecure_scan( - "empty-tag-resolution", - [format!( - "oci://{}/nichtsfrei/victim", - registry.host_with_port() - )], - ); - - let (_, status) = fakes.start_scan_and_run(&client_id, scan, 1).await; - - assert_eq!(status.status, Phase::Failed); - let host_info = status.host_info.expect("status should include host info"); - assert_eq!(host_info.all, 0); - assert_eq!(host_info.dead, 0); - assert_eq!(host_info.queued, 0); - } - - #[tokio::test] - async fn start_scan_failed_when_registry_authentication_returns_503() { - let mut fakes = Fakes::init_with_config(Fakes::config_without_retries()).await; - let client_id = client_id(); - let mut registry = mockito::Server::new_async().await; - let _auth = - mock_registry_bearer_auth(&mut registry, "repository:nichtsfrei/victim:pull", 503); - - let scan = fakes.insecure_scan( - "auth-503-during-scan", - [format!( - "oci://{}/nichtsfrei/victim:latest", - registry.host_with_port() - )], - ); - - let (_, status) = fakes.start_scan_and_run(&client_id, scan, 1).await; - - assert_eq!(status.status, Phase::Failed); - } - - #[tokio::test] - async fn start_scan_failed_when_blob_download_returns_503() { - let mut fakes = Fakes::init_with_config(Fakes::config_without_retries()).await; - let client_id = client_id(); - let mut image = DockerRegistryV2Mock::supported_images() - .into_iter() - .next() - .expect("expected at least one supported image"); - let registry = DockerRegistryV2Mock::serve_images( - &[image.clone()], - &[200, 200, 200, 200, 200, 200, 503], - ) - .await; - image.registry = registry.address().into(); - - let scan = fakes.insecure_scan("blob-503-during-scan", [image.to_string()]); - - let (_, status) = fakes.start_scan_and_run(&client_id, scan, 1).await; - - assert_eq!(status.status, Phase::Failed); - } - - #[tokio::test] - async fn get_scans() { - let entry = Fakes::init().await.entry; - for i in 0..10 { - let scan = models::Scan { - scan_id: i.to_string(), - ..Default::default() - }; - let client_id = if i % 2 == 0 { - client_id() - } else { - second_client_id() - }; - entry - .post_scans(client_id, scan) - .await - .expect("post scans should succeed"); - } - let result = entry.get_scans(client_id()); - - assert_eq!(result.filter_map(async move |x| x.ok()).count().await, 5); - let result = entry.get_scans(second_client_id()); - - assert_eq!(result.filter_map(async move |x| x.ok()).count().await, 5); - let result = entry.get_scans(ClientHash::from("third").to_string()); - assert_eq!(result.filter_map(async move |x| x.ok()).count().await, 0); - } - - #[tokio::test] - async fn get_scans_preferences() { - let entry = Fakes::init().await.entry; - let result = entry.get_scans_preferences().await; - - insta::assert_ron_snapshot!(result); - } - - mod results { - - use super::*; - - #[tokio::test] - async fn all() { - let mut fakes = Fakes::init().await; - let scan = fakes.success_scan(); - let (scan_id, _) = fakes.create_start_results(&client_id, scan).await; - let result = fakes.entry.get_scans_id_results(scan_id, None, None); - let result: Vec<_> = result.collect().await; - - let result: Vec<_> = result.into_iter().filter_map(|x| x.ok()).collect(); - - let internal: Vec<_> = result - .iter() - .filter(|x| { - x.oid.as_ref().map_or("", |x| x as &str) == "openvasd/container-image-scanner" - }) - .collect(); - // internal log messages per found host - assert_eq!( - internal.len(), - // best_os, best_os_cpe, hostname, architecture, - // packages, download, extract, scan, combined - // timings, host, start, host end per image - fakes.success_scan().target.hosts.len() * 11, - "Expected internal log messages" - ); - assert_eq!( - result - .iter() - .filter_map(|x| x.oid.as_ref()) - .filter(|x| x as &str != "openvasd/container-image-scanner") - .count(), - 275 * fakes.success_scan().target.hosts.len(), - "Expected found vulnerabilities" - ); - } - - #[tokio::test] - async fn subset() { - let mut fakes = Fakes::init().await; - let scan = fakes.success_scan(); - let (scan_id, _) = fakes.create_start_results(&client_id, scan).await; - let result = fakes - .entry - .get_scans_id_results(scan_id.clone(), None, None); - let all: Vec<_> = result.collect().await; - let all: Vec<_> = all.into_iter().filter_map(|x| x.ok()).collect(); - let all = all.len(); - let check_subset = async |range: (Option, Option)| { - let (start, end) = range; - let results = fakes.entry.get_scans_id_results(scan_id, start, end); - let results: Vec<_> = results.collect().await; - let results: Vec<_> = results.into_iter().filter_map(|x| x.ok()).collect(); - let or_all = |x| { - if x > all { None } else { Some(x) } - }; - let normalized_range = match range { - (None, Some(x)) => (None, or_all(x)), - (Some(x), None) => (or_all(x), None), - (Some(x), Some(y)) => { - // if start is higher then end we manipulate so that zero is the output - if x > y { - (Some(all), None) - } else { - (or_all(x), or_all(y)) - } - } - a => a, - }; - let expted_len = match normalized_range { - // we are inclusive - (Some(a), Some(b)) => b - a + 1, - (None, Some(b)) => b + 1, - (Some(a), None) => all - a, - (None, None) => all, - }; - let offset = start.unwrap_or(0); - assert_eq!(results.len(), expted_len); - for (i, x) in results.iter().enumerate() { - assert_eq!(i + offset, x.id, "expected matching result id") - } - }; - check_subset.clone()((Some(0), Some(5))).await; - check_subset.clone()((Some(5), None)).await; - check_subset.clone()((Some(5), Some(23))).await; - check_subset.clone()((None, Some(69))).await; - check_subset.clone()((Some(42), Some(4242))).await; - check_subset.clone()((Some(4242), Some(10))).await; - } - - #[tokio::test] - async fn single_result() { - let mut fakes = Fakes::init().await; - let scan = fakes.success_scan(); - let (scan_id, _) = fakes.create_start_results(&client_id, scan).await; - let result = fakes.entry.get_scans_id_results_id(scan_id, 42).await; - let result = result.map(|x| x.id).unwrap(); - assert_eq!(result, 42) - } - #[tokio::test] - async fn invalid_result_id() { - let mut fakes = Fakes::init().await; - let scan = fakes.success_scan(); - let (scan_id, _) = fakes.create_start_results(&client_id, scan).await; - let result = fakes.entry.get_scans_id_results_id(scan_id, 4242).await; - let result = result.map(|x| x.id); - assert!(matches!(result, Err(GetScansIDResultsIDError::NotFound))) - } - } -} diff --git a/rust/src/openvasd/container_image_scanner/endpoints/snapshots/openvasd__container_image_scanner__endpoints__scans__test__get_scans_preferences.snap b/rust/src/openvasd/container_image_scanner/endpoints/snapshots/openvasd__container_image_scanner__endpoints__scans__test__get_scans_preferences.snap deleted file mode 100644 index c276005cdd..0000000000 --- a/rust/src/openvasd/container_image_scanner/endpoints/snapshots/openvasd__container_image_scanner__endpoints__scans__test__get_scans_preferences.snap +++ /dev/null @@ -1,18 +0,0 @@ ---- -source: src/openvasd/container_image_scanner/endpoints/scans.rs -expression: result ---- -[ - ScanPreferenceInformation( - id: "accept_invalid_certs", - name: "Accepts certificates without trust chain verification", - default: true, - description: "This disables the CA chain verification for TLS certificates when connecting to a registry. This is useful for self-signed certificates.", - ), - ScanPreferenceInformation( - id: "registry_allow_insecure", - name: "Use HTTP instead of HTTPS", - default: false, - description: "This allows unencrypted communication with an registry (HTTP instead of HTTPS).", - ), -] diff --git a/rust/src/openvasd/container_image_scanner/endpoints/vts.rs b/rust/src/openvasd/container_image_scanner/endpoints/vts.rs deleted file mode 100644 index ba79aca2b7..0000000000 --- a/rust/src/openvasd/container_image_scanner/endpoints/vts.rs +++ /dev/null @@ -1,84 +0,0 @@ -use std::sync::{Arc, RwLock}; - -use crate::greenbone_scanner_framework::{GetVTsError, GetVts, entry::Prefixed}; -use futures::StreamExt; -use scannerlib::models::{self, FeedState}; - -use crate::vts::PluginFetcher; - -pub struct VTEndpoints { - fetcher: Box, - feed_state: Arc>, - prefix: &'static str, -} -impl Prefixed for VTEndpoints { - fn prefix(&self) -> &'static str { - self.prefix - } -} - -impl VTEndpoints { - pub fn new( - fetcher: T, - feed_state: Arc>, - prefix: Option<&'static str>, - ) -> Self - where - T: PluginFetcher + Send + Sync + 'static, - { - Self { - fetcher: Box::new(fetcher), - feed_state, - prefix: prefix.unwrap_or(""), - } - } -} - -impl GetVts for VTEndpoints { - fn get_oids( - &self, - _: String, - ) -> crate::greenbone_scanner_framework::StreamResult< - String, - crate::greenbone_scanner_framework::GetVTsError, - > { - let feed_state = self.feed_state.read().expect("Poison error"); - match &*feed_state { - FeedState::Unknown | FeedState::Syncing => Box::pin(futures::stream::iter(vec![Err( - GetVTsError::NotYetAvailable, - )])), - FeedState::Synced(_, _) => { - drop(feed_state); - Box::pin( - self.fetcher - .get_oids() - .map(|x| x.map_err(|e| GetVTsError::External(Box::new(e)))), - ) - } - } - } - - fn get_vts( - &self, - _: String, - ) -> crate::greenbone_scanner_framework::StreamResult< - models::VTData, - crate::greenbone_scanner_framework::GetVTsError, - > { - let feed_state = self.feed_state.read().expect("Poison error"); - match &*feed_state { - FeedState::Unknown | FeedState::Syncing => Box::pin(futures::stream::iter(vec![Err( - GetVTsError::NotYetAvailable, - )])), - FeedState::Synced(_, _) => { - // we drop earlier as we don't know how long the stream will be consumed - drop(feed_state); - Box::pin( - self.fetcher - .get_vts() - .map(|x| x.map_err(|e| GetVTsError::External(Box::new(e)))), - ) - } - } - } -} diff --git a/rust/src/openvasd/container_image_scanner/mod.rs b/rust/src/openvasd/container_image_scanner/mod.rs index 3ec2fe5659..f4277b8e53 100644 --- a/rust/src/openvasd/container_image_scanner/mod.rs +++ b/rust/src/openvasd/container_image_scanner/mod.rs @@ -1,6 +1,5 @@ pub mod config; mod detection; -pub mod endpoints; mod image; mod messages; mod notus; @@ -9,16 +8,11 @@ mod timings; pub use config::Config; pub(crate) use scannerlib::{ExternalError, PromiseRef, Streamer}; +pub use scheduling::db::scan::DBScan; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; -use crate::{ - container_image_scanner::scheduling::db::DataBase, database::sqlite::vts::SqlPluginStorage, - greenbone_scanner_framework::entry::Prefixed, -}; -use endpoints::scans::Scans; -use endpoints::vts::VTEndpoints; -use scannerlib::models::FeedState; +use crate::api::states::ScannerBridge; use scannerlib::notus::Notus; use scheduling::Scheduler; use sqlx::migrate::Migrator; @@ -26,11 +20,9 @@ use sqlx::migrate::Migrator; static MIGRATOR: Migrator = sqlx::migrate!("./src/openvasd/container_image_scanner/migrations"); pub async fn init( - vt_pool: DataBase, - feed_state: Arc>, products: Arc>, config: Config, -) -> Result<(Scans, VTEndpoints), Box> { +) -> anyhow::Result { let pool = config .database .create_pool("container-image-scanner") @@ -40,11 +32,674 @@ pub async fn init( let scheduler = Scheduler::init(config.into(), pool.clone(), products); tokio::spawn(scheduler.run()); - let scan = Scans { pool }; - let vts = VTEndpoints::new( - SqlPluginStorage::from(vt_pool), - feed_state, - Some(scan.prefix()), - ); - Ok((scan, vts)) + Ok(ScannerBridge::new(pool, None, None)) +} + +#[cfg(test)] +pub mod scans_utils { + + use std::{sync::Arc, time::Duration}; + + use tokio::sync::Mutex; + + use crate::{ + api::states::ScannerBridge, + container_image_scanner::{ + Config, MIGRATOR, + config::DBLocation, + image::{DockerRegistryV2Mock, RegistryPreference}, + scheduling::{Scheduler, db::DataBase}, + }, + }; + use scannerlib::models; + use scannerlib::notus::products_loader; + + async fn in_memory_scheduler_and_scan( + config: crate::container_image_scanner::Config, + ) -> (Scheduler, ScannerBridge) { + let pool = DataBase::connect(&DBLocation::InMemory.sqlite_address("test")) + .await + .expect("inmemory database must be available"); + + MIGRATOR + .run(&pool) + .await + .expect("need migrated database scheme"); + let products_path: &str = + concat!(env!("CARGO_MANIFEST_DIR"), "/examples/feed/notus/products"); + + let scheduler = Scheduler::init( + config.into(), + pool.clone(), + products_loader(products_path, false), + ); + let scans = ScannerBridge { + pool: pool.clone(), + crypter: None, + scheduler: None, + }; + (scheduler, scans) + } + + pub struct Fakes { + registry: DockerRegistryV2Mock, + pub entry: ScannerBridge, + pub scheduler: Scheduler, + } + + impl Fakes { + pub async fn internal_id(&self, client_id: &str, scan_id: &str) -> String { + self.entry + .get_scan_id(client_id, scan_id) + .await + .expect("scan must exist") + .to_string() + } + + pub async fn simulate_stop_scan( + &mut self, + client_id: &str, + scan_id: &str, + ) -> models::Status { + self.entry + .schedule_scan(client_id, scan_id, models::Action::Stop) + .await + .expect("scan must be scheduled to stop"); + + self.entry + .get_scan_status(client_id, scan_id) + .await + .expect("scan must have a status") + } + + pub async fn simulate_start_scan( + &mut self, + client_id: &str, + scan: models::Scan, + ) -> (String, models::Status) { + let scan_id = scan.scan_id.clone(); + self.entry + .post_scan(client_id, &scan) + .await + .expect("scan must be added"); + + self.entry + .schedule_scan(client_id, &scan_id, models::Action::Start) + .await + .expect("scan must be scheduled"); + ( + scan_id.clone(), + self.entry + .get_scan_status(client_id, &scan_id) + .await + .expect("scan must have a status"), + ) + } + + pub async fn init() -> Self { + Self::init_with_config(Config::default()).await + } + + pub async fn init_with_config(config: Config) -> Self { + let registry = DockerRegistryV2Mock::serve_default().await; + let (scheduler, entry) = in_memory_scheduler_and_scan(config).await; + + Self { + registry, + entry, + scheduler, + } + } + + async fn create_start_scan(&mut self, client_id: &str, scan: models::Scan) -> String { + let (scan_id, _) = self.simulate_start_scan(client_id, scan).await; + scan_id + } + + pub async fn run_scheduler_rounds(&self, rounds: usize) { + let conn = Arc::new(Mutex::new(self.scheduler.pool())); + for _ in 0..rounds { + Scheduler::start_scans( + self.scheduler.config(), + conn.clone(), + self.scheduler.products(), + ) + .await; + } + } + + pub async fn status_for_scan_id(&self, client_id: &str, scan_id: &str) -> models::Status { + self.entry + .get_scan_status(client_id, scan_id) + .await + .expect("scan must have a status") + } + + pub async fn start_scan_and_run( + &mut self, + client_id: &str, + scan: models::Scan, + rounds: usize, + ) -> (String, models::Status) { + let (scan_id, _) = self.simulate_start_scan(client_id, scan).await; + self.run_scheduler_rounds(rounds).await; + let status = self.status_for_scan_id(client_id, &scan_id).await; + (scan_id, status) + } + + pub async fn create_start_results( + &mut self, + client_id: &str, + scan: models::Scan, + ) -> (String, models::Status) { + let scans = scan.target.hosts.len(); + let scan_id = self.create_start_scan(client_id, scan).await; + self.run_scheduler_rounds(scans).await; + let id = self + .entry + .get_scan_id(client_id, &scan_id) + .await + .expect("scan must exist") + .to_string(); + let result = self + .entry + .get_scan_status(client_id, &scan_id) + .await + .expect("scan must have a status"); + + (id, result) + } + + pub fn insecure_scan(&self, scan_id: impl Into, hosts: I) -> models::Scan + where + I: IntoIterator, + H: Into, + { + models::Scan { + scan_id: scan_id.into(), + target: models::Target { + hosts: hosts.into_iter().map(Into::into).collect(), + ..Default::default() + }, + scan_preferences: vec![(RegistryPreference::Insecure.key(), "true").into()], + ..Default::default() + } + } + + pub fn success_scan(&self) -> models::Scan { + let credentials = vec![]; + let hosts = DockerRegistryV2Mock::supported_images() + .clone() + .into_iter() + .map(|mut x| { + x.registry = self.registry.address().into(); + x.to_string() + }) + .collect(); + + let target = models::Target { + hosts, + credentials, + ..Default::default() + }; + let scan_preferences = vec![(RegistryPreference::Insecure.key(), "true").into()]; + + models::Scan { + scan_id: uuid::Uuid::new_v4().to_string(), + target, + scan_preferences, + ..Default::default() + } + } + pub fn pool(&self) -> DataBase { + self.scheduler.pool() + } + + pub fn config_without_retries() -> Config { + let mut config = Config::default(); + config.image.scanning_retries = 0; + config.image.retry_timeout = Duration::from_millis(1); + config + } + + #[allow(dead_code)] + /// This is just a toggle to temporally use logging + fn init_logging() { + let filter = tracing_subscriber::filter::Targets::new() + .with_default(tracing::Level::WARN) + .with_target("greenbone_scanner_framework", tracing::Level::INFO) + .with_target("container_scanning", tracing::Level::TRACE); + let layer = tracing_subscriber::fmt::layer() + .with_span_events(tracing_subscriber::fmt::format::FmtSpan::FULL); + tracing_subscriber::util::SubscriberInitExt::init( + tracing_subscriber::layer::SubscriberExt::with( + tracing_subscriber::layer::SubscriberExt::with( + tracing_subscriber::registry(), + layer, + ), + filter, + ), + ); + } + } +} + +#[cfg(test)] +mod test { + use futures::StreamExt; + use scannerlib::models::{self, Phase}; + use sqlx::query_scalar; + + use crate::{ + api::error::ApiError, + container_image_scanner::{image::DockerRegistryV2Mock, scans_utils::Fakes}, + }; + + const CLIENT_ID: &str = "client1"; + + fn auth_header(registry: &mockito::ServerGuard) -> String { + format!( + r#"Bearer realm="http://{}/token""#, + registry.host_with_port() + ) + } + + fn mock_registry_bearer_auth( + registry: &mut mockito::ServerGuard, + scope: &str, + token_status: usize, + ) -> (mockito::Mock, mockito::Mock) { + let v2 = registry + .mock("GET", "/v2/") + .with_status(401) + .with_header("WWW-Authenticate", &auth_header(registry)) + .expect_at_least(1) + .create(); + let token = registry + .mock("GET", "/token") + .match_query(mockito::Matcher::UrlEncoded( + "scope".into(), + scope.to_owned(), + )) + .with_status(token_status) + .with_header("Content-Type", "application/json") + .with_body(r#"{"token": "waldfee"}"#) + .create(); + (v2, token) + } + + #[tokio::test] + async fn post_scan_double_id() { + let entry = Fakes::init().await.entry; + let scan = models::Scan { + scan_id: "test".to_owned(), + ..Default::default() + }; + entry + .post_scan(CLIENT_ID, &scan) + .await + .expect("scans must be added"); + let result = entry.post_scan(CLIENT_ID, &scan).await; + assert!( + matches!(result, Err(crate::database::dao::DAOError::DBViolation(_))), + "expected duplicate id result" + ); + } + + #[tokio::test] + async fn post_scan() { + let entry = Fakes::init().await.entry; + let hosts = vec!["oci://localhost/test/myimage".to_owned()]; + let credentials = vec![models::Credential { + credential_type: models::CredentialType::UP { + username: "me".to_owned(), + password: "password".to_owned(), + privilege: None, + }, + ..Default::default() + }]; + + let target = models::Target { + hosts, + credentials, + ..Default::default() + }; + let scan = models::Scan { + scan_id: "test".to_owned(), + target, + ..Default::default() + }; + entry + .post_scan(CLIENT_ID, &scan) + .await + .expect("scan must be added"); + let result = entry + .get_scan(CLIENT_ID, &scan.scan_id) + .await + .expect("scan must exist"); + assert_eq!(scan.scan_id, result.scan_id); + assert_eq!(scan.target.hosts, result.target.hosts); + } + + #[tokio::test] + async fn start_scan() -> Result<(), Box> { + let mut fakes = Fakes::init().await; + let scan = fakes.success_scan(); + + let (_, status) = fakes.simulate_start_scan(CLIENT_ID, scan).await; + assert_eq!(status.status, Phase::Requested); + Ok(()) + } + + #[tokio::test] + async fn stop_scan() -> Result<(), Box> { + let mut fakes = Fakes::init().await; + let scan = fakes.success_scan(); + + let (id, status) = fakes.simulate_start_scan(CLIENT_ID, scan).await; + + assert_eq!(status.status, Phase::Requested); + + let status = fakes.simulate_stop_scan(CLIENT_ID, &id).await; + + assert_eq!(status.status, Phase::Stopped); + Ok(()) + } + + #[tokio::test] + async fn delete_scan_running() -> Result<(), Box> { + let mut fakes = Fakes::init().await; + let scan = fakes.success_scan(); + + let (scan_id, _) = fakes.simulate_start_scan(CLIENT_ID, scan).await; + + let result = fakes.entry.delete_scan(CLIENT_ID, &scan_id).await; + assert!(matches!(result, Err(ApiError::ScanRunning))); + Ok(()) + } + + #[tokio::test] + async fn delete_scan() { + let mut fakes = Fakes::init().await; + let scan = fakes.success_scan(); + let scan_id = scan.scan_id.clone(); + let _ = fakes.create_start_results(CLIENT_ID, scan).await; + let result = fakes + .entry + .get_scan_results(CLIENT_ID, &scan_id, None, None); + let result: Vec<_> = result + .await + .expect("scan must have results") + .collect() + .await; + + let result: Vec<_> = result + .into_iter() + .filter_map(|x| x.ok()) + .map(|x| x.id) + .collect(); + assert!(!result.is_empty(), "expected results"); + + fakes + .entry + .delete_scan(CLIENT_ID, &scan_id) + .await + .expect("scan must be deleted"); + let result = fakes + .entry + .get_scan_results(CLIENT_ID, &scan_id, None, None) + .await; + assert!(matches!( + result, + Err(ApiError::Database(crate::database::dao::DAOError::NotFound)) + )); + + let count: i64 = query_scalar("SELECT count(id) FROM client_scan_map WHERE id = ?") + .bind(scan_id) + .fetch_one(&fakes.entry.pool) + .await + .expect("scan must be in the DB"); + assert_eq!(count, 0); + } + + #[tokio::test] + async fn start_scan_succeeded() { + let mut fakes = Fakes::init().await; + let scan = fakes.success_scan(); + let (_, status) = fakes.create_start_results(CLIENT_ID, scan).await; + + let result = status.status; + assert_eq!(result, Phase::Succeeded); + } + + #[tokio::test] + // Regression for when no images got found and the scan sticked to requested. + async fn start_scan_failed_when_tag_resolution_returns_no_images() { + let mut fakes = Fakes::init().await; + let mut registry = mockito::Server::new_async().await; + let _auth = + mock_registry_bearer_auth(&mut registry, "repository:nichtsfrei/victim:pull", 200); + let _tags = registry + .mock("GET", "/v2/nichtsfrei/victim/tags/list") + .with_status(200) + .with_header("Content-Type", "application/json") + .with_body(r#"{"name": "nichtsfrei/victim", "tags": []}"#) + .expect(1) + .create(); + + let scan = fakes.insecure_scan( + "empty-tag-resolution", + [format!( + "oci://{}/nichtsfrei/victim", + registry.host_with_port() + )], + ); + + let (_, status) = fakes.start_scan_and_run(CLIENT_ID, scan, 1).await; + + assert_eq!(status.status, Phase::Failed); + let host_info = status.host_info.expect("status should include host info"); + assert_eq!(host_info.all, 0); + assert_eq!(host_info.dead, 0); + assert_eq!(host_info.queued, 0); + } + + #[tokio::test] + async fn start_scan_failed_when_registry_authentication_returns_503() { + let mut fakes = Fakes::init_with_config(Fakes::config_without_retries()).await; + let mut registry = mockito::Server::new_async().await; + let _auth = + mock_registry_bearer_auth(&mut registry, "repository:nichtsfrei/victim:pull", 503); + + let scan = fakes.insecure_scan( + "auth-503-during-scan", + [format!( + "oci://{}/nichtsfrei/victim:latest", + registry.host_with_port() + )], + ); + + let (_, status) = fakes.start_scan_and_run(CLIENT_ID, scan, 1).await; + + assert_eq!(status.status, Phase::Failed); + } + + #[tokio::test] + async fn start_scan_failed_when_blob_download_returns_503() { + let mut fakes = Fakes::init_with_config(Fakes::config_without_retries()).await; + let mut image = DockerRegistryV2Mock::supported_images() + .into_iter() + .next() + .expect("expected at least one supported image"); + let registry = DockerRegistryV2Mock::serve_images( + &[image.clone()], + &[200, 200, 200, 200, 200, 200, 503], + ) + .await; + image.registry = registry.address().into(); + + let scan = fakes.insecure_scan("blob-503-during-scan", [image.to_string()]); + + let (_, status) = fakes.start_scan_and_run(CLIENT_ID, scan, 1).await; + + assert_eq!(status.status, Phase::Failed); + } + + #[tokio::test] + async fn get_scans() { + let client2_id = "client2"; + let entry = Fakes::init().await.entry; + for i in 0..10 { + let scan = models::Scan { + scan_id: i.to_string(), + ..Default::default() + }; + let client_id = if i % 2 == 0 { CLIENT_ID } else { client2_id }; + entry + .post_scan(client_id, &scan) + .await + .expect("post scans should succeed"); + } + let result = entry.get_scans(CLIENT_ID.to_string()).await; + assert_eq!(result.filter_map(async move |x| x.ok()).count().await, 5); + + let result = entry.get_scans(client2_id.to_string()).await; + assert_eq!(result.filter_map(async move |x| x.ok()).count().await, 5); + + let result = entry.get_scans("client3_id".to_string()).await; + assert_eq!(result.filter_map(async move |x| x.ok()).count().await, 0); + } + + mod results { + use super::*; + + #[tokio::test] + async fn all() { + let mut fakes = Fakes::init().await; + let scan = fakes.success_scan(); + let _ = fakes.create_start_results(CLIENT_ID, scan.clone()).await; + let result = fakes + .entry + .get_scan_results(CLIENT_ID, &scan.scan_id, None, None) + .await + .expect("scab must have results"); + let result: Vec<_> = result.collect().await; + + let result: Vec<_> = result.into_iter().filter_map(|x| x.ok()).collect(); + + let internal: Vec<_> = result + .iter() + .filter(|x| { + x.oid.as_ref().map_or("", |x| x as &str) == "openvasd/container-image-scanner" + }) + .collect(); + // internal log messages per found host + assert_eq!( + internal.len(), + // best_os, best_os_cpe, hostname, architecture, + // packages, download, extract, scan, combined + // timings, host, start, host end per image + fakes.success_scan().target.hosts.len() * 11, + "Expected internal log messages" + ); + assert_eq!( + result + .iter() + .filter_map(|x| x.oid.as_ref()) + .filter(|x| x as &str != "openvasd/container-image-scanner") + .count(), + 275 * fakes.success_scan().target.hosts.len(), + "Expected found vulnerabilities" + ); + } + + #[tokio::test] + async fn subset() { + let mut fakes = Fakes::init().await; + let scan = fakes.success_scan(); + let _ = fakes.create_start_results(CLIENT_ID, scan.clone()).await; + let result = fakes + .entry + .get_scan_results(CLIENT_ID, &scan.scan_id, None, None) + .await + .expect("scan must have results"); + + let all: Vec<_> = result.collect().await; + let all: Vec<_> = all.into_iter().filter_map(|x| x.ok()).collect(); + let all = all.len(); + + let check_subset = async |range: (Option, Option)| { + let (start, end) = range; + let results = fakes + .entry + .get_scan_results(CLIENT_ID, &scan.scan_id, start, end) + .await + .expect("scan must have results"); + let results: Vec<_> = results.collect().await; + let results: Vec<_> = results.into_iter().filter_map(|x| x.ok()).collect(); + let or_all = |x| { + if x > all { None } else { Some(x) } + }; + let normalized_range = match range { + (None, Some(x)) => (None, or_all(x)), + (Some(x), None) => (or_all(x), None), + (Some(x), Some(y)) => { + // if start is higher then end we manipulate so that zero is the output + if x > y { + (Some(all), None) + } else { + (or_all(x), or_all(y)) + } + } + a => a, + }; + let expted_len = match normalized_range { + // we are inclusive + (Some(a), Some(b)) => b - a + 1, + (None, Some(b)) => b + 1, + (Some(a), None) => all - a, + (None, None) => all, + }; + let offset = start.unwrap_or(0); + assert_eq!(results.len(), expted_len); + for (i, x) in results.iter().enumerate() { + assert_eq!(i + offset, x.id, "expected matching result id") + } + }; + check_subset((Some(0), Some(5))).await; + check_subset((Some(5), None)).await; + check_subset((Some(5), Some(23))).await; + check_subset((None, Some(69))).await; + check_subset((Some(42), Some(4242))).await; + check_subset((Some(4242), Some(10))).await; + } + + #[tokio::test] + async fn single_result() { + let mut fakes = Fakes::init().await; + let scan = fakes.success_scan(); + let _ = fakes.create_start_results(CLIENT_ID, scan.clone()).await; + let result = fakes + .entry + .get_scan_result(CLIENT_ID, &scan.scan_id, 42) + .await; + let result = result.map(|x| x.id).expect("scan must have results"); + assert_eq!(result, 42) + } + #[tokio::test] + async fn invalid_result_id() { + let mut fakes = Fakes::init().await; + let scan = fakes.success_scan(); + let _ = fakes.create_start_results(CLIENT_ID, scan.clone()).await; + let result = fakes + .entry + .get_scan_result(CLIENT_ID, &scan.scan_id, 4242) + .await; + let result = result.map(|x| x.id); + assert!(matches!( + result, + Err(ApiError::Database(crate::database::dao::DAOError::NotFound)) + )) + } + } } diff --git a/rust/src/openvasd/container_image_scanner/scheduling/db/sqlite/images.rs b/rust/src/openvasd/container_image_scanner/scheduling/db/sqlite/images.rs index 5f8625692c..a3a869fd15 100644 --- a/rust/src/openvasd/container_image_scanner/scheduling/db/sqlite/images.rs +++ b/rust/src/openvasd/container_image_scanner/scheduling/db/sqlite/images.rs @@ -251,8 +251,8 @@ mod test { use crate::{ container_image_scanner::{ - endpoints::scans::scans_utils::Fakes, image::{Image, ImageState, RegistryError}, + scans_utils::Fakes, scheduling::db::{images::DBImages, scan::DBScan}, }, database::dao::Execute, diff --git a/rust/src/openvasd/database/sqlite/scans.rs b/rust/src/openvasd/database/sqlite/scans.rs index 3155925385..e85c060442 100644 --- a/rust/src/openvasd/database/sqlite/scans.rs +++ b/rust/src/openvasd/database/sqlite/scans.rs @@ -3,12 +3,12 @@ use scannerlib::models::{self, AliveTestMethods}; use sqlx::{Connection, Row, Sqlite, SqlitePool, query, query_scalar, sqlite::SqliteRow}; use crate::{ + api::InternalIdentifier, crypt::{self, Crypt, Encrypted}, database::{ dao::{DAOError, DAOHandler, DAOPromiseRef, DAOStreamer, Execute, Fetch, StreamFetch}, sqlite::{DataBase, OpenVASDDB, insert_values_chunked, state_change}, }, - greenbone_scanner_framework::InternalIdentifier, }; pub type ScanDB<'o, T> = OpenVASDDB<'o, T>; diff --git a/rust/src/openvasd/database/sqlite/state_change.rs b/rust/src/openvasd/database/sqlite/state_change.rs index 4b9ebe2209..af68d79d5d 100644 --- a/rust/src/openvasd/database/sqlite/state_change.rs +++ b/rust/src/openvasd/database/sqlite/state_change.rs @@ -243,7 +243,7 @@ mod tests { use crate::scans::tests::{create_pool, prepare_scans}; #[tokio::test] - async fn set_single_state() -> crate::Result<()> { + async fn set_single_state() -> anyhow::Result<()> { let (config, pool) = create_pool().await?; let under_test = ScanStateController::init(pool.clone()).await?; @@ -255,7 +255,7 @@ mod tests { } #[tokio::test] - async fn set_all_scans() -> crate::Result<()> { + async fn set_all_scans() -> anyhow::Result<()> { let (config, pool) = create_pool().await?; let under_test = ScanStateController::init(pool.clone()).await?; let scans = prepare_scans(pool, &config).await; diff --git a/rust/src/openvasd/database/sqlite/vts.rs b/rust/src/openvasd/database/sqlite/vts.rs index d932e1e52e..b60e9554fb 100644 --- a/rust/src/openvasd/database/sqlite/vts.rs +++ b/rust/src/openvasd/database/sqlite/vts.rs @@ -15,8 +15,8 @@ use sqlx::SqlitePool; use sqlx::query; use sqlx::sqlite::SqliteRow; +use crate::api::StreamResult; use crate::config::Config; -use crate::greenbone_scanner_framework::{GetVTsError, StreamResult}; use crate::vts::FeedHash; use crate::vts::PluginFetcher; use crate::vts::PluginStorer; @@ -88,14 +88,13 @@ impl PluginStorer for SqlPluginStorage { let pool = self.pool.clone(); let typus = hash.typus; Box::pin(async move { - let json = serde_json::to_vec(&plugin).map_err(error_vts_error)?; + let json = serde_json::to_vec(&plugin)?; query(r#" INSERT INTO plugins ( oid, json_blob, feed_type) VALUES (?, ?, ?)"#) .bind(plugin.oid()) .bind(&json) .bind(typus.as_ref()) .execute(&pool) - .await - .map_err(error_vts_error)?; + .await?; Ok(()) }) @@ -115,8 +114,7 @@ impl PluginStorer for SqlPluginStorage { .bind(path) .bind(ht.as_ref()) .execute(&pool) - .await - .map_err(error_vts_error)?; + .await?; Ok(()) }) } @@ -170,13 +168,6 @@ impl Retriever for SqlPluginStorage { } } -fn error_vts_error(error: T) -> GetVTsError -where - T: std::error::Error + Sync + Send + 'static, -{ - GetVTsError::External(Box::new(error)) -} - impl orchestrator::Worker for FeedSynchronizer { fn cached_hashes(&self) -> Promise, orchestrator::WorkerError>> { let mut fetched = @@ -254,15 +245,16 @@ mod tests { use std::sync::{Arc, RwLock}; - use crate::container_image_scanner::endpoints::vts::VTEndpoints; - use crate::greenbone_scanner_framework::{GetVTsError, GetVts}; + use crate::api::{error::ApiError, states::Feed}; use scannerlib::models::FeedState; + use futures_util::StreamExt; + use crate::setup_sqlite; use super::*; - async fn create_pool() -> crate::Result<(Config, SqlitePool)> { + async fn create_pool() -> anyhow::Result<(Config, SqlitePool)> { let nasl = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/feed/nasl").into(); let notus = concat!( env!("CARGO_MANIFEST_DIR"), @@ -290,25 +282,14 @@ mod tests { } #[tokio::test] - async fn get_oids() -> crate::Result<()> { + async fn get_oids() -> anyhow::Result<()> { let (config, pool) = create_pool().await?; let feed_state = Arc::new(RwLock::new(FeedState::default())); - let endpoint = VTEndpoints::new( - SqlPluginStorage::from(pool.clone()), - feed_state.clone(), - None, - ); + let synchronizer = FeedSynchronizer::new(pool.clone(), &config); + let feed = Feed::new(SqlPluginStorage::from(pool.clone()), feed_state.clone()); - let oids = endpoint.get_oids("moep".into()).collect::>().await; - assert_eq!(oids.len(), 1); - assert_eq!( - oids.into_iter() - .filter_map(|x| x.err()) - .filter(|x| matches!(x, GetVTsError::NotYetAvailable)) - .count(), - 1 - ); + assert!(matches!(feed.get_oids(), Err(ApiError::FeedNotSynced))); orchestrator::test::verify_allowed_for( synchronizer, @@ -319,11 +300,11 @@ mod tests { // in the case that examples are changed, I don't want to change this test each time hence // we just verify if we got oids. - let oids = endpoint.get_oids("moep".into()).collect::>().await; + let oids = feed.get_oids()?.collect::>().await; let oids = oids.into_iter().filter_map(|x| x.ok()).collect::>(); assert!(!oids.is_empty()); - let vts = endpoint.get_vts("moep".into()).collect::>().await; + let vts = feed.get_vts()?.collect::>().await; let vts = vts.into_iter().filter_map(|x| x.ok()).collect::>(); assert!(!vts.is_empty()); Ok(()) diff --git a/rust/src/openvasd/greenbone_scanner_framework/delete_scans_id.rs b/rust/src/openvasd/greenbone_scanner_framework/delete_scans_id.rs deleted file mode 100644 index 5aafb79323..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/delete_scans_id.rs +++ /dev/null @@ -1,218 +0,0 @@ -use std::{pin::Pin, sync::Arc}; - -use hyper::StatusCode; - -use crate::auth_method_segments; -use crate::greenbone_scanner_framework::{ - MapScanID, - entry::{ - self, Bytes, Method, Prefixed, RequestHandler, enforce_client_id_and_scan_id, - response::BodyKind, - }, - post_scans_id::PostScansIDError, -}; - -pub trait DeleteScansId: MapScanID + Prefixed { - fn delete_scans_id( - &self, - id: String, - ) -> Pin> + Send + '_>>; -} - -pub struct DeleteScansIdHandler { - handler: Arc, -} - -impl Prefixed for DeleteScansIdHandler -where - S: Prefixed + 'static, -{ - fn prefix(&self) -> &'static str { - self.handler.prefix() - } -} - -impl RequestHandler for DeleteScansIdHandler -where - S: DeleteScansId + 'static, -{ - auth_method_segments!( - authenticated: true, - Method::DELETE, - "scans", "*" - ); - - fn call<'a, 'b>( - &'b self, - client_id: Arc, - uri: &'a entry::Uri, - _: Bytes, - ) -> Pin + Send>> - where - 'b: 'a, - { - let gsp = self.handler.clone(); - let id = self - .ids(uri) - .into_iter() - .next() - .expect("expect ID, this is a toolkit error"); - Box::pin(async move { - enforce_client_id_and_scan_id(&client_id, id, gsp.as_ref(), async |id| { - match gsp.delete_scans_id(id).await { - Ok(()) => BodyKind::no_content(StatusCode::NO_CONTENT), - Err(e) => e.into(), - } - }) - .await - }) - } -} - -impl From for DeleteScansIdHandler -where - T: DeleteScansId + 'static, -{ - fn from(value: T) -> Self { - DeleteScansIdHandler { - handler: Arc::new(value), - } - } -} - -impl From> for DeleteScansIdHandler -where - T: DeleteScansId + 'static, -{ - fn from(value: Arc) -> Self { - DeleteScansIdHandler { handler: value } - } -} - -pub type DeleteScansIDError = PostScansIDError; - -#[cfg(test)] -mod tests { - use entry::test_utilities::{self}; - use http_body_util::Empty; - use hyper::{Method, Request, service::Service}; - - use super::*; - use crate::{ - create_single_handler, - greenbone_scanner_framework::{Authentication, entry::ClientHash}, - }; - - struct Test {} - - impl MapScanID for Test { - fn contains_scan_id<'a>( - &'a self, - client_id: &'a str, - scan_id: &'a str, - ) -> Pin> + Send + 'a>> { - Box::pin(async move { - if scan_id == "id" { - Some(client_id.to_string()) - } else { - None - } - }) - } - } - - impl Prefixed for Test { - fn prefix(&self) -> &'static str { - "" - } - } - - impl DeleteScansId for Test { - fn delete_scans_id( - &self, - id: String, - ) -> Pin> + Send>> { - let client_id = id.clone(); - let ok = ClientHash::from("ok").to_string(); - let already_running = ClientHash::from("already_running").to_string(); - Box::pin(async move { - if client_id == ok { - return Ok(()); - } - if client_id == already_running { - return Err(DeleteScansIDError::Running); - } - - Err(std::io::Error::new(std::io::ErrorKind::NotFound, "").into()) - }) - } - } - - #[tokio::test] - async fn internal_server_error() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(DeleteScansIdHandler::from(Test {})), - Some(ClientHash::from("internal_server_error")), - ); - - let req = Request::builder() - .uri("/scans/id") - .method(Method::DELETE) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); - } - - #[tokio::test] - async fn missing_scan() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(DeleteScansIdHandler::from(Test {})), - Some(ClientHash::from("ok")), - ); - - let req = Request::builder() - .uri("/scans/not_found") - .method(Method::DELETE) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - } - - #[tokio::test] - async fn already_running() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(DeleteScansIdHandler::from(Test {})), - Some(ClientHash::from("already_running")), - ); - - let req = Request::builder() - .uri("/scans/id") - .method(Method::DELETE) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::CONFLICT); - } - - #[tokio::test] - async fn ok() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(DeleteScansIdHandler::from(Test {})), - Some(ClientHash::from("ok")), - ); - - let req = Request::builder() - .uri("/scans/id") - .method(Method::DELETE) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::NO_CONTENT); - } -} diff --git a/rust/src/openvasd/greenbone_scanner_framework/entry/mod.rs b/rust/src/openvasd/greenbone_scanner_framework/entry/mod.rs deleted file mode 100644 index e44ed1e1b9..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/entry/mod.rs +++ /dev/null @@ -1,767 +0,0 @@ -//! Entry contains every module struct which is considered and entry point -//! -//! An entry point handles each incoming request, it checks if an endpoint -//! requires client-id/api-key and adds all required header information for each -//! response. - -use std::{ - convert::Infallible, - fmt::Display, - pin::Pin, - sync::{ - Arc, - atomic::{AtomicUsize, Ordering}, - }, -}; -pub mod response; - -use hyper::{StatusCode, header::HeaderValue}; -use response::{BodyKind, BodyKindContent}; -use scannerlib::models; - -#[derive(Clone, Default, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct ClientHash([u8; 32]); - -impl From for ClientHash -where - T: AsRef<[u8]>, -{ - fn from(value: T) -> Self { - use sha2::{Digest, Sha256}; - let mut hasher = Sha256::new(); - hasher.update(value); - let hash = hasher.finalize(); - Self(hash.into()) - } -} - -impl Display for ClientHash { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - self.0 - .iter() - .fold(String::with_capacity(self.0.len() * 2), |mut a, x| { - a.push_str(&format!("{x:02x}")); - a - }) - ) - } -} - -/// Contains information about an authorization model of a connection (e.g. mtls) -#[derive(Default, Debug, Clone)] -pub enum ClientIdentifier { - /// When there in no information available - #[default] - Unknown, - /// Contains a hashed number of an identifier - /// - /// We uses the identifier as a key for results. This key is usually calculated by an - /// subject of a known client certificate or by creating a sha256 sum of the used API key. - Known(ClientHash), -} - -// pub type Request = hyper::Request; -pub type Uri = hyper::Uri; -pub type Bytes = hyper::body::Bytes; -pub type Method = hyper::Method; - -#[macro_export] -macro_rules! auth_method_segments { - (authenticated: $authn:expr, $method:expr, $($path:literal),*) => { - fn needs_authentication( - &self, - ) -> bool { - $authn - } - - fn path_segments(&self) -> &'static [&'static str] { - &[ $( $path, )* ] - } - - fn http_method(&self) -> $crate::greenbone_scanner_framework::entry::Method { - $method - } - - }; -} - -pub trait Prefixed { - fn prefix(&self) -> &'static str; -} - -pub trait RequestHandler: Prefixed { - fn needs_authentication(&self) -> bool; - fn path_segments(&self) -> &'static [&'static str]; - fn http_method(&self) -> Method; - fn ids(&self, uri: &Uri) -> Vec { - if Self::prefix(self).is_empty() { - uri.path() - .split('/') - .filter(|x| !x.is_empty()) - .zip(self.path_segments().iter()) - .filter(|(_, x)| x == &&"*") - .map(move |(x, _)| x.to_owned()) - .collect() - } else { - uri.path() - .split('/') - .filter(|x| !x.is_empty()) - .skip(1) - .zip(self.path_segments().iter()) - .filter(|(_, x)| x == &&"*") - .map(move |(x, _)| x.to_owned()) - .collect() - } - } - - fn call<'a, 'b>( - &'b self, - client_id: Arc, - uri: &'a Uri, - body: Bytes, - ) -> Pin + Send>> - where - 'b: 'a; -} - -/// Will be called after the authorization and sanity checks are done. -/// -/// It contains all the RequestHandler implementations and finds handler -/// with matching path, method and authorization and calls it. -#[derive(Default, Clone)] -pub struct RequestHandlers { - pub handlers: Vec>>, -} - -#[macro_export] -macro_rules! create_single_handler { - ($($path:expr),*) => {{ - let mut ir = $crate::greenbone_scanner_framework::RequestHandlers::default(); - $( - ir.push($path); - )* - ir - - - }}; -} - -fn segments_match(prefix: &str, handler_parts: &[&str], request_parts: &[&str]) -> bool { - let offset = if !prefix.is_empty() { - if handler_parts.len() as i32 != request_parts.len() as i32 - 1 - || prefix != request_parts[0] - { - return false; - } - 1 - } else { - if handler_parts.len() != request_parts.len() { - return false; - } - 0 - }; - - for (i, p) in handler_parts.iter().enumerate() { - if p == &"*" { - continue; - } - - if p != &request_parts[i + offset] { - return false; - } - } - true -} - -type BodyKindFuture = Pin + Send>>; - -impl RequestHandlers { - pub fn push(&mut self, request_handler: T) - where - T: RequestHandler + Send + Sync + 'static, - { - self.handlers.push(Arc::new( - Box::new(request_handler) as Box - )); - } - - fn call( - &self, - client_identifier: Arc, - req: hyper::Request, - ) -> BodyKindFuture - where - R: hyper::body::Body + Send + 'static, - ::Error: std::error::Error, - ::Data: Send, - { - let callbacks = self.handlers.clone(); - - Box::pin(async move { - let segments = req - .uri() - .path() - .split('/') - // handles double slashes e.g. /scans/ or /scans//id////results - .filter(|x| !x.is_empty()) - .collect::>(); - - for rh in callbacks { - if segments_match(rh.prefix(), rh.path_segments(), &segments) { - let needs_authentication = rh.needs_authentication(); - let is_authenticated = - matches!(&*client_identifier, &ClientIdentifier::Known(_)); - if !needs_authentication || is_authenticated { - tracing::debug!( - "Handling request: {}/{}", - rh.prefix(), - rh.path_segments().join("/") - ); - - if (req.method() == Method::HEAD - && rh.path_segments()[0] == "scans" - && is_authenticated) - || (req.method() == Method::HEAD && rh.path_segments()[0] != "scans") - { - return BodyKind::no_content(StatusCode::OK); - }; - - if req.method() == rh.http_method() { - let uri = req.uri().clone(); - let body = req.into_body(); - let bytes = match body.collect().await { - Ok(x) => x.to_bytes(), - Err(e) => { - return internal_server_error!(e); - } - }; - - return rh.call(client_identifier, &uri, bytes).await; - } - } else { - return BodyKind::no_content(StatusCode::UNAUTHORIZED); - } - } - } - - BodyKind::no_content(StatusCode::NOT_FOUND) - }) - } -} - -pub struct EntryPoint { - scanner: Arc, - handlers: Arc, - client_identifier: Arc, - max_connections: usize, - counter: Arc, -} - -impl EntryPoint { - pub fn new( - scanner: Arc, - client_identifier: Arc, - handlers: Arc, - max_connections: usize, - counter: Arc, - ) -> EntryPoint { - EntryPoint { - max_connections, - scanner, - client_identifier, - handlers, - counter, - } - } -} - -use http_body_util::BodyExt; - -fn api_key_to_client_identifier( - api_keys: &[String], - header: Option<&HeaderValue>, -) -> ClientIdentifier { - let used_key = match header { - Some(x) => match x.to_str() { - Ok(y) => y.to_owned(), - Err(e) => { - tracing::debug!(error=%e, "header contains invalid ascii symbol"); - "".to_owned() - } - }, - None => "".to_owned(), - }; - // we iterate through each time so that the time on success and failure is relatively equal - let mut result = ClientIdentifier::Unknown; - for x in api_keys { - if x == &used_key { - result = ClientIdentifier::Known(x.into()); - } - } - tracing::debug!( - known_api_key = matches!(result, ClientIdentifier::Known(_)), - "has used known api key" - ); - result -} - -use super::{Authentication, MapScanID}; -use crate::internal_server_error; - -impl hyper::service::Service> for EntryPoint -where - R: hyper::body::Body + Send + 'static, - ::Error: std::error::Error, - ::Data: Send, -{ - type Response = hyper::Response; - - type Error = Infallible; - - type Future = - Pin> + Send>>; - - fn call(&self, req: hyper::Request) -> Self::Future { - let cbs = self.scanner.clone(); - let cid = match &self.scanner.authentication { - Authentication::Disabled => Arc::new(ClientIdentifier::Known(Default::default())), - Authentication::Mtls => self.client_identifier.clone(), - Authentication::ApiKey(keys) => Arc::new(api_key_to_client_identifier( - keys, - req.headers().get("x-api-key"), - )), - }; - let feed_version = match &*cbs.feed_version.read().unwrap() { - models::FeedState::Unknown => "unavailable".to_string(), - models::FeedState::Syncing => "unavailable".to_string(), - models::FeedState::Synced(vt, adv) => format!("{vt}{adv}"), - }; - let rb = hyper::Response::builder() - .header("authentication", cbs.authentication.static_str()) - .header("api-version", &cbs.api_version) - .header("feed-version", &feed_version); - let incoming = self.handlers.clone(); - - let acc = self.counter.clone(); - let max_connections = self.max_connections; - Box::pin(async move { - if acc.load(Ordering::Relaxed) > max_connections { - tracing::trace!("Too many open connections, returning 503"); - return Ok(rb - .header("Retry-After", 10) - .status(StatusCode::SERVICE_UNAVAILABLE) - .body(BodyKindContent::Empty) - .unwrap()); - } - - let current = acc.fetch_add(1, Ordering::Relaxed); - tracing::trace!(current, max_connections, "handling request"); - let resp = incoming.call(cid, req).await; - let rb = match &resp.content { - BodyKindContent::Empty => rb, - BodyKindContent::Binary(x) => rb - .header("Content-Type", "application/json") - .header("Content-Length", x.len()), - BodyKindContent::BinaryStream(_) => rb.header("Content-Type", "application/json"), - }; - let current = acc.fetch_sub(1, Ordering::Relaxed); - tracing::trace!(current, max_connections, "releasing request"); - - Ok(rb.status(resp.status_code).body(resp.content).unwrap()) - }) - } -} - -pub(crate) fn enforce_client_hash(client_id: &Arc) -> &ClientHash { - match client_id.as_ref() { - super::ClientIdentifier::Unknown => { - unreachable!("get_scans is marked as authenticated, no unknown clients should pass") - } - super::ClientIdentifier::Known(client_hash) => client_hash, - } -} - -pub async fn enforce_client_id_and_scan_id( - client_id: &Arc, - scan_id: String, - verifier: &T, - f: F, -) -> BodyKind -where - T: MapScanID, - F: Fn(String) -> Fut, - Fut: std::future::Future, -{ - let client = enforce_client_hash(client_id).to_string(); - if let Some(id) = verifier.contains_scan_id(&client, &scan_id).await { - f(id).await - } else { - BodyKind::no_content(StatusCode::NOT_FOUND) - } -} - -#[cfg(test)] -pub mod test_utilities { - use std::{ - pin::Pin, - sync::{Arc, RwLock}, - }; - - use http_body_util::{Empty, Full}; - use hyper::{Request, body::Bytes}; - - use super::Authentication; - use super::{ClientHash, ClientIdentifier, EntryPoint, Method, RequestHandlers}; - use crate::greenbone_scanner_framework::Scanner; - use scannerlib::models::FeedState; - - pub fn entry_point( - authentication: Authentication, - handlers: RequestHandlers, - client_hash: Option, - ) -> EntryPoint { - let configuration = Arc::new(Scanner { - api_version: "test".to_owned(), - authentication, - feed_version: Arc::new(RwLock::new(FeedState::Synced( - "vt".to_string(), - "advisories".to_string(), - ))), - }); - - let client_identifier = Arc::new(match client_hash { - Some(x) => ClientIdentifier::Known(x), - None => ClientIdentifier::Unknown, - }); - let ir = Arc::new(handlers); - EntryPoint::new(configuration, client_identifier, ir, 10, Default::default()) - } - - pub fn empty_request(method: Method, uri: &str) -> Request> { - Request::builder() - .uri(uri) - .method(method) - .body(Empty::::new()) - .unwrap() - } - - pub fn json_request(method: Method, uri: &str, value: &T) -> Request> - where - T: serde::Serialize, - { - Request::builder() - .uri(uri) - .method(method) - .body(json_bytes(value)) - .unwrap() - } - - pub fn json_bytes(value: &T) -> Full - where - T: serde::Serialize, - { - let json = serde_json::to_vec(value).expect("value should be serializable"); - Full::from(json) - } - - pub fn on_client_id_return( - client_id: String, - f: A, - e: B, - ) -> Pin> + Send>> - where - A: Sync + Send + 'static, - B: From + Sync + Send + 'static, - { - let client_id = client_id.clone(); - let ok = ClientHash::from("ok").to_string(); - let not_found = ClientHash::from("not_found").to_string(); - Box::pin(async move { - if client_id == ok { - return Ok(f); - } - if client_id == not_found { - return Err(e); - } - - Err(std::io::Error::new(std::io::ErrorKind::NotFound, "").into()) - }) - } -} -#[cfg(test)] -mod tests { - use http_body_util::Empty; - use hyper::{Request, header::HeaderValue, service::Service}; - - use super::*; - - struct IdPart {} - - impl Prefixed for IdPart { - fn prefix(&self) -> &'static str { - "" - } - } - - impl RequestHandler for IdPart { - auth_method_segments!(authenticated: true, Method::GET, "test", "id", "*"); - fn call<'a, 'b>( - &'b self, - _: Arc, - uri: &'a Uri, - _: Bytes, - ) -> Pin + Send>> - where - 'b: 'a, - { - let ids = self.ids(uri); - Box::pin(async move { - BodyKind::json_content(StatusCode::OK, &ids.first().unwrap().to_owned()) - }) - } - } - struct Authenticated {} - impl Prefixed for Authenticated { - fn prefix(&self) -> &'static str { - "" - } - } - - impl RequestHandler for Authenticated { - auth_method_segments!(authenticated: true, Method::GET, "test", "authn"); - fn call<'a, 'b>( - &'b self, - _: Arc, - _: &'a Uri, - _: Bytes, - ) -> Pin + Send>> - where - 'b: 'a, - { - Box::pin( - async move { BodyKind::json_content(StatusCode::OK, &"test_response".to_owned()) }, - ) - } - } - - struct NotAuthenticated {} - - impl Prefixed for NotAuthenticated { - fn prefix(&self) -> &'static str { - "" - } - } - - impl RequestHandler for NotAuthenticated { - auth_method_segments!(authenticated: false, Method::GET, "test", "not_authn"); - fn call<'a, 'b>( - &'b self, - _: Arc, - _: &'a Uri, - _: Bytes, - ) -> Pin + Send>> - where - 'b: 'a, - { - Box::pin(async move { BodyKind::no_content(StatusCode::OK) }) - } - } - - #[tokio::test] - async fn contains_header_information_on_head() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(Authenticated {}), - Some(ClientHash::default()), - ); - let req = test_utilities::empty_request(Method::HEAD, "/test////authn//////"); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - let headers = resp.headers(); - assert_eq!( - headers.get("authentication").unwrap(), - HeaderValue::from_static("mTLS") - ); - assert_eq!( - headers.get("api-version").unwrap(), - HeaderValue::from_static("test") - ); - - assert_eq!( - headers.get("feed-version").unwrap(), - HeaderValue::from_static("vtadvisories") - ); - - let resp = resp.into_body().collect().await.unwrap().to_bytes(); - assert!(resp.is_empty()); - } - - #[tokio::test] - async fn contains_content_type_and_len() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(IdPart {}), - Some(ClientHash::default()), - ); - let req = test_utilities::empty_request(Method::GET, "/test/id/itsame"); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!( - resp.headers().get("Content-Type").unwrap(), - HeaderValue::from_static("application/json") - ); - - assert_eq!( - resp.headers().get("Content-Length").unwrap(), - HeaderValue::from_static("8") - ); - } - - #[tokio::test] - async fn id_path() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(IdPart {}), - Some(ClientHash::default()), - ); - let req = test_utilities::empty_request(Method::GET, "/test/id/itsame"); - let resp = entry_point.call(req).await.unwrap(); - let resp = resp.into_body().collect().await.unwrap().to_bytes(); - - let returned = String::from_utf8_lossy(resp.as_ref()); - assert_eq!(returned, "\"itsame\""); - } - #[tokio::test] - async fn id_path_missing_id() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(IdPart {}), - Some(ClientHash::default()), - ); - let req = test_utilities::empty_request(Method::GET, "/test/id///"); - let resp = entry_point.call(req).await.unwrap(); - - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - } - - #[tokio::test] - async fn not_found() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(Authenticated {}, NotAuthenticated {}), - Some(ClientHash::default()), - ); - let req = test_utilities::empty_request(Method::POST, "/test/authn"); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - } - - #[tokio::test] - async fn missing_client_id_on_mtls() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(Authenticated {}), - None, - ); - - let req = test_utilities::empty_request(Method::HEAD, "/test/authn"); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); - } - - #[tokio::test] - async fn missing_api_key() { - let entry_point = test_utilities::entry_point( - Authentication::ApiKey(vec!["test".to_owned()]), - create_single_handler!(Authenticated {}), - Some(Default::default()), - ); - - let req = test_utilities::empty_request(Method::HEAD, "/test/authn"); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); - } - - #[tokio::test] - async fn api_key() { - let entry_point = test_utilities::entry_point( - Authentication::ApiKey(vec!["test".to_owned()]), - create_single_handler!(Authenticated {}), - Some(Default::default()), - ); - - let req = Request::builder() - .uri("/test/authn") - .header("x-api-key", "test") - .method(Method::HEAD) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - - assert_eq!(resp.status(), StatusCode::OK); - } - - struct PrefixedAuth {} - impl Prefixed for PrefixedAuth { - fn prefix(&self) -> &'static str { - "achso" - } - } - - impl RequestHandler for PrefixedAuth { - auth_method_segments!(authenticated: true, Method::GET, "test", "wtf"); - fn call<'a, 'b>( - &'b self, - _: Arc, - _: &'a Uri, - _: Bytes, - ) -> Pin + Send>> - where - 'b: 'a, - { - Box::pin( - async move { BodyKind::json_content(StatusCode::OK, &"test_response".to_owned()) }, - ) - } - } - - #[tokio::test] - async fn prefixed() { - let entry_point = test_utilities::entry_point( - Authentication::ApiKey(vec!["test".to_owned()]), - create_single_handler!(PrefixedAuth {}), - Some(Default::default()), - ); - - let req = Request::builder() - .uri("/achso/test/wtf") - .header("x-api-key", "test") - .method(Method::HEAD) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - - assert_eq!(resp.status(), StatusCode::OK); - } - - #[tokio::test] - async fn prefixed_no_path() { - let entry_point = test_utilities::entry_point( - Authentication::ApiKey(vec!["test".to_owned()]), - create_single_handler!(PrefixedAuth {}), - Some(Default::default()), - ); - - let req = Request::builder() - .uri("/achso") - .header("x-api-key", "test") - .method(Method::HEAD) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - } -} diff --git a/rust/src/openvasd/greenbone_scanner_framework/get_health/alive.rs b/rust/src/openvasd/greenbone_scanner_framework/get_health/alive.rs deleted file mode 100644 index 04f787f887..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/get_health/alive.rs +++ /dev/null @@ -1,160 +0,0 @@ -use std::{pin::Pin, sync::Arc}; - -use hyper::StatusCode; - -use crate::{ - auth_method_segments, - greenbone_scanner_framework::entry::{ - self, Bytes, Method, Prefixed, RequestHandler, response::BodyKind, - }, -}; - -pub trait GetHealthAlive: Prefixed + Send + Sync { - fn get_health_alive(&self) -> Pin + Send>>; -} - -pub enum Alive { - Alive, - #[allow(unused)] - NotAlive, -} - -impl From for StatusCode { - fn from(val: Alive) -> Self { - match val { - Alive::Alive => StatusCode::OK, - Alive::NotAlive => StatusCode::SERVICE_UNAVAILABLE, - } - } -} - -pub struct GetHealthAliveHandler { - get_health_alive: Arc, -} - -#[derive(Default)] -pub struct JustAlive; - -impl Prefixed for JustAlive { - fn prefix(&self) -> &'static str { - "" - } -} - -impl GetHealthAlive for JustAlive { - fn get_health_alive(&self) -> Pin + Send>> { - Box::pin(async move { Alive::Alive }) - } -} -impl Default for GetHealthAliveHandler { - fn default() -> Self { - Self { - get_health_alive: Arc::new(JustAlive {}), - } - } -} - -impl Prefixed for GetHealthAliveHandler -where - S: Prefixed + 'static, -{ - fn prefix(&self) -> &'static str { - self.get_health_alive.prefix() - } -} - -impl RequestHandler for GetHealthAliveHandler -where - S: GetHealthAlive + 'static, -{ - auth_method_segments!( - authenticated: false, - Method::GET, - "health", "alive" - ); - - fn call<'a, 'b>( - &'b self, - _: Arc, - _: &'a entry::Uri, - _: Bytes, - ) -> Pin + Send>> - where - 'b: 'a, - { - let gsp = self.get_health_alive.clone(); - Box::pin(async move { BodyKind::no_content(gsp.get_health_alive().await.into()) }) - } -} - -impl From for GetHealthAliveHandler -where - T: GetHealthAlive + 'static, -{ - fn from(value: T) -> Self { - GetHealthAliveHandler { - get_health_alive: Arc::new(value), - } - } -} - -#[cfg(test)] -mod tests { - use entry::test_utilities; - use http_body_util::Empty; - use hyper::{Request, service::Service}; - - use super::*; - use crate::{ - create_single_handler, - greenbone_scanner_framework::{Authentication, entry::ClientHash}, - }; - - struct NotAlive {} - - impl Prefixed for NotAlive { - fn prefix(&self) -> &'static str { - "" - } - } - - impl GetHealthAlive for NotAlive { - fn get_health_alive(&self) -> Pin + Send>> { - Box::pin(async move { super::Alive::NotAlive }) - } - } - - #[tokio::test] - async fn get_health_alive() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetHealthAliveHandler::from(super::JustAlive {})), - None, - ); - - let req = Request::builder() - .uri("/health/alive") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - } - - #[tokio::test] - async fn get_health_not_alive() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetHealthAliveHandler::from(NotAlive {})), - Some(ClientHash::from("ok")), - ); - - let req = Request::builder() - .uri("/health/alive") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE); - } -} diff --git a/rust/src/openvasd/greenbone_scanner_framework/get_health/mod.rs b/rust/src/openvasd/greenbone_scanner_framework/get_health/mod.rs deleted file mode 100644 index fc42ed08c8..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/get_health/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod alive; -pub use alive::GetHealthAliveHandler; -mod ready; -pub use ready::GetHealthReadyHandler; - -mod started; -pub use started::GetHealthStartedHandler; diff --git a/rust/src/openvasd/greenbone_scanner_framework/get_health/ready.rs b/rust/src/openvasd/greenbone_scanner_framework/get_health/ready.rs deleted file mode 100644 index 9fb0ef0fc7..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/get_health/ready.rs +++ /dev/null @@ -1,160 +0,0 @@ -use std::{pin::Pin, sync::Arc}; - -use hyper::StatusCode; - -use crate::{ - auth_method_segments, - greenbone_scanner_framework::entry::{ - self, Bytes, Method, Prefixed, RequestHandler, response::BodyKind, - }, -}; - -pub trait GetHealthReady: Prefixed + Send + Sync { - fn get_health_ready(&self) -> Pin + Send>>; -} - -pub enum Ready { - Ready, - #[allow(unused)] - NotReady, -} - -impl From for StatusCode { - fn from(val: Ready) -> Self { - match val { - Ready::Ready => StatusCode::OK, - Ready::NotReady => StatusCode::SERVICE_UNAVAILABLE, - } - } -} - -pub struct GetHealthReadyHandler { - get_health_ready: Arc, -} - -impl Prefixed for GetHealthReadyHandler -where - T: Prefixed, -{ - fn prefix(&self) -> &'static str { - self.get_health_ready.prefix() - } -} - -#[derive(Default)] -pub struct JustReady; - -impl Prefixed for JustReady { - fn prefix(&self) -> &'static str { - "" - } -} - -impl GetHealthReady for JustReady { - fn get_health_ready(&self) -> Pin + Send>> { - Box::pin(async move { Ready::Ready }) - } -} -impl Default for GetHealthReadyHandler { - fn default() -> Self { - Self { - get_health_ready: Arc::new(JustReady {}), - } - } -} - -impl RequestHandler for GetHealthReadyHandler -where - S: GetHealthReady + Prefixed + 'static, -{ - auth_method_segments!( - authenticated: false, - Method::GET, - "health", "ready" - ); - - fn call<'a, 'b>( - &'b self, - _: Arc, - _: &'a entry::Uri, - _: Bytes, - ) -> Pin + Send>> - where - 'b: 'a, - { - let gsp = self.get_health_ready.clone(); - Box::pin(async move { BodyKind::no_content(gsp.get_health_ready().await.into()) }) - } -} - -impl From for GetHealthReadyHandler -where - T: GetHealthReady + 'static, -{ - fn from(value: T) -> Self { - GetHealthReadyHandler { - get_health_ready: Arc::new(value), - } - } -} - -#[cfg(test)] -mod tests { - use entry::test_utilities; - use http_body_util::Empty; - use hyper::{Request, service::Service}; - - use super::*; - use crate::{ - create_single_handler, - greenbone_scanner_framework::{Authentication, entry::ClientHash}, - }; - - struct NotReady {} - - impl GetHealthReady for NotReady { - fn get_health_ready(&self) -> Pin + Send>> { - Box::pin(async move { super::Ready::NotReady }) - } - } - - impl Prefixed for NotReady { - fn prefix(&self) -> &'static str { - "" - } - } - - #[tokio::test] - async fn get_health_ready() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetHealthReadyHandler::from(super::JustReady {})), - None, - ); - - let req = Request::builder() - .uri("/health/ready") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - } - - #[tokio::test] - async fn get_health_not_ready() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetHealthReadyHandler::from(NotReady {})), - Some(ClientHash::from("ok")), - ); - - let req = Request::builder() - .uri("/health/ready") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE); - } -} diff --git a/rust/src/openvasd/greenbone_scanner_framework/get_health/started.rs b/rust/src/openvasd/greenbone_scanner_framework/get_health/started.rs deleted file mode 100644 index 4be84af3ff..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/get_health/started.rs +++ /dev/null @@ -1,160 +0,0 @@ -use std::{pin::Pin, sync::Arc}; - -use hyper::StatusCode; - -use crate::{ - auth_method_segments, - greenbone_scanner_framework::entry::{ - self, Bytes, Method, Prefixed, RequestHandler, response::BodyKind, - }, -}; - -pub trait GetHealthStarted: Send + Sync { - fn get_health_started(&self) -> Pin + Send>>; -} - -pub enum Started { - Started, - #[allow(unused)] - NotStarted, -} - -impl From for StatusCode { - fn from(val: Started) -> Self { - match val { - Started::Started => StatusCode::OK, - Started::NotStarted => StatusCode::SERVICE_UNAVAILABLE, - } - } -} - -pub struct GetHealthStartedHandler { - get_health_started: Arc, -} - -impl Prefixed for GetHealthStartedHandler -where - T: Prefixed, -{ - fn prefix(&self) -> &'static str { - self.get_health_started.prefix() - } -} - -#[derive(Default)] -pub struct JustStarted; - -impl Prefixed for JustStarted { - fn prefix(&self) -> &'static str { - "" - } -} - -impl GetHealthStarted for JustStarted { - fn get_health_started(&self) -> Pin + Send>> { - Box::pin(async move { Started::Started }) - } -} -impl Default for GetHealthStartedHandler { - fn default() -> Self { - Self { - get_health_started: Arc::new(JustStarted {}), - } - } -} - -impl RequestHandler for GetHealthStartedHandler -where - S: GetHealthStarted + Prefixed + 'static, -{ - auth_method_segments!( - authenticated: false, - Method::GET, - "health", "started" - ); - - fn call<'a, 'b>( - &'b self, - _: Arc, - _: &'a entry::Uri, - _: Bytes, - ) -> Pin + Send>> - where - 'b: 'a, - { - let gsp = self.get_health_started.clone(); - Box::pin(async move { BodyKind::no_content(gsp.get_health_started().await.into()) }) - } -} - -impl From for GetHealthStartedHandler -where - T: GetHealthStarted + 'static, -{ - fn from(value: T) -> Self { - GetHealthStartedHandler { - get_health_started: Arc::new(value), - } - } -} - -#[cfg(test)] -mod tests { - use entry::test_utilities; - use http_body_util::Empty; - use hyper::{Request, service::Service}; - - use super::*; - use crate::{ - create_single_handler, - greenbone_scanner_framework::{Authentication, entry::ClientHash}, - }; - - struct NotStarted {} - - impl Prefixed for NotStarted { - fn prefix(&self) -> &'static str { - "" - } - } - - impl GetHealthStarted for NotStarted { - fn get_health_started(&self) -> Pin + Send>> { - Box::pin(async move { super::Started::NotStarted }) - } - } - - #[tokio::test] - async fn get_health_started() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetHealthStartedHandler::from(super::JustStarted {})), - None, - ); - - let req = Request::builder() - .uri("/health/started") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - } - - #[tokio::test] - async fn get_health_not_started() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetHealthStartedHandler::from(NotStarted {})), - Some(ClientHash::from("ok")), - ); - - let req = Request::builder() - .uri("/health/started") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE); - } -} diff --git a/rust/src/openvasd/greenbone_scanner_framework/get_scans.rs b/rust/src/openvasd/greenbone_scanner_framework/get_scans.rs deleted file mode 100644 index 66e79c1ac7..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/get_scans.rs +++ /dev/null @@ -1,184 +0,0 @@ -use std::{fmt::Debug, pin::Pin, sync::Arc}; - -use hyper::StatusCode; - -use crate::greenbone_scanner_framework::{ - ExternalError, - entry::{ - self, Bytes, Method, Prefixed, RequestHandler, - response::{BodyKind, StreamResult}, - }, -}; -use crate::{auth_method_segments, internal_server_error}; - -pub trait GetScans: Send + Sync { - fn get_scans(&self, client_id: String) -> StreamResult; -} - -pub struct GetScansHandler { - get_scans: Arc, -} - -impl Prefixed for GetScansHandler -where - T: Prefixed, -{ - fn prefix(&self) -> &'static str { - self.get_scans.prefix() - } -} - -impl RequestHandler for GetScansHandler -where - S: GetScans + Prefixed + 'static, -{ - auth_method_segments!( - authenticated: true, - Method::GET, - "scans" - ); - - fn call<'a, 'b>( - &'b self, - client_id: Arc, - _: &'a entry::Uri, - _: Bytes, - ) -> Pin + Send>> - where - 'b: 'a, - { - let gsp = self.get_scans.clone(); - Box::pin(async move { - let input = gsp.get_scans(entry::enforce_client_hash(&client_id).to_string()); - BodyKind::from_result_stream(StatusCode::OK, input).await - }) - } -} - -impl From for GetScansHandler -where - T: GetScans + 'static, -{ - fn from(value: T) -> Self { - GetScansHandler { - get_scans: Arc::new(value), - } - } -} - -impl From> for GetScansHandler -where - T: GetScans + 'static, -{ - fn from(value: Arc) -> Self { - GetScansHandler { get_scans: value } - } -} - -#[derive(Debug, thiserror::Error)] -pub enum GetScansError { - // TODO: GetScansID uses this, should be moved to there - #[error("Not found.")] - #[cfg(test)] - NotFound, - #[error("Unexpected error occurred: {0}.")] - External(Box), -} - -impl From for GetScansError { - fn from(value: std::io::Error) -> Self { - Self::External(Box::new(value)) - } -} - -impl GetScansError { - pub fn from_external(err: E) -> Self - where - E: ExternalError + Send + Sync + 'static, - { - Self::External(Box::new(err)) - } -} - -impl From for BodyKind { - fn from(e: GetScansError) -> Self { - match e { - #[cfg(test)] - GetScansError::NotFound => BodyKind::no_content(StatusCode::NOT_FOUND), - GetScansError::External(external_error) => internal_server_error!(external_error), - } - } -} - -#[cfg(test)] -mod tests { - - use entry::test_utilities; - use futures::stream; - use http_body_util::{BodyExt, Empty}; - use hyper::{Request, service::Service}; - use tokio::io; - - use super::*; - use crate::{ - create_single_handler, - greenbone_scanner_framework::{Authentication, entry::ClientHash}, - }; - - struct Test {} - impl Prefixed for Test { - fn prefix(&self) -> &'static str { - "" - } - } - - impl GetScans for Test { - fn get_scans(&self, client_id: String) -> StreamResult { - let ise = ClientHash::from("internal_server_error").to_string(); - if ise == client_id { - return Box::pin(stream::iter(vec![Err(GetScansError::External(Box::new( - io::Error::other("oh no"), - )))])); - } - - Box::pin(stream::iter(vec![Ok(String::default())])) - } - } - - #[tokio::test] - async fn internal_server_error() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetScansHandler::from(Test {})), - Some(ClientHash::from("internal_server_error")), - ); - - let req = Request::builder() - .uri("/scans") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); - } - - #[tokio::test] - async fn get_scans() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetScansHandler::from(Test {})), - Some(ClientHash::from("ok")), - ); - - let req = Request::builder() - .uri("/scans") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - let bytes = resp.into_body().collect().await.unwrap().to_bytes(); - let resp = String::from_utf8_lossy(bytes.as_ref()); - insta::assert_snapshot!(resp); - } -} diff --git a/rust/src/openvasd/greenbone_scanner_framework/get_scans_id.rs b/rust/src/openvasd/greenbone_scanner_framework/get_scans_id.rs deleted file mode 100644 index 25953b0963..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/get_scans_id.rs +++ /dev/null @@ -1,205 +0,0 @@ -use std::{pin::Pin, sync::Arc}; - -use hyper::StatusCode; - -use crate::auth_method_segments; -use crate::greenbone_scanner_framework::{ - GetScansError, MapScanID, - entry::{ - self, Bytes, Method, Prefixed, RequestHandler, enforce_client_id_and_scan_id, - response::BodyKind, - }, -}; -use scannerlib::models; - -pub trait GetScansId: MapScanID { - fn get_scans_id<'a>( - &'a self, - id: String, - ) -> Pin> + Send + 'a>>; -} - -pub struct GetScansIdHandler { - get_scans: Arc, -} - -impl Prefixed for GetScansIdHandler -where - T: Prefixed, -{ - fn prefix(&self) -> &'static str { - self.get_scans.prefix() - } -} - -impl RequestHandler for GetScansIdHandler -where - S: GetScansId + Prefixed + 'static, -{ - auth_method_segments!( - authenticated: true, - Method::GET, - "scans", "*" - ); - - fn call<'a, 'b>( - &'b self, - client_id: Arc, - uri: &'a entry::Uri, - _: Bytes, - ) -> Pin + Send>> - where - 'b: 'a, - { - let gsp = self.get_scans.clone(); - let id = self - .ids(uri) - .into_iter() - .next() - .expect("expect ID, this is a toolkit error"); - Box::pin(async move { - enforce_client_id_and_scan_id(&client_id, id, gsp.as_ref(), async |id| { - match gsp.get_scans_id(id).await { - Ok(mut x) => { - x.target.credentials = x - .clone() - .target - .credentials - .into_iter() - .map(|c| c.hide_pass()) - .collect(); - BodyKind::json_content(StatusCode::OK, &x) - } - Err(e) => e.into(), - } - }) - .await - }) - } -} - -impl From for GetScansIdHandler -where - T: GetScansId + 'static, -{ - fn from(value: T) -> Self { - GetScansIdHandler { - get_scans: Arc::new(value), - } - } -} - -impl From> for GetScansIdHandler -where - T: GetScansId + 'static, -{ - fn from(value: Arc) -> Self { - GetScansIdHandler { get_scans: value } - } -} - -pub type GetScansIDError = GetScansError; - -#[cfg(test)] -mod tests { - use entry::test_utilities; - use http_body_util::{BodyExt, Empty}; - use hyper::{Request, service::Service}; - - use super::*; - use crate::{ - create_single_handler, - greenbone_scanner_framework::{Authentication, entry::ClientHash}, - }; - - struct Test {} - - impl Prefixed for Test { - fn prefix(&self) -> &'static str { - "" - } - } - - impl MapScanID for Test { - fn contains_scan_id<'a>( - &'a self, - client_id: &'a str, - scan_id: &'a str, - ) -> Pin> + Send + 'a>> { - Box::pin(async move { - if scan_id == "id" { - Some(client_id.to_string()) - } else { - None - } - }) - } - } - - impl GetScansId for Test { - fn get_scans_id( - &self, - id: String, - ) -> Pin> + Send>> { - let result = models::Scan { - scan_id: id.clone(), - ..Default::default() - }; - test_utilities::on_client_id_return(id, result, GetScansError::NotFound) - } - } - - #[tokio::test] - async fn internal_server_error() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetScansIdHandler::from(Test {})), - Some(ClientHash::from("internal_server_error")), - ); - - let req = Request::builder() - .uri("/scans/id") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); - } - - #[tokio::test] - async fn not_found() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetScansIdHandler::from(Test {})), - Some(ClientHash::from("not_found")), - ); - - let req = Request::builder() - .uri("/scans/not_found") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - } - - #[tokio::test] - async fn get_scans() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetScansIdHandler::from(Test {})), - Some(ClientHash::from("ok")), - ); - - let req = Request::builder() - .uri("/scans/id") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - let bytes = resp.into_body().collect().await.unwrap().to_bytes(); - let resp = String::from_utf8_lossy(bytes.as_ref()); - insta::assert_snapshot!(resp); - } -} diff --git a/rust/src/openvasd/greenbone_scanner_framework/get_scans_id_results.rs b/rust/src/openvasd/greenbone_scanner_framework/get_scans_id_results.rs deleted file mode 100644 index 9693cfecea..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/get_scans_id_results.rs +++ /dev/null @@ -1,325 +0,0 @@ -use std::{pin::Pin, sync::Arc}; - -use hyper::StatusCode; - -use crate::auth_method_segments; -use crate::greenbone_scanner_framework::{ - GetScansError, MapScanID, StreamResult, - entry::{ - self, Bytes, Method, Prefixed, RequestHandler, enforce_client_id_and_scan_id, - response::BodyKind, - }, -}; -use scannerlib::models; - -pub trait GetScansIdResults: MapScanID { - fn get_scans_id_results( - &self, - id: String, - from: Option, - to: Option, - ) -> StreamResult; -} - -pub struct GetScansIdResultsHandler { - get_scans: Arc, -} - -impl Prefixed for GetScansIdResultsHandler -where - T: Prefixed, -{ - fn prefix(&self) -> &'static str { - self.get_scans.prefix() - } -} -impl RequestHandler for GetScansIdResultsHandler -where - S: GetScansIdResults + Prefixed + 'static, -{ - auth_method_segments!( - authenticated: true, - Method::GET, - "scans", "*", "results" - ); - - fn call<'a, 'b>( - &'b self, - client_id: Arc, - uri: &'a entry::Uri, - _: Bytes, - ) -> Pin + Send>> - where - 'b: 'a, - { - let gsp = self.get_scans.clone(); - let id = self - .ids(uri) - .into_iter() - .next() - .expect("expect ID, this is a toolkit error"); - let query = uri.query().map(|x| x.to_owned()); - Box::pin(async move { - let (from, to) = match parse_results_range(query.as_deref()) { - Ok(range) => range, - Err(message) => { - return BodyKind::json_content( - StatusCode::BAD_REQUEST, - &entry::response::BadRequest { - line: 0, - column: 0, - message, - }, - ); - } - }; - enforce_client_id_and_scan_id(&client_id, id, gsp.as_ref(), async |id| { - let input = gsp.get_scans_id_results(id, from, to); - BodyKind::from_result_stream(StatusCode::OK, input).await - }) - .await - }) - } -} - -impl From for GetScansIdResultsHandler -where - T: GetScansIdResults + 'static, -{ - fn from(value: T) -> Self { - GetScansIdResultsHandler { - get_scans: Arc::new(value), - } - } -} - -impl From> for GetScansIdResultsHandler -where - T: GetScansIdResults + 'static, -{ - fn from(value: Arc) -> Self { - GetScansIdResultsHandler { get_scans: value } - } -} - -pub type GetScansIDResultsError = GetScansError; - -/// Parses the optional `range` query parameter of the results endpoint. -/// -/// Returns `Ok((None, None))` when no `range` parameter is present. A present -/// `range` must be either a single number (`N`) or a `from-to` range of -/// valid, non-negative integers. Any other value is reported as an error. -fn parse_results_range(query: Option<&str>) -> Result<(Option, Option), String> { - let range = query.and_then(|q| { - q.split('&') - .filter_map(|pair| { - let mut kv = pair.splitn(2, '='); - match kv.next() { - Some("range") => kv.next(), - _ => None, - } - }) - .next() - }); - - let range = match range { - Some(range) => range, - None => return Ok((None, None)), - }; - - let invalid = - || format!("invalid range '{range}': expected a number or a 'from-to' range of numbers"); - - let mut parts = range.splitn(2, '-'); - let from = parts - .next() - .and_then(|x| x.parse::().ok()) - .ok_or_else(invalid)?; - let to = match parts.next() { - Some(x) => Some(x.parse::().map_err(|_| invalid())?), - None => None, - }; - - Ok((Some(from), to)) -} - -#[cfg(test)] -mod tests { - - use std::io; - - use entry::test_utilities; - use futures::stream; - use http_body_util::{BodyExt, Empty}; - use hyper::{Request, service::Service}; - - use super::*; - use crate::{ - create_single_handler, - greenbone_scanner_framework::{Authentication, entry::ClientHash}, - }; - - struct Test {} - - impl Prefixed for Test { - fn prefix(&self) -> &'static str { - "" - } - } - - impl MapScanID for Test { - fn contains_scan_id<'a>( - &'a self, - client_id: &'a str, - scan_id: &'a str, - ) -> Pin> + Send + 'a>> { - Box::pin(async move { - if scan_id == "id" { - Some(client_id.to_string()) - } else { - None - } - }) - } - } - - impl GetScansIdResults for Test { - fn get_scans_id_results( - &self, - client_id: String, - from: Option, - to: Option, - ) -> StreamResult { - let ise = ClientHash::from("internal_server_error").to_string(); - if ise == client_id { - return Box::pin(stream::iter(vec![Err(GetScansError::External(Box::new( - io::Error::other("oh no"), - )))])); - } - let to = if from.is_some() { - // for the test case ?range=99 - to.unwrap_or(101) - } else { - to.unwrap_or_default() - }; - let from = from.unwrap_or_default(); - let result: Vec> = (from..to) - .map(|id| { - Ok(models::Result { - id, - ..Default::default() - }) - }) - .collect(); - - Box::pin(stream::iter(result)) - } - } - - #[tokio::test] - async fn internal_server_error() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetScansIdResultsHandler::from(Test {})), - Some(ClientHash::from("internal_server_error")), - ); - - let req = Request::builder() - .uri("/scans/id/results") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); - } - - #[tokio::test] - async fn scan_results() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetScansIdResultsHandler::from(Test {})), - Some(ClientHash::from("ok")), - ); - - let req = Request::builder() - .uri("/scans/id/results") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - let bytes = resp.into_body().collect().await.unwrap().to_bytes(); - let resp = String::from_utf8_lossy(bytes.as_ref()); - insta::assert_snapshot!(resp); - } - - #[tokio::test] - async fn scan_results_from_to() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetScansIdResultsHandler::from(Test {})), - Some(ClientHash::from("ok")), - ); - - let req = Request::builder() - .uri("/scans/id/results?range=10-100") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - let bytes = resp.into_body().collect().await.unwrap().to_bytes(); - let resp: Vec = serde_json::from_slice(bytes.as_ref()).unwrap(); - assert_eq!(resp.len(), 90); - insta::assert_ron_snapshot!(resp); - } - - #[tokio::test] - async fn scan_results_from() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetScansIdResultsHandler::from(Test {})), - Some(ClientHash::from("ok")), - ); - - let req = Request::builder() - .uri("/scans/id/results?range=99") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - let bytes = resp.into_body().collect().await.unwrap().to_bytes(); - let resp: Vec = serde_json::from_slice(bytes.as_ref()).unwrap(); - assert_eq!(resp.len(), 2); - insta::assert_ron_snapshot!(resp); - } - - #[tokio::test] - async fn invalid_range_returns_bad_request() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetScansIdResultsHandler::from(Test {})), - Some(ClientHash::from("ok")), - ); - - for uri in [ - "/scans/id/results?range=zero-ten&from=1&to=10", - "/scans/id/results?range=1-2-3", - "/scans/id/results?range=1-ten", - "/scans/id/results?range=", - ] { - let req = Request::builder() - .uri(uri) - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!( - resp.status(), - StatusCode::BAD_REQUEST, - "expected bad request for {uri}" - ); - } - } -} diff --git a/rust/src/openvasd/greenbone_scanner_framework/get_scans_id_results_id.rs b/rust/src/openvasd/greenbone_scanner_framework/get_scans_id_results_id.rs deleted file mode 100644 index 8fe958d3a0..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/get_scans_id_results_id.rs +++ /dev/null @@ -1,258 +0,0 @@ -use std::{pin::Pin, sync::Arc}; - -use hyper::StatusCode; -use scannerlib::models; - -use crate::greenbone_scanner_framework::{ - ExternalError, MapScanID, - entry::{ - self, Bytes, Method, Prefixed, RequestHandler, enforce_client_id_and_scan_id, - response::BodyKind, - }, -}; -use crate::{auth_method_segments, internal_server_error}; - -pub trait GetScansIdResultsId: MapScanID { - fn get_scans_id_results_id( - &self, - id: String, - result_id: usize, - ) -> Pin> + Send + '_>>; -} - -pub struct GetScansIdResultsIdHandler { - get_scans: Arc, -} - -impl Prefixed for GetScansIdResultsIdHandler -where - T: Prefixed, -{ - fn prefix(&self) -> &'static str { - self.get_scans.prefix() - } -} -impl RequestHandler for GetScansIdResultsIdHandler -where - S: GetScansIdResultsId + Prefixed + 'static, -{ - auth_method_segments!( - authenticated: true, - Method::GET, - "scans", "*", "results", "*" - ); - - fn call<'a, 'b>( - &'b self, - client_id: Arc, - uri: &'a entry::Uri, - _: Bytes, - ) -> Pin + Send>> - where - 'b: 'a, - { - let gsp = self.get_scans.clone(); - let mut ids = self.ids(uri).into_iter(); - - let id = ids.next().expect("expect ID, this is a toolkit error"); - let rid = ids - .next() - .expect("expected result id, this is a toolkit error"); - Box::pin(async move { - let rid = match rid.parse() { - Ok(x) => x, - Err(_) => return GetScansIDResultsIDError::InvalidID.into(), - }; - - enforce_client_id_and_scan_id(&client_id, id, gsp.as_ref(), async |id| { - match gsp.get_scans_id_results_id(id, rid).await { - Ok(x) => BodyKind::json_content(StatusCode::OK, &x), - Err(e) => e.into(), - } - }) - .await - }) - } -} - -impl From for GetScansIdResultsIdHandler -where - T: GetScansIdResultsId + 'static, -{ - fn from(value: T) -> Self { - GetScansIdResultsIdHandler { - get_scans: Arc::new(value), - } - } -} - -impl From> for GetScansIdResultsIdHandler -where - T: GetScansIdResultsId + 'static, -{ - fn from(value: Arc) -> Self { - GetScansIdResultsIdHandler { get_scans: value } - } -} - -#[derive(Debug)] -pub enum GetScansIDResultsIDError { - NotFound, - InvalidID, - External(Box), -} - -impl From for GetScansIDResultsIDError -where - T: std::error::Error + Send + Sync + 'static, -{ - fn from(value: T) -> Self { - Self::External(Box::new(value)) - } -} - -impl From for BodyKind { - fn from(e: GetScansIDResultsIDError) -> Self { - use GetScansIDResultsIDError::*; - match e { - NotFound => BodyKind::no_content(StatusCode::NOT_FOUND), - External(external_error) => internal_server_error!(external_error), - InvalidID => BodyKind::json_content( - StatusCode::BAD_REQUEST, - &entry::response::BadRequest { - line: 0, - column: 0, - message: "result_id must be a positive number".to_owned(), - }, - ), - } - } -} - -#[cfg(test)] -mod tests { - use entry::test_utilities; - use http_body_util::{BodyExt, Empty}; - use hyper::{Request, service::Service}; - - use super::*; - use crate::{ - create_single_handler, - greenbone_scanner_framework::{Authentication, entry::ClientHash}, - }; - - struct Test {} - - impl Prefixed for Test { - fn prefix(&self) -> &'static str { - "" - } - } - - impl MapScanID for Test { - fn contains_scan_id<'a>( - &'a self, - client_id: &'a str, - scan_id: &'a str, - ) -> Pin> + Send + 'a>> { - Box::pin(async move { - if scan_id == "id" { - Some(client_id.to_string()) - } else { - None - } - }) - } - } - - impl GetScansIdResultsId for Test { - fn get_scans_id_results_id( - &self, - client_id: String, - result_id: usize, - ) -> Pin> + Send>> - { - let result = models::Result { - id: result_id, - ..Default::default() - }; - - test_utilities::on_client_id_return( - client_id, - result, - GetScansIDResultsIDError::NotFound, - ) - } - } - - #[tokio::test] - async fn internal_server_error() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetScansIdResultsIdHandler::from(Test {})), - Some(ClientHash::from("internal_server_error")), - ); - - let req = Request::builder() - .uri("/scans/id/results/42") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); - } - - #[tokio::test] - async fn not_found() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetScansIdResultsIdHandler::from(Test {})), - Some(ClientHash::from("not_found")), - ); - - let req = Request::builder() - .uri("/scans/id/results/42") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - } - - #[tokio::test] - async fn invalid_rid() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetScansIdResultsIdHandler::from(Test {})), - Some(ClientHash::from("not_found")), - ); - - let req = Request::builder() - .uri("/scans/id/results/fourtytwo") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - } - - #[tokio::test] - async fn get_status() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetScansIdResultsIdHandler::from(Test {})), - Some(ClientHash::from("ok")), - ); - - let req = Request::builder() - .uri("/scans/id/results/42") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - let bytes = resp.into_body().collect().await.unwrap().to_bytes(); - let resp = String::from_utf8_lossy(bytes.as_ref()); - insta::assert_snapshot!(resp); - } -} diff --git a/rust/src/openvasd/greenbone_scanner_framework/get_scans_id_status.rs b/rust/src/openvasd/greenbone_scanner_framework/get_scans_id_status.rs deleted file mode 100644 index 8eebb73e00..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/get_scans_id_status.rs +++ /dev/null @@ -1,196 +0,0 @@ -use std::{pin::Pin, sync::Arc}; - -use hyper::StatusCode; -use scannerlib::models; - -use crate::auth_method_segments; -use crate::greenbone_scanner_framework::{ - GetScansError, MapScanID, - entry::{ - self, Bytes, Method, Prefixed, RequestHandler, enforce_client_id_and_scan_id, - response::BodyKind, - }, -}; - -pub trait GetScansIdStatus: MapScanID { - fn get_scans_id_status( - &self, - id: String, - ) -> Pin> + Send + '_>>; -} - -pub struct GetScansIdStatusHandler { - get_scans: Arc, -} - -impl Prefixed for GetScansIdStatusHandler -where - T: Prefixed, -{ - fn prefix(&self) -> &'static str { - self.get_scans.prefix() - } -} - -impl RequestHandler for GetScansIdStatusHandler -where - S: GetScansIdStatus + Prefixed + 'static, -{ - auth_method_segments!( - authenticated: true, - Method::GET, - "scans", "*", "status" - ); - - fn call<'a, 'b>( - &'b self, - client_id: Arc, - uri: &'a entry::Uri, - _: Bytes, - ) -> Pin + Send>> - where - 'b: 'a, - { - let gsp = self.get_scans.clone(); - let id = self - .ids(uri) - .into_iter() - .next() - .expect("expect ID, this is a toolkit error"); - Box::pin(async move { - enforce_client_id_and_scan_id(&client_id, id, gsp.as_ref(), async |id| { - match gsp.get_scans_id_status(id).await { - Ok(x) => BodyKind::json_content(StatusCode::OK, &x), - Err(e) => e.into(), - } - }) - .await - }) - } -} - -impl From for GetScansIdStatusHandler -where - T: GetScansIdStatus + 'static, -{ - fn from(value: T) -> Self { - GetScansIdStatusHandler { - get_scans: Arc::new(value), - } - } -} - -impl From> for GetScansIdStatusHandler -where - T: GetScansIdStatus + 'static, -{ - fn from(value: Arc) -> Self { - GetScansIdStatusHandler { get_scans: value } - } -} - -pub type GetScansIDStatusError = GetScansError; - -#[cfg(test)] -mod tests { - use entry::test_utilities; - use http_body_util::{BodyExt, Empty}; - use hyper::{Request, service::Service}; - - use super::*; - use crate::{ - create_single_handler, - greenbone_scanner_framework::{Authentication, entry::ClientHash}, - }; - - struct Test {} - - impl Prefixed for Test { - fn prefix(&self) -> &'static str { - "" - } - } - - impl MapScanID for Test { - fn contains_scan_id<'a>( - &'a self, - client_id: &'a str, - scan_id: &'a str, - ) -> Pin> + Send + 'a>> { - Box::pin(async move { - if scan_id == "id" { - Some(client_id.to_string()) - } else { - None - } - }) - } - } - - impl GetScansIdStatus for Test { - fn get_scans_id_status( - &self, - client_id: String, - ) -> Pin> + Send>> - { - let result = models::Status { - ..Default::default() - }; - test_utilities::on_client_id_return(client_id, result, GetScansError::NotFound) - } - } - - #[tokio::test] - async fn internal_server_error() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetScansIdStatusHandler::from(Test {})), - Some(ClientHash::from("internal_server_error")), - ); - - let req = Request::builder() - .uri("/scans/id/status") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); - } - - #[tokio::test] - async fn not_found() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetScansIdStatusHandler::from(Test {})), - Some(ClientHash::from("not_found")), - ); - - let req = Request::builder() - .uri("/scans/not_found/status") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - } - - #[tokio::test] - async fn get_status() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetScansIdStatusHandler::from(Test {})), - Some(ClientHash::from("ok")), - ); - - let req = Request::builder() - .uri("/scans/id/status") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - let bytes = resp.into_body().collect().await.unwrap().to_bytes(); - let resp = String::from_utf8_lossy(bytes.as_ref()); - insta::assert_snapshot!(resp); - } -} diff --git a/rust/src/openvasd/greenbone_scanner_framework/get_scans_preferences.rs b/rust/src/openvasd/greenbone_scanner_framework/get_scans_preferences.rs deleted file mode 100644 index bd6494cbe1..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/get_scans_preferences.rs +++ /dev/null @@ -1,142 +0,0 @@ -use std::{pin::Pin, sync::Arc}; - -use hyper::StatusCode; -use scannerlib::models; - -use crate::auth_method_segments; -use crate::greenbone_scanner_framework::entry::{ - self, Bytes, Method, Prefixed, RequestHandler, response::BodyKind, -}; - -pub trait GetScansPreferences: Send + Sync { - fn get_scans_preferences( - &self, - ) -> Pin> + Send>>; -} - -pub struct GetScansPreferencesHandler { - get_scans_preferences: Arc, -} - -impl Prefixed for GetScansPreferencesHandler -where - T: Prefixed, -{ - fn prefix(&self) -> &'static str { - self.get_scans_preferences.prefix() - } -} - -impl RequestHandler for GetScansPreferencesHandler -where - S: GetScansPreferences + Prefixed + 'static, -{ - auth_method_segments!( - authenticated: true, - Method::GET, - "scans", "preferences" - ); - - fn call<'a, 'b>( - &'b self, - _: Arc, - _: &'a entry::Uri, - _: Bytes, - ) -> Pin + Send>> - where - 'b: 'a, - { - let gsp = self.get_scans_preferences.clone(); - Box::pin(async move { - BodyKind::json_content(StatusCode::OK, &gsp.get_scans_preferences().await) - }) - } -} - -impl From for GetScansPreferencesHandler -where - T: GetScansPreferences + 'static, -{ - fn from(value: T) -> Self { - GetScansPreferencesHandler { - get_scans_preferences: Arc::new(value), - } - } -} - -impl From> for GetScansPreferencesHandler -where - T: GetScansPreferences + 'static, -{ - fn from(value: Arc) -> Self { - GetScansPreferencesHandler { - get_scans_preferences: value, - } - } -} - -#[cfg(test)] -mod tests { - use entry::test_utilities; - use http_body_util::{BodyExt, Empty}; - use hyper::{Request, service::Service}; - - use super::*; - use crate::{ - create_single_handler, - greenbone_scanner_framework::{Authentication, entry::ClientHash}, - }; - - struct Test {} - - impl Prefixed for Test { - fn prefix(&self) -> &'static str { - "" - } - } - - impl GetScansPreferences for Test { - fn get_scans_preferences( - &self, - ) -> Pin> + Send>> { - Box::pin(async move { vec![] }) - } - } - - #[tokio::test] - async fn get_scans_preferences_unauthenticated() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetScansPreferencesHandler::from(Test {})), - None, - ); - - let req = Request::builder() - .uri("/scans/preferences") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); - } - - #[tokio::test] - async fn get_scans_preferences() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetScansPreferencesHandler::from(Test {})), - Some(ClientHash::from("ok")), - ); - - let req = Request::builder() - .uri("/scans/preferences") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - let bytes = resp.into_body().collect().await.unwrap().to_bytes(); - let resp = String::from_utf8_lossy(bytes.as_ref()); - insta::assert_snapshot!(resp); - } -} diff --git a/rust/src/openvasd/greenbone_scanner_framework/get_vts.rs b/rust/src/openvasd/greenbone_scanner_framework/get_vts.rs deleted file mode 100644 index aa88c88ff9..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/get_vts.rs +++ /dev/null @@ -1,256 +0,0 @@ -use std::{pin::Pin, sync::Arc}; - -use crate::greenbone_scanner_framework::{ - ClientIdentifier, ExternalError, StreamResult, - entry::{self, Bytes, Method, Prefixed, RequestHandler, response::BodyKind}, -}; -use crate::{auth_method_segments, internal_server_error}; -use hyper::StatusCode; -use scannerlib::models::VTData; - -pub trait GetVts: Send + Sync { - fn get_oids(&self, client_id: String) -> StreamResult; - - fn get_vts(&self, client_id: String) -> StreamResult; -} - -pub struct GetVTsHandler { - get_scans: Arc, -} - -impl Prefixed for GetVTsHandler -where - T: Prefixed, -{ - fn prefix(&self) -> &'static str { - self.get_scans.prefix() - } -} - -impl RequestHandler for GetVTsHandler -where - S: GetVts + Prefixed + 'static, -{ - auth_method_segments!( - authenticated: false, - Method::GET, - "vts" - ); - - fn call<'a, 'b>( - &'b self, - client_id: Arc, - uri: &'a entry::Uri, - _: Bytes, - ) -> Pin + Send>> - where - 'b: 'a, - { - let gsp = self.get_scans.clone(); - let details = match uri.query() { - Some("information=true") => true, - Some("information=1") => true, - Some(_) | None => false, - }; - - let client_id = match client_id.as_ref() { - ClientIdentifier::Unknown => "unknown".to_string(), - ClientIdentifier::Known(client_hash) => client_hash.to_string(), - }; - - Box::pin(async move { - if details { - BodyKind::from_result_stream(StatusCode::OK, gsp.get_vts(client_id)).await - } else { - BodyKind::from_result_stream(StatusCode::OK, gsp.get_oids(client_id)).await - } - }) - } -} - -impl From for GetVTsHandler -where - T: GetVts + 'static, -{ - fn from(value: T) -> Self { - GetVTsHandler { - get_scans: Arc::new(value), - } - } -} - -impl From> for GetVTsHandler -where - T: GetVts + 'static, -{ - fn from(value: Arc) -> Self { - GetVTsHandler { get_scans: value } - } -} - -#[derive(Debug, thiserror::Error)] -pub enum GetVTsError { - #[error("Not yet available.")] - NotYetAvailable, - - #[error("{0}")] - External(Box), -} - -impl From for BodyKind { - fn from(e: GetVTsError) -> Self { - match e { - GetVTsError::External(external_error) => internal_server_error!(external_error), - GetVTsError::NotYetAvailable => BodyKind::no_content(StatusCode::SERVICE_UNAVAILABLE), - } - } -} - -#[cfg(test)] -mod tests { - - use entry::test_utilities; - use futures::stream; - use http_body_util::{BodyExt, Empty}; - use hyper::{Request, service::Service}; - - use super::*; - use crate::{ - create_single_handler, - greenbone_scanner_framework::{Authentication, entry::ClientHash}, - }; - - struct Test {} - - impl Prefixed for Test { - fn prefix(&self) -> &'static str { - "" - } - } - - impl GetVts for Test { - fn get_oids(&self, client_id: String) -> StreamResult { - let ok = ClientHash::from("ok").to_string(); - let not_found = ClientHash::from("not_found").to_string(); - let unknown = "unknown".to_string(); - let result = if client_id == unknown || client_id == ok { - vec![Ok(client_id)] - } else if client_id == not_found { - vec![Err(GetVTsError::NotYetAvailable)] - } else { - vec![Err(GetVTsError::External(Box::new(std::io::Error::other( - "moep", - ))))] - }; - //stream::iter(result) - Box::pin(stream::iter(result)) - } - - fn get_vts(&self, _: String) -> StreamResult { - Box::pin(stream::iter(vec![Ok(VTData::default())])) - } - } - - #[tokio::test] - async fn internal_server_error() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetVTsHandler::from(Test {})), - Some(ClientHash::from("internal_server_error")), - ); - - let req = Request::builder() - .uri("/vts") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); - } - - #[tokio::test] - async fn not_yet_available() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetVTsHandler::from(Test {})), - Some(ClientHash::from("not_found")), - ); - - let req = Request::builder() - .uri("/vts") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE); - } - - #[tokio::test] - async fn get_vts_authenticated() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetVTsHandler::from(Test {})), - Some(ClientHash::from("ok")), - ); - - let req = Request::builder() - .uri("/vts") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - let bytes = resp.into_body().collect().await.unwrap().to_bytes(); - let resp = String::from_utf8_lossy(bytes.as_ref()); - insta::assert_snapshot!(resp); - } - - #[tokio::test] - async fn get_vts_detailed() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetVTsHandler::from(Test {})), - Some(ClientHash::from("ok")), - ); - - let req = Request::builder() - .uri("/vts?information=true") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - let bytes = resp.into_body().collect().await.unwrap().to_bytes(); - let first: Vec = serde_json::from_slice(&bytes).unwrap(); - let req = Request::builder() - .uri("/vts?information=1") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - let bytes = resp.into_body().collect().await.unwrap().to_bytes(); - let second: Vec = serde_json::from_slice(&bytes).unwrap(); - assert_eq!(first, second) - } - - #[tokio::test] - async fn get_vts_unauthenticated() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(GetVTsHandler::from(Test {})), - None, - ); - - let req = Request::builder() - .uri("/vts") - .method(Method::GET) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - let bytes = resp.into_body().collect().await.unwrap().to_bytes(); - let resp = String::from_utf8_lossy(bytes.as_ref()); - insta::assert_snapshot!(resp); - } -} diff --git a/rust/src/openvasd/greenbone_scanner_framework/mod.rs b/rust/src/openvasd/greenbone_scanner_framework/mod.rs deleted file mode 100644 index 8dadfeff92..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/mod.rs +++ /dev/null @@ -1,585 +0,0 @@ -use std::{ - marker::PhantomData, - net::SocketAddr, - path::PathBuf, - pin::Pin, - sync::{Arc, RwLock, atomic::AtomicUsize}, -}; - -use delete_scans_id::{DeleteScansId, DeleteScansIdHandler}; -use entry::Prefixed; -pub use entry::{ClientIdentifier, RequestHandler, RequestHandlers}; -use get_scans::GetScansHandler; -use get_scans_id::GetScansIdHandler; -use get_scans_id_results::GetScansIdResultsHandler; -use get_scans_id_results_id::GetScansIdResultsIdHandler; -use get_scans_id_status::GetScansIdStatusHandler; -use get_scans_preferences::GetScansPreferencesHandler; -use get_vts::GetVTsHandler; -use hyper_util::rt::{TokioExecutor, TokioIo}; - -mod delete_scans_id; -pub mod entry; -pub use entry::response::StreamResult; -mod get_scans; -pub use get_scans::{GetScans, GetScansError}; -//TODO: move -mod get_scans_id; -pub use get_scans_id::{GetScansIDError, GetScansId}; -mod get_scans_id_results; -pub use get_scans_id_results::{GetScansIDResultsError, GetScansIdResults}; -mod get_scans_id_results_id; -pub use get_scans_id_results_id::{GetScansIDResultsIDError, GetScansIdResultsId}; -mod get_scans_id_status; -pub use get_scans_id_status::{GetScansIDStatusError, GetScansIdStatus}; -mod get_scans_preferences; -pub use get_scans_preferences::GetScansPreferences; -mod get_vts; -pub use get_vts::{GetVTsError, GetVts}; -mod get_health; -use get_health::{GetHealthAliveHandler, GetHealthReadyHandler, GetHealthStartedHandler}; - -mod post_scans; -use once_cell::sync::Lazy; -pub use post_scans::{PostScans, PostScansError}; -use scannerlib::models::{self, FeedState}; -mod post_scans_id; -mod tls; -use post_scans::PostScansHandler; -use post_scans_id::{PostScansId, PostScansIdHandler}; - -use futures::stream::StreamExt; -use signal_hook::consts::signal::*; -use signal_hook_tokio::Signals; -use tokio::net::TcpListener; - -pub trait ExternalError: core::error::Error + Send + Sync + 'static {} - -impl ExternalError for T where T: core::error::Error + Send + Sync + 'static {} - -pub mod prelude { - //! Contains all use statements thate are exported within this framework. - //! - //! To use it call `use greenbone_scanner_framework::preluse::*`. - - pub use super::{ - GetScans, GetScansError, GetScansIDError, GetScansIDResultsError, GetScansIDResultsIDError, - GetScansIDStatusError, GetScansId, GetScansIdResults, GetScansIdResultsId, - GetScansIdStatus, GetScansPreferences, MapScanID, PostScans, PostScansError, StreamResult, - delete_scans_id::{DeleteScansIDError, DeleteScansId}, - entry::Prefixed, - post_scans_id::{PostScansIDError, PostScansId}, - }; -} - -#[derive(Debug, Default)] -pub struct ServerCertificate { - certificate: PathBuf, - key: PathBuf, -} - -impl ServerCertificate { - pub fn new(key: PathBuf, cert: PathBuf) -> Self { - Self { - certificate: cert, - key, - } - } -} - -#[derive(Debug, Default)] -struct TLSConfig { - server_tls_cer: ServerCertificate, - path_client_certs: Option, -} - -mod runtime_builder_states { - pub struct Start; - pub struct DeleteScanIDSet; - pub struct End; -} - -// we hide all other states to not pollute the namespace too much -pub use runtime_builder_states::End; - -// A scanner must have the endpoints: -// - POST /scans -// - GET /scans -// - POST /scans/{id} -// - GET /scans/{id} -// - DELETE /scans/{id} -// - GET /scans/{id}/status -// - GET /scans/{id}/results?range=n-n -// - GET /scans/{id}/results/{idx} -// - GET /vts -// Default available -// - GET /scans/preferences -// - GET /health/alive -// - GET /health/ready -// - GET /health/started -// Additionally, endpoints that are not scanner specific can be registered as well. -pub struct RuntimeBuilder { - // Contains the currently supported API versions. - api_version: Vec, - feed_state: Option>>, - listener_address: SocketAddr, - tls: Option, - api_keys: Option>, - handlers: RequestHandlers, - max_concurrent_connections: usize, - _phantom: PhantomData, -} - -fn retrieve_and_reset_client_identifier(id: Arc>) -> ClientIdentifier { - // get client information - let mut ci = id.write().unwrap(); - let cci = ci.clone(); - // reset client information - *ci = ClientIdentifier::Unknown; - cci -} - -impl Default for RuntimeBuilder { - fn default() -> Self { - Self::new(([127, 0, 0, 1], 3000).into()) - } -} - -impl RuntimeBuilder { - pub fn new(listener_address: SocketAddr) -> RuntimeBuilder { - let mut handlers = RequestHandlers::default(); - //handlers.push(GetScansPreferencesHandler::default()); - // TODO: do per prefix? - handlers.push(GetHealthAliveHandler::default()); - handlers.push(GetHealthReadyHandler::default()); - handlers.push(GetHealthStartedHandler::default()); - - RuntimeBuilder { - api_version: vec!["1".to_owned()], - feed_state: None, - tls: None, - api_keys: None, - handlers, - listener_address, - max_concurrent_connections: 10, - _phantom: PhantomData, - } - } - - pub fn feed_version(mut self, feed_state: Arc>) -> RuntimeBuilder { - self.feed_state = Some(feed_state); - self - } - - pub fn api_keys(mut self, api_keys: Vec) -> RuntimeBuilder { - self.api_keys = if api_keys.is_empty() { - None - } else { - Some(api_keys) - }; - self - } - - pub fn server_tls_cer(mut self, server_tls_cer: ServerCertificate) -> RuntimeBuilder { - self.tls = Some(match self.tls { - Some(TLSConfig { - server_tls_cer: _, - path_client_certs, - }) => TLSConfig { - server_tls_cer, - path_client_certs, - }, - None => TLSConfig { - server_tls_cer, - path_client_certs: None, - }, - }); - - self - } - - pub fn path_client_certs(mut self, path_client_certs: PathBuf) -> RuntimeBuilder { - self.tls = Some(match self.tls { - Some(TLSConfig { - server_tls_cer, - path_client_certs: _, - }) => TLSConfig { - server_tls_cer, - path_client_certs: Some(path_client_certs), - }, - None => TLSConfig { - server_tls_cer: Default::default(), - path_client_certs: Some(path_client_certs), - }, - }); - self - } - - pub fn max_concurrent_connections( - mut self, - max_concurrent_connections: usize, - ) -> RuntimeBuilder { - self.max_concurrent_connections = max_concurrent_connections; - self - } - - pub fn add_request_handler(mut self, value: R) -> RuntimeBuilder - where - R: RequestHandler + Sync + Send + 'static, - { - let mut idx = None; - for (i, or) in self.handlers.handlers.iter().enumerate() { - if or.prefix() == value.prefix() - && or.http_method() == value.http_method() - && or.path_segments() == value.path_segments() - { - idx = Some(i); - break; - } - } - let value = Arc::new(Box::new(value) as Box); - if let Some(idx) = idx { - self.handlers.handlers[idx] = value; - } else { - self.handlers.handlers.push(value); - } - self - } - - // TODO: find a better name add health endpoints - pub fn insert_additional_scan_endpoints( - self, - scans: Arc, - vts: Arc, - ) -> RuntimeBuilder - where - S: PostScans - + GetScans - + GetScansPreferences - + GetScansId - + GetScansIdResults - + GetScansIdResultsId - + GetScansIdStatus - + PostScansId - + DeleteScansId - + Prefixed - + 'static, - V: GetVts + Prefixed + 'static, - { - let ior = self - .add_request_handler(PostScansHandler::from(scans.clone())) - .add_request_handler(GetScansHandler::from(scans.clone())) - .add_request_handler(GetScansPreferencesHandler::from(scans.clone())) - .add_request_handler(GetScansIdHandler::from(scans.clone())) - .add_request_handler(GetScansIdResultsHandler::from(scans.clone())) - .add_request_handler(GetScansIdResultsIdHandler::from(scans.clone())) - .add_request_handler(GetScansIdStatusHandler::from(scans.clone())) - .add_request_handler(PostScansIdHandler::from(scans.clone())) - .add_request_handler(DeleteScansIdHandler::from(scans)) - .add_request_handler(GetVTsHandler::from(vts)); - RuntimeBuilder { - api_version: ior.api_version, - feed_state: ior.feed_state, - listener_address: ior.listener_address, - tls: ior.tls, - api_keys: ior.api_keys, - handlers: ior.handlers, - _phantom: PhantomData, - max_concurrent_connections: ior.max_concurrent_connections, - } - } - - fn build_scanner(&self) -> Scanner { - let authentication = match (&self.tls, &self.api_keys) { - ( - Some(TLSConfig { - server_tls_cer: _, - path_client_certs: Some(_), - }), - None, - ) => Authentication::Mtls, - ( - Some(TLSConfig { - server_tls_cer: _, - path_client_certs: Some(_), - }), - Some(_), - ) => { - tracing::info!("mTLS and api-key configured, favoring mTLS and disabling api-key"); - Authentication::Mtls - } - (_, Some(_)) => Authentication::ApiKey(self.api_keys.clone().unwrap_or_default()), - (_, None) => { - tracing::warn!("neither api-key nor mTLS configured. Endpoints are not secured."); - Authentication::Disabled - } - }; - Scanner { - api_version: self.api_version.join(","), - feed_version: self.feed_state.clone().unwrap_or_default(), - authentication, - } - } -} - -impl RuntimeBuilder { - pub fn insert_scans( - self, - value: Arc, - ) -> RuntimeBuilder - where - T: PostScans - + GetScans - + GetScansPreferences - + GetScansId - + GetScansIdResults - + GetScansIdResultsId - + GetScansIdStatus - + PostScansId - + DeleteScansId - + 'static, - { - let ior = self - .add_request_handler(PostScansHandler::from(value.clone())) - .add_request_handler(GetScansHandler::from(value.clone())) - .add_request_handler(GetScansPreferencesHandler::from(value.clone())) - .add_request_handler(GetScansIdHandler::from(value.clone())) - .add_request_handler(GetScansIdResultsHandler::from(value.clone())) - .add_request_handler(GetScansIdStatusHandler::from(value.clone())) - .add_request_handler(GetScansIdResultsIdHandler::from(value.clone())) - .add_request_handler(PostScansIdHandler::from(value.clone())) - .add_request_handler(DeleteScansIdHandler::from(value)); - - RuntimeBuilder { - api_version: ior.api_version, - feed_state: ior.feed_state, - listener_address: ior.listener_address, - tls: ior.tls, - api_keys: ior.api_keys, - handlers: ior.handlers, - _phantom: PhantomData, - max_concurrent_connections: ior.max_concurrent_connections, - } - } -} - -impl RuntimeBuilder { - pub fn insert_get_vts(self, value: Arc) -> RuntimeBuilder - where - T: GetVts + Prefixed + 'static, - { - let ior = self.add_request_handler(GetVTsHandler::from(value)); - RuntimeBuilder { - api_version: ior.api_version, - feed_state: ior.feed_state, - listener_address: ior.listener_address, - tls: ior.tls, - api_keys: ior.api_keys, - handlers: ior.handlers, - max_concurrent_connections: ior.max_concurrent_connections, - _phantom: PhantomData, - } - } -} - -async fn next_exit_signal(signals: &mut Signals) -> Option { - match signals.next().await { - Some(SIGHUP) => { - tracing::info!("Ignoring SIGHUP signal."); - None - } - Some(signal @ (SIGTERM | SIGINT | SIGQUIT)) => { - tracing::info!(signal, "Exit based on signal."); - Some(128 + signal) - } - Some(_) => unreachable!(), - None => { - tracing::warn!("Signal stream ended unexpectedly."); - Some(0) - } - } -} - -static REQUEST_COUNTER: Lazy> = Lazy::new(Default::default); - -fn make_service( - scanner: Arc, - handlers: Arc, - client_id: ClientIdentifier, - max_connections: usize, -) -> entry::EntryPoint { - entry::EntryPoint::new( - scanner, - Arc::new(client_id), - handlers, - max_connections, - REQUEST_COUNTER.clone(), - ) -} - -async fn run_accept_loop( - incoming: TcpListener, - mut signals: Signals, - on_accept: F, -) -> Result> -where - F: Fn(tokio::net::TcpStream) -> Fut + Send + Sync + Clone + 'static, - Fut: Future + Send + 'static, -{ - loop { - tokio::select! { - maybe_exit = next_exit_signal(&mut signals) => { - if let Some(code) = maybe_exit { - return Ok(code); - } - } - - accept_result = incoming.accept() => { - let (tcp_stream, _remote_addr) = accept_result?; - let f = on_accept.clone(); - tokio::spawn(async move { - f(tcp_stream).await; - }); - } - } - } -} - -impl RuntimeBuilder { - pub async fn run_blocking(self) -> Result> { - let signals = Signals::new([SIGHUP, SIGTERM, SIGINT, SIGQUIT])?; - - let scanner = Arc::new(self.build_scanner()); - let tls_config = match &self.tls { - Some(x) => Some(tls::tls_config(x)?), - None => None, - }; - - let incoming = TcpListener::bind(&self.listener_address).await?; - let handlers = Arc::new(self.handlers); - - let max_connections = self.max_concurrent_connections; - - if let Some(tls_config) = tls_config { - tracing::info!("listening on https://{}", self.listener_address); - - use hyper::server::conn::http2::Builder; - - let config = Arc::new(tls_config.config); - let tls_acceptor = tokio_rustls::TlsAcceptor::from(config); - let identifier = tls_config.client_identifier.clone(); - - run_accept_loop(incoming, signals, { - let scanner = scanner.clone(); - let handlers = handlers.clone(); - - move |tcp_stream| { - let tls_acceptor = tls_acceptor.clone(); - let identifier = identifier.clone(); - let scanner = scanner.clone(); - let handlers = handlers.clone(); - - async move { - let tls_stream = match tls_acceptor.accept(tcp_stream).await { - Ok(tls_stream) => tls_stream, - Err(err) => { - tracing::debug!("failed to perform tls handshake: {err:#}"); - return; - } - }; - - let cci = retrieve_and_reset_client_identifier(identifier); - let service = make_service(scanner, handlers, cci, max_connections); - - if let Err(err) = Builder::new(TokioExecutor::new()) - .max_concurrent_streams(20) - .serve_connection(TokioIo::new(tls_stream), service) - .await - { - tracing::debug!("failed to serve connection: {err:#}"); - } - } - } - }) - .await - } else { - tracing::info!("listening on http://{}", self.listener_address); - - use hyper::server::conn::http1::Builder; - - run_accept_loop(incoming, signals, { - let scanner = scanner.clone(); - let handlers = handlers.clone(); - - move |tcp_stream| { - let scanner = scanner.clone(); - let handlers = handlers.clone(); - - async move { - let service = make_service( - scanner, - handlers, - ClientIdentifier::Unknown, - max_connections, - ); - - if let Err(err) = Builder::new() - .serve_connection(TokioIo::new(tcp_stream), service) - .await - { - tracing::debug!("failed to serve connection: {err:#}"); - } - } - } - }) - .await - } - } -} - -pub type InternalIdentifier = String; - -/// This trait is used for scan id specific endpoints to return an internal identifier. -/// -/// Rather than forcing the implementation of a scan specific endpoint to check for themselves this -/// trait is used. This not just makes the implementation easier but also allows us to get rid of -/// async requirements when e.b. building a stream. -/// -/// Additionally it returns one id instead of two so that implementations don't need to be aware -/// about the client_identifier. -pub trait MapScanID: Send + Sync { - /// Returns true when scan_id is available for the given client - fn contains_scan_id<'a>( - &'a self, - client_id: &'a str, - scan_id: &'a str, - ) -> Pin> + Send + 'a>>; -} - -pub enum Authentication { - Disabled, - Mtls, - ApiKey(Vec), -} - -impl Authentication { - fn static_str(&self) -> &'static str { - match self { - Authentication::Disabled => "disabled", - Authentication::Mtls => "mTLS", - Authentication::ApiKey(_) => "api-key", - } - } -} - -impl AsRef for Authentication { - fn as_ref(&self) -> &str { - self.static_str() - } -} - -pub struct Scanner { - api_version: String, - authentication: Authentication, - feed_version: Arc>, -} diff --git a/rust/src/openvasd/greenbone_scanner_framework/post_scans.rs b/rust/src/openvasd/greenbone_scanner_framework/post_scans.rs deleted file mode 100644 index e91adb50e0..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/post_scans.rs +++ /dev/null @@ -1,262 +0,0 @@ -use std::{collections::HashSet, pin::Pin, sync::Arc}; - -use hyper::StatusCode; - -use crate::greenbone_scanner_framework::{ - ExternalError, - entry::{self, Bytes, Prefixed, RequestHandler, enforce_client_hash, response::BodyKind}, -}; -use crate::{auth_method_segments, internal_server_error}; -use scannerlib::models::{self, Service}; - -pub trait PostScans: Send + Sync { - fn post_scans( - &self, - client_id: String, - scan: models::Scan, - ) -> Pin> + Send + '_>>; -} - -pub struct PostScansHandler { - store_scan: Arc, -} - -impl Prefixed for PostScansHandler -where - T: Prefixed, -{ - fn prefix(&self) -> &'static str { - self.store_scan.prefix() - } -} - -impl RequestHandler for PostScansHandler -where - S: PostScans + Prefixed + 'static, -{ - auth_method_segments!(authenticated: true, crate::greenbone_scanner_framework::entry::Method::POST, "scans"); - - fn call<'a, 'b>( - &'b self, - client_id: Arc, - _: &'a entry::Uri, - body: Bytes, - ) -> Pin + Send>> - where - 'b: 'a, - { - let store_scan = self.store_scan.clone(); - Box::pin(async move { - match serde_json::from_slice::(&body) { - Ok(mut scan) => { - if let Some(duplicate) = - get_duplicate_credential_service(&scan.target.credentials) - { - return BodyKind::json_content( - StatusCode::BAD_REQUEST, - &format!("Duplicate credential service: {}", duplicate), - ); - } - if scan.scan_id.is_empty() { - scan.scan_id = uuid::Uuid::new_v4().into(); - } - match store_scan - .post_scans(enforce_client_hash(&client_id).to_string(), scan) - .await - { - Ok(id) => BodyKind::json_content(hyper::StatusCode::CREATED, &id), - Err(e) => e.into(), - } - } - Err(e) => e.into(), - } - }) - } -} - -fn get_duplicate_credential_service(credentials: &[models::Credential]) -> Option { - let mut map: HashSet<_> = HashSet::default(); - for c in credentials.iter() { - if !map.insert(c.service.to_string()) { - return Some(c.service.clone()); - } - } - None -} - -impl From for PostScansHandler -where - T: PostScans + 'static, -{ - fn from(value: T) -> Self { - PostScansHandler { - store_scan: Arc::new(value), - } - } -} -impl From> for PostScansHandler -where - T: PostScans + 'static, -{ - fn from(value: Arc) -> Self { - PostScansHandler { store_scan: value } - } -} - -#[derive(Debug, thiserror::Error)] -pub enum PostScansError { - #[error("ID ({0}) is already in use.")] - DuplicateId(String), - #[error("Unexpected error occurred: {0}.")] - External(Box), -} - -impl From for PostScansError { - fn from(value: std::io::Error) -> Self { - Self::External(Box::new(value)) - } -} - -impl PostScansError {} - -impl From for BodyKind { - fn from(val: PostScansError) -> Self { - match val { - PostScansError::DuplicateId(id) => { - let br = format!("ID ({id}) is already in use."); - BodyKind::json_content(StatusCode::CONFLICT, &br) - } - PostScansError::External(e) => internal_server_error!(e), - } - } -} - -#[cfg(test)] -mod tests { - use entry::test_utilities::{self, json_bytes}; - use http_body_util::{BodyExt, Empty}; - use hyper::{Method, Request, service::Service}; - - use super::*; - use crate::{ - create_single_handler, - greenbone_scanner_framework::{Authentication, entry::ClientHash}, - }; - use scannerlib::models::Credential; - - struct Test {} - impl Prefixed for Test { - fn prefix(&self) -> &'static str { - "" - } - } - - impl PostScans for Test { - fn post_scans( - &self, - client_id: String, - _: models::Scan, - ) -> Pin> + Send>> { - let result = "response_id".to_owned(); - - test_utilities::on_client_id_return( - client_id, - result, - PostScansError::DuplicateId("duplicate".to_owned()), - ) - } - } - - #[tokio::test] - async fn internal_server_error() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(PostScansHandler::from(Test {})), - Some(ClientHash::from("internal_server_error")), - ); - let scans = models::Scan::default(); - - let req = Request::builder() - .uri("/scans") - .method(Method::POST) - .body(json_bytes(&scans)) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); - } - - #[tokio::test] - async fn missing_scan() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(PostScansHandler::from(Test {})), - Some(ClientHash::from("not_found")), - ); - - let req = Request::builder() - .uri("/scans") - .method(Method::POST) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - } - - #[tokio::test] - async fn duplicate_id() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(PostScansHandler::from(Test {})), - Some(ClientHash::from("not_found")), - ); - let scans = models::Scan::default(); - - let req = Request::builder() - .uri("/scans") - .method(Method::POST) - .body(json_bytes(&scans)) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::CONFLICT); - } - - #[tokio::test] - async fn duplicate_credential_service() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(PostScansHandler::from(Test {})), - Some(ClientHash::from("ok")), - ); - let mut scans = models::Scan::default(); - scans.target.credentials = vec![Credential::default(), Credential::default()]; - - let req = Request::builder() - .uri("/scans") - .method(Method::POST) - .body(json_bytes(&scans)) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - } - - #[tokio::test] - async fn post_scans() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(PostScansHandler::from(Test {})), - Some(ClientHash::from("ok")), - ); - let scans = models::Scan::default(); - - let req = Request::builder() - .uri("/scans") - .method(Method::POST) - .body(json_bytes(&scans)) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::CREATED); - let bytes = resp.into_body().collect().await.unwrap().to_bytes(); - let resp = String::from_utf8_lossy(bytes.as_ref()); - insta::assert_snapshot!(resp); - } -} diff --git a/rust/src/openvasd/greenbone_scanner_framework/post_scans_id.rs b/rust/src/openvasd/greenbone_scanner_framework/post_scans_id.rs deleted file mode 100644 index ccb37d5676..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/post_scans_id.rs +++ /dev/null @@ -1,269 +0,0 @@ -use std::{pin::Pin, sync::Arc}; - -use hyper::StatusCode; - -use crate::greenbone_scanner_framework::{ - ExternalError, MapScanID, - entry::{ - self, Bytes, Method, Prefixed, RequestHandler, enforce_client_id_and_scan_id, - response::BodyKind, - }, -}; -use crate::{auth_method_segments, internal_server_error}; -use scannerlib::models::{self, Action}; - -pub trait PostScansId: MapScanID { - fn post_scans_id( - &self, - id: String, - action: Action, - ) -> Pin> + Send + '_>>; -} - -pub struct PostScansIdHandler { - scans: Arc, -} - -impl Prefixed for PostScansIdHandler -where - T: Prefixed, -{ - fn prefix(&self) -> &'static str { - self.scans.prefix() - } -} - -impl RequestHandler for PostScansIdHandler -where - S: PostScansId + Prefixed + 'static, -{ - auth_method_segments!( - authenticated: true, - Method::POST, - "scans", "*" - ); - - fn call<'a, 'b>( - &'b self, - client_id: Arc, - uri: &'a entry::Uri, - body: Bytes, - ) -> Pin + Send>> - where - 'b: 'a, - { - let gsp = self.scans.clone(); - let id = self - .ids(uri) - .into_iter() - .next() - .expect("expect ID, this is a toolkit error"); - Box::pin(async move { - match serde_json::from_slice::(&body) { - Ok(scan) => { - enforce_client_id_and_scan_id( - &client_id, - id, - gsp.as_ref(), - async |id| match gsp.post_scans_id(id, scan.action).await { - Ok(()) => BodyKind::no_content(StatusCode::NO_CONTENT), - Err(e) => e.into(), - }, - ) - .await - } - - Err(e) => e.into(), - } - }) - } -} - -impl From for PostScansIdHandler -where - T: PostScansId + 'static, -{ - fn from(value: T) -> Self { - PostScansIdHandler { - scans: Arc::new(value), - } - } -} - -impl From> for PostScansIdHandler -where - T: PostScansId + 'static, -{ - fn from(value: Arc) -> Self { - PostScansIdHandler { scans: value } - } -} - -#[derive(Debug, thiserror::Error)] -pub enum PostScansIDError { - #[error("ALready running.")] - Running, - #[error("Unexpected error occurred: {0}.")] - External(Box), -} - -impl From for PostScansIDError { - fn from(value: std::io::Error) -> Self { - Self::External(Box::new(value)) - } -} - -impl PostScansIDError { - pub fn from_external(err: E) -> Self - where - E: ExternalError + Send + Sync + 'static, - { - Self::External(Box::new(err)) - } -} - -impl From for BodyKind { - fn from(e: PostScansIDError) -> Self { - match e { - PostScansIDError::External(e) => internal_server_error!(e), - PostScansIDError::Running => BodyKind::no_content(StatusCode::CONFLICT), - } - } -} - -#[cfg(test)] -mod tests { - use entry::test_utilities::{self, json_bytes}; - use http_body_util::Empty; - use hyper::{Method, Request, service::Service}; - - use super::*; - use crate::{ - create_single_handler, - greenbone_scanner_framework::{Authentication, entry::ClientHash}, - }; - use scannerlib::models::ScanAction; - - struct Test {} - - impl Prefixed for Test { - fn prefix(&self) -> &'static str { - "" - } - } - - impl MapScanID for Test { - fn contains_scan_id<'a>( - &'a self, - client_id: &'a str, - scan_id: &'a str, - ) -> Pin> + Send + 'a>> { - Box::pin(async move { - if scan_id == "id" { - Some(client_id.to_string()) - } else { - None - } - }) - } - } - - impl PostScansId for Test { - fn post_scans_id( - &self, - client_id: String, - action: Action, - ) -> Pin> + Send>> { - let client_id = client_id.clone(); - let ok = ClientHash::from("ok").to_string(); - let already_running = ClientHash::from("already_running").to_string(); - Box::pin(async move { - if client_id == ok { - return Ok(()); - } - if client_id == already_running && action == Action::Start { - return Err(PostScansIDError::Running); - } - - Err(std::io::Error::new(std::io::ErrorKind::AlreadyExists, "").into()) - }) - } - } - - #[tokio::test] - async fn internal_server_error() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(PostScansIdHandler::from(Test {})), - Some(ClientHash::from("internal_server_error")), - ); - let scans = ScanAction { - action: Action::Start, - }; - - let req = Request::builder() - .uri("/scans/id") - .method(Method::POST) - .body(json_bytes(&scans)) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); - } - - #[tokio::test] - async fn missing_scan() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(PostScansIdHandler::from(Test {})), - Some(ClientHash::from("ok")), - ); - - let req = Request::builder() - .uri("/scans/not_found") - .method(Method::POST) - .body(Empty::::new()) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - } - - #[tokio::test] - async fn already_running() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(PostScansIdHandler::from(Test {})), - Some(ClientHash::from("already_running")), - ); - let scans = ScanAction { - action: Action::Start, - }; - - let req = Request::builder() - .uri("/scans/id") - .method(Method::POST) - .body(json_bytes(&scans)) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::CONFLICT); - } - - #[tokio::test] - async fn ok() { - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(PostScansIdHandler::from(Test {})), - Some(ClientHash::from("ok")), - ); - let scans = ScanAction { - action: Action::Start, - }; - - let req = Request::builder() - .uri("/scans/id") - .method(Method::POST) - .body(json_bytes(&scans)) - .unwrap(); - let resp = entry_point.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::NO_CONTENT); - } -} diff --git a/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans__tests__get_scans.snap b/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans__tests__get_scans.snap deleted file mode 100644 index 0d44ff3d08..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans__tests__get_scans.snap +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: src/openvasd/greenbone_scanner_framework/get_scans.rs -assertion_line: 180 -expression: resp ---- -[""] diff --git a/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results__tests__scan_results.snap b/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results__tests__scan_results.snap deleted file mode 100644 index 2e84a20dce..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results__tests__scan_results.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/openvasd/greenbone-scanner-framework/get_scans_id_results.rs -expression: resp ---- -[] diff --git a/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results_id__tests__get_status.snap b/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results_id__tests__get_status.snap deleted file mode 100644 index 8644cc1a7b..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans_id_results_id__tests__get_status.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/openvasd/greenbone-scanner-framework/get_scans_id_results_id.rs -expression: resp ---- -{"id":42,"type":"log"} diff --git a/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans_preferences__tests__get_scans_preferences.snap b/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans_preferences__tests__get_scans_preferences.snap deleted file mode 100644 index a939cca6c1..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_scans_preferences__tests__get_scans_preferences.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/openvasd/greenbone-scanner-framework/get_scans_preferences.rs -expression: resp ---- -[] diff --git a/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_vts__tests__get_vts_unauthenticated.snap b/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_vts__tests__get_vts_unauthenticated.snap deleted file mode 100644 index c508495b9d..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__get_vts__tests__get_vts_unauthenticated.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/openvasd/greenbone-scanner-framework/get_vts.rs -expression: resp ---- -["unknown"] diff --git a/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__post_scans__tests__post_scans.snap b/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__post_scans__tests__post_scans.snap deleted file mode 100644 index 87014a4499..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/snapshots/openvasd__greenbone_scanner_framework__post_scans__tests__post_scans.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/openvasd/greenbone-scanner-framework/post_scans.rs -expression: resp ---- -"response_id" diff --git a/rust/src/openvasd/greenbone_scanner_framework/tls.rs b/rust/src/openvasd/greenbone_scanner_framework/tls.rs deleted file mode 100644 index 2c22c9daef..0000000000 --- a/rust/src/openvasd/greenbone_scanner_framework/tls.rs +++ /dev/null @@ -1,221 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Greenbone AG -// -// SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception - -use std::{ - fs, - io::{self, BufRead}, - path::{Path, PathBuf}, - sync::{Arc, RwLock}, -}; - -use rustls::{ - RootCertStore, ServerConfig, - crypto::CryptoProvider, - pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject}, - server::{WebPkiClientVerifier, danger::ClientCertVerifier}, -}; - -use super::entry::ClientIdentifier; - -#[derive(Debug)] -struct ClientSnitch { - inner: Arc, - client_identifier: Arc>, -} - -impl ClientSnitch { - /// Construct a new `AllowAnyAnonymousOrAuthenticatedClient`. - /// - /// `roots` is the list of trust anchors to use for certificate validation. - fn new( - inner: Arc, - client_identifier: Arc>, - ) -> Self { - Self { - inner, - client_identifier, - } - } - - /// Wrap this verifier in an [`Arc`] and coerce it to `dyn ClientCertVerifier` - #[inline(always)] - fn boxed(self) -> Arc { - // This function is needed to keep it functioning like the original verifier. - Arc::new(self) - } -} - -impl ClientCertVerifier for ClientSnitch { - fn offer_client_auth(&self) -> bool { - self.inner.offer_client_auth() - } - - fn client_auth_mandatory(&self) -> bool { - false - } - - fn root_hint_subjects(&self) -> &[rustls::DistinguishedName] { - self.inner.root_hint_subjects() - } - - fn verify_tls12_signature( - &self, - message: &[u8], - cert: &rustls::pki_types::CertificateDer<'_>, - dss: &rustls::DigitallySignedStruct, - ) -> Result { - self.inner.verify_tls12_signature(message, cert, dss) - } - - fn verify_tls13_signature( - &self, - message: &[u8], - cert: &rustls::pki_types::CertificateDer<'_>, - dss: &rustls::DigitallySignedStruct, - ) -> Result { - self.inner.verify_tls13_signature(message, cert, dss) - } - - fn supported_verify_schemes(&self) -> Vec { - self.inner.supported_verify_schemes() - } - - fn verify_client_cert( - &self, - end_entity: &rustls::pki_types::CertificateDer<'_>, - intermediates: &[rustls::pki_types::CertificateDer<'_>], - now: rustls::pki_types::UnixTime, - ) -> Result { - let result = self - .inner - .verify_client_cert(end_entity, intermediates, now); - if result.is_ok() { - let mut ci = self.client_identifier.write().unwrap(); - *ci = ClientIdentifier::Known(end_entity.into()); - } - result - } -} -type Error = Box; - -fn config_to_tls_paths( - config: &super::TLSConfig, -) -> Result<(PathBuf, PathBuf, Vec), Error> { - let key_path = &config.server_tls_cer.key; - let certs = &config.server_tls_cer.certificate; - let client_certs = match &config.path_client_certs { - Some(x) => x, - None => { - return Ok((key_path.to_path_buf(), certs.to_path_buf(), vec![])); - } - }; - let client_certs = match std::fs::read_dir(client_certs) { - Ok(x) => x, - Err(_) => { - return Ok((key_path.to_path_buf(), certs.to_path_buf(), vec![])); - } - }; - let client_certs = client_certs - .filter_map(|x| { - let entry = x.ok()?; - let file_type = entry.file_type().ok()?; - if file_type.is_file() || file_type.is_symlink() && !file_type.is_dir() { - Some(entry.path()) - } else { - None - } - }) - .collect(); - Ok((key_path.to_path_buf(), certs.to_path_buf(), client_certs)) -} - -#[derive(Debug)] -pub struct TlsConfig { - pub client_identifier: Arc>, - pub config: ServerConfig, -} - -pub fn tls_config(config: &super::TLSConfig) -> Result { - // Install default crypto provider if none is set - if CryptoProvider::get_default().is_none() { - let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); - } - - let (key, certs, clients) = config_to_tls_paths(config)?; - - let mut roots = RootCertStore::empty(); - for root in clients.iter().flat_map(load_certs).flatten() { - roots.add(root)?; - } - let key = load_private_key(&key)?; - let certs = load_certs(&certs)?; - - if !roots.is_empty() { - let inner = WebPkiClientVerifier::builder(roots.into()).build()?; - let client_identifier = Arc::new(RwLock::new(ClientIdentifier::default())); - let verifier = ClientSnitch::new(inner, client_identifier.clone()).boxed(); - - let mut config: ServerConfig = ServerConfig::builder() - .with_client_cert_verifier(verifier) - .with_single_cert(certs, key)?; - - config.alpn_protocols = vec![b"h2".to_vec()]; - - Ok(TlsConfig { - client_identifier, - config, - }) - } else { - let client_identifier = Arc::new(RwLock::new(ClientIdentifier::default())); - let mut config: ServerConfig = ServerConfig::builder() - .with_no_client_auth() - .with_single_cert(certs, key)?; - - config.alpn_protocols = vec![b"h2".to_vec()]; - - Ok(TlsConfig { - client_identifier, - config, - }) - } -} - -fn error(err: String) -> io::Error { - io::Error::other(err) -} - -// Load public certificate from file. -fn load_certs

(filename: &P) -> io::Result>> -where - P: AsRef + std::fmt::Debug, -{ - // Open certificate file. - let certfile = - fs::File::open(filename).map_err(|e| error(format!("failed to open {filename:?}: {e}")))?; - let mut reader = io::BufReader::new(certfile); - CertificateDer::pem_reader_iter(&mut reader) - .collect::, _>>() - .map_err(|e| error(format!("{e}"))) -} - -// Load private key from file. -fn load_private_key

(filename: &P) -> io::Result> -where - P: AsRef + std::fmt::Debug, -{ - // Open keyfile. - let keyfile = - fs::File::open(filename).map_err(|e| error(format!("failed to open {filename:?}: {e}")))?; - let mut reader = io::BufReader::new(keyfile); - - loop { - match rustls::pki_types::PrivateKeyDer::pem_slice_iter(reader.fill_buf()?).next() { - Some(Ok(key)) => return Ok(key), - Some(Err(_)) => {} - None => break, - } - } - - Err(error(format!("No key found {filename:?}"))) -} diff --git a/rust/src/openvasd/main.rs b/rust/src/openvasd/main.rs index f74aa05647..6d50d2cffc 100644 --- a/rust/src/openvasd/main.rs +++ b/rust/src/openvasd/main.rs @@ -6,38 +6,34 @@ // We allow this fow now, since it would require lots of changes // but should eventually solve this. +mod api; #[cfg(test)] mod api_tests; mod config; mod container_image_scanner; mod crypt; mod database; -mod greenbone_scanner_framework; mod json_stream; mod notus; mod scans; mod vts; use sqlx::migrate::Migrator; -use std::{ - marker::{Send, Sync}, - sync::Arc, -}; +use std::sync::Arc; +use api::Authentication; use config::{Config, StorageType}; use container_image_scanner::config::{DBLocation, SqliteConfiguration}; -use greenbone_scanner_framework::{End, RuntimeBuilder, ServerCertificate}; use notus::config_to_products; -use scannerlib::models::FeedState; -use scannerlib::utils::version::show_version; +use scannerlib::{models::FeedState, utils::version::show_version}; use sqlx::SqlitePool; -type Result = std::result::Result>; +use crate::api::ApiConfig; static MIGRATOR: Migrator = sqlx::migrate!(); // TODO: move to config -async fn setup_sqlite(config: &Config) -> Result { +async fn setup_sqlite(config: &Config) -> anyhow::Result { let result = match config.storage.clone() { config::StorageTypes::V1(storage_v1) => { let mut sqliteconfig = SqliteConfiguration::default(); @@ -63,60 +59,56 @@ async fn setup_sqlite(config: &Config) -> Result { Ok(result) } -async fn build_runtime(config: Config) -> Result> { +/// Initializes all dependencies required to serve the API. +pub async fn init_api(config: Config) -> anyhow::Result { let products = config_to_products(&config); let pool = setup_sqlite(&config).await?; - let feed_snapshot = Arc::new(std::sync::RwLock::new(FeedState::Unknown)); - let (sender, vts) = vts::init(pool.clone(), &config, feed_snapshot.clone()).await; - let vts = Arc::new(vts); - let scan = scans::init(pool.clone(), &config, sender).await?; - let (get_notus, post_notus) = notus::init(products.clone()); - - let mut rb = RuntimeBuilder::::new(config.listener.address) - .feed_version(feed_snapshot.clone()); - if let Some(api_key) = config.endpoints.key.clone() { - rb = rb.api_keys(vec![api_key]); - } - match (config.tls.certs.clone(), config.tls.key.clone()) { - (Some(certificate), Some(key)) => { - rb = rb.server_tls_cer(ServerCertificate::new(key, certificate)) - } - (None, None) => { - // ok no TLS + let feed_state = Arc::new(std::sync::RwLock::new(FeedState::Unknown)); + let (sender, feed) = vts::init(pool.clone(), &config, feed_state.clone()).await; + let scanner = scans::init(pool.clone(), &config, sender).await?; + let image_scanner = + container_image_scanner::init(products.clone(), config.container_image_scanner).await?; + + let auth_method = match ( + config.tls.client_certs.is_some() && config.tls.certs.is_some() && config.tls.key.is_some(), + config.endpoints.key.is_some(), + ) { + (true, true) => { + tracing::info!("mTLS and api-key configured, favoring mTLS and disabling api-key"); + Authentication::Mtls } - _ => { - tracing::warn!( - "Invalid TLS configuration. Please provide a certificate path and a key path. Falling back to http." - ) + (true, false) => Authentication::Mtls, + (false, true) => Authentication::ApiKey, + (false, false) => { + tracing::warn!("neither api-key nor mTLS configured. Endpoints are not secured."); + Authentication::Disabled } }; + if !config.feed.signature_check { tracing::warn!( "Integrity check for feed has been disabled. Neither hashsums nor GPG signature will get verified." - ); + ) } - if let Some(client_certs) = config.tls.client_certs.clone() { - rb = rb.path_client_certs(client_certs); - } - - let (cis_scans, cis_vts) = container_image_scanner::init( - pool.clone(), - feed_snapshot, - products, - config.container_image_scanner, - ) - .await?; - Ok(rb - .insert_scans(Arc::new(scan)) - .insert_get_vts(vts.clone()) - .max_concurrent_connections(config.storage.max_http_connections()) - .add_request_handler(get_notus) - .add_request_handler(post_notus) - .insert_additional_scan_endpoints(Arc::new(cis_scans), Arc::new(cis_vts))) + Ok(ApiConfig { + address: config.listener.address, + auth_method, + server_cert_path: config.tls.certs, + server_key_file: config.tls.key, + client_certs_path: config.tls.client_certs, + // TODO: make new variable? + max_requests: config.storage.max_http_connections(), + api_keys: Arc::new(config.endpoints.key.map(|x| vec![x]).unwrap_or(vec![])), + feed, + scanner, + image_scanner, + notus: products, + enable_additional_routes: config.endpoints.enable_get_scans, + }) } -async fn _main() -> Result { +async fn _main() -> anyhow::Result { let config = Config::load(); let _guard = config.logging.init(); @@ -125,7 +117,8 @@ async fn _main() -> Result { return Ok(0); } - build_runtime(config).await?.run_blocking().await + let cfg = init_api(config).await?; + api::run(&cfg).await } #[tokio::main] diff --git a/rust/src/openvasd/notus/mod.rs b/rust/src/openvasd/notus/mod.rs index 31fe7e70ee..4dac4e6462 100644 --- a/rust/src/openvasd/notus/mod.rs +++ b/rust/src/openvasd/notus/mod.rs @@ -1,152 +1,22 @@ -// We allow this fow now, since it would require lots of changes -// but should eventually solve this. -#![allow(clippy::result_large_err)] +use std::sync::Arc; -use std::{pin::Pin, sync::Arc}; - -use crate::greenbone_scanner_framework::{ - ClientIdentifier, RequestHandler, - entry::{Bytes, Method, Prefixed, Uri, response::BodyKind}, -}; -use http::StatusCode; -use scannerlib::notus::{Notus, NotusError, products_loader}; +use scannerlib::notus::{Notus, products_loader}; use tokio::sync::RwLock; use crate::config::Config; -type Oz = Notus; - -pub struct GetOSIcnomingRequest(Arc>); - -impl Prefixed for GetOSIcnomingRequest { - fn prefix(&self) -> &'static str { - "" - } -} -impl RequestHandler for GetOSIcnomingRequest { - fn needs_authentication(&self) -> bool { - false - } - - fn path_segments(&self) -> &'static [&'static str] { - &["notus"] - } - - fn http_method(&self) -> Method { - Method::GET - } - - fn call<'a, 'b>( - &'b self, - _: Arc, - _: &'a Uri, - _: Bytes, - // req: Request, - ) -> Pin + Send>> - where - 'b: 'a, - { - let products = self.0.clone(); - Box::pin(async move { - let p = products.read_owned().await; - match tokio::task::spawn_blocking(move || p.get_available_os()) - .await - .expect("Tokio runtime must be available") - { - Ok(x) => BodyKind::json_content(StatusCode::OK, &x), - Err(error) => { - tracing::warn!(%error, "Unable to get available products."); - BodyKind::no_content(StatusCode::INTERNAL_SERVER_ERROR) - } - } - }) - } -} - -pub struct PostOSIcnomingRequest(Arc>); - -impl Prefixed for PostOSIcnomingRequest { - fn prefix(&self) -> &'static str { - "" - } -} - -impl RequestHandler for PostOSIcnomingRequest { - fn needs_authentication(&self) -> bool { - false - } - - fn path_segments(&self) -> &'static [&'static str] { - &["notus", "*"] - } - - fn http_method(&self) -> Method { - Method::POST - } - - fn call<'a, 'b>( - &'b self, - _: Arc, - uri: &'a Uri, - body: Bytes, - ) -> Pin + Send>> - where - 'b: 'a, - { - let products = self.0.clone(); - - let os = self - .ids(uri) - .into_iter() - .next() - .expect("expect OS, this is a toolkit error"); - - Box::pin(async move { - let mut p = products.write_owned().await; - match tokio::task::spawn_blocking(move || { - let packages: Vec = serde_json::from_slice(&body) - .map_err(|e| scannerlib::notus::NotusError::PackageParseError(e.to_string()))?; - p.scan(&os, &packages) - }) - .await - .expect("Tokio runtime must be available") - { - Ok(x) => BodyKind::json_content(StatusCode::OK, &x), - Err(NotusError::UnknownProduct(_)) => BodyKind::no_content(StatusCode::NOT_FOUND), - Err(NotusError::PackageParseError(e)) => { - BodyKind::json_content(StatusCode::BAD_REQUEST, &e) - } - Err(error) => { - tracing::warn!(%error, "Unable to get available products."); - BodyKind::json_content(StatusCode::INTERNAL_SERVER_ERROR, &error.to_string()) - } - } - }) - } -} - pub fn config_to_products(config: &Config) -> Arc> { products_loader(&config.notus.products_path, config.feed.signature_check) } -pub fn init(notus: Arc>) -> (GetOSIcnomingRequest, PostOSIcnomingRequest) { - ( - GetOSIcnomingRequest(notus.clone()), - PostOSIcnomingRequest(notus), - ) -} - #[cfg(test)] mod tests { - use crate::greenbone_scanner_framework::{ - Authentication, - entry::ClientHash, - entry::{Method, test_utilities}, + use crate::api::{routes, tests::json_request}; + use axum::{ + body::Body, + http::{Request, StatusCode}, }; - - use crate::create_single_handler; - use http::StatusCode; - use hyper::service::Service; + use tower::ServiceExt; use crate::config::Config; fn config() -> Config { @@ -178,44 +48,27 @@ mod tests { } #[tokio::test] - async fn get_notus() -> crate::Result<()> { + async fn get_notus() -> anyhow::Result<()> { let config = config(); - let (undertest, _) = super::init(super::config_to_products(&config)); - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(undertest), - Some(ClientHash::default()), - ); - let req = test_utilities::empty_request(Method::GET, "/notus"); - let resp = entry_point.call(req).await?; + let products = super::config_to_products(&config); + let router = routes::notus::router(products); + + let req = Request::get("/").body(Body::empty())?; + let resp = router.oneshot(req).await?; + assert_eq!(resp.status(), StatusCode::OK); Ok(()) } #[tokio::test] - async fn post_notus_os() -> crate::Result<()> { + async fn post_notus_os() -> anyhow::Result<()> { let config = config(); - let (_, undertest) = super::init(super::config_to_products(&config)); - let entry_point = test_utilities::entry_point( - Authentication::Mtls, - create_single_handler!(undertest), - Some(ClientHash::default()), - ); - let req = test_utilities::json_request( - Method::POST, - "/notus/not_found", - &vec!["aha".to_string()], - ); - let resp = entry_point.call(req).await?; - assert_eq!(resp.status(), StatusCode::NOT_FOUND); + let products = super::config_to_products(&config); + let router = routes::notus::router(products); - let req = test_utilities::json_request( - Method::POST, - "/notus/test", - &vec!["man-db-1.1.1".to_string()], - ); - let resp = entry_point.call(req).await?; - assert_eq!(resp.status(), StatusCode::OK); + let req = json_request("POST", "/not_found", &["aha".to_string()]); + let resp = router.oneshot(req).await?; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); Ok(()) } diff --git a/rust/src/openvasd/scans/mod.rs b/rust/src/openvasd/scans/mod.rs index f479548e29..2818693dda 100644 --- a/rust/src/openvasd/scans/mod.rs +++ b/rust/src/openvasd/scans/mod.rs @@ -1,240 +1,10 @@ -use std::{pin::Pin, sync::Arc}; - -use crate::database::{ - dao::{DAOError, DBViolation, Execute, Fetch, StreamFetch}, - sqlite::{DataBase, results::DBResults, scans::ScanDB}, -}; -use crate::greenbone_scanner_framework::InternalIdentifier; -use crate::greenbone_scanner_framework::prelude::*; -use futures::TryStreamExt; -use scannerlib::{models, scanner}; -use tokio::sync::mpsc::Sender; - -use crate::{ - config::Config, - crypt::{ChaCha20Crypt, Crypt}, - vts::orchestrator, -}; -mod scheduling; -pub struct Endpoints { - pool: DataBase, - crypter: Arc, - scheduling: Sender, -} - -impl Prefixed for Endpoints { - fn prefix(&self) -> &'static str { - "" - } -} - -impl PostScans for Endpoints -where - E: crate::crypt::Crypt + Sync + Send, -{ - fn post_scans( - &self, - client_id: String, - scan: models::Scan, - ) -> Pin> + Send + '_>> { - Box::pin(async move { - match ScanDB::new( - &self.pool, - (self.crypter.as_ref(), &client_id as &str, &scan), - ) - .exec() - .await - { - Ok(result) => Ok(result), - Err(DAOError::DBViolation(DBViolation::UniqueViolation)) => { - Err(PostScansError::DuplicateId(scan.scan_id)) - } - Err(x) => Err(PostScansError::External(Box::new(x))), - } - }) - } -} - -impl MapScanID for Endpoints -where - E: Send + Sync, -{ - fn contains_scan_id<'a>( - &'a self, - client_id: &'a str, - scan_id: &'a str, - ) -> Pin> + Send + 'a>> { - Box::pin(async move { - match ScanDB::new(&self.pool, (client_id, scan_id)).fetch().await { - Ok(x) => x, - Err(error) => { - tracing::warn!(%error, "Unable to fetch id from client_scan_map. Returning no id found."); - None - } - } - }) - } -} - -fn into_external_error(value: T) -> GetScansError -where - T: std::error::Error + Send + Sync + 'static, -{ - GetScansError::External(Box::new(value)) -} - -impl GetScans for Endpoints -where - E: Send + Sync, -{ - fn get_scans(&self, client_id: String) -> StreamResult { - Box::pin( - ScanDB::new(&self.pool, client_id) - .stream_fetch() - .map_err(into_external_error), - ) - } -} +use std::sync::Arc; -impl GetScansPreferences for Endpoints -where - E: Send + Sync, -{ - fn get_scans_preferences( - &self, - ) -> Pin> + Send>> { - Box::pin(async move { scanner::preferences::preference::PREFERENCES.to_vec() }) - } -} +use crate::api::states::ScannerBridge; +use crate::database::sqlite::DataBase; -impl GetScansId for Endpoints -where - E: Send + Sync + Crypt, -{ - fn get_scans_id( - &self, - id: String, - ) -> Pin> + Send + '_>> { - Box::pin(async move { - let id = id.parse().map_err(into_external_error)?; - ScanDB::new(&self.pool, (self.crypter.as_ref(), id)) - .fetch() - .await - .map_err(into_external_error) - }) - } -} - -impl GetScansIdResults for Endpoints -where - E: Send + Sync, -{ - fn get_scans_id_results( - &self, - id: String, - from: Option, - to: Option, - ) -> StreamResult { - let result = DBResults::new(&self.pool, (id, from, to)) - .stream_fetch() - .map_err(GetScansError::from_external); - - Box::pin(result) - } -} - -impl GetScansIdResultsId for Endpoints -where - E: Send + Sync, -{ - fn get_scans_id_results_id( - &self, - id: String, - result_id: usize, - ) -> Pin> + Send + '_>> - { - Box::pin(async move { - DBResults::new(&self.pool, (id, result_id)) - .fetch() - .await - .map_err(|e| match e { - DAOError::NotFound => GetScansIDResultsIDError::NotFound, - e => e.into(), - }) - }) - } -} - -impl GetScansIdStatus for Endpoints -where - E: Send + Sync, -{ - fn get_scans_id_status( - &self, - id: String, - ) -> Pin> + Send + '_>> - { - Box::pin(async move { - let id: i64 = id - .parse() - .map_err(|e| GetScansIDStatusError::External(Box::new(e)))?; - ScanDB::new(&self.pool, id) - .fetch() - .await - .map_err(|e| GetScansIDStatusError::External(Box::new(e))) - }) - } -} -impl PostScansId for Endpoints -where - E: Send + Sync, -{ - fn post_scans_id( - &self, - id: String, - action: models::Action, - ) -> Pin> + Send + '_>> { - Box::pin(async move { - self.scheduling - .send(match action { - models::Action::Start => scheduling::Message::Start(id), - models::Action::Stop => scheduling::Message::Stop(id), - }) - .await - .map_err(|e| PostScansIDError::External(Box::new(e))) - }) - } -} -impl DeleteScansId for Endpoints -where - E: Send + Sync, -{ - fn delete_scans_id( - &self, - id: String, - ) -> Pin> + Send + '_>> { - Box::pin(async move { - // Ensure the scan is not running. - let internal_id: i64 = id - .parse() - .map_err(|e| DeleteScansIDError::External(Box::new(e)))?; - let status: models::Status = ScanDB::new(&self.pool, internal_id) - .fetch() - .await - .map_err(|e| DeleteScansIDError::External(Box::new(e)))?; - if status.is_running() { - return Err(DeleteScansIDError::Running); - } - - // everything else should have ON DELETE CASCADE - ScanDB::new(&self.pool, id) - .exec() - .await - .map_err(|e| DeleteScansIDError::External(Box::new(e))) - .map(|_| ()) - }) - } -} +use crate::{config::Config, crypt::ChaCha20Crypt, vts::orchestrator}; +pub(crate) mod scheduling; pub(crate) fn config_to_crypt(config: &Config) -> ChaCha20Crypt { // unwrap_or_else is a safe guard in the case the db is stored on disk but no key is provided. @@ -250,15 +20,10 @@ pub async fn init( pool: DataBase, config: &Config, feed_status: orchestrator::Communicator, -) -> Result, Box> { +) -> anyhow::Result { let crypter = Arc::new(config_to_crypt(config)); - let scheduler_sender = - scheduling::init(pool.clone(), crypter.clone(), config, feed_status).await?; - Ok(Endpoints { - pool, - crypter, - scheduling: scheduler_sender, - }) + let scheduler = scheduling::init(pool.clone(), crypter.clone(), config, feed_status).await?; + Ok(ScannerBridge::new(pool, Some(crypter), Some(scheduler))) } #[cfg(test)] @@ -267,11 +32,8 @@ pub mod tests { use super::*; - use crate::greenbone_scanner_framework::{ - GetScans, GetScansId, GetScansIdResults, GetScansIdStatus, GetScansPreferences, MapScanID, - PostScans, PostScansError, prelude::PostScansId, - }; - + use axum::body::{Body, to_bytes}; + use axum::extract::Request; use futures::StreamExt; use scannerlib::models::{ self, AliveTestMethods, Credential, CredentialType, Phase, PrivilegeInformation, @@ -279,17 +41,16 @@ pub mod tests { }; use scannerlib::{scanner, utils::scanner_types::ScannerType}; use sqlx::{SqlitePool, query_scalar}; + use tower::ServiceExt; - use crate::{ - config::Config, - crypt::ChaCha20Crypt, - scans::{config_to_crypt, scheduling}, - }; + use crate::api::{error::ApiError, routes}; + use crate::database::{dao::Execute, sqlite::scans::ScanDB}; + use crate::{config::Config, scans::config_to_crypt}; - async fn init(pool: SqlitePool, config: &Config) -> super::Endpoints { + async fn init(pool: SqlitePool, config: &Config) -> anyhow::Result { let ignored = Default::default(); - super::init(pool, config, ignored).await.unwrap() + super::init(pool, config, ignored).await } fn generate_hosts() -> Vec> { @@ -506,7 +267,7 @@ pub mod tests { results } - pub async fn create_pool() -> crate::Result<(Config, SqlitePool)> { + pub async fn create_pool() -> anyhow::Result<(Config, SqlitePool)> { let nasl = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/feed/nasl").into(); let advisories_path = concat!( env!("CARGO_MANIFEST_DIR"), @@ -564,37 +325,33 @@ pub mod tests { } #[tokio::test] - async fn post_scan() -> crate::Result<()> { + async fn post_scan() -> anyhow::Result<()> { let (config, pool) = create_pool().await?; - let undertest = init(pool, &config).await; + let undertest = init(pool, &config).await?; let client_id = "moep".to_string(); for scan in generate_scan() { - let id = scan.scan_id.clone(); - let result = undertest.post_scans(client_id.clone(), scan).await.unwrap(); - assert_eq!(id, result); + undertest.post_scan(&client_id, &scan).await?; } Ok(()) } #[tokio::test] - async fn post_scan_duplicate_id() -> crate::Result<()> { + async fn post_scan_duplicate_id() -> anyhow::Result<()> { let (config, pool) = create_pool().await?; - let undertest = init(pool, &config).await; + let undertest = init(pool, &config).await?; let client_id = "moep".to_string(); let scans = generate_scan(); assert!(!scans.is_empty()); for scan in scans.clone() { - let id = scan.scan_id.clone(); - let result = undertest.post_scans(client_id.clone(), scan).await; + let result = undertest.post_scan(&client_id, &scan).await; assert!(result.is_ok(), "scan must be successfully added"); - assert_eq!(id, result.unwrap()); } for scan in scans { - let result = undertest.post_scans(client_id.clone(), scan).await; + let result = undertest.post_scan(&client_id, &scan).await; assert!( - matches!(result, Err(PostScansError::DuplicateId(_))), + matches!(result, Err(crate::database::dao::DAOError::DBViolation(_))), "scan must be declined" ); } @@ -603,39 +360,35 @@ pub mod tests { } #[tokio::test] - async fn map_id() -> crate::Result<()> { + async fn map_id() -> anyhow::Result<()> { let (config, pool) = create_pool().await?; - let undertest = init(pool, &config).await; + let undertest = init(pool, &config).await?; let client_id = "moep".to_string(); let scans = generate_scan(); assert!(!scans.is_empty()); for scan in scans.clone() { - undertest.post_scans(client_id.clone(), scan).await?; + undertest.post_scan(&client_id, &scan).await?; } for scan in scans { - let result = undertest.contains_scan_id(&client_id, &scan.scan_id).await; - assert!(result.is_some(), "scan must be found"); + let result = undertest.get_scan_id(&client_id, &scan.scan_id).await; + assert!(result.is_ok(), "scan must be found"); } Ok(()) } #[tokio::test] - async fn get_scan_id() -> crate::Result<()> { + async fn get_scan_id() -> anyhow::Result<()> { let (config, pool) = create_pool().await?; - let undertest = init(pool, &config).await; + let undertest = init(pool, &config).await?; let client_id = "moep".to_string(); let scans = generate_scan(); assert!(!scans.is_empty()); for scan in scans.clone() { - undertest.post_scans(client_id.clone(), scan).await?; + undertest.post_scan(&client_id, &scan).await?; } for scan in scans { - let result = undertest - .contains_scan_id(&client_id, &scan.scan_id) - .await - .unwrap(); - let result = undertest.get_scans_id(result).await?; + let result = undertest.get_scan(&client_id, &scan.scan_id).await?; assert_eq!(scan.scan_id, result.scan_id); assert_eq!( scan.target.credentials.len(), @@ -647,25 +400,38 @@ pub mod tests { } #[tokio::test] - async fn get_scans_preferences() -> crate::Result<()> { + async fn get_scans_preferences() -> anyhow::Result<()> { let (config, pool) = create_pool().await?; - let undertest = init(pool, &config).await; - let result = undertest.get_scans_preferences().await; + let undertest = init(pool, &config).await?; + + let router = routes::scans::router( + undertest, + crate::api::Authentication::Disabled, + Arc::new(Vec::new()), + false, + ); + + let response = router + .oneshot(Request::get("/preferences").body(Body::empty())?) + .await?; + let bytes = to_bytes(response.into_body(), usize::MAX).await?; + let preferences = String::from_utf8(bytes.to_vec())?; + assert_eq!( - result, - scanner::preferences::preference::PREFERENCES.to_vec() + preferences, + *scanner::preferences::preference::PREFERENCES_JSON ); Ok(()) } #[tokio::test] - async fn get_scan_id_status() -> crate::Result<()> { + async fn get_scan_id_status() -> anyhow::Result<()> { let (config, pool) = create_pool().await?; let crypter = Arc::new(config_to_crypt(&config)); let (_, _, communicator) = orchestrator::Communicator::init(); - let scheduler_sender = scheduling::init_with_scanner( + let scheduler = scheduling::init_with_scanner( pool.clone(), crypter.clone(), &config, @@ -673,84 +439,80 @@ pub mod tests { communicator, ) .await?; - let undertest = super::Endpoints { + + let undertest = super::ScannerBridge { pool, - crypter, - scheduling: scheduler_sender, + crypter: Some(crypter), + scheduler: Some(scheduler), }; let client_id = "moep".to_string(); let scans = generate_scan(); assert!(!scans.is_empty()); for scan in scans.clone() { - undertest.post_scans(client_id.clone(), scan).await?; + undertest.post_scan(&client_id, &scan).await?; } for scan in scans.iter() { - let result = undertest - .contains_scan_id(&client_id, &scan.scan_id) - .await - .unwrap(); - let result = undertest.get_scans_id_status(result).await?; + let result = undertest.get_scan_status(&client_id, &scan.scan_id).await?; assert_eq!(result.status, Phase::Stored); } for scan in scans.iter() { - let id = undertest - .contains_scan_id(&client_id, &scan.scan_id) - .await - .unwrap(); - undertest - .post_scans_id(id.clone(), models::Action::Start) + .schedule_scan(&client_id, &scan.scan_id, models::Action::Start) .await?; let mut status; loop { - status = undertest.get_scans_id_status(id.clone()).await?; + status = undertest.get_scan_status(&client_id, &scan.scan_id).await?; if status.is_running() { break; } } assert!(matches!(status.status, Phase::Requested | Phase::Running)); } - for scan in scans.iter() { - let id = undertest - .contains_scan_id(&client_id, &scan.scan_id) - .await - .unwrap(); + for scan in scans.iter() { + // Why start them again ? undertest - .post_scans_id(id.clone(), models::Action::Start) + .schedule_scan(&client_id, &scan.scan_id, models::Action::Start) .await?; let mut status; loop { - status = undertest.get_scans_id_status(id.clone()).await?; + status = undertest.get_scan_status(&client_id, &scan.scan_id).await?; if status.is_done() { break; } } + assert!(matches!(status.status, Phase::Succeeded)); + let result = undertest - .get_scans_id_results(id.clone(), None, None) + .get_scan_results(&client_id, &scan.scan_id, None, None) + .await? .collect::>() .await; assert_eq!(result.into_iter().filter_map(|x| x.ok()).count(), 2); let result = undertest - .get_scans_id_results(id.clone(), Some(1), None) + .get_scan_results(&client_id, &scan.scan_id, Some(1), None) + .await? .collect::>() .await; assert_eq!(result.into_iter().filter_map(|x| x.ok()).count(), 1); let result = undertest - .get_scans_id_results(id.clone(), None, Some(0)) + .get_scan_results(&client_id, &scan.scan_id, None, Some(0)) + .await? .collect::>() .await; assert_eq!(result.into_iter().filter_map(|x| x.ok()).count(), 1); let result = undertest - .get_scans_id_results(id.clone(), Some(0), Some(0)) + .get_scan_results(&client_id, &scan.scan_id, Some(0), Some(0)) + .await? .collect::>() .await; assert_eq!(result.into_iter().filter_map(|x| x.ok()).count(), 1); let result = undertest - .get_scans_id_results(id, Some(23), None) + .get_scan_results(&client_id, &scan.scan_id, Some(23), None) + .await? .collect::>() .await; assert_eq!(result.len(), 0); @@ -760,19 +522,24 @@ pub mod tests { } #[tokio::test] - async fn get_scans() -> crate::Result<()> { + async fn get_scans() -> anyhow::Result<()> { let (config, pool) = create_pool().await?; - let undertest = init(pool, &config).await; + let undertest = init(pool, &config).await?; let client_id = "moep".to_string(); let scans = generate_scan(); for scan in generate_scan() { - undertest.post_scans(client_id.clone(), scan).await?; + undertest.post_scan(&client_id, &scan).await?; } - let client_ids = undertest.get_scans(client_id).collect::>().await; + let client_ids = undertest + .get_scans(client_id) + .await + .collect::>>() + .await; assert_eq!(client_ids.iter().filter(|x| x.is_err()).count(), 0); assert_eq!(client_ids.iter().filter(|x| x.is_ok()).count(), scans.len()); let client_ids = undertest .get_scans("notme".to_string()) + .await .collect::>() .await; assert!(client_ids.is_empty()); diff --git a/rust/src/openvasd/scans/scheduling.rs b/rust/src/openvasd/scans/scheduling.rs index 2cbb1e1b12..732090426a 100644 --- a/rust/src/openvasd/scans/scheduling.rs +++ b/rust/src/openvasd/scans/scheduling.rs @@ -122,9 +122,6 @@ pub enum Message { Stop(String), } -// maybe we should just use AnyHow -type R = Result>; - #[derive(Debug, thiserror::Error)] enum LockFileError { #[error("Unable to open feed update lock file {path}: {source}")] @@ -148,7 +145,7 @@ impl ScanScheduler { /// Should be called on restart if the application crashed while there were running scans. /// /// This is to safe guard against ghost scans that will never finish. - async fn running_to_failed(&self) -> R<()> { + async fn running_to_failed(&self) -> anyhow::Result<()> { let affected = self .scan_state .change_state_all("running", "failed") @@ -163,7 +160,7 @@ impl ScanScheduler { Ok(()) } - async fn scan_to_requested(&self, id: i64) -> R<()> { + async fn scan_to_requested(&self, id: i64) -> anyhow::Result<()> { self.scan_state .change_state(id, "stored", "requested") .await?; @@ -174,7 +171,7 @@ impl ScanScheduler { Ok(()) } - async fn scan_running_to_failed(&self, id: i64, reason: &str) -> R<()> { + async fn scan_running_to_failed(&self, id: i64, reason: &str) -> anyhow::Result<()> { let changed = self .scan_state .change_state(id, "running", "failed") @@ -186,7 +183,11 @@ impl ScanScheduler { Ok(()) } - async fn scan_insert_results(&self, id: i64, results: Vec) -> R<()> { + async fn scan_insert_results( + &self, + id: i64, + results: Vec, + ) -> anyhow::Result<()> { // TODO: maybe better to use i64 in the impl? let id: &str = &id.to_string(); DBResults::new(&self.pool, (id, &results as &[_])) @@ -196,7 +197,7 @@ impl ScanScheduler { } } -fn is_file_locked(path: String) -> R { +fn is_file_locked(path: String) -> anyhow::Result { let mut file = LockFile::open(&path).map_err(|source| LockFileError::Open { path: path.clone(), source, @@ -236,7 +237,7 @@ where } } - async fn fetch_requested(&self) -> R> { + async fn fetch_requested(&self) -> anyhow::Result> { let limit: Option = if self.max_concurrent_scan > 0 { let running = self.scan_state.count_scans_in_state("running").await?; Some(if running > self.max_concurrent_scan { @@ -262,7 +263,7 @@ where .is_feed_sync_in_progress() } - async fn set_to_running(&self, id: i64) -> R<()> { + async fn set_to_running(&self, id: i64) -> anyhow::Result<()> { let running = self .scan_state .change_state(id, "requested", "running") @@ -284,7 +285,7 @@ where /// /// In the case that the ScanStarter implementation blocks start_scan is spawned as a /// background task. - async fn requested_to_running(&self) -> R<()> { + async fn requested_to_running(&self) -> anyhow::Result<()> { if self.is_feed_sync_in_progress().await { tracing::trace!("Skipping to set new scans to running because of feed sync."); return Ok(()); @@ -304,7 +305,7 @@ where Ok(()) } - async fn scan_import_results(&self, internal_id: i64, scan_id: String) -> R<()> { + async fn scan_import_results(&self, internal_id: i64, scan_id: String) -> anyhow::Result<()> { let mut results = match self.scanner.fetch_results(scan_id.clone()).await { Ok(x) => x, Err(scannerlib::scanner::Error::ScanNotFound(scan_id)) => { @@ -340,7 +341,7 @@ where Ok(()) } - async fn import_results(&self) -> R<()> { + async fn import_results(&self) -> anyhow::Result<()> { let scans = self .scan_state .fetch_scans_in_state("running", None) @@ -357,13 +358,13 @@ where Ok(()) } - async fn scanner_delete_scan(&self, internal_id: i64, scan_id: String) -> R<()> { + async fn scanner_delete_scan(&self, internal_id: i64, scan_id: String) -> anyhow::Result<()> { tracing::debug!(internal_id, scan_id, "deleting scan from scanner"); self.scanner.delete_scan(scan_id).await?; Ok(()) } - async fn scan_stop(&self, id: i64) -> R<()> { + async fn scan_stop(&self, id: i64) -> anyhow::Result<()> { let scan_id: String = ScanDB::new(&self.pool, id).fetch().await?; let current_status = self.scan_state.scan_get_status(id).await?; @@ -383,7 +384,7 @@ where Ok(()) } - async fn on_user_action(&self, message: &Message) -> R<()> { + async fn on_user_action(&self, message: &Message) -> anyhow::Result<()> { match message { Message::Start(id) => self.scan_to_requested(id.parse()?).await?, Message::Stop(id) => self.scan_stop(id.parse()?).await?, @@ -394,7 +395,7 @@ where async fn on_feed_action( &self, message: &orchestrator::FeedStatusChange, - ) -> R>> { + ) -> anyhow::Result>> { let msg = message.clone(); self.feed_sync_in_progress .write() @@ -452,7 +453,7 @@ where self.feed_sync_in_progress.write().await.approve() } - async fn on_schedule(&self) -> R> { + async fn on_schedule(&self) -> anyhow::Result> { if self.contains_need().await { let count_running = self.get_running_count().await; let filelocked = { @@ -479,7 +480,7 @@ async fn run_scheduler( check_interval: std::time::Duration, scheduler: ScanScheduler, feed: orchestrator::Communicator, -) -> R> +) -> anyhow::Result> where S: Scanner + Send + Sync + 'static, E: Crypt + Send + Sync + 'static, @@ -558,7 +559,7 @@ pub(super) async fn init_with_scanner( config: &Config, scanner: S, feed: orchestrator::Communicator, -) -> R> +) -> anyhow::Result> where S: Scanner + Send + Sync + 'static, E: Crypt + Send + Sync + 'static, @@ -582,7 +583,7 @@ pub async fn init( crypter: Arc, config: &Config, feed_status: orchestrator::Communicator, -) -> R> +) -> anyhow::Result> where E: Crypt + Send + Sync + 'static, { @@ -635,7 +636,7 @@ where #[cfg(test)] pub(crate) mod tests { use scannerlib::{ - models::Status, + models::{self, Status}, scanner::{self, ScanResults, TestScannerBuilder}, }; use sqlx::query_scalar; @@ -648,22 +649,22 @@ pub(crate) mod tests { tests::{create_pool, prepare_scans}, }, }; - type TR = R<()>; - async fn setup_test_env() -> R<(ScanScheduler, Vec)> { + async fn setup_test_env() + -> anyhow::Result<(ScanScheduler, Vec)> { setup_test_env_with_scanner(TestScannerBuilder::default()).await } async fn setup_test_env_with_scanner( builder: TestScannerBuilder, - ) -> R<(ScanScheduler, Vec)> { + ) -> anyhow::Result<(ScanScheduler, Vec)> { setup_test_env_with_scanner_and_feed_messages(builder, Default::default()).await } async fn setup_test_env_with_scanner_and_feed_messages( builder: TestScannerBuilder, feed_changes: IsInProgress, - ) -> R<(ScanScheduler, Vec)> { + ) -> anyhow::Result<(ScanScheduler, Vec)> { let (config, pool) = create_pool().await?; let scanner = Arc::new(builder.build()); let cryptor = Arc::new(scans::config_to_crypt(&config)); @@ -683,7 +684,7 @@ pub(crate) mod tests { } #[tokio::test] - async fn start_scan() -> TR { + async fn start_scan() -> anyhow::Result<()> { let (under_test, known_scans) = setup_test_env().await?; for id in known_scans.iter() { @@ -704,7 +705,7 @@ pub(crate) mod tests { } #[tokio::test] - async fn run_scans() -> TR { + async fn run_scans() -> anyhow::Result<()> { let (under_test, known_scans) = setup_test_env().await?; for id in known_scans.iter() { under_test @@ -781,7 +782,7 @@ pub(crate) mod tests { #[tokio::test] // maybe create a function of that so that it can be used within scans testing - async fn reflect_status_phase_of_scan() -> TR { + async fn reflect_status_phase_of_scan() -> anyhow::Result<()> { let (under_test, known_scans) = setup_test_env_with_scanner(scanner_succeeded()).await?; for id in known_scans.iter() { under_test @@ -816,7 +817,7 @@ pub(crate) mod tests { } #[tokio::test] - async fn run_scans_failure() -> TR { + async fn run_scans_failure() -> anyhow::Result<()> { let (under_test, known_scans) = setup_test_env_with_scanner( TestScannerBuilder::new() .with_start(|_| Err(scanner::Error::Connection("nada".to_string()))), @@ -851,7 +852,7 @@ pub(crate) mod tests { } #[tokio::test] - async fn do_not_start_when_scanner_cannot_start_scan() -> TR { + async fn do_not_start_when_scanner_cannot_start_scan() -> anyhow::Result<()> { let (under_test, known_scans) = setup_test_env_with_scanner(TestScannerBuilder::new().with_can_start(|| false)).await?; @@ -873,7 +874,7 @@ pub(crate) mod tests { } #[tokio::test] - async fn do_not_start_on_feed_sync() -> TR { + async fn do_not_start_on_feed_sync() -> anyhow::Result<()> { let iip = IsInProgress { need_approval_nasl: true, ..Default::default() diff --git a/rust/src/openvasd/vts/mod.rs b/rust/src/openvasd/vts/mod.rs index 0e1b47b2b3..eacd112f59 100644 --- a/rust/src/openvasd/vts/mod.rs +++ b/rust/src/openvasd/vts/mod.rs @@ -7,7 +7,6 @@ use std::{ sync::Arc, }; -use crate::greenbone_scanner_framework::{GetVTsError, StreamResult}; use scannerlib::Promise; use scannerlib::feed::{HashSumFileItem, HashSumNameLoader, check_signature}; use scannerlib::nasl::syntax::Loader; @@ -20,14 +19,13 @@ use scannerlib::{ }; use walkdir::WalkDir; +use crate::api::{StreamResult, states::Feed}; use crate::config::Config; pub mod orchestrator; pub mod redis; -pub use crate::container_image_scanner::endpoints::vts::VTEndpoints as Endpoints; use crate::database::sqlite::DataBase; use crate::json_stream; use crate::vts::orchestrator::WorkerError; -//use crate::vts::sql::SqlPluginStorage; #[derive(Clone, Debug, PartialEq, Eq)] /// Contains the hash values of the sha256sums for specific feeds @@ -95,13 +93,6 @@ impl Plugin for VulnerabilityData { } } -fn error_vts_error(error: T) -> GetVTsError -where - T: std::error::Error + Sync + Send + 'static, -{ - GetVTsError::External(Box::new(error)) -} - pub trait PluginFetcher { fn get_oids(&self) -> StreamResult; @@ -113,7 +104,7 @@ pub async fn _init( fetcher: F, worker: W, snapshot: Arc>, -) -> (orchestrator::Communicator, Endpoints) +) -> (orchestrator::Communicator, Feed) where F: PluginFetcher + Send + Sync + 'static, W: orchestrator::Worker + Send + Sync + 'static, @@ -122,14 +113,15 @@ where orchestrator::Orchestrator::init(config.feed.check_interval, snapshot.clone(), worker) .await; - (communicator, Endpoints::new(fetcher, snapshot, None)) + (communicator, Feed::new(fetcher, snapshot)) } + /// Initializes endpoints, spawns background task for feed verification. pub async fn init( pool: DataBase, config: &Config, snapshot: Arc>, -) -> (orchestrator::Communicator, Endpoints) { +) -> (orchestrator::Communicator, Feed) { match config.scanner.scanner_type { ScannerType::Openvas => { let socket = get_redis_socket().await; @@ -198,8 +190,7 @@ where serde_handler .await - .expect("tokio::task::spawn_blocking to be executed to run") - .map_err(error_vts_error)?; + .expect("tokio::task::spawn_blocking to be executed to run")?; forwarder .await .expect("tokio::task::spawn_blocking to be executed to run"); @@ -252,8 +243,8 @@ where synchronize_json::<_, VulnerabilityData, _>(ps, &hash, move |sender| { let loader = Loader::from_feed_path(&path); - let advisories_files = - advisory_loader(signature_check, &loader).map_err(error_vts_error)?; + let advisories_files = advisory_loader(signature_check, &loader) + .map_err(|e| WorkerError::Sync(Box::new(e)))?; for result in advisories_files { match result { Ok(x) => { @@ -289,9 +280,9 @@ where T: PluginStorer + Send + Sync + 'static, { let not_found = || { - WorkerError::Sync(GetVTsError::External(Box::new(std::io::Error::other( + WorkerError::Sync(Box::new(std::io::Error::other( "vt-metadata.json not found", - )))) + ))) }; // if a feedpath is provided we expect a vt-metadata.json // if it is not a dir we assume it is the json file and continue diff --git a/rust/src/openvasd/vts/orchestrator.rs b/rust/src/openvasd/vts/orchestrator.rs index 9598fe19dd..1f42cec799 100644 --- a/rust/src/openvasd/vts/orchestrator.rs +++ b/rust/src/openvasd/vts/orchestrator.rs @@ -3,7 +3,6 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::RwLock; -use crate::greenbone_scanner_framework::GetVTsError; use scannerlib::models::{FeedState, FeedType}; use scannerlib::{Promise, feed}; use tokio::sync::mpsc; @@ -134,7 +133,7 @@ pub enum WorkerError { #[error(transparent)] Calculation(#[from] feed::VerifyError), #[error(transparent)] - Sync(#[from] GetVTsError), + Sync(#[from] Box), #[error("Unable to serialize: {0}")] Serialization(#[from] serde_json::Error), #[error("Unable to send message. Receiver dropped.")] diff --git a/rust/src/openvasd/vts/redis.rs b/rust/src/openvasd/vts/redis.rs index 6cb8420f8c..2a389ef802 100644 --- a/rust/src/openvasd/vts/redis.rs +++ b/rust/src/openvasd/vts/redis.rs @@ -1,6 +1,6 @@ use std::{fs, path::PathBuf, task::Poll, time::UNIX_EPOCH}; -use crate::greenbone_scanner_framework::{GetVTsError, StreamResult}; +use crate::api::StreamResult; use futures::Stream; use scannerlib::{ models::{FeedType, VTData}, @@ -10,7 +10,7 @@ use scannerlib::{ use crate::{ config::Config, vts::{ - FeedHash, PluginFetcher, PluginStorer, error_vts_error, + FeedHash, PluginFetcher, PluginStorer, orchestrator::{self, WorkerError}, }, }; @@ -42,15 +42,13 @@ impl FeedSynchronizer { } } -type R = Result; - -fn init_redis_storage(redis_url: &str, ft: FeedType) -> R { +fn init_redis_storage(redis_url: &str, ft: FeedType) -> Result { use scannerlib::storage::redis::*; let selector = match ft { FeedType::Products | FeedType::Advisories => NOTUSUPDATE_SELECTOR, FeedType::NASL => FEEDUPDATE_SELECTOR, }; - RedisCtx::open(redis_url, selector).map_err(error_vts_error) + RedisCtx::open(redis_url, selector).map_err(|e| WorkerError::Sync(Box::new(e))) } #[derive(Debug, Clone)] @@ -66,7 +64,7 @@ impl RedisPluginHandler { } fn redis_error_to_worker_error(error: DbError) -> WorkerError { - WorkerError::Sync(GetVTsError::External(Box::new(error))) + WorkerError::Sync(Box::new(error)) } fn redis_with_hash( @@ -214,7 +212,7 @@ impl RedisVTDataStream { Ok(x) => x.map(Ok), Err(error) => Some(Err(redis_error_to_worker_error(error))), }, - Err(error) => Some(Err(error.into())), + Err(error) => Some(Err(error)), } } @@ -228,7 +226,7 @@ impl RedisVTDataStream { Ok(x) => x.map(Ok), Err(error) => Some(Err(redis_error_to_worker_error(error))), }, - Err(error) => Some(Err(error.into())), + Err(error) => Some(Err(error)), } } }