syncable_cli/analyzer/kubelint/templates/
rbac.rs

1//! RBAC-related check templates.
2
3use 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
8/// Template for detecting cluster-admin role bindings.
9pub 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
69/// Template for detecting wildcard rules in RBAC.
70pub 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                // Check for wildcard in verbs
116                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                // Check for wildcard in resources
127                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                // Check for wildcard in apiGroups
138                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
154/// Template for detecting access to secrets.
155pub 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                // Check if rule grants access to secrets
201                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                    // Check for sensitive verbs
210                    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
233/// Template for detecting access to create pods.
234pub 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                // Check if rule grants create access to pods
280                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}