og-arin commited on
Commit
97c73a3
Β·
verified Β·
1 Parent(s): 489366d

Update grader.py

Browse files
Files changed (1) hide show
  1. grader.py +152 -27
grader.py CHANGED
@@ -1,27 +1,152 @@
1
- def grade_phishing_task(agent_output: str, expected_output: str) -> float:
2
- """
3
- Evaluates the agent's action against the ground truth.
4
- Returns a score between 0.0 and 1.0.
5
- """
6
- # Normalize inputs to avoid case-sensitivity issues
7
- agent_action = agent_output.strip().upper()
8
- expected_action = expected_output.strip().upper()
9
-
10
- # Perfect match: Maximum reward
11
- if agent_action == expected_action:
12
- return 1.0
13
-
14
- # Partial credit: If agent quarantined instead of blocking (safe but cautious)
15
- if agent_action == "QUARANTINE" and expected_action == "BLOCK_DOMAIN":
16
- return 0.5
17
-
18
- # Failure: Incorrect categorization or allowing a threat
19
- return 0.0
20
-
21
- def calculate_overall_score(task_scores: list) -> float:
22
- """
23
- Calculates the final reproducible score for the baseline report.
24
- """
25
- if not task_scores:
26
- return 0.0
27
- return sum(task_scores) / len(task_scores)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ """
3
+ grader.py – PhishGuard-Env SOC Triage Scoring Logic
4
+ =====================================================
5
+
6
+ REWARD SCALE CONTRACT
7
+ ---------------------
8
+ All rewards are in the OPEN interval (0.0, 1.0).
9
+ The endpoints 0 and 1 are NEVER returned. This is a hard invariant.
10
+
11
+ Why open-interval?
12
+ β€’ 1.0 would saturate the leaderboard and imply a theoretically perfect agent.
13
+ β€’ 0.0 would be indistinguishable from a missing data-point in an RL pipeline.
14
+ β€’ Every decision carries a non-zero signal so the training gradient never dies.
15
+
16
+ Full reward table
17
+ -----------------
18
+ Outcome Reward Rationale
19
+ ─────────────────────────────────────────────────────────────────────────────
20
+ Perfect match (any type) 0.95 Near-ideal; headroom preserved
21
+ MALWARE β†’ QUARANTINE (safe containment) 0.75 Correct call, no partial penalty
22
+ PHISH/BEC β†’ QUARANTINE (under-response) 0.60 Stopped but domain still lives
23
+ SPAM β†’ BLOCK_DOMAIN (over-escalation) 0.40 Too aggressive, not wrong
24
+ SPAM β†’ QUARANTINE (lighter escalation) 0.35 Wastes analyst queue
25
+ General wrong procedure (no harm) 0.10 Wrong but no security/ops impact
26
+ Business Disruption (SAFE β†’ blocked) 0.05 Ops cost, below drain threshold
27
+ Security Breach (threat β†’ MARK_SAFE) 0.02 Catastrophic; minimum signal
28
+
29
+ Health-drain threshold (consumed by env.py)
30
+ -------------------------------------------
31
+ HEALTH_DRAIN_THRESHOLD = 0.15
32
+ reward < 0.15 β†’ lose one life.
33
+ This covers Security Breach (0.02), Business Disruption (0.05), and
34
+ General Wrong Procedure (0.10). Cautious/partial-credit scores never
35
+ drain health, which is the intended design.
36
+ """
37
+
38
+ from typing import Tuple
39
+
40
+ # ── Reward constants ───────────────────────────────────────────────────────────
41
+ # Change values here only β€” nowhere else in the codebase hard-codes these.
42
+ R_PERFECT = 0.95
43
+ R_MALWARE_QUARANTINE = 0.75
44
+ R_PHISH_BEC_QUARANTINE = 0.60
45
+ R_SPAM_BLOCK = 0.40
46
+ R_SPAM_QUARANTINE = 0.35
47
+ R_WRONG_PROCEDURE = 0.10
48
+ R_DISRUPTION = 0.05
49
+ R_BREACH = 0.02
50
+
51
+ # env.py compares `reward < HEALTH_DRAIN_THRESHOLD` to decide life-loss.
52
+ HEALTH_DRAIN_THRESHOLD = 0.15
53
+
54
+
55
+ def grade_action(
56
+ agent_output: str,
57
+ expected_output: str,
58
+ email_type: str,
59
+ ) -> Tuple[float, str]:
60
+ """
61
+ Grade one triage decision and return a reward in the open interval (0, 1).
62
+
63
+ Parameters
64
+ ----------
65
+ agent_output : Raw action string from the LLM (case-insensitive).
66
+ expected_output : Ground-truth correct action for this scenario.
67
+ email_type : Threat category β€” PHISH | BEC | MALWARE | SPAM | SAFE.
68
+
69
+ Returns
70
+ -------
71
+ (reward, message)
72
+ reward : float strictly in (0.0, 1.0) β€” never 0, never 1.
73
+ message : short verdict string for logs and agent feedback.
74
+ """
75
+ agent_action = agent_output.strip().upper()
76
+ expected_action = expected_output.strip().upper()
77
+ etype = email_type.strip().upper()
78
+
79
+ THREAT_TYPES = {"PHISH", "BEC", "MALWARE", "SPAM"}
80
+
81
+ # ── 1. PERFECT MATCH ──────────────────────────────────────────────────────
82
+ if agent_action == expected_action:
83
+ return R_PERFECT, "PERFECT_TRIAGE: Correct action taken"
84
+
85
+ # ── 2. SECURITY BREACH β€” most severe ──────────────────────────────────────
86
+ # A dangerous email was rubber-stamped into the inbox.
87
+ if etype in THREAT_TYPES and agent_action == "MARK_SAFE":
88
+ return R_BREACH, "SECURITY_BREACH: Threat allowed into the network"
89
+
90
+ # ── 3. BUSINESS DISRUPTION β€” severe false positive ────────────────────────
91
+ # A clean email was blocked or quarantined.
92
+ if etype == "SAFE" and agent_action in {"BLOCK_DOMAIN", "QUARANTINE", "MOVE_TO_SPAM"}:
93
+ return R_DISRUPTION, "BUSINESS_DISRUPTION: Legitimate communication blocked"
94
+
95
+ # ── 4. PARTIAL CREDIT β€” cautious but suboptimal ───────────────────────────
96
+
97
+ # MALWARE β†’ QUARANTINE: isolation is the textbook response.
98
+ # This branch fires only when the scenario's `correct` field differs
99
+ # (edge case for future scenario expansions β€” included for robustness).
100
+ if etype == "MALWARE" and agent_action == "QUARANTINE":
101
+ return R_MALWARE_QUARANTINE, "CAUTIOUS: Malware isolated β€” strong containment"
102
+
103
+ # PHISH/BEC β†’ QUARANTINE: threat is held but the domain keeps sending.
104
+ if etype in {"PHISH", "BEC"} and agent_action == "QUARANTINE":
105
+ return R_PHISH_BEC_QUARANTINE, (
106
+ "UNDER_RESPONSE: Threat quarantined but domain still active β€” "
107
+ "consider BLOCK_DOMAIN"
108
+ )
109
+
110
+ # SPAM β†’ BLOCK_DOMAIN: blocks the domain, but permanently for mere spam.
111
+ if etype == "SPAM" and agent_action == "BLOCK_DOMAIN":
112
+ return R_SPAM_BLOCK, (
113
+ "OVER_ESCALATION: BLOCK_DOMAIN is disproportionate for SPAM β€” "
114
+ "prefer MOVE_TO_SPAM"
115
+ )
116
+
117
+ # SPAM β†’ QUARANTINE: lighter over-escalation, still clogs the analyst queue.
118
+ if etype == "SPAM" and agent_action == "QUARANTINE":
119
+ return R_SPAM_QUARANTINE, (
120
+ "OVER_ESCALATION: QUARANTINE wastes analyst capacity for SPAM β€” "
121
+ "prefer MOVE_TO_SPAM"
122
+ )
123
+
124
+ # ── 5. GENERAL INCORRECT PROCEDURE ───────────────────────────────────────
125
+ return R_WRONG_PROCEDURE, "INCORRECT_PROCEDURE: Decision does not match security policy"
126
+
127
+
128
+ def calculate_overall_score(task_scores: list) -> float:
129
+ """
130
+ Compute the final benchmark score from a list of per-task rewards.
131
+
132
+ The result is clamped to (R_BREACH, R_PERFECT) β€” matching the per-step
133
+ open-interval contract β€” so downstream consumers always receive a float
134
+ that is strictly greater than 0 and strictly less than 1.
135
+
136
+ Parameters
137
+ ----------
138
+ task_scores : list of floats, each in (0.0, 1.0).
139
+
140
+ Returns
141
+ -------
142
+ float in (0.0, 1.0) β€” never exactly 0 or 1.
143
+ """
144
+ if not task_scores:
145
+ # No tasks completed: return the minimum signal value, not zero.
146
+ return R_BREACH
147
+
148
+ raw_avg = sum(task_scores) / len(task_scores)
149
+
150
+ # Clamp strictly within the open interval boundaries.
151
+ clamped = max(R_BREACH, min(R_PERFECT, raw_avg))
152
+ return round(clamped, 4)