auths_sdk/workflows/
policy_diff.rs1use auths_policy::Expr;
7use std::collections::HashSet;
8
9#[derive(Debug, Clone)]
11pub struct PolicyChange {
12 pub kind: String,
14 pub description: String,
16 pub risk: String,
18}
19
20#[derive(Debug, thiserror::Error)]
22pub enum PolicyDiffError {
23 #[error("policy parse error: {0}")]
25 Parse(String),
26}
27
28pub fn compute_policy_diff(old: &Expr, new: &Expr) -> Vec<PolicyChange> {
40 let mut changes = Vec::new();
41
42 let old_predicates = collect_predicates(old);
43 let new_predicates = collect_predicates(new);
44
45 for pred in &old_predicates {
46 if !new_predicates.contains(pred) {
47 changes.push(PolicyChange {
48 kind: "removed".into(),
49 description: pred.clone(),
50 risk: removal_risk(pred),
51 });
52 }
53 }
54
55 for pred in &new_predicates {
56 if !old_predicates.contains(pred) {
57 changes.push(PolicyChange {
58 kind: "added".into(),
59 description: pred.clone(),
60 risk: addition_risk(pred),
61 });
62 }
63 }
64
65 if let Some(change) = check_structural_change(old, new) {
66 changes.push(change);
67 }
68
69 changes
70}
71
72pub fn overall_risk_score(changes: &[PolicyChange]) -> String {
83 if changes.iter().any(|c| c.risk == "HIGH") {
84 return "HIGH".into();
85 }
86 if changes.iter().any(|c| c.risk == "MEDIUM") {
87 return "MEDIUM".into();
88 }
89 "LOW".into()
90}
91
92fn collect_predicates(expr: &Expr) -> HashSet<String> {
93 let mut predicates = HashSet::new();
94 collect_predicates_rec(expr, &mut predicates);
95 predicates
96}
97
98#[allow(clippy::too_many_lines)]
99fn collect_predicates_rec(expr: &Expr, predicates: &mut HashSet<String>) {
100 match expr {
101 Expr::True => {
102 predicates.insert("True".into());
103 }
104 Expr::False => {
105 predicates.insert("False".into());
106 }
107 Expr::And(children) | Expr::Or(children) => {
108 for child in children {
109 collect_predicates_rec(child, predicates);
110 }
111 }
112 Expr::Not(inner) => {
113 collect_predicates_rec(inner, predicates);
114 }
115 Expr::HasCapability(cap) => {
116 predicates.insert(format!("HasCapability({})", cap));
117 }
118 Expr::HasAllCapabilities(caps) => {
119 predicates.insert(format!("HasAllCapabilities({:?})", caps));
120 }
121 Expr::HasAnyCapability(caps) => {
122 predicates.insert(format!("HasAnyCapability({:?})", caps));
123 }
124 Expr::IssuerIs(did) => {
125 predicates.insert(format!("IssuerIs({})", did));
126 }
127 Expr::IssuerIn(dids) => {
128 predicates.insert(format!("IssuerIn({:?})", dids));
129 }
130 Expr::SubjectIs(did) => {
131 predicates.insert(format!("SubjectIs({})", did));
132 }
133 Expr::DelegatedBy(did) => {
134 predicates.insert(format!("DelegatedBy({})", did));
135 }
136 Expr::NotRevoked => {
137 predicates.insert("NotRevoked".into());
138 }
139 Expr::NotExpired => {
140 predicates.insert("NotExpired".into());
141 }
142 Expr::ExpiresAfter(secs) => {
143 predicates.insert(format!("ExpiresAfter({})", secs));
144 }
145 Expr::IssuedWithin(secs) => {
146 predicates.insert(format!("IssuedWithin({})", secs));
147 }
148 Expr::RoleIs(role) => {
149 predicates.insert(format!("RoleIs({})", role));
150 }
151 Expr::RoleIn(roles) => {
152 predicates.insert(format!("RoleIn({:?})", roles));
153 }
154 Expr::RepoIs(repo) => {
155 predicates.insert(format!("RepoIs({})", repo));
156 }
157 Expr::RepoIn(repos) => {
158 predicates.insert(format!("RepoIn({:?})", repos));
159 }
160 Expr::RefMatches(pattern) => {
161 predicates.insert(format!("RefMatches({})", pattern));
162 }
163 Expr::PathAllowed(patterns) => {
164 predicates.insert(format!("PathAllowed({:?})", patterns));
165 }
166 Expr::EnvIs(env) => {
167 predicates.insert(format!("EnvIs({})", env));
168 }
169 Expr::EnvIn(envs) => {
170 predicates.insert(format!("EnvIn({:?})", envs));
171 }
172 Expr::WorkloadIssuerIs(issuer) => {
173 predicates.insert(format!("WorkloadIssuerIs({})", issuer));
174 }
175 Expr::WorkloadClaimEquals { key, value } => {
176 predicates.insert(format!("WorkloadClaimEquals({}, {})", key, value));
177 }
178 Expr::MaxChainDepth(depth) => {
179 predicates.insert(format!("MaxChainDepth({})", depth));
180 }
181 Expr::AttrEquals { key, value } => {
182 predicates.insert(format!("AttrEquals({}, {})", key, value));
183 }
184 Expr::AttrIn { key, values } => {
185 predicates.insert(format!("AttrIn({}, {:?})", key, values));
186 }
187 Expr::IsAgent => {
188 predicates.insert("IsAgent".into());
189 }
190 Expr::IsHuman => {
191 predicates.insert("IsHuman".into());
192 }
193 Expr::IsWorkload => {
194 predicates.insert("IsWorkload".into());
195 }
196 Expr::ApprovalGate {
197 inner, approvers, ..
198 } => {
199 predicates.insert(format!("ApprovalGate(approvers={:?})", approvers));
200 collect_predicates_rec(inner, predicates);
201 }
202 _ => {
203 predicates.insert(format!("{:?}", expr));
204 }
205 }
206}
207
208fn removal_risk(pred: &str) -> String {
209 if pred == "NotRevoked" || pred == "NotExpired" || pred.starts_with("MaxChainDepth") {
210 return "HIGH".into();
211 }
212 if pred.starts_with("IssuerIs")
213 || pred.starts_with("RepoIs")
214 || pred.starts_with("RefMatches")
215 || pred.starts_with("EnvIs")
216 {
217 return "MEDIUM".into();
218 }
219 "LOW".into()
220}
221
222fn addition_risk(pred: &str) -> String {
223 if pred.starts_with("HasCapability") || pred.starts_with("HasAllCapabilities") {
224 return "MEDIUM".into();
225 }
226 "LOW".into()
227}
228
229fn check_structural_change(old: &Expr, new: &Expr) -> Option<PolicyChange> {
230 match (old, new) {
231 (Expr::And(_), Expr::Or(_)) => Some(PolicyChange {
232 kind: "changed".into(),
233 description: "And → Or at root".into(),
234 risk: "HIGH".into(),
235 }),
236 (Expr::Or(_), Expr::And(_)) => Some(PolicyChange {
237 kind: "changed".into(),
238 description: "Or → And at root".into(),
239 risk: "MEDIUM".into(),
240 }),
241 _ => None,
242 }
243}