use crate::diagnostics::{
EngineFailureCode, SCHEMA_VERSION, engine_failure_code_name, json_string,
};
use crate::event_hints::{
POST_COMMIT_BLOCKED_HINT, WARN_UNSAFE_HINT, engine_failure_hint, finding_hint,
push_hint_field,
};
use crate::event_path::{EventPath, base64_standard};
use crate::policy_registry::{PolicyId, Severity, policy_descriptor, severity_name};
use crate::policy_trigger::{Trigger, trigger_name};
pub const WARN_UNSAFE_CODE: &str = "warn-unsafe";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct FindingLocation {
pub byte_start: u64,
pub byte_end: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FindingEvent {
pub trigger: Trigger,
pub policy: PolicyId,
pub severity: Severity,
pub code: &'static str,
pub message: String,
pub path: Option<EventPath>,
pub location: Option<FindingLocation>,
pub fix_available: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EngineFailureHint {
OfCode,
Chosen(Option<&'static str>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PolicyEvent {
Finding(FindingEvent),
WarnUnsafe {
trigger: Trigger,
policy: PolicyId,
},
CoreFinding {
core_id: &'static str,
code: &'static str,
message: String,
},
EngineFailure {
code: EngineFailureCode,
hint: EngineFailureHint,
message: String,
trigger: Option<Trigger>,
policy: Option<PolicyId>,
path: Option<EventPath>,
},
FixSummary {
trigger: Trigger,
passes: u64,
changed_paths: Vec<EventPath>,
},
CommitLanded {
oid: String,
},
WordMerged {
lines: Vec<String>,
},
}
pub fn event_blocks(event: &PolicyEvent) -> bool {
match event {
PolicyEvent::Finding(finding) => return finding.severity == Severity::Error,
PolicyEvent::CoreFinding { .. } => return true,
PolicyEvent::WarnUnsafe { .. }
| PolicyEvent::EngineFailure { .. }
| PolicyEvent::FixSummary { .. }
| PolicyEvent::CommitLanded { .. }
| PolicyEvent::WordMerged { .. } => return false,
}
}
fn event_head(sequence: u64, event_type: &str) -> String {
return format!(
"{{\"schemaVersion\":{SCHEMA_VERSION},\"sequence\":{sequence},\"type\":{}",
json_string(event_type)
);
}
fn push_text_field(line: &mut String, name: &str, value: &str) {
line.push_str(format!(",\"{name}\":{}", json_string(value)).as_str());
}
fn push_path_field(line: &mut String, path: &EventPath) {
push_text_field(line, "path", path.text());
if let Some(exact) = path.exact() {
push_text_field(line, "pathBytes", base64_standard(exact).as_str());
}
}
fn push_finding(line: &mut String, finding: &FindingEvent) {
let policy_name: &str = policy_descriptor(finding.policy).name;
push_text_field(line, "trigger", trigger_name(finding.trigger));
push_text_field(line, "policyId", policy_name);
push_text_field(line, "severity", severity_name(finding.severity));
push_text_field(
line,
"code",
format!("{policy_name}/{}", finding.code).as_str(),
);
push_text_field(line, "message", finding.message.as_str());
push_hint_field(line, finding_hint(policy_name, finding.code, finding.fix_available));
if let Some(path) = &finding.path {
push_path_field(line, path);
}
if let Some(location) = finding.location {
line.push_str(
format!(
",\"location\":{{\"byteStart\":{},\"byteEnd\":{}}}",
location.byte_start, location.byte_end
)
.as_str(),
);
}
let fix: &str = if finding.fix_available {
"available"
} else {
"none"
};
push_text_field(line, "fix", fix);
}
fn path_array(paths: &[EventPath], entry: fn(&EventPath) -> String) -> String {
let mut array: String = String::from("[");
for (index, path) in paths.iter().enumerate() {
if index > 0 {
array.push(',');
}
array.push_str(json_string(entry(path).as_str()).as_str());
}
array.push(']');
return array;
}
fn path_text(path: &EventPath) -> String {
return String::from(path.text());
}
fn path_bytes(path: &EventPath) -> String {
return base64_standard(path.bytes());
}
fn push_path_list(line: &mut String, text_key: &str, bytes_key: &str, paths: &[EventPath]) {
line.push_str(format!(",\"{text_key}\":{}", path_array(paths, path_text)).as_str());
for path in paths {
if path.exact().is_some() {
line.push_str(format!(",\"{bytes_key}\":{}", path_array(paths, path_bytes)).as_str());
return;
}
}
}
fn event_type(event: &PolicyEvent) -> &'static str {
match event {
PolicyEvent::Finding(_) => return "finding",
PolicyEvent::WarnUnsafe { .. } => return "configuration-warning",
PolicyEvent::CoreFinding { .. } => return "core-finding",
PolicyEvent::EngineFailure { .. } => return "engine-failure",
PolicyEvent::FixSummary { .. } => return "fix-summary",
PolicyEvent::CommitLanded { .. } => return "commit-landed",
PolicyEvent::WordMerged { .. } => return "word-merged",
}
}
pub fn render_policy_event(sequence: u64, event: &PolicyEvent) -> String {
let mut line: String = event_head(sequence, event_type(event));
match event {
PolicyEvent::Finding(finding) => push_finding(&mut line, finding),
PolicyEvent::WarnUnsafe { trigger, policy } => {
let policy_name: &str = policy_descriptor(*policy).name;
push_text_field(&mut line, "trigger", trigger_name(*trigger));
push_text_field(&mut line, "policyId", policy_name);
push_text_field(&mut line, "code", WARN_UNSAFE_CODE);
push_text_field(
&mut line,
"message",
format!("Policy {policy_name} is warn-unsafe but configured as warn.").as_str(),
);
push_hint_field(&mut line, Some(WARN_UNSAFE_HINT));
}
PolicyEvent::CoreFinding {
core_id,
code,
message,
} => {
push_text_field(&mut line, "trigger", trigger_name(Trigger::PreForward));
push_text_field(&mut line, "coreId", core_id);
push_text_field(&mut line, "code", format!("{core_id}/{code}").as_str());
push_text_field(&mut line, "message", message.as_str());
}
PolicyEvent::EngineFailure {
code,
hint,
message,
trigger,
policy,
path,
} => {
let code_name: &str = engine_failure_code_name(*code);
push_text_field(&mut line, "code", code_name);
push_text_field(&mut line, "message", message.as_str());
push_hint_field(
&mut line,
match hint {
EngineFailureHint::OfCode => engine_failure_hint(code_name),
EngineFailureHint::Chosen(chosen) => *chosen,
},
);
if let Some(found) = trigger {
push_text_field(&mut line, "trigger", trigger_name(*found));
}
if let Some(found) = policy {
push_text_field(&mut line, "policyId", policy_descriptor(*found).name);
}
if let Some(found) = path {
push_path_field(&mut line, found);
}
}
PolicyEvent::FixSummary {
trigger,
passes,
changed_paths,
} => {
push_text_field(&mut line, "trigger", trigger_name(*trigger));
line.push_str(format!(",\"passes\":{passes}").as_str());
push_path_list(&mut line, "changedPaths", "changedPathBytes", changed_paths.as_slice());
}
PolicyEvent::CommitLanded { oid } => {
push_text_field(&mut line, "oid", oid);
push_text_field(&mut line, "outcome", "post-commit-blocked");
push_text_field(
&mut line,
"message",
format!("Commit {oid} remains local; post-commit gate blocked automatic backup.")
.as_str(),
);
push_hint_field(&mut line, Some(POST_COMMIT_BLOCKED_HINT));
}
PolicyEvent::WordMerged { lines } => {
push_text_field(&mut line, "outcome", "word-merged");
push_text_field(&mut line, "message", lines.join("\n").as_str());
}
}
line.push_str("}\n");
return line;
}
pub fn render_policy_events(first_sequence: u64, events: &[PolicyEvent]) -> String {
let mut rendered: String = String::new();
for (index, event) in events.iter().enumerate() {
rendered.push_str(render_policy_event(first_sequence + index as u64, event).as_str());
}
return rendered;
}
#[cfg(test)]
#[path = "policy_events_tests.rs"]
mod tests;