secator 0.15.1__py3-none-any.whl → 0.16.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 +40 -24
- secator/celery_signals.py +71 -68
- secator/celery_utils.py +43 -27
- secator/cli.py +520 -280
- secator/cli_helper.py +394 -0
- secator/click.py +87 -0
- secator/config.py +67 -39
- secator/configs/profiles/http_headless.yaml +6 -0
- secator/configs/profiles/http_record.yaml +6 -0
- secator/configs/profiles/tor.yaml +1 -1
- secator/configs/scans/domain.yaml +4 -2
- secator/configs/scans/host.yaml +1 -1
- secator/configs/scans/network.yaml +1 -4
- secator/configs/scans/subdomain.yaml +13 -1
- secator/configs/scans/url.yaml +1 -2
- secator/configs/workflows/cidr_recon.yaml +6 -4
- secator/configs/workflows/code_scan.yaml +1 -1
- secator/configs/workflows/host_recon.yaml +29 -3
- secator/configs/workflows/subdomain_recon.yaml +67 -16
- secator/configs/workflows/url_crawl.yaml +44 -15
- secator/configs/workflows/url_dirsearch.yaml +4 -4
- secator/configs/workflows/url_fuzz.yaml +25 -17
- secator/configs/workflows/url_params_fuzz.yaml +7 -0
- secator/configs/workflows/url_vuln.yaml +33 -8
- secator/configs/workflows/user_hunt.yaml +4 -2
- secator/configs/workflows/wordpress.yaml +5 -3
- secator/cve.py +718 -0
- secator/decorators.py +0 -454
- secator/definitions.py +49 -30
- secator/exporters/_base.py +2 -2
- secator/exporters/console.py +2 -2
- secator/exporters/table.py +4 -3
- secator/exporters/txt.py +1 -1
- secator/hooks/mongodb.py +2 -4
- secator/installer.py +77 -49
- secator/loader.py +116 -0
- secator/output_types/_base.py +3 -0
- secator/output_types/certificate.py +63 -63
- secator/output_types/error.py +4 -5
- secator/output_types/info.py +2 -2
- secator/output_types/ip.py +3 -1
- secator/output_types/progress.py +5 -9
- secator/output_types/state.py +17 -17
- secator/output_types/tag.py +3 -0
- secator/output_types/target.py +10 -2
- secator/output_types/url.py +19 -7
- secator/output_types/vulnerability.py +11 -7
- secator/output_types/warning.py +2 -2
- secator/report.py +27 -15
- secator/rich.py +18 -10
- secator/runners/_base.py +446 -233
- secator/runners/_helpers.py +133 -24
- secator/runners/command.py +182 -102
- secator/runners/scan.py +33 -5
- secator/runners/task.py +13 -7
- secator/runners/workflow.py +105 -72
- secator/scans/__init__.py +2 -2
- secator/serializers/dataclass.py +20 -20
- secator/tasks/__init__.py +4 -4
- secator/tasks/_categories.py +39 -27
- secator/tasks/arjun.py +9 -5
- secator/tasks/bbot.py +53 -21
- secator/tasks/bup.py +19 -5
- secator/tasks/cariddi.py +24 -3
- secator/tasks/dalfox.py +26 -7
- secator/tasks/dirsearch.py +10 -4
- secator/tasks/dnsx.py +70 -25
- secator/tasks/feroxbuster.py +11 -3
- secator/tasks/ffuf.py +42 -6
- secator/tasks/fping.py +20 -8
- secator/tasks/gau.py +3 -1
- secator/tasks/gf.py +3 -3
- secator/tasks/gitleaks.py +2 -2
- secator/tasks/gospider.py +7 -1
- secator/tasks/grype.py +5 -4
- secator/tasks/h8mail.py +2 -1
- secator/tasks/httpx.py +18 -5
- secator/tasks/katana.py +35 -15
- secator/tasks/maigret.py +4 -4
- secator/tasks/mapcidr.py +3 -3
- secator/tasks/msfconsole.py +4 -4
- secator/tasks/naabu.py +2 -2
- secator/tasks/nmap.py +12 -14
- secator/tasks/nuclei.py +3 -3
- secator/tasks/searchsploit.py +4 -5
- secator/tasks/subfinder.py +2 -2
- secator/tasks/testssl.py +264 -263
- secator/tasks/trivy.py +5 -5
- secator/tasks/wafw00f.py +21 -3
- secator/tasks/wpprobe.py +90 -83
- secator/tasks/wpscan.py +6 -5
- secator/template.py +218 -104
- secator/thread.py +15 -15
- secator/tree.py +196 -0
- secator/utils.py +131 -123
- secator/utils_test.py +60 -19
- secator/workflows/__init__.py +2 -2
- {secator-0.15.1.dist-info → secator-0.16.1.dist-info}/METADATA +36 -36
- secator-0.16.1.dist-info/RECORD +132 -0
- secator/configs/profiles/default.yaml +0 -8
- secator/configs/workflows/url_nuclei.yaml +0 -11
- secator/tasks/dnsxbrute.py +0 -42
- secator-0.15.1.dist-info/RECORD +0 -128
- {secator-0.15.1.dist-info → secator-0.16.1.dist-info}/WHEEL +0 -0
- {secator-0.15.1.dist-info → secator-0.16.1.dist-info}/entry_points.txt +0 -0
- {secator-0.15.1.dist-info → secator-0.16.1.dist-info}/licenses/LICENSE +0 -0
secator/utils.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fnmatch
|
|
2
|
-
import inspect
|
|
3
2
|
import importlib
|
|
3
|
+
import ipaddress
|
|
4
4
|
import itertools
|
|
5
5
|
import json
|
|
6
6
|
import logging
|
|
@@ -15,9 +15,7 @@ import warnings
|
|
|
15
15
|
|
|
16
16
|
from datetime import datetime, timedelta
|
|
17
17
|
from functools import reduce
|
|
18
|
-
from inspect import isclass
|
|
19
18
|
from pathlib import Path
|
|
20
|
-
from pkgutil import iter_modules
|
|
21
19
|
from time import time
|
|
22
20
|
import traceback
|
|
23
21
|
from urllib.parse import urlparse, quote
|
|
@@ -26,7 +24,8 @@ import humanize
|
|
|
26
24
|
import ifaddr
|
|
27
25
|
import yaml
|
|
28
26
|
|
|
29
|
-
from secator.definitions import (
|
|
27
|
+
from secator.definitions import (DEBUG, VERSION, DEV_PACKAGE, IP, HOST, CIDR_RANGE,
|
|
28
|
+
MAC_ADDRESS, SLUG, UUID, EMAIL, IBAN, URL, PATH, HOST_PORT)
|
|
30
29
|
from secator.config import CONFIG, ROOT_FOLDER, LIB_FOLDER, download_file
|
|
31
30
|
from secator.rich import console
|
|
32
31
|
|
|
@@ -73,18 +72,21 @@ def expand_input(input, ctx):
|
|
|
73
72
|
Returns:
|
|
74
73
|
str: Input.
|
|
75
74
|
"""
|
|
75
|
+
piped_input = ctx.obj['piped_input']
|
|
76
|
+
dry_run = ctx.obj['dry_run']
|
|
76
77
|
if input is None: # read from stdin
|
|
77
|
-
if not
|
|
78
|
-
console.print('
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
78
|
+
if not piped_input and not dry_run:
|
|
79
|
+
console.print('No input passed on stdin. Showing help page.', style='bold red')
|
|
80
|
+
ctx.get_help()
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
elif piped_input:
|
|
83
|
+
rlist, _, _ = select.select([sys.stdin], [], [], CONFIG.cli.stdin_timeout)
|
|
84
|
+
if rlist:
|
|
85
|
+
data = sys.stdin.read().splitlines()
|
|
86
|
+
return data
|
|
87
|
+
else:
|
|
88
|
+
console.print('No input passed on stdin.', style='bold red')
|
|
89
|
+
sys.exit(1)
|
|
88
90
|
elif os.path.exists(input):
|
|
89
91
|
if os.path.isfile(input):
|
|
90
92
|
with open(input, 'r') as f:
|
|
@@ -99,6 +101,9 @@ def expand_input(input, ctx):
|
|
|
99
101
|
if isinstance(input, list) and len(input) == 1:
|
|
100
102
|
return input[0]
|
|
101
103
|
|
|
104
|
+
if ctx.obj['dry_run'] and not input:
|
|
105
|
+
return ['TARGET']
|
|
106
|
+
|
|
102
107
|
return input
|
|
103
108
|
|
|
104
109
|
|
|
@@ -140,75 +145,6 @@ def deduplicate(array, attr=None):
|
|
|
140
145
|
return sorted(list(dict.fromkeys(array)))
|
|
141
146
|
|
|
142
147
|
|
|
143
|
-
def discover_internal_tasks():
|
|
144
|
-
"""Find internal secator tasks."""
|
|
145
|
-
from secator.runners import Runner
|
|
146
|
-
package_dir = Path(__file__).resolve().parent / 'tasks'
|
|
147
|
-
task_classes = []
|
|
148
|
-
for (_, module_name, _) in iter_modules([str(package_dir)]):
|
|
149
|
-
if module_name.startswith('_'):
|
|
150
|
-
continue
|
|
151
|
-
try:
|
|
152
|
-
module = importlib.import_module(f'secator.tasks.{module_name}')
|
|
153
|
-
except ImportError as e:
|
|
154
|
-
console.print(f'[bold red]Could not import secator.tasks.{module_name}:[/]')
|
|
155
|
-
console.print(f'\t[bold red]{type(e).__name__}[/]: {str(e)}')
|
|
156
|
-
continue
|
|
157
|
-
for attribute_name in dir(module):
|
|
158
|
-
attribute = getattr(module, attribute_name)
|
|
159
|
-
if isclass(attribute):
|
|
160
|
-
bases = inspect.getmro(attribute)
|
|
161
|
-
if Runner in bases and hasattr(attribute, '__task__'):
|
|
162
|
-
task_classes.append(attribute)
|
|
163
|
-
|
|
164
|
-
# Sort task_classes by category
|
|
165
|
-
task_classes = sorted(
|
|
166
|
-
task_classes,
|
|
167
|
-
# key=lambda x: (get_command_category(x), x.__name__)
|
|
168
|
-
key=lambda x: x.__name__)
|
|
169
|
-
|
|
170
|
-
return task_classes
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def discover_external_tasks():
|
|
174
|
-
"""Find external secator tasks."""
|
|
175
|
-
output = []
|
|
176
|
-
sys.dont_write_bytecode = True
|
|
177
|
-
for path in CONFIG.dirs.templates.glob('**/*.py'):
|
|
178
|
-
try:
|
|
179
|
-
task_name = path.stem
|
|
180
|
-
module_name = f'secator.tasks.{task_name}'
|
|
181
|
-
|
|
182
|
-
# console.print(f'Importing module {module_name} from {path}')
|
|
183
|
-
spec = importlib.util.spec_from_file_location(module_name, path)
|
|
184
|
-
module = importlib.util.module_from_spec(spec)
|
|
185
|
-
# console.print(f'Adding module "{module_name}" to sys path')
|
|
186
|
-
sys.modules[module_name] = module
|
|
187
|
-
|
|
188
|
-
# console.print(f'Executing module "{module}"')
|
|
189
|
-
spec.loader.exec_module(module)
|
|
190
|
-
|
|
191
|
-
# console.print(f'Checking that {module} contains task {task_name}')
|
|
192
|
-
if not hasattr(module, task_name):
|
|
193
|
-
console.print(f'[bold orange1]Could not load external task "{task_name}" from module {path.name}[/] ({path})')
|
|
194
|
-
continue
|
|
195
|
-
cls = getattr(module, task_name)
|
|
196
|
-
console.print(f'[bold green]Successfully loaded external task "{task_name}"[/] ({path})')
|
|
197
|
-
output.append(cls)
|
|
198
|
-
except Exception as e:
|
|
199
|
-
console.print(f'[bold red]Could not load external module {path.name}. Reason: {str(e)}.[/] ({path})')
|
|
200
|
-
sys.dont_write_bytecode = False
|
|
201
|
-
return output
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def discover_tasks():
|
|
205
|
-
"""Find all secator tasks (internal + external)."""
|
|
206
|
-
global _tasks
|
|
207
|
-
if not _tasks:
|
|
208
|
-
_tasks = discover_internal_tasks() + discover_external_tasks()
|
|
209
|
-
return _tasks
|
|
210
|
-
|
|
211
|
-
|
|
212
148
|
def import_dynamic(path, name=None):
|
|
213
149
|
"""Import class or module dynamically from path.
|
|
214
150
|
|
|
@@ -238,22 +174,6 @@ def import_dynamic(path, name=None):
|
|
|
238
174
|
return None
|
|
239
175
|
|
|
240
176
|
|
|
241
|
-
def get_command_cls(cls_name):
|
|
242
|
-
"""Get secator command by class name.
|
|
243
|
-
|
|
244
|
-
Args:
|
|
245
|
-
cls_name (str): Class name to load.
|
|
246
|
-
|
|
247
|
-
Returns:
|
|
248
|
-
cls: Class.
|
|
249
|
-
"""
|
|
250
|
-
tasks_classes = discover_tasks()
|
|
251
|
-
for task_cls in tasks_classes:
|
|
252
|
-
if task_cls.__name__ == cls_name:
|
|
253
|
-
return task_cls
|
|
254
|
-
return None
|
|
255
|
-
|
|
256
|
-
|
|
257
177
|
def get_command_category(command):
|
|
258
178
|
"""Get the category of a command.
|
|
259
179
|
|
|
@@ -383,7 +303,7 @@ def rich_to_ansi(text):
|
|
|
383
303
|
tmp_console.print(text, end='', soft_wrap=True)
|
|
384
304
|
return capture.get()
|
|
385
305
|
except Exception:
|
|
386
|
-
|
|
306
|
+
print(f'Could not convert rich text to ansi: {text}[/]', file=sys.stderr)
|
|
387
307
|
return text
|
|
388
308
|
|
|
389
309
|
|
|
@@ -397,11 +317,11 @@ def rich_escape(obj):
|
|
|
397
317
|
any: Initial object, or escaped Rich string.
|
|
398
318
|
"""
|
|
399
319
|
if isinstance(obj, str):
|
|
400
|
-
return obj.replace('[', r'\[').replace(']', r'\]')
|
|
320
|
+
return obj.replace('[', r'\[').replace(']', r'\]').replace(r'\[/', r'\[\/')
|
|
401
321
|
return obj
|
|
402
322
|
|
|
403
323
|
|
|
404
|
-
def
|
|
324
|
+
def format_debug_object(obj, obj_breaklines=False):
|
|
405
325
|
"""Format the debug object for printing.
|
|
406
326
|
|
|
407
327
|
Args:
|
|
@@ -413,7 +333,7 @@ def format_object(obj, obj_breaklines=False):
|
|
|
413
333
|
"""
|
|
414
334
|
sep = '\n ' if obj_breaklines else ', '
|
|
415
335
|
if isinstance(obj, dict):
|
|
416
|
-
return sep.join(f'[
|
|
336
|
+
return sep.join(f'[bold blue]{k}[/] [yellow]->[/] [blue]{v}[/]' for k, v in obj.items() if v is not None) # noqa: E501
|
|
417
337
|
elif isinstance(obj, list):
|
|
418
338
|
return f'[dim green]{sep.join(obj)}[/]'
|
|
419
339
|
return ''
|
|
@@ -421,21 +341,25 @@ def format_object(obj, obj_breaklines=False):
|
|
|
421
341
|
|
|
422
342
|
def debug(msg, sub='', id='', obj=None, lazy=None, obj_after=True, obj_breaklines=False, verbose=False):
|
|
423
343
|
"""Print debug log if DEBUG >= level."""
|
|
424
|
-
if not
|
|
425
|
-
if not
|
|
344
|
+
if not DEBUG == ['all'] and not DEBUG == ['1']:
|
|
345
|
+
if not DEBUG or DEBUG == [""]:
|
|
426
346
|
return
|
|
427
|
-
|
|
428
347
|
if sub:
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
348
|
+
for s in DEBUG:
|
|
349
|
+
if '*' in s and re.match(s + '$', sub):
|
|
350
|
+
break
|
|
351
|
+
elif not verbose and sub.startswith(s):
|
|
352
|
+
break
|
|
353
|
+
elif verbose and sub == s:
|
|
354
|
+
break
|
|
355
|
+
else:
|
|
432
356
|
return
|
|
433
357
|
|
|
434
358
|
if lazy:
|
|
435
359
|
msg = lazy(msg)
|
|
436
360
|
|
|
437
361
|
formatted_msg = f'[yellow4]{sub:13s}[/] ' if sub else ''
|
|
438
|
-
obj_str =
|
|
362
|
+
obj_str = format_debug_object(obj, obj_breaklines) if obj else ''
|
|
439
363
|
|
|
440
364
|
# Constructing the message string based on object position
|
|
441
365
|
if obj_str and not obj_after:
|
|
@@ -446,7 +370,12 @@ def debug(msg, sub='', id='', obj=None, lazy=None, obj_after=True, obj_breakline
|
|
|
446
370
|
if id:
|
|
447
371
|
formatted_msg += rf' [italic gray11]\[{id}][/]'
|
|
448
372
|
|
|
449
|
-
|
|
373
|
+
try:
|
|
374
|
+
console.print(rf'[dim]\[[magenta4]DBG[/]] {formatted_msg}[/]', highlight=False)
|
|
375
|
+
except Exception:
|
|
376
|
+
console.print(rf'[dim]\[[magenta4]DBG[/]] <MARKUP_DISABLED>{rich_escape(formatted_msg)}</MARKUP_DISABLED>[/]', highlight=False) # noqa: E501
|
|
377
|
+
if 'rich' in DEBUG:
|
|
378
|
+
raise
|
|
450
379
|
|
|
451
380
|
|
|
452
381
|
def escape_mongodb_url(url):
|
|
@@ -498,6 +427,7 @@ def extract_domain_info(input, domain_only=False):
|
|
|
498
427
|
|
|
499
428
|
Args:
|
|
500
429
|
input (str): An URL or FQDN.
|
|
430
|
+
domain_only (bool): Return only the registered domain name.
|
|
501
431
|
|
|
502
432
|
Returns:
|
|
503
433
|
tldextract.ExtractResult: Extracted info.
|
|
@@ -507,9 +437,9 @@ def extract_domain_info(input, domain_only=False):
|
|
|
507
437
|
if not result or not result.domain or not result.suffix:
|
|
508
438
|
return None
|
|
509
439
|
if domain_only:
|
|
510
|
-
if not validators.domain(result.
|
|
440
|
+
if not validators.domain(result.top_domain_under_public_suffix):
|
|
511
441
|
return None
|
|
512
|
-
return result.
|
|
442
|
+
return result.top_domain_under_public_suffix
|
|
513
443
|
return result
|
|
514
444
|
|
|
515
445
|
|
|
@@ -787,15 +717,12 @@ def process_wordlist(val):
|
|
|
787
717
|
if template_wordlist:
|
|
788
718
|
val = template_wordlist
|
|
789
719
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
offline_mode=CONFIG.offline_mode,
|
|
797
|
-
type='wordlist'
|
|
798
|
-
)
|
|
720
|
+
return download_file(
|
|
721
|
+
val,
|
|
722
|
+
target_folder=CONFIG.dirs.wordlists,
|
|
723
|
+
offline_mode=CONFIG.offline_mode,
|
|
724
|
+
type='wordlist'
|
|
725
|
+
)
|
|
799
726
|
|
|
800
727
|
|
|
801
728
|
def convert_functions_to_strings(data):
|
|
@@ -815,3 +742,84 @@ def convert_functions_to_strings(data):
|
|
|
815
742
|
return json.dumps(data.__name__) # or use inspect.getsource(data) if you want the actual function code
|
|
816
743
|
else:
|
|
817
744
|
return data
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def headers_to_dict(header_opt):
|
|
748
|
+
headers = {}
|
|
749
|
+
for header in header_opt.split(';;'):
|
|
750
|
+
split = header.strip().split(':')
|
|
751
|
+
key = split[0].strip()
|
|
752
|
+
val = ':'.join(split[1:]).strip()
|
|
753
|
+
headers[key] = val
|
|
754
|
+
return headers
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def format_object(obj, color='magenta', skip_keys=[]):
|
|
758
|
+
if isinstance(obj, list) and obj:
|
|
759
|
+
return ' [' + ', '.join([f'[{color}]{rich_escape(item)}[/]' for item in obj]) + ']'
|
|
760
|
+
elif isinstance(obj, dict) and obj.keys():
|
|
761
|
+
obj = {k: v for k, v in obj.items() if k.lower().replace('-', '_') not in skip_keys}
|
|
762
|
+
if obj:
|
|
763
|
+
return ' [' + ', '.join([f'[bold {color}]{rich_escape(k)}[/]: [{color}]{rich_escape(v)}[/]' for k, v in obj.items()]) + ']' # noqa: E501
|
|
764
|
+
return ''
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def autodetect_type(target):
|
|
768
|
+
"""Autodetect the type of a target.
|
|
769
|
+
|
|
770
|
+
Args:
|
|
771
|
+
target (str): The target to autodetect the type of.
|
|
772
|
+
|
|
773
|
+
Returns:
|
|
774
|
+
str: The type of the target.
|
|
775
|
+
"""
|
|
776
|
+
if validators.url(target, simple_host=True):
|
|
777
|
+
return URL
|
|
778
|
+
elif validate_cidr_range(target):
|
|
779
|
+
return CIDR_RANGE
|
|
780
|
+
elif validators.ipv4(target) or validators.ipv6(target) or target == 'localhost':
|
|
781
|
+
return IP
|
|
782
|
+
elif validators.domain(target):
|
|
783
|
+
return HOST
|
|
784
|
+
elif validators.domain(target.split(':')[0]):
|
|
785
|
+
return HOST_PORT
|
|
786
|
+
elif validators.mac_address(target):
|
|
787
|
+
return MAC_ADDRESS
|
|
788
|
+
elif validators.email(target):
|
|
789
|
+
return EMAIL
|
|
790
|
+
elif validators.iban(target):
|
|
791
|
+
return IBAN
|
|
792
|
+
elif validators.uuid(target):
|
|
793
|
+
return UUID
|
|
794
|
+
elif Path(target).exists():
|
|
795
|
+
return PATH
|
|
796
|
+
elif validators.slug(target):
|
|
797
|
+
return SLUG
|
|
798
|
+
|
|
799
|
+
return str(type(target).__name__).lower()
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def validate_cidr_range(target):
|
|
803
|
+
if '/' not in target:
|
|
804
|
+
return False
|
|
805
|
+
try:
|
|
806
|
+
ipaddress.ip_network(target, False)
|
|
807
|
+
return True
|
|
808
|
+
except ValueError:
|
|
809
|
+
return False
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def get_versions_from_string(string):
|
|
813
|
+
"""Get versions from a string.
|
|
814
|
+
|
|
815
|
+
Args:
|
|
816
|
+
string (str): String to get versions from.
|
|
817
|
+
|
|
818
|
+
Returns:
|
|
819
|
+
list[str]: List of versions.
|
|
820
|
+
"""
|
|
821
|
+
regex = r'v?[0-9]+\.[0-9]+\.?[0-9]*\.?[a-zA-Z]*'
|
|
822
|
+
matches = re.findall(regex, string)
|
|
823
|
+
if not matches:
|
|
824
|
+
return []
|
|
825
|
+
return matches
|
secator/utils_test.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
import json
|
|
3
3
|
import os
|
|
4
|
+
import re
|
|
4
5
|
import sys
|
|
5
6
|
import unittest.mock
|
|
6
7
|
|
|
@@ -11,11 +12,11 @@ from secator.definitions import (CIDR_RANGE, DELAY, DEPTH, EMAIL,
|
|
|
11
12
|
METHOD, PROXY, RATE_LIMIT, RETRIES,
|
|
12
13
|
THREADS, TIMEOUT, URL, USER_AGENT, USERNAME, PATH,
|
|
13
14
|
DOCKER_IMAGE, GIT_REPOSITORY)
|
|
14
|
-
from secator.
|
|
15
|
+
from secator.loader import get_configs_by_type
|
|
15
16
|
from secator.output_types import EXECUTION_TYPES, STAT_TYPES
|
|
16
|
-
from secator.runners import Command
|
|
17
|
+
from secator.runners import Command, Task
|
|
17
18
|
from secator.rich import console
|
|
18
|
-
from secator.utils import load_fixture, debug
|
|
19
|
+
from secator.utils import load_fixture, debug, traceback_as_string
|
|
19
20
|
|
|
20
21
|
#---------#
|
|
21
22
|
# GLOBALS #
|
|
@@ -24,33 +25,37 @@ USE_PROXY = bool(int(os.environ.get('USE_PROXY', '0')))
|
|
|
24
25
|
TEST_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + '/tests/'
|
|
25
26
|
FIXTURES_DIR = f'{TEST_DIR}/fixtures'
|
|
26
27
|
USE_PROXY = bool(int(os.environ.get('USE_PROXY', '0')))
|
|
28
|
+
TASKS = get_configs_by_type('task')
|
|
29
|
+
WORKFLOWS = get_configs_by_type('workflow')
|
|
30
|
+
SCANS = get_configs_by_type('scan')
|
|
31
|
+
|
|
27
32
|
|
|
28
33
|
#------------#
|
|
29
34
|
# TEST TASKS #
|
|
30
35
|
#------------#
|
|
31
36
|
TEST_TASKS = os.environ.get('TEST_TASKS', '')
|
|
32
37
|
if TEST_TASKS:
|
|
33
|
-
TEST_TASKS = [
|
|
38
|
+
TEST_TASKS = [config for config in TASKS if config.name in TEST_TASKS.split(',')]
|
|
34
39
|
else:
|
|
35
|
-
TEST_TASKS =
|
|
40
|
+
TEST_TASKS = TASKS
|
|
36
41
|
|
|
37
42
|
#----------------#
|
|
38
43
|
# TEST WORKFLOWS #
|
|
39
44
|
#----------------#
|
|
40
45
|
TEST_WORKFLOWS = os.environ.get('TEST_WORKFLOWS', '')
|
|
41
46
|
if TEST_WORKFLOWS:
|
|
42
|
-
TEST_WORKFLOWS = [config for config in
|
|
47
|
+
TEST_WORKFLOWS = [config for config in WORKFLOWS if config.name in TEST_WORKFLOWS.split(',')]
|
|
43
48
|
else:
|
|
44
|
-
TEST_WORKFLOWS =
|
|
49
|
+
TEST_WORKFLOWS = WORKFLOWS
|
|
45
50
|
|
|
46
51
|
#------------#
|
|
47
52
|
# TEST SCANS #
|
|
48
53
|
#------------#
|
|
49
54
|
TEST_SCANS = os.environ.get('TEST_SCANS', '')
|
|
50
55
|
if TEST_SCANS:
|
|
51
|
-
TEST_SCANS = [config for config in
|
|
56
|
+
TEST_SCANS = [config for config in SCANS if config.name in TEST_SCANS.split(',')]
|
|
52
57
|
else:
|
|
53
|
-
TEST_SCANS =
|
|
58
|
+
TEST_SCANS = SCANS
|
|
54
59
|
|
|
55
60
|
#-------------------#
|
|
56
61
|
# TEST INPUTS_TASKS #
|
|
@@ -71,15 +76,16 @@ INPUTS_TASKS = {
|
|
|
71
76
|
# TEST FIXTURES_TASKS #
|
|
72
77
|
#---------------------#
|
|
73
78
|
FIXTURES_TASKS = {
|
|
74
|
-
|
|
75
|
-
for
|
|
79
|
+
Task.get_task_class(task.name): load_fixture(f'{task.name}_output', FIXTURES_DIR)
|
|
80
|
+
for task in TASKS
|
|
81
|
+
if task.name in [t.name for t in TEST_TASKS]
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
#-----------#
|
|
79
85
|
# TEST OPTS #
|
|
80
86
|
#-----------#
|
|
81
87
|
META_OPTS = {
|
|
82
|
-
HEADER: 'User-Agent: Mozilla/5.0 (Windows NT 5.1; rv:7.0.1) Gecko/20100101 Firefox/7.0.1',
|
|
88
|
+
HEADER: 'User-Agent: Mozilla/5.0 (Windows NT 5.1; rv:7.0.1) Gecko/20100101 Firefox/7.0.1;; Hello: World',
|
|
83
89
|
DELAY: 0,
|
|
84
90
|
DEPTH: 2,
|
|
85
91
|
FOLLOW_REDIRECT: True,
|
|
@@ -159,6 +165,10 @@ def mock_command(cls, inputs=[], opts={}, fixture=None, method=''):
|
|
|
159
165
|
|
|
160
166
|
class CommandOutputTester: # Mixin for unittest.TestCase
|
|
161
167
|
|
|
168
|
+
@staticmethod
|
|
169
|
+
def get_item_str(item):
|
|
170
|
+
return f"Item: {repr(item)}\nItem dict: {json.dumps(item.toDict(), default=str, indent=2)}"
|
|
171
|
+
|
|
162
172
|
def _test_runner_output(
|
|
163
173
|
self,
|
|
164
174
|
runner,
|
|
@@ -166,10 +176,12 @@ class CommandOutputTester: # Mixin for unittest.TestCase
|
|
|
166
176
|
expected_output_types=[],
|
|
167
177
|
expected_results=[],
|
|
168
178
|
expected_status='SUCCESS',
|
|
169
|
-
empty_results_allowed=False
|
|
179
|
+
empty_results_allowed=False,
|
|
180
|
+
additional_checks=[]):
|
|
170
181
|
|
|
171
182
|
console.print(f'\t[dim]Testing {runner.config.type} {runner.name} ...[/]', end='')
|
|
172
183
|
debug('', sub='unittest')
|
|
184
|
+
debug('-' * 10 + f' RUNNER {runner.name} STARTING ' + '-' * 10, sub='unittest')
|
|
173
185
|
|
|
174
186
|
if not runner.inputs:
|
|
175
187
|
console.print('[dim gold3] skipped (no inputs defined).[/]')
|
|
@@ -183,8 +195,11 @@ class CommandOutputTester: # Mixin for unittest.TestCase
|
|
|
183
195
|
|
|
184
196
|
# Run runner
|
|
185
197
|
results = runner.run()
|
|
186
|
-
for
|
|
187
|
-
|
|
198
|
+
results_str = "\n".join([repr(r) for r in results])
|
|
199
|
+
debug(f'{runner.name} yielded results\n{results_str}', sub='unittest')
|
|
200
|
+
debug(f'{runner.name} yielded results\n{json.dumps([r.toDict() for r in results], default=str, indent=2)}', sub='unittest.dict', verbose=True) # noqa: E501
|
|
201
|
+
|
|
202
|
+
debug('-' * 10 + f' RUNNER {runner.name} TESTS ' + '-' * 10, sub='unittest')
|
|
188
203
|
|
|
189
204
|
# Add execution types to allowed output types
|
|
190
205
|
expected_output_types.extend(EXECUTION_TYPES + STAT_TYPES)
|
|
@@ -205,13 +220,17 @@ class CommandOutputTester: # Mixin for unittest.TestCase
|
|
|
205
220
|
self.assertEqual(runner.status, expected_status, f'{runner.name} should have the status {expected_status}. Errors: {runner.errors}') # noqa: E501
|
|
206
221
|
|
|
207
222
|
# Check results
|
|
223
|
+
failures = []
|
|
224
|
+
debug('-' * 10 + f' RUNNER {runner.name} ITEM TESTS ' + '-' * 10, sub='unittest')
|
|
208
225
|
for item in results:
|
|
209
|
-
|
|
210
|
-
debug(
|
|
226
|
+
item_str = self.get_item_str(item)
|
|
227
|
+
debug('--' * 5, sub='unittest')
|
|
228
|
+
debug(f'{runner.name} item {repr(item)}', sub='unittest')
|
|
229
|
+
debug(f'{runner.name} item [{item.toDict()}]', sub='unittest.item', verbose=True)
|
|
211
230
|
|
|
212
231
|
if expected_output_types:
|
|
213
232
|
debug(f'{runner.name} item should have an output type in {[_._type for _ in expected_output_types]}', sub='unittest') # noqa: E501
|
|
214
|
-
self.assertIn(type(item), expected_output_types, f'{runner.name}: item has an unexpected output type "{type(item)}"') # noqa: E501
|
|
233
|
+
self.assertIn(type(item), expected_output_types, f'{runner.name}: item has an unexpected output type "{type(item)}". Expected types: {expected_output_types}.\n{item_str}') # noqa: E501
|
|
215
234
|
|
|
216
235
|
if expected_output_keys:
|
|
217
236
|
keys = [k for k in list(item.keys()) if not k.startswith('_')]
|
|
@@ -219,7 +238,29 @@ class CommandOutputTester: # Mixin for unittest.TestCase
|
|
|
219
238
|
self.assertEqual(
|
|
220
239
|
set(keys).difference(set(expected_output_keys)),
|
|
221
240
|
set(),
|
|
222
|
-
f'{runner.name}: item is missing expected keys {set(expected_output_keys)}
|
|
241
|
+
f'{runner.name}: item is missing expected keys {set(expected_output_keys)}.\nItem keys: {keys}.\n{item_str}') # noqa: E501
|
|
242
|
+
|
|
243
|
+
if additional_checks and item.__class__ in additional_checks.get('output_types', {}):
|
|
244
|
+
config = additional_checks['output_types'][item.__class__]
|
|
245
|
+
runner_regex = config.get('runner', '*')
|
|
246
|
+
if not re.match(runner_regex, runner.name):
|
|
247
|
+
continue
|
|
248
|
+
checks = config.get('checks', [])
|
|
249
|
+
for check in checks:
|
|
250
|
+
error = check['error']
|
|
251
|
+
info = check['info']
|
|
252
|
+
func = check['function']
|
|
253
|
+
debug(f'{runner.name} item {info}', sub='unittest')
|
|
254
|
+
try:
|
|
255
|
+
result = func(item)
|
|
256
|
+
if not result:
|
|
257
|
+
failures.append(f'ERROR ({runner.name}): {error}.\n{item_str}')
|
|
258
|
+
except Exception as e:
|
|
259
|
+
failures.append(f'ERROR ({runner.name}): {error}.\n{item_str}\n{traceback_as_string(e)}')
|
|
260
|
+
|
|
261
|
+
# Additional checks failures
|
|
262
|
+
if failures:
|
|
263
|
+
self.fail("\n\n" + "\n\n".join(failures))
|
|
223
264
|
|
|
224
265
|
# Check if runner results in expected results
|
|
225
266
|
if expected_results:
|
secator/workflows/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from secator.
|
|
1
|
+
from secator.loader import get_configs_by_type
|
|
2
2
|
from secator.runners import Workflow
|
|
3
3
|
|
|
4
4
|
|
|
@@ -21,7 +21,7 @@ class DynamicWorkflow(Workflow):
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
DYNAMIC_WORKFLOWS = {}
|
|
24
|
-
for workflow in
|
|
24
|
+
for workflow in get_configs_by_type('workflow'):
|
|
25
25
|
instance = DynamicWorkflow(workflow)
|
|
26
26
|
DYNAMIC_WORKFLOWS[workflow.name] = instance
|
|
27
27
|
|