use gossan_core::Target;
use secfinding::{Finding, FindingKind, Severity};
const SOURCE_SIGNALS: &[&str] = &[
".git",
".env",
"source map",
"sourcemap",
"swagger",
"openapi",
"directory listing",
"backup file",
"debug",
"profiler",
"phpinfo",
"actuator",
];
const SECRET_SIGNALS: &[&str] = &[
"secret",
"api key",
"access key",
"private key",
"token",
"credential",
"password",
"aws",
"stripe",
"github pat",
"jwt",
];
pub struct SourceCodeSecretsRule;
impl super::super::CorrelationRule for SourceCodeSecretsRule {
fn name(&self) -> &'static str {
"source_code_secrets"
}
fn check(&self, findings: &[Finding], _targets: &[Target]) -> Vec<Finding> {
let source_exposures: Vec<&Finding> = findings
.iter()
.filter(|f| {
let lower = f.title().to_lowercase();
SOURCE_SIGNALS.iter().any(|sig| lower.contains(sig))
})
.collect();
let secret_findings: Vec<&Finding> = findings
.iter()
.filter(|f| {
let lower = f.title().to_lowercase();
SECRET_SIGNALS.iter().any(|sig| lower.contains(sig))
})
.collect();
if source_exposures.is_empty() || secret_findings.is_empty() {
return vec![];
}
let source_types: Vec<String> = source_exposures
.iter()
.map(|f| f.title().to_string())
.take(3)
.collect();
let secret_types: Vec<String> = secret_findings
.iter()
.map(|f| f.title().to_string())
.take(3)
.collect();
let chain = Finding::builder(
"correlation",
source_exposures
.first()
.map(|f| f.target())
.unwrap_or("unknown"),
Severity::Critical,
)
.title("Source Code Exposure → Credential Extraction Chain")
.detail(format!(
"Source code is exposed ({}) and contains hardcoded secrets ({}). \
An attacker can follow this chain: discover exposed source → \
extract credentials → authenticate as the application. \
This is a direct path to compromise. \
Fix: remove source code from production AND rotate all exposed credentials.",
source_types.join(", "),
secret_types.join(", "),
))
.kind(FindingKind::Vulnerability)
.tag("chain")
.tag("source-exposure")
.tag("credential-leak")
.build_or_log();
chain.into_iter().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CorrelationRule;
fn finding(scanner: &str, target: &str, title: &str) -> Finding {
Finding::builder(scanner, target, Severity::High)
.title(title)
.build()
.expect("test finding")
}
#[test]
fn fires_when_source_and_secrets_present() {
let rule = SourceCodeSecretsRule;
let findings = vec![
finding("hidden", "example.com", ".git/config exposed"),
finding("js", "example.com", "AWS Access Key in JavaScript"),
];
let chains = rule.check(&findings, &[]);
assert_eq!(chains.len(), 1);
assert!(chains[0].title().contains("Source Code Exposure"));
}
#[test]
fn does_not_fire_with_only_source_exposure() {
let rule = SourceCodeSecretsRule;
let findings = vec![finding("hidden", "example.com", ".git/config exposed")];
assert!(rule.check(&findings, &[]).is_empty());
}
#[test]
fn does_not_fire_with_only_secrets() {
let rule = SourceCodeSecretsRule;
let findings = vec![finding("js", "example.com", "AWS Access Key in JavaScript")];
assert!(rule.check(&findings, &[]).is_empty());
}
}