pub(crate) const KIND: &str = "config_key";
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct ConfigKey {
pub file: String,
pub key: String,
pub value: String,
#[serde(skip)]
pub value_known: bool,
}
fn ext_lower(path: &str) -> Option<String> {
std::path::Path::new(path)
.extension()
.and_then(|e| e.to_str())
.map(str::to_ascii_lowercase)
}
#[must_use]
pub fn is_config_path(path: &str) -> bool {
if path == ".github" || path.starts_with(".github/") {
return false;
}
let base = path.rsplit('/').next().unwrap_or(path).to_ascii_lowercase();
matches!(
ext_lower(path).as_deref(),
Some("toml" | "json" | "yaml" | "yml" | "env")
) || base == ".env"
|| base.starts_with(".env.")
}
#[must_use]
pub fn is_tooling_config_path(path: &str) -> bool {
let segments: Vec<&str> = path
.split('/')
.filter(|s| !s.is_empty() && *s != ".")
.collect();
let base = segments.last().copied().unwrap_or(path);
let base_lower = base.to_ascii_lowercase();
if segments.iter().any(|s| *s == ".github" || *s == ".config") {
return true;
}
if segments.len() >= 2
&& segments[segments.len() - 2] == ".cargo"
&& matches!(base_lower.as_str(), "config" | "config.toml")
{
return true;
}
matches!(
base_lower.as_str(),
"cargo.toml"
| "cargo.lock"
| "rust-toolchain"
| "rust-toolchain.toml"
| "rustfmt.toml"
| ".rustfmt.toml"
| "clippy.toml"
| "deny.toml"
| "release-plz.toml"
| ".gitlab-ci.yml"
)
}
#[must_use]
pub fn flatten(path: &str, bytes: &[u8]) -> Vec<ConfigKey> {
let Ok(text) = std::str::from_utf8(bytes) else {
return Vec::new();
};
let mut out = Vec::new();
match ext_lower(path).as_deref() {
Some("toml") => {
if let Ok(v) = toml::from_str::<toml::Value>(text) {
flatten_toml(&v, "", path, &mut out);
}
}
Some("json") => {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(text) {
flatten_json(&v, "", path, &mut out);
}
}
Some("yaml" | "yml") => flatten_yaml(text, path, &mut out),
_ => flatten_env(text, path, &mut out),
}
out
}
fn push(out: &mut Vec<ConfigKey>, file: &str, key: &str, value: String) {
if !key.is_empty() {
out.push(ConfigKey {
file: file.to_owned(),
key: key.to_owned(),
value,
value_known: true,
});
}
}
fn join(prefix: &str, seg: &str) -> String {
if prefix.is_empty() {
seg.to_owned()
} else {
format!("{prefix}.{seg}")
}
}
fn toml_scalar(v: &toml::Value) -> String {
match v {
toml::Value::String(s) => s.clone(),
other => other.to_string(),
}
}
fn json_scalar(v: &serde_json::Value) -> String {
match v {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
}
}
fn flatten_toml(v: &toml::Value, prefix: &str, file: &str, out: &mut Vec<ConfigKey>) {
match v {
toml::Value::Table(t) => {
for (k, val) in t {
flatten_toml(val, &join(prefix, k), file, out);
}
}
other => push(out, file, prefix, toml_scalar(other)),
}
}
fn flatten_json(v: &serde_json::Value, prefix: &str, file: &str, out: &mut Vec<ConfigKey>) {
match v {
serde_json::Value::Object(m) => {
for (k, val) in m {
flatten_json(val, &join(prefix, k), file, out);
}
}
other => push(out, file, prefix, json_scalar(other)),
}
}
fn flatten_env(text: &str, file: &str, out: &mut Vec<ConfigKey>) {
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((k, val)) = line.strip_prefix("export ").unwrap_or(line).split_once('=') {
let key = k.trim();
let val = val.trim().trim_matches('"').trim_matches('\'').to_owned();
push(out, file, key, val);
}
}
}
use yaml_rust2::Yaml;
fn yaml_scalar(v: &Yaml) -> Option<String> {
match v {
Yaml::String(s) | Yaml::Real(s) => Some(s.clone()),
Yaml::Integer(i) => Some(i.to_string()),
Yaml::Boolean(b) => Some(b.to_string()),
Yaml::Null => Some("null".to_owned()),
_ => None,
}
}
fn yaml_get_str<'a>(doc: &'a Yaml, key: &str) -> Option<&'a str> {
doc.as_hash()?.get(&Yaml::String(key.to_owned()))?.as_str()
}
fn yaml_get_vec<'a>(doc: &'a Yaml, key: &str) -> Option<&'a Vec<Yaml>> {
doc.as_hash()?.get(&Yaml::String(key.to_owned()))?.as_vec()
}
fn flatten_yaml(text: &str, file: &str, out: &mut Vec<ConfigKey>) {
let Ok(docs) = yaml_rust2::YamlLoader::load_from_str(text) else {
return;
};
for doc in &docs {
match k8s_kind(doc) {
Some(kind) => flatten_k8s(doc, &kind, file, out),
None => flatten_yaml_node(doc, "", file, out),
}
}
}
fn flatten_yaml_node(v: &Yaml, prefix: &str, file: &str, out: &mut Vec<ConfigKey>) {
match v {
Yaml::Hash(h) => {
for (k, val) in h {
if let Some(k) = k.as_str() {
flatten_yaml_node(val, &join(prefix, k), file, out);
}
}
}
Yaml::Array(items) => {
let parts: Vec<Option<String>> = items.iter().map(yaml_scalar).collect();
let value = if parts.iter().all(Option::is_some) {
let scalars: Vec<String> = parts.into_iter().flatten().collect();
format!("[{}]", scalars.join(", "))
} else {
format!("[<{} items>]", items.len())
};
push(out, file, prefix, value);
}
other => {
if let Some(s) = yaml_scalar(other) {
push(out, file, prefix, s);
}
}
}
}
fn k8s_kind(doc: &Yaml) -> Option<String> {
let h = doc.as_hash()?;
let has = |k: &str| h.contains_key(&Yaml::String(k.to_owned()));
(has("apiVersion") && has("kind"))
.then(|| yaml_get_str(doc, "kind").map(str::to_owned))
.flatten()
}
fn flatten_k8s(doc: &Yaml, kind: &str, file: &str, out: &mut Vec<ConfigKey>) {
match kind {
"ConfigMap" => k8s_data(doc, "data", file, out, false),
"Secret" => {
k8s_data(doc, "data", file, out, true);
k8s_data(doc, "stringData", file, out, true);
}
_ => {
if let Some(pod) = k8s_pod_spec(doc, kind) {
k8s_containers(pod, file, out);
}
}
}
}
fn k8s_data(doc: &Yaml, field: &str, file: &str, out: &mut Vec<ConfigKey>, redact: bool) {
let Some(map) = doc
.as_hash()
.and_then(|h| h.get(&Yaml::String(field.to_owned())))
.and_then(Yaml::as_hash)
else {
return;
};
for (k, v) in map {
let Some(k) = k.as_str() else { continue };
if redact {
push(out, file, k, "<redacted>".to_owned());
} else if let Some(value) = yaml_scalar(v) {
push(out, file, k, value);
}
}
}
fn k8s_pod_spec<'a>(doc: &'a Yaml, kind: &str) -> Option<&'a Yaml> {
let path: &[&str] = match kind {
"Pod" => &["spec"],
"Deployment" | "StatefulSet" | "DaemonSet" | "ReplicaSet" | "Job" => {
&["spec", "template", "spec"]
}
"CronJob" => &["spec", "jobTemplate", "spec", "template", "spec"],
_ => return None,
};
let mut cur = doc;
for seg in path {
cur = cur.as_hash()?.get(&Yaml::String((*seg).to_owned()))?;
}
Some(cur)
}
fn k8s_containers(pod: &Yaml, file: &str, out: &mut Vec<ConfigKey>) {
for field in ["containers", "initContainers"] {
let Some(list) = yaml_get_vec(pod, field) else {
continue;
};
for c in list {
let cname = yaml_get_str(c, "name").unwrap_or("container");
if let Some(image) = yaml_get_str(c, "image") {
push(
out,
file,
&format!("container.{cname}.image"),
image.to_owned(),
);
}
if let Some(env) = yaml_get_vec(c, "env") {
for e in env {
if let (Some(name), Some(value)) =
(yaml_get_str(e, "name"), yaml_get_str(e, "value"))
{
push(out, file, name, value.to_owned());
}
}
}
}
}
}
#[must_use]
pub fn is_secret_key(key: &str) -> bool {
const NEEDLES: &[&str] = &[
"secret",
"password",
"passwd",
"passphrase",
"token",
"apikey",
"credential",
"privatekey",
"accesskey",
"pwd",
];
let flat: String = key
.chars()
.filter(char::is_ascii_alphanumeric)
.map(|c| c.to_ascii_lowercase())
.collect();
NEEDLES.iter().any(|n| flat.contains(n))
}
#[must_use]
pub fn normalize(key: &str) -> String {
key.split(|c: char| !c.is_ascii_alphanumeric())
.filter(|s| !s.is_empty())
.map(str::to_ascii_lowercase)
.collect::<Vec<_>>()
.join(".")
}
#[must_use]
pub fn canonicalize(key: &str) -> String {
key.split('.')
.map(|seg| {
seg.chars()
.filter(char::is_ascii_alphanumeric)
.map(|c| c.to_ascii_lowercase())
.collect::<String>()
})
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(".")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flatten_unquotes_strings_and_treats_arrays_as_one_leaf() {
let toml = flatten(
"a.toml",
b"[serve]\naddr = \"0.0.0.0:8443\"\nmodels = [\"q8\"]\n",
);
assert!(
toml.iter()
.any(|k| k.key == "serve.addr" && k.value == "0.0.0.0:8443")
);
assert!(toml.iter().any(|k| k.key == "serve.models"));
let json = flatten(
"a.json",
br#"{"serve":{"addr":"0.0.0.0:8443","tools":false}}"#,
);
assert!(
json.iter()
.any(|k| k.key == "serve.addr" && k.value == "0.0.0.0:8443")
);
assert!(
json.iter()
.any(|k| k.key == "serve.tools" && k.value == "false")
);
let env = flatten(".env", b"# c\nexport SERVE_ADDR=127.0.0.1:8017\n");
assert!(
env.iter()
.any(|k| k.key == "SERVE_ADDR" && k.value == "127.0.0.1:8017")
);
}
#[test]
fn is_config_path_matches_toml_json_yaml_env() {
assert!(is_config_path("values.prod.yaml")); assert!(is_config_path("deploy.yml"));
assert!(is_config_path("config.toml"));
assert!(is_config_path("a/b.json"));
assert!(is_config_path(".env"));
assert!(is_config_path(".env.local"));
assert!(is_config_path("prod.env"));
assert!(!is_config_path("src/main.rs"));
assert!(!is_config_path(".github/workflows/ci.yml"));
assert!(!is_config_path(".github/dependabot.yml"));
}
#[test]
fn tooling_config_paths_are_flagged_conservatively() {
for p in [
"Cargo.toml",
"Cargo.lock",
"crates/rto-graph/Cargo.toml", "vendor/some-crate/Cargo.toml", "rust-toolchain",
"rust-toolchain.toml",
"rustfmt.toml",
".rustfmt.toml",
"clippy.toml",
"deny.toml",
"release-plz.toml",
".config/nextest.toml",
".cargo/config",
".cargo/config.toml",
".github/workflows/ci.yml",
".github/dependabot.yml",
".gitlab-ci.yml",
] {
assert!(is_tooling_config_path(p), "{p} should be tooling config");
}
for p in [
"config/app.toml",
"values.yaml",
"values.prod.yaml",
"prod.env",
".env",
".env.local",
"zerobus-example.toml",
"deploy/service.json",
"settings/config.toml", ".cargo/app.toml", ] {
assert!(!is_tooling_config_path(p), "{p} should be app config");
}
}
#[test]
fn yaml_helm_values_flatten_like_toml() {
let ks = flatten(
"values.yaml",
b"service:\n addr: 0.0.0.0:8443\n tools: false\nreplicas: 3\nmodels:\n - a\n - b\n",
);
assert!(
ks.iter()
.any(|k| k.key == "service.addr" && k.value == "0.0.0.0:8443"),
"{ks:?}"
);
assert!(
ks.iter()
.any(|k| k.key == "service.tools" && k.value == "false")
);
assert!(ks.iter().any(|k| k.key == "replicas" && k.value == "3"));
assert!(ks.iter().any(|k| k.key == "models" && k.value == "[a, b]"));
}
#[test]
fn yaml_array_of_objects_emits_a_sentinel_not_empty() {
let ks = flatten(
"values.yaml",
b"ingress:\n hosts:\n - host: a.example\n paths: [/]\n - host: b.example\n",
);
let hosts = ks
.iter()
.find(|k| k.key == "ingress.hosts")
.expect("hosts leaf");
assert_eq!(hosts.value, "[<2 items>]", "non-scalar array is a sentinel");
}
#[test]
fn k8s_configmap_skips_non_scalar_values() {
let cm = b"apiVersion: v1\nkind: ConfigMap\ndata:\n flat: ok\n nested:\n a: 1\n";
let ks = flatten("cm.yaml", cm);
assert!(ks.iter().any(|k| k.key == "flat" && k.value == "ok"));
assert!(
!ks.iter().any(|k| k.key == "nested"),
"non-scalar ConfigMap value skipped, not emitted empty: {ks:?}"
);
}
#[test]
fn k8s_manifest_mines_config_not_structural_noise() {
let dep = b"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: api\nspec:\n template:\n spec:\n containers:\n - name: api\n image: registry/app:1.2\n env:\n - name: SERVE_ADDR\n value: 0.0.0.0:8443\n - name: DB_HOST\n valueFrom:\n secretKeyRef:\n name: db\n";
let ks = flatten("deploy.yaml", dep);
assert!(
ks.iter()
.any(|k| k.key == "SERVE_ADDR" && k.value == "0.0.0.0:8443"),
"env var mined as a bare key so it matches a hub .env: {ks:?}"
);
assert!(
ks.iter()
.any(|k| k.key == "container.api.image" && k.value == "registry/app:1.2"),
"{ks:?}"
);
assert!(!ks.iter().any(|k| k.key == "DB_HOST"));
assert!(
!ks.iter()
.any(|k| k.key.starts_with("apiVersion") || k.key.contains("metadata"))
);
}
#[test]
fn k8s_configmap_and_secret_data_with_redaction() {
let cm = b"apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: c\ndata:\n serve.addr: 127.0.0.1:8017\n log_level: info\n";
let ks = flatten("cm.yaml", cm);
assert!(
ks.iter()
.any(|k| k.key == "serve.addr" && k.value == "127.0.0.1:8017")
);
assert!(ks.iter().any(|k| k.key == "log_level" && k.value == "info"));
let sec = b"apiVersion: v1\nkind: Secret\ndata:\n database-url: aHR0cA==\n";
let ks = flatten("secret.yaml", sec);
assert!(
ks.iter()
.any(|k| k.key == "database-url" && k.value == "<redacted>"),
"secret data must be redacted regardless of key name: {ks:?}"
);
}
#[test]
fn yaml_multi_document_stream_mines_each_doc() {
let stream = b"apiVersion: v1\nkind: ConfigMap\ndata:\n port: \"8443\"\n---\napiVersion: apps/v1\nkind: Deployment\nspec:\n template:\n spec:\n containers:\n - name: web\n image: app:2.0\n";
let ks = flatten("bundle.yaml", stream);
assert!(ks.iter().any(|k| k.key == "port" && k.value == "8443"));
assert!(
ks.iter()
.any(|k| k.key == "container.web.image" && k.value == "app:2.0")
);
}
#[test]
fn normalize_bridges_conventions() {
assert_eq!(normalize("SERVE_ADDR"), "serve.addr");
assert_eq!(normalize("serve-addr"), "serve.addr");
}
#[test]
fn canonicalize_bridges_camel_snake_kebab_within_a_segment() {
assert_eq!(
canonicalize("zerobus.serverEndpoint"),
"zerobus.serverendpoint"
);
assert_eq!(
canonicalize("zerobus.server_endpoint"),
"zerobus.serverendpoint"
);
assert_eq!(
canonicalize("zerobus.server-endpoint"),
"zerobus.serverendpoint"
);
assert_ne!(
normalize("zerobus.server_endpoint"),
normalize("zerobus.serverEndpoint"),
"normalize splits snake_case on `_`, so it cannot bridge camelCase"
);
assert_ne!(canonicalize("a.b"), canonicalize("ab"));
}
#[test]
fn secret_keys_are_flagged_across_conventions() {
for k in [
"API_TOKEN",
"apiKey",
"db.password",
"AWS_SECRET_ACCESS_KEY",
"PWD",
] {
assert!(is_secret_key(k), "{k} should be secret");
}
for k in ["serve.addr", "models.generative", "port", "workspace.roots"] {
assert!(!is_secret_key(k), "{k} should not be secret");
}
}
}