pyntcli 0.1.72__py3-none-any.whl → 0.1.74__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 @@
1
- __version__ = "0.1.72"
1
+ __version__ = "0.1.74"
pyntcli/analytics/send.py CHANGED
@@ -4,26 +4,34 @@ import platform
4
4
 
5
5
  from pyntcli import __version__
6
6
  from pyntcli.transport import pynt_requests
7
+ import pyntcli.log.log as log
7
8
 
8
9
 
9
10
  PYNT_DEFAULT_USER_ID = "d9e3b82b-2900-43bf-8c8f-7ffe2f0cda36"
10
11
  MIXPANEL_TOKEN = "05c26edb86084bbbb803eed6818cd8aa"
11
12
  MIXPANEL_URL = "https://api-eu.mixpanel.com/track?ip=1"
12
13
 
14
+ logger = log.get_logger()
15
+
16
+
13
17
  def stop():
14
18
  if not AnalyticsSender._instance:
15
19
  return
16
20
  AnalyticsSender.instance().done()
17
21
 
22
+
18
23
  def emit(event, properties=None):
19
24
  AnalyticsSender.instance().emit(event, properties)
20
25
 
26
+
21
27
  def deferred_emit(event, properties=None):
22
28
  AnalyticsSender.instance().deferred_emit(event, properties)
23
-
29
+
30
+
24
31
  def set_user_id(user_id):
25
32
  AnalyticsSender.instance().set_user_id(user_id)
26
33
 
34
+
27
35
  CLI_START = "cli_start"
28
36
  LOGIN_START = "cli_login_start"
29
37
  LOGIN_DONE = "cli_login_done"
@@ -31,6 +39,7 @@ CICD = "CI/CD"
31
39
  ERROR = "error"
32
40
  DOCKER_PLATFORM = "platform"
33
41
 
42
+
34
43
  class AnalyticsSender():
35
44
  _instance = None
36
45
 
@@ -38,7 +47,7 @@ class AnalyticsSender():
38
47
  self.user_id = user_id
39
48
  self.version = __version__
40
49
  self.events = []
41
-
50
+
42
51
  @staticmethod
43
52
  def instance():
44
53
  if not AnalyticsSender._instance:
@@ -47,45 +56,51 @@ class AnalyticsSender():
47
56
  return AnalyticsSender._instance
48
57
 
49
58
  def base_event(self, event_type):
50
- return {
51
- "event": event_type,
52
- "properties": {
53
- "time": time.time(),
54
- "distinct_id": self.user_id,
55
- "$os": platform.platform(),
56
- "cli_version": self.version,
57
- "token": MIXPANEL_TOKEN
59
+ return {
60
+ "event": event_type,
61
+ "properties": {
62
+ "time": time.time(),
63
+ "distinct_id": self.user_id,
64
+ "$os": platform.platform(),
65
+ "cli_version": self.version,
66
+ "token": MIXPANEL_TOKEN
58
67
  }
59
68
  }
60
69
 
61
70
  def emit(self, event, properties):
62
71
  base_event = self.base_event(event)
63
-
72
+
64
73
  if properties:
65
- for k,v in properties.items():
74
+ for k, v in properties.items():
66
75
  base_event["properties"][k] = v
67
76
 
68
77
  if self.user_id != PYNT_DEFAULT_USER_ID:
69
- pynt_requests.post(MIXPANEL_URL, json=[base_event])
78
+ try:
79
+ pynt_requests.post(MIXPANEL_URL, json=[base_event])
80
+ except Exception:
81
+ logger.info(f"mixpanel unavailable, sending to logz: {base_event}")
70
82
  else:
71
83
  self.events.append(base_event)
72
84
 
73
85
  def deferred_emit(self, event, properties):
74
86
  base_event = self.base_event(event)
75
-
87
+
76
88
  if properties:
77
- for k,v in properties.items():
89
+ for k, v in properties.items():
78
90
  base_event["properties"][k] = v
79
91
 
80
92
  self.events.append(base_event)
81
93
 
82
94
  def set_user_id(self, user_id):
83
95
  self.user_id = user_id
84
- for i, _ in enumerate(self.events):
96
+ for i, _ in enumerate(self.events):
85
97
  self.events[i]["properties"]["distinct_id"] = user_id
86
98
  self.done()
87
99
 
88
100
  def done(self):
89
101
  if self.events:
90
- pynt_requests.post(MIXPANEL_URL, json=self.events)
102
+ try:
103
+ pynt_requests.post(MIXPANEL_URL, json=self.events)
104
+ except Exception:
105
+ logger.info(f"mixpanel unavailable, sending to logz: {self.events}")
91
106
  self.events = []
pyntcli/auth/login.py CHANGED
@@ -11,37 +11,42 @@ from pyntcli.ui import ui_thread
11
11
  from pyntcli.store import CredStore
12
12
  from pyntcli.transport import pynt_requests
13
13
 
14
+
14
15
  class LoginException(Exception):
15
16
  pass
16
17
 
18
+
17
19
  class Timeout(LoginException):
18
20
  pass
19
21
 
22
+
20
23
  class InvalidTokenInEnvVarsException(LoginException):
21
24
  pass
22
25
 
23
- PYNT_CREDENTIALS = "PYNT_CREDENTIALS"
26
+
27
+ PYNT_ID = "PYNT_ID"
24
28
  PYNT_SAAS = os.environ.get("PYNT_SAAS_URL") if os.environ.get("PYNT_SAAS_URL") else "https://api.pynt.io/v1"
25
29
  PYNT_BUCKET_NAME = os.environ.get("PYNT_BUCKET_NAME") if os.environ.get("PYNT_BUCKET_NAME") else ""
26
30
  PYNT_PARAM1 = os.environ.get("PYNT_PARAM1") if os.environ.get("PYNT_PARAM1") else ""
27
31
  PYNT_PARAM2 = os.environ.get("PYNT_PARAM2") if os.environ.get("PYNT_PARAM2") else ""
28
32
 
33
+
29
34
  class Login():
30
35
  def __init__(self) -> None:
31
36
  self.delay = 5
32
37
  self.base_authorization_url = "https://pynt.io/login?"
33
38
  self.poll_url = "https://n592meacjj.execute-api.us-east-1.amazonaws.com/default/cli_validate_login"
34
- self.login_wait_period = (60 *3) #3 minutes
39
+ self.login_wait_period = (60 * 3) # 3 minutes
35
40
 
36
- def create_login_request(self):
41
+ def create_login_request(self):
37
42
  request_id = uuid.uuid4()
38
43
  request_url = self.base_authorization_url + urllib.parse.urlencode({"request_id": request_id, "utm_source": "cli"})
39
44
  webbrowser.open(request_url)
40
45
 
41
- ui_thread.print(ui_thread.PrinterText("To continue, you need to log in to your account.")\
42
- .with_line("You will now be redirected to the login page.") \
43
- .with_line("") \
44
- .with_line("If you are not automatically redirected, please click on the link provided below (or copy to your web browser)") \
46
+ ui_thread.print(ui_thread.PrinterText("To continue, you need to log in to your account.")
47
+ .with_line("You will now be redirected to the login page.")
48
+ .with_line("")
49
+ .with_line("If you are not automatically redirected, please click on the link provided below (or copy to your web browser)")
45
50
  .with_line(request_url))
46
51
  return request_id
47
52
 
@@ -54,25 +59,26 @@ class Login():
54
59
  return response.json()
55
60
  time.sleep(self.delay)
56
61
  raise Timeout()
57
-
62
+
58
63
  def login(self):
59
64
  id = self.create_login_request()
60
65
  token = self.get_token_using_request_id(id)
61
- with CredStore() as store:
66
+ with CredStore() as store:
62
67
  store.put("token", token)
63
68
 
64
69
 
65
70
  def refresh_request(refresh_token):
66
71
  return pynt_requests.post(PYNT_SAAS + "/auth/refresh", json={"refresh_token": refresh_token})
67
72
 
73
+
68
74
  def refresh_token():
69
75
  token = None
70
- with CredStore() as store:
76
+ with CredStore() as store:
71
77
  token = store.get("token")
72
78
 
73
- if not token:
79
+ if not token:
74
80
  Login().login()
75
-
81
+
76
82
  access_token = token.get("access_token")
77
83
  if access_token and not is_jwt_expired(access_token):
78
84
  return
@@ -82,67 +88,71 @@ def refresh_token():
82
88
  Login().login()
83
89
  return
84
90
 
85
- refresh_response = refresh_request(refresh)
91
+ refresh_response = refresh_request(refresh)
86
92
  if refresh_response.status_code != 200:
87
93
  Login().login()
88
- return
94
+ return
89
95
 
90
96
  with CredStore() as store:
91
97
  token["access_token"] = refresh_response.json()["token"]
92
98
  store.put("token", token)
93
99
 
100
+
94
101
  def decode_jwt(jwt_token):
95
102
  splited = jwt_token.split(".")
96
- if len(splited) != 3:
103
+ if len(splited) != 3:
97
104
  return None
98
105
 
99
106
  return json.loads(b64decode(splited[1] + '=' * (-len(splited[1]) % 4)))
100
107
 
108
+
101
109
  def user_id():
102
110
  with CredStore() as store:
103
111
  token = store.get("token")
104
112
  if not token:
105
- return None
113
+ return None
106
114
 
107
115
  decoded = decode_jwt(token["access_token"])
108
116
  if not decoded:
109
117
  return None
110
-
118
+
111
119
  return decoded.get("sub", None)
112
120
 
113
121
  return None
114
122
 
123
+
115
124
  def is_jwt_expired(jwt_token):
116
125
  decoded = decode_jwt(jwt_token)
117
126
  if not decoded:
118
127
  return True
119
128
 
120
129
  exp = decoded.get("exp", None)
121
- if not exp:
122
- return True
123
-
124
- return datetime.datetime.fromtimestamp(exp) < datetime.datetime.now() + datetime.timedelta(minutes=1)
130
+ if not exp:
131
+ return True
132
+
133
+ return datetime.datetime.fromtimestamp(exp) < datetime.datetime.now() + datetime.timedelta(minutes=1)
134
+
125
135
 
126
136
  def validate_creds_structure(data):
127
- try:
137
+ try:
128
138
  creds = json.loads(data.replace("\n", ""))
129
139
  token = creds.get("token", None)
130
140
  if not token:
131
141
  raise InvalidTokenInEnvVarsException()
132
142
  if not isinstance(token, dict):
133
143
  raise InvalidTokenInEnvVarsException()
134
-
144
+
135
145
  refresh_token = token.get("refresh_token", None)
136
146
  if not refresh_token:
137
147
  raise InvalidTokenInEnvVarsException()
138
-
148
+
139
149
  return token
140
- except json.JSONDecodeError:
150
+ except json.JSONDecodeError:
141
151
  raise InvalidTokenInEnvVarsException()
142
-
143
152
 
144
- def should_login():
145
- env_creds = os.environ.get(PYNT_CREDENTIALS, None)
153
+
154
+ def should_login():
155
+ env_creds = os.environ.get(PYNT_ID, None)
146
156
  if env_creds:
147
157
  validated_creds = validate_creds_structure(env_creds)
148
158
  with CredStore() as store:
@@ -153,8 +163,8 @@ def should_login():
153
163
 
154
164
  if not token or token == store.connector.default_value:
155
165
  return True
156
-
166
+
157
167
  if not token.get("refresh_token"):
158
- return True
159
-
168
+ return True
169
+
160
170
  return False
pyntcli/commands/burp.py CHANGED
@@ -18,7 +18,17 @@ from pyntcli.commands import util, sub_command
18
18
  from pyntcli.ui import report as cli_reporter
19
19
  from pyntcli.transport import pynt_requests
20
20
 
21
- methods = ["get", "post", "put", "delete", "patch", "options", "head", "trace", "connect"]
21
+ methods = [
22
+ "get",
23
+ "post",
24
+ "put",
25
+ "delete",
26
+ "patch",
27
+ "options",
28
+ "head",
29
+ "trace",
30
+ "connect",
31
+ ]
22
32
 
23
33
 
24
34
  def is_valid_method(method: str):
@@ -29,7 +39,7 @@ def is_valid_method(method: str):
29
39
 
30
40
  def replay_req(item, proxy_port):
31
41
  url = item["url"]
32
- decoded_req = base64.b64decode(item["request"]["#text"]).decode('utf-8')
42
+ decoded_req = base64.b64decode(item["request"]["#text"]).decode("utf-8")
33
43
  method = decoded_req.split("\r\n")[0].split(" ")[0]
34
44
 
35
45
  if not is_valid_method(method):
@@ -52,7 +62,16 @@ def replay_req(item, proxy_port):
52
62
  headers[key] = value
53
63
 
54
64
  body = decoded_req.split("\r\n\r\n")[1]
55
- pynt_requests.request_from_xml(method=method, url=url, headers=headers, data=body, proxies={'http': '0.0.0.0:{}'.format(proxy_port), 'https': '0.0.0.0:{}'.format(proxy_port)})
65
+ pynt_requests.request_from_xml(
66
+ method=method,
67
+ url=url,
68
+ headers=headers,
69
+ data=body,
70
+ proxies={
71
+ "http": "0.0.0.0:{}".format(proxy_port),
72
+ "https": "0.0.0.0:{}".format(proxy_port),
73
+ },
74
+ )
56
75
 
57
76
 
58
77
  def run_burp_xml(doc, proxy_port):
@@ -76,20 +95,25 @@ def is_valid_xml(doc) -> bool:
76
95
 
77
96
 
78
97
  def burp_usage():
79
- return ui_thread.PrinterText("Burp integration to Pynt. Run a security scan with a given burp xml output file.") \
80
- .with_line("") \
81
- .with_line("Usage:", style=ui_thread.PrinterText.HEADER) \
82
- .with_line("\tpynt burp [OPTIONS]") \
83
- .with_line("") \
84
- .with_line("Options:", style=ui_thread.PrinterText.HEADER) \
85
- .with_line("\t--xml - Path to the xml to run tests on") \
86
- .with_line("\t--port - Set the port pynt will listen to (DEFAULT: 5001)") \
87
- .with_line("\t--ca-path - The path to the CA file in PEM format") \
88
- .with_line("\t--proxy-port - Set the port proxied traffic should be routed to (DEFAULT: 6666)") \
89
- .with_line("\t--report - If present will save the generated report in this path.") \
90
- .with_line("\t--insecure - use when target uses self signed certificates") \
91
- .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.") \
92
- .with_line("\t--return-error - 'all-findings' (warnings, or errors), 'errors-only', 'never' (default), ")
98
+ return (
99
+ ui_thread.PrinterText(
100
+ "Burp integration to Pynt. Run a security scan with a given burp xml output file."
101
+ )
102
+ .with_line("")
103
+ .with_line("Usage:", style=ui_thread.PrinterText.HEADER)
104
+ .with_line("\tpynt burp [OPTIONS]")
105
+ .with_line("")
106
+ .with_line("Options:", style=ui_thread.PrinterText.HEADER)
107
+ .with_line("\t--xml - Path to the xml to run tests on")
108
+ .with_line("\t--port - Set the port pynt will listen to (DEFAULT: 5001)")
109
+ .with_line("\t--ca-path - The path to the CA file in PEM format")
110
+ .with_line("\t--proxy-port - Set the port proxied traffic should be routed to (DEFAULT: 6666)")
111
+ .with_line("\t--report - If present will save the generated report in this path.")
112
+ .with_line("\t--insecure - Use when target uses self signed certificates")
113
+ .with_line("\t--application-id - Attach the scan to an application, you can find the ID in your applications area at app.pynt.io")
114
+ .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.")
115
+ .with_line("\t--return-error - 'all-findings' (warnings, or errors), 'errors-only', 'never' (default)")
116
+ )
93
117
 
94
118
 
95
119
  class BurpCommand(sub_command.PyntSubCommand):
@@ -110,25 +134,38 @@ class BurpCommand(sub_command.PyntSubCommand):
110
134
  burp_cmd.add_argument("--xml", help="", default="", required=True)
111
135
  burp_cmd.add_argument("--ca-path", type=str, default="")
112
136
  burp_cmd.add_argument("--report", type=str, default="")
113
- burp_cmd.add_argument('--return-error', choices=['all-findings', 'errors-only', 'never'], default='never')
137
+ burp_cmd.add_argument(
138
+ "--return-error",
139
+ choices=["all-findings", "errors-only", "never"],
140
+ default="never"
141
+ )
114
142
  burp_cmd.print_usage = self.print_usage
115
143
  burp_cmd.print_help = self.print_usage
116
144
  return burp_cmd
117
145
 
118
146
  def _updated_environment(self, args):
119
147
  env_copy = deepcopy(os.environ)
120
- return env_copy.update({"HTTP_PROXY": "http://localhost:{}".format(args.proxy_port),
121
- "HTTPS_PROXY": "http://localhost:{}".format(args.proxy_port)})
148
+ return env_copy.update(
149
+ {
150
+ "HTTP_PROXY": "http://localhost:{}".format(args.proxy_port),
151
+ "HTTPS_PROXY": "http://localhost:{}".format(args.proxy_port),
152
+ }
153
+ )
122
154
 
123
155
  def _start_proxy(self, args):
124
- res = pynt_requests.put(self.proxy_server_base_url.format(args.port) + "/proxy/start")
156
+ res = pynt_requests.put(
157
+ self.proxy_server_base_url.format(args.port) + "/proxy/start"
158
+ )
125
159
  res.raise_for_status()
126
160
  self.scan_id = res.json()["scanId"]
127
161
 
128
162
  def _stop_proxy(self, args):
129
163
  start = time.time()
130
164
  while start + self.proxy_healthcheck_buffer > time.time():
131
- res = pynt_requests.put(self.proxy_server_base_url.format(args.port) + "/proxy/stop", json={"scanId": self.scan_id})
165
+ res = pynt_requests.put(
166
+ self.proxy_server_base_url.format(args.port) + "/proxy/stop",
167
+ json={"scanId": self.scan_id},
168
+ )
132
169
  if res.status_code == HTTPStatus.OK:
133
170
  return
134
171
  time.sleep(self.proxy_sleep_interval)
@@ -136,47 +173,88 @@ class BurpCommand(sub_command.PyntSubCommand):
136
173
 
137
174
  def _get_report(self, args, report_format):
138
175
  while True:
139
- res = pynt_requests.get(self.proxy_server_base_url.format(args.port) + "/report?format={}".format(report_format), params={"scanId": self.scan_id})
176
+ res = pynt_requests.get(
177
+ self.proxy_server_base_url.format(args.port)
178
+ + "/report?format={}".format(report_format),
179
+ params={"scanId": self.scan_id},
180
+ )
140
181
  if res.status_code == HTTPStatus.OK:
141
182
  return res.text
142
183
  if res.status_code == HTTPStatus.ACCEPTED:
143
184
  time.sleep(self.proxy_sleep_interval)
144
185
  continue
145
186
  if res.status_code == 517: # pynt did not recieve any requests
146
- ui_thread.print(ui_thread.PrinterText(res.json()["message"], ui_thread.PrinterText.WARNING))
187
+ ui_thread.print(
188
+ ui_thread.PrinterText(
189
+ res.json()["message"], ui_thread.PrinterText.WARNING
190
+ )
191
+ )
147
192
  return
148
193
  ui_thread.print("Error in polling for scan report: {}".format(res.text))
149
194
  return
150
195
 
151
196
  def run_cmd(self, args: argparse.Namespace):
152
- container = pynt_container.get_container_with_arguments(args, pynt_container.PyntDockerPort(args.port, args.port, "--port"),
153
- pynt_container.PyntDockerPort(args.proxy_port, args.proxy_port, "--proxy-port"))
197
+ container = pynt_container.get_container_with_arguments(
198
+ args,
199
+ pynt_container.PyntDockerPort(args.port, args.port, "--port"),
200
+ pynt_container.PyntDockerPort(
201
+ args.proxy_port, args.proxy_port, "--proxy-port"
202
+ ),
203
+ )
154
204
  if "ca_path" in args and args.ca_path:
155
205
  if not os.path.isfile(args.ca_path):
156
- ui_thread.print(ui_thread.PrinterText("Could not find the provided ca path, please provide with a valid path", ui_thread.PrinterText.WARNING))
206
+ ui_thread.print(
207
+ ui_thread.PrinterText(
208
+ "Could not find the provided ca path, please provide with a valid path",
209
+ ui_thread.PrinterText.WARNING,
210
+ )
211
+ )
157
212
  return
158
213
 
159
214
  ca_name = os.path.basename(args.ca_path)
160
215
  container.docker_arguments += ["--ca-path", ca_name]
161
- container.mounts.append(pynt_container.create_mount(os.path.abspath(args.ca_path), "/etc/pynt/{}".format(ca_name)))
216
+ container.mounts.append(
217
+ pynt_container.create_mount(
218
+ os.path.abspath(args.ca_path), "/etc/pynt/{}".format(ca_name)
219
+ )
220
+ )
221
+
222
+ container.docker_arguments += ["--test-name", os.path.basename(args.xml)]
162
223
 
163
224
  if not os.path.isfile(args.xml):
164
- ui_thread.print(ui_thread.PrinterText("Could not find the provided xml path, please provide with a valid xml path", ui_thread.PrinterText.WARNING))
225
+ ui_thread.print(
226
+ ui_thread.PrinterText(
227
+ "Could not find the provided xml path, please provide with a valid xml path",
228
+ ui_thread.PrinterText.WARNING,
229
+ )
230
+ )
165
231
  return
166
232
 
167
233
  doc = parse_xml(args.xml)
168
234
  if not doc:
169
- ui_thread.print(ui_thread.PrinterText("Invalid file format. please provide a valid xml", ui_thread.PrinterText.WARNING))
235
+ ui_thread.print(
236
+ ui_thread.PrinterText(
237
+ "Invalid file format. please provide a valid xml",
238
+ ui_thread.PrinterText.WARNING,
239
+ )
240
+ )
170
241
  return
171
242
 
172
243
  if not is_valid_xml(doc):
173
- ui_thread.print(ui_thread.PrinterText("Invalid xml file. please provide a valid xml output generated from burp", ui_thread.PrinterText.WARNING))
244
+ ui_thread.print(
245
+ ui_thread.PrinterText(
246
+ "Invalid xml file. please provide a valid xml output generated from burp",
247
+ ui_thread.PrinterText.WARNING,
248
+ )
249
+ )
174
250
  return
175
251
 
176
- proxy_docker = pynt_container.PyntContainer(image_name=pynt_container.PYNT_DOCKER_IMAGE,
177
- tag="proxy-latest",
178
- detach=True,
179
- base_container=container)
252
+ proxy_docker = pynt_container.PyntContainer(
253
+ image_name=pynt_container.PYNT_DOCKER_IMAGE,
254
+ tag="proxy-latest",
255
+ detach=True,
256
+ base_container=container,
257
+ )
180
258
  proxy_docker.run()
181
259
  ui_thread.print_generator(proxy_docker.stdout)
182
260
 
@@ -187,12 +265,21 @@ class BurpCommand(sub_command.PyntSubCommand):
187
265
 
188
266
  self._stop_proxy(args)
189
267
 
190
- with ui_thread.progress("ws://localhost:{}/progress?scanId={}".format(args.port, self.scan_id), partial(lambda *args: None), "scan in progress...", 100):
268
+ with ui_thread.progress(
269
+ "ws://localhost:{}/progress?scanId={}".format(args.port, self.scan_id),
270
+ partial(lambda *args: None),
271
+ "scan in progress...",
272
+ 100,
273
+ ):
191
274
  html_report = self._get_report(args, "html")
192
- html_report_path = os.path.join(tempfile.gettempdir(), "pynt_report_{}.html".format(int(time.time())))
275
+ html_report_path = os.path.join(
276
+ tempfile.gettempdir(), "pynt_report_{}.html".format(int(time.time()))
277
+ )
193
278
 
194
279
  json_report = self._get_report(args, "json")
195
- json_report_path = os.path.join(tempfile.gettempdir(), "pynt_report_{}.json".format(int(time.time())))
280
+ json_report_path = os.path.join(
281
+ tempfile.gettempdir(), "pynt_report_{}.json".format(int(time.time()))
282
+ )
196
283
 
197
284
  if "report" in args and args.report:
198
285
  full_path = os.path.abspath(args.report)