use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(transparent)]
pub struct LanguageId(pub String);
impl LanguageId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn rust() -> Self {
Self("rust".into())
}
}
impl std::fmt::Display for LanguageId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Info,
Low,
Medium,
High,
Critical,
}
impl Severity {
pub fn as_str(self) -> &'static str {
match self {
Self::Info => "info",
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::Critical => "critical",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"info" => Some(Self::Info),
"low" => Some(Self::Low),
"medium" | "med" => Some(Self::Medium),
"high" => Some(Self::High),
"critical" | "crit" => Some(Self::Critical),
_ => None,
}
}
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Confidence {
Low,
Medium,
High,
Definite,
}
impl Confidence {
pub fn as_str(self) -> &'static str {
match self {
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::Definite => "definite",
}
}
pub fn is_definite_bug(self) -> bool {
matches!(self, Self::Definite)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceLocation {
pub file: PathBuf,
pub start_line: u32,
pub start_column: u32,
pub end_line: u32,
pub end_column: u32,
}
impl SourceLocation {
pub fn single_line(file: impl Into<PathBuf>, line: u32, column: u32) -> Self {
Self {
file: file.into(),
start_line: line,
start_column: column,
end_line: line,
end_column: column.saturating_add(1),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct CodeContext {
pub before: Vec<String>,
pub highlight: String,
pub after: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Diagnostic {
pub message: String,
pub location: SourceLocation,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context: Option<CodeContext>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DetectionEvidence {
pub kind: String,
pub detail: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Finding {
pub id: String,
pub rule_id: String,
pub severity: Severity,
pub confidence: Confidence,
pub language: LanguageId,
pub location: SourceLocation,
pub explanation: String,
pub failure_scenario: String,
pub remediation: String,
pub evidence: Vec<DetectionEvidence>,
pub definite_bug: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context: Option<CodeContext>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fix_hint: Option<String>,
}
impl Finding {
pub fn builder(rule_id: impl Into<String>, language: LanguageId) -> FindingBuilder {
FindingBuilder {
rule_id: rule_id.into(),
language,
severity: Severity::Medium,
confidence: Confidence::Medium,
location: None,
explanation: String::new(),
failure_scenario: String::new(),
remediation: String::new(),
evidence: Vec::new(),
context: None,
fix_hint: None,
id: None,
}
}
}
pub struct FindingBuilder {
rule_id: String,
language: LanguageId,
severity: Severity,
confidence: Confidence,
location: Option<SourceLocation>,
explanation: String,
failure_scenario: String,
remediation: String,
evidence: Vec<DetectionEvidence>,
context: Option<CodeContext>,
fix_hint: Option<String>,
id: Option<String>,
}
impl FindingBuilder {
pub fn severity(mut self, s: Severity) -> Self {
self.severity = s;
self
}
pub fn confidence(mut self, c: Confidence) -> Self {
self.confidence = c;
self
}
pub fn location(mut self, loc: SourceLocation) -> Self {
self.location = Some(loc);
self
}
pub fn explanation(mut self, e: impl Into<String>) -> Self {
self.explanation = e.into();
self
}
pub fn failure_scenario(mut self, e: impl Into<String>) -> Self {
self.failure_scenario = e.into();
self
}
pub fn remediation(mut self, e: impl Into<String>) -> Self {
self.remediation = e.into();
self
}
pub fn evidence(mut self, kind: impl Into<String>, detail: impl Into<String>) -> Self {
self.evidence.push(DetectionEvidence {
kind: kind.into(),
detail: detail.into(),
});
self
}
pub fn context(mut self, c: CodeContext) -> Self {
self.context = Some(c);
self
}
pub fn fix_hint(mut self, h: impl Into<String>) -> Self {
self.fix_hint = Some(h.into());
self
}
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn build(self) -> Finding {
let location = self.location.expect("Finding requires location");
let definite_bug = self.confidence.is_definite_bug();
let id = self.id.unwrap_or_else(|| {
format!(
"{}:{}:{}:{}",
self.rule_id,
location.file.display(),
location.start_line,
location.start_column
)
});
Finding {
id,
rule_id: self.rule_id,
severity: self.severity,
confidence: self.confidence,
language: self.language,
location,
explanation: self.explanation,
failure_scenario: self.failure_scenario,
remediation: self.remediation,
evidence: self.evidence,
definite_bug,
context: self.context,
fix_hint: self.fix_hint,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Suppression {
pub rule_id: Option<String>,
pub file: Option<PathBuf>,
pub line: Option<u32>,
pub reason: Option<String>,
pub pattern: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ApprovedPattern {
pub rule_id: Option<String>,
pub path_glob: Option<String>,
pub code_contains: Option<String>,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceFile {
pub path: PathBuf,
pub content: String,
pub language: LanguageId,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ControlFlow {
pub nodes: Vec<String>,
pub edges: Vec<(usize, usize)>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ErrorPath {
pub steps: Vec<String>,
pub terminates: bool,
pub recovers: bool,
pub context_lost: bool,
}