Skip to main content

ploit_malper/frontend/
assets.rs

1use crate::config::ConfigManager;
2use crate::db::models::Asset;
3use crate::error::{AppError, Result};
4use crate::frontend::{
5    bold, fmt_ts, open_backend, parse_ts, print_json, ts_since, Args, Cell, IdMaps, Lifecycle,
6    LifecycleQuery, ObsIndex, Table,
7};
8
9pub fn usage() {
10    println!(
11        "Usage: ploit-malper assets [--lifecycle STATE|--all] [--new] [--changed] [--since DATE] \
12         [--target TERM] [--verbose] [--json] [--backend pocketbase|sqlite]"
13    );
14}
15
16pub fn cmd_assets(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 assets = storage.list_assets()?;
26    let observations = storage.list_all_observations()?;
27    let obs_index = ObsIndex::build(&observations);
28    let maps = IdMaps::load(&mut storage)?;
29
30    let since = match parsed.get("since") {
31        Some(raw) => Some(
32            parse_ts(raw)
33                .ok_or_else(|| AppError::Message(format!("invalid --since date: '{}'", raw)))?,
34        ),
35        None => None,
36    };
37
38    let lifecycle_query = LifecycleQuery::parse(&parsed, true, false)?;
39
40    let mut selected: Vec<&Asset> = Vec::new();
41    for asset in &assets {
42        if let Some(target) = parsed.get("target") {
43            let needle = target.to_lowercase();
44            let hit = asset.name.to_lowercase().contains(&needle)
45                || asset
46                    .ip
47                    .as_deref()
48                    .is_some_and(|i| i.to_lowercase().contains(&needle))
49                || asset
50                    .fqdn
51                    .as_deref()
52                    .is_some_and(|f| f.to_lowercase().contains(&needle))
53                || asset
54                    .reverse_dns
55                    .as_deref()
56                    .is_some_and(|r| r.to_lowercase().contains(&needle));
57            if !hit {
58                continue;
59            }
60        }
61        if let Some(since) = since {
62            if !ts_since(&asset.last_seen, since) {
63                continue;
64            }
65        }
66        let lifecycle = obs_index.lifecycle(&asset.stable_id, &asset.status);
67        if !lifecycle_query.matches(lifecycle) {
68            continue;
69        }
70        selected.push(asset);
71    }
72
73    if parsed.has("json") {
74        let data: Vec<Asset> = selected.into_iter().cloned().collect();
75        return print_json(&data);
76    }
77
78    if selected.is_empty() {
79        crate::frontend::empty_result("assets");
80        return Ok(());
81    }
82
83    let mut table = Table::new(vec![
84        "NAME".to_string(),
85        "TYPE".to_string(),
86        "SERVICES".to_string(),
87        "FIRST SEEN".to_string(),
88        "LAST SEEN".to_string(),
89        "CHANGE".to_string(),
90    ]);
91    for asset in &selected {
92        let lifecycle = obs_index.lifecycle(&asset.stable_id, &asset.status);
93        table.add_row(vec![
94            Cell::plain(asset.name.clone()),
95            Cell::plain(asset.asset_type.clone()),
96            Cell::plain(maps.service_summary(&asset.stable_id)),
97            Cell::plain(fmt_ts(&asset.first_seen)),
98            Cell::plain(fmt_ts(&asset.last_seen)),
99            Cell::styled(lifecycle.label().to_string(), lifecycle_style(lifecycle)),
100        ]);
101    }
102    println!("{}", table.render());
103
104    if parsed.has("verbose") {
105        println!();
106        for asset in &selected {
107            println!(
108                "{} ({})  [{}]",
109                bold(&asset.name),
110                asset.asset_type,
111                asset.stable_id
112            );
113            println!(
114                "    ip={} fqdn={} reverse_dns={} status={}",
115                asset.ip.as_deref().unwrap_or("-"),
116                asset.fqdn.as_deref().unwrap_or("-"),
117                asset.reverse_dns.as_deref().unwrap_or("-"),
118                asset.status
119            );
120            if !asset.metadata.is_null() && asset.metadata.as_object().is_some() {
121                println!("    metadata={}", asset.metadata);
122            }
123            let observations = obs_index.observations_for(&asset.stable_id);
124            if !observations.is_empty() {
125                let last = observations.last().unwrap();
126                println!("    last change: {} ({})", last.kind, last.observed_at);
127            }
128        }
129    }
130
131    Ok(())
132}
133
134fn lifecycle_style(lifecycle: Lifecycle) -> &'static str {
135    match lifecycle {
136        Lifecycle::New => "32",
137        Lifecycle::Changed => "33",
138        Lifecycle::Removed => "31",
139        Lifecycle::Active => "2",
140    }
141}