forked from HAL9000/cleveragents-core
5c49b2134b
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
396 lines
13 KiB
Python
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}"
|
|
)
|