File size: 5,236 Bytes
10bd457
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
"""Pydantic V2 models for CloudGuard-S3-Auditor environment."""

from __future__ import annotations

import enum
from typing import Literal

from pydantic import BaseModel, Field, field_validator


# ── Enums ────────────────────────────────────────────────────────────────────

class BucketPolicy(str, enum.Enum):
    PUBLIC = "public"
    PRIVATE = "private"


class EncryptionType(str, enum.Enum):
    NONE = "none"
    AES256 = "aes256"
    AWS_KMS = "aws-kms"


class ActionType(str, enum.Enum):
    MAKE_PRIVATE = "make_private"
    MAKE_PUBLIC = "make_public"
    ENABLE_ENCRYPTION = "enable_encryption"
    NOOP = "noop"


# ── Bucket State ─────────────────────────────────────────────────────────────

class S3Bucket(BaseModel):
    """Single S3 bucket in the simulated environment."""

    name: str = Field(..., description="Bucket identifier, e.g. 'website-css' or 'user-pii-db'")
    policy: BucketPolicy = Field(default=BucketPolicy.PUBLIC)
    encryption: EncryptionType = Field(default=EncryptionType.NONE)
    contains_pii: bool = Field(default=False)
    is_required_public: bool = Field(default=False)

    def policy_json(self) -> dict:
        return {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow" if self.policy == BucketPolicy.PUBLIC else "Deny",
                    "Principal": "*",
                    "Action": "s3:GetObject",
                    "Resource": f"arn:aws:s3:::{self.name}/*",
                }
            ],
        }


# ── Action ───────────────────────────────────────────────────────────────────

class Action(BaseModel):
    """Agent action submitted each step."""

    action_type: ActionType
    bucket_name: str | None = Field(
        default=None,
        description="Target bucket name (required for all actions except noop)",
    )
    encryption_algo: EncryptionType | None = Field(
        default=None,
        description="Encryption algorithm (required only for enable_encryption)",
    )

    @field_validator("bucket_name")
    @classmethod
    def require_bucket_for_non_noop(cls, v: str | None, info) -> str | None:
        if info.data.get("action_type") != ActionType.NOOP and v is None:
            raise ValueError("bucket_name is required for non-noop actions")
        return v

    def to_str(self) -> str:
        if self.action_type == ActionType.NOOP:
            return "noop"
        if self.action_type == ActionType.ENABLE_ENCRYPTION:
            algo = self.encryption_algo.value if self.encryption_algo else "aes256"
            return f"enable_encryption:{self.bucket_name}:{algo}"
        return f"{self.action_type.value}:{self.bucket_name}"

    @classmethod
    def from_str(cls, raw: str) -> "Action":
        raw = raw.strip()
        if raw == "noop":
            return cls(action_type=ActionType.NOOP)

        parts = raw.split(":")
        action_type = ActionType(parts[0])

        if action_type == ActionType.ENABLE_ENCRYPTION:
            if len(parts) < 3:
                raise ValueError("enable_encryption requires bucket_name and algo")
            return cls(
                action_type=action_type,
                bucket_name=parts[1],
                encryption_algo=EncryptionType(parts[2]),
            )

        return cls(action_type=action_type, bucket_name=parts[1])


# ── Observation ──────────────────────────────────────────────────────────────

class BucketObservation(BaseModel):
    """Visible state of a single bucket (sent to agent)."""

    name: str
    policy: BucketPolicy
    encryption: EncryptionType
    contains_pii: bool
    is_required_public: bool


class Observation(BaseModel):
    """Full observation returned to agent each step."""

    buckets: list[BucketObservation]
    step_number: int = Field(ge=0)
    max_steps: int = Field(ge=1)
    done: bool = False
    message: str = ""


# ── Reward / Step Result ─────────────────────────────────────────────────────

class StepResult(BaseModel):
    """Result of a single env.step() call."""

    observation: Observation
    reward: float = Field(ge=-10.0, le=10.0)
    done: bool = False
    info: dict = Field(default_factory=dict)


# ── Grading ──────────────────────────────────────────────────────────────────

class GradeResult(BaseModel):
    """Output of the SuccessGrader."""

    success: bool
    score: float = Field(ge=0.0, le=1.0)
    details: dict = Field(default_factory=dict)