mantis_api_client 4.9.0__14-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,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,229 @@
1
+ # -*- coding: utf-8 -*-
2
+ import argparse
3
+ import json
4
+ import sys
5
+ import traceback
6
+ from typing import Any
7
+
8
+ from mantis_scenario_model.scenario_run_config_model import ScenarioRunConfig
9
+ from mantis_scenario_model.unit_attack_model import Empty
10
+ from pydantic.json import pydantic_encoder
11
+ from ruamel.yaml import YAML
12
+
13
+ from mantis_api_client import scenario_api
14
+ from mantis_api_client.oidc import oidc_client
15
+ from mantis_api_client.utils import colored
16
+ from mantis_api_client.utils import wait_lab
17
+
18
+
19
+ #
20
+ # 'attack_list_handler' handler
21
+ #
22
+ def attack_list_handler(args: Any) -> None:
23
+ try:
24
+ attacks = scenario_api.fetch_attacks()
25
+ except Exception as e:
26
+ print(colored(f"Error when fetching attacks: '{e}'", "red"))
27
+ sys.exit(1)
28
+
29
+ if args.json:
30
+ print(json.dumps(attacks, default=pydantic_encoder))
31
+ return
32
+
33
+ width = 35
34
+ print(f"[+] Available attacks ({len(attacks)}):")
35
+ name = "NAME"
36
+ print(f" [+] \033[1m{name: <{width}}- DESCRIPTION\033[0m")
37
+
38
+ for attack in attacks:
39
+ if not isinstance(attack.mitre_data.subtechnique, Empty):
40
+ mitre_print = attack.mitre_data.subtechnique.id
41
+ else:
42
+ mitre_print = attack.mitre_data.technique.id
43
+
44
+ print(" [+] ", end="")
45
+ print(f"{attack.name: <{width}}", end="")
46
+ print(f"- {attack.title} ({mitre_print})")
47
+
48
+
49
+ #
50
+ # 'attack_info_handler' handler
51
+ #
52
+ def attack_info_handler(args: Any) -> None:
53
+ # Parameters
54
+ attack_name = args.attack_name
55
+
56
+ try:
57
+ attack = scenario_api.fetch_attack_by_name(attack_name)
58
+ except Exception as e:
59
+ print(
60
+ colored(
61
+ f"Error when fetching attack {attack_name}: '{e}'", "red", "on_white"
62
+ )
63
+ )
64
+ sys.exit(1)
65
+
66
+ if args.json:
67
+ print(attack.json())
68
+ return
69
+
70
+ print("[+] Attack information:")
71
+ print(f" [+] \033[1mID\033[0m: {attack.worker_id}")
72
+ print(f" [+] \033[1mName\033[0m: {attack.name}")
73
+ print(f" [+] \033[1mDescription\033[0m: {attack.description}")
74
+ print(" [+] \033[1mAvailable scenario config\033[0m:")
75
+ for scenario_config in attack.scenario_config:
76
+ print(
77
+ f" [+] {scenario_config['name']} (Topology: {scenario_config['file']})"
78
+ )
79
+
80
+
81
+ #
82
+ # 'attack_run_handler' handler
83
+ #
84
+ def attack_run_handler(args: Any) -> None:
85
+ # Parameters
86
+ attack_name = args.attack_name
87
+ scenario_config_name = args.scenario_config_name
88
+ scenario_run_config_path = args.scenario_run_config_path
89
+
90
+ if not args.group_id:
91
+ try:
92
+ group_id = oidc_client.get_default_group(raise_exc=True)
93
+ except Exception as e:
94
+ print(colored(f"Error when fetching attacks: '{e}'", "red"))
95
+ sys.exit(1)
96
+ else:
97
+ group_id = args.group_id
98
+
99
+ # Safety checks
100
+ try:
101
+ attack = scenario_api.fetch_attack_by_name(attack_name)
102
+ except Exception as e:
103
+ print(colored(f"{e}", "red", "on_white"))
104
+ sys.exit(1)
105
+ print(f"[+] Going to execute attack: {attack.name}")
106
+
107
+ # Safety checks
108
+ if len(attack.scenario_config) == 0:
109
+ print(
110
+ f"Cannot run attack '{attack_name}', because it does not have any unit scenario"
111
+ )
112
+ sys.exit(-1)
113
+
114
+ if scenario_config_name is None:
115
+ print(
116
+ "Needed argument --scenario_config_name, in order to choose the unit scenario to run"
117
+ )
118
+ print("Available unit scenarios:")
119
+ for available_scenario_config in attack.scenario_config:
120
+ print(f" [+] {available_scenario_config['name']}")
121
+ sys.exit(-1)
122
+
123
+ for available_scenario_config in attack.scenario_config:
124
+ if available_scenario_config["name"] == scenario_config_name:
125
+ break
126
+ else:
127
+ print(f"Select scenario config '{scenario_config_name}' is not available")
128
+ print("Available unit scenarios:")
129
+ for available_scenario_config in attack.scenario_config:
130
+ print(f" [+] {available_scenario_config['name']}")
131
+ sys.exit(-1)
132
+
133
+ # Manage scenario run configuration
134
+ if scenario_run_config_path is None:
135
+ scenario_run_config_dict = {}
136
+ else:
137
+ with open(scenario_run_config_path, "r") as fd:
138
+ yaml_content = fd.read()
139
+ loader = YAML(typ="rt")
140
+ scenario_run_config_dict = loader.load(yaml_content)
141
+ scenario_run_config = ScenarioRunConfig(**scenario_run_config_dict)
142
+
143
+ # Launch topology
144
+ try:
145
+ lab_id = scenario_api.run_attack(
146
+ attack, scenario_config_name, scenario_run_config, group_id
147
+ )
148
+
149
+ print(f"[+] Scenario lab ID: {lab_id}")
150
+
151
+ wait_lab(lab_id)
152
+
153
+ print("[+] Scenario ended")
154
+ except Exception as e:
155
+ print(colored(f"Error when running attack {attack_name}: '{e}'", "red"))
156
+ print(traceback.format_exc())
157
+ sys.exit(1)
158
+ finally:
159
+ if args.destroy_after_scenario:
160
+ print("[+] Stopping lab...")
161
+ scenario_api.stop_lab(lab_id)
162
+
163
+
164
+ def add_attack_parser(root_parser: argparse.ArgumentParser, subparsers: Any) -> None:
165
+ # --------------------
166
+ # --- Scenario API options (attack)
167
+ # --------------------
168
+
169
+ parser_attack = subparsers.add_parser(
170
+ "attack",
171
+ help="Scenario API related commands (attacks)",
172
+ formatter_class=root_parser.formatter_class,
173
+ )
174
+ subparsers_attack = parser_attack.add_subparsers()
175
+
176
+ # 'attack_list' command
177
+ parser_attack_list = subparsers_attack.add_parser(
178
+ "list",
179
+ help="List all available attacks",
180
+ formatter_class=root_parser.formatter_class,
181
+ )
182
+ parser_attack_list.set_defaults(func=attack_list_handler)
183
+ parser_attack_list.add_argument(
184
+ "--json", help="Return JSON result.", action="store_true"
185
+ )
186
+
187
+ # 'attack_info' command
188
+ parser_attack_info = subparsers_attack.add_parser(
189
+ "info",
190
+ help="Get information about an attack",
191
+ formatter_class=root_parser.formatter_class,
192
+ )
193
+ parser_attack_info.set_defaults(func=attack_info_handler)
194
+ parser_attack_info.add_argument("attack_name", type=str, help="The attack name")
195
+ parser_attack_info.add_argument(
196
+ "--json", help="Return JSON result.", action="store_true"
197
+ )
198
+
199
+ # 'attack_run' command
200
+ parser_attack_run = subparsers_attack.add_parser(
201
+ "run", help="Run a specific attack", formatter_class=root_parser.formatter_class
202
+ )
203
+ parser_attack_run.set_defaults(func=attack_run_handler)
204
+ parser_attack_run.add_argument("attack_name", type=str, help="The attack name")
205
+ parser_attack_run.add_argument(
206
+ "--destroy",
207
+ action="store_true",
208
+ dest="destroy_after_scenario",
209
+ help="Do not keep the lab up after scenario execution (False by default)",
210
+ )
211
+ parser_attack_run.add_argument(
212
+ "--scenario_config_name",
213
+ dest="scenario_config_name",
214
+ help="Allows to define the unit scenario to run for a unit attack",
215
+ )
216
+ parser_attack_run.add_argument(
217
+ "--group_id",
218
+ dest="group_id",
219
+ help="The group ID that have ownership on lab",
220
+ )
221
+ parser_attack_run.add_argument(
222
+ "--scenario_run_config",
223
+ action="store",
224
+ required=False,
225
+ dest="scenario_run_config_path",
226
+ help="Input path of a YAML configuration for the scenario run",
227
+ )
228
+
229
+ parser_attack.set_defaults(func=lambda _: parser_attack.print_help())