mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-24 20:28:06 +02:00
Refactor database schema and scanning process:
- Introduced `issues` table for detailed vulnerability storage. - Enhanced `files` table with project scoping and unique constraints. - Replaced `OutputFormat` enum with `String` for flexibility. - Added support for formatted console output of scan results. - Integrated file and issue updating logic for incremental scans. - Optimized scanning by leveraging database-stored issues.
This commit is contained in:
parent
9ef591c7b1
commit
0eecf886f2
7 changed files with 302 additions and 357 deletions
|
|
@ -1,18 +1,30 @@
|
|||
use crate::cli::OutputFormat;
|
||||
use crate::utils::project::get_project_info;
|
||||
use console::style;
|
||||
use std::path::Path;
|
||||
use crate::utils::config::Config;
|
||||
use tree_sitter::{Language, Parser, QueryCursor, StreamingIterator};
|
||||
use crate::database::index::Indexer;
|
||||
|
||||
use crate::database::index::{IssueRow, Indexer};
|
||||
use crate::patterns::Severity;
|
||||
use crate::utils::config::Config;
|
||||
use crate::utils::query_cache;
|
||||
use crate::walk::spawn_senders;
|
||||
|
||||
use tree_sitter::{Language, Parser, QueryCursor, StreamingIterator};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Diag {
|
||||
pub(crate) path: String,
|
||||
pub(crate) line: usize,
|
||||
pub(crate) col: usize,
|
||||
pub(crate) severity: Severity,
|
||||
pub(crate) id: String,
|
||||
}
|
||||
|
||||
/// Entry point called by the CLI.
|
||||
pub fn handle(
|
||||
path: &str,
|
||||
no_index: bool,
|
||||
rebuild_index: bool,
|
||||
format: OutputFormat,
|
||||
format: String,
|
||||
high_only: bool,
|
||||
database_dir: &Path,
|
||||
config: &Config,
|
||||
|
|
@ -20,73 +32,111 @@ pub fn handle(
|
|||
let scan_path = Path::new(path).canonicalize()?;
|
||||
let (project_name, db_path) = get_project_info(&scan_path, database_dir)?;
|
||||
|
||||
tracing::debug!("Config: {:?}", config);
|
||||
tracing::debug!("Scanning project: {}", project_name);
|
||||
tracing::debug!("Scan path: {}", scan_path.display());
|
||||
let mut indexer = Indexer::new(&project_name, &db_path)?;
|
||||
|
||||
let diags: Vec<Diag>;
|
||||
|
||||
if no_index {
|
||||
tracing::debug!("Scanning without index...");
|
||||
scan_filesystem(&scan_path, config)?;
|
||||
diags = scan_filesystem(&scan_path, config)?;
|
||||
} else {
|
||||
if rebuild_index || !db_path.exists() {
|
||||
tracing::debug!("Building/updating index...");
|
||||
crate::commands::index::build_index(&scan_path, &db_path)?;
|
||||
}
|
||||
|
||||
tracing::debug!("Using index: {}", db_path.display());
|
||||
scan_with_index(&scan_path, &db_path, config)?;
|
||||
diags = scan_with_index(&project_name, &db_path, config, &mut indexer)?;
|
||||
}
|
||||
|
||||
tracing::debug!("Output format: {:?}", format);
|
||||
if high_only {
|
||||
tracing::debug!("Filtering: High severity only");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn scan_filesystem(root: &Path, cfg: &Config) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let rx = spawn_senders(root, cfg);
|
||||
|
||||
for batch in rx.iter().flatten() {
|
||||
tracing::debug!("Scanning file: {}", batch.display());
|
||||
scan_single_file(&batch, cfg)?; // <-- your actual scanner
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn scan_with_index(root: &Path, db_path: &Path, cfg: &Config) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let indexer = Indexer::new(db_path)
|
||||
.map_err(|e| format!("opening index {}: {e}", db_path.display()))?;
|
||||
|
||||
let rx = spawn_senders(root, cfg);
|
||||
|
||||
for batch in rx.iter().flatten() {
|
||||
let scan = indexer.should_scan(&batch)?;
|
||||
tracing::debug!("Should scan: {}, file: {}", scan, batch.display());
|
||||
if scan {
|
||||
tracing::debug!("Scanning file: {}", batch.display());
|
||||
scan_single_file(&batch, cfg)?; // your scanner
|
||||
indexer.record_scan(&batch)?;
|
||||
if format == "console" || format == "" && config.output.default_format == "console" {
|
||||
for d in &diags {
|
||||
if high_only && d.severity != Severity::High {
|
||||
continue;
|
||||
}
|
||||
let sev_str = match d.severity {
|
||||
Severity::High => style("HIGH").red().bold(),
|
||||
Severity::Medium => style("MEDIUM").yellow().bold(),
|
||||
Severity::Low => style("LOW").cyan().bold(),
|
||||
};
|
||||
println!(
|
||||
"{}:{}:{} [{}] {}",
|
||||
style(d.path.clone()).blue().underlined(),
|
||||
d.line,
|
||||
d.col,
|
||||
sev_str,
|
||||
style(&d.id).bold(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn scan_single_file(
|
||||
// --------------------------------------------------------------------------------------------
|
||||
// Scanning helpers
|
||||
// --------------------------------------------------------------------------------------------
|
||||
|
||||
fn scan_filesystem(
|
||||
root: &Path,
|
||||
cfg: &Config,
|
||||
) -> Result<Vec<Diag>, Box<dyn std::error::Error>> {
|
||||
let rx = spawn_senders(root, cfg);
|
||||
let mut issues: Vec<Diag> = Vec::new();
|
||||
for batch in rx.iter().flatten() {
|
||||
issues.append(&mut run_rules_on_file(&batch, cfg)?);
|
||||
}
|
||||
Ok(issues)
|
||||
}
|
||||
|
||||
fn scan_with_index(
|
||||
project: &str,
|
||||
_db_path: &Path,
|
||||
cfg: &Config,
|
||||
indexer: &mut Indexer,
|
||||
) -> Result<Vec<Diag>, Box<dyn std::error::Error>> {
|
||||
let files = indexer.get_files(project).unwrap_or_default();
|
||||
let mut issues: Vec<Diag> = Vec::new();
|
||||
for file in files {
|
||||
if indexer.should_scan(&file)? {
|
||||
let mut diags = run_rules_on_file(&file, cfg)?;
|
||||
let file_id = indexer.upsert_file(&file)?;
|
||||
|
||||
let issue_rows: Vec<IssueRow> = diags
|
||||
.iter()
|
||||
.map(|d| IssueRow {
|
||||
rule_id: d.id.as_ref(),
|
||||
severity: match d.severity {
|
||||
Severity::High => "HIGH",
|
||||
Severity::Medium => "MEDIUM",
|
||||
Severity::Low => "LOW",
|
||||
},
|
||||
line: d.line as i64,
|
||||
col: d.col as i64,
|
||||
})
|
||||
.collect();
|
||||
|
||||
indexer.replace_issues(file_id, issue_rows)?;
|
||||
issues.append(&mut diags);
|
||||
continue;
|
||||
}
|
||||
issues.append(&mut indexer.get_issues_from_file(&file)?);
|
||||
}
|
||||
Ok(issues)
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
// Tree‑sitter‑based rule runner – returns a Vec<Diag>
|
||||
// --------------------------------------------------------------------------------------------
|
||||
fn run_rules_on_file(
|
||||
path: &Path,
|
||||
cfg: &Config, // assume cfg.high_only: bool
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
cfg: &Config,
|
||||
) -> Result<Vec<Diag>, Box<dyn std::error::Error>> {
|
||||
let source = std::fs::read_to_string(path)?;
|
||||
let mut parser = Parser::new();
|
||||
|
||||
let ext = path
|
||||
.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
|
||||
// Pick the right tree-sitter language *and* pre-compiled queries
|
||||
let (ts_lang, lang_key): (Language, &'static str) = match ext.as_str() {
|
||||
let lang_key = match path
|
||||
.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase()
|
||||
.as_str()
|
||||
{
|
||||
"rs" => (Language::from(tree_sitter_rust::LANGUAGE), "rust"),
|
||||
"c" => (Language::from(tree_sitter_c::LANGUAGE), "c"),
|
||||
"cpp" | "c++" => (Language::from(tree_sitter_cpp::LANGUAGE), "cpp"),
|
||||
|
|
@ -96,69 +146,35 @@ fn scan_single_file(
|
|||
"py" => (Language::from(tree_sitter_python::LANGUAGE), "python"),
|
||||
"ts" | "tsx" => (Language::from(tree_sitter_typescript::LANGUAGE_TYPESCRIPT), "typescript"),
|
||||
"js" => (Language::from(tree_sitter_javascript::LANGUAGE), "javascript"),
|
||||
_ => return Ok(()),
|
||||
_ => return Ok(Vec::new()),
|
||||
};
|
||||
let (ts_lang, lang_name) = lang_key;
|
||||
|
||||
parser.set_language(&ts_lang)?;
|
||||
let tree = parser.parse(&source, None).ok_or("tree‑sitter failed")?;
|
||||
let root = tree.root_node();
|
||||
|
||||
let tree = parser.parse(&source, None).ok_or("tree-sitter failed")?;
|
||||
let root = tree.root_node();
|
||||
|
||||
// ----- run vulnerability patterns -----
|
||||
let compiled = query_cache::for_lang(lang_key, ts_lang);
|
||||
let compiled = query_cache::for_lang(lang_name, ts_lang);
|
||||
let mut cursor = QueryCursor::new();
|
||||
let mut out = Vec::new();
|
||||
|
||||
for cq in &compiled {
|
||||
if cfg.scanner.min_severity > cq.meta.severity {
|
||||
continue;
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut matches = cursor.matches(&cq.query, root, source.as_bytes());
|
||||
|
||||
while let Some(m) = matches.next() {
|
||||
// capture 0 is the one tagged @vuln
|
||||
for cap in m.captures.iter().filter(|c| c.index == 0) {
|
||||
let point = cap.node.start_position();
|
||||
let line = point.row;
|
||||
let col = point.column;
|
||||
|
||||
match cq.meta.severity {
|
||||
Severity::High => {
|
||||
tracing::error!(
|
||||
file = %path.display(),
|
||||
line = line + 1,
|
||||
column = col + 1,
|
||||
id = cq.meta.id,
|
||||
sev = ?Severity::High,
|
||||
"pattern matched"
|
||||
);
|
||||
},
|
||||
Severity::Medium => {
|
||||
tracing::warn!(
|
||||
file = %path.display(),
|
||||
line = line + 1,
|
||||
column = col + 1,
|
||||
id = cq.meta.id,
|
||||
sev = ?Severity::Medium,
|
||||
"pattern matched"
|
||||
);
|
||||
}
|
||||
Severity::Low => {
|
||||
tracing::info!(
|
||||
file = %path.display(),
|
||||
line = line + 1,
|
||||
column = col + 1,
|
||||
id = cq.meta.id,
|
||||
sev = ?Severity::Low,
|
||||
"pattern matched"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
out.push(Diag {
|
||||
path: path.to_string_lossy().to_string(),
|
||||
line: point.row + 1,
|
||||
col: point.column + 1,
|
||||
severity: cq.meta.severity,
|
||||
id: String::from(cq.meta.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(out)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue