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 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,17 +34,18 @@ 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']
40
39
  FINDING_TYPES_LOWER = [c.__name__.lower() for c in FINDING_TYPES]
40
+ CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
41
41
 
42
42
 
43
43
  #-----#
44
44
  # CLI #
45
45
  #-----#
46
46
 
47
- @click.group(cls=OrderedGroup, invoke_without_command=True)
47
+
48
+ @click.group(cls=OrderedGroup, invoke_without_command=True, context_settings=CONTEXT_SETTINGS)
48
49
  @click.option('--version', '-version', is_flag=True, default=False)
49
50
  @click.pass_context
50
51
  def cli(ctx, version):
@@ -119,11 +120,12 @@ for config in sorted(ALL_SCANS, key=lambda x: x['name']):
119
120
  @click.option('-Q', '--queue', type=str, default='', help='Listen to a specific queue.')
120
121
  @click.option('-P', '--pool', type=str, default='eventlet', help='Pool implementation.')
121
122
  @click.option('--quiet', is_flag=True, help='Quiet mode.')
123
+ @click.option('--loglevel', type=str, default='INFO', help='Log level.')
122
124
  @click.option('--check', is_flag=True, help='Check if Celery worker is alive.')
123
125
  @click.option('--dev', is_flag=True, help='Start a worker in dev mode (celery multi).')
124
126
  @click.option('--stop', is_flag=True, help='Stop a worker in dev mode (celery multi).')
125
127
  @click.option('--show', is_flag=True, help='Show command (celery multi).')
126
- def worker(hostname, concurrency, reload, queue, pool, quiet, check, dev, stop, show):
128
+ def worker(hostname, concurrency, reload, queue, pool, quiet, loglevel, check, dev, stop, show):
127
129
  """Run a worker."""
128
130
 
129
131
  # Check Celery addon is installed
@@ -169,6 +171,7 @@ def worker(hostname, concurrency, reload, queue, pool, quiet, check, dev, stop,
169
171
 
170
172
  cmd += f' -P {pool}' if pool else ''
171
173
  cmd += f' -c {concurrency}' if concurrency else ''
174
+ cmd += f' -l {loglevel}' if loglevel else ''
172
175
 
173
176
  if reload:
174
177
  patterns = "celery.py;tasks/*.py;runners/*.py;serializers/*.py;output_types/*.py;hooks/*.py;exporters/*.py"
@@ -862,7 +865,7 @@ def health(json, debug, strict):
862
865
  error = False
863
866
  for tool, info in status['tools'].items():
864
867
  if not info['installed']:
865
- console.print(Error(message=f'{tool} not installed and strict mode is enabled.'))
868
+ console.print(Error(message=f'{tool} is not installed.'))
866
869
  error = True
867
870
  if error:
868
871
  sys.exit(1)
@@ -878,21 +881,21 @@ def run_install(title=None, cmd=None, packages=None, next_steps=None):
878
881
  if CONFIG.offline_mode:
879
882
  console.print(Error(message='Cannot run this command in offline mode.'))
880
883
  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)
884
+ # with console.status(f'[bold yellow] Installing {title}...'):
885
+ if cmd:
886
+ from secator.installer import SourceInstaller
887
+ status = SourceInstaller.install(cmd)
888
+ elif packages:
889
+ from secator.installer import PackageInstaller
890
+ status = PackageInstaller.install(packages)
891
+ return_code = 1
892
+ if status.is_ok():
893
+ return_code = 0
894
+ if next_steps:
895
+ console.print('[bold gold3]:wrench: Next steps:[/]')
896
+ for ix, step in enumerate(next_steps):
897
+ console.print(f' :keycap_{ix}: {step}')
898
+ sys.exit(return_code)
896
899
 
897
900
 
898
901
  @cli.group()
@@ -1051,8 +1054,9 @@ def install_ruby():
1051
1054
 
1052
1055
  @install.command('tools')
1053
1056
  @click.argument('cmds', required=False)
1054
- @click.option('--cleanup', is_flag=True, default=False)
1055
- def install_tools(cmds, cleanup):
1057
+ @click.option('--cleanup', is_flag=True, default=False, help='Clean up tools after installation.')
1058
+ @click.option('--fail-fast', is_flag=True, default=False, help='Fail fast if any tool fails to install.')
1059
+ def install_tools(cmds, cleanup, fail_fast):
1056
1060
  """Install supported tools."""
1057
1061
  if CONFIG.offline_mode:
1058
1062
  console.print(Error(message='Cannot run this command in offline mode.'))
@@ -1069,10 +1073,12 @@ def install_tools(cmds, cleanup):
1069
1073
  console.print(Error(message=f'No tools found for {cmd_str}.'))
1070
1074
  return
1071
1075
  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
1076
+ # with console.status(f'[bold yellow][{ix + 1}/{len(tools)}] Installing {cls.__name__} ...'):
1077
+ status = ToolInstaller.install(cls)
1078
+ if not status.is_ok():
1079
+ return_code = 1
1080
+ if fail_fast:
1081
+ sys.exit(return_code)
1076
1082
  console.print()
1077
1083
  if cleanup:
1078
1084
  distro = get_distro_config()
@@ -1132,14 +1138,13 @@ def update(all):
1132
1138
  return_code = 0
1133
1139
  for cls in ALL_TASKS:
1134
1140
  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
1141
+ version_flag = cls.get_version_flag()
1137
1142
  info = get_version_info(cmd, version_flag, cls.install_github_handle)
1138
1143
  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
1144
+ # with console.status(f'[bold yellow]Installing {cls.__name__} ...'):
1145
+ status = ToolInstaller.install(cls)
1146
+ if not status.is_ok():
1147
+ return_code = 1
1143
1148
  sys.exit(return_code)
1144
1149
 
1145
1150
  #-------#
@@ -1250,24 +1255,35 @@ def test():
1250
1255
  pass
1251
1256
 
1252
1257
 
1253
- def run_test(cmd, name):
1258
+ def run_test(cmd, name=None, exit=True, verbose=False):
1254
1259
  """Run a test and return the result.
1255
1260
 
1256
1261
  Args:
1257
- cmd: Command to run.
1258
- name: Name of the test.
1262
+ cmd (str): Command to run.
1263
+ name (str, optional): Name of the test.
1264
+ exit (bool, optional): Exit after running the test with the return code.
1265
+ verbose (bool, optional): Print verbose output.
1266
+
1267
+ Returns:
1268
+ Return code of the test.
1259
1269
  """
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)
1270
+ cmd_name = name + ' tests' if name else 'tests'
1271
+ result = Command.execute(cmd, name=cmd_name, cwd=ROOT_FOLDER, quiet=not verbose)
1272
+ if name:
1273
+ if result.return_code == 0:
1274
+ console.print(f':tada: {name.capitalize()} tests passed !', style='bold green')
1275
+ else:
1276
+ console.print(f':x: {name.capitalize()} tests failed !', style='bold red')
1277
+ if exit:
1278
+ sys.exit(result.return_code)
1279
+ return result.return_code
1264
1280
 
1265
1281
 
1266
1282
  @test.command()
1267
1283
  def lint():
1268
1284
  """Run lint tests."""
1269
1285
  cmd = f'{sys.executable} -m flake8 secator/'
1270
- run_test(cmd, 'lint')
1286
+ run_test(cmd, 'lint', verbose=True)
1271
1287
 
1272
1288
 
1273
1289
  @test.command()
@@ -1285,13 +1301,21 @@ def unit(tasks, workflows, scans, test):
1285
1301
  os.environ['SECATOR_HTTP_STORE_RESPONSES'] = '0'
1286
1302
  os.environ['SECATOR_RUNNERS_SKIP_CVE_SEARCH'] = '1'
1287
1303
 
1304
+ if not test:
1305
+ if tasks:
1306
+ test = 'test_tasks'
1307
+ elif workflows:
1308
+ test = 'test_workflows'
1309
+ elif scans:
1310
+ test = 'test_scans'
1311
+
1288
1312
  import shutil
1289
1313
  shutil.rmtree('/tmp/.secator', ignore_errors=True)
1290
1314
  cmd = f'{sys.executable} -m coverage run --omit="*test*" --data-file=.coverage.unit -m pytest -s -v tests/unit'
1291
1315
  if test:
1292
1316
  test_str = ' or '.join(test.split(','))
1293
1317
  cmd += f' -k "{test_str}"'
1294
- run_test(cmd, 'unit')
1318
+ run_test(cmd, 'unit', verbose=True)
1295
1319
 
1296
1320
 
1297
1321
  @test.command()
@@ -1307,6 +1331,14 @@ def integration(tasks, workflows, scans, test):
1307
1331
  os.environ['SECATOR_DIRS_DATA'] = '/tmp/.secator'
1308
1332
  os.environ['SECATOR_RUNNERS_SKIP_CVE_SEARCH'] = '1'
1309
1333
 
1334
+ if not test:
1335
+ if tasks:
1336
+ test = 'test_tasks'
1337
+ elif workflows:
1338
+ test = 'test_workflows'
1339
+ elif scans:
1340
+ test = 'test_scans'
1341
+
1310
1342
  import shutil
1311
1343
  shutil.rmtree('/tmp/.secator', ignore_errors=True)
1312
1344
 
@@ -1314,7 +1346,7 @@ def integration(tasks, workflows, scans, test):
1314
1346
  if test:
1315
1347
  test_str = ' or '.join(test.split(','))
1316
1348
  cmd += f' -k "{test_str}"'
1317
- run_test(cmd, 'integration')
1349
+ run_test(cmd, 'integration', verbose=True)
1318
1350
 
1319
1351
 
1320
1352
  @test.command()
@@ -1337,7 +1369,82 @@ def performance(tasks, workflows, scans, test):
1337
1369
  if test:
1338
1370
  test_str = ' or '.join(test.split(','))
1339
1371
  cmd += f' -k "{test_str}"'
1340
- run_test(cmd, 'performance')
1372
+ run_test(cmd, 'performance', verbose=True)
1373
+
1374
+
1375
+ @test.command()
1376
+ @click.argument('name', type=str)
1377
+ @click.option('--verbose', '-v', is_flag=True, default=False, help='Print verbose output')
1378
+ def task(name, verbose):
1379
+ """Test task."""
1380
+ task = [task for task in ALL_TASKS if task.__name__ == name]
1381
+ warnings = []
1382
+ exit_code = 0
1383
+
1384
+ # Check if task is correctly registered
1385
+ check_error(task, 'Check task is registered', 'Task is not registered. Make sure there is no syntax errors in the task class definition.', warnings) # noqa: E501
1386
+ task = task[0]
1387
+ task_name = task.__name__
1388
+
1389
+ # Run install
1390
+ console.print(f'\n[bold gold3]:wrench: Running install tests for task {name} ...[/]') if verbose else None
1391
+ cmd = f'secator install tools {task_name}'
1392
+ ret_code = Command.execute(cmd, name='install', quiet=not verbose, cwd=ROOT_FOLDER)
1393
+ version_info = task.get_version_info()
1394
+ check_error(version_info['installed'], 'Check task is installed', 'Failed to install command. Fix your installation command.', warnings) # noqa: E501
1395
+ check_error(any(cmd for cmd in [task.install_cmd, task.install_github_handle]), 'Check task installation command is defined', 'Task has no installation command. Please define a `install_cmd` or `install_github_handle` class attribute.', warnings) # noqa: E501
1396
+ check_error(version_info['version'], 'Check task version can be fetched', 'Failed to detect version info. Fix your `version_flag` class attribute.', warnings) # noqa: E501
1397
+
1398
+ # Run task-specific tests
1399
+ console.print(f'\n[bold gold3]:wrench: Running task-specific tests for {name} ...[/]') if verbose else None
1400
+ check_error(task.__doc__, 'Check task description is set (cls.__doc__)', 'Task has no description (class docstring).', warnings) # noqa: E501
1401
+ check_error(task.cmd, 'Check task command is set (cls.cmd)', 'Task has no cmd attribute.', warnings)
1402
+ check_error(task.input_type, 'Check task input type is set (cls.input_type)', 'Task has no input_type attribute.', warnings) # noqa: E501
1403
+ check_error(task.output_types, 'Check task output types is set (cls.output_types)', 'Task has no output_types attribute.', warnings) # noqa: E501
1404
+
1405
+ # Print all warnings
1406
+ exit_code = 1 if len(warnings) > 0 else 0
1407
+ if exit_code == 1:
1408
+ console.print()
1409
+ console.print("[bold red]Issues:[/]")
1410
+ for warning in warnings:
1411
+ console.print(warning)
1412
+ console.print()
1413
+ console.print(Info(message=f'Skipping unit and integration tests for {name} due to previous errors.'))
1414
+ console.print(Error(message=f'Task {name} tests failed. Please fix the issues above before making a PR.'))
1415
+ sys.exit(exit_code)
1416
+
1417
+ # Run unit tests
1418
+ console.print(f'\n[bold gold3]:wrench: Running unit tests for {name} ...[/]') if verbose else None
1419
+ cmd = f'secator test unit --tasks {name}'
1420
+ ret_code = run_test(cmd, exit=False, verbose=verbose)
1421
+ check_error(ret_code == 0, 'Check unit tests pass', 'Unit tests failed.', warnings)
1422
+
1423
+ # Run integration tests
1424
+ console.print(f'\n[bold gold3]:wrench: Running integration tests for {name} ...[/]') if verbose else None
1425
+ cmd = f'secator test integration --tasks {name}'
1426
+ ret_code = run_test(cmd, exit=False, verbose=verbose)
1427
+ check_error(ret_code == 0, 'Check integration tests pass', 'Integration tests failed.', warnings)
1428
+
1429
+ # Exit with exit code
1430
+ exit_code = 1 if len(warnings) > 0 else 0
1431
+ if exit_code == 0:
1432
+ console.print(f':tada: Task {name} tests passed ! You are free to make a PR.', style='bold green')
1433
+ else:
1434
+ console.print(Error(message=f'Task {name} tests failed. Please fix the issues above before making a PR.'))
1435
+
1436
+ sys.exit(exit_code)
1437
+
1438
+
1439
+ def check_error(condition, message, error, warnings=[]):
1440
+ console.print(f'[bold magenta]:zap: {message} ...[/]', end='')
1441
+ if not condition:
1442
+ warning = Warning(message=error)
1443
+ warnings.append(warning)
1444
+ console.print(' [bold red]FAILED[/]', style='dim')
1445
+ else:
1446
+ console.print(' [bold green]OK[/]', style='dim')
1447
+ return True
1341
1448
 
1342
1449
 
1343
1450
  @test.command()
@@ -0,0 +1,23 @@
1
+ type: workflow
2
+ name: url_params_fuzz
3
+ alias: url_params_fuzz
4
+ description: Extract parameters from an URL and fuzz them
5
+ tags: [http, fuzz]
6
+ input_types:
7
+ - url
8
+ tasks:
9
+ arjun:
10
+ description: Extract parameters from URLs
11
+ ffuf:
12
+ description: Fuzz URL params
13
+ wordlist: https://raw.githubusercontent.com/danielmiessler/SecLists/refs/heads/master/Discovery/Web-Content/burp-parameter-names.txt
14
+ targets_:
15
+ - type: url
16
+ field: url
17
+ condition: item._source.startswith('arjun')
18
+ httpx:
19
+ description: Probe fuzzed URLs
20
+ targets_:
21
+ - type: url
22
+ field: url
23
+ condition: item._source.startswith('ffuf')
@@ -11,4 +11,7 @@ tasks:
11
11
  description: Nuclei Wordpress scan
12
12
  tags: wordpress
13
13
  wpscan:
14
- description: WPScan
14
+ description: WPScan
15
+ wpprobe:
16
+ description: WPProbe
17
+ tags: wordpress
secator/decorators.py CHANGED
@@ -19,16 +19,17 @@ RUNNER_OPTS = {
19
19
  'print_stat': {'is_flag': True, 'short': 'stat', 'default': False, 'help': 'Print runtime statistics'},
20
20
  'print_format': {'default': '', 'short': 'fmt', 'help': 'Output formatting string'},
21
21
  'enable_profiler': {'is_flag': True, 'short': 'prof', 'default': False, 'help': 'Enable runner profiling'},
22
- 'show': {'is_flag': True, 'default': False, 'help': 'Show command that will be run (tasks only)'},
23
- 'no_process': {'is_flag': True, 'default': False, 'help': 'Disable secator processing'},
22
+ 'show': {'is_flag': True, 'short': 'sh', 'default': False, 'help': 'Show command that will be run (tasks only)'},
23
+ 'no_process': {'is_flag': True, 'short': 'nps', 'default': False, 'help': 'Disable secator processing'},
24
24
  # 'filter': {'default': '', 'short': 'f', 'help': 'Results filter', 'short': 'of'}, # TODO add this
25
- 'quiet': {'is_flag': True, 'default': False, 'help': 'Enable quiet mode'},
25
+ 'quiet': {'is_flag': True, 'short': 'q', 'default': False, 'help': 'Enable quiet mode'},
26
+ 'dry_run': {'is_flag': True, 'short': 'dr', 'default': False, 'help': 'Enable dry run'},
26
27
  }
27
28
 
28
29
  RUNNER_GLOBAL_OPTS = {
29
30
  'sync': {'is_flag': True, 'help': 'Run tasks synchronously (automatic if no worker is alive)'},
30
31
  'worker': {'is_flag': True, 'default': False, 'help': 'Run tasks in worker'},
31
- 'no_poll': {'is_flag': True, 'default': False, 'help': 'Do not live poll for tasks results when running in worker'},
32
+ 'no_poll': {'is_flag': True, 'short': 'np', 'default': False, 'help': 'Do not live poll for tasks results when running in worker'}, # noqa: E501
32
33
  'proxy': {'type': str, 'help': 'HTTP proxy'},
33
34
  'driver': {'type': str, 'help': 'Export real-time results. E.g: "mongodb"'}
34
35
  # 'debug': {'type': int, 'default': 0, 'help': 'Debug mode'},
@@ -224,12 +225,16 @@ def decorate_command_options(opts):
224
225
  for opt_name, opt_conf in reversed_opts.items():
225
226
  conf = opt_conf.copy()
226
227
  short_opt = conf.pop('short', None)
227
- conf.pop('internal', None)
228
+ internal = conf.pop('internal', False)
229
+ display = conf.pop('display', True)
230
+ if internal and not display:
231
+ continue
228
232
  conf.pop('prefix', None)
229
233
  conf.pop('shlex', None)
230
234
  conf.pop('meta', None)
231
235
  conf.pop('supported', None)
232
236
  conf.pop('process', None)
237
+ conf.pop('requires_sudo', None)
233
238
  reverse = conf.pop('reverse', False)
234
239
  long = f'--{opt_name}'
235
240
  short = f'-{short_opt}' if short_opt else f'-{opt_name}'
secator/definitions.py CHANGED
@@ -107,6 +107,9 @@ TAGS = 'tags'
107
107
  WEBSERVER = 'webserver'
108
108
  WORDLIST = 'wordlist'
109
109
  WORDS = 'words'
110
+ CERTIFICATE_STATUS_UNKNOWN = 'Unknown'
111
+ CERTIFICATE_STATUS_TRUSTED = 'Trusted'
112
+ CERTIFICATE_STATUS_REVOKED = 'Revoked'
110
113
 
111
114
 
112
115
  def is_importable(module_to_import):
secator/installer.py CHANGED
@@ -11,6 +11,7 @@ import io
11
11
  from dataclasses import dataclass
12
12
  from datetime import datetime
13
13
  from enum import Enum
14
+ from pathlib import Path
14
15
 
15
16
  import json
16
17
  import requests
@@ -227,10 +228,10 @@ class GithubInstaller:
227
228
  return InstallerStatus.GITHUB_LATEST_RELEASE_NOT_FOUND
228
229
 
229
230
  # Find the right asset to download
230
- os_identifiers, arch_identifiers = cls._get_platform_identifier()
231
+ system, arch, os_identifiers, arch_identifiers = cls._get_platform_identifier()
231
232
  download_url = cls._find_matching_asset(latest_release['assets'], os_identifiers, arch_identifiers)
232
233
  if not download_url:
233
- console.print(Error(message='Could not find a GitHub release matching distribution.'))
234
+ console.print(Error(message=f'Could not find a GitHub release matching distribution (system: {system}, arch: {arch}).')) # noqa: E501
234
235
  return InstallerStatus.GITHUB_RELEASE_NOT_FOUND
235
236
 
236
237
  # Download and unpack asset
@@ -261,6 +262,8 @@ class GithubInstaller:
261
262
  return latest_release
262
263
  except requests.RequestException as e:
263
264
  console.print(Warning(message=f'Failed to fetch latest release for {github_handle}: {str(e)}'))
265
+ if 'rate limit exceeded' in str(e):
266
+ console.print(Warning(message='Consider setting env variable SECATOR_CLI_GITHUB_TOKEN or use secator config set cli.github_token $TOKEN.')) # noqa: E501
264
267
  return None
265
268
 
266
269
  @classmethod
@@ -285,16 +288,16 @@ class GithubInstaller:
285
288
 
286
289
  # Enhanced architecture mapping to avoid conflicts
287
290
  arch_mapping = {
288
- 'x86_64': ['amd64', 'x86_64'],
289
- 'amd64': ['amd64', 'x86_64'],
291
+ 'x86_64': ['amd64', 'x86_64', '64bit', 'x64'],
292
+ 'amd64': ['amd64', 'x86_64', '64bit', 'x64'],
290
293
  'aarch64': ['arm64', 'aarch64'],
291
294
  'armv7l': ['armv7', 'arm'],
292
- '386': ['386', 'x86', 'i386'],
295
+ '386': ['386', 'x86', 'i386', '32bit', 'x32'],
293
296
  }
294
297
 
295
298
  os_identifiers = os_mapping.get(system, [])
296
299
  arch_identifiers = arch_mapping.get(arch, [])
297
- return os_identifiers, arch_identifiers
300
+ return system, arch, os_identifiers, arch_identifiers
298
301
 
299
302
  @classmethod
300
303
  def _find_matching_asset(cls, assets, os_identifiers, arch_identifiers):
@@ -348,6 +351,9 @@ class GithubInstaller:
348
351
  elif url.endswith('.tar.gz'):
349
352
  with tarfile.open(fileobj=io.BytesIO(response.content), mode='r:gz') as tar:
350
353
  tar.extractall(path=temp_dir)
354
+ else:
355
+ with Path(f'{temp_dir}/{repo_name}').open('wb') as f:
356
+ f.write(response.content)
351
357
 
352
358
  # For archives, find and move the binary that matches the repo name
353
359
  binary_path = cls._find_binary_in_directory(temp_dir, repo_name)
@@ -397,13 +403,11 @@ def get_version(version_cmd):
397
403
  import re
398
404
  regex = r'[0-9]+\.[0-9]+\.?[0-9]*\.?[a-zA-Z]*'
399
405
  ret = Command.execute(version_cmd, quiet=True, print_errors=False)
400
- return_code = ret.return_code
401
- if not return_code == 0:
402
- return '', ret.return_code
403
406
  match = re.findall(regex, ret.output)
404
407
  if not match:
405
- return '', return_code
406
- return match[0], return_code
408
+ console.print(Warning(message=f'Failed to find version in version command output. Command: {version_cmd}; Output: {ret.output}; Return code: {ret.return_code}')) # noqa: E501
409
+ return None
410
+ return match[0]
407
411
 
408
412
 
409
413
  def parse_version(ver):
@@ -436,14 +440,21 @@ def get_version_info(name, version_flag=None, install_github_handle=None, instal
436
440
  'name': name,
437
441
  'installed': False,
438
442
  'version': version,
443
+ 'version_cmd': None,
439
444
  'latest_version': None,
440
445
  'location': None,
441
- 'status': ''
446
+ 'status': '',
447
+ 'errors': []
442
448
  }
443
449
 
444
450
  # Get binary path
445
451
  location = which(name).output
452
+ if not location or not Path(location).exists():
453
+ info['installed'] = False
454
+ info['status'] = 'missing'
455
+ return info
446
456
  info['location'] = location
457
+ info['installed'] = True
447
458
 
448
459
  # Get latest version
449
460
  latest_version = None
@@ -472,34 +483,35 @@ def get_version_info(name, version_flag=None, install_github_handle=None, instal
472
483
  if ver:
473
484
  latest_version = str(ver)
474
485
  info['latest_version'] = latest_version
486
+ else:
487
+ error = f'Failed to get latest version for {name}. Command: apt-cache madison {name}'
488
+ info['errors'].append(error)
489
+ console.print(Warning(message=error))
475
490
 
476
491
  # Get current version
477
- version_ret = 1
478
492
  version_flag = None if version_flag == OPT_NOT_SUPPORTED else version_flag
479
493
  if version_flag:
480
494
  version_cmd = f'{name} {version_flag}'
481
- version, version_ret = get_version(version_cmd)
495
+ info['version_cmd'] = version_cmd
496
+ version = get_version(version_cmd)
482
497
  info['version'] = version
483
- if version_ret != 0: # version command error
484
- info['installed'] = False
485
- info['status'] = 'missing'
498
+ if not version:
499
+ info['errors'].append(f'Error fetching version for command. Version command: {version_cmd}')
500
+ info['status'] = 'version fetch error'
486
501
  return info
487
502
 
488
- if location:
489
- info['installed'] = True
490
- if version and latest_version:
491
- if parse_version(version) < parse_version(latest_version):
492
- info['status'] = 'outdated'
493
- else:
494
- info['status'] = 'latest'
495
- elif not version:
496
- info['status'] = 'current unknown'
497
- elif not latest_version:
498
- info['status'] = 'latest unknown'
499
- if CONFIG.offline_mode:
500
- info['status'] += r' [dim orange1]\[offline][/]'
501
- else:
502
- info['status'] = 'missing'
503
+ # Check if up-to-date
504
+ if version and latest_version:
505
+ if parse_version(version) < parse_version(latest_version):
506
+ info['status'] = 'outdated'
507
+ else:
508
+ info['status'] = 'latest'
509
+ elif not version:
510
+ info['status'] = 'current unknown'
511
+ elif not latest_version:
512
+ info['status'] = 'latest unknown'
513
+ if CONFIG.offline_mode:
514
+ info['status'] += r' [dim orange1]\[offline][/]'
503
515
 
504
516
  return info
505
517
 
@@ -578,6 +590,8 @@ def fmt_health_table_row(version_info, category=None):
578
590
  _version = '[bold red]missing[/]'
579
591
  elif status == 'ok':
580
592
  _version = '[bold green]ok [/]'
593
+ elif status == 'version fetch error':
594
+ _version = '[bold orange1]unknown[/] [dim](current unknown)[/]'
581
595
  elif status:
582
596
  if not version and installed:
583
597
  _version = '[bold green]ok [/]'
@@ -26,6 +26,7 @@ from secator.output_types.url import Url
26
26
  from secator.output_types.user_account import UserAccount
27
27
  from secator.output_types.vulnerability import Vulnerability
28
28
  from secator.output_types.record import Record
29
+ from secator.output_types.certificate import Certificate
29
30
  from secator.output_types.info import Info
30
31
  from secator.output_types.warning import Warning
31
32
  from secator.output_types.error import Error
@@ -39,6 +40,6 @@ STAT_TYPES = [
39
40
  Stat
40
41
  ]
41
42
  FINDING_TYPES = [
42
- Subdomain, Ip, Port, Url, Tag, Exploit, UserAccount, Vulnerability
43
+ Subdomain, Ip, Port, Url, Tag, Exploit, UserAccount, Vulnerability, Certificate
43
44
  ]
44
45
  OUTPUT_TYPES = FINDING_TYPES + EXECUTION_TYPES + STAT_TYPES