secator 0.6.0__py3-none-any.whl → 0.8.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 +427 -176
- secator/config.py +114 -68
- secator/configs/workflows/host_recon.yaml +5 -3
- secator/configs/workflows/port_scan.yaml +7 -3
- secator/configs/workflows/subdomain_recon.yaml +2 -2
- secator/configs/workflows/url_bypass.yaml +10 -0
- secator/configs/workflows/url_dirsearch.yaml +1 -1
- secator/configs/workflows/url_vuln.yaml +1 -1
- secator/decorators.py +170 -92
- secator/definitions.py +11 -4
- 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/installer.py +277 -60
- 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 +12 -8
- secator/output_types/info.py +24 -0
- secator/output_types/ip.py +8 -1
- secator/output_types/port.py +9 -2
- secator/output_types/progress.py +5 -0
- secator/output_types/record.py +5 -3
- secator/output_types/stat.py +33 -0
- secator/output_types/subdomain.py +1 -1
- secator/output_types/tag.py +8 -6
- secator/output_types/target.py +2 -2
- secator/output_types/url.py +14 -11
- secator/output_types/user_account.py +6 -6
- secator/output_types/vulnerability.py +8 -6
- secator/output_types/warning.py +24 -0
- secator/report.py +56 -23
- secator/rich.py +44 -39
- secator/runners/_base.py +629 -638
- secator/runners/_helpers.py +5 -91
- secator/runners/celery.py +18 -0
- secator/runners/command.py +404 -214
- 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 +147 -42
- secator/tasks/bbot.py +295 -0
- secator/tasks/bup.py +99 -0
- secator/tasks/cariddi.py +38 -49
- secator/tasks/dalfox.py +3 -0
- secator/tasks/dirsearch.py +14 -25
- secator/tasks/dnsx.py +49 -30
- secator/tasks/dnsxbrute.py +4 -1
- secator/tasks/feroxbuster.py +10 -20
- secator/tasks/ffuf.py +3 -2
- secator/tasks/fping.py +4 -4
- secator/tasks/gau.py +5 -0
- secator/tasks/gf.py +2 -2
- secator/tasks/gospider.py +4 -0
- secator/tasks/grype.py +11 -13
- secator/tasks/h8mail.py +32 -42
- secator/tasks/httpx.py +58 -21
- secator/tasks/katana.py +19 -23
- secator/tasks/maigret.py +27 -25
- secator/tasks/mapcidr.py +2 -3
- secator/tasks/msfconsole.py +22 -19
- secator/tasks/naabu.py +18 -2
- secator/tasks/nmap.py +82 -55
- secator/tasks/nuclei.py +13 -3
- secator/tasks/searchsploit.py +26 -11
- secator/tasks/subfinder.py +5 -1
- secator/tasks/wpscan.py +91 -94
- secator/template.py +61 -45
- secator/thread.py +24 -0
- secator/utils.py +417 -78
- secator/utils_test.py +48 -23
- secator/workflows/__init__.py +28 -0
- {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/METADATA +59 -48
- secator-0.8.0.dist-info/RECORD +115 -0
- {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/WHEEL +1 -1
- secator-0.6.0.dist-info/RECORD +0 -101
- {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/entry_points.txt +0 -0
- {secator-0.6.0.dist-info → secator-0.8.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, Info, Error
|
|
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
|
#-------#
|
|
@@ -165,12 +191,12 @@ def util():
|
|
|
165
191
|
def proxy(timeout, number):
|
|
166
192
|
"""Get random proxies from FreeProxy."""
|
|
167
193
|
if CONFIG.offline_mode:
|
|
168
|
-
console.print('
|
|
194
|
+
console.print(Error(message='Cannot run this command in offline mode.'))
|
|
169
195
|
return
|
|
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()
|
|
@@ -185,18 +211,16 @@ def revshell(name, host, port, interface, listen, force):
|
|
|
185
211
|
if host is None: # detect host automatically
|
|
186
212
|
host = detect_host(interface)
|
|
187
213
|
if not host:
|
|
188
|
-
console.print(
|
|
189
|
-
f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of available interfaces.',
|
|
190
|
-
style='bold red')
|
|
214
|
+
console.print(Error(message=f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of available interfaces')) # noqa: E501
|
|
191
215
|
return
|
|
192
216
|
else:
|
|
193
|
-
console.print(f'
|
|
217
|
+
console.print(Info(message=f'Detected host IP: [bold orange1]{host}[/]'))
|
|
194
218
|
|
|
195
219
|
# Download reverse shells JSON from repo
|
|
196
220
|
revshells_json = f'{CONFIG.dirs.revshells}/revshells.json'
|
|
197
221
|
if not os.path.exists(revshells_json) or force:
|
|
198
222
|
if CONFIG.offline_mode:
|
|
199
|
-
console.print('
|
|
223
|
+
console.print(Error(message='Cannot run this command in offline mode'))
|
|
200
224
|
return
|
|
201
225
|
ret = Command.execute(
|
|
202
226
|
f'wget https://raw.githubusercontent.com/freelabz/secator/main/scripts/revshells.json && mv revshells.json {CONFIG.dirs.revshells}', # noqa: E501
|
|
@@ -235,15 +259,14 @@ def revshell(name, host, port, interface, listen, force):
|
|
|
235
259
|
console.print('\n'.join(shells_str))
|
|
236
260
|
else:
|
|
237
261
|
shell = shell[0]
|
|
238
|
-
command = shell['command']
|
|
262
|
+
command = shell['command'].replace('[', r'\[')
|
|
239
263
|
alias = shell['alias']
|
|
240
264
|
name = shell['name']
|
|
241
265
|
command_str = Template(command).render(ip=host, port=port, shell='bash')
|
|
242
266
|
console.print(Rule(f'[bold gold3]{alias}[/] - [bold red]{name} REMOTE SHELL', style='bold red', align='left'))
|
|
243
267
|
lang = shell.get('lang') or 'sh'
|
|
244
268
|
if len(command.splitlines()) == 1:
|
|
245
|
-
console.print()
|
|
246
|
-
print(f'\033[0;36m{command_str}')
|
|
269
|
+
console.print(command_str, style='cyan', highlight=False, soft_wrap=True)
|
|
247
270
|
else:
|
|
248
271
|
md = Markdown(f'```{lang}\n{command_str}\n```')
|
|
249
272
|
console.print(md)
|
|
@@ -252,7 +275,7 @@ def revshell(name, host, port, interface, listen, force):
|
|
|
252
275
|
console.print(Rule(style='bold red'))
|
|
253
276
|
|
|
254
277
|
if listen:
|
|
255
|
-
console.print(f'Starting netcat listener on port {port} ...'
|
|
278
|
+
console.print(Info(message=f'Starting netcat listener on port {port} ...'))
|
|
256
279
|
cmd = f'nc -lvnp {port}'
|
|
257
280
|
Command.execute(cmd)
|
|
258
281
|
|
|
@@ -270,9 +293,7 @@ def serve(directory, host, port, interface):
|
|
|
270
293
|
if not host:
|
|
271
294
|
host = detect_host(interface)
|
|
272
295
|
if not host:
|
|
273
|
-
console.print(
|
|
274
|
-
f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of interfaces.',
|
|
275
|
-
style='bold red')
|
|
296
|
+
console.print(Error(message=f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of interfaces.')) # noqa: E501
|
|
276
297
|
return
|
|
277
298
|
console.print(f'{fname} [dim][/]', style='bold magenta')
|
|
278
299
|
console.print(f'wget http://{host}:{port}/{fname}', style='dim italic')
|
|
@@ -312,9 +333,9 @@ def record(record_name, script, interactive, width, height, output_dir):
|
|
|
312
333
|
# If existing cast file, remove it
|
|
313
334
|
if os.path.exists(output_cast_path):
|
|
314
335
|
os.unlink(output_cast_path)
|
|
315
|
-
console.print(f'Removed existing {output_cast_path}'
|
|
336
|
+
console.print(Info(message=f'Removed existing {output_cast_path}'))
|
|
316
337
|
|
|
317
|
-
with console.status('
|
|
338
|
+
with console.status(Info(message='Recording with asciinema ...')):
|
|
318
339
|
Command.execute(
|
|
319
340
|
f'asciinema-automation -aa "-c /bin/sh" {script} {output_cast_path} --timeout 200',
|
|
320
341
|
cls_attributes=attrs,
|
|
@@ -365,14 +386,14 @@ def record(record_name, script, interactive, width, height, output_dir):
|
|
|
365
386
|
f'agg {output_cast_path} {output_gif_path}',
|
|
366
387
|
cls_attributes=attrs,
|
|
367
388
|
)
|
|
368
|
-
console.print(f'Generated {output_gif_path}'
|
|
389
|
+
console.print(Info(message=f'Generated {output_gif_path}'))
|
|
369
390
|
|
|
370
391
|
|
|
371
392
|
@util.group('build')
|
|
372
393
|
def build():
|
|
373
394
|
"""Build secator."""
|
|
374
395
|
if not DEV_PACKAGE:
|
|
375
|
-
console.print('
|
|
396
|
+
console.print(Error(message='You MUST use a development version of secator to make builds'))
|
|
376
397
|
sys.exit(1)
|
|
377
398
|
pass
|
|
378
399
|
|
|
@@ -381,7 +402,7 @@ def build():
|
|
|
381
402
|
def build_pypi():
|
|
382
403
|
"""Build secator PyPI package."""
|
|
383
404
|
if not ADDONS_ENABLED['build']:
|
|
384
|
-
console.print('
|
|
405
|
+
console.print(Error(message='Missing build addon: please run [bold green4]secator install addons build[/]'))
|
|
385
406
|
sys.exit(1)
|
|
386
407
|
with console.status('[bold gold3]Building PyPI package...[/]'):
|
|
387
408
|
ret = Command.execute(f'{sys.executable} -m hatch build', name='hatch build', cwd=ROOT_FOLDER)
|
|
@@ -523,6 +544,41 @@ def config_default(save):
|
|
|
523
544
|
# CONFIG.save()
|
|
524
545
|
# console.print(f'\n[bold green]:tada: Saved config to [/]{CONFIG._path}')
|
|
525
546
|
|
|
547
|
+
#-----------#
|
|
548
|
+
# WORKSPACE #
|
|
549
|
+
#-----------#
|
|
550
|
+
@cli.group(aliases=['ws'])
|
|
551
|
+
def workspace():
|
|
552
|
+
"""Workspaces."""
|
|
553
|
+
pass
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
@workspace.command('list')
|
|
557
|
+
def workspace_list():
|
|
558
|
+
"""List workspaces."""
|
|
559
|
+
workspaces = {}
|
|
560
|
+
json_reports = []
|
|
561
|
+
for root, _, files in os.walk(CONFIG.dirs.reports):
|
|
562
|
+
for file in files:
|
|
563
|
+
if file.endswith('report.json'):
|
|
564
|
+
path = Path(root) / file
|
|
565
|
+
json_reports.append(path)
|
|
566
|
+
json_reports = sorted(json_reports, key=lambda x: x.stat().st_mtime, reverse=False)
|
|
567
|
+
for path in json_reports:
|
|
568
|
+
ws, runner_type, number = str(path).split('/')[-4:-1]
|
|
569
|
+
if ws not in workspaces:
|
|
570
|
+
workspaces[ws] = {'count': 0, 'path': '/'.join(str(path).split('/')[:-3])}
|
|
571
|
+
workspaces[ws]['count'] += 1
|
|
572
|
+
|
|
573
|
+
# Build table
|
|
574
|
+
table = Table()
|
|
575
|
+
table.add_column("Workspace name", style="bold gold3")
|
|
576
|
+
table.add_column("Run count", overflow='fold')
|
|
577
|
+
table.add_column("Path")
|
|
578
|
+
for workspace, config in workspaces.items():
|
|
579
|
+
table.add_row(workspace, str(config['count']), config['path'])
|
|
580
|
+
console.print(table)
|
|
581
|
+
|
|
526
582
|
|
|
527
583
|
#--------#
|
|
528
584
|
# REPORT #
|
|
@@ -531,54 +587,184 @@ def config_default(save):
|
|
|
531
587
|
|
|
532
588
|
@cli.group(aliases=['r'])
|
|
533
589
|
def report():
|
|
534
|
-
"""
|
|
590
|
+
"""Reports."""
|
|
535
591
|
pass
|
|
536
592
|
|
|
537
593
|
|
|
538
594
|
@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
|
-
|
|
595
|
+
@click.argument('report_query', required=False)
|
|
596
|
+
@click.option('-o', '--output', type=str, default='console', help='Exporters')
|
|
597
|
+
@click.option('-r', '--runner-type', type=str, default=None, help='Filter by runner type. Choices: task, workflow, scan') # noqa: E501
|
|
598
|
+
@click.option('-d', '--time-delta', type=str, default=None, help='Keep results newer than time delta. E.g: 26m, 1d, 1y') # noqa: E501
|
|
599
|
+
@click.option('-t', '--type', type=str, default='', help=f'Filter by output type. Choices: {FINDING_TYPES_LOWER}')
|
|
600
|
+
@click.option('-q', '--query', type=str, default=None, help='Query results using a Python expression')
|
|
601
|
+
@click.option('-w', '-ws', '--workspace', type=str, default=None, help='Filter by workspace name')
|
|
602
|
+
@click.option('-u', '--unified', is_flag=True, default=False, help='Show unified results (merge reports and de-duplicates results)') # noqa: E501
|
|
603
|
+
def report_show(report_query, output, runner_type, time_delta, type, query, workspace, unified):
|
|
604
|
+
"""Show report results and filter on them."""
|
|
605
|
+
|
|
606
|
+
# Get extractors
|
|
607
|
+
otypes = [o.__name__.lower() for o in FINDING_TYPES]
|
|
608
|
+
extractors = []
|
|
609
|
+
if type:
|
|
610
|
+
type = type.split(',')
|
|
611
|
+
for typedef in type:
|
|
612
|
+
if typedef:
|
|
613
|
+
if '.' in typedef:
|
|
614
|
+
_type, _field = tuple(typedef.split('.'))
|
|
615
|
+
else:
|
|
616
|
+
_type = typedef
|
|
617
|
+
_field = None
|
|
618
|
+
extractors.append({
|
|
619
|
+
'type': _type,
|
|
620
|
+
'field': _field,
|
|
621
|
+
'condition': query or 'True'
|
|
622
|
+
})
|
|
623
|
+
elif query:
|
|
624
|
+
query = query.split(';')
|
|
625
|
+
for part in query:
|
|
626
|
+
_type = part.split('.')[0]
|
|
627
|
+
if _type in otypes:
|
|
628
|
+
part = part.replace(_type, 'item')
|
|
629
|
+
extractor = {
|
|
630
|
+
'type': _type,
|
|
631
|
+
'condition': part or 'True'
|
|
632
|
+
}
|
|
633
|
+
extractors.append(extractor)
|
|
634
|
+
|
|
635
|
+
# Build runner instance
|
|
636
|
+
current = get_file_timestamp()
|
|
637
|
+
runner = DotMap({
|
|
638
|
+
"config": {
|
|
639
|
+
"name": f"consolidated_report_{current}"
|
|
640
|
+
},
|
|
641
|
+
"name": "runner",
|
|
642
|
+
"workspace_name": "_consolidated",
|
|
643
|
+
"reports_folder": Path.cwd(),
|
|
644
|
+
})
|
|
645
|
+
exporters = Runner.resolve_exporters(output)
|
|
557
646
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
647
|
+
# Build report queries from fuzzy input
|
|
648
|
+
paths = []
|
|
649
|
+
if report_query:
|
|
650
|
+
report_query = report_query.split(',')
|
|
651
|
+
else:
|
|
652
|
+
report_query = []
|
|
653
|
+
|
|
654
|
+
# Load all report paths
|
|
655
|
+
load_all_reports = any([not Path(p).exists() for p in report_query])
|
|
656
|
+
all_reports = []
|
|
657
|
+
if load_all_reports or workspace:
|
|
658
|
+
all_reports = list_reports(workspace=workspace, type=runner_type, timedelta=human_to_timedelta(time_delta))
|
|
659
|
+
if not report_query:
|
|
660
|
+
report_query = all_reports
|
|
661
|
+
|
|
662
|
+
for query in report_query:
|
|
663
|
+
query = str(query)
|
|
664
|
+
if not query.endswith('/'):
|
|
665
|
+
query += '/'
|
|
666
|
+
path = Path(query)
|
|
667
|
+
if not path.exists():
|
|
668
|
+
matches = []
|
|
669
|
+
for path in all_reports:
|
|
670
|
+
if query in str(path):
|
|
671
|
+
matches.append(path)
|
|
672
|
+
if not matches:
|
|
673
|
+
console.print(
|
|
674
|
+
f'[bold orange3]Query {query} did not return any matches. [/][bold green]Ignoring.[/]')
|
|
675
|
+
paths.extend(matches)
|
|
676
|
+
else:
|
|
677
|
+
paths.append(path)
|
|
678
|
+
paths = sort_files_by_date(paths)
|
|
679
|
+
|
|
680
|
+
# Load reports, extract results
|
|
681
|
+
all_results = []
|
|
682
|
+
for ix, path in enumerate(paths):
|
|
683
|
+
if unified:
|
|
684
|
+
console.print(rf'Loading {path} \[[bold yellow4]{ix + 1}[/]/[bold yellow4]{len(paths)}[/]] \[results={len(all_results)}]...') # noqa: E501
|
|
568
685
|
with open(path, 'r') as f:
|
|
686
|
+
data = loads_dataclass(f.read())
|
|
569
687
|
try:
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
688
|
+
info = get_info_from_report_path(path)
|
|
689
|
+
runner_type = info.get('type', 'unknowns')[:-1]
|
|
690
|
+
runner.results = flatten(list(data['results'].values()))
|
|
691
|
+
if unified:
|
|
692
|
+
all_results.extend(runner.results)
|
|
693
|
+
continue
|
|
694
|
+
report = Report(runner, title=f"Consolidated report - {current}", exporters=exporters)
|
|
695
|
+
report.build(extractors=extractors if not unified else [])
|
|
696
|
+
file_date = get_file_date(path)
|
|
697
|
+
runner_name = data['info']['name']
|
|
698
|
+
console.print(
|
|
699
|
+
f'\n{path} ([bold blue]{runner_name}[/] [dim]{runner_type}[/]) ([dim]{file_date}[/]):')
|
|
700
|
+
if report.is_empty():
|
|
701
|
+
if len(paths) == 1:
|
|
702
|
+
console.print('[bold orange4]No results in report.[/]')
|
|
703
|
+
else:
|
|
704
|
+
console.print('[bold orange4]No new results since previous scan.[/]')
|
|
705
|
+
continue
|
|
706
|
+
report.send()
|
|
707
|
+
except json.decoder.JSONDecodeError as e:
|
|
574
708
|
console.print(f'[bold red]Could not load {path}: {str(e)}')
|
|
575
709
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
710
|
+
if unified:
|
|
711
|
+
console.print(f'\n:wrench: [bold gold3]Building report by crunching {len(all_results)} results ...[/]')
|
|
712
|
+
console.print(':coffee: [dim]Note that this can take a while when the result count is high...[/]')
|
|
713
|
+
runner.results = all_results
|
|
714
|
+
report = Report(runner, title=f"Consolidated report - {current}", exporters=exporters)
|
|
715
|
+
report.build(extractors=extractors, dedupe=True)
|
|
716
|
+
report.send()
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
@report.command('list')
|
|
720
|
+
@click.option('-ws', '-w', '--workspace', type=str)
|
|
721
|
+
@click.option('-r', '--runner-type', type=str, default=None, help='Filter by runner type. Choices: task, workflow, scan') # noqa: E501
|
|
722
|
+
@click.option('-d', '--time-delta', type=str, default=None, help='Keep results newer than time delta. E.g: 26m, 1d, 1y') # noqa: E501
|
|
723
|
+
def report_list(workspace, runner_type, time_delta):
|
|
724
|
+
"""List all secator reports."""
|
|
725
|
+
paths = list_reports(workspace=workspace, type=runner_type, timedelta=human_to_timedelta(time_delta))
|
|
726
|
+
paths = sorted(paths, key=lambda x: x.stat().st_mtime, reverse=False)
|
|
727
|
+
|
|
728
|
+
# Build table
|
|
729
|
+
table = Table()
|
|
730
|
+
table.add_column("Workspace", style="bold gold3")
|
|
731
|
+
table.add_column("Path", overflow='fold')
|
|
732
|
+
table.add_column("Name")
|
|
733
|
+
table.add_column("Id")
|
|
734
|
+
table.add_column("Date")
|
|
735
|
+
table.add_column("Status", style="green")
|
|
736
|
+
|
|
737
|
+
# Load each report
|
|
738
|
+
for path in paths:
|
|
739
|
+
try:
|
|
740
|
+
info = get_info_from_report_path(path)
|
|
741
|
+
with open(path, 'r') as f:
|
|
742
|
+
content = json.loads(f.read())
|
|
743
|
+
data = {
|
|
744
|
+
'workspace': info['workspace'],
|
|
745
|
+
'name': f"[bold blue]{content['info']['name']}[/]",
|
|
746
|
+
'status': content['info'].get('status', ''),
|
|
747
|
+
'id': info['type'] + '/' + info['id'],
|
|
748
|
+
'date': get_file_date(path), # Assuming get_file_date returns a readable date
|
|
749
|
+
}
|
|
750
|
+
status_color = STATE_COLORS[data['status']] if data['status'] in STATE_COLORS else 'white'
|
|
751
|
+
|
|
752
|
+
# Update table
|
|
753
|
+
table.add_row(
|
|
754
|
+
data['workspace'],
|
|
755
|
+
str(path),
|
|
756
|
+
data['name'],
|
|
757
|
+
data['id'],
|
|
758
|
+
data['date'],
|
|
759
|
+
f"[{status_color}]{data['status']}[/]"
|
|
760
|
+
)
|
|
761
|
+
except json.JSONDecodeError as e:
|
|
762
|
+
console.print(f'[bold red]Could not load {path}: {str(e)}')
|
|
763
|
+
|
|
764
|
+
if len(paths) > 0:
|
|
765
|
+
console.print(table)
|
|
766
|
+
else:
|
|
767
|
+
console.print('[bold red]No results found.')
|
|
582
768
|
|
|
583
769
|
|
|
584
770
|
@report.command('export')
|
|
@@ -588,7 +774,6 @@ def report_list(workspace):
|
|
|
588
774
|
def report_export(json_path, output_folder, output):
|
|
589
775
|
with open(json_path, 'r') as f:
|
|
590
776
|
data = loads_dataclass(f.read())
|
|
591
|
-
flatten(list(data['results'].values()))
|
|
592
777
|
|
|
593
778
|
runner_instance = DotMap({
|
|
594
779
|
"config": {
|
|
@@ -634,7 +819,8 @@ def report_export(json_path, output_folder, output):
|
|
|
634
819
|
@cli.command(name='health')
|
|
635
820
|
@click.option('--json', '-json', is_flag=True, default=False, help='JSON lines output')
|
|
636
821
|
@click.option('--debug', '-debug', is_flag=True, default=False, help='Debug health output')
|
|
637
|
-
|
|
822
|
+
@click.option('--strict', '-strict', is_flag=True, default=False, help='Fail if missing tools')
|
|
823
|
+
def health(json, debug, strict):
|
|
638
824
|
"""[dim]Get health status.[/]"""
|
|
639
825
|
tools = ALL_TASKS
|
|
640
826
|
status = {'secator': {}, 'languages': {}, 'tools': {}, 'addons': {}}
|
|
@@ -652,14 +838,13 @@ def health(json, debug):
|
|
|
652
838
|
console.print('\n:wrench: [bold gold3]Checking installed addons ...[/]')
|
|
653
839
|
table = get_health_table()
|
|
654
840
|
with Live(table, console=console):
|
|
655
|
-
for addon
|
|
656
|
-
addon_var = ADDONS_ENABLED[addon]
|
|
841
|
+
for addon, installed in ADDONS_ENABLED.items():
|
|
657
842
|
info = {
|
|
658
843
|
'name': addon,
|
|
659
844
|
'version': None,
|
|
660
|
-
'status': 'ok' if
|
|
845
|
+
'status': 'ok' if installed else 'missing',
|
|
661
846
|
'latest_version': None,
|
|
662
|
-
'installed':
|
|
847
|
+
'installed': installed,
|
|
663
848
|
'location': None
|
|
664
849
|
}
|
|
665
850
|
row = fmt_health_table_row(info, 'addons')
|
|
@@ -682,39 +867,58 @@ def health(json, debug):
|
|
|
682
867
|
table = get_health_table()
|
|
683
868
|
with Live(table, console=console):
|
|
684
869
|
for tool in tools:
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
870
|
+
info = get_version_info(
|
|
871
|
+
tool.cmd.split(' ')[0],
|
|
872
|
+
tool.version_flag or f'{tool.opt_prefix}version',
|
|
873
|
+
tool.install_github_handle,
|
|
874
|
+
tool.install_cmd
|
|
875
|
+
)
|
|
689
876
|
row = fmt_health_table_row(info, 'tools')
|
|
690
877
|
table.add_row(*row)
|
|
691
878
|
status['tools'][tool.__name__] = info
|
|
879
|
+
console.print('')
|
|
692
880
|
|
|
693
881
|
# Print JSON health
|
|
694
882
|
if json:
|
|
695
883
|
import json as _json
|
|
696
884
|
print(_json.dumps(status))
|
|
697
885
|
|
|
886
|
+
# Strict mode
|
|
887
|
+
if strict:
|
|
888
|
+
error = False
|
|
889
|
+
for tool, info in status['tools'].items():
|
|
890
|
+
if not info['installed']:
|
|
891
|
+
console.print(Error(message=f'{tool} not installed and strict mode is enabled.[/]'))
|
|
892
|
+
error = True
|
|
893
|
+
if error:
|
|
894
|
+
sys.exit(1)
|
|
895
|
+
console.print(Info(message='Strict healthcheck passed !'))
|
|
896
|
+
|
|
897
|
+
|
|
698
898
|
#---------#
|
|
699
899
|
# INSTALL #
|
|
700
900
|
#---------#
|
|
701
901
|
|
|
702
902
|
|
|
703
|
-
def run_install(cmd,
|
|
903
|
+
def run_install(title=None, cmd=None, packages=None, next_steps=None):
|
|
704
904
|
if CONFIG.offline_mode:
|
|
705
905
|
console.print('[bold red]Cannot run this command in offline mode.[/]')
|
|
706
906
|
return
|
|
707
907
|
with console.status(f'[bold yellow] Installing {title}...'):
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
908
|
+
if cmd:
|
|
909
|
+
from secator.installer import SourceInstaller
|
|
910
|
+
status = SourceInstaller.install(cmd)
|
|
911
|
+
elif packages:
|
|
912
|
+
from secator.installer import PackageInstaller
|
|
913
|
+
status = PackageInstaller.install(packages)
|
|
914
|
+
return_code = 1
|
|
915
|
+
if status.is_ok():
|
|
916
|
+
return_code = 0
|
|
713
917
|
if next_steps:
|
|
714
918
|
console.print('[bold gold3]:wrench: Next steps:[/]')
|
|
715
919
|
for ix, step in enumerate(next_steps):
|
|
716
920
|
console.print(f' :keycap_{ix}: {step}')
|
|
717
|
-
sys.exit(
|
|
921
|
+
sys.exit(return_code)
|
|
718
922
|
|
|
719
923
|
|
|
720
924
|
@cli.group()
|
|
@@ -731,40 +935,52 @@ def addons():
|
|
|
731
935
|
|
|
732
936
|
@addons.command('worker')
|
|
733
937
|
def install_worker():
|
|
734
|
-
"Install worker addon."
|
|
938
|
+
"Install Celery worker addon."
|
|
735
939
|
run_install(
|
|
736
940
|
cmd=f'{sys.executable} -m pip install secator[worker]',
|
|
737
|
-
title='worker addon',
|
|
941
|
+
title='Celery worker addon',
|
|
738
942
|
next_steps=[
|
|
739
943
|
'Run [bold green4]secator worker[/] to run a Celery worker using the file system as a backend and broker.',
|
|
740
944
|
'Run [bold green4]secator x httpx testphp.vulnweb.com[/] to admire your task running in a worker.',
|
|
741
|
-
'[dim]\[optional][/dim] Run [bold green4]secator install addons redis[/] to setup Redis backend / broker.'
|
|
945
|
+
r'[dim]\[optional][/dim] Run [bold green4]secator install addons redis[/] to setup Redis backend / broker.'
|
|
742
946
|
]
|
|
743
947
|
)
|
|
744
948
|
|
|
745
949
|
|
|
746
|
-
@addons.command('
|
|
747
|
-
def
|
|
748
|
-
"Install
|
|
950
|
+
@addons.command('gdrive')
|
|
951
|
+
def install_gdrive():
|
|
952
|
+
"Install Google Drive addon."
|
|
749
953
|
run_install(
|
|
750
954
|
cmd=f'{sys.executable} -m pip install secator[google]',
|
|
751
|
-
title='
|
|
955
|
+
title='Google Drive addon',
|
|
752
956
|
next_steps=[
|
|
753
|
-
'Run [bold green4]secator config set addons.
|
|
754
|
-
'Run [bold green4]secator config set addons.
|
|
957
|
+
'Run [bold green4]secator config set addons.gdrive.credentials_path <VALUE>[/].',
|
|
958
|
+
'Run [bold green4]secator config set addons.gdrive.drive_parent_folder_id <VALUE>[/].',
|
|
755
959
|
'Run [bold green4]secator x httpx testphp.vulnweb.com -o gdrive[/] to send reports to Google Drive.'
|
|
756
960
|
]
|
|
757
961
|
)
|
|
758
962
|
|
|
759
963
|
|
|
964
|
+
@addons.command('gcs')
|
|
965
|
+
def install_gcs():
|
|
966
|
+
"Install Google Cloud Storage addon."
|
|
967
|
+
run_install(
|
|
968
|
+
cmd=f'{sys.executable} -m pip install secator[gcs]',
|
|
969
|
+
title='Google Cloud Storage addon',
|
|
970
|
+
next_steps=[
|
|
971
|
+
'Run [bold green4]secator config set addons.gcs.credentials_path <VALUE>[/].',
|
|
972
|
+
]
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
|
|
760
976
|
@addons.command('mongodb')
|
|
761
977
|
def install_mongodb():
|
|
762
|
-
"Install
|
|
978
|
+
"Install MongoDB addon."
|
|
763
979
|
run_install(
|
|
764
980
|
cmd=f'{sys.executable} -m pip install secator[mongodb]',
|
|
765
|
-
title='
|
|
981
|
+
title='MongoDB addon',
|
|
766
982
|
next_steps=[
|
|
767
|
-
'[dim]\[optional][/] Run [bold green4]docker run --name mongo -p 27017:27017 -d mongo:latest[/] to run a local MongoDB instance.', # noqa: E501
|
|
983
|
+
r'[dim]\[optional][/] Run [bold green4]docker run --name mongo -p 27017:27017 -d mongo:latest[/] to run a local MongoDB instance.', # noqa: E501
|
|
768
984
|
'Run [bold green4]secator config set addons.mongodb.url mongodb://<URL>[/].',
|
|
769
985
|
'Run [bold green4]secator x httpx testphp.vulnweb.com -driver mongodb[/] to save results to MongoDB.'
|
|
770
986
|
]
|
|
@@ -773,12 +989,12 @@ def install_mongodb():
|
|
|
773
989
|
|
|
774
990
|
@addons.command('redis')
|
|
775
991
|
def install_redis():
|
|
776
|
-
"Install
|
|
992
|
+
"Install Redis addon."
|
|
777
993
|
run_install(
|
|
778
994
|
cmd=f'{sys.executable} -m pip install secator[redis]',
|
|
779
|
-
title='
|
|
995
|
+
title='Redis addon',
|
|
780
996
|
next_steps=[
|
|
781
|
-
'[dim]\[optional][/] Run [bold green4]docker run --name redis -p 6379:6379 -d redis[/] to run a local Redis instance.', # noqa: E501
|
|
997
|
+
r'[dim]\[optional][/] Run [bold green4]docker run --name redis -p 6379:6379 -d redis[/] to run a local Redis instance.', # noqa: E501
|
|
782
998
|
'Run [bold green4]secator config set celery.broker_url redis://<URL>[/]',
|
|
783
999
|
'Run [bold green4]secator config set celery.result_backend redis://<URL>[/]',
|
|
784
1000
|
'Run [bold green4]secator worker[/] to run a worker.',
|
|
@@ -806,7 +1022,7 @@ def install_trace():
|
|
|
806
1022
|
"Install trace addon."
|
|
807
1023
|
run_install(
|
|
808
1024
|
cmd=f'{sys.executable} -m pip install secator[trace]',
|
|
809
|
-
title='
|
|
1025
|
+
title='trace addon',
|
|
810
1026
|
next_steps=[
|
|
811
1027
|
]
|
|
812
1028
|
)
|
|
@@ -849,7 +1065,12 @@ def install_go():
|
|
|
849
1065
|
def install_ruby():
|
|
850
1066
|
"""Install Ruby."""
|
|
851
1067
|
run_install(
|
|
852
|
-
|
|
1068
|
+
packages={
|
|
1069
|
+
'apt': ['ruby-full', 'rubygems'],
|
|
1070
|
+
'apk': ['ruby', 'ruby-dev'],
|
|
1071
|
+
'pacman': ['ruby', 'ruby-dev'],
|
|
1072
|
+
'brew': ['ruby']
|
|
1073
|
+
},
|
|
853
1074
|
title='Ruby'
|
|
854
1075
|
)
|
|
855
1076
|
|
|
@@ -859,43 +1080,22 @@ def install_ruby():
|
|
|
859
1080
|
def install_tools(cmds):
|
|
860
1081
|
"""Install supported tools."""
|
|
861
1082
|
if CONFIG.offline_mode:
|
|
862
|
-
console.print('
|
|
1083
|
+
console.print(Error(message='Cannot run this command in offline mode.'))
|
|
863
1084
|
return
|
|
864
1085
|
if cmds is not None:
|
|
865
1086
|
cmds = cmds.split(',')
|
|
866
1087
|
tools = [cls for cls in ALL_TASKS if cls.__name__ in cmds]
|
|
867
1088
|
else:
|
|
868
1089
|
tools = ALL_TASKS
|
|
869
|
-
|
|
1090
|
+
tools.sort(key=lambda x: x.__name__)
|
|
1091
|
+
return_code = 0
|
|
870
1092
|
for ix, cls in enumerate(tools):
|
|
871
|
-
with console.status(f'[bold yellow][{ix}/{len(tools)}] Installing {cls.__name__} ...'):
|
|
872
|
-
ToolInstaller.install(cls)
|
|
1093
|
+
with console.status(f'[bold yellow][{ix + 1}/{len(tools)}] Installing {cls.__name__} ...'):
|
|
1094
|
+
status = ToolInstaller.install(cls)
|
|
1095
|
+
if not status.is_ok():
|
|
1096
|
+
return_code = 1
|
|
873
1097
|
console.print()
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
@install.command('cves')
|
|
877
|
-
@click.option('--force', is_flag=True)
|
|
878
|
-
def install_cves(force):
|
|
879
|
-
"""Install CVEs (enables passive vulnerability search)."""
|
|
880
|
-
if CONFIG.offline_mode:
|
|
881
|
-
console.print('[bold red]Cannot run this command in offline mode.[/]')
|
|
882
|
-
return
|
|
883
|
-
cve_json_path = f'{CONFIG.dirs.cves}/circl-cve-search-expanded.json'
|
|
884
|
-
if not os.path.exists(cve_json_path) or force:
|
|
885
|
-
with console.status('[bold yellow]Downloading zipped CVEs from cve.circl.lu ...[/]'):
|
|
886
|
-
Command.execute('wget https://cve.circl.lu/static/circl-cve-search-expanded.json.gz', cwd=CONFIG.dirs.cves)
|
|
887
|
-
with console.status('[bold yellow]Unzipping CVEs ...[/]'):
|
|
888
|
-
Command.execute(f'gunzip {CONFIG.dirs.cves}/circl-cve-search-expanded.json.gz', cwd=CONFIG.dirs.cves)
|
|
889
|
-
with console.status(f'[bold yellow]Installing CVEs to {CONFIG.dirs.cves} ...[/]'):
|
|
890
|
-
with open(cve_json_path, 'r') as f:
|
|
891
|
-
for line in f:
|
|
892
|
-
data = json.loads(line)
|
|
893
|
-
cve_id = data['id']
|
|
894
|
-
cve_path = f'{CONFIG.dirs.cves}/{cve_id}.json'
|
|
895
|
-
with open(cve_path, 'w') as f:
|
|
896
|
-
f.write(line)
|
|
897
|
-
console.print(f'CVE saved to {cve_path}')
|
|
898
|
-
console.print(':tada: CVEs installed successfully !', style='bold green')
|
|
1098
|
+
sys.exit(return_code)
|
|
899
1099
|
|
|
900
1100
|
|
|
901
1101
|
#--------#
|
|
@@ -903,22 +1103,52 @@ def install_cves(force):
|
|
|
903
1103
|
#--------#
|
|
904
1104
|
|
|
905
1105
|
@cli.command('update')
|
|
906
|
-
|
|
1106
|
+
@click.option('--all', '-a', is_flag=True, help='Update all secator dependencies (addons, tools, ...)')
|
|
1107
|
+
def update(all):
|
|
907
1108
|
"""[dim]Update to latest version.[/]"""
|
|
908
1109
|
if CONFIG.offline_mode:
|
|
909
|
-
console.print('
|
|
910
|
-
|
|
1110
|
+
console.print(Error(message='Cannot run this command in offline mode.'))
|
|
1111
|
+
sys.exit(1)
|
|
1112
|
+
|
|
1113
|
+
# Check current and latest version
|
|
911
1114
|
info = get_version_info('secator', github_handle='freelabz/secator', version=VERSION)
|
|
912
1115
|
latest_version = info['latest_version']
|
|
1116
|
+
do_update = True
|
|
1117
|
+
|
|
1118
|
+
# Skip update if latest
|
|
913
1119
|
if info['status'] == 'latest':
|
|
914
|
-
console.print(f'
|
|
915
|
-
|
|
916
|
-
console.print(f'[bold gold3]:wrench: Updating secator from {VERSION} to {latest_version} ...[/]')
|
|
917
|
-
if 'pipx' in sys.executable:
|
|
918
|
-
Command.execute(f'pipx install secator=={latest_version} --force')
|
|
919
|
-
else:
|
|
920
|
-
Command.execute(f'pip install secator=={latest_version}')
|
|
1120
|
+
console.print(Info(message=f'secator is already at the newest version {latest_version} !'))
|
|
1121
|
+
do_update = False
|
|
921
1122
|
|
|
1123
|
+
# Fail if unknown latest
|
|
1124
|
+
if not latest_version:
|
|
1125
|
+
console.print(Error(message='Could not fetch latest secator version.'))
|
|
1126
|
+
sys.exit(1)
|
|
1127
|
+
|
|
1128
|
+
# Update secator
|
|
1129
|
+
if do_update:
|
|
1130
|
+
console.print(f'[bold gold3]:wrench: Updating secator from {VERSION} to {latest_version} ...[/]')
|
|
1131
|
+
if 'pipx' in sys.executable:
|
|
1132
|
+
ret = Command.execute(f'pipx install secator=={latest_version} --force')
|
|
1133
|
+
else:
|
|
1134
|
+
ret = Command.execute(f'pip install secator=={latest_version}')
|
|
1135
|
+
if not ret.return_code == 0:
|
|
1136
|
+
sys.exit(1)
|
|
1137
|
+
|
|
1138
|
+
# Update tools
|
|
1139
|
+
if all:
|
|
1140
|
+
return_code = 0
|
|
1141
|
+
for cls in ALL_TASKS:
|
|
1142
|
+
cmd = cls.cmd.split(' ')[0]
|
|
1143
|
+
version_flag = cls.version_flag or f'{cls.opt_prefix}version'
|
|
1144
|
+
version_flag = None if cls.version_flag == OPT_NOT_SUPPORTED else version_flag
|
|
1145
|
+
info = get_version_info(cmd, version_flag, cls.install_github_handle)
|
|
1146
|
+
if not info['installed'] or info['status'] == 'outdated' or not info['latest_version']:
|
|
1147
|
+
with console.status(f'[bold yellow]Installing {cls.__name__} ...'):
|
|
1148
|
+
status = ToolInstaller.install(cls)
|
|
1149
|
+
if not status.is_ok():
|
|
1150
|
+
return_code = 1
|
|
1151
|
+
sys.exit(return_code)
|
|
922
1152
|
|
|
923
1153
|
#-------#
|
|
924
1154
|
# ALIAS #
|
|
@@ -1020,10 +1250,10 @@ def list_aliases(silent):
|
|
|
1020
1250
|
def test():
|
|
1021
1251
|
"""[dim]Run tests."""
|
|
1022
1252
|
if not DEV_PACKAGE:
|
|
1023
|
-
console.print('
|
|
1253
|
+
console.print(Error(message='You MUST use a development version of secator to run tests.'))
|
|
1024
1254
|
sys.exit(1)
|
|
1025
1255
|
if not ADDONS_ENABLED['dev']:
|
|
1026
|
-
console.print('
|
|
1256
|
+
console.print(Error(message='Missing dev addon: please run [bold green4]secator install addons dev[/]'))
|
|
1027
1257
|
sys.exit(1)
|
|
1028
1258
|
pass
|
|
1029
1259
|
|
|
@@ -1053,23 +1283,22 @@ def lint():
|
|
|
1053
1283
|
@click.option('--workflows', type=str, default='', help='Secator workflows to test (comma-separated)')
|
|
1054
1284
|
@click.option('--scans', type=str, default='', help='Secator scans to test (comma-separated)')
|
|
1055
1285
|
@click.option('--test', '-t', type=str, help='Secator test to run')
|
|
1056
|
-
|
|
1057
|
-
def unit(tasks, workflows, scans, test, debug=False):
|
|
1286
|
+
def unit(tasks, workflows, scans, test):
|
|
1058
1287
|
"""Run unit tests."""
|
|
1059
1288
|
os.environ['TEST_TASKS'] = tasks or ''
|
|
1060
1289
|
os.environ['TEST_WORKFLOWS'] = workflows or ''
|
|
1061
1290
|
os.environ['TEST_SCANS'] = scans or ''
|
|
1062
|
-
os.environ['
|
|
1291
|
+
os.environ['SECATOR_DIRS_DATA'] = '/tmp/.secator'
|
|
1292
|
+
os.environ['SECATOR_OFFLINE_MODE'] = "1"
|
|
1063
1293
|
os.environ['SECATOR_HTTP_STORE_RESPONSES'] = '0'
|
|
1064
1294
|
os.environ['SECATOR_RUNNERS_SKIP_CVE_SEARCH'] = '1'
|
|
1065
1295
|
|
|
1066
|
-
|
|
1296
|
+
import shutil
|
|
1297
|
+
shutil.rmtree('/tmp/.secator', ignore_errors=True)
|
|
1298
|
+
cmd = f'{sys.executable} -m coverage run --omit="*test*" --data-file=.coverage.unit -m pytest -s -v tests/unit'
|
|
1067
1299
|
if test:
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
cmd += f' {test}'
|
|
1071
|
-
else:
|
|
1072
|
-
cmd += ' discover -v tests.unit'
|
|
1300
|
+
test_str = ' or '.join(test.split(','))
|
|
1301
|
+
cmd += f' -k "{test_str}"'
|
|
1073
1302
|
run_test(cmd, 'unit')
|
|
1074
1303
|
|
|
1075
1304
|
|
|
@@ -1078,35 +1307,57 @@ def unit(tasks, workflows, scans, test, debug=False):
|
|
|
1078
1307
|
@click.option('--workflows', type=str, default='', help='Secator workflows to test (comma-separated)')
|
|
1079
1308
|
@click.option('--scans', type=str, default='', help='Secator scans to test (comma-separated)')
|
|
1080
1309
|
@click.option('--test', '-t', type=str, help='Secator test to run')
|
|
1081
|
-
|
|
1082
|
-
def integration(tasks, workflows, scans, test, debug):
|
|
1310
|
+
def integration(tasks, workflows, scans, test):
|
|
1083
1311
|
"""Run integration tests."""
|
|
1084
1312
|
os.environ['TEST_TASKS'] = tasks or ''
|
|
1085
1313
|
os.environ['TEST_WORKFLOWS'] = workflows or ''
|
|
1086
1314
|
os.environ['TEST_SCANS'] = scans or ''
|
|
1087
|
-
os.environ['
|
|
1315
|
+
os.environ['SECATOR_DIRS_DATA'] = '/tmp/.secator'
|
|
1088
1316
|
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'
|
|
1317
|
+
|
|
1094
1318
|
import shutil
|
|
1095
|
-
|
|
1096
|
-
shutil.rmtree(path, ignore_errors=True)
|
|
1319
|
+
shutil.rmtree('/tmp/.secator', ignore_errors=True)
|
|
1097
1320
|
|
|
1098
|
-
cmd = f'{sys.executable} -m
|
|
1321
|
+
cmd = f'{sys.executable} -m coverage run --omit="*test*" --data-file=.coverage.integration -m pytest -s -v tests/integration' # noqa: E501
|
|
1099
1322
|
if test:
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
cmd += f' {test}'
|
|
1103
|
-
else:
|
|
1104
|
-
cmd += ' discover -v tests.integration'
|
|
1323
|
+
test_str = ' or '.join(test.split(','))
|
|
1324
|
+
cmd += f' -k "{test_str}"'
|
|
1105
1325
|
run_test(cmd, 'integration')
|
|
1106
1326
|
|
|
1107
1327
|
|
|
1108
1328
|
@test.command()
|
|
1109
|
-
|
|
1110
|
-
|
|
1329
|
+
@click.option('--tasks', type=str, default='', help='Secator tasks to test (comma-separated)')
|
|
1330
|
+
@click.option('--workflows', type=str, default='', help='Secator workflows to test (comma-separated)')
|
|
1331
|
+
@click.option('--scans', type=str, default='', help='Secator scans to test (comma-separated)')
|
|
1332
|
+
@click.option('--test', '-t', type=str, help='Secator test to run')
|
|
1333
|
+
def performance(tasks, workflows, scans, test):
|
|
1334
|
+
"""Run integration tests."""
|
|
1335
|
+
os.environ['TEST_TASKS'] = tasks or ''
|
|
1336
|
+
os.environ['TEST_WORKFLOWS'] = workflows or ''
|
|
1337
|
+
os.environ['TEST_SCANS'] = scans or ''
|
|
1338
|
+
os.environ['SECATOR_DIRS_DATA'] = '/tmp/.secator'
|
|
1339
|
+
os.environ['SECATOR_RUNNERS_SKIP_CVE_SEARCH'] = '1'
|
|
1340
|
+
|
|
1341
|
+
# import shutil
|
|
1342
|
+
# shutil.rmtree('/tmp/.secator', ignore_errors=True)
|
|
1343
|
+
|
|
1344
|
+
cmd = f'{sys.executable} -m coverage run --omit="*test*" --data-file=.coverage.performance -m pytest -s -v tests/performance' # noqa: E501
|
|
1345
|
+
if test:
|
|
1346
|
+
test_str = ' or '.join(test.split(','))
|
|
1347
|
+
cmd += f' -k "{test_str}"'
|
|
1348
|
+
run_test(cmd, 'performance')
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
@test.command()
|
|
1352
|
+
@click.option('--unit-only', '-u', is_flag=True, default=False, help='Only generate coverage for unit tests')
|
|
1353
|
+
@click.option('--integration-only', '-i', is_flag=True, default=False, help='Only generate coverage for integration tests') # noqa: E501
|
|
1354
|
+
def coverage(unit_only, integration_only):
|
|
1355
|
+
"""Run coverage combine + coverage report."""
|
|
1111
1356
|
cmd = f'{sys.executable} -m coverage report -m --omit=*/site-packages/*,*/tests/*,*/templates/*'
|
|
1357
|
+
if unit_only:
|
|
1358
|
+
cmd += ' --data-file=.coverage.unit'
|
|
1359
|
+
elif integration_only:
|
|
1360
|
+
cmd += ' --data-file=.coverage.integration'
|
|
1361
|
+
else:
|
|
1362
|
+
Command.execute(f'{sys.executable} -m coverage combine --keep', name='coverage combine', cwd=ROOT_FOLDER)
|
|
1112
1363
|
run_test(cmd, 'coverage')
|