kubenumerate 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ExtensiveRoleCheck.py ADDED
@@ -0,0 +1,449 @@
1
+ import json
2
+ import argparse
3
+ import logging
4
+ from colorama import init, Fore
5
+
6
+
7
+ def str2bool(v):
8
+ if isinstance(v, bool):
9
+ return v
10
+ if v.lower() in ("yes", "true", "t", "y", "1"):
11
+ return True
12
+ elif v.lower() in ("no", "false", "f", "n", "0"):
13
+ return False
14
+ else:
15
+ raise argparse.ArgumentTypeError("Boolean value expected.")
16
+
17
+
18
+ def get_argument_parser():
19
+ parser = argparse.ArgumentParser()
20
+ parser.add_argument(
21
+ "--clusterRole",
22
+ type=str,
23
+ required=False,
24
+ help="ClusterRoles JSON file",
25
+ )
26
+ parser.add_argument("--role", type=str, required=False, help="roles JSON file")
27
+ parser.add_argument("--rolebindings", type=str, required=False, help="RoleBindings JSON file")
28
+ parser.add_argument("--clusterrolebindings", type=str, required=False, help="ClusterRoleBindings JSON file")
29
+ parser.add_argument("--pods", type=str, required=False, help="pods JSON file")
30
+ parser.add_argument("--outputjson", type=str, required=False, help="Output JSON file")
31
+
32
+ return parser.parse_args()
33
+
34
+
35
+ # Read data from files
36
+ def open_file(file_path):
37
+ with open(file_path) as f:
38
+ return json.load(f)
39
+
40
+
41
+ class ExtensiveRolesChecker(object):
42
+ def __init__(self, json_file, role_kind):
43
+ init()
44
+ self._role = logging.getLogger(role_kind)
45
+ self._role_handler = logging.StreamHandler()
46
+ self._role_format = logging.Formatter(f"{Fore.YELLOW}[!][%(name)s]{Fore.WHITE}\u2192 %(message)s")
47
+ self._role_handler.setFormatter(self._role_format)
48
+ self._role.addHandler(self._role_handler)
49
+ self._json_file = json_file
50
+ self._results = {}
51
+ self._generate()
52
+
53
+ @property
54
+ def results(self):
55
+ return self._results
56
+
57
+ def add_result(self, name, value):
58
+ if not name:
59
+ return
60
+ if not (name in self._results.keys()):
61
+ self._results[name] = [value]
62
+ else:
63
+ self._results[name].append(value)
64
+
65
+ def _generate(self):
66
+ for entity in self._json_file["items"]:
67
+ role_name = entity["metadata"]["name"]
68
+ try:
69
+ iter(entity["rules"])
70
+ except TypeError:
71
+ continue
72
+ for rule in entity["rules"]:
73
+ if not rule.get("resources", None):
74
+ continue
75
+ self.get_read_secrets(rule, role_name)
76
+ self.get_read_configmaps(rule, role_name)
77
+ self.clusteradmin_role(rule, role_name)
78
+ self.any_resources(rule, role_name)
79
+ self.any_verb(rule, role_name)
80
+ self.high_risk_roles(rule, role_name)
81
+ self.role_and_roleBindings(rule, role_name)
82
+ self.create_pods(rule, role_name)
83
+ self.pods_exec(rule, role_name)
84
+ self.pods_attach(rule, role_name)
85
+
86
+ # Read cluster secrets:
87
+ def get_read_secrets(self, rule, role_name):
88
+ verbs = ["*", "get", "list"]
89
+ if "secrets" in rule["resources"] and any([sign for sign in verbs if sign in rule["verbs"]]):
90
+ filtered_name = self.get_non_default_name(role_name)
91
+ if filtered_name:
92
+ self._role.warning(f"{Fore.GREEN}{filtered_name}" + f"{Fore.RED} Has permission to list secrets!")
93
+ self.add_result(filtered_name, "Has permission to list secrets!")
94
+
95
+ # Read configmaps, devs store creds in configmaps all the time
96
+ def get_read_configmaps(self, rule, role_name):
97
+ verbs = ["*", "get", "list"]
98
+ if "configmaps" in rule["resources"] and any([sign for sign in verbs if sign in rule["verbs"]]):
99
+ filtered_name = self.get_non_default_name(role_name)
100
+ if filtered_name:
101
+ self._role.warning(f"{Fore.GREEN}{filtered_name}" + f"{Fore.RED} Has permission to list configmaps!")
102
+ self.add_result(filtered_name, "Has permission to list configmaps!")
103
+
104
+ # Any Any roles
105
+ def clusteradmin_role(self, rule, role_name):
106
+ if "*" in rule["resources"] and "*" in rule["verbs"]:
107
+ filtered_name = self.get_non_default_name(role_name)
108
+ if filtered_name:
109
+ self._role.warning(f"{Fore.GREEN}{filtered_name}" + f"{Fore.RED} Has Admin-Cluster permission!")
110
+ self.add_result(filtered_name, "Has Admin-Cluster permission!")
111
+
112
+ # get ANY verbs:
113
+ def any_verb(self, rule, role_name):
114
+ resources = [
115
+ "secrets",
116
+ "configmaps",
117
+ "pods",
118
+ "deployments",
119
+ "daemonsets",
120
+ "statefulsets",
121
+ "replicationcontrollers",
122
+ "replicasets",
123
+ "cronjobs",
124
+ "jobs",
125
+ "roles",
126
+ "clusterroles",
127
+ "rolebindings",
128
+ "clusterrolebindings",
129
+ "users",
130
+ "groups",
131
+ ]
132
+ found_sign = [sign for sign in resources if sign in rule["resources"]]
133
+ if not found_sign:
134
+ return
135
+ if "*" in rule["verbs"]:
136
+ filtered_name = self.get_non_default_name(role_name)
137
+ if filtered_name:
138
+ self._role.warning(
139
+ f"{Fore.GREEN}{filtered_name}"
140
+ + f"{Fore.RED} Has permission to access {found_sign[0]} with any verb!"
141
+ )
142
+ self.add_result(filtered_name, f"Has permission to access {found_sign[0]} with any verb!")
143
+
144
+ def any_resources(self, rule, role_name):
145
+ verbs = ["delete", "deletecollection", "create", "list", "get", "impersonate"]
146
+ found_sign = [sign for sign in verbs if sign in rule["verbs"]]
147
+ if not found_sign:
148
+ return
149
+ if "*" in rule["resources"]:
150
+ filtered_name = self.get_non_default_name(role_name)
151
+ if filtered_name:
152
+ self._role.warning(
153
+ f"{Fore.GREEN}{filtered_name}"
154
+ + f"{Fore.RED} Has permission to use {found_sign[0]} on any resource!"
155
+ )
156
+ self.add_result(filtered_name, f"Has permission to use {found_sign[0]} on any resource")
157
+
158
+ def high_risk_roles(self, rule, role_name):
159
+ verb_actions = ["create", "update"]
160
+ # adding pods to cover privilege pod scenarios and pods being used for mining example
161
+ resources_attributes = [
162
+ "pods",
163
+ "deployments",
164
+ "daemonsets",
165
+ "statefulsets",
166
+ "replicationcontrollers",
167
+ "replicasets",
168
+ "jobs",
169
+ "cronjobs",
170
+ ]
171
+ found_attribute = [attribute for attribute in resources_attributes if attribute in rule["resources"]]
172
+ if not (found_attribute):
173
+ return
174
+ found_actions = [action for action in verb_actions if action in rule["verbs"]]
175
+ if not (found_actions):
176
+ return
177
+ filtered_name = self.get_non_default_name(role_name)
178
+ if filtered_name:
179
+ self._role.warning(
180
+ f"{Fore.GREEN}{filtered_name}"
181
+ + f"{Fore.RED} Has permission to {found_actions[0]} {found_attribute[0]}!"
182
+ )
183
+ self.add_result(filtered_name, f"Has permission to {found_actions[0]} {found_attribute[0]}!")
184
+
185
+ def role_and_roleBindings(self, rule, role_name):
186
+ resources_attributes = ["rolebindings", "roles", "clusterrolebindings"]
187
+ found_attribute = [attribute for attribute in resources_attributes if attribute in rule["resources"]]
188
+ if not found_attribute:
189
+ return
190
+ if "create" in rule["verbs"]:
191
+ filtered_name = self.get_non_default_name(role_name)
192
+ if filtered_name:
193
+ self._role.warning(
194
+ f"{Fore.GREEN}{filtered_name}" + f"{Fore.RED} Has permission to create {found_attribute[0]}!"
195
+ )
196
+ self.add_result(filtered_name, f"Has permission to create {found_attribute[0]}!")
197
+
198
+ def create_pods(self, rule, role_name):
199
+ if "pods" in rule["resources"] and "create" in rule["verbs"]:
200
+ filtered_name = self.get_non_default_name(role_name)
201
+ if filtered_name:
202
+ self._role.warning(f"{Fore.GREEN}{filtered_name}" + f"{Fore.RED} Has permission to create pods!")
203
+ self.add_result(filtered_name, "Has permission to create pods!")
204
+
205
+ def pods_exec(self, rule, role_name):
206
+ if "pods/exec" in rule["resources"] and "create" in rule["verbs"]:
207
+ filtered_name = self.get_non_default_name(role_name)
208
+ if filtered_name:
209
+ self._role.warning(f"{Fore.GREEN}{filtered_name}" + f"{Fore.RED} Has permission to use pod exec!")
210
+ self.add_result(filtered_name, "Has permission to use pod exec!")
211
+
212
+ def pods_attach(self, rule, role_name):
213
+ if "pods/attach" in rule["resources"] and "create" in rule["verbs"]:
214
+ filtered_name = self.get_non_default_name(role_name)
215
+ if filtered_name:
216
+ self._role.warning(f"{Fore.GREEN}{filtered_name}" + f"{Fore.RED} Has permission to attach pods!")
217
+ self.add_result(filtered_name, "Has permission to attach pods!")
218
+
219
+ @staticmethod
220
+ def get_non_default_name(name):
221
+ if not (
222
+ (name[:7] == "system:")
223
+ or (name == "edit")
224
+ or (name == "admin")
225
+ or (name == "cluster-admin")
226
+ or (name == "aws-node")
227
+ or (name[:11] == "kubernetes-")
228
+ ):
229
+ return name
230
+
231
+
232
+ class roleBindingChecker(object):
233
+ def __init__(self, json_file, extensive_roles, bind_kind):
234
+ self._json_file = json_file
235
+ self._extensive_roles = extensive_roles
236
+ self._bind_kind = bind_kind
237
+ self._results = []
238
+ self.subject_risky_roles_mapping = []
239
+ self.bindsCheck()
240
+
241
+ def bindsCheck(self):
242
+ _rolebinding_found = []
243
+ for entity in self._json_file["items"]:
244
+ _role_name = entity["metadata"]["name"]
245
+ _rol_ref = entity["roleRef"]["name"]
246
+ if not entity.get("subjects", None):
247
+ continue
248
+ if _rol_ref in self._extensive_roles:
249
+ _rolebinding_found.append(_rol_ref)
250
+ for sub in entity["subjects"]:
251
+ if not sub.get("name", None):
252
+ continue
253
+ self.add_role_binding_mapping(sub, entity)
254
+ self.print_rolebinding_results(sub, _role_name, self._bind_kind)
255
+ return _rolebinding_found
256
+
257
+ def print_rolebinding_results(self, sub, role_name, bind_kind):
258
+ if sub["kind"] == "ServiceAccount":
259
+ print(
260
+ f"{Fore.YELLOW}[!][{bind_kind}]{Fore.WHITE}\u2192 "
261
+ + f'{Fore.GREEN}{role_name}{Fore.RED} is bound to {sub["name"]} ServiceAccount.'
262
+ )
263
+ else:
264
+ print(
265
+ f"{Fore.YELLOW}[!][{bind_kind}]{Fore.WHITE}\u2192 "
266
+ + f'{Fore.GREEN}{role_name}{Fore.RED} is bound to the {sub["kind"]}: {sub["name"]}!'
267
+ )
268
+
269
+ def add_role_binding_mapping(self, sub, binding):
270
+ element = next((item for item in self.subject_risky_roles_mapping if item["name"] == sub.get("name")), None)
271
+
272
+ if element is None:
273
+ element = {"kind": sub.get("kind"), "name": sub.get("name"), "riskyRoles": []}
274
+ self.subject_risky_roles_mapping.append(element)
275
+
276
+ element.get("riskyRoles").append(
277
+ {
278
+ "apiGroup": binding.get("roleRef", {}).get("apiGroup", {}),
279
+ "kind": binding.get("roleRef", {}).get("kind", {}),
280
+ "name": binding.get("roleRef", {}).get("name", {}),
281
+ "bindingKind": binding.get("kind", ""),
282
+ "bindingName": binding.get("metadata", {}).get("name"),
283
+ }
284
+ )
285
+
286
+
287
+ class SubjectViewer:
288
+ def __init__(self, subject_risky_roles_mapping, checker, all_pods=None):
289
+ self.subject_risky_roles_mapping = subject_risky_roles_mapping
290
+ self.checker = checker
291
+ self.all_pods = all_pods
292
+ self.__prepare_json()
293
+
294
+ def print_risky_roles_for_subjects(self):
295
+ for subject in self.subject_risky_roles_mapping:
296
+ print("{color}{kind}: {name}".format(color=Fore.YELLOW, kind=subject.get("kind"), name=subject.get("name")))
297
+
298
+ for role in subject.get("riskyRoles"):
299
+ for permission in role.get("riskyRolePermissions"):
300
+ self.__print_risky_permission(role, permission.get("permission"))
301
+
302
+ if self.all_pods is not None and subject.get("kind") == "ServiceAccount":
303
+ self.__print_pods_using_service_account(subject.get("podUsingServiceAccount"))
304
+
305
+ def get_json(self):
306
+ return self.subject_risky_roles_mapping
307
+
308
+ def __prepare_json(self):
309
+ for subject in self.subject_risky_roles_mapping:
310
+ for role in subject.get("riskyRoles"):
311
+ role["riskyRolePermissions"] = []
312
+ risky_role_permissions = self.checker.results.get(role.get("name"))
313
+
314
+ if risky_role_permissions is None:
315
+ continue
316
+
317
+ for permission in risky_role_permissions:
318
+ role.get("riskyRolePermissions").append(
319
+ {
320
+ "bindingKind": role.get("bindingKind"),
321
+ "bindingName": role.get("bindingName"),
322
+ "kind": role.get("kind"),
323
+ "name": role.get("name"),
324
+ "permission": permission,
325
+ }
326
+ )
327
+
328
+ if self.all_pods is not None and subject.get("kind") == "ServiceAccount":
329
+ subject["podUsingServiceAccount"] = []
330
+
331
+ for pod in self.__get_pods_for_service_account(subject.get("name")):
332
+ pod_details = {"name": pod.get("metadata").get("name")}
333
+
334
+ for key, value in self.__get_pod_metadata(pod).items():
335
+ pod_details[key] = value
336
+
337
+ subject["podUsingServiceAccount"].append(pod_details)
338
+
339
+ @classmethod
340
+ def __print_risky_permission(cls, role, permission):
341
+ binding_kind = role.get("bindingKind")
342
+ binding_name = role.get("bindingName")
343
+
344
+ role_name = role.get("name")
345
+ role_kind = role.get("kind")
346
+
347
+ print(
348
+ f" {Fore.RED}{permission}{Fore.WHITE} \u2192 "
349
+ + f"{Fore.WHITE}[{binding_kind}] {binding_name}{Fore.WHITE} \u2192 "
350
+ + f"{Fore.GREEN}[{role_kind}] {role_name} {Fore.RED}"
351
+ )
352
+
353
+ @classmethod
354
+ def __print_pods_using_service_account(cls, pods):
355
+ if len(pods) == 0:
356
+ return
357
+
358
+ for pod in pods:
359
+ text = ""
360
+
361
+ for key, value in pod.items():
362
+ color = Fore.GREEN if key == "name" else Fore.WHITE
363
+ text = f" {Fore.WHITE}Used in: " if text == "" else text + " / "
364
+ text += f'{color}[{key if key != "name" else "pod"}] {value}'
365
+
366
+ print(text)
367
+
368
+ @classmethod
369
+ def __get_pods_for_service_account(cls, service_account_name):
370
+ pods_json_file = open_file(f"{args.pods}")
371
+ pods_to_check = pods_json_file.get("items", [])
372
+ return [x for x in pods_to_check if x.get("spec", {}).get("serviceAccountName", "") == service_account_name]
373
+
374
+ @classmethod
375
+ def __get_pod_metadata(cls, pod):
376
+ metadata = {"namespace": pod.get("metadata").get("namespace")}
377
+
378
+ owner_references = pod.get("metadata").get("ownerReferences")
379
+
380
+ if pod.get("metadata").get("labels").get("app", "") != "":
381
+ metadata["app"] = pod.get("metadata").get("labels").get("app", "")
382
+
383
+ if owner_references is not None and len(owner_references) > 0:
384
+ metadata[owner_references[0].get("kind")] = owner_references[0].get("name")
385
+
386
+ if pod.get("metadata").get("labels").get("heritage", "") == "Helm":
387
+ metadata["helmChart"] = pod.get("metadata").get("labels").get("chart")
388
+
389
+ return metadata
390
+
391
+
392
+ if __name__ == "__main__":
393
+ args = get_argument_parser()
394
+ if args.clusterRole:
395
+ print("\n[*] Started enumerating risky ClusterRoles:")
396
+ role_kind = "ClusterRole"
397
+ clusterRole_json_file = open_file(args.clusterRole)
398
+ extensiveClusterRolesChecker = ExtensiveRolesChecker(clusterRole_json_file, role_kind)
399
+ extensive_ClusterRoles = [result for result in extensiveClusterRolesChecker.results]
400
+
401
+ if args.role:
402
+ print(f"{Fore.WHITE}[*] Started enumerating risky Roles:")
403
+ role_kind = "Role"
404
+ Role_json_file = open_file(args.role)
405
+ extensiveRolesChecker = ExtensiveRolesChecker(Role_json_file, role_kind)
406
+ extensive_roles = [result for result in extensiveRolesChecker.results if result not in extensive_ClusterRoles]
407
+ extensive_roles = extensive_roles + extensive_ClusterRoles
408
+
409
+ if args.clusterrolebindings:
410
+ print(f"{Fore.WHITE}[*] Started enumerating risky ClusterRoleBinding:")
411
+ bind_kind = "ClusterRoleBinding"
412
+ clusterRoleBinding_json_file = open_file(args.clusterrolebindings)
413
+ extensive_clusterRoleBindings = roleBindingChecker(clusterRoleBinding_json_file, extensive_roles, bind_kind)
414
+
415
+ if args.rolebindings:
416
+ print(f"{Fore.WHITE}[*] Started enumerating risky RoleBindings:")
417
+ bind_kind = "RoleBinding"
418
+ RoleBinding_json_file = open_file(args.rolebindings)
419
+ extensive_RoleBindings = roleBindingChecker(RoleBinding_json_file, extensive_roles, bind_kind)
420
+
421
+ pods = open_file(args.pods) if args.pods else None
422
+
423
+ if (args.role and args.rolebindings) or args.clusterRole and args.clusterrolebindings:
424
+ print(f"{Fore.WHITE}[*] Started enumerating risky subjects:")
425
+
426
+ # Implemented out_path for Kubenumerate to control where the output goes, and not to dump it to cwd
427
+ out_path = ""
428
+ if args.outputjson is not None:
429
+ out_path = args.outputjson
430
+
431
+ if args.role and args.rolebindings:
432
+ subject_viewer = SubjectViewer(extensive_RoleBindings.subject_risky_roles_mapping, extensiveRolesChecker, pods)
433
+ subject_viewer.print_risky_roles_for_subjects()
434
+
435
+ if args.outputjson:
436
+ text_file = open(f"{out_path}role_audit.json", "w")
437
+ text_file.write(json.dumps(subject_viewer.get_json()))
438
+ text_file.close()
439
+
440
+ if args.clusterRole and args.clusterrolebindings:
441
+ subject_viewer_cluster_level = SubjectViewer(
442
+ extensive_clusterRoleBindings.subject_risky_roles_mapping, extensiveClusterRolesChecker, pods
443
+ )
444
+ subject_viewer_cluster_level.print_risky_roles_for_subjects()
445
+
446
+ if args.outputjson:
447
+ text_file = open(f"{out_path}cluster_role_audit.json", "w")
448
+ text_file.write(json.dumps(subject_viewer_cluster_level.get_json()))
449
+ text_file.close()
formatter.py ADDED
@@ -0,0 +1,60 @@
1
+ import pandas as pd
2
+
3
+ # try for now only with the first sheet
4
+ df = pd.read_excel("kubenumerate_results_v1_0.xlsx", "Capabilities - Added")
5
+ # https://stackoverflow.com/questions/26521266/using-pandas-to-pd-read-excel-for-multiple-worksheets-of-the-same-workbook
6
+ # xls = pd.ExcelFile("kubenumerate_results_v1_0.xlsx")
7
+ # xls.sheet_names # list of sheets
8
+
9
+ with pd.ExcelWriter("enhanced.xlsx", engine="xlsxwriter", mode="w") as writer:
10
+ # df.to_excel(writer, index=False, sheet_name="sheet one")
11
+ workbook = writer.book
12
+ worksheet = workbook.add_worksheet("Capabilities - Added")
13
+ worksheet.set_zoom(90)
14
+
15
+ worksheet.set_column(0, len(df.columns) - 1, 20)
16
+ header_format = workbook.add_format({
17
+ 'font_name': 'Calibri', # Default right now, but including in case changes in the future
18
+ 'bg_color': '#A93545',
19
+ 'bold': True,
20
+ 'font_color': 'white',
21
+ 'align': 'left',
22
+ })
23
+
24
+ title = "Capabilities - Added"
25
+ # Merge cells
26
+ title_format = workbook.add_format({
27
+ 'font_name': 'Calibri',
28
+ 'bg_color': '#A93545',
29
+ 'font_color': 'white',
30
+ 'font_size': 20,
31
+ })
32
+ #
33
+ subtitle = "Capabilities (specifically, Linux capabilities), are used for permission management in Linux. Some capabilities are enabled by default."
34
+ # subtitle = "AppArmor is enabled by adding container.apparmor.security.beta.kubernetes.io/[container name] as a pod-level annotation and setting its value to either runtime/default or a profile (localhost/[profile name])."
35
+ # note down how many cells title and subheader require
36
+ worksheet.merge_range('A1:AC1', title, title_format)
37
+ worksheet.merge_range('A2:AC2', subtitle)
38
+ worksheet.set_row(2, 15) # row height to 15
39
+ df = df.rename(columns={
40
+ "ResourceNamespace": "Resource Namespace",
41
+ "ResourceKind": "Resource Kind",
42
+ "ResourceName": "Resource Name",
43
+ "Container": "Affected Container",
44
+ "Metadata": "Metadata",
45
+ "msg": "Recommendation",
46
+ })
47
+ for col_num, value in enumerate(df.columns.values):
48
+ worksheet.write(2, col_num, value, header_format)
49
+ worksheet.freeze_panes(3, 0)
50
+ bg_format1 = workbook.add_format({'bg_color': '#E2E2E2'})
51
+ bg_format2 = workbook.add_format({'bg_color': 'white'}) # white cell background color
52
+
53
+ skip_three = 3
54
+ for row in range(df.shape[0] + 3):
55
+ if skip_three > 0:
56
+ skip_three -= 1
57
+ continue
58
+ worksheet.set_row(row, cell_format=(bg_format1 if row % 2 == 0 else bg_format2))
59
+
60
+ df.to_excel(writer, index=False, sheet_name="Capabilities - Added", startrow=3, header=False)