use std::path::PathBuf;
use clap::{Parser, Subcommand};
use crate::error::CliError;
use whyno_core::operation::Operation;
const EXAMPLES: &str = "\x1b[1mEXAMPLES:\x1b[0m
whyno nginx read /var/log/app.log
whyno uid:33 write /tmp/upload
whyno pid:1234 execute /usr/bin/app
whyno svc:postgres read /etc/ssl/private/server.key
whyno nginx read /path --json
whyno nginx read /path --with-cap CAP_DAC_OVERRIDE
sudo whyno caps install";
#[derive(Parser, Debug)]
#[allow(clippy::struct_excessive_bools)]
#[command(
name = "whyno",
version,
about = "Linux permission debugger",
after_help = EXAMPLES,
args_conflicts_with_subcommands = true,
)]
pub struct Cli {
pub subject: Option<String>,
pub operation: Option<String>,
pub path: Option<PathBuf>,
#[arg(long)]
pub json: bool,
#[arg(long)]
pub explain: bool,
#[arg(long, alias = "no-color")]
pub no_color: bool,
#[arg(long)]
pub self_test: bool,
#[arg(long, value_name = "CAP")]
pub with_cap: Vec<String>,
#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(Subcommand, Debug, Clone, PartialEq, Eq)]
pub enum Commands {
Caps {
#[command(subcommand)]
action: CapsAction,
},
Schema,
}
#[derive(Subcommand, Debug, Clone, Copy, PartialEq, Eq)]
pub enum CapsAction {
Install,
Uninstall,
Check,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SubjectInput {
Username(String),
Uid(u32),
Pid(u32),
Service(String),
}
pub fn parse_subject(input: &str) -> Result<SubjectInput, CliError> {
if input.is_empty() {
return Err(CliError::InvalidSubject(
"subject cannot be empty".to_string(),
));
}
if let Some(rest) = input.strip_prefix("user:") {
return parse_prefixed_username(rest);
}
if let Some(rest) = input.strip_prefix("uid:") {
return parse_prefixed_uid(rest);
}
if let Some(rest) = input.strip_prefix("pid:") {
return parse_prefixed_pid(rest);
}
if let Some(rest) = input.strip_prefix("svc:") {
return parse_prefixed_service(rest);
}
Ok(parse_bare_subject(input))
}
fn parse_prefixed_username(name: &str) -> Result<SubjectInput, CliError> {
if name.is_empty() {
return Err(CliError::InvalidSubject(
"user: prefix requires a username".to_string(),
));
}
Ok(SubjectInput::Username(name.to_string()))
}
fn parse_prefixed_uid(value: &str) -> Result<SubjectInput, CliError> {
let uid = value
.parse::<u32>()
.map_err(|_| CliError::InvalidSubject(format!("invalid uid: {value}")))?;
Ok(SubjectInput::Uid(uid))
}
fn parse_prefixed_pid(value: &str) -> Result<SubjectInput, CliError> {
let pid = value
.parse::<u32>()
.map_err(|_| CliError::InvalidSubject(format!("invalid pid: {value}")))?;
Ok(SubjectInput::Pid(pid))
}
fn parse_prefixed_service(name: &str) -> Result<SubjectInput, CliError> {
if name.is_empty() {
return Err(CliError::InvalidSubject(
"svc: prefix requires a service name".to_string(),
));
}
Ok(SubjectInput::Service(name.to_string()))
}
fn parse_bare_subject(input: &str) -> SubjectInput {
if let Ok(uid) = input.parse::<u32>() {
SubjectInput::Uid(uid)
} else {
SubjectInput::Username(input.to_string())
}
}
pub fn parse_operation(input: &str) -> Result<Operation, CliError> {
match input.to_ascii_lowercase().as_str() {
"read" => Ok(Operation::Read),
"write" => Ok(Operation::Write),
"execute" => Ok(Operation::Execute),
"delete" => Ok(Operation::Delete),
"create" => Ok(Operation::Create),
"stat" => Ok(Operation::Stat),
_ => Err(CliError::InvalidOperation(input.to_string())),
}
}
pub fn validate_flags(cli: &Cli) -> Result<(), CliError> {
if cli.json && cli.explain {
return Err(CliError::ConflictingFlags);
}
Ok(())
}
pub(crate) fn parse_cap_name(s: &str) -> Result<u64, CliError> {
match s.to_ascii_uppercase().as_str() {
"CAP_CHOWN" => Ok(1u64 << 0),
"CAP_DAC_OVERRIDE" => Ok(1u64 << 1),
"CAP_DAC_READ_SEARCH" => Ok(1u64 << 2),
"CAP_FOWNER" => Ok(1u64 << 3),
"CAP_LINUX_IMMUTABLE" => Ok(1u64 << 9),
_ => Err(CliError::InvalidCap(s.to_string())),
}
}
#[cfg(test)]
#[path = "cli_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "cli_invocation_tests.rs"]
mod invocation_tests;