agent_migrate_core/
secret.rs1use std::collections::BTreeSet;
2
3use serde::{Deserialize, Serialize};
4use serde_json::{Map, Value};
5use thiserror::Error;
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub struct SecretRef {
9 pub name: String,
10 pub logical_location: String,
11 pub required: bool,
12}
13
14#[derive(Debug, Clone, PartialEq)]
15pub struct RedactedTree {
16 pub value: Value,
17 pub references: Vec<SecretRef>,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct SecretPolicy {
22 patterns: BTreeSet<String>,
23}
24
25impl Default for SecretPolicy {
26 fn default() -> Self {
27 Self {
28 patterns: [
29 "api_key",
30 "authorization",
31 "cookie",
32 "credential",
33 "password",
34 "secret",
35 "token",
36 ]
37 .into_iter()
38 .map(str::to_owned)
39 .collect(),
40 }
41 }
42}
43
44impl SecretPolicy {
45 #[must_use]
46 pub fn is_sensitive(&self, key: &str) -> bool {
47 let normalized = key.to_ascii_lowercase();
48 let compact = normalized
49 .chars()
50 .filter(char::is_ascii_alphanumeric)
51 .collect::<String>();
52 self.patterns.iter().any(|pattern| {
53 normalized.contains(pattern) || compact.contains(&pattern.replace(['_', '-'], ""))
54 })
55 }
56}
57
58pub fn redact_json(input: &Value, policy: &SecretPolicy) -> Result<RedactedTree, SecretError> {
64 let mut redactor = Redactor::new(policy);
65 let value = redactor.redact_value(input, "$")?;
66 Ok(RedactedTree {
67 value,
68 references: redactor.references,
69 })
70}
71
72#[derive(Debug)]
73struct Redactor<'a> {
74 policy: &'a SecretPolicy,
75 references: Vec<SecretRef>,
76}
77
78impl<'a> Redactor<'a> {
79 fn new(policy: &'a SecretPolicy) -> Self {
80 Self {
81 policy,
82 references: Vec::new(),
83 }
84 }
85
86 fn redact_value(&mut self, input: &Value, location: &str) -> Result<Value, SecretError> {
87 match input {
88 Value::Object(object) => self.redact_object(object, location),
89 Value::Array(values) => values
90 .iter()
91 .enumerate()
92 .map(|(index, value)| self.redact_value(value, &format!("{location}[{index}]")))
93 .collect::<Result<Vec<_>, _>>()
94 .map(Value::Array),
95 scalar => Ok(scalar.clone()),
96 }
97 }
98
99 fn redact_object(
100 &mut self,
101 input: &Map<String, Value>,
102 location: &str,
103 ) -> Result<Value, SecretError> {
104 let mut output = Map::new();
105 for (key, value) in input {
106 let child_location = format!("{location}.{key}");
107 if self.policy.is_sensitive(key) {
108 let name = reference_name(key)?;
109 self.references.push(SecretRef {
110 name: name.clone(),
111 logical_location: child_location,
112 required: !value.is_null(),
113 });
114 output.insert(
115 key.clone(),
116 Value::String(format!("${{SECRET_REF:{name}}}")),
117 );
118 } else {
119 output.insert(key.clone(), self.redact_value(value, &child_location)?);
120 }
121 }
122 Ok(Value::Object(output))
123 }
124}
125
126fn reference_name(key: &str) -> Result<String, SecretError> {
127 let name = key
128 .chars()
129 .map(|character| {
130 if character.is_ascii_alphanumeric() {
131 character.to_ascii_uppercase()
132 } else {
133 '_'
134 }
135 })
136 .collect::<String>();
137 if name.is_empty() {
138 return Err(SecretError::EmptyReferenceName);
139 }
140 Ok(name)
141}
142
143#[derive(Debug, Error, PartialEq, Eq)]
144pub enum SecretError {
145 #[error("secret reference name cannot be empty")]
146 EmptyReferenceName,
147}