""" Typed Pydantic v2 models for the SOC Incident Response OpenEnv environment. All models are fully typed and JSON-serialisable — required by the OpenEnv spec. """ from __future__ import annotations from typing import Any, Dict, List, Literal, Optional from enum import Enum from pydantic import BaseModel, Field # --------------------------------------------------------------------------- # Enumerations # --------------------------------------------------------------------------- class AlertSeverity(str, Enum): LOW = "low" MEDIUM = "medium" HIGH = "high" CRITICAL = "critical" class AlertStatus(str, Enum): PENDING = "pending" INVESTIGATING = "investigating" TRUE_POSITIVE = "true_positive" FALSE_POSITIVE = "false_positive" CONTAINED = "contained" ESCALATED = "escalated" CLOSED = "closed" class ActionType(str, Enum): ENRICH_ALERT = "enrich_alert" CORRELATE_ALERTS = "correlate_alerts" ISOLATE_ENDPOINT = "isolate_endpoint" DISABLE_ACCOUNT = "disable_account" COLLECT_FORENSICS = "collect_forensics" ESCALATE_TO_TIER2 = "escalate_to_tier2" CREATE_TICKET = "create_ticket" class MITRETactic(str, Enum): INITIAL_ACCESS = "initial_access" EXECUTION = "execution" PERSISTENCE = "persistence" PRIVILEGE_ESCALATION = "privilege_escalation" DEFENSE_EVASION = "defense_evasion" CREDENTIAL_ACCESS = "credential_access" DISCOVERY = "discovery" LATERAL_MOVEMENT = "lateral_movement" COLLECTION = "collection" EXFILTRATION = "exfiltration" IMPACT = "impact" class HostStatus(str, Enum): CLEAN = "clean" SUSPICIOUS = "suspicious" COMPROMISED = "compromised" ISOLATED = "isolated" PATCHED = "patched" # --------------------------------------------------------------------------- # Domain entities # --------------------------------------------------------------------------- class ThreatIndicator(BaseModel): type: Literal["ip", "hash", "domain", "user", "process", "registry_key"] value: str reputation: Optional[str] = None context: Optional[str] = None class SIEMAlert(BaseModel): alert_id: str timestamp: str severity: AlertSeverity rule_name: str description: str host_id: str user_id: Optional[str] = None mitre_tactic: Optional[MITRETactic] = None mitre_technique: Optional[str] = None indicators: List[ThreatIndicator] = Field(default_factory=list) raw_log: Optional[str] = None status: AlertStatus = AlertStatus.PENDING enrichment: Optional[Dict[str, Any]] = None ground_truth: Optional[bool] = None # True=TP, False=FP — never sent to agent class NetworkHost(BaseModel): host_id: str hostname: str ip_address: str subnet: str os: str role: str owner: Optional[str] = None is_critical: bool = False is_vip: bool = False status: HostStatus = HostStatus.CLEAN active_sessions: List[str] = Field(default_factory=list) running_processes: List[str] = Field(default_factory=list) class AttackChain(BaseModel): """Ground-truth kill chain — used only by grader, never exposed to agent.""" patient_zero_host: str stages: List[MITRETactic] lateral_movement_targets: List[str] = Field(default_factory=list) crown_jewel_host: str exfiltration_complete: bool = False attacker_dwell_minutes: int = 0 class BusinessConstraint(BaseModel): host_id: Optional[str] = None user_id: Optional[str] = None constraint_type: Literal[ "cannot_isolate", "legal_hold", "customer_facing", "executive", "critical_infra" ] reason: str severity: Literal["advisory", "hard_block"] class InvestigationNote(BaseModel): step: int action_taken: str finding: str timestamp: str # --------------------------------------------------------------------------- # OpenEnv spec: Observation, Action, Reward # --------------------------------------------------------------------------- class Observation(BaseModel): """What the agent sees at each step — ground_truth fields are excluded.""" step: int task_id: str task_description: str active_alerts: List[SIEMAlert] acknowledged_alerts: List[SIEMAlert] = Field(default_factory=list) hosts: List[NetworkHost] business_constraints: List[BusinessConstraint] = Field(default_factory=list) notes: List[InvestigationNote] = Field(default_factory=list) elapsed_minutes: int = 0 max_minutes: int = 120 steps_remaining: int = 40 last_action_result: Optional[str] = None last_action_success: bool = True def model_dump_safe(self) -> Dict[str, Any]: """Serialise without ground_truth — safe to send to agent.""" d = self.model_dump() for alert in d.get("active_alerts", []): alert.pop("ground_truth", None) for alert in d.get("acknowledged_alerts", []): alert.pop("ground_truth", None) return d class Action(BaseModel): """The agent's action at each step.""" action_type: ActionType alert_id: Optional[str] = None alert_ids: Optional[List[str]] = None host_id: Optional[str] = None user_id: Optional[str] = None artifact_types: Optional[List[str]] = None source: Optional[str] = None priority: Optional[Literal["P1", "P2", "P3"]] = None summary: Optional[str] = None classification: Optional[Literal["true_positive", "false_positive"]] = None correlation_hypothesis: Optional[str] = None class Reward(BaseModel): """Per-step reward with full breakdown.""" total: float = Field(ge=-1.0, le=1.0) classification_delta: float = 0.0 chain_coverage_delta: float = 0.0 containment_delta: float = 0.0 dwell_penalty: float = 0.0 fp_penalty: float = 0.0 compliance_delta: float = 0.0 info: str = "" class EnvState(BaseModel): """Full internal state — includes ground truth. Returned by state().""" task_id: str step: int done: bool cumulative_reward: float observation: Observation attack_chain: Optional[AttackChain] = None agent_classifications: Dict[str, bool] = Field(default_factory=dict) identified_stages: List[MITRETactic] = Field(default_factory=list) isolated_hosts: List[str] = Field(default_factory=list) disabled_accounts: List[str] = Field(default_factory=list) forensics_collected: Dict[str, List[str]] = Field(default_factory=dict) escalated: bool = False ticket_created: bool = False