pub mod confidence;
pub mod dedup;
pub mod relationship;
mod rules;
mod scope;
mod utils;
use gossan_core::Target;
#[allow(unused_imports)] use secfinding::{Finding, Severity};
pub use rules::{
AdminExposedRule, ApiAuthRule, CorsSecretChainRule, DebugRceRule, ShadowInfrastructureRule,
SourceCodeSecretsRule, SsrfInternalRule, TlsWeaknessRule, WildcardTakeoverRule,
};
pub trait CorrelationRule: Send + Sync {
fn name(&self) -> &'static str;
fn check(&self, findings: &[Finding], targets: &[Target]) -> Vec<Finding>;
}
pub struct CorrelationEngine {
rules: Vec<Box<dyn CorrelationRule>>,
}
impl CorrelationEngine {
pub fn new() -> Self {
Self {
rules: vec![
Box::new(TlsWeaknessRule),
Box::new(AdminExposedRule),
Box::new(ApiAuthRule),
Box::new(SsrfInternalRule),
Box::new(SourceCodeSecretsRule),
Box::new(ShadowInfrastructureRule),
Box::new(WildcardTakeoverRule),
Box::new(DebugRceRule),
Box::new(CorsSecretChainRule),
],
}
}
pub fn run(&self, findings: &[Finding], targets: &[Target]) -> Vec<Finding> {
let mut chains = Vec::new();
for rule in &self.rules {
let new = rule.check(findings, targets);
if !new.is_empty() {
tracing::info!(
rule = rule.name(),
count = new.len(),
"correlation rule fired"
);
}
chains.extend(new);
}
chains
}
}
impl Default for CorrelationEngine {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn finding(scanner: &str, target: &str, title: &str) -> Finding {
Finding::builder(scanner, target, Severity::High)
.title(title)
.build()
.expect("finding builder: required fields are set")
}
fn finding_with_tag(scanner: &str, target: &str, title: &str, tag: &str) -> Finding {
Finding::builder(scanner, target, Severity::High)
.title(title)
.tag(tag)
.build()
.expect("finding builder: required fields are set")
}
#[test]
fn engine_empty_input_produces_no_chains() {
let engine = CorrelationEngine::new();
assert!(engine.run(&[], &[]).is_empty());
}
#[test]
fn correlation_engine_runs_all_rules() {
let engine = CorrelationEngine::new();
assert_eq!(engine.rules.len(), 9, "all 9 rules should be registered");
}
#[test]
fn engine_returns_tls_chain_when_multiple_tls_issues_exist() {
let engine = CorrelationEngine::new();
let findings = vec![
finding("portscan", "example.com", "Self-signed TLS certificate"),
finding("hidden", "example.com", "Missing HSTS header"),
];
let chains = engine.run(&findings, &[]);
assert!(chains
.iter()
.any(|f| f.title().contains("Multiple TLS weaknesses")));
}
#[test]
fn engine_returns_admin_chain_when_admin_and_auth_findings_align() {
let engine = CorrelationEngine::new();
let findings = vec![
finding("hidden", "admin.example.com", "Admin panel exposed"),
finding("hidden", "admin.example.com", "No authentication required"),
];
let chains = engine.run(&findings, &[]);
assert!(chains.iter().any(|f| f
.title()
.contains("Admin panel exposed without authentication")));
}
#[test]
fn engine_returns_api_auth_chain() {
let engine = CorrelationEngine::new();
let v1 = finding_with_tag(
"hidden",
"https://api.example.com/v1",
"API version enumeration",
"api-version",
);
let auth = finding_with_tag(
"hidden",
"https://api.example.com",
"5 API endpoint(s) with no authentication requirement",
"auth-bypass",
);
let chains = engine.run(&[v1, auth], &[]);
assert!(chains
.iter()
.any(|f| f.title().contains("Unauthenticated legacy API")));
}
}