use crate::profile::ProfileReport;
use anyhow::Context;
use assay_evidence::bundle::BundleWriter;
use assay_evidence::types::{EvidenceEvent, ProducerMeta};
use chrono::{DateTime, Utc};
use std::fs::File;
use std::path::Path;
const EVENT_SOURCE: &str = "urn:assay:sandbox";
pub(super) fn emit_bundle(
report: &ProfileReport,
command: &[String],
run_id: &str,
out: &Path,
) -> anyhow::Result<()> {
let producer = ProducerMeta {
name: "assay-cli".into(),
version: env!("CARGO_PKG_VERSION").into(),
git: option_env!("ASSAY_GIT_SHA").map(|s| s.to_string()),
};
let emit_time = Utc::now();
let agg = &report.agg;
let mut events: Vec<EvidenceEvent> = Vec::new();
let summary = serde_json::json!({
"command": command,
"counters": agg.counters,
"notes": agg.notes,
"fs_count": agg.fs.len(),
"exec_count": agg.execs.len(),
"degradation_count": agg.sandbox_degradations.len(),
});
push_event(
&mut events,
"assay.sandbox.summary",
run_id,
summary,
None,
emit_time,
&producer,
);
for (op, path, backend) in &agg.fs {
let payload = serde_json::json!({
"op": op.as_str(),
"path": path,
"backend": backend.as_str(),
});
push_event(
&mut events,
"assay.sandbox.fs",
run_id,
payload,
Some(path.clone()),
emit_time,
&producer,
);
}
for (argv0, hits) in &agg.execs {
let payload = serde_json::json!({ "argv0": argv0, "hits": hits });
push_event(
&mut events,
"assay.sandbox.exec",
run_id,
payload,
Some(argv0.clone()),
emit_time,
&producer,
);
}
for degradation in &agg.sandbox_degradations {
let payload =
serde_json::to_value(degradation).context("serialize sandbox degradation payload")?;
push_event(
&mut events,
"assay.sandbox.degraded",
run_id,
payload,
None,
emit_time,
&producer,
);
}
let file = File::create(out)
.with_context(|| format!("create evidence bundle at {}", out.display()))?;
let mut writer = BundleWriter::new(file).with_producer(producer);
for event in events {
writer.add_event(event);
}
writer.finish().context("write sandbox evidence bundle")?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn push_event(
events: &mut Vec<EvidenceEvent>,
type_: &str,
run_id: &str,
payload: serde_json::Value,
subject: Option<String>,
time: DateTime<Utc>,
producer: &ProducerMeta,
) {
let seq = events.len() as u64;
let mut event = EvidenceEvent::new(type_, EVENT_SOURCE, run_id, seq, payload)
.with_time(time)
.with_producer(producer);
if let Some(subject) = subject {
event = event.with_subject(subject);
}
events.push(event);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::profile::events::{BackendHint, FsOp};
use crate::profile::{ProfileAgg, ProfileConfig, ProfileReport};
use std::collections::BTreeMap;
use std::path::PathBuf;
#[test]
fn emit_bundle_produces_a_verifiable_bundle() {
let mut execs = BTreeMap::new();
execs.insert("sh".to_string(), 2);
let agg = ProfileAgg {
fs: vec![(
FsOp::Write,
"/tmp/out.txt".to_string(),
BackendHint::Landlock,
)],
execs,
..Default::default()
};
let report = ProfileReport {
version: 1,
config: ProfileConfig {
cwd: PathBuf::from("/tmp"),
home: None,
assay_tmp: None,
},
agg,
};
let command = vec!["echo".to_string(), "hi".to_string()];
let out = std::env::temp_dir().join(format!(
"assay-sbx-bundle-test-{}.tar.gz",
std::process::id()
));
emit_bundle(&report, &command, "sandbox_testrun", &out).expect("emit bundle");
let file = File::open(&out).expect("open bundle");
let result = assay_evidence::bundle::verify_bundle(file).expect("verify bundle");
assert_eq!(result.event_count, 3);
std::fs::remove_file(&out).ok();
}
}