Skip to main content

pq_map/
cli.rs

1use crate::collect::{self, Options};
2use crate::config::Config;
3use crate::render;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6enum Mode {
7    Combined,
8    Summary,
9    Hosts,
10    Certs,
11    Graph,
12    Json,
13}
14
15const USAGE: &str = "\
16Usage:
17  pq-map DOMAIN [--refresh] [--no-probe] [--summary|--hosts|--certs|--graph|--json]
18
19Modes:
20  (default)   combined text view: names, hosts, services, DNS records
21  --summary   certificate estate summary
22  --hosts     concrete hostnames of currently valid certificates
23  --certs     CT certificate records table
24  --graph     host/crypto edges as JSON for 3d-force-graph
25  --json      full state as JSON
26
27Options:
28  --refresh   force a fresh CT snapshot (bypasses the 7-day cache)
29  --no-probe  skip live TLS probing
30  --ports     discover services on alternative ports for hosts whose 443 is
31              closed: family-driven port list (mx -> 465/587/25, mail -> 993/995/
32              465/587, ...) or explicit comma LIST (--ports=80,8443)
33  -h, --help  this help
34";
35
36pub fn run() -> i32 {
37    let mut domain: Option<String> = None;
38    let mut mode = Mode::Combined;
39    let mut refresh = false;
40    let mut probe = true;
41    let mut ports: Option<Vec<u16>> = None;
42
43    for arg in std::env::args().skip(1) {
44        match arg.as_str() {
45            "--refresh" => refresh = true,
46            "--no-probe" => probe = false,
47            "--ports" => ports = Some(Vec::new()),
48            other if other.starts_with("--ports=") => {
49                let list = other
50                    .trim_start_matches("--ports=")
51                    .split(',')
52                    .filter_map(|p| p.trim().parse().ok())
53                    .collect::<Vec<u16>>();
54                if list.is_empty() {
55                    eprintln!("ERROR: --ports=LIST requires a comma-separated port list");
56                    return 2;
57                }
58                ports = Some(list);
59            }
60            "--summary" => mode = Mode::Summary,
61            "--hosts" => mode = Mode::Hosts,
62            "--certs" => mode = Mode::Certs,
63            "--graph" => mode = Mode::Graph,
64            "--json" => mode = Mode::Json,
65            "-h" | "--help" => {
66                print!("{USAGE}");
67                return 0;
68            }
69            other if other.starts_with("--") => {
70                eprintln!("ERROR: unknown option: {other}");
71                eprint!("{USAGE}");
72                return 2;
73            }
74            other => {
75                if domain.is_none() {
76                    domain = Some(other.to_string());
77                } else {
78                    eprintln!("ERROR: unexpected argument: {other}");
79                    eprint!("{USAGE}");
80                    return 2;
81                }
82            }
83        }
84    }
85
86    let Some(domain) = domain else {
87        eprint!("{USAGE}");
88        return 2;
89    };
90    let domain = domain.to_lowercase();
91    let domain = domain.strip_suffix('.').unwrap_or(&domain).to_string();
92
93    let cfg = Config::from_env();
94    let runtime = match tokio::runtime::Builder::new_multi_thread()
95        .enable_all()
96        .build()
97    {
98        Ok(rt) => rt,
99        Err(e) => {
100            eprintln!("ERROR: cannot start async runtime: {e}");
101            return 1;
102        }
103    };
104
105    match runtime.block_on(collect::collect(
106        &domain,
107        &cfg,
108        &Options {
109            refresh,
110            probe,
111            ports,
112        },
113    )) {
114        Ok(state) => {
115            match mode {
116                Mode::Combined => print!("{}", render::text::combined(&state)),
117                Mode::Summary => print!("{}", render::text::summary(&state)),
118                Mode::Hosts => print!("{}", render::text::hosts(&state)),
119                Mode::Certs => print!("{}", render::text::certs(&state)),
120                Mode::Graph => println!("{}", render::graph::graph(&state)),
121                Mode::Json => match serde_json::to_string_pretty(&state) {
122                    Ok(json) => println!("{json}"),
123                    Err(e) => {
124                        eprintln!("ERROR: JSON serialization failed: {e}");
125                        return 1;
126                    }
127                },
128            }
129            0
130        }
131        Err(e) => {
132            eprintln!("ERROR: {e}");
133            1
134        }
135    }
136}