use std::path::{Path, PathBuf};
use std::process::Command;
use crate::security::sast::finding::SastFinding;
use crate::security::sast::scanner::{SastScanOptions, SastScanner};
use crate::security::vulnerability::Severity;
pub struct FlawfinderScanner;
impl FlawfinderScanner {
pub fn new() -> Self {
Self
}
fn parse_csv_output(&self, output: &str) -> Result<Vec<SastFinding>, String> {
let mut findings = Vec::new();
for line in output.lines() {
if line.starts_with("File,") || line.trim().is_empty() {
continue;
}
let fields = parse_csv_line(line);
if fields.len() < 8 {
continue;
}
let file_path = fields[0].clone();
let line_num = fields[1].parse::<usize>().unwrap_or(0);
let column = fields[2].parse::<usize>().ok();
let level = fields
.get(4)
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(fields[3].parse::<u32>().unwrap_or(0));
let category = fields.get(5).cloned().unwrap_or_default();
let name = fields.get(6).cloned().unwrap_or_default();
let warning = fields.get(7).cloned().unwrap_or_default();
let suggestion = fields.get(8).filter(|s| !s.is_empty()).cloned();
let cwe_ids = fields
.get(10)
.filter(|s| !s.is_empty())
.map(|s| {
s.split(", ")
.flat_map(|part| part.split('!'))
.map(|s| s.trim_start_matches('/'))
.filter(|s| !s.is_empty() && s.starts_with("CWE-"))
.map(|s| s.to_string())
.collect()
})
.unwrap_or_default();
let severity = match level {
5 => Severity::Critical,
4 => Severity::High,
3 => Severity::Medium,
2 => Severity::Low,
_ => Severity::Low,
};
let code_snippet = fields.get(11).filter(|s| !s.is_empty()).cloned();
let rule_id = if name.is_empty() {
format!("flawfinder-{}", category)
} else {
format!("{}/{}", category, name)
};
findings.push(SastFinding {
rule_id,
severity,
message: warning,
file_path: PathBuf::from(file_path),
line: line_num,
column,
end_line: None,
end_column: None,
code_snippet,
fix_suggestion: suggestion,
category,
cwe_ids,
source: "flawfinder".to_string(),
language: "c".to_string(), });
}
Ok(findings)
}
}
impl Default for FlawfinderScanner {
fn default() -> Self {
Self::new()
}
}
impl SastScanner for FlawfinderScanner {
fn name(&self) -> &str {
"flawfinder"
}
fn supported_languages(&self) -> &[&str] {
&["c", "cpp"]
}
fn is_available(&self) -> bool {
Command::new("flawfinder")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn scan(
&self,
path: &Path,
files: &[PathBuf],
options: &SastScanOptions,
) -> Result<Vec<SastFinding>, String> {
let mut args = vec![
"--csv".to_string(),
"--columns".to_string(),
"--context".to_string(),
"--quiet".to_string(),
];
if let Some(ref threshold) = options.severity_threshold {
let min_level = match threshold {
Severity::Critical => "5",
Severity::High => "4",
Severity::Medium => "3",
Severity::Low => "2",
_ => "1",
};
args.push(format!("--minlevel={}", min_level));
}
if files.is_empty() {
args.push(path.to_string_lossy().to_string());
} else {
for f in files {
args.push(f.to_string_lossy().to_string());
}
}
let output = Command::new("flawfinder")
.args(&args)
.current_dir(path)
.output()
.map_err(|e| format!("Failed to run flawfinder: {}", e))?;
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.trim().is_empty() {
return Ok(Vec::new());
}
self.parse_csv_output(&stdout)
}
fn install_hint(&self) -> String {
crate::python_tool_install_hint("flawfinder")
}
}
fn parse_csv_line(line: &str) -> Vec<String> {
let mut fields = Vec::new();
let mut current = String::new();
let mut in_quotes = false;
for ch in line.chars() {
match ch {
'"' => in_quotes = !in_quotes,
',' if !in_quotes => {
fields.push(current.trim().to_string());
current = String::new();
}
_ => current.push(ch),
}
}
fields.push(current.trim().to_string());
fields
}