#![allow(dead_code)]
use std::path::Path;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Warning {
pub message: String,
}
impl Warning {
fn new(msg: impl Into<String>) -> Self {
Warning {
message: msg.into(),
}
}
}
const WORLD_READABLE: u32 = 0o004;
const KEY_TOO_PERMISSIVE: u32 = 0o077;
const CREDENTIAL_KEYS: &[&str] = &[
"password",
"passwd",
"passphrase",
"secret",
"token",
"api_key",
"apikey",
"private_key",
"credential",
];
pub fn check_file_permissions(path: &Path) -> Option<Warning> {
let mode = file_mode(path)?;
if mode & WORLD_READABLE != 0 {
Some(Warning::new(format!(
"config file {} has world-readable permissions ({:#o}); consider `chmod 600 {}`",
path.display(),
mode & 0o777,
path.display()
)))
} else {
None
}
}
pub fn check_credential_fields(path: &Path, content: &str) -> Vec<Warning> {
let value: serde_yaml::Value = match serde_yaml::from_str(content) {
Ok(v) => v,
Err(_) => return vec![],
};
let mut warnings = Vec::new();
collect_credential_warnings(&value, path, &mut warnings);
warnings
}
pub fn check_docker_in_user_layer(path: &Path) -> Warning {
Warning::new(format!(
"docker block in user-layer config {} is ignored; \
docker configuration is only trusted from project (.yconn/) \
or system (/etc/yconn/) layers",
path.display()
))
}
pub fn check_key_file(path: &Path) -> Vec<Warning> {
let mut warnings = Vec::new();
if !path.exists() {
warnings.push(Warning::new(format!(
"key file {} does not exist",
path.display()
)));
return warnings;
}
if let Some(mode) = file_mode(path) {
if mode & KEY_TOO_PERMISSIVE != 0 {
warnings.push(Warning::new(format!(
"key file {} has insecure permissions ({:#o}); \
SSH may refuse to use it; consider `chmod 600 {}`",
path.display(),
mode & 0o777,
path.display()
)));
}
}
warnings
}
#[cfg(unix)]
fn file_mode(path: &Path) -> Option<u32> {
std::fs::metadata(path).ok().map(|m| m.permissions().mode())
}
#[cfg(not(unix))]
fn file_mode(_path: &Path) -> Option<u32> {
None
}
fn collect_credential_warnings(
value: &serde_yaml::Value,
path: &Path,
warnings: &mut Vec<Warning>,
) {
match value {
serde_yaml::Value::Mapping(map) => {
for (k, v) in map {
if let serde_yaml::Value::String(key) = k {
let lower = key.to_lowercase();
if CREDENTIAL_KEYS.iter().any(|&c| lower == c) {
warnings.push(Warning::new(format!(
"credential field `{key}` found in git-trackable config {}; \
credentials must not be stored in project-layer config files",
path.display()
)));
}
}
collect_credential_warnings(v, path, warnings);
}
}
serde_yaml::Value::Sequence(seq) => {
for item in seq {
collect_credential_warnings(item, path, warnings);
}
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use tempfile::TempDir;
fn make_file(dir: &TempDir, name: &str, content: &str, mode: u32) -> std::path::PathBuf {
let path = dir.path().join(name);
fs::write(&path, content).unwrap();
fs::set_permissions(&path, fs::Permissions::from_mode(mode)).unwrap();
path
}
#[test]
fn test_file_permissions_world_readable_warns() {
let dir = TempDir::new().unwrap();
let path = make_file(&dir, "connections.yaml", "", 0o644);
assert!(check_file_permissions(&path).is_some());
}
#[test]
fn test_file_permissions_private_no_warning() {
let dir = TempDir::new().unwrap();
let path = make_file(&dir, "connections.yaml", "", 0o600);
assert!(check_file_permissions(&path).is_none());
}
#[test]
fn test_file_permissions_missing_returns_none() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("does-not-exist.yaml");
assert!(check_file_permissions(&path).is_none());
}
#[test]
fn test_file_permissions_warning_mentions_chmod() {
let dir = TempDir::new().unwrap();
let path = make_file(&dir, "connections.yaml", "", 0o644);
let w = check_file_permissions(&path).unwrap();
assert!(w.message.contains("chmod"));
}
#[test]
fn test_credential_fields_clean_yaml() {
let yaml = "connections:\n prod:\n host: 10.0.0.1\n user: deploy\n auth:\n type: key\n key: ~/.ssh/id_rsa\n";
let path = std::path::Path::new("/repo/.yconn/connections.yaml");
let warnings = check_credential_fields(path, yaml);
assert!(warnings.is_empty());
}
#[test]
fn test_credential_fields_password_field_warns() {
let yaml = "connections:\n bad:\n password: hunter2\n";
let path = std::path::Path::new("/repo/.yconn/connections.yaml");
let warnings = check_credential_fields(path, yaml);
assert_eq!(warnings.len(), 1);
assert!(warnings[0].message.contains("password"));
}
#[test]
fn test_credential_fields_passphrase_warns() {
let yaml = "connections:\n bad:\n passphrase: abc\n";
let path = std::path::Path::new("/repo/.yconn/connections.yaml");
let warnings = check_credential_fields(path, yaml);
assert!(!warnings.is_empty());
}
#[test]
fn test_credential_fields_token_warns() {
let yaml = "token: ghp_abc123\n";
let path = std::path::Path::new("/repo/.yconn/connections.yaml");
let warnings = check_credential_fields(path, yaml);
assert!(!warnings.is_empty());
}
#[test]
fn test_credential_fields_multiple_warns() {
let yaml = "connections:\n bad:\n password: x\n secret: y\n";
let path = std::path::Path::new("/repo/.yconn/connections.yaml");
let warnings = check_credential_fields(path, yaml);
assert_eq!(warnings.len(), 2);
}
#[test]
fn test_credential_fields_invalid_yaml_no_panic() {
let yaml = ": : : invalid {{ yaml";
let path = std::path::Path::new("/repo/.yconn/connections.yaml");
let warnings = check_credential_fields(path, yaml);
assert!(warnings.is_empty());
}
#[test]
fn test_credential_fields_empty_yaml() {
let path = std::path::Path::new("/repo/.yconn/connections.yaml");
let warnings = check_credential_fields(path, "");
assert!(warnings.is_empty());
}
#[test]
fn test_docker_in_user_layer_returns_warning() {
let path = std::path::Path::new("/home/user/.config/yconn/connections.yaml");
let w = check_docker_in_user_layer(path);
assert!(w.message.contains("docker"));
assert!(w.message.contains("ignored"));
}
#[test]
fn test_key_file_missing_warns() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("missing_key");
let warnings = check_key_file(&path);
assert_eq!(warnings.len(), 1);
assert!(warnings[0].message.contains("does not exist"));
}
#[test]
fn test_key_file_secure_permissions_no_warning() {
let dir = TempDir::new().unwrap();
let path = make_file(&dir, "id_rsa", "KEY DATA", 0o600);
let warnings = check_key_file(&path);
assert!(warnings.is_empty());
}
#[test]
fn test_key_file_too_permissive_warns() {
let dir = TempDir::new().unwrap();
let path = make_file(&dir, "id_rsa", "KEY DATA", 0o644);
let warnings = check_key_file(&path);
assert_eq!(warnings.len(), 1);
assert!(warnings[0].message.contains("insecure permissions"));
}
#[test]
fn test_key_file_group_readable_warns() {
let dir = TempDir::new().unwrap();
let path = make_file(&dir, "id_rsa", "KEY DATA", 0o640);
let warnings = check_key_file(&path);
assert_eq!(warnings.len(), 1);
}
}