Skip to main content

agent_migrate_core/
secret.rs

1use 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
58/// Redacts a JSON boundary value and records logical secret references.
59///
60/// # Errors
61///
62/// Returns an error when a secret key cannot form a stable reference name.
63pub 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}