use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SecretRef {
pub name: String,
pub logical_location: String,
pub required: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RedactedTree {
pub value: Value,
pub references: Vec<SecretRef>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SecretPolicy {
patterns: BTreeSet<String>,
}
impl Default for SecretPolicy {
fn default() -> Self {
Self {
patterns: [
"api_key",
"authorization",
"cookie",
"credential",
"password",
"secret",
"token",
]
.into_iter()
.map(str::to_owned)
.collect(),
}
}
}
impl SecretPolicy {
#[must_use]
pub fn is_sensitive(&self, key: &str) -> bool {
let normalized = key.to_ascii_lowercase();
let compact = normalized
.chars()
.filter(char::is_ascii_alphanumeric)
.collect::<String>();
self.patterns.iter().any(|pattern| {
normalized.contains(pattern) || compact.contains(&pattern.replace(['_', '-'], ""))
})
}
}
pub fn redact_json(input: &Value, policy: &SecretPolicy) -> Result<RedactedTree, SecretError> {
let mut redactor = Redactor::new(policy);
let value = redactor.redact_value(input, "$")?;
Ok(RedactedTree {
value,
references: redactor.references,
})
}
#[derive(Debug)]
struct Redactor<'a> {
policy: &'a SecretPolicy,
references: Vec<SecretRef>,
}
impl<'a> Redactor<'a> {
fn new(policy: &'a SecretPolicy) -> Self {
Self {
policy,
references: Vec::new(),
}
}
fn redact_value(&mut self, input: &Value, location: &str) -> Result<Value, SecretError> {
match input {
Value::Object(object) => self.redact_object(object, location),
Value::Array(values) => values
.iter()
.enumerate()
.map(|(index, value)| self.redact_value(value, &format!("{location}[{index}]")))
.collect::<Result<Vec<_>, _>>()
.map(Value::Array),
scalar => Ok(scalar.clone()),
}
}
fn redact_object(
&mut self,
input: &Map<String, Value>,
location: &str,
) -> Result<Value, SecretError> {
let mut output = Map::new();
for (key, value) in input {
let child_location = format!("{location}.{key}");
if self.policy.is_sensitive(key) {
let name = reference_name(key)?;
self.references.push(SecretRef {
name: name.clone(),
logical_location: child_location,
required: !value.is_null(),
});
output.insert(
key.clone(),
Value::String(format!("${{SECRET_REF:{name}}}")),
);
} else {
output.insert(key.clone(), self.redact_value(value, &child_location)?);
}
}
Ok(Value::Object(output))
}
}
fn reference_name(key: &str) -> Result<String, SecretError> {
let name = key
.chars()
.map(|character| {
if character.is_ascii_alphanumeric() {
character.to_ascii_uppercase()
} else {
'_'
}
})
.collect::<String>();
if name.is_empty() {
return Err(SecretError::EmptyReferenceName);
}
Ok(name)
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum SecretError {
#[error("secret reference name cannot be empty")]
EmptyReferenceName,
}