secator 0.2.0__py2.py3-none-any.whl → 0.3.1__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/celery.py +1 -1
- secator/cli.py +434 -454
- secator/decorators.py +5 -6
- secator/definitions.py +53 -28
- secator/exporters/txt.py +1 -1
- secator/installer.py +335 -0
- secator/rich.py +2 -8
- secator/runners/_base.py +47 -15
- secator/runners/command.py +2 -16
- secator/runners/task.py +4 -3
- secator/runners/workflow.py +1 -1
- secator/tasks/_categories.py +6 -11
- secator/tasks/dalfox.py +1 -0
- secator/tasks/dnsx.py +1 -0
- secator/tasks/dnsxbrute.py +1 -0
- secator/tasks/feroxbuster.py +1 -0
- secator/tasks/ffuf.py +1 -0
- secator/tasks/gau.py +1 -0
- secator/tasks/gospider.py +1 -0
- secator/tasks/grype.py +1 -0
- secator/tasks/httpx.py +1 -0
- secator/tasks/katana.py +1 -0
- secator/tasks/mapcidr.py +1 -0
- secator/tasks/naabu.py +1 -0
- secator/tasks/nuclei.py +1 -0
- secator/tasks/searchsploit.py +1 -0
- secator/tasks/subfinder.py +1 -0
- secator/utils.py +23 -1
- secator/utils_test.py +1 -0
- {secator-0.2.0.dist-info → secator-0.3.1.dist-info}/METADATA +1 -1
- {secator-0.2.0.dist-info → secator-0.3.1.dist-info}/RECORD +34 -33
- {secator-0.2.0.dist-info → secator-0.3.1.dist-info}/WHEEL +0 -0
- {secator-0.2.0.dist-info → secator-0.3.1.dist-info}/entry_points.txt +0 -0
- {secator-0.2.0.dist-info → secator-0.3.1.dist-info}/licenses/LICENSE +0 -0
secator/cli.py
CHANGED
|
@@ -7,22 +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
14
|
from secator.config import ConfigLoader
|
|
14
15
|
from secator.decorators import OrderedGroup, register_runner
|
|
15
|
-
from secator.definitions import (ASCII, CVES_FOLDER, DATA_FOLDER,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
OPT_NOT_SUPPORTED, PAYLOADS_FOLDER,
|
|
19
|
-
REDIS_ADDON_ENABLED, REVSHELLS_FOLDER, ROOT_FOLDER,
|
|
20
|
-
TRACE_ADDON_ENABLED, VERSION, WORKER_ADDON_ENABLED)
|
|
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
|
|
21
19
|
from secator.rich import console
|
|
22
20
|
from secator.runners import Command
|
|
23
21
|
from secator.serializers.dataclass import loads_dataclass
|
|
24
|
-
from secator.utils import (debug, detect_host, discover_tasks, find_list_item,
|
|
25
|
-
|
|
22
|
+
from secator.utils import (debug, detect_host, discover_tasks, find_list_item, flatten,
|
|
23
|
+
print_results_table, print_version)
|
|
26
24
|
|
|
27
25
|
click.rich_click.USE_RICH_MARKUP = True
|
|
28
26
|
|
|
@@ -37,20 +35,14 @@ ALL_SCANS = ALL_CONFIGS.scan
|
|
|
37
35
|
#-----#
|
|
38
36
|
|
|
39
37
|
@click.group(cls=OrderedGroup, invoke_without_command=True)
|
|
40
|
-
@click.option('--no-banner', '-nb', is_flag=True, default=False)
|
|
41
38
|
@click.option('--version', '-version', is_flag=True, default=False)
|
|
42
39
|
@click.pass_context
|
|
43
|
-
def cli(ctx,
|
|
40
|
+
def cli(ctx, version):
|
|
44
41
|
"""Secator CLI."""
|
|
45
|
-
|
|
46
|
-
print(ASCII, file=sys.stderr)
|
|
42
|
+
console.print(ASCII, highlight=False)
|
|
47
43
|
if ctx.invoked_subcommand is None:
|
|
48
44
|
if version:
|
|
49
|
-
|
|
50
|
-
console.print(f'[bold gold3]Python binary[/]: {sys.executable}')
|
|
51
|
-
if DEV_PACKAGE:
|
|
52
|
-
console.print(f'[bold gold3]Root folder[/]: {ROOT_FOLDER}')
|
|
53
|
-
console.print(f'[bold gold3]Lib folder[/]: {LIB_FOLDER}')
|
|
45
|
+
print_version()
|
|
54
46
|
else:
|
|
55
47
|
ctx.get_help()
|
|
56
48
|
|
|
@@ -114,7 +106,7 @@ for config in sorted(ALL_SCANS, key=lambda x: x['name']):
|
|
|
114
106
|
@click.option('--show', is_flag=True, help='Show command (celery multi).')
|
|
115
107
|
def worker(hostname, concurrency, reload, queue, pool, check, dev, stop, show):
|
|
116
108
|
"""Run a worker."""
|
|
117
|
-
if not
|
|
109
|
+
if not ADDONS_ENABLED['worker']:
|
|
118
110
|
console.print('[bold red]Missing worker addon: please run `secator install addons worker`[/].')
|
|
119
111
|
sys.exit(1)
|
|
120
112
|
from secator.celery import app, is_celery_worker_alive
|
|
@@ -147,165 +139,443 @@ def worker(hostname, concurrency, reload, queue, pool, check, dev, stop, show):
|
|
|
147
139
|
Command.execute(cmd, name='secator worker')
|
|
148
140
|
|
|
149
141
|
|
|
150
|
-
|
|
151
|
-
#
|
|
152
|
-
|
|
142
|
+
#-------#
|
|
143
|
+
# UTILS #
|
|
144
|
+
#-------#
|
|
153
145
|
|
|
154
146
|
|
|
155
|
-
@cli.group(aliases=['
|
|
156
|
-
def
|
|
157
|
-
"""
|
|
147
|
+
@cli.group(aliases=['u'])
|
|
148
|
+
def util():
|
|
149
|
+
"""Run a utility."""
|
|
158
150
|
pass
|
|
159
151
|
|
|
160
152
|
|
|
161
|
-
@
|
|
162
|
-
@click.
|
|
163
|
-
@click.option('
|
|
164
|
-
def
|
|
165
|
-
"""
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
print_results_table(
|
|
171
|
-
results,
|
|
172
|
-
title=report['info']['title'],
|
|
173
|
-
exclude_fields=exclude_fields)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
#--------#
|
|
177
|
-
# DEPLOY #
|
|
178
|
-
#--------#
|
|
179
|
-
|
|
180
|
-
# TODO: work on this
|
|
181
|
-
# @cli.group(aliases=['d'])
|
|
182
|
-
# def deploy():
|
|
183
|
-
# """Deploy secator."""
|
|
184
|
-
# pass
|
|
185
|
-
|
|
186
|
-
# @deploy.command()
|
|
187
|
-
# def docker_compose():
|
|
188
|
-
# """Deploy secator on docker-compose."""
|
|
189
|
-
# pass
|
|
153
|
+
@util.command()
|
|
154
|
+
@click.option('--timeout', type=float, default=0.2, help='Proxy timeout (in seconds)')
|
|
155
|
+
@click.option('--number', '-n', type=int, default=1, help='Number of proxies')
|
|
156
|
+
def proxy(timeout, number):
|
|
157
|
+
"""Get random proxies from FreeProxy."""
|
|
158
|
+
proxy = FreeProxy(timeout=timeout, rand=True, anonym=True)
|
|
159
|
+
for _ in range(number):
|
|
160
|
+
url = proxy.get()
|
|
161
|
+
print(url)
|
|
190
162
|
|
|
191
|
-
# @deploy.command()
|
|
192
|
-
# @click.option('-t', '--target', type=str, default='minikube', help='Deployment target amongst minikube, gke')
|
|
193
|
-
# def k8s():
|
|
194
|
-
# """Deploy secator on Kubernetes."""
|
|
195
|
-
# pass
|
|
196
163
|
|
|
164
|
+
@util.command()
|
|
165
|
+
@click.argument('name', type=str, default=None, required=False)
|
|
166
|
+
@click.option('--host', '-h', type=str, default=None, help='Specify LHOST for revshell, otherwise autodetected.')
|
|
167
|
+
@click.option('--port', '-p', type=int, default=9001, show_default=True, help='Specify PORT for revshell')
|
|
168
|
+
@click.option('--interface', '-i', type=str, help='Interface to use to detect IP')
|
|
169
|
+
@click.option('--listen', '-l', is_flag=True, default=False, help='Spawn netcat listener on specified port')
|
|
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)."""
|
|
173
|
+
if host is None: # detect host automatically
|
|
174
|
+
host = detect_host(interface)
|
|
175
|
+
if not host:
|
|
176
|
+
console.print(
|
|
177
|
+
f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of available interfaces.',
|
|
178
|
+
style='bold red')
|
|
179
|
+
return
|
|
197
180
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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)
|
|
201
190
|
|
|
191
|
+
# Parse JSON into shells
|
|
192
|
+
with open(revshells_json) as f:
|
|
193
|
+
shells = json.loads(f.read())
|
|
194
|
+
for sh in shells:
|
|
195
|
+
sh['alias'] = '_'.join(sh['name'].lower()
|
|
196
|
+
.replace('-c', '')
|
|
197
|
+
.replace('-e', '')
|
|
198
|
+
.replace('-i', '')
|
|
199
|
+
.replace('c#', 'cs')
|
|
200
|
+
.replace('#', '')
|
|
201
|
+
.replace('(', '')
|
|
202
|
+
.replace(')', '')
|
|
203
|
+
.strip()
|
|
204
|
+
.split(' ')).replace('_1', '')
|
|
205
|
+
cmd = re.sub(r"\s\s+", "", sh.get('command', ''), flags=re.UNICODE)
|
|
206
|
+
cmd = cmd.replace('\n', ' ')
|
|
207
|
+
sh['cmd_short'] = (cmd[:30] + '..') if len(cmd) > 30 else cmd
|
|
202
208
|
|
|
203
|
-
|
|
204
|
-
|
|
209
|
+
shell = [
|
|
210
|
+
shell for shell in shells if shell['name'] == name or shell['alias'] == name
|
|
211
|
+
]
|
|
212
|
+
if not shell:
|
|
213
|
+
console.print('Available shells:', style='bold yellow')
|
|
214
|
+
shells_str = [
|
|
215
|
+
'[bold magenta]{alias:<20}[/][dim white]{name:<20}[/][dim gold3]{cmd_short:<20}[/]'.format(**sh)
|
|
216
|
+
for sh in shells
|
|
217
|
+
]
|
|
218
|
+
console.print('\n'.join(shells_str))
|
|
219
|
+
else:
|
|
220
|
+
shell = shell[0]
|
|
221
|
+
command = shell['command']
|
|
222
|
+
alias = shell['alias']
|
|
223
|
+
name = shell['name']
|
|
224
|
+
command_str = Template(command).render(ip=host, port=port, shell='bash')
|
|
225
|
+
console.print(Rule(f'[bold gold3]{alias}[/] - [bold red]{name} REMOTE SHELL', style='bold red', align='left'))
|
|
226
|
+
lang = shell.get('lang') or 'sh'
|
|
227
|
+
if len(command.splitlines()) == 1:
|
|
228
|
+
console.print()
|
|
229
|
+
print(f'\033[0;36m{command_str}')
|
|
230
|
+
else:
|
|
231
|
+
md = Markdown(f'```{lang}\n{command_str}\n```')
|
|
232
|
+
console.print(md)
|
|
233
|
+
console.print(f'Save this script as rev.{lang} and run it on your target', style='dim italic')
|
|
234
|
+
console.print()
|
|
235
|
+
console.print(Rule(style='bold red'))
|
|
205
236
|
|
|
206
|
-
|
|
207
|
-
|
|
237
|
+
if listen:
|
|
238
|
+
console.print(f'Starting netcat listener on port {port} ...', style='bold gold3')
|
|
239
|
+
cmd = f'nc -lvnp {port}'
|
|
240
|
+
Command.execute(cmd)
|
|
208
241
|
|
|
209
|
-
Returns:
|
|
210
|
-
secator.Command: Command instance.
|
|
211
|
-
"""
|
|
212
|
-
return Command.execute(f'which {command}', quiet=True, print_errors=False)
|
|
213
242
|
|
|
243
|
+
@util.command()
|
|
244
|
+
@click.option('--directory', '-d', type=str, default=PAYLOADS_FOLDER, show_default=True, help='HTTP server directory')
|
|
245
|
+
@click.option('--host', '-h', type=str, default=None, help='HTTP host')
|
|
246
|
+
@click.option('--port', '-p', type=int, default=9001, help='HTTP server port')
|
|
247
|
+
@click.option('--interface', '-i', type=str, default=None, help='Interface to use to auto-detect host IP')
|
|
248
|
+
def serve(directory, host, port, interface):
|
|
249
|
+
"""Run HTTP server to serve payloads."""
|
|
250
|
+
LSE_URL = 'https://github.com/diego-treitos/linux-smart-enumeration/releases/latest/download/lse.sh'
|
|
251
|
+
LINPEAS_URL = 'https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh'
|
|
252
|
+
SUDOKILLER_URL = 'https://raw.githubusercontent.com/TH3xACE/SUDO_KILLER/V3/SUDO_KILLERv3.sh'
|
|
253
|
+
PAYLOADS = [
|
|
254
|
+
{
|
|
255
|
+
'fname': 'lse.sh',
|
|
256
|
+
'description': 'Linux Smart Enumeration',
|
|
257
|
+
'command': f'wget {LSE_URL} -O lse.sh && chmod 700 lse.sh'
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
'fname': 'linpeas.sh',
|
|
261
|
+
'description': 'Linux Privilege Escalation Awesome Script',
|
|
262
|
+
'command': f'wget {LINPEAS_URL} -O linpeas.sh && chmod 700 linpeas.sh'
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
'fname': 'sudo_killer.sh',
|
|
266
|
+
'description': 'SUDO_KILLER',
|
|
267
|
+
'command': f'wget {SUDOKILLER_URL} -O sudo_killer.sh && chmod 700 sudo_killer.sh'
|
|
268
|
+
}
|
|
269
|
+
]
|
|
270
|
+
for ix, payload in enumerate(PAYLOADS):
|
|
271
|
+
descr = payload.get('description', '')
|
|
272
|
+
fname = payload['fname']
|
|
273
|
+
if not os.path.exists(f'{directory}/{fname}'):
|
|
274
|
+
with console.status(f'[bold yellow][{ix}/{len(PAYLOADS)}] Downloading {fname} [dim]({descr})[/] ...[/]'):
|
|
275
|
+
cmd = payload['command']
|
|
276
|
+
console.print(f'[bold magenta]{fname} [dim]({descr})[/] ...[/]', )
|
|
277
|
+
Command.execute(cmd, cls_attributes={'shell': True}, cwd=directory)
|
|
278
|
+
console.print()
|
|
214
279
|
|
|
215
|
-
|
|
216
|
-
|
|
280
|
+
console.print(Rule())
|
|
281
|
+
console.print(f'Available payloads in {directory}: ', style='bold yellow')
|
|
282
|
+
for fname in os.listdir(directory):
|
|
283
|
+
if not host:
|
|
284
|
+
host = detect_host(interface)
|
|
285
|
+
if not host:
|
|
286
|
+
console.print(
|
|
287
|
+
f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of interfaces.',
|
|
288
|
+
style='bold red')
|
|
289
|
+
return
|
|
290
|
+
payload = find_list_item(PAYLOADS, fname, key='fname', default={})
|
|
291
|
+
fdescr = payload.get('description', 'No description')
|
|
292
|
+
console.print(f'{fname} [dim]({fdescr})[/]', style='bold magenta')
|
|
293
|
+
console.print(f'wget http://{host}:{port}/{fname}', style='dim italic')
|
|
294
|
+
console.print('')
|
|
295
|
+
console.print(Rule())
|
|
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)
|
|
217
298
|
|
|
218
|
-
Args:
|
|
219
|
-
cls: Command class.
|
|
220
299
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
300
|
+
@util.command()
|
|
301
|
+
@click.argument('record_name', type=str, default=None)
|
|
302
|
+
@click.option('--script', '-s', type=str, default=None, help='Script to run. See scripts/stories/ for examples.')
|
|
303
|
+
@click.option('--interactive', '-i', is_flag=True, default=False, help='Interactive record.')
|
|
304
|
+
@click.option('--width', '-w', type=int, default=None, help='Recording width')
|
|
305
|
+
@click.option('--height', '-h', type=int, default=None, help='Recording height')
|
|
306
|
+
@click.option('--output-dir', type=str, default=f'{ROOT_FOLDER}/images')
|
|
307
|
+
def record(record_name, script, interactive, width, height, output_dir):
|
|
308
|
+
"""Record secator session using asciinema."""
|
|
309
|
+
# 120 x 30 is a good ratio for GitHub
|
|
310
|
+
width = width or console.size.width
|
|
311
|
+
height = height or console.size.height
|
|
312
|
+
attrs = {
|
|
313
|
+
'shell': False,
|
|
314
|
+
'env': {
|
|
315
|
+
'RECORD': '1',
|
|
316
|
+
'LINES': str(height),
|
|
317
|
+
'PS1': '$ ',
|
|
318
|
+
'COLUMNS': str(width),
|
|
319
|
+
'TERM': 'xterm-256color'
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
output_cast_path = f'{output_dir}/{record_name}.cast'
|
|
323
|
+
output_gif_path = f'{output_dir}/{record_name}.gif'
|
|
230
324
|
|
|
325
|
+
# Run automated 'story' script with asciinema-automation
|
|
326
|
+
if script:
|
|
327
|
+
# If existing cast file, remove it
|
|
328
|
+
if os.path.exists(output_cast_path):
|
|
329
|
+
os.unlink(output_cast_path)
|
|
330
|
+
console.print(f'Removed existing {output_cast_path}', style='bold green')
|
|
231
331
|
|
|
232
|
-
|
|
233
|
-
|
|
332
|
+
with console.status('[bold gold3]Recording with asciinema ...[/]'):
|
|
333
|
+
Command.execute(
|
|
334
|
+
f'asciinema-automation -aa "-c /bin/sh" {script} {output_cast_path} --timeout 200',
|
|
335
|
+
cls_attributes=attrs,
|
|
336
|
+
raw=True,
|
|
337
|
+
)
|
|
338
|
+
console.print(f'Generated {output_cast_path}', style='bold green')
|
|
339
|
+
elif interactive:
|
|
340
|
+
os.environ.update(attrs['env'])
|
|
341
|
+
Command.execute(f'asciinema rec -c /bin/bash --stdin --overwrite {output_cast_path}')
|
|
234
342
|
|
|
235
|
-
|
|
236
|
-
|
|
343
|
+
# Resize cast file
|
|
344
|
+
if os.path.exists(output_cast_path):
|
|
345
|
+
with console.status('[bold gold3]Cleaning up .cast and set custom settings ...'):
|
|
346
|
+
with open(output_cast_path, 'r') as f:
|
|
347
|
+
lines = f.readlines()
|
|
348
|
+
updated_lines = []
|
|
349
|
+
for ix, line in enumerate(lines):
|
|
350
|
+
tmp_line = json.loads(line)
|
|
351
|
+
if ix == 0:
|
|
352
|
+
tmp_line['width'] = width
|
|
353
|
+
tmp_line['height'] = height
|
|
354
|
+
tmp_line['env']['SHELL'] = '/bin/sh'
|
|
355
|
+
lines[0] = json.dumps(tmp_line) + '\n'
|
|
356
|
+
updated_lines.append(json.dumps(tmp_line) + '\n')
|
|
357
|
+
elif tmp_line[2].endswith(' \r'):
|
|
358
|
+
tmp_line[2] = tmp_line[2].replace(' \r', '')
|
|
359
|
+
updated_lines.append(json.dumps(tmp_line) + '\n')
|
|
360
|
+
else:
|
|
361
|
+
updated_lines.append(line)
|
|
362
|
+
with open(output_cast_path, 'w') as f:
|
|
363
|
+
f.writelines(updated_lines)
|
|
364
|
+
console.print('')
|
|
237
365
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
366
|
+
# Edit cast file to reduce long timeouts
|
|
367
|
+
with console.status('[bold gold3] Editing cast file to reduce long commands ...'):
|
|
368
|
+
Command.execute(
|
|
369
|
+
f'asciinema-edit quantize --range 1 {output_cast_path} --out {output_cast_path}.tmp',
|
|
370
|
+
cls_attributes=attrs,
|
|
371
|
+
raw=True,
|
|
372
|
+
)
|
|
373
|
+
if os.path.exists(f'{output_cast_path}.tmp'):
|
|
374
|
+
os.replace(f'{output_cast_path}.tmp', output_cast_path)
|
|
375
|
+
console.print(f'Edited {output_cast_path}', style='bold green')
|
|
376
|
+
|
|
377
|
+
# Convert to GIF
|
|
378
|
+
with console.status(f'[bold gold3]Converting to {output_gif_path} ...[/]'):
|
|
379
|
+
Command.execute(
|
|
380
|
+
f'agg {output_cast_path} {output_gif_path}',
|
|
381
|
+
cls_attributes=attrs,
|
|
382
|
+
)
|
|
383
|
+
console.print(f'Generated {output_gif_path}', style='bold green')
|
|
384
|
+
|
|
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
|
|
247
510
|
|
|
248
511
|
|
|
249
|
-
|
|
512
|
+
#--------#
|
|
513
|
+
# HEALTH #
|
|
514
|
+
#--------#
|
|
515
|
+
|
|
516
|
+
@cli.command(name='health')
|
|
250
517
|
@click.option('--json', '-json', is_flag=True, default=False, help='JSON lines output')
|
|
251
518
|
@click.option('--debug', '-debug', is_flag=True, default=False, help='Debug health output')
|
|
252
519
|
def health(json, debug):
|
|
253
|
-
"""
|
|
254
|
-
tools =
|
|
255
|
-
status = {'
|
|
256
|
-
|
|
257
|
-
def print_status(cmd, return_code, version=None, bin=None, category=None):
|
|
258
|
-
s = '[bold green]ok [/]' if return_code == 0 else '[bold red]missing [/]'
|
|
259
|
-
s = f'[bold magenta]{cmd:<15}[/] {s} '
|
|
260
|
-
if return_code == 0 and version:
|
|
261
|
-
if version == 'N/A':
|
|
262
|
-
s += f'[dim blue]{version:<12}[/]'
|
|
263
|
-
else:
|
|
264
|
-
s += f'[bold blue]{version:<12}[/]'
|
|
265
|
-
elif category:
|
|
266
|
-
s += ' '*12 + f'[dim]# secator install {category} {cmd}'
|
|
267
|
-
if bin:
|
|
268
|
-
s += f'[dim gold3]{bin}[/]'
|
|
269
|
-
console.print(s, highlight=False)
|
|
520
|
+
"""[dim]Get health status.[/]"""
|
|
521
|
+
tools = ALL_TASKS
|
|
522
|
+
status = {'secator': {}, 'languages': {}, 'tools': {}, 'addons': {}}
|
|
270
523
|
|
|
271
524
|
# Check secator
|
|
272
525
|
console.print(':wrench: [bold gold3]Checking secator ...[/]')
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
|
277
532
|
|
|
278
533
|
# Check languages
|
|
279
534
|
console.print('\n:wrench: [bold gold3]Checking installed languages ...[/]')
|
|
280
535
|
version_cmds = {'go': 'version', 'python3': '--version', 'ruby': '--version'}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
287
543
|
|
|
288
544
|
# Check tools
|
|
289
545
|
console.print('\n:wrench: [bold gold3]Checking installed tools ...[/]')
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
299
558
|
console.print('\n:wrench: [bold gold3]Checking installed addons ...[/]')
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
559
|
+
table = get_health_table()
|
|
560
|
+
with Live(table, console=console):
|
|
561
|
+
for addon in ['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
|
|
305
574
|
|
|
306
575
|
# Print JSON health
|
|
307
576
|
if json:
|
|
308
|
-
|
|
577
|
+
import json as _json
|
|
578
|
+
print(_json.dumps(status))
|
|
309
579
|
|
|
310
580
|
#---------#
|
|
311
581
|
# INSTALL #
|
|
@@ -326,9 +596,9 @@ def run_install(cmd, title, next_steps=None):
|
|
|
326
596
|
sys.exit(ret.return_code)
|
|
327
597
|
|
|
328
598
|
|
|
329
|
-
@cli.group(
|
|
599
|
+
@cli.group()
|
|
330
600
|
def install():
|
|
331
|
-
"
|
|
601
|
+
"""[dim]Install langs, tools and addons.[/]"""
|
|
332
602
|
pass
|
|
333
603
|
|
|
334
604
|
|
|
@@ -476,7 +746,7 @@ def install_tools(cmds):
|
|
|
476
746
|
|
|
477
747
|
for ix, cls in enumerate(tools):
|
|
478
748
|
with console.status(f'[bold yellow][{ix}/{len(tools)}] Installing {cls.__name__} ...'):
|
|
479
|
-
|
|
749
|
+
ToolInstaller.install(cls)
|
|
480
750
|
console.print()
|
|
481
751
|
|
|
482
752
|
|
|
@@ -502,14 +772,33 @@ def install_cves(force):
|
|
|
502
772
|
console.print(':tada: CVEs installed successfully !', style='bold green')
|
|
503
773
|
|
|
504
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
|
+
|
|
505
794
|
#-------#
|
|
506
795
|
# ALIAS #
|
|
507
796
|
#-------#
|
|
508
797
|
|
|
509
798
|
|
|
510
|
-
@cli.group(
|
|
799
|
+
@cli.group()
|
|
511
800
|
def alias():
|
|
512
|
-
"""
|
|
801
|
+
"""[dim]Configure aliases.[/]"""
|
|
513
802
|
pass
|
|
514
803
|
|
|
515
804
|
|
|
@@ -593,315 +882,6 @@ def list_aliases(silent):
|
|
|
593
882
|
return aliases
|
|
594
883
|
|
|
595
884
|
|
|
596
|
-
#-------#
|
|
597
|
-
# UTILS #
|
|
598
|
-
#-------#
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
@cli.group(aliases=['u'])
|
|
602
|
-
def utils():
|
|
603
|
-
"""Utilities."""
|
|
604
|
-
pass
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
@utils.command()
|
|
608
|
-
@click.option('--timeout', type=float, default=0.2, help='Proxy timeout (in seconds)')
|
|
609
|
-
@click.option('--number', '-n', type=int, default=1, help='Number of proxies')
|
|
610
|
-
def proxy(timeout, number):
|
|
611
|
-
"""Get random proxies from FreeProxy."""
|
|
612
|
-
proxy = FreeProxy(timeout=timeout, rand=True, anonym=True)
|
|
613
|
-
for _ in range(number):
|
|
614
|
-
url = proxy.get()
|
|
615
|
-
print(url)
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
@utils.command()
|
|
619
|
-
@click.argument('name', type=str, default=None, required=False)
|
|
620
|
-
@click.option('--host', '-h', type=str, default=None, help='Specify LHOST for revshell, otherwise autodetected.')
|
|
621
|
-
@click.option('--port', '-p', type=int, default=9001, show_default=True, help='Specify PORT for revshell')
|
|
622
|
-
@click.option('--interface', '-i', type=str, help='Interface to use to detect IP')
|
|
623
|
-
@click.option('--listen', '-l', is_flag=True, default=False, help='Spawn netcat listener on specified port')
|
|
624
|
-
@click.option('--force', is_flag=True)
|
|
625
|
-
def revshell(name, host, port, interface, listen, force):
|
|
626
|
-
"""Show reverse shell source codes and run netcat listener (-l)."""
|
|
627
|
-
if host is None: # detect host automatically
|
|
628
|
-
host = detect_host(interface)
|
|
629
|
-
if not host:
|
|
630
|
-
console.print(
|
|
631
|
-
f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of available interfaces.',
|
|
632
|
-
style='bold red')
|
|
633
|
-
return
|
|
634
|
-
|
|
635
|
-
# Download reverse shells JSON from repo
|
|
636
|
-
revshells_json = f'{REVSHELLS_FOLDER}/revshells.json'
|
|
637
|
-
if not os.path.exists(revshells_json) or force:
|
|
638
|
-
ret = Command.execute(
|
|
639
|
-
f'wget https://raw.githubusercontent.com/freelabz/secator/main/scripts/revshells.json && mv revshells.json {REVSHELLS_FOLDER}', # noqa: E501
|
|
640
|
-
cls_attributes={'shell': True}
|
|
641
|
-
)
|
|
642
|
-
if not ret.return_code == 0:
|
|
643
|
-
sys.exit(1)
|
|
644
|
-
|
|
645
|
-
# Parse JSON into shells
|
|
646
|
-
with open(revshells_json) as f:
|
|
647
|
-
shells = json.loads(f.read())
|
|
648
|
-
for sh in shells:
|
|
649
|
-
sh['alias'] = '_'.join(sh['name'].lower()
|
|
650
|
-
.replace('-c', '')
|
|
651
|
-
.replace('-e', '')
|
|
652
|
-
.replace('-i', '')
|
|
653
|
-
.replace('c#', 'cs')
|
|
654
|
-
.replace('#', '')
|
|
655
|
-
.replace('(', '')
|
|
656
|
-
.replace(')', '')
|
|
657
|
-
.strip()
|
|
658
|
-
.split(' ')).replace('_1', '')
|
|
659
|
-
cmd = re.sub(r"\s\s+", "", sh.get('command', ''), flags=re.UNICODE)
|
|
660
|
-
cmd = cmd.replace('\n', ' ')
|
|
661
|
-
sh['cmd_short'] = (cmd[:30] + '..') if len(cmd) > 30 else cmd
|
|
662
|
-
|
|
663
|
-
shell = [
|
|
664
|
-
shell for shell in shells if shell['name'] == name or shell['alias'] == name
|
|
665
|
-
]
|
|
666
|
-
if not shell:
|
|
667
|
-
console.print('Available shells:', style='bold yellow')
|
|
668
|
-
shells_str = [
|
|
669
|
-
'[bold magenta]{alias:<20}[/][dim white]{name:<20}[/][dim gold3]{cmd_short:<20}[/]'.format(**sh)
|
|
670
|
-
for sh in shells
|
|
671
|
-
]
|
|
672
|
-
console.print('\n'.join(shells_str))
|
|
673
|
-
else:
|
|
674
|
-
shell = shell[0]
|
|
675
|
-
command = shell['command']
|
|
676
|
-
alias = shell['alias']
|
|
677
|
-
name = shell['name']
|
|
678
|
-
command_str = Template(command).render(ip=host, port=port, shell='bash')
|
|
679
|
-
console.print(Rule(f'[bold gold3]{alias}[/] - [bold red]{name} REMOTE SHELL', style='bold red', align='left'))
|
|
680
|
-
lang = shell.get('lang') or 'sh'
|
|
681
|
-
if len(command.splitlines()) == 1:
|
|
682
|
-
console.print()
|
|
683
|
-
print(f'\033[0;36m{command_str}')
|
|
684
|
-
else:
|
|
685
|
-
md = Markdown(f'```{lang}\n{command_str}\n```')
|
|
686
|
-
console.print(md)
|
|
687
|
-
console.print(f'Save this script as rev.{lang} and run it on your target', style='dim italic')
|
|
688
|
-
console.print()
|
|
689
|
-
console.print(Rule(style='bold red'))
|
|
690
|
-
|
|
691
|
-
if listen:
|
|
692
|
-
console.print(f'Starting netcat listener on port {port} ...', style='bold gold3')
|
|
693
|
-
cmd = f'nc -lvnp {port}'
|
|
694
|
-
Command.execute(cmd)
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
@utils.command()
|
|
698
|
-
@click.option('--directory', '-d', type=str, default=PAYLOADS_FOLDER, show_default=True, help='HTTP server directory')
|
|
699
|
-
@click.option('--host', '-h', type=str, default=None, help='HTTP host')
|
|
700
|
-
@click.option('--port', '-p', type=int, default=9001, help='HTTP server port')
|
|
701
|
-
@click.option('--interface', '-i', type=str, default=None, help='Interface to use to auto-detect host IP')
|
|
702
|
-
def serve(directory, host, port, interface):
|
|
703
|
-
"""Run HTTP server to serve payloads."""
|
|
704
|
-
LSE_URL = 'https://github.com/diego-treitos/linux-smart-enumeration/releases/latest/download/lse.sh'
|
|
705
|
-
LINPEAS_URL = 'https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh'
|
|
706
|
-
SUDOKILLER_URL = 'https://raw.githubusercontent.com/TH3xACE/SUDO_KILLER/V3/SUDO_KILLERv3.sh'
|
|
707
|
-
PAYLOADS = [
|
|
708
|
-
{
|
|
709
|
-
'fname': 'lse.sh',
|
|
710
|
-
'description': 'Linux Smart Enumeration',
|
|
711
|
-
'command': f'wget {LSE_URL} -O lse.sh && chmod 700 lse.sh'
|
|
712
|
-
},
|
|
713
|
-
{
|
|
714
|
-
'fname': 'linpeas.sh',
|
|
715
|
-
'description': 'Linux Privilege Escalation Awesome Script',
|
|
716
|
-
'command': f'wget {LINPEAS_URL} -O linpeas.sh && chmod 700 linpeas.sh'
|
|
717
|
-
},
|
|
718
|
-
{
|
|
719
|
-
'fname': 'sudo_killer.sh',
|
|
720
|
-
'description': 'SUDO_KILLER',
|
|
721
|
-
'command': f'wget {SUDOKILLER_URL} -O sudo_killer.sh && chmod 700 sudo_killer.sh'
|
|
722
|
-
}
|
|
723
|
-
]
|
|
724
|
-
for ix, payload in enumerate(PAYLOADS):
|
|
725
|
-
descr = payload.get('description', '')
|
|
726
|
-
fname = payload['fname']
|
|
727
|
-
if not os.path.exists(f'{directory}/{fname}'):
|
|
728
|
-
with console.status(f'[bold yellow][{ix}/{len(PAYLOADS)}] Downloading {fname} [dim]({descr})[/] ...[/]'):
|
|
729
|
-
cmd = payload['command']
|
|
730
|
-
console.print(f'[bold magenta]{fname} [dim]({descr})[/] ...[/]', )
|
|
731
|
-
Command.execute(cmd, cls_attributes={'shell': True}, cwd=directory)
|
|
732
|
-
console.print()
|
|
733
|
-
|
|
734
|
-
console.print(Rule())
|
|
735
|
-
console.print(f'Available payloads in {directory}: ', style='bold yellow')
|
|
736
|
-
for fname in os.listdir(directory):
|
|
737
|
-
if not host:
|
|
738
|
-
host = detect_host(interface)
|
|
739
|
-
if not host:
|
|
740
|
-
console.print(
|
|
741
|
-
f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of interfaces.',
|
|
742
|
-
style='bold red')
|
|
743
|
-
return
|
|
744
|
-
payload = find_list_item(PAYLOADS, fname, key='fname', default={})
|
|
745
|
-
fdescr = payload.get('description', 'No description')
|
|
746
|
-
console.print(f'{fname} [dim]({fdescr})[/]', style='bold magenta')
|
|
747
|
-
console.print(f'wget http://{host}:{port}/{fname}', style='dim italic')
|
|
748
|
-
console.print('')
|
|
749
|
-
console.print(Rule())
|
|
750
|
-
console.print(f'Started HTTP server on port {port}, waiting for incoming connections ...', style='bold yellow')
|
|
751
|
-
Command.execute(f'{sys.executable} -m http.server {port}', cwd=directory)
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
@utils.command()
|
|
755
|
-
@click.argument('record_name', type=str, default=None)
|
|
756
|
-
@click.option('--script', '-s', type=str, default=None, help='Script to run. See scripts/stories/ for examples.')
|
|
757
|
-
@click.option('--interactive', '-i', is_flag=True, default=False, help='Interactive record.')
|
|
758
|
-
@click.option('--width', '-w', type=int, default=None, help='Recording width')
|
|
759
|
-
@click.option('--height', '-h', type=int, default=None, help='Recording height')
|
|
760
|
-
@click.option('--output-dir', type=str, default=f'{ROOT_FOLDER}/images')
|
|
761
|
-
def record(record_name, script, interactive, width, height, output_dir):
|
|
762
|
-
"""Record secator session using asciinema."""
|
|
763
|
-
# 120 x 30 is a good ratio for GitHub
|
|
764
|
-
width = width or console.size.width
|
|
765
|
-
height = height or console.size.height
|
|
766
|
-
attrs = {
|
|
767
|
-
'shell': False,
|
|
768
|
-
'env': {
|
|
769
|
-
'RECORD': '1',
|
|
770
|
-
'LINES': str(height),
|
|
771
|
-
'PS1': '$ ',
|
|
772
|
-
'COLUMNS': str(width),
|
|
773
|
-
'TERM': 'xterm-256color'
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
output_cast_path = f'{output_dir}/{record_name}.cast'
|
|
777
|
-
output_gif_path = f'{output_dir}/{record_name}.gif'
|
|
778
|
-
|
|
779
|
-
# Run automated 'story' script with asciinema-automation
|
|
780
|
-
if script:
|
|
781
|
-
# If existing cast file, remove it
|
|
782
|
-
if os.path.exists(output_cast_path):
|
|
783
|
-
os.unlink(output_cast_path)
|
|
784
|
-
console.print(f'Removed existing {output_cast_path}', style='bold green')
|
|
785
|
-
|
|
786
|
-
with console.status('[bold gold3]Recording with asciinema ...[/]'):
|
|
787
|
-
Command.execute(
|
|
788
|
-
f'asciinema-automation -aa "-c /bin/sh" {script} {output_cast_path} --timeout 200',
|
|
789
|
-
cls_attributes=attrs,
|
|
790
|
-
raw=True,
|
|
791
|
-
)
|
|
792
|
-
console.print(f'Generated {output_cast_path}', style='bold green')
|
|
793
|
-
elif interactive:
|
|
794
|
-
os.environ.update(attrs['env'])
|
|
795
|
-
Command.execute(f'asciinema rec -c /bin/bash --stdin --overwrite {output_cast_path}')
|
|
796
|
-
|
|
797
|
-
# Resize cast file
|
|
798
|
-
if os.path.exists(output_cast_path):
|
|
799
|
-
with console.status('[bold gold3]Cleaning up .cast and set custom settings ...'):
|
|
800
|
-
with open(output_cast_path, 'r') as f:
|
|
801
|
-
lines = f.readlines()
|
|
802
|
-
updated_lines = []
|
|
803
|
-
for ix, line in enumerate(lines):
|
|
804
|
-
tmp_line = json.loads(line)
|
|
805
|
-
if ix == 0:
|
|
806
|
-
tmp_line['width'] = width
|
|
807
|
-
tmp_line['height'] = height
|
|
808
|
-
tmp_line['env']['SHELL'] = '/bin/sh'
|
|
809
|
-
lines[0] = json.dumps(tmp_line) + '\n'
|
|
810
|
-
updated_lines.append(json.dumps(tmp_line) + '\n')
|
|
811
|
-
elif tmp_line[2].endswith(' \r'):
|
|
812
|
-
tmp_line[2] = tmp_line[2].replace(' \r', '')
|
|
813
|
-
updated_lines.append(json.dumps(tmp_line) + '\n')
|
|
814
|
-
else:
|
|
815
|
-
updated_lines.append(line)
|
|
816
|
-
with open(output_cast_path, 'w') as f:
|
|
817
|
-
f.writelines(updated_lines)
|
|
818
|
-
console.print('')
|
|
819
|
-
|
|
820
|
-
# Edit cast file to reduce long timeouts
|
|
821
|
-
with console.status('[bold gold3] Editing cast file to reduce long commands ...'):
|
|
822
|
-
Command.execute(
|
|
823
|
-
f'asciinema-edit quantize --range 1 {output_cast_path} --out {output_cast_path}.tmp',
|
|
824
|
-
cls_attributes=attrs,
|
|
825
|
-
raw=True,
|
|
826
|
-
)
|
|
827
|
-
if os.path.exists(f'{output_cast_path}.tmp'):
|
|
828
|
-
os.replace(f'{output_cast_path}.tmp', output_cast_path)
|
|
829
|
-
console.print(f'Edited {output_cast_path}', style='bold green')
|
|
830
|
-
|
|
831
|
-
# Convert to GIF
|
|
832
|
-
with console.status(f'[bold gold3]Converting to {output_gif_path} ...[/]'):
|
|
833
|
-
Command.execute(
|
|
834
|
-
f'agg {output_cast_path} {output_gif_path}',
|
|
835
|
-
cls_attributes=attrs,
|
|
836
|
-
)
|
|
837
|
-
console.print(f'Generated {output_gif_path}', style='bold green')
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
@utils.group('build')
|
|
841
|
-
def build():
|
|
842
|
-
"""Build secator."""
|
|
843
|
-
pass
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
@build.command('pypi')
|
|
847
|
-
def build_pypi():
|
|
848
|
-
"""Build secator PyPI package."""
|
|
849
|
-
if not DEV_PACKAGE:
|
|
850
|
-
console.print('[bold red]You MUST use a development version of secator to make builds.[/]')
|
|
851
|
-
sys.exit(1)
|
|
852
|
-
if not BUILD_ADDON_ENABLED:
|
|
853
|
-
console.print('[bold red]Missing build addon: please run `secator install addons build`')
|
|
854
|
-
sys.exit(1)
|
|
855
|
-
with console.status('[bold gold3]Building PyPI package...[/]'):
|
|
856
|
-
ret = Command.execute(f'{sys.executable} -m hatch build', name='hatch build', cwd=ROOT_FOLDER)
|
|
857
|
-
sys.exit(ret.return_code)
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
@build.command('docker')
|
|
861
|
-
@click.option('--dev', '-dev', is_flag=True, default=False, help='Build dev version')
|
|
862
|
-
def build_docker(dev):
|
|
863
|
-
"""Build secator Docker image."""
|
|
864
|
-
version = 'dev' if dev else VERSION
|
|
865
|
-
with console.status('[bold gold3]Building Docker image...[/]'):
|
|
866
|
-
ret = Command.execute(f'docker build -t freelabz/secator:{version} .', name='docker build', cwd=ROOT_FOLDER)
|
|
867
|
-
sys.exit(ret.return_code)
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
@utils.group('publish')
|
|
871
|
-
def publish():
|
|
872
|
-
"""Publish secator."""
|
|
873
|
-
pass
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
@publish.command('pypi')
|
|
877
|
-
def publish_pypi():
|
|
878
|
-
"""Publish secator PyPI package."""
|
|
879
|
-
if not DEV_PACKAGE:
|
|
880
|
-
console.print('[bold red]You MUST use a development version of secator to make builds.[/]')
|
|
881
|
-
sys.exit(1)
|
|
882
|
-
if not BUILD_ADDON_ENABLED:
|
|
883
|
-
console.print('[bold red]Missing build addon: please run `secator install addons build`')
|
|
884
|
-
sys.exit(1)
|
|
885
|
-
os.environ['HATCH_INDEX_USER'] = '__token__'
|
|
886
|
-
hatch_token = os.environ.get('HATCH_INDEX_AUTH')
|
|
887
|
-
if not hatch_token:
|
|
888
|
-
console.print('[bold red]Missing PyPI auth token (HATCH_INDEX_AUTH env variable).')
|
|
889
|
-
sys.exit(1)
|
|
890
|
-
with console.status('[bold gold3]Publishing PyPI package...[/]'):
|
|
891
|
-
ret = Command.execute(f'{sys.executable} -m hatch publish', name='hatch publish', cwd=ROOT_FOLDER)
|
|
892
|
-
sys.exit(ret.return_code)
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
@publish.command('docker')
|
|
896
|
-
@click.option('--dev', '-dev', is_flag=True, default=False, help='Build dev version')
|
|
897
|
-
def publish_docker(dev):
|
|
898
|
-
"""Publish secator Docker image."""
|
|
899
|
-
version = 'dev' if dev else VERSION
|
|
900
|
-
with console.status('[bold gold3]Publishing PyPI package...[/]'):
|
|
901
|
-
ret = Command.execute(f'docker push freelabz/secator:{version}', name='docker push', cwd=ROOT_FOLDER)
|
|
902
|
-
sys.exit(ret.return_code)
|
|
903
|
-
|
|
904
|
-
|
|
905
885
|
#------#
|
|
906
886
|
# TEST #
|
|
907
887
|
#------#
|
|
@@ -909,11 +889,11 @@ def publish_docker(dev):
|
|
|
909
889
|
|
|
910
890
|
@cli.group(cls=OrderedGroup)
|
|
911
891
|
def test():
|
|
912
|
-
"""
|
|
892
|
+
"""[dim]Run tests."""
|
|
913
893
|
if not DEV_PACKAGE:
|
|
914
894
|
console.print('[bold red]You MUST use a development version of secator to run tests.[/]')
|
|
915
895
|
sys.exit(1)
|
|
916
|
-
if not
|
|
896
|
+
if not ADDONS_ENABLED['dev']:
|
|
917
897
|
console.print('[bold red]Missing dev addon: please run `secator install addons dev`')
|
|
918
898
|
sys.exit(1)
|
|
919
899
|
pass
|