secator 0.10.1a12__py3-none-any.whl → 0.11.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 +10 -5
- secator/celery_signals.py +2 -11
- secator/cli.py +153 -46
- secator/config.py +2 -2
- secator/configs/workflows/url_params_fuzz.yaml +23 -0
- secator/configs/workflows/wordpress.yaml +4 -1
- secator/decorators.py +10 -5
- secator/definitions.py +3 -0
- secator/installer.py +46 -32
- secator/output_types/__init__.py +2 -1
- secator/output_types/certificate.py +78 -0
- secator/output_types/user_account.py +1 -1
- secator/rich.py +1 -1
- secator/runners/_base.py +14 -6
- secator/runners/_helpers.py +4 -3
- secator/runners/command.py +81 -21
- secator/runners/scan.py +5 -3
- secator/runners/workflow.py +22 -4
- secator/tasks/_categories.py +12 -4
- secator/tasks/arjun.py +82 -0
- secator/tasks/ffuf.py +2 -1
- secator/tasks/fping.py +1 -0
- secator/tasks/gitleaks.py +76 -0
- secator/tasks/grype.py +1 -1
- secator/tasks/mapcidr.py +1 -1
- secator/tasks/naabu.py +7 -1
- secator/tasks/nmap.py +29 -29
- secator/tasks/subfinder.py +1 -1
- secator/tasks/testssl.py +274 -0
- secator/tasks/trivy.py +95 -0
- secator/tasks/wafw00f.py +83 -0
- secator/tasks/wpprobe.py +94 -0
- secator/template.py +49 -67
- secator/utils.py +16 -7
- secator/utils_test.py +26 -8
- {secator-0.10.1a12.dist-info → secator-0.11.1.dist-info}/METADATA +1 -1
- {secator-0.10.1a12.dist-info → secator-0.11.1.dist-info}/RECORD +40 -32
- {secator-0.10.1a12.dist-info → secator-0.11.1.dist-info}/WHEEL +0 -0
- {secator-0.10.1a12.dist-info → secator-0.11.1.dist-info}/entry_points.txt +0 -0
- {secator-0.10.1a12.dist-info → secator-0.11.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.
|
|
239
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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,
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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.
|
|
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
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
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
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
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()
|
secator/config.py
CHANGED
|
@@ -623,8 +623,8 @@ for name, dir in CONFIG.dirs.items():
|
|
|
623
623
|
console.print('[bold green]ok.[/]')
|
|
624
624
|
|
|
625
625
|
# 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')
|
|
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')
|
|
628
628
|
|
|
629
629
|
# Print config
|
|
630
630
|
if CONFIG.debug.component == 'config':
|
|
@@ -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')
|
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',
|
|
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
|
-
|
|
406
|
-
|
|
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
|
-
|
|
495
|
+
info['version_cmd'] = version_cmd
|
|
496
|
+
version = get_version(version_cmd)
|
|
482
497
|
info['version'] = version
|
|
483
|
-
if
|
|
484
|
-
info['
|
|
485
|
-
info['status'] = '
|
|
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
|
|
489
|
-
|
|
490
|
-
if version
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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 [/]'
|