Skip to main content

ploit_malper/db/
pocketbase.rs

1use std::io::Write;
2use std::process::{Command, Stdio};
3
4use serde_json::{json, Value};
5
6use crate::config::DatabaseConfig;
7use crate::db::models::{Asset, ExploitExecution, Finding, Observation, ScanRun, Service};
8use crate::db::schema::{self, FieldDef};
9use crate::db::{collections, Storage};
10use crate::error::{AppError, Result};
11
12const REQUEST_TIMEOUT_S: u64 = 60;
13
14/// PocketBase storage backend. Talks to the PocketBase REST API over `curl`
15/// (matching the rest of PloitMalper's lightweight HTTP transport) and uses
16/// `pbctl` for declarative schema management when available.
17pub struct PocketBaseStorage {
18    config: DatabaseConfig,
19    token: String,
20    /// True once the current token has been verified against the API.
21    token_validated: bool,
22}
23
24struct HttpResponse {
25    status: u16,
26    body: Value,
27}
28
29fn curl_request(
30    method: &str,
31    url: &str,
32    token: Option<&str>,
33    body: Option<&Value>,
34    query: &[(&str, &str)],
35) -> Result<HttpResponse> {
36    let mut command = Command::new("curl");
37    command
38        .arg("-sS")
39        .arg("--max-time")
40        .arg(REQUEST_TIMEOUT_S.to_string())
41        .arg("-H")
42        .arg("Content-Type: application/json")
43        .arg("-w")
44        .arg("\n%{http_code}")
45        .stdout(Stdio::piped())
46        .stderr(Stdio::piped());
47
48    if let Some(token) = token {
49        command
50            .arg("-H")
51            .arg(format!("Authorization: Bearer {}", token));
52    }
53
54    if !query.is_empty() {
55        command.arg("-G");
56        for (key, value) in query {
57            command
58                .arg("--data-urlencode")
59                .arg(format!("{}={}", key, value));
60        }
61    }
62
63    if body.is_some() {
64        if method != "GET" {
65            command.arg("-X").arg(method);
66        }
67        command.arg("--data-binary").arg("@-").stdin(Stdio::piped());
68    } else if method != "GET" {
69        command.arg("-X").arg(method);
70    }
71
72    command.arg(url);
73
74    let mut child = command
75        .spawn()
76        .map_err(|e| AppError::Network(format!("failed to spawn curl: {e}")))?;
77
78    if let Some(b) = body {
79        if let Some(mut stdin) = child.stdin.take() {
80            let payload = serde_json::to_string(b)?;
81            stdin
82                .write_all(payload.as_bytes())
83                .map_err(|e| AppError::Network(format!("failed to write request body: {e}")))?;
84        }
85    }
86
87    let output = child
88        .wait_with_output()
89        .map_err(|e| AppError::Network(format!("failed to wait for curl: {e}")))?;
90
91    let raw = String::from_utf8_lossy(&output.stdout).to_string();
92    let trimmed = raw.trim_end();
93    let (body_str, status) = match trimmed.rsplit_once('\n') {
94        Some((rest, code)) => (rest, code.trim().parse::<u16>().unwrap_or(0)),
95        None => (trimmed, 0),
96    };
97
98    let body: Value = if body_str.trim().is_empty() {
99        Value::Null
100    } else {
101        serde_json::from_str(body_str).unwrap_or_else(|_| json!({ "raw": body_str }))
102    };
103
104    Ok(HttpResponse { status, body })
105}
106
107fn get_str(value: &Value, key: &str) -> String {
108    value
109        .get(key)
110        .and_then(Value::as_str)
111        .unwrap_or("")
112        .to_string()
113}
114
115fn get_opt_str(value: &Value, key: &str) -> Option<String> {
116    value
117        .get(key)
118        .and_then(Value::as_str)
119        .filter(|s| !s.is_empty())
120        .map(str::to_string)
121}
122
123fn get_vec(value: &Value, key: &str) -> Vec<String> {
124    value
125        .get(key)
126        .and_then(Value::as_array)
127        .map(|items| {
128            items
129                .iter()
130                .filter_map(|v| v.as_str().map(str::to_string))
131                .collect()
132        })
133        .unwrap_or_default()
134}
135
136fn get_vec_value(value: &Value, key: &str) -> Vec<Value> {
137    value
138        .get(key)
139        .and_then(Value::as_array)
140        .cloned()
141        .unwrap_or_default()
142}
143
144fn get_json(value: &Value, key: &str) -> Value {
145    value.get(key).cloned().unwrap_or(Value::Null)
146}
147
148fn as_json_object(value: &Value) -> Value {
149    match value {
150        Value::Null => Value::Object(Default::default()),
151        other => other.clone(),
152    }
153}
154
155// ---------------------------------------------------------------------------
156// Record <-> model conversion
157// ---------------------------------------------------------------------------
158
159fn asset_to_record(asset: &Asset) -> Value {
160    let mut record = serde_json::Map::new();
161    record.insert("stable_id".into(), json!(asset.stable_id));
162    record.insert("asset_type".into(), json!(asset.asset_type));
163    record.insert("name".into(), json!(asset.name));
164    if let Some(v) = &asset.ip {
165        record.insert("ip".into(), json!(v));
166    }
167    if let Some(v) = &asset.fqdn {
168        record.insert("fqdn".into(), json!(v));
169    }
170    if let Some(v) = &asset.reverse_dns {
171        record.insert("reverse_dns".into(), json!(v));
172    }
173    record.insert("first_seen".into(), json!(asset.first_seen));
174    record.insert("last_seen".into(), json!(asset.last_seen));
175    record.insert("status".into(), json!(asset.status));
176    record.insert("metadata".into(), as_json_object(&asset.metadata));
177    Value::Object(record)
178}
179
180fn record_to_asset(value: &Value) -> Asset {
181    Asset {
182        stable_id: get_str(value, "stable_id"),
183        asset_type: get_str(value, "asset_type"),
184        name: get_str(value, "name"),
185        ip: get_opt_str(value, "ip"),
186        fqdn: get_opt_str(value, "fqdn"),
187        reverse_dns: get_opt_str(value, "reverse_dns"),
188        first_seen: get_str(value, "first_seen"),
189        last_seen: get_str(value, "last_seen"),
190        status: get_str(value, "status"),
191        metadata: get_json(value, "metadata"),
192    }
193}
194
195fn service_to_record(service: &Service) -> Value {
196    let mut record = serde_json::Map::new();
197    record.insert("stable_id".into(), json!(service.stable_id));
198    record.insert("asset_id".into(), json!(service.asset_id));
199    record.insert("port".into(), json!(service.port));
200    record.insert("protocol".into(), json!(service.protocol));
201    record.insert("service_name".into(), json!(service.service_name));
202    if let Some(v) = &service.product {
203        record.insert("product".into(), json!(v));
204    }
205    if let Some(v) = &service.version {
206        record.insert("version".into(), json!(v));
207    }
208    if let Some(v) = &service.version_str {
209        record.insert("version_str".into(), json!(v));
210    }
211    if let Some(v) = &service.banner {
212        record.insert("banner".into(), json!(v));
213    }
214    record.insert("technologies".into(), json!(service.technologies));
215    record.insert("cpes".into(), json!(service.cpes));
216    record.insert("first_seen".into(), json!(service.first_seen));
217    record.insert("last_seen".into(), json!(service.last_seen));
218    record.insert("status".into(), json!(service.status));
219    record.insert("metadata".into(), as_json_object(&service.metadata));
220    Value::Object(record)
221}
222
223fn record_to_service(value: &Value) -> Service {
224    Service {
225        stable_id: get_str(value, "stable_id"),
226        asset_id: get_str(value, "asset_id"),
227        port: value
228            .get("port")
229            .and_then(Value::as_u64)
230            .and_then(|p| u16::try_from(p).ok())
231            .unwrap_or(0),
232        protocol: get_str(value, "protocol"),
233        service_name: get_str(value, "service_name"),
234        product: get_opt_str(value, "product"),
235        version: get_opt_str(value, "version"),
236        version_str: get_opt_str(value, "version_str"),
237        banner: get_opt_str(value, "banner"),
238        technologies: get_vec(value, "technologies"),
239        cpes: get_vec(value, "cpes"),
240        first_seen: get_str(value, "first_seen"),
241        last_seen: get_str(value, "last_seen"),
242        status: get_str(value, "status"),
243        metadata: get_json(value, "metadata"),
244    }
245}
246
247fn finding_to_record(finding: &Finding) -> Value {
248    let mut record = serde_json::Map::new();
249    record.insert("stable_id".into(), json!(finding.stable_id));
250    record.insert("asset_id".into(), json!(finding.asset_id));
251    if let Some(v) = &finding.service_id {
252        record.insert("service_id".into(), json!(v));
253    }
254    record.insert("title".into(), json!(finding.title));
255    record.insert("severity".into(), json!(finding.severity));
256    record.insert("tool".into(), json!(finding.tool));
257    if let Some(v) = &finding.target_url {
258        record.insert("target_url".into(), json!(v));
259    }
260    if let Some(v) = &finding.detail {
261        record.insert("detail".into(), json!(v));
262    }
263    if let Some(v) = &finding.detail_path {
264        record.insert("detail_path".into(), json!(v));
265    }
266    if let Some(v) = &finding.reference {
267        record.insert("reference".into(), json!(v));
268    }
269    record.insert("cves".into(), json!(finding.cves));
270    record.insert("endpoints".into(), json!(finding.endpoints));
271    record.insert("technologies".into(), json!(finding.technologies));
272    record.insert("first_seen".into(), json!(finding.first_seen));
273    record.insert("last_seen".into(), json!(finding.last_seen));
274    record.insert("status".into(), json!(finding.status));
275    if let Some(v) = &finding.exploitability {
276        record.insert("exploitability".into(), json!(v));
277    }
278    record.insert("metadata".into(), as_json_object(&finding.metadata));
279    Value::Object(record)
280}
281
282fn record_to_finding(value: &Value) -> Finding {
283    Finding {
284        stable_id: get_str(value, "stable_id"),
285        asset_id: get_str(value, "asset_id"),
286        service_id: get_opt_str(value, "service_id"),
287        title: get_str(value, "title"),
288        severity: get_str(value, "severity"),
289        tool: get_str(value, "tool"),
290        target_url: get_opt_str(value, "target_url"),
291        detail: get_opt_str(value, "detail"),
292        detail_path: get_opt_str(value, "detail_path"),
293        reference: get_opt_str(value, "reference"),
294        cves: get_vec(value, "cves"),
295        endpoints: get_vec(value, "endpoints"),
296        technologies: get_vec(value, "technologies"),
297        first_seen: get_str(value, "first_seen"),
298        last_seen: get_str(value, "last_seen"),
299        status: get_str(value, "status"),
300        exploitability: get_opt_str(value, "exploitability"),
301        metadata: get_json(value, "metadata"),
302    }
303}
304
305fn observation_to_record(observation: &Observation) -> Value {
306    json!({
307        "stable_id": observation.stable_id,
308        "run_id": observation.run_id,
309        "subject_type": observation.subject_type,
310        "subject_id": observation.subject_id,
311        "kind": observation.kind,
312        "before": as_json_object(&observation.before),
313        "after": as_json_object(&observation.after),
314        "detail": observation.detail,
315        "observed_at": observation.observed_at,
316    })
317}
318
319fn record_to_observation(value: &Value) -> Observation {
320    Observation {
321        stable_id: get_str(value, "stable_id"),
322        run_id: get_str(value, "run_id"),
323        subject_type: get_str(value, "subject_type"),
324        subject_id: get_str(value, "subject_id"),
325        kind: get_str(value, "kind"),
326        before: get_json(value, "before"),
327        after: get_json(value, "after"),
328        detail: get_str(value, "detail"),
329        observed_at: get_str(value, "observed_at"),
330    }
331}
332
333fn scan_run_to_record(run: &ScanRun) -> Value {
334    json!({
335        "stable_id": run.stable_id,
336        "target": run.target,
337        "folder": run.folder,
338        "started_at": run.started_at,
339        "finished_at": run.finished_at,
340        "tools": run.tools,
341        "artifacts": run.artifacts,
342        "content_hash": run.content_hash,
343        "imported_at": run.imported_at,
344        "stats": as_json_object(&run.stats),
345    })
346}
347
348fn record_to_scan_run(value: &Value) -> ScanRun {
349    ScanRun {
350        stable_id: get_str(value, "stable_id"),
351        target: get_str(value, "target"),
352        folder: get_str(value, "folder"),
353        started_at: get_opt_str(value, "started_at"),
354        finished_at: get_opt_str(value, "finished_at"),
355        tools: get_vec(value, "tools"),
356        artifacts: get_vec_value(value, "artifacts"),
357        content_hash: get_str(value, "content_hash"),
358        imported_at: get_str(value, "imported_at"),
359        stats: get_json(value, "stats"),
360    }
361}
362
363fn get_options_map(value: &Value, key: &str) -> std::collections::BTreeMap<String, String> {
364    value
365        .get(key)
366        .and_then(Value::as_object)
367        .map(|map| {
368            map.iter()
369                .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
370                .collect()
371        })
372        .unwrap_or_default()
373}
374
375fn exploit_execution_to_record(execution: &ExploitExecution) -> Value {
376    json!({
377        "execution_id": execution.execution_id,
378        "run_id": execution.run_id,
379        "asset_id": execution.asset_id,
380        "vulnerability_id": execution.vulnerability_id,
381        "module_type": execution.module_type,
382        "module": execution.module,
383        "host": execution.host,
384        "payload": execution.payload,
385        "status": execution.status,
386        "start_time": execution.start_time,
387        "finish_time": execution.finish_time,
388        "job_id": execution.job_id,
389        "session_id": execution.session_id,
390        "error": execution.error,
391        "loot": execution.loot,
392        "options": execution.options,
393        "selected": execution.selected,
394        "created_at": execution.created_at,
395    })
396}
397
398fn record_to_exploit_execution(value: &Value) -> ExploitExecution {
399    ExploitExecution {
400        execution_id: get_str(value, "execution_id"),
401        run_id: get_str(value, "run_id"),
402        asset_id: get_str(value, "asset_id"),
403        vulnerability_id: get_str(value, "vulnerability_id"),
404        module_type: get_str(value, "module_type"),
405        module: get_str(value, "module"),
406        host: get_str(value, "host"),
407        payload: get_opt_str(value, "payload"),
408        status: get_str(value, "status"),
409        start_time: get_str(value, "start_time"),
410        finish_time: get_opt_str(value, "finish_time"),
411        job_id: get_opt_str(value, "job_id"),
412        session_id: get_opt_str(value, "session_id"),
413        error: get_opt_str(value, "error"),
414        loot: get_vec(value, "loot"),
415        options: get_options_map(value, "options"),
416        selected: value
417            .get("selected")
418            .and_then(Value::as_bool)
419            .unwrap_or(false),
420        created_at: get_str(value, "created_at"),
421    }
422}
423
424impl PocketBaseStorage {
425    pub fn new(config: &DatabaseConfig) -> Result<Self> {
426        let mut url = config.pocketbase_url.trim().to_string();
427        if url.is_empty() {
428            url = "http://127.0.0.1:8090".to_string();
429        }
430        let url = url.trim_end_matches('/').to_string();
431        Ok(Self {
432            config: DatabaseConfig {
433                pocketbase_url: url,
434                ..config.clone()
435            },
436            token: config.pocketbase_token.clone(),
437            token_validated: false,
438        })
439    }
440
441    fn base_url(&self) -> &str {
442        &self.config.pocketbase_url
443    }
444
445    /// Current auth token, if a login has been performed.
446    pub fn token(&self) -> String {
447        self.token.clone()
448    }
449
450    /// Authenticate with the configured superuser credentials and cache a
451    /// fresh token. Returns the token.
452    pub fn login(&mut self) -> Result<String> {
453        let email = self.config.pocketbase_admin_email.trim();
454        let password = self.config.pocketbase_admin_password.trim();
455        if email.is_empty() || password.is_empty() {
456            return Err(AppError::Message(
457                "PocketBase requires superuser credentials (email + password) to connect. \
458                 Run `ploit-malper db_setup` to configure them."
459                    .to_string(),
460            ));
461        }
462        let url = format!(
463            "{}/api/collections/_superusers/auth-with-password",
464            self.base_url()
465        );
466        let body = json!({ "identity": email, "password": password });
467        let resp = curl_request("POST", &url, None, Some(&body), &[])?;
468        if resp.status != 200 && resp.status != 201 {
469            return Err(AppError::Network(format!(
470                "PocketBase authentication failed (HTTP {}): {}",
471                resp.status, resp.body
472            )));
473        }
474        let token = get_str(&resp.body, "token");
475        if token.is_empty() {
476            return Err(AppError::Network(format!(
477                "PocketBase authentication response did not include a token: {}",
478                resp.body
479            )));
480        }
481        self.token = token.clone();
482        self.config.pocketbase_token = token.clone();
483        self.token_validated = true;
484        Ok(token)
485    }
486
487    /// Ensure we have a usable token, verifying a cached one once and minting
488    /// a fresh token from credentials if it has expired.
489    fn ensure_token(&mut self) -> Result<String> {
490        if self.token_validated && !self.token.is_empty() {
491            return Ok(self.token.clone());
492        }
493        if !self.token.is_empty() {
494            let url = format!("{}/api/collections", self.base_url());
495            let resp = curl_request("GET", &url, Some(&self.token), None, &[("perPage", "1")])?;
496            if resp.status == 200 {
497                self.token_validated = true;
498                return Ok(self.token.clone());
499            }
500            self.token.clear();
501        }
502        self.login()
503    }
504
505    pub fn list_collection_names(&mut self) -> Result<Vec<String>> {
506        let token = self.ensure_token()?;
507        let url = format!("{}/api/collections", self.base_url());
508        let resp = curl_request("GET", &url, Some(&token), None, &[("perPage", "200")])?;
509        if resp.status != 200 {
510            return Err(AppError::Network(format!(
511                "Failed to list PocketBase collections (HTTP {}): {}",
512                resp.status, resp.body
513            )));
514        }
515        let names = resp
516            .body
517            .get("items")
518            .and_then(Value::as_array)
519            .map(|items| {
520                items
521                    .iter()
522                    .filter_map(|c| c.get("name").and_then(Value::as_str).map(str::to_string))
523                    .collect()
524            })
525            .unwrap_or_default();
526        Ok(names)
527    }
528
529    fn find_record(
530        &mut self,
531        collection: &str,
532        stable_id: &str,
533    ) -> Result<Option<(String, Value)>> {
534        let token = self.ensure_token()?;
535        let url = format!("{}/api/collections/{}/records", self.base_url(), collection);
536        let filter = format!("stable_id = \"{}\"", stable_id);
537        let resp = curl_request(
538            "GET",
539            &url,
540            Some(&token),
541            None,
542            &[("filter", &filter), ("perPage", "500")],
543        )?;
544        if resp.status != 200 {
545            return Err(AppError::Network(format!(
546                "Failed to query records in '{}' (HTTP {}): {}",
547                collection, resp.status, resp.body
548            )));
549        }
550        let items = resp
551            .body
552            .get("items")
553            .and_then(Value::as_array)
554            .cloned()
555            .unwrap_or_default();
556        if let Some(first) = items.into_iter().next() {
557            let id = get_str(&first, "id");
558            return Ok(Some((id, first)));
559        }
560        Ok(None)
561    }
562
563    fn create_record(&mut self, collection: &str, record: &Value) -> Result<()> {
564        let token = self.ensure_token()?;
565        let url = format!("{}/api/collections/{}/records", self.base_url(), collection);
566        let resp = curl_request("POST", &url, Some(&token), Some(record), &[])?;
567        if resp.status != 200 && resp.status != 201 {
568            return Err(AppError::Network(format!(
569                "Failed to create record in '{}' (HTTP {}): {}",
570                collection, resp.status, resp.body
571            )));
572        }
573        Ok(())
574    }
575
576    fn update_record(&mut self, collection: &str, record_id: &str, record: &Value) -> Result<()> {
577        let token = self.ensure_token()?;
578        let url = format!(
579            "{}/api/collections/{}/records/{}",
580            self.base_url(),
581            collection,
582            record_id
583        );
584        let resp = curl_request("PATCH", &url, Some(&token), Some(record), &[])?;
585        if resp.status != 200 && resp.status != 204 {
586            return Err(AppError::Network(format!(
587                "Failed to update record in '{}' (HTTP {}): {}",
588                collection, resp.status, resp.body
589            )));
590        }
591        Ok(())
592    }
593
594    fn upsert(&mut self, collection: &str, stable_id: &str, record: &Value) -> Result<()> {
595        if let Some((record_id, _)) = self.find_record(collection, stable_id)? {
596            self.update_record(collection, &record_id, record)
597        } else {
598            self.create_record(collection, record)
599        }
600    }
601
602    fn list_records(&mut self, collection: &str, filter: Option<&str>) -> Result<Vec<Value>> {
603        let token = self.ensure_token()?;
604        let url = format!("{}/api/collections/{}/records", self.base_url(), collection);
605        const PER_PAGE: u64 = 500;
606        let mut items: Vec<Value> = Vec::new();
607        let mut page: u64 = 1;
608        loop {
609            let page_str = page.to_string();
610            let per_str = PER_PAGE.to_string();
611            let mut query: Vec<(&str, &str)> =
612                vec![("page", page_str.as_str()), ("perPage", per_str.as_str())];
613            if let Some(f) = filter {
614                query.push(("filter", f));
615            }
616            let resp = curl_request("GET", &url, Some(&token), None, &query)?;
617            if resp.status != 200 {
618                return Err(AppError::Network(format!(
619                    "Failed to list records in '{}' (HTTP {}): {}",
620                    collection, resp.status, resp.body
621                )));
622            }
623            let batch = resp
624                .body
625                .get("items")
626                .and_then(Value::as_array)
627                .cloned()
628                .unwrap_or_default();
629            let count = batch.len();
630            items.extend(batch);
631            if count < PER_PAGE as usize {
632                break;
633            }
634            page += 1;
635        }
636        Ok(items)
637    }
638
639    /// Create a collection directly via the REST API (fallback when pbctl is
640    /// unavailable).
641    fn create_collection_via_api(&mut self, name: &str) -> Result<()> {
642        let token = self.ensure_token()?;
643        let url = format!("{}/api/collections", self.base_url());
644        let defs = schema::field_defs(name);
645        let fields: Vec<Value> = defs.iter().map(|def| self.field_to_json(def)).collect();
646        let body = json!({
647            "name": name,
648            "type": "base",
649            "fields": fields,
650        });
651        let resp = curl_request("POST", &url, Some(&token), Some(&body), &[])?;
652        if resp.status != 200 && resp.status != 201 {
653            return Err(AppError::Network(format!(
654                "Failed to create PocketBase collection '{}' (HTTP {}): {}",
655                name, resp.status, resp.body
656            )));
657        }
658        Ok(())
659    }
660
661    fn field_to_json(&self, def: &FieldDef) -> Value {
662        let mut field = serde_json::Map::new();
663        field.insert("name".to_string(), json!(def.name));
664        field.insert("type".to_string(), json!(def.kind));
665        field.insert("required".to_string(), json!(def.required));
666        field.insert("unique".to_string(), json!(def.unique));
667        field.insert("hidden".to_string(), json!(false));
668        if def.kind == "select" {
669            field.insert(
670                "values".to_string(),
671                json!(["ip", "hostname", "domain", "url"]),
672            );
673            field.insert("maxSelect".to_string(), json!(1));
674        }
675        Value::Object(field)
676    }
677}
678
679impl Storage for PocketBaseStorage {
680    fn kind(&self) -> &str {
681        "pocketbase"
682    }
683
684    fn ping(&mut self) -> Result<bool> {
685        let url = format!("{}/api/health", self.base_url());
686        let resp = curl_request("GET", &url, None, None, &[])?;
687        if resp.status == 200 {
688            return Ok(true);
689        }
690        // Health may be unauthenticated; fall back to an authenticated call.
691        match self.ensure_token() {
692            Ok(token) => {
693                let resp = curl_request("GET", &url, Some(&token), None, &[])?;
694                Ok(resp.status == 200)
695            }
696            Err(e) => Err(e),
697        }
698    }
699
700    fn ensure_schema(&mut self) -> Result<()> {
701        // Pure REST API path — no pbctl required. Create any required
702        // collections that are missing; leave existing ones untouched.
703        let existing = self.list_collection_names()?;
704        for name in collections::ALL {
705            if !existing.iter().any(|n| n == name) {
706                self.create_collection_via_api(name)?;
707            }
708        }
709        Ok(())
710    }
711
712    fn upsert_scan_run(&mut self, run: &ScanRun) -> Result<()> {
713        self.upsert(
714            collections::SCAN_RUNS,
715            &run.stable_id,
716            &scan_run_to_record(run),
717        )
718    }
719
720    fn get_scan_run(&mut self, run_id: &str) -> Result<Option<ScanRun>> {
721        if let Some((_, record)) = self.find_record(collections::SCAN_RUNS, run_id)? {
722            return Ok(Some(record_to_scan_run(&record)));
723        }
724        Ok(None)
725    }
726
727    fn list_scan_runs(&mut self) -> Result<Vec<ScanRun>> {
728        let records = self.list_records(collections::SCAN_RUNS, None)?;
729        Ok(records.iter().map(record_to_scan_run).collect())
730    }
731
732    fn upsert_asset(&mut self, asset: &Asset) -> Result<()> {
733        self.upsert(
734            collections::ASSETS,
735            &asset.stable_id,
736            &asset_to_record(asset),
737        )
738    }
739
740    fn get_asset(&mut self, asset_id: &str) -> Result<Option<Asset>> {
741        if let Some((_, record)) = self.find_record(collections::ASSETS, asset_id)? {
742            return Ok(Some(record_to_asset(&record)));
743        }
744        Ok(None)
745    }
746
747    fn list_assets(&mut self) -> Result<Vec<Asset>> {
748        let records = self.list_records(collections::ASSETS, None)?;
749        Ok(records.iter().map(record_to_asset).collect())
750    }
751
752    fn upsert_service(&mut self, service: &Service) -> Result<()> {
753        self.upsert(
754            collections::SERVICES,
755            &service.stable_id,
756            &service_to_record(service),
757        )
758    }
759
760    fn get_service(&mut self, service_id: &str) -> Result<Option<Service>> {
761        if let Some((_, record)) = self.find_record(collections::SERVICES, service_id)? {
762            return Ok(Some(record_to_service(&record)));
763        }
764        Ok(None)
765    }
766
767    fn list_services_for_asset(&mut self, asset_id: &str) -> Result<Vec<Service>> {
768        let filter = format!("asset_id = \"{}\"", asset_id);
769        let records = self.list_records(collections::SERVICES, Some(&filter))?;
770        Ok(records.iter().map(record_to_service).collect())
771    }
772
773    fn list_all_services(&mut self) -> Result<Vec<Service>> {
774        let records = self.list_records(collections::SERVICES, None)?;
775        Ok(records.iter().map(record_to_service).collect())
776    }
777
778    fn upsert_finding(&mut self, finding: &Finding) -> Result<()> {
779        self.upsert(
780            collections::FINDINGS,
781            &finding.stable_id,
782            &finding_to_record(finding),
783        )
784    }
785
786    fn get_finding(&mut self, finding_id: &str) -> Result<Option<Finding>> {
787        if let Some((_, record)) = self.find_record(collections::FINDINGS, finding_id)? {
788            return Ok(Some(record_to_finding(&record)));
789        }
790        Ok(None)
791    }
792
793    fn list_findings_for_asset(&mut self, asset_id: &str) -> Result<Vec<Finding>> {
794        let filter = format!("asset_id = \"{}\"", asset_id);
795        let records = self.list_records(collections::FINDINGS, Some(&filter))?;
796        Ok(records.iter().map(record_to_finding).collect())
797    }
798
799    fn list_all_findings(&mut self) -> Result<Vec<Finding>> {
800        let records = self.list_records(collections::FINDINGS, None)?;
801        Ok(records.iter().map(record_to_finding).collect())
802    }
803
804    fn add_observation(&mut self, observation: &Observation) -> Result<()> {
805        if self.get_observation(&observation.stable_id)?.is_some() {
806            return Ok(());
807        }
808        self.create_record(
809            collections::OBSERVATIONS,
810            &observation_to_record(observation),
811        )
812    }
813
814    fn get_observation(&mut self, obs_id: &str) -> Result<Option<Observation>> {
815        if let Some((_, record)) = self.find_record(collections::OBSERVATIONS, obs_id)? {
816            return Ok(Some(record_to_observation(&record)));
817        }
818        Ok(None)
819    }
820
821    fn list_observations_for(
822        &mut self,
823        subject_type: &str,
824        subject_id: &str,
825    ) -> Result<Vec<Observation>> {
826        let filter = format!(
827            "subject_type = \"{}\" && subject_id = \"{}\"",
828            subject_type, subject_id
829        );
830        let records = self.list_records(collections::OBSERVATIONS, Some(&filter))?;
831        Ok(records.iter().map(record_to_observation).collect())
832    }
833
834    fn list_all_observations(&mut self) -> Result<Vec<Observation>> {
835        let records = self.list_records(collections::OBSERVATIONS, None)?;
836        Ok(records.iter().map(record_to_observation).collect())
837    }
838
839    fn upsert_exploit_execution(&mut self, execution: &ExploitExecution) -> Result<()> {
840        self.upsert(
841            collections::EXPLOIT_EXECUTIONS,
842            &execution.execution_id,
843            &exploit_execution_to_record(execution),
844        )
845    }
846
847    fn get_exploit_execution(&mut self, execution_id: &str) -> Result<Option<ExploitExecution>> {
848        if let Some((_, record)) =
849            self.find_record(collections::EXPLOIT_EXECUTIONS, execution_id)?
850        {
851            return Ok(Some(record_to_exploit_execution(&record)));
852        }
853        Ok(None)
854    }
855
856    fn list_exploit_executions(&mut self) -> Result<Vec<ExploitExecution>> {
857        let records = self.list_records(collections::EXPLOIT_EXECUTIONS, None)?;
858        Ok(records.iter().map(record_to_exploit_execution).collect())
859    }
860
861    fn list_exploit_executions_for_run(&mut self, run_id: &str) -> Result<Vec<ExploitExecution>> {
862        let filter = format!("run_id = \"{}\"", run_id);
863        let records = self.list_records(collections::EXPLOIT_EXECUTIONS, Some(&filter))?;
864        Ok(records.iter().map(record_to_exploit_execution).collect())
865    }
866}