pub mod admin;
pub mod ctf;
pub mod dev_migration;
pub mod execution;
#[cfg(feature = "fido2")]
pub mod fido2_admin;
pub mod governance;
pub mod lifecycle;
pub mod queries;
pub use admin::*;
pub use ctf::*;
pub use dev_migration::*;
pub use execution::*;
#[cfg(feature = "fido2")]
pub use fido2_admin::{
disable as fido2_disable, enroll as fido2_enroll, status as fido2_status, Fido2StatusReport,
};
pub use governance::*;
pub use lifecycle::*;
pub use queries::*;
use crate::error::Error;
use crate::vault::Vault;
use crate::{gui, secret_health, security_config};
use std::path::{Path, PathBuf};
pub fn health_report(max_days: Option<u64>) -> Result<secret_health::HealthReport, Error> {
let vault = Vault::open_default()?;
secret_health::health_report(&vault, max_days)
}
pub use crate::guard::{GuiSecurityReport, GuiThreatLevel, PhysicalPresence};
#[derive(Debug, Clone)]
pub struct DoctorReport {
pub ptrace_scope_warning: Option<String>,
pub self_preload: Result<(), String>,
pub gui: crate::guard::GuiSecurityReport,
pub environment: EnvThreatLabel,
pub vault: VaultProbe,
pub capabilities: crate::sandbox::OsCapabilities,
pub keystore: KeystoreProbe,
pub signals: Vec<SignalProbe>,
}
#[derive(Debug, Clone)]
pub struct SignalProbe {
pub id: String,
pub category: String,
pub severity: String,
pub label: String,
pub detail: String,
pub mitigation: String,
}
#[derive(Debug, Clone)]
pub struct KeystoreProbe {
pub backend: crate::vault::hardware::Backend,
pub tier: u8,
pub name: &'static str,
}
#[derive(Debug, Clone)]
pub enum EnvThreatLabel {
Safe,
Degraded(String),
Hostile(String),
}
#[derive(Debug, Clone)]
pub struct VaultProbe {
pub root: PathBuf,
pub exists: bool,
pub master_key_present: bool,
}
pub fn doctor_report() -> Result<DoctorReport, Error> {
let root = vault_root()?;
let exists = root.exists();
let master_key_present = exists && root.join("master.key").exists();
let environment = match crate::guard::assess_environment() {
crate::guard::EnvironmentThreatLevel::Safe => EnvThreatLabel::Safe,
crate::guard::EnvironmentThreatLevel::Degraded(s) => EnvThreatLabel::Degraded(s),
crate::guard::EnvironmentThreatLevel::Hostile(s) => EnvThreatLabel::Hostile(s),
};
let backend = crate::keychain::active_backend();
let keystore = KeystoreProbe {
backend,
tier: backend.tier(),
name: backend.name(),
};
let doctor_ctx = crate::guard::DetectorContext::ambient();
let signals = crate::guard::assess_all_signals(&doctor_ctx)
.into_iter()
.map(|s| SignalProbe {
id: s.id.as_str().to_string(),
category: s.category.as_str().to_string(),
severity: s.severity.as_str().to_string(),
label: s.label.to_string(),
detail: s.detail,
mitigation: s.mitigation.to_string(),
})
.collect();
Ok(DoctorReport {
ptrace_scope_warning: crate::guard::check_ptrace_scope(),
self_preload: crate::guard::check_self_preload().map_err(|e| e.to_string()),
gui: crate::guard::assess_gui_security(),
environment,
vault: VaultProbe {
root,
exists,
master_key_present,
},
capabilities: crate::sandbox::OsCapabilities::detect(),
keystore,
signals,
})
}
pub fn verify_gui_binary(name: &str) -> Result<PathBuf, Error> {
crate::guard::verify_gui_binary(name)
}
pub fn validate_path_within_cwd(
path: &str,
field: &str,
max_path_len: usize,
) -> Result<PathBuf, String> {
if path.contains('\0') {
return Err(format!("{field} contains invalid characters"));
}
if path.len() > max_path_len {
return Err(format!(
"{field} exceeds maximum length of {max_path_len} bytes"
));
}
let p = Path::new(path);
if p.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
return Err(format!("{field} must not escape the working directory"));
}
let cwd =
std::env::current_dir().map_err(|_| "cannot determine working directory".to_string())?;
let cwd = std::fs::canonicalize(&cwd).map_err(|_| {
"cannot resolve working directory (symlink or permission error)".to_string()
})?;
let abs = if p.is_absolute() {
p.to_path_buf()
} else {
cwd.join(p)
};
let resolved = if abs.exists() {
std::fs::canonicalize(&abs)
.map_err(|_| format!("failed to resolve {field} (path or permission error)"))?
} else {
let mut anchor = abs.clone();
let mut suffix: Vec<std::ffi::OsString> = Vec::new();
while !anchor.exists() {
let leaf = anchor
.file_name()
.ok_or_else(|| format!("{field} has no resolvable ancestor"))?
.to_os_string();
suffix.push(leaf);
match anchor.parent() {
Some(parent) => anchor = parent.to_path_buf(),
None => return Err(format!("{field} has no resolvable ancestor")),
}
}
let canonical_anchor = std::fs::canonicalize(&anchor)
.map_err(|_| format!("failed to resolve {field} (path or permission error)"))?;
let mut resolved = canonical_anchor;
for piece in suffix.into_iter().rev() {
resolved.push(piece);
}
resolved
};
if resolved != cwd && !resolved.starts_with(cwd.join("")) {
return Err(format!("{field} must not escape the working directory"));
}
Ok(resolved)
}
pub fn request_key(name: &str, description: &str) -> Result<Vec<crate::guard::Signal>, Error> {
let vault = Vault::open_default()?;
let config = security_config::load_config(vault.root(), vault.master_key_bytes())?;
let agent_desc = format!(
"[AGENT-INITIATED REQUEST]\n\n{description}\n\n---\nThis prompt was initiated by an automated agent. Verify the secret name ('{name}') before entering any credentials."
);
let secret = gui::request_secret_value(name, &agent_desc, &config)?;
if secret.is_empty() {
return Err(Error::CryptoFailure(
"refusing to store an empty secret".to_string(),
));
}
store_secret(name, secret.as_bytes(), false)
}