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    /// Short excerpt kept in the database; full evidence lives in a file
113    /// referenced by `detail_path`.
114    pub detail: Option<String>,
115    /// Relative path into the content store holding the full detail/evidence.
116    pub detail_path: Option<String>,
117    pub reference: Option<String>,
118    #[serde(default)]
119    pub cves: Vec<String>,
120    #[serde(default)]
121    pub endpoints: Vec<String>,
122    #[serde(default)]
123    pub technologies: Vec<String>,
124    pub first_seen: String,
125    pub last_seen: String,
126    pub status: String,
127    pub exploitability: Option<String>,
128    #[serde(default)]
129    pub metadata: Value,
130}
131
132impl Finding {
133    pub fn new(asset_id: &str, tool: &str, title: &str) -> Self {
134        let key = format!("{}:{}:{}", asset_id, tool, title);
135        let mut finding = Self {
136            stable_id: stable_id("finding", &key),
137            asset_id: asset_id.to_string(),
138            tool: tool.to_string(),
139            title: title.to_string(),
140            ..Default::default()
141        };
142        finding.metadata = Value::Object(Default::default());
143        let now = now_utc();
144        finding.first_seen = now.clone();
145        finding.last_seen = now;
146        finding.status = ASSET_ACTIVE.to_string();
147        finding
148    }
149}
150
151/// A historical observation recording how intelligence changed across runs.
152#[derive(Debug, Clone, Serialize, Deserialize, Default)]
153pub struct Observation {
154    pub stable_id: String,
155    pub run_id: String,
156    pub subject_type: String,
157    pub subject_id: String,
158    pub kind: String,
159    #[serde(default)]
160    pub before: Value,
161    #[serde(default)]
162    pub after: Value,
163    pub detail: String,
164    pub observed_at: String,
165}
166
167impl Observation {
168    pub fn new(
169        run_id: &str,
170        subject_type: &str,
171        subject_id: &str,
172        kind: &str,
173        before: Value,
174        after: Value,
175        detail: &str,
176    ) -> Self {
177        let before_s = before.to_string();
178        let after_s = after.to_string();
179        let key = format!(
180            "{}:{}:{}:{}:{}:{}",
181            run_id, subject_type, subject_id, kind, before_s, after_s
182        );
183        Self {
184            stable_id: stable_id("observation", &key),
185            run_id: run_id.to_string(),
186            subject_type: subject_type.to_string(),
187            subject_id: subject_id.to_string(),
188            kind: kind.to_string(),
189            before,
190            after,
191            detail: detail.to_string(),
192            observed_at: now_utc(),
193        }
194    }
195}
196
197/// Metadata describing one logical scan run over a target.
198#[derive(Debug, Clone, Serialize, Deserialize, Default)]
199pub struct ScanRun {
200    pub stable_id: String,
201    pub target: String,
202    pub folder: String,
203    pub started_at: Option<String>,
204    pub finished_at: Option<String>,
205    #[serde(default)]
206    pub tools: Vec<String>,
207    #[serde(default)]
208    pub artifacts: Vec<Value>,
209    pub content_hash: String,
210    pub imported_at: String,
211    #[serde(default)]
212    pub stats: Value,
213}
214
215impl ScanRun {
216    pub fn new(
217        run_id: &str,
218        target: &str,
219        folder: &str,
220        content_hash: &str,
221        started_at: Option<String>,
222    ) -> Self {
223        Self {
224            stable_id: run_id.to_string(),
225            target: target.to_string(),
226            folder: folder.to_string(),
227            started_at,
228            finished_at: None,
229            content_hash: content_hash.to_string(),
230            imported_at: now_utc(),
231            stats: Value::Object(Default::default()),
232            ..Default::default()
233        }
234    }
235}