rmap/commands/
scan.rs

1use clap::Parser;
2use csv::WriterBuilder;
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6use crate::commands::Command;
7use remote::spawn_remote_scan;
8use scan::{GenerateConfig, ScanConfig, ScanError, spawn_generate, spawn_local_scan};
9
10#[derive(Parser, Serialize, Deserialize)]
11pub struct ScanCommand {
12    /// Remote server address for distributed scanning
13    #[arg(long)]
14    pub remote: Option<String>,
15
16    /// Generate configuration
17    #[command(flatten)]
18    pub generate: GenerateConfig,
19
20    /// Scan configuration
21    #[command(flatten)]
22    pub config: ScanConfig,
23
24    /// Optional path to write scan results as CSV
25    #[arg(short = 'o', long = "output-file", value_name = "PATH")]
26    pub output_file: Option<PathBuf>,
27
28    /// Comma-separated list of result columns for CSV output
29    #[arg(long = "columns", value_name = "COLUMNS", value_delimiter = ',')]
30    pub columns: Option<Vec<String>>,
31}
32
33impl ScanCommand {
34    pub async fn run(&self) -> Result<(), ScanError> {
35        let config = self.config.clone();
36        let generate = self.generate.clone();
37
38        let default_columns = vec![
39            "target".to_string(),
40            "alias".to_string(),
41            "alias_prefix".to_string(),
42            "port".to_string(),
43        ];
44        let columns = self
45            .columns
46            .clone()
47            .filter(|c| !c.is_empty())
48            .unwrap_or(default_columns);
49
50        let mut csv_writer = if let Some(path) = &self.output_file {
51            let writer = WriterBuilder::new()
52                .has_headers(false)
53                .from_path(path)
54                .map_err(|e| ScanError::OutputError(e.to_string()))?;
55            Some((writer, false))
56        } else {
57            None
58        };
59
60        // Spawn scan
61        let (sender, mut results) = match &self.remote {
62            Some(addr) => spawn_remote_scan(config, addr).await?,
63            None => spawn_local_scan(config).await?,
64        };
65
66        // Spawn generate
67        spawn_generate(generate, sender).await?;
68
69        // Receive results
70        while let Some(result) = results.recv().await {
71            tracing::info!("{}", result);
72
73            if let Some((writer, headers_written)) = csv_writer.as_mut() {
74                if !*headers_written {
75                    writer
76                        .write_record(columns.iter())
77                        .map_err(|e| ScanError::OutputError(e.to_string()))?;
78                    *headers_written = true;
79                }
80
81                let row = result.to_row(&columns);
82                writer
83                    .write_record(row)
84                    .map_err(|e| ScanError::OutputError(e.to_string()))?;
85            }
86        }
87
88        if let Some((mut writer, _)) = csv_writer {
89            writer
90                .flush()
91                .map_err(|e| ScanError::OutputError(e.to_string()))?;
92        }
93
94        Ok(())
95    }
96}
97
98impl Command for ScanCommand {
99    async fn run(&self) -> Result<(), String> {
100        self.run().await.map_err(|e| e.to_string())
101    }
102}