crates/dwarves-tune-graph/src/profile.rs
//! Reading what a probe saw: a Heimdall `<arch>-<libc>.probe.json` (calls
//! with the runs that made them) or an OCI `.oci.json` profile (allowed
//! names only). Read as plain JSON; no Heimdall crate is linked.
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use serde_json::Value;
use crate::{Error, Seen};
/// Read a Heimdall `<arch>-<libc>.probe.json` or an OCI `.oci.json` profile.
pub fn read_seen(path: &Path) -> Result<Seen, Error> {
let err = |msg: String| Error::Profile {
path: path.to_path_buf(),
msg,
};
let text = std::fs::read_to_string(path).map_err(|e| Error::Io {
path: path.to_path_buf(),
err: e,
})?;
let v: Value = serde_json::from_str(&text).map_err(|e| err(e.to_string()))?;
let mut seen = Seen {
source: path.display().to_string(),
..Seen::default()
};
if let Some(calls) = v.get("calls").and_then(Value::as_array) {
seen.kind = "probe".into();
seen.workload = v
.get("workload")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
for r in v.get("runs").and_then(Value::as_array).into_iter().flatten() {
if r.get("error_path").and_then(Value::as_bool) == Some(true) {
if let Some(n) = r.get("name").and_then(Value::as_str) {
seen.error_runs.insert(n.to_string());
}
}
}
for c in calls {
let name = c
.get("syscall")
.and_then(Value::as_str)
.ok_or_else(|| err("a call without \"syscall\"".into()))?;
let runs = seen.calls.entry(name.to_string()).or_default();
for r in c.get("runs").and_then(Value::as_array).into_iter().flatten() {
if let Some(r) = r.as_str() {
runs.insert(r.to_string());
}
}
}
for u in v.get("unknown").and_then(Value::as_array).into_iter().flatten() {
if let Some(c) = u.get("call").and_then(Value::as_str) {
seen.unknown.push(c.to_string());
}
}
return Ok(seen);
}
if let Some(rules) = v.get("syscalls").and_then(Value::as_array) {
seen.kind = "oci".into();
let mut calls: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for r in rules {
let action = r.get("action").and_then(Value::as_str).unwrap_or("");
if action != "SCMP_ACT_ALLOW" && action != "SCMP_ACT_LOG" {
continue;
}
for n in r.get("names").and_then(Value::as_array).into_iter().flatten() {
if let Some(n) = n.as_str() {
calls.entry(n.to_string()).or_default();
}
}
}
seen.calls = calls;
return Ok(seen);
}
Err(err(
"neither a probe.json (\"calls\") nor an OCI profile (\"syscalls\")".into(),
))
}