pub mod binary;
pub mod context;
pub mod env;
pub mod gui_security;
pub mod preload;
pub mod signal;
pub mod target_binary;
pub use binary::{
hash_binary, hash_open_file, verify_binary_hash, verify_file_hash, verify_not_symlink,
};
pub use context::{
assess_io_context_signals, detect_stdin_kind, DetectorContext, DetectorContextBuilder,
StdinKind,
};
pub use env::{
assess_env_signals, assess_environment, enforce_env_policy, sanitized_env,
EnvironmentThreatLevel,
};
pub use gui_security::{
approval_delay_seconds, assess_gui_security, assess_gui_signals, check_physical_presence,
check_x11_keylog_risk, generate_gui_challenge, verify_gui_binary, verify_gui_challenge,
GuiSecurityReport, GuiThreatLevel, PhysicalPresence,
};
#[cfg(target_os = "linux")]
pub use preload::mark_dontdump;
pub use preload::{
assess_preload_signals, assess_ptrace_signals, check_macos_hardened_runtime,
check_ptrace_scope, check_self_preload, harden_process, test_memfd_secret,
vm_read_access_blocked,
};
pub use signal::{evaluate, Action, Category, Decision, Policy, Severity, Signal, SignalId};
pub use target_binary::{assess_target_binary_signals, is_interpreter};
pub const DETECTORS: &[fn(&DetectorContext) -> Vec<Signal>] = &[
assess_gui_signals,
assess_env_signals,
assess_target_binary_signals,
assess_io_context_signals,
assess_ptrace_signals,
assess_preload_signals,
];
#[must_use]
pub fn assess_all_signals(ctx: &DetectorContext) -> Vec<Signal> {
DETECTORS
.iter()
.flat_map(|detector| detector(ctx))
.collect()
}
#[allow(clippy::needless_pass_by_value)] pub fn emit_signals_inline(
signals: Vec<Signal>,
config: &crate::security_config::SecurityConfig,
) -> Result<(), crate::error::Error> {
if signals.is_empty() {
return Ok(());
}
let policy = config.build_policy();
let decision = evaluate(&signals, &policy, config.tier);
for entry in &decision.log_entries {
if let Err(e) = crate::audit::log(&crate::audit::AuditEvent::SignalRecorded {
tier: format!("{:?}", config.tier),
classification: format!("{} [{}] {}", entry.severity.as_str(), entry.id, entry.label),
}) {
eprintln!("envseal: audit log failed: {e}");
}
}
for w in &decision.warnings {
eprintln!("envseal: ⚠️ {w}");
}
if let Some(blocking) = decision.blocking_signal {
return Err(crate::error::Error::EnvironmentCompromised(format!(
"{label} ({id}): {detail} — {mitigation}",
label = blocking.label,
id = blocking.id,
detail = blocking.detail,
mitigation = blocking.mitigation,
)));
}
Ok(())
}
pub fn emit_signal_inline(
signal: Signal,
config: &crate::security_config::SecurityConfig,
) -> Result<(), crate::error::Error> {
emit_signals_inline(vec![signal], config)
}
#[must_use]
pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
std::hint::black_box(diff) == 0
}
#[must_use]
pub fn startup_audit() -> Vec<String> {
startup_audit_decision().warnings
}
#[must_use]
pub fn startup_audit_decision() -> signal::Decision {
let ctx = DetectorContext::ambient();
let signals = assess_all_signals(&ctx);
if signals.is_empty() {
return signal::Decision::default();
}
let policy = signal::Policy::new();
let tier = crate::security_config::load_system_defaults().tier;
evaluate(&signals, &policy, tier)
}
#[cfg(test)]
mod registry_tests {
use super::{DetectorContext, DETECTORS};
#[test]
fn registry_is_non_empty_and_deterministic() {
assert!(
!DETECTORS.is_empty(),
"DETECTORS slice must register at least one detector — \
an empty registry silently disables the entire signal pipeline"
);
let ctx = DetectorContext::ambient();
for detector in DETECTORS {
let _ = detector(&ctx);
}
}
#[test]
fn assess_all_signals_aggregates_every_detector() {
let ctx = DetectorContext::ambient();
let total: usize = DETECTORS.iter().map(|d| d(&ctx).len()).sum();
assert_eq!(super::assess_all_signals(&ctx).len(), total);
}
}