adversaria 0.1.0

Adversarial Testing Harness for Large Language Models
Documentation
use crate::core::Config;
use crate::reporters::{create_reporter, list_reports, load_report};
use anyhow::{Context, Result};
use clap::Args;
use colored::Colorize;
use comfy_table::{presets::UTF8_FULL, Cell, Color, Table};
use std::path::PathBuf;

#[derive(Args)]
pub struct ReportCommand {
    #[arg(help = "Report ID or path to display")]
    report_id: Option<String>,

    #[arg(
        short,
        long,
        help = "Path to config file",
        default_value = "adversaria.config.yaml"
    )]
    config: PathBuf,

    #[arg(short, long, help = "List all available reports")]
    list: bool,

    #[arg(short, long, help = "Show detailed results")]
    verbose: bool,
}

impl ReportCommand {
    pub async fn execute(self) -> Result<()> {
        let config = if self.config.exists() {
            Config::load(&self.config).context("Failed to load config file")?
        } else {
            Config::default()
        };

        if self.list {
            return self.list_reports(&config).await;
        }

        if let Some(ref report_id) = self.report_id {
            return self.show_report(&config, report_id).await;
        }

        self.list_reports(&config).await
    }

    async fn list_reports(&self, config: &Config) -> Result<()> {
        println!("{}", "📊 Available Reports".bold().bright_cyan());
        println!();

        let reports = list_reports(config)?;

        if reports.is_empty() {
            println!(
                "{} No reports found in {}",
                "⚠️".yellow(),
                config.reporting.output_directory.display()
            );
            return Ok(());
        }

        let mut table = Table::new();
        table.load_preset(UTF8_FULL);
        table.set_header(vec![
            Cell::new("Timestamp").fg(Color::Cyan),
            Cell::new("Model").fg(Color::Cyan),
            Cell::new("Provider").fg(Color::Cyan),
            Cell::new("Risk Score").fg(Color::Cyan),
            Cell::new("Attacks").fg(Color::Cyan),
            Cell::new("File").fg(Color::Cyan),
        ]);

        for report_path in &reports {
            if let Ok(test_run) = load_report(report_path.clone()) {
                let risk_color = match test_run.overall_risk_score {
                    0..=25 => Color::Green,
                    26..=50 => Color::Yellow,
                    51..=75 => Color::Red,
                    _ => Color::DarkRed,
                };

                table.add_row(vec![
                    Cell::new(test_run.timestamp.format("%Y-%m-%d %H:%M:%S")),
                    Cell::new(&test_run.model),
                    Cell::new(&test_run.provider),
                    Cell::new(format!("{}/100", test_run.overall_risk_score)).fg(risk_color),
                    Cell::new(format!(
                        "{}/{}",
                        test_run.successful_attacks, test_run.total_attacks
                    )),
                    Cell::new(report_path.file_name().unwrap().to_string_lossy()),
                ]);
            }
        }

        println!("{}", table);
        println!("\n{}: {} report(s) found", "Total".bold(), reports.len());
        println!(
            "\n{} Use {} to view a specific report",
            "💡".bright_yellow(),
            "adversaria report <filename>".bold()
        );

        Ok(())
    }

    async fn show_report(&self, config: &Config, report_id: &str) -> Result<()> {
        let report_path = if PathBuf::from(report_id).exists() {
            PathBuf::from(report_id)
        } else {
            config.reporting.output_directory.join(report_id)
        };

        if !report_path.exists() {
            anyhow::bail!("Report not found: {}", report_path.display());
        }

        let test_run = load_report(report_path)?;

        let reporter = create_reporter(config);
        let summary = reporter.format_summary(&test_run);
        println!("{}", summary);

        if self.verbose {
            println!("\n{}", "DETAILED RESULTS".bold().underline());

            let successful_attacks: Vec<_> =
                test_run.results.iter().filter(|r| r.success).collect();

            if !successful_attacks.is_empty() {
                println!("\n{} Successful Attacks:", "⚠️".red().bold());
                for (i, result) in successful_attacks.iter().enumerate() {
                    println!(
                        "\n  {}. {} ({})",
                        i + 1,
                        result.payload_name.bold(),
                        format!("{:?}", result.severity).red()
                    );
                    println!("     Category: {}", result.category.to_string());
                    println!("     Risk Score: {}/100", result.risk_score);
                    println!("     Execution Time: {}ms", result.execution_time_ms);
                    println!(
                        "     Prompt: {}",
                        result.prompt.chars().take(100).collect::<String>()
                    );
                    if result.prompt.len() > 100 {
                        println!("            ...");
                    }
                    println!(
                        "     Response: {}",
                        result.response.chars().take(150).collect::<String>()
                    );
                    if result.response.len() > 150 {
                        println!("              ...");
                    }
                }
            }

            let failed_attacks: Vec<_> = test_run.results.iter().filter(|r| !r.success).collect();

            if !failed_attacks.is_empty() && failed_attacks.len() <= 10 {
                println!("\n{} Failed Attacks (Model Defended):", "".green().bold());
                for (i, result) in failed_attacks.iter().enumerate() {
                    println!(
                        "  {}. {} - {}",
                        i + 1,
                        result.payload_name,
                        result
                            .detection_reason
                            .as_ref()
                            .unwrap_or(&"Unknown".to_string())
                    );
                }
            }
        }

        Ok(())
    }
}