use crate::core::rules_runtime;
use crate::core::types::{CooldownTracker, InfraEvent, RulebookConfig};
use crate::core::webhook_server::run_webhook_server;
use crate::core::webhook_source::WebhookConfig;
use std::path::Path;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
#[allow(clippy::too_many_arguments)]
pub fn build_config(
bind: String,
port: u16,
secret_file: Option<&Path>,
paths: Vec<String>,
allow_unauthenticated: bool,
tls_terminated_upstream: bool,
tolerance_secs: u64,
) -> Result<WebhookConfig, String> {
let secret = match secret_file {
None => None,
Some(p) => {
let raw = std::fs::read_to_string(p)
.map_err(|e| format!("read secret file {}: {e}", p.display()))?;
let trimmed = raw.trim().to_string();
if trimmed.is_empty() {
return Err(format!("secret file {} is empty", p.display()));
}
Some(trimmed)
}
};
Ok(WebhookConfig {
bind,
port,
secret,
allowed_paths: paths,
allow_unauthenticated,
tls_terminated_upstream,
signature_tolerance_secs: tolerance_secs,
machine: Some(hostname_or_local()),
..WebhookConfig::default()
})
}
fn hostname_or_local() -> String {
std::env::var("HOSTNAME")
.ok()
.filter(|h| !h.is_empty())
.unwrap_or_else(|| "local".to_string())
}
pub fn load_rulebook(file: &Path) -> Result<RulebookConfig, String> {
let content =
std::fs::read_to_string(file).map_err(|e| format!("read {}: {e}", file.display()))?;
let issues = crate::core::rules_engine::validate_rulebook_yaml(&content)?;
let errors: Vec<_> = issues
.iter()
.filter(|i| i.severity == crate::core::rules_engine::IssueSeverity::Error)
.collect();
if !errors.is_empty() {
let msgs: Vec<String> = errors
.iter()
.map(|i| format!(" {}: {}", i.rulebook, i.message))
.collect();
return Err(format!("rulebook validation failed:\n{}", msgs.join("\n")));
}
serde_yaml_ng::from_str(&content).map_err(|e| format!("parse rulebook: {e}"))
}
pub fn print_check(config: &WebhookConfig, rulebooks: usize, json: bool) {
let authenticated = config.secret.is_some();
if json {
let out = serde_json::json!({
"check": true,
"bind": config.bind,
"port": config.port,
"authenticated": authenticated,
"allowed_paths": config.allowed_paths,
"signature_tolerance_secs": config.signature_tolerance_secs,
"rulebooks": rulebooks,
"actions_executed": false,
});
println!("{}", serde_json::to_string_pretty(&out).unwrap_or_default());
} else {
println!("Webhook receiver configuration OK");
println!(" Bind: {}:{}", config.bind, config.port);
println!(
" Auth: {}",
if authenticated {
"HMAC-SHA256 (X-Forjar-Signature: t=…,v1=…; X-Hub-Signature-256 accepted)"
} else {
"NONE (--allow-unauthenticated)"
}
);
println!(" Paths: {}", config.allowed_paths.join(", "));
println!(" Tolerance: {}s", config.signature_tolerance_secs);
println!(" Rulebooks: {rulebooks}");
println!(" Actions: reported, NOT executed (see module docs)");
}
}
pub fn serve(config: WebhookConfig, rulebook: RulebookConfig, json: bool) -> Result<(), String> {
config.validate_startup()?;
let (tx, rx) = std::sync::mpsc::channel::<InfraEvent>();
let shutdown = Arc::new(AtomicBool::new(false));
let server_config = config.clone();
let server_shutdown = Arc::clone(&shutdown);
let handle =
std::thread::spawn(move || run_webhook_server(&server_config, tx, server_shutdown));
eprintln!(
"forjar: webhook receiver listening on {}:{} ({} path(s), {})",
config.bind,
config.port,
config.allowed_paths.len(),
if config.secret.is_some() {
"signed"
} else {
"UNAUTHENTICATED"
}
);
let mut tracker = CooldownTracker::default();
for event in rx {
let results = rules_runtime::evaluate_event(&event, &rulebook, &mut tracker);
let fired: Vec<_> = results
.iter()
.filter(|r| !r.cooldown_blocked && !r.disabled && !r.actions.is_empty())
.collect();
report(&event, &fired, json);
}
shutdown.store(true, std::sync::atomic::Ordering::Relaxed);
match handle.join() {
Ok(result) => result,
Err(_) => Err("webhook server thread panicked".to_string()),
}
}
fn report(event: &InfraEvent, fired: &[&rules_runtime::EvalResult], json: bool) {
if json {
let out = serde_json::json!({
"event": format!("{:?}", event.event_type),
"timestamp": event.timestamp,
"machine": event.machine,
"event_id": event.payload.get("_event_id"),
"path": event.payload.get("_path"),
"fired": fired.iter().map(|r| serde_json::json!({
"rulebook": r.rulebook,
"actions": r.actions.len(),
})).collect::<Vec<_>>(),
"actions_executed": false,
});
println!("{}", serde_json::to_string(&out).unwrap_or_default());
} else {
println!(
"event {:?} at {} path={} → {} rulebook(s) matched",
event.event_type,
event.timestamp,
event.payload.get("_path").map_or("-", String::as_str),
fired.len()
);
for r in fired {
println!(
" {} → {} action(s) (not executed)",
r.rulebook,
r.actions.len()
);
}
}
}