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.

Files changed (73) hide show
  1. secator/celery.py +10 -5
  2. secator/celery_signals.py +2 -11
  3. secator/cli.py +309 -69
  4. secator/config.py +3 -2
  5. secator/configs/profiles/aggressive.yaml +6 -5
  6. secator/configs/profiles/default.yaml +6 -7
  7. secator/configs/profiles/insane.yaml +8 -0
  8. secator/configs/profiles/paranoid.yaml +8 -0
  9. secator/configs/profiles/polite.yaml +8 -0
  10. secator/configs/profiles/sneaky.yaml +8 -0
  11. secator/configs/profiles/tor.yaml +5 -0
  12. secator/configs/workflows/host_recon.yaml +11 -2
  13. secator/configs/workflows/url_dirsearch.yaml +5 -0
  14. secator/configs/workflows/url_params_fuzz.yaml +25 -0
  15. secator/configs/workflows/wordpress.yaml +4 -1
  16. secator/decorators.py +64 -34
  17. secator/definitions.py +8 -4
  18. secator/installer.py +84 -49
  19. secator/output_types/__init__.py +2 -1
  20. secator/output_types/certificate.py +78 -0
  21. secator/output_types/stat.py +3 -0
  22. secator/output_types/user_account.py +1 -1
  23. secator/report.py +2 -2
  24. secator/rich.py +1 -1
  25. secator/runners/_base.py +50 -11
  26. secator/runners/_helpers.py +15 -3
  27. secator/runners/command.py +85 -21
  28. secator/runners/scan.py +6 -3
  29. secator/runners/task.py +1 -0
  30. secator/runners/workflow.py +22 -4
  31. secator/tasks/_categories.py +25 -17
  32. secator/tasks/arjun.py +92 -0
  33. secator/tasks/bbot.py +33 -4
  34. secator/tasks/bup.py +4 -2
  35. secator/tasks/cariddi.py +17 -4
  36. secator/tasks/dalfox.py +4 -2
  37. secator/tasks/dirsearch.py +4 -2
  38. secator/tasks/dnsx.py +5 -2
  39. secator/tasks/dnsxbrute.py +4 -1
  40. secator/tasks/feroxbuster.py +5 -2
  41. secator/tasks/ffuf.py +7 -3
  42. secator/tasks/fping.py +4 -1
  43. secator/tasks/gau.py +5 -2
  44. secator/tasks/gf.py +4 -2
  45. secator/tasks/gitleaks.py +79 -0
  46. secator/tasks/gospider.py +5 -2
  47. secator/tasks/grype.py +5 -2
  48. secator/tasks/h8mail.py +4 -2
  49. secator/tasks/httpx.py +6 -3
  50. secator/tasks/katana.py +6 -3
  51. secator/tasks/maigret.py +4 -2
  52. secator/tasks/mapcidr.py +5 -3
  53. secator/tasks/msfconsole.py +8 -6
  54. secator/tasks/naabu.py +16 -5
  55. secator/tasks/nmap.py +31 -29
  56. secator/tasks/nuclei.py +18 -10
  57. secator/tasks/searchsploit.py +8 -3
  58. secator/tasks/subfinder.py +6 -3
  59. secator/tasks/testssl.py +276 -0
  60. secator/tasks/trivy.py +98 -0
  61. secator/tasks/wafw00f.py +85 -0
  62. secator/tasks/wpprobe.py +96 -0
  63. secator/tasks/wpscan.py +8 -4
  64. secator/template.py +61 -67
  65. secator/utils.py +31 -18
  66. secator/utils_test.py +34 -10
  67. {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/METADATA +11 -3
  68. secator-0.15.1.dist-info/RECORD +128 -0
  69. secator/configs/profiles/stealth.yaml +0 -7
  70. secator-0.10.1a12.dist-info/RECORD +0 -116
  71. {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/WHEEL +0 -0
  72. {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/entry_points.txt +0 -0
  73. {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/licenses/LICENSE +0 -0
secator/celery.py CHANGED
@@ -223,10 +223,11 @@ def forward_results(results):
223
223
 
224
224
 
225
225
  @app.task
226
- def mark_runner_started(runner, enable_hooks=True):
226
+ def mark_runner_started(results, runner, enable_hooks=True):
227
227
  """Mark a runner as started and run on_start hooks.
228
228
 
229
229
  Args:
230
+ results (List): Previous results.
230
231
  runner (Runner): Secator runner instance.
231
232
  enable_hooks (bool): Enable hooks.
232
233
 
@@ -234,9 +235,12 @@ def mark_runner_started(runner, enable_hooks=True):
234
235
  list: Runner results
235
236
  """
236
237
  debug(f'Runner {runner.unique_name} has started, running mark_started', sub='celery')
238
+ if results:
239
+ runner.results = forward_results(results)
237
240
  runner.enable_hooks = enable_hooks
238
- runner.mark_started()
239
- return forward_results(runner.results)
241
+ if not runner.dry_run:
242
+ runner.mark_started()
243
+ return runner.results
240
244
 
241
245
 
242
246
  @app.task
@@ -254,8 +258,9 @@ def mark_runner_completed(results, runner, enable_hooks=True):
254
258
  debug(f'Runner {runner.unique_name} has finished, running mark_completed', sub='celery')
255
259
  results = forward_results(results)
256
260
  runner.enable_hooks = enable_hooks
257
- [runner.add_result(item) for item in results]
258
- runner.mark_completed()
261
+ if not runner.dry_run:
262
+ [runner.add_result(item) for item in results]
263
+ runner.mark_completed()
259
264
  return runner.results
260
265
 
261
266
 
secator/celery_signals.py CHANGED
@@ -65,16 +65,6 @@ def setup_idle_timer(timeout):
65
65
  timer.start()
66
66
 
67
67
 
68
- def maybe_override_logging():
69
- def decorator(func):
70
- if CONFIG.celery.override_default_logging:
71
- return signals.setup_logging.connect(func)
72
- else:
73
- return func
74
- return decorator
75
-
76
-
77
- @maybe_override_logging()
78
68
  def setup_logging(*args, **kwargs):
79
69
  """Override celery's logging setup to prevent it from altering our settings.
80
70
  github.com/celery/celery/issues/1867
@@ -134,8 +124,9 @@ def worker_shutdown_handler(**kwargs):
134
124
 
135
125
 
136
126
  def setup_handlers():
127
+ if CONFIG.celery.override_default_logging:
128
+ signals.setup_logging.connect(setup_logging)
137
129
  signals.celeryd_after_setup.connect(capture_worker_name)
138
- signals.setup_logging.connect(setup_logging)
139
130
  signals.task_prerun.connect(task_prerun_handler)
140
131
  signals.task_postrun.connect(task_postrun_handler)
141
132
  signals.task_revoked.connect(task_revoked_handler)
secator/cli.py CHANGED
@@ -18,14 +18,14 @@ from rich.table import Table
18
18
 
19
19
  from secator.config import CONFIG, ROOT_FOLDER, Config, default_config, config_path
20
20
  from secator.decorators import OrderedGroup, register_runner
21
- from secator.definitions import ADDONS_ENABLED, ASCII, DEV_PACKAGE, OPT_NOT_SUPPORTED, VERSION, STATE_COLORS
21
+ from secator.definitions import ADDONS_ENABLED, ASCII, DEV_PACKAGE, VERSION, STATE_COLORS
22
22
  from secator.installer import ToolInstaller, fmt_health_table_row, get_health_table, get_version_info, get_distro_config
23
23
  from secator.output_types import FINDING_TYPES, Info, Warning, Error
24
24
  from secator.report import Report
25
25
  from secator.rich import console
26
26
  from secator.runners import Command, Runner
27
27
  from secator.serializers.dataclass import loads_dataclass
28
- from secator.template import TemplateLoader
28
+ from secator.template import TEMPLATES, TemplateLoader
29
29
  from secator.utils import (
30
30
  debug, detect_host, discover_tasks, flatten, print_version, get_file_date,
31
31
  sort_files_by_date, get_file_timestamp, list_reports, get_info_from_report_path, human_to_timedelta
@@ -34,26 +34,29 @@ from secator.utils import (
34
34
  click.rich_click.USE_RICH_MARKUP = True
35
35
 
36
36
  ALL_TASKS = discover_tasks()
37
- ALL_CONFIGS = TemplateLoader.load_all()
38
- ALL_WORKFLOWS = ALL_CONFIGS.workflow
39
- ALL_SCANS = ALL_CONFIGS.scan
37
+ ALL_WORKFLOWS = [t for t in TEMPLATES if t.type == 'workflow']
38
+ ALL_SCANS = [t for t in TEMPLATES if t.type == 'scan']
39
+ ALL_PROFILES = [t for t in TEMPLATES if t.type == 'profile']
40
40
  FINDING_TYPES_LOWER = [c.__name__.lower() for c in FINDING_TYPES]
41
+ CONTEXT_SETTINGS = dict(help_option_names=['-h', '-help', '--help'])
41
42
 
42
43
 
43
44
  #-----#
44
45
  # CLI #
45
46
  #-----#
46
47
 
47
- @click.group(cls=OrderedGroup, invoke_without_command=True)
48
- @click.option('--version', '-version', is_flag=True, default=False)
48
+
49
+ @click.group(cls=OrderedGroup, invoke_without_command=True, context_settings=CONTEXT_SETTINGS)
50
+ @click.option('--version', '-version', '-v', is_flag=True, default=False)
51
+ @click.option('--quiet', '-quiet', '-q', is_flag=True, default=False)
49
52
  @click.pass_context
50
- def cli(ctx, version):
53
+ def cli(ctx, version, quiet):
51
54
  """Secator CLI."""
52
55
  ctx.obj = {
53
56
  'piped_input': S_ISFIFO(os.fstat(0).st_mode),
54
57
  'piped_output': not sys.stdout.isatty()
55
58
  }
56
- if not ctx.obj['piped_output']:
59
+ if not ctx.obj['piped_output'] and not quiet:
57
60
  console.print(ASCII, highlight=False)
58
61
  if ctx.invoked_subcommand is None:
59
62
  if version:
@@ -66,11 +69,16 @@ def cli(ctx, version):
66
69
  # TASK #
67
70
  #------#
68
71
 
69
- @cli.group(aliases=['x', 't'])
72
+ @cli.group(aliases=['x', 't'], invoke_without_command=True)
73
+ @click.option('--list', '-list', is_flag=True, default=False)
70
74
  @click.pass_context
71
- def task(ctx):
75
+ def task(ctx, list=False):
72
76
  """Run a task."""
73
- pass
77
+ if list:
78
+ print("\n".join(sorted([t.__name__ for t in ALL_TASKS])))
79
+ return
80
+ if ctx.invoked_subcommand is None:
81
+ ctx.get_help()
74
82
 
75
83
 
76
84
  for cls in ALL_TASKS:
@@ -82,11 +90,16 @@ for cls in ALL_TASKS:
82
90
  #----------#
83
91
 
84
92
 
85
- @cli.group(cls=OrderedGroup, aliases=['w'])
93
+ @cli.group(cls=OrderedGroup, aliases=['w'], invoke_without_command=True)
94
+ @click.option('--list', '-list', is_flag=True, default=False)
86
95
  @click.pass_context
87
- def workflow(ctx):
96
+ def workflow(ctx, list=False):
88
97
  """Run a workflow."""
89
- pass
98
+ if list:
99
+ print("\n".join(sorted([t.name for t in ALL_WORKFLOWS])))
100
+ return
101
+ if ctx.invoked_subcommand is None:
102
+ ctx.get_help()
90
103
 
91
104
 
92
105
  for config in sorted(ALL_WORKFLOWS, key=lambda x: x['name']):
@@ -97,17 +110,41 @@ for config in sorted(ALL_WORKFLOWS, key=lambda x: x['name']):
97
110
  # SCAN #
98
111
  #------#
99
112
 
100
- @cli.group(cls=OrderedGroup, aliases=['s'])
113
+ @cli.group(cls=OrderedGroup, aliases=['s'], invoke_without_command=True)
114
+ @click.option('--list', '-list', is_flag=True, default=False)
101
115
  @click.pass_context
102
- def scan(ctx):
116
+ def scan(ctx, list=False):
103
117
  """Run a scan."""
104
- pass
118
+ if list:
119
+ print("\n".join(sorted([t.name for t in ALL_SCANS])))
120
+ return
121
+ if ctx.invoked_subcommand is None:
122
+ ctx.get_help()
105
123
 
106
124
 
107
125
  for config in sorted(ALL_SCANS, key=lambda x: x['name']):
108
126
  register_runner(scan, config)
109
127
 
110
128
 
129
+ @cli.group(aliases=['p'])
130
+ @click.pass_context
131
+ def profile(ctx):
132
+ """Show profiles"""
133
+ pass
134
+
135
+
136
+ @profile.command('list')
137
+ def profile_list():
138
+ table = Table()
139
+ table.add_column("Profile name", style="bold gold3")
140
+ table.add_column("Description", overflow='fold')
141
+ table.add_column("Options", overflow='fold')
142
+ for profile in ALL_PROFILES:
143
+ opts_str = ','.join(f'{k}={v}' for k, v in profile.opts.items())
144
+ table.add_row(profile.name, profile.description, opts_str)
145
+ console.print(table)
146
+
147
+
111
148
  #--------#
112
149
  # WORKER #
113
150
  #--------#
@@ -118,12 +155,13 @@ for config in sorted(ALL_SCANS, key=lambda x: x['name']):
118
155
  @click.option('-r', '--reload', is_flag=True, help='Autoreload Celery on code changes.')
119
156
  @click.option('-Q', '--queue', type=str, default='', help='Listen to a specific queue.')
120
157
  @click.option('-P', '--pool', type=str, default='eventlet', help='Pool implementation.')
121
- @click.option('--quiet', is_flag=True, help='Quiet mode.')
158
+ @click.option('--quiet', is_flag=True, default=False, help='Quiet mode.')
159
+ @click.option('--loglevel', type=str, default='INFO', help='Log level.')
122
160
  @click.option('--check', is_flag=True, help='Check if Celery worker is alive.')
123
161
  @click.option('--dev', is_flag=True, help='Start a worker in dev mode (celery multi).')
124
162
  @click.option('--stop', is_flag=True, help='Stop a worker in dev mode (celery multi).')
125
163
  @click.option('--show', is_flag=True, help='Show command (celery multi).')
126
- def worker(hostname, concurrency, reload, queue, pool, quiet, check, dev, stop, show):
164
+ def worker(hostname, concurrency, reload, queue, pool, quiet, loglevel, check, dev, stop, show):
127
165
  """Run a worker."""
128
166
 
129
167
  # Check Celery addon is installed
@@ -169,6 +207,7 @@ def worker(hostname, concurrency, reload, queue, pool, quiet, check, dev, stop,
169
207
 
170
208
  cmd += f' -P {pool}' if pool else ''
171
209
  cmd += f' -c {concurrency}' if concurrency else ''
210
+ cmd += f' -l {loglevel}' if loglevel else ''
172
211
 
173
212
  if reload:
174
213
  patterns = "celery.py;tasks/*.py;runners/*.py;serializers/*.py;output_types/*.py;hooks/*.py;exporters/*.py"
@@ -666,7 +705,7 @@ def report_show(report_query, output, runner_type, time_delta, type, query, work
666
705
  all_results.extend(runner.results)
667
706
  continue
668
707
  report = Report(runner, title=f"Consolidated report - {current}", exporters=exporters)
669
- report.build(extractors=extractors if not unified else [])
708
+ report.build(extractors=extractors if not unified else [], dedupe=unified)
670
709
  file_date = get_file_date(path)
671
710
  runner_name = data['info']['name']
672
711
  console.print(
@@ -744,7 +783,7 @@ def report_list(workspace, runner_type, time_delta):
744
783
  @report.command('export')
745
784
  @click.argument('json_path', type=str)
746
785
  @click.option('--output-folder', '-of', type=str)
747
- @click.option('-output', '-o', type=str)
786
+ @click.option('-output', '-o', type=str, required=True)
748
787
  def report_export(json_path, output_folder, output):
749
788
  with open(json_path, 'r') as f:
750
789
  data = loads_dataclass(f.read())
@@ -857,13 +896,21 @@ def health(json, debug, strict):
857
896
  import json as _json
858
897
  print(_json.dumps(status))
859
898
 
899
+ # Print errors and warnings
900
+ error = False
901
+ for tool, info in status['tools'].items():
902
+ if not info['installed']:
903
+ console.print(Warning(message=f'{tool} is not installed.'))
904
+ error = True
905
+ elif info['outdated']:
906
+ message = (
907
+ f'{tool} is outdated (current:{info["version"]}, latest:{info["latest_version"]}).'
908
+ f' Run `secator install tools {tool}` to update it.'
909
+ )
910
+ console.print(Warning(message=message))
911
+
860
912
  # Strict mode
861
913
  if strict:
862
- error = False
863
- for tool, info in status['tools'].items():
864
- if not info['installed']:
865
- console.print(Error(message=f'{tool} not installed and strict mode is enabled.'))
866
- error = True
867
914
  if error:
868
915
  sys.exit(1)
869
916
  console.print(Info(message='Strict healthcheck passed !'))
@@ -878,21 +925,21 @@ def run_install(title=None, cmd=None, packages=None, next_steps=None):
878
925
  if CONFIG.offline_mode:
879
926
  console.print(Error(message='Cannot run this command in offline mode.'))
880
927
  return
881
- with console.status(f'[bold yellow] Installing {title}...'):
882
- if cmd:
883
- from secator.installer import SourceInstaller
884
- status = SourceInstaller.install(cmd)
885
- elif packages:
886
- from secator.installer import PackageInstaller
887
- status = PackageInstaller.install(packages)
888
- return_code = 1
889
- if status.is_ok():
890
- return_code = 0
891
- if next_steps:
892
- console.print('[bold gold3]:wrench: Next steps:[/]')
893
- for ix, step in enumerate(next_steps):
894
- console.print(f' :keycap_{ix}: {step}')
895
- sys.exit(return_code)
928
+ # with console.status(f'[bold yellow] Installing {title}...'):
929
+ if cmd:
930
+ from secator.installer import SourceInstaller
931
+ status = SourceInstaller.install(cmd)
932
+ elif packages:
933
+ from secator.installer import PackageInstaller
934
+ status = PackageInstaller.install(packages)
935
+ return_code = 1
936
+ if status.is_ok():
937
+ return_code = 0
938
+ if next_steps:
939
+ console.print('[bold gold3]:wrench: Next steps:[/]')
940
+ for ix, step in enumerate(next_steps):
941
+ console.print(f' :keycap_{ix}: {step}')
942
+ sys.exit(return_code)
896
943
 
897
944
 
898
945
  @cli.group()
@@ -1051,28 +1098,42 @@ def install_ruby():
1051
1098
 
1052
1099
  @install.command('tools')
1053
1100
  @click.argument('cmds', required=False)
1054
- @click.option('--cleanup', is_flag=True, default=False)
1055
- def install_tools(cmds, cleanup):
1101
+ @click.option('--cleanup', is_flag=True, default=False, help='Clean up tools after installation.')
1102
+ @click.option('--fail-fast', is_flag=True, default=False, help='Fail fast if any tool fails to install.')
1103
+ def install_tools(cmds, cleanup, fail_fast):
1056
1104
  """Install supported tools."""
1057
1105
  if CONFIG.offline_mode:
1058
1106
  console.print(Error(message='Cannot run this command in offline mode.'))
1059
1107
  return
1108
+ tools = []
1060
1109
  if cmds is not None:
1061
1110
  cmds = cmds.split(',')
1062
- tools = [cls for cls in ALL_TASKS if cls.__name__ in cmds]
1111
+ for cmd in cmds:
1112
+ if '==' in cmd:
1113
+ cmd, version = tuple(cmd.split('=='))
1114
+ else:
1115
+ cmd, version = cmd, None
1116
+ cls = next((cls for cls in ALL_TASKS if cls.__name__ == cmd), None)
1117
+ if cls:
1118
+ if version:
1119
+ cls.install_version = version
1120
+ tools.append(cls)
1121
+ else:
1122
+ console.print(Warning(message=f'Tool {cmd} is not supported or inexistent.'))
1063
1123
  else:
1064
1124
  tools = ALL_TASKS
1065
1125
  tools.sort(key=lambda x: x.__name__)
1066
1126
  return_code = 0
1067
1127
  if not tools:
1068
- cmd_str = ' '.join(cmds)
1069
- console.print(Error(message=f'No tools found for {cmd_str}.'))
1128
+ console.print(Error(message='No tools found for installing.'))
1070
1129
  return
1071
1130
  for ix, cls in enumerate(tools):
1072
- with console.status(f'[bold yellow][{ix + 1}/{len(tools)}] Installing {cls.__name__} ...'):
1073
- status = ToolInstaller.install(cls)
1074
- if not status.is_ok():
1075
- return_code = 1
1131
+ # with console.status(f'[bold yellow][{ix + 1}/{len(tools)}] Installing {cls.__name__} ...'):
1132
+ status = ToolInstaller.install(cls)
1133
+ if not status.is_ok():
1134
+ return_code = 1
1135
+ if fail_fast:
1136
+ sys.exit(return_code)
1076
1137
  console.print()
1077
1138
  if cleanup:
1078
1139
  distro = get_distro_config()
@@ -1132,14 +1193,13 @@ def update(all):
1132
1193
  return_code = 0
1133
1194
  for cls in ALL_TASKS:
1134
1195
  cmd = cls.cmd.split(' ')[0]
1135
- version_flag = cls.version_flag or f'{cls.opt_prefix}version'
1136
- version_flag = None if cls.version_flag == OPT_NOT_SUPPORTED else version_flag
1196
+ version_flag = cls.get_version_flag()
1137
1197
  info = get_version_info(cmd, version_flag, cls.install_github_handle)
1138
- if not info['installed'] or info['status'] == 'outdated' or not info['latest_version']:
1139
- with console.status(f'[bold yellow]Installing {cls.__name__} ...'):
1140
- status = ToolInstaller.install(cls)
1141
- if not status.is_ok():
1142
- return_code = 1
1198
+ if not info['installed'] or info['outdated'] or not info['latest_version']:
1199
+ # with console.status(f'[bold yellow]Installing {cls.__name__} ...'):
1200
+ status = ToolInstaller.install(cls)
1201
+ if not status.is_ok():
1202
+ return_code = 1
1143
1203
  sys.exit(return_code)
1144
1204
 
1145
1205
  #-------#
@@ -1250,24 +1310,35 @@ def test():
1250
1310
  pass
1251
1311
 
1252
1312
 
1253
- def run_test(cmd, name):
1313
+ def run_test(cmd, name=None, exit=True, verbose=False):
1254
1314
  """Run a test and return the result.
1255
1315
 
1256
1316
  Args:
1257
- cmd: Command to run.
1258
- name: Name of the test.
1317
+ cmd (str): Command to run.
1318
+ name (str, optional): Name of the test.
1319
+ exit (bool, optional): Exit after running the test with the return code.
1320
+ verbose (bool, optional): Print verbose output.
1321
+
1322
+ Returns:
1323
+ Return code of the test.
1259
1324
  """
1260
- result = Command.execute(cmd, name=name + ' tests', cwd=ROOT_FOLDER)
1261
- if result.return_code == 0:
1262
- console.print(f':tada: {name.capitalize()} tests passed !', style='bold green')
1263
- sys.exit(result.return_code)
1325
+ cmd_name = name + ' tests' if name else 'tests'
1326
+ result = Command.execute(cmd, name=cmd_name, cwd=ROOT_FOLDER, quiet=not verbose)
1327
+ if name:
1328
+ if result.return_code == 0:
1329
+ console.print(f':tada: {name.capitalize()} tests passed !', style='bold green')
1330
+ else:
1331
+ console.print(f':x: {name.capitalize()} tests failed !', style='bold red')
1332
+ if exit:
1333
+ sys.exit(result.return_code)
1334
+ return result.return_code
1264
1335
 
1265
1336
 
1266
1337
  @test.command()
1267
1338
  def lint():
1268
1339
  """Run lint tests."""
1269
1340
  cmd = f'{sys.executable} -m flake8 secator/'
1270
- run_test(cmd, 'lint')
1341
+ run_test(cmd, 'lint', verbose=True)
1271
1342
 
1272
1343
 
1273
1344
  @test.command()
@@ -1285,13 +1356,21 @@ def unit(tasks, workflows, scans, test):
1285
1356
  os.environ['SECATOR_HTTP_STORE_RESPONSES'] = '0'
1286
1357
  os.environ['SECATOR_RUNNERS_SKIP_CVE_SEARCH'] = '1'
1287
1358
 
1359
+ if not test:
1360
+ if tasks:
1361
+ test = 'test_tasks'
1362
+ elif workflows:
1363
+ test = 'test_workflows'
1364
+ elif scans:
1365
+ test = 'test_scans'
1366
+
1288
1367
  import shutil
1289
1368
  shutil.rmtree('/tmp/.secator', ignore_errors=True)
1290
1369
  cmd = f'{sys.executable} -m coverage run --omit="*test*" --data-file=.coverage.unit -m pytest -s -v tests/unit'
1291
1370
  if test:
1292
1371
  test_str = ' or '.join(test.split(','))
1293
1372
  cmd += f' -k "{test_str}"'
1294
- run_test(cmd, 'unit')
1373
+ run_test(cmd, 'unit', verbose=True)
1295
1374
 
1296
1375
 
1297
1376
  @test.command()
@@ -1307,6 +1386,14 @@ def integration(tasks, workflows, scans, test):
1307
1386
  os.environ['SECATOR_DIRS_DATA'] = '/tmp/.secator'
1308
1387
  os.environ['SECATOR_RUNNERS_SKIP_CVE_SEARCH'] = '1'
1309
1388
 
1389
+ if not test:
1390
+ if tasks:
1391
+ test = 'test_tasks'
1392
+ elif workflows:
1393
+ test = 'test_workflows'
1394
+ elif scans:
1395
+ test = 'test_scans'
1396
+
1310
1397
  import shutil
1311
1398
  shutil.rmtree('/tmp/.secator', ignore_errors=True)
1312
1399
 
@@ -1314,7 +1401,7 @@ def integration(tasks, workflows, scans, test):
1314
1401
  if test:
1315
1402
  test_str = ' or '.join(test.split(','))
1316
1403
  cmd += f' -k "{test_str}"'
1317
- run_test(cmd, 'integration')
1404
+ run_test(cmd, 'integration', verbose=True)
1318
1405
 
1319
1406
 
1320
1407
  @test.command()
@@ -1337,7 +1424,160 @@ def performance(tasks, workflows, scans, test):
1337
1424
  if test:
1338
1425
  test_str = ' or '.join(test.split(','))
1339
1426
  cmd += f' -k "{test_str}"'
1340
- run_test(cmd, 'performance')
1427
+ run_test(cmd, 'performance', verbose=True)
1428
+
1429
+
1430
+ @test.command()
1431
+ @click.argument('name', type=str)
1432
+ @click.option('--verbose', '-v', is_flag=True, default=False, help='Print verbose output')
1433
+ @click.option('--check', '-c', is_flag=True, default=False, help='Check task semantics only (no unit + integration tests)') # noqa: E501
1434
+ def task(name, verbose, check):
1435
+ """Test a single task for semantics errors, and run unit + integration tests."""
1436
+ console.print(f'[bold gold3]:wrench: Testing task {name} ...[/]')
1437
+ task = [task for task in ALL_TASKS if task.__name__ == name]
1438
+ warnings = []
1439
+ errors = []
1440
+ exit_code = 0
1441
+
1442
+ # Check if task is correctly registered
1443
+ task = task[0]
1444
+ task_name = task.__name__
1445
+
1446
+ # Check task command is set
1447
+ check_test(
1448
+ task.cmd,
1449
+ 'Check task command is set (cls.cmd)',
1450
+ 'Task has no cmd attribute.',
1451
+ errors
1452
+ )
1453
+ if errors:
1454
+ sys.exit(0)
1455
+
1456
+ # Run install
1457
+ cmd = f'secator install tools {task_name}'
1458
+ ret_code = Command.execute(cmd, name='install', quiet=not verbose, cwd=ROOT_FOLDER)
1459
+ version_info = task.get_version_info()
1460
+ if verbose:
1461
+ console.print(f'Version info:\n{version_info}')
1462
+ status = version_info['status']
1463
+ check_test(
1464
+ version_info['installed'],
1465
+ 'Check task is installed',
1466
+ 'Failed to install command. Fix your installation command.',
1467
+ errors
1468
+ )
1469
+ check_test(
1470
+ any(cmd for cmd in [task.install_pre, task.install_cmd, task.install_github_handle]),
1471
+ 'Check task installation command is defined',
1472
+ 'Task has no installation command. Please define one or more of the following class attributes: `install_pre`, `install_cmd`, `install_post`, `install_github_handle`.', # noqa: E501
1473
+ errors
1474
+ )
1475
+ check_test(
1476
+ version_info['version'],
1477
+ 'Check task version can be fetched',
1478
+ 'Failed to detect current version. Consider updating your `version_flag` class attribute.',
1479
+ warnings,
1480
+ warn=True
1481
+ )
1482
+ check_test(
1483
+ status != 'latest unknown',
1484
+ 'Check latest version',
1485
+ 'Failed to detect latest version.',
1486
+ warnings,
1487
+ warn=True
1488
+ )
1489
+ check_test(
1490
+ not version_info['outdated'],
1491
+ 'Check task version is up to date',
1492
+ f'Task is not up to date (current version: {version_info["version"]}, latest: {version_info["latest_version"]}). Consider updating your `install_version` class attribute.', # noqa: E501
1493
+ warnings,
1494
+ warn=True
1495
+ )
1496
+
1497
+ # Run task-specific tests
1498
+ check_test(
1499
+ task.__doc__,
1500
+ 'Check task description is set (cls.__doc__)',
1501
+ 'Task has no description (class docstring).',
1502
+ errors
1503
+ )
1504
+ check_test(
1505
+ task.input_types,
1506
+ 'Check task input type is set (cls.input_type)',
1507
+ 'Task has no input_type attribute.',
1508
+ warnings,
1509
+ warn=True
1510
+ )
1511
+ check_test(
1512
+ task.output_types,
1513
+ 'Check task output types is set (cls.output_types)',
1514
+ 'Task has no output_types attribute. Consider setting some so that secator can load your task outputs.',
1515
+ warnings,
1516
+ warn=True
1517
+ )
1518
+ check_test(
1519
+ task.install_version,
1520
+ 'Check task install_version is set (cls.install_version)',
1521
+ 'Task has no install_version attribute. Consider setting it to pin the tool version and ensure it does not break in the future.', # noqa: E501
1522
+ warnings,
1523
+ warn=True
1524
+ )
1525
+
1526
+ if not check:
1527
+
1528
+ # Run unit tests
1529
+ cmd = f'secator test unit --tasks {name}'
1530
+ ret_code = run_test(cmd, exit=False, verbose=verbose)
1531
+ check_test(
1532
+ ret_code == 0,
1533
+ 'Check unit tests pass',
1534
+ 'Unit tests failed.',
1535
+ errors
1536
+ )
1537
+
1538
+ # Run integration tests
1539
+ cmd = f'secator test integration --tasks {name}'
1540
+ ret_code = run_test(cmd, exit=False, verbose=verbose)
1541
+ check_test(
1542
+ ret_code == 0,
1543
+ 'Check integration tests pass',
1544
+ 'Integration tests failed.',
1545
+ errors
1546
+ )
1547
+
1548
+ # Exit with exit code
1549
+ exit_code = 1 if len(errors) > 0 else 0
1550
+ if exit_code == 0:
1551
+ console.print(f':tada: Task {name} tests passed !', style='bold green')
1552
+ else:
1553
+ console.print('\n[bold gold3]Errors:[/]')
1554
+ for error in errors:
1555
+ console.print(error)
1556
+ console.print(Error(message=f'Task {name} tests failed. Please fix the issues above.'))
1557
+
1558
+ if warnings:
1559
+ console.print('\n[bold gold3]Warnings:[/]')
1560
+ for warning in warnings:
1561
+ console.print(warning)
1562
+
1563
+ console.print("\n")
1564
+ sys.exit(exit_code)
1565
+
1566
+
1567
+ def check_test(condition, message, fail_message, results=[], warn=False):
1568
+ console.print(f'[bold magenta]:zap: {message} ...[/]', end='')
1569
+ if not condition:
1570
+ if not warn:
1571
+ error = Error(message=fail_message)
1572
+ console.print(' [bold red]FAILED[/]', style='dim')
1573
+ results.append(error)
1574
+ else:
1575
+ warning = Warning(message=fail_message)
1576
+ console.print(' [bold yellow]WARNING[/]', style='dim')
1577
+ results.append(warning)
1578
+ else:
1579
+ console.print(' [bold green]OK[/]', style='dim')
1580
+ return True
1341
1581
 
1342
1582
 
1343
1583
  @test.command()
secator/config.py CHANGED
@@ -94,6 +94,7 @@ class Runners(StrictModel):
94
94
  skip_cve_low_confidence: bool = False
95
95
  remove_duplicates: bool = False
96
96
  show_chunk_progress: bool = False
97
+ show_command_output: bool = False
97
98
 
98
99
 
99
100
  class Security(StrictModel):
@@ -623,8 +624,8 @@ for name, dir in CONFIG.dirs.items():
623
624
  console.print('[bold green]ok.[/]')
624
625
 
625
626
  # Download wordlists and payloads
626
- download_files(CONFIG.wordlists.templates, CONFIG.dirs.wordlists, CONFIG.offline_mode, 'wordlist')
627
- download_files(CONFIG.payloads.templates, CONFIG.dirs.payloads, CONFIG.offline_mode, 'payload')
627
+ # download_files(CONFIG.wordlists.templates, CONFIG.dirs.wordlists, CONFIG.offline_mode, 'wordlist')
628
+ # download_files(CONFIG.payloads.templates, CONFIG.dirs.payloads, CONFIG.offline_mode, 'payload')
628
629
 
629
630
  # Print config
630
631
  if CONFIG.debug.component == 'config':
@@ -1,7 +1,8 @@
1
1
  type: profile
2
2
  name: aggressive
3
- options:
4
- rate_limit: 100000
5
- delay: 0
6
- proxy: random
7
- user_agent: random
3
+ description: "Internal networks or time-sensitive scans"
4
+ opts:
5
+ rate_limit: 10000
6
+ delay: 0
7
+ timeout: 1
8
+ retries: 1
@@ -1,9 +1,8 @@
1
1
  type: profile
2
2
  name: default
3
- options:
4
- rate_limit: 1000
5
- delay: 1
6
- proxy: null
7
- user_agent: 'Mozilla ...'
8
- nuclei.retries: 5
9
- nuclei.timeout: 15
3
+ description: "General scanning"
4
+ opts:
5
+ rate_limit: 1000
6
+ delay: 0
7
+ timeout: 5
8
+ retries: 3