Spaces:
Sleeping
Sleeping
Update grader.py
Browse files
grader.py
CHANGED
|
@@ -2,89 +2,91 @@
|
|
| 2 |
grader.py β PhishGuard-Env | Triage Scoring & Task Registry
|
| 3 |
==============================================================
|
| 4 |
|
| 5 |
-
|
| 6 |
-----------------------
|
| 7 |
-
The OpenEnv validator
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
1.
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
Perfect match (any task type) 0.95 Near-ideal; headroom kept
|
| 42 |
-
MALWARE β QUARANTINE (safe containment) 0.75 Correct containment call
|
| 43 |
-
PHISH/BEC β QUARANTINE (under-response) 0.60 Held but domain still active
|
| 44 |
-
SPAM β BLOCK_DOMAIN (over-escalation) 0.40 Too aggressive, not wrong
|
| 45 |
-
SPAM β QUARANTINE (lighter escalation) 0.35 Wastes analyst queue
|
| 46 |
-
General wrong procedure (no active harm) 0.10 Wrong but not dangerous
|
| 47 |
-
Business Disruption (SAFE email blocked) 0.05 Ops impact, below drain floor
|
| 48 |
-
Security Breach (threat β MARK_SAFE) 0.02 Catastrophic; minimum signal
|
| 49 |
-
|
| 50 |
-
Health-drain threshold (consumed by env.py)
|
| 51 |
-
-------------------------------------------
|
| 52 |
HEALTH_DRAIN_THRESHOLD = 0.15
|
| 53 |
-
|
| 54 |
-
Covers:
|
| 55 |
-
|
| 56 |
-
Cautious / partial-credit scores (β₯ 0.35) never drain health.
|
| 57 |
"""
|
| 58 |
|
| 59 |
from typing import Tuple
|
| 60 |
|
| 61 |
-
# ββ
|
| 62 |
-
#
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
HEALTH_DRAIN_THRESHOLD = 0.15 # env.py: reward < this β lose one life
|
| 73 |
|
| 74 |
|
| 75 |
# ββ Task Registry ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 76 |
-
#
|
| 77 |
#
|
| 78 |
-
# Keys
|
| 79 |
-
#
|
| 80 |
-
#
|
| 81 |
-
# email_types : set of raw email type strings that map to this task_id.
|
| 82 |
-
# Used by grade_action() to look up the correct grader.
|
| 83 |
-
# description : human-readable summary for the /metadata endpoint.
|
| 84 |
-
# threat : True β dangerous email; graded as a threat task.
|
| 85 |
-
# False β legitimate email; graded as a false-positive task.
|
| 86 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 87 |
-
|
| 88 |
TASK_REGISTRY: dict[str, dict] = {
|
| 89 |
"task_spam": {
|
| 90 |
"email_types": {"SPAM"},
|
|
@@ -128,8 +130,7 @@ TASK_REGISTRY: dict[str, dict] = {
|
|
| 128 |
},
|
| 129 |
}
|
| 130 |
|
| 131 |
-
# Reverse lookup: raw email_type string β task_id
|
| 132 |
-
# Built once at import time; O(1) per grade_action call.
|
| 133 |
_TYPE_TO_TASK_ID: dict[str, str] = {
|
| 134 |
etype: task_id
|
| 135 |
for task_id, meta in TASK_REGISTRY.items()
|
|
@@ -139,11 +140,10 @@ _TYPE_TO_TASK_ID: dict[str, str] = {
|
|
| 139 |
|
| 140 |
def resolve_task_id(email_type: str) -> str:
|
| 141 |
"""
|
| 142 |
-
Convert a raw email type
|
| 143 |
-
(e.g. "task_phishing").
|
| 144 |
|
| 145 |
-
Raises ValueError for
|
| 146 |
-
|
| 147 |
"""
|
| 148 |
key = email_type.strip().upper()
|
| 149 |
if key not in _TYPE_TO_TASK_ID:
|
|
@@ -154,7 +154,51 @@ def resolve_task_id(email_type: str) -> str:
|
|
| 154 |
return _TYPE_TO_TASK_ID[key]
|
| 155 |
|
| 156 |
|
| 157 |
-
# ββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
def grade_action(
|
| 160 |
agent_output: str,
|
|
@@ -162,110 +206,74 @@ def grade_action(
|
|
| 162 |
email_type: str,
|
| 163 |
) -> Tuple[float, str]:
|
| 164 |
"""
|
| 165 |
-
Grade one SOC triage decision
|
| 166 |
-
(0.0, 1.0) β the endpoints 0 and 1 are NEVER returned.
|
| 167 |
-
|
| 168 |
-
Parameters
|
| 169 |
-
----------
|
| 170 |
-
agent_output : Raw action string from the LLM (case-insensitive).
|
| 171 |
-
expected_output : Ground-truth correct action for this scenario.
|
| 172 |
-
email_type : Raw threat category β PHISH | BEC | MALWARE | SPAM | SAFE.
|
| 173 |
-
The function resolves this to a task_id internally.
|
| 174 |
|
| 175 |
Returns
|
| 176 |
-------
|
| 177 |
(reward, message)
|
| 178 |
-
reward : float strictly in (0.0, 1.0).
|
| 179 |
message : short verdict string for logs and agent feedback.
|
| 180 |
"""
|
| 181 |
agent_action = agent_output.strip().upper()
|
| 182 |
expected_action = expected_output.strip().upper()
|
| 183 |
-
etype = email_type.strip().upper()
|
| 184 |
|
| 185 |
-
#
|
| 186 |
-
task_id = resolve_task_id(
|
| 187 |
is_threat = TASK_REGISTRY[task_id]["threat"]
|
| 188 |
|
| 189 |
-
# ββ 1.
|
| 190 |
if agent_action == expected_action:
|
| 191 |
return R_PERFECT, "PERFECT_TRIAGE: Correct action taken"
|
| 192 |
|
| 193 |
-
# ββ 2.
|
| 194 |
-
# Threat email rubber-stamped as safe β worst possible outcome.
|
| 195 |
if is_threat and agent_action == "MARK_SAFE":
|
| 196 |
return R_BREACH, "SECURITY_BREACH: Threat allowed into the network"
|
| 197 |
|
| 198 |
-
# ββ 3.
|
| 199 |
-
# Legitimate email blocked or quarantined β costly false positive.
|
| 200 |
if not is_threat and agent_action in {"BLOCK_DOMAIN", "QUARANTINE", "MOVE_TO_SPAM"}:
|
| 201 |
return R_DISRUPTION, "BUSINESS_DISRUPTION: Legitimate communication blocked"
|
| 202 |
|
| 203 |
-
# ββ 4.
|
| 204 |
-
|
| 205 |
-
# MALWARE quarantined: textbook containment response.
|
| 206 |
-
# Branch fires only when scenario's `correct` field differs from QUARANTINE
|
| 207 |
-
# (kept for robustness against future scenario expansions).
|
| 208 |
if task_id == "task_malware" and agent_action == "QUARANTINE":
|
| 209 |
return R_MALWARE_QUARANTINE, "CAUTIOUS: Malware isolated β strong containment"
|
| 210 |
|
| 211 |
-
# PHISH or BEC quarantined instead of domain-blocked.
|
| 212 |
-
# Threat is held, but the malicious domain stays active.
|
| 213 |
if task_id in {"task_phishing", "task_bec"} and agent_action == "QUARANTINE":
|
| 214 |
return R_PHISH_BEC_QUARANTINE, (
|
| 215 |
"UNDER_RESPONSE: Threat quarantined but domain still active β "
|
| 216 |
"consider BLOCK_DOMAIN"
|
| 217 |
)
|
| 218 |
|
| 219 |
-
# SPAM domain fully blocked β disproportionate but not dangerous.
|
| 220 |
if task_id == "task_spam" and agent_action == "BLOCK_DOMAIN":
|
| 221 |
return R_SPAM_BLOCK, (
|
| 222 |
"OVER_ESCALATION: BLOCK_DOMAIN is disproportionate for SPAM β "
|
| 223 |
"prefer MOVE_TO_SPAM"
|
| 224 |
)
|
| 225 |
|
| 226 |
-
# SPAM quarantined β lighter over-escalation, clogs analyst queue.
|
| 227 |
if task_id == "task_spam" and agent_action == "QUARANTINE":
|
| 228 |
return R_SPAM_QUARANTINE, (
|
| 229 |
"OVER_ESCALATION: QUARANTINE wastes analyst capacity for SPAM β "
|
| 230 |
"prefer MOVE_TO_SPAM"
|
| 231 |
)
|
| 232 |
|
| 233 |
-
# ββ 5.
|
| 234 |
return R_WRONG_PROCEDURE, "INCORRECT_PROCEDURE: Decision does not match security policy"
|
| 235 |
|
| 236 |
|
| 237 |
-
# ββ
|
| 238 |
|
| 239 |
-
def calculate_overall_score(task_scores: list) -> float:
|
| 240 |
"""
|
| 241 |
-
Compute
|
| 242 |
-
|
| 243 |
-
Result is clamped to (R_BREACH, R_PERFECT) to honour the open-interval
|
| 244 |
-
contract at the episode level as well as the step level.
|
| 245 |
-
|
| 246 |
-
Parameters
|
| 247 |
-
----------
|
| 248 |
-
task_scores : list of floats, each in (0.0, 1.0).
|
| 249 |
-
|
| 250 |
-
Returns
|
| 251 |
-
-------
|
| 252 |
-
float in (0.0, 1.0) β never exactly 0 or 1.
|
| 253 |
"""
|
| 254 |
-
|
| 255 |
-
return R_BREACH # Minimum signal value, not zero
|
| 256 |
-
|
| 257 |
-
raw_avg = sum(task_scores) / len(task_scores)
|
| 258 |
-
clamped = max(R_BREACH, min(R_PERFECT, raw_avg))
|
| 259 |
-
return round(clamped, 4)
|
| 260 |
|
| 261 |
|
| 262 |
def calculate_per_task_scores(
|
| 263 |
task_score_map: dict[str, list[float]],
|
| 264 |
) -> dict[str, float]:
|
| 265 |
"""
|
| 266 |
-
Compute per-task-type average scores
|
| 267 |
-
|
| 268 |
-
Used by GET /state to give the validator per-task breakdowns.
|
| 269 |
|
| 270 |
Parameters
|
| 271 |
----------
|
|
@@ -273,10 +281,10 @@ def calculate_per_task_scores(
|
|
| 273 |
|
| 274 |
Returns
|
| 275 |
-------
|
| 276 |
-
{ task_id:
|
| 277 |
"""
|
| 278 |
return {
|
| 279 |
-
task_id:
|
| 280 |
for task_id, scores in task_score_map.items()
|
| 281 |
if scores
|
| 282 |
}
|
|
|
|
| 2 |
grader.py β PhishGuard-Env | Triage Scoring & Task Registry
|
| 3 |
==============================================================
|
| 4 |
|
| 5 |
+
TASK IDENTITY CONTRACT
|
| 6 |
-----------------------
|
| 7 |
+
The OpenEnv validator counts "tasks with graders" by matching the
|
| 8 |
+
`task_id` values declared in openenv.yaml against `task_id` values
|
| 9 |
+
returned in /step responses at runtime.
|
| 10 |
+
|
| 11 |
+
Both sides must use the SAME five semantic IDs:
|
| 12 |
+
|
| 13 |
+
task_spam task_phishing task_safe
|
| 14 |
+
task_malware task_bec
|
| 15 |
+
|
| 16 |
+
These are defined once in TASK_REGISTRY below and imported everywhere
|
| 17 |
+
else β env.py, openenv.yaml is written to match them exactly.
|
| 18 |
+
|
| 19 |
+
REWARD SCALE CONTRACT
|
| 20 |
+
---------------------
|
| 21 |
+
All rewards strictly in the open interval (0.0, 1.0).
|
| 22 |
+
0 and 1 are NEVER returned β guaranteed by safe_score().
|
| 23 |
+
|
| 24 |
+
safe_score(raw) = 0.01 + 0.98 * raw, raw in [0.0, 1.0]
|
| 25 |
+
|
| 26 |
+
Full reward table
|
| 27 |
+
-----------------
|
| 28 |
+
Outcome raw safe Rationale
|
| 29 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 30 |
+
Perfect match 1.00 0.99 Near-ideal
|
| 31 |
+
MALWARE β QUARANTINE (containment) 0.75 0.745 Correct call
|
| 32 |
+
PHISH/BEC β QUARANTINE (under-response) 0.60 0.598 Domain still active
|
| 33 |
+
SPAM β BLOCK_DOMAIN (over-escalation) 0.40 0.402 Disproportionate
|
| 34 |
+
SPAM β QUARANTINE (lighter escalation) 0.35 0.353 Wastes analyst queue
|
| 35 |
+
General wrong procedure 0.10 0.108 Wrong but no harm
|
| 36 |
+
Business Disruption (SAFE β blocked) 0.05 0.059 Ops impact
|
| 37 |
+
Security Breach (threat β MARK_SAFE) 0.02 0.0296 Catastrophic
|
| 38 |
+
|
| 39 |
+
Health-drain threshold
|
| 40 |
+
----------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
HEALTH_DRAIN_THRESHOLD = 0.15
|
| 42 |
+
env.py deducts one life when safe_score result < 0.15.
|
| 43 |
+
Covers: Breach (0.030), Disruption (0.059), Wrong (0.108).
|
| 44 |
+
Cautious/partial scores (β₯ 0.35 raw β β₯ 0.353 safe) never drain health.
|
|
|
|
| 45 |
"""
|
| 46 |
|
| 47 |
from typing import Tuple
|
| 48 |
|
| 49 |
+
# ββ safe_score βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 50 |
+
# Linear bijection [0,1] β (0.01, 0.99).
|
| 51 |
+
# Guarantees the open-interval contract at every call site.
|
| 52 |
+
def safe_score(raw: float) -> float:
|
| 53 |
+
"""Map a raw score in [0.0, 1.0] to a safe score strictly in (0.0, 1.0)."""
|
| 54 |
+
clamped = max(0.0, min(1.0, raw))
|
| 55 |
+
result = 0.01 + 0.98 * clamped
|
| 56 |
+
assert 0.0 < result < 1.0, f"safe_score contract violated: {result}"
|
| 57 |
+
return round(result, 6)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# ββ Reward constants (raw values, passed through safe_score before use) ββββββββ
|
| 61 |
+
_R_PERFECT_RAW = 1.00
|
| 62 |
+
_R_MALWARE_QUARANTINE_RAW = 0.75
|
| 63 |
+
_R_PHISH_BEC_QUARANTINE_RAW = 0.60
|
| 64 |
+
_R_SPAM_BLOCK_RAW = 0.40
|
| 65 |
+
_R_SPAM_QUARANTINE_RAW = 0.35
|
| 66 |
+
_R_WRONG_PROCEDURE_RAW = 0.10
|
| 67 |
+
_R_DISRUPTION_RAW = 0.05
|
| 68 |
+
_R_BREACH_RAW = 0.02
|
| 69 |
+
|
| 70 |
+
# Public safe-score constants (what env.py, inference.py actually use)
|
| 71 |
+
R_PERFECT = safe_score(_R_PERFECT_RAW) # 0.99
|
| 72 |
+
R_MALWARE_QUARANTINE = safe_score(_R_MALWARE_QUARANTINE_RAW) # 0.745
|
| 73 |
+
R_PHISH_BEC_QUARANTINE = safe_score(_R_PHISH_BEC_QUARANTINE_RAW) # 0.598
|
| 74 |
+
R_SPAM_BLOCK = safe_score(_R_SPAM_BLOCK_RAW) # 0.402
|
| 75 |
+
R_SPAM_QUARANTINE = safe_score(_R_SPAM_QUARANTINE_RAW) # 0.353
|
| 76 |
+
R_WRONG_PROCEDURE = safe_score(_R_WRONG_PROCEDURE_RAW) # 0.108
|
| 77 |
+
R_DISRUPTION = safe_score(_R_DISRUPTION_RAW) # 0.059
|
| 78 |
+
R_BREACH = safe_score(_R_BREACH_RAW) # 0.0296
|
| 79 |
|
| 80 |
HEALTH_DRAIN_THRESHOLD = 0.15 # env.py: reward < this β lose one life
|
| 81 |
|
| 82 |
|
| 83 |
# ββ Task Registry ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 84 |
+
# SINGLE SOURCE OF TRUTH for task identity.
|
| 85 |
#
|
| 86 |
+
# Keys must exactly match the task `id` values in openenv.yaml.
|
| 87 |
+
# The `email_types` set drives grade_action() dispatch.
|
| 88 |
+
# `threat` determines Breach vs Disruption penalty direction.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 90 |
TASK_REGISTRY: dict[str, dict] = {
|
| 91 |
"task_spam": {
|
| 92 |
"email_types": {"SPAM"},
|
|
|
|
| 130 |
},
|
| 131 |
}
|
| 132 |
|
| 133 |
+
# Reverse lookup: raw email_type string β task_id (built once at import time)
|
|
|
|
| 134 |
_TYPE_TO_TASK_ID: dict[str, str] = {
|
| 135 |
etype: task_id
|
| 136 |
for task_id, meta in TASK_REGISTRY.items()
|
|
|
|
| 140 |
|
| 141 |
def resolve_task_id(email_type: str) -> str:
|
| 142 |
"""
|
| 143 |
+
Convert a raw email type (e.g. "PHISH") β task_id (e.g. "task_phishing").
|
|
|
|
| 144 |
|
| 145 |
+
Raises ValueError for unknown types so misconfigured scenarios fail loudly
|
| 146 |
+
at startup rather than silently corrupting benchmark results.
|
| 147 |
"""
|
| 148 |
key = email_type.strip().upper()
|
| 149 |
if key not in _TYPE_TO_TASK_ID:
|
|
|
|
| 154 |
return _TYPE_TO_TASK_ID[key]
|
| 155 |
|
| 156 |
|
| 157 |
+
# ββ Per-task graders (one function per task_id) ββββββββββββββββββββββββββββββββ
|
| 158 |
+
# Each grader takes a list of per-step rewards for that task type and returns
|
| 159 |
+
# a final safe_score. This is what openenv.yaml's grader field points to.
|
| 160 |
+
|
| 161 |
+
def grade_task_spam(rewards: list[float]) -> float:
|
| 162 |
+
"""Final score for all task_spam steps in an episode."""
|
| 163 |
+
return _aggregate(rewards)
|
| 164 |
+
|
| 165 |
+
def grade_task_phishing(rewards: list[float]) -> float:
|
| 166 |
+
"""Final score for all task_phishing steps in an episode."""
|
| 167 |
+
return _aggregate(rewards)
|
| 168 |
+
|
| 169 |
+
def grade_task_safe(rewards: list[float]) -> float:
|
| 170 |
+
"""Final score for all task_safe steps in an episode."""
|
| 171 |
+
return _aggregate(rewards)
|
| 172 |
+
|
| 173 |
+
def grade_task_malware(rewards: list[float]) -> float:
|
| 174 |
+
"""Final score for all task_malware steps in an episode."""
|
| 175 |
+
return _aggregate(rewards)
|
| 176 |
+
|
| 177 |
+
def grade_task_bec(rewards: list[float]) -> float:
|
| 178 |
+
"""Final score for all task_bec steps in an episode."""
|
| 179 |
+
return _aggregate(rewards)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
# Map task_id β its grader function (consumed by env.py /grade endpoint)
|
| 183 |
+
TASK_GRADERS: dict[str, callable] = {
|
| 184 |
+
"task_spam": grade_task_spam,
|
| 185 |
+
"task_phishing": grade_task_phishing,
|
| 186 |
+
"task_safe": grade_task_safe,
|
| 187 |
+
"task_malware": grade_task_malware,
|
| 188 |
+
"task_bec": grade_task_bec,
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def _aggregate(rewards: list[float]) -> float:
|
| 193 |
+
"""Average a list of per-step rewards, return as a safe_score."""
|
| 194 |
+
if not rewards:
|
| 195 |
+
return R_BREACH
|
| 196 |
+
raw_avg = sum(rewards) / len(rewards)
|
| 197 |
+
# rewards are already safe_scores; average is still in (0,1) but clamp defensively
|
| 198 |
+
return round(max(R_BREACH, min(R_PERFECT, raw_avg)), 6)
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
# ββ Core step grading function βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 202 |
|
| 203 |
def grade_action(
|
| 204 |
agent_output: str,
|
|
|
|
| 206 |
email_type: str,
|
| 207 |
) -> Tuple[float, str]:
|
| 208 |
"""
|
| 209 |
+
Grade one SOC triage decision.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
|
| 211 |
Returns
|
| 212 |
-------
|
| 213 |
(reward, message)
|
| 214 |
+
reward : float strictly in (0.0, 1.0) via safe_score().
|
| 215 |
message : short verdict string for logs and agent feedback.
|
| 216 |
"""
|
| 217 |
agent_action = agent_output.strip().upper()
|
| 218 |
expected_action = expected_output.strip().upper()
|
|
|
|
| 219 |
|
| 220 |
+
# resolve_task_id validates the type β raises ValueError on bad input
|
| 221 |
+
task_id = resolve_task_id(email_type)
|
| 222 |
is_threat = TASK_REGISTRY[task_id]["threat"]
|
| 223 |
|
| 224 |
+
# ββ 1. Perfect match βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 225 |
if agent_action == expected_action:
|
| 226 |
return R_PERFECT, "PERFECT_TRIAGE: Correct action taken"
|
| 227 |
|
| 228 |
+
# ββ 2. Security Breach β threat marked safe ββββββββββββββββββββββββββββ
|
|
|
|
| 229 |
if is_threat and agent_action == "MARK_SAFE":
|
| 230 |
return R_BREACH, "SECURITY_BREACH: Threat allowed into the network"
|
| 231 |
|
| 232 |
+
# ββ 3. Business Disruption β safe email blocked ββββββββββββββββββββββββ
|
|
|
|
| 233 |
if not is_threat and agent_action in {"BLOCK_DOMAIN", "QUARANTINE", "MOVE_TO_SPAM"}:
|
| 234 |
return R_DISRUPTION, "BUSINESS_DISRUPTION: Legitimate communication blocked"
|
| 235 |
|
| 236 |
+
# ββ 4. Partial credit βββββββββββββββββββββββββββοΏ½οΏ½ββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
if task_id == "task_malware" and agent_action == "QUARANTINE":
|
| 238 |
return R_MALWARE_QUARANTINE, "CAUTIOUS: Malware isolated β strong containment"
|
| 239 |
|
|
|
|
|
|
|
| 240 |
if task_id in {"task_phishing", "task_bec"} and agent_action == "QUARANTINE":
|
| 241 |
return R_PHISH_BEC_QUARANTINE, (
|
| 242 |
"UNDER_RESPONSE: Threat quarantined but domain still active β "
|
| 243 |
"consider BLOCK_DOMAIN"
|
| 244 |
)
|
| 245 |
|
|
|
|
| 246 |
if task_id == "task_spam" and agent_action == "BLOCK_DOMAIN":
|
| 247 |
return R_SPAM_BLOCK, (
|
| 248 |
"OVER_ESCALATION: BLOCK_DOMAIN is disproportionate for SPAM β "
|
| 249 |
"prefer MOVE_TO_SPAM"
|
| 250 |
)
|
| 251 |
|
|
|
|
| 252 |
if task_id == "task_spam" and agent_action == "QUARANTINE":
|
| 253 |
return R_SPAM_QUARANTINE, (
|
| 254 |
"OVER_ESCALATION: QUARANTINE wastes analyst capacity for SPAM β "
|
| 255 |
"prefer MOVE_TO_SPAM"
|
| 256 |
)
|
| 257 |
|
| 258 |
+
# ββ 5. General wrong procedure βββββββββββββββββββββββββββββββββββββββββ
|
| 259 |
return R_WRONG_PROCEDURE, "INCORRECT_PROCEDURE: Decision does not match security policy"
|
| 260 |
|
| 261 |
|
| 262 |
+
# ββ Episode-level aggregation ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 263 |
|
| 264 |
+
def calculate_overall_score(task_scores: list[float]) -> float:
|
| 265 |
"""
|
| 266 |
+
Compute a final benchmark score from all per-step rewards.
|
| 267 |
+
Returns a float strictly in (0.0, 1.0).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
"""
|
| 269 |
+
return _aggregate(task_scores)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
|
| 271 |
|
| 272 |
def calculate_per_task_scores(
|
| 273 |
task_score_map: dict[str, list[float]],
|
| 274 |
) -> dict[str, float]:
|
| 275 |
"""
|
| 276 |
+
Compute per-task-type average scores.
|
|
|
|
|
|
|
| 277 |
|
| 278 |
Parameters
|
| 279 |
----------
|
|
|
|
| 281 |
|
| 282 |
Returns
|
| 283 |
-------
|
| 284 |
+
{ task_id: score } β each value strictly in (0.0, 1.0).
|
| 285 |
"""
|
| 286 |
return {
|
| 287 |
+
task_id: _aggregate(scores)
|
| 288 |
for task_id, scores in task_score_map.items()
|
| 289 |
if scores
|
| 290 |
}
|