Skip to main content

ploit_malper/frontend/
services.rs

1use crate::config::ConfigManager;
2use crate::db::models::Service;
3use crate::error::Result;
4use crate::frontend::{
5    empty_result, fmt_ts, open_backend, print_json, Args, Cell, ChangeState, IdMaps, ObsIndex,
6    Table,
7};
8
9pub fn usage() {
10    println!(
11        "Usage: ploit-malper services [--port N] [--product TERM] [--asset TERM] [--changed] \
12         [--verbose] [--json] [--backend pocketbase|sqlite]"
13    );
14}
15
16pub fn cmd_services(args: &[String], config_mgr: &mut ConfigManager) -> Result<()> {
17    let parsed = Args::parse(args)?;
18    if parsed.has("help") {
19        usage();
20        return Ok(());
21    }
22
23    let mut storage = open_backend(config_mgr, &parsed)?;
24
25    let services = storage.list_all_services()?;
26    let observations = storage.list_all_observations()?;
27    let obs_index = ObsIndex::build(&observations);
28    let maps = IdMaps::load(&mut storage)?;
29
30    let port_filter = parsed.get("port").map(|p| p.parse::<u16>());
31    let port_filter = match port_filter {
32        Some(Ok(port)) => Some(port),
33        Some(Err(_)) => {
34            return Err(crate::error::AppError::Message(format!(
35                "invalid --port value: '{}'",
36                parsed.get("port").unwrap()
37            )));
38        }
39        None => None,
40    };
41
42    let mut selected: Vec<&Service> = Vec::new();
43    for service in &services {
44        if let Some(port) = port_filter {
45            if service.port != port {
46                continue;
47            }
48        }
49        if let Some(product) = parsed.get("product") {
50            let needle = product.to_lowercase();
51            let hit = service
52                .product
53                .as_deref()
54                .is_some_and(|p| p.to_lowercase().contains(&needle))
55                || service
56                    .version
57                    .as_deref()
58                    .is_some_and(|v| v.to_lowercase().contains(&needle))
59                || service
60                    .version_str
61                    .as_deref()
62                    .is_some_and(|v| v.to_lowercase().contains(&needle))
63                || service.service_name.to_lowercase().contains(&needle);
64            if !hit {
65                continue;
66            }
67        }
68        if let Some(asset) = parsed.get("asset") {
69            let asset_name = maps.asset_name(&service.asset_id);
70            if !asset_name.to_lowercase().contains(&asset.to_lowercase()) {
71                continue;
72            }
73        }
74        let state = obs_index.state(&service.stable_id, &service.status);
75        if parsed.has("changed") && state != ChangeState::Changed {
76            continue;
77        }
78        selected.push(service);
79    }
80
81    if parsed.has("json") {
82        let data: Vec<Service> = selected.into_iter().cloned().collect();
83        return print_json(&data);
84    }
85
86    if selected.is_empty() {
87        empty_result("services");
88        return Ok(());
89    }
90
91    let mut table = Table::new(vec![
92        "ASSET".to_string(),
93        "PORT".to_string(),
94        "PROTO".to_string(),
95        "SERVICE".to_string(),
96        "PRODUCT/VERSION".to_string(),
97        "FIRST SEEN".to_string(),
98        "LAST SEEN".to_string(),
99        "CHANGE".to_string(),
100    ]);
101    for service in &selected {
102        let state = obs_index.state(&service.stable_id, &service.status);
103        let product_version = match (&service.product, &service.version) {
104            (Some(p), Some(v)) => format!("{} {}", p, v),
105            (Some(p), None) => p.clone(),
106            (None, Some(v)) => v.clone(),
107            (None, None) => "-".to_string(),
108        };
109        table.add_row(vec![
110            Cell::plain(maps.asset_name(&service.asset_id)),
111            Cell::plain(service.port.to_string()),
112            Cell::plain(if service.protocol.is_empty() {
113                "tcp".to_string()
114            } else {
115                service.protocol.clone()
116            }),
117            Cell::plain(service.service_name.clone()),
118            Cell::plain(product_version),
119            Cell::plain(fmt_ts(&service.first_seen)),
120            Cell::plain(fmt_ts(&service.last_seen)),
121            Cell::plain(state.label().to_string()),
122        ]);
123    }
124    println!("{}", table.render());
125
126    if parsed.has("verbose") {
127        println!();
128        for service in &selected {
129            println!(
130                "{}:{} ({})  [{}]",
131                maps.asset_name(&service.asset_id),
132                service.port,
133                service.protocol,
134                service.stable_id
135            );
136            println!(
137                "    service={} product={} version={} version_str={} status={}",
138                service.service_name,
139                service.product.as_deref().unwrap_or("-"),
140                service.version.as_deref().unwrap_or("-"),
141                service.version_str.as_deref().unwrap_or("-"),
142                service.status
143            );
144            if !service.technologies.is_empty() {
145                println!("    technologies={}", service.technologies.join(", "));
146            }
147            if !service.cpes.is_empty() {
148                println!("    cpes={}", service.cpes.join(", "));
149            }
150            if let Some(banner) = &service.banner {
151                println!("    banner={}", crate::frontend::truncate(banner, 200));
152            }
153        }
154    }
155
156    Ok(())
157}