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.
- mantis_api_client/__init__.py +21 -0
- mantis_api_client/cli_parser/account_parser.py +451 -0
- mantis_api_client/cli_parser/attack_parser.py +317 -0
- mantis_api_client/cli_parser/bas_parser.py +1204 -0
- mantis_api_client/cli_parser/basebox_parser.py +258 -0
- mantis_api_client/cli_parser/dataset_parser.py +200 -0
- mantis_api_client/cli_parser/lab_parser.py +805 -0
- mantis_api_client/cli_parser/labs_parser.py +87 -0
- mantis_api_client/cli_parser/log_collector_parser.py +71 -0
- mantis_api_client/cli_parser/redteam_parser.py +375 -0
- mantis_api_client/cli_parser/scenario_parser.py +311 -0
- mantis_api_client/cli_parser/signature_parser.py +147 -0
- mantis_api_client/cli_parser/topology_parser.py +255 -0
- mantis_api_client/cli_parser/user_parser.py +225 -0
- mantis_api_client/config.py +73 -0
- mantis_api_client/dataset_api.py +267 -0
- mantis_api_client/exceptions.py +27 -0
- mantis_api_client/mantis.py +186 -0
- mantis_api_client/oidc.py +302 -0
- mantis_api_client/scenario_api.py +1196 -0
- mantis_api_client/user_api.py +282 -0
- mantis_api_client/utils.py +130 -0
- mantis_api_client-5.5.0.dist-info/AUTHORS +1 -0
- mantis_api_client-5.5.0.dist-info/LICENSE +19 -0
- mantis_api_client-5.5.0.dist-info/METADATA +33 -0
- mantis_api_client-5.5.0.dist-info/RECORD +28 -0
- mantis_api_client-5.5.0.dist-info/WHEEL +4 -0
- mantis_api_client-5.5.0.dist-info/entry_points.txt +3 -0
|
@@ -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())
|