use crate::scoring::RiskScore;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseEnumError {
type_name: &'static str,
value: String,
}
impl ParseEnumError {
pub fn invalid(type_name: &'static str, value: &str) -> Self {
Self {
type_name,
value: value.to_string(),
}
}
}
impl std::fmt::Display for ParseEnumError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "invalid {} value: '{}'", self.type_name, self.value)
}
}
impl std::error::Error for ParseEnumError {}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
Default,
clap::ValueEnum,
)]
#[serde(rename_all = "lowercase")]
pub enum RuleSeverity {
Warn,
#[default]
Error,
}
impl RuleSeverity {
pub fn as_str(&self) -> &'static str {
match self {
RuleSeverity::Warn => "warn",
RuleSeverity::Error => "error",
}
}
}
impl std::fmt::Display for RuleSeverity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str().to_uppercase())
}
}
impl std::str::FromStr for RuleSeverity {
type Err = ParseEnumError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"warn" | "warning" => Ok(RuleSeverity::Warn),
"error" | "err" => Ok(RuleSeverity::Error),
_ => Err(ParseEnumError::invalid("RuleSeverity", s)),
}
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
clap::ValueEnum,
)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Low,
Medium,
High,
Critical,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
Default,
clap::ValueEnum,
)]
#[serde(rename_all = "lowercase")]
pub enum Confidence {
Tentative,
#[default]
Firm,
Certain,
}
impl Confidence {
pub fn as_str(&self) -> &'static str {
match self {
Confidence::Tentative => "tentative",
Confidence::Firm => "firm",
Confidence::Certain => "certain",
}
}
pub fn downgrade(&self) -> Self {
match self {
Confidence::Certain => Confidence::Firm,
Confidence::Firm => Confidence::Tentative,
Confidence::Tentative => Confidence::Tentative,
}
}
}
impl std::fmt::Display for Confidence {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl std::str::FromStr for Confidence {
type Err = ParseEnumError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"tentative" => Ok(Confidence::Tentative),
"firm" => Ok(Confidence::Firm),
"certain" => Ok(Confidence::Certain),
_ => Err(ParseEnumError::invalid("Confidence", s)),
}
}
}
impl Severity {
pub fn as_str(&self) -> &'static str {
match self {
Severity::Low => "low",
Severity::Medium => "medium",
Severity::High => "high",
Severity::Critical => "critical",
}
}
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str().to_uppercase())
}
}
impl std::str::FromStr for Severity {
type Err = ParseEnumError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"low" => Ok(Severity::Low),
"medium" | "med" => Ok(Severity::Medium),
"high" => Ok(Severity::High),
"critical" | "crit" => Ok(Severity::Critical),
_ => Err(ParseEnumError::invalid("Severity", s)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Category {
Exfiltration,
PrivilegeEscalation,
Persistence,
PromptInjection,
Overpermission,
Obfuscation,
SupplyChain,
SecretLeak,
}
impl Category {
pub fn as_str(&self) -> &'static str {
match self {
Category::Exfiltration => "exfiltration",
Category::PrivilegeEscalation => "privilege_escalation",
Category::Persistence => "persistence",
Category::PromptInjection => "prompt_injection",
Category::Overpermission => "overpermission",
Category::Obfuscation => "obfuscation",
Category::SupplyChain => "supply_chain",
Category::SecretLeak => "secret_leak",
}
}
}
impl std::str::FromStr for Category {
type Err = ParseEnumError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().replace(['_', '-'], "").as_str() {
"exfiltration" | "exfil" => Ok(Category::Exfiltration),
"privilegeescalation" | "privesc" => Ok(Category::PrivilegeEscalation),
"persistence" => Ok(Category::Persistence),
"promptinjection" => Ok(Category::PromptInjection),
"overpermission" => Ok(Category::Overpermission),
"obfuscation" => Ok(Category::Obfuscation),
"supplychain" => Ok(Category::SupplyChain),
"secretleak" => Ok(Category::SecretLeak),
_ => Err(ParseEnumError::invalid("Category", s)),
}
}
}
#[derive(Debug, Clone)]
pub struct Rule {
pub id: &'static str,
pub name: &'static str,
pub description: &'static str,
pub severity: Severity,
pub category: Category,
pub confidence: Confidence,
pub patterns: Vec<regex::Regex>,
pub exclusions: Vec<regex::Regex>,
pub message: &'static str,
pub recommendation: &'static str,
pub fix_hint: Option<&'static str>,
pub cwe_ids: &'static [&'static str],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Location {
pub file: String,
pub line: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub column: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Finding {
pub id: String,
pub severity: Severity,
pub category: Category,
pub confidence: Confidence,
pub name: String,
pub location: Location,
pub code: String,
pub message: String,
pub recommendation: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub fix_hint: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cwe_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rule_severity: Option<RuleSeverity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context: Option<crate::context::ContentContext>,
}
impl Finding {
pub fn new(rule: &Rule, location: Location, code: String) -> Self {
Self {
id: rule.id.to_string(),
severity: rule.severity,
category: rule.category,
confidence: rule.confidence,
name: rule.name.to_string(),
location,
code,
message: rule.message.to_string(),
recommendation: rule.recommendation.to_string(),
fix_hint: rule.fix_hint.map(|s| s.to_string()),
cwe_ids: rule.cwe_ids.iter().map(|s| s.to_string()).collect(),
rule_severity: None, client: None, context: None, }
}
pub fn with_context(mut self, context: crate::context::ContentContext) -> Self {
self.context = Some(context);
self
}
pub fn with_client(mut self, client: Option<String>) -> Self {
self.client = client;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Summary {
pub critical: usize,
pub high: usize,
pub medium: usize,
pub low: usize,
pub passed: bool,
#[serde(default)]
pub errors: usize,
#[serde(default)]
pub warnings: usize,
}
impl Summary {
pub fn from_findings(findings: &[Finding]) -> Self {
let (critical, high, medium, low) =
findings
.iter()
.fold((0, 0, 0, 0), |(c, h, m, l), f| match f.severity {
Severity::Critical => (c + 1, h, m, l),
Severity::High => (c, h + 1, m, l),
Severity::Medium => (c, h, m + 1, l),
Severity::Low => (c, h, m, l + 1),
});
Self {
critical,
high,
medium,
low,
passed: critical == 0 && high == 0,
errors: 0,
warnings: 0,
}
}
pub fn from_findings_with_rule_severity(findings: &[Finding]) -> Self {
let (critical, high, medium, low, errors, warnings) =
findings
.iter()
.fold((0, 0, 0, 0, 0, 0), |(c, h, m, l, e, w), f| {
let (new_c, new_h, new_m, new_l) = match f.severity {
Severity::Critical => (c + 1, h, m, l),
Severity::High => (c, h + 1, m, l),
Severity::Medium => (c, h, m + 1, l),
Severity::Low => (c, h, m, l + 1),
};
let (new_e, new_w) = match f.rule_severity {
Some(RuleSeverity::Error) | None => (e + 1, w), Some(RuleSeverity::Warn) => (e, w + 1),
};
(new_c, new_h, new_m, new_l, new_e, new_w)
});
Self {
critical,
high,
medium,
low,
passed: errors == 0, errors,
warnings,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanResult {
pub version: String,
pub scanned_at: String,
pub target: String,
pub summary: Summary,
pub findings: Vec<Finding>,
#[serde(skip_serializing_if = "Option::is_none")]
pub risk_score: Option<RiskScore>,
pub elapsed_ms: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_severity_as_str() {
assert_eq!(Severity::Low.as_str(), "low");
assert_eq!(Severity::Medium.as_str(), "medium");
assert_eq!(Severity::High.as_str(), "high");
assert_eq!(Severity::Critical.as_str(), "critical");
}
#[test]
fn test_severity_display() {
assert_eq!(format!("{}", Severity::Low), "LOW");
assert_eq!(format!("{}", Severity::Medium), "MEDIUM");
assert_eq!(format!("{}", Severity::High), "HIGH");
assert_eq!(format!("{}", Severity::Critical), "CRITICAL");
}
#[test]
fn test_severity_ordering() {
assert!(Severity::Low < Severity::Medium);
assert!(Severity::Medium < Severity::High);
assert!(Severity::High < Severity::Critical);
}
#[test]
fn test_category_as_str() {
assert_eq!(Category::Exfiltration.as_str(), "exfiltration");
assert_eq!(
Category::PrivilegeEscalation.as_str(),
"privilege_escalation"
);
assert_eq!(Category::Persistence.as_str(), "persistence");
assert_eq!(Category::PromptInjection.as_str(), "prompt_injection");
assert_eq!(Category::Overpermission.as_str(), "overpermission");
assert_eq!(Category::Obfuscation.as_str(), "obfuscation");
assert_eq!(Category::SupplyChain.as_str(), "supply_chain");
assert_eq!(Category::SecretLeak.as_str(), "secret_leak");
}
#[test]
fn test_summary_from_empty_findings() {
let findings: Vec<Finding> = vec![];
let summary = Summary::from_findings(&findings);
assert_eq!(summary.critical, 0);
assert_eq!(summary.high, 0);
assert_eq!(summary.medium, 0);
assert_eq!(summary.low, 0);
assert!(summary.passed);
}
#[test]
fn test_summary_from_findings_with_critical() {
let findings = vec![Finding {
id: "EX-001".to_string(),
severity: Severity::Critical,
category: Category::Exfiltration,
confidence: Confidence::Certain,
name: "Test".to_string(),
location: Location {
file: "test.sh".to_string(),
line: 1,
column: None,
},
code: "test".to_string(),
message: "test".to_string(),
recommendation: "test".to_string(),
fix_hint: None,
cwe_ids: vec![],
rule_severity: None,
client: None,
context: None,
}];
let summary = Summary::from_findings(&findings);
assert_eq!(summary.critical, 1);
assert!(!summary.passed);
}
#[test]
fn test_summary_from_findings_all_severities() {
let findings = vec![
Finding {
id: "C-001".to_string(),
severity: Severity::Critical,
category: Category::Exfiltration,
confidence: Confidence::Certain,
name: "Critical".to_string(),
location: Location {
file: "test.sh".to_string(),
line: 1,
column: None,
},
code: "test".to_string(),
message: "test".to_string(),
recommendation: "test".to_string(),
fix_hint: None,
cwe_ids: vec![],
rule_severity: None,
client: None,
context: None,
},
Finding {
id: "H-001".to_string(),
severity: Severity::High,
category: Category::PrivilegeEscalation,
confidence: Confidence::Firm,
name: "High".to_string(),
location: Location {
file: "test.sh".to_string(),
line: 2,
column: None,
},
code: "test".to_string(),
message: "test".to_string(),
recommendation: "test".to_string(),
fix_hint: None,
cwe_ids: vec![],
rule_severity: None,
client: None,
context: None,
},
Finding {
id: "M-001".to_string(),
severity: Severity::Medium,
category: Category::Persistence,
confidence: Confidence::Tentative,
name: "Medium".to_string(),
location: Location {
file: "test.sh".to_string(),
line: 3,
column: Some(5),
},
code: "test".to_string(),
message: "test".to_string(),
recommendation: "test".to_string(),
fix_hint: None,
cwe_ids: vec![],
rule_severity: None,
client: None,
context: None,
},
Finding {
id: "L-001".to_string(),
severity: Severity::Low,
category: Category::Overpermission,
confidence: Confidence::Firm,
name: "Low".to_string(),
location: Location {
file: "test.sh".to_string(),
line: 4,
column: None,
},
code: "test".to_string(),
message: "test".to_string(),
recommendation: "test".to_string(),
fix_hint: None,
cwe_ids: vec![],
rule_severity: None,
client: None,
context: None,
},
];
let summary = Summary::from_findings(&findings);
assert_eq!(summary.critical, 1);
assert_eq!(summary.high, 1);
assert_eq!(summary.medium, 1);
assert_eq!(summary.low, 1);
assert!(!summary.passed);
}
#[test]
fn test_summary_passes_with_only_medium_low() {
let findings = vec![
Finding {
id: "M-001".to_string(),
severity: Severity::Medium,
category: Category::Persistence,
confidence: Confidence::Firm,
name: "Medium".to_string(),
location: Location {
file: "test.sh".to_string(),
line: 1,
column: None,
},
code: "test".to_string(),
message: "test".to_string(),
recommendation: "test".to_string(),
fix_hint: None,
cwe_ids: vec![],
rule_severity: None,
client: None,
context: None,
},
Finding {
id: "L-001".to_string(),
severity: Severity::Low,
category: Category::Overpermission,
confidence: Confidence::Firm,
name: "Low".to_string(),
location: Location {
file: "test.sh".to_string(),
line: 2,
column: None,
},
code: "test".to_string(),
message: "test".to_string(),
recommendation: "test".to_string(),
fix_hint: None,
cwe_ids: vec![],
rule_severity: None,
client: None,
context: None,
},
];
let summary = Summary::from_findings(&findings);
assert!(summary.passed);
}
#[test]
fn test_finding_new() {
let rule = Rule {
id: "TEST-001",
name: "Test Rule",
description: "A test rule",
severity: Severity::High,
category: Category::Exfiltration,
confidence: Confidence::Certain,
patterns: vec![],
exclusions: vec![],
message: "Test message",
recommendation: "Test recommendation",
fix_hint: Some("Test fix hint"),
cwe_ids: &["CWE-200", "CWE-78"],
};
let location = Location {
file: "test.sh".to_string(),
line: 42,
column: Some(10),
};
let finding = Finding::new(&rule, location, "test code".to_string());
assert_eq!(finding.id, "TEST-001");
assert_eq!(finding.name, "Test Rule");
assert_eq!(finding.severity, Severity::High);
assert_eq!(finding.category, Category::Exfiltration);
assert_eq!(finding.confidence, Confidence::Certain);
assert_eq!(finding.location.file, "test.sh");
assert_eq!(finding.location.line, 42);
assert_eq!(finding.location.column, Some(10));
assert_eq!(finding.code, "test code");
assert_eq!(finding.message, "Test message");
assert_eq!(finding.recommendation, "Test recommendation");
assert_eq!(finding.cwe_ids, vec!["CWE-200", "CWE-78"]);
}
#[test]
fn test_confidence_as_str() {
assert_eq!(Confidence::Tentative.as_str(), "tentative");
assert_eq!(Confidence::Firm.as_str(), "firm");
assert_eq!(Confidence::Certain.as_str(), "certain");
}
#[test]
fn test_confidence_display() {
assert_eq!(format!("{}", Confidence::Tentative), "tentative");
assert_eq!(format!("{}", Confidence::Firm), "firm");
assert_eq!(format!("{}", Confidence::Certain), "certain");
}
#[test]
fn test_confidence_downgrade() {
assert_eq!(Confidence::Certain.downgrade(), Confidence::Firm);
assert_eq!(Confidence::Firm.downgrade(), Confidence::Tentative);
assert_eq!(Confidence::Tentative.downgrade(), Confidence::Tentative);
}
#[test]
fn test_confidence_downgrade_twice() {
let confidence = Confidence::Certain;
let downgraded_once = confidence.downgrade();
let downgraded_twice = downgraded_once.downgrade();
assert_eq!(downgraded_twice, Confidence::Tentative);
}
#[test]
fn test_confidence_ordering() {
assert!(Confidence::Tentative < Confidence::Firm);
assert!(Confidence::Firm < Confidence::Certain);
}
#[test]
fn test_confidence_default() {
assert_eq!(Confidence::default(), Confidence::Firm);
}
#[test]
fn test_confidence_serialization() {
let confidence = Confidence::Certain;
let json = serde_json::to_string(&confidence).unwrap();
assert_eq!(json, "\"certain\"");
let deserialized: Confidence = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, Confidence::Certain);
}
#[test]
fn test_severity_serialization() {
let severity = Severity::Critical;
let json = serde_json::to_string(&severity).unwrap();
assert_eq!(json, "\"critical\"");
let deserialized: Severity = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, Severity::Critical);
}
#[test]
fn test_category_serialization() {
let category = Category::PromptInjection;
let json = serde_json::to_string(&category).unwrap();
assert_eq!(json, "\"promptinjection\"");
let deserialized: Category = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, Category::PromptInjection);
}
#[test]
fn test_location_without_column_serialization() {
let location = Location {
file: "test.sh".to_string(),
line: 10,
column: None,
};
let json = serde_json::to_string(&location).unwrap();
assert!(!json.contains("column"));
}
#[test]
fn test_location_with_column_serialization() {
let location = Location {
file: "test.sh".to_string(),
line: 10,
column: Some(5),
};
let json = serde_json::to_string(&location).unwrap();
assert!(json.contains("\"column\":5"));
}
#[test]
fn test_rule_severity_default_is_error() {
assert_eq!(RuleSeverity::default(), RuleSeverity::Error);
}
#[test]
fn test_rule_severity_as_str() {
assert_eq!(RuleSeverity::Error.as_str(), "error");
assert_eq!(RuleSeverity::Warn.as_str(), "warn");
}
#[test]
fn test_rule_severity_display() {
assert_eq!(format!("{}", RuleSeverity::Error), "ERROR");
assert_eq!(format!("{}", RuleSeverity::Warn), "WARN");
}
#[test]
fn test_rule_severity_ordering() {
assert!(RuleSeverity::Warn < RuleSeverity::Error);
}
#[test]
fn test_rule_severity_serialization() {
let error = RuleSeverity::Error;
let json = serde_json::to_string(&error).unwrap();
assert_eq!(json, "\"error\"");
let warn = RuleSeverity::Warn;
let json = serde_json::to_string(&warn).unwrap();
assert_eq!(json, "\"warn\"");
let deserialized: RuleSeverity = serde_json::from_str("\"error\"").unwrap();
assert_eq!(deserialized, RuleSeverity::Error);
let deserialized: RuleSeverity = serde_json::from_str("\"warn\"").unwrap();
assert_eq!(deserialized, RuleSeverity::Warn);
}
fn create_test_finding(
id: &str,
severity: Severity,
rule_severity: Option<RuleSeverity>,
) -> Finding {
Finding {
id: id.to_string(),
severity,
category: Category::Exfiltration,
confidence: Confidence::Firm,
name: "Test".to_string(),
location: Location {
file: "test.sh".to_string(),
line: 1,
column: None,
},
code: "test".to_string(),
message: "test".to_string(),
recommendation: "test".to_string(),
fix_hint: None,
cwe_ids: vec![],
rule_severity,
client: None,
context: None,
}
}
#[test]
fn test_summary_with_rule_severity_empty() {
let findings: Vec<Finding> = vec![];
let summary = Summary::from_findings_with_rule_severity(&findings);
assert_eq!(summary.errors, 0);
assert_eq!(summary.warnings, 0);
assert!(summary.passed);
}
#[test]
fn test_summary_with_rule_severity_all_errors() {
let findings = vec![
create_test_finding("E-001", Severity::Critical, Some(RuleSeverity::Error)),
create_test_finding("E-002", Severity::High, Some(RuleSeverity::Error)),
];
let summary = Summary::from_findings_with_rule_severity(&findings);
assert_eq!(summary.errors, 2);
assert_eq!(summary.warnings, 0);
assert!(!summary.passed);
}
#[test]
fn test_summary_with_rule_severity_all_warnings() {
let findings = vec![
create_test_finding("W-001", Severity::Critical, Some(RuleSeverity::Warn)),
create_test_finding("W-002", Severity::High, Some(RuleSeverity::Warn)),
];
let summary = Summary::from_findings_with_rule_severity(&findings);
assert_eq!(summary.errors, 0);
assert_eq!(summary.warnings, 2);
assert!(summary.passed); }
#[test]
fn test_summary_with_rule_severity_mixed() {
let findings = vec![
create_test_finding("E-001", Severity::Critical, Some(RuleSeverity::Error)),
create_test_finding("W-001", Severity::High, Some(RuleSeverity::Warn)),
create_test_finding("W-002", Severity::Medium, Some(RuleSeverity::Warn)),
];
let summary = Summary::from_findings_with_rule_severity(&findings);
assert_eq!(summary.errors, 1);
assert_eq!(summary.warnings, 2);
assert!(!summary.passed); assert_eq!(summary.critical, 1);
assert_eq!(summary.high, 1);
assert_eq!(summary.medium, 1);
}
#[test]
fn test_summary_with_rule_severity_none_defaults_to_error() {
let findings = vec![
create_test_finding("N-001", Severity::Low, None), ];
let summary = Summary::from_findings_with_rule_severity(&findings);
assert_eq!(summary.errors, 1);
assert_eq!(summary.warnings, 0);
assert!(!summary.passed);
}
#[test]
fn test_finding_rule_severity_not_serialized_when_none() {
let finding = create_test_finding("TEST-001", Severity::High, None);
let json = serde_json::to_string(&finding).unwrap();
assert!(!json.contains("rule_severity"));
}
#[test]
fn test_finding_rule_severity_serialized_when_some() {
let finding = create_test_finding("TEST-001", Severity::High, Some(RuleSeverity::Warn));
let json = serde_json::to_string(&finding).unwrap();
assert!(json.contains("\"rule_severity\":\"warn\""));
}
#[test]
fn test_parse_enum_error_invalid() {
let error = ParseEnumError::invalid("TestType", "bad_value");
assert_eq!(error.type_name, "TestType");
assert_eq!(error.value, "bad_value");
}
#[test]
fn test_parse_enum_error_display() {
let error = ParseEnumError::invalid("RuleSeverity", "unknown");
let display = format!("{}", error);
assert_eq!(display, "invalid RuleSeverity value: 'unknown'");
}
#[test]
fn test_parse_enum_error_debug() {
let error = ParseEnumError::invalid("TestType", "value");
let debug = format!("{:?}", error);
assert!(debug.contains("ParseEnumError"));
assert!(debug.contains("TestType"));
assert!(debug.contains("value"));
}
#[test]
fn test_parse_enum_error_is_error() {
let error = ParseEnumError::invalid("Test", "val");
let _: &dyn std::error::Error = &error;
}
#[test]
fn test_rule_severity_from_str_valid() {
use std::str::FromStr;
assert_eq!(RuleSeverity::from_str("warn").unwrap(), RuleSeverity::Warn);
assert_eq!(
RuleSeverity::from_str("error").unwrap(),
RuleSeverity::Error
);
assert_eq!(
RuleSeverity::from_str("warning").unwrap(),
RuleSeverity::Warn
);
assert_eq!(RuleSeverity::from_str("err").unwrap(), RuleSeverity::Error);
assert_eq!(RuleSeverity::from_str("WARN").unwrap(), RuleSeverity::Warn);
assert_eq!(
RuleSeverity::from_str("ERROR").unwrap(),
RuleSeverity::Error
);
assert_eq!(
RuleSeverity::from_str("Warning").unwrap(),
RuleSeverity::Warn
);
}
#[test]
fn test_rule_severity_from_str_invalid() {
use std::str::FromStr;
let result = RuleSeverity::from_str("invalid");
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.type_name, "RuleSeverity");
assert_eq!(error.value, "invalid");
let result = RuleSeverity::from_str("");
assert!(result.is_err());
let result = RuleSeverity::from_str("critical");
assert!(result.is_err());
}
#[test]
fn test_severity_from_str_valid() {
use std::str::FromStr;
assert_eq!(Severity::from_str("low").unwrap(), Severity::Low);
assert_eq!(Severity::from_str("LOW").unwrap(), Severity::Low);
assert_eq!(Severity::from_str("Low").unwrap(), Severity::Low);
assert_eq!(Severity::from_str("medium").unwrap(), Severity::Medium);
assert_eq!(Severity::from_str("MEDIUM").unwrap(), Severity::Medium);
assert_eq!(Severity::from_str("med").unwrap(), Severity::Medium);
assert_eq!(Severity::from_str("MED").unwrap(), Severity::Medium);
assert_eq!(Severity::from_str("high").unwrap(), Severity::High);
assert_eq!(Severity::from_str("HIGH").unwrap(), Severity::High);
assert_eq!(Severity::from_str("critical").unwrap(), Severity::Critical);
assert_eq!(Severity::from_str("CRITICAL").unwrap(), Severity::Critical);
assert_eq!(Severity::from_str("crit").unwrap(), Severity::Critical);
assert_eq!(Severity::from_str("CRIT").unwrap(), Severity::Critical);
}
#[test]
fn test_severity_from_str_invalid() {
use std::str::FromStr;
let result = Severity::from_str("invalid");
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.type_name, "Severity");
assert_eq!(error.value, "invalid");
let result = Severity::from_str("");
assert!(result.is_err());
}
#[test]
fn test_category_from_str_valid() {
use std::str::FromStr;
assert_eq!(
Category::from_str("exfiltration").unwrap(),
Category::Exfiltration
);
assert_eq!(
Category::from_str("EXFILTRATION").unwrap(),
Category::Exfiltration
);
assert_eq!(Category::from_str("exfil").unwrap(), Category::Exfiltration);
assert_eq!(Category::from_str("EXFIL").unwrap(), Category::Exfiltration);
assert_eq!(
Category::from_str("privilege_escalation").unwrap(),
Category::PrivilegeEscalation
);
assert_eq!(
Category::from_str("privilege-escalation").unwrap(),
Category::PrivilegeEscalation
);
assert_eq!(
Category::from_str("privilegeescalation").unwrap(),
Category::PrivilegeEscalation
);
assert_eq!(
Category::from_str("privesc").unwrap(),
Category::PrivilegeEscalation
);
assert_eq!(
Category::from_str("persistence").unwrap(),
Category::Persistence
);
assert_eq!(
Category::from_str("prompt_injection").unwrap(),
Category::PromptInjection
);
assert_eq!(
Category::from_str("promptinjection").unwrap(),
Category::PromptInjection
);
assert_eq!(
Category::from_str("overpermission").unwrap(),
Category::Overpermission
);
assert_eq!(
Category::from_str("obfuscation").unwrap(),
Category::Obfuscation
);
assert_eq!(
Category::from_str("supply_chain").unwrap(),
Category::SupplyChain
);
assert_eq!(
Category::from_str("supplychain").unwrap(),
Category::SupplyChain
);
assert_eq!(
Category::from_str("secret_leak").unwrap(),
Category::SecretLeak
);
assert_eq!(
Category::from_str("secretleak").unwrap(),
Category::SecretLeak
);
}
#[test]
fn test_category_from_str_invalid() {
use std::str::FromStr;
let result = Category::from_str("invalid");
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.type_name, "Category");
assert_eq!(error.value, "invalid");
let result = Category::from_str("");
assert!(result.is_err());
}
#[test]
fn test_finding_with_context() {
use crate::context::ContentContext;
let finding = create_test_finding("TEST-001", Severity::High, None);
assert!(finding.context.is_none());
let finding_with_context = finding.with_context(ContentContext::Documentation);
assert_eq!(
finding_with_context.context,
Some(ContentContext::Documentation)
);
}
#[test]
fn test_finding_with_client() {
let finding = create_test_finding("TEST-001", Severity::High, None);
assert!(finding.client.is_none());
let finding_with_client = finding.with_client(Some("claude".to_string()));
assert_eq!(finding_with_client.client, Some("claude".to_string()));
let finding2 = create_test_finding("TEST-002", Severity::Medium, None);
let finding_without_client = finding2.with_client(None);
assert!(finding_without_client.client.is_none());
}
}