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