Files
temp/features/steps/tui_shell_danger_detection_steps.py
freemo 5c49b2134b feat(tui): implement shell danger detection patterns
Implement dangerous shell pattern detection with configurable pattern
registry, danger level classification, and user warning system.

- Domain: ShellDangerLevel enum (LOW, MEDIUM, HIGH, CRITICAL)
- Domain: DangerousPattern value object with regex matching
- Domain: DangerousCommandWarning value object
- Domain: DangerousPatternDetector with configurable pattern registry
- Application: ShellSafetyService with warn_callback and block_level
- Default patterns: rm -rf, fork bomb, dd if=, mkfs, chmod 777,
  sudo rm, wget/curl piped to sh/bash, git push --force
- BDD tests covering all pattern categories, safe commands,
  registry management, and service behavior

ISSUES CLOSED: #1003
2026-04-02 20:48:33 +00:00

396 lines
13 KiB
Python

"""Step definitions for tui_shell_danger_detection.feature.
Tests for:
- ShellDangerLevel enum
- DangerousPattern value object
- DangerousCommandWarning value object
- DangerousPatternDetector domain service
- ShellSafetyService application service
"""
from __future__ import annotations
from behave import given, then, when
from behave.runner import Context
from cleveragents.tui.shell_safety import (
DangerousCommandWarning,
DangerousPattern,
DangerousPatternDetector,
ShellDangerLevel,
ShellSafetyService,
)
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("the shell safety module is imported")
def step_shell_safety_imported(context: Context) -> None:
"""Verify the shell safety module is importable."""
assert ShellDangerLevel is not None
assert DangerousPattern is not None
assert DangerousCommandWarning is not None
assert DangerousPatternDetector is not None
assert ShellSafetyService is not None
# ---------------------------------------------------------------------------
# ShellDangerLevel
# ---------------------------------------------------------------------------
@then("the ShellDangerLevel enum should have levels LOW MEDIUM HIGH CRITICAL")
def step_danger_level_has_four_levels(context: Context) -> None:
"""Verify all four danger levels exist."""
assert hasattr(ShellDangerLevel, "LOW")
assert hasattr(ShellDangerLevel, "MEDIUM")
assert hasattr(ShellDangerLevel, "HIGH")
assert hasattr(ShellDangerLevel, "CRITICAL")
@then("LOW should be less severe than MEDIUM")
def step_low_less_than_medium(context: Context) -> None:
assert ShellDangerLevel.LOW < ShellDangerLevel.MEDIUM
@then("MEDIUM should be less severe than HIGH")
def step_medium_less_than_high(context: Context) -> None:
assert ShellDangerLevel.MEDIUM < ShellDangerLevel.HIGH
@then("HIGH should be less severe than CRITICAL")
def step_high_less_than_critical(context: Context) -> None:
assert ShellDangerLevel.HIGH < ShellDangerLevel.CRITICAL
# ---------------------------------------------------------------------------
# DangerousPattern
# ---------------------------------------------------------------------------
def _level_from_name(name: str) -> ShellDangerLevel:
return ShellDangerLevel[name.upper()]
@given('a DangerousPattern named "{name}" with pattern "{pattern}" at {level} level')
def step_create_dangerous_pattern(
context: Context, name: str, pattern: str, level: str
) -> None:
context.pattern = DangerousPattern(
name=name,
pattern=pattern,
level=_level_from_name(level),
description=f"Test pattern: {name}",
)
@given(
'a DangerousPattern named "{name}" with pattern "{pattern}" at {level} level and description "{description}"'
)
def step_create_dangerous_pattern_with_description(
context: Context, name: str, pattern: str, level: str, description: str
) -> None:
context.pattern = DangerousPattern(
name=name,
pattern=pattern,
level=_level_from_name(level),
description=description,
)
@when('I check if "{command}" matches the pattern')
def step_check_pattern_match(context: Context, command: str) -> None:
context.pattern_matched = context.pattern.matches(command)
@then("the pattern should match")
def step_pattern_should_match(context: Context) -> None:
assert context.pattern_matched is True, "Expected pattern to match but it did not"
@then("the pattern should not match")
def step_pattern_should_not_match(context: Context) -> None:
assert context.pattern_matched is False, "Expected pattern not to match but it did"
# ---------------------------------------------------------------------------
# DangerousCommandWarning
# ---------------------------------------------------------------------------
@when('I create a warning for command "{command}" from the pattern')
def step_create_warning_from_pattern(context: Context, command: str) -> None:
context.warning = DangerousCommandWarning.from_pattern(
command,
context.pattern,
)
@then('the warning command should be "{expected}"')
def step_warning_command(context: Context, expected: str) -> None:
assert context.warning.command == expected, (
f"Expected command={expected!r}, got {context.warning.command!r}"
)
@then("the warning danger level should be {level}")
def step_warning_danger_level(context: Context, level: str) -> None:
expected = _level_from_name(level)
assert context.warning.danger_level == expected, (
f"Expected danger_level={expected}, got {context.warning.danger_level}"
)
@then('the warning message should contain "{fragment}"')
def step_warning_message_contains(context: Context, fragment: str) -> None:
assert fragment in context.warning.message, (
f"Expected message to contain {fragment!r}, got {context.warning.message!r}"
)
# ---------------------------------------------------------------------------
# DangerousPatternDetector
# ---------------------------------------------------------------------------
@given("a DangerousPatternDetector with default patterns")
def step_create_default_detector(context: Context) -> None:
context.detector = DangerousPatternDetector()
@then("the detector should have patterns registered")
def step_detector_has_patterns(context: Context) -> None:
assert len(context.detector.patterns) > 0, "Expected detector to have patterns"
@when('I check the command "{command}"')
def step_check_command(context: Context, command: str) -> None:
context.last_command = command
context.is_dangerous = context.detector.is_dangerous(command)
context.max_level = context.detector.max_danger_level(command)
@then("the command should be detected as dangerous")
def step_command_is_dangerous(context: Context) -> None:
assert context.is_dangerous is True, (
f"Expected command {context.last_command!r} to be dangerous"
)
@then("the command should not be detected as dangerous")
def step_command_is_not_dangerous(context: Context) -> None:
assert context.is_dangerous is False, (
f"Expected command {context.last_command!r} to be safe but it was flagged as dangerous"
)
@then("the max danger level should be {level}")
def step_max_danger_level(context: Context, level: str) -> None:
expected = _level_from_name(level)
assert context.max_level == expected, (
f"Expected max danger level={expected}, got {context.max_level}"
)
@when('I call check_first on "{command}"')
def step_call_check_first(context: Context, command: str) -> None:
context.check_first_result = context.detector.check_first(command)
@then("check_first should return a warning")
def step_check_first_returns_warning(context: Context) -> None:
assert context.check_first_result is not None, (
"Expected check_first to return a warning"
)
@then("check_first should return None")
def step_check_first_returns_none(context: Context) -> None:
assert context.check_first_result is None, (
f"Expected check_first to return None, got {context.check_first_result!r}"
)
@then("the first warning danger level should be {level}")
def step_first_warning_danger_level(context: Context, level: str) -> None:
expected = _level_from_name(level)
assert context.check_first_result.danger_level == expected, (
f"Expected danger_level={expected}, got {context.check_first_result.danger_level}"
)
@when('I call check on "{command}"')
def step_call_check(context: Context, command: str) -> None:
context.check_result = context.detector.check(command)
@then("check should return at least one warning")
def step_check_returns_warnings(context: Context) -> None:
assert len(context.check_result) >= 1, (
f"Expected at least one warning, got {context.check_result!r}"
)
@when('I add a custom pattern named "{name}" with pattern "{pattern}" at {level} level')
def step_add_custom_pattern(
context: Context, name: str, pattern: str, level: str
) -> None:
custom = DangerousPattern(
name=name,
pattern=pattern,
level=_level_from_name(level),
description=f"Custom test pattern: {name}",
)
context.detector.add_pattern(custom)
@when('I remove the pattern named "{name}"')
def step_remove_pattern(context: Context, name: str) -> None:
context.detector.remove_pattern(name)
@when(
'I replace all patterns with a single pattern named "{name}" matching "{pattern}" at {level} level'
)
def step_replace_patterns(
context: Context, name: str, pattern: str, level: str
) -> None:
single = DangerousPattern(
name=name,
pattern=pattern,
level=_level_from_name(level),
description=f"Replacement pattern: {name}",
)
context.detector.replace_patterns([single])
@then("the detector patterns property should return a tuple")
def step_patterns_property_is_tuple(context: Context) -> None:
result = context.detector.patterns
assert isinstance(result, tuple), f"Expected tuple, got {type(result)}"
# ---------------------------------------------------------------------------
# ShellSafetyService
# ---------------------------------------------------------------------------
@given("a ShellSafetyService with default settings")
def step_create_default_safety_service(context: Context) -> None:
context.service = ShellSafetyService()
context.warn_callback_called = False
@given("a ShellSafetyService with a warn_callback that returns True")
def step_create_service_with_allow_callback(context: Context) -> None:
context.warn_callback_called = False
def callback(warning: DangerousCommandWarning) -> bool:
context.warn_callback_called = True
return True
context.service = ShellSafetyService(warn_callback=callback)
@given("a ShellSafetyService with a warn_callback that returns False")
def step_create_service_with_block_callback(context: Context) -> None:
context.warn_callback_called = False
def callback(warning: DangerousCommandWarning) -> bool:
context.warn_callback_called = True
return False
context.service = ShellSafetyService(warn_callback=callback)
@given(
'a ShellSafetyService with an extra pattern "{name}" matching "{pattern}" at {level} level'
)
def step_create_service_with_extra_pattern(
context: Context, name: str, pattern: str, level: str
) -> None:
extra = DangerousPattern(
name=name,
pattern=pattern,
level=_level_from_name(level),
description=f"Extra pattern: {name}",
)
context.service = ShellSafetyService(extra_patterns=[extra])
@when('I check the safety of command "{command}"')
def step_check_safety(context: Context, command: str) -> None:
context.safety_result = context.service.check_command(command)
@then("the safety check result should be allowed")
def step_safety_result_allowed(context: Context) -> None:
assert context.safety_result.allowed is True, (
f"Expected result to be allowed, got blocked. Warning: {context.safety_result.warning}"
)
@then("the safety check result should be blocked")
def step_safety_result_blocked(context: Context) -> None:
assert context.safety_result.allowed is False, (
"Expected result to be blocked, got allowed"
)
@then("the safety check warning should be None")
def step_safety_warning_is_none(context: Context) -> None:
assert context.safety_result.warning is None, (
f"Expected warning to be None, got {context.safety_result.warning!r}"
)
@then("the safety check warning should not be None")
def step_safety_warning_is_not_none(context: Context) -> None:
assert context.safety_result.warning is not None, "Expected warning to be set"
@then("the warn_callback should have been called")
def step_warn_callback_called(context: Context) -> None:
assert context.warn_callback_called is True, (
"Expected warn_callback to have been called"
)
@when('I call is_safe on "{command}"')
def step_call_is_safe(context: Context, command: str) -> None:
context.is_safe_result = context.service.is_safe(command)
@then("is_safe should return True")
def step_is_safe_true(context: Context) -> None:
assert context.is_safe_result is True, "Expected is_safe to return True"
@then("is_safe should return False")
def step_is_safe_false(context: Context) -> None:
assert context.is_safe_result is False, "Expected is_safe to return False"
@then("the service detector property should be a DangerousPatternDetector")
def step_service_detector_property(context: Context) -> None:
assert isinstance(context.service.detector, DangerousPatternDetector), (
f"Expected DangerousPatternDetector, got {type(context.service.detector)}"
)
@then("the service block_level should be MEDIUM")
def step_service_block_level(context: Context) -> None:
assert context.service.block_level == ShellDangerLevel.MEDIUM, (
f"Expected block_level=MEDIUM, got {context.service.block_level}"
)
@then('the safety check result repr should contain "{fragment}"')
def step_safety_result_repr_contains(context: Context, fragment: str) -> None:
result_repr = repr(context.safety_result)
assert fragment in result_repr, (
f"Expected repr to contain {fragment!r}, got {result_repr!r}"
)