1use crate::value::VmDictExt;
17use std::collections::BTreeMap;
18use std::rc::Rc;
19use std::sync::Once;
20
21use serde_json::Value as JsonValue;
22
23use crate::event_log::{active_event_log, EventLog, LogEvent, Topic};
24use crate::redact::{
25 self, custom_pattern_names, default_pattern_names, drain_audit_ring, install_audit_sink,
26 register_custom_pattern, RedactionEvent, TOKEN_REDACTION_AUDIT_TOPIC,
27 TOKEN_REDACTION_DIAGNOSTIC,
28};
29use crate::value::{VmError, VmValue};
30use crate::vm::Vm;
31
32static AUDIT_SINK_INIT: Once = Once::new();
33
34pub fn ensure_audit_sink() {
51 AUDIT_SINK_INIT.call_once(|| {
52 install_audit_sink(Some(Rc::new(|event: &RedactionEvent| {
53 forward_event_to_log(event);
54 forward_event_to_events_sink(event);
55 })));
56 });
57}
58
59fn forward_event_to_log(event: &RedactionEvent) {
60 let Some(log) = active_event_log() else {
61 return;
62 };
63 let topic = match Topic::new(TOKEN_REDACTION_AUDIT_TOPIC) {
64 Ok(topic) => topic,
65 Err(_) => return,
66 };
67 let payload = serde_json::json!({
68 "code": TOKEN_REDACTION_DIAGNOSTIC,
69 "pattern": event.pattern_name,
70 "match_count": event.match_count,
71 "bytes_redacted": event.bytes_redacted,
72 });
73 let log_event = LogEvent::new("token_redaction", payload);
74 let log_ref = log;
75 let topic_clone = topic;
76 let owned = log_event;
77 let task = async move {
78 if let Err(error) = log_ref.append(&topic_clone, owned).await {
79 crate::events::log_warn(
80 "token_redaction.audit",
81 &format!("failed to append token redaction audit event: {error}"),
82 );
83 }
84 };
85 if let Ok(handle) = tokio::runtime::Handle::try_current() {
90 let metrics = handle.metrics();
91 if metrics.num_workers() > 1 {
92 handle.spawn(task);
93 }
94 }
95}
96
97fn forward_event_to_events_sink(event: &RedactionEvent) {
98 let metadata: BTreeMap<String, JsonValue> = BTreeMap::from([
99 (
100 "code".to_string(),
101 JsonValue::String(TOKEN_REDACTION_DIAGNOSTIC.to_string()),
102 ),
103 (
104 "pattern".to_string(),
105 JsonValue::String(event.pattern_name.clone()),
106 ),
107 (
108 "match_count".to_string(),
109 JsonValue::Number(event.match_count.into()),
110 ),
111 (
112 "bytes_redacted".to_string(),
113 JsonValue::Number(event.bytes_redacted.into()),
114 ),
115 ]);
116 crate::events::log_info_meta(
117 "token_redaction.audit",
118 "redacted token in persistence sink",
119 metadata,
120 );
121}
122
123pub(crate) fn register_token_redaction_builtins(vm: &mut Vm) {
124 ensure_audit_sink();
125 for def in MODULE_BUILTINS {
126 vm.register_builtin_def(def);
127 }
128}
129
130pub(crate) const MODULE_BUILTINS: &[&crate::stdlib::macros::VmBuiltinDef] = &[
131 &TOKEN_REDACTION_REGISTER_PATTERN_IMPL_DEF,
132 &TOKEN_REDACTION_CLEAR_CUSTOM_PATTERNS_IMPL_DEF,
133 &TOKEN_REDACTION_REDACT_IMPL_DEF,
134 &TOKEN_REDACTION_DEFAULT_PATTERNS_IMPL_DEF,
135 &TOKEN_REDACTION_CUSTOM_PATTERNS_IMPL_DEF,
136 &TOKEN_REDACTION_DRAIN_AUDIT_IMPL_DEF,
137];
138
139#[crate::stdlib::macros::harn_builtin(
140 sig = "__token_redaction_register_pattern(name: string, regex: string) -> nil",
141 category = "token_redaction"
142)]
143fn token_redaction_register_pattern_impl(
144 args: &[VmValue],
145 _out: &mut String,
146) -> Result<VmValue, VmError> {
147 let name = required_string_arg(args, 0, "__token_redaction_register_pattern", "name")?;
148 let regex_source = required_string_arg(args, 1, "__token_redaction_register_pattern", "regex")?;
149 if name.is_empty() {
150 return Err(VmError::Runtime(
151 "__token_redaction_register_pattern: `name` must not be empty".to_string(),
152 ));
153 }
154 register_custom_pattern(&name, ®ex_source).map_err(|message| {
155 VmError::Runtime(format!("token_redaction.register_pattern: {message}"))
156 })?;
157 Ok(VmValue::Nil)
158}
159
160#[crate::stdlib::macros::harn_builtin(
161 sig = "__token_redaction_clear_custom_patterns() -> nil",
162 category = "token_redaction"
163)]
164fn token_redaction_clear_custom_patterns_impl(
165 args: &[VmValue],
166 _out: &mut String,
167) -> Result<VmValue, VmError> {
168 if !args.is_empty() {
169 return Err(VmError::Runtime(
170 "__token_redaction_clear_custom_patterns: expected 0 arguments".to_string(),
171 ));
172 }
173 redact::clear_custom_patterns();
174 Ok(VmValue::Nil)
175}
176
177#[crate::stdlib::macros::harn_builtin(
178 sig = "__token_redaction_redact(text: string) -> string",
179 category = "token_redaction"
180)]
181fn token_redaction_redact_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
182 let text = required_string_arg(args, 0, "__token_redaction_redact", "text")?;
183 let policy = redact::current_policy();
184 let redacted = policy.redact_string(&text).into_owned();
185 Ok(VmValue::String(arcstr::ArcStr::from(redacted.as_str())))
186}
187
188#[crate::stdlib::macros::harn_builtin(
189 sig = "__token_redaction_default_patterns() -> list",
190 category = "token_redaction"
191)]
192fn token_redaction_default_patterns_impl(
193 args: &[VmValue],
194 _out: &mut String,
195) -> Result<VmValue, VmError> {
196 if !args.is_empty() {
197 return Err(VmError::Runtime(
198 "__token_redaction_default_patterns: expected 0 arguments".to_string(),
199 ));
200 }
201 let names: Vec<VmValue> = default_pattern_names()
202 .into_iter()
203 .map(|name| VmValue::String(arcstr::ArcStr::from(name)))
204 .collect();
205 Ok(VmValue::List(std::sync::Arc::new(names)))
206}
207
208#[crate::stdlib::macros::harn_builtin(
209 sig = "__token_redaction_custom_patterns() -> list",
210 category = "token_redaction"
211)]
212fn token_redaction_custom_patterns_impl(
213 args: &[VmValue],
214 _out: &mut String,
215) -> Result<VmValue, VmError> {
216 if !args.is_empty() {
217 return Err(VmError::Runtime(
218 "__token_redaction_custom_patterns: expected 0 arguments".to_string(),
219 ));
220 }
221 let names: Vec<VmValue> = custom_pattern_names()
222 .into_iter()
223 .map(|name| VmValue::String(arcstr::ArcStr::from(name.as_str())))
224 .collect();
225 Ok(VmValue::List(std::sync::Arc::new(names)))
226}
227
228#[crate::stdlib::macros::harn_builtin(
229 sig = "__token_redaction_drain_audit() -> list",
230 category = "token_redaction"
231)]
232fn token_redaction_drain_audit_impl(
233 args: &[VmValue],
234 _out: &mut String,
235) -> Result<VmValue, VmError> {
236 if !args.is_empty() {
237 return Err(VmError::Runtime(
238 "__token_redaction_drain_audit: expected 0 arguments".to_string(),
239 ));
240 }
241 let events = drain_audit_ring();
242 let list: Vec<VmValue> = events
243 .into_iter()
244 .map(|event| {
245 let mut entry: crate::value::DictMap = crate::value::DictMap::new();
246 entry.put_str("code", TOKEN_REDACTION_DIAGNOSTIC);
247 entry.put_str("pattern", event.pattern_name.as_str());
248 entry.insert(
249 crate::value::intern_key("match_count"),
250 VmValue::Int(event.match_count as i64),
251 );
252 entry.insert(
253 crate::value::intern_key("bytes_redacted"),
254 VmValue::Int(event.bytes_redacted as i64),
255 );
256 VmValue::dict(entry)
257 })
258 .collect();
259 Ok(VmValue::List(std::sync::Arc::new(list)))
260}
261
262fn required_string_arg(
263 args: &[VmValue],
264 index: usize,
265 fn_name: &str,
266 arg_name: &str,
267) -> Result<String, VmError> {
268 match args.get(index) {
269 Some(VmValue::String(s)) => Ok(s.to_string()),
270 Some(other) => Err(VmError::Runtime(format!(
271 "{fn_name}: `{arg_name}` must be a string, got {}",
272 other.type_name()
273 ))),
274 None => Err(VmError::Runtime(format!(
275 "{fn_name}: `{arg_name}` argument is required"
276 ))),
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn ensure_audit_sink_is_idempotent() {
286 ensure_audit_sink();
287 ensure_audit_sink();
288 let _ = redact::scan_secret_patterns("AKIAABCDEFGHIJKLMNOP", redact::REDACTED_PLACEHOLDER);
291 }
292}