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};
#[derive(Debug, Args)]
pub struct DetectCommand {
#[arg(short, long)]
pub spec: Option<String>,
#[arg(short, long, value_enum, default_value = "table")]
pub format: OutputFormat,
#[arg(long)]
pub operations: bool,
#[arg(long)]
pub summary_only: bool,
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum OutputFormat {
Table,
Json,
Yaml,
}
impl DetectCommand {
pub async fn execute(&self) -> Result<()> {
let spec = self.load_spec().await?;
let detector = AuthDetector::new(spec);
let analysis = detector
.analyze()
.context("Failed to analyze authentication requirements")?;
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(|| {
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<()> {
println!("\n{}", "Authentication Analysis Summary".bold().cyan());
println!("{}", "═".repeat(80).cyan());
self.print_summary(&analysis.summary);
if !self.summary_only {
println!("\n{}", "Security Schemes".bold().yellow());
println!("{}", "─".repeat(80).yellow());
self.print_schemes_table(&analysis.schemes);
if !analysis.global_requirements.options.is_empty() {
println!("\n{}", "Global Security Requirements".bold().green());
println!("{}", "─".repeat(80).green());
self.print_requirements(&analysis.global_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);
}
}
}
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();
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 => {
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(())
}
}