Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions cyberai/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import os

import click

from cyberai.version import __version__
from rich.console import Console
from rich.panel import Panel

Expand Down Expand Up @@ -55,6 +57,11 @@ 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,
plan_web_order: bool | None = None,
planned_redteam: bool | None = None,
) -> CyberAIConfig:
"""Apply CLI feature-flag overrides onto a config built from the env.

Expand All @@ -74,6 +81,16 @@ 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
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


Expand All @@ -91,6 +108,7 @@ def _apply_feature_overrides(


@click.group()
@click.version_option(__version__, "-V", "--version", prog_name="cyberai")
def cli() -> None:
"""CyberAI — AI-powered pentest platform."""

Expand Down Expand Up @@ -135,6 +153,31 @@ 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",
)
@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,
Expand All @@ -150,6 +193,11 @@ def scan(
replan: bool | None,
planner: bool | None,
air_gapped: bool | None,
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()
Expand Down Expand Up @@ -177,6 +225,11 @@ def scan(
replan=replan,
planner=planner,
air_gapped=air_gapped,
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:
Expand Down
88 changes: 88 additions & 0 deletions tests/unit/test_cli_feature_flags.py
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions tests/unit/test_cli_version.py
Original file line number Diff line number Diff line change
@@ -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
Loading