use std::collections::HashMap;
use clap::ValueEnum;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OutputFormat {
#[default]
Json,
Text,
Sarif,
}
impl std::fmt::Display for OutputFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Json => write!(f, "json"),
Self::Text => write!(f, "text"),
Self::Sarif => write!(f, "sarif"),
}
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
ValueEnum,
Default,
)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Critical,
High,
#[default]
Medium,
Low,
Info,
}
impl Severity {
pub fn order(&self) -> u8 {
match self {
Self::Critical => 0,
Self::High => 1,
Self::Medium => 2,
Self::Low => 3,
Self::Info => 4,
}
}
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Critical => write!(f, "critical"),
Self::High => write!(f, "high"),
Self::Medium => write!(f, "medium"),
Self::Low => write!(f, "low"),
Self::Info => write!(f, "info"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Location {
pub file: String,
pub line: u32,
#[serde(default)]
pub column: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_line: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_column: Option<u32>,
}
impl Location {
pub fn new(file: impl Into<String>, line: u32) -> Self {
Self {
file: file.into(),
line,
column: 0,
end_line: None,
end_column: None,
}
}
pub fn with_column(file: impl Into<String>, line: u32, column: u32) -> Self {
Self {
file: file.into(),
line,
column,
end_line: None,
end_column: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TodoItem {
pub category: String,
pub priority: u32,
pub description: String,
#[serde(default)]
pub file: String,
#[serde(default)]
pub line: u32,
#[serde(default)]
pub severity: String,
#[serde(default)]
pub score: f64,
}
impl TodoItem {
pub fn new(category: impl Into<String>, priority: u32, description: impl Into<String>) -> Self {
Self {
category: category.into(),
priority,
description: description.into(),
file: String::new(),
line: 0,
severity: String::new(),
score: 0.0,
}
}
pub fn with_location(mut self, file: impl Into<String>, line: u32) -> Self {
self.file = file.into();
self.line = line;
self
}
pub fn with_severity(mut self, severity: impl Into<String>) -> Self {
self.severity = severity.into();
self
}
pub fn with_score(mut self, score: f64) -> Self {
self.score = score;
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TodoSummary {
pub dead_count: u32,
pub similar_pairs: u32,
pub low_cohesion_count: u32,
pub hotspot_count: u32,
pub equivalence_groups: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TodoReport {
pub wrapper: String,
pub path: String,
pub items: Vec<TodoItem>,
pub summary: TodoSummary,
#[serde(default)]
pub sub_results: HashMap<String, Value>,
pub total_elapsed_ms: f64,
}
impl TodoReport {
pub fn new(path: impl Into<String>) -> Self {
Self {
wrapper: "todo".to_string(),
path: path.into(),
items: Vec::new(),
summary: TodoSummary::default(),
sub_results: HashMap::new(),
total_elapsed_ms: 0.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecureFinding {
pub category: String,
pub severity: String,
pub description: String,
#[serde(default)]
pub file: String,
#[serde(default)]
pub line: u32,
}
impl SecureFinding {
pub fn new(
category: impl Into<String>,
severity: impl Into<String>,
description: impl Into<String>,
) -> Self {
Self {
category: category.into(),
severity: severity.into(),
description: description.into(),
file: String::new(),
line: 0,
}
}
pub fn with_location(mut self, file: impl Into<String>, line: u32) -> Self {
self.file = file.into();
self.line = line;
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SecureSummary {
pub taint_count: u32,
pub taint_critical: u32,
pub leak_count: u32,
pub bounds_warnings: u32,
pub missing_contracts: u32,
pub mutable_params: u32,
#[serde(default)]
pub unsafe_blocks: u32,
#[serde(default)]
pub raw_pointer_ops: u32,
#[serde(default)]
pub unwrap_calls: u32,
#[serde(default)]
pub todo_markers: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecureReport {
pub wrapper: String,
pub path: String,
pub findings: Vec<SecureFinding>,
pub summary: SecureSummary,
#[serde(default)]
pub sub_results: HashMap<String, Value>,
pub total_elapsed_ms: f64,
}
impl SecureReport {
pub fn new(path: impl Into<String>) -> Self {
Self {
wrapper: "secure".to_string(),
path: path.into(),
findings: Vec::new(),
summary: SecureSummary::default(),
sub_results: HashMap::new(),
total_elapsed_ms: 0.0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_output_format_serialization() {
let json = serde_json::to_string(&OutputFormat::Json).unwrap();
assert_eq!(json, r#""json""#);
let text = serde_json::to_string(&OutputFormat::Text).unwrap();
assert_eq!(text, r#""text""#);
let sarif = serde_json::to_string(&OutputFormat::Sarif).unwrap();
assert_eq!(sarif, r#""sarif""#);
}
#[test]
fn test_severity_ordering() {
assert!(Severity::Critical.order() < Severity::High.order());
assert!(Severity::High.order() < Severity::Medium.order());
assert!(Severity::Medium.order() < Severity::Low.order());
assert!(Severity::Low.order() < Severity::Info.order());
}
#[test]
fn test_location_serialization() {
let loc = Location::new("test.py", 42);
let json = serde_json::to_string(&loc).unwrap();
assert!(json.contains(r#""file":"test.py""#));
assert!(json.contains(r#""line":42"#));
}
#[test]
fn test_todo_report_serialization() {
let mut report = TodoReport::new("/path/to/file.py");
report
.items
.push(TodoItem::new("dead_code", 1, "Unused function"));
report.summary.dead_count = 1;
let json = serde_json::to_string(&report).unwrap();
assert!(json.contains(r#""wrapper":"todo""#));
assert!(json.contains(r#""dead_count":1"#));
}
#[test]
fn test_todo_item_builder() {
let item = TodoItem::new("complexity", 2, "High cyclomatic complexity")
.with_location("src/main.py", 100)
.with_severity("high")
.with_score(0.85);
assert_eq!(item.category, "complexity");
assert_eq!(item.file, "src/main.py");
assert_eq!(item.line, 100);
assert_eq!(item.severity, "high");
assert!((item.score - 0.85).abs() < 0.001);
}
#[test]
fn test_secure_report_serialization() {
let mut report = SecureReport::new("/path/to/file.py");
report
.findings
.push(SecureFinding::new("taint", "critical", "SQL injection"));
report.summary.taint_count = 1;
report.summary.taint_critical = 1;
let json = serde_json::to_string(&report).unwrap();
assert!(json.contains(r#""wrapper":"secure""#));
assert!(json.contains(r#""taint_count":1"#));
assert!(json.contains(r#""taint_critical":1"#));
}
#[test]
fn test_secure_finding_builder() {
let finding = SecureFinding::new("resource_leak", "high", "File not closed")
.with_location("src/db.py", 42);
assert_eq!(finding.category, "resource_leak");
assert_eq!(finding.severity, "high");
assert_eq!(finding.file, "src/db.py");
assert_eq!(finding.line, 42);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParamInfo {
pub name: String,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub type_hint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
}
impl ParamInfo {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
type_hint: None,
default: None,
}
}
pub fn with_type(mut self, type_hint: impl Into<String>) -> Self {
self.type_hint = Some(type_hint.into());
self
}
pub fn with_default(mut self, default: impl Into<String>) -> Self {
self.default = Some(default.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignatureInfo {
pub params: Vec<ParamInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub return_type: Option<String>,
#[serde(default)]
pub decorators: Vec<String>,
#[serde(default)]
pub is_async: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub docstring: Option<String>,
}
impl SignatureInfo {
pub fn new() -> Self {
Self {
params: Vec::new(),
return_type: None,
decorators: Vec::new(),
is_async: false,
docstring: None,
}
}
pub fn with_param(mut self, param: ParamInfo) -> Self {
self.params.push(param);
self
}
pub fn with_return_type(mut self, return_type: impl Into<String>) -> Self {
self.return_type = Some(return_type.into());
self
}
pub fn with_docstring(mut self, docstring: impl Into<String>) -> Self {
self.docstring = Some(docstring.into());
self
}
pub fn set_async(mut self, is_async: bool) -> Self {
self.is_async = is_async;
self
}
}
impl Default for SignatureInfo {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PurityInfo {
pub classification: String,
#[serde(default)]
pub effects: Vec<String>,
pub confidence: String,
}
impl PurityInfo {
pub fn pure() -> Self {
Self {
classification: "pure".to_string(),
effects: Vec::new(),
confidence: "high".to_string(),
}
}
pub fn impure(effects: Vec<String>) -> Self {
Self {
classification: "impure".to_string(),
effects,
confidence: "high".to_string(),
}
}
pub fn unknown() -> Self {
Self {
classification: "unknown".to_string(),
effects: Vec::new(),
confidence: "low".to_string(),
}
}
pub fn with_confidence(mut self, confidence: impl Into<String>) -> Self {
self.confidence = confidence.into();
self
}
}
impl Default for PurityInfo {
fn default() -> Self {
Self::unknown()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplexityInfo {
pub cyclomatic: u32,
pub num_blocks: u32,
pub num_edges: u32,
pub has_loops: bool,
}
impl ComplexityInfo {
pub fn new(cyclomatic: u32, num_blocks: u32, num_edges: u32, has_loops: bool) -> Self {
Self {
cyclomatic,
num_blocks,
num_edges,
has_loops,
}
}
}
impl Default for ComplexityInfo {
fn default() -> Self {
Self {
cyclomatic: 1,
num_blocks: 1,
num_edges: 0,
has_loops: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallInfo {
pub name: String,
pub file: String,
pub line: u32,
}
impl CallInfo {
pub fn new(name: impl Into<String>, file: impl Into<String>, line: u32) -> Self {
Self {
name: name.into(),
file: file.into(),
line,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExplainReport {
pub function_name: String,
pub file: String,
pub line_start: u32,
pub line_end: u32,
pub language: String,
pub signature: SignatureInfo,
pub purity: PurityInfo,
#[serde(skip_serializing_if = "Option::is_none")]
pub complexity: Option<ComplexityInfo>,
#[serde(default)]
pub callers: Vec<CallInfo>,
#[serde(default)]
pub callees: Vec<CallInfo>,
}
impl ExplainReport {
pub fn new(
function_name: impl Into<String>,
file: impl Into<String>,
line_start: u32,
line_end: u32,
language: impl Into<String>,
) -> Self {
Self {
function_name: function_name.into(),
file: file.into(),
line_start,
line_end,
language: language.into(),
signature: SignatureInfo::default(),
purity: PurityInfo::default(),
complexity: None,
callers: Vec::new(),
callees: Vec::new(),
}
}
pub fn with_signature(mut self, signature: SignatureInfo) -> Self {
self.signature = signature;
self
}
pub fn with_purity(mut self, purity: PurityInfo) -> Self {
self.purity = purity;
self
}
pub fn with_complexity(mut self, complexity: ComplexityInfo) -> Self {
self.complexity = Some(complexity);
self
}
pub fn add_caller(&mut self, caller: CallInfo) {
self.callers.push(caller);
}
pub fn add_callee(&mut self, callee: CallInfo) {
self.callees.push(callee);
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Serialize,
Deserialize,
ValueEnum,
Default,
)]
#[serde(rename_all = "snake_case")]
pub enum SymbolKind {
Function,
Class,
Method,
Variable,
Parameter,
Constant,
Module,
Type,
Interface,
Property,
#[default]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SymbolInfo {
pub name: String,
pub kind: SymbolKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<Location>,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_annotation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub docstring: Option<String>,
#[serde(default)]
pub is_builtin: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub module: Option<String>,
}
impl SymbolInfo {
pub fn new(name: impl Into<String>, kind: SymbolKind) -> Self {
Self {
name: name.into(),
kind,
location: None,
type_annotation: None,
docstring: None,
is_builtin: false,
module: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DefinitionResult {
pub symbol: SymbolInfo,
#[serde(skip_serializing_if = "Option::is_none")]
pub definition: Option<Location>,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_definition: Option<Location>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChangeType {
Insert,
Delete,
Update,
Move,
Rename,
Extract,
Inline,
Format,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DiffGranularity {
Token,
Expression,
Statement,
#[default]
Function,
Class,
File,
Module,
Architecture,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BaseChanges {
pub added: Vec<String>,
pub removed: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NodeKind {
Function,
Class,
Method,
Field,
Statement,
Expression,
Block,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ASTChange {
pub change_type: ChangeType,
pub node_kind: NodeKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub old_location: Option<Location>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_location: Option<Location>,
#[serde(skip_serializing_if = "Option::is_none")]
pub old_text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub similarity: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub children: Option<Vec<ASTChange>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub base_changes: Option<BaseChanges>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DiffSummary {
pub total_changes: u32,
pub semantic_changes: u32,
pub inserts: u32,
pub deletes: u32,
pub updates: u32,
pub moves: u32,
pub renames: u32,
pub formats: u32,
pub extracts: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileLevelChange {
pub relative_path: String,
pub change_type: ChangeType,
#[serde(skip_serializing_if = "Option::is_none")]
pub old_fingerprint: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_fingerprint: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature_changes: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportEdge {
pub source_file: String,
pub target_module: String,
pub imported_names: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModuleLevelChange {
pub module_path: String,
pub change_type: ChangeType,
pub imports_added: Vec<ImportEdge>,
pub imports_removed: Vec<ImportEdge>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_change: Option<FileLevelChange>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportGraphSummary {
pub total_edges_a: usize,
pub total_edges_b: usize,
pub edges_added: usize,
pub edges_removed: usize,
pub modules_with_import_changes: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ArchChangeType {
LayerMigration,
Added,
Removed,
CompositionChanged,
CycleIntroduced,
CycleResolved,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArchLevelChange {
pub directory: String,
pub change_type: ArchChangeType,
#[serde(skip_serializing_if = "Option::is_none")]
pub old_layer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_layer: Option<String>,
#[serde(default)]
pub migrated_functions: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArchDiffSummary {
pub layer_migrations: usize,
pub directories_added: usize,
pub directories_removed: usize,
pub cycles_introduced: usize,
pub cycles_resolved: usize,
pub stability_score: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffReport {
pub file_a: String,
pub file_b: String,
pub identical: bool,
pub changes: Vec<ASTChange>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<DiffSummary>,
#[serde(default)]
pub granularity: DiffGranularity,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_changes: Option<Vec<FileLevelChange>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub module_changes: Option<Vec<ModuleLevelChange>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub import_graph_summary: Option<ImportGraphSummary>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arch_changes: Option<Vec<ArchLevelChange>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arch_summary: Option<ArchDiffSummary>,
}
impl DiffReport {
pub fn new(file_a: impl Into<String>, file_b: impl Into<String>) -> Self {
Self {
file_a: file_a.into(),
file_b: file_b.into(),
identical: true,
changes: Vec::new(),
summary: Some(DiffSummary::default()),
granularity: DiffGranularity::Function,
file_changes: None,
module_changes: None,
import_graph_summary: None,
arch_changes: None,
arch_summary: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChangedFunction {
pub name: String,
pub file: String,
pub line: u32,
#[serde(default)]
pub callers: Vec<CallInfo>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DiffImpactSummary {
pub files_changed: u32,
pub functions_changed: u32,
pub tests_to_run: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffImpactReport {
pub changed_functions: Vec<ChangedFunction>,
pub suggested_tests: Vec<String>,
pub summary: DiffImpactSummary,
}
impl DiffImpactReport {
pub fn new() -> Self {
Self {
changed_functions: Vec::new(),
suggested_tests: Vec::new(),
summary: DiffImpactSummary::default(),
}
}
}
impl Default for DiffImpactReport {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
#[serde(rename_all = "snake_case")]
pub enum MisuseCategory {
CallOrder,
ErrorHandling,
Parameters,
Resources,
Crypto,
Concurrency,
Security,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
#[serde(rename_all = "snake_case")]
pub enum MisuseSeverity {
Info,
Low,
Medium,
High,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct APIRule {
pub id: String,
pub name: String,
pub category: MisuseCategory,
pub severity: MisuseSeverity,
pub description: String,
pub correct_usage: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MisuseFinding {
pub file: String,
pub line: u32,
pub column: u32,
pub rule: APIRule,
pub api_call: String,
pub message: String,
pub fix_suggestion: String,
#[serde(default)]
pub code_context: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct APICheckSummary {
pub total_findings: u32,
#[serde(default)]
pub by_category: HashMap<String, u32>,
#[serde(default)]
pub by_severity: HashMap<String, u32>,
#[serde(default)]
pub apis_checked: Vec<String>,
pub files_scanned: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct APICheckReport {
pub findings: Vec<MisuseFinding>,
pub summary: APICheckSummary,
pub rules_applied: u32,
}
impl APICheckReport {
pub fn new() -> Self {
Self {
findings: Vec::new(),
summary: APICheckSummary::default(),
rules_applied: 0,
}
}
}
impl Default for APICheckReport {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExpressionRef {
pub text: String,
pub line: u32,
pub value_number: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GVNEquivalence {
pub value_number: u32,
pub expressions: Vec<ExpressionRef>,
#[serde(default)]
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Redundancy {
pub original: ExpressionRef,
pub redundant: ExpressionRef,
#[serde(default)]
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GVNSummary {
pub total_expressions: u32,
pub unique_values: u32,
pub compression_ratio: f64,
}
impl Default for GVNSummary {
fn default() -> Self {
Self {
total_expressions: 0,
unique_values: 0,
compression_ratio: 1.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GVNReport {
pub function: String,
#[serde(default)]
pub equivalences: Vec<GVNEquivalence>,
#[serde(default)]
pub redundancies: Vec<Redundancy>,
pub summary: GVNSummary,
}
impl GVNReport {
pub fn new(function: impl Into<String>) -> Self {
Self {
function: function.into(),
equivalences: Vec::new(),
redundancies: Vec::new(),
summary: GVNSummary::default(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, ValueEnum)]
#[serde(rename_all = "snake_case")]
#[value(rename_all = "snake_case")]
pub enum VulnType {
SqlInjection,
Xss,
CommandInjection,
Ssrf,
PathTraversal,
Deserialization,
UnsafeCode,
MemorySafety,
Panic,
Xxe,
OpenRedirect,
LdapInjection,
XpathInjection,
}
impl VulnType {
pub fn cwe_id(&self) -> &'static str {
match self {
Self::SqlInjection => "CWE-89",
Self::Xss => "CWE-79",
Self::CommandInjection => "CWE-78",
Self::Ssrf => "CWE-918",
Self::PathTraversal => "CWE-22",
Self::Deserialization => "CWE-502",
Self::UnsafeCode => "CWE-242",
Self::MemorySafety => "CWE-119",
Self::Panic => "CWE-703",
Self::Xxe => "CWE-611",
Self::OpenRedirect => "CWE-601",
Self::LdapInjection => "CWE-90",
Self::XpathInjection => "CWE-643",
}
}
pub fn default_severity(&self) -> Severity {
match self {
Self::SqlInjection
| Self::CommandInjection
| Self::Deserialization
| Self::MemorySafety => Severity::Critical,
Self::Xxe
| Self::Xss
| Self::Ssrf
| Self::PathTraversal
| Self::LdapInjection
| Self::XpathInjection
| Self::UnsafeCode => Severity::High,
Self::OpenRedirect | Self::Panic => Severity::Medium,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaintFlow {
pub file: String,
pub line: u32,
pub column: u32,
pub code_snippet: String,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VulnFinding {
pub vuln_type: VulnType,
pub severity: Severity,
pub cwe_id: String,
pub title: String,
pub description: String,
pub file: String,
pub line: u32,
pub column: u32,
pub taint_flow: Vec<TaintFlow>,
pub remediation: String,
pub confidence: f64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct VulnSummary {
pub total_findings: u32,
#[serde(default)]
pub by_severity: HashMap<String, u32>,
#[serde(default)]
pub by_type: HashMap<String, u32>,
pub files_with_vulns: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VulnReport {
pub findings: Vec<VulnFinding>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<VulnSummary>,
pub scan_duration_ms: u64,
pub files_scanned: u32,
}
impl VulnReport {
pub fn new() -> Self {
Self {
findings: Vec::new(),
summary: None,
scan_duration_ms: 0,
files_scanned: 0,
}
}
}
impl Default for VulnReport {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod unit_types_tests {
use super::*;
#[test]
fn test_output_format_serialization() {
let json = serde_json::to_string(&OutputFormat::Json).unwrap();
assert_eq!(json, r#""json""#);
let text = serde_json::to_string(&OutputFormat::Text).unwrap();
assert_eq!(text, r#""text""#);
let sarif = serde_json::to_string(&OutputFormat::Sarif).unwrap();
assert_eq!(sarif, r#""sarif""#);
}
#[test]
fn test_output_format_deserialization() {
let json: OutputFormat = serde_json::from_str(r#""json""#).unwrap();
assert_eq!(json, OutputFormat::Json);
let text: OutputFormat = serde_json::from_str(r#""text""#).unwrap();
assert_eq!(text, OutputFormat::Text);
}
#[test]
fn test_severity_ordering() {
assert!(Severity::Critical.order() < Severity::High.order());
assert!(Severity::High.order() < Severity::Medium.order());
assert!(Severity::Medium.order() < Severity::Low.order());
assert!(Severity::Low.order() < Severity::Info.order());
}
#[test]
fn test_severity_serialization() {
let critical = serde_json::to_string(&Severity::Critical).unwrap();
assert_eq!(critical, r#""critical""#);
let info = serde_json::to_string(&Severity::Info).unwrap();
assert_eq!(info, r#""info""#);
}
#[test]
fn test_location_serialization() {
let loc = Location::new("test.py", 42);
let json = serde_json::to_string(&loc).unwrap();
assert!(json.contains(r#""file":"test.py""#));
assert!(json.contains(r#""line":42"#));
}
#[test]
fn test_location_with_column() {
let loc = Location::with_column("test.py", 42, 10);
assert_eq!(loc.column, 10);
}
#[test]
fn test_todo_report_serialization() {
let mut report = TodoReport::new("/path/to/file.py");
report
.items
.push(TodoItem::new("dead_code", 1, "Unused function"));
report.summary.dead_count = 1;
let json = serde_json::to_string(&report).unwrap();
assert!(json.contains(r#""wrapper":"todo""#));
assert!(json.contains(r#""dead_count":1"#));
}
#[test]
fn test_todo_item_builder() {
let item = TodoItem::new("complexity", 2, "High cyclomatic complexity")
.with_location("src/main.py", 100)
.with_severity("high")
.with_score(0.85);
assert_eq!(item.category, "complexity");
assert_eq!(item.file, "src/main.py");
assert_eq!(item.line, 100);
assert_eq!(item.severity, "high");
assert!((item.score - 0.85).abs() < 0.001);
}
#[test]
fn test_explain_report_serialization() {
let mut report = ExplainReport::new("calculate_total", "/path/file.py", 10, 20, "python");
report.purity = PurityInfo::pure();
let json = serde_json::to_string(&report).unwrap();
assert!(json.contains(r#""function_name":"calculate_total""#));
assert!(json.contains(r#""classification":"pure""#));
}
#[test]
fn test_signature_info_builder() {
let sig = SignatureInfo::new()
.with_param(ParamInfo::new("x").with_type("int"))
.with_return_type("int")
.with_docstring("Doubles the input");
assert_eq!(sig.params.len(), 1);
assert_eq!(sig.params[0].name, "x");
assert_eq!(sig.return_type.unwrap(), "int");
}
#[test]
fn test_secure_report_serialization() {
let mut report = SecureReport::new("/path/to/file.py");
report
.findings
.push(SecureFinding::new("taint", "critical", "SQL injection"));
let json = serde_json::to_string(&report).unwrap();
assert!(json.contains(r#""wrapper":"secure""#));
}
#[test]
fn test_definition_result_serialization() {
let result = DefinitionResult {
symbol: SymbolInfo::new("my_func", SymbolKind::Function),
definition: Some(Location::new("file.py", 10)),
type_definition: None,
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains(r#""name":"my_func""#));
assert!(json.contains(r#""kind":"function""#));
}
#[test]
fn test_symbol_kind_serialization() {
let kind = SymbolKind::Function;
let json = serde_json::to_string(&kind).unwrap();
assert_eq!(json, r#""function""#);
}
#[test]
fn test_diff_report_serialization() {
let mut report = DiffReport::new("a.py", "b.py");
report.identical = false;
if let Some(ref mut summary) = report.summary {
summary.inserts = 1;
}
let json = serde_json::to_string(&report).unwrap();
assert!(json.contains(r#""file_a":"a.py""#));
assert!(json.contains(r#""identical":false"#));
}
#[test]
fn test_change_type_serialization() {
let insert = serde_json::to_string(&ChangeType::Insert).unwrap();
assert_eq!(insert, r#""insert""#);
let rename = serde_json::to_string(&ChangeType::Rename).unwrap();
assert_eq!(rename, r#""rename""#);
}
#[test]
fn test_api_check_report_serialization() {
let mut report = APICheckReport::new();
report.rules_applied = 5;
report.summary.total_findings = 2;
let json = serde_json::to_string(&report).unwrap();
assert!(json.contains(r#""rules_applied":5"#));
assert!(json.contains(r#""total_findings":2"#));
}
#[test]
fn test_gvn_report_serialization() {
let mut report = GVNReport::new("test_func");
report.summary.total_expressions = 10;
report.summary.unique_values = 7;
report.summary.compression_ratio = 0.7;
let json = serde_json::to_string(&report).unwrap();
assert!(json.contains(r#""function":"test_func""#));
assert!(json.contains(r#""compression_ratio":0.7"#));
}
#[test]
fn test_vuln_report_serialization() {
let mut report = VulnReport::new();
report.files_scanned = 5;
report.scan_duration_ms = 100;
let json = serde_json::to_string(&report).unwrap();
assert!(json.contains(r#""files_scanned":5"#));
assert!(json.contains(r#""scan_duration_ms":100"#));
}
#[test]
fn test_vuln_type_cwe_mapping() {
assert_eq!(VulnType::SqlInjection.cwe_id(), "CWE-89");
assert_eq!(VulnType::Xss.cwe_id(), "CWE-79");
assert_eq!(VulnType::CommandInjection.cwe_id(), "CWE-78");
assert_eq!(VulnType::Ssrf.cwe_id(), "CWE-918");
assert_eq!(VulnType::PathTraversal.cwe_id(), "CWE-22");
assert_eq!(VulnType::Deserialization.cwe_id(), "CWE-502");
assert_eq!(VulnType::UnsafeCode.cwe_id(), "CWE-242");
assert_eq!(VulnType::MemorySafety.cwe_id(), "CWE-119");
assert_eq!(VulnType::Panic.cwe_id(), "CWE-703");
assert_eq!(VulnType::Xxe.cwe_id(), "CWE-611");
assert_eq!(VulnType::OpenRedirect.cwe_id(), "CWE-601");
assert_eq!(VulnType::LdapInjection.cwe_id(), "CWE-90");
assert_eq!(VulnType::XpathInjection.cwe_id(), "CWE-643");
}
#[test]
fn test_vuln_type_default_severity() {
assert_eq!(
VulnType::SqlInjection.default_severity(),
Severity::Critical
);
assert_eq!(
VulnType::CommandInjection.default_severity(),
Severity::Critical
);
assert_eq!(
VulnType::MemorySafety.default_severity(),
Severity::Critical
);
assert_eq!(VulnType::Xss.default_severity(), Severity::High);
assert_eq!(VulnType::UnsafeCode.default_severity(), Severity::High);
assert_eq!(VulnType::OpenRedirect.default_severity(), Severity::Medium);
assert_eq!(VulnType::Panic.default_severity(), Severity::Medium);
}
#[test]
fn test_vuln_type_serialization() {
let sql_inj = serde_json::to_string(&VulnType::SqlInjection).unwrap();
assert_eq!(sql_inj, r#""sql_injection""#);
let xss = serde_json::to_string(&VulnType::Xss).unwrap();
assert_eq!(xss, r#""xss""#);
}
}