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.
- secator/celery.py +160 -185
- secator/celery_utils.py +268 -0
- secator/cli.py +327 -106
- secator/config.py +27 -11
- secator/configs/workflows/host_recon.yaml +5 -3
- secator/configs/workflows/port_scan.yaml +7 -3
- secator/configs/workflows/url_bypass.yaml +10 -0
- secator/configs/workflows/url_vuln.yaml +1 -1
- secator/decorators.py +169 -92
- secator/definitions.py +10 -3
- secator/exporters/__init__.py +7 -5
- secator/exporters/console.py +10 -0
- secator/exporters/csv.py +27 -19
- secator/exporters/gdrive.py +16 -11
- secator/exporters/json.py +3 -1
- secator/exporters/table.py +30 -2
- secator/exporters/txt.py +20 -16
- secator/hooks/gcs.py +53 -0
- secator/hooks/mongodb.py +53 -27
- secator/output_types/__init__.py +29 -11
- secator/output_types/_base.py +11 -1
- secator/output_types/error.py +36 -0
- secator/output_types/exploit.py +1 -1
- secator/output_types/info.py +24 -0
- secator/output_types/ip.py +7 -0
- secator/output_types/port.py +8 -1
- secator/output_types/progress.py +5 -0
- secator/output_types/record.py +3 -1
- secator/output_types/stat.py +33 -0
- secator/output_types/tag.py +6 -4
- secator/output_types/url.py +6 -3
- secator/output_types/vulnerability.py +3 -2
- secator/output_types/warning.py +24 -0
- secator/report.py +55 -23
- secator/rich.py +44 -39
- secator/runners/_base.py +622 -635
- secator/runners/_helpers.py +5 -91
- secator/runners/celery.py +18 -0
- secator/runners/command.py +364 -211
- secator/runners/scan.py +8 -24
- secator/runners/task.py +21 -55
- secator/runners/workflow.py +41 -40
- secator/scans/__init__.py +28 -0
- secator/serializers/dataclass.py +6 -0
- secator/serializers/json.py +10 -5
- secator/serializers/regex.py +12 -4
- secator/tasks/_categories.py +5 -2
- secator/tasks/bbot.py +293 -0
- secator/tasks/bup.py +98 -0
- secator/tasks/cariddi.py +38 -49
- secator/tasks/dalfox.py +3 -0
- secator/tasks/dirsearch.py +12 -23
- secator/tasks/dnsx.py +49 -30
- secator/tasks/dnsxbrute.py +2 -0
- secator/tasks/feroxbuster.py +8 -17
- secator/tasks/ffuf.py +3 -2
- secator/tasks/fping.py +3 -3
- secator/tasks/gau.py +5 -0
- secator/tasks/gf.py +2 -2
- secator/tasks/gospider.py +4 -0
- secator/tasks/grype.py +9 -9
- secator/tasks/h8mail.py +31 -41
- secator/tasks/httpx.py +58 -21
- secator/tasks/katana.py +18 -22
- secator/tasks/maigret.py +26 -24
- secator/tasks/mapcidr.py +2 -3
- secator/tasks/msfconsole.py +4 -16
- secator/tasks/naabu.py +3 -1
- secator/tasks/nmap.py +50 -35
- secator/tasks/nuclei.py +9 -2
- secator/tasks/searchsploit.py +17 -9
- secator/tasks/subfinder.py +5 -1
- secator/tasks/wpscan.py +79 -93
- secator/template.py +61 -45
- secator/thread.py +24 -0
- secator/utils.py +330 -80
- secator/utils_test.py +48 -23
- secator/workflows/__init__.py +28 -0
- {secator-0.6.0.dist-info → secator-0.7.0.dist-info}/METADATA +11 -5
- secator-0.7.0.dist-info/RECORD +115 -0
- {secator-0.6.0.dist-info → secator-0.7.0.dist-info}/WHEEL +1 -1
- secator-0.6.0.dist-info/RECORD +0 -101
- {secator-0.6.0.dist-info → secator-0.7.0.dist-info}/entry_points.txt +0 -0
- {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.
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
123
|
-
debug('registered tasks', obj=list(app.tasks.keys()), obj_breaklines=True, sub='celery.
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
if concurrency
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
594
|
+
"""Reports."""
|
|
535
595
|
pass
|
|
536
596
|
|
|
537
597
|
|
|
538
598
|
@report.command('show')
|
|
539
|
-
@click.argument('
|
|
540
|
-
@click.option('-o', '--output', type=str, default='console', help='
|
|
541
|
-
@click.option('-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
|
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
|
|
848
|
+
'status': 'ok' if installed else 'missing',
|
|
661
849
|
'latest_version': None,
|
|
662
|
-
'installed':
|
|
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},
|
|
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
|
|
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('
|
|
747
|
-
def
|
|
748
|
-
"Install
|
|
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='
|
|
939
|
+
title='Google Drive addon',
|
|
752
940
|
next_steps=[
|
|
753
|
-
'Run [bold green4]secator config set addons.
|
|
754
|
-
'Run [bold green4]secator config set addons.
|
|
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
|
|
962
|
+
"Install MongoDB addon."
|
|
763
963
|
run_install(
|
|
764
964
|
cmd=f'{sys.executable} -m pip install secator[mongodb]',
|
|
765
|
-
title='
|
|
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
|
|
976
|
+
"Install Redis addon."
|
|
777
977
|
run_install(
|
|
778
978
|
cmd=f'{sys.executable} -m pip install secator[redis]',
|
|
779
|
-
title='
|
|
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='
|
|
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
|
-
|
|
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['
|
|
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
|
-
|
|
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
|
-
|
|
1069
|
-
|
|
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
|
-
|
|
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['
|
|
1285
|
+
os.environ['SECATOR_DIRS_DATA'] = '/tmp/.secator'
|
|
1088
1286
|
os.environ['SECATOR_RUNNERS_SKIP_CVE_SEARCH'] = '1'
|
|
1089
|
-
|
|
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
|
-
|
|
1096
|
-
shutil.rmtree(path, ignore_errors=True)
|
|
1289
|
+
shutil.rmtree('/tmp/.secator', ignore_errors=True)
|
|
1097
1290
|
|
|
1098
|
-
cmd = f'{sys.executable} -m
|
|
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
|
-
|
|
1101
|
-
|
|
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
|
-
|
|
1110
|
-
|
|
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')
|