pub mod mcp_scanner;
pub mod patterns;
pub mod skill_scanner;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity {
Info,
Warning,
Error,
Critical,
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Severity::Info => write!(f, "INFO"),
Severity::Warning => write!(f, "WARN"),
Severity::Error => write!(f, "ERROR"),
Severity::Critical => write!(f, "CRITICAL"),
}
}
}
#[derive(Debug, Clone)]
pub struct Finding {
pub code: String,
pub severity: Severity,
pub title: String,
pub description: String,
pub location: Option<String>,
pub line: Option<usize>,
}
impl fmt::Display for Finding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{}] {} ({}): {}",
self.severity, self.code, self.title, self.description
)?;
if let Some(ref loc) = self.location {
write!(f, " at {}", loc)?;
}
if let Some(line) = self.line {
write!(f, ":{}", line)?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct ScanReport {
pub target: String,
pub findings: Vec<Finding>,
}
impl ScanReport {
pub fn new(target: &str) -> Self {
Self {
target: target.to_string(),
findings: Vec::new(),
}
}
pub fn add(&mut self, finding: Finding) {
self.findings.push(finding);
}
pub fn filtered(&self, min: Severity) -> Self {
Self {
target: self.target.clone(),
findings: self
.findings
.iter()
.filter(|f| f.severity >= min)
.cloned()
.collect(),
}
}
pub fn findings_at_severity(&self, min: Severity) -> Vec<&Finding> {
self.findings.iter().filter(|f| f.severity >= min).collect()
}
pub fn has_critical(&self) -> bool {
self.findings
.iter()
.any(|f| f.severity == Severity::Critical)
}
pub fn has_errors(&self) -> bool {
self.findings.iter().any(|f| f.severity >= Severity::Error)
}
pub fn is_clean(&self) -> bool {
self.findings.is_empty()
}
pub fn format_text(&self) -> String {
if self.findings.is_empty() {
return format!("[PASS] {} — no security issues found", self.target);
}
let mut lines = vec![format!(
"[SCAN] {} — {} issue(s) found",
self.target,
self.findings.len()
)];
let mut sorted = self.findings.clone();
sorted.sort_by(|a, b| b.severity.cmp(&a.severity));
for f in &sorted {
lines.push(format!(" {}", f));
}
lines.join("\n")
}
pub fn format_json(&self) -> serde_json::Value {
serde_json::json!({
"target": self.target,
"findings": self.findings.iter().map(|f| {
let mut obj = serde_json::json!({
"code": f.code,
"severity": format!("{}", f.severity),
"title": f.title,
"description": f.description,
});
if let Some(ref loc) = f.location {
obj["location"] = serde_json::json!(loc);
}
if let Some(line) = f.line {
obj["line"] = serde_json::json!(line);
}
obj
}).collect::<Vec<_>>(),
"summary": {
"total": self.findings.len(),
"critical": self.findings.iter().filter(|f| f.severity == Severity::Critical).count(),
"error": self.findings.iter().filter(|f| f.severity == Severity::Error).count(),
"warning": self.findings.iter().filter(|f| f.severity == Severity::Warning).count(),
"info": self.findings.iter().filter(|f| f.severity == Severity::Info).count(),
}
})
}
}