secator 0.10.1a12__py3-none-any.whl → 0.15.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.
- secator/celery.py +10 -5
- secator/celery_signals.py +2 -11
- secator/cli.py +309 -69
- secator/config.py +3 -2
- secator/configs/profiles/aggressive.yaml +6 -5
- secator/configs/profiles/default.yaml +6 -7
- secator/configs/profiles/insane.yaml +8 -0
- secator/configs/profiles/paranoid.yaml +8 -0
- secator/configs/profiles/polite.yaml +8 -0
- secator/configs/profiles/sneaky.yaml +8 -0
- secator/configs/profiles/tor.yaml +5 -0
- secator/configs/workflows/host_recon.yaml +11 -2
- secator/configs/workflows/url_dirsearch.yaml +5 -0
- secator/configs/workflows/url_params_fuzz.yaml +25 -0
- secator/configs/workflows/wordpress.yaml +4 -1
- secator/decorators.py +64 -34
- secator/definitions.py +8 -4
- secator/installer.py +84 -49
- secator/output_types/__init__.py +2 -1
- secator/output_types/certificate.py +78 -0
- secator/output_types/stat.py +3 -0
- secator/output_types/user_account.py +1 -1
- secator/report.py +2 -2
- secator/rich.py +1 -1
- secator/runners/_base.py +50 -11
- secator/runners/_helpers.py +15 -3
- secator/runners/command.py +85 -21
- secator/runners/scan.py +6 -3
- secator/runners/task.py +1 -0
- secator/runners/workflow.py +22 -4
- secator/tasks/_categories.py +25 -17
- secator/tasks/arjun.py +92 -0
- secator/tasks/bbot.py +33 -4
- secator/tasks/bup.py +4 -2
- secator/tasks/cariddi.py +17 -4
- secator/tasks/dalfox.py +4 -2
- secator/tasks/dirsearch.py +4 -2
- secator/tasks/dnsx.py +5 -2
- secator/tasks/dnsxbrute.py +4 -1
- secator/tasks/feroxbuster.py +5 -2
- secator/tasks/ffuf.py +7 -3
- secator/tasks/fping.py +4 -1
- secator/tasks/gau.py +5 -2
- secator/tasks/gf.py +4 -2
- secator/tasks/gitleaks.py +79 -0
- secator/tasks/gospider.py +5 -2
- secator/tasks/grype.py +5 -2
- secator/tasks/h8mail.py +4 -2
- secator/tasks/httpx.py +6 -3
- secator/tasks/katana.py +6 -3
- secator/tasks/maigret.py +4 -2
- secator/tasks/mapcidr.py +5 -3
- secator/tasks/msfconsole.py +8 -6
- secator/tasks/naabu.py +16 -5
- secator/tasks/nmap.py +31 -29
- secator/tasks/nuclei.py +18 -10
- secator/tasks/searchsploit.py +8 -3
- secator/tasks/subfinder.py +6 -3
- secator/tasks/testssl.py +276 -0
- secator/tasks/trivy.py +98 -0
- secator/tasks/wafw00f.py +85 -0
- secator/tasks/wpprobe.py +96 -0
- secator/tasks/wpscan.py +8 -4
- secator/template.py +61 -67
- secator/utils.py +31 -18
- secator/utils_test.py +34 -10
- {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/METADATA +11 -3
- secator-0.15.1.dist-info/RECORD +128 -0
- secator/configs/profiles/stealth.yaml +0 -7
- secator-0.10.1a12.dist-info/RECORD +0 -116
- {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/WHEEL +0 -0
- {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/entry_points.txt +0 -0
- {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/licenses/LICENSE +0 -0
secator/runners/command.py
CHANGED
|
@@ -30,6 +30,8 @@ class Command(Runner):
|
|
|
30
30
|
# Base cmd
|
|
31
31
|
cmd = None
|
|
32
32
|
|
|
33
|
+
# Tags
|
|
34
|
+
tags = []
|
|
33
35
|
# Meta options
|
|
34
36
|
meta_opts = {}
|
|
35
37
|
|
|
@@ -71,6 +73,7 @@ class Command(Runner):
|
|
|
71
73
|
|
|
72
74
|
# Flag to take a file as input
|
|
73
75
|
file_flag = None
|
|
76
|
+
file_eof_newline = False
|
|
74
77
|
|
|
75
78
|
# Flag to enable output JSON
|
|
76
79
|
json_flag = None
|
|
@@ -83,6 +86,7 @@ class Command(Runner):
|
|
|
83
86
|
install_post = None
|
|
84
87
|
install_cmd = None
|
|
85
88
|
install_github_handle = None
|
|
89
|
+
install_version = None
|
|
86
90
|
|
|
87
91
|
# Serializer
|
|
88
92
|
item_loader = None
|
|
@@ -164,6 +168,9 @@ class Command(Runner):
|
|
|
164
168
|
# Process
|
|
165
169
|
self.process = None
|
|
166
170
|
|
|
171
|
+
# Sudo
|
|
172
|
+
self.requires_sudo = False
|
|
173
|
+
|
|
167
174
|
# Proxy config (global)
|
|
168
175
|
self.proxy = self.run_opts.pop('proxy', False)
|
|
169
176
|
self.configure_proxy()
|
|
@@ -177,6 +184,10 @@ class Command(Runner):
|
|
|
177
184
|
# Run on_cmd hook
|
|
178
185
|
self.run_hooks('on_cmd')
|
|
179
186
|
|
|
187
|
+
# Add sudo to command if it is required
|
|
188
|
+
if self.requires_sudo:
|
|
189
|
+
self.cmd = f'sudo {self.cmd}'
|
|
190
|
+
|
|
180
191
|
# Build item loaders
|
|
181
192
|
instance_func = getattr(self, 'item_loader', None)
|
|
182
193
|
item_loaders = self.item_loaders.copy()
|
|
@@ -228,6 +239,22 @@ class Command(Runner):
|
|
|
228
239
|
dict(self.opts, **self.meta_opts),
|
|
229
240
|
opt_prefix=self.config.name)
|
|
230
241
|
|
|
242
|
+
@classmethod
|
|
243
|
+
def get_version_flag(cls):
|
|
244
|
+
if cls.version_flag == OPT_NOT_SUPPORTED:
|
|
245
|
+
return None
|
|
246
|
+
return cls.version_flag or f'{cls.opt_prefix}version'
|
|
247
|
+
|
|
248
|
+
@classmethod
|
|
249
|
+
def get_version_info(cls):
|
|
250
|
+
from secator.installer import get_version_info
|
|
251
|
+
return get_version_info(
|
|
252
|
+
cls.cmd.split(' ')[0],
|
|
253
|
+
cls.get_version_flag(),
|
|
254
|
+
cls.install_github_handle,
|
|
255
|
+
cls.install_cmd
|
|
256
|
+
)
|
|
257
|
+
|
|
231
258
|
@classmethod
|
|
232
259
|
def get_supported_opts(cls):
|
|
233
260
|
def convert(d):
|
|
@@ -346,6 +373,12 @@ class Command(Runner):
|
|
|
346
373
|
if self.has_children:
|
|
347
374
|
return
|
|
348
375
|
|
|
376
|
+
# Abort if dry run
|
|
377
|
+
if self.dry_run:
|
|
378
|
+
self._print('')
|
|
379
|
+
self.print_command()
|
|
380
|
+
return
|
|
381
|
+
|
|
349
382
|
# Print task description
|
|
350
383
|
self.print_description()
|
|
351
384
|
|
|
@@ -464,15 +497,12 @@ class Command(Runner):
|
|
|
464
497
|
if line is None:
|
|
465
498
|
return
|
|
466
499
|
|
|
500
|
+
# Yield line if no items were yielded
|
|
501
|
+
yield line
|
|
502
|
+
|
|
467
503
|
# Run item_loader to try parsing as dict
|
|
468
|
-
item_count = 0
|
|
469
504
|
for item in self.run_item_loaders(line):
|
|
470
505
|
yield item
|
|
471
|
-
item_count += 1
|
|
472
|
-
|
|
473
|
-
# Yield line if no items were yielded
|
|
474
|
-
if item_count == 0:
|
|
475
|
-
yield line
|
|
476
506
|
|
|
477
507
|
# Skip rest of iteration (no process mode)
|
|
478
508
|
if self.no_process:
|
|
@@ -499,6 +529,7 @@ class Command(Runner):
|
|
|
499
529
|
cmd_str += f' [dim gray11]({self.chunk}/{self.chunk_count})[/]'
|
|
500
530
|
self._print(cmd_str, color='bold cyan', rich=True)
|
|
501
531
|
self.debug('Command', obj={'cmd': self.cmd}, sub='init')
|
|
532
|
+
self.debug('Options', obj={'opts': self.cmd_options}, sub='init')
|
|
502
533
|
|
|
503
534
|
def handle_file_not_found(self, exc):
|
|
504
535
|
"""Handle case where binary is not found.
|
|
@@ -687,11 +718,17 @@ class Command(Runner):
|
|
|
687
718
|
opt_value_map (dict, str | Callable): A dict to map option values with their actual values.
|
|
688
719
|
opt_prefix (str, default: '-'): Option prefix.
|
|
689
720
|
command_name (str | None, default: None): Command name.
|
|
721
|
+
|
|
722
|
+
Returns:
|
|
723
|
+
dict: Processed options dict.
|
|
690
724
|
"""
|
|
691
|
-
|
|
725
|
+
opts_dict = {}
|
|
692
726
|
for opt_name, opt_conf in opts_conf.items():
|
|
693
727
|
debug('before get_opt_value', obj={'name': opt_name, 'conf': opt_conf}, obj_after=False, sub='command.options', verbose=True) # noqa: E501
|
|
694
728
|
|
|
729
|
+
# Save original opt name
|
|
730
|
+
original_opt_name = opt_name
|
|
731
|
+
|
|
695
732
|
# Get opt value
|
|
696
733
|
default_val = opt_conf.get('default')
|
|
697
734
|
opt_val = Command._get_opt_value(
|
|
@@ -743,15 +780,10 @@ class Command(Runner):
|
|
|
743
780
|
|
|
744
781
|
# Append opt name + opt value to option string.
|
|
745
782
|
# Note: does not append opt value if value is True (flag)
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
if opt_val is not True:
|
|
749
|
-
if shlex_quote:
|
|
750
|
-
opt_val = shlex.quote(str(opt_val))
|
|
751
|
-
opts_str += f' {opt_val}'
|
|
752
|
-
debug('final', obj={'name': opt_name, 'value': opt_val}, sub='command.options', obj_after=False, verbose=True)
|
|
783
|
+
opts_dict[original_opt_name] = {'name': opt_name, 'value': opt_val, 'conf': opt_conf}
|
|
784
|
+
debug('final', obj={'name': original_opt_name, 'value': opt_val}, sub='command.options', obj_after=False, verbose=True) # noqa: E501
|
|
753
785
|
|
|
754
|
-
return
|
|
786
|
+
return opts_dict
|
|
755
787
|
|
|
756
788
|
@staticmethod
|
|
757
789
|
def _validate_chunked_input(self, inputs):
|
|
@@ -806,27 +838,57 @@ class Command(Runner):
|
|
|
806
838
|
if self.json_flag:
|
|
807
839
|
self.cmd += f' {self.json_flag}'
|
|
808
840
|
|
|
841
|
+
# Opts str
|
|
842
|
+
opts_str = ''
|
|
843
|
+
opts = {}
|
|
844
|
+
|
|
809
845
|
# Add options to cmd
|
|
810
|
-
|
|
846
|
+
opts_dict = Command._process_opts(
|
|
811
847
|
self.run_opts,
|
|
812
848
|
self.opts,
|
|
813
849
|
self.opt_key_map,
|
|
814
850
|
self.opt_value_map,
|
|
815
851
|
self.opt_prefix,
|
|
816
852
|
command_name=self.config.name)
|
|
817
|
-
if opts_str:
|
|
818
|
-
self.cmd += f' {opts_str}'
|
|
819
853
|
|
|
820
854
|
# Add meta options to cmd
|
|
821
|
-
|
|
855
|
+
meta_opts_dict = Command._process_opts(
|
|
822
856
|
self.run_opts,
|
|
823
857
|
self.meta_opts,
|
|
824
858
|
self.opt_key_map,
|
|
825
859
|
self.opt_value_map,
|
|
826
860
|
self.opt_prefix,
|
|
827
861
|
command_name=self.config.name)
|
|
828
|
-
|
|
829
|
-
|
|
862
|
+
|
|
863
|
+
if opts_dict:
|
|
864
|
+
opts.update(opts_dict)
|
|
865
|
+
if meta_opts_dict:
|
|
866
|
+
opts.update(meta_opts_dict)
|
|
867
|
+
|
|
868
|
+
if opts:
|
|
869
|
+
for opt_conf in opts.values():
|
|
870
|
+
conf = opt_conf['conf']
|
|
871
|
+
internal = conf.get('internal', False)
|
|
872
|
+
if internal:
|
|
873
|
+
continue
|
|
874
|
+
if conf.get('requires_sudo', False):
|
|
875
|
+
self.requires_sudo = True
|
|
876
|
+
opts_str += ' ' + Command._build_opt_str(opt_conf)
|
|
877
|
+
self.cmd_options = opts
|
|
878
|
+
self.cmd += opts_str
|
|
879
|
+
|
|
880
|
+
@staticmethod
|
|
881
|
+
def _build_opt_str(opt):
|
|
882
|
+
"""Build option string."""
|
|
883
|
+
conf = opt['conf']
|
|
884
|
+
opts_str = f'{opt["name"]}'
|
|
885
|
+
shlex_quote = conf.get('shlex', True)
|
|
886
|
+
value = opt['value']
|
|
887
|
+
if value is not True:
|
|
888
|
+
if shlex_quote:
|
|
889
|
+
value = shlex.quote(str(value))
|
|
890
|
+
opts_str += f' {value}'
|
|
891
|
+
return opts_str
|
|
830
892
|
|
|
831
893
|
def _build_cmd_input(self):
|
|
832
894
|
"""Many commands take as input a string or a list. This function facilitate this based on whether we pass a
|
|
@@ -859,6 +921,8 @@ class Command(Runner):
|
|
|
859
921
|
# Write the input to a file
|
|
860
922
|
with open(fpath, 'w') as f:
|
|
861
923
|
f.write('\n'.join(inputs))
|
|
924
|
+
if self.file_eof_newline:
|
|
925
|
+
f.write('\n')
|
|
862
926
|
|
|
863
927
|
if self.file_flag == OPT_PIPE_INPUT:
|
|
864
928
|
cmd = f'cat {fpath} | {cmd}'
|
secator/runners/scan.py
CHANGED
|
@@ -33,9 +33,12 @@ class Scan(Runner):
|
|
|
33
33
|
sigs = []
|
|
34
34
|
for name, workflow_opts in self.config.workflows.items():
|
|
35
35
|
run_opts = self.run_opts.copy()
|
|
36
|
+
run_opts.pop('profiles', None)
|
|
36
37
|
run_opts['no_poll'] = True
|
|
38
|
+
run_opts['caller'] = 'Scan'
|
|
37
39
|
opts = merge_opts(scan_opts, workflow_opts, run_opts)
|
|
38
|
-
|
|
40
|
+
name = name.split('/')[0]
|
|
41
|
+
config = TemplateLoader(name=f'workflow/{name}')
|
|
39
42
|
workflow = Workflow(
|
|
40
43
|
config,
|
|
41
44
|
self.inputs,
|
|
@@ -44,13 +47,13 @@ class Scan(Runner):
|
|
|
44
47
|
hooks=self._hooks,
|
|
45
48
|
context=self.context.copy()
|
|
46
49
|
)
|
|
47
|
-
celery_workflow = workflow.build_celery_workflow()
|
|
50
|
+
celery_workflow = workflow.build_celery_workflow(chain_previous_results=True)
|
|
48
51
|
for task_id, task_info in workflow.celery_ids_map.items():
|
|
49
52
|
self.add_subtask(task_id, task_info['name'], task_info['descr'])
|
|
50
53
|
sigs.append(celery_workflow)
|
|
51
54
|
|
|
52
55
|
return chain(
|
|
53
|
-
mark_runner_started.si(self).set(queue='results'),
|
|
56
|
+
mark_runner_started.si([], self).set(queue='results'),
|
|
54
57
|
*sigs,
|
|
55
58
|
mark_runner_completed.s(self).set(queue='results'),
|
|
56
59
|
)
|
secator/runners/task.py
CHANGED
secator/runners/workflow.py
CHANGED
|
@@ -20,9 +20,12 @@ class Workflow(Runner):
|
|
|
20
20
|
from secator.celery import run_workflow
|
|
21
21
|
return run_workflow.s(args=args, kwargs=kwargs)
|
|
22
22
|
|
|
23
|
-
def build_celery_workflow(self):
|
|
23
|
+
def build_celery_workflow(self, chain_previous_results=False):
|
|
24
24
|
"""Build Celery workflow for workflow execution.
|
|
25
25
|
|
|
26
|
+
Args:
|
|
27
|
+
chain_previous_results (bool): Chain previous results.
|
|
28
|
+
|
|
26
29
|
Returns:
|
|
27
30
|
celery.Signature: Celery task signature.
|
|
28
31
|
"""
|
|
@@ -49,21 +52,31 @@ class Workflow(Runner):
|
|
|
49
52
|
opts['skip_if_no_inputs'] = True
|
|
50
53
|
opts['caller'] = 'Workflow'
|
|
51
54
|
|
|
55
|
+
forwarded_opts = {}
|
|
56
|
+
if chain_previous_results:
|
|
57
|
+
forwarded_opts = {k: v for k, v in self.run_opts.items() if k.endswith('_')}
|
|
58
|
+
|
|
52
59
|
# Build task signatures
|
|
53
60
|
sigs = self.get_tasks(
|
|
54
61
|
self.config.tasks.toDict(),
|
|
55
62
|
self.inputs,
|
|
56
63
|
self.config.options,
|
|
57
|
-
opts
|
|
64
|
+
opts,
|
|
65
|
+
forwarded_opts=forwarded_opts
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
start_sig = mark_runner_started.si([], self, enable_hooks=True).set(queue='results')
|
|
69
|
+
if chain_previous_results:
|
|
70
|
+
start_sig = mark_runner_started.s(self, enable_hooks=True).set(queue='results')
|
|
58
71
|
|
|
59
72
|
# Build workflow chain with lifecycle management
|
|
60
73
|
return chain(
|
|
61
|
-
|
|
74
|
+
start_sig,
|
|
62
75
|
*sigs,
|
|
63
76
|
mark_runner_completed.s(self, enable_hooks=True).set(queue='results'),
|
|
64
77
|
)
|
|
65
78
|
|
|
66
|
-
def get_tasks(self, config, inputs, workflow_opts, run_opts):
|
|
79
|
+
def get_tasks(self, config, inputs, workflow_opts, run_opts, forwarded_opts={}):
|
|
67
80
|
"""Get tasks recursively as Celery chains / chords.
|
|
68
81
|
|
|
69
82
|
Args:
|
|
@@ -71,6 +84,7 @@ class Workflow(Runner):
|
|
|
71
84
|
inputs (list): Inputs.
|
|
72
85
|
workflow_opts (dict): Workflow options.
|
|
73
86
|
run_opts (dict): Run options.
|
|
87
|
+
forwarded_opts (dict): Opts forwarded from parent runner (e.g: scan).
|
|
74
88
|
sync (bool): Synchronous mode (chain of tasks, no chords).
|
|
75
89
|
|
|
76
90
|
Returns:
|
|
@@ -78,6 +92,7 @@ class Workflow(Runner):
|
|
|
78
92
|
"""
|
|
79
93
|
from celery import chain, group
|
|
80
94
|
sigs = []
|
|
95
|
+
ix = 0
|
|
81
96
|
for task_name, task_opts in config.items():
|
|
82
97
|
# Task opts can be None
|
|
83
98
|
task_opts = task_opts or {}
|
|
@@ -105,6 +120,8 @@ class Workflow(Runner):
|
|
|
105
120
|
|
|
106
121
|
# Merge task options (order of priority with overrides)
|
|
107
122
|
opts = merge_opts(workflow_opts, task_opts, run_opts)
|
|
123
|
+
if ix == 0 and forwarded_opts:
|
|
124
|
+
opts.update(forwarded_opts)
|
|
108
125
|
opts['name'] = task_name
|
|
109
126
|
|
|
110
127
|
# Create task signature
|
|
@@ -113,5 +130,6 @@ class Workflow(Runner):
|
|
|
113
130
|
sig = task.s(inputs, **opts).set(queue=task.profile, task_id=task_id)
|
|
114
131
|
self.add_subtask(task_id, task_name, task_opts.get('description', ''))
|
|
115
132
|
self.output_types.extend(task.output_types)
|
|
133
|
+
ix += 1
|
|
116
134
|
sigs.append(sig)
|
|
117
135
|
return sigs
|
secator/tasks/_categories.py
CHANGED
|
@@ -18,11 +18,16 @@ from secator.config import CONFIG
|
|
|
18
18
|
from secator.runners import Command
|
|
19
19
|
from secator.utils import debug, process_wordlist
|
|
20
20
|
|
|
21
|
+
USER_AGENTS = {
|
|
22
|
+
'chrome_134.0_win10': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36', # noqa: E501
|
|
23
|
+
'chrome_134.0_macos': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36', # noqa: E501
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
|
|
22
27
|
OPTS = {
|
|
23
|
-
HEADER: {'type': str, 'help': 'Custom header to add to each request in the form "KEY1:VALUE1; KEY2:VALUE2"'},
|
|
28
|
+
HEADER: {'type': str, 'help': 'Custom header to add to each request in the form "KEY1:VALUE1; KEY2:VALUE2"', 'default': 'User-Agent: ' + USER_AGENTS['chrome_134.0_win10']}, # noqa: E501
|
|
24
29
|
DELAY: {'type': float, 'short': 'd', 'help': 'Delay to add between each requests'},
|
|
25
|
-
DEPTH: {'type': int, 'help': 'Scan depth'
|
|
30
|
+
DEPTH: {'type': int, 'help': 'Scan depth'},
|
|
26
31
|
FILTER_CODES: {'type': str, 'short': 'fc', 'help': 'Filter out responses with HTTP codes'},
|
|
27
32
|
FILTER_REGEX: {'type': str, 'short': 'fr', 'help': 'Filter out responses with regular expression'},
|
|
28
33
|
FILTER_SIZE: {'type': str, 'short': 'fs', 'help': 'Filter out responses with size'},
|
|
@@ -36,7 +41,7 @@ OPTS = {
|
|
|
36
41
|
PROXY: {'type': str, 'help': 'HTTP(s) / SOCKS5 proxy'},
|
|
37
42
|
RATE_LIMIT: {'type': int, 'short': 'rl', 'help': 'Rate limit, i.e max number of requests per second'},
|
|
38
43
|
RETRIES: {'type': int, 'help': 'Retries'},
|
|
39
|
-
THREADS: {'type': int, 'help': 'Number of threads to run'
|
|
44
|
+
THREADS: {'type': int, 'help': 'Number of threads to run'},
|
|
40
45
|
TIMEOUT: {'type': int, 'help': 'Request timeout'},
|
|
41
46
|
USER_AGENT: {'type': str, 'short': 'ua', 'help': 'User agent, e.g "Mozilla Firefox 1.0"'},
|
|
42
47
|
WORDLIST: {'type': str, 'short': 'w', 'default': 'http', 'process': process_wordlist, 'help': 'Wordlist to use'}
|
|
@@ -68,19 +73,19 @@ OPTS_VULN = [
|
|
|
68
73
|
|
|
69
74
|
class Http(Command):
|
|
70
75
|
meta_opts = {k: OPTS[k] for k in OPTS_HTTP_CRAWLERS}
|
|
71
|
-
|
|
76
|
+
input_types = [URL]
|
|
72
77
|
output_types = [Url]
|
|
73
78
|
|
|
74
79
|
|
|
75
80
|
class HttpCrawler(Command):
|
|
76
81
|
meta_opts = {k: OPTS[k] for k in OPTS_HTTP_CRAWLERS}
|
|
77
|
-
|
|
82
|
+
input_types = [URL]
|
|
78
83
|
output_types = [Url]
|
|
79
84
|
|
|
80
85
|
|
|
81
86
|
class HttpFuzzer(Command):
|
|
82
87
|
meta_opts = {k: OPTS[k] for k in OPTS_HTTP_FUZZERS}
|
|
83
|
-
|
|
88
|
+
input_types = [URL]
|
|
84
89
|
output_types = [Url]
|
|
85
90
|
|
|
86
91
|
|
|
@@ -94,22 +99,22 @@ class Recon(Command):
|
|
|
94
99
|
|
|
95
100
|
|
|
96
101
|
class ReconDns(Recon):
|
|
97
|
-
|
|
102
|
+
input_types = [HOST]
|
|
98
103
|
output_types = [Subdomain]
|
|
99
104
|
|
|
100
105
|
|
|
101
106
|
class ReconUser(Recon):
|
|
102
|
-
|
|
107
|
+
input_types = [USERNAME]
|
|
103
108
|
output_types = [UserAccount]
|
|
104
109
|
|
|
105
110
|
|
|
106
111
|
class ReconIp(Recon):
|
|
107
|
-
|
|
112
|
+
input_types = [CIDR_RANGE]
|
|
108
113
|
output_types = [Ip]
|
|
109
114
|
|
|
110
115
|
|
|
111
116
|
class ReconPort(Recon):
|
|
112
|
-
|
|
117
|
+
input_types = [IP]
|
|
113
118
|
output_types = [Port]
|
|
114
119
|
|
|
115
120
|
|
|
@@ -388,11 +393,11 @@ class Vuln(Command):
|
|
|
388
393
|
|
|
389
394
|
@cache
|
|
390
395
|
@staticmethod
|
|
391
|
-
def
|
|
396
|
+
def lookup_cve_from_ghsa(ghsa_id):
|
|
392
397
|
"""Search for a GHSA on Github and and return associated CVE vulnerability data.
|
|
393
398
|
|
|
394
399
|
Args:
|
|
395
|
-
ghsa (str):
|
|
400
|
+
ghsa (str): GHSA ID in the form GHSA-*
|
|
396
401
|
|
|
397
402
|
Returns:
|
|
398
403
|
dict: vulnerability data.
|
|
@@ -405,7 +410,10 @@ class Vuln(Command):
|
|
|
405
410
|
return None
|
|
406
411
|
soup = BeautifulSoup(resp.text, 'lxml')
|
|
407
412
|
sidebar_items = soup.find_all('div', {'class': 'discussion-sidebar-item'})
|
|
408
|
-
cve_id = sidebar_items[
|
|
413
|
+
cve_id = sidebar_items[3].find('div').text.strip()
|
|
414
|
+
if not cve_id.startswith('CVE'):
|
|
415
|
+
debug(f'{ghsa_id}: No CVE_ID extracted from https://github.com/advisories/{ghsa_id}', sub='cve')
|
|
416
|
+
return None
|
|
409
417
|
vuln = Vuln.lookup_cve(cve_id)
|
|
410
418
|
if vuln:
|
|
411
419
|
vuln[TAGS].append('ghsa')
|
|
@@ -426,15 +434,15 @@ class Vuln(Command):
|
|
|
426
434
|
|
|
427
435
|
|
|
428
436
|
class VulnHttp(Vuln):
|
|
429
|
-
|
|
437
|
+
input_types = [HOST]
|
|
430
438
|
|
|
431
439
|
|
|
432
440
|
class VulnCode(Vuln):
|
|
433
|
-
|
|
441
|
+
input_types = [PATH]
|
|
434
442
|
|
|
435
443
|
|
|
436
444
|
class VulnMulti(Vuln):
|
|
437
|
-
|
|
445
|
+
input_types = [HOST]
|
|
438
446
|
output_types = [Vulnerability]
|
|
439
447
|
|
|
440
448
|
|
|
@@ -443,7 +451,7 @@ class VulnMulti(Vuln):
|
|
|
443
451
|
#--------------#
|
|
444
452
|
|
|
445
453
|
class Tagger(Command):
|
|
446
|
-
|
|
454
|
+
input_types = [URL]
|
|
447
455
|
output_types = [Tag]
|
|
448
456
|
|
|
449
457
|
#----------------#
|
secator/tasks/arjun.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import yaml
|
|
3
|
+
|
|
4
|
+
from secator.decorators import task
|
|
5
|
+
from secator.definitions import (OUTPUT_PATH, RATE_LIMIT, THREADS, DELAY, TIMEOUT, METHOD, WORDLIST,
|
|
6
|
+
HEADER, URL, FOLLOW_REDIRECT)
|
|
7
|
+
from secator.output_types import Info, Url, Warning, Error
|
|
8
|
+
from secator.runners import Command
|
|
9
|
+
from secator.tasks._categories import OPTS
|
|
10
|
+
from secator.utils import process_wordlist
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@task()
|
|
14
|
+
class arjun(Command):
|
|
15
|
+
"""HTTP Parameter Discovery Suite."""
|
|
16
|
+
cmd = 'arjun'
|
|
17
|
+
tags = ['url', 'fuzz', 'params']
|
|
18
|
+
input_flag = '-u'
|
|
19
|
+
input_types = [URL]
|
|
20
|
+
version_flag = ' '
|
|
21
|
+
opts = {
|
|
22
|
+
'chunk_size': {'type': int, 'help': 'Control query/chunk size'},
|
|
23
|
+
'stable': {'is_flag': True, 'default': False, 'help': 'Use stable mode'},
|
|
24
|
+
'include': {'type': str, 'help': 'Include persistent data (e.g: "api_key=xxxxx" or {"api_key": "xxxx"})'},
|
|
25
|
+
'passive': {'is_flag': True, 'default': False, 'help': 'Passive mode'},
|
|
26
|
+
'casing': {'type': str, 'help': 'Casing style for params e.g. like_this, likeThis, LIKE_THIS, like_this'}, # noqa: E501
|
|
27
|
+
WORDLIST: {'type': str, 'short': 'w', 'default': None, 'process': process_wordlist, 'help': 'Wordlist to use (default: arjun wordlist)'}, # noqa: E501
|
|
28
|
+
}
|
|
29
|
+
meta_opts = {
|
|
30
|
+
THREADS: OPTS[THREADS],
|
|
31
|
+
DELAY: OPTS[DELAY],
|
|
32
|
+
TIMEOUT: OPTS[TIMEOUT],
|
|
33
|
+
RATE_LIMIT: OPTS[RATE_LIMIT],
|
|
34
|
+
METHOD: OPTS[METHOD],
|
|
35
|
+
HEADER: OPTS[HEADER],
|
|
36
|
+
FOLLOW_REDIRECT: OPTS[FOLLOW_REDIRECT],
|
|
37
|
+
}
|
|
38
|
+
opt_key_map = {
|
|
39
|
+
THREADS: 't',
|
|
40
|
+
DELAY: 'd',
|
|
41
|
+
TIMEOUT: 'T',
|
|
42
|
+
RATE_LIMIT: '--rate-limit',
|
|
43
|
+
METHOD: 'm',
|
|
44
|
+
WORDLIST: 'w',
|
|
45
|
+
HEADER: '--headers',
|
|
46
|
+
'chunk_size': 'c',
|
|
47
|
+
'stable': '--stable',
|
|
48
|
+
'passive': '--passive',
|
|
49
|
+
'casing': '--casing',
|
|
50
|
+
'follow_redirect': '--follow-redirect',
|
|
51
|
+
}
|
|
52
|
+
output_types = [Url]
|
|
53
|
+
install_version = '2.2.7'
|
|
54
|
+
install_cmd = 'pipx install arjun==[install_version] --force'
|
|
55
|
+
install_github_handle = 's0md3v/Arjun'
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def on_line(self, line):
|
|
59
|
+
if 'Processing chunks' in line:
|
|
60
|
+
return ''
|
|
61
|
+
return line
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def on_cmd(self):
|
|
65
|
+
follow_redirect = self.get_opt_value(FOLLOW_REDIRECT)
|
|
66
|
+
self.cmd = self.cmd.replace(' --follow-redirect', '')
|
|
67
|
+
if not follow_redirect:
|
|
68
|
+
self.cmd += ' --disable-redirects'
|
|
69
|
+
|
|
70
|
+
self.output_path = self.get_opt_value(OUTPUT_PATH)
|
|
71
|
+
if not self.output_path:
|
|
72
|
+
self.output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.json'
|
|
73
|
+
self.cmd += f' -oJ {self.output_path}'
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def on_cmd_done(self):
|
|
77
|
+
if not os.path.exists(self.output_path):
|
|
78
|
+
yield Error(message=f'Could not find JSON results in {self.output_path}')
|
|
79
|
+
return
|
|
80
|
+
yield Info(message=f'JSON results saved to {self.output_path}')
|
|
81
|
+
with open(self.output_path, 'r') as f:
|
|
82
|
+
results = yaml.safe_load(f.read())
|
|
83
|
+
if not results:
|
|
84
|
+
yield Warning(message='No results found !')
|
|
85
|
+
return
|
|
86
|
+
for url, values in results.items():
|
|
87
|
+
for param in values['params']:
|
|
88
|
+
yield Url(
|
|
89
|
+
url=url + '?' + param + '=' + 'FUZZ',
|
|
90
|
+
headers=values['headers'],
|
|
91
|
+
method=values['method'],
|
|
92
|
+
)
|
secator/tasks/bbot.py
CHANGED
|
@@ -2,6 +2,7 @@ import shutil
|
|
|
2
2
|
|
|
3
3
|
from secator.config import CONFIG
|
|
4
4
|
from secator.decorators import task
|
|
5
|
+
from secator.definitions import FILENAME, HOST, IP, ORG_NAME, PORT, URL, USERNAME
|
|
5
6
|
from secator.runners import Command
|
|
6
7
|
from secator.serializers import RegexSerializer
|
|
7
8
|
from secator.output_types import Vulnerability, Port, Url, Record, Ip, Tag, Info, Error
|
|
@@ -121,6 +122,29 @@ BBOT_PRESETS = [
|
|
|
121
122
|
'web-screenshots',
|
|
122
123
|
'web-thorough'
|
|
123
124
|
]
|
|
125
|
+
BBOT_FLAGS = [
|
|
126
|
+
'active',
|
|
127
|
+
'affiliates',
|
|
128
|
+
'aggressive',
|
|
129
|
+
'baddns',
|
|
130
|
+
'cloud-enum,'
|
|
131
|
+
'code-enum,deadly',
|
|
132
|
+
'email-enum',
|
|
133
|
+
'iis-shortnames',
|
|
134
|
+
'passive',
|
|
135
|
+
'portscan',
|
|
136
|
+
'report',
|
|
137
|
+
'safe',
|
|
138
|
+
'service-enum',
|
|
139
|
+
'slow',
|
|
140
|
+
'social-enum',
|
|
141
|
+
'subdomain-enum',
|
|
142
|
+
'subdomain-hijack',
|
|
143
|
+
'web-basic',
|
|
144
|
+
'web-paramminer',
|
|
145
|
+
'web-screenshots',
|
|
146
|
+
'web-thorough'
|
|
147
|
+
]
|
|
124
148
|
BBOT_MODULES_STR = ' '.join(BBOT_MODULES)
|
|
125
149
|
BBOT_MAP_TYPES = {
|
|
126
150
|
'IP_ADDRESS': Ip,
|
|
@@ -154,17 +178,21 @@ def output_discriminator(self, item):
|
|
|
154
178
|
class bbot(Command):
|
|
155
179
|
"""Multipurpose scanner."""
|
|
156
180
|
cmd = 'bbot -y --allow-deadly --force'
|
|
181
|
+
tags = ['vuln', 'scan']
|
|
157
182
|
json_flag = '--json'
|
|
158
183
|
input_flag = '-t'
|
|
184
|
+
input_types = [HOST, IP, URL, PORT, ORG_NAME, USERNAME, FILENAME]
|
|
159
185
|
file_flag = None
|
|
160
186
|
version_flag = '--help'
|
|
161
187
|
opts = {
|
|
162
|
-
'modules': {'type': str, 'short': 'm', '
|
|
163
|
-
'presets': {'type': str, 'short': 'ps', '
|
|
188
|
+
'modules': {'type': str, 'short': 'm', 'help': ','.join(BBOT_MODULES)},
|
|
189
|
+
'presets': {'type': str, 'short': 'ps', 'help': ','.join(BBOT_PRESETS), 'shlex': False},
|
|
190
|
+
'flags': {'type': str, 'short': 'fl', 'help': ','.join(BBOT_FLAGS)}
|
|
164
191
|
}
|
|
165
192
|
opt_key_map = {
|
|
166
193
|
'modules': 'm',
|
|
167
|
-
'presets': 'p'
|
|
194
|
+
'presets': 'p',
|
|
195
|
+
'flags': 'f'
|
|
168
196
|
}
|
|
169
197
|
opt_value_map = {
|
|
170
198
|
'presets': lambda x: ' '.join(x.split(','))
|
|
@@ -222,7 +250,8 @@ class bbot(Command):
|
|
|
222
250
|
'apk': ['python3-dev', 'linux-headers', 'musl-dev', 'gcc', 'git', 'openssl', 'unzip', 'tar', 'chromium'],
|
|
223
251
|
'*': ['gcc', 'git', 'openssl', 'unzip', 'tar', 'chromium']
|
|
224
252
|
}
|
|
225
|
-
|
|
253
|
+
install_version = '2.4.2'
|
|
254
|
+
install_cmd = 'pipx install bbot==[install_version] --force'
|
|
226
255
|
install_post = {
|
|
227
256
|
'*': f'rm -fr {CONFIG.dirs.share}/pipx/venvs/bbot/lib/python3.12/site-packages/ansible_collections/*'
|
|
228
257
|
}
|
secator/tasks/bup.py
CHANGED
|
@@ -16,8 +16,9 @@ from secator.tasks._categories import Http
|
|
|
16
16
|
class bup(Http):
|
|
17
17
|
"""40X bypasser."""
|
|
18
18
|
cmd = 'bup'
|
|
19
|
+
tags = ['url', 'bypass']
|
|
19
20
|
input_flag = '-u'
|
|
20
|
-
|
|
21
|
+
input_types = [URL]
|
|
21
22
|
json_flag = '--jsonl'
|
|
22
23
|
opt_prefix = '--'
|
|
23
24
|
opts = {
|
|
@@ -63,7 +64,8 @@ class bup(Http):
|
|
|
63
64
|
'stored_response_path': 'response_html_filename',
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
+
install_version = '0.4.4'
|
|
68
|
+
install_cmd = 'pipx install bypass-url-parser==[install_version] --force'
|
|
67
69
|
|
|
68
70
|
@staticmethod
|
|
69
71
|
def on_init(self):
|