Skip to main content

ploit_malper/db/
models.rs

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