ploit_malper/frontend/
services.rs1use 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}