secator 0.1.1__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 CHANGED
@@ -12,15 +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,
16
- OPT_NOT_SUPPORTED, PAYLOADS_FOLDER,
17
- ROOT_FOLDER, SCRIPTS_FOLDER, VERSION,
18
- WORKER_ADDON_ENABLED, DEV_ADDON_ENABLED, DEV_PACKAGE)
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
19
20
  from secator.rich import console
20
21
  from secator.runners import Command
21
22
  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)
23
+ from secator.utils import debug, detect_host, discover_tasks, find_list_item, flatten, print_results_table
24
24
 
25
25
  click.rich_click.USE_RICH_MARKUP = True
26
26
 
@@ -28,10 +28,15 @@ ALL_TASKS = discover_tasks()
28
28
  ALL_CONFIGS = ConfigLoader.load_all()
29
29
  ALL_WORKFLOWS = ALL_CONFIGS.workflow
30
30
  ALL_SCANS = ALL_CONFIGS.scan
31
- DEFAULT_CMD_OPTS = {
32
- 'no_capture': True,
33
- 'print_cmd': True,
34
- }
31
+
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}')
35
40
 
36
41
 
37
42
  #-----#
@@ -39,16 +44,18 @@ DEFAULT_CMD_OPTS = {
39
44
  #-----#
40
45
 
41
46
  @click.group(cls=OrderedGroup, invoke_without_command=True)
42
- @click.option('--no-banner', '-nb', is_flag=True, default=False)
43
47
  @click.option('--version', '-version', is_flag=True, default=False)
44
48
  @click.pass_context
45
- def cli(ctx, no_banner, version):
49
+ def cli(ctx, version):
46
50
  """Secator CLI."""
47
- if not no_banner:
48
- print(ASCII, file=sys.stderr)
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')
49
56
  if ctx.invoked_subcommand is None:
50
57
  if version:
51
- print(f'Current Version: v{VERSION}')
58
+ print_version()
52
59
  else:
53
60
  ctx.get_help()
54
61
 
@@ -142,11 +149,329 @@ def worker(hostname, concurrency, reload, queue, pool, check, dev, stop, show):
142
149
  if reload:
143
150
  patterns = "celery.py;tasks/*.py;runners/*.py;serializers/*.py;output_types/*.py;hooks/*.py;exporters/*.py"
144
151
  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
- )
152
+ Command.execute(cmd, name='secator worker')
153
+
154
+
155
+ #-------#
156
+ # UTILS #
157
+ #-------#
158
+
159
+
160
+ @cli.group(aliases=['u'])
161
+ def util():
162
+ """Run a utility."""
163
+ pass
164
+
165
+
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)
175
+
176
+
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
193
+
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)
203
+
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
221
+
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'))
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)
254
+
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()
292
+
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)
311
+
312
+
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'
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')
344
+
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}')
355
+
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('')
378
+
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)
150
475
 
151
476
 
152
477
  #--------#
@@ -156,7 +481,7 @@ def worker(hostname, concurrency, reload, queue, pool, check, dev, stop, show):
156
481
 
157
482
  @cli.group(aliases=['r'])
158
483
  def report():
159
- """Reports."""
484
+ """View previous reports."""
160
485
  pass
161
486
 
162
487
 
@@ -211,14 +536,10 @@ def which(command):
211
536
  Returns:
212
537
  secator.Command: Command instance.
213
538
  """
214
- return Command.run_command(
215
- f'which {command}',
216
- quiet=True,
217
- print_errors=False
218
- )
539
+ return Command.execute(f'which {command}', quiet=True, print_errors=False)
219
540
 
220
541
 
221
- def version(cls):
542
+ def get_version_cls(cls):
222
543
  """Get version for a Command.
223
544
 
224
545
  Args:
@@ -245,27 +566,23 @@ def get_version(version_cmd):
245
566
  str: Version string.
246
567
  """
247
568
  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
- )
569
+ ret = Command.execute(version_cmd, quiet=True, print_errors=False)
253
570
  match = re.findall(regex, ret.output)
254
571
  if not match:
255
572
  return 'n/a'
256
573
  return match[0]
257
574
 
258
575
 
259
- @cli.command(name='health', aliases=['h'])
576
+ @cli.command(name='health')
260
577
  @click.option('--json', '-json', is_flag=True, default=False, help='JSON lines output')
261
578
  @click.option('--debug', '-debug', is_flag=True, default=False, help='Debug health output')
262
579
  def health(json, debug):
263
- """Health."""
580
+ """[dim]Get health status.[/]"""
264
581
  tools = [cls for cls in ALL_TASKS]
265
582
  status = {'tools': {}, 'languages': {}, 'secator': {}}
266
583
 
267
584
  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 [/]'
585
+ s = '[bold green]ok [/]' if return_code == 0 else '[bold red]missing [/]'
269
586
  s = f'[bold magenta]{cmd:<15}[/] {s} '
270
587
  if return_code == 0 and version:
271
588
  if version == 'N/A':
@@ -279,35 +596,49 @@ def health(json, debug):
279
596
  console.print(s, highlight=False)
280
597
 
281
598
  # Check secator
282
- console.print(':wrench: [bold gold3]Checking secator ...[/]')
599
+ if not json:
600
+ console.print(':wrench: [bold gold3]Checking secator ...[/]')
283
601
  ret = which('secator')
284
602
  if not json:
285
603
  print_status('secator', ret.return_code, VERSION, ret.output, None)
286
- status['secator'] = {'installed': ret.return_code == 0}
604
+ status['secator'] = {'installed': ret.return_code == 0, 'version': VERSION}
287
605
 
288
606
  # Check languages
289
- console.print('\n:wrench: [bold gold3]Checking installed languages ...[/]')
290
- version_cmds = {'go': 'version', 'python3': '--version', 'ruby': '--version', 'rustc': '--version'}
607
+ if not json:
608
+ console.print('\n:wrench: [bold gold3]Checking installed languages ...[/]')
609
+ version_cmds = {'go': 'version', 'python3': '--version', 'ruby': '--version'}
291
610
  for lang, version_flag in version_cmds.items():
292
611
  ret = which(lang)
293
- ret2 = get_version(f'{lang} {version_flag}')
612
+ version = get_version(f'{lang} {version_flag}')
294
613
  if not json:
295
- print_status(lang, ret.return_code, ret2, ret.output, 'lang')
296
- 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}
297
616
 
298
617
  # Check tools
299
- console.print('\n:wrench: [bold gold3]Checking installed tools ...[/]')
618
+ if not json:
619
+ console.print('\n:wrench: [bold gold3]Checking installed tools ...[/]')
300
620
  for tool in tools:
301
621
  cmd = tool.cmd.split(' ')[0]
302
622
  ret = which(cmd)
303
- ret2 = version(tool)
623
+ version = get_version_cls(tool)
304
624
  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}
625
+ print_status(tool.__name__, ret.return_code, version, ret.output, 'tools')
626
+ status['tools'][tool.__name__] = {'installed': ret.return_code == 0, 'version': version}
627
+
628
+ # Check addons
629
+ if not json:
630
+ console.print('\n:wrench: [bold gold3]Checking installed addons ...[/]')
631
+ for addon in ['google', 'mongodb', 'redis', 'dev', 'trace', 'build']:
632
+ addon_var = globals()[f'{addon.upper()}_ADDON_ENABLED']
633
+ ret = 0 if addon_var == 1 else 1
634
+ bin = None if addon_var == 0 else ' '
635
+ if not json:
636
+ print_status(addon, ret, 'N/A', bin, 'addons')
307
637
 
308
638
  # Print JSON health
309
639
  if json:
310
- console.print(status)
640
+ import json as _json
641
+ print(_json.dumps(status))
311
642
 
312
643
  #---------#
313
644
  # INSTALL #
@@ -316,12 +647,7 @@ def health(json, debug):
316
647
 
317
648
  def run_install(cmd, title, next_steps=None):
318
649
  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
- )
650
+ ret = Command.execute(cmd, cls_attributes={'shell': True}, print_cmd=True, print_line=True)
325
651
  if ret.return_code != 0:
326
652
  console.print(f':exclamation_mark: Failed to install {title}.', style='bold red')
327
653
  else:
@@ -333,9 +659,9 @@ def run_install(cmd, title, next_steps=None):
333
659
  sys.exit(ret.return_code)
334
660
 
335
661
 
336
- @cli.group(aliases=['i'])
662
+ @cli.group()
337
663
  def install():
338
- "Installations."
664
+ """[dim]Install langs, tools and addons.[/]"""
339
665
  pass
340
666
 
341
667
 
@@ -349,7 +675,7 @@ def addons():
349
675
  def install_worker():
350
676
  "Install worker addon."
351
677
  run_install(
352
- cmd=f'{sys.executable} -m pip install secator[worker]',
678
+ cmd=f'{sys.executable} -m pip install .[worker]',
353
679
  title='worker addon',
354
680
  next_steps=[
355
681
  'Run "secator worker" to run a Celery worker using the file system as a backend and broker.',
@@ -363,7 +689,7 @@ def install_worker():
363
689
  def install_google():
364
690
  "Install google addon."
365
691
  run_install(
366
- cmd=f'{sys.executable} -m pip install secator[google]',
692
+ cmd=f'{sys.executable} -m pip install .[google]',
367
693
  title='google addon',
368
694
  next_steps=[
369
695
  'Set the "GOOGLE_CREDENTIALS_PATH" and "GOOGLE_DRIVE_PARENT_FOLDER_ID" environment variables.',
@@ -376,7 +702,7 @@ def install_google():
376
702
  def install_mongodb():
377
703
  "Install mongodb addon."
378
704
  run_install(
379
- cmd=f'{sys.executable} -m pip install secator[mongodb]',
705
+ cmd=f'{sys.executable} -m pip install .[mongodb]',
380
706
  title='mongodb addon',
381
707
  next_steps=[
382
708
  '[dim]\[optional][/] Run "docker run --name mongo -p 27017:27017 -d mongo:latest" to run a local MongoDB instance.',
@@ -390,7 +716,7 @@ def install_mongodb():
390
716
  def install_redis():
391
717
  "Install redis addon."
392
718
  run_install(
393
- cmd=f'{sys.executable} -m pip install secator[redis]',
719
+ cmd=f'{sys.executable} -m pip install .[redis]',
394
720
  title='redis addon',
395
721
  next_steps=[
396
722
  '[dim]\[optional][/] Run "docker run --name redis -p 6379:6379 -d redis" to run a local Redis instance.',
@@ -416,6 +742,34 @@ def install_dev():
416
742
  )
417
743
 
418
744
 
745
+ @addons.command('trace')
746
+ def install_trace():
747
+ "Install trace addon."
748
+ run_install(
749
+ cmd=f'{sys.executable} -m pip install secator[trace]',
750
+ title='dev addon',
751
+ next_steps=[
752
+ 'Run "secator test lint" to run lint tests.',
753
+ 'Run "secator test unit" to run unit tests.',
754
+ 'Run "secator test integration" to run integration tests.',
755
+ ]
756
+ )
757
+
758
+
759
+ @addons.command('build')
760
+ def install_build():
761
+ "Install build addon."
762
+ run_install(
763
+ cmd=f'{sys.executable} -m pip install secator[build]',
764
+ title='build addon',
765
+ next_steps=[
766
+ 'Run "secator test lint" to run lint tests.',
767
+ 'Run "secator test unit" to run unit tests.',
768
+ 'Run "secator test integration" to run integration tests.',
769
+ ]
770
+ )
771
+
772
+
419
773
  @install.group()
420
774
  def langs():
421
775
  "Install languages."
@@ -427,7 +781,10 @@ def install_go():
427
781
  """Install Go."""
428
782
  run_install(
429
783
  cmd='wget -O - https://raw.githubusercontent.com/freelabz/secator/main/scripts/install_go.sh | sudo sh',
430
- title='Go'
784
+ title='Go',
785
+ next_steps=[
786
+ 'Add ~/go/bin to your $PATH'
787
+ ]
431
788
  )
432
789
 
433
790
 
@@ -452,28 +809,20 @@ def install_tools(cmds):
452
809
 
453
810
  for ix, cls in enumerate(tools):
454
811
  with console.status(f'[bold yellow][{ix}/{len(tools)}] Installing {cls.__name__} ...'):
455
- cls.install()
812
+ ToolInstaller.install(cls)
456
813
  console.print()
457
814
 
458
815
 
459
816
  @install.command('cves')
460
817
  @click.option('--force', is_flag=True)
461
818
  def install_cves(force):
462
- """Install CVEs to file system for passive vulnerability search."""
819
+ """Install CVEs (enables passive vulnerability search)."""
463
820
  cve_json_path = f'{CVES_FOLDER}/circl-cve-search-expanded.json'
464
821
  if not os.path.exists(cve_json_path) or force:
465
822
  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
- )
823
+ Command.execute('wget https://cve.circl.lu/static/circl-cve-search-expanded.json.gz', cwd=CVES_FOLDER)
471
824
  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
- )
825
+ Command.execute(f'gunzip {CVES_FOLDER}/circl-cve-search-expanded.json.gz', cwd=CVES_FOLDER)
477
826
  with console.status(f'[bold yellow]Installing CVEs to {CVES_FOLDER} ...[/]'):
478
827
  with open(cve_json_path, 'r') as f:
479
828
  for line in f:
@@ -486,14 +835,30 @@ def install_cves(force):
486
835
  console.print(':tada: CVEs installed successfully !', style='bold green')
487
836
 
488
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
+
489
854
  #-------#
490
855
  # ALIAS #
491
856
  #-------#
492
857
 
493
858
 
494
- @cli.group(aliases=['a'])
859
+ @cli.group()
495
860
  def alias():
496
- """Aliases."""
861
+ """[dim]Configure aliases.[/]"""
497
862
  pass
498
863
 
499
864
 
@@ -577,259 +942,6 @@ def list_aliases(silent):
577
942
  return aliases
578
943
 
579
944
 
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
945
  #------#
834
946
  # TEST #
835
947
  #------#
@@ -837,7 +949,7 @@ def record(record_name, script, interactive, width, height, output_dir):
837
949
 
838
950
  @cli.group(cls=OrderedGroup)
839
951
  def test():
840
- """Tests."""
952
+ """[dim]Run tests."""
841
953
  if not DEV_PACKAGE:
842
954
  console.print('[bold red]You MUST use a development version of secator to run tests.[/]')
843
955
  sys.exit(1)
@@ -854,12 +966,7 @@ def run_test(cmd, name):
854
966
  cmd: Command to run.
855
967
  name: Name of the test.
856
968
  """
857
- result = Command.run_command(
858
- cmd,
859
- name=name + ' tests',
860
- cwd=ROOT_FOLDER,
861
- **DEFAULT_CMD_OPTS
862
- )
969
+ result = Command.execute(cmd, name=name + ' tests', cwd=ROOT_FOLDER)
863
970
  if result.return_code == 0:
864
971
  console.print(f':tada: {name.capitalize()} tests passed !', style='bold green')
865
972
  sys.exit(result.return_code)