From e2eeecd23f35bd5accb060400a7e2cdffb0033a9 Mon Sep 17 00:00:00 2001 From: Evgeny Kiriyak <224408464+evkir@users.noreply.github.com> Date: Thu, 30 Jul 2026 20:25:02 +0200 Subject: [PATCH 1/4] feat(cli): report the package version with --version --- cyberai/__main__.py | 3 +++ tests/unit/test_cli_version.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 tests/unit/test_cli_version.py diff --git a/cyberai/__main__.py b/cyberai/__main__.py index 8117f68..7d6f62c 100644 --- a/cyberai/__main__.py +++ b/cyberai/__main__.py @@ -5,6 +5,8 @@ import os import click + +from cyberai.version import __version__ from rich.console import Console from rich.panel import Panel @@ -91,6 +93,7 @@ def _apply_feature_overrides( @click.group() +@click.version_option(__version__, "-V", "--version", prog_name="cyberai") def cli() -> None: """CyberAI — AI-powered pentest platform.""" diff --git a/tests/unit/test_cli_version.py b/tests/unit/test_cli_version.py new file mode 100644 index 0000000..d812f32 --- /dev/null +++ b/tests/unit/test_cli_version.py @@ -0,0 +1,19 @@ +"""The CLI reports its version, and reports the one the package declares.""" + +from __future__ import annotations + +from click.testing import CliRunner + +from cyberai.__main__ import cli +from cyberai.version import __version__ + + +def test_version_option_prints_the_package_version(): + result = CliRunner().invoke(cli, ["--version"]) + assert result.exit_code == 0 + # A hardcoded string here would pass a bump it never followed. + assert __version__ in result.output + + +def test_short_version_flag_works(): + assert CliRunner().invoke(cli, ["-V"]).exit_code == 0 From df94e2d56a130ea34072b71495b6922e2bea80e6 Mon Sep 17 00:00:00 2001 From: Evgeny Kiriyak <224408464+evkir@users.noreply.github.com> Date: Thu, 30 Jul 2026 20:26:45 +0200 Subject: [PATCH 2/4] feat(cli): web recon and exploitation feature flags --- cyberai/__main__.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/cyberai/__main__.py b/cyberai/__main__.py index 7d6f62c..11be46b 100644 --- a/cyberai/__main__.py +++ b/cyberai/__main__.py @@ -57,6 +57,9 @@ def _apply_feature_overrides( replan: bool | None = None, planner: bool | None = None, air_gapped: bool | None = None, + web_recon: bool | None = None, + web_exploit: bool | None = None, + api_discovery: bool | None = None, ) -> CyberAIConfig: """Apply CLI feature-flag overrides onto a config built from the env. @@ -76,6 +79,12 @@ def _apply_feature_overrides( config.enable_planner = planner if air_gapped is not None: config.air_gapped = air_gapped + if web_recon is not None: + config.use_web_recon = web_recon + if web_exploit is not None: + config.use_web_exploit = web_exploit + if api_discovery is not None: + config.use_api_discovery = api_discovery return config @@ -138,6 +147,21 @@ def cli() -> None: default=None, help="Force local-only (no-egress) LLM path on or off", ) +@click.option( + "--web-recon/--no-web-recon", + default=None, + help="Force crawling the target for injectable HTTP endpoints on or off", +) +@click.option( + "--web-exploit/--no-web-exploit", + default=None, + help="Force direct exploitation of the discovered HTTP surface on or off", +) +@click.option( + "--api-discovery/--no-api-discovery", + default=None, + help="Force reading API specs and JS bundles during web recon on or off", +) def scan( target: str, verbose: bool, @@ -153,6 +177,9 @@ def scan( replan: bool | None, planner: bool | None, air_gapped: bool | None, + web_recon: bool | None, + web_exploit: bool | None, + api_discovery: bool | None, ) -> None: """Run full pentest pipeline against TARGET.""" _detach_stdin_from_tty() @@ -180,6 +207,9 @@ def scan( replan=replan, planner=planner, air_gapped=air_gapped, + web_recon=web_recon, + web_exploit=web_exploit, + api_discovery=api_discovery, ) if max_rps: From 476e30b6be8cc35415962bd5c0a645fdf63a7a92 Mon Sep 17 00:00:00 2001 From: Evgeny Kiriyak <224408464+evkir@users.noreply.github.com> Date: Thu, 30 Jul 2026 20:28:37 +0200 Subject: [PATCH 3/4] feat(cli): plan-driven ordering and red-team feature flags --- cyberai/__main__.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/cyberai/__main__.py b/cyberai/__main__.py index 11be46b..bb9144b 100644 --- a/cyberai/__main__.py +++ b/cyberai/__main__.py @@ -60,6 +60,8 @@ def _apply_feature_overrides( web_recon: bool | None = None, web_exploit: bool | None = None, api_discovery: bool | None = None, + plan_web_order: bool | None = None, + planned_redteam: bool | None = None, ) -> CyberAIConfig: """Apply CLI feature-flag overrides onto a config built from the env. @@ -85,6 +87,10 @@ def _apply_feature_overrides( config.use_web_exploit = web_exploit if api_discovery is not None: config.use_api_discovery = api_discovery + if plan_web_order is not None: + config.use_plan_web_order = plan_web_order + if planned_redteam is not None: + config.use_planned_redteam = planned_redteam return config @@ -162,6 +168,16 @@ def cli() -> None: default=None, help="Force reading API specs and JS bundles during web recon on or off", ) +@click.option( + "--plan-web-order/--no-plan-web-order", + default=None, + help="Force attacking the endpoints the planner named first on or off", +) +@click.option( + "--planned-redteam/--no-planned-redteam", + default=None, + help="Force fuzzing the LLM channels the planner named on or off", +) def scan( target: str, verbose: bool, @@ -180,6 +196,8 @@ def scan( web_recon: bool | None, web_exploit: bool | None, api_discovery: bool | None, + plan_web_order: bool | None, + planned_redteam: bool | None, ) -> None: """Run full pentest pipeline against TARGET.""" _detach_stdin_from_tty() @@ -210,6 +228,8 @@ def scan( web_recon=web_recon, web_exploit=web_exploit, api_discovery=api_discovery, + plan_web_order=plan_web_order, + planned_redteam=planned_redteam, ) if max_rps: From 1c16930a90c6a0d87329dcd6992c7d2306d4b404 Mon Sep 17 00:00:00 2001 From: Evgeny Kiriyak <224408464+evkir@users.noreply.github.com> Date: Thu, 30 Jul 2026 20:29:50 +0200 Subject: [PATCH 4/4] test(cli): tri-state feature flags and precedence over the environment --- tests/unit/test_cli_feature_flags.py | 88 ++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/unit/test_cli_feature_flags.py diff --git a/tests/unit/test_cli_feature_flags.py b/tests/unit/test_cli_feature_flags.py new file mode 100644 index 0000000..b4ad713 --- /dev/null +++ b/tests/unit/test_cli_feature_flags.py @@ -0,0 +1,88 @@ +"""CLI feature flags: tri-state semantics and precedence over the environment. + +Every one of these flags gates a capability that is off by default, so a +silent wiring mistake does not fail anything -- it just means the feature +never runs and no test notices. The env-precedence cases matter most: the +bench builds its config with `from_env`, so a CLI flag that clobbered an +unset value would switch a capability off for runs that never passed a flag. +""" + +from __future__ import annotations + +import pytest +from click.testing import CliRunner + +from cyberai.__main__ import _apply_feature_overrides, scan +from cyberai.core.config import CyberAIConfig + +# (CLI keyword, config attribute, environment variable) +FLAGS = [ + ("web_recon", "use_web_recon", "CYBERAI_USE_WEB_RECON"), + ("web_exploit", "use_web_exploit", "CYBERAI_USE_WEB_EXPLOIT"), + ("api_discovery", "use_api_discovery", "CYBERAI_USE_API_DISCOVERY"), + ("plan_web_order", "use_plan_web_order", "CYBERAI_USE_PLAN_WEB_ORDER"), + ("planned_redteam", "use_planned_redteam", "CYBERAI_USE_PLANNED_REDTEAM"), +] + +IDS = [f[0] for f in FLAGS] + + +@pytest.mark.parametrize("kw,attr,_env", FLAGS, ids=IDS) +def test_flag_forces_on(kw, attr, _env): + cfg = _apply_feature_overrides(CyberAIConfig(), **{kw: True}) + assert getattr(cfg, attr) is True + + +@pytest.mark.parametrize("kw,attr,_env", FLAGS, ids=IDS) +def test_flag_forces_off(kw, attr, _env): + cfg = CyberAIConfig() + setattr(cfg, attr, True) + assert getattr(_apply_feature_overrides(cfg, **{kw: False}), attr) is False + + +@pytest.mark.parametrize("kw,attr,_env", FLAGS, ids=IDS) +def test_flag_none_leaves_the_env_value(kw, attr, _env): + cfg = CyberAIConfig() + setattr(cfg, attr, True) + assert getattr(_apply_feature_overrides(cfg, **{kw: None}), attr) is True + + +@pytest.mark.parametrize("kw,attr,env", FLAGS, ids=IDS) +def test_env_survives_when_no_flag_is_passed(kw, attr, env, monkeypatch): + monkeypatch.setenv(env, "1") + cfg = _apply_feature_overrides(CyberAIConfig.from_env()) + assert getattr(cfg, attr) is True + + +@pytest.mark.parametrize("kw,attr,env", FLAGS, ids=IDS) +def test_flag_overrides_the_env(kw, attr, env, monkeypatch): + monkeypatch.setenv(env, "1") + cfg = _apply_feature_overrides(CyberAIConfig.from_env(), **{kw: False}) + assert getattr(cfg, attr) is False + + +def test_overriding_one_flag_leaves_the_others_alone(): + cfg = _apply_feature_overrides(CyberAIConfig(), web_recon=True) + assert cfg.use_web_recon is True + assert cfg.use_web_exploit is False + assert cfg.use_api_discovery is False + assert cfg.use_plan_web_order is False + assert cfg.use_planned_redteam is False + + +@pytest.mark.parametrize("kw,_attr,_env", FLAGS, ids=IDS) +def test_flag_pair_is_exposed_in_help(kw, _attr, _env): + out = CliRunner().invoke(scan, ["--help"]).output + name = kw.replace("_", "-") + assert f"--{name}" in out + assert f"--no-{name}" in out + + +def test_scan_accepts_every_new_flag(tmp_path, monkeypatch): + """A flag the command signature forgot would fail here, not in the field.""" + monkeypatch.chdir(tmp_path) + args = ["example.com", "--dry-run"] + for kw, _attr, _env in FLAGS: + args.append(f"--{kw.replace('_', '-')}") + result = CliRunner().invoke(scan, args) + assert result.exit_code == 0