mrapids 0.1.31

Your OpenAPI, but executable
Documentation
use anyhow::{Context, Result};
use clap::Args;
use colored::*;
use prettytable::{format, Cell, Row, Table};
use serde_json;
use serde_yaml;
use std::path::Path;

use crate::core::auth::detection::AuthDetector;
use crate::core::spec::UnifiedSpec;
use crate::models::auth::{AuthComplexity, SchemeType, SecurityAnalysis};

/// Detect and analyze authentication requirements from OpenAPI spec
#[derive(Debug, Args)]
pub struct DetectCommand {
    /// Path to OpenAPI specification file
    #[arg(short, long)]
    pub spec: Option<String>,

    /// Output format
    #[arg(short, long, value_enum, default_value = "table")]
    pub format: OutputFormat,

    /// Show detailed operation-level requirements
    #[arg(long)]
    pub operations: bool,

    /// Show only summary without scheme details
    #[arg(long)]
    pub summary_only: bool,
}

#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum OutputFormat {
    /// Human-readable table format
    Table,
    /// JSON format for programmatic consumption
    Json,
    /// YAML format for configuration files
    Yaml,
}

impl DetectCommand {
    pub async fn execute(&self) -> Result<()> {
        // Load the OpenAPI specification
        let spec = self.load_spec().await?;

        // Analyze security requirements
        let detector = AuthDetector::new(spec);
        let analysis = detector
            .analyze()
            .context("Failed to analyze authentication requirements")?;

        // Render output in requested format
        match self.format {
            OutputFormat::Table => self.render_table(&analysis)?,
            OutputFormat::Json => self.render_json(&analysis)?,
            OutputFormat::Yaml => self.render_yaml(&analysis)?,
        }

        Ok(())
    }

    async fn load_spec(&self) -> Result<UnifiedSpec> {
        let spec_path = self
            .spec
            .as_ref()
            .map(Path::new)
            .or_else(|| {
                // Try to find spec in common locations
                for path in &[
                    "openapi.yaml",
                    "openapi.json",
                    "swagger.yaml",
                    "swagger.json",
                    "api.yaml",
                    "api.json",
                ] {
                    if Path::new(path).exists() {
                        return Some(Path::new(path));
                    }
                }
                None
            })
            .context("No OpenAPI specification found. Use --spec to specify the file path")?;

        UnifiedSpec::from_file(spec_path).context("Failed to load OpenAPI specification")
    }

    fn render_table(&self, analysis: &SecurityAnalysis) -> Result<()> {
        // Print summary header
        println!("\n{}", "Authentication Analysis Summary".bold().cyan());
        println!("{}", "".repeat(80).cyan());

        // Summary section
        self.print_summary(&analysis.summary);

        if !self.summary_only {
            // Security schemes section
            println!("\n{}", "Security Schemes".bold().yellow());
            println!("{}", "".repeat(80).yellow());
            self.print_schemes_table(&analysis.schemes);

            // Global requirements section
            if !analysis.global_requirements.options.is_empty() {
                println!("\n{}", "Global Security Requirements".bold().green());
                println!("{}", "".repeat(80).green());
                self.print_requirements(&analysis.global_requirements);
            }

            // Operation-specific requirements
            if self.operations && !analysis.operation_requirements.is_empty() {
                println!("\n{}", "Operation-Specific Requirements".bold().magenta());
                println!("{}", "".repeat(80).magenta());
                for (op_id, req) in &analysis.operation_requirements {
                    println!("\n  {}: ", op_id.bold());
                    self.print_requirements(req);
                }
            }
        }

        // Next steps
        self.print_next_steps(analysis);

        Ok(())
    }

    fn print_summary(&self, summary: &crate::models::auth::AuthSummary) {
        let complexity_color = match summary.complexity_score {
            AuthComplexity::None => "green",
            AuthComplexity::Simple => "green",
            AuthComplexity::Moderate => "yellow",
            AuthComplexity::Complex => "red",
        };

        println!(
            "  Total Schemes: {}",
            summary.total_schemes.to_string().bold()
        );
        println!(
            "  Required: {}",
            summary.required_schemes.len().to_string().green()
        );
        println!(
            "  Optional: {}",
            summary.optional_schemes.len().to_string().yellow()
        );
        println!(
            "  Complexity: {}",
            format!("{:?}", summary.complexity_score).color(complexity_color)
        );

        if let Some(common) = &summary.most_common_scheme {
            println!("  Most Common: {}", common.cyan());
        }

        if summary.operations_with_custom_auth > 0 {
            println!(
                "  Custom Auth Operations: {}",
                summary.operations_with_custom_auth.to_string().yellow()
            );
        }

        if summary.operations_without_auth > 0 {
            println!(
                "  Unprotected Operations: {}",
                summary.operations_without_auth.to_string().red()
            );
        }
    }

    fn print_schemes_table(
        &self,
        schemes: &std::collections::HashMap<String, crate::models::auth::SecuritySchemeDetails>,
    ) {
        let mut table = Table::new();
        table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
        table.set_titles(Row::new(vec![
            Cell::new("Name").style_spec("b"),
            Cell::new("Type").style_spec("b"),
            Cell::new("Location").style_spec("b"),
            Cell::new("Details").style_spec("b"),
        ]));

        for (name, scheme) in schemes {
            let type_str = match scheme.scheme_type {
                SchemeType::ApiKey => "API Key".yellow(),
                SchemeType::Http => {
                    if scheme.bearer_format.is_some() {
                        "Bearer Token".green()
                    } else {
                        "HTTP Auth".blue()
                    }
                }
                SchemeType::OAuth2 => "OAuth 2.0".cyan(),
                SchemeType::OpenIdConnect => "OpenID Connect".magenta(),
                SchemeType::MutualTls => "mTLS".red(),
            };

            let location_str = scheme
                .location
                .as_ref()
                .map(|l| format!("{:?}", l))
                .unwrap_or_else(|| "-".to_string());

            let mut details = Vec::new();

            if let Some(format) = &scheme.bearer_format {
                details.push(format!("Format: {}", format));
            }

            if let Some(flows) = &scheme.flows {
                let flow_types: Vec<&str> = vec![
                    flows.implicit.as_ref().map(|_| "implicit"),
                    flows.password.as_ref().map(|_| "password"),
                    flows
                        .client_credentials
                        .as_ref()
                        .map(|_| "client_credentials"),
                    flows
                        .authorization_code
                        .as_ref()
                        .map(|_| "authorization_code"),
                    flows.device_code.as_ref().map(|_| "device_code"),
                ]
                .into_iter()
                .flatten()
                .collect();

                if !flow_types.is_empty() {
                    details.push(format!("Flows: {}", flow_types.join(", ")));
                }
            }

            if let Some(url) = &scheme.openid_connect_url {
                details.push(format!("OIDC: {}", url));
            }

            table.add_row(Row::new(vec![
                Cell::new(name),
                Cell::new(&type_str.to_string()),
                Cell::new(&location_str),
                Cell::new(&details.join("\n")),
            ]));
        }

        table.printstd();
    }

    fn print_requirements(&self, req: &crate::models::auth::SecurityRequirement) {
        if req.options.is_empty() {
            println!("    No authentication required");
            return;
        }

        for (i, option) in req.options.iter().enumerate() {
            if req.options.len() > 1 {
                print!("    Option {}: ", i + 1);
            } else {
                print!("    Required: ");
            }

            let schemes: Vec<String> = option
                .schemes
                .iter()
                .map(|s| {
                    if s.scopes.is_empty() {
                        s.name.clone()
                    } else {
                        format!("{} (scopes: {})", s.name, s.scopes.join(", "))
                    }
                })
                .collect();

            if schemes.len() > 1 {
                println!("{} (all required)", schemes.join(" AND ").bold());
            } else {
                println!("{}", schemes.join(", ").bold());
            }
        }

        if req.options.len() > 1 {
            println!("    {} Any one option is sufficient", "Note:".dimmed());
        }
    }

    fn print_next_steps(&self, analysis: &SecurityAnalysis) {
        println!("\n{}", "Next Steps".bold().cyan());
        println!("{}", "".repeat(80).cyan());

        let mut steps = Vec::new();

        // Check if any auth is required
        if !analysis.summary.required_schemes.is_empty() {
            steps.push(format!("1. Configure authentication credentials:"));

            for scheme in &analysis.summary.required_schemes {
                if let Some(details) = analysis.schemes.get(scheme) {
                    let cmd = match details.scheme_type {
                        SchemeType::ApiKey => {
                            format!("   mrapids auth connect {} --auth-type api-key", scheme)
                        }
                        SchemeType::Http => {
                            // Check if it's bearer or basic auth
                            if details.bearer_format.is_some() {
                                format!("   mrapids auth connect {} --auth-type bearer", scheme)
                            } else {
                                format!("   mrapids auth connect {} --auth-type basic", scheme)
                            }
                        }
                        SchemeType::OAuth2 => {
                            format!("   mrapids auth connect {} --auth-type oauth2", scheme)
                        }
                        _ => format!("   mrapids auth connect {}", scheme),
                    };
                    steps.push(cmd.green().to_string());
                }
            }
        }

        steps.push(format!("\n2. Validate your configuration:"));
        steps.push("   mrapids auth validate".green().to_string());

        steps.push(format!("\n3. Test an authenticated endpoint:"));
        steps.push(
            "   mrapids run <operation> --auth <scheme-name>"
                .green()
                .to_string(),
        );

        for step in steps {
            println!("{}", step);
        }
    }

    fn render_json(&self, analysis: &SecurityAnalysis) -> Result<()> {
        let json = serde_json::to_string_pretty(analysis)
            .context("Failed to serialize analysis to JSON")?;
        println!("{}", json);
        Ok(())
    }

    fn render_yaml(&self, analysis: &SecurityAnalysis) -> Result<()> {
        let yaml =
            serde_yaml::to_string(analysis).context("Failed to serialize analysis to YAML")?;
        println!("{}", yaml);
        Ok(())
    }
}