From d4eb31d5553dd4dcc9737bd277ee63153a4f90c9 Mon Sep 17 00:00:00 2001 From: DeeTomPanda Date: Sun, 5 Jul 2026 19:26:01 +0530 Subject: [PATCH 1/6] feat: symbol table for JS/TS using oxc, modify tests, add analyze arg to get quick symbol stats --- src/cli/mod.rs | 67 ++++- src/main.rs | 49 +++- src/server/detectors/golang/go_scanner.rs | 9 +- src/server/detectors/golang/tests.rs | 32 +-- src/server/detectors/javascript/js_scanner.rs | 19 +- src/server/detectors/javascript/tests.rs | 72 ++--- src/server/detectors/mod.rs | 3 +- src/server/detectors/shared/common_parser.rs | 272 ++++++++++++++++-- src/server/detectors/typescript/tests.rs | 16 +- src/server/detectors/typescript/ts_scanner.rs | 17 +- src/server/models/mod.rs | 2 + src/server/models/results.rs | 7 + src/server/models/symbols.rs | 24 ++ src/server/service.rs | 19 +- src/state.rs | 4 +- 15 files changed, 481 insertions(+), 131 deletions(-) create mode 100644 src/server/models/results.rs create mode 100644 src/server/models/symbols.rs diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 11fe91e..095f21f 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,10 +1,14 @@ use rayon::prelude::*; +use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; use uuid::Uuid; use walkdir::WalkDir; -use crate::server::models::findings::{severity_order, FinalFindings}; +use crate::server::models::{ + findings::{severity_order, FinalFindings}, + symbols::{Symbol, SymbolTable}, +}; use crate::server::service::OWASPScanner; use crate::AppState; @@ -40,22 +44,27 @@ pub async fn scan(path: String, state: Arc>) -> String { .filter(|e| OWASPScanner::determine_language(&e.path().to_string_lossy()).is_some()) .collect(); - // scan in parallel using rayon - let all_findings: Vec = entries + // collect both findings AND symbols in parallel + let all_results: Vec<(FinalFindings, Vec)> = entries .par_iter() .filter_map(|entry| { let path = entry.path(); match std::fs::read_to_string(path) { Ok(content) => { - let mut findings = owasp_scanner.scan(&content, &path.to_string_lossy()); - if findings.is_empty() { + let mut scan_result = owasp_scanner.scan(&content, &path.to_string_lossy()); + if scan_result.findings.is_empty() && scan_result.symbols.is_empty() { return None; } - findings.sort_by_key(|f| severity_order(&f.severity)); - Some(FinalFindings { - file_name: path.to_string_lossy().to_string(), - findings, - }) + scan_result + .findings + .sort_by_key(|f| severity_order(&f.severity)); + Some(( + FinalFindings { + file_name: path.to_string_lossy().to_string(), + findings: scan_result.findings, + }, + scan_result.symbols, + )) } Err(e) => { eprintln!("Error reading file {}: {}", path.display(), e); @@ -65,8 +74,46 @@ pub async fn scan(path: String, state: Arc>) -> String { }) .collect(); + /* after all that it would be like e.g. + all_results = [ + (FinalFindings { file: "express/index.js" }, vec![Symbol, Symbol]), + (FinalFindings { file: "express/router.js" }, vec![Symbol, Symbol]), + (FinalFindings { file: "express/utils.js" }, vec![Symbol]), + ] */ + + // separate them after parallel scan is done + let (all_findings, all_symbols): (Vec, Vec>) = + all_results.into_iter().unzip(); + + // flatten symbols into SymbolTable + let mut symbol_map: HashMap> = HashMap::new(); + for symbols in all_symbols { + for symbol in symbols { + symbol_map + .entry(symbol.file.clone()) + .or_insert_with(Vec::new) + .push(symbol); + } + } + let scan_id = Uuid::new_v4().to_string(); let mut state = state.write().await; state.results.insert(scan_id.clone(), all_findings); + state.symbol_table = Some(SymbolTable { + symbols: symbol_map, + }); + + // temporary debug, remove later + if let Some(ref table) = state.symbol_table { + let total: usize = table.symbols.values().map(|v| v.len()).sum(); + println!("Symbols extracted: {}", total); + for (file, symbols) in &table.symbols { + println!(" {}", file); + for sym in symbols { + println!(" {:?} {} (scope: {})", sym.kind, sym.name, sym.scope); + } + } + } + scan_id } diff --git a/src/main.rs b/src/main.rs index 192dd82..a1009b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use tokio::sync::RwLock; use crate::server::detectors::shared::sarif::*; -use crate::server::models::findings::Findings; +use crate::server::models::{findings::Findings, symbols::SymbolKind}; use crate::state::AppState; mod cli; @@ -19,10 +19,16 @@ enum Command { #[arg(long)] sarif: bool, + #[arg(long)] + analyze: bool, + #[arg(long)] output: Option, }, Serve, + Analyze { + path: String, + }, } #[tokio::main] @@ -38,6 +44,7 @@ async fn main() { Command::Scan { path, sarif, + analyze, output, } => { let scan_id = cli::scan(path, Arc::clone(&state)).await; @@ -70,5 +77,45 @@ async fn main() { } tokio::signal::ctrl_c().await.unwrap(); } + Command::Analyze { path } => { + let scan_id = cli::scan(path, Arc::clone(&state)).await; + let state_read = state.read().await; + + if let Some(ref table) = state_read.symbol_table { + let mut functions = 0; + let mut methods = 0; + let mut variables = 0; + let mut parameters = 0; + let mut imports = 0; + let mut classes = 0; + + for symbols in table.symbols.values() { + for sym in symbols { + match sym.kind { + SymbolKind::Function => functions += 1, + SymbolKind::Method => methods += 1, + SymbolKind::Variable => variables += 1, + SymbolKind::Parameter => parameters += 1, + SymbolKind::Import => imports += 1, + SymbolKind::Class => classes += 1, + _ => {} + } + } + } + + let total = functions + methods + variables + parameters + imports + classes; + + println!("\nAnalysis complete"); + println!("─────────────────────────────"); + println!(" Functions: {}", functions); + println!(" Methods: {}", methods); + println!(" Variables: {}", variables); + println!(" Parameters: {}", parameters); + println!(" Imports: {}", imports); + println!(" Classes: {}", classes); + println!("─────────────────────────────"); + println!(" Total: {}", total); + } + } } } diff --git a/src/server/detectors/golang/go_scanner.rs b/src/server/detectors/golang/go_scanner.rs index 064eba2..cadcf7e 100644 --- a/src/server/detectors/golang/go_scanner.rs +++ b/src/server/detectors/golang/go_scanner.rs @@ -1,5 +1,5 @@ use crate::server::models::findings::{Severity, VulnerabilityType}; -use crate::server::{detectors::Scanner, models::findings::Findings}; +use crate::server::{detectors::Scanner, models::{results::ScanResult,findings::Findings}}; pub struct GolangScanner; @@ -9,10 +9,13 @@ pub(super) struct GolangTreeSitter<'a> { } impl Scanner for GolangScanner { - fn scan(&self, code: &str, file_path: &str) -> Vec { + fn scan(&self, code: &str, file_path: &str) -> ScanResult { let mut tree_sitter = GolangTreeSitter::new(file_path); tree_sitter.analyze(code); - tree_sitter.list_possible_threats() + ScanResult { + findings: tree_sitter.list_possible_threats(), + symbols: Vec::new(), // Placeholder - implement symbol analysis if needed + } } } diff --git a/src/server/detectors/golang/tests.rs b/src/server/detectors/golang/tests.rs index ccd2b11..36cfdb0 100644 --- a/src/server/detectors/golang/tests.rs +++ b/src/server/detectors/golang/tests.rs @@ -23,8 +23,8 @@ mod tests { } "#; - let findings = SCANNER.scan(code, "test.go"); - let ops: Vec<_> = findings + let scan_result = SCANNER.scan(code, "test.go"); + let ops: Vec<_> = scan_result.findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::UnsafeFileOperation) .collect(); @@ -42,8 +42,8 @@ mod tests { var token = "tokval" "#; - let findings = SCANNER.scan(code, "test.go"); - let secrets: Vec<_> = findings + let scan_result = SCANNER.scan(code, "test.go"); + let secrets: Vec<_> = scan_result.findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::HardcodedSecret) .collect(); @@ -66,9 +66,9 @@ mod tests { } "#; - let findings = SCANNER.scan(code, "test.go"); + let scan_result = SCANNER.scan(code, "test.go"); - assert!(findings + assert!(scan_result.findings .iter() .any(|f| f.vuln_type == VulnerabilityType::SQLInjection)); } @@ -96,9 +96,9 @@ mod tests { } "#; - let findings = SCANNER.scan(code, "test.go"); + let scan_result = SCANNER.scan(code, "test.go"); - assert!(findings + assert!(scan_result.findings .iter() .any(|f| f.vuln_type == VulnerabilityType::SQLInjection)); } @@ -124,9 +124,9 @@ mod tests { } "#; - let findings = SCANNER.scan(code, "test.go"); + let scan_result = SCANNER.scan(code, "test.go"); - assert!(findings + assert!(scan_result.findings .iter() .any(|f| f.vuln_type == VulnerabilityType::SQLInjection)); } @@ -144,8 +144,8 @@ mod tests { } "#; - let findings = SCANNER.scan(code, "test.go"); - assert!(findings + let scan_result = SCANNER.scan(code, "test.go"); + assert!(scan_result.findings .iter() .any(|f| f.vuln_type == VulnerabilityType::UnsafeCodeExecution)); } @@ -163,8 +163,8 @@ mod tests { } "#; - let findings = SCANNER.scan(code, "test.go"); - assert_eq!(findings.len(), 0); + let scan_result = SCANNER.scan(code, "test.go"); + assert_eq!(scan_result.findings.len(), 0); } #[test] @@ -202,7 +202,7 @@ mod tests { } "#; - let findings = SCANNER.scan(code, "test.go"); - assert!(findings.is_empty()); + let scan_result = SCANNER.scan(code, "test.go"); + assert!(scan_result.findings.is_empty()); } } diff --git a/src/server/detectors/javascript/js_scanner.rs b/src/server/detectors/javascript/js_scanner.rs index c32e53d..b5add2b 100644 --- a/src/server/detectors/javascript/js_scanner.rs +++ b/src/server/detectors/javascript/js_scanner.rs @@ -1,6 +1,8 @@ +use std::iter::Scan; + use crate::server::detectors::shared::common_parser::{parse_to_ast, CodeVisitor}; use crate::server::detectors::Scanner; -use crate::Findings; +use crate::server::models::results::ScanResult; use oxc::allocator::Allocator; use oxc::ast_visit::Visit; @@ -10,24 +12,23 @@ pub struct JavaScriptScanner; // hardcoded secrets // SQL Injection vulnerabilities impl Scanner for JavaScriptScanner { - fn scan(&self, code: &str, file_path: &str) -> Vec { + fn scan(&self, code: &str, file_path: &str) -> ScanResult { let allocator = Allocator::default(); let ast = match parse_to_ast(code, &allocator, file_path) { Ok(program) => program, Err(e) => { eprintln!("Parse error: {}", e); - return vec![]; + return ScanResult{ + findings:vec![], + symbols:vec![] + }; } }; - let mut visitor = CodeVisitor { - findings: Vec::new(), - file_path, - source_text: code, - }; + let mut visitor = CodeVisitor::new(file_path,code); visitor.visit_program(&ast); - visitor.list_possible_threats() + visitor.into_scan_result() } } diff --git a/src/server/detectors/javascript/tests.rs b/src/server/detectors/javascript/tests.rs index ef11b7f..51ea296 100644 --- a/src/server/detectors/javascript/tests.rs +++ b/src/server/detectors/javascript/tests.rs @@ -20,8 +20,8 @@ mod tests { password: "objsecret" } "#; - let findings = SCANNER.scan(code, "test.js"); - let secrets: Vec<_> = findings + let scan_result = SCANNER.scan(code, "test.js"); + let secrets: Vec<_> = scan_result.findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::HardcodedSecret) .collect(); @@ -36,8 +36,8 @@ mod tests { const apiKey = fetchApiKey(); const token = generateToken(); "#; - let findings = SCANNER.scan(code, "test.js"); - let secrets: Vec<_> = findings + let scan_result = SCANNER.scan(code, "test.js"); + let secrets: Vec<_> = scan_result.findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::HardcodedSecret) .collect(); @@ -51,8 +51,8 @@ mod tests { const passwd = `${getPassword()}`; const token = `Bearer ${fetchToken()}`; "#; - let findings = SCANNER.scan(code, "test.js"); - let secrets: Vec<_> = findings + let scan_result = SCANNER.scan(code, "test.js"); + let secrets: Vec<_> = scan_result.findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::HardcodedSecret) .collect(); @@ -67,8 +67,8 @@ mod tests { const username = "admin"; const greeting = "hello world"; "#; - let findings = SCANNER.scan(code, "test.js"); - let secrets: Vec<_> = findings + let scan_result = SCANNER.scan(code, "test.js"); + let secrets: Vec<_> = scan_result.findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::HardcodedSecret) .collect(); @@ -81,8 +81,8 @@ mod tests { const password="supersecret"; const config = { password: "secret" } "#; - let findings = SCANNER.scan(code, "test.js"); - let secrets: Vec<_> = findings + let scan_result = SCANNER.scan(code, "test.js"); + let secrets: Vec<_> = scan_result.findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::HardcodedSecret) .collect(); @@ -98,8 +98,8 @@ mod tests { let code = r#" eval("alert(1)"); "#; - let findings = SCANNER.scan(code, "test.js"); - let evals: Vec<_> = findings + let scan_result = SCANNER.scan(code, "test.js"); + let evals: Vec<_> = scan_result.findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::Eval) .collect(); @@ -112,8 +112,8 @@ mod tests { let code = r#" const fn = new Function("return 1"); "#; - let findings = SCANNER.scan(code, "test.js"); - let evals: Vec<_> = findings + let scan_result = SCANNER.scan(code, "test.js"); + let evals: Vec<_> = scan_result.findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::Eval) .collect(); @@ -127,8 +127,8 @@ mod tests { setTimeout("doSomething()", 1000); setInterval("doSomethingElse()", 500); "#; - let findings = SCANNER.scan(code, "test.js"); - let evals: Vec<_> = findings + let scan_result = SCANNER.scan(code, "test.js"); + let evals: Vec<_> = scan_result.findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::Eval) .collect(); @@ -142,8 +142,8 @@ mod tests { Math.random(); parseInt("42"); "#; - let findings = SCANNER.scan(code, "test.js"); - let evals: Vec<_> = findings + let scan_result = SCANNER.scan(code, "test.js"); + let evals: Vec<_> = scan_result.findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::Eval) .collect(); @@ -159,8 +159,8 @@ mod tests { let code = r#" db.query(`SELECT * FROM ${table}`); "#; - let findings = SCANNER.scan(code, "test.js"); - let sql: Vec<_> = findings + let scan_result = SCANNER.scan(code, "test.js"); + let sql: Vec<_> = scan_result.findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::SQLInjection) .collect(); @@ -172,8 +172,8 @@ mod tests { let code = r#" db.query("SELECT * FROM " + table + " WHERE id = " + id); "#; - let findings = SCANNER.scan(code, "test.js"); - let sql: Vec<_> = findings + let scan_result = SCANNER.scan(code, "test.js"); + let sql: Vec<_> = scan_result.findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::SQLInjection) .collect(); @@ -186,8 +186,8 @@ mod tests { let code = r#" db.query("SELECT * FROM users WHERE id = 1"); "#; - let findings = SCANNER.scan(code, "test.js"); - let sql: Vec<_> = findings + let scan_result = SCANNER.scan(code, "test.js"); + let sql: Vec<_> = scan_result.findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::SQLInjection) .collect(); @@ -200,8 +200,8 @@ mod tests { let code = r#" db.query(`${"SELECT * FROM " + table}`); "#; - let findings = SCANNER.scan(code, "test.js"); - let sql: Vec<_> = findings + let scan_result = SCANNER.scan(code, "test.js"); + let sql: Vec<_> = scan_result.findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::SQLInjection) .collect(); @@ -215,8 +215,8 @@ mod tests { db.query("SELECT * FROM users WHERE id = ?", [id]); db.query("SELECT * FROM users WHERE id = $1", [id]); "#; - let findings = SCANNER.scan(code, "test.js"); - let sql: Vec<_> = findings + let scan_result = SCANNER.scan(code, "test.js"); + let sql: Vec<_> = scan_result.findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::SQLInjection) .collect(); @@ -230,8 +230,8 @@ mod tests { const msg = `Hello ${name}`; console.log(`User ${id} logged in`); "#; - let findings = SCANNER.scan(code, "test.js"); - let sql: Vec<_> = findings + let scan_result = SCANNER.scan(code, "test.js"); + let sql: Vec<_> = scan_result.findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::SQLInjection) .collect(); @@ -249,14 +249,14 @@ mod tests { eval(userInput); db.query(`SELECT * FROM ${table}`); "#; - let findings = SCANNER.scan(code, "test.js"); - assert!(findings + let scan_result = SCANNER.scan(code, "test.js"); + assert!(scan_result.findings .iter() .any(|f| f.vuln_type == VulnerabilityType::HardcodedSecret)); - assert!(findings + assert!(scan_result.findings .iter() .any(|f| f.vuln_type == VulnerabilityType::Eval)); - assert!(findings + assert!(scan_result.findings .iter() .any(|f| f.vuln_type == VulnerabilityType::SQLInjection)); } @@ -268,7 +268,7 @@ mod tests { const version = getVersion(); const result = db.query("SELECT * FROM users WHERE id = ?", [userId]); "#; - let findings = SCANNER.scan(code, "test.js"); - assert_eq!(findings.len(), 0); + let scan_result = SCANNER.scan(code, "test.js"); + assert_eq!(scan_result.findings.len(), 0); } } diff --git a/src/server/detectors/mod.rs b/src/server/detectors/mod.rs index b99eec2..abba18a 100644 --- a/src/server/detectors/mod.rs +++ b/src/server/detectors/mod.rs @@ -5,7 +5,8 @@ mod typescript; pub use golang::go_scanner::GolangScanner; pub use javascript::js_scanner::JavaScriptScanner; pub use typescript::ts_scanner::TypeScriptScanner; +use crate::server::models::results::ScanResult; pub trait Scanner: Send + Sync { - fn scan(&self, code: &str, file_path: &str) -> Vec; + fn scan(&self, code: &str, file_path: &str) -> ScanResult; } diff --git a/src/server/detectors/shared/common_parser.rs b/src/server/detectors/shared/common_parser.rs index 0803e2d..aefe0a5 100644 --- a/src/server/detectors/shared/common_parser.rs +++ b/src/server/detectors/shared/common_parser.rs @@ -1,5 +1,9 @@ use super::js_and_ts::utils::*; -use crate::server::models::findings::{Severity, VulnerabilityType}; +use crate::server::models::{ + findings::{Severity, VulnerabilityType}, + results::ScanResult, + symbols::{Symbol, SymbolKind}, +}; use crate::Findings; use oxc::ast_visit::Visit; use oxc::span::GetSpan; @@ -11,18 +15,87 @@ pub struct CodeVisitor<'a> { pub findings: Vec, pub file_path: &'a str, pub source_text: &'a str, + pub symbols: Vec, + scope_stack: Vec, +} + +impl<'a> CodeVisitor<'a> { + pub fn new(file_path: &'a str, source_text: &'a str) -> Self { + Self { + file_path, + source_text, + findings: vec![], + symbols: vec![], + scope_stack: vec![], + } + } + + // get scope of element, i.e. wher they were declared + fn current_scope(&self) -> String { + if self.scope_stack.is_empty() { + "global".to_string() + } else { + self.scope_stack.join("::") + } + } + + fn span_to_line(&self, span_start: usize) -> usize { + let safe = span_start.min(self.source_text.len()); + self.source_text[..safe].lines().count() + 1 + } + + // add to findings + fn report( + &mut self, + snippet: &str, + span_start: usize, + vuln_type: VulnerabilityType, + severity: Severity, + ) { + let safe = span_start.min(self.source_text.len()); + let line = self.source_text[..safe].lines().count() + 1; + + self.findings.push(Findings { + vuln_type, + line_no: line.to_string(), + file_path: self.file_path.to_string(), + snippet: snippet.to_string(), + severity, + }); + } + + pub fn list_possible_threats(self) -> Vec { + self.findings + } + + pub fn into_scan_result(self) -> ScanResult { + ScanResult { + findings: self.findings, + symbols: self.symbols, + } + } } // visitor implementation for AST traversal impl<'a> Visit<'a> for CodeVisitor<'a> { + // let var a=1; fn visit_variable_declarator(&mut self, node: &VariableDeclarator<'a>) { // Get variable name from AST first if let BindingPattern::BindingIdentifier(ident) = &node.id { - // unwrap `satisfies` / `as` / `!` before checking the shape + self.symbols.push(Symbol { + name: ident.name.to_string(), + kind: SymbolKind::Variable, + file: self.file_path.to_string(), + line: self.span_to_line(node.span.start as usize), + column: node.span.start as usize, + scope: self.current_scope(), + }); + + // unwrap `satisfies` or `as` or `!` assertions before checking the shape let init = node.init.as_ref().map(unwrap_ts_expression); - // check for Object Pattern like const config = {password: "secret"} + // check for Object Pattern like const { password }= config; if let Some(Expression::ObjectExpression(obj_lit)) = init { - // iterate through the objects + // iterate through properties of object for prop in &obj_lit.properties { if let oxc_ast::ast::ObjectPropertyKind::ObjectProperty(prop) = prop { if let oxc_ast::ast::PropertyKey::StaticIdentifier(ident) = &prop.key { @@ -62,10 +135,11 @@ impl<'a> Visit<'a> for CodeVisitor<'a> { } } } - // recurse into child + // recurse into child, i.e deep traversal oxc::ast_visit::walk::walk_variable_declarator(self, node); } + // e.g. new Function(...) fn visit_new_expression(&mut self, node: &NewExpression<'a>) { if let Expression::Identifier(ident) = &node.callee { if ident.name.as_str() == "Function" { @@ -82,10 +156,13 @@ impl<'a> Visit<'a> for CodeVisitor<'a> { oxc::ast_visit::walk::walk_new_expression(self, node); // visits callee + all arguments } - // for fucntions + // for functions + // eg: save(x) + // db.query() + // require() fn visit_call_expression(&mut self, node: &CallExpression<'a>) { // for TS - // unwrap (eval as any)(...) → eval(...) — do this FIRST, unconditionally + // unwrap (eval as any)(...) to eval(...) do this FIRST, unconditionally let callee = unwrap_ts_expression(&node.callee); if let Expression::Identifier(ident) = callee { @@ -99,6 +176,26 @@ impl<'a> Visit<'a> for CodeVisitor<'a> { Severity::High, ); } + + // CommonJS imports via require() + // e.g. module.exports = require('./lib/express'); + // var debug = require('debug'); + if name == "require" { + if let Some(first_arg) = node.arguments.first() { + if let Some(expr) = first_arg.as_expression() { + if let Expression::StringLiteral(lit) = expr { + self.symbols.push(Symbol { + name: lit.value.to_string(), + kind: SymbolKind::Import, + file: self.file_path.to_string(), + line: self.span_to_line(node.span.start as usize), + column: node.span.start as usize, + scope: self.current_scope(), + }); + } + } + } + } } // sql injection check @@ -122,7 +219,7 @@ impl<'a> Visit<'a> for CodeVisitor<'a> { ); } } - // flag if any part is dynamic OR if it's all static strings forming SQL + // flag if concatenated SQl with no params Expression::BinaryExpression(_) => { let has_params = node.arguments.len() > 1; if !has_params { @@ -146,6 +243,7 @@ impl<'a> Visit<'a> for CodeVisitor<'a> { oxc::ast_visit::walk::walk_call_expression(self, node); } + // let a as int? fn visit_ts_as_expression(&mut self, node: &TSAsExpression<'a>) { if let TSType::TSAnyKeyword(_) = &node.type_annotation { self.report( @@ -155,36 +253,152 @@ impl<'a> Visit<'a> for CodeVisitor<'a> { Severity::Low, ); } - // manual call to dleev deeper ! - // because we manully need o walk over transparent wrappers! + // manual call to delve deeper ! + // because we manully need to walk over transparent wrappers! // continue traversal into the inner expression oxc::ast_visit::walk::walk_ts_as_expression(self, node); } -} -impl CodeVisitor<'_> { - // add to findings - fn report( + // visit every function + fn visit_function( &mut self, - snippet: &str, - span_start: usize, - vuln_type: VulnerabilityType, - severity: Severity, + node: &oxc_ast::ast::Function<'a>, + flags: oxc::syntax::scope::ScopeFlags, ) { - let safe = span_start.min(self.source_text.len()); - let line = self.source_text[..safe].lines().count() + 1; + if let Some(id) = &node.id { + let name = id.name.to_string(); + let scope = self.current_scope(); - self.findings.push(Findings { - vuln_type, - line_no: line.to_string(), - file_path: self.file_path.to_string(), - snippet: snippet.to_string(), - severity, - }); + let kind = match scope.as_str() { + "global" => SymbolKind::Function, + _ => SymbolKind::Method, + }; + + self.symbols.push(Symbol { + name: name.clone(), + kind, + file: self.file_path.to_string(), + line: self.span_to_line(node.span.start as usize), + column: node.span.start as usize, + scope, + }); + + // push scope before walking children + self.scope_stack.push(name); + + // collect parameters + for param in &node.params.items { + if let BindingPattern::BindingIdentifier(ident) = ¶m.pattern { + self.symbols.push(Symbol { + name: ident.name.to_string(), + kind: SymbolKind::Parameter, + file: self.file_path.to_string(), + line: self.span_to_line(param.span.start as usize), + column: param.span.start as usize, + scope: self.current_scope(), + }); + } + } + + oxc::ast_visit::walk::walk_function(self, node, flags); + self.scope_stack.pop(); // restore after + } else { + // anonymous function, still walk + oxc::ast_visit::walk::walk_function(self, node, flags); + } } - pub fn list_possible_threats(self) -> Vec { - self.findings + // visit classes + fn visit_class(&mut self, node: &oxc_ast::ast::Class<'a>) { + if let Some(id) = &node.id { + let name = id.name.to_string(); + + self.symbols.push(Symbol { + name: name.clone(), + kind: SymbolKind::Class, + file: self.file_path.to_string(), + line: self.span_to_line(node.span.start as usize), + column: node.span.start as usize, + scope: self.current_scope(), + }); + + self.scope_stack.push(name); + oxc::ast_visit::walk::walk_class(self, node); + self.scope_stack.pop(); + } else { + oxc::ast_visit::walk::walk_class(self, node); + } + } + + // methods are class functions + // e.g. + /* class App{ + method(ab){} + } */ + fn visit_method_definition(&mut self, node: &oxc_ast::ast::MethodDefinition<'a>) { + if let oxc_ast::ast::PropertyKey::StaticIdentifier(ident) = &node.key { + let name = ident.name.to_string(); + + self.symbols.push(Symbol { + name: name.clone(), + kind: SymbolKind::Method, + file: self.file_path.to_string(), + line: self.span_to_line(node.span.start as usize), + column: node.span.start as usize, + scope: self.current_scope(), + }); + + // push method name onto scope + self.scope_stack.push(name); + + // collect parameters + for param in &node.value.params.items { + if let BindingPattern::BindingIdentifier(ident) = ¶m.pattern { + self.symbols.push(Symbol { + name: ident.name.to_string(), + kind: SymbolKind::Parameter, + file: self.file_path.to_string(), + line: self.span_to_line(param.span.start as usize), + column: param.span.start as usize, + scope: self.current_scope(), // now inside method scope + }); + } + } + + oxc::ast_visit::walk::walk_method_definition(self, node); + self.scope_stack.pop(); + } else { + oxc::ast_visit::walk::walk_method_definition(self, node); + } + } + + // e.g. import 'express' from express + fn visit_import_declaration(&mut self, node: &oxc_ast::ast::ImportDeclaration<'a>) { + if let Some(specifiers) = &node.specifiers { + for specifier in specifiers { + let name = match specifier { + oxc_ast::ast::ImportDeclarationSpecifier::ImportSpecifier(s) => { + s.local.name.to_string() + } + oxc_ast::ast::ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => { + s.local.name.to_string() + } + oxc_ast::ast::ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => { + s.local.name.to_string() + } + }; + + self.symbols.push(Symbol { + name, + kind: SymbolKind::Import, + file: self.file_path.to_string(), + line: self.span_to_line(node.span.start as usize), + column: node.span.start as usize, + scope: "global".to_string(), // imports are always global + }); + } + } + oxc::ast_visit::walk::walk_import_declaration(self, node); } } diff --git a/src/server/detectors/typescript/tests.rs b/src/server/detectors/typescript/tests.rs index 7b7996f..4633772 100644 --- a/src/server/detectors/typescript/tests.rs +++ b/src/server/detectors/typescript/tests.rs @@ -9,8 +9,8 @@ mod test { #[test] fn detects_eval_wrapped_in_as_any() { let code = r#"(eval as any)("alert(1)");"#; - let findings = SCANNER.scan(code, "test.ts"); - assert!(findings + let scan_result = SCANNER.scan(code, "test.ts"); + assert!(scan_result.findings .iter() .any(|f| f.vuln_type == VulnerabilityType::Eval)); } @@ -22,8 +22,8 @@ mod test { password: "secret123" } satisfies Config; "#; - let findings = SCANNER.scan(code, "test.ts"); - assert!(findings + let scan_result = SCANNER.scan(code, "test.ts"); + assert!(scan_result.findings .iter() .any(|f| f.vuln_type == VulnerabilityType::HardcodedSecret)); } @@ -31,8 +31,8 @@ mod test { #[test] fn detects_as_any_usage() { let code = r#"const x = userInput as any;"#; - let findings = SCANNER.scan(code, "test.ts"); - assert!(findings + let scan_result = SCANNER.scan(code, "test.ts"); + assert!(scan_result.findings .iter() .any(|f| f.vuln_type == VulnerabilityType::UnsafeTypeAssertion)); } @@ -40,8 +40,8 @@ mod test { #[test] fn detects_eval_nested_in_call_arguments() { let code = r#"db.query(eval("SELECT * FROM " + table));"#; - let findings = SCANNER.scan(code, "test.ts"); - assert!(findings + let scan_result = SCANNER.scan(code, "test.ts"); + assert!(scan_result.findings .iter() .any(|f| f.vuln_type == VulnerabilityType::Eval)); } diff --git a/src/server/detectors/typescript/ts_scanner.rs b/src/server/detectors/typescript/ts_scanner.rs index c8d553b..ef23315 100644 --- a/src/server/detectors/typescript/ts_scanner.rs +++ b/src/server/detectors/typescript/ts_scanner.rs @@ -1,5 +1,6 @@ use crate::server::detectors::shared::common_parser::{parse_to_ast, CodeVisitor}; use crate::server::detectors::Scanner; +use crate::server::models::results::ScanResult; use crate::Findings; use oxc::allocator::Allocator; use oxc::ast_visit::Visit; @@ -10,24 +11,22 @@ pub struct TypeScriptScanner; // hardcoded secrets // SQL Injection vulnerabilities impl Scanner for TypeScriptScanner { - fn scan(&self, code: &str, file_path: &str) -> Vec { + fn scan(&self, code: &str, file_path: &str) -> ScanResult { let allocator = Allocator::default(); let ast = match parse_to_ast(code, &allocator, file_path) { Ok(program) => program, Err(e) => { eprintln!("Parse error: {}", e); - return vec![]; + return ScanResult { + findings: vec![], + symbols: vec![], + }; } }; - let mut visitor = CodeVisitor { - findings: Vec::new(), - file_path, - source_text: code, - }; - + let mut visitor = CodeVisitor::new(file_path, code); visitor.visit_program(&ast); - visitor.list_possible_threats() + visitor.into_scan_result() } } diff --git a/src/server/models/mod.rs b/src/server/models/mod.rs index 02995fe..80268da 100644 --- a/src/server/models/mod.rs +++ b/src/server/models/mod.rs @@ -1 +1,3 @@ pub mod findings; +pub mod results; +pub mod symbols; \ No newline at end of file diff --git a/src/server/models/results.rs b/src/server/models/results.rs new file mode 100644 index 0000000..42f916f --- /dev/null +++ b/src/server/models/results.rs @@ -0,0 +1,7 @@ +use super::findings::Findings; +use super::symbols::Symbol; + +pub struct ScanResult { + pub findings: Vec, + pub symbols: Vec, +} \ No newline at end of file diff --git a/src/server/models/symbols.rs b/src/server/models/symbols.rs new file mode 100644 index 0000000..d744380 --- /dev/null +++ b/src/server/models/symbols.rs @@ -0,0 +1,24 @@ +use std::collections::HashMap; +#[derive(Debug)] +pub enum SymbolKind { + Function, + Method, + Variable, + Parameter, + Import, + Class, + Struct, +} + +pub struct Symbol { + pub name: String, + pub kind: SymbolKind, + pub file: String, + pub line: usize, + pub column: usize, + pub scope: String, +} + +pub struct SymbolTable { + pub symbols: HashMap>, +} \ No newline at end of file diff --git a/src/server/service.rs b/src/server/service.rs index 63369ff..7d813bc 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -1,5 +1,6 @@ use crate::server::detectors::{GolangScanner, JavaScriptScanner, Scanner, TypeScriptScanner}; -use crate::server::models::findings::Findings; +use crate::server::models::results::ScanResult; + use std::collections::HashMap; #[derive(Eq, Hash, PartialEq)] @@ -26,21 +27,23 @@ impl OWASPScanner { OWASPScanner { scanners } } - pub fn scan(&self, codebase: &str, file_path: &str) -> Vec { - let mut all_findings: Vec = Vec::new(); + pub fn scan(&self, codebase: &str, file_path: &str) -> ScanResult { + let dummy=ScanResult{ + findings:Vec::new(), + symbols:Vec::new() + }; let language = match Self::determine_language(file_path) { Some(lang) => lang, - None => return all_findings, + None => return dummy, }; let scanner = match self.scanners.get(&language) { Some(d) => d, - None => return all_findings, + None => return dummy, }; - let findings = scanner.scan(codebase, file_path); - all_findings.extend(findings); - all_findings + let scan_result = scanner.scan(codebase, file_path); + scan_result } // this can gow to support more languages diff --git a/src/state.rs b/src/state.rs index 6b7f853..81984b4 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,15 +1,17 @@ -use crate::server::models::findings::FinalFindings; +use crate::server::models::{symbols::SymbolTable,findings::FinalFindings}; use std::collections::HashMap; pub struct AppState { // HashMap>> pub results: HashMap>, + pub symbol_table: Option, } impl AppState { pub fn new() -> Self { Self { results: HashMap::new(), + symbol_table:None } } } From 3766806c089bf296e5720b3322c6ba52bd64f9b4 Mon Sep 17 00:00:00 2001 From: DeeTomPanda Date: Sun, 5 Jul 2026 22:55:45 +0530 Subject: [PATCH 2/6] feat: add symbol map for go, unify symbol in APpState,obey .gitignore when scanning dirs,add col no fild for results, improve line no acquisition --- Cargo.lock | 40 +++ Cargo.toml | 1 + src/cli/mod.rs | 64 ++-- src/main.rs | 10 +- src/server/detectors/golang/exec_injection.rs | 16 +- ...{file_opertaions.rs => file_operations.rs} | 6 +- src/server/detectors/golang/go_scanner.rs | 276 ++++++++++++++++-- .../detectors/golang/hardcoded_secrets.rs | 6 +- src/server/detectors/golang/mod.rs | 2 +- src/server/detectors/golang/sql_injection.rs | 34 +-- src/server/detectors/javascript/js_scanner.rs | 2 - src/server/detectors/shared/common_parser.rs | 56 ++-- src/server/handlers/handler.rs | 4 +- src/server/models/findings.rs | 1 + src/state.rs | 13 +- 15 files changed, 399 insertions(+), 132 deletions(-) rename src/server/detectors/golang/{file_opertaions.rs => file_operations.rs} (88%) diff --git a/Cargo.lock b/Cargo.lock index 96f160f..88e9426 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -193,6 +193,16 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "bstr" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" +dependencies = [ + "memchr", + "serde_core", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -528,6 +538,19 @@ dependencies = [ "wasip3", ] +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + [[package]] name = "guard" version = "0.1.0" @@ -536,6 +559,7 @@ dependencies = [ "chrono", "clap", "http", + "ignore", "open", "oxc", "oxc_ast", @@ -702,6 +726,22 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "ignore" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe112b004901c62c2faa11f4f75e9864e0cc5af8da71c9115d184a3aa888749f" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "2.14.0" diff --git a/Cargo.toml b/Cargo.toml index 8c80ebc..0c02d44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,4 @@ tree-sitter = "0.25" tree-sitter-go = "0.25" streaming-iterator = "0.1.9" rayon = "1.12.0" +ignore = "0.4" diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 095f21f..1793b94 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,3 +1,4 @@ +use ignore::WalkBuilder; use rayon::prelude::*; use std::collections::HashMap; use std::sync::Arc; @@ -5,42 +6,26 @@ use tokio::sync::RwLock; use uuid::Uuid; use walkdir::WalkDir; -use crate::server::models::{ - findings::{severity_order, FinalFindings}, - symbols::{Symbol, SymbolTable}, -}; use crate::server::service::OWASPScanner; use crate::AppState; - -const IGNORED_DIRS: &[&str] = &[ - "node_modules", - "vendor", - "target", - "__pycache__", - ".venv", - "venv", - ".git", - ".idea", - ".vscode", - "dist", - "build", -]; +use crate::{ + server::models::{ + findings::{severity_order, FinalFindings}, + symbols::{Symbol, SymbolTable}, + }, + state::ScanData, +}; // start the scan of the directory pub async fn scan(path: String, state: Arc>) -> String { let owasp_scanner = OWASPScanner::new(); - - let entries: Vec<_> = WalkDir::new(&path) - .into_iter() + let entries: Vec<_> = WalkBuilder::new(&path) + .hidden(false) + .git_ignore(true) + .git_global(true) + .git_exclude(true) + .build() .filter_map(|e| e.ok()) - .filter(|e| { - !e.path().components().any(|c| { - c.as_os_str() - .to_str() - .map(|s| IGNORED_DIRS.contains(&s)) - .unwrap_or(false) // maybe not utf-8, but still try and log error if any - }) - }) .filter(|e| OWASPScanner::determine_language(&e.path().to_string_lossy()).is_some()) .collect(); @@ -98,22 +83,15 @@ pub async fn scan(path: String, state: Arc>) -> String { let scan_id = Uuid::new_v4().to_string(); let mut state = state.write().await; - state.results.insert(scan_id.clone(), all_findings); - state.symbol_table = Some(SymbolTable { + let symbol_table = SymbolTable { symbols: symbol_map, - }); + }; + let scan_data = ScanData { + findings: all_findings, + symbol_table, + }; - // temporary debug, remove later - if let Some(ref table) = state.symbol_table { - let total: usize = table.symbols.values().map(|v| v.len()).sum(); - println!("Symbols extracted: {}", total); - for (file, symbols) in &table.symbols { - println!(" {}", file); - for sym in symbols { - println!(" {:?} {} (scope: {})", sym.kind, sym.name, sym.scope); - } - } - } + state.results.insert(scan_id.clone(), scan_data); scan_id } diff --git a/src/main.rs b/src/main.rs index a1009b4..4a5d379 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,9 +19,6 @@ enum Command { #[arg(long)] sarif: bool, - #[arg(long)] - analyze: bool, - #[arg(long)] output: Option, }, @@ -44,7 +41,6 @@ async fn main() { Command::Scan { path, sarif, - analyze, output, } => { let scan_id = cli::scan(path, Arc::clone(&state)).await; @@ -57,7 +53,7 @@ async fn main() { let state_read = state.read().await; if let Some(results) = state_read.results.get(&scan_id) { - match to_sarif_json(&results) { + match to_sarif_json(&results.findings) { Ok(json) => { std::fs::write(file_path, json).unwrap(); } @@ -81,7 +77,7 @@ async fn main() { let scan_id = cli::scan(path, Arc::clone(&state)).await; let state_read = state.read().await; - if let Some(ref table) = state_read.symbol_table { + if let Some(scan_data) = state_read.results.get(&scan_id) { let mut functions = 0; let mut methods = 0; let mut variables = 0; @@ -89,7 +85,7 @@ async fn main() { let mut imports = 0; let mut classes = 0; - for symbols in table.symbols.values() { + for symbols in scan_data.symbol_table.symbols.values() { for sym in symbols { match sym.kind { SymbolKind::Function => functions += 1, diff --git a/src/server/detectors/golang/exec_injection.rs b/src/server/detectors/golang/exec_injection.rs index 8077ce0..5c3a329 100644 --- a/src/server/detectors/golang/exec_injection.rs +++ b/src/server/detectors/golang/exec_injection.rs @@ -46,23 +46,23 @@ const QUERY_SYSCALL_EXEC: &str = r#" "#; impl GolangTreeSitter<'_> { - pub(super) fn check_exec(&mut self, root: Node, src: &[u8]) { - for m in match_pattern(QUERY_EXEC_COMMAND_DYNAMIC, root, src) { + pub(super) fn check_exec(&mut self, root: Node, code_bytes: &[u8]) { + for m in match_pattern(QUERY_EXEC_COMMAND_DYNAMIC, root, code_bytes) { if let Some((_, snippet, line, _)) = m.iter().find(|(n, ..)| n == "snippet") { self.report( snippet, - &line.to_string(), + root, VulnerabilityType::UnsafeCodeExecution, Severity::High, ); } } - for m in match_pattern(QUERY_EXEC_COMMAND_SHELL, root, src) { + for m in match_pattern(QUERY_EXEC_COMMAND_SHELL, root, code_bytes) { if let Some((_, snippet, line, _)) = m.iter().find(|(n, ..)| n == "snippet") { self.report( snippet, - &line.to_string(), + root, VulnerabilityType::UnsafeCodeExecution, Severity::High, ); @@ -70,15 +70,15 @@ impl GolangTreeSitter<'_> { } } - pub(super) fn check_syscall(&mut self, root: Node, src: &[u8]) { - for m in match_pattern(QUERY_SYSCALL_EXEC, root, src) { + pub(super) fn check_syscall(&mut self, root: Node, code_bytes: &[u8]) { + for m in match_pattern(QUERY_SYSCALL_EXEC, root, code_bytes) { if let (Some((_, snippet, line, _)), Some((_, method, _, _))) = ( m.iter().find(|(n, ..)| n == "snippet"), m.iter().find(|(n, ..)| n == "method"), ) { self.report( snippet, - &line.to_string(), + root, VulnerabilityType::UnsafeCodeExecution, Severity::High, ); diff --git a/src/server/detectors/golang/file_opertaions.rs b/src/server/detectors/golang/file_operations.rs similarity index 88% rename from src/server/detectors/golang/file_opertaions.rs rename to src/server/detectors/golang/file_operations.rs index 7be2319..e150d12 100644 --- a/src/server/detectors/golang/file_opertaions.rs +++ b/src/server/detectors/golang/file_operations.rs @@ -20,15 +20,15 @@ const QUERY_FILE_OS: &str = r#" // For now we keep filepath.Join unreported to avoid false positives. impl GolangTreeSitter<'_> { - pub(super) fn check_file_ops(&mut self, root: Node, src: &[u8]) { - for m in match_pattern(QUERY_FILE_OS, root, src) { + pub(super) fn check_file_ops(&mut self, root: Node, code_bytes: &[u8]) { + for m in match_pattern(QUERY_FILE_OS, root, code_bytes) { if let (Some((_, snippet, line, _)), Some((_, method, _, _))) = ( m.iter().find(|(n, ..)| n == "snippet"), m.iter().find(|(n, ..)| n == "method"), ) { self.report( snippet, - &line.to_string(), + root, VulnerabilityType::UnsafeFileOperation, Severity::High, ); diff --git a/src/server/detectors/golang/go_scanner.rs b/src/server/detectors/golang/go_scanner.rs index cadcf7e..765451f 100644 --- a/src/server/detectors/golang/go_scanner.rs +++ b/src/server/detectors/golang/go_scanner.rs @@ -1,29 +1,256 @@ use crate::server::models::findings::{Severity, VulnerabilityType}; -use crate::server::{detectors::Scanner, models::{results::ScanResult,findings::Findings}}; +use crate::server::{ + detectors::Scanner, + models::{ + findings::Findings, + results::ScanResult, + symbols::{Symbol, SymbolKind}, + }, +}; pub struct GolangScanner; pub(super) struct GolangTreeSitter<'a> { findings: Vec, + symbols: Vec, file_path: &'a str, + scope_stack: Vec, } impl Scanner for GolangScanner { fn scan(&self, code: &str, file_path: &str) -> ScanResult { let mut tree_sitter = GolangTreeSitter::new(file_path); tree_sitter.analyze(code); - ScanResult { - findings: tree_sitter.list_possible_threats(), - symbols: Vec::new(), // Placeholder - implement symbol analysis if needed - } + tree_sitter.into_result() } } impl<'a> GolangTreeSitter<'a> { pub fn new(file_path: &'a str) -> Self { + // for (i,ch) in Self { findings: Vec::new(), file_path, + symbols: Vec::new(), + scope_stack: Vec::new(), + } + } + + fn current_scope(scope_stack: &Vec) -> String { + if scope_stack.is_empty() { + "global".to_string() + } else { + scope_stack.join("::") + } + } + + fn extract_symbols(&mut self, root: tree_sitter::Node, code_bytes: &[u8]) { + self.walk_node(root, code_bytes); + } + + fn walk_node(&mut self, node: tree_sitter::Node, code_bytes: &[u8]) { + match node.kind() { + // standalone function + "function_declaration" => { + if let Some(name_node) = node.child_by_field_name("name") { + let name = name_node.utf8_text(code_bytes).unwrap_or("").to_string(); + let line = name_node.start_position().row + 1; + + self.symbols.push(Symbol { + name: name.clone(), + kind: SymbolKind::Function, + file: self.file_path.to_string(), + line, + column: name_node.start_position().column, + scope: Self::current_scope(&self.scope_stack), + }); + + self.scope_stack.push(name); + // extract parameters + if let Some(params) = node.child_by_field_name("parameters") { + self.extract_go_params(params, code_bytes); + } + + self.walk_children(node, code_bytes); + self.scope_stack.pop(); + } + } + + // method on a type + "method_declaration" => { + if let Some(name_node) = node.child_by_field_name("name") { + let name = name_node.utf8_text(code_bytes).unwrap_or("").to_string(); + let line = name_node.start_position().row + 1; + + // get receiver type for scope + let receiver_type = node + .child_by_field_name("receiver") + .and_then(|r| r.child_by_field_name("type")) + .and_then(|t| t.utf8_text(code_bytes).ok()) + .unwrap_or("unknown") + .trim_start_matches('*') // remove pointer prefix + .to_string(); + + self.symbols.push(Symbol { + name: name.clone(), + kind: SymbolKind::Method, + file: self.file_path.to_string(), + line, + column: name_node.start_position().column, + scope: receiver_type.clone(), + }); + + self.scope_stack.push(receiver_type); + self.scope_stack.push(name); + + // extract parameters + if let Some(params) = node.child_by_field_name("parameters") { + self.extract_go_params(params, code_bytes); + } + + self.walk_children(node, code_bytes); + self.scope_stack.pop(); + self.scope_stack.pop(); + } + } + + // short variable declaration: x := something + "short_var_declaration" => { + if let Some(left) = node.child_by_field_name("left") { + let mut cursor = left.walk(); + for child in left.children(&mut cursor) { + if child.kind() == "identifier" { + let name = child.utf8_text(code_bytes).unwrap_or("").to_string(); + self.symbols.push(Symbol { + name, + kind: SymbolKind::Variable, + file: self.file_path.to_string(), + line: child.start_position().row + 1, + column: child.start_position().column, + scope: Self::current_scope(&self.scope_stack), + }); + } + } + } + self.walk_children(node, code_bytes); + } + + // var declaration: var x int = something + "var_declaration" => { + let mut cursor = node.walk(); + for child in node.children(&mut cursor) { + if child.kind() == "var_spec" { + if let Some(name_node) = child.child_by_field_name("name") { + let name = name_node.utf8_text(code_bytes).unwrap_or("").to_string(); + self.symbols.push(Symbol { + name, + kind: SymbolKind::Variable, + file: self.file_path.to_string(), + line: name_node.start_position().row + 1, + column: name_node.start_position().column, + scope: Self::current_scope(&self.scope_stack), + }); + } + } + } + self.walk_children(node, code_bytes); + } + + "import_declaration" => { + let mut cursor = node.walk(); + for child in node.children(&mut cursor) { + match child.kind() { + // single import + "import_spec" => { + self.collect_import_spec(child, code_bytes); + } + // grouped imports + "import_spec_list" => { + let mut list_cursor = child.walk(); + for spec in child.children(&mut list_cursor) { + if spec.kind() == "import_spec" { + self.collect_import_spec(spec, code_bytes); + } + } + } + _ => {} + } + } + } + + // struct types + "type_declaration" => { + let mut cursor = node.walk(); + for child in node.children(&mut cursor) { + if child.kind() == "type_spec" { + let has_struct = child + .child_by_field_name("type") + .map(|t| t.kind() == "struct_type") + .unwrap_or(false); + + if has_struct { + if let Some(name_node) = child.child_by_field_name("name") { + let name = + name_node.utf8_text(code_bytes).unwrap_or("").to_string(); + self.symbols.push(Symbol { + name, + kind: SymbolKind::Struct, + file: self.file_path.to_string(), + line: name_node.start_position().row + 1, + column: name_node.start_position().column, + scope: "global".to_string(), + }); + } + } + } + } + } + + // everything else — just walk children + _ => { + self.walk_children(node, code_bytes); + } + } + } + + fn walk_children(&mut self, node: tree_sitter::Node, code_bytes: &[u8]) { + let mut cursor = node.walk(); + for child in node.children(&mut cursor) { + self.walk_node(child, code_bytes); + } + } + + fn extract_go_params(&mut self, params_node: tree_sitter::Node, code_bytes: &[u8]) { + let mut cursor = params_node.walk(); + for child in params_node.children(&mut cursor) { + if child.kind() == "parameter_declaration" { + if let Some(name_node) = child.child_by_field_name("name") { + let name = name_node.utf8_text(code_bytes).unwrap_or("").to_string(); + self.symbols.push(Symbol { + name, + kind: SymbolKind::Parameter, + file: self.file_path.to_string(), + line: name_node.start_position().row + 1, + column: name_node.start_position().column, + scope: Self::current_scope(&self.scope_stack), + }); + } + } + } + } + + fn collect_import_spec(&mut self, node: tree_sitter::Node, code: &[u8]) { + if let Some(path_node) = node.child_by_field_name("path") { + let raw = path_node.utf8_text(code).unwrap_or("\"\""); + let name = raw.trim_matches('"').to_string(); + self.symbols.push(Symbol { + name, + kind: SymbolKind::Import, + file: self.file_path.to_string(), + line: path_node.start_position().row + 1, + column: path_node.start_position().column, + scope: "global".to_string(), + }); } } @@ -46,32 +273,35 @@ impl<'a> GolangTreeSitter<'a> { } }; let root = tree.root_node(); - let bytes = code.as_bytes(); + let code_bytes = code.as_bytes(); + self.extract_symbols(root, code_bytes); // check for hardcoded secrets - self.check_secrets(root, bytes); + self.check_secrets(root, code_bytes); // check for SQL injection vulnerabilities - self.check_sql_sprintf(root, bytes); - self.check_sql_db(root, bytes); - self.check_gorm(root, bytes); + self.check_sql_sprintf(root, code_bytes); + self.check_sql_db(root, code_bytes); + self.check_gorm(root, code_bytes); // check for command injection vulnerabilities - self.check_exec(root, bytes); - self.check_syscall(root, bytes); + self.check_exec(root, code_bytes); + self.check_syscall(root, code_bytes); // check for unsafe file operations - self.check_file_ops(root, bytes); + self.check_file_ops(root, code_bytes); } pub(super) fn report( &mut self, snippet: &str, - line: &str, + node: tree_sitter::Node, vuln_type: VulnerabilityType, severity: Severity, ) { - // avoid duplicate findings for the same vuln type, line and snippet - let exists = self - .findings - .iter() - .any(|f| f.vuln_type == vuln_type && f.line_no == line && f.snippet == snippet); + let line = (node.start_position().row + 1).to_string(); + let col = node.start_position().column.to_string(); + + // avoid duplicate findings for the same vuln type, line,column and snippet + let exists = self.findings.iter().any(|f| { + f.vuln_type == vuln_type && f.line_no == line && f.col_no == col && f.snippet == snippet + }); if exists { return; @@ -80,12 +310,20 @@ impl<'a> GolangTreeSitter<'a> { self.findings.push(Findings { vuln_type, line_no: line.to_string(), + col_no: col.to_string(), file_path: self.file_path.to_string(), snippet: snippet.to_string(), severity, }); } + pub fn into_result(self) -> ScanResult { + ScanResult { + findings: self.findings, + symbols: self.symbols, + } + } + pub fn list_possible_threats(self) -> Vec { self.findings } diff --git a/src/server/detectors/golang/hardcoded_secrets.rs b/src/server/detectors/golang/hardcoded_secrets.rs index fcdcc78..a75c48d 100644 --- a/src/server/detectors/golang/hardcoded_secrets.rs +++ b/src/server/detectors/golang/hardcoded_secrets.rs @@ -26,13 +26,13 @@ const QUERY_SECRET_STRUCT: &str = r#" "#; impl GolangTreeSitter<'_> { - pub(super) fn check_secrets(&mut self, root: Node, src: &[u8]) { + pub(super) fn check_secrets(&mut self, root: Node, code_bytes: &[u8]) { for query_src in [QUERY_SECRET_VAR, QUERY_SECRET_CONST, QUERY_SECRET_STRUCT] { - for m in match_pattern(query_src, root, src) { + for m in match_pattern(query_src, root, code_bytes) { if let Some((_, snippet, line, _)) = m.iter().find(|(n, ..)| n == "snippet") { self.report( snippet, - &line.to_string(), + root, VulnerabilityType::HardcodedSecret, Severity::Critical, ); diff --git a/src/server/detectors/golang/mod.rs b/src/server/detectors/golang/mod.rs index b93e73d..b136aa3 100644 --- a/src/server/detectors/golang/mod.rs +++ b/src/server/detectors/golang/mod.rs @@ -1,5 +1,5 @@ mod exec_injection; -mod file_opertaions; +mod file_operations; pub mod go_scanner; mod hardcoded_secrets; mod sql_injection; diff --git a/src/server/detectors/golang/sql_injection.rs b/src/server/detectors/golang/sql_injection.rs index 6f8d6ad..87e4750 100644 --- a/src/server/detectors/golang/sql_injection.rs +++ b/src/server/detectors/golang/sql_injection.rs @@ -122,8 +122,8 @@ const QUERY_GORM_RAW_VAR: &str = r#" "#; impl GolangTreeSitter<'_> { - pub(super) fn check_sql_sprintf(&mut self, root: Node, src: &[u8]) { - for m in match_pattern(QUERY_SQL_SPRINTF, root, src) { + pub(super) fn check_sql_sprintf(&mut self, root: Node, code_bytes: &[u8]) { + for m in match_pattern(QUERY_SQL_SPRINTF, root, code_bytes) { let query = m .iter() .find(|(n, ..)| n == "query") @@ -144,7 +144,7 @@ impl GolangTreeSitter<'_> { { self.report( snippet, - &(line.to_string()), + root, VulnerabilityType::SQLInjection, Severity::Critical, ); @@ -152,7 +152,7 @@ impl GolangTreeSitter<'_> { } } - for m in match_pattern(QUERY_SQL_SPRINTF_CONTEXT, root, src) { + for m in match_pattern(QUERY_SQL_SPRINTF_CONTEXT, root, code_bytes) { let query = m .iter() .find(|(n, ..)| n == "query") @@ -173,7 +173,7 @@ impl GolangTreeSitter<'_> { { self.report( snippet, - &(line.to_string()), + root, VulnerabilityType::SQLInjection, Severity::Critical, ); @@ -182,8 +182,8 @@ impl GolangTreeSitter<'_> { } } - pub(super) fn check_sql_db(&mut self, root: Node, src: &[u8]) { - for m in match_pattern(QUERY_SQL_DB, root, src) { + pub(super) fn check_sql_db(&mut self, root: Node, code_bytes: &[u8]) { + for m in match_pattern(QUERY_SQL_DB, root, code_bytes) { let method = m .iter() .find(|(n, ..)| n == "method") @@ -197,14 +197,14 @@ impl GolangTreeSitter<'_> { if let (Some(method), Some((snippet, line, col))) = (method, snippet) { self.report( snippet, - &(line.to_string()), + root, VulnerabilityType::SQLInjection, Severity::Critical, ); } } - for m in match_pattern(QUERY_SQL_DB_CONTEXT, root, src) { + for m in match_pattern(QUERY_SQL_DB_CONTEXT, root, code_bytes) { let method = m .iter() .find(|(n, ..)| n == "method") @@ -218,7 +218,7 @@ impl GolangTreeSitter<'_> { if let (Some(method), Some((snippet, line, col))) = (method, snippet) { self.report( snippet, - &(line.to_string()), + root, VulnerabilityType::SQLInjection, Severity::Critical, ); @@ -228,13 +228,13 @@ impl GolangTreeSitter<'_> { // ── GORM ─── - pub(super) fn check_gorm(&mut self, root: Node, src: &[u8]) { + pub(super) fn check_gorm(&mut self, root: Node, code_bytes: &[u8]) { // string concat: db.Where("id = " + val) - for m in match_pattern(QUERY_GORM_CONCAT, root, src) { + for m in match_pattern(QUERY_GORM_CONCAT, root, code_bytes) { if let Some((_, snippet, line, _)) = m.iter().find(|(n, ..)| n == "snippet") { self.report( snippet, - &(line.to_string()), + root, VulnerabilityType::SQLInjection, Severity::Critical, ); @@ -242,11 +242,11 @@ impl GolangTreeSitter<'_> { } // Sprintf inside Where: db.Where(fmt.Sprintf(...)) - for m in match_pattern(QUERY_GORM_SPRINTF, root, src) { + for m in match_pattern(QUERY_GORM_SPRINTF, root, code_bytes) { if let Some((_, snippet, line, _)) = m.iter().find(|(n, ..)| n == "snippet") { self.report( snippet, - &(line.to_string()), + root, VulnerabilityType::SQLInjection, Severity::Critical, ); @@ -254,11 +254,11 @@ impl GolangTreeSitter<'_> { } // raw variable: db.Raw(query) - for m in match_pattern(QUERY_GORM_RAW_VAR, root, src) { + for m in match_pattern(QUERY_GORM_RAW_VAR, root, code_bytes) { if let Some((_, snippet, line, _)) = m.iter().find(|(n, ..)| n == "snippet") { self.report( snippet, - &(line.to_string()), + root, VulnerabilityType::SQLInjection, Severity::Critical, ); diff --git a/src/server/detectors/javascript/js_scanner.rs b/src/server/detectors/javascript/js_scanner.rs index b5add2b..fb2b56a 100644 --- a/src/server/detectors/javascript/js_scanner.rs +++ b/src/server/detectors/javascript/js_scanner.rs @@ -1,5 +1,3 @@ -use std::iter::Scan; - use crate::server::detectors::shared::common_parser::{parse_to_ast, CodeVisitor}; use crate::server::detectors::Scanner; use crate::server::models::results::ScanResult; diff --git a/src/server/detectors/shared/common_parser.rs b/src/server/detectors/shared/common_parser.rs index aefe0a5..a2b7aac 100644 --- a/src/server/detectors/shared/common_parser.rs +++ b/src/server/detectors/shared/common_parser.rs @@ -6,7 +6,7 @@ use crate::server::models::{ }; use crate::Findings; use oxc::ast_visit::Visit; -use oxc::span::GetSpan; +use oxc::span::{GetSpan, Span}; use oxc::{allocator::Allocator, parser::Parser, span::SourceType}; use oxc_ast::ast::{BindingPattern, CallExpression, Expression, NewExpression, VariableDeclarator}; use oxc_ast::ast::{TSAsExpression, TSType}; @@ -17,16 +17,29 @@ pub struct CodeVisitor<'a> { pub source_text: &'a str, pub symbols: Vec, scope_stack: Vec, + line_starts:Vec } impl<'a> CodeVisitor<'a> { pub fn new(file_path: &'a str, source_text: &'a str) -> Self { + // feault start for all files + let mut line_starts=vec![0]; + + // loop through entire file bytes for start + + for (i,c) in source_text.char_indices(){ + if c=='\n'{ + line_starts.push(i+1); + } + } + Self { file_path, source_text, findings: vec![], symbols: vec![], scope_stack: vec![], + line_starts } } @@ -44,20 +57,29 @@ impl<'a> CodeVisitor<'a> { self.source_text[..safe].lines().count() + 1 } + fn offset_to_line_col(&self, pos: usize) -> (usize, usize) { + let idx = self.line_starts.partition_point(|&x| x <= pos) - 1; + + let line = idx + 1; + let col = pos - self.line_starts[idx]; + + (line, col) + } + // add to findings fn report( &mut self, snippet: &str, - span_start: usize, + span: Span, vuln_type: VulnerabilityType, severity: Severity, ) { - let safe = span_start.min(self.source_text.len()); - let line = self.source_text[..safe].lines().count() + 1; + let (line, col) = self.offset_to_line_col(span.start as usize); self.findings.push(Findings { vuln_type, line_no: line.to_string(), + col_no:col.to_string(), file_path: self.file_path.to_string(), snippet: snippet.to_string(), severity, @@ -106,7 +128,7 @@ impl<'a> Visit<'a> for CodeVisitor<'a> { if let Some(value) = is_hardcoded_secret(value) { self.report( &value, - prop.span().start as usize, + prop.span(), VulnerabilityType::HardcodedSecret, Severity::Critical, ); @@ -126,7 +148,7 @@ impl<'a> Visit<'a> for CodeVisitor<'a> { if let Some(value) = is_hardcoded_secret(init) { self.report( &value, - init.span().start as usize, + init.span(), VulnerabilityType::HardcodedSecret, Severity::Critical, ); @@ -144,12 +166,7 @@ impl<'a> Visit<'a> for CodeVisitor<'a> { if let Expression::Identifier(ident) = &node.callee { if ident.name.as_str() == "Function" { let name = ident.name.as_str(); - self.report( - name, - node.span().start as usize, - VulnerabilityType::Eval, - Severity::High, - ); + self.report(name, node.span(), VulnerabilityType::Eval, Severity::High); } } // walk into children and recurse @@ -158,7 +175,7 @@ impl<'a> Visit<'a> for CodeVisitor<'a> { // for functions // eg: save(x) - // db.query() + // db.query() // require() fn visit_call_expression(&mut self, node: &CallExpression<'a>) { // for TS @@ -169,12 +186,7 @@ impl<'a> Visit<'a> for CodeVisitor<'a> { let name = ident.name.as_str(); if is_dangerous_call(name) { - self.report( - name, - node.span().start as usize, - VulnerabilityType::Eval, - Severity::High, - ); + self.report(name, node.span(), VulnerabilityType::Eval, Severity::High); } // CommonJS imports via require() @@ -213,7 +225,7 @@ impl<'a> Visit<'a> for CodeVisitor<'a> { let snippet = &self.source_text[start..end]; self.report( snippet, - node.span().start as usize, + node.span(), VulnerabilityType::SQLInjection, Severity::Critical, ); @@ -228,7 +240,7 @@ impl<'a> Visit<'a> for CodeVisitor<'a> { let snippet = &self.source_text[start..end]; self.report( snippet, - node.span().start as usize, + node.span(), VulnerabilityType::SQLInjection, Severity::Critical, ); @@ -248,7 +260,7 @@ impl<'a> Visit<'a> for CodeVisitor<'a> { if let TSType::TSAnyKeyword(_) = &node.type_annotation { self.report( "as any", - node.span().start as usize, + node.span(), VulnerabilityType::UnsafeTypeAssertion, Severity::Low, ); diff --git a/src/server/handlers/handler.rs b/src/server/handlers/handler.rs index 18304b9..6261905 100644 --- a/src/server/handlers/handler.rs +++ b/src/server/handlers/handler.rs @@ -16,8 +16,8 @@ pub async fn get_results( let state_read = state.read().await; - if let Some(findings) = state_read.results.get(&scan_id) { - Json(findings).into_response() + if let Some(scan_result) = state_read.results.get(&scan_id) { + Json(&scan_result.findings).into_response() } else { StatusCode::NOT_FOUND.into_response() } diff --git a/src/server/models/findings.rs b/src/server/models/findings.rs index bf811eb..be7cab4 100644 --- a/src/server/models/findings.rs +++ b/src/server/models/findings.rs @@ -116,6 +116,7 @@ impl Debug for Severity { pub struct Findings { pub vuln_type: VulnerabilityType, pub line_no: String, + pub col_no:String, pub file_path: String, pub snippet: String, pub severity: Severity, diff --git a/src/state.rs b/src/state.rs index 81984b4..4388ae0 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,17 +1,20 @@ use crate::server::models::{symbols::SymbolTable,findings::FinalFindings}; use std::collections::HashMap; + +pub struct ScanData { + pub findings: Vec, + pub symbol_table: SymbolTable, +} pub struct AppState { - // HashMap>> - pub results: HashMap>, - pub symbol_table: Option, + // HashMap,SymbolTable]>> + pub results: HashMap, } impl AppState { pub fn new() -> Self { Self { - results: HashMap::new(), - symbol_table:None + results: HashMap::new() } } } From d2ac96aabecf1e63d8b6f12e24e35471db488b8c Mon Sep 17 00:00:00 2001 From: DeeTomPanda Date: Sun, 5 Jul 2026 23:10:50 +0530 Subject: [PATCH 3/6] chore: add column number to sarif output --- src/server/detectors/shared/sarif.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/detectors/shared/sarif.rs b/src/server/detectors/shared/sarif.rs index f83af1f..f764198 100644 --- a/src/server/detectors/shared/sarif.rs +++ b/src/server/detectors/shared/sarif.rs @@ -24,6 +24,7 @@ pub fn to_sarif(all_findings: &[FinalFindings]) -> Sarif { let region = Region::builder() .start_line(finding.line_no.parse::().unwrap_or(1)) + .start_column(finding.col_no.parse().unwrap_or(1)) .build(); let location = Location::builder() From 5f4b5250bfeba84d63ca1781739c6dc6d910ee82 Mon Sep 17 00:00:00 2001 From: DeeTomPanda Date: Mon, 6 Jul 2026 11:03:16 +0530 Subject: [PATCH 4/6] feat: add function call info for Go and JS/TS, slight refactor --- src/cli/mod.rs | 41 ++++-- src/server/detectors/golang/go_scanner.rs | 119 ++++++++++++++++-- src/server/detectors/javascript/js_scanner.rs | 3 +- src/server/detectors/shared/common_parser.rs | 107 ++++++++++++---- src/server/detectors/typescript/ts_scanner.rs | 1 + src/server/models/calls.rs | 34 +++++ src/server/models/mod.rs | 3 +- src/server/models/results.rs | 2 + src/server/service.rs | 7 +- src/state.rs | 3 +- 10 files changed, 264 insertions(+), 56 deletions(-) create mode 100644 src/server/models/calls.rs diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 1793b94..91f8d56 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -4,12 +4,13 @@ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; use uuid::Uuid; -use walkdir::WalkDir; +use crate::server::models::calls::CallSite; use crate::server::service::OWASPScanner; use crate::AppState; use crate::{ server::models::{ + calls::CallTable, findings::{severity_order, FinalFindings}, symbols::{Symbol, SymbolTable}, }, @@ -21,16 +22,16 @@ pub async fn scan(path: String, state: Arc>) -> String { let owasp_scanner = OWASPScanner::new(); let entries: Vec<_> = WalkBuilder::new(&path) .hidden(false) - .git_ignore(true) - .git_global(true) + .git_ignore(true) + .git_global(true) .git_exclude(true) .build() .filter_map(|e| e.ok()) .filter(|e| OWASPScanner::determine_language(&e.path().to_string_lossy()).is_some()) .collect(); - // collect both findings AND symbols in parallel - let all_results: Vec<(FinalFindings, Vec)> = entries + // collect both findings AND symbols AND callsites in parallel + let all_results: Vec<(FinalFindings, Vec, Vec)> = entries .par_iter() .filter_map(|entry| { let path = entry.path(); @@ -49,6 +50,7 @@ pub async fn scan(path: String, state: Arc>) -> String { findings: scan_result.findings, }, scan_result.symbols, + scan_result.calls, )) } Err(e) => { @@ -61,14 +63,21 @@ pub async fn scan(path: String, state: Arc>) -> String { /* after all that it would be like e.g. all_results = [ - (FinalFindings { file: "express/index.js" }, vec![Symbol, Symbol]), - (FinalFindings { file: "express/router.js" }, vec![Symbol, Symbol]), - (FinalFindings { file: "express/utils.js" }, vec![Symbol]), + (FinalFindings { file: "express/index.js" }, vec![Symbol, Symbol], vec![CallSite, CallSite]), + (FinalFindings { file: "express/router.js" }, vec![Symbol, Symbol], vec![CallSite, CallSite]), + (FinalFindings { file: "express/utils.js" }, vec![Symbol], vec![CallSite, CallSite]), ] */ // separate them after parallel scan is done - let (all_findings, all_symbols): (Vec, Vec>) = - all_results.into_iter().unzip(); + let mut all_findings = Vec::new(); + let mut all_symbols = Vec::new(); + let mut all_calls = Vec::new(); + + for (findings, symbols, calls) in all_results { + all_findings.push(findings); + all_symbols.push(symbols); + all_calls.push(calls); + } // flatten symbols into SymbolTable let mut symbol_map: HashMap> = HashMap::new(); @@ -81,6 +90,17 @@ pub async fn scan(path: String, state: Arc>) -> String { } } + // flatten calls into CallTable + let mut call_map: HashMap> = HashMap::new(); + for calls in all_calls { + for call in calls { + call_map + .entry(call.file.clone()) + .or_insert_with(Vec::new) + .push(call); + } + } + let scan_id = Uuid::new_v4().to_string(); let mut state = state.write().await; let symbol_table = SymbolTable { @@ -89,6 +109,7 @@ pub async fn scan(path: String, state: Arc>) -> String { let scan_data = ScanData { findings: all_findings, symbol_table, + call_table: CallTable { calls: call_map }, }; state.results.insert(scan_id.clone(), scan_data); diff --git a/src/server/detectors/golang/go_scanner.rs b/src/server/detectors/golang/go_scanner.rs index 765451f..ea83477 100644 --- a/src/server/detectors/golang/go_scanner.rs +++ b/src/server/detectors/golang/go_scanner.rs @@ -2,6 +2,7 @@ use crate::server::models::findings::{Severity, VulnerabilityType}; use crate::server::{ detectors::Scanner, models::{ + calls::CallSite, findings::Findings, results::ScanResult, symbols::{Symbol, SymbolKind}, @@ -15,6 +16,7 @@ pub(super) struct GolangTreeSitter<'a> { symbols: Vec, file_path: &'a str, scope_stack: Vec, + calls: Vec, } impl Scanner for GolangScanner { @@ -27,20 +29,20 @@ impl Scanner for GolangScanner { impl<'a> GolangTreeSitter<'a> { pub fn new(file_path: &'a str) -> Self { - // for (i,ch) in Self { findings: Vec::new(), file_path, symbols: Vec::new(), scope_stack: Vec::new(), + calls: Vec::new(), } } - fn current_scope(scope_stack: &Vec) -> String { - if scope_stack.is_empty() { + fn current_scope(&self) -> String { + if self.scope_stack.is_empty() { "global".to_string() } else { - scope_stack.join("::") + self.scope_stack.join("::") } } @@ -62,7 +64,7 @@ impl<'a> GolangTreeSitter<'a> { file: self.file_path.to_string(), line, column: name_node.start_position().column, - scope: Self::current_scope(&self.scope_stack), + scope: self.current_scope(), }); self.scope_stack.push(name); @@ -85,11 +87,26 @@ impl<'a> GolangTreeSitter<'a> { // get receiver type for scope let receiver_type = node .child_by_field_name("receiver") - .and_then(|r| r.child_by_field_name("type")) - .and_then(|t| t.utf8_text(code_bytes).ok()) - .unwrap_or("unknown") - .trim_start_matches('*') // remove pointer prefix - .to_string(); + .and_then(|r| { + // bind to variable so cursor lifetime is valid + let mut cursor = r.walk(); + let param = r + .children(&mut cursor) + .find(|c| c.kind() == "parameter_declaration"); + + param + .and_then(|p| p.child_by_field_name("type")) + .and_then(|t| { + if t.kind() == "pointer_type" { + t.child(1) + .and_then(|inner| inner.utf8_text(code_bytes).ok()) + } else { + t.utf8_text(code_bytes).ok() + } + }) + .map(|s| s.trim_start_matches('*').to_string()) + }) + .unwrap_or("unknown".to_string()); self.symbols.push(Symbol { name: name.clone(), @@ -127,7 +144,7 @@ impl<'a> GolangTreeSitter<'a> { file: self.file_path.to_string(), line: child.start_position().row + 1, column: child.start_position().column, - scope: Self::current_scope(&self.scope_stack), + scope: self.current_scope(), }); } } @@ -148,7 +165,7 @@ impl<'a> GolangTreeSitter<'a> { file: self.file_path.to_string(), line: name_node.start_position().row + 1, column: name_node.start_position().column, - scope: Self::current_scope(&self.scope_stack), + scope: self.current_scope(), }); } } @@ -161,10 +178,16 @@ impl<'a> GolangTreeSitter<'a> { for child in node.children(&mut cursor) { match child.kind() { // single import + // import import_1 "import_spec" => { self.collect_import_spec(child, code_bytes); } // grouped imports + /* + import( + import_1 + import_2 + ) */ "import_spec_list" => { let mut list_cursor = child.walk(); for spec in child.children(&mut list_cursor) { @@ -206,6 +229,75 @@ impl<'a> GolangTreeSitter<'a> { } } + "call_expression" => { + let function_node = node.child_by_field_name("function"); + let args_node = node.child_by_field_name("arguments"); + + // extract arguments + let arguments: Vec = args_node + .map(|args| { + let mut cursor = args.walk(); + args.children(&mut cursor) + // strip away ',' and parantheses! + .filter(|c| c.kind() != "," && c.kind() != "(" && c.kind() != ")") + .map(|c| c.utf8_text(code_bytes).unwrap_or("").to_string()) + .collect() + }) + .unwrap_or_default(); + + if let Some(func) = function_node { + match func.kind() { + // plain call: save(x) + "identifier" => { + let callee = func.utf8_text(code_bytes).unwrap_or("").to_string(); + self.calls.push(CallSite { + callee, + object: None, + arguments, + caller: self.current_scope(), + file: self.file_path.to_string(), + line: func.start_position().row + 1, + column: func.start_position().column, + }); + } + + // method call: db.Query(x) + "selector_expression" => { + let callee = func + .child_by_field_name("field") + .and_then(|f| f.utf8_text(code_bytes).ok()) + .unwrap_or("") + .to_string(); + + let object = func.child_by_field_name("operand").map(|o| { + // if operand is itself a selector (r.db), take the field + // if operand is plain identifier (db), take it directly + if o.kind() == "selector_expression" { + o.child_by_field_name("field") + .and_then(|f| f.utf8_text(code_bytes).ok()) + .unwrap_or("") + .to_string() + } else { + o.utf8_text(code_bytes).unwrap_or("").to_string() + } + }); + + self.calls.push(CallSite { + callee, + object, + arguments, + caller: self.current_scope(), + file: self.file_path.to_string(), + line: func.start_position().row + 1, + column: func.start_position().column, + }); + } + + _ => {} + } + } + } + // everything else — just walk children _ => { self.walk_children(node, code_bytes); @@ -232,7 +324,7 @@ impl<'a> GolangTreeSitter<'a> { file: self.file_path.to_string(), line: name_node.start_position().row + 1, column: name_node.start_position().column, - scope: Self::current_scope(&self.scope_stack), + scope: self.current_scope(), }); } } @@ -321,6 +413,7 @@ impl<'a> GolangTreeSitter<'a> { ScanResult { findings: self.findings, symbols: self.symbols, + calls: self.calls, } } diff --git a/src/server/detectors/javascript/js_scanner.rs b/src/server/detectors/javascript/js_scanner.rs index fb2b56a..902d3de 100644 --- a/src/server/detectors/javascript/js_scanner.rs +++ b/src/server/detectors/javascript/js_scanner.rs @@ -19,7 +19,8 @@ impl Scanner for JavaScriptScanner { eprintln!("Parse error: {}", e); return ScanResult{ findings:vec![], - symbols:vec![] + symbols:vec![], + calls:vec![] }; } }; diff --git a/src/server/detectors/shared/common_parser.rs b/src/server/detectors/shared/common_parser.rs index a2b7aac..280b2e4 100644 --- a/src/server/detectors/shared/common_parser.rs +++ b/src/server/detectors/shared/common_parser.rs @@ -1,5 +1,6 @@ use super::js_and_ts::utils::*; use crate::server::models::{ + calls::CallSite, findings::{Severity, VulnerabilityType}, results::ScanResult, symbols::{Symbol, SymbolKind}, @@ -16,20 +17,21 @@ pub struct CodeVisitor<'a> { pub file_path: &'a str, pub source_text: &'a str, pub symbols: Vec, + pub calls: Vec, scope_stack: Vec, - line_starts:Vec + line_starts: Vec, } impl<'a> CodeVisitor<'a> { pub fn new(file_path: &'a str, source_text: &'a str) -> Self { // feault start for all files - let mut line_starts=vec![0]; + let mut line_starts = vec![0]; // loop through entire file bytes for start - for (i,c) in source_text.char_indices(){ - if c=='\n'{ - line_starts.push(i+1); + for (i, c) in source_text.char_indices() { + if c == '\n' { + line_starts.push(i + 1); } } @@ -39,7 +41,8 @@ impl<'a> CodeVisitor<'a> { findings: vec![], symbols: vec![], scope_stack: vec![], - line_starts + calls: vec![], + line_starts, } } @@ -79,7 +82,7 @@ impl<'a> CodeVisitor<'a> { self.findings.push(Findings { vuln_type, line_no: line.to_string(), - col_no:col.to_string(), + col_no: col.to_string(), file_path: self.file_path.to_string(), snippet: snippet.to_string(), severity, @@ -94,6 +97,7 @@ impl<'a> CodeVisitor<'a> { ScanResult { findings: self.findings, symbols: self.symbols, + calls: self.calls, } } } @@ -182,32 +186,81 @@ impl<'a> Visit<'a> for CodeVisitor<'a> { // unwrap (eval as any)(...) to eval(...) do this FIRST, unconditionally let callee = unwrap_ts_expression(&node.callee); - if let Expression::Identifier(ident) = callee { - let name = ident.name.as_str(); + let arguments: Vec = node + .arguments + .iter() + .filter_map(|arg| arg.as_expression()) + .map(|expr| { + let start = expr.span().start as usize; + let end = expr.span().end as usize; + self.source_text[start..end].to_string() + }) + .collect(); + + let (line, col) = self.offset_to_line_col(node.span().start as usize); + + match callee { + /* + save(y) + call(a) + */ + Expression::Identifier(ident) => { + let name = ident.name.as_str(); - if is_dangerous_call(name) { - self.report(name, node.span(), VulnerabilityType::Eval, Severity::High); - } + if is_dangerous_call(name) { + self.report(name, node.span(), VulnerabilityType::Eval, Severity::High); + } - // CommonJS imports via require() - // e.g. module.exports = require('./lib/express'); - // var debug = require('debug'); - if name == "require" { - if let Some(first_arg) = node.arguments.first() { - if let Some(expr) = first_arg.as_expression() { - if let Expression::StringLiteral(lit) = expr { - self.symbols.push(Symbol { - name: lit.value.to_string(), - kind: SymbolKind::Import, - file: self.file_path.to_string(), - line: self.span_to_line(node.span.start as usize), - column: node.span.start as usize, - scope: self.current_scope(), - }); + // CommonJS imports via require() + // e.g. module.exports = require('./lib/express'); + // var debug = require('debug'); + if name == "require" { + if let Some(first_arg) = node.arguments.first() { + if let Some(expr) = first_arg.as_expression() { + if let Expression::StringLiteral(lit) = expr { + self.symbols.push(Symbol { + name: lit.value.to_string(), + kind: SymbolKind::Import, + file: self.file_path.to_string(), + line: self.span_to_line(node.span.start as usize), + column: node.span.start as usize, + scope: self.current_scope(), + }); + } } } } + + self.calls.push(CallSite { + callee: name.to_string(), + object: None, + arguments, + caller: self.current_scope(), + file: self.file_path.to_string(), + line, + column: col, + }); + } + + // method call: db.query(x), fs.readFile(x) + Expression::StaticMemberExpression(member) => { + let callee_name = member.property.name.to_string(); + let object_name = match &member.object { + Expression::Identifier(id) => Some(id.name.to_string()), + _ => None, + }; + + self.calls.push(CallSite { + callee: callee_name, + object: object_name, + arguments, + caller: self.current_scope(), + file: self.file_path.to_string(), + line, + column: col, + }); } + _ => {} } // sql injection check diff --git a/src/server/detectors/typescript/ts_scanner.rs b/src/server/detectors/typescript/ts_scanner.rs index ef23315..ec8a8f7 100644 --- a/src/server/detectors/typescript/ts_scanner.rs +++ b/src/server/detectors/typescript/ts_scanner.rs @@ -21,6 +21,7 @@ impl Scanner for TypeScriptScanner { return ScanResult { findings: vec![], symbols: vec![], + calls:vec![] }; } }; diff --git a/src/server/models/calls.rs b/src/server/models/calls.rs new file mode 100644 index 0000000..66e1787 --- /dev/null +++ b/src/server/models/calls.rs @@ -0,0 +1,34 @@ +use std::collections::HashMap; + +pub struct CallSite { + pub callee: String, // function name + pub object: Option, // e.g. db.query, db is object + pub arguments: Vec, + pub caller: String, // not to be confused with calle + pub file: String, + pub line: usize, + pub column: usize, +} + +pub struct CallTable { + pub calls: HashMap>, +} + +impl CallTable { + pub fn new() -> Self { + Self { + calls: HashMap::new(), + } + } + + pub fn insert(&mut self, file: String, call: CallSite) { + self.calls + .entry(file) + .or_insert_with(Vec::new) + .push(call); + } + + pub fn total(&self) -> usize { + self.calls.values().map(|v| v.len()).sum() + } +} \ No newline at end of file diff --git a/src/server/models/mod.rs b/src/server/models/mod.rs index 80268da..065dc41 100644 --- a/src/server/models/mod.rs +++ b/src/server/models/mod.rs @@ -1,3 +1,4 @@ pub mod findings; pub mod results; -pub mod symbols; \ No newline at end of file +pub mod symbols; +pub mod calls; \ No newline at end of file diff --git a/src/server/models/results.rs b/src/server/models/results.rs index 42f916f..8ae85fd 100644 --- a/src/server/models/results.rs +++ b/src/server/models/results.rs @@ -1,7 +1,9 @@ use super::findings::Findings; use super::symbols::Symbol; +use super::calls::CallSite; pub struct ScanResult { pub findings: Vec, pub symbols: Vec, + pub calls :Vec } \ No newline at end of file diff --git a/src/server/service.rs b/src/server/service.rs index 7d813bc..7e5c90e 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -28,9 +28,10 @@ impl OWASPScanner { } pub fn scan(&self, codebase: &str, file_path: &str) -> ScanResult { - let dummy=ScanResult{ - findings:Vec::new(), - symbols:Vec::new() + let dummy = ScanResult { + findings: Vec::new(), + symbols: Vec::new(), + calls: Vec::new(), }; let language = match Self::determine_language(file_path) { Some(lang) => lang, diff --git a/src/state.rs b/src/state.rs index 4388ae0..ae56cad 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,10 +1,11 @@ -use crate::server::models::{symbols::SymbolTable,findings::FinalFindings}; +use crate::server::models::{symbols::SymbolTable,findings::FinalFindings,calls::CallTable}; use std::collections::HashMap; pub struct ScanData { pub findings: Vec, pub symbol_table: SymbolTable, + pub call_table: CallTable, } pub struct AppState { // HashMap,SymbolTable]>> From cfd55d4074f1f581daa8817c6f8bdeace4b07b72 Mon Sep 17 00:00:00 2001 From: DeeTomPanda Date: Mon, 6 Jul 2026 11:10:11 +0530 Subject: [PATCH 5/6] chore:update readme --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ddf3607..6867030 100644 --- a/README.md +++ b/README.md @@ -75,12 +75,18 @@ Guard can export scan results in **SARIF format**, which is compatible with GitH --- +### Generate quick analysis + +```bash +guard analyze path/to/(file or dir) +``` ### Generate SARIF report ```bash -guard scan path/to/file.js --sarif +guard scan path/to/(file or dir) --sarif ``` + ### Note This will: @@ -131,6 +137,8 @@ flutter build web --base-href /app/ ## Planned +- [ ] Resolution of Symbol and Calls +- [ ] Call Graph (cross file referencing) - [ ] Taint Analysis - [ ] Python support - [ ] Java support @@ -141,3 +149,4 @@ flutter build web --base-href /app/ - [x] TypeScript support - [x] Go support - [x] SARIF output format (GitHub Code Scanning compatible) +- [x] SymbolTable and CallTable extraction From c4f539f735f491f575e8cf9177a33130a0312b66 Mon Sep 17 00:00:00 2001 From: DeeTomPanda Date: Mon, 6 Jul 2026 11:16:24 +0530 Subject: [PATCH 6/6] chore: linter lint --- src/server/detectors/golang/go_scanner.rs | 2 +- src/server/detectors/golang/tests.rs | 18 ++++--- src/server/detectors/javascript/js_scanner.rs | 10 ++-- src/server/detectors/javascript/tests.rs | 54 ++++++++++++------- src/server/detectors/mod.rs | 2 +- src/server/detectors/typescript/tests.rs | 12 +++-- src/server/detectors/typescript/ts_scanner.rs | 2 +- src/server/models/calls.rs | 17 +++--- src/server/models/findings.rs | 2 +- src/server/models/mod.rs | 2 +- src/server/models/results.rs | 6 +-- src/server/models/symbols.rs | 2 +- src/state.rs | 9 ++-- 13 files changed, 81 insertions(+), 57 deletions(-) diff --git a/src/server/detectors/golang/go_scanner.rs b/src/server/detectors/golang/go_scanner.rs index ea83477..edc1cbc 100644 --- a/src/server/detectors/golang/go_scanner.rs +++ b/src/server/detectors/golang/go_scanner.rs @@ -238,7 +238,7 @@ impl<'a> GolangTreeSitter<'a> { .map(|args| { let mut cursor = args.walk(); args.children(&mut cursor) - // strip away ',' and parantheses! + // strip away ',' and parantheses! .filter(|c| c.kind() != "," && c.kind() != "(" && c.kind() != ")") .map(|c| c.utf8_text(code_bytes).unwrap_or("").to_string()) .collect() diff --git a/src/server/detectors/golang/tests.rs b/src/server/detectors/golang/tests.rs index 36cfdb0..d9765f5 100644 --- a/src/server/detectors/golang/tests.rs +++ b/src/server/detectors/golang/tests.rs @@ -24,7 +24,8 @@ mod tests { "#; let scan_result = SCANNER.scan(code, "test.go"); - let ops: Vec<_> = scan_result.findings + let ops: Vec<_> = scan_result + .findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::UnsafeFileOperation) .collect(); @@ -43,7 +44,8 @@ mod tests { "#; let scan_result = SCANNER.scan(code, "test.go"); - let secrets: Vec<_> = scan_result.findings + let secrets: Vec<_> = scan_result + .findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::HardcodedSecret) .collect(); @@ -68,7 +70,8 @@ mod tests { let scan_result = SCANNER.scan(code, "test.go"); - assert!(scan_result.findings + assert!(scan_result + .findings .iter() .any(|f| f.vuln_type == VulnerabilityType::SQLInjection)); } @@ -98,7 +101,8 @@ mod tests { let scan_result = SCANNER.scan(code, "test.go"); - assert!(scan_result.findings + assert!(scan_result + .findings .iter() .any(|f| f.vuln_type == VulnerabilityType::SQLInjection)); } @@ -126,7 +130,8 @@ mod tests { let scan_result = SCANNER.scan(code, "test.go"); - assert!(scan_result.findings + assert!(scan_result + .findings .iter() .any(|f| f.vuln_type == VulnerabilityType::SQLInjection)); } @@ -145,7 +150,8 @@ mod tests { "#; let scan_result = SCANNER.scan(code, "test.go"); - assert!(scan_result.findings + assert!(scan_result + .findings .iter() .any(|f| f.vuln_type == VulnerabilityType::UnsafeCodeExecution)); } diff --git a/src/server/detectors/javascript/js_scanner.rs b/src/server/detectors/javascript/js_scanner.rs index 902d3de..5e92279 100644 --- a/src/server/detectors/javascript/js_scanner.rs +++ b/src/server/detectors/javascript/js_scanner.rs @@ -17,15 +17,15 @@ impl Scanner for JavaScriptScanner { Ok(program) => program, Err(e) => { eprintln!("Parse error: {}", e); - return ScanResult{ - findings:vec![], - symbols:vec![], - calls:vec![] + return ScanResult { + findings: vec![], + symbols: vec![], + calls: vec![], }; } }; - let mut visitor = CodeVisitor::new(file_path,code); + let mut visitor = CodeVisitor::new(file_path, code); visitor.visit_program(&ast); visitor.into_scan_result() diff --git a/src/server/detectors/javascript/tests.rs b/src/server/detectors/javascript/tests.rs index 51ea296..019100f 100644 --- a/src/server/detectors/javascript/tests.rs +++ b/src/server/detectors/javascript/tests.rs @@ -21,7 +21,8 @@ mod tests { } "#; let scan_result = SCANNER.scan(code, "test.js"); - let secrets: Vec<_> = scan_result.findings + let secrets: Vec<_> = scan_result + .findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::HardcodedSecret) .collect(); @@ -37,7 +38,8 @@ mod tests { const token = generateToken(); "#; let scan_result = SCANNER.scan(code, "test.js"); - let secrets: Vec<_> = scan_result.findings + let secrets: Vec<_> = scan_result + .findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::HardcodedSecret) .collect(); @@ -52,7 +54,8 @@ mod tests { const token = `Bearer ${fetchToken()}`; "#; let scan_result = SCANNER.scan(code, "test.js"); - let secrets: Vec<_> = scan_result.findings + let secrets: Vec<_> = scan_result + .findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::HardcodedSecret) .collect(); @@ -68,7 +71,8 @@ mod tests { const greeting = "hello world"; "#; let scan_result = SCANNER.scan(code, "test.js"); - let secrets: Vec<_> = scan_result.findings + let secrets: Vec<_> = scan_result + .findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::HardcodedSecret) .collect(); @@ -82,7 +86,8 @@ mod tests { const config = { password: "secret" } "#; let scan_result = SCANNER.scan(code, "test.js"); - let secrets: Vec<_> = scan_result.findings + let secrets: Vec<_> = scan_result + .findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::HardcodedSecret) .collect(); @@ -99,7 +104,8 @@ mod tests { eval("alert(1)"); "#; let scan_result = SCANNER.scan(code, "test.js"); - let evals: Vec<_> = scan_result.findings + let evals: Vec<_> = scan_result + .findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::Eval) .collect(); @@ -113,7 +119,8 @@ mod tests { const fn = new Function("return 1"); "#; let scan_result = SCANNER.scan(code, "test.js"); - let evals: Vec<_> = scan_result.findings + let evals: Vec<_> = scan_result + .findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::Eval) .collect(); @@ -128,7 +135,8 @@ mod tests { setInterval("doSomethingElse()", 500); "#; let scan_result = SCANNER.scan(code, "test.js"); - let evals: Vec<_> = scan_result.findings + let evals: Vec<_> = scan_result + .findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::Eval) .collect(); @@ -143,7 +151,8 @@ mod tests { parseInt("42"); "#; let scan_result = SCANNER.scan(code, "test.js"); - let evals: Vec<_> = scan_result.findings + let evals: Vec<_> = scan_result + .findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::Eval) .collect(); @@ -160,7 +169,8 @@ mod tests { db.query(`SELECT * FROM ${table}`); "#; let scan_result = SCANNER.scan(code, "test.js"); - let sql: Vec<_> = scan_result.findings + let sql: Vec<_> = scan_result + .findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::SQLInjection) .collect(); @@ -173,7 +183,8 @@ mod tests { db.query("SELECT * FROM " + table + " WHERE id = " + id); "#; let scan_result = SCANNER.scan(code, "test.js"); - let sql: Vec<_> = scan_result.findings + let sql: Vec<_> = scan_result + .findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::SQLInjection) .collect(); @@ -187,7 +198,8 @@ mod tests { db.query("SELECT * FROM users WHERE id = 1"); "#; let scan_result = SCANNER.scan(code, "test.js"); - let sql: Vec<_> = scan_result.findings + let sql: Vec<_> = scan_result + .findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::SQLInjection) .collect(); @@ -201,7 +213,8 @@ mod tests { db.query(`${"SELECT * FROM " + table}`); "#; let scan_result = SCANNER.scan(code, "test.js"); - let sql: Vec<_> = scan_result.findings + let sql: Vec<_> = scan_result + .findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::SQLInjection) .collect(); @@ -216,7 +229,8 @@ mod tests { db.query("SELECT * FROM users WHERE id = $1", [id]); "#; let scan_result = SCANNER.scan(code, "test.js"); - let sql: Vec<_> = scan_result.findings + let sql: Vec<_> = scan_result + .findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::SQLInjection) .collect(); @@ -231,7 +245,8 @@ mod tests { console.log(`User ${id} logged in`); "#; let scan_result = SCANNER.scan(code, "test.js"); - let sql: Vec<_> = scan_result.findings + let sql: Vec<_> = scan_result + .findings .iter() .filter(|f| f.vuln_type == VulnerabilityType::SQLInjection) .collect(); @@ -250,13 +265,16 @@ mod tests { db.query(`SELECT * FROM ${table}`); "#; let scan_result = SCANNER.scan(code, "test.js"); - assert!(scan_result.findings + assert!(scan_result + .findings .iter() .any(|f| f.vuln_type == VulnerabilityType::HardcodedSecret)); - assert!(scan_result.findings + assert!(scan_result + .findings .iter() .any(|f| f.vuln_type == VulnerabilityType::Eval)); - assert!(scan_result.findings + assert!(scan_result + .findings .iter() .any(|f| f.vuln_type == VulnerabilityType::SQLInjection)); } diff --git a/src/server/detectors/mod.rs b/src/server/detectors/mod.rs index abba18a..ad957ef 100644 --- a/src/server/detectors/mod.rs +++ b/src/server/detectors/mod.rs @@ -2,10 +2,10 @@ mod golang; mod javascript; pub mod shared; mod typescript; +use crate::server::models::results::ScanResult; pub use golang::go_scanner::GolangScanner; pub use javascript::js_scanner::JavaScriptScanner; pub use typescript::ts_scanner::TypeScriptScanner; -use crate::server::models::results::ScanResult; pub trait Scanner: Send + Sync { fn scan(&self, code: &str, file_path: &str) -> ScanResult; diff --git a/src/server/detectors/typescript/tests.rs b/src/server/detectors/typescript/tests.rs index 4633772..42496fa 100644 --- a/src/server/detectors/typescript/tests.rs +++ b/src/server/detectors/typescript/tests.rs @@ -10,7 +10,8 @@ mod test { fn detects_eval_wrapped_in_as_any() { let code = r#"(eval as any)("alert(1)");"#; let scan_result = SCANNER.scan(code, "test.ts"); - assert!(scan_result.findings + assert!(scan_result + .findings .iter() .any(|f| f.vuln_type == VulnerabilityType::Eval)); } @@ -23,7 +24,8 @@ mod test { } satisfies Config; "#; let scan_result = SCANNER.scan(code, "test.ts"); - assert!(scan_result.findings + assert!(scan_result + .findings .iter() .any(|f| f.vuln_type == VulnerabilityType::HardcodedSecret)); } @@ -32,7 +34,8 @@ mod test { fn detects_as_any_usage() { let code = r#"const x = userInput as any;"#; let scan_result = SCANNER.scan(code, "test.ts"); - assert!(scan_result.findings + assert!(scan_result + .findings .iter() .any(|f| f.vuln_type == VulnerabilityType::UnsafeTypeAssertion)); } @@ -41,7 +44,8 @@ mod test { fn detects_eval_nested_in_call_arguments() { let code = r#"db.query(eval("SELECT * FROM " + table));"#; let scan_result = SCANNER.scan(code, "test.ts"); - assert!(scan_result.findings + assert!(scan_result + .findings .iter() .any(|f| f.vuln_type == VulnerabilityType::Eval)); } diff --git a/src/server/detectors/typescript/ts_scanner.rs b/src/server/detectors/typescript/ts_scanner.rs index ec8a8f7..568e2f1 100644 --- a/src/server/detectors/typescript/ts_scanner.rs +++ b/src/server/detectors/typescript/ts_scanner.rs @@ -21,7 +21,7 @@ impl Scanner for TypeScriptScanner { return ScanResult { findings: vec![], symbols: vec![], - calls:vec![] + calls: vec![], }; } }; diff --git a/src/server/models/calls.rs b/src/server/models/calls.rs index 66e1787..3694da3 100644 --- a/src/server/models/calls.rs +++ b/src/server/models/calls.rs @@ -1,17 +1,17 @@ use std::collections::HashMap; pub struct CallSite { - pub callee: String, // function name - pub object: Option, // e.g. db.query, db is object - pub arguments: Vec, - pub caller: String, // not to be confused with calle + pub callee: String, // function name + pub object: Option, // e.g. db.query, db is object + pub arguments: Vec, + pub caller: String, // not to be confused with calle pub file: String, pub line: usize, pub column: usize, } pub struct CallTable { - pub calls: HashMap>, + pub calls: HashMap>, } impl CallTable { @@ -22,13 +22,10 @@ impl CallTable { } pub fn insert(&mut self, file: String, call: CallSite) { - self.calls - .entry(file) - .or_insert_with(Vec::new) - .push(call); + self.calls.entry(file).or_insert_with(Vec::new).push(call); } pub fn total(&self) -> usize { self.calls.values().map(|v| v.len()).sum() } -} \ No newline at end of file +} diff --git a/src/server/models/findings.rs b/src/server/models/findings.rs index be7cab4..e3d58ea 100644 --- a/src/server/models/findings.rs +++ b/src/server/models/findings.rs @@ -116,7 +116,7 @@ impl Debug for Severity { pub struct Findings { pub vuln_type: VulnerabilityType, pub line_no: String, - pub col_no:String, + pub col_no: String, pub file_path: String, pub snippet: String, pub severity: Severity, diff --git a/src/server/models/mod.rs b/src/server/models/mod.rs index 065dc41..122c4a1 100644 --- a/src/server/models/mod.rs +++ b/src/server/models/mod.rs @@ -1,4 +1,4 @@ +pub mod calls; pub mod findings; pub mod results; pub mod symbols; -pub mod calls; \ No newline at end of file diff --git a/src/server/models/results.rs b/src/server/models/results.rs index 8ae85fd..a10ffe1 100644 --- a/src/server/models/results.rs +++ b/src/server/models/results.rs @@ -1,9 +1,9 @@ +use super::calls::CallSite; use super::findings::Findings; use super::symbols::Symbol; -use super::calls::CallSite; pub struct ScanResult { pub findings: Vec, pub symbols: Vec, - pub calls :Vec -} \ No newline at end of file + pub calls: Vec, +} diff --git a/src/server/models/symbols.rs b/src/server/models/symbols.rs index d744380..1bdf3c4 100644 --- a/src/server/models/symbols.rs +++ b/src/server/models/symbols.rs @@ -21,4 +21,4 @@ pub struct Symbol { pub struct SymbolTable { pub symbols: HashMap>, -} \ No newline at end of file +} diff --git a/src/state.rs b/src/state.rs index ae56cad..f642f59 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,21 +1,20 @@ -use crate::server::models::{symbols::SymbolTable,findings::FinalFindings,calls::CallTable}; +use crate::server::models::{calls::CallTable, findings::FinalFindings, symbols::SymbolTable}; use std::collections::HashMap; - pub struct ScanData { pub findings: Vec, pub symbol_table: SymbolTable, - pub call_table: CallTable, + pub call_table: CallTable, } pub struct AppState { // HashMap,SymbolTable]>> - pub results: HashMap, + pub results: HashMap, } impl AppState { pub fn new() -> Self { Self { - results: HashMap::new() + results: HashMap::new(), } } }