use crate::constraints::Constraint;
use crate::warrant::{Clearance, Warrant};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum ChangeType {
Unchanged,
Added,
Removed,
Narrowed,
Reduced,
Increased,
Demoted,
Promoted,
Dropped,
}
impl ChangeType {
pub fn as_str(&self) -> &'static str {
match self {
ChangeType::Unchanged => "unchanged",
ChangeType::Added => "added",
ChangeType::Removed => "removed",
ChangeType::Narrowed => "narrowed",
ChangeType::Reduced => "reduced",
ChangeType::Increased => "increased",
ChangeType::Demoted => "demoted",
ChangeType::Promoted => "promoted",
ChangeType::Dropped => "dropped",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolsDiff {
pub parent_tools: Vec<String>,
pub child_tools: Vec<String>,
pub kept: Vec<String>,
pub dropped: Vec<String>,
}
impl ToolsDiff {
pub fn new(parent_tools: Vec<String>, child_tools: Vec<String>) -> Self {
let kept: Vec<String> = child_tools
.iter()
.filter(|t| parent_tools.contains(t))
.cloned()
.collect();
let dropped: Vec<String> = parent_tools
.iter()
.filter(|t| !child_tools.contains(t))
.cloned()
.collect();
Self {
parent_tools,
child_tools,
kept,
dropped,
}
}
pub fn has_changes(&self) -> bool {
!self.dropped.is_empty()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConstraintDiff {
pub field: String,
pub parent_constraint: Option<Constraint>,
pub child_constraint: Option<Constraint>,
pub change: ChangeType,
}
impl ConstraintDiff {
pub fn new(
field: String,
parent_constraint: Option<Constraint>,
child_constraint: Option<Constraint>,
) -> Self {
let change = match (&parent_constraint, &child_constraint) {
(None, Some(_)) => ChangeType::Added,
(Some(_), None) => ChangeType::Removed,
(Some(p), Some(c)) if p != c => ChangeType::Narrowed,
_ => ChangeType::Unchanged,
};
Self {
field,
parent_constraint,
child_constraint,
change,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TtlDiff {
pub parent_ttl_seconds: Option<i64>,
pub child_ttl_seconds: Option<i64>,
pub change: ChangeType,
}
impl TtlDiff {
pub fn new(parent_ttl_seconds: Option<i64>, child_ttl_seconds: Option<i64>) -> Self {
let change = match (parent_ttl_seconds, child_ttl_seconds) {
(Some(p), Some(c)) if c < p => ChangeType::Reduced,
(Some(p), Some(c)) if c > p => ChangeType::Increased,
_ => ChangeType::Unchanged,
};
Self {
parent_ttl_seconds,
child_ttl_seconds,
change,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClearanceDiff {
pub parent_clearance: Option<Clearance>,
pub child_clearance: Option<Clearance>,
pub change: ChangeType,
}
impl ClearanceDiff {
pub fn new(parent_clearance: Option<Clearance>, child_clearance: Option<Clearance>) -> Self {
let change = match (parent_clearance, child_clearance) {
(Some(p), Some(c)) if c < p => ChangeType::Demoted,
(Some(p), Some(c)) if c > p => ChangeType::Promoted,
_ => ChangeType::Unchanged,
};
Self {
parent_clearance,
child_clearance,
change,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DepthDiff {
pub parent_depth: u32,
pub child_depth: u32,
pub is_terminal: bool,
}
impl DepthDiff {
pub fn new(parent_depth: u32, child_depth: u32, max_depth: Option<u32>) -> Self {
let is_terminal = max_depth.map(|m| child_depth >= m).unwrap_or(false);
Self {
parent_depth,
child_depth,
is_terminal,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DelegationDiff {
pub parent_warrant_id: String,
pub child_warrant_id: Option<String>,
pub timestamp: DateTime<Utc>,
pub tools: ToolsDiff,
pub capabilities: HashMap<String, HashMap<String, ConstraintDiff>>,
pub ttl: TtlDiff,
pub clearance: ClearanceDiff,
pub depth: DepthDiff,
pub intent: Option<String>,
}
impl DelegationDiff {
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn to_human(&self) -> String {
let mut lines = Vec::new();
let width = 68;
lines.push(format!("╔{}╗", "═".repeat(width)));
lines.push(format!(
"║ DELEGATION DIFF{:width$}║",
"",
width = width - 17
));
let child_id = self.child_warrant_id.as_deref().unwrap_or("(pending)");
let header = format!(
" Parent: {} → Child: {}",
&self.parent_warrant_id, child_id
);
let padding = width.saturating_sub(header.len());
lines.push(format!("║{}{:padding$}║", header, "", padding = padding));
lines.push(format!("╠{}╣", "═".repeat(width)));
lines.push(format!("║{:width$}║", "", width = width));
lines.push(format!("║ TOOLS{:width$}║", "", width = width - 7));
for tool in &self.tools.child_tools {
let line = format!(" ✓ {}", tool);
let padding = width.saturating_sub(line.len());
lines.push(format!("║{}{:padding$}║", line, "", padding = padding));
}
for tool in &self.tools.dropped {
let line = format!(" ✗ {} DROPPED", tool);
let padding = width.saturating_sub(line.len());
lines.push(format!("║{}{:padding$}║", line, "", padding = padding));
}
lines.push(format!("║{:width$}║", "", width = width));
if !self.capabilities.is_empty() {
lines.push(format!("║ CAPABILITIES{:width$}║", "", width = width - 14));
for (tool, tool_constraints) in &self.capabilities {
let line = format!(" TOOL: {}", tool);
let padding = width.saturating_sub(line.len());
lines.push(format!("║{}{:padding$}║", line, "", padding = padding));
for (field, diff) in tool_constraints {
let line = format!(" {}", field);
let padding = width.saturating_sub(line.len());
lines.push(format!("║{}{:padding$}║", line, "", padding = padding));
if let Some(ref pc) = diff.parent_constraint {
let line = format!(" parent: {:?}", pc);
let padding = width.saturating_sub(line.len());
lines.push(format!("║{}{:padding$}║", line, "", padding = padding));
}
if let Some(ref cc) = diff.child_constraint {
let line = format!(" child: {:?}", cc);
let padding = width.saturating_sub(line.len());
lines.push(format!("║{}{:padding$}║", line, "", padding = padding));
}
let line = format!(" change: {}", diff.change.as_str().to_uppercase());
let padding = width.saturating_sub(line.len());
lines.push(format!("║{}{:padding$}║", line, "", padding = padding));
}
}
lines.push(format!("║{:width$}║", "", width = width));
}
lines.push(format!("║ TTL{:width$}║", "", width = width - 5));
if let Some(parent_ttl) = self.ttl.parent_ttl_seconds {
let line = format!(" parent: {}s remaining", parent_ttl);
let padding = width.saturating_sub(line.len());
lines.push(format!("║{}{:padding$}║", line, "", padding = padding));
}
if let Some(child_ttl) = self.ttl.child_ttl_seconds {
let line = format!(" child: {}s", child_ttl);
let padding = width.saturating_sub(line.len());
lines.push(format!("║{}{:padding$}║", line, "", padding = padding));
}
if self.ttl.change != ChangeType::Unchanged {
let line = format!(" change: {}", self.ttl.change.as_str().to_uppercase());
let padding = width.saturating_sub(line.len());
lines.push(format!("║{}{:padding$}║", line, "", padding = padding));
}
lines.push(format!("║{:width$}║", "", width = width));
lines.push(format!("║ CLEARANCE{:width$}║", "", width = width - 11));
if let Some(ref pc) = self.clearance.parent_clearance {
let line = format!(" parent: {}", pc);
let padding = width.saturating_sub(line.len());
lines.push(format!("║{}{:padding$}║", line, "", padding = padding));
}
if let Some(ref cc) = self.clearance.child_clearance {
let line = format!(" child: {}", cc);
let padding = width.saturating_sub(line.len());
lines.push(format!("║{}{:padding$}║", line, "", padding = padding));
}
if self.clearance.change != ChangeType::Unchanged {
let line = format!(
" change: {}",
self.clearance.change.as_str().to_uppercase()
);
let padding = width.saturating_sub(line.len());
lines.push(format!("║{}{:padding$}║", line, "", padding = padding));
}
lines.push(format!("║{:width$}║", "", width = width));
lines.push(format!("║ DEPTH{:width$}║", "", width = width - 7));
let line = format!(" parent: {}", self.depth.parent_depth);
let padding = width.saturating_sub(line.len());
lines.push(format!("║{}{:padding$}║", line, "", padding = padding));
let terminal_str = if self.depth.is_terminal {
"(terminal)"
} else {
"(non-terminal)"
};
let line = format!(" child: {} {}", self.depth.child_depth, terminal_str);
let padding = width.saturating_sub(line.len());
lines.push(format!("║{}{:padding$}║", line, "", padding = padding));
lines.push(format!("╚{}╝", "═".repeat(width)));
lines.join("\n")
}
pub fn to_siem_json(&self) -> Result<String, serde_json::Error> {
let mut deltas = Vec::new();
if !self.tools.dropped.is_empty() {
deltas.push(serde_json::json!({
"field": "tools",
"change": "dropped",
"value": self.tools.dropped
}));
}
for (tool, tool_constraints) in &self.capabilities {
for (field, diff) in tool_constraints {
if diff.change != ChangeType::Unchanged {
let mut delta = serde_json::json!({
"field": format!("capabilities.{}.{}", tool, field),
"change": diff.change.as_str()
});
if let Some(ref pc) = diff.parent_constraint {
delta["from"] = serde_json::json!(format!("{:?}", pc));
}
if let Some(ref cc) = diff.child_constraint {
delta["to"] = serde_json::json!(format!("{:?}", cc));
}
deltas.push(delta);
}
}
}
if self.ttl.change != ChangeType::Unchanged {
let mut delta = serde_json::json!({
"field": "ttl",
"change": self.ttl.change.as_str()
});
if let Some(pt) = self.ttl.parent_ttl_seconds {
delta["from"] = serde_json::json!(pt);
}
if let Some(ct) = self.ttl.child_ttl_seconds {
delta["to"] = serde_json::json!(ct);
}
deltas.push(delta);
}
if self.clearance.change != ChangeType::Unchanged {
let mut delta = serde_json::json!({
"field": "clearance",
"change": self.clearance.change.as_str()
});
if let Some(ref pc) = self.clearance.parent_clearance {
delta["from"] = serde_json::json!(format!("{}", pc));
}
if let Some(ref cc) = self.clearance.child_clearance {
delta["to"] = serde_json::json!(format!("{}", cc));
}
deltas.push(delta);
}
let output = serde_json::json!({
"event_type": "tenuo.delegation",
"parent_warrant_id": self.parent_warrant_id,
"child_warrant_id": self.child_warrant_id,
"warrant_type": "EXECUTION",
"intent": self.intent,
"deltas": deltas,
"summary": {
"tools_dropped": self.tools.dropped,
"tools_kept": self.tools.kept,
"capabilities_changed": self.capabilities.keys().count(),
"ttl_reduced": self.ttl.change == ChangeType::Reduced,
"clearance_demoted": self.clearance.change == ChangeType::Demoted,
"is_terminal": self.depth.is_terminal
}
});
serde_json::to_string_pretty(&output)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DelegationReceipt {
pub parent_warrant_id: String,
pub child_warrant_id: String,
pub timestamp: DateTime<Utc>,
pub tools: ToolsDiff,
pub capabilities: HashMap<String, HashMap<String, ConstraintDiff>>,
pub ttl: TtlDiff,
pub clearance: ClearanceDiff,
pub depth: DepthDiff,
pub delegator_fingerprint: String,
pub delegatee_fingerprint: String,
pub intent: Option<String>,
pub used_pass_through: bool,
pub pass_through_reason: Option<String>,
}
impl DelegationReceipt {
pub fn from_diff(
diff: DelegationDiff,
child_warrant_id: String,
delegator_fingerprint: String,
delegatee_fingerprint: String,
) -> Self {
Self {
parent_warrant_id: diff.parent_warrant_id,
child_warrant_id,
timestamp: diff.timestamp,
tools: diff.tools,
capabilities: diff.capabilities,
ttl: diff.ttl,
clearance: diff.clearance,
depth: diff.depth,
delegator_fingerprint,
delegatee_fingerprint,
intent: diff.intent,
used_pass_through: false,
pass_through_reason: None,
}
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn to_siem_json(&self) -> Result<String, serde_json::Error> {
let mut deltas = Vec::new();
if !self.tools.dropped.is_empty() {
deltas.push(serde_json::json!({
"field": "tools",
"change": "dropped",
"value": self.tools.dropped
}));
}
for (tool, tool_constraints) in &self.capabilities {
for (field, diff) in tool_constraints {
if diff.change != ChangeType::Unchanged {
let mut delta = serde_json::json!({
"field": format!("capabilities.{}.{}", tool, field),
"change": diff.change.as_str()
});
if let Some(ref pc) = diff.parent_constraint {
delta["from"] = serde_json::json!(format!("{:?}", pc));
}
if let Some(ref cc) = diff.child_constraint {
delta["to"] = serde_json::json!(format!("{:?}", cc));
}
deltas.push(delta);
}
}
}
if self.ttl.change != ChangeType::Unchanged {
let mut delta = serde_json::json!({
"field": "ttl",
"change": self.ttl.change.as_str()
});
if let Some(pt) = self.ttl.parent_ttl_seconds {
delta["from"] = serde_json::json!(pt);
}
if let Some(ct) = self.ttl.child_ttl_seconds {
delta["to"] = serde_json::json!(ct);
}
deltas.push(delta);
}
if self.clearance.change != ChangeType::Unchanged {
let mut delta = serde_json::json!({
"field": "clearance",
"change": self.clearance.change.as_str()
});
if let Some(ref pt) = self.clearance.parent_clearance {
delta["from"] = serde_json::json!(format!("{:?}", pt));
}
if let Some(ref ct) = self.clearance.child_clearance {
delta["to"] = serde_json::json!(format!("{:?}", ct));
}
deltas.push(delta);
}
let output = serde_json::json!({
"event_type": "tenuo.delegation.complete",
"parent_warrant_id": self.parent_warrant_id,
"child_warrant_id": self.child_warrant_id,
"warrant_type": "EXECUTION",
"intent": self.intent,
"delegator_fingerprint": self.delegator_fingerprint,
"delegatee_fingerprint": self.delegatee_fingerprint,
"deltas": deltas,
"summary": {
"tools_dropped": self.tools.dropped,
"tools_kept": self.tools.kept,
"capabilities_changed": self.capabilities.keys().count(),
"ttl_reduced": self.ttl.change == ChangeType::Reduced,
"clearance_demoted": self.clearance.change == ChangeType::Demoted,
"is_terminal": self.depth.is_terminal,
"used_pass_through": self.used_pass_through
}
});
serde_json::to_string_pretty(&output)
}
}
pub fn compute_diff(parent: &Warrant, child: &Warrant) -> DelegationDiff {
let parent_tools = parent.tools();
let child_tools = child.tools();
let tools = ToolsDiff::new(parent_tools, child_tools);
let mut capabilities: HashMap<String, HashMap<String, ConstraintDiff>> = HashMap::new();
let mut all_tools: Vec<String> = Vec::new();
if let Some(p_caps) = parent.capabilities() {
for tool in p_caps.keys() {
all_tools.push(tool.clone());
}
}
if let Some(c_caps) = child.capabilities() {
for tool in c_caps.keys() {
if !all_tools.contains(tool) {
all_tools.push(tool.clone());
}
}
}
for tool in all_tools {
let parent_constraints = parent
.capabilities()
.and_then(|c| c.get(&tool))
.cloned()
.unwrap_or_default();
let child_constraints = child
.capabilities()
.and_then(|c| c.get(&tool))
.cloned()
.unwrap_or_default();
let mut tool_diffs = HashMap::new();
let mut all_fields: Vec<String> = Vec::new();
for (field, _) in parent_constraints.iter() {
all_fields.push(field.clone());
}
for (field, _) in child_constraints.iter() {
if !all_fields.contains(field) {
all_fields.push(field.clone());
}
}
for field in all_fields {
let pc = parent_constraints.get(&field).cloned();
let cc = child_constraints.get(&field).cloned();
tool_diffs.insert(field.clone(), ConstraintDiff::new(field, pc, cc));
}
if !tool_diffs.is_empty() {
capabilities.insert(tool, tool_diffs);
}
}
let now = Utc::now();
let parent_ttl = (parent.expires_at() - now).num_seconds();
let child_ttl = (child.expires_at() - now).num_seconds();
let ttl = TtlDiff::new(Some(parent_ttl.max(0)), Some(child_ttl.max(0)));
let clearance = ClearanceDiff::new(parent.clearance(), child.clearance());
let depth = DepthDiff::new(parent.depth(), child.depth(), parent.max_depth());
DelegationDiff {
parent_warrant_id: parent.id().to_string(),
child_warrant_id: Some(child.id().to_string()),
timestamp: Utc::now(),
tools,
capabilities,
ttl,
clearance,
depth,
intent: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_change_type_as_str() {
assert_eq!(ChangeType::Unchanged.as_str(), "unchanged");
assert_eq!(ChangeType::Narrowed.as_str(), "narrowed");
assert_eq!(ChangeType::Reduced.as_str(), "reduced");
assert_eq!(ChangeType::Demoted.as_str(), "demoted");
}
#[test]
fn test_tools_diff() {
let parent = vec![
"read".to_string(),
"write".to_string(),
"delete".to_string(),
];
let child = vec!["read".to_string(), "write".to_string()];
let diff = ToolsDiff::new(parent, child);
assert_eq!(diff.kept, vec!["read", "write"]);
assert_eq!(diff.dropped, vec!["delete"]);
assert!(diff.has_changes());
}
#[test]
fn test_ttl_diff() {
let diff = TtlDiff::new(Some(3600), Some(60));
assert_eq!(diff.change, ChangeType::Reduced);
let diff = TtlDiff::new(Some(60), Some(3600));
assert_eq!(diff.change, ChangeType::Increased);
let diff = TtlDiff::new(Some(60), Some(60));
assert_eq!(diff.change, ChangeType::Unchanged);
}
#[test]
fn test_clearance_diff() {
let diff = ClearanceDiff::new(Some(Clearance::SYSTEM), Some(Clearance::EXTERNAL));
assert_eq!(diff.change, ChangeType::Demoted);
let diff = ClearanceDiff::new(Some(Clearance::EXTERNAL), Some(Clearance::SYSTEM));
assert_eq!(diff.change, ChangeType::Promoted);
}
}