use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
use walkdir::WalkDir;
use crate::patterns::PATTERNS;
use crate::report::{Issue, Report, Severity};
use crate::custom_rules::{load_custom_rules, CompiledRule};
use crate::ignore::IgnorePatterns;
pub struct Scanner {
root_path: String,
custom_rules: Vec<CompiledRule>,
specific_files: Option<Vec<String>>,
ignore_patterns: IgnorePatterns,
}
impl Scanner {
pub fn new(path: &str) -> Result<Self> {
let root_path = fs::canonicalize(path)
.context("Failed to resolve path")?
.to_string_lossy()
.to_string();
let custom_rules = load_custom_rules(Path::new(&root_path))?;
let ignore_patterns = IgnorePatterns::load(Path::new(&root_path))?;
Ok(Self {
root_path,
custom_rules,
specific_files: None,
ignore_patterns,
})
}
pub fn new_with_files(path: &str, files: Vec<String>) -> Result<Self> {
let root_path = fs::canonicalize(path)
.context("Failed to resolve path")?
.to_string_lossy()
.to_string();
let custom_rules = load_custom_rules(Path::new(&root_path))?;
let ignore_patterns = IgnorePatterns::load(Path::new(&root_path))?;
Ok(Self {
root_path,
custom_rules,
specific_files: Some(files),
ignore_patterns,
})
}
pub fn scan(&self, verbose: bool) -> Result<Report> {
let mut issues = Vec::new();
let mut files_scanned = 0;
if let Some(ref specific_files) = self.specific_files {
for file in specific_files {
let path = Path::new(&self.root_path).join(file);
if !path.exists() || !path.is_file() {
continue;
}
if !self.should_scan(&path) {
continue;
}
files_scanned += 1;
if let Ok(content) = fs::read_to_string(&path) {
let file_issues = self.scan_file(&path, &content, verbose);
issues.extend(file_issues);
}
}
} else {
for entry in WalkDir::new(&self.root_path)
.follow_links(false)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if !path.is_file() || !self.should_scan(path) {
continue;
}
files_scanned += 1;
if let Ok(content) = fs::read_to_string(path) {
let file_issues = self.scan_file(path, &content, verbose);
issues.extend(file_issues);
}
}
}
Ok(Report::new(issues, files_scanned))
}
fn should_scan(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
if self.ignore_patterns.should_ignore(&path_str) {
return false;
}
if path_str.contains("/node_modules/")
|| path_str.contains("/target/")
|| path_str.contains("/.git/")
|| path_str.contains("/dist/")
|| path_str.contains("/build/")
{
return false;
}
if let Some(ext) = path.extension() {
let ext = ext.to_string_lossy().to_lowercase();
matches!(
ext.as_str(),
"rs" | "js" | "ts" | "jsx" | "tsx" | "py" | "go" | "java" | "c" | "cpp" | "h"
| "hpp" | "cs" | "php" | "rb" | "swift" | "kt" | "scala" | "sh" | "bash"
| "env" | "yml" | "yaml" | "json" | "toml" | "sql"
)
} else {
false
}
}
fn scan_file(&self, path: &Path, content: &str, verbose: bool) -> Vec<Issue> {
let mut issues = Vec::new();
let relative_path = path
.strip_prefix(&self.root_path)
.unwrap_or(path)
.to_string_lossy()
.to_string();
for pattern in PATTERNS.iter() {
if !verbose && pattern.severity == Severity::Low {
continue;
}
for (line_num, line) in content.lines().enumerate() {
if let Some(captures) = pattern.regex.captures(line) {
let matched = captures.get(0).map(|m| m.as_str()).unwrap_or("");
issues.push(Issue {
severity: pattern.severity.clone(),
title: pattern.title.to_string(),
file: relative_path.clone(),
line: line_num + 1,
code: line.trim().to_string(),
matched: matched.to_string(),
description: pattern.description.to_string(),
fix_suggestion: Some(pattern.fix_suggestion.to_string()),
risk_score: pattern.severity.score(),
});
}
}
}
for rule in &self.custom_rules {
if !verbose && rule.severity == Severity::Low {
continue;
}
for (line_num, line) in content.lines().enumerate() {
if let Some(captures) = rule.regex.captures(line) {
let matched = captures.get(0).map(|m| m.as_str()).unwrap_or("");
issues.push(Issue {
severity: rule.severity.clone(),
title: rule.title.clone(),
file: relative_path.clone(),
line: line_num + 1,
code: line.trim().to_string(),
matched: matched.to_string(),
description: rule.description.clone(),
fix_suggestion: Some(rule.fix_suggestion.clone()),
risk_score: rule.severity.score(),
});
}
}
}
issues
}
}