mod rules;
use gossan_core::Target;
#[allow(unused_imports)] use secfinding::{Finding, Severity};
pub use rules::{AdminExposedRule, TlsWeaknessRule};
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)],
}
}
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")
}
#[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(), 2, "all 2 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")));
}
}