secator 0.10.1a12__py3-none-any.whl → 0.11.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 +10 -5
- secator/celery_signals.py +2 -11
- secator/cli.py +153 -46
- secator/configs/workflows/url_params_fuzz.yaml +23 -0
- secator/configs/workflows/wordpress.yaml +4 -1
- secator/decorators.py +10 -5
- secator/definitions.py +3 -0
- secator/installer.py +46 -32
- secator/output_types/__init__.py +2 -1
- secator/output_types/certificate.py +78 -0
- secator/output_types/user_account.py +1 -1
- secator/rich.py +1 -1
- secator/runners/_base.py +14 -6
- secator/runners/_helpers.py +4 -3
- secator/runners/command.py +81 -21
- secator/runners/scan.py +5 -3
- secator/runners/workflow.py +22 -4
- secator/tasks/_categories.py +6 -1
- secator/tasks/arjun.py +82 -0
- secator/tasks/ffuf.py +2 -1
- secator/tasks/gitleaks.py +76 -0
- secator/tasks/mapcidr.py +1 -1
- secator/tasks/naabu.py +7 -1
- secator/tasks/nmap.py +29 -29
- secator/tasks/subfinder.py +1 -1
- secator/tasks/testssl.py +274 -0
- secator/tasks/trivy.py +95 -0
- secator/tasks/wafw00f.py +83 -0
- secator/tasks/wpprobe.py +94 -0
- secator/template.py +49 -67
- secator/utils.py +13 -5
- secator/utils_test.py +26 -8
- {secator-0.10.1a12.dist-info → secator-0.11.0.dist-info}/METADATA +1 -1
- {secator-0.10.1a12.dist-info → secator-0.11.0.dist-info}/RECORD +37 -29
- {secator-0.10.1a12.dist-info → secator-0.11.0.dist-info}/WHEEL +0 -0
- {secator-0.10.1a12.dist-info → secator-0.11.0.dist-info}/entry_points.txt +0 -0
- {secator-0.10.1a12.dist-info → secator-0.11.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from secator.output_types import OutputType
|
|
5
|
+
from secator.utils import rich_to_ansi
|
|
6
|
+
from secator.definitions import CERTIFICATE_STATUS_UNKNOWN
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Certificate(OutputType):
|
|
11
|
+
host: str
|
|
12
|
+
fingerprint_sha256: str = field(default='')
|
|
13
|
+
ip: str = field(default='', compare=False)
|
|
14
|
+
raw_value: str = field(default='', compare=False)
|
|
15
|
+
subject_cn: str = field(default='', compare=False)
|
|
16
|
+
subject_an: list[str] = field(default_factory=list, compare=False)
|
|
17
|
+
not_before: datetime = field(default=None, compare=False)
|
|
18
|
+
not_after: datetime = field(default=None, compare=False)
|
|
19
|
+
issuer_dn: str = field(default='', compare=False)
|
|
20
|
+
issuer_cn: str = field(default='', compare=False)
|
|
21
|
+
issuer: str = field(default='', compare=False)
|
|
22
|
+
self_signed: bool = field(default=True, compare=False)
|
|
23
|
+
trusted: bool = field(default=False, compare=False)
|
|
24
|
+
status: str = field(default=CERTIFICATE_STATUS_UNKNOWN, compare=False)
|
|
25
|
+
keysize: int = field(default=None, compare=False)
|
|
26
|
+
serial_number: str = field(default='', compare=False)
|
|
27
|
+
ciphers: list[str] = field(default_factory=list, compare=False)
|
|
28
|
+
# parent_certificate: 'Certificate' = None # noqa: F821
|
|
29
|
+
_source: str = field(default='', repr=True)
|
|
30
|
+
_type: str = field(default='certificate', repr=True)
|
|
31
|
+
_timestamp: int = field(default_factory=lambda: time.time(), compare=False)
|
|
32
|
+
_uuid: str = field(default='', repr=True, compare=False)
|
|
33
|
+
_context: dict = field(default_factory=dict, repr=True, compare=False)
|
|
34
|
+
_tagged: bool = field(default=False, repr=True, compare=False)
|
|
35
|
+
_duplicate: bool = field(default=False, repr=True, compare=False)
|
|
36
|
+
_related: list = field(default_factory=list, compare=False)
|
|
37
|
+
_table_fields = ['ip', 'host']
|
|
38
|
+
_sort_by = ('ip',)
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
return self.subject_cn
|
|
42
|
+
|
|
43
|
+
def is_expired(self) -> bool:
|
|
44
|
+
if self.not_after:
|
|
45
|
+
return self.not_after < datetime.now()
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
def is_expired_soon(self, months: int = 1) -> bool:
|
|
49
|
+
if self.not_after:
|
|
50
|
+
return self.not_after < datetime.now() + timedelta(days=months * 30)
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def format_date(date):
|
|
55
|
+
if date:
|
|
56
|
+
return date.strftime("%m/%d/%Y")
|
|
57
|
+
return '?'
|
|
58
|
+
|
|
59
|
+
def __repr__(self) -> str:
|
|
60
|
+
s = f'📜 [bold white]{self.host}[/]'
|
|
61
|
+
s += f' [cyan]{self.status}[/]'
|
|
62
|
+
s += rf' [white]\[fingerprint={self.fingerprint_sha256[:10]}][/]'
|
|
63
|
+
if self.subject_cn:
|
|
64
|
+
s += rf' [white]\[cn={self.subject_cn}][/]'
|
|
65
|
+
if self.subject_an:
|
|
66
|
+
s += rf' [white]\[an={", ".join(self.subject_an)}][/]'
|
|
67
|
+
if self.issuer:
|
|
68
|
+
s += rf' [white]\[issuer={self.issuer}][/]'
|
|
69
|
+
elif self.issuer_cn:
|
|
70
|
+
s += rf' [white]\[issuer_cn={self.issuer_cn}][/]'
|
|
71
|
+
expiry_date = Certificate.format_date(self.not_after)
|
|
72
|
+
if self.is_expired():
|
|
73
|
+
s += f' [red]expired since {expiry_date}[/red]'
|
|
74
|
+
elif self.is_expired_soon(months=2):
|
|
75
|
+
s += f' [yellow]expires <2 months[/yellow], [yellow]valid until {expiry_date}[/yellow]'
|
|
76
|
+
else:
|
|
77
|
+
s += f' [green]not expired[/green], [yellow]valid until {expiry_date}[/yellow]'
|
|
78
|
+
return rich_to_ansi(s)
|
|
@@ -37,5 +37,5 @@ class UserAccount(OutputType):
|
|
|
37
37
|
if self.url:
|
|
38
38
|
s += rf' \[[white]{_s(self.url)}[/]]'
|
|
39
39
|
if self.extra_data:
|
|
40
|
-
s += r' \[[bold yellow]' + _s(', '.join(f'{k}:{v}' for k, v in self.extra_data.items()) + '[/]]'
|
|
40
|
+
s += r' \[[bold yellow]' + _s(', '.join(f'{k}:{v}' for k, v in self.extra_data.items())) + '[/]]'
|
|
41
41
|
return rich_to_ansi(s)
|
secator/rich.py
CHANGED
|
@@ -4,7 +4,7 @@ import yaml
|
|
|
4
4
|
from rich.console import Console
|
|
5
5
|
from rich.table import Table
|
|
6
6
|
|
|
7
|
-
console = Console(stderr=True)
|
|
7
|
+
console = Console(stderr=True, record=True)
|
|
8
8
|
console_stdout = Console(record=True)
|
|
9
9
|
# handler = RichHandler(rich_tracebacks=True) # TODO: add logging handler
|
|
10
10
|
|
secator/runners/_base.py
CHANGED
|
@@ -102,6 +102,7 @@ class Runner:
|
|
|
102
102
|
self.piped_input = self.run_opts.get('piped_input', False)
|
|
103
103
|
self.piped_output = self.run_opts.get('piped_output', False)
|
|
104
104
|
self.enable_duplicate_check = self.run_opts.get('enable_duplicate_check', True)
|
|
105
|
+
self.dry_run = self.run_opts.get('dry_run', False)
|
|
105
106
|
|
|
106
107
|
# Runner print opts
|
|
107
108
|
self.print_item = self.run_opts.get('print_item', False)
|
|
@@ -128,12 +129,8 @@ class Runner:
|
|
|
128
129
|
[self.add_result(result, print=False, output=False) for result in results]
|
|
129
130
|
|
|
130
131
|
# Determine inputs
|
|
131
|
-
inputs = [inputs] if not isinstance(inputs, list) else inputs
|
|
132
|
-
|
|
133
|
-
inputs, run_opts, errors = run_extractors(self.results, run_opts, inputs)
|
|
134
|
-
for error in errors:
|
|
135
|
-
self.add_result(error, print=True)
|
|
136
|
-
self.inputs = list(set(inputs))
|
|
132
|
+
self.inputs = [inputs] if not isinstance(inputs, list) else inputs
|
|
133
|
+
self.filter_results(results)
|
|
137
134
|
|
|
138
135
|
# Debug
|
|
139
136
|
self.debug('Inputs', obj=self.inputs, sub='init')
|
|
@@ -310,6 +307,8 @@ class Runner:
|
|
|
310
307
|
self.mark_completed()
|
|
311
308
|
|
|
312
309
|
finally:
|
|
310
|
+
if self.dry_run:
|
|
311
|
+
return
|
|
313
312
|
if self.sync:
|
|
314
313
|
self.mark_completed()
|
|
315
314
|
if self.enable_reports:
|
|
@@ -328,6 +327,15 @@ class Runner:
|
|
|
328
327
|
self.add_result(error, print=True)
|
|
329
328
|
yield error
|
|
330
329
|
|
|
330
|
+
def filter_results(self, results):
|
|
331
|
+
"""Filter results based on the runner's config."""
|
|
332
|
+
if not self.chunk:
|
|
333
|
+
inputs, run_opts, errors = run_extractors(results, self.run_opts, self.inputs, self.dry_run)
|
|
334
|
+
for error in errors:
|
|
335
|
+
self.add_result(error, print=True)
|
|
336
|
+
self.inputs = list(set(inputs))
|
|
337
|
+
self.run_opts = run_opts
|
|
338
|
+
|
|
331
339
|
def add_result(self, item, print=False, output=True):
|
|
332
340
|
"""Add item to runner results.
|
|
333
341
|
|
secator/runners/_helpers.py
CHANGED
|
@@ -4,13 +4,14 @@ from secator.output_types import Error
|
|
|
4
4
|
from secator.utils import deduplicate, debug
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
def run_extractors(results, opts, inputs=[]):
|
|
7
|
+
def run_extractors(results, opts, inputs=[], dry_run=False):
|
|
8
8
|
"""Run extractors and merge extracted values with option dict.
|
|
9
9
|
|
|
10
10
|
Args:
|
|
11
11
|
results (list): List of results.
|
|
12
12
|
opts (dict): Options.
|
|
13
13
|
inputs (list): Original inputs.
|
|
14
|
+
dry_run (bool): Dry run.
|
|
14
15
|
|
|
15
16
|
Returns:
|
|
16
17
|
tuple: inputs, options, errors.
|
|
@@ -22,9 +23,9 @@ def run_extractors(results, opts, inputs=[]):
|
|
|
22
23
|
values, err = extract_from_results(results, val)
|
|
23
24
|
errors.extend(err)
|
|
24
25
|
if key == 'targets':
|
|
25
|
-
inputs = deduplicate(values)
|
|
26
|
+
inputs = ['<COMPUTED>'] if dry_run else deduplicate(values)
|
|
26
27
|
else:
|
|
27
|
-
opts[key] = deduplicate(values)
|
|
28
|
+
opts[key] = ['<COMPUTED>'] if dry_run else deduplicate(values)
|
|
28
29
|
return inputs, opts, errors
|
|
29
30
|
|
|
30
31
|
|
secator/runners/command.py
CHANGED
|
@@ -71,6 +71,7 @@ class Command(Runner):
|
|
|
71
71
|
|
|
72
72
|
# Flag to take a file as input
|
|
73
73
|
file_flag = None
|
|
74
|
+
file_eof_newline = False
|
|
74
75
|
|
|
75
76
|
# Flag to enable output JSON
|
|
76
77
|
json_flag = None
|
|
@@ -164,6 +165,9 @@ class Command(Runner):
|
|
|
164
165
|
# Process
|
|
165
166
|
self.process = None
|
|
166
167
|
|
|
168
|
+
# Sudo
|
|
169
|
+
self.requires_sudo = False
|
|
170
|
+
|
|
167
171
|
# Proxy config (global)
|
|
168
172
|
self.proxy = self.run_opts.pop('proxy', False)
|
|
169
173
|
self.configure_proxy()
|
|
@@ -177,6 +181,10 @@ class Command(Runner):
|
|
|
177
181
|
# Run on_cmd hook
|
|
178
182
|
self.run_hooks('on_cmd')
|
|
179
183
|
|
|
184
|
+
# Add sudo to command if it is required
|
|
185
|
+
if self.requires_sudo:
|
|
186
|
+
self.cmd = f'sudo {self.cmd}'
|
|
187
|
+
|
|
180
188
|
# Build item loaders
|
|
181
189
|
instance_func = getattr(self, 'item_loader', None)
|
|
182
190
|
item_loaders = self.item_loaders.copy()
|
|
@@ -228,6 +236,22 @@ class Command(Runner):
|
|
|
228
236
|
dict(self.opts, **self.meta_opts),
|
|
229
237
|
opt_prefix=self.config.name)
|
|
230
238
|
|
|
239
|
+
@classmethod
|
|
240
|
+
def get_version_flag(cls):
|
|
241
|
+
if cls.version_flag == OPT_NOT_SUPPORTED:
|
|
242
|
+
return None
|
|
243
|
+
return cls.version_flag or f'{cls.opt_prefix}version'
|
|
244
|
+
|
|
245
|
+
@classmethod
|
|
246
|
+
def get_version_info(cls):
|
|
247
|
+
from secator.installer import get_version_info
|
|
248
|
+
return get_version_info(
|
|
249
|
+
cls.__name__,
|
|
250
|
+
cls.get_version_flag(),
|
|
251
|
+
cls.install_github_handle,
|
|
252
|
+
cls.install_cmd
|
|
253
|
+
)
|
|
254
|
+
|
|
231
255
|
@classmethod
|
|
232
256
|
def get_supported_opts(cls):
|
|
233
257
|
def convert(d):
|
|
@@ -346,6 +370,11 @@ class Command(Runner):
|
|
|
346
370
|
if self.has_children:
|
|
347
371
|
return
|
|
348
372
|
|
|
373
|
+
# Abort if dry run
|
|
374
|
+
if self.dry_run:
|
|
375
|
+
self.print_command()
|
|
376
|
+
return
|
|
377
|
+
|
|
349
378
|
# Print task description
|
|
350
379
|
self.print_description()
|
|
351
380
|
|
|
@@ -464,15 +493,12 @@ class Command(Runner):
|
|
|
464
493
|
if line is None:
|
|
465
494
|
return
|
|
466
495
|
|
|
496
|
+
# Yield line if no items were yielded
|
|
497
|
+
yield line
|
|
498
|
+
|
|
467
499
|
# Run item_loader to try parsing as dict
|
|
468
|
-
item_count = 0
|
|
469
500
|
for item in self.run_item_loaders(line):
|
|
470
501
|
yield item
|
|
471
|
-
item_count += 1
|
|
472
|
-
|
|
473
|
-
# Yield line if no items were yielded
|
|
474
|
-
if item_count == 0:
|
|
475
|
-
yield line
|
|
476
502
|
|
|
477
503
|
# Skip rest of iteration (no process mode)
|
|
478
504
|
if self.no_process:
|
|
@@ -499,6 +525,7 @@ class Command(Runner):
|
|
|
499
525
|
cmd_str += f' [dim gray11]({self.chunk}/{self.chunk_count})[/]'
|
|
500
526
|
self._print(cmd_str, color='bold cyan', rich=True)
|
|
501
527
|
self.debug('Command', obj={'cmd': self.cmd}, sub='init')
|
|
528
|
+
self.debug('Options', obj={'opts': self.cmd_options}, sub='init')
|
|
502
529
|
|
|
503
530
|
def handle_file_not_found(self, exc):
|
|
504
531
|
"""Handle case where binary is not found.
|
|
@@ -687,11 +714,17 @@ class Command(Runner):
|
|
|
687
714
|
opt_value_map (dict, str | Callable): A dict to map option values with their actual values.
|
|
688
715
|
opt_prefix (str, default: '-'): Option prefix.
|
|
689
716
|
command_name (str | None, default: None): Command name.
|
|
717
|
+
|
|
718
|
+
Returns:
|
|
719
|
+
dict: Processed options dict.
|
|
690
720
|
"""
|
|
691
|
-
|
|
721
|
+
opts_dict = {}
|
|
692
722
|
for opt_name, opt_conf in opts_conf.items():
|
|
693
723
|
debug('before get_opt_value', obj={'name': opt_name, 'conf': opt_conf}, obj_after=False, sub='command.options', verbose=True) # noqa: E501
|
|
694
724
|
|
|
725
|
+
# Save original opt name
|
|
726
|
+
original_opt_name = opt_name
|
|
727
|
+
|
|
695
728
|
# Get opt value
|
|
696
729
|
default_val = opt_conf.get('default')
|
|
697
730
|
opt_val = Command._get_opt_value(
|
|
@@ -743,15 +776,10 @@ class Command(Runner):
|
|
|
743
776
|
|
|
744
777
|
# Append opt name + opt value to option string.
|
|
745
778
|
# 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)
|
|
779
|
+
opts_dict[original_opt_name] = {'name': opt_name, 'value': opt_val, 'conf': opt_conf}
|
|
780
|
+
debug('final', obj={'name': original_opt_name, 'value': opt_val}, sub='command.options', obj_after=False, verbose=True) # noqa: E501
|
|
753
781
|
|
|
754
|
-
return
|
|
782
|
+
return opts_dict
|
|
755
783
|
|
|
756
784
|
@staticmethod
|
|
757
785
|
def _validate_chunked_input(self, inputs):
|
|
@@ -806,27 +834,57 @@ class Command(Runner):
|
|
|
806
834
|
if self.json_flag:
|
|
807
835
|
self.cmd += f' {self.json_flag}'
|
|
808
836
|
|
|
837
|
+
# Opts str
|
|
838
|
+
opts_str = ''
|
|
839
|
+
opts = {}
|
|
840
|
+
|
|
809
841
|
# Add options to cmd
|
|
810
|
-
|
|
842
|
+
opts_dict = Command._process_opts(
|
|
811
843
|
self.run_opts,
|
|
812
844
|
self.opts,
|
|
813
845
|
self.opt_key_map,
|
|
814
846
|
self.opt_value_map,
|
|
815
847
|
self.opt_prefix,
|
|
816
848
|
command_name=self.config.name)
|
|
817
|
-
if opts_str:
|
|
818
|
-
self.cmd += f' {opts_str}'
|
|
819
849
|
|
|
820
850
|
# Add meta options to cmd
|
|
821
|
-
|
|
851
|
+
meta_opts_dict = Command._process_opts(
|
|
822
852
|
self.run_opts,
|
|
823
853
|
self.meta_opts,
|
|
824
854
|
self.opt_key_map,
|
|
825
855
|
self.opt_value_map,
|
|
826
856
|
self.opt_prefix,
|
|
827
857
|
command_name=self.config.name)
|
|
828
|
-
|
|
829
|
-
|
|
858
|
+
|
|
859
|
+
if opts_dict:
|
|
860
|
+
opts.update(opts_dict)
|
|
861
|
+
if meta_opts_dict:
|
|
862
|
+
opts.update(meta_opts_dict)
|
|
863
|
+
|
|
864
|
+
if opts:
|
|
865
|
+
for opt_conf in opts.values():
|
|
866
|
+
conf = opt_conf['conf']
|
|
867
|
+
internal = conf.get('internal', False)
|
|
868
|
+
if internal:
|
|
869
|
+
continue
|
|
870
|
+
if conf.get('requires_sudo', False):
|
|
871
|
+
self.requires_sudo = True
|
|
872
|
+
opts_str += ' ' + Command._build_opt_str(opt_conf)
|
|
873
|
+
self.cmd_options = opts
|
|
874
|
+
self.cmd += opts_str
|
|
875
|
+
|
|
876
|
+
@staticmethod
|
|
877
|
+
def _build_opt_str(opt):
|
|
878
|
+
"""Build option string."""
|
|
879
|
+
conf = opt['conf']
|
|
880
|
+
opts_str = f'{opt["name"]}'
|
|
881
|
+
shlex_quote = conf.get('shlex', True)
|
|
882
|
+
value = opt['value']
|
|
883
|
+
if value is not True:
|
|
884
|
+
if shlex_quote:
|
|
885
|
+
value = shlex.quote(str(value))
|
|
886
|
+
opts_str += f' {value}'
|
|
887
|
+
return opts_str
|
|
830
888
|
|
|
831
889
|
def _build_cmd_input(self):
|
|
832
890
|
"""Many commands take as input a string or a list. This function facilitate this based on whether we pass a
|
|
@@ -859,6 +917,8 @@ class Command(Runner):
|
|
|
859
917
|
# Write the input to a file
|
|
860
918
|
with open(fpath, 'w') as f:
|
|
861
919
|
f.write('\n'.join(inputs))
|
|
920
|
+
if self.file_eof_newline:
|
|
921
|
+
f.write('\n')
|
|
862
922
|
|
|
863
923
|
if self.file_flag == OPT_PIPE_INPUT:
|
|
864
924
|
cmd = f'cat {fpath} | {cmd}'
|
secator/runners/scan.py
CHANGED
|
@@ -34,8 +34,10 @@ class Scan(Runner):
|
|
|
34
34
|
for name, workflow_opts in self.config.workflows.items():
|
|
35
35
|
run_opts = self.run_opts.copy()
|
|
36
36
|
run_opts['no_poll'] = True
|
|
37
|
+
run_opts['caller'] = 'Scan'
|
|
37
38
|
opts = merge_opts(scan_opts, workflow_opts, run_opts)
|
|
38
|
-
|
|
39
|
+
name = name.split('/')[0]
|
|
40
|
+
config = TemplateLoader(name=f'workflow/{name}')
|
|
39
41
|
workflow = Workflow(
|
|
40
42
|
config,
|
|
41
43
|
self.inputs,
|
|
@@ -44,13 +46,13 @@ class Scan(Runner):
|
|
|
44
46
|
hooks=self._hooks,
|
|
45
47
|
context=self.context.copy()
|
|
46
48
|
)
|
|
47
|
-
celery_workflow = workflow.build_celery_workflow()
|
|
49
|
+
celery_workflow = workflow.build_celery_workflow(chain_previous_results=True)
|
|
48
50
|
for task_id, task_info in workflow.celery_ids_map.items():
|
|
49
51
|
self.add_subtask(task_id, task_info['name'], task_info['descr'])
|
|
50
52
|
sigs.append(celery_workflow)
|
|
51
53
|
|
|
52
54
|
return chain(
|
|
53
|
-
mark_runner_started.si(self).set(queue='results'),
|
|
55
|
+
mark_runner_started.si([], self).set(queue='results'),
|
|
54
56
|
*sigs,
|
|
55
57
|
mark_runner_completed.s(self).set(queue='results'),
|
|
56
58
|
)
|
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,9 +18,14 @@ 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
30
|
DEPTH: {'type': int, 'help': 'Scan depth', 'default': 2},
|
|
26
31
|
FILTER_CODES: {'type': str, 'short': 'fc', 'help': 'Filter out responses with HTTP codes'},
|
secator/tasks/arjun.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
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, HEADER, URL)
|
|
6
|
+
from secator.output_types import Info, Url, Warning, Error
|
|
7
|
+
from secator.runners import Command
|
|
8
|
+
from secator.tasks._categories import OPTS
|
|
9
|
+
from secator.utils import process_wordlist
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@task()
|
|
13
|
+
class arjun(Command):
|
|
14
|
+
"""HTTP Parameter Discovery Suite."""
|
|
15
|
+
cmd = 'arjun'
|
|
16
|
+
input_flag = '-u'
|
|
17
|
+
input_type = URL
|
|
18
|
+
version_flag = ' '
|
|
19
|
+
opts = {
|
|
20
|
+
'chunk_size': {'type': int, 'help': 'Control query/chunk size'},
|
|
21
|
+
'stable': {'is_flag': True, 'default': False, 'help': 'Use stable mode'},
|
|
22
|
+
'include': {'type': str, 'help': 'Include persistent data (e.g: "api_key=xxxxx" or {"api_key": "xxxx"})'},
|
|
23
|
+
'passive': {'is_flag': True, 'default': False, 'help': 'Passive mode'},
|
|
24
|
+
'casing': {'type': str, 'help': 'Casing style for params e.g. like_this, likeThis, LIKE_THIS, like_this'}, # noqa: E501
|
|
25
|
+
WORDLIST: {'type': str, 'short': 'w', 'default': None, 'process': process_wordlist, 'help': 'Wordlist to use (default: arjun wordlist)'}, # noqa: E501
|
|
26
|
+
}
|
|
27
|
+
meta_opts = {
|
|
28
|
+
THREADS: OPTS[THREADS],
|
|
29
|
+
DELAY: OPTS[DELAY],
|
|
30
|
+
TIMEOUT: OPTS[TIMEOUT],
|
|
31
|
+
RATE_LIMIT: OPTS[RATE_LIMIT],
|
|
32
|
+
METHOD: OPTS[METHOD],
|
|
33
|
+
HEADER: OPTS[HEADER],
|
|
34
|
+
}
|
|
35
|
+
opt_key_map = {
|
|
36
|
+
THREADS: 't',
|
|
37
|
+
DELAY: 'd',
|
|
38
|
+
TIMEOUT: 'T',
|
|
39
|
+
RATE_LIMIT: '--rate-limit',
|
|
40
|
+
METHOD: 'm',
|
|
41
|
+
WORDLIST: 'w',
|
|
42
|
+
HEADER: '--headers',
|
|
43
|
+
'chunk_size': 'c',
|
|
44
|
+
'stable': '--stable',
|
|
45
|
+
'passive': '--passive',
|
|
46
|
+
'casing': '--casing',
|
|
47
|
+
}
|
|
48
|
+
output_types = [Url]
|
|
49
|
+
install_cmd = 'pipx install arjun && pipx upgrade arjun'
|
|
50
|
+
install_github_handle = 's0md3v/Arjun'
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def on_line(self, line):
|
|
54
|
+
if 'Processing chunks' in line:
|
|
55
|
+
return ''
|
|
56
|
+
return line
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def on_cmd(self):
|
|
60
|
+
self.output_path = self.get_opt_value(OUTPUT_PATH)
|
|
61
|
+
if not self.output_path:
|
|
62
|
+
self.output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.json'
|
|
63
|
+
self.cmd += f' -oJ {self.output_path}'
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def on_cmd_done(self):
|
|
67
|
+
if not os.path.exists(self.output_path):
|
|
68
|
+
yield Error(message=f'Could not find JSON results in {self.output_path}')
|
|
69
|
+
return
|
|
70
|
+
yield Info(message=f'JSON results saved to {self.output_path}')
|
|
71
|
+
with open(self.output_path, 'r') as f:
|
|
72
|
+
results = yaml.safe_load(f.read())
|
|
73
|
+
if not results:
|
|
74
|
+
yield Warning(message='No results found !')
|
|
75
|
+
return
|
|
76
|
+
for url, values in results.items():
|
|
77
|
+
for param in values['params']:
|
|
78
|
+
yield Url(
|
|
79
|
+
url=url + '?' + param + '=' + 'FUZZ',
|
|
80
|
+
headers=values['headers'],
|
|
81
|
+
method=values['method'],
|
|
82
|
+
)
|
secator/tasks/ffuf.py
CHANGED
|
@@ -18,7 +18,7 @@ FFUF_PROGRESS_REGEX = r':: Progress: \[(?P<count>\d+)/(?P<total>\d+)\] :: Job \[
|
|
|
18
18
|
@task()
|
|
19
19
|
class ffuf(HttpFuzzer):
|
|
20
20
|
"""Fast web fuzzer written in Go."""
|
|
21
|
-
cmd = 'ffuf -noninteractive
|
|
21
|
+
cmd = 'ffuf -noninteractive'
|
|
22
22
|
input_flag = '-u'
|
|
23
23
|
input_chunk_size = 1
|
|
24
24
|
file_flag = None
|
|
@@ -30,6 +30,7 @@ class ffuf(HttpFuzzer):
|
|
|
30
30
|
]
|
|
31
31
|
opts = {
|
|
32
32
|
AUTO_CALIBRATION: {'is_flag': True, 'short': 'ac', 'help': 'Auto-calibration'},
|
|
33
|
+
'recursion': {'is_flag': True, 'default': True, 'short': 'recursion', 'help': 'Recursion'},
|
|
33
34
|
}
|
|
34
35
|
opt_key_map = {
|
|
35
36
|
HEADER: 'H',
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import os
|
|
3
|
+
import yaml
|
|
4
|
+
|
|
5
|
+
from secator.config import CONFIG
|
|
6
|
+
from secator.decorators import task
|
|
7
|
+
from secator.runners import Command
|
|
8
|
+
from secator.definitions import (OUTPUT_PATH)
|
|
9
|
+
from secator.utils import caml_to_snake
|
|
10
|
+
from secator.output_types import Tag, Info, Error
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@task()
|
|
14
|
+
class gitleaks(Command):
|
|
15
|
+
"""Tool for detecting secrets like passwords, API keys, and tokens in git repos, files, and stdin."""
|
|
16
|
+
cmd = 'gitleaks'
|
|
17
|
+
input_flag = None
|
|
18
|
+
json_flag = '-f json'
|
|
19
|
+
opt_prefix = '--'
|
|
20
|
+
opts = {
|
|
21
|
+
'ignore_path': {'type': str, 'help': 'Path to .gitleaksignore file or folder containing one'},
|
|
22
|
+
'mode': {'type': click.Choice(['git', 'dir']), 'default': 'dir', 'help': 'Gitleaks mode', 'internal': True, 'display': True}, # noqa: E501
|
|
23
|
+
'config': {'type': str, 'short': 'config', 'help': 'Gitleaks config file path'}
|
|
24
|
+
}
|
|
25
|
+
opt_key_map = {
|
|
26
|
+
"ignore_path": "gitleaks-ignore-path"
|
|
27
|
+
}
|
|
28
|
+
input_type = "folder"
|
|
29
|
+
output_types = [Tag]
|
|
30
|
+
output_map = {
|
|
31
|
+
Tag: {
|
|
32
|
+
'name': 'RuleID',
|
|
33
|
+
'match': lambda x: f'{x["File"]}:{x["StartLine"]}:{x["StartColumn"]}',
|
|
34
|
+
'extra_data': lambda x: {caml_to_snake(k): v for k, v in x.items() if k not in ['RuleID', 'File']}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
install_pre = {'*': ['git', 'make']}
|
|
38
|
+
install_cmd = (
|
|
39
|
+
f'git clone https://github.com/gitleaks/gitleaks.git {CONFIG.dirs.share}/gitleaks || true &&'
|
|
40
|
+
f'cd {CONFIG.dirs.share}/gitleaks && make build &&'
|
|
41
|
+
f'mv {CONFIG.dirs.share}/gitleaks/gitleaks {CONFIG.dirs.bin}'
|
|
42
|
+
)
|
|
43
|
+
install_github_handle = 'gitleaks/gitleaks'
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def on_cmd(self):
|
|
47
|
+
# replace fake -mode opt by subcommand
|
|
48
|
+
mode = self.get_opt_value('mode')
|
|
49
|
+
self.cmd = self.cmd.replace(f'{gitleaks.cmd} ', f'{gitleaks.cmd} {mode} ')
|
|
50
|
+
|
|
51
|
+
# add output path
|
|
52
|
+
output_path = self.get_opt_value(OUTPUT_PATH)
|
|
53
|
+
if not output_path:
|
|
54
|
+
output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.json'
|
|
55
|
+
self.output_path = output_path
|
|
56
|
+
self.cmd += f' -r {self.output_path}'
|
|
57
|
+
self.cmd += ' --exit-code 0'
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def on_cmd_done(self):
|
|
61
|
+
if not os.path.exists(self.output_path):
|
|
62
|
+
yield Error(message=f'Could not find JSON results in {self.output_path}')
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
yield Info(message=f'JSON results saved to {self.output_path}')
|
|
66
|
+
with open(self.output_path, 'r') as f:
|
|
67
|
+
results = yaml.safe_load(f.read())
|
|
68
|
+
for result in results:
|
|
69
|
+
yield Tag(
|
|
70
|
+
name=result['RuleID'],
|
|
71
|
+
match='{File}:{StartLine}:{StartColumn}'.format(**result),
|
|
72
|
+
extra_data={
|
|
73
|
+
caml_to_snake(k): v for k, v in result.items()
|
|
74
|
+
if k not in ['RuleID', 'File']
|
|
75
|
+
}
|
|
76
|
+
)
|
secator/tasks/mapcidr.py
CHANGED
|
@@ -10,7 +10,7 @@ from secator.tasks._categories import ReconIp
|
|
|
10
10
|
@task()
|
|
11
11
|
class mapcidr(ReconIp):
|
|
12
12
|
"""Utility program to perform multiple operations for a given subnet/cidr ranges."""
|
|
13
|
-
cmd = 'mapcidr
|
|
13
|
+
cmd = 'mapcidr'
|
|
14
14
|
input_flag = '-cidr'
|
|
15
15
|
file_flag = '-cl'
|
|
16
16
|
install_pre = {
|