use openmoat_core::{Secret, SecretSource};
use serde_json::{Value, json};
use super::Generated;
use crate::sandbox::Report;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Mask {
Active,
FileDenied,
Skipped(String),
NotWritten,
}
impl Mask {
pub fn describe(&self) -> String {
match self {
Self::Active => "Claude Code masks it".to_owned(),
Self::FileDenied => "Claude Code denies the file instead of masking it on macOS, so \
the tool that reads it does not authenticate in the sandbox"
.to_owned(),
Self::Skipped(why) => format!("Claude Code skips it: {why}"),
Self::NotWritten => {
"not masked: the settings lack its mask entry; run `openmoat sandbox sync`"
.to_owned()
}
}
}
}
pub fn states<'a>(
root: &Value,
secrets: &'a [Secret],
report: &Report,
macos: bool,
) -> Vec<(&'a Secret, Mask)> {
let tls = root
.pointer("/sandbox/network/tlsTerminate")
.is_some_and(Value::is_object);
secrets
.iter()
.map(|secret| {
let rule = format!("secrets.{}", secret.id);
let state = if let Some(loss) = report.losses.iter().find(|l| l.rule == rule) {
Mask::Skipped(loss.message.clone())
} else if !tls || !has_entry(root, secret) {
Mask::NotWritten
} else if macos && matches!(secret.source, SecretSource::File(_)) {
Mask::FileDenied
} else {
Mask::Active
};
(secret, state)
})
.collect()
}
fn has_entry(root: &Value, secret: &Secret) -> bool {
let (list, key, value) = match &secret.source {
SecretSource::Env(name) => ("envVars", "name", name),
SecretSource::File(path) => ("files", "path", path),
SecretSource::Keychain { .. } => return false,
};
root.pointer(&format!("/sandbox/credentials/{list}"))
.and_then(Value::as_array)
.into_iter()
.flatten()
.any(|entry| {
entry.get(key) == Some(&json!(value))
&& entry.get("mode") == Some(&json!("mask"))
&& entry
.get("injectHosts")
.and_then(Value::as_array)
.is_some_and(|hosts| hosts.contains(&json!(secret.host)))
})
}
pub fn weaknesses(root: &Value, generated: &Generated) -> Vec<String> {
let ours = generated.sandbox.contains_key("credentials");
let lifted = generated
.sandbox
.get("credentials")
.and_then(|c| c.get("allowPlaintextInject"))
== Some(&json!(true));
let set = root.pointer("/sandbox/credentials/allowPlaintextInject") == Some(&json!(true));
if ours && set && !lifted {
vec![
"sandbox.credentials.allowPlaintextInject is true: masked secrets may go to \
plain-HTTP hosts in clear text"
.to_owned(),
]
} else {
Vec::new()
}
}
#[cfg(test)]
mod tests;