use std::sync::Arc;
use procage_policy::{Base, Construct, HardDenyClass, Policy, PresetTag, Relaxed};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::violation::{Coverage, Violations};
const VERSION: u32 = 1;
pub fn policy_digest(policy: &Policy) -> String {
let canonical = canonical(policy);
let json = serde_json::to_vec(&canonical)
.unwrap_or_else(|_| unreachable!("a policy always serialises: its fields are plain data"));
let digest = Sha256::digest(&json);
let mut out = String::from("sha256:");
for byte in digest {
out.push_str(&format!("{byte:02x}"));
}
out
}
fn canonical(policy: &Policy) -> Policy {
let mut p = policy.clone();
p.fs.sort();
p.fs.dedup();
p.grants.sort();
p.grants.dedup();
p.ipc.unix_sockets.sort();
p.ipc.unix_sockets.dedup();
match &mut p.env {
procage_policy::EnvPolicy::Allowlist(names) => {
names.sort();
names.dedup();
}
procage_policy::EnvPolicy::Passthrough { deny } => {
deny.sort();
deny.dedup();
}
}
if let Some(layer) = &mut p.linux {
layer.syscalls_deny.sort();
layer.syscalls_deny.dedup();
layer.landlock_extra.sort();
layer.landlock_extra.dedup();
}
p
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct Receipt {
pub version: u32,
pub procage: String,
pub policy_digest: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub policy: Option<Policy>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub preset: Option<PresetTag>,
pub base: Base,
pub grants: Vec<String>,
pub backend: ReceiptBackend,
pub approximations: Vec<ReceiptApproximation>,
pub relaxations: Vec<ReceiptRelaxation>,
pub waivers: Vec<HardDenyClass>,
pub enforced_as_written: bool,
pub violations: ReceiptViolations,
pub exit: ReceiptExit,
pub audit: ReceiptAudit,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReceiptAudit {
pub links_mode: String,
pub links: usize,
pub links_complete: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub handoff: Option<usize>,
pub handoff_complete: bool,
pub visited: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub paths: Option<Vec<ReceiptPath>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ReceiptPath {
Text(String),
Bytes(Vec<u8>),
}
impl From<&std::path::Path> for ReceiptPath {
fn from(path: &std::path::Path) -> Self {
use std::os::unix::ffi::OsStrExt;
match std::str::from_utf8(path.as_os_str().as_bytes()) {
Ok(text) => ReceiptPath::Text(text.into()),
Err(_) => ReceiptPath::Bytes(path.as_os_str().as_bytes().to_vec()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "system", rename_all = "kebab-case", deny_unknown_fields)]
#[non_exhaustive]
pub enum ReceiptBackend {
#[serde(rename = "macos")]
MacOs {
apply: String,
os_release: String,
os_build: String,
},
Linux {
tier: String,
landlock_abi: u8,
errata: u32,
kernel: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReceiptApproximation {
pub construct: Construct,
pub note: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReceiptRelaxation {
pub construct: Construct,
pub relaxation: String,
pub lift: ReceiptLift,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReceiptLift {
pub tier1: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub landlock_abi: Option<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum ReceiptCoverage {
Off,
Observed,
SystemCalls,
Unavailable,
Unsupported,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReceiptViolations {
pub coverage: ReceiptCoverage,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
pub complete: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub count: Option<usize>,
pub dropped: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReceiptExit {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub code: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signal: Option<i32>,
pub ended_by_caller: bool,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ReceiptError {
UnsupportedVersion(u64),
Invalid(String),
}
impl std::fmt::Display for ReceiptError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReceiptError::UnsupportedVersion(v) => write!(f, "unsupported receipt version {v}"),
ReceiptError::Invalid(e) => write!(f, "not a receipt this version can read: {e}"),
}
}
}
impl std::error::Error for ReceiptError {}
impl Receipt {
pub fn to_json(&self) -> String {
serde_json::to_string(self).unwrap_or_else(|_| unreachable!("a receipt always serialises"))
}
pub fn from_json(text: &str) -> Result<Receipt, ReceiptError> {
let invalid = |e: serde_json::Error| ReceiptError::Invalid(e.to_string());
let value: serde_json::Value = serde_json::from_str(text).map_err(invalid)?;
match value.get("version").and_then(serde_json::Value::as_u64) {
Some(v) if v == u64::from(VERSION) => {}
Some(v) => return Err(ReceiptError::UnsupportedVersion(v)),
None => return Err(ReceiptError::Invalid("no version".into())),
}
serde_json::from_value(value).map_err(invalid)
}
}
#[derive(Debug)]
pub(crate) struct CageFacts {
digest: String,
policy: Option<Policy>,
preset: Option<PresetTag>,
base: Base,
backend: ReceiptBackend,
approximations: Vec<ReceiptApproximation>,
relaxations: Vec<ReceiptRelaxation>,
waivers: Vec<HardDenyClass>,
}
impl CageFacts {
pub(crate) fn new(
policy: &Policy,
include_policy: bool,
backend: ReceiptBackend,
approximations: Vec<ReceiptApproximation>,
relaxed: &[Relaxed],
) -> CageFacts {
let relaxations = relaxed
.iter()
.map(|r| ReceiptRelaxation {
construct: r.gap.construct,
relaxation: r.relaxation.to_string(),
lift: ReceiptLift {
tier1: r.lift.tier1,
landlock_abi: r.lift.landlock_abi,
},
subject: include_policy.then(|| r.gap.subject.clone()).flatten(),
})
.collect();
CageFacts {
digest: policy_digest(policy),
policy: include_policy.then(|| policy.clone()),
preset: policy.preset.clone(),
base: policy.base,
backend,
approximations,
relaxations,
waivers: policy.waive.iter().copied().collect(),
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct RunFacts {
pub(crate) cage: Arc<CageFacts>,
pub(crate) grants: Vec<String>,
approximations: Vec<ReceiptApproximation>,
}
impl RunFacts {
pub(crate) fn new(cage: Arc<CageFacts>, mut grants: Vec<String>) -> RunFacts {
grants.sort();
grants.dedup();
RunFacts {
cage,
grants,
approximations: Vec::new(),
}
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub(crate) fn with_approximation(mut self, approximation: ReceiptApproximation) -> RunFacts {
let listed = |a: &ReceiptApproximation| a.construct == approximation.construct;
if !self.cage.approximations.iter().any(listed) && !self.approximations.iter().any(listed) {
self.approximations.push(approximation);
}
self
}
pub(crate) fn receipt(
&self,
status: std::process::ExitStatus,
record: &Violations,
ended_by_caller: bool,
audit: &crate::Audit,
) -> Receipt {
use std::os::unix::process::ExitStatusExt;
let c = &self.cage;
let mut approximations = c.approximations.clone();
approximations.extend(self.approximations.iter().cloned());
approximations.sort_by_key(|a| a.construct);
let (coverage, reason, observed) = match record.coverage() {
Coverage::Off => (ReceiptCoverage::Off, None, false),
Coverage::Observed => (ReceiptCoverage::Observed, None, true),
Coverage::SystemCalls => (ReceiptCoverage::SystemCalls, None, true),
Coverage::Unavailable(why) => (ReceiptCoverage::Unavailable, Some(why.clone()), false),
_ => (ReceiptCoverage::Unsupported, None, false),
};
Receipt {
version: VERSION,
procage: env!("CARGO_PKG_VERSION").into(),
policy_digest: c.digest.clone(),
policy: c.policy.clone(),
preset: c.preset.clone(),
base: c.base,
grants: self.grants.clone(),
backend: c.backend.clone(),
enforced_as_written: approximations.is_empty()
&& c.relaxations.is_empty()
&& c.waivers.is_empty(),
approximations,
relaxations: c.relaxations.clone(),
waivers: c.waivers.clone(),
violations: ReceiptViolations {
coverage,
reason,
complete: record.complete(),
count: observed.then(|| record.len()),
dropped: record.dropped(),
},
exit: ReceiptExit {
code: status.code(),
signal: if status.code().is_some() {
None
} else {
status.signal()
},
ended_by_caller,
},
audit: ReceiptAudit {
links_mode: match audit.mode {
crate::LinkAudit::Refuse => "refuse",
crate::LinkAudit::Report => "report",
_ => "off",
}
.into(),
links: audit.links.len(),
links_complete: audit.links_complete,
handoff: audit.handoff.as_ref().map(Vec::len),
handoff_complete: audit.handoff_complete,
visited: audit.links_visited + audit.handoff_visited,
paths: c.policy.is_some().then(|| {
audit
.links
.iter()
.map(|f| ReceiptPath::from(f.path.as_path()))
.chain(
audit
.handoff
.iter()
.flatten()
.map(|f| ReceiptPath::from(f.path.as_path())),
)
.collect()
}),
},
}
}
}
pub(crate) fn approximations(
degradations: &[procage_policy::Degradation],
) -> Vec<ReceiptApproximation> {
let mut out: Vec<ReceiptApproximation> = degradations
.iter()
.map(|d| ReceiptApproximation {
construct: d.construct,
note: d.note.into(),
})
.collect();
out.sort_by_key(|a| a.construct);
out.dedup();
out
}