use super::super::heuristics::{self, HeuristicsConfig};
use super::super::profile_types::{self, stability_smoothed, Profile, ProfileEntry};
use super::args::GenerateArgs;
use super::model::{Entry, Meta, NetSection, Policy, Section};
pub fn generate_from_profile(
name: &str,
profile: &Profile,
args: &GenerateArgs,
heur_cfg: &HeuristicsConfig,
) -> Policy {
let total_runs = profile.total_runs;
let alpha = args.alpha;
let mut files = Section::default();
let mut network = NetSection::default();
let mut processes = Section::default();
for (path, entry) in &profile.entries.files {
let stab_display = stability_smoothed(entry.runs_seen, total_runs, alpha);
let stab_gate =
profile_types::stability_wilson_lower(entry.runs_seen, total_runs, args.wilson_z);
let risk = if args.heuristics {
Some(heuristics::analyze_entropy(path, heur_cfg))
} else {
None
};
if let Some((section, is_deny)) =
classify_entry(stab_gate, risk.as_ref(), args, total_runs, entry.runs_seen)
{
let out_entry = make_entry_profile(
path,
entry,
stab_display,
stab_gate,
total_runs,
args,
risk.as_ref(),
);
match (section, is_deny) {
("deny", _) => files.deny.push(path.clone()),
("needs_review", _) => files.needs_review.push(out_entry),
("allow", _) => files.allow.push(out_entry),
_ => {}
}
}
}
for (dest, entry) in &profile.entries.network {
let stab_display = stability_smoothed(entry.runs_seen, total_runs, alpha);
let stab_gate =
profile_types::stability_wilson_lower(entry.runs_seen, total_runs, args.wilson_z);
let risk = if args.heuristics {
Some(heuristics::analyze_dest(dest, heur_cfg))
} else {
None
};
if let Some((section, _)) =
classify_entry(stab_gate, risk.as_ref(), args, total_runs, entry.runs_seen)
{
let out_entry = make_entry_profile(
dest,
entry,
stab_display,
stab_gate,
total_runs,
args,
risk.as_ref(),
);
match section {
"deny" => network.deny_destinations.push(dest.clone()),
"needs_review" => network.needs_review.push(out_entry),
"allow" => network.allow_destinations.push(out_entry),
_ => {}
}
}
}
for (path, entry) in &profile.entries.processes {
let stab_display = stability_smoothed(entry.runs_seen, total_runs, alpha);
let stab_gate =
profile_types::stability_wilson_lower(entry.runs_seen, total_runs, args.wilson_z);
let risk = if args.heuristics {
Some(heuristics::analyze_entropy(path, heur_cfg))
} else {
None
};
if let Some((section, _)) =
classify_entry(stab_gate, risk.as_ref(), args, total_runs, entry.runs_seen)
{
let out_entry = make_entry_profile(
path,
entry,
stab_display,
stab_gate,
total_runs,
args,
risk.as_ref(),
);
match section {
"deny" => processes.deny.push(path.clone()),
"needs_review" => processes.needs_review.push(out_entry),
"allow" => processes.allow.push(out_entry),
_ => {}
}
}
}
Policy {
_meta: Some(Meta {
name: name.into(),
generated_at: chrono::Utc::now().to_rfc3339(),
profile_runs: Some(total_runs),
min_stability: Some(args.min_stability),
min_runs: if args.min_runs > 1 {
Some(args.min_runs)
} else {
None
},
}),
files,
network,
processes,
}
}
pub(super) fn classify_entry(
stab_gate: f64,
risk: Option<&heuristics::RiskAssessment>,
args: &GenerateArgs,
total_runs: u32,
runs_seen: u32,
) -> Option<(&'static str, bool)> {
if let Some(r) = risk {
match r.level {
heuristics::RiskLevel::DenyRecommended => return Some(("deny", true)),
heuristics::RiskLevel::NeedsReview => return Some(("needs_review", false)),
_ => {}
}
}
if total_runs < args.min_runs || runs_seen < args.min_runs {
return if args.new_is_risky {
Some(("needs_review", false))
} else {
None
};
}
if stab_gate >= args.min_stability {
Some(("allow", false))
} else if stab_gate >= args.review_threshold || args.new_is_risky {
Some(("needs_review", false))
} else {
None }
}
fn make_entry_profile(
pattern: &str,
entry: &ProfileEntry,
stab_display: f64,
stab_gate: f64,
total_runs: u32,
args: &GenerateArgs,
risk: Option<&heuristics::RiskAssessment>,
) -> Entry {
let mut reasons = Vec::new();
if total_runs < args.min_runs || entry.runs_seen < args.min_runs {
reasons.push(format!(
"min_runs gate: need >= {} runs (profile={}, entry={})",
args.min_runs, total_runs, entry.runs_seen
));
}
reasons.push(format!(
"wilson_lb {:.2} (z={:.2})",
stab_gate, args.wilson_z
));
reasons.push(format!(
"laplace {:.2} (α={:.1}, {}/{})",
stab_display, args.alpha, entry.runs_seen, total_runs
));
if let Some(r) = risk {
reasons.extend(r.reasons.clone());
}
Entry::WithMeta {
pattern: pattern.into(),
count: Some(entry.hits_total as u32),
stability: Some((stab_display * 100.0).round() / 100.0), runs_seen: Some(entry.runs_seen),
risk: risk.map(|r| match r.level {
heuristics::RiskLevel::Low => "low".into(),
heuristics::RiskLevel::NeedsReview => "needs_review".into(),
heuristics::RiskLevel::DenyRecommended => "deny_recommended".into(),
}),
reasons: Some(reasons),
}
}
#[cfg(test)]
mod tests {
use super::classify_entry;
use crate::cli::commands::generate::GenerateArgs;
use crate::cli::commands::heuristics;
use std::path::PathBuf;
fn default_args() -> GenerateArgs {
GenerateArgs {
input: None,
profile: None,
output: PathBuf::from("policy.yaml"),
name: "".into(),
format: "yaml".into(),
dry_run: false,
diff: false,
heuristics: false,
entropy_threshold: 3.8,
min_stability: 0.8,
min_runs: 1,
wilson_z: 1.96,
review_threshold: 0.6,
new_is_risky: false,
alpha: 1.0,
}
}
#[test]
fn classify_stable_low_risk() {
let args = default_args();
assert_eq!(
classify_entry(0.9, None, &args, 10, 10),
Some(("allow", false))
);
assert_eq!(
classify_entry(0.7, None, &args, 10, 7),
Some(("needs_review", false))
);
assert_eq!(classify_entry(0.3, None, &args, 10, 3), None);
}
#[test]
fn classify_new_is_risky() {
let mut args = default_args();
args.new_is_risky = true;
assert_eq!(
classify_entry(0.3, None, &args, 10, 3),
Some(("needs_review", false))
);
}
#[test]
fn classify_min_runs_gates_early_noise() {
let mut args = default_args();
args.min_runs = 5;
assert_eq!(classify_entry(0.99, None, &args, 1, 1), None);
args.new_is_risky = true;
assert_eq!(
classify_entry(0.99, None, &args, 1, 1),
Some(("needs_review", false))
);
args.new_is_risky = false;
assert_eq!(classify_entry(0.99, None, &args, 10, 1), None);
}
#[test]
fn classify_risk_overrides() {
let mut args = default_args();
args.heuristics = true;
args.min_runs = 5;
let mut risk = heuristics::RiskAssessment::default();
risk.add(heuristics::RiskLevel::DenyRecommended, "test".to_string());
assert_eq!(
classify_entry(0.95, Some(&risk), &args, 1, 1),
Some(("deny", true))
);
}
}