secator 0.7.0__py3-none-any.whl → 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of secator might be problematic. Click here for more details.
- secator/celery.py +3 -3
- secator/cli.py +106 -76
- secator/config.py +88 -58
- secator/configs/workflows/subdomain_recon.yaml +2 -2
- secator/configs/workflows/url_dirsearch.yaml +1 -1
- secator/decorators.py +1 -0
- secator/definitions.py +1 -1
- secator/installer.py +277 -60
- secator/output_types/error.py +3 -3
- secator/output_types/exploit.py +11 -7
- secator/output_types/info.py +2 -2
- secator/output_types/ip.py +1 -1
- secator/output_types/port.py +3 -3
- secator/output_types/record.py +4 -4
- secator/output_types/stat.py +2 -2
- secator/output_types/subdomain.py +1 -1
- secator/output_types/tag.py +3 -3
- secator/output_types/target.py +2 -2
- secator/output_types/url.py +11 -11
- secator/output_types/user_account.py +6 -6
- secator/output_types/vulnerability.py +5 -4
- secator/output_types/warning.py +2 -2
- secator/report.py +1 -0
- secator/runners/_base.py +17 -13
- secator/runners/command.py +44 -7
- secator/tasks/_categories.py +145 -43
- secator/tasks/bbot.py +2 -0
- secator/tasks/bup.py +1 -0
- secator/tasks/dirsearch.py +2 -2
- secator/tasks/dnsxbrute.py +2 -1
- secator/tasks/feroxbuster.py +2 -3
- secator/tasks/fping.py +1 -1
- secator/tasks/grype.py +2 -4
- secator/tasks/h8mail.py +1 -1
- secator/tasks/katana.py +1 -1
- secator/tasks/maigret.py +1 -1
- secator/tasks/msfconsole.py +18 -3
- secator/tasks/naabu.py +15 -1
- secator/tasks/nmap.py +32 -20
- secator/tasks/nuclei.py +4 -1
- secator/tasks/searchsploit.py +9 -2
- secator/tasks/wpscan.py +12 -1
- secator/template.py +1 -1
- secator/utils.py +151 -62
- {secator-0.7.0.dist-info → secator-0.8.0.dist-info}/METADATA +50 -45
- {secator-0.7.0.dist-info → secator-0.8.0.dist-info}/RECORD +49 -49
- {secator-0.7.0.dist-info → secator-0.8.0.dist-info}/WHEEL +1 -1
- {secator-0.7.0.dist-info → secator-0.8.0.dist-info}/entry_points.txt +0 -0
- {secator-0.7.0.dist-info → secator-0.8.0.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,
|
|
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
|
|
341
|
+
console.print(Info(message='Celery worker is available, running remotely'))
|
|
342
342
|
else:
|
|
343
|
-
console.print(
|
|
343
|
+
console.print(Info(message='No Celery worker available, running locally'))
|
|
344
344
|
return result
|
secator/cli.py
CHANGED
|
@@ -20,7 +20,7 @@ from secator.config import CONFIG, ROOT_FOLDER, Config, default_config, config_p
|
|
|
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
22
|
from secator.installer import ToolInstaller, fmt_health_table_row, get_health_table, get_version_info
|
|
23
|
-
from secator.output_types import FINDING_TYPES
|
|
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('
|
|
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'
|
|
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('
|
|
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} ...'
|
|
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}'
|
|
336
|
+
console.print(Info(message=f'Removed existing {output_cast_path}'))
|
|
341
337
|
|
|
342
|
-
with console.status('
|
|
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}'
|
|
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('
|
|
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('
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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,
|
|
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
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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(
|
|
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
|
-
|
|
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,22 @@ def install_ruby():
|
|
|
1059
1080
|
def install_tools(cmds):
|
|
1060
1081
|
"""Install supported tools."""
|
|
1061
1082
|
if CONFIG.offline_mode:
|
|
1062
|
-
console.print('
|
|
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
|
+
sys.exit(return_code)
|
|
1099
1099
|
|
|
1100
1100
|
|
|
1101
1101
|
#--------#
|
|
@@ -1103,22 +1103,52 @@ def install_cves(force):
|
|
|
1103
1103
|
#--------#
|
|
1104
1104
|
|
|
1105
1105
|
@cli.command('update')
|
|
1106
|
-
|
|
1106
|
+
@click.option('--all', '-a', is_flag=True, help='Update all secator dependencies (addons, tools, ...)')
|
|
1107
|
+
def update(all):
|
|
1107
1108
|
"""[dim]Update to latest version.[/]"""
|
|
1108
1109
|
if CONFIG.offline_mode:
|
|
1109
|
-
console.print('
|
|
1110
|
-
|
|
1110
|
+
console.print(Error(message='Cannot run this command in offline mode.'))
|
|
1111
|
+
sys.exit(1)
|
|
1112
|
+
|
|
1113
|
+
# Check current and latest version
|
|
1111
1114
|
info = get_version_info('secator', github_handle='freelabz/secator', version=VERSION)
|
|
1112
1115
|
latest_version = info['latest_version']
|
|
1116
|
+
do_update = True
|
|
1117
|
+
|
|
1118
|
+
# Skip update if latest
|
|
1113
1119
|
if info['status'] == 'latest':
|
|
1114
|
-
console.print(f'
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
if
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1120
|
+
console.print(Info(message=f'secator is already at the newest version {latest_version} !'))
|
|
1121
|
+
do_update = False
|
|
1122
|
+
|
|
1123
|
+
# Fail if unknown latest
|
|
1124
|
+
if not latest_version:
|
|
1125
|
+
console.print(Error(message='Could not fetch latest secator version.'))
|
|
1126
|
+
sys.exit(1)
|
|
1127
|
+
|
|
1128
|
+
# Update secator
|
|
1129
|
+
if do_update:
|
|
1130
|
+
console.print(f'[bold gold3]:wrench: Updating secator from {VERSION} to {latest_version} ...[/]')
|
|
1131
|
+
if 'pipx' in sys.executable:
|
|
1132
|
+
ret = Command.execute(f'pipx install secator=={latest_version} --force')
|
|
1133
|
+
else:
|
|
1134
|
+
ret = Command.execute(f'pip install secator=={latest_version}')
|
|
1135
|
+
if not ret.return_code == 0:
|
|
1136
|
+
sys.exit(1)
|
|
1121
1137
|
|
|
1138
|
+
# Update tools
|
|
1139
|
+
if all:
|
|
1140
|
+
return_code = 0
|
|
1141
|
+
for cls in ALL_TASKS:
|
|
1142
|
+
cmd = cls.cmd.split(' ')[0]
|
|
1143
|
+
version_flag = cls.version_flag or f'{cls.opt_prefix}version'
|
|
1144
|
+
version_flag = None if cls.version_flag == OPT_NOT_SUPPORTED else version_flag
|
|
1145
|
+
info = get_version_info(cmd, version_flag, cls.install_github_handle)
|
|
1146
|
+
if not info['installed'] or info['status'] == 'outdated' or not info['latest_version']:
|
|
1147
|
+
with console.status(f'[bold yellow]Installing {cls.__name__} ...'):
|
|
1148
|
+
status = ToolInstaller.install(cls)
|
|
1149
|
+
if not status.is_ok():
|
|
1150
|
+
return_code = 1
|
|
1151
|
+
sys.exit(return_code)
|
|
1122
1152
|
|
|
1123
1153
|
#-------#
|
|
1124
1154
|
# ALIAS #
|
|
@@ -1220,10 +1250,10 @@ def list_aliases(silent):
|
|
|
1220
1250
|
def test():
|
|
1221
1251
|
"""[dim]Run tests."""
|
|
1222
1252
|
if not DEV_PACKAGE:
|
|
1223
|
-
console.print('
|
|
1253
|
+
console.print(Error(message='You MUST use a development version of secator to run tests.'))
|
|
1224
1254
|
sys.exit(1)
|
|
1225
1255
|
if not ADDONS_ENABLED['dev']:
|
|
1226
|
-
console.print('
|
|
1256
|
+
console.print(Error(message='Missing dev addon: please run [bold green4]secator install addons dev[/]'))
|
|
1227
1257
|
sys.exit(1)
|
|
1228
1258
|
pass
|
|
1229
1259
|
|
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
|
-
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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]
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
|
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:
|
|
16
|
+
# wordlist: combined_subdomains
|
|
17
17
|
# gobuster:
|
|
18
18
|
# input: dns
|
|
19
19
|
# domain_:
|
|
20
20
|
# - target.name
|
|
21
|
-
# wordlist:
|
|
21
|
+
# wordlist: combined_subdomains
|
|
22
22
|
_group:
|
|
23
23
|
nuclei:
|
|
24
24
|
description: Check for subdomain takeovers
|
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}'
|