1use crate::ast::{FieldValue, PgnPacket};
2use crate::registry::{ActionRegistry, SafetyRules, WorkflowRegistry};
3use crate::resolver::ResolvedRef;
4
5#[derive(Debug, Clone, PartialEq)]
6pub enum SafetyRuleId {
7 Sg1,
8 Sg2,
9 Sg3,
10 Sg4,
11 Sg5,
12 Sg6,
13 Sg7,
14 Sg8,
15 Sg9,
16}
17
18impl std::fmt::Display for SafetyRuleId {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 match self {
21 SafetyRuleId::Sg1 => write!(f, "SG-1"),
22 SafetyRuleId::Sg2 => write!(f, "SG-2"),
23 SafetyRuleId::Sg3 => write!(f, "SG-3"),
24 SafetyRuleId::Sg4 => write!(f, "SG-4"),
25 SafetyRuleId::Sg5 => write!(f, "SG-5"),
26 SafetyRuleId::Sg6 => write!(f, "SG-6"),
27 SafetyRuleId::Sg7 => write!(f, "SG-7"),
28 SafetyRuleId::Sg8 => write!(f, "SG-8"),
29 SafetyRuleId::Sg9 => write!(f, "SG-9"),
30 }
31 }
32}
33
34#[derive(Debug, Clone)]
35pub struct SafetyResult {
36 pub allowed: bool,
37 pub blocked: bool,
38 pub fired_rules: Vec<SafetyRuleId>,
39 pub human_required: bool,
40 pub effective_risk: String,
41}
42
43#[derive(Debug, Clone)]
44pub struct BlockReason {
45 pub rule: SafetyRuleId,
46 pub message: String,
47}
48
49pub fn check_safety(
50 packet: &PgnPacket,
51 action_registry: &ActionRegistry,
52 safety_rules: &SafetyRules,
53 workflows: &WorkflowRegistry,
54) -> SafetyResult {
55 let mut fired_rules = Vec::new();
56 let mut human_required = false;
57
58 let effective_risk = compute_effective_risk(packet, workflows);
60
61 if let (Some(FieldValue::List(do_list)), Some(FieldValue::List(denied_list))) =
63 (packet.fields.get("do"), packet.fields.get("deny"))
64 {
65 for action in do_list {
66 if denied_list.contains(action) {
67 fired_rules.push(SafetyRuleId::Sg1);
68 break;
69 }
70 }
71 }
72
73 if let Some(FieldValue::List(do_list)) = packet.fields.get("do") {
75 let human_field = packet.fields.get("human");
76 let has_human_yes = matches!(human_field, Some(FieldValue::Scalar(s)) if s == "yes");
77
78 for action in do_list {
79 if action_registry.human_gated.contains(action) && !has_human_yes {
80 fired_rules.push(SafetyRuleId::Sg2);
81 break;
82 }
83 }
84 }
85
86 if effective_risk == "high" || effective_risk == "crit" {
88 human_required = true;
89 if let Some(FieldValue::Scalar(human)) = packet.fields.get("human")
90 && human == "no"
91 {
92 fired_rules.push(SafetyRuleId::Sg3);
93 }
94 }
95
96 if let Some(FieldValue::List(references)) = packet.fields.get("in") {
98 for reference in references {
99 for pattern in &safety_rules.private_paths {
100 if reference.contains(pattern) || reference_matches_pattern(reference, pattern) {
101 fired_rules.push(SafetyRuleId::Sg4);
102 break;
103 }
104 }
105 }
106 }
107
108 if let Some(FieldValue::Scalar(wf_name)) = packet.fields.get("wf")
110 && !workflows.workflows.contains_key(wf_name)
111 {
112 fired_rules.push(SafetyRuleId::Sg5);
113 }
114
115 if let (Some(FieldValue::Scalar(wf_name)), Some(FieldValue::Scalar(mode))) =
117 (packet.fields.get("wf"), packet.fields.get("mode"))
118 && let Some(workflow) = workflows.workflows.get(wf_name)
119 && !workflow.allowed_modes.contains(mode)
120 {
121 fired_rules.push(SafetyRuleId::Sg6);
122 }
123
124 let blocked = !fired_rules.is_empty();
131 let allowed = !blocked;
132
133 SafetyResult {
134 allowed,
135 blocked,
136 fired_rules,
137 human_required,
138 effective_risk,
139 }
140}
141
142fn compute_effective_risk(packet: &PgnPacket, workflows: &WorkflowRegistry) -> String {
143 let declared_risk = packet
144 .fields
145 .get("risk")
146 .and_then(|v| match v {
147 FieldValue::Scalar(s) => Some(s.as_str()),
148 _ => None,
149 })
150 .unwrap_or("med");
151
152 let workflow_default = packet
153 .fields
154 .get("wf")
155 .and_then(|v| match v {
156 FieldValue::Scalar(s) => workflows.workflows.get(s),
157 _ => None,
158 })
159 .map(|w| w.risk_default.as_str())
160 .unwrap_or("med");
161
162 if packet.fields.contains_key("risk") {
164 declared_risk.to_string()
165 } else {
166 workflow_default.to_string()
167 }
168}
169
170fn reference_matches_pattern(reference: &str, pattern: &str) -> bool {
171 if let Some(suffix) = pattern.strip_prefix("**/") {
173 reference.contains(suffix)
174 } else if let Some(prefix) = pattern.strip_suffix(".*") {
175 reference.starts_with(prefix)
176 } else if pattern.starts_with('.') {
177 reference == pattern || reference.starts_with(&format!("{}.", pattern))
179 } else {
180 reference == pattern
181 }
182}
183
184pub fn check_resolved_refs_safety(
188 resolved: &[ResolvedRef],
189 private_paths: &[String],
190) -> Vec<SafetyRuleId> {
191 let mut fired = Vec::new();
192 for r in resolved {
193 let path_str = r.resolved_path.as_ref().map(|p| p.display().to_string());
194 let check_str = path_str.as_deref().unwrap_or(&r.ref_id);
195 for pattern in private_paths {
196 if check_str.contains(pattern) || reference_matches_pattern(check_str, pattern) {
197 fired.push(SafetyRuleId::Sg4);
198 break;
199 }
200 }
201 }
202 fired
203}