Skip to main content

ploit_malper/db/
models.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use sha2::{Digest, Sha256};
4use std::collections::BTreeMap;
5use time::format_description::well_known::Rfc3339;
6use time::OffsetDateTime;
7
8pub fn now_utc() -> String {
9    OffsetDateTime::now_utc()
10        .format(&Rfc3339)
11        .unwrap_or_else(|_| format!("{:?}", std::time::SystemTime::now()))
12}
13
14pub fn sha256_hex(input: &str) -> String {
15    let mut hasher = Sha256::new();
16    hasher.update(input.as_bytes());
17    format!("{:x}", hasher.finalize())
18}
19
20fn stable_id(prefix: &str, key: &str) -> String {
21    sha256_hex(&format!("{}:{}", prefix, key))
22}
23
24pub const ASSET_ACTIVE: &str = "active";
25pub const ASSET_REMOVED: &str = "removed";
26
27/// A scannable target (IP, hostname, or root domain) with lifecycle state.
28#[derive(Debug, Clone, Serialize, Deserialize, Default)]
29pub struct Asset {
30    pub stable_id: String,
31    pub asset_type: String,
32    pub name: String,
33    pub ip: Option<String>,
34    pub fqdn: Option<String>,
35    pub reverse_dns: Option<String>,
36    pub first_seen: String,
37    pub last_seen: String,
38    pub status: String,
39    #[serde(default)]
40    pub metadata: Value,
41}
42
43impl Asset {
44    pub fn new(name: &str, asset_type: &str) -> Self {
45        let mut asset = Self {
46            stable_id: stable_id("asset", name),
47            asset_type: asset_type.to_string(),
48            name: name.to_string(),
49            ..Default::default()
50        };
51        asset.metadata = Value::Object(Default::default());
52        let now = now_utc();
53        asset.first_seen = now.clone();
54        asset.last_seen = now;
55        asset.status = ASSET_ACTIVE.to_string();
56        asset
57    }
58}
59
60/// A network service discovered on an asset (open port + protocol + product).
61#[derive(Debug, Clone, Serialize, Deserialize, Default)]
62pub struct Service {
63    pub stable_id: String,
64    pub asset_id: String,
65    pub port: u16,
66    pub protocol: String,
67    pub service_name: String,
68    pub product: Option<String>,
69    pub version: Option<String>,
70    pub version_str: Option<String>,
71    pub banner: Option<String>,
72    #[serde(default)]
73    pub technologies: Vec<String>,
74    #[serde(default)]
75    pub cpes: Vec<String>,
76    pub first_seen: String,
77    pub last_seen: String,
78    pub status: String,
79    #[serde(default)]
80    pub metadata: Value,
81}
82
83impl Service {
84    pub fn new(asset_id: &str, port: u16, protocol: &str, service_name: &str) -> Self {
85        let key = format!("{}:{}:{}:{}", asset_id, port, protocol, service_name);
86        let mut service = Self {
87            stable_id: stable_id("service", &key),
88            asset_id: asset_id.to_string(),
89            port,
90            protocol: protocol.to_string(),
91            service_name: service_name.to_string(),
92            ..Default::default()
93        };
94        service.metadata = Value::Object(Default::default());
95        let now = now_utc();
96        service.first_seen = now.clone();
97        service.last_seen = now;
98        service.status = ASSET_ACTIVE.to_string();
99        service
100    }
101}
102
103/// A vulnerability / security finding on an asset with exploitability tracking.
104#[derive(Debug, Clone, Serialize, Deserialize, Default)]
105pub struct Finding {
106    pub stable_id: String,
107    pub asset_id: String,
108    pub service_id: Option<String>,
109    pub title: String,
110    pub severity: String,
111    pub tool: String,
112    pub target_url: Option<String>,
113    /// Short excerpt kept in the database; full evidence lives in a file
114    /// referenced by `detail_path`.
115    pub detail: Option<String>,
116    /// Relative path into the content store holding the full detail/evidence.
117    pub detail_path: Option<String>,
118    pub reference: Option<String>,
119    #[serde(default)]
120    pub cves: Vec<String>,
121    #[serde(default)]
122    pub endpoints: Vec<String>,
123    #[serde(default)]
124    pub technologies: Vec<String>,
125    pub first_seen: String,
126    pub last_seen: String,
127    pub status: String,
128    pub exploitability: Option<String>,
129    #[serde(default)]
130    pub metadata: Value,
131}
132
133impl Finding {
134    pub fn new(asset_id: &str, tool: &str, title: &str) -> Self {
135        let key = format!("{}:{}:{}", asset_id, tool, title);
136        let mut finding = Self {
137            stable_id: stable_id("finding", &key),
138            asset_id: asset_id.to_string(),
139            tool: tool.to_string(),
140            title: title.to_string(),
141            ..Default::default()
142        };
143        finding.metadata = Value::Object(Default::default());
144        let now = now_utc();
145        finding.first_seen = now.clone();
146        finding.last_seen = now;
147        finding.status = ASSET_ACTIVE.to_string();
148        finding
149    }
150}
151
152/// A historical observation recording how intelligence changed across runs.
153#[derive(Debug, Clone, Serialize, Deserialize, Default)]
154pub struct Observation {
155    pub stable_id: String,
156    pub run_id: String,
157    pub subject_type: String,
158    pub subject_id: String,
159    pub kind: String,
160    #[serde(default)]
161    pub before: Value,
162    #[serde(default)]
163    pub after: Value,
164    pub detail: String,
165    pub observed_at: String,
166}
167
168impl Observation {
169    pub fn new(
170        run_id: &str,
171        subject_type: &str,
172        subject_id: &str,
173        kind: &str,
174        before: Value,
175        after: Value,
176        detail: &str,
177    ) -> Self {
178        let before_s = before.to_string();
179        let after_s = after.to_string();
180        let key = format!(
181            "{}:{}:{}:{}:{}:{}",
182            run_id, subject_type, subject_id, kind, before_s, after_s
183        );
184        Self {
185            stable_id: stable_id("observation", &key),
186            run_id: run_id.to_string(),
187            subject_type: subject_type.to_string(),
188            subject_id: subject_id.to_string(),
189            kind: kind.to_string(),
190            before,
191            after,
192            detail: detail.to_string(),
193            observed_at: now_utc(),
194        }
195    }
196}
197
198/// Metadata describing one logical scan run over a target.
199#[derive(Debug, Clone, Serialize, Deserialize, Default)]
200pub struct ScanRun {
201    pub stable_id: String,
202    pub target: String,
203    pub folder: String,
204    pub started_at: Option<String>,
205    pub finished_at: Option<String>,
206    #[serde(default)]
207    pub tools: Vec<String>,
208    #[serde(default)]
209    pub artifacts: Vec<Value>,
210    pub content_hash: String,
211    pub imported_at: String,
212    #[serde(default)]
213    pub stats: Value,
214}
215
216impl ScanRun {
217    pub fn new(
218        run_id: &str,
219        target: &str,
220        folder: &str,
221        content_hash: &str,
222        started_at: Option<String>,
223    ) -> Self {
224        Self {
225            stable_id: run_id.to_string(),
226            target: target.to_string(),
227            folder: folder.to_string(),
228            started_at,
229            finished_at: None,
230            content_hash: content_hash.to_string(),
231            imported_at: now_utc(),
232            stats: Value::Object(Default::default()),
233            ..Default::default()
234        }
235    }
236}
237
238/// A single recorded module execution against a target. History is append-only
239/// so that every attempt — including dry runs, skipped steps, and failures —
240/// is preserved alongside the run it was planned from.
241#[derive(Debug, Clone, Serialize, Deserialize, Default)]
242pub struct ExploitExecution {
243    pub execution_id: String,
244    pub run_id: String,
245    pub asset_id: String,
246    pub vulnerability_id: String,
247    pub module_type: String,
248    pub module: String,
249    pub host: String,
250    pub payload: Option<String>,
251    pub status: String,
252    pub start_time: String,
253    pub finish_time: Option<String>,
254    pub job_id: Option<String>,
255    pub session_id: Option<String>,
256    pub error: Option<String>,
257    #[serde(default)]
258    pub loot: Vec<String>,
259    #[serde(default)]
260    pub options: BTreeMap<String, String>,
261    /// Whether the operator selected this step in the checklist.
262    pub selected: bool,
263    pub created_at: String,
264}
265
266impl ExploitExecution {
267    pub fn new(run_id: &str, asset_id: &str, vulnerability_id: &str, module: &str) -> Self {
268        let key = format!("{}:{}:{}:{}", run_id, asset_id, vulnerability_id, module);
269        let now = now_utc();
270        Self {
271            execution_id: sha256_hex(&format!("exploit_exec:{}:{}", key, now)),
272            run_id: run_id.to_string(),
273            asset_id: asset_id.to_string(),
274            vulnerability_id: vulnerability_id.to_string(),
275            module: module.to_string(),
276            module_type: String::new(),
277            host: String::new(),
278            payload: None,
279            status: String::new(),
280            start_time: now.clone(),
281            finish_time: None,
282            job_id: None,
283            session_id: None,
284            error: None,
285            loot: Vec::new(),
286            options: BTreeMap::new(),
287            selected: false,
288            created_at: now,
289        }
290    }
291}