secator 0.0.1__py3-none-any.whl → 0.3.6__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 (68) hide show
  1. secator/.gitignore +162 -0
  2. secator/celery.py +7 -67
  3. secator/cli.py +631 -274
  4. secator/decorators.py +54 -11
  5. secator/definitions.py +104 -33
  6. secator/exporters/csv.py +1 -2
  7. secator/exporters/gdrive.py +1 -1
  8. secator/exporters/json.py +1 -2
  9. secator/exporters/txt.py +1 -2
  10. secator/hooks/mongodb.py +12 -12
  11. secator/installer.py +335 -0
  12. secator/report.py +2 -14
  13. secator/rich.py +3 -10
  14. secator/runners/_base.py +105 -34
  15. secator/runners/_helpers.py +18 -17
  16. secator/runners/command.py +91 -55
  17. secator/runners/scan.py +2 -1
  18. secator/runners/task.py +5 -4
  19. secator/runners/workflow.py +12 -11
  20. secator/tasks/_categories.py +14 -19
  21. secator/tasks/cariddi.py +2 -1
  22. secator/tasks/dalfox.py +2 -0
  23. secator/tasks/dirsearch.py +5 -7
  24. secator/tasks/dnsx.py +1 -0
  25. secator/tasks/dnsxbrute.py +1 -0
  26. secator/tasks/feroxbuster.py +6 -7
  27. secator/tasks/ffuf.py +4 -7
  28. secator/tasks/gau.py +1 -4
  29. secator/tasks/gf.py +2 -1
  30. secator/tasks/gospider.py +1 -0
  31. secator/tasks/grype.py +47 -47
  32. secator/tasks/h8mail.py +5 -6
  33. secator/tasks/httpx.py +24 -18
  34. secator/tasks/katana.py +11 -15
  35. secator/tasks/maigret.py +3 -3
  36. secator/tasks/mapcidr.py +1 -0
  37. secator/tasks/msfconsole.py +3 -1
  38. secator/tasks/naabu.py +2 -1
  39. secator/tasks/nmap.py +14 -17
  40. secator/tasks/nuclei.py +4 -3
  41. secator/tasks/searchsploit.py +3 -2
  42. secator/tasks/subfinder.py +1 -0
  43. secator/tasks/wpscan.py +11 -13
  44. secator/utils.py +64 -82
  45. secator/utils_test.py +3 -2
  46. secator-0.3.6.dist-info/METADATA +411 -0
  47. secator-0.3.6.dist-info/RECORD +100 -0
  48. {secator-0.0.1.dist-info → secator-0.3.6.dist-info}/WHEEL +1 -2
  49. secator-0.0.1.dist-info/METADATA +0 -199
  50. secator-0.0.1.dist-info/RECORD +0 -114
  51. secator-0.0.1.dist-info/top_level.txt +0 -2
  52. tests/__init__.py +0 -0
  53. tests/integration/__init__.py +0 -0
  54. tests/integration/inputs.py +0 -42
  55. tests/integration/outputs.py +0 -392
  56. tests/integration/test_scans.py +0 -82
  57. tests/integration/test_tasks.py +0 -103
  58. tests/integration/test_workflows.py +0 -163
  59. tests/performance/__init__.py +0 -0
  60. tests/performance/loadtester.py +0 -56
  61. tests/unit/__init__.py +0 -0
  62. tests/unit/test_celery.py +0 -39
  63. tests/unit/test_scans.py +0 -0
  64. tests/unit/test_serializers.py +0 -51
  65. tests/unit/test_tasks.py +0 -348
  66. tests/unit/test_workflows.py +0 -96
  67. {secator-0.0.1.dist-info → secator-0.3.6.dist-info}/entry_points.txt +0 -0
  68. {secator-0.0.1.dist-info → secator-0.3.6.dist-info/licenses}/LICENSE +0 -0
secator/cli.py CHANGED
@@ -7,19 +7,20 @@ import rich_click as click
7
7
  from dotmap import DotMap
8
8
  from fp.fp import FreeProxy
9
9
  from jinja2 import Template
10
+ from rich.live import Live
10
11
  from rich.markdown import Markdown
11
12
  from rich.rule import Rule
12
13
 
13
- from secator.celery import app, is_celery_worker_alive
14
14
  from secator.config import ConfigLoader
15
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)
16
+ from secator.definitions import (ADDONS_ENABLED, ASCII, CVES_FOLDER, DATA_FOLDER, DEV_PACKAGE, OPT_NOT_SUPPORTED,
17
+ PAYLOADS_FOLDER, REVSHELLS_FOLDER, ROOT_FOLDER, VERSION)
18
+ from secator.installer import ToolInstaller, get_version_info, get_health_table, fmt_health_table_row
18
19
  from secator.rich import console
19
20
  from secator.runners import Command
20
21
  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)
22
+ from secator.utils import (debug, detect_host, discover_tasks, find_list_item, flatten,
23
+ print_results_table, print_version)
23
24
 
24
25
  click.rich_click.USE_RICH_MARKUP = True
25
26
 
@@ -27,40 +28,45 @@ ALL_TASKS = discover_tasks()
27
28
  ALL_CONFIGS = ConfigLoader.load_all()
28
29
  ALL_WORKFLOWS = ALL_CONFIGS.workflow
29
30
  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
31
 
37
32
 
38
- #--------#
39
- # GROUPS #
40
- #--------#
41
-
33
+ #-----#
34
+ # CLI #
35
+ #-----#
42
36
 
43
- @click.group(cls=OrderedGroup)
44
- @click.option('--no-banner', '-nb', is_flag=True, default=False)
45
- def cli(no_banner):
37
+ @click.group(cls=OrderedGroup, invoke_without_command=True)
38
+ @click.option('--version', '-version', is_flag=True, default=False)
39
+ @click.pass_context
40
+ def cli(ctx, version):
46
41
  """Secator CLI."""
47
- if not no_banner:
48
- print(ASCII, file=sys.stderr)
49
- pass
42
+ console.print(ASCII, highlight=False)
43
+ if ctx.invoked_subcommand is None:
44
+ if version:
45
+ print_version()
46
+ else:
47
+ ctx.get_help()
48
+
50
49
 
50
+ #------#
51
+ # TASK #
52
+ #------#
51
53
 
52
- @cli.group(aliases=['x', 't', 'cmd'])
54
+ @cli.group(aliases=['x', 't'])
53
55
  def task():
54
56
  """Run a task."""
55
57
  pass
56
58
 
57
59
 
58
60
  for cls in ALL_TASKS:
59
- config = DotMap({'name': cls.__name__})
61
+ config = DotMap({'name': cls.__name__, 'type': 'task'})
60
62
  register_runner(task, config)
61
63
 
64
+ #----------#
65
+ # WORKFLOW #
66
+ #----------#
67
+
62
68
 
63
- @cli.group(cls=OrderedGroup, aliases=['w', 'wf', 'flow'])
69
+ @cli.group(cls=OrderedGroup, aliases=['w'])
64
70
  def workflow():
65
71
  """Run a workflow."""
66
72
  pass
@@ -70,7 +76,11 @@ for config in sorted(ALL_WORKFLOWS, key=lambda x: x['name']):
70
76
  register_runner(workflow, config)
71
77
 
72
78
 
73
- @cli.group(cls=OrderedGroup, aliases=['z', 's', 'sc'])
79
+ #------#
80
+ # SCAN #
81
+ #------#
82
+
83
+ @cli.group(cls=OrderedGroup, aliases=['s'])
74
84
  def scan():
75
85
  """Run a scan."""
76
86
  pass
@@ -80,65 +90,11 @@ for config in sorted(ALL_SCANS, key=lambda x: x['name']):
80
90
  register_runner(scan, config)
81
91
 
82
92
 
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
93
  #--------#
138
94
  # WORKER #
139
95
  #--------#
140
96
 
141
- @cli.command(context_settings=dict(ignore_unknown_options=True))
97
+ @cli.command(name='worker', context_settings=dict(ignore_unknown_options=True), aliases=['wk'])
142
98
  @click.option('-n', '--hostname', type=str, default='runner', help='Celery worker hostname (unique).')
143
99
  @click.option('-c', '--concurrency', type=int, default=100, help='Number of child processes processing the queue.')
144
100
  @click.option('-r', '--reload', is_flag=True, help='Autoreload Celery on code changes.')
@@ -149,13 +105,20 @@ def report_show(json_path, exclude_fields):
149
105
  @click.option('--stop', is_flag=True, help='Stop a worker in dev mode (celery multi).')
150
106
  @click.option('--show', is_flag=True, help='Show command (celery multi).')
151
107
  def worker(hostname, concurrency, reload, queue, pool, check, dev, stop, show):
152
- """Celery worker."""
108
+ """Run a worker."""
109
+ if not ADDONS_ENABLED['worker']:
110
+ console.print('[bold red]Missing worker addon: please run `secator install addons worker`[/].')
111
+ sys.exit(1)
112
+ from secator.celery import app, is_celery_worker_alive
113
+ debug('conf', obj=dict(app.conf), obj_breaklines=True, sub='celery.app.conf', level=4)
114
+ debug('registered tasks', obj=list(app.tasks.keys()), obj_breaklines=True, sub='celery.tasks', level=4)
153
115
  if check:
154
116
  is_celery_worker_alive()
155
117
  return
156
118
  if not queue:
157
119
  queue = 'io,cpu,' + ','.join([r['queue'] for r in app.conf.task_routes.values()])
158
120
  app_str = 'secator.celery.app'
121
+ celery = f'{sys.executable} -m celery'
159
122
  if dev:
160
123
  subcmd = 'stop' if stop else 'show' if show else 'start'
161
124
  logfile = '%n.log'
@@ -163,9 +126,9 @@ def worker(hostname, concurrency, reload, queue, pool, check, dev, stop, show):
163
126
  queues = '-Q:1 celery -Q:2 io -Q:3 cpu'
164
127
  concur = '-c:1 10 -c:2 100 -c:3 4'
165
128
  pool = 'eventlet'
166
- cmd = f'celery -A {app_str} multi {subcmd} 3 {queues} -P {pool} {concur} --logfile={logfile} --pidfile={pidfile}'
129
+ cmd = f'{celery} -A {app_str} multi {subcmd} 3 {queues} -P {pool} {concur} --logfile={logfile} --pidfile={pidfile}'
167
130
  else:
168
- cmd = f'celery -A {app_str} worker -n {hostname} -Q {queue}'
131
+ cmd = f'{celery} -A {app_str} worker -n {hostname} -Q {queue}'
169
132
  if pool:
170
133
  cmd += f' -P {pool}'
171
134
  if concurrency:
@@ -173,10 +136,7 @@ def worker(hostname, concurrency, reload, queue, pool, check, dev, stop, show):
173
136
  if reload:
174
137
  patterns = "celery.py;tasks/*.py;runners/*.py;serializers/*.py;output_types/*.py;hooks/*.py;exporters/*.py"
175
138
  cmd = f'watchmedo auto-restart --directory=./ --patterns="{patterns}" --recursive -- {cmd}'
176
- Command.run_command(
177
- cmd,
178
- **DEFAULT_CMD_OPTS
179
- )
139
+ Command.execute(cmd, name='secator worker')
180
140
 
181
141
 
182
142
  #-------#
@@ -184,138 +144,32 @@ def worker(hostname, concurrency, reload, queue, pool, check, dev, stop, show):
184
144
  #-------#
185
145
 
186
146
 
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()
147
+ @cli.group(aliases=['u'])
148
+ def util():
149
+ """Run a utility."""
150
+ pass
200
151
 
201
152
 
202
- @utils.command()
153
+ @util.command()
203
154
  @click.option('--timeout', type=float, default=0.2, help='Proxy timeout (in seconds)')
204
155
  @click.option('--number', '-n', type=int, default=1, help='Number of proxies')
205
- def get_proxy(timeout, number):
206
- """Get a random proxy."""
156
+ def proxy(timeout, number):
157
+ """Get random proxies from FreeProxy."""
207
158
  proxy = FreeProxy(timeout=timeout, rand=True, anonym=True)
208
159
  for _ in range(number):
209
160
  url = proxy.get()
210
161
  print(url)
211
162
 
212
163
 
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()
164
+ @util.command()
312
165
  @click.argument('name', type=str, default=None, required=False)
313
166
  @click.option('--host', '-h', type=str, default=None, help='Specify LHOST for revshell, otherwise autodetected.')
314
167
  @click.option('--port', '-p', type=int, default=9001, show_default=True, help='Specify PORT for revshell')
315
168
  @click.option('--interface', '-i', type=str, help='Interface to use to detect IP')
316
169
  @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."""
170
+ @click.option('--force', is_flag=True)
171
+ def revshell(name, host, port, interface, listen, force):
172
+ """Show reverse shell source codes and run netcat listener (-l)."""
319
173
  if host is None: # detect host automatically
320
174
  host = detect_host(interface)
321
175
  if not host:
@@ -324,7 +178,18 @@ def revshells(name, host, port, interface, listen):
324
178
  style='bold red')
325
179
  return
326
180
 
327
- with open(f'{SCRIPTS_FOLDER}/revshells.json') as f:
181
+ # Download reverse shells JSON from repo
182
+ revshells_json = f'{REVSHELLS_FOLDER}/revshells.json'
183
+ if not os.path.exists(revshells_json) or force:
184
+ ret = Command.execute(
185
+ f'wget https://raw.githubusercontent.com/freelabz/secator/main/scripts/revshells.json && mv revshells.json {REVSHELLS_FOLDER}', # noqa: E501
186
+ cls_attributes={'shell': True}
187
+ )
188
+ if not ret.return_code == 0:
189
+ sys.exit(1)
190
+
191
+ # Parse JSON into shells
192
+ with open(revshells_json) as f:
328
193
  shells = json.loads(f.read())
329
194
  for sh in shells:
330
195
  sh['alias'] = '_'.join(sh['name'].lower()
@@ -372,22 +237,19 @@ def revshells(name, host, port, interface, listen):
372
237
  if listen:
373
238
  console.print(f'Starting netcat listener on port {port} ...', style='bold gold3')
374
239
  cmd = f'nc -lvnp {port}'
375
- Command.run_command(
376
- cmd,
377
- **DEFAULT_CMD_OPTS
378
- )
240
+ Command.execute(cmd)
379
241
 
380
242
 
381
- @utils.command()
243
+ @util.command()
382
244
  @click.option('--directory', '-d', type=str, default=PAYLOADS_FOLDER, show_default=True, help='HTTP server directory')
383
245
  @click.option('--host', '-h', type=str, default=None, help='HTTP host')
384
246
  @click.option('--port', '-p', type=int, default=9001, help='HTTP server port')
385
247
  @click.option('--interface', '-i', type=str, default=None, help='Interface to use to auto-detect host IP')
386
248
  def serve(directory, host, port, interface):
387
- """Serve payloads in HTTP server."""
249
+ """Run HTTP server to serve payloads."""
388
250
  LSE_URL = 'https://github.com/diego-treitos/linux-smart-enumeration/releases/latest/download/lse.sh'
389
251
  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'
252
+ SUDOKILLER_URL = 'https://raw.githubusercontent.com/TH3xACE/SUDO_KILLER/V3/SUDO_KILLERv3.sh'
391
253
  PAYLOADS = [
392
254
  {
393
255
  'fname': 'lse.sh',
@@ -412,20 +274,11 @@ def serve(directory, host, port, interface):
412
274
  with console.status(f'[bold yellow][{ix}/{len(PAYLOADS)}] Downloading {fname} [dim]({descr})[/] ...[/]'):
413
275
  cmd = payload['command']
414
276
  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
- )
277
+ Command.execute(cmd, cls_attributes={'shell': True}, cwd=directory)
423
278
  console.print()
424
279
 
425
280
  console.print(Rule())
426
281
  console.print(f'Available payloads in {directory}: ', style='bold yellow')
427
- opts = DEFAULT_CMD_OPTS.copy()
428
- opts['print_cmd'] = False
429
282
  for fname in os.listdir(directory):
430
283
  if not host:
431
284
  host = detect_host(interface)
@@ -440,15 +293,11 @@ def serve(directory, host, port, interface):
440
293
  console.print(f'wget http://{host}:{port}/{fname}', style='dim italic')
441
294
  console.print('')
442
295
  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
- )
296
+ console.print(f'Started HTTP server on port {port}, waiting for incoming connections ...', style='bold yellow')
297
+ Command.execute(f'{sys.executable} -m http.server {port}', cwd=directory)
449
298
 
450
299
 
451
- @utils.command()
300
+ @util.command()
452
301
  @click.argument('record_name', type=str, default=None)
453
302
  @click.option('--script', '-s', type=str, default=None, help='Script to run. See scripts/stories/ for examples.')
454
303
  @click.option('--interactive', '-i', is_flag=True, default=False, help='Interactive record.')
@@ -481,18 +330,15 @@ def record(record_name, script, interactive, width, height, output_dir):
481
330
  console.print(f'Removed existing {output_cast_path}', style='bold green')
482
331
 
483
332
  with console.status('[bold gold3]Recording with asciinema ...[/]'):
484
- Command.run_command(
333
+ Command.execute(
485
334
  f'asciinema-automation -aa "-c /bin/sh" {script} {output_cast_path} --timeout 200',
486
335
  cls_attributes=attrs,
487
336
  raw=True,
488
- **DEFAULT_CMD_OPTS,
489
337
  )
490
338
  console.print(f'Generated {output_cast_path}', style='bold green')
491
339
  elif interactive:
492
340
  os.environ.update(attrs['env'])
493
- Command.run_command(
494
- f'asciinema rec -c /bin/bash --stdin --overwrite {output_cast_path}',
495
- )
341
+ Command.execute(f'asciinema rec -c /bin/bash --stdin --overwrite {output_cast_path}')
496
342
 
497
343
  # Resize cast file
498
344
  if os.path.exists(output_cast_path):
@@ -519,11 +365,10 @@ def record(record_name, script, interactive, width, height, output_dir):
519
365
 
520
366
  # Edit cast file to reduce long timeouts
521
367
  with console.status('[bold gold3] Editing cast file to reduce long commands ...'):
522
- Command.run_command(
368
+ Command.execute(
523
369
  f'asciinema-edit quantize --range 1 {output_cast_path} --out {output_cast_path}.tmp',
524
370
  cls_attributes=attrs,
525
371
  raw=True,
526
- **DEFAULT_CMD_OPTS,
527
372
  )
528
373
  if os.path.exists(f'{output_cast_path}.tmp'):
529
374
  os.replace(f'{output_cast_path}.tmp', output_cast_path)
@@ -531,48 +376,572 @@ def record(record_name, script, interactive, width, height, output_dir):
531
376
 
532
377
  # Convert to GIF
533
378
  with console.status(f'[bold gold3]Converting to {output_gif_path} ...[/]'):
534
- Command.run_command(
379
+ Command.execute(
535
380
  f'agg {output_cast_path} {output_gif_path}',
536
381
  cls_attributes=attrs,
537
- **DEFAULT_CMD_OPTS,
538
382
  )
539
383
  console.print(f'Generated {output_gif_path}', style='bold green')
540
384
 
541
385
 
386
+ @util.group('build')
387
+ def build():
388
+ """Build secator."""
389
+ if not DEV_PACKAGE:
390
+ console.print('[bold red]You MUST use a development version of secator to make builds.[/]')
391
+ sys.exit(1)
392
+ pass
393
+
394
+
395
+ @build.command('pypi')
396
+ def build_pypi():
397
+ """Build secator PyPI package."""
398
+ if not ADDONS_ENABLED['build']:
399
+ console.print('[bold red]Missing build addon: please run `secator install addons build`')
400
+ sys.exit(1)
401
+ with console.status('[bold gold3]Building PyPI package...[/]'):
402
+ ret = Command.execute(f'{sys.executable} -m hatch build', name='hatch build', cwd=ROOT_FOLDER)
403
+ sys.exit(ret.return_code)
404
+
405
+
406
+ @build.command('docker')
407
+ @click.option('--tag', '-t', type=str, default=None, help='Specific tag')
408
+ @click.option('--latest', '-l', is_flag=True, default=False, help='Latest tag')
409
+ def build_docker(tag, latest):
410
+ """Build secator Docker image."""
411
+ if not tag:
412
+ tag = VERSION if latest else 'dev'
413
+ cmd = f'docker build -t freelabz/secator:{tag}'
414
+ if latest:
415
+ cmd += ' -t freelabz/secator:latest'
416
+ cmd += ' .'
417
+ with console.status('[bold gold3]Building Docker image...[/]'):
418
+ ret = Command.execute(cmd, name='docker build', cwd=ROOT_FOLDER)
419
+ sys.exit(ret.return_code)
420
+
421
+
422
+ @util.group('publish')
423
+ def publish():
424
+ """Publish secator."""
425
+ if not DEV_PACKAGE:
426
+ console.print('[bold red]You MUST use a development version of secator to publish builds.[/]')
427
+ sys.exit(1)
428
+ pass
429
+
430
+
431
+ @publish.command('pypi')
432
+ def publish_pypi():
433
+ """Publish secator PyPI package."""
434
+ if not ADDONS_ENABLED['build']:
435
+ console.print('[bold red]Missing build addon: please run `secator install addons build`')
436
+ sys.exit(1)
437
+ os.environ['HATCH_INDEX_USER'] = '__token__'
438
+ hatch_token = os.environ.get('HATCH_INDEX_AUTH')
439
+ if not hatch_token:
440
+ console.print('[bold red]Missing PyPI auth token (HATCH_INDEX_AUTH env variable).')
441
+ sys.exit(1)
442
+ with console.status('[bold gold3]Publishing PyPI package...[/]'):
443
+ ret = Command.execute(f'{sys.executable} -m hatch publish', name='hatch publish', cwd=ROOT_FOLDER)
444
+ sys.exit(ret.return_code)
445
+
446
+
447
+ @publish.command('docker')
448
+ @click.option('--tag', '-t', default=None, help='Specific tag')
449
+ @click.option('--latest', '-l', is_flag=True, default=False, help='Latest tag')
450
+ def publish_docker(tag, latest):
451
+ """Publish secator Docker image."""
452
+ if not tag:
453
+ tag = VERSION if latest else 'dev'
454
+ cmd = f'docker push freelabz/secator:{tag}'
455
+ cmd2 = 'docker push freelabz/secator:latest'
456
+ with console.status(f'[bold gold3]Publishing Docker image {tag}...[/]'):
457
+ ret = Command.execute(cmd, name=f'docker push ({tag})', cwd=ROOT_FOLDER)
458
+ if latest:
459
+ ret2 = Command.execute(cmd2, name='docker push (latest)')
460
+ sys.exit(max(ret.return_code, ret2.return_code))
461
+ sys.exit(ret.return_code)
462
+
463
+
464
+ #--------#
465
+ # REPORT #
466
+ #--------#
467
+
468
+
469
+ @cli.group(aliases=['r'])
470
+ def report():
471
+ """View previous reports."""
472
+ pass
473
+
474
+
475
+ @report.command('show')
476
+ @click.argument('json_path')
477
+ @click.option('-e', '--exclude-fields', type=str, default='', help='List of fields to exclude (comma-separated)')
478
+ def report_show(json_path, exclude_fields):
479
+ """Show a JSON report as a nicely-formatted table."""
480
+ with open(json_path, 'r') as f:
481
+ report = loads_dataclass(f.read())
482
+ results = flatten(list(report['results'].values()))
483
+ exclude_fields = exclude_fields.split(',')
484
+ print_results_table(
485
+ results,
486
+ title=report['info']['title'],
487
+ exclude_fields=exclude_fields)
488
+
489
+
490
+ #--------#
491
+ # DEPLOY #
492
+ #--------#
493
+
494
+ # TODO: work on this
495
+ # @cli.group(aliases=['d'])
496
+ # def deploy():
497
+ # """Deploy secator."""
498
+ # pass
499
+
500
+ # @deploy.command()
501
+ # def docker_compose():
502
+ # """Deploy secator on docker-compose."""
503
+ # pass
504
+
505
+ # @deploy.command()
506
+ # @click.option('-t', '--target', type=str, default='minikube', help='Deployment target amongst minikube, gke')
507
+ # def k8s():
508
+ # """Deploy secator on Kubernetes."""
509
+ # pass
510
+
511
+
512
+ #--------#
513
+ # HEALTH #
514
+ #--------#
515
+
516
+ @cli.command(name='health')
517
+ @click.option('--json', '-json', is_flag=True, default=False, help='JSON lines output')
518
+ @click.option('--debug', '-debug', is_flag=True, default=False, help='Debug health output')
519
+ def health(json, debug):
520
+ """[dim]Get health status.[/]"""
521
+ tools = ALL_TASKS
522
+ status = {'secator': {}, 'languages': {}, 'tools': {}, 'addons': {}}
523
+
524
+ # Check secator
525
+ console.print(':wrench: [bold gold3]Checking secator ...[/]')
526
+ info = get_version_info('secator', '-version', 'freelabz/secator')
527
+ table = get_health_table()
528
+ with Live(table, console=console):
529
+ row = fmt_health_table_row(info)
530
+ table.add_row(*row)
531
+ status['secator'] = info
532
+
533
+ # Check languages
534
+ console.print('\n:wrench: [bold gold3]Checking installed languages ...[/]')
535
+ version_cmds = {'go': 'version', 'python3': '--version', 'ruby': '--version'}
536
+ table = get_health_table()
537
+ with Live(table, console=console):
538
+ for lang, version_flag in version_cmds.items():
539
+ info = get_version_info(lang, version_flag)
540
+ row = fmt_health_table_row(info, 'langs')
541
+ table.add_row(*row)
542
+ status['languages'][lang] = info
543
+
544
+ # Check tools
545
+ console.print('\n:wrench: [bold gold3]Checking installed tools ...[/]')
546
+ table = get_health_table()
547
+ with Live(table, console=console):
548
+ for tool in tools:
549
+ cmd = tool.cmd.split(' ')[0]
550
+ version_flag = tool.version_flag or f'{tool.opt_prefix}version'
551
+ version_flag = None if tool.version_flag == OPT_NOT_SUPPORTED else version_flag
552
+ info = get_version_info(cmd, version_flag, tool.install_github_handle)
553
+ row = fmt_health_table_row(info, 'tools')
554
+ table.add_row(*row)
555
+ status['tools'][tool.__name__] = info
556
+
557
+ # # Check addons
558
+ console.print('\n:wrench: [bold gold3]Checking installed addons ...[/]')
559
+ table = get_health_table()
560
+ with Live(table, console=console):
561
+ for addon in ['worker', 'google', 'mongodb', 'redis', 'dev', 'trace', 'build']:
562
+ addon_var = ADDONS_ENABLED[addon]
563
+ info = {
564
+ 'name': addon,
565
+ 'version': None,
566
+ 'status': 'ok' if addon_var else 'missing',
567
+ 'latest_version': None,
568
+ 'installed': addon_var,
569
+ 'location': None
570
+ }
571
+ row = fmt_health_table_row(info, 'addons')
572
+ table.add_row(*row)
573
+ status['addons'][addon] = info
574
+
575
+ # Print JSON health
576
+ if json:
577
+ import json as _json
578
+ print(_json.dumps(status))
579
+
580
+ #---------#
581
+ # INSTALL #
582
+ #---------#
583
+
584
+
585
+ def run_install(cmd, title, next_steps=None):
586
+ with console.status(f'[bold yellow] Installing {title}...'):
587
+ ret = Command.execute(cmd, cls_attributes={'shell': True}, print_cmd=True, print_line=True)
588
+ if ret.return_code != 0:
589
+ console.print(f':exclamation_mark: Failed to install {title}.', style='bold red')
590
+ else:
591
+ console.print(f':tada: {title.capitalize()} installed successfully !', style='bold green')
592
+ if next_steps:
593
+ console.print('[bold gold3]:wrench: Next steps:[/]')
594
+ for ix, step in enumerate(next_steps):
595
+ console.print(f' :keycap_{ix}: {step}')
596
+ sys.exit(ret.return_code)
597
+
598
+
599
+ @cli.group()
600
+ def install():
601
+ """[dim]Install langs, tools and addons.[/]"""
602
+ pass
603
+
604
+
605
+ @install.group()
606
+ def addons():
607
+ "Install addons."
608
+ pass
609
+
610
+
611
+ @addons.command('worker')
612
+ def install_worker():
613
+ "Install worker addon."
614
+ run_install(
615
+ cmd=f'{sys.executable} -m pip install secator[worker]',
616
+ title='worker addon',
617
+ next_steps=[
618
+ 'Run "secator worker" to run a Celery worker using the file system as a backend and broker.',
619
+ 'Run "secator x httpx testphp.vulnweb.com" to admire your task running in a worker.',
620
+ '[dim]\[optional][/dim] Run "secator install addons redis" to install the Redis addon.'
621
+ ]
622
+ )
623
+
624
+
625
+ @addons.command('google')
626
+ def install_google():
627
+ "Install google addon."
628
+ run_install(
629
+ cmd=f'{sys.executable} -m pip install secator[google]',
630
+ title='google addon',
631
+ next_steps=[
632
+ 'Set the "GOOGLE_CREDENTIALS_PATH" and "GOOGLE_DRIVE_PARENT_FOLDER_ID" environment variables.',
633
+ 'Run "secator x httpx testphp.vulnweb.com -o gdrive" to admire your results flowing to Google Drive.'
634
+ ]
635
+ )
636
+
637
+
638
+ @addons.command('mongodb')
639
+ def install_mongodb():
640
+ "Install mongodb addon."
641
+ run_install(
642
+ cmd=f'{sys.executable} -m pip install secator[mongodb]',
643
+ title='mongodb addon',
644
+ next_steps=[
645
+ '[dim]\[optional][/] Run "docker run --name mongo -p 27017:27017 -d mongo:latest" to run a local MongoDB instance.',
646
+ 'Set the "MONGODB_URL=mongodb://<url>" environment variable pointing to your MongoDB instance.',
647
+ 'Run "secator x httpx testphp.vulnweb.com -driver mongodb" to save results to MongoDB.'
648
+ ]
649
+ )
650
+
651
+
652
+ @addons.command('redis')
653
+ def install_redis():
654
+ "Install redis addon."
655
+ run_install(
656
+ cmd=f'{sys.executable} -m pip install secator[redis]',
657
+ title='redis addon',
658
+ next_steps=[
659
+ '[dim]\[optional][/] Run "docker run --name redis -p 6379:6379 -d redis" to run a local Redis instance.',
660
+ 'Set the "CELERY_BROKER_URL=redis://<url>" environment variable pointing to your Redis instance.',
661
+ 'Set the "CELERY_RESULT_BACKEND=redis://<url>" environment variable pointing to your Redis instance.',
662
+ 'Run "secator worker" to run a worker.',
663
+ 'Run "secator x httpx testphp.vulnweb.com" to run a test task.'
664
+ ]
665
+ )
666
+
667
+
668
+ @addons.command('dev')
669
+ def install_dev():
670
+ "Install dev addon."
671
+ run_install(
672
+ cmd=f'{sys.executable} -m pip install secator[dev]',
673
+ title='dev addon',
674
+ next_steps=[
675
+ 'Run "secator test lint" to run lint tests.',
676
+ 'Run "secator test unit" to run unit tests.',
677
+ 'Run "secator test integration" to run integration tests.',
678
+ ]
679
+ )
680
+
681
+
682
+ @addons.command('trace')
683
+ def install_trace():
684
+ "Install trace addon."
685
+ run_install(
686
+ cmd=f'{sys.executable} -m pip install secator[trace]',
687
+ title='dev addon',
688
+ next_steps=[
689
+ 'Run "secator test lint" to run lint tests.',
690
+ 'Run "secator test unit" to run unit tests.',
691
+ 'Run "secator test integration" to run integration tests.',
692
+ ]
693
+ )
694
+
695
+
696
+ @addons.command('build')
697
+ def install_build():
698
+ "Install build addon."
699
+ run_install(
700
+ cmd=f'{sys.executable} -m pip install secator[build]',
701
+ title='build addon',
702
+ next_steps=[
703
+ 'Run "secator test lint" to run lint tests.',
704
+ 'Run "secator test unit" to run unit tests.',
705
+ 'Run "secator test integration" to run integration tests.',
706
+ ]
707
+ )
708
+
709
+
710
+ @install.group()
711
+ def langs():
712
+ "Install languages."
713
+ pass
714
+
715
+
716
+ @langs.command('go')
717
+ def install_go():
718
+ """Install Go."""
719
+ run_install(
720
+ cmd='wget -O - https://raw.githubusercontent.com/freelabz/secator/main/scripts/install_go.sh | sudo sh',
721
+ title='Go',
722
+ next_steps=[
723
+ 'Add ~/go/bin to your $PATH'
724
+ ]
725
+ )
726
+
727
+
728
+ @langs.command('ruby')
729
+ def install_ruby():
730
+ """Install Ruby."""
731
+ run_install(
732
+ cmd='wget -O - https://raw.githubusercontent.com/freelabz/secator/main/scripts/install_ruby.sh | sudo sh',
733
+ title='Ruby'
734
+ )
735
+
736
+
737
+ @install.command('tools')
738
+ @click.argument('cmds', required=False)
739
+ def install_tools(cmds):
740
+ """Install supported tools."""
741
+ if cmds is not None:
742
+ cmds = cmds.split(',')
743
+ tools = [cls for cls in ALL_TASKS if cls.__name__ in cmds]
744
+ else:
745
+ tools = ALL_TASKS
746
+
747
+ for ix, cls in enumerate(tools):
748
+ with console.status(f'[bold yellow][{ix}/{len(tools)}] Installing {cls.__name__} ...'):
749
+ ToolInstaller.install(cls)
750
+ console.print()
751
+
752
+
753
+ @install.command('cves')
754
+ @click.option('--force', is_flag=True)
755
+ def install_cves(force):
756
+ """Install CVEs (enables passive vulnerability search)."""
757
+ cve_json_path = f'{CVES_FOLDER}/circl-cve-search-expanded.json'
758
+ if not os.path.exists(cve_json_path) or force:
759
+ with console.status('[bold yellow]Downloading zipped CVEs from cve.circl.lu ...[/]'):
760
+ Command.execute('wget https://cve.circl.lu/static/circl-cve-search-expanded.json.gz', cwd=CVES_FOLDER)
761
+ with console.status('[bold yellow]Unzipping CVEs ...[/]'):
762
+ Command.execute(f'gunzip {CVES_FOLDER}/circl-cve-search-expanded.json.gz', cwd=CVES_FOLDER)
763
+ with console.status(f'[bold yellow]Installing CVEs to {CVES_FOLDER} ...[/]'):
764
+ with open(cve_json_path, 'r') as f:
765
+ for line in f:
766
+ data = json.loads(line)
767
+ cve_id = data['id']
768
+ cve_path = f'{CVES_FOLDER}/{cve_id}.json'
769
+ with open(cve_path, 'w') as f:
770
+ f.write(line)
771
+ console.print(f'CVE saved to {cve_path}')
772
+ console.print(':tada: CVEs installed successfully !', style='bold green')
773
+
774
+
775
+ #--------#
776
+ # UPDATE #
777
+ #--------#
778
+
779
+ @cli.command('update')
780
+ def update():
781
+ """[dim]Update to latest version.[/]"""
782
+ info = get_version_info('secator', github_handle='freelabz/secator', version=VERSION)
783
+ latest_version = info['latest_version']
784
+ if info['status'] == 'latest':
785
+ console.print(f'[bold green]secator is already at the newest version {latest_version}[/] !')
786
+ sys.exit(0)
787
+ console.print(f'[bold gold3]:wrench: Updating secator from {VERSION} to {latest_version} ...[/]')
788
+ if 'pipx' in sys.executable:
789
+ Command.execute(f'pipx install secator=={latest_version} --force')
790
+ else:
791
+ Command.execute(f'pip install secator=={latest_version}')
792
+
793
+
794
+ #-------#
795
+ # ALIAS #
796
+ #-------#
797
+
798
+
799
+ @cli.group()
800
+ def alias():
801
+ """[dim]Configure aliases.[/]"""
802
+ pass
803
+
804
+
805
+ @alias.command('enable')
806
+ @click.pass_context
807
+ def enable_aliases(ctx):
808
+ """Enable aliases."""
809
+ fpath = f'{DATA_FOLDER}/.aliases'
810
+ aliases = ctx.invoke(list_aliases, silent=True)
811
+ aliases_str = '\n'.join(aliases)
812
+ with open(fpath, 'w') as f:
813
+ f.write(aliases_str)
814
+ console.print('')
815
+ console.print(f':file_cabinet: Alias file written to {fpath}', style='bold green')
816
+ console.print('To load the aliases, run:')
817
+ md = f"""
818
+ ```sh
819
+ source {fpath} # load the aliases in the current shell
820
+ echo "source {fpath} >> ~/.bashrc" # or add this line to your ~/.bashrc to load them automatically
821
+ ```
822
+ """
823
+ console.print(Markdown(md))
824
+ console.print()
825
+
826
+
827
+ @alias.command('disable')
828
+ @click.pass_context
829
+ def disable_aliases(ctx):
830
+ """Disable aliases."""
831
+ fpath = f'{DATA_FOLDER}/.unalias'
832
+ aliases = ctx.invoke(list_aliases, silent=True)
833
+ aliases_str = ''
834
+ for alias in aliases:
835
+ aliases_str += alias.split('=')[0].replace('alias', 'unalias') + '\n'
836
+ console.print(f':file_cabinet: Unalias file written to {fpath}', style='bold green')
837
+ console.print('To unload the aliases, run:')
838
+ with open(fpath, 'w') as f:
839
+ f.write(aliases_str)
840
+ md = f"""
841
+ ```sh
842
+ source {fpath}
843
+ ```
844
+ """
845
+ console.print(Markdown(md))
846
+ console.print()
847
+
848
+
849
+ @alias.command('list')
850
+ @click.option('--silent', is_flag=True, default=False, help='No print')
851
+ def list_aliases(silent):
852
+ """List aliases"""
853
+ aliases = []
854
+ aliases.extend([
855
+ f'alias {task.__name__}="secator x {task.__name__}"'
856
+ for task in ALL_TASKS
857
+ ])
858
+ aliases.extend([
859
+ f'alias {workflow.alias}="secator w {workflow.name}"'
860
+ for workflow in ALL_WORKFLOWS
861
+ ])
862
+ aliases.extend([
863
+ f'alias {workflow.name}="secator w {workflow.name}"'
864
+ for workflow in ALL_WORKFLOWS
865
+ ])
866
+ aliases.extend([
867
+ f'alias scan_{scan.name}="secator s {scan.name}"'
868
+ for scan in ALL_SCANS
869
+ ])
870
+ aliases.append('alias listx="secator x"')
871
+ aliases.append('alias listw="secator w"')
872
+ aliases.append('alias lists="secator s"')
873
+
874
+ if silent:
875
+ return aliases
876
+ console.print('Aliases:')
877
+ for alias in aliases:
878
+ alias_split = alias.split('=')
879
+ alias_name, alias_cmd = alias_split[0].replace('alias ', ''), alias_split[1].replace('"', '')
880
+ console.print(f'[bold magenta]{alias_name:<15}-> {alias_cmd}')
881
+
882
+ return aliases
883
+
884
+
542
885
  #------#
543
886
  # TEST #
544
887
  #------#
545
888
 
546
889
 
547
- @cli.group()
890
+ @cli.group(cls=OrderedGroup)
548
891
  def test():
549
- """Run tests."""
892
+ """[dim]Run tests."""
893
+ if not DEV_PACKAGE:
894
+ console.print('[bold red]You MUST use a development version of secator to run tests.[/]')
895
+ sys.exit(1)
896
+ if not ADDONS_ENABLED['dev']:
897
+ console.print('[bold red]Missing dev addon: please run `secator install addons dev`')
898
+ sys.exit(1)
550
899
  pass
551
900
 
552
901
 
902
+ def run_test(cmd, name):
903
+ """Run a test and return the result.
904
+
905
+ Args:
906
+ cmd: Command to run.
907
+ name: Name of the test.
908
+ """
909
+ result = Command.execute(cmd, name=name + ' tests', cwd=ROOT_FOLDER)
910
+ if result.return_code == 0:
911
+ console.print(f':tada: {name.capitalize()} tests passed !', style='bold green')
912
+ sys.exit(result.return_code)
913
+
914
+
915
+ @test.command()
916
+ def lint():
917
+ """Run lint tests."""
918
+ cmd = f'{sys.executable} -m flake8 secator/'
919
+ run_test(cmd, 'lint')
920
+
921
+
553
922
  @test.command()
554
923
  @click.option('--tasks', type=str, default='', help='Secator tasks to test (comma-separated)')
555
924
  @click.option('--workflows', type=str, default='', help='Secator workflows to test (comma-separated)')
556
925
  @click.option('--scans', type=str, default='', help='Secator scans to test (comma-separated)')
557
926
  @click.option('--test', '-t', type=str, help='Secator test to run')
558
927
  @click.option('--debug', '-d', type=int, default=0, help='Add debug information')
559
- def integration(tasks, workflows, scans, test, debug):
928
+ def unit(tasks, workflows, scans, test, debug=False):
929
+ """Run unit tests."""
560
930
  os.environ['TEST_TASKS'] = tasks or ''
561
931
  os.environ['TEST_WORKFLOWS'] = workflows or ''
562
932
  os.environ['TEST_SCANS'] = scans or ''
563
933
  os.environ['DEBUG'] = str(debug)
564
- cmd = 'python -m unittest'
934
+ os.environ['DEFAULT_STORE_HTTP_RESPONSES'] = '0'
935
+ os.environ['DEFAULT_SKIP_CVE_SEARCH'] = '1'
936
+
937
+ cmd = f'{sys.executable} -m coverage run --omit="*test*" -m unittest'
565
938
  if test:
566
- if not test.startswith('tests.integration'):
567
- test = f'tests.integration.{test}'
939
+ if not test.startswith('tests.unit'):
940
+ test = f'tests.unit.{test}'
568
941
  cmd += f' {test}'
569
942
  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)
943
+ cmd += ' discover -v tests.unit'
944
+ run_test(cmd, 'unit')
576
945
 
577
946
 
578
947
  @test.command()
@@ -580,38 +949,26 @@ def integration(tasks, workflows, scans, test, debug):
580
949
  @click.option('--workflows', type=str, default='', help='Secator workflows to test (comma-separated)')
581
950
  @click.option('--scans', type=str, default='', help='Secator scans to test (comma-separated)')
582
951
  @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
952
  @click.option('--debug', '-d', type=int, default=0, help='Add debug information')
585
- def unit(tasks, workflows, scans, test, coverage=False, debug=False):
953
+ def integration(tasks, workflows, scans, test, debug):
954
+ """Run integration tests."""
586
955
  os.environ['TEST_TASKS'] = tasks or ''
587
956
  os.environ['TEST_WORKFLOWS'] = workflows or ''
588
957
  os.environ['TEST_SCANS'] = scans or ''
589
958
  os.environ['DEBUG'] = str(debug)
590
-
591
- cmd = 'coverage run --omit="*test*" -m unittest'
959
+ os.environ['DEFAULT_SKIP_CVE_SEARCH'] = '1'
960
+ cmd = f'{sys.executable} -m unittest'
592
961
  if test:
593
- if not test.startswith('tests.unit'):
594
- test = f'tests.unit.{test}'
962
+ if not test.startswith('tests.integration'):
963
+ test = f'tests.integration.{test}'
595
964
  cmd += f' {test}'
596
965
  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)
966
+ cmd += ' discover -v tests.integration'
967
+ run_test(cmd, 'integration')
609
968
 
610
969
 
611
970
  @test.command()
612
- def lint():
613
- result = Command.run_command(
614
- 'flake8 secator/',
615
- **DEFAULT_CMD_OPTS
616
- )
617
- sys.exit(result.return_code)
971
+ def coverage():
972
+ """Run coverage report."""
973
+ cmd = f'{sys.executable} -m coverage report -m --omit=*/site-packages/*,*/tests/*'
974
+ run_test(cmd, 'coverage')