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