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