binsleuth 0.4.0

ELF/PE binary security hardening checker and section-level entropy analyzer
Documentation
use anyhow::{Context, Result};
use clap::Parser;
use colored::Colorize;
use std::fs;
use std::path::PathBuf;

use binsleuth::analyzer::AnalysisReport;
use binsleuth::analyzer::hardening::CheckResult;
use binsleuth::report;
use binsleuth::report::terminal::TerminalReporter;

/// BinSleuth — ELF/PE binary security analyzer
#[derive(Parser, Debug)]
#[command(
    name = "binsleuth",
    version,
    about = "Analyze ELF/PE binaries for security hardening flags and entropy anomalies",
    long_about = "BinSleuth inspects compiled binaries for:\n\
                  • Security hardening flags (NX, PIE, RELRO, Stack Canary)\n\
                  • Shannon entropy per section (detects packing/encryption)\n\
                  • Dangerous symbol usage (system(), execve(), mprotect(), …)\n\
                  • Debug symbol / DWARF info presence"
)]
struct Cli {
    /// Path to the ELF or PE binary to analyze
    #[arg(value_name = "FILE")]
    path: PathBuf,

    /// Show all sections, even those with normal entropy
    #[arg(short, long)]
    verbose: bool,

    /// Output results as JSON instead of the colored terminal report
    #[arg(long)]
    json: bool,

    /// Exit with code 2 if any hardening protection is missing or dangerous symbols are found
    #[arg(long)]
    strict: bool,
}

fn main() {
    match run() {
        Err(e) => {
            eprintln!("{}: {}", "error".red().bold(), e);
            std::process::exit(1);
        }
        Ok(strict_fail) => {
            if strict_fail {
                std::process::exit(2);
            }
        }
    }
}

fn run() -> Result<bool> {
    let cli = Cli::parse();

    let path = &cli.path;
    if !path.exists() {
        anyhow::bail!(
            "File not found: '{}'\nPlease provide a valid path to an ELF or PE binary.",
            path.display()
        );
    }

    let data =
        fs::read(path).with_context(|| format!("Failed to read file: '{}'", path.display()))?;

    if data.is_empty() {
        anyhow::bail!("File '{}' is empty.", path.display());
    }

    let report = AnalysisReport::analyze(&data).with_context(|| {
        format!(
            "Failed to parse '{}'. Is it a valid ELF or PE binary?",
            path.display()
        )
    })?;

    // ── Report ──────────────────────────────────────────────────────────────
    if cli.json {
        report::json::print_json(path, &report);
    } else {
        let reporter = TerminalReporter::new(cli.verbose);
        reporter.print_report(path, &report.hardening, &report.sections);
    }

    // ── Strict mode ─────────────────────────────────────────────────────────
    let strict_fail = cli.strict && has_security_issues(&report);
    Ok(strict_fail)
}

/// Returns true if the binary has any hardening issues that should fail strict mode.
fn has_security_issues(report: &AnalysisReport) -> bool {
    let h = &report.hardening;
    h.nx == CheckResult::Disabled
        || h.pie == CheckResult::Disabled
        || h.stack_canary == CheckResult::Disabled
        || h.relro == CheckResult::Disabled
        || !h.dangerous_symbols.is_empty()
}