diff --git a/rust/src/feed/update/mod.rs b/rust/src/feed/update/mod.rs index 39f50246d..8503898da 100644 --- a/rust/src/feed/update/mod.rs +++ b/rust/src/feed/update/mod.rs @@ -15,6 +15,7 @@ use crate::nasl::interpreter::{CodeInterpreter, Interpreter}; use crate::nasl::nasl_std_functions; use crate::nasl::prelude::*; use crate::nasl::syntax::AsBufReader; +use crate::nasl::utils::context::Target; use crate::nasl::ContextType; use crate::storage::{item::NVTField, ContextKey, Dispatcher, NoOpRetriever}; @@ -48,7 +49,7 @@ pub async fn feed_version( let register = Register::default(); let k = ContextKey::default(); let fr = NoOpRetriever::default(); - let target = String::default(); + let target = Target::default(); // TODO add parameter to struct let functions = nasl_std_functions(); let context = Context::new(k, target, dispatcher, &fr, loader, &functions); @@ -147,9 +148,8 @@ where let register = Register::root_initial(&self.initial); let fr = NoOpRetriever::default(); - let target = String::default(); + let target = Target::default(); let functions = nasl_std_functions(); - let context = Context::new( key.clone(), target, diff --git a/rust/src/nasl/builtin/host/README.md b/rust/src/nasl/builtin/host/README.md index 1bb78031a..a28c3c343 100644 --- a/rust/src/nasl/builtin/host/README.md +++ b/rust/src/nasl/builtin/host/README.md @@ -1,10 +1,5 @@ ## Implements -- get_host_name -- get_host_names - -## Missing - - TARGET_IS_IPV6 - add_host_name - get_host_name diff --git a/rust/src/nasl/builtin/host/mod.rs b/rust/src/nasl/builtin/host/mod.rs index 8a941ea79..9d57a0bc8 100644 --- a/rust/src/nasl/builtin/host/mod.rs +++ b/rust/src/nasl/builtin/host/mod.rs @@ -5,56 +5,50 @@ #[cfg(test)] mod tests; -use std::{net::IpAddr, str::FromStr}; +use std::{ + net::{IpAddr, Ipv6Addr}, + str::FromStr, +}; -use crate::function_set; -use crate::nasl::utils::{error::FunctionErrorKind, lookup_keys::TARGET}; +use dns_lookup::lookup_addr; +use nasl_function_proc_macro::nasl_function; -use crate::nasl::syntax::NaslValue; -use crate::nasl::utils::{Context, ContextType, Register}; - -/// Resolves IP address of target to hostname -/// -/// It does lookup TARGET and when not found falls back to 127.0.0.1 to resolve. -/// If the TARGET is not a IP address than we assume that it already is a fqdn or a hostname and will return that instead. -fn resolve_hostname(register: &Register) -> Result { - use std::net::ToSocketAddrs; +use crate::nasl::utils::{error::FunctionErrorKind, hosts::resolve}; +use crate::{function_set, nasl::FromNaslValue}; - let default_ip = "127.0.0.1"; - // currently we use shadow variables as _FC_ANON_ARGS; the original openvas uses redis for that purpose. - let target = register.named(TARGET).map_or_else( - || default_ip.to_owned(), - |x| match x { - ContextType::Value(NaslValue::String(x)) => x.clone(), - _ => default_ip.to_owned(), - }, - ); +use crate::nasl::syntax::NaslValue; +use crate::nasl::utils::{Context, Register}; - match target.to_socket_addrs() { - Ok(mut addr) => Ok(addr.next().map_or_else(String::new, |x| x.to_string())), - // assumes that target is already a hostname - Err(_) => Ok(target), +struct Hostname(String); +impl<'a> FromNaslValue<'a> for Hostname { + fn from_nasl_value(value: &'a NaslValue) -> Result { + let str = String::from_nasl_value(value)?; + if str.is_empty() { + Err(FunctionErrorKind::diagnostic_ret_null("Empty hostname.")) + } else { + Ok(Self(str)) + } } } -/// NASL function to get all stored vhosts -/// -/// As of now (2023-01-20) there is no vhost handling. -/// Therefore this function does load the registered TARGET and if it is an IP Address resolves it via DNS instead. -fn get_host_names(register: &Register, _: &Context) -> Result { - resolve_hostname(register).map(|x| NaslValue::Array(vec![NaslValue::String(x)])) -} - -/// NASL function to get the current hostname -/// -/// As of now (2023-01-20) there is no vhost handling. -/// Therefore this function does load the registered TARGET and if it is an IP Address resolves it via DNS instead. -fn get_host_name(register: &Register, _: &Context) -> Result { - resolve_hostname(register).map(NaslValue::String) +/// Get a list of found hostnames or a IP of the current target in case no hostnames were found yet. +#[nasl_function] +fn get_host_names(context: &Context) -> Result { + let hns = context.target_vhosts(); + if !hns.is_empty() { + let hns = hns + .into_iter() + .map(|(h, _s)| NaslValue::String(h)) + .collect::>(); + return Ok(NaslValue::Array(hns)); + }; + Ok(NaslValue::Array(vec![NaslValue::String( + context.target().to_string(), + )])) } /// Return the target's IP address as IpAddr. -pub fn get_host_ip(context: &Context) -> Result { +fn get_host_ip(context: &Context) -> Result { let default_ip = "127.0.0.1"; let r_sock_addr = match context.target() { x if !x.is_empty() => IpAddr::from_str(x), @@ -70,6 +64,66 @@ pub fn get_host_ip(context: &Context) -> Result { } } +///Expands the vHosts list with the given hostname. +///The mandatory parameter hostname is of type string. It contains the hostname which should be added to the list of vHosts +///Additionally a source, how the hostname was detected can be added with the named argument source as a string. If it is not given, the value NASL is set as default. +#[nasl_function(named(hostname, source))] +pub fn add_host_name( + context: &Context, + hostname: Hostname, + source: Option<&str>, +) -> Result { + let source = source.filter(|x| !x.is_empty()).unwrap_or("NASL"); + context.add_hostname(hostname.0, source.into()); + Ok(NaslValue::Null) +} + +/// Get the host name of the currently scanned target. If there is no host name available, the IP of the target is returned instead. +pub fn get_host_name( + _register: &Register, + context: &Context, +) -> Result { + let vh = context.target_vhosts(); + let v = if !vh.is_empty() { + vh.iter() + .map(|(v, _s)| NaslValue::String(v.to_string())) + .collect::>() + } else { + vec![] + }; + + //TODO: store the current hostname being forked. + //TODO: don't fork if expand_vhost is disabled. + //TODO: don't fork if already in a vhost + if !v.is_empty() { + return Ok(NaslValue::Fork(v)); + } + + let host = match get_host_ip(context) { + Ok(ip) => match lookup_addr(&ip) { + Ok(host) => host, + Err(_) => ip.to_string(), + }, + Err(_) => context.target().to_string(), + }; + Ok(NaslValue::String(host)) +} + +/// This function returns the source of detection of a given hostname. +/// The named parameter hostname is a string containing the hostname. +/// When no hostname is given, the current scanned host is taken. +/// If no virtual hosts are found yet this function always returns IP-address. +#[nasl_function(named(hostname))] +pub fn get_host_name_source(context: &Context, hostname: Hostname) -> String { + let vh = context.target_vhosts(); + if !vh.is_empty() { + if let Some((_, source)) = vh.into_iter().find(|(v, _)| v == &hostname.0) { + return source; + }; + } + context.target().to_string() +} + /// Return the target's IP address or 127.0.0.1 if not set. fn nasl_get_host_ip( _register: &Register, @@ -79,14 +133,76 @@ fn nasl_get_host_ip( Ok(NaslValue::String(ip.to_string())) } +/// Get an IP address corresponding to the host name +#[nasl_function(named(hostname))] +fn resolve_host_name(hostname: Hostname) -> String { + resolve(hostname.0).map_or_else( + |_| "127.0.0.1".to_string(), + |x| x.first().map_or("127.0.0.1".to_string(), |v| v.to_string()), + ) +} + +/// Resolve a hostname to all found addresses and return them in an NaslValue::Array +#[nasl_function(named(hostname))] +fn resolve_hostname_to_multiple_ips(hostname: Hostname) -> Result { + let ips = resolve(hostname.0)? + .into_iter() + .map(|x| NaslValue::String(x.to_string())) + .collect(); + Ok(NaslValue::Array(ips)) +} + +/// Check if the currently scanned target is an IPv6 address. +/// Return TRUE if the current target is an IPv6 address, else FALSE. In case of an error, NULL is returned. +#[nasl_function] +fn target_is_ipv6(context: &Context) -> Result { + let target = match context.target().is_empty() { + true => { + return Err(FunctionErrorKind::diagnostic_ret_null("Address is NULL!")); + } + false => context.target(), + }; + Ok(target.parse::().is_ok()) +} + +/// Compare if two hosts are the same. +/// The first two unnamed arguments are string containing the host to compare +/// If the named argument cmp_hostname is set to TRUE, the given hosts are resolved into their hostnames +#[nasl_function(named(cmp_hostname))] +fn same_host(h1: &str, h2: &str, cmp_hostname: Option) -> Result { + let h1 = resolve(h1.to_string())?; + let h2 = resolve(h2.to_string())?; + + let hostnames1 = h1 + .iter() + .filter_map(|x| lookup_addr(x).ok()) + .collect::>(); + let hostnames2 = h2 + .iter() + .filter_map(|x| lookup_addr(x).ok()) + .collect::>(); + + let any_ip_address_matches = h1.iter().any(|a1| h2.contains(a1)); + let any_hostname_matches = hostnames1.iter().any(|h1| hostnames2.contains(h1)); + let cmp_hostname = cmp_hostname.filter(|x| *x).unwrap_or(false); + + Ok(any_ip_address_matches || (cmp_hostname && any_hostname_matches)) +} + pub struct Host; function_set! { Host, sync_stateless, ( - get_host_name, get_host_names, - (nasl_get_host_ip, "get_host_ip") + (nasl_get_host_ip, "get_host_ip"), + resolve_host_name, + resolve_hostname_to_multiple_ips, + (target_is_ipv6, "TARGET_IS_IPV6"), + same_host, + add_host_name, + get_host_name, + get_host_name_source ) } diff --git a/rust/src/nasl/builtin/mod.rs b/rust/src/nasl/builtin/mod.rs index 9f7be4f39..21fb57575 100644 --- a/rust/src/nasl/builtin/mod.rs +++ b/rust/src/nasl/builtin/mod.rs @@ -30,6 +30,8 @@ use crate::nasl::syntax::{Loader, NoOpLoader}; use crate::nasl::utils::{Context, Executor, NaslVarRegister, NaslVarRegisterBuilder, Register}; use crate::storage::{ContextKey, DefaultDispatcher, Storage}; +use super::utils::context::Target; + /// Creates a new Executor and adds all the functions to it. /// /// When you have a function that is considered experimental due to either dependencies on @@ -137,11 +139,12 @@ where /// Creates a new Context with the shared loader, logger and function register pub fn build(&self, key: ContextKey) -> Context { - let target = match &key { + let mut target = Target::default(); + target.set_target(match &key { ContextKey::Scan(_, Some(target)) => target.clone(), ContextKey::Scan(_, None) => String::default(), ContextKey::FileName(target) => target.clone(), - }; + }); Context::new( key, target, diff --git a/rust/src/nasl/builtin/ssh/mod.rs b/rust/src/nasl/builtin/ssh/mod.rs index a144d0e91..f3d5a6a0e 100644 --- a/rust/src/nasl/builtin/ssh/mod.rs +++ b/rust/src/nasl/builtin/ssh/mod.rs @@ -153,9 +153,7 @@ impl Ssh { let port = port .filter(|_| socket.is_none()) .unwrap_or(DEFAULT_SSH_PORT); - let ip = ctx.target_ip().map_err(|e| { - SshError::from(SshErrorKind::InvalidIpAddr(ctx.target().to_string(), e)) - })?; + let ip = ctx.target_ip(); let timeout = timeout.map(Duration::from_secs); let keytype = keytype .map(|keytype| keytype.0) diff --git a/rust/src/nasl/utils/context.rs b/rust/src/nasl/utils/context.rs index 2c1457bb4..cc8d39d0f 100644 --- a/rust/src/nasl/utils/context.rs +++ b/rust/src/nasl/utils/context.rs @@ -7,6 +7,7 @@ use crate::nasl::syntax::{Loader, NaslValue, Statement}; use crate::storage::{ContextKey, Dispatcher, Retriever}; +use super::hosts::resolve; use super::{executor::Executor, lookup_keys::FC_ANON_ARGS}; /// Contexts are responsible to locate, add and delete everything that is declared within a NASL plugin @@ -289,7 +290,10 @@ impl Default for Register { } } use std::collections::HashMap; -use std::net::{AddrParseError, IpAddr}; +use std::net::IpAddr; +use std::str::FromStr; +use std::sync::Mutex; + type Named = HashMap; /// NaslContext is a struct to contain variables and if root declared functions @@ -329,6 +333,56 @@ impl NaslContext { } } +#[derive(Debug)] +pub struct Target { + /// The original target. IP or hostname + target: String, + /// The IP address. Always has a valid IP. It defaults to 127.0.0.1 if not possible to resolve target. + ip_addr: IpAddr, + // The shared state is guarded by a mutex. This is a `std::sync::Mutex` and + // not a Tokio mutex. This is because there are no asynchronous operations + // being performed while holding the mutex. Additionally, the critical + // sections are very small. + // + // A Tokio mutex is mostly intended to be used when locks need to be held + // across `.await` yield points. All other cases are **usually** best + // served by a std mutex. If the critical section does not include any + // async operations but is long (CPU intensive or performing blocking + // operations), then the entire operation, including waiting for the mutex, + // is considered a "blocking" operation and `tokio::task::spawn_blocking` + // should be used. + /// vhost list which resolve to the IP address and their sources. + vhosts: Mutex>, +} + +impl Target { + pub fn set_target(&mut self, target: String) -> &Target { + // Target can be an ip address or a hostname + self.target = target; + + // Store the IpAddr if possible, else default to localhost + self.ip_addr = match resolve(self.target.clone()) { + Ok(a) => *a.first().unwrap_or(&IpAddr::from_str("127.0.0.1").unwrap()), + Err(_) => IpAddr::from_str("127.0.0.1").unwrap(), + }; + self + } + + pub fn add_hostname(&self, hostname: String, source: String) -> &Target { + self.vhosts.lock().unwrap().push((hostname, source)); + self + } +} + +impl Default for Target { + fn default() -> Self { + Self { + target: String::new(), + ip_addr: IpAddr::from_str("127.0.0.1").unwrap(), + vhosts: Mutex::new(vec![]), + } + } +} /// Configurations /// /// This struct includes all objects that a nasl function requires. @@ -337,7 +391,7 @@ pub struct Context<'a> { /// key for this context. A file name or a scan id key: ContextKey, /// target to run a scan against - target: String, + target: Target, /// Default Dispatcher dispatcher: &'a dyn Dispatcher, /// Default Retriever @@ -352,7 +406,7 @@ impl<'a> Context<'a> { /// Creates an empty configuration pub fn new( key: ContextKey, - target: String, + target: Target, dispatcher: &'a dyn Dispatcher, retriever: &'a dyn Retriever, loader: &'a dyn Loader, @@ -394,18 +448,27 @@ impl<'a> Context<'a> { &self.key } - /// Get the target host + /// Get the target IP as string pub fn target(&self) -> &str { - &self.target + &self.target.target } - /// Get the target host - pub fn target_ip(&self) -> Result { - match self.target() { - x if !x.is_empty() => x.to_string(), - _ => "127.0.0.1".to_string(), - } - .parse() + /// Get the target host as IpAddr enum member + pub fn target_ip(&self) -> IpAddr { + self.target.ip_addr + } + + /// Get the target VHost list + pub fn target_vhosts(&self) -> Vec<(String, String)> { + self.target.vhosts.lock().unwrap().clone() + } + + pub fn set_target(&mut self, target: String) { + self.target.target = target; + } + + pub fn add_hostname(&self, hostname: String, source: String) { + self.target.add_hostname(hostname, source); } /// Get the storage diff --git a/rust/src/nasl/utils/error.rs b/rust/src/nasl/utils/error.rs index 16cc74641..797d9fba9 100644 --- a/rust/src/nasl/utils/error.rs +++ b/rust/src/nasl/utils/error.rs @@ -98,6 +98,12 @@ impl FunctionErrorKind { pub fn missing_argument(val: &str) -> Self { Self::MissingArguments(vec![val.to_string()]) } + + /// Helper function to quickly construct a `MissingArguments` variant + /// for a single missing argument and returning a NaslValue::Null + pub fn diagnostic_ret_null(val: &str) -> Self { + Self::Diagnostic(val.to_string(), Some(NaslValue::Null)) + } } impl From<(&str, &str, &NaslValue)> for FunctionErrorKind { diff --git a/rust/src/nasl/utils/hosts.rs b/rust/src/nasl/utils/hosts.rs new file mode 100644 index 000000000..f3d105128 --- /dev/null +++ b/rust/src/nasl/utils/hosts.rs @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2024 Greenbone AG +// +// SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception + +use std::net::{IpAddr, ToSocketAddrs}; + +use crate::nasl::utils::error::FunctionErrorKind; + +pub fn resolve(mut hostname: String) -> Result, FunctionErrorKind> { + //std::net to_socket_addrs() requires a port. Therefore, using a dummy port + hostname.push_str(":5000"); + + match hostname.to_socket_addrs() { + Ok(addr) => { + let ips = addr.into_iter().map(|x| x.ip()).collect::>(); + Ok(ips) + } + // assumes that target is already a hostname + Err(_) => Err(FunctionErrorKind::diagnostic_ret_null("Missing Hostname")), + } +} diff --git a/rust/src/nasl/utils/mod.rs b/rust/src/nasl/utils/mod.rs index 701ce7fcd..afb7fa0ab 100644 --- a/rust/src/nasl/utils/mod.rs +++ b/rust/src/nasl/utils/mod.rs @@ -7,11 +7,12 @@ pub mod context; pub mod error; mod executor; pub mod function; +pub mod hosts; pub mod lookup_keys; use std::collections::HashMap; -pub use context::{Context, ContextType, Register}; +pub use context::{Context, ContextType, Register, Target}; pub use error::FunctionErrorKind; pub use executor::{Executor, IntoFunctionSet, StoredFunctionSet}; diff --git a/rust/src/scanner/scan_runner.rs b/rust/src/scanner/scan_runner.rs index b0b9081ff..8cc68a796 100644 --- a/rust/src/scanner/scan_runner.rs +++ b/rust/src/scanner/scan_runner.rs @@ -121,6 +121,7 @@ pub(super) mod tests { use crate::nasl::utils::Context; use crate::nasl::utils::Executor; use crate::nasl::utils::Register; + use crate::nasl::utils::Target as ContextTarget; use crate::nasl::{interpreter::CodeInterpreter, nasl_std_functions}; use crate::scanner::{ error::{ExecuteError, ScriptResult}, @@ -320,7 +321,7 @@ exit({rc}); let storage = DefaultDispatcher::new(); let register = Register::root_initial(&initial); - let target = String::default(); + let target = ContextTarget::default(); let functions = nasl_std_functions(); let loader = |_: &str| code.to_string(); let key = ContextKey::FileName(id.to_string()); diff --git a/rust/src/scanner/vt_runner.rs b/rust/src/scanner/vt_runner.rs index eae697909..6a9c78831 100644 --- a/rust/src/scanner/vt_runner.rs +++ b/rust/src/scanner/vt_runner.rs @@ -1,5 +1,6 @@ use crate::models::{Host, Parameter, Protocol, ScanId}; use crate::nasl::syntax::{Loader, NaslValue}; +use crate::nasl::utils::context::Target; use crate::nasl::utils::{Executor, Register}; use crate::scheduling::Stage; use crate::storage::item::Nvt; @@ -193,10 +194,12 @@ impl<'a, Stack: ScannerStack> VTRunner<'a, Stack> { if let Err(e) = self.check_keys(self.vt) { return e; } + let mut target = Target::default(); + target.set_target(self.target.clone()); let context = Context::new( self.generate_key(), - self.target.clone(), + target, self.storage.as_dispatcher(), self.storage.as_retriever(), self.loader, diff --git a/rust/src/scannerctl/interpret/mod.rs b/rust/src/scannerctl/interpret/mod.rs index d9fc5430c..83226432d 100644 --- a/rust/src/scannerctl/interpret/mod.rs +++ b/rust/src/scannerctl/interpret/mod.rs @@ -125,10 +125,13 @@ where } async fn run(&self, script: &str) -> Result<(), CliErrorKind> { - let context = self.context_builder.build(ContextKey::Scan( - self.scan_id.clone(), - Some(self.target.clone()), - )); + let target = match self.target.is_empty() { + true => None, + false => Some(self.target.clone()), + }; + let context = self + .context_builder + .build(ContextKey::Scan(self.scan_id.clone(), target)); let register = RegisterBuilder::build(); let code = self.load(script)?; let results: Vec<_> = CodeInterpreter::new(&code, register, &context)