Initial commit.

This commit is contained in:
Eduard Tolosa 2020-09-02 22:31:23 -05:00
parent 7c308a7d74
commit 43b878e0d1
19 changed files with 4892 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
*.txt

19
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,19 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug",
"program": "${workspaceRoot}/target/debug/${workspaceRootFolderName}",
"args": "-f targets.txt -u log.csv --resolvers resolvers.txt --iport 1 --lport 10000 --min-rate 10",
"cwd": "${workspaceRoot}",
"sourceLanguages": [
"rust"
]
}
]
}

15
.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,15 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "shell",
"command": "cargo build",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": []
}
]
}

1844
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

37
Cargo.toml Normal file
View file

@ -0,0 +1,37 @@
[package]
name = "unimap"
version = "0.1.0"
authors = ["Eduard Tolosa <edu4rdshl@protonmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
trust-dns-resolver = "0.19.5"
rayon = "1.4.0"
log = { version = "0.4.11", features = ["std"] }
colored = { version = "1.9.3", optional = true }
rand = "0.7.3"
lazy_static = "1.4.0"
serde = { version = "1.0.114", features = ["derive"] }
serde_derive = "1.0.114"
prettytable-rs = "0.8.0"
serde-xml-rs = "0.4.0"
webpage = "1.1.0"
chrono = "0.4.15"
clap = { version = "2.33.1", features = ["yaml"] }
config = { version = "0.10.1", features = ["yaml", "json", "toml", "hjson", "ini"] }
failure = "0.1.8"
[target.arm-unknown-linux-gnueabihf.dependencies]
openssl = { version = "0.10.30", features = ["vendored"] }
[target.aarch64-unknown-linux-gnu.dependencies]
openssl = { version = "0.10.30", features = ["vendored"] }
[target.armv7-unknown-linux-gnueabihf.dependencies]
openssl = { version = "0.10.30", features = ["vendored"] }
[target.'cfg(windows)'.dependencies]
atty = "0.2.14"
winapi = { version = "0.3.9", features = ["handleapi", "winbase"] }

72
src/args.rs Normal file
View file

@ -0,0 +1,72 @@
use {
crate::{
defaults,
logic::validate_target,
misc::{return_matches_vec, sanitize_target_string},
structs::Args,
},
chrono::Utc,
clap::{load_yaml, value_t, App},
std::{collections::HashSet, time::Instant},
};
#[allow(clippy::cognitive_complexity)]
pub fn get_args() -> Args {
let yaml = load_yaml!("cli.yml");
let matches = App::from_yaml(yaml)
.version(clap::crate_version!())
.get_matches();
Args {
target: {
let target = sanitize_target_string(
value_t!(matches, "target", String).unwrap_or_else(|_| String::new()),
);
if validate_target(&target) {
target
} else {
String::new()
}
},
file_name: if matches.is_present("output") {
value_t!(matches, "logs-dir", String).unwrap_or_else(|_| "logs".to_string())
+ "/"
+ "unimap"
+ &Utc::now().format("-log-%Y-%m-%d_%H-%M-%S").to_string()
+ ".csv"
} else if matches.is_present("unique-output") {
matches.value_of("unique-output").unwrap().to_string()
} else {
String::new()
},
logs_dir: value_t!(matches, "logs-dir", String).unwrap_or_else(|_| "logs".to_string()),
threads: if (matches.is_present("port-scan")
|| matches.is_present("initial-port")
|| matches.is_present("last-port"))
&& !matches.is_present("threads")
{
30
} else {
value_t!(matches, "threads", usize).unwrap_or_else(|_| 50)
},
version: clap::crate_version!().to_string(),
initial_port: value_t!(matches, "initial-port", usize).unwrap_or_else(|_| 1),
last_port: value_t!(matches, "last-port", usize).unwrap_or_else(|_| 65535),
with_output: matches.is_present("output") || matches.is_present("unique-output"),
unique_output_flag: matches.is_present("unique-output"),
from_file_flag: matches.is_present("files"),
quiet_flag: matches.is_present("quiet"),
custom_resolvers: matches.is_present("custom-resolvers"),
custom_ports_range: matches.is_present("initial-port") || matches.is_present("last-port"),
fast_scan: matches.is_present("fast-scan"),
files: return_matches_vec(&matches, "files"),
min_rate: value_t!(matches, "min-rate", usize).unwrap_or_else(|_| 30000),
resolvers: if matches.is_present("custom-resolvers") {
return_matches_vec(&matches, "custom-resolvers")
} else {
defaults::ipv4_resolvers()
},
targets: HashSet::new(),
time_wasted: Instant::now(),
}
}

84
src/cli.yml Normal file
View file

@ -0,0 +1,84 @@
name: Unimap
author: Eduard Tolosa <edu4rdshl@protonmail.com>
settings:
- ArgRequiredElseHelp
- StrictUtf8
about: Scan only once by IP address.
args:
- target:
short: t
long: target
help: Target host.
takes_value: true
multiple: false
conflicts_with:
- files
- files:
short: f
long: file
help: Use a list of targets writen in a file as input.
takes_value: true
multiple: true
conflicts_with:
- target
- output:
short: o
long: output
help: Write to an output file. The name of the output file will be the target string with TXT format.
takes_value: false
- unique-output:
short: u
long: unique-output
help: Write all the results for a target or a list of targets to a specified filename.
takes_value: true
multiple: false
conflicts_with:
- output
- quiet:
short: q
long: quiet
help: Remove informative messages.
takes_value: false
- threads:
help: Number of threads to use to perform the resolution.
long: threads
takes_value: true
- custom-resolvers:
help: Path to a file (or files) containing a list of DNS IP address. If no specified then 1.6k of built-in valid DNS servers from public-dns.info are used.
long: resolvers
takes_value: true
multiple: true
- initial-port:
help: Initial port to scan. Default 0.
long: iport
takes_value: true
multiple: false
- last-port:
help: Last port to scan. Default 1000.
long: lport
takes_value: true
multiple: false
- min-rate:
help: Nmap --min-rate value for ports scan.
long: min-rate
takes_value: true
- fast-scan:
help: Use fast scanning for ports (no version detection).
long: fast-scan
takes_value: false
- logs-dir:
help: Path to save the subdomains HTML data.
long: logs-dir
takes_value: true
multiple: false

1957
src/defaults.rs Normal file

File diff suppressed because it is too large Load diff

2
src/errors.rs Normal file
View file

@ -0,0 +1,2 @@
pub use failure::{Error, ResultExt};
pub type Result<T> = ::std::result::Result<T, Error>;

81
src/files.rs Normal file
View file

@ -0,0 +1,81 @@
use {
crate::{errors::*, structs::Args},
log::error,
prettytable::Table,
std::{
collections::HashSet,
fs::{self, File, OpenOptions},
io::{BufRead, BufReader, Write},
path::Path,
},
};
pub fn return_file_targets(args: &Args, mut files: Vec<String>) -> Vec<String> {
let mut targets: Vec<String> = Vec::new();
files.sort();
files.dedup();
for f in files {
match File::open(&f) {
Ok(file) => {
for target in BufReader::new(file).lines().flatten() {
targets.push(target);
}
}
Err(e) => {
if args.files.len() == 1 {
error!("Can not open file {}. Error: {}\n", f, e);
std::process::exit(1)
} else if !args.quiet_flag {
error!(
"Can not open file {}, working with next file. Error: {}\n",
f, e
);
}
}
}
}
targets.sort();
targets.dedup();
targets.iter().map(|target| target.to_lowercase()).collect()
}
pub fn table_to_file(table: &Table, file_name: Option<std::fs::File>) -> Result<()> {
table.to_csv(file_name.unwrap())?;
Ok(())
}
pub fn return_output_file(args: &Args) -> Option<File> {
if args.file_name.is_empty() || !args.with_output {
None
} else {
Some(
OpenOptions::new()
.append(true)
.create(true)
.open(&args.file_name)
.with_context(|_| format!("Can't create file {}", &args.file_name))
.unwrap(),
)
}
}
pub fn check_full_path(full_path: &str) -> bool {
(Path::new(full_path).exists() && Path::new(full_path).is_dir())
|| fs::create_dir_all(full_path).is_ok()
}
pub fn delete_files(paths: &HashSet<String>) {
for file in paths {
if Path::new(&file).exists() {
match std::fs::remove_file(&file) {
Ok(_) => (),
Err(e) => error!("Error deleting the file {}. Description: {}", &file, e),
}
}
}
}
pub fn string_to_file(data: String, mut file: File) -> Result<()> {
file.write_all(data.as_bytes())?;
Ok(())
}

21
src/lib.rs Normal file
View file

@ -0,0 +1,21 @@
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate prettytable;
extern crate log;
pub mod args;
pub mod errors;
pub mod files;
pub mod logger;
pub mod resolver_engine;
mod defaults;
mod logic;
mod misc;
mod networking;
mod nmap;
mod structs;

121
src/logger.rs Normal file
View file

@ -0,0 +1,121 @@
#[cfg(windows)]
extern crate atty;
#[cfg(feature = "chrono")]
extern crate chrono;
#[cfg(feature = "colored")]
extern crate colored;
extern crate log;
#[cfg(windows)]
extern crate winapi;
#[cfg(feature = "chrono")]
use chrono::Local;
#[cfg(feature = "colored")]
use colored::*;
use log::{Level, Log, Metadata, Record, SetLoggerError};
struct SimpleLogger {
level: Level,
}
impl Log for SimpleLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= self.level
}
fn log(&self, record: &Record) {
let target = if !record.target().is_empty() {
record.target()
} else {
record.module_path().unwrap_or_default()
};
if target.contains("unimap") && self.enabled(record.metadata()) {
let level_string = {
#[cfg(feature = "colored")]
{
match record.level() {
Level::Error => record.level().to_string().red(),
Level::Warn => record.level().to_string().yellow(),
Level::Info => record.level().to_string().cyan(),
Level::Debug => record.level().to_string().purple(),
Level::Trace => record.level().to_string().normal(),
}
}
#[cfg(not(feature = "colored"))]
{
record.level().to_string()
}
};
#[cfg(feature = "chrono")]
{
print!(
"\n{} [{}] {}",
Local::now().format("%Y-%m-%d %H:%M:%S,%3f"),
level_string,
record.args()
);
}
#[cfg(not(feature = "chrono"))]
{
print!("\n[{}] {}", level_string, record.args());
}
}
}
fn flush(&self) {}
}
#[cfg(windows)]
fn set_up_color_terminal() {
use atty::Stream;
if atty::is(Stream::Stdout) {
unsafe {
use winapi::um::consoleapi::*;
use winapi::um::handleapi::*;
use winapi::um::processenv::*;
use winapi::um::winbase::*;
use winapi::um::wincon::*;
let stdout = GetStdHandle(STD_OUTPUT_HANDLE);
if stdout == INVALID_HANDLE_VALUE {
return;
}
let mut mode: winapi::shared::minwindef::DWORD = 0;
if GetConsoleMode(stdout, &mut mode) == 0 {
return;
}
SetConsoleMode(stdout, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
}
}
}
pub fn init_with_level(level: Level) -> Result<(), SetLoggerError> {
#[cfg(all(windows, feature = "colored"))]
set_up_color_terminal();
let logger = SimpleLogger { level };
log::set_boxed_logger(Box::new(logger))?;
log::set_max_level(level.to_level_filter());
Ok(())
}
pub fn init() -> Result<(), SetLoggerError> {
init_with_level(Level::Trace)
}
pub fn init_by_env() {
match std::env::var("UNIMAP_LOG_LEVEL") {
Ok(x) => match x.to_lowercase().as_str() {
"trace" => init_with_level(log::Level::Trace).unwrap(),
"debug" => init_with_level(log::Level::Debug).unwrap(),
"info" => init_with_level(log::Level::Info).unwrap(),
"warn" => init_with_level(log::Level::Warn).unwrap(),
_ => init_with_level(log::Level::Error).unwrap(),
},
_ => init_with_level(log::Level::Error).unwrap(),
}
}

31
src/logic.rs Normal file
View file

@ -0,0 +1,31 @@
use lazy_static;
lazy_static! {
static ref SPECIAL_CHARS: Vec<char> = vec![
'[', ']', '{', '}', '(', ')', '*', '|', ':', '<', '>', '/', '\\', '%', '&', '¿', '?', '¡',
'!', '#', '\'', ' ', ','
];
}
pub fn validate_target(target: &str) -> bool {
!target.starts_with('.')
&& target.contains('.')
&& !target.contains(&SPECIAL_CHARS[..])
&& target.chars().all(|c| c.is_ascii())
}
pub fn null_ip_checker(ip: &str) -> String {
if ip.is_empty() {
String::from("NULL")
} else {
ip.to_string()
}
}
pub fn return_ports_string(ports: &Vec<String>) -> String {
if ports.is_empty() {
String::from("NULL")
} else {
ports.join(";")
}
}

46
src/main.rs Normal file
View file

@ -0,0 +1,46 @@
use {
log::{error, Level},
std::{collections::HashSet, iter::FromIterator},
unimap::{args, errors::*, files::return_file_targets, logger, resolver_engine},
};
fn run() -> Result<()> {
if std::env::var("UNIMAP_LOG_LEVEL").is_ok() {
logger::init_by_env()
} else {
logger::init_with_level(Level::Info).unwrap()
}
let mut arguments = args::get_args();
if !arguments.files.is_empty() {
arguments.targets =
HashSet::from_iter(return_file_targets(&arguments, arguments.files.clone()))
} else {
arguments.targets.insert(arguments.target.clone());
}
if arguments.targets.len() < 50 {
arguments.threads = arguments.targets.len()
}
rayon::ThreadPoolBuilder::new()
.num_threads(arguments.threads)
.build_global()
.unwrap();
if !arguments.target.is_empty() || !arguments.files.is_empty() {
resolver_engine::async_resolver_all(&mut arguments)
} else {
error!("Error: Target is empty or invalid!\n");
std::process::exit(1)
}
}
fn main() {
if let Err(err) = run() {
error!("Error: {}", err);
for cause in err.iter_chain().skip(1) {
error!("Error description: {}\n", cause);
}
std::process::exit(1);
}
}

34
src/misc.rs Normal file
View file

@ -0,0 +1,34 @@
use std::collections::HashSet;
pub fn sanitize_target_string(target: String) -> String {
target
.replace("www.", "")
.replace("https://", "")
.replace("http://", "")
.replace("/", "")
}
pub fn return_matches_vec(matches: &clap::ArgMatches, value: &str) -> Vec<String> {
if matches.is_present(value) {
matches
.values_of(value)
.unwrap()
.map(str::to_owned)
.collect()
} else {
Vec::new()
}
}
#[allow(dead_code)]
pub fn return_matches_hashset(matches: &clap::ArgMatches, value: &str) -> HashSet<String> {
if matches.is_present(value) {
matches
.values_of(value)
.unwrap()
.map(str::to_owned)
.collect()
} else {
HashSet::new()
}
}

68
src/networking.rs Normal file
View file

@ -0,0 +1,68 @@
use {
log::error,
rand::Rng,
std::net::{IpAddr, Ipv4Addr},
trust_dns_resolver::{
config::{NameServerConfigGroup, ResolverConfig, ResolverOpts},
proto::rr::RecordType,
Resolver,
},
};
pub fn get_records(resolver: &Resolver, domain: &str, record_type: RecordType) -> String {
if let Ok(rdata) = resolver.lookup(&domain, record_type) {
let mut record_data: Vec<String> = Vec::new();
if record_type == RecordType::AAAA {
record_data = rdata
.iter()
.filter_map(|rdata| rdata.as_aaaa())
.map(|ipv6| ipv6.to_string())
.collect();
} else if record_type == RecordType::A {
record_data = rdata
.iter()
.filter_map(|rdata| rdata.as_a())
.map(|ipv4| ipv4.to_string())
.collect();
} else if record_type == RecordType::CNAME {
record_data = rdata
.iter()
.filter_map(|rdata| rdata.as_cname())
.map(|name| {
let name = name.to_string();
name[..name.len() - 1].to_owned()
})
.collect();
}
record_data
.iter()
.next()
.expect("Failed retrieving records data.")
.to_owned()
} else {
String::new()
}
}
pub fn get_resolver(resolvers_ips: &[Ipv4Addr], opts: &ResolverOpts) -> Resolver {
match Resolver::new(
ResolverConfig::from_parts(
None,
vec![],
NameServerConfigGroup::from_ips_clear(
&[IpAddr::V4(
resolvers_ips[rand::thread_rng().gen_range(0, resolvers_ips.len())],
)],
53,
),
),
*opts,
) {
Ok(resolver) => resolver,
Err(e) => {
error!("Failed to create the resolver. Error: {}\n", e);
std::process::exit(1)
}
}
}

183
src/nmap.rs Normal file
View file

@ -0,0 +1,183 @@
use {
crate::resolver_engine,
log::error,
std::{path::Path, process::Command},
};
lazy_static! {
static ref NMAP_DNS_RESOLVERS: String = resolver_engine::RESOLVERS
.clone()
.iter()
.map(|f| f.to_string())
.collect::<Vec<String>>()
.join(",");
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Root {
pub nmaprun: Nmaprun,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Nmaprun {
pub scanner: String,
pub args: String,
pub start: String,
pub startstr: String,
pub version: String,
pub xmloutputversion: String,
pub host: Option<Host>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Host {
pub starttime: String,
pub endtime: String,
pub status: Status,
pub address: Address,
pub hostnames: Hostnames,
pub ports: Option<Ports>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Status {
pub state: String,
pub reason: String,
#[serde(rename = "reason_ttl")]
pub reason_ttl: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Address {
pub addr: Option<String>,
pub addrtype: Option<String>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Hostnames {
pub hostname: Option<Hostname>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Hostname {
pub name: String,
#[serde(rename = "type")]
pub type_field: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Ports {
pub port: Vec<Port>,
}
#[derive(Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Hash)]
#[serde(rename_all = "camelCase")]
pub struct Port {
pub protocol: String,
pub portid: String,
pub state: State,
pub service: Option<Service>,
}
#[derive(Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Hash)]
#[serde(rename_all = "camelCase")]
pub struct State {
pub state: String,
pub reason: String,
#[serde(rename = "reason_ttl")]
pub reason_ttl: String,
}
#[derive(Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Hash)]
#[serde(rename_all = "camelCase")]
pub struct Service {
pub name: String,
pub method: String,
pub conf: String,
pub product: Option<String>,
pub ostype: Option<String>,
pub version: Option<String>,
pub extrainfo: Option<String>,
}
pub fn get_nmap_data(
filename: &str,
host: &str,
min_rate: usize,
initial_port: usize,
last_port: usize,
fast_scan: bool,
) -> Result<Nmaprun, serde_xml_rs::Error> {
let _ports_range = format!("{}-{}", initial_port, last_port);
let min_rate = min_rate.to_string();
let nmap_args = if fast_scan {
vec![
"nmap",
"-n",
"--dns-servers",
&NMAP_DNS_RESOLVERS,
"-Pn",
"--host-timeout",
"5m",
"--min-rate",
&min_rate,
"-sS",
"--open",
"-dd",
"-T4",
"--max-retries",
"2",
"-oX",
filename,
host,
]
} else {
vec![
"nmap",
"-n",
"--dns-servers",
&NMAP_DNS_RESOLVERS,
"-Pn",
"--host-timeout",
"10m",
"-sV",
"--min-rate",
&min_rate,
"-sS",
"-p-",
"--open",
"-dd",
"-T4",
"--max-retries",
"2",
"-oX",
filename,
host,
]
};
match Command::new("sudo").args(&nmap_args).output() {
Ok(_) => {
if Path::new(&filename).exists() && Path::new(&filename).is_file() {
serde_xml_rs::from_str(&std::fs::read_to_string(filename).unwrap_or_default())
} else {
error!("Error executing nmap, you need root permissions. Leaving.\n");
std::process::exit(1)
}
}
Err(e) => {
error!(
"Error waiting command to finish for {}, continuing with remaining hosts. Description: {}",
host, e
);
Ok(Nmaprun::default())
}
}
}

234
src/resolver_engine.rs Normal file
View file

@ -0,0 +1,234 @@
use {
crate::{
args,
errors::*,
files, logic, networking,
nmap::{self, Nmaprun},
structs::{Args, ResolvData},
},
log::{error, info},
prettytable,
prettytable::Table,
rayon::prelude::*,
std::{
collections::{HashMap, HashSet},
net::Ipv4Addr,
time::Duration,
},
trust_dns_resolver::{config::ResolverOpts, proto::rr::RecordType},
};
lazy_static! {
pub static ref RESOLVERS: Vec<Ipv4Addr> = {
let args = args::get_args();
let mut resolver_ips = Vec::new();
if args.custom_resolvers {
for r in &files::return_file_targets(&args, args.resolvers.clone()) {
match r.parse::<Ipv4Addr>() {
Ok(ip) => resolver_ips.push(ip),
Err(e) => {
error!("Error parsing the {} IP from resolvers file to IP address. Please check and try again. Error: {}\n", r, e);
std::process::exit(1)
}
}
}
} else {
for r in args.resolvers {
match r.parse::<Ipv4Addr>() {
Ok(ip) => resolver_ips.push(ip),
Err(e) => {
error!("Error parsing the {} IP from resolvers file to IP address. Please check and try again. Error: {}\n", r, e);
std::process::exit(1)
}
}
}
}
resolver_ips
};
}
pub fn async_resolver_all(args: &mut Args) -> Result<()> {
if args.with_output && !args.unique_output_flag && !files::check_full_path(&args.logs_dir) {
error!(
"Error creating the logs folder in the path {}. Leaving...",
&args.logs_dir
);
std::process::exit(1)
}
if !args.quiet_flag {
info!(
"Performing asynchronous resolution for {} targets with {} threads, it will take a while...\n",
args.targets.len(), args.threads
)
}
let data = async_resolver_engine(&args, args.targets.clone());
let mut table = Table::new();
table.set_titles(row![
bcFg => "HOST",
"IP",
"OPEN PORTS",
"SERVICES"
]);
for (target, resolv_data) in &data {
if !resolv_data.ip.is_empty() {
let mut services_table = Table::new();
for port_data in &resolv_data.ports_data {
services_table
.add_row(row![bc => &format!("PORT => {}", port_data.portid.clone())]);
services_table.add_row(
row![c => &format!("SERVICE: {}", port_data.service.clone().unwrap_or_default().name)],
);
services_table.add_row(row![c => &format!("VERSION: {}" ,port_data
.service.clone().unwrap_or_default()
.version
.clone()
.unwrap_or_else(|| "NULL".to_string()))]);
services_table.add_row(row![c => &format!("PRODUCT: {}", port_data
.service.clone().unwrap_or_default()
.product
.clone()
.unwrap_or_else(|| "NULL".to_string()))]);
services_table.add_row(row![c => &format!("OS TYPE: {}", port_data
.service.clone().unwrap_or_default()
.ostype
.clone()
.unwrap_or_else(|| "NULL".to_string()))]);
services_table.add_row(row![c => &format!("EXTRA INFO: {}", port_data
.service.clone().unwrap_or_default()
.extrainfo
.clone()
.unwrap_or_else(|| "NULL".to_string()))]);
}
table.add_row(row![ d =>
target,
logic::null_ip_checker(&resolv_data.ip),
logic::return_ports_string(
&resolv_data
.ports_data
.iter()
.map(|f| f.portid.clone())
.collect(),
),
services_table,
]);
}
}
if args.with_output && !args.targets.is_empty() {
if files::table_to_file(&table, files::return_output_file(&args)).is_err()
&& !args.quiet_flag
{
error!(
"An error occurred while writing the output file {}.\n",
args.file_name
)
}
}
if !args.quiet_flag {
table.printstd();
}
if ((args.with_output && !args.unique_output_flag) || args.unique_output_flag)
&& !args.quiet_flag
{
info!(
"Job finished in {} seconds.\n",
args.time_wasted.elapsed().as_secs()
);
info!("Logfile saved in {}\n\n", args.file_name);
}
Ok(())
}
fn async_resolver_engine(args: &Args, targets: HashSet<String>) -> HashMap<String, ResolvData> {
let mut opts = ResolverOpts::default();
opts.timeout = Duration::from_secs(2);
let resolv_data: HashMap<String, ResolvData> = targets
.par_iter()
.map(|target| {
let fqdn_target = format!("{}.", target);
let mut resolv_data = ResolvData::default();
resolv_data.ip = networking::get_records(
&networking::get_resolver(&RESOLVERS, &opts),
&fqdn_target,
RecordType::A,
);
(target.to_owned(), resolv_data)
})
.collect();
let mut nmap_ips: HashSet<String> = resolv_data
.iter()
.map(|(_, resolv_data)| resolv_data.ip.clone())
.collect();
nmap_ips.retain(|ip| !ip.is_empty());
let nmap_data: HashMap<String, Nmaprun> = nmap_ips
.par_iter()
.map(|ip| {
let filename = format!("{}.xml", &ip);
match nmap::get_nmap_data(
&filename,
&ip,
args.min_rate,
args.initial_port,
args.last_port,
args.fast_scan,
) {
Ok(nmap_data) => {
nmap_data
.host
.clone()
.unwrap_or_default()
.ports
.unwrap_or_default()
.port
.retain(|f| f.state.state == "open");
match std::fs::remove_file(&filename) {
Ok(_) => (),
Err(e) => {
error!("Error removing filename {}. Description: {}", &filename, e)
}
};
(ip.clone(), nmap_data)
}
Err(e) => {
error!("Error scanning the ip {}. Description: {}", &ip, e);
(String::new(), Nmaprun::default())
}
}
})
.collect();
resolv_data
.iter()
.map(|(target, resolv_data)| {
(
target.clone(),
ResolvData {
ip: resolv_data.ip.clone(),
ports_data: if resolv_data.ip.is_empty() {
resolv_data.ports_data.clone()
} else {
nmap_data
.get_key_value(&resolv_data.ip)
.unwrap()
.1
.host
.clone()
.unwrap_or_default()
.ports
.unwrap_or_default()
.port
},
},
)
})
.collect()
}

41
src/structs.rs Normal file
View file

@ -0,0 +1,41 @@
use {
crate::nmap::Port,
std::{collections::HashSet, time::Instant},
};
#[derive(Clone, Debug)]
pub struct Args {
pub target: String,
pub file_name: String,
pub version: String,
pub logs_dir: String,
pub threads: usize,
pub initial_port: usize,
pub last_port: usize,
pub with_output: bool,
pub unique_output_flag: bool,
pub min_rate: usize,
pub from_file_flag: bool,
pub quiet_flag: bool,
pub custom_resolvers: bool,
pub custom_ports_range: bool,
pub fast_scan: bool,
pub files: Vec<String>,
pub resolvers: Vec<String>,
pub targets: HashSet<String>,
pub time_wasted: Instant,
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct ResolvData {
pub ip: String,
pub ports_data: Vec<Port>,
}
impl ResolvData {
pub fn default() -> ResolvData {
ResolvData {
ip: String::new(),
ports_data: Vec::new(),
}
}
}