bastion_toolkit/
scanner.rs1use anyhow::Result;
6use colored::*;
7use regex::Regex;
8use std::fs;
9use std::path::Path;
10use std::process::Command;
11use walkdir::WalkDir;
12
13use crate::common::{self, ProjectType};
14use crate::python_check;
15
16pub fn run_scan() -> Result<()> {
18 println!("{}", "=== BASTION SECURITY CHECK START ===".bold().cyan());
19
20 let project_type = common::detect_project_type();
21
22 match project_type {
23 ProjectType::Rust => {
24 println!("{}", "[+] Rust Project Detected".green());
25 run_rust_checks()?;
26 }
27 ProjectType::Python => {
28 println!("{}", "[+] Python Project Detected".green());
29 run_python_checks()?;
30 if Path::new("requirements.txt").exists() {
31 python_check::check_secure_requirements("requirements.txt")?;
32 }
33 }
34 ProjectType::Unknown => {
35 println!("{}", "[!] Generic Project / Unknown Language".yellow());
36 }
37 }
38
39 println!("{}", "\n[+] Starting Secret Scan...".yellow());
40 scan_for_secrets(".")?;
41
42 println!("{}", "\n=== CHECK FINISHED ===".bold().cyan());
43 Ok(())
44}
45
46fn run_rust_checks() -> Result<()> {
47 println!("Running cargo audit...");
48 if Command::new("cargo").args(["audit"]).status().is_err() {
49 println!("{}", "Warning: 'cargo-audit' not found. Skip.".red());
50 }
51
52 println!("Running cargo clippy...");
53 Command::new("cargo").args(["clippy", "--", "-D", "warnings"]).status()?;
54 Ok(())
55}
56
57fn run_python_checks() -> Result<()> {
58 println!("Running pip-audit...");
59 if Command::new("pip-audit").status().is_err() {
60 println!("{}", "Warning: 'pip-audit' not found. Skip.".red());
61 }
62
63 println!("Running bandit...");
64 if Command::new("bandit").args(["-r", "."]).status().is_err() {
65 println!("{}", "Warning: 'bandit' not found. Skip.".red());
66 }
67 Ok(())
68}
69
70fn scan_for_secrets(dir: &str) -> Result<()> {
71 let re = Regex::new(
73 r#"(?i)\b(api_key|password|secret|token|private_key|access_key|auth_token)\b\s*[:=]\s*['""]([a-zA-Z0-9_\-]{12,})['""]"#,
74 ).unwrap();
75
76 let walker = WalkDir::new(dir).into_iter();
77
78 for entry in walker.filter_entry(|e| !common::is_ignored_path(e.path())) {
79 let entry = entry?;
80 if entry.file_type().is_file() {
81 let path = entry.path();
82 if is_scannable_file(path) {
83 check_file_content(path, &re)?;
84 }
85 }
86 }
87 Ok(())
88}
89
90fn is_scannable_file(path: &Path) -> bool {
91 path.extension()
92 .and_then(|s| s.to_str())
93 .map(|ext| matches!(ext, "rs" | "py" | "js" | "ts" | "env" | "json" | "toml" | "yaml" | "yml" | "md"))
94 .unwrap_or(false)
95}
96
97fn check_file_content(path: &Path, re: &Regex) -> Result<()> {
98 if let Ok(content) = fs::read_to_string(path) {
99 for (i, line) in content.lines().enumerate() {
100 if re.is_match(line) {
101 println!(
102 "{} Found potential secret in {:?}:{} -> {}",
103 "[ALERT]".red().bold(),
104 path,
105 i + 1,
106 line.trim()
107 );
108 }
109 }
110 }
111 Ok(())
112}