diff --git a/Cargo.lock b/Cargo.lock index 98c4188ba..904260892 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,6 +42,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "anyhow" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1fd36ffbb1fb7c834eac128ea8d0e310c5aeb635548f9d58861e1308d46e71c" + [[package]] name = "arrayref" version = "0.3.6" @@ -654,6 +660,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" + [[package]] name = "js-sys" version = "0.3.45" @@ -1119,6 +1131,7 @@ name = "rustscan" version = "1.10.0" dependencies = [ "ansi_term 0.12.1", + "anyhow", "async-std", "cidr-utils", "colored", @@ -1135,11 +1148,19 @@ dependencies = [ "serde_derive", "shell-words", "structopt", + "subprocess", + "text_placeholder", "toml", "trust-dns-resolver", "wait-timeout", ] +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + [[package]] name = "sct" version = "0.6.0" @@ -1155,18 +1176,32 @@ name = "serde" version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" +dependencies = [ + "serde_derive", +] [[package]] name = "serde_derive" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f630a6370fd8e457873b4bd2ffdae75408bc291ba72be773772a4c2a065d9ae8" +checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "serde_json" +version = "1.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "shell-words" version = "1.0.0" @@ -1233,6 +1268,16 @@ dependencies = [ "syn", ] +[[package]] +name = "subprocess" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b9ad6c3e1b525a55872a4d2f2d404b3c959b7bbcbfd83c364580f68ed157bd" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "syn" version = "1.0.42" @@ -1253,6 +1298,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "text_placeholder" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd6afcbe8d748e35406f4c3a79b60567a5104b451f1b618097f139294969ef4" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "textwrap" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index f7f3374a4..fc884e36d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,9 @@ serde_derive = "1.0.116" cidr-utils = "0.5.0" itertools = "0.9.0" trust-dns-resolver = { version = "0.19.5", features = ["dns-over-rustls"] } +anyhow = "1.0.33" +subprocess = "0.2.6" +text_placeholder = { version = "0.4", features = ["struct_context"] } [dev-dependencies] wait-timeout = "0.2" diff --git a/fixtures/test_rustscan_scripts.toml b/fixtures/test_rustscan_scripts.toml new file mode 100644 index 000000000..233a0223d --- /dev/null +++ b/fixtures/test_rustscan_scripts.toml @@ -0,0 +1,14 @@ +# Test/Example ScriptConfig file + +# Tags to filter on scripts. Only scripts containing all these tags will run. +tags = ["core_approved", "example"] + +# If it's present then only those scripts will run which has a tag ports = "80". Not yet implemented. +# +# ex.: +# ports = [80] +# ports = [80,81,8080] +ports = [80] + +# Only this developer(s) scripts to run. Not yet implemented. +developer = ["example"] \ No newline at end of file diff --git a/fixtures/test_script.pl b/fixtures/test_script.pl new file mode 100644 index 000000000..c51cc014a --- /dev/null +++ b/fixtures/test_script.pl @@ -0,0 +1,23 @@ +#!/usr/bin/perl +#tags = ["core_approved", "example",] +#developer = [ "example", "https://example.org" ] +#ports_separator = "," +#call_format = "perl {{script}} {{ip}} {{port}}" + +# Sriptfile parser stops at the first blank line with parsing. +# This script will run itself as an argument with the system installed perl interpreter, ports will be concatenated with "," . +# Unused field: trigger_port = "80" +# get total arg passed to this script +my $total = $#ARGV + 1; +my $counter = 1; + +# get script name +my $scriptname = $0; + +print "Total args passed to $scriptname : $total\n"; + +# Use loop to print all args stored in an array called @ARGV +foreach my $a(@ARGV) { + print "Arg # $counter : $a\n"; + $counter++; +} \ No newline at end of file diff --git a/fixtures/test_script.py b/fixtures/test_script.py new file mode 100644 index 000000000..b990fd846 --- /dev/null +++ b/fixtures/test_script.py @@ -0,0 +1,13 @@ +#!/usr/bin/python3 +#tags = ["core_approved", "example",] +#developer = [ "example", "https://example.org" ] +#trigger_port = "80" +#call_format = "python3 {{script}} {{ip}} {{port}}" + +# Sriptfile parser stops at the first blank line with parsing. +# This script will run itself as an argument with the system installed python interpreter, only scanning port 80. +# Unused filed: ports_separator = "," + +import sys + +print('Python script ran with arguments', str(sys.argv)) \ No newline at end of file diff --git a/fixtures/test_script.sh b/fixtures/test_script.sh new file mode 100644 index 000000000..20b2f0e57 --- /dev/null +++ b/fixtures/test_script.sh @@ -0,0 +1,12 @@ +#!/bin/bash +#tags = ["core_approved", "example",] +#developer = [ "example", "https://example.org" ] +#ports_separator = "," +#call_format = "bash {{script}} {{ip}} {{port}}" + +# Sriptfile parser stops at the first blank line with parsing. +# This script will run itself as an argument with the system installed bash interpreter, scanning all ports concatenated with "," . +# Unused filed: trigger_port = "80" + +# print all arguments passed to the script +echo $@ \ No newline at end of file diff --git a/fixtures/test_script.txt b/fixtures/test_script.txt new file mode 100644 index 000000000..a5e0b73b4 --- /dev/null +++ b/fixtures/test_script.txt @@ -0,0 +1,9 @@ +#!intentional_blank_line +#tags = ["core_approved", "example"] +#developer = [ "example", "https://example.org" ] +#ports_separator = "," +#call_format = "nmap -vvv -p {{port}} {{ip}}" + +# Sriptfile parser stops at the first blank line with parsing. +# This script will run the system installed nmap, ports will be concatenated with "," . +# Unused field: trigger_port = "80" \ No newline at end of file diff --git a/src/input.rs b/src/input.rs index 23cc2ae4c..8969e783e 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,7 +1,6 @@ use serde_derive::Deserialize; use std::collections::HashMap; use std::fs; - use structopt::{clap::arg_enum, StructOpt}; const LOWEST_PORT_NUMBER: u16 = 1; @@ -18,6 +17,19 @@ arg_enum! { } } +arg_enum! { + /// Represents the scripts variant. + /// - none will avoid running any script, only portscan results will be shown. + /// - default will run the default embedded nmap script, that's part of RustScan since the beginning. + /// - custom will read the ScriptConfig file and the available scripts in the predefined folders + #[derive(Deserialize, Debug, StructOpt, Clone, PartialEq, Copy)] + pub enum ScriptsRequired { + None, + Default, + Custom, + } +} + /// Represents the range of ports to be scanned. #[derive(Deserialize, Debug, Clone, PartialEq)] pub struct PortRange { @@ -49,7 +61,7 @@ fn parse_range(input: &str) -> Result { } } -#[derive(StructOpt, Debug)] +#[derive(StructOpt, Debug, Clone)] #[structopt(name = "rustscan", setting = structopt::clap::AppSettings::TrailingVarArg)] /// Fast Port Scanner built in Rust. /// WARNING Do not use this program against sensitive infrastructure since the @@ -81,10 +93,6 @@ pub struct Opts { #[structopt(long)] pub accessible: bool, - /// Turns off Nmap. - #[structopt(long)] - pub no_nmap: bool, - /// The batch size for port scanning, it increases or slows the speed of /// scanning. Depends on the open file limit of your OS. If you do 65535 /// it will do every port at the same time. Although, your OS may not @@ -111,17 +119,21 @@ pub struct Opts { #[structopt(long, possible_values = &ScanOrder::variants(), case_insensitive = true, default_value = "serial")] pub scan_order: ScanOrder, - /// The Nmap arguments to run. + /// Level of scripting required for the run. + #[structopt(long, possible_values = &ScriptsRequired::variants(), case_insensitive = true, default_value = "default")] + pub scripts: ScriptsRequired, + + /// Use the top 1000 ports. + #[structopt(long)] + pub top: bool, + + /// The Script arguments to run. /// To use the argument -A, end RustScan's args with '-- -A'. /// Example: 'rustscan -T 1500 127.0.0.1 -- -A -sC'. /// This command adds -Pn -vvv -p $PORTS automatically to nmap. /// For things like --script '(safe and vuln)' enclose it in quotations marks \"'(safe and vuln)'\"") #[structopt(last = true)] pub command: Vec, - - /// Use the top 1000 ports. - #[structopt(long)] - pub top: bool, } #[cfg(not(tarpaulin_include))] @@ -160,7 +172,8 @@ impl Opts { } merge_required!( - addresses, greppable, accessible, batch_size, timeout, tries, scan_order, command + addresses, greppable, accessible, batch_size, timeout, tries, scan_order, scripts, + command ); } @@ -204,10 +217,10 @@ pub struct Config { batch_size: Option, timeout: Option, tries: Option, - no_nmap: Option, ulimit: Option, scan_order: Option, command: Option>, + scripts: Option, } #[cfg(not(tarpaulin_include))] @@ -251,7 +264,7 @@ impl Config { #[cfg(test)] mod tests { - use super::{Config, Opts, PortRange, ScanOrder}; + use super::{Config, Opts, PortRange, ScanOrder, ScriptsRequired}; impl Config { fn default() -> Self { Self { @@ -263,10 +276,10 @@ mod tests { timeout: Some(1_000), tries: Some(1), ulimit: None, - no_nmap: Some(false), command: Some(vec!["-A".to_owned()]), accessible: Some(true), scan_order: Some(ScanOrder::Random), + scripts: None, } } } @@ -284,10 +297,10 @@ mod tests { ulimit: None, command: vec![], accessible: false, - no_nmap: false, scan_order: ScanOrder::Serial, no_config: true, top: false, + scripts: ScriptsRequired::Default, } } } @@ -320,6 +333,7 @@ mod tests { assert_eq!(opts.command, config.command.unwrap()); assert_eq!(opts.accessible, config.accessible.unwrap()); assert_eq!(opts.scan_order, config.scan_order.unwrap()); + assert_eq!(opts.scripts, ScriptsRequired::Default) } #[test] diff --git a/src/main.rs b/src/main.rs index fca733145..25f838235 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ extern crate shell_words; mod tui; mod input; -use input::{Config, Opts, PortRange, ScanOrder}; +use input::{Config, Opts, PortRange, ScanOrder, ScriptsRequired}; mod scanner; use scanner::Scanner; @@ -14,6 +14,9 @@ use port_strategy::PortStrategy; mod benchmark; use benchmark::{Benchmark, NamedTimer}; +mod scripts; +use scripts::{init_scripts, Script, ScriptFile}; + use cidr_utils::cidr::IpCidr; use colorful::{Color, Colorful}; use futures::executor::block_on; @@ -23,7 +26,6 @@ use std::fs::File; use std::io::{prelude::*, BufReader}; use std::net::{IpAddr, ToSocketAddrs}; use std::path::Path; -use std::process::Command; use std::time::Duration; use trust_dns_resolver::{config::*, Resolver}; @@ -52,6 +54,10 @@ fn main() { debug!("Main() `opts` arguments are {:?}", opts); + let scripts_to_run: Vec = + init_scripts(opts.scripts).expect("could not initiate scripting part."); + debug!("Scripts initialized {:?}", &scripts_to_run); + if !opts.greppable && !opts.accessible { print_opening(&opts); } @@ -112,46 +118,71 @@ fn main() { warning!(x, opts.greppable, opts.accessible); } - let mut nmap_bench = NamedTimer::start("Nmap"); + let mut script_bench = NamedTimer::start("Scripts"); for (ip, ports) in ports_per_ip.iter_mut() { - let nmap_str_ports: Vec = ports.into_iter().map(|port| port.to_string()).collect(); + let vec_str_ports: Vec = ports.into_iter().map(|port| port.to_string()).collect(); // nmap port style is 80,443. Comma separated with no spaces. - let ports_str = nmap_str_ports.join(","); + let ports_str = vec_str_ports.join(","); - // if greppable mode is on nmap should not be spawned - if opts.greppable || opts.no_nmap { + // if option scripts is none, no script will be spawned + if opts.greppable || opts.scripts.clone() == ScriptsRequired::None { println!("{} -> [{}]", &ip, ports_str); continue; } - detail!("Starting Nmap", opts.greppable, opts.accessible); - - let addr = ip.to_string(); - let user_nmap_args = - shell_words::split(&opts.command.join(" ")).expect("failed to parse nmap arguments"); - let nmap_args = build_nmap_arguments(&addr, &ports_str, &user_nmap_args, ip.is_ipv6()); - - output!( - format!( - "The Nmap command to be run is nmap {}\n", - &nmap_args.join(" ") - ), - opts.greppable.clone(), - opts.accessible.clone() - ); + detail!("Starting Script(s)", opts.greppable, opts.accessible); + + // Run all the scripts we found and parsed based on the script config file tags field. + for mut script_f in scripts_to_run.clone() { + output!( + format!("Script to be run {:?}\n", script_f.call_format,), + opts.greppable.clone(), + opts.accessible.clone() + ); - // Runs the nmap command and spawns it as a process. - let mut child = Command::new("nmap") - .args(&nmap_args) - .spawn() - .expect("failed to execute nmap process"); + // This part allows us to add commandline arguments to the Script call_format, appending them to the end of the command. + if opts.command.len() > 0 { + let user_extra_args: Vec = shell_words::split(&opts.command.join(" ")) + .expect("Failed to parse extra user commandline arguments"); + if script_f.call_format.is_some() { + let mut call_f = script_f.call_format.unwrap(); + call_f.push_str(&format!(" {}", &user_extra_args.join(" "))); + script_f.call_format = Some(call_f); + } + } - child.wait().expect("failed to wait on nmap process"); + // Building the script with the arguments from the ScriptFile, and ip-ports. + let script = Script::build( + script_f.path, + *ip, + ports.to_vec(), + script_f.port, + script_f.ports_separator, + script_f.tags, + script_f.call_format, + ); + match script.run() { + Ok(script_result) => { + detail!( + format!("{}", script_result), + opts.greppable, + opts.accessible + ); + } + Err(e) => { + warning!( + &format!("Error {}", e.to_string()), + opts.greppable, + opts.accessible + ); + } + } + } } // To use the runtime benchmark, run the process as: RUST_LOG=info ./rustscan - nmap_bench.end(); - benchmarks.push(nmap_bench); + script_bench.end(); + benchmarks.push(script_bench); rustscan_bench.end(); benchmarks.push(rustscan_bench); debug!("Benchmarks raw {:?}", benchmarks); @@ -165,7 +196,7 @@ fn print_opening(opts: &Opts) { | {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| | | .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ | `-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-' -Faster Nmap scanning with Rust."#; +The Modern Day Port Scanner."#; println!("{}", s.gradient(Color::Green).bold()); let info = r#"________________________________________ : https://discord.gg/GFrQsGy : @@ -298,27 +329,6 @@ fn read_ips_from_file( Ok(ips) } -#[cfg(not(tarpaulin_include))] -fn build_nmap_arguments<'a>( - addr: &'a str, - ports: &'a str, - user_args: &'a Vec, - is_ipv6: bool, -) -> Vec<&'a str> { - let mut arguments: Vec<&str> = user_args.iter().map(AsRef::as_ref).collect(); - arguments.push("-vvv"); - - if is_ipv6 { - arguments.push("-6"); - } - - arguments.push("-p"); - arguments.push(ports); - arguments.push(addr); - - arguments -} - fn adjust_ulimit_size(opts: &Opts) -> RawRlim { if opts.ulimit.is_some() { let limit: Rlim = Rlim::from_raw(opts.ulimit.unwrap()); diff --git a/src/scanner/mod.rs b/src/scanner/mod.rs index 9d901cb11..986143391 100644 --- a/src/scanner/mod.rs +++ b/src/scanner/mod.rs @@ -106,11 +106,7 @@ impl Scanner { async fn scan_socket(&self, socket: SocketAddr) -> io::Result { let tries = self.tries.get(); - debug!("self.tries: {}", tries); - for nr_try in 1..=tries { - debug!("Try number: {}", nr_try); - match self.connect(socket).await { Ok(x) => { debug!( diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs new file mode 100644 index 000000000..7a6d6cddf --- /dev/null +++ b/src/scripts/mod.rs @@ -0,0 +1,355 @@ +//! Scripting engine to run scripts based on tags. +//! This module serves to filter and run the scripts selected by the user. +//! +//! A new commandline and configuration file option was added. +//! +//! --scripts +//! +//! default +//! This is the default behavior, like as it was from the beginning of RustScan. +//! The user do not have to chose anything for this. This is the only script embedded in RustScan running as default. +//! +//! none +//! The user have to use the --scripts none commandline argument or scripts = "none" in the config file. +//! None of the scripts will run, this replaces the removed --no-nmap option. +//! +//! custom +//! The user have to use the --scripts custom commandline argument or scripts = "custom" in the config file. +//! Rustscan will look for the script configuration file in the user's home dir: home_dir/.rustscan_scripts.toml +//! The config file have 3 optional fields, tag, developer and port. Just the tag field will be used forther in the process. +//! RustScan will also look for available scripts in the user's home dir: home_dir/.rustscan_scripts +//! and will try to read all the files, and parse them into a vector of ScriptFiles. +//! Filtering on tags means the tags found in the rustscan_scripts.toml file will also have to be present in the Scriptfile, +//! otherwise the script will not be selected. +//! All of the rustscan_script.toml tags have to be present at minimum in a Scriptfile to get selected, but can be also more. +//! +//! Config file example: +//! fixtures/test_rustscan_scripts.toml +//! +//! Script file examples: +//! fixtures/test_script.py +//! fixtures/test_script.pl +//! fixtures/test_script.sh +//! fixtures/test_script.txt +//! +//! call_format in script files can be of 2 variants. +//! One is where all of the possible tags {{script}} {{ip}} {{port}} are there. +//! The {{script}} part will be replaced with the scriptfile full path gathered while parsing available scripts. +//! The {{ip}} part will be replaced with the ip we got from the scan. +//! The {{port}} part will be reaplced with the ports separated with the ports_separator found in the script file +//! +//! And when there is only {{ip}} and {{port}} is in the format, ony those will be replaced with the arguments from the scan. +//! This makes it easy to run a system installed command like nmap, and give any kind of arguments to it. +//! +//! If the format is different, the script will be silently discarded and will not run. With the Debug option it's possible to see where it goes wrong. + +use crate::input::ScriptsRequired; +use anyhow::{anyhow, Result}; +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::fs::{self, File}; +use std::io::{self, prelude::*}; +use std::net::IpAddr; +use std::path::PathBuf; +use subprocess::{Exec, ExitStatus}; +use text_placeholder::Template; + +static DEFAULT: &'static str = r#"tags = ["core_approved", "RustScan", "default"] +developer = [ "RustScan", "https://github.com/RustScan" ] +ports_separator = "," +call_format = "nmap -vvv -p {{port}} {{ip}}" +"#; + +pub fn init_scripts(scripts: ScriptsRequired) -> Result> { + let mut scripts_to_run: Vec = Vec::new(); + + match scripts { + ScriptsRequired::None => Ok(scripts_to_run), + ScriptsRequired::Default => { + let default_script = + toml::from_str::(&DEFAULT).expect("Failed to parse Script file."); + scripts_to_run.push(default_script); + Ok(scripts_to_run) + } + ScriptsRequired::Custom => { + let scripts_dir_base = match dirs::home_dir() { + Some(dir) => dir, + None => return Err(anyhow!("Could not infer scripts path.")), + }; + let script_paths = match find_scripts(scripts_dir_base) { + Ok(script_paths) => script_paths, + Err(e) => return Err(anyhow!(e)), + }; + debug!("Scripts paths \n{:?}", script_paths); + + let parsed_scripts = parse_scripts(script_paths); + debug!("Scripts parsed \n{:?}", parsed_scripts); + + let script_config = match ScriptConfig::read_config() { + Ok(script_config) => script_config, + Err(e) => return Err(anyhow!(e)), + }; + debug!("Script config \n{:?}", script_config); + + // Only Scripts that contain all the tags found in ScriptConfig will be selected. + if script_config.tags.is_some() { + let config_hashset: HashSet = + script_config.tags.unwrap().into_iter().collect(); + for script in &parsed_scripts { + if script.tags.is_some() { + let script_hashset: HashSet = + script.tags.clone().unwrap().into_iter().collect(); + if config_hashset.is_subset(&script_hashset) { + scripts_to_run.push(script.to_owned()); + } else { + debug!( + "\nScript tags does not match config tags {:?} {}", + &script_hashset, + script.path.clone().unwrap().display() + ); + } + } + } + } + debug!("\nScript(s) to run {:?}", scripts_to_run); + Ok(scripts_to_run) + } + } +} + +pub fn parse_scripts(scripts: Vec) -> Vec { + let mut parsed_scripts: Vec = Vec::with_capacity(scripts.len()); + for script in scripts { + debug!("Parsing script {}", &script.display()); + if let Some(script_file) = ScriptFile::new(script) { + parsed_scripts.push(script_file); + } + } + parsed_scripts +} + +#[derive(Clone, Debug)] +pub struct Script { + // Path to the script itself. + path: Option, + + // Ip got from scanner. + ip: IpAddr, + + // Ports found with portscan. + open_ports: Vec, + + // Port found in ScriptFile, if defined only this will run with the ip. + trigger_port: Option, + + // Character to join ports in case we want to use a string format of them, for example nmap -p. + ports_separator: Option, + + // Tags found in ScriptFile. + tags: Option>, + + // The format how we want the script to run. + call_format: Option, +} + +#[derive(Serialize)] +struct ExecPartsScript { + script: String, + ip: String, + port: String, +} + +#[derive(Serialize)] +struct ExecParts { + ip: String, + port: String, +} + +impl Script { + pub fn build( + path: Option, + ip: IpAddr, + open_ports: Vec, + trigger_port: Option, + ports_separator: Option, + tags: Option>, + call_format: Option, + ) -> Self { + Self { + path: path, + ip: ip, + open_ports: open_ports, + trigger_port: trigger_port, + ports_separator: ports_separator, + tags: tags, + call_format: call_format, + } + } + + // Some variables get changed before read, and compiler throws warning on warn(unused_assignments) + #[allow(unused_assignments)] + pub fn run(self) -> Result { + debug!("run self {:?}", &self); + + let separator = self.ports_separator.unwrap_or(",".into()); + + let mut ports_str = self + .open_ports + .iter() + .map(|port| port.to_string()) + .collect::>() + .join(&separator); + if let Some(port) = self.trigger_port { + ports_str = port; + } + + let mut final_call_format = String::new(); + if let Some(call_format) = self.call_format { + final_call_format = call_format; + } else { + return Err(anyhow!("Failed to parse execution format.")); + } + let default_template: Template = Template::new(&final_call_format); + let mut to_run = String::new(); + + if final_call_format.contains("{{script}}") { + let exec_parts_script: ExecPartsScript = ExecPartsScript { + script: self.path.unwrap().to_str().unwrap().to_string(), + ip: self.ip.to_string(), + port: ports_str, + }; + to_run = default_template.fill_with_struct(&exec_parts_script)?; + } else { + let exec_parts: ExecParts = ExecParts { + ip: self.ip.to_string(), + port: ports_str, + }; + to_run = default_template.fill_with_struct(&exec_parts)?; + } + + debug!("\nTo run {}", to_run); + + let arguments = shell_words::split( + &to_run + .split(" ") + .map(|arg| arg.to_string()) + .collect::>() + .join(" "), + ) + .expect("Failed to parse script arguments"); + + match execute_script(arguments) { + Ok(result) => return Ok(result), + Err(e) => return Err(e), + } + } +} + +#[cfg(not(tarpaulin_include))] +fn execute_script(mut arguments: Vec) -> Result { + debug!("\nArguments vec: {:?}", &arguments); + let process = Exec::cmd(&arguments.remove(0)).args(&arguments); + match process.capture() { + Ok(c) => { + let es = match c.exit_status { + ExitStatus::Exited(c) => c as i32, + ExitStatus::Signaled(c) => c as i32, + ExitStatus::Other(c) => c, + _ => -1, + }; + if es != 0 { + return Err(anyhow!("Exit code = {}", es)); + } + Ok(c.stdout_str()) + } + Err(error) => { + debug!("Command error {}", error.to_string()); + return Err(anyhow!(error.to_string())); + } + } +} + +pub fn find_scripts(mut path: PathBuf) -> Result> { + path.push(".rustscan_scripts"); + if path.is_dir() { + debug!("Scripts folder found {}", &path.display()); + let mut files_vec: Vec = Vec::new(); + for entry in fs::read_dir(path)? { + let entry = entry?; + files_vec.push(entry.path()); + } + return Ok(files_vec); + } else { + return Err(anyhow!("Can't find scripts folder")); + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ScriptFile { + pub path: Option, + pub tags: Option>, + pub developer: Option>, + pub port: Option, + pub ports_separator: Option, + pub call_format: Option, +} + +impl ScriptFile { + fn new(script: PathBuf) -> Option { + let real_path = script.clone(); + let mut lines_buf = String::new(); + if let Ok(file) = File::open(script) { + for line in io::BufReader::new(file).lines().skip(1) { + if let Ok(mut line) = line { + if line.starts_with("#") { + line.retain(|c| c != '#'); + line = line.trim().to_string(); + line.push_str("\n"); + lines_buf.push_str(&line); + } else { + break; + } + } + } + } else { + debug!("Failed to read file: {}", &real_path.display()); + return None; + } + debug!("ScriptFile {} lines\n{}", &real_path.display(), &lines_buf); + + match toml::from_str::(&lines_buf) { + Ok(mut parsed) => { + debug!("Parsed ScriptFile{} \n{:?}", &real_path.display(), &parsed); + parsed.path = Some(real_path); + // parsed_scripts.push(parsed); + return Some(parsed); + } + Err(e) => { + debug!("Failed to parse ScriptFile headers {}", e.to_string()); + return None; + } + } + } +} + +#[derive(Debug, Deserialize, Clone)] +pub struct ScriptConfig { + pub tags: Option>, + pub ports: Option>, + pub developer: Option>, +} + +#[cfg(not(tarpaulin_include))] +impl ScriptConfig { + pub fn read_config() -> Result { + let mut home_dir = match dirs::home_dir() { + Some(dir) => dir, + None => return Err(anyhow!("Could not infer ScriptConfig path.")), + }; + home_dir.push(".rustscan_scripts.toml"); + + let content = fs::read_to_string(home_dir)?; + let config = toml::from_str::(&content)?; + Ok(config) + } +}