hessra_context_token/
inspect.rs1extern crate biscuit_auth as biscuit;
2
3use biscuit::macros::authorizer;
4use chrono::Utc;
5use hessra_token_core::{Biscuit, PublicKey, TokenError};
6
7#[derive(Debug, Clone)]
9pub struct ContextInspectResult {
10 pub subject: String,
12 pub taint_labels: Vec<String>,
14 pub taint_sources: Vec<String>,
16 pub expiry: Option<i64>,
18 pub is_expired: bool,
20 pub taint_block_count: usize,
22}
23
24pub fn inspect_context_token(
36 token: String,
37 public_key: PublicKey,
38) -> Result<ContextInspectResult, TokenError> {
39 let biscuit = Biscuit::from_base64(&token, public_key)?;
40 let now = Utc::now().timestamp();
41
42 let authorizer = authorizer!(
44 r#"
45 time({now});
46 allow if true;
47 "#
48 );
49
50 let mut authorizer = authorizer
51 .build(&biscuit)
52 .map_err(|e| TokenError::internal(format!("failed to build authorizer: {e}")))?;
53
54 let subjects: Vec<(String,)> = authorizer
55 .query("data($name) <- context($name)")
56 .map_err(|e| TokenError::internal(format!("failed to query context subject: {e}")))?;
57
58 let subject = subjects.first().map(|(s,)| s.clone()).unwrap_or_default();
59
60 let mut taint_labels = Vec::new();
62 let mut taint_sources = Vec::new();
63 let mut taint_block_count = 0;
64
65 let block_count = biscuit.block_count();
66 for i in 0..block_count {
67 let block_source = biscuit.print_block_source(i).unwrap_or_default();
68 let mut block_has_taint = false;
69
70 for line in block_source.lines() {
71 let trimmed = line.trim();
72 if let Some(rest) = trimmed.strip_prefix("taint(") {
73 if let Some(label_str) = rest.strip_suffix(");") {
74 let label = label_str.trim_matches('"').to_string();
75 if !taint_labels.contains(&label) {
76 taint_labels.push(label);
77 }
78 block_has_taint = true;
79 }
80 }
81 if let Some(rest) = trimmed.strip_prefix("taint_source(") {
82 if let Some(source_str) = rest.strip_suffix(");") {
83 let source = source_str.trim_matches('"').to_string();
84 if !taint_sources.contains(&source) {
85 taint_sources.push(source);
86 }
87 }
88 }
89 }
90
91 if block_has_taint {
92 taint_block_count += 1;
93 }
94 }
95
96 let token_content = biscuit.print();
98 let expiry = extract_expiry_from_content(&token_content);
99 let is_expired = expiry.is_some_and(|exp| exp < now);
100
101 Ok(ContextInspectResult {
102 subject,
103 taint_labels,
104 taint_sources,
105 expiry,
106 is_expired,
107 taint_block_count,
108 })
109}
110
111fn extract_expiry_from_content(content: &str) -> Option<i64> {
113 let mut earliest_expiry: Option<i64> = None;
114
115 for line in content.lines() {
116 if line.contains("check if") && line.contains("time") && line.contains("<") {
117 if let Some(pos) = line.find("$time <") {
118 let after_lt = &line[pos + 8..].trim();
119 let number_str = after_lt
120 .chars()
121 .take_while(|c| c.is_ascii_digit() || *c == '-')
122 .collect::<String>();
123
124 if let Ok(timestamp) = number_str.parse::<i64>() {
125 earliest_expiry = Some(earliest_expiry.map_or(timestamp, |e| e.min(timestamp)));
126 }
127 }
128 }
129 }
130
131 earliest_expiry
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::mint::HessraContext;
138 use crate::taint::add_taint;
139 use hessra_token_core::{KeyPair, TokenTimeConfig};
140
141 #[test]
142 fn test_inspect_fresh_context() {
143 let keypair = KeyPair::new();
144 let public_key = keypair.public();
145
146 let token = HessraContext::new("agent:openclaw".to_string(), TokenTimeConfig::default())
147 .issue(&keypair)
148 .expect("Failed to create context token");
149
150 let result =
151 inspect_context_token(token, public_key).expect("Failed to inspect context token");
152
153 assert_eq!(result.subject, "agent:openclaw");
154 assert!(result.taint_labels.is_empty());
155 assert!(result.taint_sources.is_empty());
156 assert!(!result.is_expired);
157 assert!(result.expiry.is_some());
158 assert_eq!(result.taint_block_count, 0);
159 }
160
161 #[test]
162 fn test_inspect_tainted_context() {
163 let keypair = KeyPair::new();
164 let public_key = keypair.public();
165
166 let token = HessraContext::new("agent:openclaw".to_string(), TokenTimeConfig::default())
167 .issue(&keypair)
168 .expect("Failed to create context token");
169
170 let tainted = add_taint(
171 &token,
172 public_key,
173 &["PII:SSN".to_string()],
174 "data:user-ssn".to_string(),
175 )
176 .expect("Failed to add taint");
177
178 let result =
179 inspect_context_token(tainted, public_key).expect("Failed to inspect tainted context");
180
181 assert_eq!(result.subject, "agent:openclaw");
182 assert_eq!(result.taint_labels, vec!["PII:SSN".to_string()]);
183 assert_eq!(result.taint_sources, vec!["data:user-ssn".to_string()]);
184 assert_eq!(result.taint_block_count, 1);
185 }
186
187 #[test]
188 fn test_inspect_multi_tainted_context() {
189 let keypair = KeyPair::new();
190 let public_key = keypair.public();
191
192 let token = HessraContext::new("agent:openclaw".to_string(), TokenTimeConfig::default())
193 .issue(&keypair)
194 .expect("Failed to create context token");
195
196 let tainted = add_taint(
197 &token,
198 public_key,
199 &["PII:email".to_string(), "PII:address".to_string()],
200 "data:user-profile".to_string(),
201 )
202 .expect("Failed to add profile taint");
203
204 let more_tainted = add_taint(
205 &tainted,
206 public_key,
207 &["PII:SSN".to_string()],
208 "data:user-ssn".to_string(),
209 )
210 .expect("Failed to add SSN taint");
211
212 let result = inspect_context_token(more_tainted, public_key)
213 .expect("Failed to inspect multi-tainted context");
214
215 assert_eq!(result.subject, "agent:openclaw");
216 assert_eq!(result.taint_labels.len(), 3);
217 assert!(result.taint_labels.contains(&"PII:email".to_string()));
218 assert!(result.taint_labels.contains(&"PII:address".to_string()));
219 assert!(result.taint_labels.contains(&"PII:SSN".to_string()));
220 assert_eq!(result.taint_sources.len(), 2);
221 assert_eq!(result.taint_block_count, 2);
222 }
223
224 #[test]
225 fn test_inspect_expired_context() {
226 let keypair = KeyPair::new();
227 let public_key = keypair.public();
228
229 let expired_config = TokenTimeConfig {
230 start_time: Some(0),
231 duration: 1,
232 };
233
234 let token = HessraContext::new("agent:test".to_string(), expired_config)
235 .issue(&keypair)
236 .expect("Failed to create expired context token");
237
238 let result = inspect_context_token(token, public_key)
239 .expect("Should be able to inspect expired token");
240
241 assert_eq!(result.subject, "agent:test");
242 assert!(result.is_expired);
243 assert_eq!(result.expiry, Some(1));
244 }
245}