secator 0.6.0__py3-none-any.whl → 0.7.0__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 (84) hide show
  1. secator/celery.py +160 -185
  2. secator/celery_utils.py +268 -0
  3. secator/cli.py +327 -106
  4. secator/config.py +27 -11
  5. secator/configs/workflows/host_recon.yaml +5 -3
  6. secator/configs/workflows/port_scan.yaml +7 -3
  7. secator/configs/workflows/url_bypass.yaml +10 -0
  8. secator/configs/workflows/url_vuln.yaml +1 -1
  9. secator/decorators.py +169 -92
  10. secator/definitions.py +10 -3
  11. secator/exporters/__init__.py +7 -5
  12. secator/exporters/console.py +10 -0
  13. secator/exporters/csv.py +27 -19
  14. secator/exporters/gdrive.py +16 -11
  15. secator/exporters/json.py +3 -1
  16. secator/exporters/table.py +30 -2
  17. secator/exporters/txt.py +20 -16
  18. secator/hooks/gcs.py +53 -0
  19. secator/hooks/mongodb.py +53 -27
  20. secator/output_types/__init__.py +29 -11
  21. secator/output_types/_base.py +11 -1
  22. secator/output_types/error.py +36 -0
  23. secator/output_types/exploit.py +1 -1
  24. secator/output_types/info.py +24 -0
  25. secator/output_types/ip.py +7 -0
  26. secator/output_types/port.py +8 -1
  27. secator/output_types/progress.py +5 -0
  28. secator/output_types/record.py +3 -1
  29. secator/output_types/stat.py +33 -0
  30. secator/output_types/tag.py +6 -4
  31. secator/output_types/url.py +6 -3
  32. secator/output_types/vulnerability.py +3 -2
  33. secator/output_types/warning.py +24 -0
  34. secator/report.py +55 -23
  35. secator/rich.py +44 -39
  36. secator/runners/_base.py +622 -635
  37. secator/runners/_helpers.py +5 -91
  38. secator/runners/celery.py +18 -0
  39. secator/runners/command.py +364 -211
  40. secator/runners/scan.py +8 -24
  41. secator/runners/task.py +21 -55
  42. secator/runners/workflow.py +41 -40
  43. secator/scans/__init__.py +28 -0
  44. secator/serializers/dataclass.py +6 -0
  45. secator/serializers/json.py +10 -5
  46. secator/serializers/regex.py +12 -4
  47. secator/tasks/_categories.py +5 -2
  48. secator/tasks/bbot.py +293 -0
  49. secator/tasks/bup.py +98 -0
  50. secator/tasks/cariddi.py +38 -49
  51. secator/tasks/dalfox.py +3 -0
  52. secator/tasks/dirsearch.py +12 -23
  53. secator/tasks/dnsx.py +49 -30
  54. secator/tasks/dnsxbrute.py +2 -0
  55. secator/tasks/feroxbuster.py +8 -17
  56. secator/tasks/ffuf.py +3 -2
  57. secator/tasks/fping.py +3 -3
  58. secator/tasks/gau.py +5 -0
  59. secator/tasks/gf.py +2 -2
  60. secator/tasks/gospider.py +4 -0
  61. secator/tasks/grype.py +9 -9
  62. secator/tasks/h8mail.py +31 -41
  63. secator/tasks/httpx.py +58 -21
  64. secator/tasks/katana.py +18 -22
  65. secator/tasks/maigret.py +26 -24
  66. secator/tasks/mapcidr.py +2 -3
  67. secator/tasks/msfconsole.py +4 -16
  68. secator/tasks/naabu.py +3 -1
  69. secator/tasks/nmap.py +50 -35
  70. secator/tasks/nuclei.py +9 -2
  71. secator/tasks/searchsploit.py +17 -9
  72. secator/tasks/subfinder.py +5 -1
  73. secator/tasks/wpscan.py +79 -93
  74. secator/template.py +61 -45
  75. secator/thread.py +24 -0
  76. secator/utils.py +330 -80
  77. secator/utils_test.py +48 -23
  78. secator/workflows/__init__.py +28 -0
  79. {secator-0.6.0.dist-info → secator-0.7.0.dist-info}/METADATA +11 -5
  80. secator-0.7.0.dist-info/RECORD +115 -0
  81. {secator-0.6.0.dist-info → secator-0.7.0.dist-info}/WHEEL +1 -1
  82. secator-0.6.0.dist-info/RECORD +0 -101
  83. {secator-0.6.0.dist-info → secator-0.7.0.dist-info}/entry_points.txt +0 -0
  84. {secator-0.6.0.dist-info → secator-0.7.0.dist-info}/licenses/LICENSE +0 -0
secator/cli.py CHANGED
@@ -5,6 +5,7 @@ import shutil
5
5
  import sys
6
6
 
7
7
  from pathlib import Path
8
+ from stat import S_ISFIFO
8
9
 
9
10
  import rich_click as click
10
11
  from dotmap import DotMap
@@ -13,17 +14,22 @@ from jinja2 import Template
13
14
  from rich.live import Live
14
15
  from rich.markdown import Markdown
15
16
  from rich.rule import Rule
17
+ from rich.table import Table
16
18
 
17
19
  from secator.config import CONFIG, ROOT_FOLDER, Config, default_config, config_path
18
- from secator.template import TemplateLoader
19
20
  from secator.decorators import OrderedGroup, register_runner
20
- from secator.definitions import ADDONS_ENABLED, ASCII, DEV_PACKAGE, OPT_NOT_SUPPORTED, VERSION
21
+ from secator.definitions import ADDONS_ENABLED, ASCII, DEV_PACKAGE, OPT_NOT_SUPPORTED, VERSION, STATE_COLORS
21
22
  from secator.installer import ToolInstaller, fmt_health_table_row, get_health_table, get_version_info
23
+ from secator.output_types import FINDING_TYPES
24
+ from secator.report import Report
22
25
  from secator.rich import console
23
26
  from secator.runners import Command, Runner
24
- from secator.report import Report
25
27
  from secator.serializers.dataclass import loads_dataclass
26
- from secator.utils import debug, detect_host, discover_tasks, flatten, print_results_table, print_version
28
+ from secator.template import TemplateLoader
29
+ from secator.utils import (
30
+ debug, detect_host, discover_tasks, flatten, print_version, get_file_date,
31
+ sort_files_by_date, get_file_timestamp, list_reports, get_info_from_report_path, human_to_timedelta
32
+ )
27
33
 
28
34
  click.rich_click.USE_RICH_MARKUP = True
29
35
 
@@ -31,6 +37,7 @@ ALL_TASKS = discover_tasks()
31
37
  ALL_CONFIGS = TemplateLoader.load_all()
32
38
  ALL_WORKFLOWS = ALL_CONFIGS.workflow
33
39
  ALL_SCANS = ALL_CONFIGS.scan
40
+ FINDING_TYPES_LOWER = [c.__name__.lower() for c in FINDING_TYPES]
34
41
 
35
42
 
36
43
  #-----#
@@ -42,7 +49,12 @@ ALL_SCANS = ALL_CONFIGS.scan
42
49
  @click.pass_context
43
50
  def cli(ctx, version):
44
51
  """Secator CLI."""
45
- console.print(ASCII, highlight=False)
52
+ ctx.obj = {
53
+ 'piped_input': S_ISFIFO(os.fstat(0).st_mode),
54
+ 'piped_output': not sys.stdout.isatty()
55
+ }
56
+ if not ctx.obj['piped_output']:
57
+ console.print(ASCII, highlight=False)
46
58
  if ctx.invoked_subcommand is None:
47
59
  if version:
48
60
  print_version()
@@ -55,13 +67,14 @@ def cli(ctx, version):
55
67
  #------#
56
68
 
57
69
  @cli.group(aliases=['x', 't'])
58
- def task():
70
+ @click.pass_context
71
+ def task(ctx):
59
72
  """Run a task."""
60
73
  pass
61
74
 
62
75
 
63
76
  for cls in ALL_TASKS:
64
- config = DotMap({'name': cls.__name__, 'type': 'task'})
77
+ config = TemplateLoader(input={'name': cls.__name__, 'type': 'task'})
65
78
  register_runner(task, config)
66
79
 
67
80
  #----------#
@@ -70,7 +83,8 @@ for cls in ALL_TASKS:
70
83
 
71
84
 
72
85
  @cli.group(cls=OrderedGroup, aliases=['w'])
73
- def workflow():
86
+ @click.pass_context
87
+ def workflow(ctx):
74
88
  """Run a workflow."""
75
89
  pass
76
90
 
@@ -84,7 +98,8 @@ for config in sorted(ALL_WORKFLOWS, key=lambda x: x['name']):
84
98
  #------#
85
99
 
86
100
  @cli.group(cls=OrderedGroup, aliases=['s'])
87
- def scan():
101
+ @click.pass_context
102
+ def scan(ctx):
88
103
  """Run a scan."""
89
104
  pass
90
105
 
@@ -109,25 +124,35 @@ for config in sorted(ALL_SCANS, key=lambda x: x['name']):
109
124
  @click.option('--show', is_flag=True, help='Show command (celery multi).')
110
125
  def worker(hostname, concurrency, reload, queue, pool, check, dev, stop, show):
111
126
  """Run a worker."""
127
+
128
+ # Check Celery addon is installed
112
129
  if not ADDONS_ENABLED['worker']:
113
130
  console.print('[bold red]Missing worker addon: please run [bold green4]secator install addons worker[/][/].')
114
131
  sys.exit(1)
132
+
133
+ # Check broken / backend addon is installed
115
134
  broker_protocol = CONFIG.celery.broker_url.split('://')[0]
116
135
  backend_protocol = CONFIG.celery.result_backend.split('://')[0]
117
136
  if CONFIG.celery.broker_url:
118
137
  if (broker_protocol == 'redis' or backend_protocol == 'redis') and not ADDONS_ENABLED['redis']:
119
138
  console.print('[bold red]Missing `redis` addon: please run [bold green4]secator install addons redis[/][/].')
120
139
  sys.exit(1)
140
+
141
+ # Debug Celery config
121
142
  from secator.celery import app, is_celery_worker_alive
122
- debug('conf', obj=dict(app.conf), obj_breaklines=True, sub='celery.app.conf', level=4)
123
- debug('registered tasks', obj=list(app.tasks.keys()), obj_breaklines=True, sub='celery.tasks', level=4)
143
+ debug('conf', obj=dict(app.conf), obj_breaklines=True, sub='celery.app')
144
+ debug('registered tasks', obj=list(app.tasks.keys()), obj_breaklines=True, sub='celery.app')
145
+
124
146
  if check:
125
147
  is_celery_worker_alive()
126
148
  return
149
+
127
150
  if not queue:
128
151
  queue = 'io,cpu,' + ','.join([r['queue'] for r in app.conf.task_routes.values()])
152
+
129
153
  app_str = 'secator.celery.app'
130
154
  celery = f'{sys.executable} -m celery'
155
+
131
156
  if dev:
132
157
  subcmd = 'stop' if stop else 'show' if show else 'start'
133
158
  logfile = '%n.log'
@@ -138,14 +163,15 @@ def worker(hostname, concurrency, reload, queue, pool, check, dev, stop, show):
138
163
  cmd = f'{celery} -A {app_str} multi {subcmd} 3 {queues} -P {pool} {concur} --logfile={logfile} --pidfile={pidfile}'
139
164
  else:
140
165
  cmd = f'{celery} -A {app_str} worker -n {hostname} -Q {queue}'
141
- if pool:
142
- cmd += f' -P {pool}'
143
- if concurrency:
144
- cmd += f' -c {concurrency}'
166
+
167
+ cmd += f' -P {pool}' if pool else ''
168
+ cmd += f' -c {concurrency}' if concurrency else ''
169
+
145
170
  if reload:
146
171
  patterns = "celery.py;tasks/*.py;runners/*.py;serializers/*.py;output_types/*.py;hooks/*.py;exporters/*.py"
147
172
  cmd = f'watchmedo auto-restart --directory=./ --patterns="{patterns}" --recursive -- {cmd}'
148
- Command.execute(cmd, name='secator worker')
173
+
174
+ Command.execute(cmd, name='secator_worker')
149
175
 
150
176
 
151
177
  #-------#
@@ -170,7 +196,7 @@ def proxy(timeout, number):
170
196
  proxy = FreeProxy(timeout=timeout, rand=True, anonym=True)
171
197
  for _ in range(number):
172
198
  url = proxy.get()
173
- print(url)
199
+ console.print(url)
174
200
 
175
201
 
176
202
  @util.command()
@@ -235,15 +261,14 @@ def revshell(name, host, port, interface, listen, force):
235
261
  console.print('\n'.join(shells_str))
236
262
  else:
237
263
  shell = shell[0]
238
- command = shell['command']
264
+ command = shell['command'].replace('[', '\[')
239
265
  alias = shell['alias']
240
266
  name = shell['name']
241
267
  command_str = Template(command).render(ip=host, port=port, shell='bash')
242
268
  console.print(Rule(f'[bold gold3]{alias}[/] - [bold red]{name} REMOTE SHELL', style='bold red', align='left'))
243
269
  lang = shell.get('lang') or 'sh'
244
270
  if len(command.splitlines()) == 1:
245
- console.print()
246
- print(f'\033[0;36m{command_str}')
271
+ console.print(command_str, style='cyan', highlight=False, soft_wrap=True)
247
272
  else:
248
273
  md = Markdown(f'```{lang}\n{command_str}\n```')
249
274
  console.print(md)
@@ -523,6 +548,41 @@ def config_default(save):
523
548
  # CONFIG.save()
524
549
  # console.print(f'\n[bold green]:tada: Saved config to [/]{CONFIG._path}')
525
550
 
551
+ #-----------#
552
+ # WORKSPACE #
553
+ #-----------#
554
+ @cli.group(aliases=['ws'])
555
+ def workspace():
556
+ """Workspaces."""
557
+ pass
558
+
559
+
560
+ @workspace.command('list')
561
+ def workspace_list():
562
+ """List workspaces."""
563
+ workspaces = {}
564
+ json_reports = []
565
+ for root, _, files in os.walk(CONFIG.dirs.reports):
566
+ for file in files:
567
+ if file.endswith('report.json'):
568
+ path = Path(root) / file
569
+ json_reports.append(path)
570
+ json_reports = sorted(json_reports, key=lambda x: x.stat().st_mtime, reverse=False)
571
+ for path in json_reports:
572
+ ws, runner_type, number = str(path).split('/')[-4:-1]
573
+ if ws not in workspaces:
574
+ workspaces[ws] = {'count': 0, 'path': '/'.join(str(path).split('/')[:-3])}
575
+ workspaces[ws]['count'] += 1
576
+
577
+ # Build table
578
+ table = Table()
579
+ table.add_column("Workspace name", style="bold gold3")
580
+ table.add_column("Run count", overflow='fold')
581
+ table.add_column("Path")
582
+ for workspace, config in workspaces.items():
583
+ table.add_row(workspace, str(config['count']), config['path'])
584
+ console.print(table)
585
+
526
586
 
527
587
  #--------#
528
588
  # REPORT #
@@ -531,54 +591,184 @@ def config_default(save):
531
591
 
532
592
  @cli.group(aliases=['r'])
533
593
  def report():
534
- """View previous reports."""
594
+ """Reports."""
535
595
  pass
536
596
 
537
597
 
538
598
  @report.command('show')
539
- @click.argument('json_path')
540
- @click.option('-o', '--output', type=str, default='console', help='Format')
541
- @click.option('-e', '--exclude-fields', type=str, default='', help='List of fields to exclude (comma-separated)')
542
- def report_show(json_path, output, exclude_fields):
543
- """Show a JSON report."""
544
- with open(json_path, 'r') as f:
545
- report = loads_dataclass(f.read())
546
- results = flatten(list(report['results'].values()))
547
- if output == 'console':
548
- for result in results:
549
- console.print(result)
550
- elif output == 'table':
551
- exclude_fields = exclude_fields.split(',')
552
- print_results_table(
553
- results,
554
- title=report['info']['title'],
555
- exclude_fields=exclude_fields)
556
-
599
+ @click.argument('report_query', required=False)
600
+ @click.option('-o', '--output', type=str, default='console', help='Exporters')
601
+ @click.option('-r', '--runner-type', type=str, default=None, help='Filter by runner type. Choices: task, workflow, scan') # noqa: E501
602
+ @click.option('-d', '--time-delta', type=str, default=None, help='Keep results newer than time delta. E.g: 26m, 1d, 1y') # noqa: E501
603
+ @click.option('-t', '--type', type=str, default='', help=f'Filter by output type. Choices: {FINDING_TYPES_LOWER}')
604
+ @click.option('-q', '--query', type=str, default=None, help='Query results using a Python expression')
605
+ @click.option('-w', '-ws', '--workspace', type=str, default=None, help='Filter by workspace name')
606
+ @click.option('-u', '--unified', is_flag=True, default=False, help='Show unified results (merge reports and de-duplicates results)') # noqa: E501
607
+ def report_show(report_query, output, runner_type, time_delta, type, query, workspace, unified):
608
+ """Show report results and filter on them."""
609
+
610
+ # Get extractors
611
+ otypes = [o.__name__.lower() for o in FINDING_TYPES]
612
+ extractors = []
613
+ if type:
614
+ type = type.split(',')
615
+ for typedef in type:
616
+ if typedef:
617
+ if '.' in typedef:
618
+ _type, _field = tuple(typedef.split('.'))
619
+ else:
620
+ _type = typedef
621
+ _field = None
622
+ extractors.append({
623
+ 'type': _type,
624
+ 'field': _field,
625
+ 'condition': query or 'True'
626
+ })
627
+ elif query:
628
+ query = query.split(';')
629
+ for part in query:
630
+ _type = part.split('.')[0]
631
+ if _type in otypes:
632
+ part = part.replace(_type, 'item')
633
+ extractor = {
634
+ 'type': _type,
635
+ 'condition': part or 'True'
636
+ }
637
+ extractors.append(extractor)
638
+
639
+ # Build runner instance
640
+ current = get_file_timestamp()
641
+ runner = DotMap({
642
+ "config": {
643
+ "name": f"consolidated_report_{current}"
644
+ },
645
+ "name": "runner",
646
+ "workspace_name": "_consolidated",
647
+ "reports_folder": Path.cwd(),
648
+ })
649
+ exporters = Runner.resolve_exporters(output)
557
650
 
558
- @report.command('list')
559
- @click.option('-ws', '--workspace', type=str)
560
- def report_list(workspace):
561
- reports_dir = CONFIG.dirs.reports
562
- json_reports = reports_dir.glob("**/**/report.json")
563
- ws_reports = {}
564
- for path in json_reports:
565
- ws, runner, number = str(path).split('/')[-4:-1]
566
- if ws not in ws_reports:
567
- ws_reports[ws] = []
651
+ # Build report queries from fuzzy input
652
+ paths = []
653
+ if report_query:
654
+ report_query = report_query.split(',')
655
+ else:
656
+ report_query = []
657
+
658
+ # Load all report paths
659
+ load_all_reports = any([not Path(p).exists() for p in report_query])
660
+ all_reports = []
661
+ if load_all_reports:
662
+ all_reports = list_reports(workspace=workspace, type=runner_type, timedelta=human_to_timedelta(time_delta))
663
+ if not report_query:
664
+ report_query = all_reports
665
+
666
+ for query in report_query:
667
+ query = str(query)
668
+ if not query.endswith('/'):
669
+ query += '/'
670
+ path = Path(query)
671
+ if not path.exists():
672
+ matches = []
673
+ for path in all_reports:
674
+ if query in str(path):
675
+ matches.append(path)
676
+ if not matches:
677
+ console.print(
678
+ f'[bold orange3]Query {query} did not return any matches. [/][bold green]Ignoring.[/]')
679
+ paths.extend(matches)
680
+ else:
681
+ paths.append(path)
682
+ paths = sort_files_by_date(paths)
683
+
684
+ # Load reports, extract results
685
+ all_results = []
686
+ for ix, path in enumerate(paths):
687
+ if unified:
688
+ console.print(f'Loading {path} \[[bold yellow4]{ix + 1}[/]/[bold yellow4]{len(paths)}[/]] \[results={len(all_results)}]...') # noqa: E501
568
689
  with open(path, 'r') as f:
690
+ data = loads_dataclass(f.read())
569
691
  try:
570
- content = json.loads(f.read())
571
- data = {'path': path, 'name': content['info']['name'], 'runner': runner}
572
- ws_reports[ws].append(data)
573
- except json.JSONDecodeError as e:
692
+ info = get_info_from_report_path(path)
693
+ runner_type = info.get('type', 'unknowns')[:-1]
694
+ runner.results = flatten(list(data['results'].values()))
695
+ if unified:
696
+ all_results.extend(runner.results)
697
+ continue
698
+ report = Report(runner, title=f"Consolidated report - {current}", exporters=exporters)
699
+ report.build(extractors=extractors if not unified else [])
700
+ file_date = get_file_date(path)
701
+ runner_name = data['info']['name']
702
+ console.print(
703
+ f'\n{path} ([bold blue]{runner_name}[/] [dim]{runner_type}[/]) ([dim]{file_date}[/]):')
704
+ if report.is_empty():
705
+ if len(paths) == 1:
706
+ console.print('[bold orange4]No results in report.[/]')
707
+ else:
708
+ console.print('[bold orange4]No new results since previous scan.[/]')
709
+ continue
710
+ report.send()
711
+ except json.decoder.JSONDecodeError as e:
574
712
  console.print(f'[bold red]Could not load {path}: {str(e)}')
575
713
 
576
- for ws in ws_reports:
577
- if workspace and not ws == workspace:
578
- continue
579
- console.print(f'[bold gold3]{ws}:')
580
- for data in sorted(ws_reports[ws], key=lambda x: x['path']):
581
- console.print(f' • {data["path"]} ([bold blue]{data["name"]}[/] [dim]{data["runner"][:-1]}[/])')
714
+ if unified:
715
+ console.print(f'\n:wrench: [bold gold3]Building report by crunching {len(all_results)} results ...[/]')
716
+ console.print(':coffee: [dim]Note that this can take a while when the result count is high...[/]')
717
+ runner.results = all_results
718
+ report = Report(runner, title=f"Consolidated report - {current}", exporters=exporters)
719
+ report.build(extractors=extractors, dedupe=True)
720
+ report.send()
721
+
722
+
723
+ @report.command('list')
724
+ @click.option('-ws', '-w', '--workspace', type=str)
725
+ @click.option('-r', '--runner-type', type=str, default=None, help='Filter by runner type. Choices: task, workflow, scan') # noqa: E501
726
+ @click.option('-d', '--time-delta', type=str, default=None, help='Keep results newer than time delta. E.g: 26m, 1d, 1y') # noqa: E501
727
+ def report_list(workspace, runner_type, time_delta):
728
+ """List all secator reports."""
729
+ paths = list_reports(workspace=workspace, type=runner_type, timedelta=human_to_timedelta(time_delta))
730
+ paths = sorted(paths, key=lambda x: x.stat().st_mtime, reverse=False)
731
+
732
+ # Build table
733
+ table = Table()
734
+ table.add_column("Workspace", style="bold gold3")
735
+ table.add_column("Path", overflow='fold')
736
+ table.add_column("Name")
737
+ table.add_column("Id")
738
+ table.add_column("Date")
739
+ table.add_column("Status", style="green")
740
+
741
+ # Load each report
742
+ for path in paths:
743
+ try:
744
+ info = get_info_from_report_path(path)
745
+ with open(path, 'r') as f:
746
+ content = json.loads(f.read())
747
+ data = {
748
+ 'workspace': info['workspace'],
749
+ 'name': f"[bold blue]{content['info']['name']}[/]",
750
+ 'status': content['info'].get('status', ''),
751
+ 'id': info['type'] + '/' + info['id'],
752
+ 'date': get_file_date(path), # Assuming get_file_date returns a readable date
753
+ }
754
+ status_color = STATE_COLORS[data['status']] if data['status'] in STATE_COLORS else 'white'
755
+
756
+ # Update table
757
+ table.add_row(
758
+ data['workspace'],
759
+ str(path),
760
+ data['name'],
761
+ data['id'],
762
+ data['date'],
763
+ f"[{status_color}]{data['status']}[/]"
764
+ )
765
+ except json.JSONDecodeError as e:
766
+ console.print(f'[bold red]Could not load {path}: {str(e)}')
767
+
768
+ if len(paths) > 0:
769
+ console.print(table)
770
+ else:
771
+ console.print('[bold red]No results found.')
582
772
 
583
773
 
584
774
  @report.command('export')
@@ -588,7 +778,6 @@ def report_list(workspace):
588
778
  def report_export(json_path, output_folder, output):
589
779
  with open(json_path, 'r') as f:
590
780
  data = loads_dataclass(f.read())
591
- flatten(list(data['results'].values()))
592
781
 
593
782
  runner_instance = DotMap({
594
783
  "config": {
@@ -652,14 +841,13 @@ def health(json, debug):
652
841
  console.print('\n:wrench: [bold gold3]Checking installed addons ...[/]')
653
842
  table = get_health_table()
654
843
  with Live(table, console=console):
655
- for addon in ['worker', 'google', 'mongodb', 'redis', 'dev', 'trace', 'build']:
656
- addon_var = ADDONS_ENABLED[addon]
844
+ for addon, installed in ADDONS_ENABLED.items():
657
845
  info = {
658
846
  'name': addon,
659
847
  'version': None,
660
- 'status': 'ok' if addon_var else 'missing',
848
+ 'status': 'ok' if installed else 'missing',
661
849
  'latest_version': None,
662
- 'installed': addon_var,
850
+ 'installed': installed,
663
851
  'location': None
664
852
  }
665
853
  row = fmt_health_table_row(info, 'addons')
@@ -705,11 +893,11 @@ def run_install(cmd, title, next_steps=None):
705
893
  console.print('[bold red]Cannot run this command in offline mode.[/]')
706
894
  return
707
895
  with console.status(f'[bold yellow] Installing {title}...'):
708
- ret = Command.execute(cmd, cls_attributes={'shell': True}, print_cmd=True, print_line=True)
896
+ ret = Command.execute(cmd, cls_attributes={'shell': True}, print_line=True)
709
897
  if ret.return_code != 0:
710
898
  console.print(f':exclamation_mark: Failed to install {title}.', style='bold red')
711
899
  else:
712
- console.print(f':tada: {title.capitalize()} installed successfully !', style='bold green')
900
+ console.print(f':tada: {title} installed successfully !', style='bold green')
713
901
  if next_steps:
714
902
  console.print('[bold gold3]:wrench: Next steps:[/]')
715
903
  for ix, step in enumerate(next_steps):
@@ -731,10 +919,10 @@ def addons():
731
919
 
732
920
  @addons.command('worker')
733
921
  def install_worker():
734
- "Install worker addon."
922
+ "Install Celery worker addon."
735
923
  run_install(
736
924
  cmd=f'{sys.executable} -m pip install secator[worker]',
737
- title='worker addon',
925
+ title='Celery worker addon',
738
926
  next_steps=[
739
927
  'Run [bold green4]secator worker[/] to run a Celery worker using the file system as a backend and broker.',
740
928
  'Run [bold green4]secator x httpx testphp.vulnweb.com[/] to admire your task running in a worker.',
@@ -743,26 +931,38 @@ def install_worker():
743
931
  )
744
932
 
745
933
 
746
- @addons.command('google')
747
- def install_google():
748
- "Install google addon."
934
+ @addons.command('gdrive')
935
+ def install_gdrive():
936
+ "Install Google Drive addon."
749
937
  run_install(
750
938
  cmd=f'{sys.executable} -m pip install secator[google]',
751
- title='google addon',
939
+ title='Google Drive addon',
752
940
  next_steps=[
753
- 'Run [bold green4]secator config set addons.google.credentials_path <VALUE>[/].',
754
- 'Run [bold green4]secator config set addons.google.drive_parent_folder_id <VALUE>[/].',
941
+ 'Run [bold green4]secator config set addons.gdrive.credentials_path <VALUE>[/].',
942
+ 'Run [bold green4]secator config set addons.gdrive.drive_parent_folder_id <VALUE>[/].',
755
943
  'Run [bold green4]secator x httpx testphp.vulnweb.com -o gdrive[/] to send reports to Google Drive.'
756
944
  ]
757
945
  )
758
946
 
759
947
 
948
+ @addons.command('gcs')
949
+ def install_gcs():
950
+ "Install Google Cloud Storage addon."
951
+ run_install(
952
+ cmd=f'{sys.executable} -m pip install secator[gcs]',
953
+ title='Google Cloud Storage addon',
954
+ next_steps=[
955
+ 'Run [bold green4]secator config set addons.gcs.credentials_path <VALUE>[/].',
956
+ ]
957
+ )
958
+
959
+
760
960
  @addons.command('mongodb')
761
961
  def install_mongodb():
762
- "Install mongodb addon."
962
+ "Install MongoDB addon."
763
963
  run_install(
764
964
  cmd=f'{sys.executable} -m pip install secator[mongodb]',
765
- title='mongodb addon',
965
+ title='MongoDB addon',
766
966
  next_steps=[
767
967
  '[dim]\[optional][/] Run [bold green4]docker run --name mongo -p 27017:27017 -d mongo:latest[/] to run a local MongoDB instance.', # noqa: E501
768
968
  'Run [bold green4]secator config set addons.mongodb.url mongodb://<URL>[/].',
@@ -773,10 +973,10 @@ def install_mongodb():
773
973
 
774
974
  @addons.command('redis')
775
975
  def install_redis():
776
- "Install redis addon."
976
+ "Install Redis addon."
777
977
  run_install(
778
978
  cmd=f'{sys.executable} -m pip install secator[redis]',
779
- title='redis addon',
979
+ title='Redis addon',
780
980
  next_steps=[
781
981
  '[dim]\[optional][/] Run [bold green4]docker run --name redis -p 6379:6379 -d redis[/] to run a local Redis instance.', # noqa: E501
782
982
  'Run [bold green4]secator config set celery.broker_url redis://<URL>[/]',
@@ -806,7 +1006,7 @@ def install_trace():
806
1006
  "Install trace addon."
807
1007
  run_install(
808
1008
  cmd=f'{sys.executable} -m pip install secator[trace]',
809
- title='dev addon',
1009
+ title='trace addon',
810
1010
  next_steps=[
811
1011
  ]
812
1012
  )
@@ -1053,23 +1253,22 @@ def lint():
1053
1253
  @click.option('--workflows', type=str, default='', help='Secator workflows to test (comma-separated)')
1054
1254
  @click.option('--scans', type=str, default='', help='Secator scans to test (comma-separated)')
1055
1255
  @click.option('--test', '-t', type=str, help='Secator test to run')
1056
- @click.option('--debug', '-d', type=int, default=0, help='Add debug information')
1057
- def unit(tasks, workflows, scans, test, debug=False):
1256
+ def unit(tasks, workflows, scans, test):
1058
1257
  """Run unit tests."""
1059
1258
  os.environ['TEST_TASKS'] = tasks or ''
1060
1259
  os.environ['TEST_WORKFLOWS'] = workflows or ''
1061
1260
  os.environ['TEST_SCANS'] = scans or ''
1062
- os.environ['SECATOR_DEBUG_LEVEL'] = str(debug)
1261
+ os.environ['SECATOR_DIRS_DATA'] = '/tmp/.secator'
1262
+ os.environ['SECATOR_OFFLINE_MODE'] = "1"
1063
1263
  os.environ['SECATOR_HTTP_STORE_RESPONSES'] = '0'
1064
1264
  os.environ['SECATOR_RUNNERS_SKIP_CVE_SEARCH'] = '1'
1065
1265
 
1066
- cmd = f'{sys.executable} -m coverage run --omit="*test*" -m unittest'
1266
+ import shutil
1267
+ shutil.rmtree('/tmp/.secator', ignore_errors=True)
1268
+ cmd = f'{sys.executable} -m coverage run --omit="*test*" --data-file=.coverage.unit -m pytest -s -v tests/unit'
1067
1269
  if test:
1068
- if not test.startswith('tests.unit'):
1069
- test = f'tests.unit.{test}'
1070
- cmd += f' {test}'
1071
- else:
1072
- cmd += ' discover -v tests.unit'
1270
+ test_str = ' or '.join(test.split(','))
1271
+ cmd += f' -k "{test_str}"'
1073
1272
  run_test(cmd, 'unit')
1074
1273
 
1075
1274
 
@@ -1078,35 +1277,57 @@ def unit(tasks, workflows, scans, test, debug=False):
1078
1277
  @click.option('--workflows', type=str, default='', help='Secator workflows to test (comma-separated)')
1079
1278
  @click.option('--scans', type=str, default='', help='Secator scans to test (comma-separated)')
1080
1279
  @click.option('--test', '-t', type=str, help='Secator test to run')
1081
- @click.option('--debug', '-d', type=int, default=0, help='Add debug information')
1082
- def integration(tasks, workflows, scans, test, debug):
1280
+ def integration(tasks, workflows, scans, test):
1083
1281
  """Run integration tests."""
1084
1282
  os.environ['TEST_TASKS'] = tasks or ''
1085
1283
  os.environ['TEST_WORKFLOWS'] = workflows or ''
1086
1284
  os.environ['TEST_SCANS'] = scans or ''
1087
- os.environ['SECATOR_DEBUG_LEVEL'] = str(debug)
1285
+ os.environ['SECATOR_DIRS_DATA'] = '/tmp/.secator'
1088
1286
  os.environ['SECATOR_RUNNERS_SKIP_CVE_SEARCH'] = '1'
1089
- os.environ['SECATOR_DIRS_DATA'] = '/tmp/data'
1090
- os.environ['SECATOR_DIRS_REPORTS'] = '/tmp/data/reports'
1091
- os.environ['SECATOR_DIRS_CELERY'] = '/tmp/celery'
1092
- os.environ['SECATOR_DIRS_CELERY_DATA'] = '/tmp/celery/data'
1093
- os.environ['SECATOR_DIRS_CELERY_RESULTS'] = '/tmp/celery/results'
1287
+
1094
1288
  import shutil
1095
- for path in ['/tmp/data', '/tmp/celery', '/tmp/celery/data', '/tmp/celery/results']:
1096
- shutil.rmtree(path, ignore_errors=True)
1289
+ shutil.rmtree('/tmp/.secator', ignore_errors=True)
1097
1290
 
1098
- cmd = f'{sys.executable} -m unittest'
1291
+ cmd = f'{sys.executable} -m coverage run --omit="*test*" --data-file=.coverage.integration -m pytest -s -v tests/integration' # noqa: E501
1099
1292
  if test:
1100
- if not test.startswith('tests.integration'):
1101
- test = f'tests.integration.{test}'
1102
- cmd += f' {test}'
1103
- else:
1104
- cmd += ' discover -v tests.integration'
1293
+ test_str = ' or '.join(test.split(','))
1294
+ cmd += f' -k "{test_str}"'
1105
1295
  run_test(cmd, 'integration')
1106
1296
 
1107
1297
 
1108
1298
  @test.command()
1109
- def coverage():
1110
- """Run coverage report."""
1299
+ @click.option('--tasks', type=str, default='', help='Secator tasks to test (comma-separated)')
1300
+ @click.option('--workflows', type=str, default='', help='Secator workflows to test (comma-separated)')
1301
+ @click.option('--scans', type=str, default='', help='Secator scans to test (comma-separated)')
1302
+ @click.option('--test', '-t', type=str, help='Secator test to run')
1303
+ def performance(tasks, workflows, scans, test):
1304
+ """Run integration tests."""
1305
+ os.environ['TEST_TASKS'] = tasks or ''
1306
+ os.environ['TEST_WORKFLOWS'] = workflows or ''
1307
+ os.environ['TEST_SCANS'] = scans or ''
1308
+ os.environ['SECATOR_DIRS_DATA'] = '/tmp/.secator'
1309
+ os.environ['SECATOR_RUNNERS_SKIP_CVE_SEARCH'] = '1'
1310
+
1311
+ # import shutil
1312
+ # shutil.rmtree('/tmp/.secator', ignore_errors=True)
1313
+
1314
+ cmd = f'{sys.executable} -m coverage run --omit="*test*" --data-file=.coverage.performance -m pytest -s -v tests/performance' # noqa: E501
1315
+ if test:
1316
+ test_str = ' or '.join(test.split(','))
1317
+ cmd += f' -k "{test_str}"'
1318
+ run_test(cmd, 'performance')
1319
+
1320
+
1321
+ @test.command()
1322
+ @click.option('--unit-only', '-u', is_flag=True, default=False, help='Only generate coverage for unit tests')
1323
+ @click.option('--integration-only', '-i', is_flag=True, default=False, help='Only generate coverage for integration tests') # noqa: E501
1324
+ def coverage(unit_only, integration_only):
1325
+ """Run coverage combine + coverage report."""
1111
1326
  cmd = f'{sys.executable} -m coverage report -m --omit=*/site-packages/*,*/tests/*,*/templates/*'
1327
+ if unit_only:
1328
+ cmd += ' --data-file=.coverage.unit'
1329
+ elif integration_only:
1330
+ cmd += ' --data-file=.coverage.integration'
1331
+ else:
1332
+ Command.execute(f'{sys.executable} -m coverage combine --keep', name='coverage combine', cwd=ROOT_FOLDER)
1112
1333
  run_test(cmd, 'coverage')