From 413699192129822da00fd3d2ff3e34674c7a61ee Mon Sep 17 00:00:00 2001 From: Kraemii Date: Mon, 9 Feb 2026 12:02:14 +0100 Subject: [PATCH 1/8] Add: Notus address configuration In preparation for the notus migration to skiron, this adds a config to use an external API to get notus results. --- rust/examples/openvasd/config.example.toml | 3 ++- rust/src/openvasd/config/mod.rs | 13 +++++++++++++ rust/src/openvasd/notus/mod.rs | 1 + rust/src/openvasd/scans/mod.rs | 1 + 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/rust/examples/openvasd/config.example.toml b/rust/examples/openvasd/config.example.toml index f6b0df8080..2d1721d8ec 100644 --- a/rust/examples/openvasd/config.example.toml +++ b/rust/examples/openvasd/config.example.toml @@ -18,6 +18,8 @@ check_interval = "3600s" products_path = "/var/lib/notus/products/" # path to the notus advisories feed. This is required for the /vts endpoint advisories_path = "/var/lib/notus/advisories/" +# Address to reach notus on. If not set, internal notus implementation is used. +# address = "127.0.0.1:3001" [endpoints] # Enables GET /scans endpoint @@ -102,4 +104,3 @@ max_scanning = 10 batch_size = 2 # How long openvasd should pause before retrying retry_timeout = "1s" - diff --git a/rust/src/openvasd/config/mod.rs b/rust/src/openvasd/config/mod.rs index f781b5a942..82349a4307 100644 --- a/rust/src/openvasd/config/mod.rs +++ b/rust/src/openvasd/config/mod.rs @@ -36,6 +36,7 @@ pub struct Feed { pub struct Notus { pub products_path: PathBuf, pub advisories_path: PathBuf, + pub address: Option, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -139,6 +140,7 @@ impl Default for Notus { Notus { products_path: PathBuf::from("/var/lib/notus/products"), advisories_path: PathBuf::from("/var/lib/notus/advisories"), + address: None, } } } @@ -437,6 +439,14 @@ impl Config { .value_parser(clap::builder::PathBufValueParser::new()) .action(ArgAction::Set) .help("Path containing the Notus products directory")) + .arg( + clap::Arg::new("notus-address") + .env("NOTUS_ADDRESS") + .long("notus-address") + .value_name("IP:PORT") + .value_parser(clap::value_parser!(SocketAddr)) + .action(ArgAction::Set) + .help("the address to reach notus on")) .arg( clap::Arg::new("redis-url") .long("redis-url") @@ -658,6 +668,9 @@ impl Config { if let Some(path) = cmds.get_one::("notus-advisories") { config.notus.advisories_path.clone_from(path); } + if let Some(address) = cmds.get_one::("notus-address") { + config.notus.address = Some(*address); + } if let Some(_path) = cmds.get_one::("redis-url") { // is actually ignored as on scanner openvas the redis-url of openvas is used } diff --git a/rust/src/openvasd/notus/mod.rs b/rust/src/openvasd/notus/mod.rs index 9faa30ebf7..d16ff066f6 100644 --- a/rust/src/openvasd/notus/mod.rs +++ b/rust/src/openvasd/notus/mod.rs @@ -158,6 +158,7 @@ mod tests { let notus = crate::config::Notus { advisories_path, products_path, + address: None, }; Config { diff --git a/rust/src/openvasd/scans/mod.rs b/rust/src/openvasd/scans/mod.rs index 6401c9d7a9..fdd48b0ea0 100644 --- a/rust/src/openvasd/scans/mod.rs +++ b/rust/src/openvasd/scans/mod.rs @@ -511,6 +511,7 @@ pub mod tests { let notus = crate::config::Notus { advisories_path, products_path, + address: None, }; let scanner = crate::config::Scanner { scanner_type: crate::config::ScannerType::Openvasd, From 5a9971eaa71717427c4886263c1602c488515e08 Mon Sep 17 00:00:00 2001 From: Kraemii Date: Mon, 9 Feb 2026 19:10:54 +0100 Subject: [PATCH 2/8] Add Notus Context to Scan Context In order for Notus to be available in NASL it must be available in the Scan Context. To be future prove the psosibility to either use the internal Notus implementation, as well as using an external API was already added. --- rust/benches/interpreter.rs | 1 + rust/src/feed/update/mod.rs | 2 ++ rust/src/nasl/test_utils.rs | 7 ++++++- rust/src/nasl/utils/scan_ctx.rs | 16 +++++++++++++++- rust/src/scanner/mod.rs | 8 ++++++-- rust/src/scanner/running_scan.rs | 6 +++++- rust/src/scanner/scan_runner.rs | 9 +++++++-- rust/src/scanner/tests.rs | 7 ++++--- rust/src/scanner/vt_runner.rs | 6 +++++- rust/src/scannerctl/execute/mod.rs | 21 ++++++++++++++++++--- rust/src/scannerctl/interpret/mod.rs | 17 ++++++++++++++++- rust/src/scannerctl/utils.rs | 20 +++++++++++++++++++- 12 files changed, 104 insertions(+), 16 deletions(-) diff --git a/rust/benches/interpreter.rs b/rust/benches/interpreter.rs index b5026e2f4a..fb6b2ab1f2 100644 --- a/rust/benches/interpreter.rs +++ b/rust/benches/interpreter.rs @@ -27,6 +27,7 @@ pub fn run_interpreter_in_description_mode(c: &mut Criterion) { loader: &Loader::test_empty(), scan_preferences: ScanPrefs::new(), alive_test_methods: Vec::new(), + notus: None, }; let context = cb.build(); let code = Code::from_string(code) diff --git a/rust/src/feed/update/mod.rs b/rust/src/feed/update/mod.rs index b830fb66bc..cbdae81f52 100644 --- a/rust/src/feed/update/mod.rs +++ b/rust/src/feed/update/mod.rs @@ -70,6 +70,7 @@ pub async fn feed_version( scan_id, scan_preferences: scan_params, alive_test_methods, + notus: None, }; let context = cb.build(); let mut interpreter = ForkingInterpreter::new( @@ -170,6 +171,7 @@ where executor: &self.executor, scan_preferences: scan_params, alive_test_methods, + notus: None, }; let context = context.build(); let file = code.source_file(); diff --git a/rust/src/nasl/test_utils.rs b/rust/src/nasl/test_utils.rs index 17721aa061..54b9e09c5a 100644 --- a/rust/src/nasl/test_utils.rs +++ b/rust/src/nasl/test_utils.rs @@ -8,10 +8,14 @@ use std::{ fmt::{self, Display, Formatter}, panic::Location, path::PathBuf, + sync::Mutex, }; -use crate::storage::{ScanID, inmemory::InMemoryStorage}; use crate::{nasl::prelude::*, scanner::preferences::preference::ScanPrefs}; +use crate::{ + notus::{HashsumProductLoader, Notus}, + storage::{ScanID, inmemory::InMemoryStorage}, +}; use futures::{Stream, StreamExt}; use super::{ @@ -355,6 +359,7 @@ where filename: self.filename.clone(), scan_preferences: ScanPrefs::new(), alive_test_methods: Vec::default(), + notus: None, } .build() } diff --git a/rust/src/nasl/utils/scan_ctx.rs b/rust/src/nasl/utils/scan_ctx.rs index e3a6c50dbf..f281828835 100644 --- a/rust/src/nasl/utils/scan_ctx.rs +++ b/rust/src/nasl/utils/scan_ctx.rs @@ -13,6 +13,7 @@ use tokio::sync::RwLock; use crate::nasl::builtin::{KBError, NaslSockets}; use crate::nasl::syntax::Loader; use crate::nasl::{FromNaslValue, WithErrorInfo}; +use crate::notus::{HashsumProductLoader, Notus}; use crate::scanner::preferences::preference::{ScanPrefs, pref_is_true}; use crate::storage::error::StorageError; use crate::storage::infisto::json::JsonStorage; @@ -37,7 +38,7 @@ use super::executor::Executor; use super::hosts::{LOCALHOST, resolve_hostname}; use super::{FnError, Register}; use std::io::Write; -use std::net::IpAddr; +use std::net::{IpAddr, SocketAddr}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; @@ -266,6 +267,13 @@ impl ContextStorage for RedisStorage where } impl ContextStorage for Arc where T: ContextStorage {} +#[derive(Clone)] +pub enum NotusCtx { + Direct(Arc>>), + Address(SocketAddr), + // We might want to add more contexts here in the future, e.g. for other plugins. +} + /// NASL execution context. pub struct ScanCtx<'a> { /// The key for this context. @@ -287,6 +295,8 @@ pub struct ScanCtx<'a> { pub scan_preferences: ScanPrefs, /// Alive test methods alive_test_methods: Vec, + /// Notus + pub notus: Option, } impl<'a> ScanCtx<'a> { @@ -300,6 +310,7 @@ impl<'a> ScanCtx<'a> { executor: &'a Executor, scan_preferences: ScanPrefs, alive_test_methods: Vec, + notus: Option, ) -> Self { let mut sockets = NaslSockets::default(); sockets.with_recv_timeout(scan_preferences.get_preference_int("checks_read_timeout")); @@ -315,6 +326,7 @@ impl<'a> ScanCtx<'a> { sockets: RwLock::new(sockets), scan_preferences, alive_test_methods, + notus, } } @@ -649,6 +661,7 @@ pub struct ScanCtxBuilder<'a, P: AsRef> { pub filename: P, pub scan_preferences: ScanPrefs, pub alive_test_methods: Vec, + pub notus: Option, } impl<'a, P: AsRef> ScanCtxBuilder<'a, P> { @@ -663,6 +676,7 @@ impl<'a, P: AsRef> ScanCtxBuilder<'a, P> { self.executor, self.scan_preferences, self.alive_test_methods, + self.notus, ) } } diff --git a/rust/src/scanner/mod.rs b/rust/src/scanner/mod.rs index 09a874719c..68e6f43278 100644 --- a/rust/src/scanner/mod.rs +++ b/rust/src/scanner/mod.rs @@ -40,6 +40,7 @@ use tokio::sync::RwLock; use crate::nasl::syntax::Loader; use crate::nasl::utils::Executor; use crate::nasl::utils::scan_ctx::ContextStorage; +use crate::nasl::utils::scan_ctx::NotusCtx; use crate::scheduling::SchedulerStorage; use crate::storage::Remover; use crate::storage::ScanID; @@ -52,6 +53,7 @@ pub struct OpenvasdScanner { storage: Arc, loader: Arc, function_executor: Arc, + notus: Option, } impl OpenvasdScanner @@ -61,12 +63,13 @@ where // TODO: Actually use this in normal execution, so we can remove // the cfg directive here. #[cfg(test)] - fn new(storage: S, loader: Loader, executor: Executor) -> Self { + fn new(storage: S, loader: Loader, executor: Executor, notus: Option) -> Self { Self { running: Arc::new(RwLock::new(HashMap::default())), storage: Arc::new(storage), loader: Arc::new(loader), function_executor: Arc::new(executor), + notus, } } @@ -75,7 +78,8 @@ where let loader = self.loader.clone(); let function_executor = self.function_executor.clone(); let id = scan.scan_id.clone(); - let handle = RunningScan::::start(scan, storage, loader, function_executor); + let handle = + RunningScan::::start(scan, storage, loader, function_executor, self.notus.clone()); self.running.write().await.insert(id, handle); Ok(()) } diff --git a/rust/src/scanner/running_scan.rs b/rust/src/scanner/running_scan.rs index 5217038867..e68dbfe3f4 100644 --- a/rust/src/scanner/running_scan.rs +++ b/rust/src/scanner/running_scan.rs @@ -10,7 +10,7 @@ use std::{ time::SystemTime, }; -use crate::nasl::utils::scan_ctx::ContextStorage; +use crate::nasl::utils::scan_ctx::{ContextStorage, NotusCtx}; use crate::nasl::{syntax::Loader, utils::Executor}; use crate::scanner::Error; use crate::{ @@ -34,6 +34,7 @@ pub struct RunningScan { function_executor: Arc, keep_running: Arc, status: Arc>, + notus: Option, } pub(super) fn current_time_in_seconds(name: &'static str) -> u64 { @@ -55,6 +56,7 @@ where storage: Arc, loader: Arc, function_executor: Arc, + notus: Option, ) -> RunningScanHandle { let keep_running: Arc = Arc::new(true.into()); let status = Arc::new(RwLock::new(Status { @@ -69,6 +71,7 @@ where function_executor, keep_running: keep_running.clone(), status: status.clone(), + notus, } // TODO run per target .run(), @@ -107,6 +110,7 @@ where &self.function_executor, schedule.into_iter().map(Ok), &self.scan, + &self.notus, ) .map_err(make_scheduling_error) } diff --git a/rust/src/scanner/scan_runner.rs b/rust/src/scanner/scan_runner.rs index 489a608988..1debed4e59 100644 --- a/rust/src/scanner/scan_runner.rs +++ b/rust/src/scanner/scan_runner.rs @@ -4,7 +4,7 @@ use crate::nasl::syntax::Loader; use crate::nasl::utils::Executor; -use crate::nasl::utils::scan_ctx::{ContextStorage, Target}; +use crate::nasl::utils::scan_ctx::{ContextStorage, NotusCtx, Target}; use futures::{Stream, stream}; use greenbone_scanner_framework::models::HostInfo; @@ -45,6 +45,7 @@ pub struct ScanRunner<'a, S> { loader: &'a Loader, executor: &'a Executor, concurrent_vts: Vec, + notus: &'a Option, } impl<'a, S> ScanRunner<'a, S> @@ -57,6 +58,7 @@ where executor: &'a Executor, schedule: Sched, scan: &'a Scan, + notus: &'a Option, ) -> Result where Sched: Iterator + 'a, @@ -68,6 +70,7 @@ where loader, executor, concurrent_vts, + notus, }) } @@ -95,6 +98,7 @@ where host.clone(), ports.clone(), self.scan.scan_id.clone(), + self.notus.clone(), ) }); // The usage of unfold here will prevent any real asynchronous running of VTs @@ -103,7 +107,7 @@ where // new implementation. stream::unfold(data, move |mut data| async move { match data.next() { - Some((stage, vt, param, host, ports, scan_id)) => { + Some((stage, vt, param, host, ports, scan_id, notus)) => { let result = VTRunner::::run( self.storage, self.loader, @@ -116,6 +120,7 @@ where scan_id, &self.scan.scan_preferences, &self.scan.alive_test_methods, + ¬us, ) .await; Some((result, data)) diff --git a/rust/src/scanner/tests.rs b/rust/src/scanner/tests.rs index ba52054afb..91c34373a3 100644 --- a/rust/src/scanner/tests.rs +++ b/rust/src/scanner/tests.rs @@ -66,12 +66,12 @@ fn setup(scripts: &[(String, VTData)]) -> (TestStack, Loader, Executor, Scan) { fn make_scanner_and_scan_success() -> (OpenvasdScanner, Scan) { let (storage, loader, executor, scan) = setup(&only_success()); - (OpenvasdScanner::new(storage, loader, executor), scan) + (OpenvasdScanner::new(storage, loader, executor, None), scan) } fn make_scanner_and_scan(scripts: &[(String, VTData)]) -> (OpenvasdScanner, Scan) { let (storage, loader, executor, scan) = setup(scripts); - (OpenvasdScanner::new(storage, loader, executor), scan) + (OpenvasdScanner::new(storage, loader, executor, None), scan) } fn loader() -> Loader { @@ -240,6 +240,7 @@ fn parse_meta_data(filename: &str, code: &str) -> Option { filename, scan_preferences, alive_test_methods, + notus: None, }; let context = cb.build(); let ast = Code::from_string(code) @@ -299,7 +300,7 @@ async fn run( let scheduler = Scheduler::new(&*storage); let schedule = scheduler.execution_plan(&scan.vts)?; let interpreter: ScanRunner> = - ScanRunner::new(&storage, &loader, &executor, schedule, &scan)?; + ScanRunner::new(&storage, &loader, &executor, schedule, &scan, &None)?; let results = interpreter.stream().collect::>().await; Ok(results) } diff --git a/rust/src/scanner/vt_runner.rs b/rust/src/scanner/vt_runner.rs index bddf16e01d..3645ecd7e5 100644 --- a/rust/src/scanner/vt_runner.rs +++ b/rust/src/scanner/vt_runner.rs @@ -7,7 +7,7 @@ use std::path::PathBuf; use crate::nasl::interpreter::{ForkingInterpreter, InterpreterError}; use crate::nasl::syntax::Loader; use crate::nasl::utils::lookup_keys::SCRIPT_PARAMS; -use crate::nasl::utils::scan_ctx::{ContextStorage, Ports, Target}; +use crate::nasl::utils::scan_ctx::{ContextStorage, NotusCtx, Ports, Target}; use crate::nasl::utils::{Executor, Register}; use crate::scheduling::Stage; use crate::storage::error::StorageError; @@ -37,6 +37,7 @@ pub struct VTRunner<'a, S> { scan_id: String, scan_preferences: &'a ScanPrefs, alive_test_methods: &'a Vec, + notus: &'a Option, } impl<'a, S> VTRunner<'a, S> @@ -56,6 +57,7 @@ where scan_id: String, scan_preferences: &'a ScanPrefs, alive_test_methods: &'a Vec, + notus: &'a Option, ) -> Result { let s = Self { storage, @@ -69,6 +71,7 @@ where scan_id, scan_preferences, alive_test_methods, + notus, }; s.execute().await } @@ -211,6 +214,7 @@ where executor: self.executor, scan_preferences: self.scan_preferences.clone(), alive_test_methods: self.alive_test_methods.to_vec(), + notus: self.notus.clone(), } .build(); context.set_nvt(self.vt.clone()); diff --git a/rust/src/scannerctl/execute/mod.rs b/rust/src/scannerctl/execute/mod.rs index c4ccae6390..16660c4cdf 100644 --- a/rust/src/scannerctl/execute/mod.rs +++ b/rust/src/scannerctl/execute/mod.rs @@ -5,7 +5,7 @@ use std::fs::File; use std::io::stdin; use std::path::PathBuf; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use clap::Subcommand; use futures::StreamExt; @@ -13,13 +13,15 @@ use scannerlib::feed::{HashSumNameLoader, Update}; use scannerlib::models; use scannerlib::nasl::nasl_std_functions; use scannerlib::nasl::syntax::Loader; +use scannerlib::nasl::utils::scan_ctx::NotusCtx; +use scannerlib::notus::{HashsumProductLoader, Notus}; use scannerlib::scanner::preferences::preference::ScanPrefs; use scannerlib::scanner::{Scan, ScanRunner}; use scannerlib::scheduling::Scheduler; use scannerlib::storage::inmemory::InMemoryStorage; use tracing::{info, warn, warn_span}; -use crate::utils::ArgOrStdin; +use crate::utils::{ArgOrStdin, NotusArgs}; use crate::{CliError, CliErrorKind, Db, interpret}; #[derive(clap::Parser)] @@ -56,6 +58,11 @@ struct ScriptArgs { timeout: Option, #[clap(long = "vendor")] vendor_version: Option, + /// Notus configuration. Use "" to connect to a running Notus + /// instance or "" to use the internal implementation. If not + /// given Notus will be disabled. + #[clap(short, long = "notus")] + notus: Option, } #[derive(clap::Parser)] @@ -117,7 +124,7 @@ async fn scan(args: ScanArgs) -> Result<(), CliError> { let executor = nasl_std_functions(); let scan = Scan::default_to_localhost(scan); let runner: ScanRunner> = - ScanRunner::new(&storage, &loader, &executor, schedule, &scan).unwrap(); + ScanRunner::new(&storage, &loader, &executor, schedule, &scan, &None).unwrap(); let mut results = Box::pin(runner.stream()); while let Some(x) = results.next().await { match x { @@ -142,6 +149,13 @@ async fn scan(args: ScanArgs) -> Result<(), CliError> { } async fn script(args: ScriptArgs) -> Result<(), CliError> { + let notus = args.notus.map(|x| match x { + NotusArgs::Address(addr) => NotusCtx::Address(addr), + NotusArgs::Internal(path) => NotusCtx::Direct(Arc::new(Mutex::new(Notus::new( + HashsumProductLoader::new(Loader::from_feed_path(path)), + false, + )))), + }); let scan_preferences = ScanPrefs::new() .set_default_recv_timeout(args.timeout) .set_vendor_version(args.vendor_version); @@ -154,6 +168,7 @@ async fn script(args: ScriptArgs) -> Result<(), CliError> { args.ports.clone(), args.udp_ports.clone(), scan_preferences, + notus, ) .await } diff --git a/rust/src/scannerctl/interpret/mod.rs b/rust/src/scannerctl/interpret/mod.rs index f95a808d2d..db00b778eb 100644 --- a/rust/src/scannerctl/interpret/mod.rs +++ b/rust/src/scannerctl/interpret/mod.rs @@ -13,6 +13,7 @@ use scannerlib::nasl::{ NaslValue, WithErrorInfo, interpreter::InterpreterErrorKind, syntax::{LoadError, Loader, read_non_utf8_path}, + utils::scan_ctx::NotusCtx, }; use scannerlib::{ feed, @@ -131,6 +132,7 @@ async fn run_on_storage( ports: Ports, script: &Path, scan_preferences: ScanPrefs, + notus: Option, ) -> Result<(), CliErrorKind> { let scan_id = ScanID(format!("scannerctl-{}", script.to_string_lossy())); let filename = script; @@ -160,6 +162,7 @@ async fn run_on_storage( filename, scan_preferences, alive_test_methods: Vec::new(), + notus, }; run_with_context(cb.build(), script).await } @@ -174,6 +177,7 @@ pub async fn run( tcp_ports: Vec, udp_ports: Vec, scan_preferences: ScanPrefs, + notus: Option, ) -> Result<(), CliError> { let target = target .map(|target| { @@ -196,6 +200,7 @@ pub async fn run( ports, script, scan_preferences, + notus, ) .await } @@ -208,7 +213,17 @@ pub async fn run( } else { load_feed_by_exec(&storage, &loader).await? } - run_on_storage(storage, loader, target, kb, ports, script, scan_preferences).await + run_on_storage( + storage, + loader, + target, + kb, + ports, + script, + scan_preferences, + notus, + ) + .await } }; diff --git a/rust/src/scannerctl/utils.rs b/rust/src/scannerctl/utils.rs index 0686b47ea2..2e5ed33a3e 100644 --- a/rust/src/scannerctl/utils.rs +++ b/rust/src/scannerctl/utils.rs @@ -1,4 +1,4 @@ -use std::str::FromStr; +use std::{net::SocketAddr, path::PathBuf, str::FromStr}; #[derive(Clone)] pub enum ArgOrStdin { @@ -21,3 +21,21 @@ where } } } + +#[derive(Clone)] +pub enum NotusArgs { + Address(SocketAddr), + Internal(PathBuf), +} + +impl FromStr for NotusArgs { + type Err = std::io::Error; + + fn from_str(s: &str) -> Result { + if let Ok(addr) = s.parse() { + Ok(Self::Address(addr)) + } else { + Ok(Self::Internal(PathBuf::from(s))) + } + } +} From 90b1fda1cfb437d5614a4bbe34432533804af431 Mon Sep 17 00:00:00 2001 From: Kraemii Date: Mon, 9 Feb 2026 19:13:02 +0100 Subject: [PATCH 3/8] Add: Notus related built-in functions This includes: - security_notus - notus_error - notus - notus_type --- .../update_table_driven_lsc_data.md | 10 +- rust/src/nasl/builtin/error.rs | 4 + rust/src/nasl/builtin/mod.rs | 7 +- rust/src/nasl/builtin/notus/mod.rs | 159 ++++++++++++++++++ rust/src/nasl/builtin/report_functions/mod.rs | 43 +++++ .../nasl/builtin/report_functions/tests.rs | 32 ++++ rust/src/nasl/test_utils.rs | 6 +- rust/src/storage/items/kb.rs | 22 +++ 8 files changed, 274 insertions(+), 9 deletions(-) create mode 100644 rust/src/nasl/builtin/notus/mod.rs diff --git a/doc/manual/nasl/built-in-functions/glue-functions/update_table_driven_lsc_data.md b/doc/manual/nasl/built-in-functions/glue-functions/update_table_driven_lsc_data.md index cf680c3620..7072199679 100644 --- a/doc/manual/nasl/built-in-functions/glue-functions/update_table_driven_lsc_data.md +++ b/doc/manual/nasl/built-in-functions/glue-functions/update_table_driven_lsc_data.md @@ -2,6 +2,8 @@ ## NAME +DEPRECATED + **update_table_driven_lsc_data** - Set information, so that openvas can start a table driven lsc ## SYNOPSIS @@ -19,10 +21,16 @@ os_release: identifier for the operating system of the target system After the KB items are set, these information is also transferred to the main process and a notus scan is triggered. The results of the notus scan are then directly published. +## DEPRECATED + +This function is deprecated and **[notus(3)](notus.md)** and **[security_notus(3)](security_notus.md)** should be used instead. + ## RETURN VALUE This function returns nothing. ## SEE ALSO -**[log_message(3)](log_message.md)** \ No newline at end of file +**[log_message(3)](log_message.md)**, +**[notus(3)](notus.md)**, +**[security_notus(3)](security_notus.md)** diff --git a/rust/src/nasl/builtin/error.rs b/rust/src/nasl/builtin/error.rs index 31ddcdcad4..ed77012628 100644 --- a/rust/src/nasl/builtin/error.rs +++ b/rust/src/nasl/builtin/error.rs @@ -6,6 +6,7 @@ use thiserror::Error; use crate::nasl::prelude::*; use crate::nasl::utils::error::FnErrorKind; +use crate::notus::NotusError; use super::KBError; use super::cert::CertError; @@ -27,6 +28,8 @@ pub enum BuiltinError { #[error("{0}")] Http(HttpError), #[error("{0}")] + Notus(NotusError), + #[error("{0}")] String(StringError), #[error("{0}")] Misc(MiscError), @@ -99,4 +102,5 @@ builtin_error_variant!(CertError, Cert); builtin_error_variant!(SysError, Sys); builtin_error_variant!(FindServiceError, FindService); builtin_error_variant!(SnmpError, Snmp); +builtin_error_variant!(NotusError, Notus); builtin_error_variant!(RawIpError, RawIp); diff --git a/rust/src/nasl/builtin/mod.rs b/rust/src/nasl/builtin/mod.rs index 702bea742b..b5c490f302 100644 --- a/rust/src/nasl/builtin/mod.rs +++ b/rust/src/nasl/builtin/mod.rs @@ -16,12 +16,12 @@ mod isotime; mod knowledge_base; pub mod misc; pub mod network; -mod snmp; - +mod notus; mod preferences; pub mod raw_ip; mod regex; mod report_functions; +mod snmp; mod ssh; mod string; mod sys; @@ -63,7 +63,8 @@ pub fn nasl_std_functions() -> Executor { .add_set(find_service::FindService) .add_set(wmi::Wmi) .add_set(snmp::Snmp) - .add_set(cert::NaslCerts::default()); + .add_set(cert::NaslCerts::default()) + .add_set(notus::NaslNotus::default()); executor.add_set(raw_ip::RawIp); executor.add_global_vars(raw_ip::RawIp); diff --git a/rust/src/nasl/builtin/notus/mod.rs b/rust/src/nasl/builtin/notus/mod.rs new file mode 100644 index 0000000000..62559934d7 --- /dev/null +++ b/rust/src/nasl/builtin/notus/mod.rs @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: 2026 Greenbone AG +// +// SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception + +use std::{ + collections::HashMap, + io::{Read, Write}, + net::{SocketAddr, TcpStream}, +}; + +use greenbone_scanner_framework::models::FixedVersion; +use nasl_function_proc_macro::nasl_function; +use serde::{Deserialize, Serialize}; +use serde_json; + +use crate::{ + function_set, + nasl::{ + ArgumentError, FnError, NaslValue, ScanCtx, builtin::http::HttpError, + utils::scan_ctx::NotusCtx, + }, + notus::{HashsumProductLoader, Notus}, +}; + +#[nasl_function] +fn notus_type() -> i64 { + 1 +} + +#[derive(Serialize, Deserialize, Debug)] +struct NotusResult { + oid: String, + message: String, +} + +impl NaslNotus { + fn notus_self( + &self, + notus: &mut Notus, + pkg_list: &[String], + product: &str, + ) -> Result { + let res = notus.scan(product, pkg_list)?; + + let mut ret = HashMap::new(); + for (oid, vuls) in res { + let message = vuls.into_iter().map(|vul| match vul.fixed_version { + FixedVersion::Single { version, specifier } => format!("Vulnerable package: {}\nInstalled version: {}\nFixed version: {:2}{}", vul.name, vul.installed_version, specifier.to_string(), version), + FixedVersion::Range { start, end } => format!("Vulnerable package: {}\nInstalled version: {}\nFixed version: < {}\nFixed version: >={}", vul.name, vul.installed_version, start, end), + }).collect::>().join("\n\n"); + ret.insert(oid, NaslValue::String(message)); + } + + Ok(NaslValue::Dict(ret)) + } + + fn notus_extern( + &self, + addr: &SocketAddr, + pkg_list: &[String], + product: &str, + ) -> Result { + let mut sock = TcpStream::connect(addr).map_err(|e| HttpError::IO(e.kind()))?; + let pkg_json = serde_json::to_string(pkg_list).unwrap(); + + let request = format!( + "POST /notus/{} HTTP/1.1\r\nContent-Length: {}\r\n\r\n{}", + product, + pkg_json.len(), + pkg_json + ); + sock.write_all(request.as_bytes()) + .map_err(|e| HttpError::IO(e.kind()))?; + let mut response = Vec::new(); + sock.read_to_end(&mut response) + .map_err(|e| HttpError::IO(e.kind()))?; + let response_str = String::from_utf8(response).unwrap(); + + // Split headers and body + let parts: Vec<&str> = response_str.split("\r\n\r\n").collect(); + let body = if parts.len() > 1 { + parts[1] + } else { + &response_str + }; + + // Parse JSON array of results + let results: Vec = serde_json::from_str(body).unwrap(); + + // Convert to NaslValue (Dict mapping oid -> message) + let mut ret = HashMap::new(); + for result in results { + ret.insert(result.oid, NaslValue::String(result.message)); + } + + Ok(NaslValue::Dict(ret)) + } + + #[nasl_function] + fn notus_error(&self) -> Option { + self.last_error.clone() + } + + #[nasl_function(named(pkg_list, product))] + fn notus( + &mut self, + context: &ScanCtx, + pkg_list: NaslValue, + product: &str, + ) -> Result { + let notus = if let Some(notus) = &context.notus { + notus + } else { + self.last_error = Some("Configuration Error: Notus context not found".to_string()); + return Ok(NaslValue::Null); + }; + let pkg_list: Vec = match pkg_list { + NaslValue::String(s) => s.split(',').map(|s| s.trim().to_string()).collect(), + NaslValue::Array(arr) => arr.iter().map(|v| v.to_string()).collect(), + x => { + return Err(ArgumentError::wrong_argument( + "pkg_list", + "String as Comma Separated List or Array of Strings", + &format!("{:?}", x), + ) + .into()); + } + }; + let ret = match notus { + NotusCtx::Direct(notus) => { + self.notus_self(&mut notus.lock().unwrap(), &pkg_list, product) + } + NotusCtx::Address(addr) => self.notus_extern(addr, &pkg_list, product), + }; + match ret { + Err(e) => { + self.last_error = Some(e.to_string()); + Ok(NaslValue::Null) + } + Ok(ret) => { + self.last_error = None; + Ok(ret) + } + } + } +} + +#[derive(Default)] +pub struct NaslNotus { + last_error: Option, +} + +function_set! { + NaslNotus, + ( + NaslNotus::notus_error, + NaslNotus::notus, + ) +} diff --git a/rust/src/nasl/builtin/report_functions/mod.rs b/rust/src/nasl/builtin/report_functions/mod.rs index 0c6c011c45..707ac5d57e 100644 --- a/rust/src/nasl/builtin/report_functions/mod.rs +++ b/rust/src/nasl/builtin/report_functions/mod.rs @@ -111,6 +111,48 @@ impl Reporting { fn error_message(&self, register: &Register, context: &ScanCtx) -> Result { self.store_result(ResultType::Error, register, context) } + + #[nasl_function(named(result))] + fn security_notus(&self, context: &ScanCtx, result: NaslValue) -> Result<(), FnError> { + match result { + NaslValue::Dict(dict) => { + if let (Some(NaslValue::String(oid)), Some(NaslValue::String(message))) = + (dict.get("oid"), dict.get("message")) + { + let result = models::Result { + id: self.id(), + r_type: ResultType::Alarm, + ip_address: Some(context.target().ip_addr().to_string()), + hostname: None, + oid: Some(oid.to_owned()), + port: None, + protocol: None, // TODO: This field is set to "package" in the c scanner result + message: Some(message.to_owned()), + detail: None, + }; + context + .storage() + .retry_dispatch(context.scan().clone(), result, 5)?; + } else { + return Err(ArgumentError::wrong_argument( + "result", + "Dict with 'oid' and 'message' as String values", + &format!("{:?}", dict), + ) + .into()); + } + Ok(()) + } + x => { + return Err(ArgumentError::wrong_argument( + "result", + "Dict with 'oid' and 'message' as String values", + &format!("{:?}", x), + ) + .into()); + } + } + } } function_set! { @@ -119,5 +161,6 @@ function_set! { (Reporting::log_message, "log_message"), (Reporting::security_message, "security_message"), (Reporting::error_message, "error_message"), + (Reporting::security_notus, "security_notus"), ) } diff --git a/rust/src/nasl/builtin/report_functions/tests.rs b/rust/src/nasl/builtin/report_functions/tests.rs index eb9807ceb1..9c14fd456b 100644 --- a/rust/src/nasl/builtin/report_functions/tests.rs +++ b/rust/src/nasl/builtin/report_functions/tests.rs @@ -71,3 +71,35 @@ fn security_message() { fn error_message() { verify("error_message", ResultType::Error) } + +#[test] +fn security_notus() { + let mut t = TestBuilder::default(); + t.run_all( + r###" + result["oid"] = "1.2.3.4.5"; + result["message"] = "test message"; + security_notus(result: result); + "###, + ); + t.check_no_errors(); + let (results, context) = t.results_and_context(); + assert_eq!(results.len(), 3); + let result = context + .storage() + .retrieve(&(context.scan().clone(), 0)) + .unwrap() + .unwrap(); + assert_eq!(result.id, 0); + assert_eq!(result.r_type, ResultType::Alarm); + assert_eq!( + result.ip_address, + Some(context.target().ip_addr().to_string()) + ); + assert_eq!(result.hostname, None); + assert_eq!(result.oid, Some("1.2.3.4.5".to_string())); + assert_eq!(result.port, None); + assert_eq!(result.protocol, None); + assert_eq!(result.message, Some("test message".into())); + assert_eq!(result.detail, None); +} diff --git a/rust/src/nasl/test_utils.rs b/rust/src/nasl/test_utils.rs index 54b9e09c5a..ca2ab96e0d 100644 --- a/rust/src/nasl/test_utils.rs +++ b/rust/src/nasl/test_utils.rs @@ -8,14 +8,10 @@ use std::{ fmt::{self, Display, Formatter}, panic::Location, path::PathBuf, - sync::Mutex, }; +use crate::storage::{ScanID, inmemory::InMemoryStorage}; use crate::{nasl::prelude::*, scanner::preferences::preference::ScanPrefs}; -use crate::{ - notus::{HashsumProductLoader, Notus}, - storage::{ScanID, inmemory::InMemoryStorage}, -}; use futures::{Stream, StreamExt}; use super::{ diff --git a/rust/src/storage/items/kb.rs b/rust/src/storage/items/kb.rs index 747a2f7ad3..bfca2e7943 100644 --- a/rust/src/storage/items/kb.rs +++ b/rust/src/storage/items/kb.rs @@ -17,6 +17,8 @@ pub enum KbKey { /// Contains SSL/TLS Kb keys Ssl(Ssl), + Ssh(Ssh), + /// Contains Port related Kb keys Port(Port), @@ -73,6 +75,19 @@ pub enum Ssl { Ca, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Ssh { + Login(Login), +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Login { + PackageListNotus, + ReleaseNotus, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "snake_case")] pub enum Port { @@ -175,6 +190,13 @@ impl Display for KbKey { KbKey::Ssl(Ssl::Password) => write!(f, "SSL/password"), KbKey::Ssl(Ssl::Ca) => write!(f, "SSL/ca"), + KbKey::Ssh(Ssh::Login(Login::PackageListNotus)) => { + write!(f, "ssh/login/package_list_notus") + } + KbKey::Ssh(Ssh::Login(Login::ReleaseNotus)) => { + write!(f, "ssh/login/release_notus") + } + KbKey::Port(Port::Tcp(port)) => write!(f, "Ports/tcp/{port}"), KbKey::Port(Port::Udp(port)) => write!(f, "Ports/udp/{port}"), From bdedf3449ae178dba38aa75f56a315c3e9ed591a Mon Sep 17 00:00:00 2001 From: Kraemii Date: Tue, 10 Feb 2026 13:02:00 +0100 Subject: [PATCH 4/8] Add test for Nasl notus functionality --- rust/data/notus/sha256sums | 1 + rust/src/nasl/builtin/notus/mod.rs | 30 +++++++---- rust/src/nasl/builtin/notus/tests.rs | 78 ++++++++++++++++++++++++++++ rust/src/nasl/test_utils.rs | 34 ++++++++++-- rust/src/scannerctl/interpret/mod.rs | 1 + 5 files changed, 128 insertions(+), 16 deletions(-) create mode 100644 rust/data/notus/sha256sums create mode 100644 rust/src/nasl/builtin/notus/tests.rs diff --git a/rust/data/notus/sha256sums b/rust/data/notus/sha256sums new file mode 100644 index 0000000000..54019c9e6e --- /dev/null +++ b/rust/data/notus/sha256sums @@ -0,0 +1 @@ +98b0943d0ed58ef00b7ae838bbcb22728475bc910527e1f7f0001d52d7651e96 debian_10.notus diff --git a/rust/src/nasl/builtin/notus/mod.rs b/rust/src/nasl/builtin/notus/mod.rs index 62559934d7..f32f220c73 100644 --- a/rust/src/nasl/builtin/notus/mod.rs +++ b/rust/src/nasl/builtin/notus/mod.rs @@ -2,6 +2,9 @@ // // SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception +#[cfg(test)] +mod tests; + use std::{ collections::HashMap, io::{Read, Write}, @@ -42,16 +45,18 @@ impl NaslNotus { ) -> Result { let res = notus.scan(product, pkg_list)?; - let mut ret = HashMap::new(); + let mut ret = vec![]; for (oid, vuls) in res { + let mut dict = HashMap::new(); let message = vuls.into_iter().map(|vul| match vul.fixed_version { - FixedVersion::Single { version, specifier } => format!("Vulnerable package: {}\nInstalled version: {}\nFixed version: {:2}{}", vul.name, vul.installed_version, specifier.to_string(), version), - FixedVersion::Range { start, end } => format!("Vulnerable package: {}\nInstalled version: {}\nFixed version: < {}\nFixed version: >={}", vul.name, vul.installed_version, start, end), + FixedVersion::Single { version, specifier } => format!("Vulnerable package: {}\nInstalled version: {}-{}\nFixed version: {:2}{}-{}", vul.name, vul.name, vul.installed_version, specifier.to_string(), vul.name, version), + FixedVersion::Range { start, end } => format!("Vulnerable package: {}\nInstalled version: {}-{}\nFixed version: < {}-{}\nFixed version: >={}-{}", vul.name, vul.name, vul.installed_version, vul.name, start, vul.name, end), }).collect::>().join("\n\n"); - ret.insert(oid, NaslValue::String(message)); + dict.insert("oid".to_string(), NaslValue::String(oid)); + dict.insert("message".to_string(), NaslValue::String(message)); + ret.push(NaslValue::Dict(dict)) } - - Ok(NaslValue::Dict(ret)) + Ok(NaslValue::Array(ret)) } fn notus_extern( @@ -88,12 +93,15 @@ impl NaslNotus { let results: Vec = serde_json::from_str(body).unwrap(); // Convert to NaslValue (Dict mapping oid -> message) - let mut ret = HashMap::new(); + let mut ret = vec![]; for result in results { - ret.insert(result.oid, NaslValue::String(result.message)); + let mut dict = HashMap::new(); + dict.insert("oid".to_string(), NaslValue::String(result.oid)); + dict.insert("message".to_string(), NaslValue::String(result.message)); + ret.push(NaslValue::Dict(dict)); } - Ok(NaslValue::Dict(ret)) + Ok(NaslValue::Array(ret)) } #[nasl_function] @@ -153,7 +161,7 @@ pub struct NaslNotus { function_set! { NaslNotus, ( - NaslNotus::notus_error, - NaslNotus::notus, + (NaslNotus::notus_error, "notus_error"), + (NaslNotus::notus, "notus"), ) } diff --git a/rust/src/nasl/builtin/notus/tests.rs b/rust/src/nasl/builtin/notus/tests.rs new file mode 100644 index 0000000000..e13e9659a1 --- /dev/null +++ b/rust/src/nasl/builtin/notus/tests.rs @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2026 Greenbone AG +// +// SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception + +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; + +use crate::{ + nasl::{Loader, test_utils::TestBuilder}, + notus::{HashsumProductLoader, Notus}, +}; + +fn make_test_path(sub_components: &[&str]) -> std::path::PathBuf { + let mut path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).to_owned(); + for component in sub_components { + path = path.join(component); + } + path.to_owned() +} + +pub fn setup_loader() -> HashsumProductLoader { + HashsumProductLoader::new(Loader::from_feed_path(make_test_path(&["data", "notus"]))) +} + +fn setup() -> Arc>> { + let loader = setup_loader(); + Arc::new(Mutex::new(Notus::new(loader, false))) +} + +#[test] +fn test_notus() { + let notus = setup(); + let mut t = TestBuilder::from_notus(notus); + + let mut expected = HashMap::new(); + expected.insert( + "1.3.6.1.4.1.25623.1.1.7.2.2023.10089729899100".to_string(), + "Vulnerable package: gitlab-ce\nInstalled version: gitlab-ce-16.0.1\nFixed version: < gitlab-ce-16.0.0\nFixed version: >=gitlab-ce-16.0.7" + .to_string(), + ); + expected.insert( + "1.3.6.1.4.1.25623.1.1.7.2.2023.0988598199100".to_string(), + "Vulnerable package: grafana\nInstalled version: grafana-8.5.23\nFixed version: >=grafana-8.5.24\n\nVulnerable package: grafana8\nInstalled version: grafana8-8.5.23\nFixed version: >=grafana8-8.5.24" + .to_string(), + ); + + t.run(r#"notus(product: "debian_10", pkg_list: "gitlab-ce-16.0.1, grafana-8.5.23, grafana8-8.5.23");"#); + + t.check_no_errors(); + let results = t.results(); + assert_eq!(results.len(), 1); + let result = results[0].as_ref().unwrap(); + match result { + crate::nasl::NaslValue::Array(items) => { + let mut actual = HashMap::new(); + for item in items { + match item { + crate::nasl::NaslValue::Dict(dict) => { + let oid = match dict.get("oid") { + Some(crate::nasl::NaslValue::String(value)) => value.clone(), + _ => panic!("Expected string oid in notus result"), + }; + let message = match dict.get("message") { + Some(crate::nasl::NaslValue::String(value)) => value.clone(), + _ => panic!("Expected string message in notus result"), + }; + actual.insert(oid, message); + } + _ => panic!("Expected dict items in notus result array"), + } + } + assert_eq!(actual, expected); + } + _ => panic!("Expected array result from notus"), + } +} diff --git a/rust/src/nasl/test_utils.rs b/rust/src/nasl/test_utils.rs index ca2ab96e0d..3cb46b5080 100644 --- a/rust/src/nasl/test_utils.rs +++ b/rust/src/nasl/test_utils.rs @@ -8,10 +8,15 @@ use std::{ fmt::{self, Display, Formatter}, panic::Location, path::PathBuf, + sync::{Arc, Mutex}, }; -use crate::storage::{ScanID, inmemory::InMemoryStorage}; -use crate::{nasl::prelude::*, scanner::preferences::preference::ScanPrefs}; +use crate::{nasl::prelude::*, notus::Notus, scanner::preferences::preference::ScanPrefs}; +use crate::{ + nasl::utils::scan_ctx::NotusCtx, + notus::HashsumProductLoader, + storage::{ScanID, inmemory::InMemoryStorage}, +}; use futures::{Stream, StreamExt}; use super::{ @@ -129,6 +134,7 @@ pub struct TestBuilder { storage: S, executor: Executor, version: NaslVersion, + notus: Option>>>, } pub type DefaultTestBuilder = TestBuilder; @@ -147,6 +153,7 @@ impl Default for TestBuilder { storage: InMemoryStorage::default(), executor: nasl_std_functions(), version: NaslVersion::default(), + notus: None, } } } @@ -172,6 +179,7 @@ where storage, executor: nasl_std_functions(), version: NaslVersion::default(), + notus: None, } } } @@ -194,11 +202,10 @@ impl TestBuilder { storage: InMemoryStorage::default(), executor: nasl_std_functions(), version: NaslVersion::default(), + notus: None, } } -} -impl TestBuilder { /// Construct a `TestBuilder`, immediately run the /// given code on it and return it. pub fn from_code(code: impl AsRef) -> Self { @@ -212,6 +219,23 @@ impl TestBuilder { t.run_all(code.as_ref()); t } + + pub fn from_notus(notus: Arc>>) -> Self { + Self { + lines: vec![], + results: vec![], + scan_id: Default::default(), + filename: Default::default(), + target: Default::default(), + variables: vec![], + should_verify: true, + loader: Loader::test_empty(), + storage: InMemoryStorage::default(), + executor: nasl_std_functions(), + version: NaslVersion::default(), + notus: Some(notus), + } + } } impl TestBuilder @@ -355,7 +379,7 @@ where filename: self.filename.clone(), scan_preferences: ScanPrefs::new(), alive_test_methods: Vec::default(), - notus: None, + notus: self.notus.as_ref().map(|x| NotusCtx::Direct(x.clone())), } .build() } diff --git a/rust/src/scannerctl/interpret/mod.rs b/rust/src/scannerctl/interpret/mod.rs index db00b778eb..c77f3687ff 100644 --- a/rust/src/scannerctl/interpret/mod.rs +++ b/rust/src/scannerctl/interpret/mod.rs @@ -124,6 +124,7 @@ fn load_feed_by_json(store: &InMemoryStorage, path: &PathBuf) -> Result<(), CliE Ok(()) } +#[allow(clippy::too_many_arguments)] async fn run_on_storage( storage: S, loader: Loader, From edac1e39dc9a69758f2200277944954b024eebca Mon Sep 17 00:00:00 2001 From: Kraemii Date: Tue, 10 Feb 2026 15:26:44 +0100 Subject: [PATCH 5/8] Remove SSH Kb Item tiem again As we do not need these items intenrally, they are removed again --- rust/src/storage/items/kb.rs | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/rust/src/storage/items/kb.rs b/rust/src/storage/items/kb.rs index bfca2e7943..747a2f7ad3 100644 --- a/rust/src/storage/items/kb.rs +++ b/rust/src/storage/items/kb.rs @@ -17,8 +17,6 @@ pub enum KbKey { /// Contains SSL/TLS Kb keys Ssl(Ssl), - Ssh(Ssh), - /// Contains Port related Kb keys Port(Port), @@ -75,19 +73,6 @@ pub enum Ssl { Ca, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum Ssh { - Login(Login), -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum Login { - PackageListNotus, - ReleaseNotus, -} - #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "snake_case")] pub enum Port { @@ -190,13 +175,6 @@ impl Display for KbKey { KbKey::Ssl(Ssl::Password) => write!(f, "SSL/password"), KbKey::Ssl(Ssl::Ca) => write!(f, "SSL/ca"), - KbKey::Ssh(Ssh::Login(Login::PackageListNotus)) => { - write!(f, "ssh/login/package_list_notus") - } - KbKey::Ssh(Ssh::Login(Login::ReleaseNotus)) => { - write!(f, "ssh/login/release_notus") - } - KbKey::Port(Port::Tcp(port)) => write!(f, "Ports/tcp/{port}"), KbKey::Port(Port::Udp(port)) => write!(f, "Ports/udp/{port}"), From 302174ff807446e23dc8a62817fe1276b3497497 Mon Sep 17 00:00:00 2001 From: Kraemii Date: Wed, 11 Feb 2026 12:30:19 +0100 Subject: [PATCH 6/8] Add notus config to scanerctl execute scan --- rust/Cargo.lock | 62 +++++++++++++++++++- rust/Cargo.toml | 1 + rust/src/nasl/builtin/notus/mod.rs | 87 +++++++++++++++++----------- rust/src/nasl/utils/scan_ctx.rs | 1 - rust/src/scannerctl/execute/mod.rs | 18 +++++- rust/src/scannerctl/interpret/mod.rs | 1 + 6 files changed, 131 insertions(+), 39 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index d1c52182fa..d8187b843c 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -814,6 +814,16 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -1337,6 +1347,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "enum_dispatch" version = "0.3.13" @@ -1997,9 +2016,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -3701,8 +3722,11 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", + "futures-channel", "futures-core", "futures-util", + "h2", "http", "http-body", "http-body-util", @@ -3712,6 +3736,7 @@ dependencies = [ "hyper-util", "js-sys", "log", + "mime", "native-tls", "percent-encoding", "pin-project-lite", @@ -4051,7 +4076,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "jni", "log", @@ -4182,6 +4207,7 @@ dependencies = [ "rc4", "redis", "regex", + "reqwest 0.12.28", "ripemd", "rpmdb", "rsa", @@ -4271,7 +4297,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -4929,6 +4955,27 @@ dependencies = [ "windows 0.61.3", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tar" version = "0.4.44" @@ -5873,6 +5920,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.3.4" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index cfa4e36c7e..e5d5a6cf63 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -84,6 +84,7 @@ rand_core = "0.6.4" rc4 = "0.1.0" redis = "0.32.5" regex = "1.10.6" +reqwest = { version = "0.12", features = ["json", "blocking"] } ripemd = "0.1.3" rsa = { version = "0.9.8", features = ["hazmat"] } russh = "0.54.2" diff --git a/rust/src/nasl/builtin/notus/mod.rs b/rust/src/nasl/builtin/notus/mod.rs index f32f220c73..b47c0fc3d0 100644 --- a/rust/src/nasl/builtin/notus/mod.rs +++ b/rust/src/nasl/builtin/notus/mod.rs @@ -5,11 +5,7 @@ #[cfg(test)] mod tests; -use std::{ - collections::HashMap, - io::{Read, Write}, - net::{SocketAddr, TcpStream}, -}; +use std::{collections::HashMap, net::SocketAddr}; use greenbone_scanner_framework::models::FixedVersion; use nasl_function_proc_macro::nasl_function; @@ -59,38 +55,32 @@ impl NaslNotus { Ok(NaslValue::Array(ret)) } - fn notus_extern( + async fn notus_extern( &self, addr: &SocketAddr, pkg_list: &[String], product: &str, ) -> Result { - let mut sock = TcpStream::connect(addr).map_err(|e| HttpError::IO(e.kind()))?; - let pkg_json = serde_json::to_string(pkg_list).unwrap(); - - let request = format!( - "POST /notus/{} HTTP/1.1\r\nContent-Length: {}\r\n\r\n{}", - product, - pkg_json.len(), - pkg_json - ); - sock.write_all(request.as_bytes()) - .map_err(|e| HttpError::IO(e.kind()))?; - let mut response = Vec::new(); - sock.read_to_end(&mut response) - .map_err(|e| HttpError::IO(e.kind()))?; - let response_str = String::from_utf8(response).unwrap(); - - // Split headers and body - let parts: Vec<&str> = response_str.split("\r\n\r\n").collect(); - let body = if parts.len() > 1 { - parts[1] - } else { - &response_str - }; + let pkg_json = serde_json::to_string(pkg_list) + .map_err(|e| FnError::wrong_unnamed_argument("pkg_list", &e.to_string()))?; + + // TODO: Currently we only support http + let url = format!("http://{}/notus/{}", addr, product); + + let client = reqwest::Client::new(); + let response = client + .post(&url) + .header("Content-Type", "application/json") + .body(pkg_json) + .send() + .await + .map_err(|e| HttpError::Custom(e.to_string()))?; // Parse JSON array of results - let results: Vec = serde_json::from_str(body).unwrap(); + let results: Vec = response + .json() + .await + .map_err(|e| HttpError::Custom(e.to_string()))?; // Convert to NaslValue (Dict mapping oid -> message) let mut ret = vec![]; @@ -104,15 +94,45 @@ impl NaslNotus { Ok(NaslValue::Array(ret)) } + /// Returns the last error message from the Notus function. #[nasl_function] fn notus_error(&self) -> Option { self.last_error.clone() } + /// This function takes the given information and starts a notus scan. Its arguments are: + /// pkg_list: comma separated list or array of installed packages of the target system + /// product: identifier for the notus scanner to get list of vulnerable packages + /// + /// This function returns a json like structure, + /// so information can be adjusted and must be published using + /// security_notus. The json like format depends + /// one the scanner that is used. + /// The format of the result has the following structure: + /// ```json + /// [ + /// { + /// "oid": "[oid1]", + /// "message": "[message1]" + /// }, + /// { + /// "oid": "[oid2]", + /// "message": "[message2]" + /// } + /// ] + /// ``` + /// It is a list of dictionaries. Each dictionary has the key `oid` and `message`. + /// + /// In case of an Error a NULL value is returned and an Error is set. The error can be gathered using the + /// notus_error function, which yields the last occurred error. + /// + /// Internally this functions supports two modes, which is selected by the configuration of the notus context. + /// First is the direct mode, which uses the internal notus implementation directly, the second is the external + /// mode, which sends a request to an external notus service. #[nasl_function(named(pkg_list, product))] - fn notus( + async fn notus( &mut self, - context: &ScanCtx, + context: &ScanCtx<'_>, pkg_list: NaslValue, product: &str, ) -> Result { @@ -138,7 +158,7 @@ impl NaslNotus { NotusCtx::Direct(notus) => { self.notus_self(&mut notus.lock().unwrap(), &pkg_list, product) } - NotusCtx::Address(addr) => self.notus_extern(addr, &pkg_list, product), + NotusCtx::Address(addr) => self.notus_extern(addr, &pkg_list, product).await, }; match ret { Err(e) => { @@ -163,5 +183,6 @@ function_set! { ( (NaslNotus::notus_error, "notus_error"), (NaslNotus::notus, "notus"), + notus_type ) } diff --git a/rust/src/nasl/utils/scan_ctx.rs b/rust/src/nasl/utils/scan_ctx.rs index f281828835..7d3a479690 100644 --- a/rust/src/nasl/utils/scan_ctx.rs +++ b/rust/src/nasl/utils/scan_ctx.rs @@ -271,7 +271,6 @@ impl ContextStorage for Arc where T: ContextStorage {} pub enum NotusCtx { Direct(Arc>>), Address(SocketAddr), - // We might want to add more contexts here in the future, e.g. for other plugins. } /// NASL execution context. diff --git a/rust/src/scannerctl/execute/mod.rs b/rust/src/scannerctl/execute/mod.rs index 16660c4cdf..bb616766c4 100644 --- a/rust/src/scannerctl/execute/mod.rs +++ b/rust/src/scannerctl/execute/mod.rs @@ -59,8 +59,8 @@ struct ScriptArgs { #[clap(long = "vendor")] vendor_version: Option, /// Notus configuration. Use "" to connect to a running Notus - /// instance or "" to use the internal implementation. If not - /// given Notus will be disabled. + /// instance or "" to product files to use the internal + /// implementation. If not given Notus will be disabled. #[clap(short, long = "notus")] notus: Option, } @@ -77,6 +77,11 @@ struct ScanArgs { /// Target to scan. #[clap(short, long)] target: Option, + /// Notus configuration. Use "" to connect to a running Notus + /// instance or "" to product files to use the internal + /// implementation. If not given Notus will be disabled. + #[clap(short, long = "notus")] + notus: Option, } pub async fn run(args: ExecuteArgs) -> Result<(), CliError> { @@ -123,8 +128,15 @@ async fn scan(args: ScanArgs) -> Result<(), CliError> { } else { let executor = nasl_std_functions(); let scan = Scan::default_to_localhost(scan); + let notus = args.notus.map(|x| match x { + NotusArgs::Address(addr) => NotusCtx::Address(addr), + NotusArgs::Internal(path) => NotusCtx::Direct(Arc::new(Mutex::new(Notus::new( + HashsumProductLoader::new(Loader::from_feed_path(path)), + false, + )))), + }); let runner: ScanRunner> = - ScanRunner::new(&storage, &loader, &executor, schedule, &scan, &None).unwrap(); + ScanRunner::new(&storage, &loader, &executor, schedule, &scan, ¬us).unwrap(); let mut results = Box::pin(runner.stream()); while let Some(x) = results.next().await { match x { diff --git a/rust/src/scannerctl/interpret/mod.rs b/rust/src/scannerctl/interpret/mod.rs index c77f3687ff..fff769b233 100644 --- a/rust/src/scannerctl/interpret/mod.rs +++ b/rust/src/scannerctl/interpret/mod.rs @@ -124,6 +124,7 @@ fn load_feed_by_json(store: &InMemoryStorage, path: &PathBuf) -> Result<(), CliE Ok(()) } +// TODO: Redesign #[allow(clippy::too_many_arguments)] async fn run_on_storage( storage: S, From 4346a4358d9970eb738685e2054fe5a170802469 Mon Sep 17 00:00:00 2001 From: Kraemii Date: Mon, 23 Feb 2026 11:34:28 +0100 Subject: [PATCH 7/8] Adjust Notus documentation --- .../glue-functions/notus.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/doc/manual/nasl/built-in-functions/glue-functions/notus.md b/doc/manual/nasl/built-in-functions/glue-functions/notus.md index 614cea7338..7b4b533030 100644 --- a/doc/manual/nasl/built-in-functions/glue-functions/notus.md +++ b/doc/manual/nasl/built-in-functions/glue-functions/notus.md @@ -19,7 +19,7 @@ product: identifier for the notus scanner to get list of vulnerable packages In contrast to **[update_table_driven_lsc_data(3)](update_table_driven_lsc_data.md)** this function does not publish results by itself, but returns a json like structure, so information can be adjusted and must be published using -**[security_lsc(3)](../report-functions/security_lsc.md)**. The json like format depends +**[security_notus(3)](../report-functions/security_notus.md)**. The json like format depends one the scanner that is used. There are currently 2 scanner types available: Notus and Skiron. Their response have different formats and also will be parsed differently. The format for Notus has the following structure: @@ -54,11 +54,18 @@ The elements can be accessed by using the normal NASL array handling. For more i The format for Skiron has the following structure: ```json -{ - "[oid1]": "some message", - "[oid2]": "some message" -} -It is just a dictionary with the OID of the result as key and the result message as value. +[ + { + "oid": "[oid1]", + "message": "[message1]" + }, + { + "oid": "[oid2]", + "message": "[message2]" + } +] +``` +It is a list of dictionaries. Each dictionary has the key `oid` and `message`. To determine which format is used, the builtin function **[notus_type(3)](notus_type.md)** can be used. From 3a72753271fbfe0f021e4393e72560605e4848a4 Mon Sep 17 00:00:00 2001 From: Kraemii Date: Wed, 4 Mar 2026 11:47:32 +0100 Subject: [PATCH 8/8] Check Status Code from external Notus call --- rust/src/nasl/builtin/notus/mod.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rust/src/nasl/builtin/notus/mod.rs b/rust/src/nasl/builtin/notus/mod.rs index b47c0fc3d0..f2d503e812 100644 --- a/rust/src/nasl/builtin/notus/mod.rs +++ b/rust/src/nasl/builtin/notus/mod.rs @@ -8,6 +8,7 @@ mod tests; use std::{collections::HashMap, net::SocketAddr}; use greenbone_scanner_framework::models::FixedVersion; +use http::StatusCode; use nasl_function_proc_macro::nasl_function; use serde::{Deserialize, Serialize}; use serde_json; @@ -76,6 +77,14 @@ impl NaslNotus { .await .map_err(|e| HttpError::Custom(e.to_string()))?; + if response.status() != StatusCode::OK { + return Err(HttpError::Custom(format!( + "Notus service returned status code {}", + response.status() + )) + .into()); + } + // Parse JSON array of results let results: Vec = response .json()