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, 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}