rastray 0.15.0

Blazing-fast static analysis CLI for security, dependency, and performance audits.
use std::path::PathBuf;
use std::str::FromStr;

use clap::{Parser, Subcommand, ValueEnum};

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
#[value(rename_all = "lowercase")]
pub enum Severity {
    Info,
    Low,
    Medium,
    High,
    Critical,
}

impl Severity {
    pub fn as_str(self) -> &'static str {
        match self {
            Severity::Info => "info",
            Severity::Low => "low",
            Severity::Medium => "medium",
            Severity::High => "high",
            Severity::Critical => "critical",
        }
    }
}

impl FromStr for Severity {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_ascii_lowercase().as_str() {
            "info" => Ok(Severity::Info),
            "low" => Ok(Severity::Low),
            "medium" | "med" => Ok(Severity::Medium),
            "high" => Ok(Severity::High),
            "critical" | "crit" => Ok(Severity::Critical),
            other => Err(format!("unknown severity '{other}'")),
        }
    }
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
#[value(rename_all = "lowercase")]
pub enum Confidence {
    Low,
    Medium,
    #[default]
    High,
}

impl Confidence {
    pub fn as_str(self) -> &'static str {
        match self {
            Confidence::Low => "low",
            Confidence::Medium => "medium",
            Confidence::High => "high",
        }
    }

    pub fn short_label(self) -> &'static str {
        match self {
            Confidence::Low => "low",
            Confidence::Medium => "med",
            Confidence::High => "high",
        }
    }
}

impl FromStr for Confidence {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_ascii_lowercase().as_str() {
            "low" => Ok(Confidence::Low),
            "medium" | "med" => Ok(Confidence::Medium),
            "high" => Ok(Confidence::High),
            other => Err(format!("unknown confidence '{other}'")),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum OutputFormat {
    Human,
    Json,
    GhActions,
    Sarif,
    Markdown,
    Html,
    Cyclonedx,
    SpdxJson,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FailOn {
    Never,
    AtOrAbove(Severity),
}

impl FailOn {
    pub fn as_threshold(self) -> Option<Severity> {
        match self {
            FailOn::Never => None,
            FailOn::AtOrAbove(s) => Some(s),
        }
    }
}

fn parse_fail_on(s: &str) -> Result<FailOn, String> {
    if s.eq_ignore_ascii_case("never") || s.eq_ignore_ascii_case("none") {
        Ok(FailOn::Never)
    } else {
        s.parse::<Severity>().map(FailOn::AtOrAbove)
    }
}

#[derive(Debug, Parser)]
#[command(
    name = "rastray",
    author,
    version,
    about = "Blazing-fast static analysis for security, dependencies, and performance.",
    long_about = None,
    propagate_version = true,
)]
pub struct Cli {
    #[arg(value_name = "PATH", default_value = ".")]
    pub path: PathBuf,

    #[arg(long = "min-severity", value_enum, default_value_t = Severity::Low)]
    pub min_severity: Severity,

    #[arg(long = "min-confidence", value_enum, default_value_t = Confidence::Low)]
    pub min_confidence: Confidence,

    #[arg(long = "json", default_value_t = false)]
    pub json: bool,

    #[arg(long = "format", value_enum)]
    pub format: Option<OutputFormat>,

    #[arg(long = "output", short = 'o', value_name = "FILE")]
    pub output: Option<PathBuf>,

    #[arg(long = "config", value_name = "FILE")]
    pub config: Option<PathBuf>,

    #[arg(long = "no-config", default_value_t = false)]
    pub no_config: bool,

    #[arg(long = "fail-on", value_name = "LEVEL", value_parser = parse_fail_on)]
    pub fail_on: Option<FailOn>,

    #[arg(long = "baseline", value_name = "FILE")]
    pub baseline: Option<PathBuf>,

    #[arg(long = "write-baseline", value_name = "FILE")]
    pub write_baseline: Option<PathBuf>,

    #[arg(long = "since", value_name = "REF")]
    pub since: Option<String>,

    #[arg(long = "changed-only", default_value_t = false)]
    pub changed_only: bool,

    #[arg(long = "summary-only", default_value_t = false)]
    pub summary_only: bool,

    #[arg(long = "fix", default_value_t = false)]
    pub fix: bool,

    #[arg(long = "yes", default_value_t = false)]
    pub fix_yes: bool,

    #[arg(long = "offline", default_value_t = false)]
    pub offline: bool,

    #[arg(long = "no-cache", default_value_t = false)]
    pub no_cache: bool,

    #[arg(long = "no-default-skip", default_value_t = false)]
    pub no_default_skip: bool,

    #[arg(long = "no-ignore", default_value_t = false)]
    pub no_ignore: bool,

    #[arg(long = "hidden", default_value_t = false)]
    pub include_hidden: bool,

    #[arg(long = "follow-links", default_value_t = false)]
    pub follow_links: bool,

    #[arg(long = "include-minified", default_value_t = false)]
    pub include_minified: bool,

    #[arg(long = "threads", short = 'j', value_name = "N")]
    pub threads: Option<usize>,

    #[arg(long = "max-depth", value_name = "N")]
    pub max_depth: Option<usize>,

    #[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
    pub verbose: u8,

    #[arg(
        short = 'q',
        long = "quiet",
        default_value_t = false,
        conflicts_with = "verbose"
    )]
    pub quiet: bool,

    #[command(subcommand)]
    pub command: Option<Command>,
}

#[derive(Debug, Subcommand)]
pub enum Command {
    Lsp,
    Secrets {
        #[arg(long = "history", default_value_t = false)]
        history: bool,

        #[arg(long = "since", value_name = "REF")]
        since: Option<String>,

        #[arg(long = "max-commits", value_name = "N")]
        max_commits: Option<usize>,

        #[arg(value_name = "PATH", default_value = ".")]
        path: PathBuf,
    },
    Image {
        #[arg(long = "max-file-bytes", value_name = "N", default_value_t = 4 * 1024 * 1024)]
        max_file_bytes: u64,

        #[arg(value_name = "ARCHIVE")]
        archive: PathBuf,
    },
    InstallHooks {
        #[arg(long = "force", default_value_t = false)]
        force: bool,

        #[arg(value_name = "PATH", default_value = ".")]
        path: PathBuf,
    },
}

impl Cli {
    pub fn parsed() -> Self {
        Self::parse()
    }

    pub fn effective_format(&self) -> OutputFormat {
        if let Some(fmt) = self.format {
            return fmt;
        }
        if self.json {
            OutputFormat::Json
        } else {
            OutputFormat::Human
        }
    }
}