use clap::{Parser, Subcommand};
use tracing::{debug, error, warn, Level};
use tracing_subscriber::FmtSubscriber;
use crate::config::ScannerConfig;
mod banner;
mod baseline;
mod cache;
mod config;
mod constants;
mod core;
#[cfg(test)]
mod integration_tests;
mod mcp_client;
mod mcp_server;
mod normalize;
mod osv;
#[cfg(test)]
mod rule_eval;
mod sarif;
mod scanner;
mod security;
mod server;
mod skills;
mod taxonomy;
mod tls;
mod types;
mod utils;
use banner::display_banner;
use scanner::MCPScanner;
use server::MCPScannerServer;
use types::{config_utils, ScanConfigBuilder, ScanOptions};
use utils::error_utils;
#[derive(Parser)]
#[command(
name = "ramparts",
about = "A CLI tool for scanning Model Context Protocol (MCP) servers",
version,
long_about = "Scans MCP servers to discover available tools, resources, and capabilities with comprehensive security analysis.
SECURITY ASSESSMENTS:
Tool Security Assessments:
• Tool Poisoning: Detects tools with destructive or malicious intent that could harm the system or data
• SQL Injection: Identifies tools allowing SQL injection attacks that could compromise databases
• Command Injection: Detects tools that may execute system commands, posing critical security risks
• Path Traversal: Finds tools allowing directory traversal attacks to access unauthorized files
• Authentication Bypass: Identifies tools that could allow unauthorized access to protected resources
• Secrets Leakage: Detects tools processing sensitive credentials like API keys, passwords, tokens
Prompt Security Assessments:
• Prompt Injection: Identifies prompts vulnerable to injection attacks that could override safety measures
• Jailbreak: Detects prompts that could bypass AI safety measures and restrictions
• PII Leakage: Finds prompts handling personal information like emails, addresses, SSNs, credit cards
Resource Security Assessments:
• Path Traversal: Detects resources with directory traversal vulnerabilities in URIs
• Sensitive Data Exposure: Identifies resources containing sensitive information or credentials
IMPACT LEVELS:
• CRITICAL: Immediate security risk requiring immediate attention
• HIGH: Significant security vulnerability that should be addressed promptly
• MEDIUM: Moderate security concern that should be reviewed
• LOW: Minor security issue that may need monitoring
EXAMPLES:
• Basic scan: ramparts scan http://localhost:3000
• Security scan: ramparts scan http://localhost:3000
• From IDE config: ramparts scan-config
• Initialize config: ramparts init-config"
)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(short, long)]
verbose: bool,
#[arg(short, long)]
debug: bool,
}
#[derive(Subcommand)]
enum Commands {
Scan {
#[arg(value_name = "URL")]
url: String,
#[arg(long, value_delimiter = ',')]
auth_headers: Vec<String>,
#[arg(long, value_name = "FORMAT")]
format: Option<String>,
#[arg(long)]
report: bool,
#[arg(long, value_name = "SECONDS")]
timeout: Option<u64>,
#[arg(long, value_name = "SECONDS")]
http_timeout: Option<u64>,
#[arg(long, value_name = "KINDS")]
only: Option<String>,
},
ScanConfig {
#[arg(long, value_delimiter = ',')]
auth_headers: Vec<String>,
#[arg(long, value_name = "FORMAT")]
format: Option<String>,
#[arg(long)]
report: bool,
#[arg(long, value_name = "SECONDS")]
timeout: Option<u64>,
#[arg(long, value_name = "SECONDS")]
http_timeout: Option<u64>,
#[arg(long, value_name = "PATH")]
root: Option<std::path::PathBuf>,
#[arg(long, value_name = "KINDS")]
only: Option<String>,
},
InitConfig {
#[arg(short, long)]
force: bool,
},
Server {
#[arg(short, long, default_value = "3000")]
port: u16,
#[arg(long, default_value = "0.0.0.0")]
host: String,
},
McpStdio,
McpSse {
#[arg(long, default_value = "0.0.0.0")]
host: String,
#[arg(short, long, default_value = "8000")]
port: u16,
},
McpHttp {
#[arg(long, default_value = "0.0.0.0")]
host: String,
#[arg(short, long, default_value = "8081")]
port: u16,
},
Replay {
#[arg(value_name = "PATH")]
input: std::path::PathBuf,
#[arg(long, value_name = "FORMAT")]
format: Option<String>,
},
Skills(SkillsArgs),
}
#[derive(clap::Args, Debug)]
struct SkillsArgs {
#[command(subcommand)]
command: SkillsCommand,
}
#[derive(Subcommand, Debug)]
enum SkillsCommand {
Scan {
#[arg(value_name = "PATH")]
path: std::path::PathBuf,
#[arg(long, value_name = "FORMAT", conflicts_with_all = ["json", "sarif"])]
format: Option<String>,
#[arg(long, conflicts_with = "sarif")]
json: bool,
#[arg(long)]
sarif: bool,
#[arg(long)]
report: bool,
#[arg(long, value_name = "SECONDS")]
timeout: Option<u64>,
},
ScanConfig {
#[arg(long, value_name = "FORMAT", conflicts_with_all = ["json", "sarif"])]
format: Option<String>,
#[arg(long, conflicts_with = "sarif")]
json: bool,
#[arg(long)]
sarif: bool,
#[arg(long)]
report: bool,
#[arg(long, value_name = "SECONDS")]
timeout: Option<u64>,
},
}
fn resolve_format(format: Option<String>, json: bool, sarif: bool) -> Option<String> {
if format.is_some() {
format
} else if sarif {
Some("sarif".to_string())
} else if json {
Some("json".to_string())
} else {
None
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let cli = Cli::parse();
if should_display_banner(&cli.command) {
display_banner();
}
let scanner_config = load_scanner_config();
setup_logging(&cli, &scanner_config);
debug!("Starting MCP Scanner");
let scanner = create_scanner_if_needed(&cli, &scanner_config);
execute_command(cli, scanner_config, scanner).await?;
Ok(())
}
fn should_display_banner(command: &Commands) -> bool {
if matches!(command, Commands::McpStdio) {
return false;
}
let (format, machine_shortcut) = match command {
Commands::Scan { format, .. }
| Commands::ScanConfig { format, .. }
| Commands::Replay { format, .. } => (format.as_deref(), false),
Commands::Skills(args) => match &args.command {
SkillsCommand::Scan {
format,
json,
sarif,
..
}
| SkillsCommand::ScanConfig {
format,
json,
sarif,
..
} => (format.as_deref(), *json || *sarif),
},
_ => (None, false),
};
if machine_shortcut {
return false;
}
!matches!(
format.map(str::to_ascii_lowercase).as_deref(),
Some("json") | Some("raw") | Some("sarif")
)
}
fn load_scanner_config() -> ScannerConfig {
let config_manager = config::ScannerConfigManager::new();
match config_manager.load_config() {
Ok(config) => config,
Err(e) => {
warn!("Failed to load scanner config, using defaults: {}", e);
ScannerConfig::default()
}
}
}
fn setup_logging(cli: &Cli, scanner_config: &ScannerConfig) {
let level = determine_log_level(cli, scanner_config);
let filter = match level {
Level::DEBUG | Level::TRACE => {
tracing_subscriber::EnvFilter::from_default_env().add_directive(
format!("ramparts={level}")
.parse()
.expect("Failed to parse logging directive for debug/trace level"),
)
}
_ => {
tracing_subscriber::EnvFilter::new("warn").add_directive(
format!("ramparts={level}")
.parse()
.expect("Failed to parse logging directive for ramparts level"),
)
}
};
FmtSubscriber::builder()
.with_max_level(Level::TRACE) .with_env_filter(filter)
.with_target(false)
.with_thread_ids(false)
.with_thread_names(false)
.with_writer(std::io::stderr)
.init();
}
fn determine_log_level(cli: &Cli, scanner_config: &ScannerConfig) -> Level {
if cli.debug || cli.verbose {
Level::DEBUG
} else {
match scanner_config.logging.level.to_lowercase().as_str() {
"trace" => Level::TRACE,
"debug" => Level::DEBUG,
"warn" => Level::WARN,
"error" => Level::ERROR,
_ => Level::INFO,
}
}
}
fn create_scanner_if_needed(cli: &Cli, scanner_config: &ScannerConfig) -> Option<MCPScanner> {
let http_timeout_override = match &cli.command {
Commands::Scan { http_timeout, .. } | Commands::ScanConfig { http_timeout, .. } => {
*http_timeout
}
_ => return None,
};
let http_timeout = http_timeout_override.unwrap_or(scanner_config.scanner.http_timeout);
match MCPScanner::with_timeout(http_timeout) {
Ok(scanner) => Some(scanner),
Err(e) => {
error!("Failed to create scanner: {}", e);
std::process::exit(1);
}
}
}
async fn execute_command(
cli: Cli,
scanner_config: ScannerConfig,
scanner: Option<MCPScanner>,
) -> Result<(), Box<dyn std::error::Error>> {
match cli.command {
Commands::Scan {
url,
auth_headers,
format,
report,
timeout,
http_timeout,
only,
} => {
handle_scan_command(
url,
auth_headers,
format,
report,
timeout,
http_timeout,
only,
&scanner_config,
scanner,
)
.await
}
Commands::ScanConfig {
auth_headers,
format,
report,
timeout,
http_timeout,
root,
only,
} => {
handle_scan_config_command(
auth_headers,
format,
report,
timeout,
http_timeout,
root,
only,
&scanner_config,
scanner,
)
.await
}
Commands::InitConfig { force } => {
handle_init_config_command(force);
Ok(())
}
Commands::Server { port, host } => handle_server_command(port, host).await,
Commands::McpStdio => handle_mcp_stdio_command().await,
Commands::McpSse { host, port } => handle_mcp_sse_command(host, port).await,
Commands::McpHttp { host, port } => handle_mcp_http_command(host, port).await,
Commands::Replay { input, format } => handle_replay_command(input, format, &scanner_config),
Commands::Skills(args) => handle_skills_command(args, &scanner_config).await,
}
}
async fn handle_skills_command(
args: SkillsArgs,
scanner_config: &ScannerConfig,
) -> Result<(), Box<dyn std::error::Error>> {
match args.command {
SkillsCommand::Scan {
path,
format,
json,
sarif,
report,
timeout,
} => {
let format = resolve_format(format, json, sarif);
handle_skills_scan_command(vec![path], format, report, timeout, scanner_config).await
}
SkillsCommand::ScanConfig {
format,
json,
sarif,
report,
timeout,
} => {
let format = resolve_format(format, json, sarif);
let candidates = skills::default_discovery_roots();
let existing: Vec<std::path::PathBuf> =
candidates.iter().filter(|p| p.exists()).cloned().collect();
if existing.is_empty() {
let looked_at = candidates
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(", ");
error!("No skill discovery roots found. Looked at: {looked_at}");
return Err(
format!("No skill discovery roots found. Looked at: {looked_at}").into(),
);
}
handle_skills_scan_command(existing, format, report, timeout, scanner_config).await
}
}
}
async fn handle_skills_scan_command(
roots: Vec<std::path::PathBuf>,
format: Option<String>,
report: bool,
timeout: Option<u64>,
scanner_config: &ScannerConfig,
) -> Result<(), Box<dyn std::error::Error>> {
let output_format = format.unwrap_or(scanner_config.scanner.format.clone());
let mut skill_paths: Vec<std::path::PathBuf> = Vec::new();
let mut seen: std::collections::HashSet<std::path::PathBuf> = std::collections::HashSet::new();
let push_unique =
|p: std::path::PathBuf,
skill_paths: &mut Vec<std::path::PathBuf>,
seen: &mut std::collections::HashSet<std::path::PathBuf>| {
let key = std::fs::canonicalize(&p).unwrap_or_else(|_| p.clone());
if seen.insert(key) {
skill_paths.push(p);
}
};
for root in &roots {
if root.is_file() {
push_unique(root.clone(), &mut skill_paths, &mut seen);
} else if root.is_dir() {
match skills::discover_skills_in_root(root) {
Ok(found) => {
for p in found {
push_unique(p, &mut skill_paths, &mut seen);
}
}
Err(e) => warn!("Skipping skill root {}: {e}", root.display()),
}
} else {
warn!("Skill path not found: {}", root.display());
}
}
if skill_paths.is_empty() {
let msg = format!("No skill files found in: {roots:?}");
error!("{msg}");
return Err(msg.into());
}
let bundle_roots: std::collections::HashSet<std::path::PathBuf> = skill_paths
.iter()
.filter_map(|p| skills::bundle_root_of(p).map(std::path::Path::to_path_buf))
.collect();
if !bundle_roots.is_empty() {
let before = skill_paths.len();
skill_paths.retain(|p| !skills::is_under_bundle_sibling_dir(p, &bundle_roots));
let dropped = before - skill_paths.len();
if dropped > 0 {
debug!(
"Dropped {dropped} agentskills.io bundle-sibling path(s) from top-level walk \
(will be picked up by bundle parser)"
);
}
}
debug!("Found {} skill file(s) to scan", skill_paths.len());
let mut prompts: Vec<types::MCPPrompt> = Vec::with_capacity(skill_paths.len());
let mut prompt_paths: Vec<std::path::PathBuf> = Vec::with_capacity(skill_paths.len());
let mut parser_findings: Vec<types::YaraScanResult> = Vec::new();
let mut bundle_resources: Vec<types::MCPResource> = Vec::new();
let mut bundle_prompt_names: std::collections::HashSet<String> =
std::collections::HashSet::new();
let mut baseline_store = baseline::BaselineStore::load_default();
for p in &skill_paths {
let parsed_prompt = if skills::is_agentskills_bundle(p) {
skills::parse_agentskills_bundle(p).map(|(parsed, resources)| {
bundle_prompt_names.insert(parsed.prompt.name.clone());
bundle_resources.extend(resources);
parsed
})
} else {
skills::parse_skill_file(p)
};
if let Some(parsed) = parsed_prompt {
let path_key = std::fs::canonicalize(p)
.unwrap_or_else(|_| p.clone())
.display()
.to_string();
if let Some(finding) = baseline::check_skill_drift(
&mut baseline_store,
&path_key,
parsed.prompt.description.as_deref().unwrap_or(""),
) {
parser_findings.push(finding);
}
prompts.push(parsed.prompt);
prompt_paths.push(p.clone());
parser_findings.extend(parsed.heuristic_findings);
}
}
baseline_store.save();
let mut manifest_specs: Vec<osv::PackageSpec> = Vec::new();
for root in &bundle_roots {
for candidate in [
"requirements.txt",
"package.json",
"scripts/requirements.txt",
"scripts/package.json",
] {
let Some(content) = skills::read_bundle_file_no_escape(root, candidate) else {
continue;
};
let fname = std::path::Path::new(candidate)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
manifest_specs.extend(osv::parse_manifest_specs(fname, &content));
}
}
manifest_specs.sort_by(|a, b| {
(a.ecosystem, &a.name, &a.version).cmp(&(b.ecosystem, &b.name, &b.version))
});
manifest_specs
.dedup_by(|a, b| a.ecosystem == b.ecosystem && a.name == b.name && a.version == b.version);
manifest_specs.truncate(64);
let osv_handle = if manifest_specs.is_empty() {
None
} else {
let client = reqwest::Client::new();
Some(tokio::spawn(futures::future::join_all(
manifest_specs
.into_iter()
.map(|spec| osv::query_osv(client.clone(), spec)),
)))
};
if prompts.is_empty() {
let msg = "All discovered skill files failed to parse";
error!("{msg}");
return Err(msg.into());
}
let skill_set: Vec<(&std::path::Path, &types::MCPPrompt)> = prompt_paths
.iter()
.map(std::path::PathBuf::as_path)
.zip(prompts.iter())
.collect();
parser_findings.extend(skills::analyze_skill_set(&skill_set));
let scan_timer = utils::Timer::start();
let display_url = match roots.as_slice() {
[single] => format!("skills:{}", single.display()),
many => {
let joined = many
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(",");
format!("skills:[{}]({} files)", joined, skill_paths.len())
}
};
let mut result = types::ScanResult::new(display_url);
result.prompts = prompts;
#[cfg(feature = "yara-x-scanning")]
{
use scanner::ScanPhase;
let mut scan_data = scanner::ScanData::new();
scan_data.prompts = std::mem::take(&mut result.prompts);
scan_data.resources = std::mem::take(&mut bundle_resources);
match scanner::YaraScanner::new("rules", ScanPhase::PreScan) {
Ok(yara) => {
let mut chain = scanner::ScannerChain::new();
chain.add(Box::new(yara));
chain.run_pre_scan(&mut scan_data);
for finding in scan_data.yara_results.iter_mut() {
if finding.target_type != "resource" {
continue;
}
let Some((bundle_name, _)) = finding.target_name.split_once('/') else {
continue;
};
if bundle_prompt_names.contains(bundle_name) {
finding.target_type = "prompt".to_string();
}
}
result
.yara_results
.extend(std::mem::take(&mut scan_data.yara_results));
}
Err(e) => {
warn!("Skipping YARA pre-scan for skills (rules dir unreadable): {e}");
}
}
result.prompts = std::mem::take(&mut scan_data.prompts);
}
result.yara_results.append(&mut parser_findings);
if let Some(handle) = osv_handle {
match handle.await {
Ok(all) => {
for findings in all {
result.yara_results.extend(findings);
}
}
Err(e) => warn!("OSV manifest lookup task failed: {e}"),
}
}
let security_scanner = if scanner_config.security.enabled {
security::SecurityScanner::with_config(scanner_config.clone())
} else {
security::SecurityScanner::default()
};
let mut security_result = security::SecurityScanResult::new();
let scan_timeout =
std::time::Duration::from_secs(timeout.unwrap_or(scanner_config.scanner.scan_timeout));
match tokio::time::timeout(
scan_timeout,
security_scanner.scan_skills_batch(&result.prompts, scanner_config.scanner.detailed),
)
.await
{
Ok(Ok(prompt_issues)) => security_result.add_prompt_issues(prompt_issues),
Ok(Err(e)) => {
let msg = format!("Skill LLM analysis failed: {e}");
warn!("{msg}");
result.add_error(msg);
}
Err(_) => {
let msg = format!(
"Skill LLM analysis timed out after {}s",
scan_timeout.as_secs()
);
warn!("{msg}");
result.add_error(msg);
}
}
result.security_issues = Some(security_result);
for finding in &mut result.yara_results {
finding
.owasp_tags
.extend(taxonomy::ast_tags_for_rule(&finding.rule_name));
}
if let Some(sec) = result.security_issues.as_mut() {
for issue in sec
.tool_issues
.iter_mut()
.chain(sec.prompt_issues.iter_mut())
.chain(sec.resource_issues.iter_mut())
{
issue
.owasp_tags
.extend(taxonomy::ast_tags_for_security_issue(issue.issue_type));
}
}
result.response_time_ms = scan_timer.elapsed_ms();
utils::print_result(&result, &output_format, scanner_config.scanner.detailed);
if report {
match utils::write_markdown_report(&[result]) {
Ok(filename) => println!("\n📄 Detailed report generated: {filename}"),
Err(e) => warn!("Failed to generate report: {e}"),
}
}
Ok(())
}
fn handle_replay_command(
input: std::path::PathBuf,
format: Option<String>,
scanner_config: &ScannerConfig,
) -> Result<(), Box<dyn std::error::Error>> {
let bytes = std::fs::read(&input).map_err(|e| {
Box::<dyn std::error::Error>::from(format!(
"Failed to read replay input {}: {e}",
input.display()
))
})?;
let output_format = format.unwrap_or(scanner_config.scanner.format.clone());
if let Ok(results) = serde_json::from_slice::<Vec<types::ScanResult>>(&bytes) {
utils::print_multi_server_results(
&results,
&output_format,
scanner_config.scanner.detailed,
);
return Ok(());
}
let result: types::ScanResult = serde_json::from_slice(&bytes).map_err(|e| {
Box::<dyn std::error::Error>::from(format!(
"Replay input is neither a `ScanResult` nor a `Vec<ScanResult>`: {e}"
))
})?;
utils::print_result(&result, &output_format, scanner_config.scanner.detailed);
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn handle_scan_command(
url: String,
auth_headers: Vec<String>,
format: Option<String>,
report: bool,
timeout: Option<u64>,
http_timeout: Option<u64>,
only: Option<String>,
scanner_config: &ScannerConfig,
scanner: Option<MCPScanner>,
) -> Result<(), Box<dyn std::error::Error>> {
let auth_headers_map = parse_auth_headers(&auth_headers);
let output_format = format.unwrap_or(scanner_config.scanner.format.clone());
let only_kinds = parse_only_filter(only)?;
let options = build_scan_options(
scanner_config,
&output_format,
auth_headers_map,
timeout,
http_timeout,
only_kinds,
);
validate_scan_config(&options);
let scanner = scanner
.as_ref()
.expect("Scanner should be initialized for scan command");
match scanner.scan_single(&url, options.clone()).await {
Ok(result) => {
utils::print_result(&result, &output_format, options.detailed);
if report {
match utils::write_markdown_report(&[result]) {
Ok(filename) => {
println!("\n📄 Detailed report generated: {filename}");
}
Err(e) => {
warn!("Failed to generate report: {}", e);
}
}
}
Ok(())
}
Err(e) => {
error!(
"{}",
error_utils::format_error("Scan operation", &e.to_string())
);
std::process::exit(1);
}
}
}
#[allow(clippy::too_many_arguments)]
async fn handle_scan_config_command(
auth_headers: Vec<String>,
format: Option<String>,
report: bool,
timeout: Option<u64>,
http_timeout: Option<u64>,
root: Option<std::path::PathBuf>,
only: Option<String>,
scanner_config: &ScannerConfig,
scanner: Option<MCPScanner>,
) -> Result<(), Box<dyn std::error::Error>> {
let auth_headers_map = parse_auth_headers(&auth_headers);
let output_format = format.unwrap_or(scanner_config.scanner.format.clone());
let only_kinds = parse_only_filter(only)?;
let options = build_scan_options(
scanner_config,
&output_format,
auth_headers_map,
timeout,
http_timeout,
only_kinds,
);
validate_scan_config(&options);
let scanner = scanner
.as_ref()
.expect("Scanner should be initialized for scan-config command");
let scan_outcome = match root.as_deref() {
Some(root_path) => scanner.scan_config_in_root(root_path, options).await,
None => scanner.scan_config_by_ide(options).await,
};
match scan_outcome {
Ok(results) => {
utils::print_multi_server_results(
&results,
&output_format,
scanner_config.scanner.detailed,
);
if report {
match utils::write_markdown_report(&results) {
Ok(filename) => {
println!("\n📄 Detailed report generated: {filename}");
}
Err(e) => {
warn!("Failed to generate report: {}", e);
}
}
}
Ok(())
}
Err(e) => {
error!(
"{}",
error_utils::format_error("IDE configuration scan operation", &e.to_string())
);
std::process::exit(1);
}
}
}
fn handle_init_config_command(force: bool) {
let config_manager = config::ScannerConfigManager::new();
if config_manager.has_config_file() && !force {
println!("config.yaml already exists. Use --force to overwrite.");
std::process::exit(1);
}
match config_manager.save_config(&config::ScannerConfig::default()) {
Ok(()) => {
println!("Created config.yaml with default settings");
println!(
"📝 Edit the file to customize LLM settings, security checks, and other options"
);
}
Err(e) => {
error!("Failed to create config.yaml: {}", e);
std::process::exit(1);
}
}
}
async fn handle_server_command(port: u16, host: String) -> Result<(), Box<dyn std::error::Error>> {
debug!("Starting MCP Scanner microservice on {}:{}", host, port);
match MCPScannerServer::new() {
Ok(server) => {
let server = server.with_port(port).with_host(host);
if let Err(e) = server.start().await {
error!("Server failed: {}", e);
std::process::exit(1);
}
Ok(())
}
Err(e) => {
error!("Failed to create server: {}", e);
std::process::exit(1);
}
}
}
async fn handle_mcp_stdio_command() -> Result<(), Box<dyn std::error::Error>> {
mcp_server::run_stdio_server().await
}
async fn handle_mcp_sse_command(host: String, port: u16) -> Result<(), Box<dyn std::error::Error>> {
mcp_server::run_sse_server(&host, port).await
}
async fn handle_mcp_http_command(
host: String,
port: u16,
) -> Result<(), Box<dyn std::error::Error>> {
mcp_server::run_streamable_http_server(&host, port).await
}
fn build_scan_options(
scanner_config: &ScannerConfig,
output_format: &str,
auth_headers_map: Option<std::collections::HashMap<String, String>>,
timeout_override: Option<u64>,
http_timeout_override: Option<u64>,
only_kinds: Option<Vec<types::ArtifactKind>>,
) -> ScanOptions {
ScanConfigBuilder::new()
.timeout(timeout_override.unwrap_or(scanner_config.scanner.scan_timeout))
.http_timeout(http_timeout_override.unwrap_or(scanner_config.scanner.http_timeout))
.detailed(scanner_config.scanner.detailed)
.format(output_format.to_string())
.auth_headers(auth_headers_map)
.only(only_kinds)
.build()
}
fn parse_only_filter(
raw: Option<String>,
) -> Result<Option<Vec<types::ArtifactKind>>, Box<dyn std::error::Error>> {
match raw {
None => Ok(None),
Some(s) => {
let kinds = types::ArtifactKind::parse_set(&s)
.map_err(|e| Box::<dyn std::error::Error>::from(e.to_string()))?;
if kinds.is_empty() {
Ok(None)
} else {
Ok(Some(kinds))
}
}
}
}
fn validate_scan_config(options: &ScanOptions) {
if let Err(e) = config_utils::validate_scan_config(options) {
error!("Invalid configuration: {}", e);
std::process::exit(1);
}
}
fn parse_auth_headers(headers: &[String]) -> Option<std::collections::HashMap<String, String>> {
if headers.is_empty() {
return None;
}
let mut map = std::collections::HashMap::new();
for header in headers {
if let Some((key, value)) = header.split_once(':') {
map.insert(key.trim().to_string(), value.trim().to_string());
}
}
Some(map)
}