diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ea4bd512..32466472b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [0.12.0](https://github.com/freelabz/secator/compare/v0.11.1...v0.12.0) (2025-04-24) + + +### Features + +* add --show option to display yaml ([#601](https://github.com/freelabz/secator/issues/601)) ([8edffe9](https://github.com/freelabz/secator/commit/8edffe9f879bb7a99a7e5fead1a18dbf6da5d140)) +* **arjun:** add --disable-redirects opts ([#598](https://github.com/freelabz/secator/issues/598)) ([17618c7](https://github.com/freelabz/secator/commit/17618c7ca647536207a96ce2c235ec5227db2cef)) +* **workflows:** improve url params fuzz workflow ([#597](https://github.com/freelabz/secator/issues/597)) ([38fa1bc](https://github.com/freelabz/secator/commit/38fa1bcd76c9c6dbe0bc85c9c8c707ae642b9830)) + + +### Bug Fixes + +* better options overrides for CLI ([#596](https://github.com/freelabz/secator/issues/596)) ([74e256b](https://github.com/freelabz/secator/commit/74e256b434d36f1e16390f8d06571e726517f5ce)) +* worker quiet option ([#602](https://github.com/freelabz/secator/issues/602)) ([9b2c084](https://github.com/freelabz/secator/commit/9b2c08458e3412a89ec39547a728a26885fe1162)) + ## [0.11.1](https://github.com/freelabz/secator/compare/v0.11.0...v0.11.1) (2025-04-23) diff --git a/pyproject.toml b/pyproject.toml index e02a1914c..23b144bb2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = 'hatchling.build' [project] name = 'secator' -version = "0.11.1" +version = "0.12.0" authors = [{ name = 'FreeLabz', email = 'sales@freelabz.com' }] readme = 'README.md' description = "The pentester's swiss knife." diff --git a/secator/cli.py b/secator/cli.py index 67555993d..851800fa5 100644 --- a/secator/cli.py +++ b/secator/cli.py @@ -119,7 +119,7 @@ def scan(ctx): @click.option('-r', '--reload', is_flag=True, help='Autoreload Celery on code changes.') @click.option('-Q', '--queue', type=str, default='', help='Listen to a specific queue.') @click.option('-P', '--pool', type=str, default='eventlet', help='Pool implementation.') -@click.option('--quiet', is_flag=True, help='Quiet mode.') +@click.option('--quiet', is_flag=True, default=False, help='Quiet mode.') @click.option('--loglevel', type=str, default='INFO', help='Log level.') @click.option('--check', is_flag=True, help='Check if Celery worker is alive.') @click.option('--dev', is_flag=True, help='Start a worker in dev mode (celery multi).') diff --git a/secator/config.py b/secator/config.py index 32185fc05..29aaec90d 100644 --- a/secator/config.py +++ b/secator/config.py @@ -94,6 +94,7 @@ class Runners(StrictModel): skip_cve_low_confidence: bool = False remove_duplicates: bool = False show_chunk_progress: bool = False + show_command_output: bool = False class Security(StrictModel): diff --git a/secator/configs/workflows/url_params_fuzz.yaml b/secator/configs/workflows/url_params_fuzz.yaml index a30cd3283..1f7e3da5e 100644 --- a/secator/configs/workflows/url_params_fuzz.yaml +++ b/secator/configs/workflows/url_params_fuzz.yaml @@ -11,6 +11,8 @@ tasks: ffuf: description: Fuzz URL params wordlist: https://raw.githubusercontent.com/danielmiessler/SecLists/refs/heads/master/Discovery/Web-Content/burp-parameter-names.txt + auto_calibration: true + follow_redirect: true targets_: - type: url field: url diff --git a/secator/decorators.py b/secator/decorators.py index 618ced3b6..3569fe87c 100644 --- a/secator/decorators.py +++ b/secator/decorators.py @@ -19,11 +19,11 @@ 'print_stat': {'is_flag': True, 'short': 'stat', 'default': False, 'help': 'Print runtime statistics'}, 'print_format': {'default': '', 'short': 'fmt', 'help': 'Output formatting string'}, 'enable_profiler': {'is_flag': True, 'short': 'prof', 'default': False, 'help': 'Enable runner profiling'}, - 'show': {'is_flag': True, 'short': 'sh', 'default': False, 'help': 'Show command that will be run (tasks only)'}, 'no_process': {'is_flag': True, 'short': 'nps', 'default': False, 'help': 'Disable secator processing'}, # 'filter': {'default': '', 'short': 'f', 'help': 'Results filter', 'short': 'of'}, # TODO add this - 'quiet': {'is_flag': True, 'short': 'q', 'default': False, 'help': 'Enable quiet mode'}, + 'quiet': {'is_flag': True, 'short': 'q', 'default': not CONFIG.runners.show_command_output, 'opposite': 'verbose', 'help': 'Enable quiet mode'}, # noqa: E501 'dry_run': {'is_flag': True, 'short': 'dr', 'default': False, 'help': 'Enable dry run'}, + 'show': {'is_flag': True, 'short': 'yml', 'default': False, 'help': 'Show runner yaml'}, } RUNNER_GLOBAL_OPTS = { @@ -163,35 +163,39 @@ def get_command_options(config): elif opt in RUNNER_GLOBAL_OPTS: prefix = 'Execution' + # Get opt value from YAML config + opt_conf_value = task_config_opts.get(opt) + # Get opt conf conf = opt_conf.copy() + opt_is_flag = conf.get('is_flag', False) + opt_default = conf.get('default', False if opt_is_flag else None) + opt_is_required = conf.get('required', False) conf['show_default'] = True conf['prefix'] = prefix - opt_default = conf.get('default', None) - opt_is_flag = conf.get('is_flag', False) - opt_value_in_config = task_config_opts.get(opt) + conf['default'] = opt_default + conf['reverse'] = False - # Check if opt already defined in config - if opt_value_in_config: - if conf.get('required', False): + # Change CLI opt defaults if opt was overriden in YAML config + if opt_conf_value: + if opt_is_required: debug('OPT (skipped: opt is required and defined in config)', obj={'opt': opt}, sub=f'cli.{config.name}', verbose=True) # noqa: E501 continue mapped_value = cls.opt_value_map.get(opt) if callable(mapped_value): - opt_value_in_config = mapped_value(opt_value_in_config) + opt_conf_value = mapped_value(opt_conf_value) elif mapped_value: - opt_value_in_config = mapped_value - if opt_value_in_config != opt_default: + opt_conf_value = mapped_value + + # Handle option defaults + if opt_conf_value != opt_default: if opt in opt_cache: continue if opt_is_flag: - conf['reverse'] = True - conf['default'] = not conf['default'] - # print(f'{opt}: change default to {opt_value_in_config}') - conf['default'] = opt_value_in_config + conf['default'] = opt_default = opt_conf_value - # If opt is a flag but the default is True, add opposite flag - if opt_is_flag and opt_default is True: + # Add reverse flag + if opt_default is True: conf['reverse'] = True # Check if opt already processed before @@ -205,7 +209,7 @@ def get_command_options(config): all_opts[opt] = conf # Debug - debug_conf = OrderedDict({'opt': opt, 'config_val': opt_value_in_config or 'N/A', **conf.copy()}) + debug_conf = OrderedDict({'opt': opt, 'config_val': opt_conf_value or 'N/A', **conf.copy()}) debug('OPT', obj=debug_conf, sub=f'cli.{config.name}', verbose=True) return all_opts @@ -236,11 +240,17 @@ def decorator(f): conf.pop('process', None) conf.pop('requires_sudo', None) reverse = conf.pop('reverse', False) + opposite = conf.pop('opposite', None) long = f'--{opt_name}' short = f'-{short_opt}' if short_opt else f'-{opt_name}' if reverse: - long += f'/--no-{opt_name}' - short += f'/-n{short_opt}' if short else f'/-n{opt_name}' + if opposite: + long += f'/--{opposite}' + short += f'/-{opposite[0]}' + conf['help'] = conf['help'].replace(opt_name, f'{opt_name} / {opposite}') + else: + long += f'/--no-{opt_name}' + short += f'/-n{short_opt}' if short else f'/-n{opt_name}' f = click.option(long, short, **conf)(f) return f return decorator @@ -321,9 +331,16 @@ def func(ctx, **opts): worker = opts.pop('worker') ws = opts.pop('workspace') driver = opts.pop('driver', '') + quiet = opts['quiet'] + dry_run = opts['dry_run'] show = opts['show'] context = {'workspace_name': ws} + # Show runner yaml + if show: + config.print() + sys.exit(0) + # Remove options whose values are default values for k, v in options.items(): opt_name = k.replace('-', '_') @@ -364,7 +381,7 @@ def func(ctx, **opts): hooks = deep_merge_dicts(*hooks) # Enable sync or not - if sync or show: + if sync or dry_run: sync = True else: from secator.celery import is_celery_worker_alive @@ -394,6 +411,7 @@ def func(ctx, **opts): 'piped_output': ctx.obj['piped_output'], 'caller': 'cli', 'sync': sync, + 'quiet': quiet }) # Start runner diff --git a/secator/runners/command.py b/secator/runners/command.py index 632129376..5c2f8b6bd 100644 --- a/secator/runners/command.py +++ b/secator/runners/command.py @@ -372,6 +372,7 @@ def yielder(self): # Abort if dry run if self.dry_run: + self._print('') self.print_command() return diff --git a/secator/tasks/arjun.py b/secator/tasks/arjun.py index a054ed21b..9d8853335 100644 --- a/secator/tasks/arjun.py +++ b/secator/tasks/arjun.py @@ -2,7 +2,8 @@ import yaml from secator.decorators import task -from secator.definitions import (OUTPUT_PATH, RATE_LIMIT, THREADS, DELAY, TIMEOUT, METHOD, WORDLIST, HEADER, URL) +from secator.definitions import (OUTPUT_PATH, RATE_LIMIT, THREADS, DELAY, TIMEOUT, METHOD, WORDLIST, + HEADER, URL, FOLLOW_REDIRECT) from secator.output_types import Info, Url, Warning, Error from secator.runners import Command from secator.tasks._categories import OPTS @@ -31,6 +32,7 @@ class arjun(Command): RATE_LIMIT: OPTS[RATE_LIMIT], METHOD: OPTS[METHOD], HEADER: OPTS[HEADER], + FOLLOW_REDIRECT: OPTS[FOLLOW_REDIRECT], } opt_key_map = { THREADS: 't', @@ -44,6 +46,7 @@ class arjun(Command): 'stable': '--stable', 'passive': '--passive', 'casing': '--casing', + 'follow_redirect': '--follow-redirect', } output_types = [Url] install_cmd = 'pipx install arjun && pipx upgrade arjun' @@ -57,6 +60,11 @@ def on_line(self, line): @staticmethod def on_cmd(self): + follow_redirect = self.get_opt_value(FOLLOW_REDIRECT) + self.cmd = self.cmd.replace(' --follow-redirect', '') + if not follow_redirect: + self.cmd += ' --disable-redirects' + self.output_path = self.get_opt_value(OUTPUT_PATH) if not self.output_path: self.output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.json' diff --git a/secator/template.py b/secator/template.py index d08399b23..b06fdc440 100644 --- a/secator/template.py +++ b/secator/template.py @@ -30,6 +30,7 @@ def __init__(self, input={}, name=None, **kwargs): config = input elif isinstance(input, Path) or Path(input).exists(): config = self._load_from_path(input) + config['_path'] = str(input) elif isinstance(input, str): config = self._load(input) super().__init__(config, **kwargs) @@ -57,6 +58,17 @@ def flat_tasks(self): """Property to access tasks easily.""" return self._extract_tasks() + def print(self): + """Print config as highlighted yaml.""" + config = self.toDict() + _path = config.pop('_path', None) + if _path: + console.print(f'[italic green]{_path}[/]\n') + yaml_str = yaml.dump(config, indent=4) + from rich.syntax import Syntax + yaml_highlight = Syntax(yaml_str, 'yaml', line_numbers=True) + console.print(yaml_highlight) + def _collect_supported_opts(self): """Collect supported options from the tasks extracted from the config.""" tasks = self._extract_tasks()