secator 0.2.0__py2.py3-none-any.whl → 0.3.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/cli.py +445 -405
- secator/decorators.py +2 -2
- secator/definitions.py +25 -5
- secator/exporters/txt.py +1 -1
- secator/installer.py +192 -0
- secator/runners/command.py +2 -16
- 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/subfinder.py +1 -0
- secator/utils_test.py +1 -0
- {secator-0.2.0.dist-info → secator-0.3.0.dist-info}/METADATA +1 -1
- {secator-0.2.0.dist-info → secator-0.3.0.dist-info}/RECORD +26 -25
- {secator-0.2.0.dist-info → secator-0.3.0.dist-info}/WHEEL +0 -0
- {secator-0.2.0.dist-info → secator-0.3.0.dist-info}/entry_points.txt +0 -0
- {secator-0.2.0.dist-info → secator-0.3.0.dist-info}/licenses/LICENSE +0 -0
secator/cli.py
CHANGED
|
@@ -12,17 +12,15 @@ from rich.rule import Rule
|
|
|
12
12
|
|
|
13
13
|
from secator.config import ConfigLoader
|
|
14
14
|
from secator.decorators import OrderedGroup, register_runner
|
|
15
|
-
from secator.definitions import (ASCII, CVES_FOLDER, DATA_FOLDER, # noqa: F401
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
TRACE_ADDON_ENABLED, VERSION, WORKER_ADDON_ENABLED)
|
|
15
|
+
from secator.definitions import (ASCII, BUILD_ADDON_ENABLED, CVES_FOLDER, DATA_FOLDER, DEV_ADDON_ENABLED, # noqa: F401
|
|
16
|
+
DEV_PACKAGE, GOOGLE_ADDON_ENABLED, VERSION_LATEST, LIB_FOLDER, MONGODB_ADDON_ENABLED,
|
|
17
|
+
VERSION_OBSOLETE, OPT_NOT_SUPPORTED, PAYLOADS_FOLDER, REDIS_ADDON_ENABLED, REVSHELLS_FOLDER, ROOT_FOLDER,
|
|
18
|
+
TRACE_ADDON_ENABLED, VERSION, VERSION_STR, WORKER_ADDON_ENABLED)
|
|
19
|
+
from secator.installer import ToolInstaller
|
|
21
20
|
from secator.rich import console
|
|
22
21
|
from secator.runners import Command
|
|
23
22
|
from secator.serializers.dataclass import loads_dataclass
|
|
24
|
-
from secator.utils import
|
|
25
|
-
flatten, print_results_table)
|
|
23
|
+
from secator.utils import debug, detect_host, discover_tasks, find_list_item, flatten, print_results_table
|
|
26
24
|
|
|
27
25
|
click.rich_click.USE_RICH_MARKUP = True
|
|
28
26
|
|
|
@@ -32,25 +30,32 @@ ALL_WORKFLOWS = ALL_CONFIGS.workflow
|
|
|
32
30
|
ALL_SCANS = ALL_CONFIGS.scan
|
|
33
31
|
|
|
34
32
|
|
|
33
|
+
def print_version():
|
|
34
|
+
console.print(f'[bold gold3]Current version[/]: {VERSION}', highlight=False)
|
|
35
|
+
console.print(f'[bold gold3]Latest version[/]: {VERSION_LATEST}', highlight=False)
|
|
36
|
+
console.print(f'[bold gold3]Python binary[/]: {sys.executable}')
|
|
37
|
+
if DEV_PACKAGE:
|
|
38
|
+
console.print(f'[bold gold3]Root folder[/]: {ROOT_FOLDER}')
|
|
39
|
+
console.print(f'[bold gold3]Lib folder[/]: {LIB_FOLDER}')
|
|
40
|
+
|
|
41
|
+
|
|
35
42
|
#-----#
|
|
36
43
|
# CLI #
|
|
37
44
|
#-----#
|
|
38
45
|
|
|
39
46
|
@click.group(cls=OrderedGroup, invoke_without_command=True)
|
|
40
|
-
@click.option('--no-banner', '-nb', is_flag=True, default=False)
|
|
41
47
|
@click.option('--version', '-version', is_flag=True, default=False)
|
|
42
48
|
@click.pass_context
|
|
43
|
-
def cli(ctx,
|
|
49
|
+
def cli(ctx, version):
|
|
44
50
|
"""Secator CLI."""
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
console.print(ASCII, highlight=False)
|
|
52
|
+
if VERSION_OBSOLETE:
|
|
53
|
+
console.print(
|
|
54
|
+
'[bold red]:warning: secator version is outdated: '
|
|
55
|
+
f'run "secator update" to install the newest version ({VERSION_LATEST}).\n')
|
|
47
56
|
if ctx.invoked_subcommand is None:
|
|
48
57
|
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}')
|
|
58
|
+
print_version()
|
|
54
59
|
else:
|
|
55
60
|
ctx.get_help()
|
|
56
61
|
|
|
@@ -147,84 +152,406 @@ def worker(hostname, concurrency, reload, queue, pool, check, dev, stop, show):
|
|
|
147
152
|
Command.execute(cmd, name='secator worker')
|
|
148
153
|
|
|
149
154
|
|
|
150
|
-
|
|
151
|
-
#
|
|
152
|
-
|
|
155
|
+
#-------#
|
|
156
|
+
# UTILS #
|
|
157
|
+
#-------#
|
|
153
158
|
|
|
154
159
|
|
|
155
|
-
@cli.group(aliases=['
|
|
156
|
-
def
|
|
157
|
-
"""
|
|
160
|
+
@cli.group(aliases=['u'])
|
|
161
|
+
def util():
|
|
162
|
+
"""Run a utility."""
|
|
158
163
|
pass
|
|
159
164
|
|
|
160
165
|
|
|
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)
|
|
166
|
+
@util.command()
|
|
167
|
+
@click.option('--timeout', type=float, default=0.2, help='Proxy timeout (in seconds)')
|
|
168
|
+
@click.option('--number', '-n', type=int, default=1, help='Number of proxies')
|
|
169
|
+
def proxy(timeout, number):
|
|
170
|
+
"""Get random proxies from FreeProxy."""
|
|
171
|
+
proxy = FreeProxy(timeout=timeout, rand=True, anonym=True)
|
|
172
|
+
for _ in range(number):
|
|
173
|
+
url = proxy.get()
|
|
174
|
+
print(url)
|
|
174
175
|
|
|
175
176
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
177
|
+
@util.command()
|
|
178
|
+
@click.argument('name', type=str, default=None, required=False)
|
|
179
|
+
@click.option('--host', '-h', type=str, default=None, help='Specify LHOST for revshell, otherwise autodetected.')
|
|
180
|
+
@click.option('--port', '-p', type=int, default=9001, show_default=True, help='Specify PORT for revshell')
|
|
181
|
+
@click.option('--interface', '-i', type=str, help='Interface to use to detect IP')
|
|
182
|
+
@click.option('--listen', '-l', is_flag=True, default=False, help='Spawn netcat listener on specified port')
|
|
183
|
+
@click.option('--force', is_flag=True)
|
|
184
|
+
def revshell(name, host, port, interface, listen, force):
|
|
185
|
+
"""Show reverse shell source codes and run netcat listener (-l)."""
|
|
186
|
+
if host is None: # detect host automatically
|
|
187
|
+
host = detect_host(interface)
|
|
188
|
+
if not host:
|
|
189
|
+
console.print(
|
|
190
|
+
f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of available interfaces.',
|
|
191
|
+
style='bold red')
|
|
192
|
+
return
|
|
179
193
|
|
|
180
|
-
#
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
#
|
|
194
|
+
# Download reverse shells JSON from repo
|
|
195
|
+
revshells_json = f'{REVSHELLS_FOLDER}/revshells.json'
|
|
196
|
+
if not os.path.exists(revshells_json) or force:
|
|
197
|
+
ret = Command.execute(
|
|
198
|
+
f'wget https://raw.githubusercontent.com/freelabz/secator/main/scripts/revshells.json && mv revshells.json {REVSHELLS_FOLDER}', # noqa: E501
|
|
199
|
+
cls_attributes={'shell': True}
|
|
200
|
+
)
|
|
201
|
+
if not ret.return_code == 0:
|
|
202
|
+
sys.exit(1)
|
|
185
203
|
|
|
186
|
-
#
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
204
|
+
# Parse JSON into shells
|
|
205
|
+
with open(revshells_json) as f:
|
|
206
|
+
shells = json.loads(f.read())
|
|
207
|
+
for sh in shells:
|
|
208
|
+
sh['alias'] = '_'.join(sh['name'].lower()
|
|
209
|
+
.replace('-c', '')
|
|
210
|
+
.replace('-e', '')
|
|
211
|
+
.replace('-i', '')
|
|
212
|
+
.replace('c#', 'cs')
|
|
213
|
+
.replace('#', '')
|
|
214
|
+
.replace('(', '')
|
|
215
|
+
.replace(')', '')
|
|
216
|
+
.strip()
|
|
217
|
+
.split(' ')).replace('_1', '')
|
|
218
|
+
cmd = re.sub(r"\s\s+", "", sh.get('command', ''), flags=re.UNICODE)
|
|
219
|
+
cmd = cmd.replace('\n', ' ')
|
|
220
|
+
sh['cmd_short'] = (cmd[:30] + '..') if len(cmd) > 30 else cmd
|
|
190
221
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
222
|
+
shell = [
|
|
223
|
+
shell for shell in shells if shell['name'] == name or shell['alias'] == name
|
|
224
|
+
]
|
|
225
|
+
if not shell:
|
|
226
|
+
console.print('Available shells:', style='bold yellow')
|
|
227
|
+
shells_str = [
|
|
228
|
+
'[bold magenta]{alias:<20}[/][dim white]{name:<20}[/][dim gold3]{cmd_short:<20}[/]'.format(**sh)
|
|
229
|
+
for sh in shells
|
|
230
|
+
]
|
|
231
|
+
console.print('\n'.join(shells_str))
|
|
232
|
+
else:
|
|
233
|
+
shell = shell[0]
|
|
234
|
+
command = shell['command']
|
|
235
|
+
alias = shell['alias']
|
|
236
|
+
name = shell['name']
|
|
237
|
+
command_str = Template(command).render(ip=host, port=port, shell='bash')
|
|
238
|
+
console.print(Rule(f'[bold gold3]{alias}[/] - [bold red]{name} REMOTE SHELL', style='bold red', align='left'))
|
|
239
|
+
lang = shell.get('lang') or 'sh'
|
|
240
|
+
if len(command.splitlines()) == 1:
|
|
241
|
+
console.print()
|
|
242
|
+
print(f'\033[0;36m{command_str}')
|
|
243
|
+
else:
|
|
244
|
+
md = Markdown(f'```{lang}\n{command_str}\n```')
|
|
245
|
+
console.print(md)
|
|
246
|
+
console.print(f'Save this script as rev.{lang} and run it on your target', style='dim italic')
|
|
247
|
+
console.print()
|
|
248
|
+
console.print(Rule(style='bold red'))
|
|
196
249
|
|
|
250
|
+
if listen:
|
|
251
|
+
console.print(f'Starting netcat listener on port {port} ...', style='bold gold3')
|
|
252
|
+
cmd = f'nc -lvnp {port}'
|
|
253
|
+
Command.execute(cmd)
|
|
197
254
|
|
|
198
|
-
#--------#
|
|
199
|
-
# HEALTH #
|
|
200
|
-
#--------#
|
|
201
255
|
|
|
256
|
+
@util.command()
|
|
257
|
+
@click.option('--directory', '-d', type=str, default=PAYLOADS_FOLDER, show_default=True, help='HTTP server directory')
|
|
258
|
+
@click.option('--host', '-h', type=str, default=None, help='HTTP host')
|
|
259
|
+
@click.option('--port', '-p', type=int, default=9001, help='HTTP server port')
|
|
260
|
+
@click.option('--interface', '-i', type=str, default=None, help='Interface to use to auto-detect host IP')
|
|
261
|
+
def serve(directory, host, port, interface):
|
|
262
|
+
"""Run HTTP server to serve payloads."""
|
|
263
|
+
LSE_URL = 'https://github.com/diego-treitos/linux-smart-enumeration/releases/latest/download/lse.sh'
|
|
264
|
+
LINPEAS_URL = 'https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh'
|
|
265
|
+
SUDOKILLER_URL = 'https://raw.githubusercontent.com/TH3xACE/SUDO_KILLER/V3/SUDO_KILLERv3.sh'
|
|
266
|
+
PAYLOADS = [
|
|
267
|
+
{
|
|
268
|
+
'fname': 'lse.sh',
|
|
269
|
+
'description': 'Linux Smart Enumeration',
|
|
270
|
+
'command': f'wget {LSE_URL} -O lse.sh && chmod 700 lse.sh'
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
'fname': 'linpeas.sh',
|
|
274
|
+
'description': 'Linux Privilege Escalation Awesome Script',
|
|
275
|
+
'command': f'wget {LINPEAS_URL} -O linpeas.sh && chmod 700 linpeas.sh'
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
'fname': 'sudo_killer.sh',
|
|
279
|
+
'description': 'SUDO_KILLER',
|
|
280
|
+
'command': f'wget {SUDOKILLER_URL} -O sudo_killer.sh && chmod 700 sudo_killer.sh'
|
|
281
|
+
}
|
|
282
|
+
]
|
|
283
|
+
for ix, payload in enumerate(PAYLOADS):
|
|
284
|
+
descr = payload.get('description', '')
|
|
285
|
+
fname = payload['fname']
|
|
286
|
+
if not os.path.exists(f'{directory}/{fname}'):
|
|
287
|
+
with console.status(f'[bold yellow][{ix}/{len(PAYLOADS)}] Downloading {fname} [dim]({descr})[/] ...[/]'):
|
|
288
|
+
cmd = payload['command']
|
|
289
|
+
console.print(f'[bold magenta]{fname} [dim]({descr})[/] ...[/]', )
|
|
290
|
+
Command.execute(cmd, cls_attributes={'shell': True}, cwd=directory)
|
|
291
|
+
console.print()
|
|
202
292
|
|
|
203
|
-
|
|
204
|
-
|
|
293
|
+
console.print(Rule())
|
|
294
|
+
console.print(f'Available payloads in {directory}: ', style='bold yellow')
|
|
295
|
+
for fname in os.listdir(directory):
|
|
296
|
+
if not host:
|
|
297
|
+
host = detect_host(interface)
|
|
298
|
+
if not host:
|
|
299
|
+
console.print(
|
|
300
|
+
f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of interfaces.',
|
|
301
|
+
style='bold red')
|
|
302
|
+
return
|
|
303
|
+
payload = find_list_item(PAYLOADS, fname, key='fname', default={})
|
|
304
|
+
fdescr = payload.get('description', 'No description')
|
|
305
|
+
console.print(f'{fname} [dim]({fdescr})[/]', style='bold magenta')
|
|
306
|
+
console.print(f'wget http://{host}:{port}/{fname}', style='dim italic')
|
|
307
|
+
console.print('')
|
|
308
|
+
console.print(Rule())
|
|
309
|
+
console.print(f'Started HTTP server on port {port}, waiting for incoming connections ...', style='bold yellow')
|
|
310
|
+
Command.execute(f'{sys.executable} -m http.server {port}', cwd=directory)
|
|
205
311
|
|
|
206
|
-
Args:
|
|
207
|
-
command (str): Command to check.
|
|
208
312
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
313
|
+
@util.command()
|
|
314
|
+
@click.argument('record_name', type=str, default=None)
|
|
315
|
+
@click.option('--script', '-s', type=str, default=None, help='Script to run. See scripts/stories/ for examples.')
|
|
316
|
+
@click.option('--interactive', '-i', is_flag=True, default=False, help='Interactive record.')
|
|
317
|
+
@click.option('--width', '-w', type=int, default=None, help='Recording width')
|
|
318
|
+
@click.option('--height', '-h', type=int, default=None, help='Recording height')
|
|
319
|
+
@click.option('--output-dir', type=str, default=f'{ROOT_FOLDER}/images')
|
|
320
|
+
def record(record_name, script, interactive, width, height, output_dir):
|
|
321
|
+
"""Record secator session using asciinema."""
|
|
322
|
+
# 120 x 30 is a good ratio for GitHub
|
|
323
|
+
width = width or console.size.width
|
|
324
|
+
height = height or console.size.height
|
|
325
|
+
attrs = {
|
|
326
|
+
'shell': False,
|
|
327
|
+
'env': {
|
|
328
|
+
'RECORD': '1',
|
|
329
|
+
'LINES': str(height),
|
|
330
|
+
'PS1': '$ ',
|
|
331
|
+
'COLUMNS': str(width),
|
|
332
|
+
'TERM': 'xterm-256color'
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
output_cast_path = f'{output_dir}/{record_name}.cast'
|
|
336
|
+
output_gif_path = f'{output_dir}/{record_name}.gif'
|
|
213
337
|
|
|
338
|
+
# Run automated 'story' script with asciinema-automation
|
|
339
|
+
if script:
|
|
340
|
+
# If existing cast file, remove it
|
|
341
|
+
if os.path.exists(output_cast_path):
|
|
342
|
+
os.unlink(output_cast_path)
|
|
343
|
+
console.print(f'Removed existing {output_cast_path}', style='bold green')
|
|
214
344
|
|
|
215
|
-
|
|
216
|
-
|
|
345
|
+
with console.status('[bold gold3]Recording with asciinema ...[/]'):
|
|
346
|
+
Command.execute(
|
|
347
|
+
f'asciinema-automation -aa "-c /bin/sh" {script} {output_cast_path} --timeout 200',
|
|
348
|
+
cls_attributes=attrs,
|
|
349
|
+
raw=True,
|
|
350
|
+
)
|
|
351
|
+
console.print(f'Generated {output_cast_path}', style='bold green')
|
|
352
|
+
elif interactive:
|
|
353
|
+
os.environ.update(attrs['env'])
|
|
354
|
+
Command.execute(f'asciinema rec -c /bin/bash --stdin --overwrite {output_cast_path}')
|
|
217
355
|
|
|
218
|
-
|
|
219
|
-
|
|
356
|
+
# Resize cast file
|
|
357
|
+
if os.path.exists(output_cast_path):
|
|
358
|
+
with console.status('[bold gold3]Cleaning up .cast and set custom settings ...'):
|
|
359
|
+
with open(output_cast_path, 'r') as f:
|
|
360
|
+
lines = f.readlines()
|
|
361
|
+
updated_lines = []
|
|
362
|
+
for ix, line in enumerate(lines):
|
|
363
|
+
tmp_line = json.loads(line)
|
|
364
|
+
if ix == 0:
|
|
365
|
+
tmp_line['width'] = width
|
|
366
|
+
tmp_line['height'] = height
|
|
367
|
+
tmp_line['env']['SHELL'] = '/bin/sh'
|
|
368
|
+
lines[0] = json.dumps(tmp_line) + '\n'
|
|
369
|
+
updated_lines.append(json.dumps(tmp_line) + '\n')
|
|
370
|
+
elif tmp_line[2].endswith(' \r'):
|
|
371
|
+
tmp_line[2] = tmp_line[2].replace(' \r', '')
|
|
372
|
+
updated_lines.append(json.dumps(tmp_line) + '\n')
|
|
373
|
+
else:
|
|
374
|
+
updated_lines.append(line)
|
|
375
|
+
with open(output_cast_path, 'w') as f:
|
|
376
|
+
f.writelines(updated_lines)
|
|
377
|
+
console.print('')
|
|
220
378
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
379
|
+
# Edit cast file to reduce long timeouts
|
|
380
|
+
with console.status('[bold gold3] Editing cast file to reduce long commands ...'):
|
|
381
|
+
Command.execute(
|
|
382
|
+
f'asciinema-edit quantize --range 1 {output_cast_path} --out {output_cast_path}.tmp',
|
|
383
|
+
cls_attributes=attrs,
|
|
384
|
+
raw=True,
|
|
385
|
+
)
|
|
386
|
+
if os.path.exists(f'{output_cast_path}.tmp'):
|
|
387
|
+
os.replace(f'{output_cast_path}.tmp', output_cast_path)
|
|
388
|
+
console.print(f'Edited {output_cast_path}', style='bold green')
|
|
389
|
+
|
|
390
|
+
# Convert to GIF
|
|
391
|
+
with console.status(f'[bold gold3]Converting to {output_gif_path} ...[/]'):
|
|
392
|
+
Command.execute(
|
|
393
|
+
f'agg {output_cast_path} {output_gif_path}',
|
|
394
|
+
cls_attributes=attrs,
|
|
395
|
+
)
|
|
396
|
+
console.print(f'Generated {output_gif_path}', style='bold green')
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@util.group('build')
|
|
400
|
+
def build():
|
|
401
|
+
"""Build secator."""
|
|
402
|
+
if not DEV_PACKAGE:
|
|
403
|
+
console.print('[bold red]You MUST use a development version of secator to make builds.[/]')
|
|
404
|
+
sys.exit(1)
|
|
405
|
+
pass
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@build.command('pypi')
|
|
409
|
+
def build_pypi():
|
|
410
|
+
"""Build secator PyPI package."""
|
|
411
|
+
if not BUILD_ADDON_ENABLED:
|
|
412
|
+
console.print('[bold red]Missing build addon: please run `secator install addons build`')
|
|
413
|
+
sys.exit(1)
|
|
414
|
+
with console.status('[bold gold3]Building PyPI package...[/]'):
|
|
415
|
+
ret = Command.execute(f'{sys.executable} -m hatch build', name='hatch build', cwd=ROOT_FOLDER)
|
|
416
|
+
sys.exit(ret.return_code)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@build.command('docker')
|
|
420
|
+
@click.option('--tag', '-t', type=str, default=None, help='Specific tag')
|
|
421
|
+
@click.option('--latest', '-l', is_flag=True, default=False, help='Latest tag')
|
|
422
|
+
def build_docker(tag, latest):
|
|
423
|
+
"""Build secator Docker image."""
|
|
424
|
+
if not tag:
|
|
425
|
+
tag = VERSION if latest else 'dev'
|
|
426
|
+
cmd = f'docker build -t freelabz/secator:{tag}'
|
|
427
|
+
if latest:
|
|
428
|
+
cmd += ' -t freelabz/secator:latest'
|
|
429
|
+
cmd += ' .'
|
|
430
|
+
with console.status('[bold gold3]Building Docker image...[/]'):
|
|
431
|
+
ret = Command.execute(cmd, name='docker build', cwd=ROOT_FOLDER)
|
|
432
|
+
sys.exit(ret.return_code)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@util.group('publish')
|
|
436
|
+
def publish():
|
|
437
|
+
"""Publish secator."""
|
|
438
|
+
if not DEV_PACKAGE:
|
|
439
|
+
console.print('[bold red]You MUST use a development version of secator to publish builds.[/]')
|
|
440
|
+
sys.exit(1)
|
|
441
|
+
pass
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@publish.command('pypi')
|
|
445
|
+
def publish_pypi():
|
|
446
|
+
"""Publish secator PyPI package."""
|
|
447
|
+
if not BUILD_ADDON_ENABLED:
|
|
448
|
+
console.print('[bold red]Missing build addon: please run `secator install addons build`')
|
|
449
|
+
sys.exit(1)
|
|
450
|
+
os.environ['HATCH_INDEX_USER'] = '__token__'
|
|
451
|
+
hatch_token = os.environ.get('HATCH_INDEX_AUTH')
|
|
452
|
+
if not hatch_token:
|
|
453
|
+
console.print('[bold red]Missing PyPI auth token (HATCH_INDEX_AUTH env variable).')
|
|
454
|
+
sys.exit(1)
|
|
455
|
+
with console.status('[bold gold3]Publishing PyPI package...[/]'):
|
|
456
|
+
ret = Command.execute(f'{sys.executable} -m hatch publish', name='hatch publish', cwd=ROOT_FOLDER)
|
|
457
|
+
sys.exit(ret.return_code)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
@publish.command('docker')
|
|
461
|
+
@click.option('--tag', '-t', default=None, help='Specific tag')
|
|
462
|
+
@click.option('--latest', '-l', is_flag=True, default=False, help='Latest tag')
|
|
463
|
+
def publish_docker(tag, latest):
|
|
464
|
+
"""Publish secator Docker image."""
|
|
465
|
+
if not tag:
|
|
466
|
+
tag = VERSION if latest else 'dev'
|
|
467
|
+
cmd = f'docker push freelabz/secator:{tag}'
|
|
468
|
+
cmd2 = 'docker push freelabz/secator:latest'
|
|
469
|
+
with console.status(f'[bold gold3]Publishing Docker image {tag}...[/]'):
|
|
470
|
+
ret = Command.execute(cmd, name=f'docker push ({tag})', cwd=ROOT_FOLDER)
|
|
471
|
+
if latest:
|
|
472
|
+
ret2 = Command.execute(cmd2, name='docker push (latest)')
|
|
473
|
+
sys.exit(max(ret.return_code, ret2.return_code))
|
|
474
|
+
sys.exit(ret.return_code)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
#--------#
|
|
478
|
+
# REPORT #
|
|
479
|
+
#--------#
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
@cli.group(aliases=['r'])
|
|
483
|
+
def report():
|
|
484
|
+
"""View previous reports."""
|
|
485
|
+
pass
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
@report.command('show')
|
|
489
|
+
@click.argument('json_path')
|
|
490
|
+
@click.option('-e', '--exclude-fields', type=str, default='', help='List of fields to exclude (comma-separated)')
|
|
491
|
+
def report_show(json_path, exclude_fields):
|
|
492
|
+
"""Show a JSON report as a nicely-formatted table."""
|
|
493
|
+
with open(json_path, 'r') as f:
|
|
494
|
+
report = loads_dataclass(f.read())
|
|
495
|
+
results = flatten(list(report['results'].values()))
|
|
496
|
+
exclude_fields = exclude_fields.split(',')
|
|
497
|
+
print_results_table(
|
|
498
|
+
results,
|
|
499
|
+
title=report['info']['title'],
|
|
500
|
+
exclude_fields=exclude_fields)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
#--------#
|
|
504
|
+
# DEPLOY #
|
|
505
|
+
#--------#
|
|
506
|
+
|
|
507
|
+
# TODO: work on this
|
|
508
|
+
# @cli.group(aliases=['d'])
|
|
509
|
+
# def deploy():
|
|
510
|
+
# """Deploy secator."""
|
|
511
|
+
# pass
|
|
512
|
+
|
|
513
|
+
# @deploy.command()
|
|
514
|
+
# def docker_compose():
|
|
515
|
+
# """Deploy secator on docker-compose."""
|
|
516
|
+
# pass
|
|
517
|
+
|
|
518
|
+
# @deploy.command()
|
|
519
|
+
# @click.option('-t', '--target', type=str, default='minikube', help='Deployment target amongst minikube, gke')
|
|
520
|
+
# def k8s():
|
|
521
|
+
# """Deploy secator on Kubernetes."""
|
|
522
|
+
# pass
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
#--------#
|
|
526
|
+
# HEALTH #
|
|
527
|
+
#--------#
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def which(command):
|
|
531
|
+
"""Run which on a command.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
command (str): Command to check.
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
secator.Command: Command instance.
|
|
538
|
+
"""
|
|
539
|
+
return Command.execute(f'which {command}', quiet=True, print_errors=False)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def get_version_cls(cls):
|
|
543
|
+
"""Get version for a Command.
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
cls: Command class.
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
string: Version string or 'n/a' if not found.
|
|
550
|
+
"""
|
|
551
|
+
base_cmd = cls.cmd.split(' ')[0]
|
|
552
|
+
if cls.version_flag == OPT_NOT_SUPPORTED:
|
|
553
|
+
return 'N/A'
|
|
554
|
+
version_flag = cls.version_flag or f'{cls.opt_prefix}version'
|
|
228
555
|
version_cmd = f'{base_cmd} {version_flag}'
|
|
229
556
|
return get_version(version_cmd)
|
|
230
557
|
|
|
@@ -246,11 +573,11 @@ def get_version(version_cmd):
|
|
|
246
573
|
return match[0]
|
|
247
574
|
|
|
248
575
|
|
|
249
|
-
@cli.command(name='health'
|
|
576
|
+
@cli.command(name='health')
|
|
250
577
|
@click.option('--json', '-json', is_flag=True, default=False, help='JSON lines output')
|
|
251
578
|
@click.option('--debug', '-debug', is_flag=True, default=False, help='Debug health output')
|
|
252
579
|
def health(json, debug):
|
|
253
|
-
"""
|
|
580
|
+
"""[dim]Get health status.[/]"""
|
|
254
581
|
tools = [cls for cls in ALL_TASKS]
|
|
255
582
|
status = {'tools': {}, 'languages': {}, 'secator': {}}
|
|
256
583
|
|
|
@@ -269,43 +596,49 @@ def health(json, debug):
|
|
|
269
596
|
console.print(s, highlight=False)
|
|
270
597
|
|
|
271
598
|
# Check secator
|
|
272
|
-
|
|
599
|
+
if not json:
|
|
600
|
+
console.print(':wrench: [bold gold3]Checking secator ...[/]')
|
|
273
601
|
ret = which('secator')
|
|
274
602
|
if not json:
|
|
275
603
|
print_status('secator', ret.return_code, VERSION, ret.output, None)
|
|
276
|
-
status['secator'] = {'installed': ret.return_code == 0}
|
|
604
|
+
status['secator'] = {'installed': ret.return_code == 0, 'version': VERSION}
|
|
277
605
|
|
|
278
606
|
# Check languages
|
|
279
|
-
|
|
607
|
+
if not json:
|
|
608
|
+
console.print('\n:wrench: [bold gold3]Checking installed languages ...[/]')
|
|
280
609
|
version_cmds = {'go': 'version', 'python3': '--version', 'ruby': '--version'}
|
|
281
610
|
for lang, version_flag in version_cmds.items():
|
|
282
611
|
ret = which(lang)
|
|
283
|
-
|
|
612
|
+
version = get_version(f'{lang} {version_flag}')
|
|
284
613
|
if not json:
|
|
285
|
-
print_status(lang, ret.return_code,
|
|
286
|
-
status['languages'][lang] = {'installed': ret.return_code == 0}
|
|
614
|
+
print_status(lang, ret.return_code, version, ret.output, 'langs')
|
|
615
|
+
status['languages'][lang] = {'installed': ret.return_code == 0, 'version': version}
|
|
287
616
|
|
|
288
617
|
# Check tools
|
|
289
|
-
|
|
618
|
+
if not json:
|
|
619
|
+
console.print('\n:wrench: [bold gold3]Checking installed tools ...[/]')
|
|
290
620
|
for tool in tools:
|
|
291
621
|
cmd = tool.cmd.split(' ')[0]
|
|
292
622
|
ret = which(cmd)
|
|
293
|
-
|
|
623
|
+
version = get_version_cls(tool)
|
|
294
624
|
if not json:
|
|
295
|
-
print_status(tool.__name__, ret.return_code,
|
|
296
|
-
status['tools'][tool.__name__] = {'installed': ret.return_code == 0}
|
|
625
|
+
print_status(tool.__name__, ret.return_code, version, ret.output, 'tools')
|
|
626
|
+
status['tools'][tool.__name__] = {'installed': ret.return_code == 0, 'version': version}
|
|
297
627
|
|
|
298
628
|
# Check addons
|
|
299
|
-
|
|
629
|
+
if not json:
|
|
630
|
+
console.print('\n:wrench: [bold gold3]Checking installed addons ...[/]')
|
|
300
631
|
for addon in ['google', 'mongodb', 'redis', 'dev', 'trace', 'build']:
|
|
301
632
|
addon_var = globals()[f'{addon.upper()}_ADDON_ENABLED']
|
|
302
633
|
ret = 0 if addon_var == 1 else 1
|
|
303
634
|
bin = None if addon_var == 0 else ' '
|
|
304
|
-
|
|
635
|
+
if not json:
|
|
636
|
+
print_status(addon, ret, 'N/A', bin, 'addons')
|
|
305
637
|
|
|
306
638
|
# Print JSON health
|
|
307
639
|
if json:
|
|
308
|
-
|
|
640
|
+
import json as _json
|
|
641
|
+
print(_json.dumps(status))
|
|
309
642
|
|
|
310
643
|
#---------#
|
|
311
644
|
# INSTALL #
|
|
@@ -326,9 +659,9 @@ def run_install(cmd, title, next_steps=None):
|
|
|
326
659
|
sys.exit(ret.return_code)
|
|
327
660
|
|
|
328
661
|
|
|
329
|
-
@cli.group(
|
|
662
|
+
@cli.group()
|
|
330
663
|
def install():
|
|
331
|
-
"
|
|
664
|
+
"""[dim]Install langs, tools and addons.[/]"""
|
|
332
665
|
pass
|
|
333
666
|
|
|
334
667
|
|
|
@@ -476,7 +809,7 @@ def install_tools(cmds):
|
|
|
476
809
|
|
|
477
810
|
for ix, cls in enumerate(tools):
|
|
478
811
|
with console.status(f'[bold yellow][{ix}/{len(tools)}] Installing {cls.__name__} ...'):
|
|
479
|
-
|
|
812
|
+
ToolInstaller.install(cls)
|
|
480
813
|
console.print()
|
|
481
814
|
|
|
482
815
|
|
|
@@ -502,14 +835,30 @@ def install_cves(force):
|
|
|
502
835
|
console.print(':tada: CVEs installed successfully !', style='bold green')
|
|
503
836
|
|
|
504
837
|
|
|
838
|
+
#--------#
|
|
839
|
+
# UPDATE #
|
|
840
|
+
#--------#
|
|
841
|
+
|
|
842
|
+
@cli.command('update')
|
|
843
|
+
def update():
|
|
844
|
+
"""[dim]Update to latest version.[/]"""
|
|
845
|
+
if not VERSION_OBSOLETE:
|
|
846
|
+
console.print(f'[bold green]secator is already at the newest version {VERSION_LATEST}[/]')
|
|
847
|
+
console.print(f'[bold gold3]:wrench: Updating secator from {VERSION} to {VERSION_LATEST} ...[/]')
|
|
848
|
+
if 'pipx' in sys.executable:
|
|
849
|
+
Command.execute(f'pipx install secator=={VERSION_LATEST} --force')
|
|
850
|
+
else:
|
|
851
|
+
Command.execute(f'pip install secator=={VERSION_LATEST}')
|
|
852
|
+
|
|
853
|
+
|
|
505
854
|
#-------#
|
|
506
855
|
# ALIAS #
|
|
507
856
|
#-------#
|
|
508
857
|
|
|
509
858
|
|
|
510
|
-
@cli.group(
|
|
859
|
+
@cli.group()
|
|
511
860
|
def alias():
|
|
512
|
-
"""
|
|
861
|
+
"""[dim]Configure aliases.[/]"""
|
|
513
862
|
pass
|
|
514
863
|
|
|
515
864
|
|
|
@@ -593,315 +942,6 @@ def list_aliases(silent):
|
|
|
593
942
|
return aliases
|
|
594
943
|
|
|
595
944
|
|
|
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
945
|
#------#
|
|
906
946
|
# TEST #
|
|
907
947
|
#------#
|
|
@@ -909,7 +949,7 @@ def publish_docker(dev):
|
|
|
909
949
|
|
|
910
950
|
@cli.group(cls=OrderedGroup)
|
|
911
951
|
def test():
|
|
912
|
-
"""
|
|
952
|
+
"""[dim]Run tests."""
|
|
913
953
|
if not DEV_PACKAGE:
|
|
914
954
|
console.print('[bold red]You MUST use a development version of secator to run tests.[/]')
|
|
915
955
|
sys.exit(1)
|