og-arin commited on
Commit
ccb41ec
Β·
verified Β·
1 Parent(s): a93cf12

Update grader.py

Browse files
Files changed (1) hide show
  1. grader.py +151 -75
grader.py CHANGED
@@ -1,56 +1,83 @@
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,
@@ -58,80 +85,124 @@ def grade_action(
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
  ----------
@@ -139,14 +210,19 @@ def calculate_overall_score(task_scores: list) -> float:
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)
 
 
1
  """
2
  grader.py – PhishGuard-Env SOC Triage Scoring Logic
3
+ ====================================================
4
 
5
+ REWARD CONTRACT β†’ OPEN INTERVAL (0.0, 1.0)
6
+ ---------------------------------------------
7
+ All rewards are STRICTLY greater than 0 and STRICTLY less than 1.
8
+ The endpoints 0 and 1 are NEVER returned. This is a hard invariant
9
+ enforced by the constant table below and by the calculate_overall_score()
10
+ clamp.
11
 
12
  Why open-interval?
13
+ β€’ 1.0 saturates the leaderboard and implies a theoretically perfect agent.
14
+ β€’ 0.0 is indistinguishable from a missing data-point in an RL pipeline.
15
+ β€’ Every decision carries a non-zero gradient signal so training never dies.
16
 
17
+ REWARD TABLE
18
+ ────────────────────────────────────────────────────────────────────────
19
  Outcome Reward Rationale
20
+ ───────────────────────────────────────────────────────────────────────
21
+ Perfect match (any type) 0.95 Near-ideal; headroom for 1.0 preserved
22
+ MALWARE β†’ QUARANTINE (correct containment) 0.75 Textbook isolation β€” full credit
23
+ PHISH/BEC β†’ QUARANTINE (under-response) 0.60 Stopped but domain still active
24
+ SPAM β†’ BLOCK_DOMAIN (over-escalation) 0.40 Disproportionate but not harmful
25
+ SPAM β†’ QUARANTINE (lighter escalation) 0.35 Wastes analyst queue capacity
26
+ General wrong procedure (no harm) 0.10 Wrong, no security/ops impact
27
+ Business Disruption (SAFE β†’ blocked) 0.05 Operational cost; breaches threshold
28
+ Security Breach (threat β†’ MARK_SAFE) 0.02 Catastrophic; minimum non-zero signal
29
+
30
+ HEALTH-DRAIN THRESHOLD
31
+ ────────────────────────────────────────────────────────────────────────
32
  HEALTH_DRAIN_THRESHOLD = 0.15
33
+
34
+ reward < 0.15 β†’ agent loses one life. Covers:
35
+ Security Breach (0.02), Business Disruption (0.05), Wrong Procedure (0.10)
36
+
37
+ Cautious / partial-credit scores (β‰₯ 0.35) NEVER drain health.
38
+
39
+ VALID AGENT ACTIONS
40
+ ────────────────────────────────────────────────────────────────────────
41
+ MARK_SAFE – deliver to inbox (use ONLY for confirmed-safe email)
42
+ MOVE_TO_SPAM – bulk/unsolicited mail; no active threat
43
+ QUARANTINE – hold for analyst review
44
+ BLOCK_DOMAIN – perimeter block; for confirmed phishing / BEC sources
45
  """
46
 
47
+ from __future__ import annotations
48
+
49
  from typing import Tuple
50
 
51
+
52
+ # ══════════════════════════════════════════════════════════════════════════════
53
+ # REWARD CONSTANTS
54
+ # ══════════════════════════════════════════════════════════════════════════════
55
+ # All numeric reward values are defined ONCE here.
56
+ # env.py and inference.py import these β€” neither file hard-codes numbers.
57
+
58
+ R_PERFECT = 0.95 # Perfect triage decision
59
+ R_MALWARE_QUARANTINE = 0.75 # MALWARE isolated β€” strong containment
60
+ R_PHISH_BEC_QUARANTINE = 0.60 # PHISH/BEC held but domain still live
61
+ R_SPAM_BLOCK = 0.40 # SPAM β†’ BLOCK_DOMAIN (over-escalation)
62
+ R_SPAM_QUARANTINE = 0.35 # SPAM β†’ QUARANTINE (lighter over-escalation)
63
+ R_WRONG_PROCEDURE = 0.10 # Wrong action, no security/operational harm
64
+ R_DISRUPTION = 0.05 # Business Disruption β€” SAFE email blocked
65
+ R_BREACH = 0.02 # Security Breach β€” threat allowed through
66
 
67
  # env.py compares `reward < HEALTH_DRAIN_THRESHOLD` to decide life-loss.
68
+ # Must remain above R_BREACH and R_DISRUPTION and R_WRONG_PROCEDURE,
69
+ # and below R_SPAM_QUARANTINE so cautious calls never lose a life.
70
+ HEALTH_DRAIN_THRESHOLD = 0.15
71
+
72
+ # Convenience set used by grade_action() internal logic
73
+ _THREAT_TYPES = frozenset({"PHISH", "BEC", "MALWARE", "SPAM"})
74
+ _BLOCKED_MOVES = frozenset({"BLOCK_DOMAIN", "QUARANTINE", "MOVE_TO_SPAM"})
75
+ _VALID_ACTIONS = frozenset({"MARK_SAFE", "MOVE_TO_SPAM", "QUARANTINE", "BLOCK_DOMAIN"})
76
+
77
 
78
+ # ══════════════════════════════════════════════════════════════════════════════
79
+ # GRADE_ACTION
80
+ # ══════════════════════════════════════════════════════════════════════════════
81
 
82
  def grade_action(
83
  agent_output: str,
 
85
  email_type: str,
86
  ) -> Tuple[float, str]:
87
  """
88
+ Grade one SOC triage decision and return a reward in (0.0, 1.0).
89
 
90
  Parameters
91
  ----------
92
+ agent_output : Raw action string from the LLM agent (case-insensitive).
93
  expected_output : Ground-truth correct action for this scenario.
94
  email_type : Threat category β€” PHISH | BEC | MALWARE | SPAM | SAFE.
95
 
96
  Returns
97
  -------
98
  (reward, message)
99
+ reward : float strictly in (0.0, 1.0) β€” NEVER 0, NEVER 1.
100
  message : short verdict string for logs and agent feedback.
101
+
102
+ Decision Tree
103
+ -------------
104
+ 1. PERFECT MATCH β†’ 0.95
105
+ 2. SECURITY BREACH β†’ 0.02 (threat rubber-stamped into inbox)
106
+ 3. BUSINESS DISRUPTION β†’ 0.05 (clean email blocked)
107
+ 4. Cautious partial credit:
108
+ MALWARE β†’ QUARANTINE β†’ 0.75
109
+ PHISH/BEC β†’ QUARANTINE β†’ 0.60
110
+ SPAM β†’ BLOCK_DOMAIN β†’ 0.40
111
+ SPAM β†’ QUARANTINE β†’ 0.35
112
+ 5. General wrong procedure β†’ 0.10 (catch-all; no harm done)
113
  """
114
  agent_action = agent_output.strip().upper()
115
  expected_action = expected_output.strip().upper()
116
  etype = email_type.strip().upper()
117
 
118
+ # ── Normalise unexpected agent output ────────────────────────────────────
119
+ # If the LLM produces an unrecognised token, treat as wrong procedure
120
+ # rather than raising (avoids crashing the entire episode).
121
+ if agent_action not in _VALID_ACTIONS:
122
+ return (
123
+ R_WRONG_PROCEDURE,
124
+ f"INVALID_ACTION: '{agent_action}' is not a recognised triage action",
125
+ )
126
 
127
+ # ── 1. PERFECT MATCH ─────────────────────────────────────────────────────
128
  if agent_action == expected_action:
129
  return R_PERFECT, "PERFECT_TRIAGE: Correct action taken"
130
 
131
+ # ── 2. SECURITY BREACH β€” most severe outcome ──────────────────────────────
132
+ # A dangerous email was rubber-stamped as safe and delivered to the inbox.
133
+ # This covers ALL threat types (PHISH, BEC, MALWARE, SPAM).
134
+ if etype in _THREAT_TYPES and agent_action == "MARK_SAFE":
135
+ return (
136
+ R_BREACH,
137
+ f"SECURITY_BREACH: {etype} threat allowed into the network unimpeded",
138
+ )
139
 
140
+ # ── 3. BUSINESS DISRUPTION β€” severe false-positive ───────────────────────
141
+ # A clean, legitimate email was blocked, quarantined, or spammed.
142
+ if etype == "SAFE" and agent_action in _BLOCKED_MOVES:
143
+ return (
144
+ R_DISRUPTION,
145
+ "BUSINESS_DISRUPTION: Legitimate communication was incorrectly blocked",
146
+ )
147
 
148
+ # ── 4. PARTIAL CREDIT β€” cautious but sub-optimal ─────────────────────────
149
 
150
+ # MALWARE β†’ QUARANTINE: isolation is the textbook containment response.
151
+ # The scenario may require BLOCK_DOMAIN in future expansions; this branch
152
+ # handles that gracefully and still awards near-full credit.
153
  if etype == "MALWARE" and agent_action == "QUARANTINE":
154
+ return (
155
+ R_MALWARE_QUARANTINE,
156
+ "CAUTIOUS: Malware isolated via QUARANTINE β€” strong containment; "
157
+ "no further propagation risk",
158
+ )
159
 
160
+ # PHISH/BEC β†’ QUARANTINE: threat is held but the sending domain stays live.
161
+ # Preferred action is BLOCK_DOMAIN; QUARANTINE is under-response.
162
  if etype in {"PHISH", "BEC"} and agent_action == "QUARANTINE":
163
+ return (
164
+ R_PHISH_BEC_QUARANTINE,
165
+ f"UNDER_RESPONSE: {etype} quarantined but source domain still active "
166
+ "β€” consider BLOCK_DOMAIN to prevent further delivery",
167
  )
168
 
169
+ # SPAM β†’ BLOCK_DOMAIN: disproportionate escalation for mere spam;
170
+ # burns perimeter block-list capacity on low-severity senders.
171
  if etype == "SPAM" and agent_action == "BLOCK_DOMAIN":
172
+ return (
173
+ R_SPAM_BLOCK,
174
  "OVER_ESCALATION: BLOCK_DOMAIN is disproportionate for SPAM β€” "
175
+ "prefer MOVE_TO_SPAM to avoid exhausting block-list resources",
176
  )
177
 
178
+ # SPAM β†’ QUARANTINE: lighter over-escalation; clogs the analyst review queue.
179
  if etype == "SPAM" and agent_action == "QUARANTINE":
180
+ return (
181
+ R_SPAM_QUARANTINE,
182
  "OVER_ESCALATION: QUARANTINE wastes analyst capacity for SPAM β€” "
183
+ "prefer MOVE_TO_SPAM",
184
  )
185
 
186
+ # ── 5. GENERAL INCORRECT PROCEDURE (catch-all) ────────────────────────────
187
+ # Wrong action with no direct security or operational breach impact.
188
+ return (
189
+ R_WRONG_PROCEDURE,
190
+ f"INCORRECT_PROCEDURE: '{agent_action}' does not match security policy "
191
+ f"for {etype} email (expected: {expected_action})",
192
+ )
193
+
194
 
195
+ # ═════════════════════════��════════════════════════════════════════════════════
196
+ # CALCULATE_OVERALL_SCORE
197
+ # ══════════════════════════════════════════════════════════════════════════════
198
 
199
  def calculate_overall_score(task_scores: list) -> float:
200
  """
201
+ Compute the final benchmark score from a list of per-step rewards.
202
 
203
+ The result is clamped to [R_BREACH, R_PERFECT] β€” maintaining the
204
  open-interval contract β€” so downstream consumers always receive a float
205
+ strictly greater than 0 and strictly less than 1.
206
 
207
  Parameters
208
  ----------
 
210
 
211
  Returns
212
  -------
213
+ float in (R_BREACH, R_PERFECT) β€” always a valid open-interval value.
214
+
215
+ Edge cases
216
+ ----------
217
+ β€’ Empty list β†’ R_BREACH (minimum signal, not zero)
218
+ β€’ Single-step list β†’ that step's reward (clamped)
219
+ β€’ All-perfect run β†’ 0.95 (R_PERFECT), not 1.0
220
  """
221
  if not task_scores:
222
+ return R_BREACH # No tasks completed: return minimum signal, not zero
 
223
 
224
  raw_avg = sum(task_scores) / len(task_scores)
225
 
226
+ # Clamp strictly within the open-interval boundary constants.
227
  clamped = max(R_BREACH, min(R_PERFECT, raw_avg))
228
  return round(clamped, 4)