use std::fmt;
use crate::config::Config;
use crate::scanner::{FileData, ScanResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum Severity {
Critical,
High,
Warning,
Info,
}
impl Severity {
pub fn label(&self) -> &'static str {
match self {
Severity::Critical => "CRITICAL",
Severity::High => "HIGH",
Severity::Warning => "WARNING",
Severity::Info => "INFO",
}
}
pub fn from_word(word: &str) -> Option<Severity> {
match word.to_ascii_lowercase().as_str() {
"critical" => Some(Severity::Critical),
"high" => Some(Severity::High),
"medium" | "warning" | "warn" => Some(Severity::Warning),
"low" | "info" | "informational" => Some(Severity::Info),
_ => None,
}
}
pub fn score_weight(&self) -> u32 {
match self {
Severity::Critical => 25,
Severity::High => 10,
Severity::Warning => 3,
Severity::Info => 1,
}
}
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum Category {
Security,
Quality,
Dependencies,
}
impl Category {
pub fn title(&self) -> &'static str {
match self {
Category::Security => "Security",
Category::Quality => "Quality",
Category::Dependencies => "Dependencies",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Finding {
pub rule: String,
pub title: String,
pub category: Category,
pub severity: Severity,
pub file: String,
pub line: usize,
pub message: String,
pub suggestion: Option<String>,
}
impl Finding {
pub fn new(
rule: impl Into<String>,
title: impl Into<String>,
category: Category,
severity: Severity,
file: impl Into<String>,
line: usize,
message: impl Into<String>,
) -> Finding {
Finding {
rule: rule.into(),
title: title.into(),
category,
severity,
file: file.into(),
line,
message: message.into(),
suggestion: None,
}
}
pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Finding {
self.suggestion = Some(suggestion.into());
self
}
}
pub trait Rule: Send + Sync {
fn id(&self) -> &str;
fn check(&self, file: &FileData) -> Vec<Finding>;
fn name(&self) -> &str {
self.id()
}
fn description(&self) -> &str {
""
}
fn category(&self) -> Category {
Category::Quality
}
}
pub struct Analyzer {
rules: Vec<Box<dyn Rule>>,
}
impl Analyzer {
pub fn new(rules: Vec<Box<dyn Rule>>) -> Self {
Analyzer { rules }
}
pub fn with_default_rules(config: &Config) -> Self {
let rules = crate::rules::default_rules(config);
Analyzer::new(rules)
}
pub fn rule_count(&self) -> usize {
self.rules.len()
}
pub fn analyze(&self, scan: &ScanResult) -> Vec<Finding> {
self.analyze_files(&scan.files)
}
pub fn analyze_files(&self, files: &[FileData]) -> Vec<Finding> {
let mut findings = Vec::new();
for file in files {
for rule in &self.rules {
findings.extend(rule.check(file));
}
}
findings.sort_by(|a, b| {
a.severity
.cmp(&b.severity)
.then_with(|| a.file.cmp(&b.file))
.then_with(|| a.line.cmp(&b.line))
});
findings
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn severity_ordering() {
assert!(Severity::Critical < Severity::High);
assert!(Severity::High < Severity::Warning);
assert!(Severity::Warning < Severity::Info);
}
#[test]
fn score_weights_decrease_with_severity() {
assert!(Severity::Critical.score_weight() > Severity::High.score_weight());
assert!(Severity::High.score_weight() > Severity::Warning.score_weight());
assert!(Severity::Warning.score_weight() > Severity::Info.score_weight());
}
#[test]
fn finding_constructor_matches_struct_literal() {
let built = Finding::new(
"my-rule",
"Something happened",
Category::Quality,
Severity::Info,
"src/lib.rs",
7,
"details",
)
.with_suggestion("fix it");
assert_eq!(
built,
Finding {
rule: "my-rule".to_string(),
title: "Something happened".to_string(),
category: Category::Quality,
severity: Severity::Info,
file: "src/lib.rs".to_string(),
line: 7,
message: "details".to_string(),
suggestion: Some("fix it".to_string()),
}
);
}
#[test]
fn finding_constructor_leaves_suggestion_unset() {
let built = Finding::new(
"my-rule",
"t",
Category::Security,
Severity::Critical,
"f",
1,
"m",
);
assert_eq!(built.suggestion, None);
}
struct MinimalRule;
impl Rule for MinimalRule {
fn id(&self) -> &str {
"minimal"
}
fn check(&self, _file: &FileData) -> Vec<Finding> {
Vec::new()
}
}
#[test]
fn rule_defaults_are_conservative() {
let rule = MinimalRule;
assert_eq!(rule.name(), "minimal", "name falls back to the id");
assert_eq!(rule.description(), "");
assert_eq!(
rule.category(),
Category::Quality,
"an undeclared rule must not count as a security rule"
);
}
}