pyntcli 0.1.92__py3-none-any.whl → 0.1.94__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.
pyntcli/__init__.py CHANGED
@@ -1 +1,20 @@
1
- __version__ = "0.1.92"
1
+ __version__ = "0.1.94"
2
+
3
+ try:
4
+ from logzio import sender
5
+ import logging
6
+
7
+
8
+ def get_silent_logger():
9
+ logger = logging.getLogger("silent_logger")
10
+ logger.addHandler(logging.NullHandler())
11
+ return logger
12
+
13
+
14
+ def patched_get_stdout_logger(debug):
15
+ return get_silent_logger()
16
+
17
+
18
+ sender.get_stdout_logger = patched_get_stdout_logger
19
+ except Exception as e:
20
+ pass
pyntcli/commands/burp.py CHANGED
@@ -147,7 +147,7 @@ def burp_usage():
147
147
  .with_line("\t--insecure - Use when target uses self signed certificates")
148
148
  .with_line("\t--application-id - Attach the scan to an application, you can find the ID in your applications area at app.pynt.io")
149
149
  .with_line("\t--host-ca - Path to the CA file in PEM format to enable SSL certificate verification for pynt when running through a VPN.")
150
- .with_line("\t--return-error - 'all-findings' (warnings, or errors), 'errors-only', 'never' (default)")
150
+ .with_line("\t--severity-level - 'all', 'medium', 'high', 'critical', 'none' (default) ")
151
151
  .with_line("\t--verbose - Use to get more detailed information about the run")
152
152
  )
153
153
 
@@ -171,11 +171,7 @@ class BurpCommand(sub_command.PyntSubCommand):
171
171
  burp_cmd.add_argument("--ca-path", type=str, default="")
172
172
  burp_cmd.add_argument("--report", type=str, default="")
173
173
  burp_cmd.add_argument("--captured-domains", nargs="+", help="", default="")
174
- burp_cmd.add_argument(
175
- "--return-error",
176
- choices=["all-findings", "errors-only", "never"],
177
- default="never",
178
- )
174
+ burp_cmd.add_argument("--severity-level", choices=["all", "medium", "high", "critical", "none"], default="none")
179
175
  burp_cmd.print_usage = self.print_usage
180
176
  burp_cmd.print_help = self.print_usage
181
177
  return burp_cmd
@@ -296,7 +292,10 @@ class BurpCommand(sub_command.PyntSubCommand):
296
292
  base_container=container,
297
293
  use_native=args.use_docker_native)
298
294
 
295
+ proxy_docker.prepare_client()
296
+ proxy_docker.pre_run_validation(args.port)
299
297
  proxy_docker.run()
298
+
300
299
  ui_thread.print_generator(proxy_docker.stdout)
301
300
 
302
301
  util.wait_for_healthcheck("http://localhost:{}".format(args.port))
@@ -355,3 +354,4 @@ class BurpCommand(sub_command.PyntSubCommand):
355
354
  json_obj = json.loads(json_report)
356
355
  if json_obj:
357
356
  util.check_for_findings_or_warnings(args, json_obj)
357
+ util.check_severity(args.severity_level, json_obj)
@@ -42,7 +42,7 @@ def command_usage():
42
42
  .with_line("\t--no-proxy-export - Pynt will not export the proxy settings to the environment")
43
43
  .with_line("\t--application-id - Attach the scan to an application, you can find the ID in your applications area at app.pynt.io")
44
44
  .with_line("\t--host-ca - Path to the CA file in PEM format to enable SSL certificate verification for pynt when running through a VPN.")
45
- .with_line("\t--return-error - 'all-findings' (warnings, or errors), 'errors-only', 'never' (default) ")
45
+ .with_line("\t--severity-level - 'all', 'medium', 'high', 'critical', 'none' (default) ")
46
46
  .with_line("\t--verbose - Use to get more detailed information about the run")
47
47
  )
48
48
 
@@ -70,7 +70,7 @@ class CommandSubCommand(sub_command.PyntSubCommand):
70
70
  proxy_cmd.add_argument("--ca-path", type=str, default="")
71
71
  proxy_cmd.add_argument("--self-signed", action="store_true")
72
72
  proxy_cmd.add_argument("--report", type=str, default="")
73
- proxy_cmd.add_argument("--return-error", choices=["all-findings", "errors-only", "never"], default="never")
73
+ proxy_cmd.add_argument("--severity-level", choices=["all", "medium", "high", "critical", "none"], default="none")
74
74
  proxy_cmd.print_usage = self.print_usage
75
75
  proxy_cmd.print_help = self.print_usage
76
76
  return proxy_cmd
@@ -183,7 +183,10 @@ class CommandSubCommand(sub_command.PyntSubCommand):
183
183
  base_container=container,
184
184
  use_native=args.use_docker_native)
185
185
 
186
+ proxy_docker.prepare_client()
187
+ proxy_docker.pre_run_validation(args.port)
186
188
  proxy_docker.run()
189
+
187
190
  ui_thread.print_generator(proxy_docker.stdout)
188
191
 
189
192
  util.wait_for_healthcheck("http://localhost:{}".format(args.port))
@@ -261,3 +264,4 @@ class CommandSubCommand(sub_command.PyntSubCommand):
261
264
  json_obj = json.loads(json_report)
262
265
  if json_obj:
263
266
  util.check_for_findings_or_warnings(args, json_obj)
267
+ util.check_severity(args.severity_level, json_obj)
pyntcli/commands/har.py CHANGED
@@ -30,6 +30,7 @@ def har_usage():
30
30
  .with_line(
31
31
  "\t--host-ca - Path to the CA file in PEM format to enable SSL certificate verification for pynt when running through a VPN."
32
32
  )
33
+ .with_line("\t--severity-level - 'all', 'medium', 'high', 'critical', 'none' (default) ")
33
34
  .with_line("\t--verbose - Use to get more detailed information about the run")
34
35
  .with_line("")
35
36
  )
@@ -49,6 +50,7 @@ class HarSubCommand(sub_command.PyntSubCommand):
49
50
  "--captured-domains", nargs="+", help="", default="", required=True
50
51
  )
51
52
  har_cmd.add_argument("--reporters", action="store_true")
53
+ har_cmd.add_argument("--severity-level", choices=["all", "medium", "high", "critical", "none"], default="none")
52
54
  har_cmd.print_usage = self.usage
53
55
  har_cmd.print_help = self.usage
54
56
  return har_cmd
@@ -91,6 +93,8 @@ class HarSubCommand(sub_command.PyntSubCommand):
91
93
  base_container=container,
92
94
  use_native=args.use_docker_native)
93
95
 
96
+ har_docker.prepare_client()
97
+ har_docker.pre_run_validation(port)
94
98
  har_docker.run()
95
99
 
96
100
  healthcheck = partial(
@@ -36,7 +36,7 @@ def listen_usage():
36
36
  .with_line("\t--application-id - Attach the scan to an application, you can find the ID in your applications area at app.pynt.io")
37
37
  .with_line("\t--insecure - use when target uses self signed certificates")
38
38
  .with_line("\t--host-ca - path to the CA file in PEM format to enable SSL certificate verification for pynt when running through a VPN.")
39
- .with_line("\t--return-error - 'all-findings' (warnings, or errors), 'errors-only', 'never' (default) ")
39
+ .with_line("\t--severity-level - 'all', 'medium', 'high', 'critical', 'none' (default) ")
40
40
  .with_line("\t--verbose - Use to get more detailed information about the run")
41
41
  )
42
42
 
@@ -61,7 +61,7 @@ class ListenSubCommand(sub_command.PyntSubCommand):
61
61
  listen_cmd.add_argument("--allow-errors", action="store_true")
62
62
  listen_cmd.add_argument("--ca-path", type=str, default="")
63
63
  listen_cmd.add_argument("--report", type=str, default="")
64
- listen_cmd.add_argument("--return-error", choices=["all-findings", "errors-only", "never"], default="never")
64
+ listen_cmd.add_argument("--severity-level", choices=["all", "medium", "high", "critical", "none"], default="none")
65
65
  listen_cmd.print_usage = self.print_usage
66
66
  listen_cmd.print_help = self.print_usage
67
67
  return listen_cmd
@@ -140,7 +140,11 @@ class ListenSubCommand(sub_command.PyntSubCommand):
140
140
  tag="proxy-latest",
141
141
  detach=True,
142
142
  base_container=container)
143
+
144
+ proxy_docker.prepare_client()
145
+ proxy_docker.pre_run_validation(args.port)
143
146
  proxy_docker.run()
147
+
144
148
  ui_thread.print_generator(proxy_docker.stdout)
145
149
 
146
150
  util.wait_for_healthcheck("http://localhost:{}".format(args.port))
@@ -208,3 +212,4 @@ class ListenSubCommand(sub_command.PyntSubCommand):
208
212
  json_obj = json.loads(json_report)
209
213
  if json_obj:
210
214
  util.check_for_findings_or_warnings(args, json_obj)
215
+ util.check_severity(args.severity_level, json_obj)
@@ -30,9 +30,7 @@ def newman_usage():
30
30
  .with_line(
31
31
  "\t--application-id - Attach the scan to an application, you can find the ID in your applications area at app.pynt.io"
32
32
  )
33
- .with_line(
34
- "\t--return-error - 'all-findings' (warnings, or errors), 'errors-only', 'never' (default) "
35
- )
33
+ .with_line("\t--severity-level - 'all', 'medium', 'high', 'critical', 'none' (default) ")
36
34
  .with_line("\t--verbose - Use to get more detailed information about the run")
37
35
  )
38
36
 
@@ -51,12 +49,7 @@ class NewmanSubCommand(sub_command.PyntSubCommand):
51
49
  newman_cmd.add_argument(
52
50
  "--reporters", action="store_true", default=False, required=False
53
51
  )
54
- newman_cmd.add_argument(
55
- "--return-error",
56
- choices=["all-findings", "errors-only", "never"],
57
- default="never"
58
- )
59
-
52
+ newman_cmd.add_argument("--severity-level", choices=["all", "medium", "high", "critical", "none"], default="none")
60
53
  newman_cmd.print_usage = self.usage
61
54
  newman_cmd.print_help = self.usage
62
55
  return newman_cmd
@@ -113,7 +106,11 @@ class NewmanSubCommand(sub_command.PyntSubCommand):
113
106
  detach=True,
114
107
  base_container=container,
115
108
  use_native=args.use_docker_native)
109
+
110
+ newman_docker.prepare_client()
111
+ newman_docker.pre_run_validation(port)
116
112
  newman_docker.run()
113
+
117
114
  healthcheck = partial(
118
115
  util.wait_for_healthcheck, "http://localhost:{}".format(port)
119
116
  )
@@ -102,8 +102,10 @@ class PostmanSubCommand(sub_command.PyntSubCommand):
102
102
  detach=True,
103
103
  base_container=container,
104
104
  use_native=args.use_docker_native)
105
-
105
+ postman_docker.prepare_client()
106
+ postman_docker.pre_run_validation(args.port)
106
107
  postman_docker.run()
108
+
107
109
  ui_thread.print_generator(postman_docker.stdout)
108
110
 
109
111
  util.wait_for_healthcheck("http://localhost:{}".format(args.port))
@@ -1,15 +1,22 @@
1
1
  import argparse
2
+ import requests
2
3
  from typing import Dict, List
4
+ from datetime import datetime, timedelta
3
5
  from pyntcli import __version__ as cli_version
4
6
  from pyntcli.analytics import send as analytics
5
7
  from pyntcli.transport import pynt_requests
6
8
  from pyntcli.ui import ui_thread
9
+ from pyntcli.ui import prompt
10
+ from pyntcli.store import CredStore, StateStore
11
+ import pyntcli.log.log as log
7
12
 
8
13
  from requests.exceptions import SSLError, HTTPError
9
14
 
10
15
 
11
16
  from . import command, listen, postman, root, sub_command, id_command, newman, har, burp
12
17
 
18
+ logger = log.get_logger()
19
+
13
20
  avail_sub_commands = [
14
21
  postman.PostmanSubCommand("postman"),
15
22
  id_command.PyntShowIdCommand("pynt-id"),
@@ -20,11 +27,17 @@ avail_sub_commands = [
20
27
  burp.BurpCommand("burp"),
21
28
  ]
22
29
 
30
+ commands_without_app_id = ["postman", "pynt-id"]
31
+
23
32
 
24
33
  class PyntCommandException(Exception):
25
34
  pass
26
35
 
27
36
 
37
+ class UserAbortedException(Exception):
38
+ pass
39
+
40
+
28
41
  class BadArgumentsException(PyntCommandException):
29
42
  pass
30
43
 
@@ -48,18 +61,22 @@ def check_is_latest_version(current_version):
48
61
  ui_thread.print(ui_thread.PrinterText("""Pynt CLI new version is available, upgrade now with:
49
62
  python3 -m pip install --upgrade pyntcli""", ui_thread.PrinterText.WARNING))
50
63
  except SSLError:
51
- ui_thread.print(ui_thread.PrinterText("""Error: Unable to check if Pynt CLI version is up-to-date due to VPN/proxy. Run Pynt with --insecure to fix.""", ui_thread.PrinterText.WARNING))
64
+ ui_thread.print(ui_thread.PrinterText(
65
+ """Error: Unable to check if Pynt CLI version is up-to-date due to VPN/proxy. Run Pynt with --insecure to fix.""", ui_thread.PrinterText.WARNING))
52
66
  except HTTPError:
53
- ui_thread.print("""Unable to check if Pynt CLI version is up-to-date""")
67
+ ui_thread.print(
68
+ """Unable to check if Pynt CLI version is up-to-date""")
54
69
  except Exception as e:
55
- ui_thread.print(ui_thread.PrinterText("""We could not check for updates.""", ui_thread.PrinterText.WARNING))
70
+ ui_thread.print(ui_thread.PrinterText(
71
+ """We could not check for updates.""", ui_thread.PrinterText.WARNING))
56
72
  pass
57
73
 
58
74
 
59
75
  class PyntCommand:
60
76
  def __init__(self) -> None:
61
77
  self.base: root.BaseCommand = root.BaseCommand()
62
- self.sub_commands: Dict[str, sub_command.PyntSubCommand] = {sc.get_name(): sc for sc in avail_sub_commands}
78
+ self.sub_commands: Dict[str, sub_command.PyntSubCommand] = {
79
+ sc.get_name(): sc for sc in avail_sub_commands}
63
80
  self._start_command()
64
81
 
65
82
  def _start_command(self):
@@ -77,15 +94,93 @@ class PyntCommand:
77
94
  command = getattr(args, "command")
78
95
  if not command in self.sub_commands:
79
96
  raise NoSuchCommandException()
80
-
97
+ check_is_latest_version(cli_version)
98
+ analytics.emit(analytics.CLI_START)
81
99
  if "host_ca" in args and args.host_ca:
82
100
  pynt_requests.add_host_ca(args.host_ca)
83
101
 
84
102
  if "insecure" in args and args.insecure:
85
103
  pynt_requests.disable_tls_termination()
86
104
 
87
- check_is_latest_version(cli_version)
88
- analytics.emit(analytics.CLI_START)
89
-
90
105
  self.base.run_cmd(args)
106
+
107
+ # Some args are recommended/required for business-plan users.
108
+ # These can be verified only after running 'self.base.run_cmd()' where the login occurs
109
+ is_business_plan_user = self._is_business_plan_user()
110
+ try:
111
+ self._post_login_args_validation(
112
+ args, command, is_business_plan_user)
113
+ except UserAbortedException as e:
114
+ ui_thread.print("Aborting...")
115
+ return
116
+
91
117
  self.sub_commands[command].run_cmd(args)
118
+
119
+ def _is_business_plan_user(self):
120
+ """
121
+ A workaround for checking whether it's a free tier user or not
122
+ by try accessing api.pynt.io/v1/config (it's a free tier if the user can't access it)
123
+
124
+ Returns:
125
+ bool: True if it's a business-plan user
126
+ """
127
+ saas_url = "https://api.pynt.io/v1/config"
128
+ headers = {}
129
+ with CredStore() as store:
130
+ access_keys = store.get_access_token()
131
+ token_type = store.get_token_type()
132
+ headers["Authorization"] = f"{token_type} {access_keys}"
133
+ try:
134
+ response = requests.get(saas_url, headers=headers, timeout=10)
135
+
136
+ if response.status_code == requests.codes.ok:
137
+ return True
138
+ else:
139
+ return False
140
+ except requests.exceptions.RequestException as e:
141
+ # Catch all other request exceptions
142
+ logger.debug(f"An error occurred when accessing '{saas_url}': {e}")
143
+ return False
144
+
145
+ def _post_login_args_validation(self, args: argparse.Namespace, command: str, is_business_plan_user: bool):
146
+ # Confirm not using application-id flag if applicable
147
+ if getattr(args, "application_id") or command in commands_without_app_id:
148
+ return
149
+
150
+ # For a business user, it's recommended to always provide the application-id.
151
+ if is_business_plan_user:
152
+ if getattr(args, "yes") or self._is_missing_app_id_confirmed():
153
+ return
154
+ else:
155
+ raise UserAbortedException()
156
+
157
+ def _is_missing_app_id_confirmed(self) -> bool:
158
+ """
159
+ Ask for the user's confirmation to continue if he/she wasn't asked in the last week.
160
+
161
+ Returns:
162
+ bool: True if the user confirms or he/she has confirmed once in the last week. Otherwise, returns False
163
+ """
164
+ with StateStore() as state_store:
165
+ current_time = datetime.now()
166
+ prompts_history = state_store.get_prompts_history()
167
+ last_confirmed = prompts_history.get(
168
+ "missing_app_id", {}).get("last_confirmation", "")
169
+ if last_confirmed:
170
+ # Calculate the time delta
171
+ parsed_datetime = datetime.strptime(
172
+ last_confirmed, "%Y-%m-%d %H:%M:%S")
173
+ difference = current_time - parsed_datetime
174
+ if difference < timedelta(days=7):
175
+ return True
176
+
177
+ if prompt.confirmation_prompt_with_timeout("Application ID is missing. Use the '--application-id' flag to provide it.\n" +
178
+ "Without an Application ID, the scan will not be associated with your application.\n" +
179
+ "The Application ID can be fetched from https://app.pynt.io/dashboard/applications.\n" +
180
+ "Do you want to continue without associating the scan?", default="yes", timeout=15):
181
+ prompts_history["missing_app_id"] = {"last_confirmation": current_time.strftime("%Y-%m-%d %H:%M:%S")}
182
+ state_store.put_prompts_history(
183
+ prompts_history)
184
+ return True
185
+
186
+ return False
pyntcli/commands/root.py CHANGED
@@ -63,17 +63,27 @@ class BaseCommand:
63
63
  action="store_true",
64
64
  help="use when target uses self signed certificates",
65
65
  )
66
- parser.add_argument("--dev-flags", type=str, default="", help=argparse.SUPPRESS)
66
+ parser.add_argument("--dev-flags", type=str,
67
+ default="", help=argparse.SUPPRESS)
67
68
  parser.add_argument("--host-ca", type=str, default="")
68
69
  parser.add_argument("--transport-config", type=str, default="")
69
- parser.add_argument("--application-id", type=str, default="", required=False)
70
+ parser.add_argument("--application-id", type=str,
71
+ default="", required=False)
70
72
  parser.add_argument("--proxy", type=str, default="", required=False)
73
+ parser.add_argument(
74
+ "--yes",
75
+ "-y",
76
+ default=False,
77
+ required=False,
78
+ action="store_true",
79
+ help="Automatically answer yes to all prompts and run non-interactively ",
80
+ )
71
81
  parser.add_argument(
72
82
  "--verbose",
73
83
  default=False,
74
84
  required=False,
75
85
  action="store_true",
76
- help="use to get more detailed execution information",
86
+ help="Use to get more detailed execution information",
77
87
  )
78
88
  parser.add_argument(
79
89
  "--use-docker-native",
pyntcli/commands/util.py CHANGED
@@ -68,6 +68,10 @@ class SomeFindingsOrWarningsException(Exception):
68
68
  pass
69
69
 
70
70
 
71
+ class SeverityException(Exception):
72
+ pass
73
+
74
+
71
75
  @contextmanager
72
76
  def create_default_file_mounts(args):
73
77
  html_report_path = os.path.join(tempfile.gettempdir(), "results.html")
@@ -93,8 +97,10 @@ def create_default_file_mounts(args):
93
97
  report.PyntReporter(json_report_path).print_summary()
94
98
 
95
99
  check_for_findings_or_warnings(args, json.load(open(json_report_path)))
100
+ check_severity(args.severity_level, json.load(open(json_report_path)))
96
101
 
97
102
 
103
+ # Deprecate - keep it for backward customers that use it
98
104
  def check_for_findings_or_warnings(args, json_report):
99
105
  security_tests = json_report.get("securityTests", {})
100
106
  findings = security_tests.get("Findings", 0)
@@ -105,3 +111,30 @@ def check_for_findings_or_warnings(args, json_report):
105
111
 
106
112
  if "return_error" in args and args.return_error == "all-findings" and warnings != 0:
107
113
  raise SomeFindingsOrWarningsException()
114
+
115
+
116
+ def check_severity(severity_flag, json_report):
117
+ severity_levels = ['medium', 'high', 'critical']
118
+
119
+ if severity_flag is None or severity_flag.lower() == "none":
120
+ return
121
+
122
+ risk_data = json_report.get("securityTests", {})
123
+
124
+ # Normalize all te keys to lower to reduce the risk that user input is diff then what we have internally
125
+ risks_counter = {key.lower(): value for key, value in risk_data['RisksCounter'].items()}
126
+
127
+ severity_filter = severity_flag.lower()
128
+
129
+ if severity_filter == "all":
130
+ severities_to_check = severity_levels
131
+ else:
132
+ if severity_filter not in severity_levels:
133
+ raise ValueError(f"Invalid user filter for severity level: {severity_filter}")
134
+
135
+ flag_index = severity_levels.index(severity_filter)
136
+ severities_to_check = severity_levels[flag_index:]
137
+
138
+ for severity in severities_to_check:
139
+ if risks_counter.get(severity, 0) > 0:
140
+ raise SeverityException(f"Severity '{severity}' has a count greater than 0.")
pyntcli/main.py CHANGED
@@ -12,7 +12,7 @@ from requests.exceptions import SSLError
12
12
  from requests.exceptions import ProxyError
13
13
  from pyntcli.transport.pynt_requests import InvalidPathException, InvalidCertFormat
14
14
  from pyntcli.commands.util import HtmlReportNotCreatedException
15
- from pyntcli.commands.util import SomeFindingsOrWarningsException
15
+ from pyntcli.commands.util import SomeFindingsOrWarningsException, SeverityException
16
16
  from pyntcli.commands.postman import PyntWebSocketException
17
17
  from pyntcli import __version__
18
18
 
@@ -131,6 +131,8 @@ def main():
131
131
  analytics.emit(analytics.ERROR, {"error": "port in use. e: {}".format(e)})
132
132
  except SomeFindingsOrWarningsException as e:
133
133
  exit(1)
134
+ except SeverityException as e:
135
+ exit(1)
134
136
  except Exception as e:
135
137
  analytics.emit(analytics.ERROR, {"error": "{}".format(e)})
136
138
  pynt_errors.unexpected_error(e)
@@ -2,6 +2,7 @@ import platform
2
2
  import subprocess
3
3
  import docker
4
4
  from docker.errors import DockerException, APIError, ImageNotFound
5
+ from requests.exceptions import ReadTimeout
5
6
  from docker.types import Mount
6
7
  import os
7
8
  import json
@@ -17,6 +18,9 @@ from pyntcli.store import CredStore
17
18
  from pyntcli.auth.login import PYNT_ID, PYNT_SAAS, PYNT_BUCKET_NAME, PYNT_PARAM1, PYNT_PARAM2
18
19
 
19
20
  PYNT_DOCKER_IMAGE = "ghcr.io/pynt-io/pynt"
21
+ IMAGE_TAGS = ["postman-latest", "newman-latest", "har-latest", "proxy-latest"]
22
+
23
+ PYNT_CONTAINER_INTERNAL_PORT = "5001"
20
24
 
21
25
 
22
26
  def create_mount(src, destination, mount_type="bind"):
@@ -37,7 +41,8 @@ class ImageUnavailableException(Exception):
37
41
 
38
42
  class PortInUseException(Exception):
39
43
  def __init__(self, port=""):
40
- self.message = ui_thread.print(ui_thread.PrinterText("Port: {} already in use, please use a different one".format(port), ui_thread.PrinterText.WARNING))
44
+ self.message = ui_thread.print(
45
+ ui_thread.PrinterText(f"Port: {port} already in use, please use a different one", ui_thread.PrinterText.WARNING))
41
46
  super().__init__(self.message)
42
47
 
43
48
 
@@ -87,8 +92,6 @@ def get_container_with_arguments(args: argparse.Namespace, *port_args: PyntDocke
87
92
  docker_arguments = []
88
93
  ports = {}
89
94
  for p in port_args:
90
- if container_utils.is_port_in_use(p.dest):
91
- raise PortInUseException(p.dest)
92
95
  if "desktop" in get_docker_type().lower():
93
96
  ports[str(p.src)] = int(p.dest)
94
97
  else:
@@ -189,9 +192,11 @@ class PyntContainerNative:
189
192
 
190
193
  return len(result.stdout.splitlines()) > 1
191
194
 
195
+ def prepare_client(self):
196
+ pass
197
+
192
198
  def run(self):
193
199
  self.running = True
194
- self.kill_other_instances()
195
200
 
196
201
  self.get_image()
197
202
  args = self.base_container.docker_arguments if self.base_container.docker_arguments else None
@@ -247,18 +252,22 @@ class PyntContainerNative:
247
252
 
248
253
  self.stdout = logs_stdout
249
254
 
250
- def kill_other_instances(self):
255
+ def kill_other_instances(self, report_to_user=True):
256
+ ui_thread.print_verbose("Killing other pynt containers if such exist")
251
257
  try:
252
- ui_thread.print_verbose("Killing other pynt containers if such exist")
253
- command = ["docker", "ps", "-q", "-f", f"ancestor={self.image_name}:{self.tag}"]
254
- containers_output = subprocess.check_output(command, text=True)
255
- if not containers_output:
256
- return
257
-
258
- container_ids = containers_output.splitlines()
259
- for container_id in container_ids:
260
- command = ["docker", "kill", container_id]
261
- subprocess.run(command)
258
+ for tag in IMAGE_TAGS:
259
+ command = ["docker", "ps", "-q", "-f", f"ancestor={self.image_name}:{tag}"]
260
+ containers_output = subprocess.check_output(command, text=True)
261
+ if not containers_output:
262
+ continue
263
+
264
+ container_ids = containers_output.splitlines()
265
+ for container_id in container_ids:
266
+ command = ["docker", "kill", container_id]
267
+ subprocess.run(command)
268
+ if report_to_user:
269
+ ui_thread.print(
270
+ ui_thread.PrinterText("Another Pynt container was running, killed it", ui_thread.PrinterText))
262
271
 
263
272
  except subprocess.CalledProcessError:
264
273
  analytics.emit(analytics.ERROR, {"error": "Unable to kill other pynt containers"})
@@ -305,7 +314,7 @@ class PyntContainerNative:
305
314
  def stop(self):
306
315
  if not self.running:
307
316
  return
308
- self.kill_other_instances()
317
+ self.kill_other_instances(report_to_user=False)
309
318
  self.running = False
310
319
 
311
320
  def adapt_run_command(self, docker_command=[]):
@@ -333,6 +342,7 @@ class PyntContainerSDK:
333
342
  self.container_name = ""
334
343
  self.stdout = None
335
344
  self.running = False
345
+ self.system = platform.system().lower()
336
346
 
337
347
  def _initialize(self):
338
348
  self.docker_client = docker.from_env()
@@ -352,15 +362,16 @@ class PyntContainerSDK:
352
362
 
353
363
  return l[0].status == "running"
354
364
 
365
+ def prepare_client(self):
366
+ if not self.docker_client:
367
+ self._initialize()
368
+
355
369
  def run(self):
356
370
  if not self.docker_client:
357
371
  self._initialize()
358
372
 
359
373
  self.running = True
360
374
 
361
- ui_thread.print_verbose("Killing other pynt containers if such exist")
362
- self.kill_other_instances()
363
-
364
375
  image = self.get_image()
365
376
  ui_thread.print(ui_thread.PrinterText("Docker pull done", ui_thread.PrinterText.INFO))
366
377
 
@@ -383,10 +394,25 @@ class PyntContainerSDK:
383
394
  self.container_name = c.name
384
395
  self.stdout = c.logs(stream=True)
385
396
 
386
- def kill_other_instances(self):
397
+ def kill_other_instances(self, report_to_user=True):
387
398
  for c in self.docker_client.containers.list():
388
399
  if len(c.image.tags) and _container_image_from_tag(c.image.tags[0]) == self.image_name:
389
400
  c.kill()
401
+ self.wait_for_container_end(c)
402
+ if report_to_user:
403
+ ui_thread.print(ui_thread.PrinterText("Another Pynt container was running, killed it", ui_thread.PrinterText))
404
+
405
+ def wait_for_container_end(self, container):
406
+ # only windows kill is require a wait for the container to stop, otherwise the port stays in use
407
+ if self.system != "windows":
408
+ return
409
+ try:
410
+ container.wait(timeout=10)
411
+ except ReadTimeout: # container is still running
412
+ ui_thread.print(
413
+ ui_thread.PrinterText("Timeout reached while waiting for container to stop", ui_thread.PrinterText))
414
+ except APIError: # container is already stopped
415
+ pass
390
416
 
391
417
  def pull_image(self):
392
418
  try:
@@ -410,7 +436,7 @@ class PyntContainerSDK:
410
436
  def stop(self):
411
437
  if not self.running:
412
438
  return
413
- self.kill_other_instances()
439
+ self.kill_other_instances(report_to_user=False)
414
440
  self.docker_client.close()
415
441
  self.docker_client = None
416
442
  self.running = False
@@ -452,6 +478,15 @@ class PyntContainer:
452
478
  self.stdout = self.client_implementation.stdout
453
479
  PyntContainerRegistry.instance().register_container(self)
454
480
 
481
+ def pre_run_validation(self, port):
482
+ self.kill_other_instances()
483
+
484
+ if container_utils.is_port_in_use(int(port)):
485
+ raise PortInUseException(port)
486
+
487
+ def prepare_client(self):
488
+ self.client_implementation.prepare_client()
489
+
455
490
  def running(self):
456
491
  return self.client_implementation.running
457
492
 
pyntcli/store/__init__.py CHANGED
@@ -1 +1 @@
1
- from .store import CredStore
1
+ from .store import CredStore, StateStore
pyntcli/store/store.py CHANGED
@@ -22,7 +22,8 @@ class Store():
22
22
 
23
23
  if not os.path.exists(self.file_location):
24
24
  with open(self.file_location, "w") as f:
25
- self.connector = self._connector_type(self._connector_type.default_value())
25
+ self.connector = self._connector_type(
26
+ self._connector_type.default_value())
26
27
  return
27
28
 
28
29
  with open(self.file_location, "r+") as f:
@@ -39,6 +40,9 @@ class Store():
39
40
  def get_path(self):
40
41
  return self.file_location
41
42
 
43
+ def _get_default_store_dir(self):
44
+ return os.path.join(os.path.expanduser("~"), ".pynt")
45
+
42
46
  def __enter__(self):
43
47
  self._get_file_data()
44
48
  return self
@@ -50,14 +54,31 @@ class Store():
50
54
 
51
55
  class CredStore(Store):
52
56
  def __init__(self) -> None:
53
- dir = ".pynt"
54
- super().__init__(file_location=os.path.join(os.path.expanduser("~"), dir, "creds.json"),
57
+ pynt_dir = super()._get_default_store_dir()
58
+ super().__init__(file_location=os.path.join(pynt_dir, "creds.json"),
55
59
  connector_type=JsonStoreConnector)
56
60
 
57
61
  def get_access_token(self):
58
62
  return self.get("token")["access_token"]
59
63
 
64
+ def get_token_type(self):
65
+ return self.get("token")["token_type"]
66
+
60
67
  def get_tokens(self):
61
68
  all_tokens = self.get("token")
62
- token_to_json_string = '{"token":' + str(all_tokens).replace("\'", "\"") + "}"
69
+ token_to_json_string = '{"token":' + \
70
+ str(all_tokens).replace("\'", "\"") + "}"
63
71
  return token_to_json_string
72
+
73
+
74
+ class StateStore(Store):
75
+ def __init__(self) -> None:
76
+ pynt_dir = super()._get_default_store_dir()
77
+ super().__init__(file_location=os.path.join(pynt_dir, "state.json"),
78
+ connector_type=JsonStoreConnector)
79
+
80
+ def get_prompts_history(self):
81
+ return self.get("prompts_history") or {}
82
+
83
+ def put_prompts_history(self, value):
84
+ return self.put("prompts_history", value)
pyntcli/ui/prompt.py ADDED
@@ -0,0 +1,53 @@
1
+ import signal
2
+ from pyntcli.ui import ui_thread
3
+
4
+
5
+ class TimeoutExpired(Exception):
6
+ pass
7
+
8
+
9
+ def _timeout_handler(signum, frame):
10
+ raise TimeoutExpired
11
+
12
+
13
+ def confirmation_prompt_with_timeout(question, default, timeout=10):
14
+ """Prompt the user with a confirmation question (Yes/No) and return True for 'yes' or False for 'no'.
15
+
16
+ Args:
17
+ question (str): The question to present to the user.
18
+ default (str): The default answer if the user just presses Enter. It should be 'yes' or 'no'.
19
+ timeout (int): The timeout in seconds. If the user doesn't respond within the timeout, the default answer will be selected.
20
+
21
+ Returns:
22
+ bool: True if the answer is 'yes', False if the answer is 'no'.
23
+ """
24
+ valid = {"yes": True, "y": True, "no": False, "n": False}
25
+
26
+ if default is None:
27
+ prompt = " [y/n] "
28
+ elif default == "yes":
29
+ prompt = " [Y/n] "
30
+ elif default == "no":
31
+ prompt = " [y/N] "
32
+ else:
33
+ raise ValueError(f"Invalid default answer: '{default}'")
34
+
35
+ signal.signal(signal.SIGALRM, _timeout_handler)
36
+ signal.alarm(timeout)
37
+
38
+ try:
39
+ while True:
40
+ choice = input(question + prompt).strip().lower()
41
+
42
+ if choice == "" and default: # Only 'Enter' with default will continue
43
+ return valid[default]
44
+ elif choice in valid:
45
+ return valid[choice]
46
+ else:
47
+ ui_thread.print("Please respond with 'yes' or 'no' (or 'y' or 'n').")
48
+ except TimeoutExpired:
49
+ ui_thread.print(ui_thread.PrinterText(f"\nNo response within {timeout} seconds, defaulting to {'Yes' if default == 'yes' else 'No'}.\n" +
50
+ "You can use '--yes' flag for auto-confirm", ui_thread.PrinterText.WARNING))
51
+ return valid[default]
52
+ finally:
53
+ signal.alarm(0)
@@ -1,16 +1,16 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyntcli
3
- Version: 0.1.92
3
+ Version: 0.1.94
4
4
  Summary: Command line utility to handle all of Pynt's different integrations
5
5
  Author-email: Pynt-io <support@pynt.io>
6
6
  Project-URL: Homepage, https://pynt.io
7
7
  Requires-Python: >=3.7
8
8
  Requires-Dist: docker
9
9
  Requires-Dist: rich
10
- Requires-Dist: requests ==2.31.0
10
+ Requires-Dist: requests==2.31.0
11
11
  Requires-Dist: pem
12
- Requires-Dist: certifi >=2017.4.17
13
- Requires-Dist: logzio-python-handler >=4.1.0
12
+ Requires-Dist: certifi>=2017.4.17
13
+ Requires-Dist: logzio-python-handler>=4.1.0
14
14
  Requires-Dist: websocket-client
15
15
  Requires-Dist: xmltodict
16
16
 
@@ -0,0 +1,46 @@
1
+ ignoreTests/conftest.py,sha256=gToq5K74GtgeGQXjFvXSzMaE6axBYxAzcFG5XJPOXjI,427
2
+ ignoreTests/auth/login.py,sha256=KFlzWhXBAuwdi7GXf16gCB3ya94LQG2wjcSChE149rQ,3798
3
+ ignoreTests/store/cred_store.py,sha256=_7-917EtNC9eKEumO2_lt-7KuDmCwOZFaowCm7DbA_A,254
4
+ pyntcli/__init__.py,sha256=5NPBuxeQo_sfTOMuw7XfyXg8HYg3BMvDd69Cm_HznpU,402
5
+ pyntcli/main.py,sha256=HORWY5AEK5xmEQybUZBW9ZEDIga8vSnqDfmAR3TY9F0,6502
6
+ pyntcli/analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ pyntcli/analytics/send.py,sha256=9TRAEoPbv4rWOZfcNaGanrRJAFvNs39P-uKSl49GcQE,3679
8
+ pyntcli/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ pyntcli/auth/login.py,sha256=TljsRXbEkNI1YUrKm5mlTw4YiecYScYUsit8Z8vstss,5228
10
+ pyntcli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ pyntcli/commands/burp.py,sha256=TYUspSLhubtKEL4ShUOvT7Fnq3MFsFo-HyrPXiZboZ4,12380
12
+ pyntcli/commands/command.py,sha256=cMVrlxMZgx0jFrlpMTKPEYfsSv2RhN6XuPrRqqqYB6k,11125
13
+ pyntcli/commands/har.py,sha256=yUuLvxlAoq2dQotJe8PV02sZD-WgYSefzGc6bhdroSo,4464
14
+ pyntcli/commands/id_command.py,sha256=UBEgMIpm4vauTCsKyixltiGUolNg_OfHEJvJ_i5BpJY,943
15
+ pyntcli/commands/listen.py,sha256=b_ES6VRmPFkIkCCQ6MuP6KOeUlf5vT1e7AafnDqJVak,9063
16
+ pyntcli/commands/newman.py,sha256=o3kobgUFoC3jw2XmbYEsQNdYpGO5suFA92lxHw71dzw,5301
17
+ pyntcli/commands/postman.py,sha256=VNARuyCDaDYlrzRMmJk4oM4YTCExOQUDziOS8NpU-KA,5256
18
+ pyntcli/commands/pynt_cmd.py,sha256=T7jee0yw67Zth3lTkSDInCLnbu_IhpNqb7GqnTiTceQ,7012
19
+ pyntcli/commands/root.py,sha256=4yeGlTeL3Xu2xlSbMYhhaDwUnP29sF2Y2sxq8E1P3lo,3986
20
+ pyntcli/commands/static_file_extensions.py,sha256=PZJb02BI-64tbU-j3rdCNsXzTh7gkIDGxGKbKNw3h5k,1995
21
+ pyntcli/commands/sub_command.py,sha256=GF3-rE_qk2L4jGPFqHLm9SdGINmu3EakhjJTFyWjRms,374
22
+ pyntcli/commands/util.py,sha256=csZHQ2Xbdh-_KX-yIVrnaeNsT0NbuS-ej6kND3CxD_w,4414
23
+ pyntcli/log/__init__.py,sha256=cOGwOYzMoshEbZiiasBGkj6wF0SBu3Jdpl-AuakDesw,19
24
+ pyntcli/log/log.py,sha256=cWCdWmUaAwePwdhYDcgNMEG9d9RM34sGahxBCYEdv2Y,1069
25
+ pyntcli/pynt_docker/__init__.py,sha256=PQIOVxc7XXtMLfEX7ojgwf_Z3mmTllO3ZvzUZTPOxQY,30
26
+ pyntcli/pynt_docker/container_utils.py,sha256=_Onn7loInzyJAG2-Uk6CGpsuRyelmUFHOvtJj4Uzi9A,175
27
+ pyntcli/pynt_docker/pynt_container.py,sha256=R2iq4Gbbkxg3OEJfJwUp-vroHf9-pJ6HWFqvWucPsGA,18965
28
+ pyntcli/store/__init__.py,sha256=1fP8cEAQCF_myja3gnhHH9FEqtBiOJ-2aBmUXSKBdFA,41
29
+ pyntcli/store/json_connector.py,sha256=UGs3uORw3iyn0YJ8kzab-veEZToA6d-ByXYuqEleWsA,560
30
+ pyntcli/store/store.py,sha256=ZLSe0WAjHDp8cSt4BBFDkPGRux4cgOo5UfF7V4naM7U,2559
31
+ pyntcli/store/store_connector.py,sha256=w4LzcpRZesUZL1f63RmLlWEFRtJ6Y6rcS6PkkGtO4MA,357
32
+ pyntcli/transport/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
+ pyntcli/transport/pynt_requests.py,sha256=C7OPvcKkRTcxSYuyiWKE59KgA9sRX0d6fm1wnopAmPo,1719
34
+ pyntcli/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
+ pyntcli/ui/progress.py,sha256=RrnO_jJNunoyupylakmWmHOEPw3lh99OHpKBzL6OBiE,1008
36
+ pyntcli/ui/prompt.py,sha256=6URUWCGpKUPhluWTYVaXvCQuABjiFfJSE3rfRJMvAS0,1838
37
+ pyntcli/ui/pynt_errors.py,sha256=00UprD4tFViREv7kuXGQ99PAKGTpXYixxi3Ndeoeiew,689
38
+ pyntcli/ui/report.py,sha256=W-icPSZrGLOubXgam0LpOvHLl_aZg9Zx9qIkL8Ym5PE,1930
39
+ pyntcli/ui/ui_thread.py,sha256=XUBgLpYQjVhrilU-ofw7VSXgTiwneSdTxm61EvC3x4Q,5091
40
+ tests/test_utils.py,sha256=t5fTQUk1U_Js6iMxcGYGqt4C-crzOJ0CqCKtLkRtUi0,2050
41
+ tests/commands/test_pynt_cmd.py,sha256=BjGFCFACcSziLrNA6_27t6TjSmvdu54wx9njwLpRSJY,8379
42
+ pyntcli-0.1.94.dist-info/METADATA,sha256=m__wWpEYNkwzQMqrCy14feFm2kT8jxPRamGVZSk1cJ4,460
43
+ pyntcli-0.1.94.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
44
+ pyntcli-0.1.94.dist-info/entry_points.txt,sha256=kcGmqAxXDttNk2EPRcqunc_LTVp61gzakz0v-GEE2SY,43
45
+ pyntcli-0.1.94.dist-info/top_level.txt,sha256=64XSgBzSpgwjYjEKHZE7q3JH2a816zEeyZBXfJi3AKI,42
46
+ pyntcli-0.1.94.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (71.1.0)
2
+ Generator: setuptools (74.1.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,4 +1,5 @@
1
1
  automation
2
2
  dist
3
+ ignoreTests
3
4
  pyntcli
4
5
  tests
@@ -0,0 +1,178 @@
1
+ import unittest
2
+ import argparse
3
+ from datetime import datetime, timedelta
4
+ from unittest.mock import patch, MagicMock
5
+ from pyntcli.commands.pynt_cmd import PyntCommand, UserAbortedException
6
+ import requests
7
+
8
+
9
+ class TestIsBusinessPlanUser(unittest.TestCase):
10
+
11
+ @patch('pyntcli.commands.pynt_cmd.requests.get')
12
+ @patch('pyntcli.commands.pynt_cmd.CredStore')
13
+ def test_is_business_plan_user_success(self, mock_cred_store, mock_requests_get):
14
+ # Mock the CredStore context manager
15
+ mock_store_instance = MagicMock()
16
+ mock_store_instance.get_access_token.return_value = 'fake_token'
17
+ mock_store_instance.get_token_type.return_value = 'Bearer'
18
+ mock_cred_store.return_value.__enter__.return_value = mock_store_instance
19
+
20
+ # Mock the requests.get response
21
+ mock_response = MagicMock()
22
+ mock_response.status_code = 200
23
+ mock_requests_get.return_value = mock_response
24
+
25
+ # Instantiate the class and call the method
26
+ instance = PyntCommand()
27
+ result = instance._is_business_plan_user()
28
+
29
+ # Assert the result
30
+ self.assertTrue(result)
31
+
32
+ @patch('pyntcli.commands.pynt_cmd.requests.get')
33
+ @patch('pyntcli.commands.pynt_cmd.CredStore')
34
+ def test_is_business_plan_user_failure(self, mock_cred_store, mock_requests_get):
35
+ # Mock the CredStore context manager
36
+ mock_store_instance = MagicMock()
37
+ mock_store_instance.get_access_token.return_value = 'fake_token'
38
+ mock_store_instance.get_token_type.return_value = 'Bearer'
39
+ mock_cred_store.return_value.__enter__.return_value = mock_store_instance
40
+
41
+ # Mock the requests.get response
42
+ mock_response = MagicMock()
43
+ mock_response.status_code = 403
44
+ mock_requests_get.return_value = mock_response
45
+
46
+ # Instantiate the class and call the method
47
+ instance = PyntCommand()
48
+ result = instance._is_business_plan_user()
49
+
50
+ # Assert the result
51
+ self.assertFalse(result)
52
+
53
+ @patch('pyntcli.commands.pynt_cmd.requests.get')
54
+ @patch('pyntcli.commands.pynt_cmd.CredStore')
55
+ def test_is_business_plan_user_exception(self, mock_cred_store, mock_requests_get):
56
+ # Mock the CredStore context manager
57
+ mock_store_instance = MagicMock()
58
+ mock_store_instance.get_access_token.return_value = 'fake_token'
59
+ mock_store_instance.get_token_type.return_value = 'Bearer'
60
+ mock_cred_store.return_value.__enter__.return_value = mock_store_instance
61
+
62
+ # Mock the requests.get to raise an exception
63
+ mock_requests_get.side_effect = requests.exceptions.RequestException
64
+
65
+ # Instantiate the class and call the method
66
+ instance = PyntCommand()
67
+ result = instance._is_business_plan_user()
68
+
69
+ # Assert the result
70
+ self.assertFalse(result)
71
+
72
+ @patch.object(PyntCommand, '_is_missing_app_id_confirmed', return_value=True)
73
+ def test_post_login_args_validation_missing_app_id_user_confirmed(self, _):
74
+ # Instantiate the class and call the method with valid arguments
75
+ instance = PyntCommand()
76
+ try:
77
+ result = instance._post_login_args_validation(argparse.Namespace(
78
+ application_id="", yes=False), "non-postman-command", True)
79
+ except Exception as e:
80
+ self.fail(f"Unexpected exception raised {e}")
81
+
82
+ @patch.object(PyntCommand, '_is_missing_app_id_confirmed', return_value=False)
83
+ def test_post_login_args_validation_missing_app_id_yes_flag_used(self, _):
84
+ # Instantiate the class and call the method with valid arguments
85
+ instance = PyntCommand()
86
+ try:
87
+ result = instance._post_login_args_validation(argparse.Namespace(
88
+ application_id="", yes=True), "non-postman-command", True)
89
+ except Exception as e:
90
+ self.fail(f"Unexpected exception raised {e}")
91
+
92
+ @patch.object(PyntCommand, '_is_missing_app_id_confirmed', return_value=False)
93
+ def test_post_login_args_validation_missing_app_id_user_aborted(self, _):
94
+ # Instantiate the class and call the method with valid arguments
95
+ instance = PyntCommand()
96
+ with self.assertRaises(UserAbortedException):
97
+ result = instance._post_login_args_validation(argparse.Namespace(
98
+ application_id="", yes=False), "non-postman-command", True)
99
+
100
+ @patch.object(PyntCommand, '_is_missing_app_id_confirmed', return_value=False)
101
+ def test_post_login_args_validation_missing_app_id_free_tier(self, _):
102
+ # Instantiate the class and call the method with valid arguments
103
+ instance = PyntCommand()
104
+ try:
105
+ result = instance._post_login_args_validation(argparse.Namespace(
106
+ application_id="", yes=False), "non-postman-command", False)
107
+ except Exception as e:
108
+ self.fail(f"Unexpected exception raised {e}")
109
+
110
+ @patch.object(PyntCommand, '_is_missing_app_id_confirmed', return_value=False)
111
+ def test_post_login_args_validation_missing_app_id_not_required(self, _):
112
+ # Instantiate the class and call the method with valid arguments
113
+ instance = PyntCommand()
114
+ try:
115
+ result = instance._post_login_args_validation(
116
+ argparse.Namespace(application_id="", yes=False), "postman", True)
117
+ result = instance._post_login_args_validation(
118
+ argparse.Namespace(application_id="", yes=False), "pynt-id", True)
119
+ except Exception as e:
120
+ self.fail(f"Unexpected exception raised {e}")
121
+
122
+ @patch('pyntcli.commands.pynt_cmd.prompt.confirmation_prompt_with_timeout', return_value=True)
123
+ @patch('pyntcli.commands.pynt_cmd.StateStore')
124
+ def test_missing_app_id_confirmation_confirmation_expired_user_confirms(self, mock_state_store, _):
125
+ long_time_ago = datetime.now() - timedelta(days=100)
126
+ # Mock the CredStore context manager
127
+ mock_store_instance = MagicMock()
128
+ mock_store_instance.get_prompts_history.return_value = {"missing_app_id": {
129
+ "last_confirmation": long_time_ago.strftime("%Y-%m-%d %H:%M:%S")}}
130
+ mock_store_instance.put_prompts_history.return_value = None
131
+ mock_state_store.return_value.__enter__.return_value = mock_store_instance
132
+
133
+ # Instantiate the class and call the method with app_id present
134
+ instance = PyntCommand()
135
+ result = instance._is_missing_app_id_confirmed()
136
+
137
+ # Assert the result
138
+ self.assertTrue(result)
139
+
140
+ @patch('pyntcli.commands.pynt_cmd.prompt.confirmation_prompt_with_timeout', return_value=False)
141
+ @patch('pyntcli.commands.pynt_cmd.StateStore')
142
+ def test_missing_app_id_confirmation_confirmation_expired_user_aborts(self, mock_state_store, _):
143
+ long_time_ago = datetime.now() - timedelta(days=100)
144
+ # Mock the CredStore context manager
145
+ mock_store_instance = MagicMock()
146
+ mock_store_instance.get_prompts_history.return_value = {"missing_app_id": {
147
+ "last_confirmation": long_time_ago.strftime("%Y-%m-%d %H:%M:%S")}}
148
+ mock_store_instance.put_prompts_history.return_value = None
149
+ mock_state_store.return_value.__enter__.return_value = mock_store_instance
150
+
151
+ # Instantiate the class and call the method with app_id present
152
+ instance = PyntCommand()
153
+ result = instance._is_missing_app_id_confirmed()
154
+
155
+ # Assert the result
156
+ self.assertFalse(result)
157
+
158
+ @patch('pyntcli.commands.pynt_cmd.prompt.confirmation_prompt_with_timeout', return_value=False)
159
+ @patch('pyntcli.commands.pynt_cmd.StateStore')
160
+ def test_missing_app_id_confirmation_user_already_confirmed(self, mock_state_store, _):
161
+ recently = datetime.now() - timedelta(hours=1)
162
+ # Mock the CredStore context manager
163
+ mock_store_instance = MagicMock()
164
+ mock_store_instance.get_prompts_history.return_value = {"missing_app_id": {
165
+ "last_confirmation": recently.strftime("%Y-%m-%d %H:%M:%S")}}
166
+ mock_store_instance.put_prompts_history.return_value = None
167
+ mock_state_store.return_value.__enter__.return_value = mock_store_instance
168
+
169
+ # Instantiate the class and call the method with app_id present
170
+ instance = PyntCommand()
171
+ result = instance._is_missing_app_id_confirmed()
172
+
173
+ # Assert the result
174
+ self.assertTrue(result)
175
+
176
+
177
+ if __name__ == '__main__':
178
+ unittest.main()
tests/test_utils.py ADDED
@@ -0,0 +1,75 @@
1
+ import pytest
2
+ from pyntcli.commands.util import check_severity, SeverityException
3
+
4
+
5
+ @pytest.fixture
6
+ def risk_data():
7
+ return {
8
+ "securityTests": {
9
+ 'DidNotRun': 0,
10
+ 'Duration': 0,
11
+ 'Findings': 5,
12
+ 'Passed': 27,
13
+ 'RisksCounter': {'Critical': 2, 'High': 0, 'Low': 3, 'Medium': 3},
14
+ 'Warnings': 1
15
+ }
16
+ }
17
+
18
+
19
+ def test_exception_on_medium_severity(risk_data):
20
+ with pytest.raises(SeverityException):
21
+ check_severity('MEDIUM', risk_data)
22
+
23
+
24
+ def test_exception_on_high_severity(risk_data):
25
+ with pytest.raises(SeverityException):
26
+ check_severity('HIGH', risk_data)
27
+
28
+
29
+ def test_exception_on_critical_severity(risk_data):
30
+ with pytest.raises(SeverityException):
31
+ check_severity('CRITICAL', risk_data)
32
+
33
+
34
+ def test_no_exception_on_higher_than_all_severity():
35
+ data_with_low_risk_only = {
36
+ "securityTests": {
37
+ 'DidNotRun': 0,
38
+ 'Duration': 0,
39
+ 'Findings': 5,
40
+ 'Passed': 27,
41
+ 'RisksCounter': {'Critical': 0, 'High': 0, 'Low': 3, 'Medium': 0},
42
+ 'Warnings': 1
43
+ }
44
+ }
45
+ try:
46
+ check_severity('HIGH', data_with_low_risk_only)
47
+ except SeverityException:
48
+ pytest.fail("SeverityException was raised unexpectedly!")
49
+
50
+
51
+ def test_invalid_severity_flag(risk_data):
52
+ with pytest.raises(ValueError):
53
+ check_severity('invalid', risk_data)
54
+
55
+
56
+ def test_exception_on_all_flag(risk_data):
57
+ with pytest.raises(SeverityException):
58
+ check_severity('ALL', risk_data)
59
+
60
+
61
+ def test_no_exception_on_all_flag_when_empty():
62
+ data_with_no_risks = {
63
+ "securityTests": {
64
+ 'DidNotRun': 0,
65
+ 'Duration': 0,
66
+ 'Findings': 5,
67
+ 'Passed': 27,
68
+ 'RisksCounter': {'Critical': 0, 'High': 0, 'Low': 0, 'Medium': 0},
69
+ 'Warnings': 1
70
+ }
71
+ }
72
+ try:
73
+ check_severity('ALL', data_with_no_risks)
74
+ except SeverityException:
75
+ pytest.fail("SeverityException was raised unexpectedly!")
@@ -1,43 +0,0 @@
1
- pyntcli/__init__.py,sha256=jibR5k16CvIIIRqIusW7mrU6CMM0wet7LwtTuFLXbmU,23
2
- pyntcli/main.py,sha256=ucC9d4wlhUpvA92OXRIZ55iMdFXEDiAOVvP_wtRu7zs,6432
3
- pyntcli/analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- pyntcli/analytics/send.py,sha256=9TRAEoPbv4rWOZfcNaGanrRJAFvNs39P-uKSl49GcQE,3679
5
- pyntcli/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- pyntcli/auth/login.py,sha256=TljsRXbEkNI1YUrKm5mlTw4YiecYScYUsit8Z8vstss,5228
7
- pyntcli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- pyntcli/commands/burp.py,sha256=NfvMPlw8yISaDzvSuF96MXGVTPKRS7yR84TNDqFiblU,12274
9
- pyntcli/commands/command.py,sha256=mYeNer6WeswYLPSZhvNC04S5FOdMqH-Jk8a4iPRJtpc,10973
10
- pyntcli/commands/har.py,sha256=IZGd900hz0fFM3iZTG3NNXB2NiuAJGeuv-I49eXrf-o,4158
11
- pyntcli/commands/id_command.py,sha256=UBEgMIpm4vauTCsKyixltiGUolNg_OfHEJvJ_i5BpJY,943
12
- pyntcli/commands/listen.py,sha256=0wY9itDQyxSeHQA4GCGF67Xkb4ym48uP17WCPJMcLBE,8910
13
- pyntcli/commands/newman.py,sha256=ocg85L7HopXRYkhLc75QUtHH9ey48Z6Yxy1stgVW1aE,5283
14
- pyntcli/commands/postman.py,sha256=T1qdFHXX_kTW9p8PPbY9P7ynK4UcGoG3FfEMrRetwco,5163
15
- pyntcli/commands/pynt_cmd.py,sha256=X39hiDq6blGh2sGpwVeUXOgnDlhHruGkOoR5aUBzw_k,2982
16
- pyntcli/commands/root.py,sha256=-2aAiOKc_yXAlymxGove6su1mcvYsk4kCNaEawDKwy8,3677
17
- pyntcli/commands/static_file_extensions.py,sha256=PZJb02BI-64tbU-j3rdCNsXzTh7gkIDGxGKbKNw3h5k,1995
18
- pyntcli/commands/sub_command.py,sha256=GF3-rE_qk2L4jGPFqHLm9SdGINmu3EakhjJTFyWjRms,374
19
- pyntcli/commands/util.py,sha256=4p4rzNqL8nUJJfj1uy_JOHsFBEvVT6-k-V9GbAqHVyM,3218
20
- pyntcli/log/__init__.py,sha256=cOGwOYzMoshEbZiiasBGkj6wF0SBu3Jdpl-AuakDesw,19
21
- pyntcli/log/log.py,sha256=cWCdWmUaAwePwdhYDcgNMEG9d9RM34sGahxBCYEdv2Y,1069
22
- pyntcli/pynt_docker/__init__.py,sha256=PQIOVxc7XXtMLfEX7ojgwf_Z3mmTllO3ZvzUZTPOxQY,30
23
- pyntcli/pynt_docker/container_utils.py,sha256=_Onn7loInzyJAG2-Uk6CGpsuRyelmUFHOvtJj4Uzi9A,175
24
- pyntcli/pynt_docker/pynt_container.py,sha256=sqWKi3HUIn3AR9HgDYQtofCkaEQiOT3hG1nYQHf8RD8,17500
25
- pyntcli/store/__init__.py,sha256=xuS9OB21F6B1sUx5XPGxz_6WpG6-KTMbuq50RrZS5OY,29
26
- pyntcli/store/json_connector.py,sha256=UGs3uORw3iyn0YJ8kzab-veEZToA6d-ByXYuqEleWsA,560
27
- pyntcli/store/store.py,sha256=4YqmcHRltbbSWewKQoOZH8QdyMFs6i3L8o2bXY_YvSw,1909
28
- pyntcli/store/store_connector.py,sha256=w4LzcpRZesUZL1f63RmLlWEFRtJ6Y6rcS6PkkGtO4MA,357
29
- pyntcli/transport/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
- pyntcli/transport/pynt_requests.py,sha256=C7OPvcKkRTcxSYuyiWKE59KgA9sRX0d6fm1wnopAmPo,1719
31
- pyntcli/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
- pyntcli/ui/progress.py,sha256=RrnO_jJNunoyupylakmWmHOEPw3lh99OHpKBzL6OBiE,1008
33
- pyntcli/ui/pynt_errors.py,sha256=00UprD4tFViREv7kuXGQ99PAKGTpXYixxi3Ndeoeiew,689
34
- pyntcli/ui/report.py,sha256=W-icPSZrGLOubXgam0LpOvHLl_aZg9Zx9qIkL8Ym5PE,1930
35
- pyntcli/ui/ui_thread.py,sha256=XUBgLpYQjVhrilU-ofw7VSXgTiwneSdTxm61EvC3x4Q,5091
36
- tests/conftest.py,sha256=gToq5K74GtgeGQXjFvXSzMaE6axBYxAzcFG5XJPOXjI,427
37
- tests/auth/test_login.py,sha256=KFlzWhXBAuwdi7GXf16gCB3ya94LQG2wjcSChE149rQ,3798
38
- tests/store/test_cred_store.py,sha256=_7-917EtNC9eKEumO2_lt-7KuDmCwOZFaowCm7DbA_A,254
39
- pyntcli-0.1.92.dist-info/METADATA,sha256=MS97IJ6CIx7fidgjFBCYP8gKop_deMK3JGRBfXCsj5Y,463
40
- pyntcli-0.1.92.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
41
- pyntcli-0.1.92.dist-info/entry_points.txt,sha256=kcGmqAxXDttNk2EPRcqunc_LTVp61gzakz0v-GEE2SY,43
42
- pyntcli-0.1.92.dist-info/top_level.txt,sha256=u9MDStwVHB7UG8PUcODeWCul_NvzL2EzoLvSlgwLHFs,30
43
- pyntcli-0.1.92.dist-info/RECORD,,
File without changes
File without changes