#![deny(clippy::all)]
#![warn(clippy::pedantic)]
mod caps;
mod cli;
mod crosscheck;
mod error;
mod output;
use std::io::{self, Write};
#[cfg(not(all(feature = "selinux", feature = "apparmor")))]
use std::path::Path;
use std::process::ExitCode;
use clap::Parser;
use cli::{
build_metadata_params, parse_cap_name, parse_subject, resolve_operation, validate_flags,
CapsAction, Cli, Commands,
};
use error::WhynoError;
use whyno_core::state::subject::ResolvedSubject;
use whyno_core::state::Probe;
fn main() -> ExitCode {
let cli = Cli::parse();
configure_color(&cli);
match run(&cli) {
Ok(allowed) => {
if allowed {
ExitCode::SUCCESS
} else {
ExitCode::from(1)
}
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(2)
}
}
}
fn configure_color(cli: &Cli) {
let no_color = cli.no_color || std::env::var_os("NO_COLOR").is_some();
if no_color {
owo_colors::set_override(false);
}
}
fn run(cli: &Cli) -> Result<bool, WhynoError> {
match cli.command {
Some(Commands::Caps { action }) => {
run_caps(action)?;
Ok(true)
}
Some(Commands::Schema) => {
run_schema()?;
Ok(true)
}
None => run_check(cli),
}
}
fn run_schema() -> Result<(), WhynoError> {
let schema = output::json::generate_schema();
let json =
serde_json::to_string_pretty(&schema).map_err(error::OutputError::SerializationFailed)?;
println!("{json}");
Ok(())
}
fn run_caps(action: CapsAction) -> Result<(), WhynoError> {
match action {
CapsAction::Install => caps::caps_install(),
CapsAction::Uninstall => caps::caps_uninstall(),
CapsAction::Check => caps::caps_check(),
}
}
fn run_check(cli: &Cli) -> Result<bool, WhynoError> {
validate_flags(cli)?;
let (subject_str, operation_str, path) = extract_args(cli)?;
let subject_input = parse_subject(subject_str)?;
let operation = resolve_operation(operation_str, cli.xattr_key.as_deref())?;
let params = build_metadata_params(cli)?;
let mut subject = resolve_subject(&subject_input)?;
apply_cap_overrides(&mut subject, &cli.with_cap)?;
let state = whyno_gather::gather_state(&subject, operation, path)?;
warn_mac_systems();
let report = whyno_core::checks::run_checks(&state, ¶ms);
let plan = whyno_core::fix::generate_fixes(&report, &state, ¶ms);
run_cross_check_if_enabled(cli.self_test, &state, &report);
let mode = output_mode(cli);
let mut stdout = io::stdout().lock();
output::render(&report, &plan, &state, mode, &mut stdout)?;
stdout.flush().map_err(error::OutputError::WriteFailed)?;
Ok(report.is_allowed())
}
fn extract_args(cli: &Cli) -> Result<(&str, &str, &std::path::Path), WhynoError> {
let subject = cli
.subject
.as_deref()
.ok_or_else(|| error::CliError::InvalidSubject("missing subject argument".into()))?;
let operation = cli
.operation
.as_deref()
.ok_or_else(|| error::CliError::InvalidOperation("missing operation argument".into()))?;
let path = cli
.path
.as_deref()
.ok_or_else(|| error::CliError::MissingArg("path".into()))?;
Ok((subject, operation, path))
}
fn apply_cap_overrides(
subject: &mut ResolvedSubject,
cap_names: &[String],
) -> Result<(), WhynoError> {
if cap_names.is_empty() {
return Ok(());
}
let mut bitmask: u64 = 0;
for name in cap_names {
bitmask |= parse_cap_name(name)?;
}
subject.capabilities = Probe::Known(bitmask);
Ok(())
}
fn resolve_subject(input: &cli::SubjectInput) -> Result<ResolvedSubject, WhynoError> {
let subject = match input {
cli::SubjectInput::Username(name) => whyno_gather::subject::resolve_username(name)?,
cli::SubjectInput::Uid(uid) => whyno_gather::subject::resolve_uid(*uid)?,
cli::SubjectInput::Pid(pid) => whyno_gather::subject::resolve_pid(*pid)?,
cli::SubjectInput::Service(name) => whyno_gather::subject::resolve_service(name)?,
};
Ok(subject)
}
#[must_use]
fn output_mode(cli: &Cli) -> output::OutputMode {
if cli.json {
output::OutputMode::Json
} else if cli.explain {
output::OutputMode::Explain
} else {
output::OutputMode::Checklist
}
}
fn run_cross_check_if_enabled(
self_test: bool,
state: &whyno_core::state::SystemState,
report: &whyno_core::checks::CheckReport,
) {
let should_run = self_test || cfg!(debug_assertions);
if should_run {
crosscheck::maybe_cross_check(state, report);
}
}
fn warn_mac_systems() {
#[cfg(not(feature = "selinux"))]
if Path::new("/sys/fs/selinux").exists() {
eprintln!(
"Note: SELinux is active — rebuild with --features selinux to include MAC checks"
);
}
#[cfg(not(feature = "apparmor"))]
if Path::new("/sys/kernel/security/apparmor").exists() {
eprintln!(
"Note: AppArmor is active — rebuild with --features apparmor to include MAC checks"
);
}
}