use crate::core::{Finding, Severity};
use crate::plugins::traits::{PluginError, PluginReport, ScanContext, ScanPhase, SecurityPlugin};
use async_trait::async_trait;
use lazy_static::lazy_static;
use regex::Regex;
use std::time::Instant;
lazy_static! {
static ref SECRET_PATTERNS: Vec<(Regex, &'static str, Severity)> = vec![
(
Regex::new(r"(?i)AKIA[0-9A-Z]{16}").unwrap(),
"AWS_Access_Key_ID",
Severity::Critical
),
(
Regex::new(r"(?i)aws[_-]?secret[_-]?access[_-]?key").unwrap(),
"AWS_Secret_Key",
Severity::Critical
),
(
Regex::new(r"(?i)github[_-]?token").unwrap(),
"GitHub_Token",
Severity::High
),
(
Regex::new(r"ghp_[0-9a-zA-Z]{36}").unwrap(),
"GitHub_Personal_Access_Token",
Severity::Critical
),
(
Regex::new(r"gho_[0-9a-zA-Z]{36}").unwrap(),
"GitHub_OAuth_Token",
Severity::Critical
),
(
Regex::new(r"(?i)slack[_-]?token").unwrap(),
"Slack_Token",
Severity::High
),
(
Regex::new(r"xox[baprs]-[0-9a-zA-Z-]+").unwrap(),
"Slack_API_Token",
Severity::Critical
),
(
Regex::new(r"(?i)private[_-]?key").unwrap(),
"Private_Key_Reference",
Severity::High
),
(
Regex::new(r"-----BEGIN (RSA |EC )?PRIVATE KEY-----").unwrap(),
"Private_Key",
Severity::Critical
),
(
Regex::new(r"sk-[0-9a-zA-Z]{48}").unwrap(),
"OpenAI_API_Key",
Severity::Critical
),
(
Regex::new(r"AIza[0-9A-Za-z-_]{35}").unwrap(),
"Google_API_Key",
Severity::Critical
),
(
Regex::new(r"(?i)db[_-]?password").unwrap(),
"Database_Password",
Severity::High
),
];
}
pub struct SecretsScanner;
impl Default for SecretsScanner {
fn default() -> Self {
Self::new()
}
}
impl SecretsScanner {
pub fn new() -> Self {
Self
}
}
#[async_trait]
impl SecurityPlugin for SecretsScanner {
fn name(&self) -> &str {
"secrets"
}
fn version(&self) -> &str {
"0.1.0"
}
fn description(&self) -> &str {
"Detect_exposed_credentials_and_API_keys"
}
fn scan_phase(&self) -> ScanPhase {
ScanPhase::All
}
async fn initialize(&mut self) -> Result<(), PluginError> {
Ok(())
}
async fn scan(&self, context: &ScanContext<'_>) -> Result<PluginReport, PluginError> {
let start = Instant::now();
let mut report = PluginReport::new(self.name().to_string());
if let Some(content) = context.file_content {
let content_str = String::from_utf8_lossy(content);
for (line_num, line) in content_str.lines().enumerate() {
for (pattern, description, severity) in SECRET_PATTERNS.iter() {
if pattern.is_match(line) {
let redacted_line = redact_secrets(line);
let finding = Finding::new(
format!("SEC-{:03}", line_num),
format!("Found: {}", description.replace('_', " ")),
*severity,
)
.with_file(context.path.to_path_buf())
.with_line((line_num + 1) as u32)
.with_evidence(redacted_line)
.with_description(format!(
"Potential {} found in source code. This should be removed and rotated.",
description.replace('_', " ")
));
report.findings.push(finding);
}
}
}
report.scanned_files = 1;
}
report.duration_ms = start.elapsed().as_millis() as u64;
Ok(report)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plugins::traits::ScanContext;
#[tokio::test]
async fn test_aws_key_detection() {
let scanner = SecretsScanner::new();
let content = b"AKIAIOSFODNN7EXAMPLE";
let context = ScanContext {
path: std::path::Path::new("test.txt"),
scan_phase: ScanPhase::PostExtract,
file_content: Some(content),
metadata: std::collections::HashMap::new(),
};
let report = scanner.scan(&context).await.unwrap();
assert!(!report.findings.is_empty());
}
#[tokio::test]
async fn test_github_pat_detection() {
let scanner = SecretsScanner::new();
let content = b"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij";
let context = ScanContext {
path: std::path::Path::new("test.txt"),
scan_phase: ScanPhase::PostExtract,
file_content: Some(content),
metadata: std::collections::HashMap::new(),
};
let report = scanner.scan(&context).await.unwrap();
assert!(!report.findings.is_empty());
}
#[tokio::test]
async fn test_private_key_detection() {
let scanner = SecretsScanner::new();
let content = b"-----BEGIN RSA PRIVATE KEY-----";
let context = ScanContext {
path: std::path::Path::new("test.txt"),
scan_phase: ScanPhase::PostExtract,
file_content: Some(content),
metadata: std::collections::HashMap::new(),
};
let report = scanner.scan(&context).await.unwrap();
assert!(!report.findings.is_empty());
}
#[tokio::test]
async fn test_clean_file() {
let scanner = SecretsScanner::new();
let content = b"fn main() { println!(\"hello\"); }";
let context = ScanContext {
path: std::path::Path::new("test.txt"),
scan_phase: ScanPhase::PostExtract,
file_content: Some(content),
metadata: std::collections::HashMap::new(),
};
let report = scanner.scan(&context).await.unwrap();
assert_eq!(report.findings.len(), 0);
}
}
fn redact_secrets(line: &str) -> String {
let mut redacted = line.to_string();
let redact_pattern = Regex::new(r"[A-Za-z0-9+/=]{20,}").unwrap();
redacted = redact_pattern
.replace_all(&redacted, "REDACTED")
.to_string();
redacted
}