mantis_api_client 5.5.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.
@@ -0,0 +1,1204 @@
1
+ # -*- coding: utf-8 -*-
2
+ import argparse
3
+ import datetime
4
+ import json
5
+ import sys
6
+ import time
7
+ import traceback
8
+ from typing import Any
9
+ from typing import Dict
10
+ from typing import List
11
+
12
+ from mantis_common_model.pagination import Pagination
13
+ from mantis_scenario_model.api_scenario_model import LabListReply
14
+ from mantis_scenario_model.lab_config_model import ContentType
15
+ from mantis_scenario_model.lab_config_model import LabConfig
16
+ from mantis_scenario_model.lab_model import Lab
17
+ from ruamel.yaml import YAML
18
+
19
+ from cr_api_client import redteam_api # type: ignore
20
+ from mantis_api_client import scenario_api
21
+ from mantis_api_client import user_api
22
+ from mantis_api_client.cli_parser.lab_parser import set_current_lab
23
+ from mantis_api_client.cli_parser.lab_parser import unset_current_lab
24
+ from mantis_api_client.cli_parser.redteam_parser import redteam_atomic_handler
25
+ from mantis_api_client.cli_parser.redteam_parser import redteam_command_execute_handler
26
+ from mantis_api_client.cli_parser.redteam_parser import redteam_command_get_handler
27
+ from mantis_api_client.cli_parser.redteam_parser import redteam_command_history_handler
28
+ from mantis_api_client.oidc import get_oidc_client
29
+ from mantis_api_client.utils import colored
30
+ from mantis_api_client.utils import wait_lab
31
+
32
+
33
+ def show_bas_info(lab_id) -> None:
34
+ # Show available BAS agents from lab infrastructure
35
+ active_profile_domain = get_oidc_client().get_active_profile_domain(raise_exc=False)
36
+ if not active_profile_domain:
37
+ print("[+] Not authenticated")
38
+ return
39
+
40
+ print("[+] \033[1mBAS lab infrastructure endpoint\033[0m:")
41
+ print(
42
+ f" - https://app.{active_profile_domain}/proxy/ (must be reachable from the agent's execution environment)"
43
+ )
44
+
45
+ # # Show BAS agent download URLs
46
+ # if len(redteam_api.attack_knowledge()["payloads"]) > 0:
47
+ # print("[+] \033[1mAvailable agents\033[0m:")
48
+
49
+ # for payload in redteam_api.attack_knowledge()["payloads"]:
50
+
51
+ # if payload["payload_type"] == "beacon":
52
+ # print(
53
+ # f" - \033[1m{payload['payload_os']}_agent\033[0m: https://app.{active_profile_domain}/proxy/{lab_id}/http/nginxserver/{payload['name']}"
54
+ # )
55
+
56
+ # Show BAS agent sessions, if any
57
+ attack_sessions = redteam_api.attack_sessions()
58
+ if len(attack_sessions) > 0:
59
+ print("[+] \033[1mAgent sessions\033[0m:")
60
+
61
+ for session in attack_sessions:
62
+ print(
63
+ f" - \033[1msession_id\033[0m: {session['identifier']} - \033[1mhost\033[0m: {session['target']['host']['hostname']} - \033[1mhost_IP\033[0m: {session['target']['ip']} - \033[1musername\033[0m: {session['username']} - \033[1msession_type\033[0m: {session['type']} - \033[1mprivilege_level\033[0m: {session['privilege_level']} - \033[1mactive\033[0m: {session['is_up']}"
64
+ )
65
+
66
+ print(
67
+ f" [hint] You can retrieve available attacks with: '$ mantis bas attack list -s {session['identifier']}'"
68
+ )
69
+
70
+
71
+ def is_agent_up(last_ping: str) -> bool:
72
+ last_ping_dt = datetime.datetime.fromisoformat(last_ping)
73
+ now = datetime.datetime.utcnow()
74
+ delta = (now - last_ping_dt).total_seconds()
75
+ return delta < 60
76
+
77
+
78
+ #
79
+ # 'bas_create_handler' handler
80
+ #
81
+ def bas_create_handler(args: Any) -> None: # noqa:C901
82
+ # Force launching BAS lab infrastructure
83
+ do_run: bool = True
84
+
85
+ # Manage lab configuration
86
+ lab_config_path = args.lab_config_path
87
+ if lab_config_path is None:
88
+ lab_config_dict = {}
89
+ else:
90
+ with open(lab_config_path, "r") as fd:
91
+ yaml_content = fd.read()
92
+ loader = YAML(typ="rt")
93
+ lab_config_dict = loader.load(yaml_content)
94
+ lab_config = LabConfig(**lab_config_dict)
95
+
96
+ if not args.workspace_id:
97
+ try:
98
+ workspace_id = get_oidc_client().get_default_workspace(raise_exc=True)
99
+ except Exception as e:
100
+ print(colored(f"Error when fetching BAS API: '{e}'", "red"))
101
+ sys.exit(1)
102
+ else:
103
+ workspace_id = args.workspace_id
104
+
105
+ # Retrieve associated workspace id
106
+ if workspace_id is None:
107
+ print(
108
+ colored(
109
+ "Your subscription level does not permit to launch BAS campaigns", "red"
110
+ )
111
+ )
112
+ sys.exit(1)
113
+ workspace = user_api.fetch_current_workspaces()
114
+ organization_id = workspace[0]["organization_id"]
115
+
116
+ agents = scenario_api.fetch_agents(organization_id=organization_id)
117
+
118
+ if not args.agent_id:
119
+ print("[+] \033[1mSelect the agent on which to run the BAS campaign\033[0m: ")
120
+ i = 0
121
+ agents_up = []
122
+ for agent in agents:
123
+ if is_agent_up(agent["last_ping"]):
124
+ agents_up.append(agent)
125
+
126
+ if len(agents_up) == 0:
127
+ print(colored("No agents are online", "red"))
128
+ sys.exit(1)
129
+
130
+ for agent in agents_up:
131
+ print(
132
+ f" - \033[1m[{i}] \033[0m\033[1mID\033[0m: {agent['id']:<30} - \033[1mIP\033[0m: {agent['host']['ip']:<18} - \033[1mMAC\033[0m: {agent['host']['mac']:<30}"
133
+ )
134
+ i = i + 1
135
+
136
+ while True:
137
+ index = input("> ")
138
+ if index.isdigit() and int(index) < len(agents_up):
139
+ break
140
+ else:
141
+ print(
142
+ colored(
143
+ f"Incorrect choice, must be between 0 - {len(agents_up) - 1}",
144
+ "red",
145
+ )
146
+ )
147
+
148
+ agent = agents_up[int(index)]
149
+ print(f"[+] \033[1mYou chose agent {agents_up[int(index)]['id']}\033[0m")
150
+ else:
151
+ agent = next((agent for agent in agents if agent["id"] == args.agent_id), None)
152
+ if not agent:
153
+ print(colored(f"Error agent {args.agent_id} doesn't exist.", "red"))
154
+ sys.exit(1)
155
+
156
+ if not args.privilege:
157
+ print("[+] \033[1mInstall beacon with admin right ?\033[0m: ")
158
+ action_type = "install"
159
+ while True:
160
+ admin = input("y/n(Default) > ")
161
+ if admin == "y":
162
+ action_type = "install_admin"
163
+ break
164
+ elif admin == "n" or admin == "":
165
+ break
166
+ else:
167
+ print(colored("Incorrect choice, must be y or n.", "red"))
168
+ else:
169
+ if args.privilege == "admin":
170
+ action_type = "install_admin"
171
+ elif args.privilege == "user":
172
+ action_type = "user"
173
+ else:
174
+ print(
175
+ colored(
176
+ "The privilege argument only accepts the values admin or user",
177
+ "red",
178
+ )
179
+ )
180
+ sys.exit(1)
181
+
182
+ # Launch lab
183
+ try:
184
+
185
+ if do_run:
186
+ lab_id = scenario_api.run_lab_bas(lab_config, workspace_id)
187
+ else:
188
+ lab_id = scenario_api.create_lab_bas(lab_config, workspace_id)
189
+
190
+ print(f"[+] \033[1mBAS campaign ID\033[0m: {lab_id}")
191
+ print("[+] Launching BAS lab infrastructure")
192
+
193
+ if do_run:
194
+ # In BAS mode, stop wait loop after attack provisioning
195
+ wait_lab(
196
+ lab_id,
197
+ quiet=True,
198
+ )
199
+
200
+ except Exception as e:
201
+ print(colored(f"Error when creating BAS campaign: '{e}'", "red"))
202
+ print(traceback.format_exc())
203
+ sys.exit(1)
204
+
205
+ # Set current lab
206
+ set_current_lab(lab_id)
207
+ try:
208
+ if len(redteam_api.attack_knowledge()["payloads"]) > 0:
209
+ active_profile_domain = get_oidc_client().get_active_profile_domain(
210
+ raise_exc=False
211
+ )
212
+ for payload in redteam_api.attack_knowledge()["payloads"]:
213
+
214
+ if (
215
+ payload["payload_type"] == "beacon"
216
+ and payload["payload_os"] == "windows"
217
+ ):
218
+ url = f"https://app.{active_profile_domain}/proxy/{lab_id}/http/nginxserver/{payload['name']}"
219
+ scenario_api.add_order_agents(
220
+ agent_id=agent["id"],
221
+ action_type=action_type,
222
+ content=url + "," + lab_id,
223
+ organization_id=organization_id,
224
+ )
225
+ print(f"Waiting for attack session created on agent {agent['id']} ...")
226
+ limit = 0
227
+ while len(redteam_api.attack_sessions()) < 1 and limit < 60:
228
+ time.sleep(1)
229
+ limit = limit + 1
230
+ show_bas_info(lab_id)
231
+
232
+ finally:
233
+ unset_current_lab()
234
+
235
+ print(
236
+ f"[hint] Once an agent has been executed, retrieve its session with: '$ mantis bas info {lab_id}'"
237
+ )
238
+
239
+
240
+ def fetch_bas_labs(all_labs: bool = False) -> List[Lab]:
241
+ """Return BAS labs.
242
+
243
+ By default, only return active campaigns.
244
+
245
+ If 'all_labs' is set to True, also returns non active campaigns
246
+ (i.e. lab infrastructure is not running)
247
+
248
+ """
249
+
250
+ try:
251
+ offset = 100
252
+ labs_to_list: List[Lab] = []
253
+ page: int = 1
254
+
255
+ while True:
256
+ pagination: Pagination = Pagination(**{"page": page, "limit": offset})
257
+ labs: LabListReply = scenario_api.fetch_labs(
258
+ all_labs=all_labs, pagination=pagination
259
+ )
260
+ labs_to_list.extend(
261
+ [
262
+ Lab(**lab)
263
+ for lab in labs["data"]
264
+ if lab["content_type"] == ContentType.BAS
265
+ ]
266
+ )
267
+
268
+ if page * offset >= labs["pagination"]["total_records"]:
269
+ break
270
+
271
+ page += 1
272
+
273
+ except Exception as e:
274
+ print(f"Error when fetching labs: '{e}'")
275
+ sys.exit(1)
276
+
277
+ return labs_to_list
278
+
279
+
280
+ #
281
+ # 'bas_list_handler' handler
282
+ #
283
+
284
+
285
+ def bas_list_handler(args: Any) -> None:
286
+ all_labs = args.all_labs
287
+
288
+ bas_labs: List[Lab] = fetch_bas_labs(all_labs=all_labs)
289
+
290
+ print("[+] BAS campaigns:")
291
+ for lab in bas_labs:
292
+ lab_creation_timestamp = (
293
+ datetime.datetime.fromtimestamp(
294
+ lab.lab_creation_timestamp, datetime.timezone.utc
295
+ ).strftime("%Y-%m-%d %H:%M:%S")
296
+ + " UTC"
297
+ )
298
+
299
+ print(f" [+] \033[1mID\033[0m: {lab.runner_id}")
300
+ print(f" - \033[1mCampaign creation time\033[0m: {lab_creation_timestamp}")
301
+ print(f" - \033[1mStatus\033[0m: {lab.status}")
302
+
303
+
304
+ #
305
+ # 'bas_info_handler' handler
306
+ #
307
+
308
+
309
+ def bas_info_handler(args: Any) -> None:
310
+ # Parameters
311
+ campaign_id = args.campaign_id
312
+
313
+ # Set current lab
314
+ set_current_lab(campaign_id)
315
+ try:
316
+ show_bas_info(campaign_id)
317
+ finally:
318
+ unset_current_lab()
319
+
320
+
321
+ #
322
+ # 'bas_agents_handler' handler
323
+ #
324
+
325
+
326
+ def bas_agents_handler(args: Any) -> None:
327
+
328
+ active_profile_domain = get_oidc_client().get_active_profile_domain(raise_exc=False)
329
+ if not active_profile_domain:
330
+ print("[+] Not authenticated")
331
+ return
332
+ workspace = user_api.fetch_current_workspaces()
333
+
334
+ organization_name = workspace[0]["name"]
335
+ print(
336
+ f"[+] \033[1mAvailable agents for your organization\033[0m: {organization_name} "
337
+ )
338
+ agents = scenario_api.fetch_agents(organization_id=workspace[0]["organization_id"])
339
+ for agent in agents:
340
+ if is_agent_up(agent["last_ping"]):
341
+ status = "UP"
342
+ else:
343
+ status = "DOWN"
344
+ print(
345
+ f" - \033[1m[{status}]\033[0m \033[1mID\033[0m: {agent['id']:<30} - \033[1mIP\033[0m: {agent['host']['ip']:<18} - \033[1mMAC\033[0m: {agent['host']['mac']:<30}",
346
+ end="",
347
+ )
348
+ if agent["bas_id"] != "":
349
+ print(f"\033[1mBAS Campaign ID\033[0m: {agent['bas_id']}")
350
+ else:
351
+ print()
352
+
353
+
354
+ def bas_agents_delete_handler(args: Any) -> None:
355
+
356
+ agent_id = args.agent_id
357
+ active_profile_domain = get_oidc_client().get_active_profile_domain(raise_exc=False)
358
+ if not active_profile_domain:
359
+ print("[+] Not authenticated")
360
+ return
361
+ workspace = user_api.fetch_current_workspaces()
362
+
363
+ scenario_api.delete_agents(
364
+ agent_id=agent_id, organization_id=workspace[0]["organization_id"]
365
+ )
366
+
367
+ print(f"[+] Agent '{agent_id}' has been deleted.")
368
+
369
+
370
+ def bas_agents_install_handler(args: Any) -> None:
371
+
372
+ active_profile_domain = get_oidc_client().get_active_profile_domain(raise_exc=False)
373
+ if not active_profile_domain:
374
+ print("[+] Not authenticated")
375
+ return
376
+ workspace = user_api.fetch_current_workspaces()
377
+ active_profile_domain = get_oidc_client().get_active_profile_domain(raise_exc=False)
378
+
379
+ print(
380
+ f"[+] Download agent here : https://app.{active_profile_domain}/api/scenario/bas_agent/download"
381
+ )
382
+ print(
383
+ f"[+] To install agent : ./mantis_agent.ps1 -OrgaID {workspace[0]['organization_id']} "
384
+ )
385
+ print("[+] To uninstall agent : ./mantis_agent.ps1 -action uninstall ")
386
+
387
+
388
+ #
389
+ # 'bas_stop_handler' handler
390
+ #
391
+ def bas_stop_handler(args: Any) -> None:
392
+ # Parameters
393
+ campaign_id = args.campaign_id
394
+ workspace = user_api.fetch_current_workspaces()
395
+ organization_id = workspace[0]["organization_id"]
396
+ agents = scenario_api.fetch_agents(
397
+ organization_id=workspace[0]["organization_id"], bas_id=campaign_id
398
+ )
399
+
400
+ try:
401
+ scenario_api.stop_lab(campaign_id)
402
+ for agent in agents:
403
+ beacons = scenario_api.fetch_beacons(agent_id=agent["id"])
404
+ for beacon in beacons:
405
+ scenario_api.add_order_agents(
406
+ agent_id=agent["id"],
407
+ action_type="remove",
408
+ content=beacon["pid"],
409
+ organization_id=organization_id,
410
+ )
411
+ print("Wait until the attack sessions are down ...")
412
+ while redteam_api.attack_sessions()[0]["is_up"]:
413
+ time.sleep(1)
414
+ except Exception as e:
415
+ print(f"Error when stopping BAS campaign: '{e}'")
416
+ sys.exit(1)
417
+
418
+ print(f"[+] This BAS campaign '{campaign_id}' has been stopped")
419
+
420
+
421
+ def show_available_attacks( # noqa: C901
422
+ session_id: str, filter_tactic: str, filter_technique: str
423
+ ):
424
+ print(f"[+] \033[1mAvailable attacks for BAS agent session\033[0m: {session_id}")
425
+
426
+ attacks = redteam_api.list_attacks()
427
+ for attack in attacks:
428
+
429
+ # Check if the attack is runnable
430
+ if attack["status"] not in ["runnable", "failed"]:
431
+ continue
432
+
433
+ # Check if the attack is BAS compatible
434
+ if (
435
+ ("worker" not in attack)
436
+ or ("bas_compat" not in attack["worker"])
437
+ or (attack["worker"]["bas_compat"] is False)
438
+ ):
439
+ continue
440
+
441
+ # FIXME: attack['values'] is currently a string! Do not provide string dict from REST API
442
+ dict_values = json.loads(attack["values"])
443
+ if not isinstance(dict_values, dict):
444
+ continue
445
+
446
+ # Check if the attack is part of the current agent session
447
+ if "attack_session_id" not in dict_values:
448
+ continue
449
+ attack_session_id = dict_values["attack_session_id"]
450
+ if attack_session_id != session_id:
451
+ continue
452
+
453
+ # Filter list by technique or tactic
454
+ select = True
455
+ if filter_tactic is not None:
456
+ select = False
457
+ for tactic in attack["worker"]["mitre_data"]["tactics"]:
458
+ if filter_tactic.lower() in [
459
+ tactic["name"].lower(),
460
+ tactic["id"].lower(),
461
+ ]:
462
+ if filter_technique is not None:
463
+ if (
464
+ filter_technique
465
+ == attack["worker"]["mitre_data"]["technique"]["id"]
466
+ ):
467
+ select = True
468
+ else:
469
+ select = True
470
+ else:
471
+ # No filter_tactic
472
+ if filter_technique is not None:
473
+ select = False
474
+ if (
475
+ filter_technique
476
+ == attack["worker"]["mitre_data"]["technique"]["id"]
477
+ ):
478
+ select = True
479
+
480
+ if not select:
481
+ continue
482
+
483
+ # Retrieve attacks values, after removing redondant attack session infos
484
+ attack_values = dict_values.copy()
485
+ attack_values.pop("attack_session_type", None)
486
+ attack_values.pop("attack_session_source", None)
487
+ attack_values.pop("attack_session_id", None)
488
+
489
+ if attack["worker"]["mitre_data"]["subtechnique"]:
490
+ technique_id = attack["worker"]["mitre_data"]["subtechnique"]["id"]
491
+ else:
492
+ technique_id = attack["worker"]["mitre_data"]["technique"]["id"]
493
+
494
+ mitre_attack_tactics = ", ".join(
495
+ [tactic["id"] for tactic in attack["worker"]["mitre_data"]["tactics"]]
496
+ )
497
+ mitre_attack_infos = f"{technique_id} [{mitre_attack_tactics}]"
498
+
499
+ print(
500
+ f" - \033[1mname\033[0m: {attack['worker']['name']:<30} - \033[1mATT&CK\033[0m: {mitre_attack_infos:<18} - \033[1mdescription\033[0m: {attack['worker']['title']:<30} - \033[1mvalues\033[0m:{attack_values}"
501
+ )
502
+
503
+ # Retrieve potential attack parameters that user can control
504
+ # attack_info = scenario_api.fetch_attack_by_name(attack["worker"]["name"])
505
+
506
+ if attack["worker"]["options"]:
507
+ print(" \033[1mparameters\033[0m: ")
508
+ for option in attack["worker"]["options"]:
509
+ default = ""
510
+ if option["default"]:
511
+ default = f"(\033[1mdefault\033[0m: {option['default']})"
512
+ print(
513
+ f" - \033[1m{option['name']}\033[0m: {option['description']} {default}"
514
+ )
515
+
516
+
517
+ def find_bas_lab_from_session_id(session_id: str):
518
+ """Search in active campaigns the agent session ID.
519
+
520
+ Returns the lab id that matches.
521
+
522
+ """
523
+ bas_labs: List[Lab] = fetch_bas_labs(all_labs=False)
524
+
525
+ bas_lab_id = None
526
+ for bas_lab in bas_labs:
527
+
528
+ # Check if session_id exists in current lab
529
+ set_current_lab(bas_lab.runner_id)
530
+ try:
531
+ for session in redteam_api.attack_sessions():
532
+ if session["identifier"] == session_id:
533
+ bas_lab_id = bas_lab.runner_id
534
+ break
535
+
536
+ finally:
537
+ unset_current_lab()
538
+
539
+ if bas_lab_id is not None:
540
+ break
541
+ else:
542
+ print("No active BAS campaign with this agent session ID")
543
+ sys.exit(1)
544
+
545
+ return bas_lab_id
546
+
547
+
548
+ #
549
+ # 'bas_attack_list_handler' handler
550
+ #
551
+
552
+
553
+ def bas_attack_list_handler(args: Any) -> None:
554
+ # Parameters
555
+ session_id = args.session_id
556
+ filter_tactic = args.filter_tactic
557
+ filter_technique = args.filter_technique
558
+
559
+ # Retrieve matching bas lab
560
+ lab_id = find_bas_lab_from_session_id(session_id)
561
+
562
+ # Set current lab
563
+ set_current_lab(lab_id)
564
+ try:
565
+ show_available_attacks(session_id, filter_tactic, filter_technique)
566
+ finally:
567
+ unset_current_lab()
568
+
569
+ print(
570
+ f"[hint] You can launch an attack with: '$ mantis bas attack run -s {session_id} -a ATTACK_NAME'"
571
+ )
572
+
573
+
574
+ def run_attack(
575
+ session_id: str,
576
+ attack_name: str,
577
+ show_output: bool,
578
+ params: Dict[str, str],
579
+ ):
580
+ # Retrieve matching attacks according to input attack name
581
+ matching_attacks = redteam_api.get_attacks_by_values(
582
+ attack_name=attack_name,
583
+ attack_session_identifier=session_id,
584
+ )
585
+
586
+ if not matching_attacks:
587
+ print(f"No matching attack '{attack_name}'")
588
+ sys.exit(1)
589
+
590
+ # Retrieve runnable attacks
591
+ runnable_attacks = []
592
+ for attack in matching_attacks:
593
+ if attack["status"] == "runnable":
594
+ runnable_attacks.append(attack)
595
+
596
+ if not runnable_attacks:
597
+ print(f"No matching attack '{attack_name}' can be run")
598
+ sys.exit(1)
599
+
600
+ if len(runnable_attacks) > 1:
601
+ print(
602
+ f"[+] Multiple attacks match '{attack_name}'. Please choose the ID to run:"
603
+ )
604
+
605
+ # FIXME: attack['values'] is currently a string! Do not provide string dict from REST API
606
+ dict_values = json.loads(attack["values"])
607
+
608
+ # Retrieve attacks values, after removing redondant attack session infos
609
+ attack_values = dict_values.copy()
610
+ attack_values.pop("attack_session_type", None)
611
+ attack_values.pop("attack_session_source", None)
612
+ attack_values.pop("attack_session_id", None)
613
+
614
+ mitre_attack_tactics = ", ".join(
615
+ [tactic["id"] for tactic in attack["worker"]["mitre_data"]["tactics"]]
616
+ )
617
+ mitre_attack_infos = f"{attack['worker']['mitre_data']['technique']['id']} {attack['worker']['mitre_data']['technique']['name']} [{mitre_attack_tactics}]"
618
+
619
+ for idx, attack in enumerate(runnable_attacks, start=1):
620
+ print(
621
+ f" \033[1mID\033[0m: {idx:>3} - - \033[1mattack_name\033[0m: {attack['worker']['name']} - \033[1mDescription\033[0m: {attack['worker']['title']} \033[1mATT&CK\033[0m: {mitre_attack_infos} - \033[1mattack_parameters\033[0m:{attack_values}"
622
+ )
623
+
624
+ selected_idx_str = input("Selected ID: ")
625
+
626
+ try:
627
+ selected_idx = int(selected_idx_str)
628
+ except ValueError:
629
+ print(f"'{selected_idx_str}' is not a valid ID type")
630
+ sys.exit(1)
631
+
632
+ if selected_idx not in list(range(1, idx + 1)):
633
+ print(f"'{selected_idx_str}' is not a valid ID")
634
+ sys.exit(1)
635
+
636
+ selected_idx = (
637
+ selected_idx - 1
638
+ ) # -1 to handle the shift in the above enumerate()
639
+
640
+ else:
641
+ selected_idx = 0
642
+
643
+ # Retrieve attack structure based on the selected one
644
+ attack = runnable_attacks[selected_idx]
645
+
646
+ # Disable logs emitted by cr_api_client.redteam_api, as we want to rewrite output
647
+ from loguru import logger
648
+
649
+ logger.disable("cr_api_client.redteam_api")
650
+
651
+ print(f"[+] \033[1mPlaying attack\033[0m: {attack_name}")
652
+
653
+ # Disable cleaning for every worker called through bas
654
+ params["cleaning"] = "False"
655
+
656
+ try:
657
+ attack_id = redteam_api.execute_attack_by_id(
658
+ id_attack=attack["idAttack"],
659
+ name=attack["worker"]["name"],
660
+ title=attack["worker"]["title"],
661
+ allow_to_failed=True,
662
+ options=params,
663
+ )
664
+ except Exception as e:
665
+ print(e)
666
+ sys.exit(1)
667
+ finally:
668
+ logger.enable("cr_api_client.redteam_api")
669
+
670
+ # Show attack result
671
+ attack_result = redteam_api.attack_infos(attack_id)
672
+ print(f"[+] \033[1mResult\033[0m: {attack_result['status']}")
673
+ print("[+] \033[1mTimestamps\033[0m:")
674
+ print(f" - \033[1mstart\033[0m: {attack_result['started_date']} UTC")
675
+ print(f" - \033[1mend\033[0m: {attack_result['last_update']} UTC")
676
+ print("[+] \033[1mExecuted commands\033[0m:")
677
+ for command in attack_result["commands"]:
678
+ for k, v in command.items():
679
+ print(f" - {k}: {v}")
680
+ if show_output:
681
+ print("[+] \033[1mOutput\033[0m:")
682
+ for elt in attack_result["output"]:
683
+ print(f" - {elt}")
684
+
685
+
686
+ #
687
+ # 'bas_attack_run_handler' handler
688
+ #
689
+
690
+
691
+ def bas_attack_run_handler(args: Any) -> None:
692
+ # Parameters
693
+ session_id = args.session_id
694
+ attack_name = args.attack_name
695
+ show_output = args.show_output
696
+ params = args.params
697
+
698
+ # Parse options to create a dict expected by the redteam API
699
+ def parse_var(s):
700
+ """
701
+ Parse a key, value pair, separated by '='
702
+ That's the reverse of ShellArgs.
703
+
704
+ On the command line (argparse) a declaration will typically look like:
705
+ foo=hello
706
+ or
707
+ foo="hello world"
708
+ """
709
+ items = s.split("=")
710
+ key = items[0].strip() # we remove blanks around keys, as is logical
711
+ if len(items) > 1:
712
+ # rejoin the rest
713
+ value = "=".join(items[1:])
714
+ return (key, value)
715
+
716
+ def parse_vars(items):
717
+ """
718
+ Parse a series of key-value pairs and return a dictionary
719
+ """
720
+ d = {}
721
+
722
+ if items:
723
+ for item in items:
724
+ key, value = parse_var(item)
725
+ d[key] = value
726
+ return d
727
+
728
+ params = parse_vars(params)
729
+
730
+ # Retrieve potential attack parameters that user can control, and
731
+ # check that the attack accepts those parameters
732
+ attack_info = scenario_api.fetch_attack_by_name(attack_name)
733
+
734
+ option_list = [option.name for option in attack_info.options]
735
+ for key in params.keys():
736
+ if key not in option_list:
737
+ print(
738
+ colored(
739
+ f"Parameter '{key}' is not in the supported parameter list. Supported parameters are: {', '.join(option_list)}",
740
+ "red",
741
+ )
742
+ )
743
+
744
+ # Retrieve matching bas lab
745
+ lab_id = find_bas_lab_from_session_id(session_id)
746
+
747
+ # Set current lab
748
+ set_current_lab(lab_id)
749
+ try:
750
+ run_attack(session_id, attack_name, show_output, params)
751
+ finally:
752
+ unset_current_lab()
753
+
754
+
755
+ def bas_attack_history_handler(args):
756
+ # Parameters
757
+ session_id = args.session_id
758
+ show_output = args.show_output
759
+
760
+ attacks = redteam_api.list_attacks()
761
+ for attack in attacks:
762
+
763
+ # Check if the attack has been previously executed
764
+ if attack["status"] in ["runnable"]:
765
+ continue
766
+
767
+ # Check if the attack is BAS compatible
768
+ if (
769
+ ("worker" not in attack)
770
+ or ("bas_compat" not in attack["worker"])
771
+ or (attack["worker"]["bas_compat"] is False)
772
+ ):
773
+ continue
774
+
775
+ # FIXME: attack['values'] is currently a string! Do not provide string dict from REST API
776
+ dict_values = json.loads(attack["values"])
777
+ if not isinstance(dict_values, dict):
778
+ continue
779
+
780
+ # Check if the attack is part if the current agent session
781
+ if "attack_session_id" not in dict_values:
782
+ continue
783
+ attack_session_id = dict_values["attack_session_id"]
784
+ if attack_session_id != session_id:
785
+ continue
786
+
787
+ # Show attack result
788
+ attack_result = redteam_api.attack_infos(attack["idAttack"])
789
+ print(f"[+] \033[1mAttack\033[0m: {attack_result['worker']['name']}")
790
+ print(f"[+] \033[1mResult\033[0m: {attack_result['status']}")
791
+ print("[+] \033[1mTimestamps\033[0m:")
792
+ print(f" - \033[1mstart\033[0m: {attack_result['started_date']} UTC")
793
+ print(f" - \033[1mend\033[0m: {attack_result['last_update']} UTC")
794
+ print("[+] \033[1mExecuted commands\033[0m:")
795
+ for command in attack_result["commands"]:
796
+ for k, v in command.items():
797
+ print(f" - {k}: {v}")
798
+ if show_output:
799
+ print("[+] \033[1mOutput\033[0m:")
800
+ for elt in attack_result["output"]:
801
+ print(f" - {elt}")
802
+ print("----")
803
+
804
+
805
+ #
806
+ # 'bas_attack_custom_run_handler' handler
807
+ #
808
+
809
+
810
+ def bas_attack_custom_run_handler(args: Any) -> None:
811
+ # Parameters
812
+ session_id = args.session_id
813
+ command = args.command
814
+ atr_file = args.atr_file
815
+ background = args.background
816
+
817
+ # Sanity check
818
+ if command is None and atr_file is None:
819
+ print("Either --command or --atr_file should be used")
820
+ sys.exit(1)
821
+
822
+ if command is not None and atr_file is not None:
823
+ print("Either --command or --atr_file should be used, but not both")
824
+ sys.exit(1)
825
+
826
+ if atr_file is not None and background is True:
827
+ print("--background is not compatible with --atr_file option")
828
+ sys.exit(1)
829
+
830
+ # Retrieve matching bas lab
831
+ lab_id = find_bas_lab_from_session_id(session_id)
832
+
833
+ # Set current lab
834
+ set_current_lab(lab_id)
835
+ try:
836
+ if command is not None:
837
+ redteam_command_execute_handler(args)
838
+ else:
839
+ redteam_atomic_handler(args)
840
+
841
+ print(
842
+ f"[hint] You can retrieve test results with: '$ mantis bas attack-custom history -s {session_id}'"
843
+ )
844
+ finally:
845
+ unset_current_lab()
846
+
847
+
848
+ #
849
+ # 'bas_attack_custom_history_handler' handler
850
+ #
851
+
852
+
853
+ def bas_attack_custom_history_handler(args: Any) -> None:
854
+ # Parameters
855
+ session_id = args.session_id
856
+
857
+ # Retrieve matching bas lab
858
+ lab_id = find_bas_lab_from_session_id(session_id)
859
+
860
+ # Set current lab
861
+ set_current_lab(lab_id)
862
+ try:
863
+ redteam_command_history_handler(args)
864
+ finally:
865
+ unset_current_lab()
866
+
867
+
868
+ #
869
+ # 'bas_attack_custom_get_handler' handler
870
+ #
871
+
872
+
873
+ def bas_attack_custom_get_handler(args: Any) -> None:
874
+ # Parameters
875
+ session_id = args.session_id
876
+
877
+ # Retrieve matching bas lab
878
+ lab_id = find_bas_lab_from_session_id(session_id)
879
+
880
+ # Set current lab
881
+ set_current_lab(lab_id)
882
+ try:
883
+ redteam_command_get_handler(args)
884
+ finally:
885
+ unset_current_lab()
886
+
887
+
888
+ def add_bas_parser(root_parser: argparse.ArgumentParser, subparsers: Any) -> None:
889
+ # --------------------
890
+ # --- Scenario API options (bas)
891
+ # --------------------
892
+
893
+ # =============================================================================
894
+ # BAS PARSER : global
895
+ # =============================================================================
896
+
897
+ parser_bas = subparsers.add_parser(
898
+ "bas",
899
+ help="BAS API related commands",
900
+ formatter_class=root_parser.formatter_class,
901
+ )
902
+ subparsers_bas = parser_bas.add_subparsers()
903
+
904
+ # 'bas_create' command
905
+ parser_bas_create = subparsers_bas.add_parser(
906
+ "create",
907
+ help="Create a BAS campaign and launch its infrastructure",
908
+ formatter_class=root_parser.formatter_class,
909
+ )
910
+ parser_bas_create.set_defaults(func=bas_create_handler)
911
+ parser_bas_create.add_argument(
912
+ "--workspace",
913
+ dest="workspace_id",
914
+ help="The workspace ID on which you want to create the BAS lab infrastructure",
915
+ )
916
+ parser_bas_create.add_argument(
917
+ "--agent_id",
918
+ dest="agent_id",
919
+ help="Agent ID session to use for BAS.",
920
+ )
921
+ parser_bas_create.add_argument(
922
+ "--privilege",
923
+ dest="privilege",
924
+ help="Choose privilege level to execute beacon (admin or user)",
925
+ )
926
+ parser_bas_create.add_argument(
927
+ "--lab_config",
928
+ "-lc",
929
+ action="store",
930
+ required=False,
931
+ dest="lab_config_path",
932
+ help="Input path of a YAML configuration for the lab to run",
933
+ )
934
+
935
+ # 'bas_list' command
936
+ parser_bas_list = subparsers_bas.add_parser(
937
+ "list",
938
+ aliases=["ls"],
939
+ help="List BAS campaigns (by default, only active campaigns are displayed)",
940
+ formatter_class=root_parser.formatter_class,
941
+ )
942
+ parser_bas_list.add_argument(
943
+ "-a",
944
+ "--all",
945
+ action="store_true",
946
+ dest="all_labs",
947
+ help="Include non active BAS campaigns",
948
+ )
949
+ parser_bas_list.set_defaults(func=bas_list_handler)
950
+
951
+ # 'bas_info' command
952
+ parser_bas_info = subparsers_bas.add_parser(
953
+ "info",
954
+ help="Info of BAS campaign",
955
+ formatter_class=root_parser.formatter_class,
956
+ )
957
+ parser_bas_info.set_defaults(func=bas_info_handler)
958
+ parser_bas_info.add_argument("campaign_id", type=str, help="The BAS campaign ID")
959
+
960
+ # 'bas_stop' command
961
+ parser_bas_stop = subparsers_bas.add_parser(
962
+ "stop",
963
+ help="Stop an active BAS campaign",
964
+ formatter_class=root_parser.formatter_class,
965
+ )
966
+ parser_bas_stop.set_defaults(func=bas_stop_handler)
967
+ parser_bas_stop.add_argument("campaign_id", type=str, help="The BAS campaign ID")
968
+
969
+ # =============================================================================
970
+ # BAS PARSER : agents
971
+ # =============================================================================
972
+
973
+ # 'bas_list_agents' command
974
+ parser_bas_agents = subparsers_bas.add_parser(
975
+ "agents",
976
+ help="Agents of BAS campaign",
977
+ formatter_class=root_parser.formatter_class,
978
+ )
979
+ subparsers_bas_agents = parser_bas_agents.add_subparsers()
980
+ parser_bas_agents.set_defaults(func=bas_agents_handler)
981
+
982
+ # 'agent_delete' command
983
+ parser_bas_agent_delete = subparsers_bas_agents.add_parser(
984
+ "remove",
985
+ aliases=["rm"],
986
+ help="Delete an active BAS campaign",
987
+ formatter_class=root_parser.formatter_class,
988
+ )
989
+ parser_bas_agent_delete.set_defaults(func=bas_agents_delete_handler)
990
+ parser_bas_agent_delete.add_argument("agent_id", type=str, help="The agent& ID")
991
+
992
+ # 'agent_create' command
993
+ parser_bas_agent_delete = subparsers_bas_agents.add_parser(
994
+ "install",
995
+ help="Procedure to install agent for BAS",
996
+ formatter_class=root_parser.formatter_class,
997
+ )
998
+ parser_bas_agent_delete.set_defaults(func=bas_agents_install_handler)
999
+
1000
+ # =============================================================================
1001
+ # BAS PARSER : attacks
1002
+ # =============================================================================
1003
+
1004
+ # bas attack subparser
1005
+ parser_bas_attack = subparsers_bas.add_parser(
1006
+ "attack",
1007
+ help="BAS attack related commands",
1008
+ formatter_class=root_parser.formatter_class,
1009
+ )
1010
+ subparsers_bas_attack = parser_bas_attack.add_subparsers()
1011
+ parser_bas_attack.set_defaults(func=lambda _: parser_bas_attack.print_help())
1012
+
1013
+ # 'bas_attack_list' command
1014
+ parser_bas_attack_list = subparsers_bas_attack.add_parser(
1015
+ "list",
1016
+ aliases=["ls"],
1017
+ help="Show available attacks for a specific BAS agent session",
1018
+ formatter_class=root_parser.formatter_class,
1019
+ )
1020
+ parser_bas_attack_list.set_defaults(func=bas_attack_list_handler)
1021
+ parser_bas_attack_list.add_argument(
1022
+ "--session_id",
1023
+ "-s",
1024
+ type=str,
1025
+ help="The BAS agent session ID",
1026
+ required=True,
1027
+ )
1028
+ parser_bas_attack_list.add_argument(
1029
+ "-T",
1030
+ "--tactic",
1031
+ action="store",
1032
+ nargs="?",
1033
+ dest="filter_tactic",
1034
+ help="Filter attack according to ATT&CK tactics, either a name or its ID",
1035
+ )
1036
+ parser_bas_attack_list.add_argument(
1037
+ "-t",
1038
+ "--technique",
1039
+ action="store",
1040
+ nargs="?",
1041
+ dest="filter_technique",
1042
+ help="Filter attack according to ATT&CK technique ID",
1043
+ )
1044
+
1045
+ # 'bas_attack_run' command
1046
+ parser_bas_attack_run = subparsers_bas_attack.add_parser(
1047
+ "run",
1048
+ help="Run an attack on a specific BAS agent session",
1049
+ formatter_class=root_parser.formatter_class,
1050
+ )
1051
+ parser_bas_attack_run.set_defaults(func=bas_attack_run_handler)
1052
+ parser_bas_attack_run.add_argument(
1053
+ "--session_id",
1054
+ "-s",
1055
+ type=str,
1056
+ help="The BAS agent session ID",
1057
+ required=True,
1058
+ )
1059
+ parser_bas_attack_run.add_argument(
1060
+ "--attack_name",
1061
+ "-a",
1062
+ type=str,
1063
+ help="The attack name to run (from 'mantis bas attack list' command)",
1064
+ required=True,
1065
+ )
1066
+ parser_bas_attack_run.add_argument(
1067
+ "--show_output",
1068
+ "-o",
1069
+ help="Display attack output (can result in lengthy output)",
1070
+ action="store_true",
1071
+ )
1072
+ parser_bas_attack_run.add_argument(
1073
+ "--params",
1074
+ "-p",
1075
+ metavar="KEY=VALUE",
1076
+ nargs="+",
1077
+ help="Set a number of key-value attack parameters "
1078
+ "in the form '--params key1=var1 key2=var2'. "
1079
+ "If a value contains spaces, you should use double quotes: "
1080
+ 'key="a var".',
1081
+ )
1082
+
1083
+ # 'bas_attack_history' command
1084
+ parser_bas_attack_history = subparsers_bas_attack.add_parser(
1085
+ "history",
1086
+ help="Attack history on a specific BAS agent session",
1087
+ formatter_class=root_parser.formatter_class,
1088
+ )
1089
+ parser_bas_attack_history.set_defaults(func=bas_attack_history_handler)
1090
+ parser_bas_attack_history.add_argument(
1091
+ "--session_id",
1092
+ "-s",
1093
+ type=str,
1094
+ help="The BAS agent session ID",
1095
+ required=True,
1096
+ )
1097
+ parser_bas_attack_history.add_argument(
1098
+ "--show_output",
1099
+ "-o",
1100
+ help="Display attack output (can result in lengthy output)",
1101
+ action="store_true",
1102
+ )
1103
+
1104
+ # =============================================================================
1105
+ # BAS PARSER : attack-custom
1106
+ # =============================================================================
1107
+
1108
+ # bas attack-custom subparser
1109
+ parser_bas_attack_custom = subparsers_bas.add_parser(
1110
+ "attack-custom",
1111
+ help="BAS attack-custom related commands",
1112
+ formatter_class=root_parser.formatter_class,
1113
+ )
1114
+ subparsers_bas_attack_custom = parser_bas_attack_custom.add_subparsers()
1115
+ parser_bas_attack_custom.set_defaults(
1116
+ func=lambda _: parser_bas_attack_custom.print_help()
1117
+ )
1118
+
1119
+ # 'bas_attack_custom_run' command
1120
+ parser_bas_attack_custom_run = subparsers_bas_attack_custom.add_parser(
1121
+ "run",
1122
+ help="Execute custom command on agent session",
1123
+ formatter_class=root_parser.formatter_class,
1124
+ )
1125
+ parser_bas_attack_custom_run.add_argument(
1126
+ "--session_id",
1127
+ "-s",
1128
+ help="Attack session identifier",
1129
+ required=True,
1130
+ )
1131
+ parser_bas_attack_custom_run.add_argument(
1132
+ "--atr_file",
1133
+ "-f",
1134
+ help="Import and execute an ATR (Atomic Red Team) file containing commands",
1135
+ )
1136
+ parser_bas_attack_custom_run.add_argument(
1137
+ "--command",
1138
+ "-c",
1139
+ help="Command to execute, must be surrounded by quotation marks",
1140
+ )
1141
+ parser_bas_attack_custom_run.add_argument(
1142
+ "--background",
1143
+ "-b",
1144
+ help="Execute command in background or not (Invoke-WmiMethod on Windows and & on Linux.)",
1145
+ default="false",
1146
+ )
1147
+ parser_bas_attack_custom_run.add_argument(
1148
+ "--timeout",
1149
+ "-t",
1150
+ help="Maximum time (seconds) to wait result command before timeout",
1151
+ default=60,
1152
+ )
1153
+ parser_bas_attack_custom_run.set_defaults(func=bas_attack_custom_run_handler)
1154
+
1155
+ # 'bas_attack_custom_history' command
1156
+ parser_bas_attack_custom_history = subparsers_bas_attack_custom.add_parser(
1157
+ "history",
1158
+ help="Custom attack history on a specific BAS agent session",
1159
+ formatter_class=root_parser.formatter_class,
1160
+ )
1161
+ parser_bas_attack_custom_history.add_argument(
1162
+ "--session_id",
1163
+ "-s",
1164
+ help="Attack session identifier",
1165
+ required=True,
1166
+ )
1167
+ parser_bas_attack_custom_history.add_argument(
1168
+ "--show_output",
1169
+ "-o",
1170
+ help="Display attack output (can result in lengthy output)",
1171
+ action="store_true",
1172
+ )
1173
+ parser_bas_attack_custom_history.set_defaults(
1174
+ func=bas_attack_custom_history_handler
1175
+ )
1176
+
1177
+ # 'bas_attack_custom_get' command
1178
+ parser_bas_attack_custom_get = subparsers_bas_attack_custom.add_parser(
1179
+ "get",
1180
+ help="Get information regarding a specific custom command",
1181
+ formatter_class=root_parser.formatter_class,
1182
+ )
1183
+ parser_bas_attack_custom_get.set_defaults(func=bas_attack_custom_get_handler)
1184
+ parser_bas_attack_custom_get.add_argument(
1185
+ "--session_id",
1186
+ "-s",
1187
+ help="Attack session identifier",
1188
+ required=True,
1189
+ )
1190
+ parser_bas_attack_custom_get.add_argument(
1191
+ "--command_id",
1192
+ "-i",
1193
+ help="The command ID",
1194
+ type=str,
1195
+ required=True,
1196
+ )
1197
+ parser_bas_attack_custom_get.add_argument(
1198
+ "--show_output",
1199
+ "-o",
1200
+ help="Display attack output (can result in lengthy output)",
1201
+ action="store_true",
1202
+ )
1203
+
1204
+ parser_bas.set_defaults(func=lambda _: parser_bas.print_help())