pyntcli 0.1.107__py3-none-any.whl → 0.1.109__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 +1 -1
- pyntcli/commands/burp.py +62 -17
- pyntcli/commands/command.py +4 -7
- pyntcli/commands/har.py +1 -3
- pyntcli/commands/listen.py +5 -7
- pyntcli/commands/newman.py +3 -6
- pyntcli/commands/postman.py +2 -3
- pyntcli/commands/pynt_cmd.py +5 -6
- pyntcli/commands/template.py +182 -0
- pyntcli/pynt_docker/pynt_container.py +29 -53
- {pyntcli-0.1.107.dist-info → pyntcli-0.1.109.dist-info}/METADATA +1 -1
- {pyntcli-0.1.107.dist-info → pyntcli-0.1.109.dist-info}/RECORD +15 -14
- {pyntcli-0.1.107.dist-info → pyntcli-0.1.109.dist-info}/WHEEL +1 -1
- {pyntcli-0.1.107.dist-info → pyntcli-0.1.109.dist-info}/entry_points.txt +0 -0
- {pyntcli-0.1.107.dist-info → pyntcli-0.1.109.dist-info}/top_level.txt +0 -0
pyntcli/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.1.
|
|
1
|
+
__version__ = "0.1.109"
|
pyntcli/commands/burp.py
CHANGED
|
@@ -10,6 +10,9 @@ from subprocess import Popen, PIPE
|
|
|
10
10
|
from functools import partial
|
|
11
11
|
import xmltodict
|
|
12
12
|
import base64
|
|
13
|
+
|
|
14
|
+
from pyntcli.store import store
|
|
15
|
+
|
|
13
16
|
import pyntcli.log.log as log
|
|
14
17
|
from xml.parsers.expat import ExpatError
|
|
15
18
|
|
|
@@ -19,9 +22,6 @@ from pyntcli.commands import util, sub_command
|
|
|
19
22
|
from pyntcli.ui import report as cli_reporter
|
|
20
23
|
from pyntcli.transport import pynt_requests
|
|
21
24
|
|
|
22
|
-
PYNT_CONTAINER_INTERNAL_PORT = "5001"
|
|
23
|
-
PYNT_CONTAINER_INTERNAL_PROXY_PORT = "6666"
|
|
24
|
-
|
|
25
25
|
methods = [
|
|
26
26
|
"get",
|
|
27
27
|
"post",
|
|
@@ -140,9 +140,8 @@ def burp_usage():
|
|
|
140
140
|
.with_line("Options:", style=ui_thread.PrinterText.HEADER)
|
|
141
141
|
.with_line("\t--xml - Path to the xml to run tests on")
|
|
142
142
|
.with_line('\t--captured-domains - Pynt will scan only these domains and subdomains. For all domains write "*"')
|
|
143
|
-
.with_line("\t--port - Set the port pynt will listen to (DEFAULT:
|
|
143
|
+
.with_line("\t--port - Set the port pynt will listen to (DEFAULT: random)")
|
|
144
144
|
.with_line("\t--ca-path - The path to the CA file in PEM format")
|
|
145
|
-
.with_line("\t--proxy-port - Set the port proxied traffic should be routed to (DEFAULT: 6666)")
|
|
146
145
|
.with_line("\t--report - If present will save the generated report in this path.")
|
|
147
146
|
.with_line("\t--insecure - Use when target uses self signed certificates")
|
|
148
147
|
.with_line("\t--application-id - Attach the scan to an application, you can find the ID in your applications area at app.pynt.io")
|
|
@@ -165,23 +164,23 @@ class BurpCommand(sub_command.PyntSubCommand):
|
|
|
165
164
|
|
|
166
165
|
def add_cmd(self, parent: argparse._SubParsersAction) -> argparse.ArgumentParser:
|
|
167
166
|
burp_cmd = parent.add_parser(self.name)
|
|
168
|
-
burp_cmd.add_argument("--port", "-p", help="", type=int, default=
|
|
169
|
-
burp_cmd.add_argument("--proxy-port", help="", type=int, default=6666)
|
|
167
|
+
burp_cmd.add_argument("--port", "-p", help="", type=int, default=util.find_open_port())
|
|
170
168
|
burp_cmd.add_argument("--xml", help="", default="", required=True)
|
|
171
169
|
burp_cmd.add_argument("--ca-path", type=str, default="")
|
|
172
170
|
burp_cmd.add_argument("--report", type=str, default="")
|
|
171
|
+
burp_cmd.add_argument("--save-collection", action="store_true", help="Get postman collection")
|
|
173
172
|
burp_cmd.add_argument("--captured-domains", nargs="+", help="", default="")
|
|
174
173
|
burp_cmd.add_argument("--severity-level", choices=["all", "medium", "high", "critical", "none"], default="none")
|
|
175
174
|
burp_cmd.print_usage = self.print_usage
|
|
176
175
|
burp_cmd.print_help = self.print_usage
|
|
177
176
|
return burp_cmd
|
|
178
177
|
|
|
179
|
-
def _updated_environment(self,
|
|
178
|
+
def _updated_environment(self, proxy_port: int):
|
|
180
179
|
env_copy = deepcopy(os.environ)
|
|
181
180
|
return env_copy.update(
|
|
182
181
|
{
|
|
183
|
-
"HTTP_PROXY": "http://localhost:{}".format(
|
|
184
|
-
"HTTPS_PROXY": "http://localhost:{}".format(
|
|
182
|
+
"HTTP_PROXY": "http://localhost:{}".format(proxy_port),
|
|
183
|
+
"HTTPS_PROXY": "http://localhost:{}".format(proxy_port),
|
|
185
184
|
}
|
|
186
185
|
)
|
|
187
186
|
|
|
@@ -226,12 +225,43 @@ class BurpCommand(sub_command.PyntSubCommand):
|
|
|
226
225
|
ui_thread.print("Error in polling for scan report: {}".format(res.text))
|
|
227
226
|
return
|
|
228
227
|
|
|
228
|
+
def _get_postman_collection(self, args):
|
|
229
|
+
while True:
|
|
230
|
+
res = pynt_requests.get(
|
|
231
|
+
self.proxy_server_base_url.format(args.port)
|
|
232
|
+
+ "/collection",
|
|
233
|
+
params={"scanId": self.scan_id},
|
|
234
|
+
)
|
|
235
|
+
if res.status_code == HTTPStatus.OK:
|
|
236
|
+
return res.text
|
|
237
|
+
if res.status_code == HTTPStatus.ACCEPTED:
|
|
238
|
+
time.sleep(self.proxy_sleep_interval)
|
|
239
|
+
continue
|
|
240
|
+
if res.status_code == 517: # pynt did not recieve any requests
|
|
241
|
+
ui_thread.print(
|
|
242
|
+
ui_thread.PrinterText(
|
|
243
|
+
res.json()["message"], ui_thread.PrinterText.WARNING
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
return
|
|
247
|
+
if res.status_code == 404:
|
|
248
|
+
ui_thread.print(
|
|
249
|
+
ui_thread.PrinterText(
|
|
250
|
+
"No collection found", ui_thread.PrinterText.WARNING
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
return
|
|
254
|
+
ui_thread.print("Error in polling for scan report: {}".format(res.text))
|
|
255
|
+
return
|
|
256
|
+
|
|
229
257
|
def run_cmd(self, args: argparse.Namespace):
|
|
258
|
+
proxy_port = util.find_open_port()
|
|
259
|
+
|
|
230
260
|
container_config = pynt_container.DockerContainerConfig(
|
|
231
261
|
args,
|
|
232
262
|
"proxy",
|
|
233
|
-
pynt_container.
|
|
234
|
-
pynt_container.
|
|
263
|
+
pynt_container.api_port(args.port),
|
|
264
|
+
pynt_container.proxy_port(proxy_port),
|
|
235
265
|
)
|
|
236
266
|
|
|
237
267
|
for host in args.captured_domains:
|
|
@@ -303,7 +333,7 @@ class BurpCommand(sub_command.PyntSubCommand):
|
|
|
303
333
|
|
|
304
334
|
self._start_proxy(args)
|
|
305
335
|
|
|
306
|
-
run_burp_xml(doc,
|
|
336
|
+
run_burp_xml(doc, proxy_port)
|
|
307
337
|
|
|
308
338
|
self._stop_proxy(args)
|
|
309
339
|
|
|
@@ -315,10 +345,10 @@ class BurpCommand(sub_command.PyntSubCommand):
|
|
|
315
345
|
)
|
|
316
346
|
|
|
317
347
|
with ui_thread.progress(
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
348
|
+
"ws://localhost:{}/progress?scanId={}".format(args.port, self.scan_id),
|
|
349
|
+
partial(lambda *args: None),
|
|
350
|
+
"scan in progress...",
|
|
351
|
+
100,
|
|
322
352
|
):
|
|
323
353
|
html_report = self._get_report(args, "html")
|
|
324
354
|
html_report_path = os.path.join(
|
|
@@ -330,6 +360,11 @@ class BurpCommand(sub_command.PyntSubCommand):
|
|
|
330
360
|
tempfile.gettempdir(), "pynt_report_{}.json".format(int(time.time()))
|
|
331
361
|
)
|
|
332
362
|
|
|
363
|
+
collection = self._get_postman_collection(args) if args.save_collection else None
|
|
364
|
+
collection_path = os.path.join(
|
|
365
|
+
store.get_default_store_dir(), "postman_collection_{}.json".format(int(time.time()))
|
|
366
|
+
)
|
|
367
|
+
|
|
333
368
|
if "report" in args and args.report:
|
|
334
369
|
full_path = os.path.abspath(args.report)
|
|
335
370
|
html_report_path = util.get_user_report_path(full_path, "html")
|
|
@@ -346,6 +381,16 @@ class BurpCommand(sub_command.PyntSubCommand):
|
|
|
346
381
|
reporter = cli_reporter.PyntReporter(json_report_path)
|
|
347
382
|
reporter.print_summary()
|
|
348
383
|
|
|
384
|
+
if collection:
|
|
385
|
+
with open(collection_path, "w", encoding="utf-8") as collection_file:
|
|
386
|
+
collection_file.write(collection)
|
|
387
|
+
ui_thread.print(
|
|
388
|
+
ui_thread.PrinterText(
|
|
389
|
+
"Postman collection saved at: {}".format(collection_path),
|
|
390
|
+
ui_thread.PrinterText.INFO,
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
|
|
349
394
|
if json_report:
|
|
350
395
|
json_obj = json.loads(json_report)
|
|
351
396
|
if json_obj:
|
pyntcli/commands/command.py
CHANGED
|
@@ -15,9 +15,6 @@ from pyntcli.commands import util, sub_command
|
|
|
15
15
|
from pyntcli.ui import report as cli_reporter
|
|
16
16
|
from pyntcli.transport import pynt_requests
|
|
17
17
|
|
|
18
|
-
PYNT_CONTAINER_INTERNAL_PORT = "5001"
|
|
19
|
-
PYNT_CONTAINER_INTERNAL_PROXY_PORT = "6666"
|
|
20
|
-
|
|
21
18
|
|
|
22
19
|
def command_usage():
|
|
23
20
|
return (
|
|
@@ -32,7 +29,7 @@ def command_usage():
|
|
|
32
29
|
.with_line("\t--cmd - The command that runs the functional tests")
|
|
33
30
|
.with_line('\t--captured-domains - Pynt will scan only these domains and subdomains. For all domains write "*"')
|
|
34
31
|
.with_line('\t--test-name - A name for your Pynt scan')
|
|
35
|
-
.with_line("\t--port - Set the port pynt will listen to (DEFAULT:
|
|
32
|
+
.with_line("\t--port - Set the port pynt will listen to (DEFAULT: random)")
|
|
36
33
|
.with_line("\t--allow-errors - If present will allow command to fail and continue execution")
|
|
37
34
|
.with_line("\t--ca-path - The path to the CA file in PEM format")
|
|
38
35
|
.with_line("\t--proxy-port - Set the port proxied traffic should be routed to (DEFAULT: 6666)")
|
|
@@ -60,7 +57,7 @@ class CommandSubCommand(sub_command.PyntSubCommand):
|
|
|
60
57
|
|
|
61
58
|
def add_cmd(self, parent: argparse._SubParsersAction) -> argparse.ArgumentParser:
|
|
62
59
|
proxy_cmd = parent.add_parser(self.name)
|
|
63
|
-
proxy_cmd.add_argument("--port", "-p", help="", type=int, default=
|
|
60
|
+
proxy_cmd.add_argument("--port", "-p", help="", type=int, default=util.find_open_port())
|
|
64
61
|
proxy_cmd.add_argument("--proxy-port", help="", type=int, default=6666)
|
|
65
62
|
proxy_cmd.add_argument("--cmd", help="", default="", required=True)
|
|
66
63
|
proxy_cmd.add_argument("--captured-domains", nargs="+", help="", default="", required=False)
|
|
@@ -148,8 +145,8 @@ class CommandSubCommand(sub_command.PyntSubCommand):
|
|
|
148
145
|
container_config = pynt_container.DockerContainerConfig(
|
|
149
146
|
args,
|
|
150
147
|
"proxy",
|
|
151
|
-
pynt_container.
|
|
152
|
-
pynt_container.
|
|
148
|
+
pynt_container.api_port(args.port),
|
|
149
|
+
pynt_container.proxy_port(args.proxy_port),
|
|
153
150
|
)
|
|
154
151
|
|
|
155
152
|
if args.captured_domains:
|
pyntcli/commands/har.py
CHANGED
|
@@ -8,8 +8,6 @@ from pyntcli.ui import ui_thread
|
|
|
8
8
|
from pyntcli.ui.progress import PyntProgress
|
|
9
9
|
from pyntcli.commands import sub_command, util
|
|
10
10
|
|
|
11
|
-
PYNT_CONTAINER_INTERNAL_PORT = "5001"
|
|
12
|
-
|
|
13
11
|
|
|
14
12
|
def har_usage():
|
|
15
13
|
return (
|
|
@@ -61,7 +59,7 @@ class HarSubCommand(sub_command.PyntSubCommand):
|
|
|
61
59
|
container_config = pynt_container.DockerContainerConfig(
|
|
62
60
|
args,
|
|
63
61
|
"har",
|
|
64
|
-
pynt_container.
|
|
62
|
+
pynt_container.api_port(port),
|
|
65
63
|
)
|
|
66
64
|
|
|
67
65
|
if not os.path.isfile(args.har):
|
pyntcli/commands/listen.py
CHANGED
|
@@ -15,9 +15,6 @@ from pyntcli.commands import util, sub_command
|
|
|
15
15
|
from pyntcli.ui import report as cli_reporter
|
|
16
16
|
from pyntcli.transport import pynt_requests
|
|
17
17
|
|
|
18
|
-
PYNT_CONTAINER_INTERNAL_PORT = "5001"
|
|
19
|
-
PYNT_CONTAINER_INTERNAL_PROXY_PORT = "6666"
|
|
20
|
-
|
|
21
18
|
|
|
22
19
|
def listen_usage():
|
|
23
20
|
return (
|
|
@@ -29,7 +26,7 @@ def listen_usage():
|
|
|
29
26
|
.with_line("Options:", style=ui_thread.PrinterText.HEADER)
|
|
30
27
|
.with_line('\t--captured-domains - Pynt will scan only these domains and subdomains. For all domains write "*"')
|
|
31
28
|
.with_line('\t--test-name - A name for your Pynt scan')
|
|
32
|
-
.with_line("\t--port - Set the port pynt will listen to (DEFAULT:
|
|
29
|
+
.with_line("\t--port - Set the port pynt will listen to (DEFAULT: random)")
|
|
33
30
|
.with_line("\t--ca-path - The path to the CA file in PEM format")
|
|
34
31
|
.with_line("\t--proxy-port - Set the port proxied traffic should be routed to (DEFAULT: 6666)")
|
|
35
32
|
.with_line("\t--report - If present will save the generated report in this path.")
|
|
@@ -54,7 +51,7 @@ class ListenSubCommand(sub_command.PyntSubCommand):
|
|
|
54
51
|
|
|
55
52
|
def add_cmd(self, parent: argparse._SubParsersAction) -> argparse.ArgumentParser:
|
|
56
53
|
listen_cmd = parent.add_parser(self.name)
|
|
57
|
-
listen_cmd.add_argument("--port", "-p", help="", type=int, default=
|
|
54
|
+
listen_cmd.add_argument("--port", "-p", help="", type=int, default=util.find_open_port())
|
|
58
55
|
listen_cmd.add_argument("--proxy-port", help="", type=int, default=6666)
|
|
59
56
|
listen_cmd.add_argument("--captured-domains", nargs="+", help="", default="", required=True)
|
|
60
57
|
listen_cmd.add_argument("--test-name", help="", default="", required=False)
|
|
@@ -109,8 +106,9 @@ class ListenSubCommand(sub_command.PyntSubCommand):
|
|
|
109
106
|
container_config = pynt_container.DockerContainerConfig(
|
|
110
107
|
args,
|
|
111
108
|
"proxy",
|
|
112
|
-
pynt_container.
|
|
113
|
-
pynt_container.
|
|
109
|
+
pynt_container.api_port(args.port),
|
|
110
|
+
pynt_container.proxy_port(args.proxy_port),
|
|
111
|
+
)
|
|
114
112
|
|
|
115
113
|
for host in args.captured_domains:
|
|
116
114
|
container_config.docker_arguments += ["--host-targets", host]
|
pyntcli/commands/newman.py
CHANGED
|
@@ -8,8 +8,6 @@ from pyntcli.commands import sub_command, util
|
|
|
8
8
|
from pyntcli.ui import ui_thread
|
|
9
9
|
from pyntcli.ui.progress import PyntProgress
|
|
10
10
|
|
|
11
|
-
PYNT_CONTAINER_INTERNAL_PORT = "5001"
|
|
12
|
-
|
|
13
11
|
|
|
14
12
|
def newman_usage():
|
|
15
13
|
return (
|
|
@@ -55,12 +53,11 @@ class NewmanSubCommand(sub_command.PyntSubCommand):
|
|
|
55
53
|
return newman_cmd
|
|
56
54
|
|
|
57
55
|
def run_cmd(self, args: argparse.Namespace):
|
|
58
|
-
|
|
59
56
|
port = util.find_open_port()
|
|
60
57
|
container_config = pynt_container.DockerContainerConfig(
|
|
61
58
|
args,
|
|
62
59
|
"newman",
|
|
63
|
-
pynt_container.
|
|
60
|
+
pynt_container.api_port(port),
|
|
64
61
|
)
|
|
65
62
|
|
|
66
63
|
if not os.path.isfile(args.collection):
|
|
@@ -117,8 +114,8 @@ class NewmanSubCommand(sub_command.PyntSubCommand):
|
|
|
117
114
|
ui_thread.print(ui_thread.PrinterText(
|
|
118
115
|
"Pynt docker is ready",
|
|
119
116
|
ui_thread.PrinterText.INFO,
|
|
120
|
-
))
|
|
121
|
-
|
|
117
|
+
))
|
|
118
|
+
|
|
122
119
|
ui_thread.print_generator(newman_docker.stdout)
|
|
123
120
|
|
|
124
121
|
with ui_thread.progress(
|
pyntcli/commands/postman.py
CHANGED
|
@@ -14,8 +14,6 @@ from pyntcli.pynt_docker import pynt_container
|
|
|
14
14
|
from pyntcli.ui import ui_thread
|
|
15
15
|
from pyntcli.transport import pynt_requests
|
|
16
16
|
|
|
17
|
-
PYNT_CONTAINER_INTERNAL_PORT = "5001"
|
|
18
|
-
|
|
19
17
|
|
|
20
18
|
class PyntPostmanException(Exception):
|
|
21
19
|
pass
|
|
@@ -98,7 +96,8 @@ class PostmanSubCommand(sub_command.PyntSubCommand):
|
|
|
98
96
|
container_config = pynt_container.DockerContainerConfig(
|
|
99
97
|
args,
|
|
100
98
|
"postman",
|
|
101
|
-
pynt_container.
|
|
99
|
+
pynt_container.api_port(args.port),
|
|
100
|
+
)
|
|
102
101
|
|
|
103
102
|
postman_docker = pynt_container.PyntContainerNative(container_config)
|
|
104
103
|
postman_docker.prepare_client()
|
pyntcli/commands/pynt_cmd.py
CHANGED
|
@@ -12,8 +12,7 @@ import pyntcli.log.log as log
|
|
|
12
12
|
|
|
13
13
|
from requests.exceptions import SSLError, HTTPError
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
from . import command, listen, postman, root, sub_command, id_command, newman, har, burp
|
|
15
|
+
from . import command, listen, postman, root, sub_command, id_command, newman, har, burp, template
|
|
17
16
|
|
|
18
17
|
logger = log.get_logger()
|
|
19
18
|
|
|
@@ -25,6 +24,7 @@ avail_sub_commands = [
|
|
|
25
24
|
command.CommandSubCommand("command"),
|
|
26
25
|
listen.ListenSubCommand("listen"),
|
|
27
26
|
burp.BurpCommand("burp"),
|
|
27
|
+
template.TemplateSubCommand("template")
|
|
28
28
|
]
|
|
29
29
|
|
|
30
30
|
commands_without_app_id = ["postman", "pynt-id"]
|
|
@@ -50,7 +50,6 @@ VERSION_CHECK_URL = "https://d1efigcr4c19qn.cloudfront.net/cli/version"
|
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
def check_is_latest_version(current_version):
|
|
53
|
-
|
|
54
53
|
try:
|
|
55
54
|
res = pynt_requests.get(VERSION_CHECK_URL)
|
|
56
55
|
res.raise_for_status()
|
|
@@ -175,9 +174,9 @@ class PyntCommand:
|
|
|
175
174
|
return True
|
|
176
175
|
|
|
177
176
|
if prompt.confirmation_prompt_with_timeout("Application ID is missing. Use the '--application-id' flag to provide it.\n" +
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
"Without an Application ID, the scan will not be associated with your application.\n" +
|
|
178
|
+
"The Application ID can be fetched from https://app.pynt.io/dashboard/applications.\n" +
|
|
179
|
+
"Do you want to continue without associating the scan?", default="yes", timeout=15):
|
|
181
180
|
prompts_history["missing_app_id"] = {"last_confirmation": current_time.strftime("%Y-%m-%d %H:%M:%S")}
|
|
182
181
|
state_store.put_prompts_history(
|
|
183
182
|
prompts_history)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
import tempfile
|
|
5
|
+
import time
|
|
6
|
+
from functools import partial
|
|
7
|
+
|
|
8
|
+
from pyntcli.pynt_docker import pynt_container
|
|
9
|
+
from pyntcli.ui import ui_thread
|
|
10
|
+
from pyntcli.commands import sub_command, util
|
|
11
|
+
|
|
12
|
+
PYNT_CONTAINER_INTERNAL_PORT = "5001"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def template_usage():
|
|
16
|
+
return (
|
|
17
|
+
ui_thread.PrinterText("Integration with template testing")
|
|
18
|
+
.with_line("")
|
|
19
|
+
.with_line("Usage:", style=ui_thread.PrinterText.HEADER)
|
|
20
|
+
.with_line("\tpynt template [OPTIONS]")
|
|
21
|
+
.with_line("")
|
|
22
|
+
.with_line("Options:", style=ui_thread.PrinterText.HEADER)
|
|
23
|
+
.with_line("\t--template-file / -tf - Path to the template file")
|
|
24
|
+
.with_line("\t--template-paths-file / -tpf - Path to the file containing the template paths (with new line separator) for the attacks")
|
|
25
|
+
.with_line("\t--url - Base URL for the attacks")
|
|
26
|
+
.with_line("\t--urls - Path to the file containing the base URLs (with new line separator) for the attacks")
|
|
27
|
+
.with_line("\t--reporters - Output results to json")
|
|
28
|
+
.with_line("\t--severity-level - 'all', 'medium', 'high', 'critical', 'none' (default)")
|
|
29
|
+
.with_line("\t--verbose - Use to get more detailed information about the run")
|
|
30
|
+
.with_line("")
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TemplateSubCommand(sub_command.PyntSubCommand):
|
|
35
|
+
def __init__(self, name) -> None:
|
|
36
|
+
super().__init__(name)
|
|
37
|
+
|
|
38
|
+
def usage(self, *args):
|
|
39
|
+
ui_thread.print(template_usage())
|
|
40
|
+
|
|
41
|
+
def add_cmd(self, parent: argparse._SubParsersAction) -> argparse.ArgumentParser:
|
|
42
|
+
template_cmd = parent.add_parser(self.name)
|
|
43
|
+
template_cmd.add_argument("--template-file", "-tf", type=str, help="Path to the template file")
|
|
44
|
+
template_cmd.add_argument("--template-paths-file", "-tpf", type=str, help="Path to the file containing the template paths (with new line separator) for the attacks")
|
|
45
|
+
template_cmd.add_argument("--url", "-u", type=str, help="Base URL for the attacks")
|
|
46
|
+
template_cmd.add_argument("--urls", type=str, help="Path to the file containing the base URLs (with new line separator) for the attacks")
|
|
47
|
+
template_cmd.add_argument("--reporters", action="store_true")
|
|
48
|
+
template_cmd.add_argument("--severity-level", choices=["all", "medium", "high", "critical", "none"], default="none")
|
|
49
|
+
template_cmd.print_usage = self.usage
|
|
50
|
+
template_cmd.print_help = self.usage
|
|
51
|
+
return template_cmd
|
|
52
|
+
|
|
53
|
+
def run_cmd(self, args: argparse.Namespace):
|
|
54
|
+
pynt_folder_path = "/etc/pynt"
|
|
55
|
+
templates_folder_path = f"{pynt_folder_path}/templates"
|
|
56
|
+
ui_thread.print_verbose("Building container")
|
|
57
|
+
port = util.find_open_port()
|
|
58
|
+
container_config = pynt_container.DockerContainerConfig(
|
|
59
|
+
args,
|
|
60
|
+
"template",
|
|
61
|
+
pynt_container._PyntDockerPort(port=port, name="--port")
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
validate_args(args)
|
|
66
|
+
except ValueError as e:
|
|
67
|
+
ui_thread.print(
|
|
68
|
+
ui_thread.PrinterText(
|
|
69
|
+
str(e),
|
|
70
|
+
ui_thread.PrinterText.WARNING,
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
if args.template_file:
|
|
76
|
+
if not os.path.isfile(args.template_file):
|
|
77
|
+
ui_thread.print(
|
|
78
|
+
ui_thread.PrinterText(
|
|
79
|
+
"Could not find the provided path, please provide a valid path",
|
|
80
|
+
ui_thread.PrinterText.WARNING,
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
return
|
|
84
|
+
template_name = os.path.basename(args.template_file)
|
|
85
|
+
full_template_path = f"{templates_folder_path}/{template_name}"
|
|
86
|
+
container_config.mounts.append(
|
|
87
|
+
pynt_container.create_mount(
|
|
88
|
+
os.path.abspath(args.template_file), full_template_path
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
container_config.docker_arguments += ["--template-file", full_template_path]
|
|
92
|
+
|
|
93
|
+
if args.template_paths_file:
|
|
94
|
+
if not os.path.isfile(args.template_paths_file):
|
|
95
|
+
ui_thread.print(
|
|
96
|
+
ui_thread.PrinterText(
|
|
97
|
+
"Could not find the provided paths file, please provide a valid paths file",
|
|
98
|
+
ui_thread.PrinterText.WARNING,
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
# read the file, mount each uncommented file to /etc/pynt/templates, create a temp file with the paths as they are in the container (in /etc/pynt) and mount it to /etc/pynt/paths.txt
|
|
104
|
+
with open(args.template_paths_file, "r") as f:
|
|
105
|
+
paths = [line.strip() for line in f if not (line.strip().startswith("#") or line.startswith("//"))]
|
|
106
|
+
for path in paths:
|
|
107
|
+
container_config.mounts.append(
|
|
108
|
+
pynt_container.create_mount(
|
|
109
|
+
os.path.abspath(path), f"{templates_folder_path}/{os.path.basename(path)}"
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
container_config.docker_arguments += ["--templates-folder", templates_folder_path]
|
|
114
|
+
|
|
115
|
+
if args.url:
|
|
116
|
+
container_config.docker_arguments += ["--url", args.url]
|
|
117
|
+
|
|
118
|
+
if args.urls and not os.path.isfile(args.urls):
|
|
119
|
+
ui_thread.print(
|
|
120
|
+
ui_thread.PrinterText(
|
|
121
|
+
"Could not find the provided urls file, please provide a valid urls file",
|
|
122
|
+
ui_thread.PrinterText.WARNING,
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
if args.urls:
|
|
128
|
+
base_urls_path = f"{pynt_folder_path}/base_urls.txt"
|
|
129
|
+
container_config.docker_arguments += ["--urls", base_urls_path]
|
|
130
|
+
container_config.mounts.append(
|
|
131
|
+
pynt_container.create_mount(
|
|
132
|
+
os.path.abspath(args.urls), base_urls_path
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
with util.create_default_file_mounts(args) as m:
|
|
137
|
+
container_config.mounts += m
|
|
138
|
+
|
|
139
|
+
template_docker = pynt_container.PyntContainerNative(container_config)
|
|
140
|
+
|
|
141
|
+
template_docker.prepare_client()
|
|
142
|
+
template_docker.pre_run_validation(port)
|
|
143
|
+
template_docker.run()
|
|
144
|
+
|
|
145
|
+
healthcheck = partial(
|
|
146
|
+
util.wait_for_healthcheck, "http://localhost:{}".format(port)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
healthcheck()
|
|
150
|
+
ui_thread.print_verbose(util.GOT_INITIAL_HEALTHCHECK_MESSAGE)
|
|
151
|
+
ui_thread.print(ui_thread.PrinterText(
|
|
152
|
+
"Pynt docker is ready",
|
|
153
|
+
ui_thread.PrinterText.INFO,
|
|
154
|
+
))
|
|
155
|
+
|
|
156
|
+
ui_thread.print_generator(template_docker.stdout)
|
|
157
|
+
|
|
158
|
+
with ui_thread.progress(
|
|
159
|
+
"ws://localhost:{}/progress".format(port),
|
|
160
|
+
healthcheck,
|
|
161
|
+
"scan in progress...",
|
|
162
|
+
100,
|
|
163
|
+
):
|
|
164
|
+
while template_docker.is_alive():
|
|
165
|
+
time.sleep(1)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def validate_args(args: argparse.Namespace):
|
|
169
|
+
if not args.template_file and not args.template_paths_file:
|
|
170
|
+
raise ValueError("Please provide either a template file or a template paths file")
|
|
171
|
+
if args.template_file and args.template_paths_file:
|
|
172
|
+
raise ValueError("Please provide either a template file or a template paths file, not both")
|
|
173
|
+
if not args.url and not args.urls:
|
|
174
|
+
raise ValueError("Please provide either a url or a urls file")
|
|
175
|
+
if args.url and args.urls:
|
|
176
|
+
raise ValueError("Please provide either a url or a urls file, not both")
|
|
177
|
+
if args.urls and not os.path.isfile(args.urls):
|
|
178
|
+
raise ValueError("Could not find the provided urls file, please provide a valid urls file")
|
|
179
|
+
if args.template_file and not os.path.isfile(args.template_file):
|
|
180
|
+
raise ValueError("Could not find the provided path, please provide a valid path")
|
|
181
|
+
if args.template_paths_file and not os.path.isfile(args.template_paths_file):
|
|
182
|
+
raise ValueError("Could not find the provided paths file, please provide a valid paths file")
|
|
@@ -17,8 +17,6 @@ from pyntcli.auth.login import PYNT_ID, PYNT_SAAS, PYNT_BUCKET_NAME, PYNT_PARAM1
|
|
|
17
17
|
PYNT_DOCKER_IMAGE = "ghcr.io/pynt-io/pynt"
|
|
18
18
|
IMAGE_TAGS = ["postman-latest", "newman-latest", "har-latest", "proxy-latest", "v1-latest"]
|
|
19
19
|
|
|
20
|
-
PYNT_CONTAINER_INTERNAL_PORT = "5001"
|
|
21
|
-
|
|
22
20
|
|
|
23
21
|
def create_mount(src, destination, mount_type="bind"):
|
|
24
22
|
return {
|
|
@@ -67,13 +65,20 @@ class PyntBaseContainer():
|
|
|
67
65
|
self.environment = environment
|
|
68
66
|
|
|
69
67
|
|
|
70
|
-
class
|
|
71
|
-
def __init__(self,
|
|
72
|
-
self.
|
|
73
|
-
self.dest = dest
|
|
68
|
+
class _PyntDockerPort:
|
|
69
|
+
def __init__(self, port: int, name: str) -> None:
|
|
70
|
+
self.port = port
|
|
74
71
|
self.name = name
|
|
75
72
|
|
|
76
73
|
|
|
74
|
+
def api_port(port: int) -> _PyntDockerPort:
|
|
75
|
+
return _PyntDockerPort(port=port, name="--port")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def proxy_port(port: int) -> _PyntDockerPort:
|
|
79
|
+
return _PyntDockerPort(port=port, name="--proxy-port")
|
|
80
|
+
|
|
81
|
+
|
|
77
82
|
class PyntDockerImage:
|
|
78
83
|
def __init__(self, name, is_self_managed) -> None:
|
|
79
84
|
self.name = name
|
|
@@ -81,31 +86,16 @@ class PyntDockerImage:
|
|
|
81
86
|
|
|
82
87
|
|
|
83
88
|
class DockerContainerConfig:
|
|
84
|
-
def __init__(self, args: argparse.Namespace, integration_name: str, *port_args:
|
|
89
|
+
def __init__(self, args: argparse.Namespace, integration_name: str, *port_args: _PyntDockerPort):
|
|
85
90
|
self.image = get_image_config(args)
|
|
86
91
|
self.is_detach = True
|
|
87
|
-
self.ports = port_args
|
|
88
|
-
self.docker_arguments = build_docker_args(integration_name, args)
|
|
92
|
+
self.ports: List[int] = [port_arg.port for port_arg in port_args]
|
|
93
|
+
self.docker_arguments = build_docker_args(integration_name, args, port_args)
|
|
89
94
|
self.mounts = get_docker_mounts(args)
|
|
90
95
|
self.env_vars = {PYNT_ID: CredStore().get_tokens(), "PYNT_SAAS_URL": PYNT_SAAS}
|
|
91
96
|
if user_set_all_variables():
|
|
92
97
|
add_env_variables(self.env_vars)
|
|
93
98
|
|
|
94
|
-
ports = {}
|
|
95
|
-
create_network_host = is_network_host()
|
|
96
|
-
for p in port_args:
|
|
97
|
-
if create_network_host:
|
|
98
|
-
self.docker_arguments.append(p.name)
|
|
99
|
-
self.docker_arguments.append(str(p.dest))
|
|
100
|
-
else:
|
|
101
|
-
# `dest` is the host port, `src` is in the container
|
|
102
|
-
ports[str(p.dest)] = int(p.src)
|
|
103
|
-
|
|
104
|
-
if create_network_host:
|
|
105
|
-
self.docker_type = PyntNativeHost(network="host")
|
|
106
|
-
else:
|
|
107
|
-
self.docker_type = PyntDockerDesktopContainer(ports=ports)
|
|
108
|
-
|
|
109
99
|
|
|
110
100
|
def get_image_config(args: argparse.Namespace) -> PyntDockerImage:
|
|
111
101
|
default_image = f'{PYNT_DOCKER_IMAGE}:v1-latest'
|
|
@@ -136,8 +126,8 @@ def user_set_all_variables():
|
|
|
136
126
|
|
|
137
127
|
def add_env_variables(env: dict):
|
|
138
128
|
env["PYNT_BUCKET_NAME"] = PYNT_BUCKET_NAME
|
|
139
|
-
env["PYNT_PARAM1"] = base64.b64encode(PYNT_PARAM1.encode('utf-8'))
|
|
140
|
-
env["PYNT_PARAM2"] = base64.b64encode(PYNT_PARAM2.encode('utf-8'))
|
|
129
|
+
env["PYNT_PARAM1"] = base64.b64encode(PYNT_PARAM1.encode('utf-8')).decode('utf-8')
|
|
130
|
+
env["PYNT_PARAM2"] = base64.b64encode(PYNT_PARAM2.encode('utf-8')).decode('utf-8')
|
|
141
131
|
|
|
142
132
|
|
|
143
133
|
def value_from_environment_variable(key):
|
|
@@ -150,7 +140,7 @@ def value_from_environment_variable(key):
|
|
|
150
140
|
return None
|
|
151
141
|
|
|
152
142
|
|
|
153
|
-
def build_docker_args(integration_name:str, args: argparse.Namespace) ->
|
|
143
|
+
def build_docker_args(integration_name: str, args: argparse.Namespace, port_args: List[_PyntDockerPort]) -> List[str]:
|
|
154
144
|
docker_arguments = [integration_name]
|
|
155
145
|
|
|
156
146
|
if "insecure" in args and args.insecure:
|
|
@@ -176,6 +166,9 @@ def build_docker_args(integration_name:str, args: argparse.Namespace) -> list[st
|
|
|
176
166
|
if "verbose" in args and args.verbose:
|
|
177
167
|
docker_arguments.append("--verbose")
|
|
178
168
|
|
|
169
|
+
for port_arg in port_args:
|
|
170
|
+
docker_arguments += [port_arg.name, str(port_arg.port)]
|
|
171
|
+
|
|
179
172
|
return docker_arguments
|
|
180
173
|
|
|
181
174
|
|
|
@@ -233,22 +226,21 @@ class PyntContainerNative:
|
|
|
233
226
|
for key, value in self.config.env_vars.items():
|
|
234
227
|
env_vars.extend(self.adapt_environment_variable_partial(key, value))
|
|
235
228
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
else:
|
|
243
|
-
docker_type_options.extend([f"--{key}={value}"])
|
|
229
|
+
if is_network_host():
|
|
230
|
+
ports_exposure = ["--network=host"]
|
|
231
|
+
else:
|
|
232
|
+
ports_exposure = []
|
|
233
|
+
for port in self.config.ports:
|
|
234
|
+
ports_exposure.extend(["-p", f"{port}:{port}"])
|
|
244
235
|
|
|
245
236
|
docker_command += mounts
|
|
246
237
|
docker_command += env_vars
|
|
247
|
-
docker_command +=
|
|
238
|
+
docker_command += ports_exposure
|
|
248
239
|
docker_command += [f"{self.config.image.name}"]
|
|
249
240
|
docker_command += args
|
|
250
241
|
|
|
251
242
|
command = self.adapt_run_command(docker_command)
|
|
243
|
+
|
|
252
244
|
PyntContainerRegistry.instance().register_container(self)
|
|
253
245
|
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
254
246
|
stdout, stderr = process.communicate()
|
|
@@ -297,7 +289,7 @@ class PyntContainerNative:
|
|
|
297
289
|
for container_id in container_ids:
|
|
298
290
|
kill_command = ["docker", "kill", container_id]
|
|
299
291
|
remove_command = ["docker", "remove", container_id]
|
|
300
|
-
subprocess.run(kill_command,stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
292
|
+
subprocess.run(kill_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
301
293
|
subprocess.run(remove_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
302
294
|
if report_to_user:
|
|
303
295
|
ui_thread.print(
|
|
@@ -355,22 +347,6 @@ class PyntContainerNative:
|
|
|
355
347
|
raise PortInUseException(port)
|
|
356
348
|
|
|
357
349
|
|
|
358
|
-
class PyntDockerDesktopContainer:
|
|
359
|
-
def __init__(self, ports) -> None:
|
|
360
|
-
self.ports = ports
|
|
361
|
-
|
|
362
|
-
def get_arguments(self):
|
|
363
|
-
return {"ports": self.ports} if self.ports else {}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
class PyntNativeHost:
|
|
367
|
-
def __init__(self, network) -> None:
|
|
368
|
-
self.network = network
|
|
369
|
-
|
|
370
|
-
def get_arguments(self):
|
|
371
|
-
return {"network": self.network} if self.network else {}
|
|
372
|
-
|
|
373
|
-
|
|
374
350
|
class PyntContainerRegistry:
|
|
375
351
|
_instance = None
|
|
376
352
|
|
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
ignoreTests/conftest.py,sha256=gToq5K74GtgeGQXjFvXSzMaE6axBYxAzcFG5XJPOXjI,427
|
|
2
2
|
ignoreTests/auth/login.py,sha256=KFlzWhXBAuwdi7GXf16gCB3ya94LQG2wjcSChE149rQ,3798
|
|
3
3
|
ignoreTests/store/cred_store.py,sha256=_7-917EtNC9eKEumO2_lt-7KuDmCwOZFaowCm7DbA_A,254
|
|
4
|
-
pyntcli/__init__.py,sha256=
|
|
4
|
+
pyntcli/__init__.py,sha256=Cd4SdQA_B1lvL3flAA4RSH333OefFegLyvOlQ3QX7IA,24
|
|
5
5
|
pyntcli/main.py,sha256=RD0W2_0ogYBCXubo-YewxHYkiIXxNv6NkZOh3n1VujE,5964
|
|
6
6
|
pyntcli/analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
pyntcli/analytics/send.py,sha256=0hJ0WJNFHLqyohtRr_xOg5WEXzxHrUOlcePPg-k65Hk,3846
|
|
8
8
|
pyntcli/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
pyntcli/auth/login.py,sha256=TljsRXbEkNI1YUrKm5mlTw4YiecYScYUsit8Z8vstss,5228
|
|
10
10
|
pyntcli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
pyntcli/commands/burp.py,sha256=
|
|
12
|
-
pyntcli/commands/command.py,sha256=
|
|
13
|
-
pyntcli/commands/har.py,sha256=
|
|
11
|
+
pyntcli/commands/burp.py,sha256=sR5D2vlOXj9Qae3c1-VWfIXMouXoEv2Ik3O-C-Wqqkc,13891
|
|
12
|
+
pyntcli/commands/command.py,sha256=mMf1_bU7IFbE54FN3S3FuPODjHSSSu8aHzHJX-MgEFA,10832
|
|
13
|
+
pyntcli/commands/har.py,sha256=v6yWTuxPpXLlEi3yJ7gUDhFFGJZSpNQ3ehl2oaZJ-wc,4202
|
|
14
14
|
pyntcli/commands/id_command.py,sha256=UBEgMIpm4vauTCsKyixltiGUolNg_OfHEJvJ_i5BpJY,943
|
|
15
|
-
pyntcli/commands/listen.py,sha256=
|
|
16
|
-
pyntcli/commands/newman.py,sha256=
|
|
17
|
-
pyntcli/commands/postman.py,sha256=
|
|
18
|
-
pyntcli/commands/pynt_cmd.py,sha256=
|
|
15
|
+
pyntcli/commands/listen.py,sha256=bsO7c3cHmZyxUTXspCcq_O9BR8tI2qYqOpHtJDb8hZY,8827
|
|
16
|
+
pyntcli/commands/newman.py,sha256=68P1bCjSSjTWrzOwKN_-fGQC4j484KlVpMbezMJOCMo,5045
|
|
17
|
+
pyntcli/commands/postman.py,sha256=ExfvG0iLRmK_rHcqIj8itcd3mh2-BdPuQEDm8viLP6Q,4891
|
|
18
|
+
pyntcli/commands/pynt_cmd.py,sha256=AeXxKy8dtZTdwWhTirHVvkPkeFUSoZNb3FRyuwkL6kg,7103
|
|
19
19
|
pyntcli/commands/root.py,sha256=UtGky7LfbfQ6mELHoNJcdHznFrUVBlhXI47_8QdApfc,4068
|
|
20
20
|
pyntcli/commands/static_file_extensions.py,sha256=PZJb02BI-64tbU-j3rdCNsXzTh7gkIDGxGKbKNw3h5k,1995
|
|
21
21
|
pyntcli/commands/sub_command.py,sha256=GF3-rE_qk2L4jGPFqHLm9SdGINmu3EakhjJTFyWjRms,374
|
|
22
|
+
pyntcli/commands/template.py,sha256=KocIH1T7occrrjaWJQaKKGgGWrU1bieTKEJbhFw3KLI,7976
|
|
22
23
|
pyntcli/commands/util.py,sha256=FGtuXLxMAL0b9sbsrJaQR_-5sEu-PwZOJonMwuCjO2U,4450
|
|
23
24
|
pyntcli/log/__init__.py,sha256=cOGwOYzMoshEbZiiasBGkj6wF0SBu3Jdpl-AuakDesw,19
|
|
24
25
|
pyntcli/log/log.py,sha256=YXCvcCzuhQ5QUT2L02uQEdN_lTCzLEuet4OnLuEnjlM,112
|
|
25
26
|
pyntcli/pynt_docker/__init__.py,sha256=PQIOVxc7XXtMLfEX7ojgwf_Z3mmTllO3ZvzUZTPOxQY,30
|
|
26
27
|
pyntcli/pynt_docker/container_utils.py,sha256=_Onn7loInzyJAG2-Uk6CGpsuRyelmUFHOvtJj4Uzi9A,175
|
|
27
|
-
pyntcli/pynt_docker/pynt_container.py,sha256=
|
|
28
|
+
pyntcli/pynt_docker/pynt_container.py,sha256=42Qgwcd5EgtF0RKa2e9O2AjGSngAz9rZveCMOHGZaS8,13233
|
|
28
29
|
pyntcli/store/__init__.py,sha256=1fP8cEAQCF_myja3gnhHH9FEqtBiOJ-2aBmUXSKBdFA,41
|
|
29
30
|
pyntcli/store/json_connector.py,sha256=UGs3uORw3iyn0YJ8kzab-veEZToA6d-ByXYuqEleWsA,560
|
|
30
31
|
pyntcli/store/store.py,sha256=Kl_HT9FJFdKlAH7SwAt3g4-bW-r-1ve_u13OPggQai0,2529
|
|
@@ -39,8 +40,8 @@ pyntcli/ui/report.py,sha256=W-icPSZrGLOubXgam0LpOvHLl_aZg9Zx9qIkL8Ym5PE,1930
|
|
|
39
40
|
pyntcli/ui/ui_thread.py,sha256=XUBgLpYQjVhrilU-ofw7VSXgTiwneSdTxm61EvC3x4Q,5091
|
|
40
41
|
tests/test_utils.py,sha256=t5fTQUk1U_Js6iMxcGYGqt4C-crzOJ0CqCKtLkRtUi0,2050
|
|
41
42
|
tests/commands/test_pynt_cmd.py,sha256=BjGFCFACcSziLrNA6_27t6TjSmvdu54wx9njwLpRSJY,8379
|
|
42
|
-
pyntcli-0.1.
|
|
43
|
-
pyntcli-0.1.
|
|
44
|
-
pyntcli-0.1.
|
|
45
|
-
pyntcli-0.1.
|
|
46
|
-
pyntcli-0.1.
|
|
43
|
+
pyntcli-0.1.109.dist-info/METADATA,sha256=fHnwDHSJvgjpR3B2PBXIdU9t3WVMl6bLFbD2p1mz9PU,428
|
|
44
|
+
pyntcli-0.1.109.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
|
45
|
+
pyntcli-0.1.109.dist-info/entry_points.txt,sha256=kcGmqAxXDttNk2EPRcqunc_LTVp61gzakz0v-GEE2SY,43
|
|
46
|
+
pyntcli-0.1.109.dist-info/top_level.txt,sha256=64XSgBzSpgwjYjEKHZE7q3JH2a816zEeyZBXfJi3AKI,42
|
|
47
|
+
pyntcli-0.1.109.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|