mantis_api_client 4.9.0__9-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 +347 -0
- mantis_api_client/cli_parser/attack_parser.py +221 -0
- mantis_api_client/cli_parser/basebox_parser.py +187 -0
- mantis_api_client/cli_parser/dataset_parser.py +189 -0
- mantis_api_client/cli_parser/lab_parser.py +457 -0
- mantis_api_client/cli_parser/labs_parser.py +47 -0
- mantis_api_client/cli_parser/log_collector_parser.py +66 -0
- mantis_api_client/cli_parser/redteam_parser.py +298 -0
- mantis_api_client/cli_parser/scenario_parser.py +231 -0
- mantis_api_client/cli_parser/topology_parser.py +179 -0
- mantis_api_client/config.py +72 -0
- mantis_api_client/dataset_api.py +271 -0
- mantis_api_client/exceptions.py +27 -0
- mantis_api_client/mantis.py +166 -0
- mantis_api_client/oidc.py +277 -0
- mantis_api_client/scenario_api.py +671 -0
- mantis_api_client/utils.py +103 -0
- mantis_api_client-4.9.0.dist-info/AUTHORS +1 -0
- mantis_api_client-4.9.0.dist-info/LICENSE +19 -0
- mantis_api_client-4.9.0.dist-info/METADATA +33 -0
- mantis_api_client-4.9.0.dist-info/RECORD +24 -0
- mantis_api_client-4.9.0.dist-info/WHEEL +4 -0
- mantis_api_client-4.9.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
import os
|
|
4
|
+
import threading
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
# Component version
|
|
8
|
+
__version__ = "4.8.0"
|
|
9
|
+
|
|
10
|
+
# Remove incompatible env variable
|
|
11
|
+
os.environ.pop("USE_PERMISSIONS", None)
|
|
12
|
+
|
|
13
|
+
# Get component full version from file generated at build time
|
|
14
|
+
current_file_dir = Path(__file__).resolve().parent
|
|
15
|
+
fullversion_file = Path(current_file_dir, "fullversion.txt")
|
|
16
|
+
if os.path.isfile(fullversion_file):
|
|
17
|
+
__fullversion__ = open(fullversion_file, "r").read().strip()
|
|
18
|
+
else:
|
|
19
|
+
__fullversion__ = __version__
|
|
20
|
+
|
|
21
|
+
shutil_make_archive_lock = threading.Lock()
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import argparse
|
|
3
|
+
import getpass
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from http.server import BaseHTTPRequestHandler
|
|
8
|
+
from http.server import HTTPServer
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from threading import Thread
|
|
11
|
+
from typing import Any
|
|
12
|
+
from typing import List
|
|
13
|
+
from urllib.parse import parse_qs
|
|
14
|
+
from urllib.parse import urlparse
|
|
15
|
+
|
|
16
|
+
import requests
|
|
17
|
+
from jose import jwt
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from rich.prompt import Prompt
|
|
20
|
+
|
|
21
|
+
import mantis_api_client.dataset_api as dataset_api
|
|
22
|
+
import mantis_api_client.oidc
|
|
23
|
+
import mantis_api_client.scenario_api as scenario_api
|
|
24
|
+
from mantis_api_client.config import mantis_api_client_config
|
|
25
|
+
from mantis_api_client.oidc import oidc_client
|
|
26
|
+
from mantis_api_client.utils import colored
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Version:
|
|
30
|
+
def __init__(self, str_vers: str) -> None:
|
|
31
|
+
try:
|
|
32
|
+
self.major, self.minor, self.patch = str_vers.split(".")
|
|
33
|
+
except Exception as e:
|
|
34
|
+
raise Exception(
|
|
35
|
+
"Bad version format for '{}': 'X.Y.Z' expected. Error: {}".format(
|
|
36
|
+
str_vers, e
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
#
|
|
42
|
+
# 'status' related functions
|
|
43
|
+
#
|
|
44
|
+
def status_handler(args: Any) -> None: # noqa: C901
|
|
45
|
+
"""Get platform status."""
|
|
46
|
+
|
|
47
|
+
exit_code = 0
|
|
48
|
+
|
|
49
|
+
client_version = mantis_api_client.__version__
|
|
50
|
+
client_vers = Version(str_vers=client_version)
|
|
51
|
+
client_fullversion = mantis_api_client.__fullversion__
|
|
52
|
+
print(
|
|
53
|
+
f"[+] mantis_api_client version: {client_version} ({client_fullversion})".format(
|
|
54
|
+
client_version
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
active_profile_domain = oidc_client.get_active_profile_domain(raise_exc=False)
|
|
59
|
+
if active_profile_domain:
|
|
60
|
+
print(f"[+] Authenticated to {active_profile_domain}")
|
|
61
|
+
else:
|
|
62
|
+
print(
|
|
63
|
+
colored(
|
|
64
|
+
"[+] Not authenticated, you need to execute 'mantis account login'",
|
|
65
|
+
"red",
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
print("[+] APIs status")
|
|
70
|
+
|
|
71
|
+
# Dataset API
|
|
72
|
+
print(" [+] Dataset API")
|
|
73
|
+
print(" [+] address: {}".format(mantis_api_client_config.dataset_api_url))
|
|
74
|
+
try:
|
|
75
|
+
dataset_api_version = dataset_api.get_version()
|
|
76
|
+
dataset_vers = Version(str_vers=dataset_api_version)
|
|
77
|
+
except requests.exceptions.ConnectionError:
|
|
78
|
+
exit_code = 1
|
|
79
|
+
print(" [-] API status: " + colored("not running !", "white", "on_red"))
|
|
80
|
+
else:
|
|
81
|
+
print(" [+] API status: " + colored("OK", "grey", "on_green"))
|
|
82
|
+
print(" [+] version: {}".format(dataset_api_version))
|
|
83
|
+
if dataset_vers.major != client_vers.major:
|
|
84
|
+
exit_code = 1
|
|
85
|
+
print(
|
|
86
|
+
" [-] "
|
|
87
|
+
+ colored(
|
|
88
|
+
"Error: Dataset API major version ({}) mismatchs with mantis_api_client major version ({})".format(
|
|
89
|
+
dataset_vers.major, client_vers.major
|
|
90
|
+
),
|
|
91
|
+
"white",
|
|
92
|
+
"on_red",
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Scenario API
|
|
97
|
+
print(" [+] Scenario API")
|
|
98
|
+
print(" [+] address: {}".format(mantis_api_client_config.scenario_api_url))
|
|
99
|
+
try:
|
|
100
|
+
scenario_api_version = scenario_api.get_version()
|
|
101
|
+
scenario_vers = Version(str_vers=scenario_api_version)
|
|
102
|
+
except requests.exceptions.ConnectionError:
|
|
103
|
+
exit_code = 1
|
|
104
|
+
print(" [-] API status: " + colored("not running !", "white", "on_red"))
|
|
105
|
+
else:
|
|
106
|
+
print(" [+] API status: " + colored("OK", "grey", "on_green"))
|
|
107
|
+
print(" [+] version: {}".format(scenario_api_version))
|
|
108
|
+
if scenario_vers.major != client_vers.major:
|
|
109
|
+
exit_code = 1
|
|
110
|
+
print(
|
|
111
|
+
" [-] "
|
|
112
|
+
+ colored(
|
|
113
|
+
"Error: Scenario API major version ({}) mismatchs with mantis_api_client major version ({})".format(
|
|
114
|
+
scenario_vers.major, client_vers.major
|
|
115
|
+
),
|
|
116
|
+
"white",
|
|
117
|
+
"on_red",
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if exit_code != 0:
|
|
122
|
+
sys.exit(exit_code)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
#
|
|
126
|
+
# 'info' related functions
|
|
127
|
+
#
|
|
128
|
+
def info_handler(args: Any) -> None: # noqa: C901
|
|
129
|
+
"""Get personal info."""
|
|
130
|
+
|
|
131
|
+
active_profile_domain = oidc_client.get_active_profile_domain(raise_exc=False)
|
|
132
|
+
if not active_profile_domain:
|
|
133
|
+
print("[+] Not authenticated")
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
console = Console(highlight=False)
|
|
137
|
+
rprint = console.print
|
|
138
|
+
active_tokens = oidc_client.get_active_tokens()
|
|
139
|
+
access_token = jwt.get_unverified_claims(active_tokens["access_token"])
|
|
140
|
+
id_token = jwt.get_unverified_claims(active_tokens["id_token"])
|
|
141
|
+
default_group = oidc_client.get_default_group()
|
|
142
|
+
groups_comma_sep = ", ".join(id_token["groups"])
|
|
143
|
+
if default_group:
|
|
144
|
+
# mark default group bold
|
|
145
|
+
groups_comma_sep = groups_comma_sep.replace(
|
|
146
|
+
default_group, f"[bold]{default_group}[/bold]"
|
|
147
|
+
)
|
|
148
|
+
sorted_scopes = ", ".join(sorted(access_token["scope"].split()))
|
|
149
|
+
|
|
150
|
+
print(f"[+] Connected to {active_profile_domain}")
|
|
151
|
+
print(f" [+] Username: {id_token['preferred_username']}")
|
|
152
|
+
print(f" [+] Email: {id_token.get('email', 'N/A')}")
|
|
153
|
+
rprint(rf" \[+] Group membership: {groups_comma_sep}")
|
|
154
|
+
print(f" [+] Scopes: {sorted_scopes}")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
#
|
|
158
|
+
# 'login_handler' handler
|
|
159
|
+
#
|
|
160
|
+
def login_handler(args: Any) -> None:
|
|
161
|
+
# Parameters
|
|
162
|
+
oidc_domain = args.domain
|
|
163
|
+
username = args.username
|
|
164
|
+
if args.password_stdin:
|
|
165
|
+
password = sys.stdin.read().rstrip()
|
|
166
|
+
elif args.password_fd:
|
|
167
|
+
with os.fdopen(args.password_fd) as f:
|
|
168
|
+
password = f.read().rstrip()
|
|
169
|
+
elif args.password_file:
|
|
170
|
+
password = args.password_file.read_text().rstrip()
|
|
171
|
+
elif args.username:
|
|
172
|
+
password = getpass.getpass()
|
|
173
|
+
else:
|
|
174
|
+
password = None
|
|
175
|
+
|
|
176
|
+
scope = "openid offline_access groups profile email"
|
|
177
|
+
redirect_uri = mantis_api_client_config.oidc.redirect_uri
|
|
178
|
+
redirect_auto = redirect_uri.startswith("http")
|
|
179
|
+
code_placeholder: List[str] = []
|
|
180
|
+
if redirect_auto:
|
|
181
|
+
thread = _init_create_callback_request_handler_thread(code_placeholder)
|
|
182
|
+
thread.start()
|
|
183
|
+
if username and password:
|
|
184
|
+
token = oidc_client.token(
|
|
185
|
+
oidc_domain, username, password, redirect_uri=redirect_uri, scope=scope
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
auth_url = oidc_client.auth_url(
|
|
189
|
+
oidc_domain,
|
|
190
|
+
redirect_uri=redirect_uri,
|
|
191
|
+
scope=scope,
|
|
192
|
+
)
|
|
193
|
+
subprocess.run(["xdg-open", auth_url])
|
|
194
|
+
print(f"A web browser should been opened for {oidc_domain!r}")
|
|
195
|
+
if redirect_auto:
|
|
196
|
+
timeout = mantis_api_client_config.oidc.redirect_url_timeout
|
|
197
|
+
print(f"Waiting callback for {timeout}s")
|
|
198
|
+
thread.join(timeout)
|
|
199
|
+
if thread.is_alive():
|
|
200
|
+
print("Timeout exceeded, exiting")
|
|
201
|
+
print("Access NOT granted")
|
|
202
|
+
exit(1)
|
|
203
|
+
code = code_placeholder[0]
|
|
204
|
+
else:
|
|
205
|
+
code = input("Paste the code displayed in the webpage: ")
|
|
206
|
+
token = oidc_client.token(
|
|
207
|
+
oidc_domain,
|
|
208
|
+
grant_type="authorization_code",
|
|
209
|
+
code=code,
|
|
210
|
+
redirect_uri=redirect_uri,
|
|
211
|
+
)
|
|
212
|
+
print("Access granted")
|
|
213
|
+
|
|
214
|
+
claims = jwt.get_unverified_claims(token["access_token"])
|
|
215
|
+
selected_group = args.group
|
|
216
|
+
claimed_groups = claims.get("groups")
|
|
217
|
+
if claimed_groups and len(claimed_groups) == 1 and selected_group is None:
|
|
218
|
+
selected_group = claimed_groups[0]
|
|
219
|
+
if selected_group not in claimed_groups:
|
|
220
|
+
if selected_group:
|
|
221
|
+
print(f"You are not member of group '{selected_group}'")
|
|
222
|
+
selected_group = Prompt.ask(
|
|
223
|
+
"Select a group to activate", choices=claimed_groups
|
|
224
|
+
)
|
|
225
|
+
print(f"Group `{selected_group}` activated")
|
|
226
|
+
oidc_client.configure_profile(oidc_domain, token["refresh_token"], selected_group)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
#
|
|
230
|
+
# 'logout_handler' handler
|
|
231
|
+
#
|
|
232
|
+
def logout_handler(args: Any) -> None:
|
|
233
|
+
oidc_client.configure_profile(None)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _init_create_callback_request_handler_thread(code_placeholder: list) -> Thread:
|
|
237
|
+
class CallbackHTTPRequestHandler(BaseHTTPRequestHandler):
|
|
238
|
+
def do_GET(self) -> None:
|
|
239
|
+
nonlocal code_placeholder
|
|
240
|
+
query = urlparse(self.path).query
|
|
241
|
+
if "error" in query:
|
|
242
|
+
self.send_response(404)
|
|
243
|
+
else:
|
|
244
|
+
code = parse_qs(query)["code"][0]
|
|
245
|
+
code_placeholder.append(code)
|
|
246
|
+
self.send_response(200)
|
|
247
|
+
self.send_header("Content-Type", "text/html")
|
|
248
|
+
self.end_headers()
|
|
249
|
+
self.wfile.write(
|
|
250
|
+
"<script>window.close()</script> Authorization {}. You may close this window.\r\n".format(
|
|
251
|
+
"failed" if "error" in query else "succeeded"
|
|
252
|
+
).encode()
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def log_request(*args, **kwargs):
|
|
256
|
+
# do nothing
|
|
257
|
+
pass
|
|
258
|
+
|
|
259
|
+
def serve_one_redirect_callback() -> None:
|
|
260
|
+
with HTTPServer(
|
|
261
|
+
(
|
|
262
|
+
mantis_api_client_config.oidc.redirect_url.host,
|
|
263
|
+
mantis_api_client_config.oidc.redirect_url.port,
|
|
264
|
+
),
|
|
265
|
+
CallbackHTTPRequestHandler,
|
|
266
|
+
) as server:
|
|
267
|
+
server.handle_request()
|
|
268
|
+
|
|
269
|
+
return Thread(target=serve_one_redirect_callback, daemon=True)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def add_account_parser(root_parser: argparse.ArgumentParser, subparsers: Any) -> None:
|
|
273
|
+
# -------------------
|
|
274
|
+
# --- login options
|
|
275
|
+
# -------------------
|
|
276
|
+
|
|
277
|
+
parser_account = subparsers.add_parser(
|
|
278
|
+
"account",
|
|
279
|
+
help="Authentication actions for M&NTIS CLI.",
|
|
280
|
+
formatter_class=root_parser.formatter_class,
|
|
281
|
+
)
|
|
282
|
+
subparsers_account = parser_account.add_subparsers()
|
|
283
|
+
|
|
284
|
+
# 'status' command
|
|
285
|
+
parser_status = subparsers_account.add_parser(
|
|
286
|
+
"status",
|
|
287
|
+
help="Get platform status",
|
|
288
|
+
formatter_class=root_parser.formatter_class,
|
|
289
|
+
)
|
|
290
|
+
parser_status.set_defaults(func=status_handler)
|
|
291
|
+
|
|
292
|
+
# 'info' command
|
|
293
|
+
parser_info = subparsers_account.add_parser(
|
|
294
|
+
"info", help="Get personal info", formatter_class=root_parser.formatter_class
|
|
295
|
+
)
|
|
296
|
+
parser_info.set_defaults(func=info_handler)
|
|
297
|
+
|
|
298
|
+
parser_login = subparsers_account.add_parser(
|
|
299
|
+
"login",
|
|
300
|
+
help="Log into a M&ntis account",
|
|
301
|
+
formatter_class=root_parser.formatter_class,
|
|
302
|
+
)
|
|
303
|
+
parser_login.add_argument(
|
|
304
|
+
"--domain",
|
|
305
|
+
help="The M&ntis cluster SSO domain (default: %(default)s)",
|
|
306
|
+
default="mantis-platform.io",
|
|
307
|
+
)
|
|
308
|
+
parser_login.add_argument(
|
|
309
|
+
"--username",
|
|
310
|
+
"-u",
|
|
311
|
+
help="Your M&ntis cluster SSO username",
|
|
312
|
+
)
|
|
313
|
+
parser_login_mex_group = parser_login.add_mutually_exclusive_group()
|
|
314
|
+
parser_login_mex_group.add_argument(
|
|
315
|
+
"--password-stdin",
|
|
316
|
+
action="store_true",
|
|
317
|
+
help="Read your M&ntis cluster SSO password from stdin",
|
|
318
|
+
)
|
|
319
|
+
parser_login_mex_group.add_argument(
|
|
320
|
+
"--password-fd",
|
|
321
|
+
type=int,
|
|
322
|
+
help="Read your M&ntis cluster SSO password from a descriptor",
|
|
323
|
+
)
|
|
324
|
+
parser_login_mex_group.add_argument(
|
|
325
|
+
"--password-file",
|
|
326
|
+
type=Path,
|
|
327
|
+
help="Read your M&ntis cluster SSO password from a file",
|
|
328
|
+
)
|
|
329
|
+
parser_login.add_argument(
|
|
330
|
+
"-g",
|
|
331
|
+
"--group",
|
|
332
|
+
help="Select the group name that will be used for contextual commands like `mantis scenario run`",
|
|
333
|
+
)
|
|
334
|
+
parser_login.set_defaults(func=login_handler)
|
|
335
|
+
|
|
336
|
+
# -------------------
|
|
337
|
+
# --- logout options
|
|
338
|
+
# -------------------
|
|
339
|
+
|
|
340
|
+
parser_logout = subparsers_account.add_parser(
|
|
341
|
+
"logout",
|
|
342
|
+
help="Log out from your M&ntis account",
|
|
343
|
+
formatter_class=root_parser.formatter_class,
|
|
344
|
+
)
|
|
345
|
+
parser_logout.set_defaults(func=logout_handler)
|
|
346
|
+
|
|
347
|
+
parser_account.set_defaults(func=lambda _: parser_account.print_help())
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import argparse
|
|
3
|
+
import sys
|
|
4
|
+
import traceback
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from mantis_scenario_model.scenario_run_config_model import ScenarioRunConfig
|
|
8
|
+
from mantis_scenario_model.unit_attack_model import Empty
|
|
9
|
+
from ruamel.yaml import YAML
|
|
10
|
+
|
|
11
|
+
from mantis_api_client import scenario_api
|
|
12
|
+
from mantis_api_client.oidc import oidc_client
|
|
13
|
+
from mantis_api_client.utils import colored
|
|
14
|
+
from mantis_api_client.utils import wait_lab
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
#
|
|
18
|
+
# 'attack_list_handler' handler
|
|
19
|
+
#
|
|
20
|
+
def attack_list_handler(args: Any) -> None:
|
|
21
|
+
try:
|
|
22
|
+
attacks = scenario_api.fetch_attacks()
|
|
23
|
+
except Exception as e:
|
|
24
|
+
print(colored(f"Error when fetching attacks: '{e}'", "red"))
|
|
25
|
+
sys.exit(1)
|
|
26
|
+
|
|
27
|
+
if args.json:
|
|
28
|
+
print([item.dict() for item in attacks])
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
width = 35
|
|
32
|
+
print(f"[+] Available attacks ({len(attacks)}):")
|
|
33
|
+
name = "NAME"
|
|
34
|
+
print(f" [+] \033[1m{name: <{width}}- DESCRIPTION\033[0m")
|
|
35
|
+
|
|
36
|
+
for attack in attacks:
|
|
37
|
+
if not isinstance(attack.mitre_data.subtechnique, Empty):
|
|
38
|
+
mitre_print = attack.mitre_data.subtechnique.id
|
|
39
|
+
else:
|
|
40
|
+
mitre_print = attack.mitre_data.technique.id
|
|
41
|
+
|
|
42
|
+
print(" [+] ", end="")
|
|
43
|
+
print(f"{attack.name: <{width}}", end="")
|
|
44
|
+
print(f"- {attack.title} ({mitre_print})")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
#
|
|
48
|
+
# 'attack_info_handler' handler
|
|
49
|
+
#
|
|
50
|
+
def attack_info_handler(args: Any) -> None:
|
|
51
|
+
# Parameters
|
|
52
|
+
attack_name = args.attack_name
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
attack = scenario_api.fetch_attack_by_name(attack_name)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print(
|
|
58
|
+
colored(
|
|
59
|
+
f"Error when fetching attack {attack_name}: '{e}'", "red", "on_white"
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
sys.exit(1)
|
|
63
|
+
|
|
64
|
+
if args.json:
|
|
65
|
+
print(attack.json())
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
print("[+] Attack information:")
|
|
69
|
+
print(f" [+] \033[1mID\033[0m: {attack.worker_id}")
|
|
70
|
+
print(f" [+] \033[1mName\033[0m: {attack.name}")
|
|
71
|
+
print(f" [+] \033[1mDescription\033[0m: {attack.description}")
|
|
72
|
+
print(" [+] \033[1mAvailable scenario config\033[0m:")
|
|
73
|
+
for scenario_config in attack.scenario_config:
|
|
74
|
+
print(
|
|
75
|
+
f" [+] {scenario_config['name']} (Topology: {scenario_config['file']})"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
#
|
|
80
|
+
# 'attack_run_handler' handler
|
|
81
|
+
#
|
|
82
|
+
def attack_run_handler(args: Any) -> None:
|
|
83
|
+
# Parameters
|
|
84
|
+
attack_name = args.attack_name
|
|
85
|
+
scenario_config_name = args.scenario_config_name
|
|
86
|
+
scenario_run_config_path = args.scenario_run_config_path
|
|
87
|
+
|
|
88
|
+
# Safety checks
|
|
89
|
+
attack = scenario_api.fetch_attack_by_name(attack_name)
|
|
90
|
+
print(f"[+] Going to execute attack: {attack.name}")
|
|
91
|
+
|
|
92
|
+
# Safety checks
|
|
93
|
+
if len(attack.scenario_config) == 0:
|
|
94
|
+
print(
|
|
95
|
+
f"Cannot run attack '{attack_name}', because it does not have any unit scenario"
|
|
96
|
+
)
|
|
97
|
+
sys.exit(-1)
|
|
98
|
+
|
|
99
|
+
if scenario_config_name is None:
|
|
100
|
+
print(
|
|
101
|
+
"Needed argument --scenario_config_name, in order to choose the unit scenario to run"
|
|
102
|
+
)
|
|
103
|
+
print("Available unit scenarios:")
|
|
104
|
+
for available_scenario_config in attack.scenario_config:
|
|
105
|
+
print(f" [+] {available_scenario_config['name']}")
|
|
106
|
+
sys.exit(-1)
|
|
107
|
+
|
|
108
|
+
for available_scenario_config in attack.scenario_config:
|
|
109
|
+
if available_scenario_config["name"] == scenario_config_name:
|
|
110
|
+
break
|
|
111
|
+
else:
|
|
112
|
+
print(f"Select scenario config '{scenario_config_name}' is not available")
|
|
113
|
+
print("Available unit scenarios:")
|
|
114
|
+
for available_scenario_config in attack.scenario_config:
|
|
115
|
+
print(f" [+] {available_scenario_config['name']}")
|
|
116
|
+
sys.exit(-1)
|
|
117
|
+
|
|
118
|
+
# Manage scenario run configuration
|
|
119
|
+
if scenario_run_config_path is None:
|
|
120
|
+
scenario_run_config_dict = {}
|
|
121
|
+
else:
|
|
122
|
+
with open(scenario_run_config_path, "r") as fd:
|
|
123
|
+
yaml_content = fd.read()
|
|
124
|
+
loader = YAML(typ="rt")
|
|
125
|
+
scenario_run_config_dict = loader.load(yaml_content)
|
|
126
|
+
scenario_run_config = ScenarioRunConfig(**scenario_run_config_dict)
|
|
127
|
+
|
|
128
|
+
# Launch topology
|
|
129
|
+
try:
|
|
130
|
+
lab_id = scenario_api.run_attack(
|
|
131
|
+
attack, scenario_config_name, scenario_run_config, args.group_id
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
print(f"[+] Scenario lab ID: {lab_id}")
|
|
135
|
+
|
|
136
|
+
wait_lab(lab_id)
|
|
137
|
+
|
|
138
|
+
print("[+] Scenario ended")
|
|
139
|
+
except Exception as e:
|
|
140
|
+
print(colored(f"Error when running attack {attack_name}: '{e}'", "red"))
|
|
141
|
+
print(traceback.format_exc())
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
finally:
|
|
144
|
+
if args.destroy_after_scenario:
|
|
145
|
+
print("[+] Stopping lab...")
|
|
146
|
+
scenario_api.stop_lab(lab_id)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def add_attack_parser(
|
|
150
|
+
root_parser: argparse.ArgumentParser,
|
|
151
|
+
subparsers: Any,
|
|
152
|
+
group_required: bool,
|
|
153
|
+
) -> None:
|
|
154
|
+
# --------------------
|
|
155
|
+
# --- Scenario API options (attack)
|
|
156
|
+
# --------------------
|
|
157
|
+
|
|
158
|
+
parser_attack = subparsers.add_parser(
|
|
159
|
+
"attack",
|
|
160
|
+
help="Scenario API related commands (attacks)",
|
|
161
|
+
formatter_class=root_parser.formatter_class,
|
|
162
|
+
)
|
|
163
|
+
subparsers_attack = parser_attack.add_subparsers()
|
|
164
|
+
|
|
165
|
+
# 'attack_list' command
|
|
166
|
+
parser_attack_list = subparsers_attack.add_parser(
|
|
167
|
+
"list",
|
|
168
|
+
help="List all available attacks",
|
|
169
|
+
formatter_class=root_parser.formatter_class,
|
|
170
|
+
)
|
|
171
|
+
parser_attack_list.set_defaults(func=attack_list_handler)
|
|
172
|
+
parser_attack_list.add_argument(
|
|
173
|
+
"--json", help="Return JSON result.", action="store_true"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# 'attack_info' command
|
|
177
|
+
parser_attack_info = subparsers_attack.add_parser(
|
|
178
|
+
"info",
|
|
179
|
+
help="Get information about an attack",
|
|
180
|
+
formatter_class=root_parser.formatter_class,
|
|
181
|
+
)
|
|
182
|
+
parser_attack_info.set_defaults(func=attack_info_handler)
|
|
183
|
+
parser_attack_info.add_argument("attack_name", type=str, help="The attack name")
|
|
184
|
+
parser_attack_info.add_argument(
|
|
185
|
+
"--json", help="Return JSON result.", action="store_true"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# 'attack_run' command
|
|
189
|
+
parser_attack_run = subparsers_attack.add_parser(
|
|
190
|
+
"run", help="Run a specific attack", formatter_class=root_parser.formatter_class
|
|
191
|
+
)
|
|
192
|
+
parser_attack_run.set_defaults(func=attack_run_handler)
|
|
193
|
+
parser_attack_run.add_argument("attack_name", type=str, help="The attack name")
|
|
194
|
+
parser_attack_run.add_argument(
|
|
195
|
+
"--destroy",
|
|
196
|
+
action="store_true",
|
|
197
|
+
dest="destroy_after_scenario",
|
|
198
|
+
help="Do not keep the lab up after scenario execution (False by default)",
|
|
199
|
+
)
|
|
200
|
+
parser_attack_run.add_argument(
|
|
201
|
+
"--scenario_config_name",
|
|
202
|
+
dest="scenario_config_name",
|
|
203
|
+
help="Allows to define the unit scenario to run for a unit attack",
|
|
204
|
+
)
|
|
205
|
+
parser_attack_run.add_argument(
|
|
206
|
+
"group_id",
|
|
207
|
+
metavar="group-id",
|
|
208
|
+
type=str,
|
|
209
|
+
nargs=1 if group_required else "?",
|
|
210
|
+
default=oidc_client.get_default_group(raise_exc=False),
|
|
211
|
+
help="The group ID that have ownership on lab (default: %(default)s)",
|
|
212
|
+
)
|
|
213
|
+
parser_attack_run.add_argument(
|
|
214
|
+
"--scenario_run_config",
|
|
215
|
+
action="store",
|
|
216
|
+
required=False,
|
|
217
|
+
dest="scenario_run_config_path",
|
|
218
|
+
help="Input path of a YAML configuration for the scenario run",
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
parser_attack.set_defaults(func=lambda _: parser_attack.print_help())
|