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