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
98fn collect_predicates_rec(expr: &Expr, predicates: &mut HashSet<String>) {
99 match expr {
100 Expr::True => {
101 predicates.insert("True".into());
102 }
103 Expr::False => {
104 predicates.insert("False".into());
105 }
106 Expr::And(children) | Expr::Or(children) => {
107 for child in children {
108 collect_predicates_rec(child, predicates);
109 }
110 }
111 Expr::Not(inner) => {
112 collect_predicates_rec(inner, predicates);
113 }
114 Expr::HasCapability(cap) => {
115 predicates.insert(format!("HasCapability({})", cap));
116 }
117 Expr::HasAllCapabilities(caps) => {
118 predicates.insert(format!("HasAllCapabilities({:?})", caps));
119 }
120 Expr::HasAnyCapability(caps) => {
121 predicates.insert(format!("HasAnyCapability({:?})", caps));
122 }
123 Expr::IssuerIs(did) => {
124 predicates.insert(format!("IssuerIs({})", did));
125 }
126 Expr::IssuerIn(dids) => {
127 predicates.insert(format!("IssuerIn({:?})", dids));
128 }
129 Expr::SubjectIs(did) => {
130 predicates.insert(format!("SubjectIs({})", did));
131 }
132 Expr::DelegatedBy(did) => {
133 predicates.insert(format!("DelegatedBy({})", did));
134 }
135 Expr::NotRevoked => {
136 predicates.insert("NotRevoked".into());
137 }
138 Expr::NotExpired => {
139 predicates.insert("NotExpired".into());
140 }
141 Expr::ExpiresAfter(secs) => {
142 predicates.insert(format!("ExpiresAfter({})", secs));
143 }
144 Expr::IssuedWithin(secs) => {
145 predicates.insert(format!("IssuedWithin({})", secs));
146 }
147 Expr::RoleIs(role) => {
148 predicates.insert(format!("RoleIs({})", role));
149 }
150 Expr::RoleIn(roles) => {
151 predicates.insert(format!("RoleIn({:?})", roles));
152 }
153 Expr::RepoIs(repo) => {
154 predicates.insert(format!("RepoIs({})", repo));
155 }
156 Expr::RepoIn(repos) => {
157 predicates.insert(format!("RepoIn({:?})", repos));
158 }
159 Expr::RefMatches(pattern) => {
160 predicates.insert(format!("RefMatches({})", pattern));
161 }
162 Expr::PathAllowed(patterns) => {
163 predicates.insert(format!("PathAllowed({:?})", patterns));
164 }
165 Expr::EnvIs(env) => {
166 predicates.insert(format!("EnvIs({})", env));
167 }
168 Expr::EnvIn(envs) => {
169 predicates.insert(format!("EnvIn({:?})", envs));
170 }
171 Expr::WorkloadIssuerIs(issuer) => {
172 predicates.insert(format!("WorkloadIssuerIs({})", issuer));
173 }
174 Expr::WorkloadClaimEquals { key, value } => {
175 predicates.insert(format!("WorkloadClaimEquals({}, {})", key, value));
176 }
177 Expr::MaxChainDepth(depth) => {
178 predicates.insert(format!("MaxChainDepth({})", depth));
179 }
180 Expr::AttrEquals { key, value } => {
181 predicates.insert(format!("AttrEquals({}, {})", key, value));
182 }
183 Expr::AttrIn { key, values } => {
184 predicates.insert(format!("AttrIn({}, {:?})", key, values));
185 }
186 Expr::IsAgent => {
187 predicates.insert("IsAgent".into());
188 }
189 Expr::IsHuman => {
190 predicates.insert("IsHuman".into());
191 }
192 Expr::IsWorkload => {
193 predicates.insert("IsWorkload".into());
194 }
195 }
196}
197
198fn removal_risk(pred: &str) -> String {
199 if pred == "NotRevoked" || pred == "NotExpired" || pred.starts_with("MaxChainDepth") {
200 return "HIGH".into();
201 }
202 if pred.starts_with("IssuerIs")
203 || pred.starts_with("RepoIs")
204 || pred.starts_with("RefMatches")
205 || pred.starts_with("EnvIs")
206 {
207 return "MEDIUM".into();
208 }
209 "LOW".into()
210}
211
212fn addition_risk(pred: &str) -> String {
213 if pred.starts_with("HasCapability") || pred.starts_with("HasAllCapabilities") {
214 return "MEDIUM".into();
215 }
216 "LOW".into()
217}
218
219fn check_structural_change(old: &Expr, new: &Expr) -> Option<PolicyChange> {
220 match (old, new) {
221 (Expr::And(_), Expr::Or(_)) => Some(PolicyChange {
222 kind: "changed".into(),
223 description: "And → Or at root".into(),
224 risk: "HIGH".into(),
225 }),
226 (Expr::Or(_), Expr::And(_)) => Some(PolicyChange {
227 kind: "changed".into(),
228 description: "Or → And at root".into(),
229 risk: "MEDIUM".into(),
230 }),
231 _ => None,
232 }
233}