Skip to main content

bastion_toolkit/
scanner.rs

1//! # Scanner - 脆弱性スキャンモジュール
2//!
3//! プロジェクトの脆弱性スキャン・シークレット検出を行う。
4
5use 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
16/// メインのスキャン処理を実行する
17pub 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    // 改善されたシークレット検出用正規表現(誤検知を減らすために境界を意識)
72    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}