from __future__ import annotations from datetime import UTC, datetime, timedelta import pytest from tests.helpers import make_alert from vuln_scan.core.models import FindingStatus, Severity from vuln_scan.core.policy import BlockingPolicy, GracePeriods from vuln_scan.core.time import FixedClock # -------------------------------------------------------- # Helpers # -------------------------------------------------------- _DEFAULT_NOW = datetime(2025, 1, 20, tzinfo=UTC) def _policy( now: datetime = _DEFAULT_NOW, **grace_kwargs: int, ) -> BlockingPolicy: return BlockingPolicy(grace=GracePeriods(**grace_kwargs), clock=FixedClock(now)) # -------------------------------------------------------- # GracePeriods.for_alert # -------------------------------------------------------- _GRACE = GracePeriods( critical_days=5, high_days=10, moderate_days=20, low_days=40, non_blocking_days=90 ) _GRACE_CREATED = datetime(2025, 1, 1, tzinfo=UTC) @pytest.mark.parametrize( "severity, patched_version, expected_days", [ pytest.param(Severity.CRITICAL, "2.0.0", 5, id="critical"), pytest.param(Severity.HIGH, "2.0.0", 10, id="high"), pytest.param(Severity.MODERATE, "2.0.0", 20, id="moderate"), pytest.param(Severity.LOW, "2.0.0", 40, id="low"), pytest.param(Severity.UNKNOWN, "2.0.0", 90, id="unknown-severity-uses-non-blocking"), pytest.param(Severity.CRITICAL, None, 90, id="no-patch-uses-non-blocking"), ], ) def test_grace_periods_for_alert( severity: Severity, patched_version: str | None, expected_days: int, ) -> None: assert ( _GRACE.for_alert( make_alert( created_at=_GRACE_CREATED, severity=severity, first_patched=patched_version, ), ) == expected_days ) # -------------------------------------------------------- # BlockingPolicy.evaluate — early-exit cases # -------------------------------------------------------- @pytest.mark.parametrize( "kwargs, expected_status, expected_blocking", [ pytest.param( dict(is_vulnerable=True, has_patch=True, is_removed=True), FindingStatus.REMOVED, False, id="removed-dependency", ), pytest.param( dict(is_vulnerable=True, has_patch=False), FindingStatus.NO_PATCH, False, id="no-patch-available", ), pytest.param( dict(is_vulnerable=False, has_patch=True), FindingStatus.FIXED, False, id="already-fixed", ), ], ) def test_blocking_policy_non_blocking_early_exits( kwargs: dict, expected_status: FindingStatus, expected_blocking: bool, ) -> None: policy = _policy() decision = policy.evaluate( alert=make_alert(created_at=_DEFAULT_NOW - timedelta(days=1)), **kwargs, ) assert decision.status is expected_status assert decision.is_blocking is expected_blocking assert decision.blocking_date is None assert decision.remaining_days == 0 # -------------------------------------------------------- # BlockingPolicy.evaluate — introduced dependency # -------------------------------------------------------- def test_blocking_policy_introduced_vulnerability_blocks_immediately() -> None: policy = _policy() decision = policy.evaluate( alert=make_alert(created_at=_DEFAULT_NOW - timedelta(days=1), severity=Severity.CRITICAL), is_vulnerable=True, has_patch=True, is_introduced=True, ) assert decision.status is FindingStatus.VULNERABLE assert decision.is_blocking is True assert decision.blocking_date == _DEFAULT_NOW assert decision.remaining_days == 0 assert "New dependency" in decision.reason # -------------------------------------------------------- # BlockingPolicy.evaluate — grace period logic # -------------------------------------------------------- @pytest.mark.parametrize( "created_at, now, grace_days, expected_blocking, expected_remaining, reason_fragment", [ pytest.param( datetime(2025, 1, 10, tzinfo=UTC), datetime(2025, 1, 15, tzinfo=UTC), 14, False, 9, "9 days remaining", id="within-grace-not-blocking", ), pytest.param( datetime(2025, 1, 1, tzinfo=UTC), datetime(2025, 1, 20, tzinfo=UTC), 14, True, 0, "Grace period exceeded", id="past-grace-blocking", ), ], ) def test_blocking_policy_grace_period( created_at: datetime, now: datetime, grace_days: int, expected_blocking: bool, expected_remaining: int, reason_fragment: str, ) -> None: policy = _policy(now=now, high_days=grace_days) decision = policy.evaluate( alert=make_alert(created_at=created_at, severity=Severity.HIGH), is_vulnerable=True, has_patch=True, ) assert decision.status is FindingStatus.VULNERABLE assert decision.is_blocking is expected_blocking assert decision.blocking_date == created_at + timedelta(days=grace_days) assert decision.remaining_days == expected_remaining assert reason_fragment in decision.reason