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 && 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
70pub 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 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 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 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
155pub 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 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 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
234pub 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 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}