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, IdMaps, LifecycleQuery, ObsIndex,
6    Table,
7};
8
9pub fn usage() {
10    println!(
11        "Usage: ploit-malper services [--port N] [--product TERM] [--asset TERM] \
12         [--lifecycle STATE|--all] [--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 lifecycle_query = LifecycleQuery::parse(&parsed, false, false)?;
43
44    let mut selected: Vec<&Service> = Vec::new();
45    for service in &services {
46        if let Some(port) = port_filter {
47            if service.port != port {
48                continue;
49            }
50        }
51        if let Some(product) = parsed.get("product") {
52            let needle = product.to_lowercase();
53            let hit = service
54                .product
55                .as_deref()
56                .is_some_and(|p| p.to_lowercase().contains(&needle))
57                || service
58                    .version
59                    .as_deref()
60                    .is_some_and(|v| v.to_lowercase().contains(&needle))
61                || service
62                    .version_str
63                    .as_deref()
64                    .is_some_and(|v| v.to_lowercase().contains(&needle))
65                || service.service_name.to_lowercase().contains(&needle);
66            if !hit {
67                continue;
68            }
69        }
70        if let Some(asset) = parsed.get("asset") {
71            let asset_name = maps.asset_name(&service.asset_id);
72            if !asset_name.to_lowercase().contains(&asset.to_lowercase()) {
73                continue;
74            }
75        }
76        let lifecycle = obs_index.lifecycle(&service.stable_id, &service.status);
77        if !lifecycle_query.matches(lifecycle) {
78            continue;
79        }
80        selected.push(service);
81    }
82
83    if parsed.has("json") {
84        let data: Vec<Service> = selected.into_iter().cloned().collect();
85        return print_json(&data);
86    }
87
88    if selected.is_empty() {
89        empty_result("services");
90        return Ok(());
91    }
92
93    let mut table = Table::new(vec![
94        "ASSET".to_string(),
95        "PORT".to_string(),
96        "PROTO".to_string(),
97        "SERVICE".to_string(),
98        "PRODUCT/VERSION".to_string(),
99        "FIRST SEEN".to_string(),
100        "LAST SEEN".to_string(),
101        "CHANGE".to_string(),
102    ]);
103    for service in &selected {
104        let lifecycle = obs_index.lifecycle(&service.stable_id, &service.status);
105        let product_version = match (&service.product, &service.version) {
106            (Some(p), Some(v)) => format!("{} {}", p, v),
107            (Some(p), None) => p.clone(),
108            (None, Some(v)) => v.clone(),
109            (None, None) => "-".to_string(),
110        };
111        table.add_row(vec![
112            Cell::plain(maps.asset_name(&service.asset_id)),
113            Cell::plain(service.port.to_string()),
114            Cell::plain(if service.protocol.is_empty() {
115                "tcp".to_string()
116            } else {
117                service.protocol.clone()
118            }),
119            Cell::plain(service.service_name.clone()),
120            Cell::plain(product_version),
121            Cell::plain(fmt_ts(&service.first_seen)),
122            Cell::plain(fmt_ts(&service.last_seen)),
123            Cell::plain(lifecycle.label().to_string()),
124        ]);
125    }
126    println!("{}", table.render());
127
128    if parsed.has("verbose") {
129        println!();
130        for service in &selected {
131            println!(
132                "{}:{} ({})  [{}]",
133                maps.asset_name(&service.asset_id),
134                service.port,
135                service.protocol,
136                service.stable_id
137            );
138            println!(
139                "    service={} product={} version={} version_str={} status={}",
140                service.service_name,
141                service.product.as_deref().unwrap_or("-"),
142                service.version.as_deref().unwrap_or("-"),
143                service.version_str.as_deref().unwrap_or("-"),
144                service.status
145            );
146            if !service.technologies.is_empty() {
147                println!("    technologies={}", service.technologies.join(", "));
148            }
149            if !service.cpes.is_empty() {
150                println!("    cpes={}", service.cpes.join(", "));
151            }
152            if let Some(banner) = &service.banner {
153                println!("    banner={}", crate::frontend::truncate(banner, 200));
154            }
155        }
156    }
157
158    Ok(())
159}