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.

Files changed (106) hide show
  1. secator/celery.py +40 -24
  2. secator/celery_signals.py +71 -68
  3. secator/celery_utils.py +43 -27
  4. secator/cli.py +520 -280
  5. secator/cli_helper.py +394 -0
  6. secator/click.py +87 -0
  7. secator/config.py +67 -39
  8. secator/configs/profiles/http_headless.yaml +6 -0
  9. secator/configs/profiles/http_record.yaml +6 -0
  10. secator/configs/profiles/tor.yaml +1 -1
  11. secator/configs/scans/domain.yaml +4 -2
  12. secator/configs/scans/host.yaml +1 -1
  13. secator/configs/scans/network.yaml +1 -4
  14. secator/configs/scans/subdomain.yaml +13 -1
  15. secator/configs/scans/url.yaml +1 -2
  16. secator/configs/workflows/cidr_recon.yaml +6 -4
  17. secator/configs/workflows/code_scan.yaml +1 -1
  18. secator/configs/workflows/host_recon.yaml +29 -3
  19. secator/configs/workflows/subdomain_recon.yaml +67 -16
  20. secator/configs/workflows/url_crawl.yaml +44 -15
  21. secator/configs/workflows/url_dirsearch.yaml +4 -4
  22. secator/configs/workflows/url_fuzz.yaml +25 -17
  23. secator/configs/workflows/url_params_fuzz.yaml +7 -0
  24. secator/configs/workflows/url_vuln.yaml +33 -8
  25. secator/configs/workflows/user_hunt.yaml +4 -2
  26. secator/configs/workflows/wordpress.yaml +5 -3
  27. secator/cve.py +718 -0
  28. secator/decorators.py +0 -454
  29. secator/definitions.py +49 -30
  30. secator/exporters/_base.py +2 -2
  31. secator/exporters/console.py +2 -2
  32. secator/exporters/table.py +4 -3
  33. secator/exporters/txt.py +1 -1
  34. secator/hooks/mongodb.py +2 -4
  35. secator/installer.py +77 -49
  36. secator/loader.py +116 -0
  37. secator/output_types/_base.py +3 -0
  38. secator/output_types/certificate.py +63 -63
  39. secator/output_types/error.py +4 -5
  40. secator/output_types/info.py +2 -2
  41. secator/output_types/ip.py +3 -1
  42. secator/output_types/progress.py +5 -9
  43. secator/output_types/state.py +17 -17
  44. secator/output_types/tag.py +3 -0
  45. secator/output_types/target.py +10 -2
  46. secator/output_types/url.py +19 -7
  47. secator/output_types/vulnerability.py +11 -7
  48. secator/output_types/warning.py +2 -2
  49. secator/report.py +27 -15
  50. secator/rich.py +18 -10
  51. secator/runners/_base.py +446 -233
  52. secator/runners/_helpers.py +133 -24
  53. secator/runners/command.py +182 -102
  54. secator/runners/scan.py +33 -5
  55. secator/runners/task.py +13 -7
  56. secator/runners/workflow.py +105 -72
  57. secator/scans/__init__.py +2 -2
  58. secator/serializers/dataclass.py +20 -20
  59. secator/tasks/__init__.py +4 -4
  60. secator/tasks/_categories.py +39 -27
  61. secator/tasks/arjun.py +9 -5
  62. secator/tasks/bbot.py +53 -21
  63. secator/tasks/bup.py +19 -5
  64. secator/tasks/cariddi.py +24 -3
  65. secator/tasks/dalfox.py +26 -7
  66. secator/tasks/dirsearch.py +10 -4
  67. secator/tasks/dnsx.py +70 -25
  68. secator/tasks/feroxbuster.py +11 -3
  69. secator/tasks/ffuf.py +42 -6
  70. secator/tasks/fping.py +20 -8
  71. secator/tasks/gau.py +3 -1
  72. secator/tasks/gf.py +3 -3
  73. secator/tasks/gitleaks.py +2 -2
  74. secator/tasks/gospider.py +7 -1
  75. secator/tasks/grype.py +5 -4
  76. secator/tasks/h8mail.py +2 -1
  77. secator/tasks/httpx.py +18 -5
  78. secator/tasks/katana.py +35 -15
  79. secator/tasks/maigret.py +4 -4
  80. secator/tasks/mapcidr.py +3 -3
  81. secator/tasks/msfconsole.py +4 -4
  82. secator/tasks/naabu.py +2 -2
  83. secator/tasks/nmap.py +12 -14
  84. secator/tasks/nuclei.py +3 -3
  85. secator/tasks/searchsploit.py +4 -5
  86. secator/tasks/subfinder.py +2 -2
  87. secator/tasks/testssl.py +264 -263
  88. secator/tasks/trivy.py +5 -5
  89. secator/tasks/wafw00f.py +21 -3
  90. secator/tasks/wpprobe.py +90 -83
  91. secator/tasks/wpscan.py +6 -5
  92. secator/template.py +218 -104
  93. secator/thread.py +15 -15
  94. secator/tree.py +196 -0
  95. secator/utils.py +131 -123
  96. secator/utils_test.py +60 -19
  97. secator/workflows/__init__.py +2 -2
  98. {secator-0.15.1.dist-info → secator-0.16.1.dist-info}/METADATA +36 -36
  99. secator-0.16.1.dist-info/RECORD +132 -0
  100. secator/configs/profiles/default.yaml +0 -8
  101. secator/configs/workflows/url_nuclei.yaml +0 -11
  102. secator/tasks/dnsxbrute.py +0 -42
  103. secator-0.15.1.dist-info/RECORD +0 -128
  104. {secator-0.15.1.dist-info → secator-0.16.1.dist-info}/WHEEL +0 -0
  105. {secator-0.15.1.dist-info → secator-0.16.1.dist-info}/entry_points.txt +0 -0
  106. {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 (DEBUG_COMPONENT, VERSION, DEV_PACKAGE)
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 ctx.obj['piped_input']:
78
- console.print('Waiting for input on stdin ...', style='bold yellow')
79
- rlist, _, _ = select.select([sys.stdin], [], [], CONFIG.cli.stdin_timeout)
80
- if rlist:
81
- data = sys.stdin.read().splitlines()
82
- else:
83
- console.print(
84
- 'No input passed on stdin. Showing help page.',
85
- style='bold red')
86
- return None
87
- return data
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
- console.print(f'[bold red]Could not convert rich text to ansi: {text}[/]', highlight=False, markup=False)
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 format_object(obj, obj_breaklines=False):
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'[dim cyan]{k}[/] [dim yellow]->[/] [dim green]{v}[/]' for k, v in obj.items() if v is not None) # noqa: E501
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 DEBUG_COMPONENT == ['all']:
425
- if not DEBUG_COMPONENT or DEBUG_COMPONENT == [""]:
344
+ if not DEBUG == ['all'] and not DEBUG == ['1']:
345
+ if not DEBUG or DEBUG == [""]:
426
346
  return
427
-
428
347
  if sub:
429
- if verbose and sub not in DEBUG_COMPONENT:
430
- sub = f'debug.{sub}'
431
- if not any(sub.startswith(s) for s in DEBUG_COMPONENT):
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 = format_object(obj, obj_breaklines) if obj else ''
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
- console.print(rf'[dim]\[[magenta4]DBG[/]] {formatted_msg}[/]')
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.registered_domain):
440
+ if not validators.domain(result.top_domain_under_public_suffix):
511
441
  return None
512
- return result.registered_domain
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
- if Path(val).exists():
791
- return val
792
- else:
793
- return download_file(
794
- val,
795
- target_folder=CONFIG.dirs.wordlists,
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.cli import ALL_WORKFLOWS, ALL_TASKS, ALL_SCANS
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 = [cls for cls in ALL_TASKS if cls.__name__ in TEST_TASKS.split(',')]
38
+ TEST_TASKS = [config for config in TASKS if config.name in TEST_TASKS.split(',')]
34
39
  else:
35
- TEST_TASKS = ALL_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 ALL_WORKFLOWS if config.name in TEST_WORKFLOWS.split(',')]
47
+ TEST_WORKFLOWS = [config for config in WORKFLOWS if config.name in TEST_WORKFLOWS.split(',')]
43
48
  else:
44
- TEST_WORKFLOWS = ALL_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 ALL_SCANS if config.name in TEST_SCANS.split(',')]
56
+ TEST_SCANS = [config for config in SCANS if config.name in TEST_SCANS.split(',')]
52
57
  else:
53
- TEST_SCANS = ALL_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
- tool_cls: load_fixture(f'{tool_cls.__name__}_output', FIXTURES_DIR)
75
- for tool_cls in TEST_TASKS
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 result in results:
187
- debug(result.toDict(), sub='unittest')
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
- debug(f'{runner.name} yielded {repr(item)}', sub='unittest')
210
- debug(f'{runner.name} yielded (JSON): {json.dumps(item.toDict(), default=str)}', sub='unittest.dict', verbose=True)
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)}. Item keys: {keys}') # noqa: E501
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:
@@ -1,4 +1,4 @@
1
- from secator.cli import ALL_WORKFLOWS
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 ALL_WORKFLOWS:
24
+ for workflow in get_configs_by_type('workflow'):
25
25
  instance = DynamicWorkflow(workflow)
26
26
  DYNAMIC_WORKFLOWS[workflow.name] = instance
27
27