syncable_cli/analyzer/kubelint/templates/
rbac.rs1use crate::analyzer::kubelint::context::K8sObject;
4use crate::analyzer::kubelint::context::Object;
5use crate::analyzer::kubelint::templates::{CheckFunc, ParameterDesc, Template, TemplateError};
6use crate::analyzer::kubelint::types::{Diagnostic, ObjectKindsDesc};
7
8pub struct ClusterAdminRoleBindingTemplate;
10
11impl Template for ClusterAdminRoleBindingTemplate {
12 fn key(&self) -> &str {
13 "cluster-admin-role-binding"
14 }
15
16 fn human_name(&self) -> &str {
17 "Cluster Admin Role Binding"
18 }
19
20 fn description(&self) -> &str {
21 "Detects bindings to the cluster-admin ClusterRole"
22 }
23
24 fn supported_object_kinds(&self) -> ObjectKindsDesc {
25 ObjectKindsDesc::new(&["ClusterRoleBinding", "RoleBinding"])
26 }
27
28 fn parameters(&self) -> Vec<ParameterDesc> {
29 Vec::new()
30 }
31
32 fn instantiate(
33 &self,
34 _params: &serde_yaml::Value,
35 ) -> Result<Box<dyn CheckFunc>, TemplateError> {
36 Ok(Box::new(ClusterAdminRoleBindingCheck))
37 }
38}
39
40struct ClusterAdminRoleBindingCheck;
41
42impl CheckFunc for ClusterAdminRoleBindingCheck {
43 fn check(&self, object: &Object) -> Vec<Diagnostic> {
44 let mut diagnostics = Vec::new();
45
46 let role_ref = match &object.k8s_object {
47 K8sObject::ClusterRoleBinding(crb) => Some(&crb.role_ref),
48 K8sObject::RoleBinding(rb) => Some(&rb.role_ref),
49 _ => None,
50 };
51
52 if let Some(role_ref) = role_ref {
53 if role_ref.kind == "ClusterRole" && role_ref.name == "cluster-admin" {
54 diagnostics.push(Diagnostic {
55 message: "Binding grants cluster-admin privileges".to_string(),
56 remediation: Some(
57 "Avoid binding to cluster-admin. Create a more restrictive ClusterRole \
58 with only the required permissions."
59 .to_string(),
60 ),
61 });
62 }
63 }
64
65 diagnostics
66 }
67}
68
69pub struct WildcardInRulesTemplate;
71
72impl Template for WildcardInRulesTemplate {
73 fn key(&self) -> &str {
74 "wildcard-in-rules"
75 }
76
77 fn human_name(&self) -> &str {
78 "Wildcard in RBAC Rules"
79 }
80
81 fn description(&self) -> &str {
82 "Detects use of wildcards (*) in RBAC rules"
83 }
84
85 fn supported_object_kinds(&self) -> ObjectKindsDesc {
86 ObjectKindsDesc::new(&["Role", "ClusterRole"])
87 }
88
89 fn parameters(&self) -> Vec<ParameterDesc> {
90 Vec::new()
91 }
92
93 fn instantiate(
94 &self,
95 _params: &serde_yaml::Value,
96 ) -> Result<Box<dyn CheckFunc>, TemplateError> {
97 Ok(Box::new(WildcardInRulesCheck))
98 }
99}
100
101struct WildcardInRulesCheck;
102
103impl CheckFunc for WildcardInRulesCheck {
104 fn check(&self, object: &Object) -> Vec<Diagnostic> {
105 let mut diagnostics = Vec::new();
106
107 let rules = match &object.k8s_object {
108 K8sObject::Role(r) => Some(&r.rules),
109 K8sObject::ClusterRole(cr) => Some(&cr.rules),
110 _ => None,
111 };
112
113 if let Some(rules) = rules {
114 for rule in rules {
115 if rule.verbs.iter().any(|v| v == "*") {
117 diagnostics.push(Diagnostic {
118 message: "Rule uses wildcard (*) in verbs".to_string(),
119 remediation: Some(
120 "Explicitly list the required verbs instead of using wildcard."
121 .to_string(),
122 ),
123 });
124 }
125
126 if rule.resources.iter().any(|r| r == "*") {
128 diagnostics.push(Diagnostic {
129 message: "Rule uses wildcard (*) in resources".to_string(),
130 remediation: Some(
131 "Explicitly list the required resources instead of using wildcard."
132 .to_string(),
133 ),
134 });
135 }
136
137 if rule.api_groups.iter().any(|g| g == "*") {
139 diagnostics.push(Diagnostic {
140 message: "Rule uses wildcard (*) in apiGroups".to_string(),
141 remediation: Some(
142 "Explicitly list the required API groups instead of using wildcard."
143 .to_string(),
144 ),
145 });
146 }
147 }
148 }
149
150 diagnostics
151 }
152}
153
154pub struct AccessToSecretsTemplate;
156
157impl Template for AccessToSecretsTemplate {
158 fn key(&self) -> &str {
159 "access-to-secrets"
160 }
161
162 fn human_name(&self) -> &str {
163 "Access to Secrets"
164 }
165
166 fn description(&self) -> &str {
167 "Detects RBAC rules that grant access to secrets"
168 }
169
170 fn supported_object_kinds(&self) -> ObjectKindsDesc {
171 ObjectKindsDesc::new(&["Role", "ClusterRole"])
172 }
173
174 fn parameters(&self) -> Vec<ParameterDesc> {
175 Vec::new()
176 }
177
178 fn instantiate(
179 &self,
180 _params: &serde_yaml::Value,
181 ) -> Result<Box<dyn CheckFunc>, TemplateError> {
182 Ok(Box::new(AccessToSecretsCheck))
183 }
184}
185
186struct AccessToSecretsCheck;
187
188impl CheckFunc for AccessToSecretsCheck {
189 fn check(&self, object: &Object) -> Vec<Diagnostic> {
190 let mut diagnostics = Vec::new();
191
192 let rules = match &object.k8s_object {
193 K8sObject::Role(r) => Some(&r.rules),
194 K8sObject::ClusterRole(cr) => Some(&cr.rules),
195 _ => None,
196 };
197
198 if let Some(rules) = rules {
199 for rule in rules {
200 let grants_secret_access =
202 rule.resources.iter().any(|r| r == "secrets" || r == "*")
203 && rule
204 .api_groups
205 .iter()
206 .any(|g| g.is_empty() || g == "*" || g == "core");
207
208 if grants_secret_access {
209 let sensitive_verbs = ["get", "list", "watch", "*"];
211 if rule
212 .verbs
213 .iter()
214 .any(|v| sensitive_verbs.contains(&v.as_str()))
215 {
216 diagnostics.push(Diagnostic {
217 message: "Rule grants read access to secrets".to_string(),
218 remediation: Some(
219 "Avoid granting broad access to secrets. Consider using \
220 resourceNames to limit access to specific secrets."
221 .to_string(),
222 ),
223 });
224 }
225 }
226 }
227 }
228
229 diagnostics
230 }
231}
232
233pub struct AccessToCreatePodsTemplate;
235
236impl Template for AccessToCreatePodsTemplate {
237 fn key(&self) -> &str {
238 "access-to-create-pods"
239 }
240
241 fn human_name(&self) -> &str {
242 "Access to Create Pods"
243 }
244
245 fn description(&self) -> &str {
246 "Detects RBAC rules that grant permission to create pods"
247 }
248
249 fn supported_object_kinds(&self) -> ObjectKindsDesc {
250 ObjectKindsDesc::new(&["Role", "ClusterRole"])
251 }
252
253 fn parameters(&self) -> Vec<ParameterDesc> {
254 Vec::new()
255 }
256
257 fn instantiate(
258 &self,
259 _params: &serde_yaml::Value,
260 ) -> Result<Box<dyn CheckFunc>, TemplateError> {
261 Ok(Box::new(AccessToCreatePodsCheck))
262 }
263}
264
265struct AccessToCreatePodsCheck;
266
267impl CheckFunc for AccessToCreatePodsCheck {
268 fn check(&self, object: &Object) -> Vec<Diagnostic> {
269 let mut diagnostics = Vec::new();
270
271 let rules = match &object.k8s_object {
272 K8sObject::Role(r) => Some(&r.rules),
273 K8sObject::ClusterRole(cr) => Some(&cr.rules),
274 _ => None,
275 };
276
277 if let Some(rules) = rules {
278 for rule in rules {
279 let grants_pod_create = rule.resources.iter().any(|r| r == "pods" || r == "*")
281 && rule
282 .api_groups
283 .iter()
284 .any(|g| g.is_empty() || g == "*" || g == "core")
285 && rule.verbs.iter().any(|v| v == "create" || v == "*");
286
287 if grants_pod_create {
288 diagnostics.push(Diagnostic {
289 message: "Rule grants permission to create pods".to_string(),
290 remediation: Some(
291 "Pod creation permission can be used for privilege escalation. \
292 Ensure this is intentional and the scope is limited."
293 .to_string(),
294 ),
295 });
296 }
297 }
298 }
299
300 diagnostics
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use crate::analyzer::kubelint::parser::yaml::parse_yaml;
308
309 #[test]
310 fn test_cluster_admin_binding_detected() {
311 let yaml = r#"
312apiVersion: rbac.authorization.k8s.io/v1
313kind: ClusterRoleBinding
314metadata:
315 name: admin-binding
316roleRef:
317 apiGroup: rbac.authorization.k8s.io
318 kind: ClusterRole
319 name: cluster-admin
320subjects:
321- kind: User
322 name: admin
323"#;
324 let objects = parse_yaml(yaml).unwrap();
325 let check = ClusterAdminRoleBindingCheck;
326 let diagnostics = check.check(&objects[0]);
327 assert_eq!(diagnostics.len(), 1);
328 assert!(diagnostics[0].message.contains("cluster-admin"));
329 }
330
331 #[test]
332 fn test_non_admin_binding_ok() {
333 let yaml = r#"
334apiVersion: rbac.authorization.k8s.io/v1
335kind: ClusterRoleBinding
336metadata:
337 name: viewer-binding
338roleRef:
339 apiGroup: rbac.authorization.k8s.io
340 kind: ClusterRole
341 name: view
342subjects:
343- kind: User
344 name: viewer
345"#;
346 let objects = parse_yaml(yaml).unwrap();
347 let check = ClusterAdminRoleBindingCheck;
348 let diagnostics = check.check(&objects[0]);
349 assert!(diagnostics.is_empty());
350 }
351
352 #[test]
353 fn test_wildcard_verbs_detected() {
354 let yaml = r#"
355apiVersion: rbac.authorization.k8s.io/v1
356kind: ClusterRole
357metadata:
358 name: wildcard-role
359rules:
360- apiGroups: [""]
361 resources: ["pods"]
362 verbs: ["*"]
363"#;
364 let objects = parse_yaml(yaml).unwrap();
365 let check = WildcardInRulesCheck;
366 let diagnostics = check.check(&objects[0]);
367 assert_eq!(diagnostics.len(), 1);
368 assert!(diagnostics[0].message.contains("verbs"));
369 }
370
371 #[test]
372 fn test_access_to_secrets_detected() {
373 let yaml = r#"
374apiVersion: rbac.authorization.k8s.io/v1
375kind: ClusterRole
376metadata:
377 name: secret-reader
378rules:
379- apiGroups: [""]
380 resources: ["secrets"]
381 verbs: ["get", "list"]
382"#;
383 let objects = parse_yaml(yaml).unwrap();
384 let check = AccessToSecretsCheck;
385 let diagnostics = check.check(&objects[0]);
386 assert_eq!(diagnostics.len(), 1);
387 assert!(diagnostics[0].message.contains("secrets"));
388 }
389
390 #[test]
391 fn test_pod_create_detected() {
392 let yaml = r#"
393apiVersion: rbac.authorization.k8s.io/v1
394kind: Role
395metadata:
396 name: pod-creator
397 namespace: default
398rules:
399- apiGroups: [""]
400 resources: ["pods"]
401 verbs: ["create"]
402"#;
403 let objects = parse_yaml(yaml).unwrap();
404 let check = AccessToCreatePodsCheck;
405 let diagnostics = check.check(&objects[0]);
406 assert_eq!(diagnostics.len(), 1);
407 assert!(diagnostics[0].message.contains("create pods"));
408 }
409}