From 7b78eeb0cb2c874b23dd8876308b0a54d20ea0b1 Mon Sep 17 00:00:00 2001 From: Philipp Eder Date: Mon, 15 Jul 2024 09:18:28 +0200 Subject: [PATCH] Add: log_message, error_message, security_message Adds the possibility to store results within a Storage and adds the builtin functions: - log_message, - error_message, - security_message --- rust/Cargo.lock | 1 + rust/json-storage/src/lib.rs | 6 +- rust/models/src/result.rs | 1 + rust/nasl-builtin-knowledge-base/src/lib.rs | 2 +- rust/nasl-builtin-std/Cargo.toml | 1 + rust/nasl-builtin-std/src/lib.rs | 3 + rust/nasl-builtin-std/src/report_functions.rs | 130 ++++++++++++++++++ rust/nasl-builtin-std/tests/log.rs | 98 +++++++++++++ rust/redis-storage/src/connector.rs | 6 +- rust/storage/src/item.rs | 3 +- rust/storage/src/lib.rs | 55 +++++++- rust/storage/src/retrieve.rs | 38 +++++ 12 files changed, 335 insertions(+), 9 deletions(-) create mode 100644 rust/nasl-builtin-std/src/report_functions.rs create mode 100644 rust/nasl-builtin-std/tests/log.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index ac9dc89f8..141f7e130 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1894,6 +1894,7 @@ dependencies = [ name = "nasl-builtin-std" version = "0.1.0" dependencies = [ + "models", "nasl-builtin-cryptographic", "nasl-builtin-description", "nasl-builtin-host", diff --git a/rust/json-storage/src/lib.rs b/rust/json-storage/src/lib.rs index 5c70aaa16..cd0d2927d 100644 --- a/rust/json-storage/src/lib.rs +++ b/rust/json-storage/src/lib.rs @@ -138,9 +138,9 @@ where ) -> Result>, StorageError> { Ok(match scope { // currently not supported - storage::Retrieve::NVT(_) | storage::Retrieve::NotusAdvisory(_) => { - Box::new([].into_iter()) - } + storage::Retrieve::NVT(_) + | storage::Retrieve::NotusAdvisory(_) + | storage::Retrieve::Result(_) => Box::new([].into_iter()), storage::Retrieve::KB(s) => Box::new({ let kbs = self.kbs.lock().map_err(StorageError::from)?; let kbs = kbs.clone(); diff --git a/rust/models/src/result.rs b/rust/models/src/result.rs index fe093c486..f548810d2 100644 --- a/rust/models/src/result.rs +++ b/rust/models/src/result.rs @@ -61,6 +61,7 @@ pub struct Result { feature = "serde_support", serde(skip_serializing_if = "Option::is_none", default) )] + /// Details are only set on status and can be ignored pub detail: Option, } diff --git a/rust/nasl-builtin-knowledge-base/src/lib.rs b/rust/nasl-builtin-knowledge-base/src/lib.rs index ad2fbc673..d8bea2f7c 100644 --- a/rust/nasl-builtin-knowledge-base/src/lib.rs +++ b/rust/nasl-builtin-knowledge-base/src/lib.rs @@ -54,7 +54,7 @@ fn get_kb_item(register: &Register, c: &Context) -> Result None, + Field::NVT(_) | Field::NotusAdvisory(_) | Field::Result(_) => None, Field::KB(kb) => Some(kb.value.into()), }) .collect::>() diff --git a/rust/nasl-builtin-std/Cargo.toml b/rust/nasl-builtin-std/Cargo.toml index 9cbb6492b..e13b21c83 100644 --- a/rust/nasl-builtin-std/Cargo.toml +++ b/rust/nasl-builtin-std/Cargo.toml @@ -15,6 +15,7 @@ nasl-builtin-http = { version = "0.1.0", path = "../nasl-builtin-http" } nasl-builtin-description = {path = "../nasl-builtin-description"} nasl-builtin-misc = {path = "../nasl-builtin-misc"} storage = {path = "../storage"} +models = { path = "../models" } nasl-syntax = {path = "../nasl-syntax"} # depend on c libraries and are considered unstable for now diff --git a/rust/nasl-builtin-std/src/lib.rs b/rust/nasl-builtin-std/src/lib.rs index 34c739df4..00323291f 100644 --- a/rust/nasl-builtin-std/src/lib.rs +++ b/rust/nasl-builtin-std/src/lib.rs @@ -8,6 +8,7 @@ use nasl_builtin_utils::{Context, NaslFunctionRegister, NaslVarRegister, Register}; use storage::{ContextKey, DefaultDispatcher}; mod array; +mod report_functions; /// The description builtin function /// @@ -70,6 +71,7 @@ impl nasl_builtin_utils::NaslFunctionExecuter for Std { pub fn nasl_std_functions() -> nasl_builtin_utils::NaslFunctionRegister { let mut builder = nasl_builtin_utils::NaslfunctionRegisterBuilder::new() .push_register(Std) + .push_register(report_functions::Reporting::default()) .push_register(nasl_builtin_knowledge_base::KnowledgeBase) .push_register(nasl_builtin_misc::Misc) .push_register(nasl_builtin_string::NaslString) @@ -77,6 +79,7 @@ pub fn nasl_std_functions() -> nasl_builtin_utils::NaslFunctionRegister { .push_register(nasl_builtin_http::NaslHttp::default()) .push_register(nasl_builtin_cryptographic::Cryptographic) .push_register(nasl_builtin_description::Description); + builder = add_ssh(builder); builder = add_raw_ip(builder); builder.build() diff --git a/rust/nasl-builtin-std/src/report_functions.rs b/rust/nasl-builtin-std/src/report_functions.rs new file mode 100644 index 000000000..cce42de66 --- /dev/null +++ b/rust/nasl-builtin-std/src/report_functions.rs @@ -0,0 +1,130 @@ +use std::sync::{Arc, RwLock}; + +use models::{Protocol, ResultType}; +use nasl_builtin_utils::{Context, ContextType, FunctionErrorKind, Register}; +use nasl_syntax::NaslValue; + +#[derive(Debug, Clone, Default)] +/// The description builtin function +pub struct Reporting { + id: Arc>, +} + +impl Reporting { + fn id(&self) -> usize { + let mut id = self.id.as_ref().write().expect("expected write lock"); + let result = *id; + *id += 1; + result + } + + fn store_result( + &self, + typus: ResultType, + register: &Register, + context: &Context, + ) -> Result { + let data = register.named("data").map(|x| x.to_string()); + let port = register + .named("port") + .and_then(|x| match x { + ContextType::Value(x) => Some(x.into()), + _ => None, + }) + .map(|x: i64| x as i16); + + let protocol = match register + .named("proto") + .map(|x| x.to_string()) + .as_ref() + .map(|x| x as &str) + { + Some("udp") => Protocol::UDP, + _ => Protocol::TCP, + }; + // TOO: enhance id + let result = models::Result { + id: self.id(), + r_type: typus, + ip_address: Some(context.target().to_string()), + // TODO: + hostname: None, + oid: Some(context.key().value()), + port, + protocol: Some(protocol), + message: data, + detail: None, + }; + context.dispatcher().retry_dispatch( + 5, + context.key(), + storage::Field::Result(result.into()), + )?; + Ok(NaslValue::Null) + } + + /// *void* **log_message**(data: *string*, port:*int* , proto: *string*, uri: *string*); + /// + /// Creates a log result based on the given arguments + /// - data, is the text report + /// - port, optional TCP or UDP port number of the service + /// - proto is the protocol ("tcp" by default; "udp" is the other value). + /// - uri specifies the location of a found product + fn log_message( + &self, + register: &Register, + context: &Context, + ) -> Result { + self.store_result(ResultType::Log, register, context) + } + + /// *void* **security_message**(data: *string*, port:*int* , proto: *string*, uri: *string*); + /// + /// Creates a alarm result based on the given arguments + /// - data, is the text report + /// - port, optional TCP or UDP port number of the service + /// - proto is the protocol ("tcp" by default; "udp" is the other value). + /// - uri specifies the location of a found product + fn security_message( + &self, + register: &Register, + context: &Context, + ) -> Result { + self.store_result(ResultType::Alarm, register, context) + } + + /// *void* **error_message**(data: *string*, port:*int* , proto: *string*, uri: *string*); + /// + /// Creates a error result based on the given arguments + /// - data, is the text report + /// - port, optional TCP or UDP port number of the service + /// - proto is the protocol ("tcp" by default; "udp" is the other value). + /// - uri specifies the location of a found product + fn error_message( + &self, + register: &Register, + context: &Context, + ) -> Result { + self.store_result(ResultType::Error, register, context) + } +} + +impl nasl_builtin_utils::NaslFunctionExecuter for Reporting { + fn nasl_fn_execute( + &self, + name: &str, + register: &Register, + context: &Context, + ) -> Option { + match name { + "log_message" => Some(self.log_message(register, context)), + "security_message" => Some(self.security_message(register, context)), + "error_message" => Some(self.error_message(register, context)), + _ => None, + } + } + + fn nasl_fn_defined(&self, name: &str) -> bool { + matches!(name, "log_message" | "security_message" | "error_message") + } +} diff --git a/rust/nasl-builtin-std/tests/log.rs b/rust/nasl-builtin-std/tests/log.rs new file mode 100644 index 000000000..188d00312 --- /dev/null +++ b/rust/nasl-builtin-std/tests/log.rs @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2023 Greenbone AG +// +// SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception + +#[cfg(test)] +mod tests { + + use nasl_builtin_std::ContextFactory; + use nasl_builtin_utils::Register; + use nasl_interpreter::CodeInterpreter; + + fn verify(function: &str, result_type: models::ResultType) { + let code = format!( + r###" + {function}(data: "test0", port: 12, proto: "udp", uri: "moep"); + {function}(data: "test1", port: 12, proto: "tcp", uri: "moep"); + {function}(data: "test2", port: 12, proto: "nonsense", uri: "moep"); + {function}(data: "test3"); + "### + ); + let register = Register::default(); + let binding = ContextFactory::default(); + let context = binding.build(Default::default(), Default::default()); + + let mut parser = CodeInterpreter::new(&code, register, &context); + let no_error = parser.find_map(|x| x.err()); + assert_eq!( + no_error, None, + "there should be no error when creating log_messages" + ); + let results = context + .retriever() + .results(context.key()) + .unwrap() + .collect::>(); + assert_eq!( + results.len(), + 4, + "expected the same results as log_message calls" + ); + + let create_expected = |id, port, protocol| models::Result { + id, + r_type: result_type.clone(), + ip_address: Some(context.target().to_string()), + hostname: None, + oid: Some(context.key().value()), + port, + protocol: Some(protocol), + message: Some(format!("test{id}")), + detail: None, + }; + let udp = context + .retriever() + .result(context.key(), 0) + .expect("expected udp result of first call") + .unwrap(); + let expected = create_expected(0, Some(12), models::Protocol::UDP); + assert_eq!(udp, expected); + + let tcp = context + .retriever() + .result(context.key(), 1) + .expect("expected udp result of first call") + .unwrap(); + let expected = create_expected(1, Some(12), models::Protocol::TCP); + assert_eq!(tcp, expected); + let defaults_to_tcp = context + .retriever() + .result(context.key(), 2) + .expect("expected udp result of first call") + .unwrap(); + let expected = create_expected(2, Some(12), models::Protocol::TCP); + assert_eq!(defaults_to_tcp, expected); + let default = context + .retriever() + .result(context.key(), 3) + .expect("expected udp result of first call") + .unwrap(); + let expected = create_expected(3, None, models::Protocol::TCP); + assert_eq!(default, expected); + } + + #[test] + fn log_message() { + verify("log_message", models::ResultType::Log) + } + + #[test] + fn security_message() { + verify("security_message", models::ResultType::Alarm) + } + + #[test] + fn error_message() { + verify("error_message", models::ResultType::Error) + } +} diff --git a/rust/redis-storage/src/connector.rs b/rust/redis-storage/src/connector.rs index 8772572ec..7c9b5e95c 100644 --- a/rust/redis-storage/src/connector.rs +++ b/rust/redis-storage/src/connector.rs @@ -734,9 +734,9 @@ where scope: storage::Retrieve, ) -> Result>, StorageError> { Ok(match scope { - storage::Retrieve::NotusAdvisory(_) | storage::Retrieve::NVT(_) => { - Box::new(Vec::new().into_iter()) - } + storage::Retrieve::NotusAdvisory(_) + | storage::Retrieve::NVT(_) + | storage::Retrieve::Result(_) => Box::new(Vec::new().into_iter()), storage::Retrieve::KB(s) => Box::new({ let kbs = self.kbs.lock().map_err(StorageError::from)?; let kbs = kbs.clone(); diff --git a/rust/storage/src/item.rs b/rust/storage/src/item.rs index d6d59a16b..797701a54 100644 --- a/rust/storage/src/item.rs +++ b/rust/storage/src/item.rs @@ -554,7 +554,7 @@ impl Nvt { NVTField::Nvt(x) => self == x, NVTField::NoOp | NVTField::Version(_) => false, }, - Field::KB(_) | Field::NotusAdvisory(_) => false, + Field::KB(_) | Field::NotusAdvisory(_) | Field::Result(_) => false, } } /// Verifies if a nvt is matching a field @@ -918,6 +918,7 @@ where Field::NVT(nvt) => self.store_nvt_field(nvt), Field::KB(kb) => self.dispatcher.dispatch_kb(key, kb), Field::NotusAdvisory(adv) => self.dispatcher.dispatch_advisory(key.as_ref(), *adv), + Field::Result(result) => self.dispatch(key, Field::Result(result)), } } diff --git a/rust/storage/src/lib.rs b/rust/storage/src/lib.rs index 02b936aae..0e92085de 100644 --- a/rust/storage/src/lib.rs +++ b/rust/storage/src/lib.rs @@ -117,6 +117,8 @@ pub enum Field { NVT(NVTField), /// Knowledge Base item KB(Kb), + /// Result send by log_message, security_message, error_message + Result(Box), /// Notus advisories, when None then the impl can assume finish // moving notusadvisory into the heap to reduce the size of the other enum members NotusAdvisory(Box>), @@ -298,6 +300,9 @@ type Kbs = HashMap>>; /// lookups relative simple. type Vts = HashMap; +/// The results generated by log_, security_, error_message. +type Results = HashMap>; + /// Is a in-memory dispatcher that behaves like a Storage. #[derive(Default)] pub struct DefaultDispatcher { @@ -307,6 +312,7 @@ pub struct DefaultDispatcher { feed_version: Arc>, advisories: Arc>>, kbs: Arc>, + results: Arc>, } impl DefaultDispatcher { @@ -368,6 +374,15 @@ impl DefaultDispatcher { Ok(()) } + fn cache_result(&self, scan_id: &str, result: models::Result) -> Result<(), StorageError> { + let mut data = self.results.as_ref().write()?; + if let Some(entry) = data.get_mut(scan_id) { + entry.push(result) + } else { + data.insert(scan_id.to_string(), vec![result]); + } + Ok(()) + } fn cache_notus_advisory(&self, adv: NotusAdvisory) -> Result<(), StorageError> { let mut data = self.advisories.as_ref().write()?; data.insert(adv); @@ -397,6 +412,7 @@ impl Dispatcher for DefaultDispatcher { self.cache_notus_advisory(x)? } } + Field::Result(x) => self.cache_result(key.as_ref(), *x)?, } Ok(()) } @@ -495,6 +511,35 @@ impl Retriever for DefaultDispatcher { } } } + Retrieve::Result(None) => { + let results = self.results.as_ref().read()?; + let results = if let Some(x) = results.get(key.as_ref()) { + let mut y = Vec::with_capacity(x.len()); + x.clone_into(&mut y); + y + } else { + vec![] + }; + Ok(Box::new( + results.into_iter().map(|x| Field::Result(x.into())), + )) + } + Retrieve::Result(Some(id)) => { + let results = self.results.as_ref().read()?; + let results = if let Some(x) = results.get(key.as_ref()) { + let mut y = Vec::with_capacity(x.len()); + x.clone_into(&mut y); + y + } else { + vec![] + }; + Ok(Box::new( + results + .into_iter() + .filter(move |x| x.id == id) + .map(|x| Field::Result(x.into())), + )) + } } } @@ -560,6 +605,14 @@ impl Retriever for DefaultDispatcher { ); Ok(Box::new(vec![].into_iter())) } + Retrieve::Result(x) => { + // are there use cases to get a KB outside of a scan? + tracing::warn!( + result = x, + "trying to get results without scan_id returning empty result" + ); + Ok(Box::new(vec![].into_iter())) + } } } } @@ -568,7 +621,6 @@ impl Retriever for DefaultDispatcher { mod tests { use crate::item::NVTKey; - use super::Field::*; use super::NVTField::*; use super::*; @@ -576,6 +628,7 @@ mod tests { pub fn default_storage() -> Result<(), StorageError> { let storage = DefaultDispatcher::default(); let key = ContextKey::FileName(String::new()); + use super::Field::*; storage.dispatch(&key, NVT(Oid("moep".to_owned())))?; assert_eq!( storage diff --git a/rust/storage/src/retrieve.rs b/rust/storage/src/retrieve.rs index 9335a35f4..628687414 100644 --- a/rust/storage/src/retrieve.rs +++ b/rust/storage/src/retrieve.rs @@ -18,6 +18,10 @@ pub enum Retrieve { KB(String), /// Metadata of the Notus advisory NotusAdvisory(Option), + /// Result + /// + /// When None retrieve all results when set the result with matching index. + Result(Option), } impl Retrieve { @@ -27,6 +31,7 @@ impl Retrieve { Retrieve::NVT(_) => "nvt", Retrieve::KB(_) => "kb", Retrieve::NotusAdvisory(_) => "notus", + Retrieve::Result(_) => "result", } } @@ -78,6 +83,14 @@ impl Retrieve { } Retrieve::NotusAdvisory(_) => matches!(field, Field::NotusAdvisory(_)), + Retrieve::Result(None) => matches!(field, Field::Result(_)), + Retrieve::Result(Some(id)) => { + if let Field::Result(r) = field { + &r.id == id + } else { + false + } + } } } } @@ -107,6 +120,31 @@ pub trait Retriever { )) } + /// Returns all results of a scan + fn results( + &self, + key: &ContextKey, + ) -> Result>, StorageError> { + Ok(Box::new( + self.retrieve(key, Retrieve::Result(None))? + .filter_map(|x| match x { + Field::Result(r) => Some(*r), + _ => None, + }), + )) + } + + /// Returns result with the given id + fn result(&self, key: &ContextKey, id: usize) -> Result, StorageError> { + Ok(self + .retrieve(key, Retrieve::Result(Some(id)))? + .filter_map(|x| match x { + Field::Result(r) => Some(*r), + _ => None, + }) + .next()) + } + /// Gets Fields find by field and scope. fn retrieve_by_field(&self, field: Field, scope: Retrieve) -> FieldKeyResult;