secator 0.7.0__py3-none-any.whl → 0.8.1__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.

Files changed (49) hide show
  1. secator/celery.py +3 -3
  2. secator/cli.py +119 -77
  3. secator/config.py +88 -58
  4. secator/configs/workflows/subdomain_recon.yaml +2 -2
  5. secator/configs/workflows/url_dirsearch.yaml +1 -1
  6. secator/decorators.py +1 -0
  7. secator/definitions.py +1 -1
  8. secator/installer.py +284 -60
  9. secator/output_types/error.py +3 -3
  10. secator/output_types/exploit.py +11 -7
  11. secator/output_types/info.py +2 -2
  12. secator/output_types/ip.py +1 -1
  13. secator/output_types/port.py +3 -3
  14. secator/output_types/record.py +4 -4
  15. secator/output_types/stat.py +2 -2
  16. secator/output_types/subdomain.py +1 -1
  17. secator/output_types/tag.py +3 -3
  18. secator/output_types/target.py +2 -2
  19. secator/output_types/url.py +11 -11
  20. secator/output_types/user_account.py +6 -6
  21. secator/output_types/vulnerability.py +5 -4
  22. secator/output_types/warning.py +2 -2
  23. secator/report.py +1 -0
  24. secator/runners/_base.py +17 -13
  25. secator/runners/command.py +44 -7
  26. secator/tasks/_categories.py +145 -43
  27. secator/tasks/bbot.py +2 -0
  28. secator/tasks/bup.py +1 -0
  29. secator/tasks/dirsearch.py +2 -2
  30. secator/tasks/dnsxbrute.py +2 -1
  31. secator/tasks/feroxbuster.py +2 -3
  32. secator/tasks/fping.py +1 -1
  33. secator/tasks/grype.py +2 -4
  34. secator/tasks/h8mail.py +1 -1
  35. secator/tasks/katana.py +1 -1
  36. secator/tasks/maigret.py +1 -1
  37. secator/tasks/msfconsole.py +17 -3
  38. secator/tasks/naabu.py +15 -1
  39. secator/tasks/nmap.py +32 -20
  40. secator/tasks/nuclei.py +4 -1
  41. secator/tasks/searchsploit.py +9 -2
  42. secator/tasks/wpscan.py +12 -1
  43. secator/template.py +1 -1
  44. secator/utils.py +151 -62
  45. {secator-0.7.0.dist-info → secator-0.8.1.dist-info}/METADATA +50 -45
  46. {secator-0.7.0.dist-info → secator-0.8.1.dist-info}/RECORD +49 -49
  47. {secator-0.7.0.dist-info → secator-0.8.1.dist-info}/WHEEL +1 -1
  48. {secator-0.7.0.dist-info → secator-0.8.1.dist-info}/entry_points.txt +0 -0
  49. {secator-0.7.0.dist-info → secator-0.8.1.dist-info}/licenses/LICENSE +0 -0
secator/celery.py CHANGED
@@ -12,7 +12,7 @@ from rich.logging import RichHandler
12
12
  from retry import retry
13
13
 
14
14
  from secator.config import CONFIG
15
- from secator.output_types import Info, Warning, Error
15
+ from secator.output_types import Info, Error
16
16
  from secator.rich import console
17
17
  from secator.runners import Scan, Task, Workflow
18
18
  from secator.runners._helpers import run_extractors
@@ -338,7 +338,7 @@ def is_celery_worker_alive():
338
338
  result = app.control.broadcast('ping', reply=True, limit=1, timeout=1)
339
339
  result = bool(result)
340
340
  if result:
341
- console.print(Info(message='Celery worker is alive !'))
341
+ console.print(Info(message='Celery worker is available, running remotely'))
342
342
  else:
343
- console.print(Warning(message='No Celery worker alive.'))
343
+ console.print(Info(message='No Celery worker available, running locally'))
344
344
  return result
secator/cli.py CHANGED
@@ -19,8 +19,8 @@ from rich.table import Table
19
19
  from secator.config import CONFIG, ROOT_FOLDER, Config, default_config, config_path
20
20
  from secator.decorators import OrderedGroup, register_runner
21
21
  from secator.definitions import ADDONS_ENABLED, ASCII, DEV_PACKAGE, OPT_NOT_SUPPORTED, VERSION, STATE_COLORS
22
- from secator.installer import ToolInstaller, fmt_health_table_row, get_health_table, get_version_info
23
- from secator.output_types import FINDING_TYPES
22
+ from secator.installer import ToolInstaller, fmt_health_table_row, get_health_table, get_version_info, get_distro_config
23
+ from secator.output_types import FINDING_TYPES, Info, Error
24
24
  from secator.report import Report
25
25
  from secator.rich import console
26
26
  from secator.runners import Command, Runner
@@ -191,7 +191,7 @@ def util():
191
191
  def proxy(timeout, number):
192
192
  """Get random proxies from FreeProxy."""
193
193
  if CONFIG.offline_mode:
194
- console.print('[bold red]Cannot run this command in offline mode.[/]')
194
+ console.print(Error(message='Cannot run this command in offline mode.'))
195
195
  return
196
196
  proxy = FreeProxy(timeout=timeout, rand=True, anonym=True)
197
197
  for _ in range(number):
@@ -211,18 +211,16 @@ def revshell(name, host, port, interface, listen, force):
211
211
  if host is None: # detect host automatically
212
212
  host = detect_host(interface)
213
213
  if not host:
214
- console.print(
215
- f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of available interfaces.',
216
- style='bold red')
214
+ console.print(Error(message=f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of available interfaces')) # noqa: E501
217
215
  return
218
216
  else:
219
- console.print(f'[bold green]Detected host IP: [bold orange1]{host}[/].[/]')
217
+ console.print(Info(message=f'Detected host IP: [bold orange1]{host}[/]'))
220
218
 
221
219
  # Download reverse shells JSON from repo
222
220
  revshells_json = f'{CONFIG.dirs.revshells}/revshells.json'
223
221
  if not os.path.exists(revshells_json) or force:
224
222
  if CONFIG.offline_mode:
225
- console.print('[bold red]Cannot run this command in offline mode.[/]')
223
+ console.print(Error(message='Cannot run this command in offline mode'))
226
224
  return
227
225
  ret = Command.execute(
228
226
  f'wget https://raw.githubusercontent.com/freelabz/secator/main/scripts/revshells.json && mv revshells.json {CONFIG.dirs.revshells}', # noqa: E501
@@ -261,7 +259,7 @@ def revshell(name, host, port, interface, listen, force):
261
259
  console.print('\n'.join(shells_str))
262
260
  else:
263
261
  shell = shell[0]
264
- command = shell['command'].replace('[', '\[')
262
+ command = shell['command'].replace('[', r'\[')
265
263
  alias = shell['alias']
266
264
  name = shell['name']
267
265
  command_str = Template(command).render(ip=host, port=port, shell='bash')
@@ -277,7 +275,7 @@ def revshell(name, host, port, interface, listen, force):
277
275
  console.print(Rule(style='bold red'))
278
276
 
279
277
  if listen:
280
- console.print(f'Starting netcat listener on port {port} ...', style='bold gold3')
278
+ console.print(Info(message=f'Starting netcat listener on port {port} ...'))
281
279
  cmd = f'nc -lvnp {port}'
282
280
  Command.execute(cmd)
283
281
 
@@ -295,9 +293,7 @@ def serve(directory, host, port, interface):
295
293
  if not host:
296
294
  host = detect_host(interface)
297
295
  if not host:
298
- console.print(
299
- f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of interfaces.',
300
- style='bold red')
296
+ console.print(Error(message=f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of interfaces.')) # noqa: E501
301
297
  return
302
298
  console.print(f'{fname} [dim][/]', style='bold magenta')
303
299
  console.print(f'wget http://{host}:{port}/{fname}', style='dim italic')
@@ -337,9 +333,9 @@ def record(record_name, script, interactive, width, height, output_dir):
337
333
  # If existing cast file, remove it
338
334
  if os.path.exists(output_cast_path):
339
335
  os.unlink(output_cast_path)
340
- console.print(f'Removed existing {output_cast_path}', style='bold green')
336
+ console.print(Info(message=f'Removed existing {output_cast_path}'))
341
337
 
342
- with console.status('[bold gold3]Recording with asciinema ...[/]'):
338
+ with console.status(Info(message='Recording with asciinema ...')):
343
339
  Command.execute(
344
340
  f'asciinema-automation -aa "-c /bin/sh" {script} {output_cast_path} --timeout 200',
345
341
  cls_attributes=attrs,
@@ -390,14 +386,14 @@ def record(record_name, script, interactive, width, height, output_dir):
390
386
  f'agg {output_cast_path} {output_gif_path}',
391
387
  cls_attributes=attrs,
392
388
  )
393
- console.print(f'Generated {output_gif_path}', style='bold green')
389
+ console.print(Info(message=f'Generated {output_gif_path}'))
394
390
 
395
391
 
396
392
  @util.group('build')
397
393
  def build():
398
394
  """Build secator."""
399
395
  if not DEV_PACKAGE:
400
- console.print('[bold red]You MUST use a development version of secator to make builds.[/]')
396
+ console.print(Error(message='You MUST use a development version of secator to make builds'))
401
397
  sys.exit(1)
402
398
  pass
403
399
 
@@ -406,7 +402,7 @@ def build():
406
402
  def build_pypi():
407
403
  """Build secator PyPI package."""
408
404
  if not ADDONS_ENABLED['build']:
409
- console.print('[bold red]Missing build addon: please run [bold green4]secator install addons build[/][/]')
405
+ console.print(Error(message='Missing build addon: please run [bold green4]secator install addons build[/]'))
410
406
  sys.exit(1)
411
407
  with console.status('[bold gold3]Building PyPI package...[/]'):
412
408
  ret = Command.execute(f'{sys.executable} -m hatch build', name='hatch build', cwd=ROOT_FOLDER)
@@ -658,7 +654,7 @@ def report_show(report_query, output, runner_type, time_delta, type, query, work
658
654
  # Load all report paths
659
655
  load_all_reports = any([not Path(p).exists() for p in report_query])
660
656
  all_reports = []
661
- if load_all_reports:
657
+ if load_all_reports or workspace:
662
658
  all_reports = list_reports(workspace=workspace, type=runner_type, timedelta=human_to_timedelta(time_delta))
663
659
  if not report_query:
664
660
  report_query = all_reports
@@ -685,7 +681,7 @@ def report_show(report_query, output, runner_type, time_delta, type, query, work
685
681
  all_results = []
686
682
  for ix, path in enumerate(paths):
687
683
  if unified:
688
- console.print(f'Loading {path} \[[bold yellow4]{ix + 1}[/]/[bold yellow4]{len(paths)}[/]] \[results={len(all_results)}]...') # noqa: E501
684
+ console.print(rf'Loading {path} \[[bold yellow4]{ix + 1}[/]/[bold yellow4]{len(paths)}[/]] \[results={len(all_results)}]...') # noqa: E501
689
685
  with open(path, 'r') as f:
690
686
  data = loads_dataclass(f.read())
691
687
  try:
@@ -823,7 +819,8 @@ def report_export(json_path, output_folder, output):
823
819
  @cli.command(name='health')
824
820
  @click.option('--json', '-json', is_flag=True, default=False, help='JSON lines output')
825
821
  @click.option('--debug', '-debug', is_flag=True, default=False, help='Debug health output')
826
- def health(json, debug):
822
+ @click.option('--strict', '-strict', is_flag=True, default=False, help='Fail if missing tools')
823
+ def health(json, debug, strict):
827
824
  """[dim]Get health status.[/]"""
828
825
  tools = ALL_TASKS
829
826
  status = {'secator': {}, 'languages': {}, 'tools': {}, 'addons': {}}
@@ -870,39 +867,58 @@ def health(json, debug):
870
867
  table = get_health_table()
871
868
  with Live(table, console=console):
872
869
  for tool in tools:
873
- cmd = tool.cmd.split(' ')[0]
874
- version_flag = tool.version_flag or f'{tool.opt_prefix}version'
875
- version_flag = None if tool.version_flag == OPT_NOT_SUPPORTED else version_flag
876
- info = get_version_info(cmd, version_flag, tool.install_github_handle)
870
+ info = get_version_info(
871
+ tool.cmd.split(' ')[0],
872
+ tool.version_flag or f'{tool.opt_prefix}version',
873
+ tool.install_github_handle,
874
+ tool.install_cmd
875
+ )
877
876
  row = fmt_health_table_row(info, 'tools')
878
877
  table.add_row(*row)
879
878
  status['tools'][tool.__name__] = info
879
+ console.print('')
880
880
 
881
881
  # Print JSON health
882
882
  if json:
883
883
  import json as _json
884
884
  print(_json.dumps(status))
885
885
 
886
+ # Strict mode
887
+ if strict:
888
+ error = False
889
+ for tool, info in status['tools'].items():
890
+ if not info['installed']:
891
+ console.print(Error(message=f'{tool} not installed and strict mode is enabled.[/]'))
892
+ error = True
893
+ if error:
894
+ sys.exit(1)
895
+ console.print(Info(message='Strict healthcheck passed !'))
896
+
897
+
886
898
  #---------#
887
899
  # INSTALL #
888
900
  #---------#
889
901
 
890
902
 
891
- def run_install(cmd, title, next_steps=None):
903
+ def run_install(title=None, cmd=None, packages=None, next_steps=None):
892
904
  if CONFIG.offline_mode:
893
905
  console.print('[bold red]Cannot run this command in offline mode.[/]')
894
906
  return
895
907
  with console.status(f'[bold yellow] Installing {title}...'):
896
- ret = Command.execute(cmd, cls_attributes={'shell': True}, print_line=True)
897
- if ret.return_code != 0:
898
- console.print(f':exclamation_mark: Failed to install {title}.', style='bold red')
899
- else:
900
- console.print(f':tada: {title} installed successfully !', style='bold green')
908
+ if cmd:
909
+ from secator.installer import SourceInstaller
910
+ status = SourceInstaller.install(cmd)
911
+ elif packages:
912
+ from secator.installer import PackageInstaller
913
+ status = PackageInstaller.install(packages)
914
+ return_code = 1
915
+ if status.is_ok():
916
+ return_code = 0
901
917
  if next_steps:
902
918
  console.print('[bold gold3]:wrench: Next steps:[/]')
903
919
  for ix, step in enumerate(next_steps):
904
920
  console.print(f' :keycap_{ix}: {step}')
905
- sys.exit(ret.return_code)
921
+ sys.exit(return_code)
906
922
 
907
923
 
908
924
  @cli.group()
@@ -926,7 +942,7 @@ def install_worker():
926
942
  next_steps=[
927
943
  'Run [bold green4]secator worker[/] to run a Celery worker using the file system as a backend and broker.',
928
944
  'Run [bold green4]secator x httpx testphp.vulnweb.com[/] to admire your task running in a worker.',
929
- '[dim]\[optional][/dim] Run [bold green4]secator install addons redis[/] to setup Redis backend / broker.'
945
+ r'[dim]\[optional][/dim] Run [bold green4]secator install addons redis[/] to setup Redis backend / broker.'
930
946
  ]
931
947
  )
932
948
 
@@ -964,7 +980,7 @@ def install_mongodb():
964
980
  cmd=f'{sys.executable} -m pip install secator[mongodb]',
965
981
  title='MongoDB addon',
966
982
  next_steps=[
967
- '[dim]\[optional][/] Run [bold green4]docker run --name mongo -p 27017:27017 -d mongo:latest[/] to run a local MongoDB instance.', # noqa: E501
983
+ r'[dim]\[optional][/] Run [bold green4]docker run --name mongo -p 27017:27017 -d mongo:latest[/] to run a local MongoDB instance.', # noqa: E501
968
984
  'Run [bold green4]secator config set addons.mongodb.url mongodb://<URL>[/].',
969
985
  'Run [bold green4]secator x httpx testphp.vulnweb.com -driver mongodb[/] to save results to MongoDB.'
970
986
  ]
@@ -978,7 +994,7 @@ def install_redis():
978
994
  cmd=f'{sys.executable} -m pip install secator[redis]',
979
995
  title='Redis addon',
980
996
  next_steps=[
981
- '[dim]\[optional][/] Run [bold green4]docker run --name redis -p 6379:6379 -d redis[/] to run a local Redis instance.', # noqa: E501
997
+ r'[dim]\[optional][/] Run [bold green4]docker run --name redis -p 6379:6379 -d redis[/] to run a local Redis instance.', # noqa: E501
982
998
  'Run [bold green4]secator config set celery.broker_url redis://<URL>[/]',
983
999
  'Run [bold green4]secator config set celery.result_backend redis://<URL>[/]',
984
1000
  'Run [bold green4]secator worker[/] to run a worker.',
@@ -1049,7 +1065,12 @@ def install_go():
1049
1065
  def install_ruby():
1050
1066
  """Install Ruby."""
1051
1067
  run_install(
1052
- cmd='wget -O - https://raw.githubusercontent.com/freelabz/secator/main/scripts/install_ruby.sh | sudo sh',
1068
+ packages={
1069
+ 'apt': ['ruby-full', 'rubygems'],
1070
+ 'apk': ['ruby', 'ruby-dev'],
1071
+ 'pacman': ['ruby', 'ruby-dev'],
1072
+ 'brew': ['ruby']
1073
+ },
1053
1074
  title='Ruby'
1054
1075
  )
1055
1076
 
@@ -1059,43 +1080,34 @@ def install_ruby():
1059
1080
  def install_tools(cmds):
1060
1081
  """Install supported tools."""
1061
1082
  if CONFIG.offline_mode:
1062
- console.print('[bold red]Cannot run this command in offline mode.[/]')
1083
+ console.print(Error(message='Cannot run this command in offline mode.'))
1063
1084
  return
1064
1085
  if cmds is not None:
1065
1086
  cmds = cmds.split(',')
1066
1087
  tools = [cls for cls in ALL_TASKS if cls.__name__ in cmds]
1067
1088
  else:
1068
1089
  tools = ALL_TASKS
1069
-
1090
+ tools.sort(key=lambda x: x.__name__)
1091
+ return_code = 0
1070
1092
  for ix, cls in enumerate(tools):
1071
- with console.status(f'[bold yellow][{ix}/{len(tools)}] Installing {cls.__name__} ...'):
1072
- ToolInstaller.install(cls)
1093
+ with console.status(f'[bold yellow][{ix + 1}/{len(tools)}] Installing {cls.__name__} ...'):
1094
+ status = ToolInstaller.install(cls)
1095
+ if not status.is_ok():
1096
+ return_code = 1
1073
1097
  console.print()
1074
-
1075
-
1076
- @install.command('cves')
1077
- @click.option('--force', is_flag=True)
1078
- def install_cves(force):
1079
- """Install CVEs (enables passive vulnerability search)."""
1080
- if CONFIG.offline_mode:
1081
- console.print('[bold red]Cannot run this command in offline mode.[/]')
1082
- return
1083
- cve_json_path = f'{CONFIG.dirs.cves}/circl-cve-search-expanded.json'
1084
- if not os.path.exists(cve_json_path) or force:
1085
- with console.status('[bold yellow]Downloading zipped CVEs from cve.circl.lu ...[/]'):
1086
- Command.execute('wget https://cve.circl.lu/static/circl-cve-search-expanded.json.gz', cwd=CONFIG.dirs.cves)
1087
- with console.status('[bold yellow]Unzipping CVEs ...[/]'):
1088
- Command.execute(f'gunzip {CONFIG.dirs.cves}/circl-cve-search-expanded.json.gz', cwd=CONFIG.dirs.cves)
1089
- with console.status(f'[bold yellow]Installing CVEs to {CONFIG.dirs.cves} ...[/]'):
1090
- with open(cve_json_path, 'r') as f:
1091
- for line in f:
1092
- data = json.loads(line)
1093
- cve_id = data['id']
1094
- cve_path = f'{CONFIG.dirs.cves}/{cve_id}.json'
1095
- with open(cve_path, 'w') as f:
1096
- f.write(line)
1097
- console.print(f'CVE saved to {cve_path}')
1098
- console.print(':tada: CVEs installed successfully !', style='bold green')
1098
+ distro = get_distro_config()
1099
+ cleanup_cmds = [
1100
+ 'go clean -cache',
1101
+ 'go clean -modcache',
1102
+ 'pip cache purge',
1103
+ 'gem cleanup --user-install',
1104
+ 'gem clean --user-install',
1105
+ ]
1106
+ if distro.pm_finalizer:
1107
+ cleanup_cmds.append(f'sudo {distro.pm_finalizer}')
1108
+ cmd = ' && '.join(cleanup_cmds)
1109
+ Command.execute(cmd, cls_attributes={'shell': True}, quiet=False)
1110
+ sys.exit(return_code)
1099
1111
 
1100
1112
 
1101
1113
  #--------#
@@ -1103,22 +1115,52 @@ def install_cves(force):
1103
1115
  #--------#
1104
1116
 
1105
1117
  @cli.command('update')
1106
- def update():
1118
+ @click.option('--all', '-a', is_flag=True, help='Update all secator dependencies (addons, tools, ...)')
1119
+ def update(all):
1107
1120
  """[dim]Update to latest version.[/]"""
1108
1121
  if CONFIG.offline_mode:
1109
- console.print('[bold red]Cannot run this command in offline mode.[/]')
1110
- return
1122
+ console.print(Error(message='Cannot run this command in offline mode.'))
1123
+ sys.exit(1)
1124
+
1125
+ # Check current and latest version
1111
1126
  info = get_version_info('secator', github_handle='freelabz/secator', version=VERSION)
1112
1127
  latest_version = info['latest_version']
1128
+ do_update = True
1129
+
1130
+ # Skip update if latest
1113
1131
  if info['status'] == 'latest':
1114
- console.print(f'[bold green]secator is already at the newest version {latest_version}[/] !')
1115
- sys.exit(0)
1116
- console.print(f'[bold gold3]:wrench: Updating secator from {VERSION} to {latest_version} ...[/]')
1117
- if 'pipx' in sys.executable:
1118
- Command.execute(f'pipx install secator=={latest_version} --force')
1119
- else:
1120
- Command.execute(f'pip install secator=={latest_version}')
1132
+ console.print(Info(message=f'secator is already at the newest version {latest_version} !'))
1133
+ do_update = False
1134
+
1135
+ # Fail if unknown latest
1136
+ if not latest_version:
1137
+ console.print(Error(message='Could not fetch latest secator version.'))
1138
+ sys.exit(1)
1139
+
1140
+ # Update secator
1141
+ if do_update:
1142
+ console.print(f'[bold gold3]:wrench: Updating secator from {VERSION} to {latest_version} ...[/]')
1143
+ if 'pipx' in sys.executable:
1144
+ ret = Command.execute(f'pipx install secator=={latest_version} --force')
1145
+ else:
1146
+ ret = Command.execute(f'pip install secator=={latest_version}')
1147
+ if not ret.return_code == 0:
1148
+ sys.exit(1)
1121
1149
 
1150
+ # Update tools
1151
+ if all:
1152
+ return_code = 0
1153
+ for cls in ALL_TASKS:
1154
+ cmd = cls.cmd.split(' ')[0]
1155
+ version_flag = cls.version_flag or f'{cls.opt_prefix}version'
1156
+ version_flag = None if cls.version_flag == OPT_NOT_SUPPORTED else version_flag
1157
+ info = get_version_info(cmd, version_flag, cls.install_github_handle)
1158
+ if not info['installed'] or info['status'] == 'outdated' or not info['latest_version']:
1159
+ with console.status(f'[bold yellow]Installing {cls.__name__} ...'):
1160
+ status = ToolInstaller.install(cls)
1161
+ if not status.is_ok():
1162
+ return_code = 1
1163
+ sys.exit(return_code)
1122
1164
 
1123
1165
  #-------#
1124
1166
  # ALIAS #
@@ -1220,10 +1262,10 @@ def list_aliases(silent):
1220
1262
  def test():
1221
1263
  """[dim]Run tests."""
1222
1264
  if not DEV_PACKAGE:
1223
- console.print('[bold red]You MUST use a development version of secator to run tests.[/]')
1265
+ console.print(Error(message='You MUST use a development version of secator to run tests.'))
1224
1266
  sys.exit(1)
1225
1267
  if not ADDONS_ENABLED['dev']:
1226
- console.print('[bold red]Missing dev addon: please run [bold green4]secator install addons dev[/][/]')
1268
+ console.print(Error(message='Missing dev addon: please run [bold green4]secator install addons dev[/]'))
1227
1269
  sys.exit(1)
1228
1270
  pass
1229
1271
 
secator/config.py CHANGED
@@ -29,6 +29,7 @@ class StrictModel(BaseModel, extra='forbid'):
29
29
 
30
30
  class Directories(StrictModel):
31
31
  bin: Directory = Path.home() / '.local' / 'bin'
32
+ share: Directory = Path.home() / '.local' / 'share'
32
33
  data: Directory = Path(DATA_FOLDER)
33
34
  templates: Directory = ''
34
35
  reports: Directory = ''
@@ -67,7 +68,7 @@ class Celery(StrictModel):
67
68
 
68
69
 
69
70
  class Cli(StrictModel):
70
- github_token: str = ''
71
+ github_token: str = os.environ.get('GITHUB_TOKEN', '')
71
72
  record: bool = False
72
73
  stdin_timeout: int = 1000
73
74
 
@@ -79,10 +80,17 @@ class Runners(StrictModel):
79
80
  backend_update_frequency: int = 5
80
81
  poll_frequency: int = 5
81
82
  skip_cve_search: bool = False
82
- skip_cve_low_confidence: bool = True
83
+ skip_exploit_search: bool = False
84
+ skip_cve_low_confidence: bool = False
83
85
  remove_duplicates: bool = False
84
86
 
85
87
 
88
+ class Security(StrictModel):
89
+ allow_local_file_access: bool = True
90
+ auto_install_commands: bool = True
91
+ force_source_install: bool = False
92
+
93
+
86
94
  class HTTP(StrictModel):
87
95
  socks5_proxy: str = 'socks5://127.0.0.1:9050'
88
96
  http_proxy: str = 'https://127.0.0.1:9080'
@@ -116,7 +124,8 @@ class Wordlists(StrictModel):
116
124
  defaults: Dict[str, str] = {'http': 'bo0m_fuzz', 'dns': 'combined_subdomains'}
117
125
  templates: Dict[str, str] = {
118
126
  'bo0m_fuzz': 'https://raw.githubusercontent.com/Bo0oM/fuzz.txt/master/fuzz.txt',
119
- 'combined_subdomains': 'https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/DNS/combined_subdomains.txt' # noqa: E501
127
+ 'combined_subdomains': 'https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/DNS/combined_subdomains.txt', # noqa: E501
128
+ 'directory_list_small': 'https://raw.githubusercontent.com/danielmiessler/SecLists/refs/heads/master/Discovery/Web-Content/directory-list-2.3-small.txt', # noqa: E501
120
129
  }
121
130
  lists: Dict[str, List[str]] = {}
122
131
 
@@ -165,6 +174,7 @@ class SecatorConfig(StrictModel):
165
174
  payloads: Payloads = Payloads()
166
175
  wordlists: Wordlists = Wordlists()
167
176
  addons: Addons = Addons()
177
+ security: Security = Security()
168
178
  offline_mode: bool = False
169
179
 
170
180
 
@@ -495,56 +505,81 @@ def download_files(data: dict, target_folder: Path, offline_mode: bool, type: st
495
505
  offline_mode (bool): Offline mode.
496
506
  """
497
507
  for name, url_or_path in data.items():
498
- if url_or_path.startswith('git+'):
499
- # Clone Git repository
500
- git_url = url_or_path[4:] # remove 'git+' prefix
501
- repo_name = git_url.split('/')[-1]
502
- if repo_name.endswith('.git'):
503
- repo_name = repo_name[:-4]
504
- target_path = target_folder / repo_name
505
- if not target_path.exists():
506
- console.print(f'[bold turquoise4]Cloning git {type} [bold magenta]{repo_name}[/] ...[/] ', end='')
508
+ target_path = download_file(url_or_path, target_folder, offline_mode, type, name=name)
509
+ if target_path:
510
+ data[name] = target_path
511
+
512
+
513
+ def download_file(url_or_path, target_folder: Path, offline_mode: bool, type: str, name: str = None):
514
+ """Download remote file to target folder, clone git repos, or symlink local files.
515
+
516
+ Args:
517
+ data (dict): Dict of name to url or local path prefixed with 'git+' for Git repos.
518
+ target_folder (Path): Target folder for storing files or repos.
519
+ offline_mode (bool): Offline mode.
520
+ type (str): Type of files to handle.
521
+ name (str, Optional): Name of object.
522
+
523
+ Returns:
524
+ path (Path): Path to downloaded file / folder.
525
+ """
526
+ if url_or_path.startswith('git+'):
527
+ # Clone Git repository
528
+ git_url = url_or_path[4:] # remove 'git+' prefix
529
+ repo_name = git_url.split('/')[-1]
530
+ if repo_name.endswith('.git'):
531
+ repo_name = repo_name[:-4]
532
+ target_path = target_folder / repo_name
533
+ if not target_path.exists():
534
+ console.print(f'[bold turquoise4]Cloning git {type} [bold magenta]{repo_name}[/] ...[/] ', end='')
535
+ if offline_mode:
536
+ console.print('[bold orange1]skipped [dim][offline[/].[/]')
537
+ return
538
+ try:
539
+ call(['git', 'clone', git_url, str(target_path)], stderr=DEVNULL, stdout=DEVNULL)
540
+ console.print('[bold green]ok.[/]')
541
+ except Exception as e:
542
+ console.print(f'[bold red]failed ({str(e)}).[/]')
543
+ return target_path.resolve()
544
+ elif Path(url_or_path).exists():
545
+ # Create a symbolic link for a local file
546
+ local_path = Path(url_or_path)
547
+ target_path = target_folder / local_path.name
548
+ if not name:
549
+ name = url_or_path.split('/')[-1]
550
+ if not CONFIG.security.allow_local_file_access:
551
+ console.print(f'[bold red]Cannot reference local file {url_or_path}(disabled for security reasons)[/]')
552
+ return
553
+ if not target_path.exists():
554
+ console.print(f'[bold turquoise4]Symlinking {type} [bold magenta]{name}[/] ...[/] ', end='')
555
+ try:
556
+ target_path.symlink_to(local_path)
557
+ console.print('[bold green]ok.[/]')
558
+ except Exception as e:
559
+ console.print(f'[bold red]failed ({str(e)}).[/]')
560
+ return target_path.resolve()
561
+ else:
562
+ # Download file from URL
563
+ ext = url_or_path.split('.')[-1]
564
+ if not name:
565
+ name = url_or_path.split('/')[-1]
566
+ filename = f'{name}.{ext}' if not name.endswith(ext) else name
567
+ target_path = target_folder / filename
568
+ if not target_path.exists():
569
+ try:
570
+ console.print(f'[bold turquoise4]Downloading {type} [bold magenta]{filename}[/] ...[/] ', end='')
507
571
  if offline_mode:
508
- console.print('[bold orange1]skipped [dim][offline[/].[/]')
509
- continue
510
- try:
511
- call(['git', 'clone', git_url, str(target_path)], stderr=DEVNULL, stdout=DEVNULL)
512
- console.print('[bold green]ok.[/]')
513
- except Exception as e:
514
- console.print(f'[bold red]failed ({str(e)}).[/]')
515
- data[name] = target_path.resolve()
516
- elif Path(url_or_path).exists():
517
- # Create a symbolic link for a local file
518
- local_path = Path(url_or_path)
519
- target_path = target_folder / local_path.name
520
- if not target_path.exists():
521
- console.print(f'[bold turquoise4]Symlinking {type} [bold magenta]{name}[/] ...[/] ', end='')
522
- try:
523
- target_path.symlink_to(local_path)
524
- console.print('[bold green]ok.[/]')
525
- except Exception as e:
526
- console.print(f'[bold red]failed ({str(e)}).[/]')
527
- data[name] = target_path.resolve()
528
- else:
529
- # Download file from URL
530
- ext = url_or_path.split('.')[-1]
531
- filename = f'{name}.{ext}' if not name.endswith(ext) else name
532
- target_path = target_folder / filename
533
- if not target_path.exists():
534
- try:
535
- console.print(f'[bold turquoise4]Downloading {type} [bold magenta]{filename}[/] ...[/] ', end='')
536
- if offline_mode:
537
- console.print('[bold orange1]skipped [dim](offline)[/].[/]')
538
- continue
539
- resp = requests.get(url_or_path, timeout=3)
540
- resp.raise_for_status()
541
- with open(target_path, 'wb') as f:
542
- f.write(resp.content)
543
- console.print('[bold green]ok.[/]')
544
- except requests.RequestException as e:
545
- console.print(f'[bold red]failed ({str(e)}).[/]')
546
- continue
547
- data[name] = target_path.resolve()
572
+ console.print('[bold orange1]skipped [dim](offline)[/].[/]')
573
+ return
574
+ resp = requests.get(url_or_path, timeout=3)
575
+ resp.raise_for_status()
576
+ with open(target_path, 'wb') as f:
577
+ f.write(resp.content)
578
+ console.print('[bold green]ok.[/]')
579
+ except requests.RequestException as e:
580
+ console.print(f'[bold red]failed ({str(e)}).[/]')
581
+ return
582
+ return target_path.resolve()
548
583
 
549
584
 
550
585
  # Load default_config
@@ -576,13 +611,8 @@ for name, dir in CONFIG.dirs.items():
576
611
  dir.mkdir(parents=False)
577
612
  console.print('[bold green]ok.[/]')
578
613
 
579
- # Download wordlists and set defaults
614
+ # Download wordlists and payloads
580
615
  download_files(CONFIG.wordlists.templates, CONFIG.dirs.wordlists, CONFIG.offline_mode, 'wordlist')
581
- for category, name in CONFIG.wordlists.defaults.items():
582
- if name in CONFIG.wordlists.templates.keys():
583
- CONFIG.wordlists.defaults[category] = str(CONFIG.wordlists.templates[name])
584
-
585
- # Download payloads
586
616
  download_files(CONFIG.payloads.templates, CONFIG.dirs.payloads, CONFIG.offline_mode, 'payload')
587
617
 
588
618
  # Print config
@@ -13,12 +13,12 @@ tasks:
13
13
  # input: vhost
14
14
  # domain_:
15
15
  # - target.name
16
- # wordlist: /usr/share/seclists/Discovery/DNS/combined_subdomains.txt
16
+ # wordlist: combined_subdomains
17
17
  # gobuster:
18
18
  # input: dns
19
19
  # domain_:
20
20
  # - target.name
21
- # wordlist: /usr/share/seclists/Discovery/DNS/combined_subdomains.txt
21
+ # wordlist: combined_subdomains
22
22
  _group:
23
23
  nuclei:
24
24
  description: Check for subdomain takeovers
@@ -8,7 +8,7 @@ input_types:
8
8
  tasks:
9
9
  ffuf:
10
10
  description: Search for HTTP directories
11
- wordlist: /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-small.txt
11
+ wordlist: directory_list_small
12
12
  targets_:
13
13
  - type: target
14
14
  field: '{name}/FUZZ'
secator/decorators.py CHANGED
@@ -228,6 +228,7 @@ def decorate_command_options(opts):
228
228
  conf.pop('shlex', None)
229
229
  conf.pop('meta', None)
230
230
  conf.pop('supported', None)
231
+ conf.pop('process', None)
231
232
  reverse = conf.pop('reverse', False)
232
233
  long = f'--{opt_name}'
233
234
  short = f'-{short_opt}' if short_opt else f'-{opt_name}'
secator/definitions.py CHANGED
@@ -9,7 +9,7 @@ from secator.config import CONFIG, ROOT_FOLDER
9
9
 
10
10
  # Globals
11
11
  VERSION = version('secator')
12
- ASCII = f"""
12
+ ASCII = rf"""
13
13
  __
14
14
  ________ _________ _/ /_____ _____
15
15
  / ___/ _ \/ ___/ __ `/ __/ __ \/ ___/