diff --git a/rust/crates/nasl-function-proc-macro/src/codegen.rs b/rust/crates/nasl-function-proc-macro/src/codegen.rs index 7adc48b13..e0af5e482 100644 --- a/rust/crates/nasl-function-proc-macro/src/codegen.rs +++ b/rust/crates/nasl-function-proc-macro/src/codegen.rs @@ -90,6 +90,18 @@ impl<'a> ArgsStruct<'a> { _register } }, + ArgKind::NaslSockets(arg) => { + if arg.mutable { + quote! { + &mut *_context.write_sockets().await + } + } + else { + quote! { + &*_context.read_sockets().await + } + } + }, ArgKind::PositionalIterator(arg) => { let position = arg.position; quote! { diff --git a/rust/crates/nasl-function-proc-macro/src/parse.rs b/rust/crates/nasl-function-proc-macro/src/parse.rs index 7cafc0061..06fdba646 100644 --- a/rust/crates/nasl-function-proc-macro/src/parse.rs +++ b/rust/crates/nasl-function-proc-macro/src/parse.rs @@ -2,7 +2,9 @@ use std::collections::HashSet; use crate::error::{Error, ErrorKind, Result}; use crate::types::*; -use crate::utils::{get_subty_if_name_is, ty_is_context, ty_is_register, ty_name_is}; +use crate::utils::{ + get_subty_if_name_is, ty_is_context, ty_is_nasl_sockets, ty_is_register, ty_name_is, +}; use syn::punctuated::Punctuated; use syn::token::Comma; use syn::{parenthesized, parse::Parse, spanned::Spanned, FnArg, Ident, ItemFn, Token, Type}; @@ -43,6 +45,9 @@ impl Attrs { if ty_is_register(ty) { return ArgKind::Register; } + if let Some(mutable) = ty_is_nasl_sockets(ty) { + return ArgKind::NaslSockets(NaslSocketsArg { mutable }); + } if ty_name_is(ty, "Positionals") { return ArgKind::PositionalIterator(PositionalsArg { position }); } diff --git a/rust/crates/nasl-function-proc-macro/src/types.rs b/rust/crates/nasl-function-proc-macro/src/types.rs index 57a9ef193..e17fd5389 100644 --- a/rust/crates/nasl-function-proc-macro/src/types.rs +++ b/rust/crates/nasl-function-proc-macro/src/types.rs @@ -35,12 +35,17 @@ pub struct Arg<'a> { pub mutable: bool, } +pub struct NaslSocketsArg { + pub mutable: bool, +} + pub enum ArgKind { Positional(PositionalArg), Named(NamedArg), MaybeNamed(PositionalArg, NamedArg), Context, Register, + NaslSockets(NaslSocketsArg), PositionalIterator(PositionalsArg), CheckedPositionalIterator(PositionalsArg), } @@ -66,11 +71,12 @@ impl ArgKind { match self { ArgKind::Context => 0, ArgKind::Register => 0, - ArgKind::Positional(_) => 1, - ArgKind::MaybeNamed(_, _) => 2, - ArgKind::Named(_) => 3, - ArgKind::PositionalIterator(_) => 4, - ArgKind::CheckedPositionalIterator(_) => 4, + ArgKind::NaslSockets(_) => 1, + ArgKind::Positional(_) => 2, + ArgKind::MaybeNamed(_, _) => 3, + ArgKind::Named(_) => 4, + ArgKind::PositionalIterator(_) => 5, + ArgKind::CheckedPositionalIterator(_) => 5, } } } diff --git a/rust/crates/nasl-function-proc-macro/src/utils.rs b/rust/crates/nasl-function-proc-macro/src/utils.rs index 684709c09..a52b660c1 100644 --- a/rust/crates/nasl-function-proc-macro/src/utils.rs +++ b/rust/crates/nasl-function-proc-macro/src/utils.rs @@ -22,6 +22,21 @@ pub fn ty_is_register(ty: &Type) -> bool { } } +pub fn ty_is_nasl_sockets(ty: &Type) -> Option<bool> { + if let Type::Reference(TypeReference { + elem, mutability, .. + }) = ty + { + if ty_name_is(elem, "NaslSockets") { + Some(mutability.is_some()) + } else { + None + } + } else { + None + } +} + pub fn get_subty_if_name_is<'a>(ty: &'a Type, name: &str) -> Option<&'a Type> { get_last_segment(ty) .filter(|segment| segment.ident == name) diff --git a/rust/src/models/port.rs b/rust/src/models/port.rs index 853808f7d..beb806c2c 100644 --- a/rust/src/models/port.rs +++ b/rust/src/models/port.rs @@ -1,3 +1,4 @@ +use core::iter::IntoIterator; // SPDX-FileCopyrightText: 2023 Greenbone AG // // SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception @@ -42,6 +43,18 @@ pub struct PortRange { pub end: Option<usize>, } +impl IntoIterator for PortRange { + type Item = u16; + + type IntoIter = std::ops::Range<u16>; + + fn into_iter(self) -> Self::IntoIter { + let start = self.start as u16; + let end = self.end.map(|end| end as u16).unwrap_or(u16::MAX); + (start..end).into_iter() + } +} + impl Display for PortRange { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.end { diff --git a/rust/src/nasl/builtin/error.rs b/rust/src/nasl/builtin/error.rs index 7899d10a7..55dc2e6e0 100644 --- a/rust/src/nasl/builtin/error.rs +++ b/rust/src/nasl/builtin/error.rs @@ -9,6 +9,7 @@ use crate::nasl::utils::error::FnErrorKind; use super::cert::CertError; use super::cryptographic::CryptographicError; +use super::find_service::FindServiceError; use super::host::HostError; use super::http::HttpError; use super::isotime::IsotimeError; @@ -43,6 +44,8 @@ pub enum BuiltinError { Cert(CertError), #[error("{0}")] Sys(SysError), + #[error("{0}")] + FindService(FindServiceError), #[cfg(feature = "nasl-builtin-raw-ip")] #[error("{0}")] RawIp(super::raw_ip::RawIpError), @@ -89,6 +92,7 @@ builtin_error_variant!(KBError, KB); builtin_error_variant!(HostError, Host); builtin_error_variant!(CertError, Cert); builtin_error_variant!(SysError, Sys); +builtin_error_variant!(FindServiceError, FindService); #[cfg(feature = "nasl-builtin-raw-ip")] builtin_error_variant!(super::raw_ip::RawIpError, RawIp); diff --git a/rust/src/nasl/builtin/find_service/mod.rs b/rust/src/nasl/builtin/find_service/mod.rs new file mode 100644 index 000000000..62a4552a3 --- /dev/null +++ b/rust/src/nasl/builtin/find_service/mod.rs @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2023 Greenbone AG +// +// SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception + +use std::{io, net::IpAddr, time::Duration}; + +use thiserror::Error; + +use crate::nasl::prelude::*; + +use super::network::socket::{make_tcp_socket, SocketError}; + +const TIMEOUT_MILLIS: u64 = 10000; + +#[derive(Debug, Error)] +pub enum FindServiceError { + #[error("{0}")] + SocketError(#[from] SocketError), +} + +struct Service { + name: String, + generate_result: GenerateResult, + save_banner: bool, + special_behavior: Option<SpecialBehavior>, +} + +enum GenerateResult { + No, + Yes { is_vulnerability: bool }, +} + +enum SpecialBehavior { + // TODO fill this in for services +} + +enum ReadResult { + Data(String), + Timeout, +} + +enum ScanPortResult { + Service(Service), + Timeout, +} + +async fn read_from_tcp_at_port(target: IpAddr, port: u16) -> Result<ReadResult, FindServiceError> { + let mut socket = make_tcp_socket(target, port, 0)?; + let mut buf: &mut [u8] = &mut [0; 100]; + let result = socket.read_with_timeout(buf, Duration::from_millis(TIMEOUT_MILLIS)); + match result { + Ok(pos) => Ok(ReadResult::Data( + String::from_utf8(buf[0..pos].to_vec()).unwrap(), + )), + Err(e) if e.kind() == io::ErrorKind::TimedOut => Ok(ReadResult::Timeout), + Err(e) => Err(SocketError::IO(e).into()), + } +} + +async fn scan_port(target: IpAddr, port: u16) -> Result<ScanPortResult, FindServiceError> { + let result = read_from_tcp_at_port(target, port).await?; + match result { + ReadResult::Data(data) => Ok(ScanPortResult::Service(find_service(data))), + ReadResult::Timeout => Ok(ScanPortResult::Timeout), + } +} + +fn find_service(data: String) -> Service { + todo!() +} + +#[nasl_function] +async fn plugin_run_find_service(context: &Context<'_>) -> () { + for port in context.port_range() { + match scan_port(context.target_ip(), port).await { + Ok(_) => {} + Err(e) => {} + } + } +} + +#[derive(Default)] +pub struct FindService { + services: Vec<Service>, +} + +function_set! { + FindService, + ( + plugin_run_find_service + ) +} diff --git a/rust/src/nasl/builtin/host/mod.rs b/rust/src/nasl/builtin/host/mod.rs index e45feea72..88ebda2af 100644 --- a/rust/src/nasl/builtin/host/mod.rs +++ b/rust/src/nasl/builtin/host/mod.rs @@ -45,7 +45,7 @@ fn get_host_names(context: &Context) -> Result<NaslValue, FnError> { if !hns.is_empty() { let hns = hns .into_iter() - .map(|(h, _s)| NaslValue::String(h)) + .map(|vhost| NaslValue::String(vhost.hostname().to_string())) .collect::<Vec<_>>(); return Ok(NaslValue::Array(hns)); }; @@ -91,7 +91,7 @@ pub fn get_host_name(_register: &Register, context: &Context) -> Result<NaslValu let vh = context.target_vhosts(); let v = if !vh.is_empty() { vh.iter() - .map(|(v, _s)| NaslValue::String(v.to_string())) + .map(|vhost| NaslValue::String(vhost.hostname().to_string())) .collect::<Vec<_>>() } else { vec![] @@ -122,8 +122,8 @@ pub fn get_host_name(_register: &Register, context: &Context) -> Result<NaslValu 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; + if let Some(vhost) = vh.into_iter().find(|vhost| vhost.hostname() == &hostname.0) { + return vhost.source().to_string(); }; } context.target().to_string() diff --git a/rust/src/nasl/builtin/mod.rs b/rust/src/nasl/builtin/mod.rs index 595e38b76..86a60f492 100644 --- a/rust/src/nasl/builtin/mod.rs +++ b/rust/src/nasl/builtin/mod.rs @@ -9,6 +9,7 @@ mod cert; mod cryptographic; mod description; mod error; +mod find_service; mod host; mod http; mod isotime; @@ -36,6 +37,8 @@ use crate::storage::{ContextKey, DefaultDispatcher, Storage}; use super::utils::context::Target; +pub use network::socket::NaslSockets; + /// 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 @@ -52,7 +55,7 @@ pub fn nasl_std_functions() -> Executor { .add_set(string::NaslString) .add_set(host::Host) .add_set(http::NaslHttp::default()) - .add_set(network::socket::NaslSockets::default()) + .add_set(network::socket::SocketFns) .add_set(network::network::Network) .add_set(regex::RegularExpressions) .add_set(cryptographic::Cryptographic) @@ -61,6 +64,7 @@ pub fn nasl_std_functions() -> Executor { .add_set(cryptographic::rc4::CipherHandlers::default()) .add_set(sys::Sys) .add_set(ssh::Ssh::default()) + .add_set(find_service::FindService::default()) .add_set(cert::NaslCerts::default()); #[cfg(feature = "nasl-builtin-raw-ip")] diff --git a/rust/src/nasl/builtin/network/mod.rs b/rust/src/nasl/builtin/network/mod.rs index 1f8f4e989..11b992a95 100644 --- a/rust/src/nasl/builtin/network/mod.rs +++ b/rust/src/nasl/builtin/network/mod.rs @@ -94,7 +94,7 @@ pub fn get_retry(context: &Context) -> u8 { } } -struct Port(u16); +pub struct Port(u16); impl FromNaslValue<'_> for Port { fn from_nasl_value(value: &NaslValue) -> Result<Self, FnError> { diff --git a/rust/src/nasl/builtin/network/socket.rs b/rust/src/nasl/builtin/network/socket.rs index 05d0ba8b6..8b9849c46 100644 --- a/rust/src/nasl/builtin/network/socket.rs +++ b/rust/src/nasl/builtin/network/socket.rs @@ -10,7 +10,7 @@ use std::{ time::{Duration, SystemTime}, }; -use crate::nasl::prelude::*; +use crate::nasl::{prelude::*, utils::function::Seconds}; use dns_lookup::lookup_host; use rustls::ClientConnection; use thiserror::Error; @@ -90,13 +90,29 @@ struct TlsConfig { /// Representation of a NASL socket. A NASL socket can be either TCP (including TLS), /// or UDP. -enum NaslSocket { +pub enum NaslSocket { // The TCP Connection is boxed, because it uses a lot of space // This way the size of the enum is reduced Tcp(Box<TcpConnection>), Udp(UdpConnection), } +impl NaslSocket { + pub fn read_with_timeout(&mut self, buf: &mut [u8], timeout: Duration) -> io::Result<usize> { + match self { + NaslSocket::Tcp(tcp_connection) => tcp_connection.read_with_timeout(buf, timeout), + NaslSocket::Udp(udp_connetion) => udp_connetion.read_with_timeout(buf, timeout), + } + } + + pub fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + match self { + NaslSocket::Tcp(tcp_connection) => tcp_connection.read(buf), + NaslSocket::Udp(udp_connetion) => udp_connetion.read(buf), + } + } +} + /// The Top level struct storing all NASL sockets, a list of the /// closed sockets which should be overwritten next, as well as the /// interval for TCP requests. @@ -144,19 +160,6 @@ impl NaslSockets { } } - /// Close a given file descriptor taken as an unnamed argument. - #[nasl_function] - fn close(&mut self, socket_fd: usize) -> Result<(), FnError> { - let socket = self.get_socket_mut(socket_fd)?; - if socket.is_none() { - return Err(SocketError::SocketClosed(socket_fd).into()); - } else { - *socket = None; - }; - self.closed_fd.push(socket_fd); - Ok(()) - } - /// Check if a new tcp request can be sent and waits if necessary. fn wait_before_next_probe(&self) { if let Some(interval) = &self.interval { @@ -164,321 +167,6 @@ impl NaslSockets { } } - /// Send data on a socket. - /// Args: - /// takes the following named arguments: - /// - socket: the socket, of course. - /// - data: the data block. A string is expected here (pure or impure, this does not matter). - /// - length: is optional and will be the full data length if not set - /// - option: is the flags for the send() system call. You should not use a raw numeric value here. - /// - /// On success the number of sent bytes is returned. - #[nasl_function(named(socket, data, option, len))] - fn send( - &mut self, - socket: usize, - data: &[u8], - option: Option<i64>, - len: Option<usize>, - ) -> Result<usize, SocketError> { - let len = if let Some(len) = len { - if len < 1 || len > data.len() { - data.len() - } else { - len - } - } else { - data.len() - }; - - let data = &data[0..len]; - - // Do this before we borrow the socket mutably to make the - // borrow checker happy. Please give me partial borrows. - if let NaslSocket::Tcp(_) = self.get_open_socket(socket)? { - self.wait_before_next_probe(); - } - match self.get_open_socket_mut(socket)? { - NaslSocket::Tcp(conn) => { - if let Some(flags) = option { - if flags < 0 || flags > i32::MAX as i64 { - return Err(SocketError::WrongArgument( - "the given flags value is out of range".to_string(), - )); - } - Ok(conn.send_with_flags(data, flags as i32)?) - } else { - Ok(conn.write(data)?) - } - } - NaslSocket::Udp(conn) => { - if let Some(flags) = option { - conn.set_flags(flags as i32); - } - Ok(conn.write(data)?) - } - } - } - - /// Receives data from a TCP or UDP socket. For a UDP socket, if it cannot read data, NASL will - /// suppose that the last sent datagram was lost and will sent it again a couple of time. - /// Args: - /// - socket which was returned by an open sock function - /// - length the number of bytes that you want to read at most. recv may return before length - /// bytes have been read: as soon as at least one byte has been received, the timeout is - /// lowered to 1 second. If no data is received during that time, the function returns the - /// already read data; otherwise, if the full initial timeout has not been reached, a - /// 1 second timeout is re-armed and the script tries to receive more data from the socket. - /// This special feature was implemented to get a good compromise between reliability and - /// speed when openvas-scanner talks to unknown or complex protocols. Two other optional - /// named integer arguments can twist this behavior: - /// - min is the minimum number of data that must be read in case the “magic read function” is activated and the timeout is lowered. By default this is 0. It works together with length. More info https://lists.archive.carbon60.com/nessus/devel/13796 - /// - timeout can be changed from the default. - #[nasl_function(named(socket, length, min, timeout))] - fn recv( - &mut self, - socket: usize, - length: usize, - min: Option<i64>, - timeout: Option<i64>, - ) -> Result<NaslValue, SocketError> { - let min = min - .map(|min| if min < 0 { length } else { min as usize }) - .unwrap_or(length); - let mut data = vec![0; length]; - - match self.get_open_socket_mut(socket)? { - NaslSocket::Tcp(conn) => { - let mut pos = match convert_timeout(timeout) { - Some(timeout) => conn.read_with_timeout(&mut data, timeout), - None => conn.read(&mut data), - }?; - let timeout = Duration::from_secs(1); - while pos < min { - match conn.read_with_timeout(&mut data[pos..], timeout) { - Ok(n) => pos += n, - Err(e) if e.kind() == io::ErrorKind::TimedOut => break, - Err(e) => return Err(SocketError::from(e)), - } - } - Ok(NaslValue::Data(data[..pos].to_vec())) - } - NaslSocket::Udp(conn) => { - let pos = match convert_timeout(timeout) { - Some(timeout) => conn.read_with_timeout(&mut data, timeout), - None => conn.read(&mut data), - }?; - - Ok(NaslValue::Data(data[..pos].to_vec())) - } - } - } - - /// Receives a line from a TCP response. Note that this only works for NASL sockets - /// of type TCP. - /// Args: - /// - socket which was returned by an open sock function - /// - timeout can be changed from the default. - #[nasl_function(named(socket, length, timeout))] - fn recv_line( - &mut self, - socket: usize, - #[allow(unused_variables)] length: usize, - timeout: Option<i64>, - ) -> Result<NaslValue, SocketError> { - let mut data = String::new(); - match self.get_open_socket_mut(socket)? { - NaslSocket::Tcp(conn) => { - let pos = match convert_timeout(timeout) { - Some(timeout) => conn.read_line_with_timeout(&mut data, timeout), - None => conn.read_line(&mut data), - }?; - Ok(NaslValue::Data(data.as_bytes()[..pos].to_vec())) - } - NaslSocket::Udp(_) => Err(SocketError::SupportedOnlyOnTcp("recv_line".into())), - } - } - - /// Open a KDC socket. This function takes no arguments, but it is mandatory that keys are set. The following keys are required: - /// - Secret/kdc_hostname - /// - Secret/kdc_port - /// - Secret/kdc_use_tcp - #[nasl_function] - fn open_sock_kdc(&mut self, context: &Context) -> Result<NaslValue, FnError> { - let hostname: String = context.get_single_kb_item("Secret/kdc_hostname")?; - - let ip = lookup_host(&hostname) - .map_err(|_| SocketError::HostnameLookupFailed(hostname.clone()))? - .into_iter() - .next() - .ok_or(SocketError::HostnameNoIpFound(hostname))?; - - let port = context.get_single_kb_item::<Port>("Secret/kdc_port")?.0; - - let use_tcp: bool = context.get_single_kb_item("Secret/kdc_use_tcp")?; - - let socket = if use_tcp { - let tcp = TcpConnection::connect( - ip, - port, - None, - Duration::from_secs(30), - None, - get_retry(context), - ) - .map_err(SocketError::from)?; - NaslSocket::Tcp(Box::new(tcp)) - } else { - let udp = UdpConnection::new(ip, port)?; - NaslSocket::Udp(udp) - }; - - let ret = self.add(socket); - - Ok(NaslValue::Number(ret as i64)) - } - - fn make_tls_client_connection(context: &Context, vhost: &str) -> Option<ClientConnection> { - Self::get_tls_conf(context).ok().and_then(|conf| { - create_tls_client( - vhost, - &conf.cert_path, - &conf.key_path, - &conf.password, - &conf.cafile_path, - ) - .ok() - }) - } - - fn open_sock_tcp_vhost( - context: &Context, - addr: IpAddr, - timeout: Duration, - bufsz: Option<usize>, - port: u16, - vhost: &str, - transport: i64, - ) -> Result<Option<NaslSocket>, SocketError> { - if transport < 0 { - // TODO: Get port transport and open connection depending on it - todo!() - } - let tls = match OpenvasEncaps::from_i64(transport) { - // Auto Detection - Some(OpenvasEncaps::Auto) => { - // Try SSL/TLS first - Self::make_tls_client_connection(context, vhost) - } - // IP - Some(OpenvasEncaps::Ip) => None, - // Unsupported transport layer - None | Some(OpenvasEncaps::Max) => { - return Err(SocketError::UnsupportedTransportLayerUnknown(transport)) - } - // TLS/SSL - Some(tls_version) => match tls_version { - OpenvasEncaps::Tls12 | OpenvasEncaps::Tls13 => { - Self::make_tls_client_connection(context, vhost) - } - _ => return Err(SocketError::UnsupportedTransportLayerTlsVersion(transport)), - }, - }; - Ok( - TcpConnection::connect(addr, port, tls, timeout, bufsz, get_retry(context)) - .map(|tcp| NaslSocket::Tcp(Box::new(tcp))) - .ok(), - ) - } - - /// Open a TCP socket to the target host. - /// This function is used to create a TCP connection to the target host. It requires the port - /// number as its argument and has various optional named arguments to control encapsulation, - /// timeout and buffering. - /// It takes an unnamed integer argument (the port number) and four optional named arguments: - /// - bufsz: An integer with the the size buffer size. Note that by default, no buffering is - /// used. - /// - timeout: An integer with the timeout value in seconds. The default timeout is controlled - /// by a global value. - /// - transport: One of the ENCAPS_* constants to force a specific encapsulation mode or force - /// trying of all modes (ENCAPS_AUTO). This is for example useful to select a specific TLS or - /// SSL version or use specific TLS connection setup priorities. See *get_port_transport for - /// a description of the ENCAPS constants. - /// - priority A string value with priorities for an TLS encapsulation. For the syntax of the - /// priority string see the GNUTLS manual. This argument is only used in ENCAPS_TLScustom - /// encapsulation. - #[nasl_function(named(timeout, transport, bufsz))] - fn open_sock_tcp( - &mut self, - context: &Context, - port: Port, - timeout: Option<i64>, - transport: Option<i64>, - bufsz: Option<i64>, - // TODO: Extract information from custom priority string - // priority: Option<&str>, - ) -> Result<NaslValue, FnError> { - // Get port - let transport = transport.unwrap_or(-1); - - let addr = ipstr2ipaddr(context.target())?; - - self.wait_before_next_probe(); - - let bufsz = bufsz - .filter(|bufsz| *bufsz >= 0) - .map(|bufsz| bufsz as usize); - - // TODO: set timeout to global recv timeout * 2 when available - let timeout = convert_timeout(timeout).unwrap_or(Duration::from_secs(10)); - // TODO: for every vhost - let vhosts = ["localhost"]; - let sockets: Vec<Option<NaslSocket>> = vhosts - .iter() - .map(|vhost| { - Self::open_sock_tcp_vhost(context, addr, timeout, bufsz, port.0, vhost, transport) - }) - .collect::<Result<_, _>>()?; - - Ok(NaslValue::Fork( - sockets - .into_iter() - .flatten() - .map(|socket| { - let fd = self.add(socket); - NaslValue::Number(fd as i64) - }) - .collect(), - )) - } - - /// Reads the information necessary for a TLS connection from the KB and - /// return a TlsConfig on success. - fn get_tls_conf(context: &Context) -> Result<TlsConfig, FnError> { - let cert_path = context.get_single_kb_item("SSL/cert")?; - let key_path = context.get_single_kb_item("SSL/key")?; - let password = context.get_single_kb_item("SSL/password")?; - let cafile_path = context.get_single_kb_item("SSL/CA")?; - - Ok(TlsConfig { - cert_path, - key_path, - password, - cafile_path, - }) - } - - /// Open a UDP socket to the target host - #[nasl_function] - fn open_sock_udp(&mut self, context: &Context, port: Port) -> Result<NaslValue, FnError> { - let addr = ipstr2ipaddr(context.target())?; - - let socket = NaslSocket::Udp(UdpConnection::new(addr, port.0)?); - let fd = self.add(socket); - - Ok(NaslValue::Number(fd as i64)) - } - fn connect_priv_sock( &mut self, addr: IpAddr, @@ -530,127 +218,455 @@ impl NaslSockets { } Err(SocketError::UnableToOpenPrivSocket(addr).into()) } +} - /// Open a privileged socket to the target host. - /// It takes three named integer arguments: - /// - dport is the destination port - /// - sport is the source port, which may be inferior to 1024. This argument is optional. - /// If it is not set, the function will try to open a socket on any port from 1 to 1023. - /// - timeout: An integer with the timeout value in seconds. The default timeout is controlled by a global value. - #[nasl_function(named(dport, sport))] - fn open_priv_sock_tcp( - &mut self, - context: &Context, - dport: Port, - sport: Option<Port>, - ) -> Result<NaslValue, FnError> { - let addr = ipstr2ipaddr(context.target())?; - self.open_priv_sock(addr, dport, sport, true) - } +/// Close a given file descriptor taken as an unnamed argument. +#[nasl_function] +async fn close(sockets: &mut NaslSockets, socket_fd: usize) -> Result<(), FnError> { + let socket = sockets.get_socket_mut(socket_fd)?; + if socket.is_none() { + return Err(SocketError::SocketClosed(socket_fd).into()); + } else { + *socket = None; + }; + sockets.closed_fd.push(socket_fd); + Ok(()) +} - /// Open a privileged UDP socket to the target host. - /// It takes three named integer arguments: - /// - dport is the destination port - /// - sport is the source port, which may be inferior to 1024. This argument is optional. - /// If it is not set, the function will try to open a socket on any port from 1 to 1023. - #[nasl_function(named(dport, sport))] - fn open_priv_sock_udp( - &mut self, - context: &Context, - dport: Port, - sport: Option<Port>, - ) -> Result<NaslValue, FnError> { - let addr = ipstr2ipaddr(context.target())?; - self.open_priv_sock(addr, dport, sport, false) - } +/// Send data on a socket. +/// Args: +/// takes the following named arguments: +/// - socket: the socket, of course. +/// - data: the data block. A string is expected here (pure or impure, this does not matter). +/// - length: is optional and will be the full data length if not set +/// - option: is the flags for the send() system call. You should not use a raw numeric value here. +/// +/// On success the number of sent bytes is returned. +#[nasl_function(named(socket, data, option, len))] +async fn send( + sockets: &mut NaslSockets, + socket: usize, + data: &[u8], + option: Option<i64>, + len: Option<usize>, +) -> Result<usize, SocketError> { + let len = if let Some(len) = len { + if len < 1 || len > data.len() { + data.len() + } else { + len + } + } else { + data.len() + }; - /// Get the source port of a open socket - #[nasl_function] - fn get_source_port(&self, socket: usize) -> Result<NaslValue, SocketError> { - let socket = self.get_open_socket(socket)?; - let port = match socket { - NaslSocket::Tcp(conn) => conn.local_addr()?.port(), - NaslSocket::Udp(conn) => conn.local_addr()?.port(), - }; - Ok(NaslValue::Number(port as i64)) + let data = &data[0..len]; + + // Do this before we borrow the socket mutably to make the + // borrow checker happy. Please give me partial borrows. + if let NaslSocket::Tcp(_) = sockets.get_open_socket(socket)? { + sockets.wait_before_next_probe(); } + match sockets.get_open_socket_mut(socket)? { + NaslSocket::Tcp(conn) => { + if let Some(flags) = option { + if flags < 0 || flags > i32::MAX as i64 { + return Err(SocketError::WrongArgument( + "the given flags value is out of range".to_string(), + )); + } + Ok(conn.send_with_flags(data, flags as i32)?) + } else { + Ok(conn.write(data)?) + } + } + NaslSocket::Udp(conn) => { + if let Some(flags) = option { + conn.set_flags(flags as i32); + } + Ok(conn.write(data)?) + } + } +} - /// Receive a response of a FTP server and checks the status code of it. - /// This status code is compared to a list of expected status codes and - /// returned, if it is contained in that list. - pub fn check_ftp_response( - mut conn: impl BufRead, - expected_codes: &[usize], - ) -> Result<usize, SocketError> { - let mut line = String::with_capacity(5); - conn.read_line(&mut line)?; +/// Receives data from a TCP or UDP socket. For a UDP socket, if it cannot read data, NASL will +/// suppose that the last sent datagram was lost and will sent it again a couple of time. +/// Args: +/// - socket which was returned by an open sock function +/// - length the number of bytes that you want to read at most. recv may return before length +/// bytes have been read: as soon as at least one byte has been received, the timeout is +/// lowered to 1 second. If no data is received during that time, the function returns the +/// already read data; otherwise, if the full initial timeout has not been reached, a +/// 1 second timeout is re-armed and the script tries to receive more data from the socket. +/// This special feature was implemented to get a good compromise between reliability and +/// speed when openvas-scanner talks to unknown or complex protocols. Two other optional +/// named integer arguments can twist this behavior: +/// - min is the minimum number of data that must be read in case the “magic read function” is activated and the timeout is lowered. By default this is 0. It works together with length. More info https://lists.archive.carbon60.com/nessus/devel/13796 +/// - timeout can be changed from the default. +#[nasl_function(named(socket, length, min, timeout))] +async fn recv( + sockets: &mut NaslSockets, + socket: usize, + length: usize, + min: Option<i64>, + timeout: Option<Seconds>, +) -> Result<NaslValue, SocketError> { + let min = min + .map(|min| if min < 0 { length } else { min as usize }) + .unwrap_or(length); + let mut data = vec![0; length]; + + let socket = sockets.get_open_socket_mut(socket)?; + let mut pos = match timeout { + Some(timeout) => socket.read_with_timeout(&mut data, timeout.as_duration())?, + None => socket.read(&mut data)?, + }; + if let NaslSocket::Tcp(conn) = socket { + let timeout = Duration::from_secs(1); + while pos < min { + match conn.read_with_timeout(&mut data[pos..], timeout) { + Ok(n) => pos += n, + Err(e) if e.kind() == io::ErrorKind::TimedOut => break, + Err(e) => return Err(SocketError::from(e)), + } + } + }; + Ok(NaslValue::Data(data[..pos].to_vec())) +} - if line.len() < 5 { - return Err(SocketError::FailedToReadResponseCode); +/// Receives a line from a TCP response. Note that this only works for NASL sockets +/// of type TCP. +/// Args: +/// - socket which was returned by an open sock function +/// - timeout can be changed from the default. +#[nasl_function(named(socket, length, timeout))] +async fn recv_line( + sockets: &mut NaslSockets, + socket: usize, + #[allow(unused_variables)] length: usize, + timeout: Option<i64>, +) -> Result<NaslValue, SocketError> { + let mut data = String::new(); + match sockets.get_open_socket_mut(socket)? { + NaslSocket::Tcp(conn) => { + let pos = match convert_timeout(timeout) { + Some(timeout) => conn.read_line_with_timeout(&mut data, timeout), + None => conn.read_line(&mut data), + }?; + Ok(NaslValue::Data(data.as_bytes()[..pos].to_vec())) } + NaslSocket::Udp(_) => Err(SocketError::SupportedOnlyOnTcp("recv_line".into())), + } +} - let code: usize = line[0..3] - .parse() - .map_err(SocketError::FailedToParseResponseCode)?; +pub fn make_tcp_socket(ip: IpAddr, port: u16, retry: u8) -> Result<NaslSocket, SocketError> { + let tcp = TcpConnection::connect(ip, port, None, Duration::from_secs(30), None, retry) + .map_err(SocketError::from)?; + Ok(NaslSocket::Tcp(Box::new(tcp))) +} - // multiple line reply - // loop while the line does not begin with the code and a space - let expected = format!("{} ", &line[0..3]); - while line.len() < 5 || line[0..4] != expected { - line.clear(); - conn.read_line(&mut line)?; - } +/// Open a KDC socket. This function takes no arguments, but it is mandatory that keys are set. The following keys are required: +/// - Secret/kdc_hostname +/// - Secret/kdc_port +/// - Secret/kdc_use_tcp +#[nasl_function] +async fn open_sock_kdc( + context: &Context<'_>, + sockets: &mut NaslSockets, +) -> Result<NaslValue, FnError> { + let hostname: String = context.get_single_kb_item("Secret/kdc_hostname")?; + + let ip = lookup_host(&hostname) + .map_err(|_| SocketError::HostnameLookupFailed(hostname.clone()))? + .into_iter() + .next() + .ok_or(SocketError::HostnameNoIpFound(hostname))?; + + let port = context.get_single_kb_item::<Port>("Secret/kdc_port")?.0; + + let use_tcp: bool = context.get_single_kb_item("Secret/kdc_use_tcp")?; + + let socket = if use_tcp { + make_tcp_socket(ip, port, get_retry(context))? + } else { + let udp = UdpConnection::new(ip, port)?; + NaslSocket::Udp(udp) + }; + + let ret = sockets.add(socket); + + Ok(NaslValue::Number(ret as i64)) +} - line = String::from(line.trim()); +fn make_tls_client_connection(context: &Context<'_>, vhost: &str) -> Option<ClientConnection> { + get_tls_conf(context).ok().and_then(|conf| { + create_tls_client( + vhost, + &conf.cert_path, + &conf.key_path, + &conf.password, + &conf.cafile_path, + ) + .ok() + }) +} - if expected_codes.iter().any(|ec| code == *ec) { - Ok(code) - } else { - Err(SocketError::ResponseCodeMismatch( - expected_codes.to_vec(), - line, - )) +fn open_sock_tcp_vhost( + context: &Context<'_>, + addr: IpAddr, + timeout: Duration, + bufsz: Option<usize>, + port: u16, + vhost: &str, + transport: i64, +) -> Result<Option<NaslSocket>, SocketError> { + if transport < 0 { + // TODO: Get port transport and open connection depending on it + todo!() + } + let tls = match OpenvasEncaps::from_i64(transport) { + // Auto Detection + Some(OpenvasEncaps::Auto) => { + // Try SSL/TLS first + make_tls_client_connection(context, vhost) + } + // IP + Some(OpenvasEncaps::Ip) => None, + // Unsupported transport layer + None | Some(OpenvasEncaps::Max) => { + return Err(SocketError::UnsupportedTransportLayerUnknown(transport)) } + // TLS/SSL + Some(tls_version) => match tls_version { + OpenvasEncaps::Tls12 | OpenvasEncaps::Tls13 => { + make_tls_client_connection(context, vhost) + } + _ => return Err(SocketError::UnsupportedTransportLayerTlsVersion(transport)), + }, + }; + Ok( + TcpConnection::connect(addr, port, tls, timeout, bufsz, get_retry(context)) + .map(|tcp| NaslSocket::Tcp(Box::new(tcp))) + .ok(), + ) +} + +/// Open a TCP socket to the target host. +/// This function is used to create a TCP connection to the target host. It requires the port +/// number as its argument and has various optional named arguments to control encapsulation, +/// timeout and buffering. +/// It takes an unnamed integer argument (the port number) and four optional named arguments: +/// - bufsz: An integer with the the size buffer size. Note that by default, no buffering is +/// used. +/// - timeout: An integer with the timeout value in seconds. The default timeout is controlled +/// by a global value. +/// - transport: One of the ENCAPS_* constants to force a specific encapsulation mode or force +/// trying of all modes (ENCAPS_AUTO). This is for example useful to select a specific TLS or +/// SSL version or use specific TLS connection setup priorities. See *get_port_transport for +/// a description of the ENCAPS constants. +/// - priority A string value with priorities for an TLS encapsulation. For the syntax of the +/// priority string see the GNUTLS manual. This argument is only used in ENCAPS_TLScustom +/// encapsulation. +#[nasl_function(named(timeout, transport, bufsz))] +async fn open_sock_tcp( + context: &Context<'_>, + nasl_sockets: &mut NaslSockets, + port: Port, + timeout: Option<i64>, + transport: Option<i64>, + bufsz: Option<i64>, + // TODO: Extract information from custom priority string + // priority: Option<&str>, +) -> Result<NaslValue, FnError> { + // Get port + let transport = transport.unwrap_or(-1); + + let addr = ipstr2ipaddr(context.target())?; + + nasl_sockets.wait_before_next_probe(); + + let bufsz = bufsz + .filter(|bufsz| *bufsz >= 0) + .map(|bufsz| bufsz as usize); + + // TODO: set timeout to global recv timeout * 2 when available + let timeout = convert_timeout(timeout).unwrap_or(Duration::from_secs(10)); + // TODO: for every vhost + let vhosts = ["localhost"]; + let sockets: Vec<Option<NaslSocket>> = vhosts + .iter() + .map(|vhost| open_sock_tcp_vhost(context, addr, timeout, bufsz, port.0, vhost, transport)) + .collect::<Result<_, _>>()?; + + Ok(NaslValue::Fork( + sockets + .into_iter() + .flatten() + .map(|socket| { + let fd = nasl_sockets.add(socket); + NaslValue::Number(fd as i64) + }) + .collect(), + )) +} + +/// Reads the information necessary for a TLS connection from the KB and +/// return a TlsConfig on success. +fn get_tls_conf(context: &Context) -> Result<TlsConfig, FnError> { + let cert_path = context.get_single_kb_item("SSL/cert")?; + let key_path = context.get_single_kb_item("SSL/key")?; + let password = context.get_single_kb_item("SSL/password")?; + let cafile_path = context.get_single_kb_item("SSL/CA")?; + + Ok(TlsConfig { + cert_path, + key_path, + password, + cafile_path, + }) +} + +/// Open a UDP socket to the target host +#[nasl_function] +async fn open_sock_udp( + context: &Context<'_>, + sockets: &mut NaslSockets, + port: Port, +) -> Result<NaslValue, FnError> { + let addr = ipstr2ipaddr(context.target())?; + + let socket = NaslSocket::Udp(UdpConnection::new(addr, port.0)?); + let fd = sockets.add(socket); + + Ok(NaslValue::Number(fd as i64)) +} + +/// Open a privileged socket to the target host. +/// It takes three named integer arguments: +/// - dport is the destination port +/// - sport is the source port, which may be inferior to 1024. This argument is optional. +/// If it is not set, the function will try to open a socket on any port from 1 to 1023. +/// - timeout: An integer with the timeout value in seconds. The default timeout is controlled by a global value. +#[nasl_function(named(dport, sport))] +async fn open_priv_sock_tcp( + context: &Context<'_>, + sockets: &mut NaslSockets, + dport: Port, + sport: Option<Port>, +) -> Result<NaslValue, FnError> { + let addr = ipstr2ipaddr(context.target())?; + sockets.open_priv_sock(addr, dport, sport, true) +} + +/// Open a privileged UDP socket to the target host. +/// It takes three named integer arguments: +/// - dport is the destination port +/// - sport is the source port, which may be inferior to 1024. This argument is optional. +/// If it is not set, the function will try to open a socket on any port from 1 to 1023. +#[nasl_function(named(dport, sport))] +async fn open_priv_sock_udp( + context: &Context<'_>, + sockets: &mut NaslSockets, + dport: Port, + sport: Option<Port>, +) -> Result<NaslValue, FnError> { + let addr = ipstr2ipaddr(context.target())?; + sockets.open_priv_sock(addr, dport, sport, false) +} + +/// Get the source port of a open socket +#[nasl_function] +async fn get_source_port(sockets: &NaslSockets, socket: usize) -> Result<NaslValue, SocketError> { + let socket = sockets.get_open_socket(socket)?; + let port = match socket { + NaslSocket::Tcp(conn) => conn.local_addr()?.port(), + NaslSocket::Udp(conn) => conn.local_addr()?.port(), + }; + Ok(NaslValue::Number(port as i64)) +} + +/// Receive a response of a FTP server and checks the status code of it. +/// This status code is compared to a list of expected status codes and +/// returned, if it is contained in that list. +pub fn check_ftp_response( + mut conn: impl BufRead, + expected_codes: &[usize], +) -> Result<usize, SocketError> { + let mut line = String::with_capacity(5); + conn.read_line(&mut line)?; + + if line.len() < 5 { + return Err(SocketError::FailedToReadResponseCode); } - /// **ftp_log_in** takes three named arguments: - /// - user: is the user name (it has no default value like “anonymous” or “ftp”) - /// - pass: is the password (again, no default value like the user e-mail address) - /// - socket: an open socket. - #[nasl_function(named(user, pass, socket))] - fn ftp_log_in(&mut self, user: &str, pass: &str, socket: usize) -> Result<bool, SocketError> { - match self.get_open_socket_mut(socket)? { - NaslSocket::Tcp(conn) => { - Self::check_ftp_response(&mut *conn, &[220])?; - let data = format!("USER {}\r\n", user); - conn.write_all(data.as_bytes())?; + let code: usize = line[0..3] + .parse() + .map_err(SocketError::FailedToParseResponseCode)?; - let code = Self::check_ftp_response(&mut *conn, &[230, 331])?; - if code == 331 { - let data = format!("PASS {}\r\n", pass); - conn.write_all(data.as_bytes())?; - Self::check_ftp_response(&mut *conn, &[230])?; - } - Ok(true) + // multiple line reply + // loop while the line does not begin with the code and a space + let expected = format!("{} ", &line[0..3]); + while line.len() < 5 || line[0..4] != expected { + line.clear(); + conn.read_line(&mut line)?; + } + + line = String::from(line.trim()); + + if expected_codes.iter().any(|ec| code == *ec) { + Ok(code) + } else { + Err(SocketError::ResponseCodeMismatch( + expected_codes.to_vec(), + line, + )) + } +} + +/// **ftp_log_in** takes three named arguments: +/// - user: is the user name (it has no default value like “anonymous” or “ftp”) +/// - pass: is the password (again, no default value like the user e-mail address) +/// - socket: an open socket. +#[nasl_function(named(user, pass, socket))] +async fn ftp_log_in( + sockets: &mut NaslSockets, + user: &str, + pass: &str, + socket: usize, +) -> Result<bool, SocketError> { + match sockets.get_open_socket_mut(socket)? { + NaslSocket::Tcp(conn) => { + check_ftp_response(&mut *conn, &[220])?; + let data = format!("USER {}\r\n", user); + conn.write_all(data.as_bytes())?; + + let code = check_ftp_response(&mut *conn, &[230, 331])?; + if code == 331 { + let data = format!("PASS {}\r\n", pass); + conn.write_all(data.as_bytes())?; + check_ftp_response(&mut *conn, &[230])?; } - NaslSocket::Udp(_) => Err(SocketError::SupportedOnlyOnTcp("ftp_log_in".into())), + Ok(true) } + NaslSocket::Udp(_) => Err(SocketError::SupportedOnlyOnTcp("ftp_log_in".into())), } } +pub struct SocketFns; + function_set! { - NaslSockets, + SocketFns, ( - (NaslSockets::open_sock_kdc, "open_sock_kdc"), - (NaslSockets::open_sock_tcp, "open_sock_tcp"), - (NaslSockets::open_priv_sock_tcp, "open_priv_sock_tcp"), - (NaslSockets::open_sock_udp, "open_sock_udp"), - (NaslSockets::open_priv_sock_udp, "open_priv_sock_udp"), - (NaslSockets::close, "close"), - (NaslSockets::send, "send"), - (NaslSockets::recv, "recv"), - (NaslSockets::recv_line, "recv_line"), - (NaslSockets::get_source_port, "get_source_port"), - (NaslSockets::ftp_log_in, "ftp_log_in"), + (open_sock_kdc, "open_sock_kdc"), + (open_sock_tcp, "open_sock_tcp"), + (open_priv_sock_tcp, "open_priv_sock_tcp"), + (open_sock_udp, "open_sock_udp"), + (open_priv_sock_udp, "open_priv_sock_udp"), + (close, "close"), + (send, "send"), + (recv, "recv"), + (recv_line, "recv_line"), + (get_source_port, "get_source_port"), + (ftp_log_in, "ftp_log_in"), ) } diff --git a/rust/src/nasl/utils/context.rs b/rust/src/nasl/utils/context.rs index 66be540e2..7b5276138 100644 --- a/rust/src/nasl/utils/context.rs +++ b/rust/src/nasl/utils/context.rs @@ -5,8 +5,10 @@ //! Defines the context used within the interpreter and utilized by the builtin functions use itertools::Itertools; +use tokio::sync::RwLock; -use crate::nasl::builtin::KBError; +use crate::models::PortRange; +use crate::nasl::builtin::{KBError, NaslSockets}; use crate::nasl::syntax::{Loader, NaslValue, Statement}; use crate::nasl::{FromNaslValue, WithErrorInfo}; use crate::storage::{ContextKey, Dispatcher, Field, Retrieve, Retriever}; @@ -358,7 +360,7 @@ pub struct Target { // 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<Vec<(String, String)>>, + vhosts: Mutex<Vec<VHost>>, } impl Target { @@ -375,7 +377,7 @@ impl Target { } pub fn add_hostname(&self, hostname: String, source: String) -> &Target { - self.vhosts.lock().unwrap().push((hostname, source)); + self.vhosts.lock().unwrap().push(VHost { hostname, source }); self } } @@ -389,23 +391,32 @@ impl Default for Target { } } } -/// Configurations -/// + +#[derive(Debug, Clone)] +pub struct VHost { + source: String, + hostname: String, +} + +impl VHost { + pub fn hostname(&self) -> &str { + &self.hostname + } + + pub fn source(&self) -> &str { + &self.source + } +} + /// This struct includes all objects that a nasl function requires. -/// New objects must be added here in pub struct Context<'a> { - /// key for this context. A file name or a scan id key: ContextKey, - /// target to run a scan against target: Target, - /// Default Dispatcher dispatcher: &'a dyn Dispatcher, - /// Default Retriever retriever: &'a dyn Retriever, - /// Default Loader loader: &'a dyn Loader, - /// Default function executor. executor: &'a Executor, + sockets: RwLock<NaslSockets>, } impl<'a> Context<'a> { @@ -425,6 +436,7 @@ impl<'a> Context<'a> { retriever, loader, executor, + sockets: RwLock::new(NaslSockets::default()), } } @@ -465,7 +477,7 @@ impl<'a> Context<'a> { } /// Get the target VHost list - pub fn target_vhosts(&self) -> Vec<(String, String)> { + pub fn target_vhosts(&self) -> Vec<VHost> { self.target.vhosts.lock().unwrap().clone() } @@ -477,6 +489,14 @@ impl<'a> Context<'a> { self.target.add_hostname(hostname, source); } + pub fn port_range(&self) -> PortRange { + // TODO Get this from the scan prefs + PortRange { + start: 0, + end: None, + } + } + /// Get the storage pub fn dispatcher(&self) -> &dyn Dispatcher { self.dispatcher @@ -525,6 +545,16 @@ impl<'a> Context<'a> { .ok_or_else(|| KBError::ItemNotFound(name.to_string()))?; Ok(single_item) } + + pub async fn read_sockets(&self) -> tokio::sync::RwLockReadGuard<'_, NaslSockets> { + // TODO do not unwrap? + self.sockets.read().await + } + + pub async fn write_sockets(&self) -> tokio::sync::RwLockWriteGuard<'_, NaslSockets> { + // TODO do not unwrap? + self.sockets.write().await + } } impl From<&ContextType> for NaslValue { diff --git a/rust/src/nasl/utils/function/mod.rs b/rust/src/nasl/utils/function/mod.rs index beaddb234..b532ea578 100644 --- a/rust/src/nasl/utils/function/mod.rs +++ b/rust/src/nasl/utils/function/mod.rs @@ -18,4 +18,5 @@ pub use positionals::CheckedPositionals; pub use positionals::Positionals; pub use to_nasl_result::ToNaslResult; pub use types::bytes_to_str; +pub use types::Seconds; pub use types::StringOrData; diff --git a/rust/src/nasl/utils/function/types.rs b/rust/src/nasl/utils/function/types.rs index 6b3c718fd..56582eec8 100644 --- a/rust/src/nasl/utils/function/types.rs +++ b/rust/src/nasl/utils/function/types.rs @@ -2,6 +2,8 @@ // // SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception +use std::time::Duration; + use crate::nasl::prelude::*; /// `Some(string)` if constructed from either a `NaslValue::String` @@ -23,3 +25,21 @@ impl<'a> FromNaslValue<'a> for StringOrData { pub fn bytes_to_str(bytes: &[u8]) -> String { bytes.iter().map(|x| *x as char).collect::<String>() } + +pub struct Seconds(pub u64); + +impl Seconds { + pub fn as_duration(&self) -> Duration { + Duration::from_secs(self.0) + } +} + +impl<'a> FromNaslValue<'a> for Seconds { + fn from_nasl_value(value: &'a NaslValue) -> Result<Self, FnError> { + let value: i64 = i64::from_nasl_value(value)?; + let value: u64 = value + .try_into() + .map_err(|e| ArgumentError::WrongArgument("Expected positive number".to_string()))?; + Ok(Seconds(value)) + } +}