Skip to content

Commit

Permalink
Add host nasl built-in functions (#1758)
Browse files Browse the repository at this point in the history
* Add host nasl built-in functions
  • Loading branch information
jjnicola authored Dec 3, 2024
1 parent c38c5bf commit 5cfa721
Show file tree
Hide file tree
Showing 12 changed files with 284 additions and 74 deletions.
6 changes: 3 additions & 3 deletions rust/src/feed/update/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 0 additions & 5 deletions rust/src/nasl/builtin/host/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
## Implements

- get_host_name
- get_host_names

## Missing

- TARGET_IS_IPV6
- add_host_name
- get_host_name
Expand Down
200 changes: 158 additions & 42 deletions rust/src/nasl/builtin/host/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, FunctionErrorKind> {
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<Self, FunctionErrorKind> {
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<NaslValue, FunctionErrorKind> {
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<NaslValue, FunctionErrorKind> {
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<NaslValue, FunctionErrorKind> {
let hns = context.target_vhosts();
if !hns.is_empty() {
let hns = hns
.into_iter()
.map(|(h, _s)| NaslValue::String(h))
.collect::<Vec<_>>();
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<IpAddr, FunctionErrorKind> {
fn get_host_ip(context: &Context) -> Result<IpAddr, FunctionErrorKind> {
let default_ip = "127.0.0.1";
let r_sock_addr = match context.target() {
x if !x.is_empty() => IpAddr::from_str(x),
Expand All @@ -70,6 +64,66 @@ pub fn get_host_ip(context: &Context) -> Result<IpAddr, FunctionErrorKind> {
}
}

///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<NaslValue, FunctionErrorKind> {
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<NaslValue, FunctionErrorKind> {
let vh = context.target_vhosts();
let v = if !vh.is_empty() {
vh.iter()
.map(|(v, _s)| NaslValue::String(v.to_string()))
.collect::<Vec<_>>()
} 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,
Expand All @@ -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<NaslValue, FunctionErrorKind> {
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<bool, FunctionErrorKind> {
let target = match context.target().is_empty() {
true => {
return Err(FunctionErrorKind::diagnostic_ret_null("Address is NULL!"));
}
false => context.target(),
};
Ok(target.parse::<Ipv6Addr>().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<bool>) -> Result<bool, FunctionErrorKind> {
let h1 = resolve(h1.to_string())?;
let h2 = resolve(h2.to_string())?;

let hostnames1 = h1
.iter()
.filter_map(|x| lookup_addr(x).ok())
.collect::<Vec<_>>();
let hostnames2 = h2
.iter()
.filter_map(|x| lookup_addr(x).ok())
.collect::<Vec<_>>();

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
)
}
7 changes: 5 additions & 2 deletions rust/src/nasl/builtin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 1 addition & 3 deletions rust/src/nasl/builtin/ssh/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 5cfa721

Please sign in to comment.