Drac0528 commited on
Commit
2110640
·
verified ·
1 Parent(s): f4fc63c

Delete server

Browse files
server/Dockerfile DELETED
@@ -1,49 +0,0 @@
1
- ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
2
- FROM ${BASE_IMAGE} AS builder
3
-
4
- WORKDIR /app
5
-
6
- COPY envs/code_security_auditor_env /app/env
7
- WORKDIR /app/env
8
-
9
- RUN if ! command -v uv >/dev/null 2>&1; then \
10
- curl -LsSf https://astral.sh/uv/install.sh | sh && \
11
- mv /root/.local/bin/uv /usr/local/bin/uv && \
12
- mv /root/.local/bin/uvx /usr/local/bin/uvx; \
13
- fi
14
-
15
- RUN apt-get update && apt-get install -y --no-install-recommends \
16
- git \
17
- curl \
18
- ca-certificates \
19
- && rm -rf /var/lib/apt/lists/*
20
-
21
- RUN --mount=type=cache,target=/root/.cache/uv \
22
- if [ -f uv.lock ]; then \
23
- uv sync --frozen --no-install-project --no-editable; \
24
- else \
25
- uv sync --no-install-project --no-editable; \
26
- fi
27
-
28
- RUN --mount=type=cache,target=/root/.cache/uv \
29
- if [ -f uv.lock ]; then \
30
- uv sync --frozen --no-editable; \
31
- else \
32
- uv sync --no-editable; \
33
- fi
34
-
35
- FROM ${BASE_IMAGE}
36
-
37
- WORKDIR /app
38
- COPY --from=builder /app/env/.venv /app/.venv
39
- COPY --from=builder /app/env /app/env
40
-
41
- ENV PATH="/app/.venv/bin:$PATH"
42
- ENV PYTHONPATH="/app/env:$PYTHONPATH"
43
- ENV PYTHONUNBUFFERED=1
44
- ENV ENABLE_WEB_INTERFACE=true
45
-
46
- HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
47
- CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
48
-
49
- CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
server/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Server package for Code Security Auditor environment."""
 
 
server/__pycache__/__init__.cpython-312.pyc DELETED
Binary file (263 Bytes)
 
server/__pycache__/__init__.cpython-314.pyc DELETED
Binary file (187 Bytes)
 
server/__pycache__/app.cpython-314.pyc DELETED
Binary file (1.34 kB)
 
server/__pycache__/grader.cpython-312.pyc DELETED
Binary file (6.56 kB)
 
server/__pycache__/grader.cpython-314.pyc DELETED
Binary file (7.38 kB)
 
server/__pycache__/security_environment.cpython-312.pyc DELETED
Binary file (17.9 kB)
 
server/__pycache__/security_environment.cpython-314.pyc DELETED
Binary file (20.1 kB)
 
server/__pycache__/tasks.cpython-312.pyc DELETED
Binary file (8.78 kB)
 
server/__pycache__/tasks.cpython-314.pyc DELETED
Binary file (9.1 kB)
 
server/app.py DELETED
@@ -1,33 +0,0 @@
1
- from __future__ import annotations
2
-
3
- try:
4
- from core.env_server.http_server import create_app
5
- except ImportError:
6
- try:
7
- from openenv.core.env_server.http_server import create_app
8
- except ImportError:
9
- from openenv_core.env_server.http_server import create_app
10
-
11
- try:
12
- from ..models import CodeSecurityAction, CodeSecurityObservation
13
- from .security_environment import CodeSecurityAuditorEnvironment
14
- except ImportError:
15
- from models import CodeSecurityAction, CodeSecurityObservation
16
- from server.security_environment import CodeSecurityAuditorEnvironment
17
-
18
- app = create_app(
19
- CodeSecurityAuditorEnvironment,
20
- CodeSecurityAction,
21
- CodeSecurityObservation,
22
- env_name="code_security_auditor_env",
23
- )
24
-
25
-
26
- def main() -> None:
27
- import uvicorn
28
-
29
- uvicorn.run(app, host="0.0.0.0", port=8000)
30
-
31
-
32
- if __name__ == "__main__":
33
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
server/grader.py DELETED
@@ -1,181 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from typing import Iterable, Optional
5
-
6
- from .tasks import SEVERITY_WEIGHTS, TARGET_CONFIDENCE, TaskSpec, VulnerabilitySpec
7
-
8
-
9
- @dataclass(frozen=True)
10
- class FindingEvaluation:
11
- component_score: float
12
- matched_vulnerability_id: Optional[str]
13
- is_confirmed_match: bool
14
- feedback: str
15
- confidence_calibration: float
16
-
17
-
18
- def _line_overlap_score(submitted_start: int, submitted_end: int, target_line: int) -> float:
19
- if submitted_start <= target_line <= submitted_end:
20
- return 1.0
21
- min_distance = min(abs(target_line - submitted_start), abs(target_line - submitted_end))
22
- if min_distance <= 2:
23
- return 0.6
24
- if min_distance <= 5:
25
- return 0.3
26
- return 0.0
27
-
28
-
29
- def _best_candidate(
30
- task: TaskSpec,
31
- filename: str,
32
- vuln_type: str,
33
- severity: str,
34
- line_start: int,
35
- line_end: int,
36
- ) -> tuple[Optional[VulnerabilitySpec], float, float, float, float]:
37
- best_target = None
38
- best_score = -1.0
39
- best_type_match = 0.0
40
- best_line_match = 0.0
41
- best_severity_match = 0.0
42
-
43
- for target in task.vulnerabilities:
44
- file_match = 1.0 if target.filename == filename else 0.0
45
- type_match = 1.0 if target.vuln_type == vuln_type else 0.0
46
- severity_match = 1.0 if target.severity == severity else 0.0
47
- line_match = _line_overlap_score(line_start, line_end, target.line)
48
-
49
- candidate_score = (
50
- 0.35 * file_match
51
- + 0.30 * type_match
52
- + 0.20 * line_match
53
- + 0.15 * severity_match
54
- )
55
-
56
- if candidate_score > best_score:
57
- best_score = candidate_score
58
- best_target = target
59
- best_type_match = type_match
60
- best_line_match = line_match
61
- best_severity_match = severity_match
62
-
63
- return best_target, max(best_score, 0.0), best_type_match, best_line_match, best_severity_match
64
-
65
-
66
- def evaluate_finding(
67
- *,
68
- task: TaskSpec,
69
- filename: str,
70
- vuln_type: str,
71
- severity: str,
72
- line_start: int,
73
- line_end: int,
74
- confidence: float,
75
- matched_already: Iterable[str],
76
- ) -> FindingEvaluation:
77
- target, structure_score, type_match, line_match, severity_match = _best_candidate(
78
- task,
79
- filename,
80
- vuln_type,
81
- severity,
82
- line_start,
83
- line_end,
84
- )
85
-
86
- if target is None:
87
- return FindingEvaluation(
88
- component_score=0.0,
89
- matched_vulnerability_id=None,
90
- is_confirmed_match=False,
91
- feedback="No plausible vulnerability match for this finding.",
92
- confidence_calibration=0.0,
93
- )
94
-
95
- target_conf = TARGET_CONFIDENCE[target.severity]
96
- calibration = max(0.0, 1.0 - abs(confidence - target_conf))
97
-
98
- component_score = 0.8 * structure_score + 0.2 * calibration
99
- component_score = max(0.0, min(1.0, component_score))
100
-
101
- confirmed = (
102
- target.filename == filename
103
- and type_match == 1.0
104
- and line_match >= 0.6
105
- and severity_match == 1.0
106
- )
107
-
108
- if target.id in set(matched_already) and confirmed:
109
- return FindingEvaluation(
110
- component_score=0.25 * component_score,
111
- matched_vulnerability_id=target.id,
112
- is_confirmed_match=False,
113
- feedback="Duplicate of a previously confirmed vulnerability.",
114
- confidence_calibration=calibration,
115
- )
116
-
117
- if confirmed:
118
- return FindingEvaluation(
119
- component_score=component_score,
120
- matched_vulnerability_id=target.id,
121
- is_confirmed_match=True,
122
- feedback="Confirmed vulnerability: file/type/line/severity align with ground truth.",
123
- confidence_calibration=calibration,
124
- )
125
-
126
- if target.filename != filename:
127
- hint = "Wrong file."
128
- elif type_match == 0.0:
129
- hint = "Correct file, vulnerability type mismatch."
130
- elif line_match < 0.6:
131
- hint = "Correct file/type, but location is off."
132
- elif severity_match == 0.0:
133
- hint = "Severity mismatch."
134
- else:
135
- hint = "Partial match, refine details."
136
-
137
- return FindingEvaluation(
138
- component_score=component_score,
139
- matched_vulnerability_id=None,
140
- is_confirmed_match=False,
141
- feedback=hint,
142
- confidence_calibration=calibration,
143
- )
144
-
145
-
146
- def final_grade(
147
- *,
148
- task: TaskSpec,
149
- confirmed_vulnerability_ids: Iterable[str],
150
- findings_count: int,
151
- false_positive_count: int,
152
- duplicate_count: int,
153
- avg_component_score: float,
154
- avg_confidence_calibration: float,
155
- ) -> float:
156
- confirmed_ids = set(confirmed_vulnerability_ids)
157
-
158
- total_weight = sum(SEVERITY_WEIGHTS[v.severity] for v in task.vulnerabilities)
159
- covered_weight = sum(
160
- SEVERITY_WEIGHTS[v.severity] for v in task.vulnerabilities if v.id in confirmed_ids
161
- )
162
- weighted_recall = (covered_weight / total_weight) if total_weight > 0 else 0.0
163
-
164
- precision = (len(confirmed_ids) / findings_count) if findings_count > 0 else 0.0
165
-
166
- fp_penalty = min(0.5, 0.08 * false_positive_count)
167
- dup_penalty = min(0.2, 0.05 * duplicate_count)
168
- volume_penalty = 0.0
169
- optimal_findings = len(task.vulnerabilities) + 1
170
- if findings_count > optimal_findings:
171
- volume_penalty = min(0.2, 0.03 * (findings_count - optimal_findings))
172
-
173
- score = (
174
- 0.55 * weighted_recall
175
- + 0.20 * precision
176
- + 0.15 * avg_component_score
177
- + 0.10 * avg_confidence_calibration
178
- )
179
- score -= fp_penalty + dup_penalty + volume_penalty
180
-
181
- return max(0.0, min(1.0, score))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
server/security_environment.py DELETED
@@ -1,386 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import random
4
- import uuid
5
- from typing import Any, Optional
6
-
7
- try:
8
- from core.env_server.interfaces import Environment
9
- except ImportError:
10
- try:
11
- from openenv.core.env_server.interfaces import Environment
12
- except ImportError:
13
- from openenv_core.env_server.interfaces import Environment
14
-
15
- try:
16
- from ..models import (
17
- CodeSecurityAction,
18
- CodeSecurityObservation,
19
- CodeSecurityState,
20
- FindingRecord,
21
- )
22
- from .grader import evaluate_finding, final_grade
23
- from .tasks import TaskSpec, get_task, list_task_ids
24
- except ImportError:
25
- from models import (
26
- CodeSecurityAction,
27
- CodeSecurityObservation,
28
- CodeSecurityState,
29
- FindingRecord,
30
- )
31
- from server.grader import evaluate_finding, final_grade
32
- from server.tasks import TaskSpec, get_task, list_task_ids
33
-
34
-
35
- class CodeSecurityAuditorEnvironment(
36
- Environment[CodeSecurityAction, CodeSecurityObservation, CodeSecurityState]
37
- ):
38
- """Real-world code security auditing simulator with deterministic graders."""
39
-
40
- SUPPORTS_CONCURRENT_SESSIONS = True
41
-
42
- def __init__(self, default_task_id: str = "easy"):
43
- self._default_task_id = default_task_id
44
- self._task_cursor = 0
45
- self._task: Optional[TaskSpec] = None
46
- self._state = CodeSecurityState()
47
-
48
- def reset(
49
- self,
50
- seed: Optional[int] = None,
51
- episode_id: Optional[str] = None,
52
- **kwargs: Any,
53
- ) -> CodeSecurityObservation:
54
- requested_task = kwargs.get("task_id") or kwargs.get("task")
55
-
56
- if requested_task is not None:
57
- task = get_task(str(requested_task))
58
- elif seed is not None:
59
- rng = random.Random(seed)
60
- task = get_task(rng.choice(list_task_ids()))
61
- elif self._default_task_id:
62
- task = get_task(self._default_task_id)
63
- else:
64
- task_order = list_task_ids()
65
- task = get_task(task_order[self._task_cursor % len(task_order)])
66
- self._task_cursor += 1
67
-
68
- self._task = task
69
- self._state = CodeSecurityState(
70
- episode_id=episode_id or str(uuid.uuid4()),
71
- step_count=0,
72
- task_id=task.id,
73
- task_title=task.title,
74
- difficulty=task.difficulty,
75
- objective=task.objective,
76
- max_steps=task.max_steps,
77
- inspected_files=[],
78
- findings_submitted=[],
79
- matched_vulnerability_ids=[],
80
- false_positive_count=0,
81
- duplicate_submission_count=0,
82
- quality_multiplier=1.0,
83
- final_score=None,
84
- )
85
-
86
- return self._build_observation(
87
- reward=0.0,
88
- done=False,
89
- feedback=(
90
- "Audit started. Use inspect_file before submit_finding. "
91
- "Finish with submit_final_report."
92
- ),
93
- focused_file=None,
94
- excerpt="",
95
- extra_metadata={
96
- "available_task_ids": list_task_ids(),
97
- "task_id": task.id,
98
- },
99
- )
100
-
101
- def step(
102
- self,
103
- action: CodeSecurityAction,
104
- timeout_s: Optional[float] = None,
105
- **kwargs: Any,
106
- ) -> CodeSecurityObservation:
107
- del timeout_s, kwargs
108
-
109
- task = self._require_task()
110
-
111
- if self._state.final_score is not None:
112
- return self._build_observation(
113
- reward=0.0,
114
- done=True,
115
- feedback="Episode already terminated. Call reset() to start a new task.",
116
- focused_file=None,
117
- excerpt="",
118
- )
119
-
120
- self._state.step_count += 1
121
- feedback = ""
122
- reward = 0.0
123
- focused_file = None
124
- excerpt = ""
125
-
126
- if action.action_type == "inspect_file":
127
- reward, feedback, focused_file, excerpt = self._handle_inspect_file(action, task)
128
- elif action.action_type == "submit_finding":
129
- reward, feedback = self._handle_submit_finding(action, task)
130
- elif action.action_type == "submit_final_report":
131
- reward, feedback = self._handle_submit_final_report()
132
- else:
133
- feedback = f"Unsupported action_type={action.action_type}."
134
- self._degrade_quality(0.03)
135
-
136
- done = self._state.final_score is not None
137
-
138
- if not done and self._state.step_count >= self._state.max_steps:
139
- score = self._compute_final_score(task)
140
- self._state.final_score = score
141
- done = True
142
- reward = score
143
- feedback = (
144
- f"Max steps reached. Auto-finalized audit score={score:.3f}. "
145
- "Use fewer but higher-quality findings to improve precision."
146
- )
147
-
148
- return self._build_observation(
149
- reward=reward,
150
- done=done,
151
- feedback=feedback,
152
- focused_file=focused_file,
153
- excerpt=excerpt,
154
- extra_metadata={
155
- "last_action_error": None,
156
- },
157
- )
158
-
159
- @property
160
- def state(self) -> CodeSecurityState:
161
- return self._state
162
-
163
- def _require_task(self) -> TaskSpec:
164
- if self._task is None:
165
- raise RuntimeError("Environment has no active task. Call reset() first.")
166
- return self._task
167
-
168
- def _degrade_quality(self, amount: float) -> None:
169
- self._state.quality_multiplier = max(0.2, self._state.quality_multiplier - amount)
170
-
171
- def _format_file(self, content: str) -> str:
172
- lines = content.splitlines()
173
- numbered = [f"{idx + 1:>3}: {line}" for idx, line in enumerate(lines)]
174
- return "\n".join(numbered)
175
-
176
- def _handle_inspect_file(
177
- self,
178
- action: CodeSecurityAction,
179
- task: TaskSpec,
180
- ) -> tuple[float, str, Optional[str], str]:
181
- filename = action.filename or ""
182
- if filename not in task.repository:
183
- self._degrade_quality(0.04)
184
- return 0.0, f"Unknown file '{filename}'.", filename or None, ""
185
-
186
- first_time = filename not in self._state.inspected_files
187
- if first_time:
188
- self._state.inspected_files.append(filename)
189
-
190
- excerpt = self._format_file(task.repository[filename])
191
-
192
- unmatched_in_file = any(
193
- vuln.filename == filename and vuln.id not in self._state.matched_vulnerability_ids
194
- for vuln in task.vulnerabilities
195
- )
196
-
197
- if first_time and unmatched_in_file:
198
- reward = 0.04
199
- feedback = "Useful inspection: this file likely contains unresolved security issues."
200
- elif first_time:
201
- reward = 0.02
202
- feedback = "Inspection noted. No strong security signal yet."
203
- else:
204
- reward = 0.0
205
- feedback = "File already inspected; repeated reads do not improve score."
206
- self._degrade_quality(0.01)
207
-
208
- return reward, feedback, filename, excerpt
209
-
210
- def _handle_submit_finding(
211
- self,
212
- action: CodeSecurityAction,
213
- task: TaskSpec,
214
- ) -> tuple[float, str]:
215
- required_missing = []
216
- if not action.filename:
217
- required_missing.append("filename")
218
- if action.line_start is None:
219
- required_missing.append("line_start")
220
- if not action.vuln_type:
221
- required_missing.append("vuln_type")
222
- if not action.severity:
223
- required_missing.append("severity")
224
-
225
- if required_missing:
226
- self._degrade_quality(0.05)
227
- missing = ", ".join(required_missing)
228
- return 0.0, f"Incomplete finding. Missing fields: {missing}."
229
-
230
- line_end = action.line_end if action.line_end is not None else action.line_start
231
-
232
- evaluation = evaluate_finding(
233
- task=task,
234
- filename=action.filename,
235
- vuln_type=action.vuln_type,
236
- severity=action.severity,
237
- line_start=action.line_start,
238
- line_end=line_end,
239
- confidence=action.confidence,
240
- matched_already=self._state.matched_vulnerability_ids,
241
- )
242
-
243
- finding_id = f"finding-{len(self._state.findings_submitted) + 1}"
244
- finding_record = FindingRecord(
245
- finding_id=finding_id,
246
- filename=action.filename,
247
- line_start=action.line_start,
248
- line_end=line_end,
249
- vuln_type=action.vuln_type,
250
- severity=action.severity,
251
- confidence=action.confidence,
252
- evidence=(action.evidence or "").strip(),
253
- summary=(action.summary or "").strip(),
254
- matched_vulnerability_id=evaluation.matched_vulnerability_id,
255
- component_score=evaluation.component_score,
256
- )
257
- self._state.findings_submitted.append(finding_record)
258
-
259
- if evaluation.is_confirmed_match and evaluation.matched_vulnerability_id is not None:
260
- self._state.matched_vulnerability_ids.append(evaluation.matched_vulnerability_id)
261
- reward = min(1.0, (0.25 + 0.75 * evaluation.component_score) * self._state.quality_multiplier)
262
- feedback = (
263
- f"{evaluation.feedback} "
264
- f"Confirmed={len(self._state.matched_vulnerability_ids)}/{len(task.vulnerabilities)}."
265
- )
266
- return reward, feedback
267
-
268
- if (
269
- evaluation.matched_vulnerability_id is not None
270
- and evaluation.matched_vulnerability_id in self._state.matched_vulnerability_ids
271
- ):
272
- self._state.duplicate_submission_count += 1
273
- self._degrade_quality(0.04)
274
- return 0.01, evaluation.feedback
275
-
276
- if evaluation.component_score >= 0.45:
277
- self._degrade_quality(0.01)
278
- reward = min(0.2, 0.2 * evaluation.component_score * self._state.quality_multiplier)
279
- return reward, f"Partial progress: {evaluation.feedback}"
280
-
281
- self._state.false_positive_count += 1
282
- self._degrade_quality(0.05)
283
- return 0.0, f"Likely false positive: {evaluation.feedback}"
284
-
285
- def _handle_submit_final_report(self) -> tuple[float, str]:
286
- task = self._require_task()
287
- score = self._compute_final_score(task)
288
- self._state.final_score = score
289
- feedback = (
290
- f"Audit finalized. Final deterministic score={score:.3f}. "
291
- f"Confirmed {len(self._state.matched_vulnerability_ids)} of {len(task.vulnerabilities)} vulnerabilities."
292
- )
293
- return score, feedback
294
-
295
- def _compute_final_score(self, task: TaskSpec) -> float:
296
- if self._state.findings_submitted:
297
- avg_component = sum(f.component_score for f in self._state.findings_submitted) / len(
298
- self._state.findings_submitted
299
- )
300
- else:
301
- avg_component = 0.0
302
-
303
- if self._state.findings_submitted:
304
- avg_calibration = sum(
305
- max(0.0, 1.0 - abs(f.confidence - 0.75)) for f in self._state.findings_submitted
306
- ) / len(self._state.findings_submitted)
307
- else:
308
- avg_calibration = 0.0
309
-
310
- score = final_grade(
311
- task=task,
312
- confirmed_vulnerability_ids=self._state.matched_vulnerability_ids,
313
- findings_count=len(self._state.findings_submitted),
314
- false_positive_count=self._state.false_positive_count,
315
- duplicate_count=self._state.duplicate_submission_count,
316
- avg_component_score=avg_component,
317
- avg_confidence_calibration=avg_calibration,
318
- )
319
-
320
- # This quality factor makes spam and random guesses strictly dominated,
321
- # limiting reward hacking while preserving partial-credit gradients.
322
- score *= self._state.quality_multiplier
323
- return max(0.0, min(1.0, score))
324
-
325
- def _build_observation(
326
- self,
327
- *,
328
- reward: float,
329
- done: bool,
330
- feedback: str,
331
- focused_file: Optional[str],
332
- excerpt: str,
333
- extra_metadata: Optional[dict[str, Any]] = None,
334
- ) -> CodeSecurityObservation:
335
- task = self._require_task()
336
-
337
- findings_public = [
338
- {
339
- "finding_id": f.finding_id,
340
- "filename": f.filename,
341
- "line_start": f.line_start,
342
- "line_end": f.line_end,
343
- "vuln_type": f.vuln_type,
344
- "severity": f.severity,
345
- "confidence": f.confidence,
346
- "component_score": round(f.component_score, 3),
347
- }
348
- for f in self._state.findings_submitted
349
- ]
350
-
351
- score_hint = len(self._state.matched_vulnerability_ids) / max(1, len(task.vulnerabilities))
352
-
353
- metadata = {
354
- "quality_multiplier": round(self._state.quality_multiplier, 4),
355
- "false_positive_count": self._state.false_positive_count,
356
- "duplicate_submission_count": self._state.duplicate_submission_count,
357
- "confirmed_vulnerabilities": len(self._state.matched_vulnerability_ids),
358
- "total_vulnerabilities": len(task.vulnerabilities),
359
- "task_id": task.id,
360
- "difficulty": task.difficulty,
361
- "available_task_ids": list_task_ids(),
362
- "last_action_error": None,
363
- }
364
- if extra_metadata:
365
- metadata.update(extra_metadata)
366
-
367
- return CodeSecurityObservation(
368
- done=done,
369
- reward=max(0.0, min(1.0, reward)),
370
- metadata=metadata,
371
- task_id=task.id,
372
- task_title=task.title,
373
- difficulty=task.difficulty,
374
- objective=task.objective,
375
- instructions=(
376
- "Valid actions: inspect_file, submit_finding, submit_final_report. "
377
- "For submit_finding include filename, line_start/line_end, vuln_type, severity, confidence."
378
- ),
379
- available_files=sorted(task.repository.keys()),
380
- focused_file=focused_file,
381
- file_excerpt=excerpt,
382
- findings_so_far=findings_public,
383
- steps_remaining=max(0, self._state.max_steps - self._state.step_count),
384
- last_feedback=feedback,
385
- score_hint=max(0.0, min(1.0, score_hint)),
386
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
server/tasks.py DELETED
@@ -1,208 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from typing import Dict, List, Literal
5
-
6
- Difficulty = Literal["easy", "medium", "hard"]
7
- Severity = Literal["low", "medium", "high", "critical"]
8
-
9
-
10
- @dataclass(frozen=True)
11
- class VulnerabilitySpec:
12
- id: str
13
- filename: str
14
- line: int
15
- vuln_type: str
16
- severity: Severity
17
- title: str
18
- rationale: str
19
-
20
-
21
- @dataclass(frozen=True)
22
- class TaskSpec:
23
- id: str
24
- title: str
25
- difficulty: Difficulty
26
- objective: str
27
- max_steps: int
28
- repository: Dict[str, str]
29
- vulnerabilities: List[VulnerabilitySpec]
30
-
31
-
32
- TASKS: Dict[str, TaskSpec] = {
33
- "easy": TaskSpec(
34
- id="easy",
35
- title="Legacy Flask Patch Review",
36
- difficulty="easy",
37
- objective=(
38
- "Audit a small Flask service before release. Identify concrete "
39
- "security vulnerabilities with accurate location, type, and severity."
40
- ),
41
- max_steps=12,
42
- repository={
43
- "app/routes.py": """from flask import Blueprint, request, jsonify\nfrom app.db import db\n\nbp = Blueprint('api', __name__)\n\n@bp.get('/user')\ndef get_user():\n user_id = request.args.get('id', '')\n query = f\"SELECT id, email, role FROM users WHERE id = '{user_id}'\"\n row = db.execute(query).fetchone()\n return jsonify(dict(row) if row else {})\n\n@bp.post('/login')\ndef login():\n payload = request.json or {}\n if payload.get('token') == 'letmein':\n return jsonify({'ok': True})\n return jsonify({'ok': False}), 401\n""",
44
- "app/config.py": """import os\n\nclass Config:\n DEBUG = os.getenv('DEBUG', '0') == '1'\n SECRET_KEY = 'prod-secret-2026'\n DB_URL = os.getenv('DB_URL', 'postgresql://localhost/app')\n""",
45
- "app/db.py": """import sqlite3\n\n_conn = sqlite3.connect(':memory:', check_same_thread=False)\n_conn.execute('CREATE TABLE IF NOT EXISTS users (id TEXT, email TEXT, role TEXT)')\n\ndef execute(query: str):\n return _conn.execute(query)\n\nclass DB:\n execute = staticmethod(execute)\n\ndb = DB()\n""",
46
- },
47
- vulnerabilities=[
48
- VulnerabilitySpec(
49
- id="E-01",
50
- filename="app/routes.py",
51
- line=8,
52
- vuln_type="sql_injection",
53
- severity="high",
54
- title="Unsanitized SQL query with user-controlled id",
55
- rationale="Direct string interpolation builds SQL using request input.",
56
- ),
57
- VulnerabilitySpec(
58
- id="E-02",
59
- filename="app/config.py",
60
- line=5,
61
- vuln_type="hardcoded_secret",
62
- severity="high",
63
- title="Hardcoded production secret key",
64
- rationale="Embedding secrets in code leaks credentials and breaks rotation.",
65
- ),
66
- VulnerabilitySpec(
67
- id="E-03",
68
- filename="app/routes.py",
69
- line=15,
70
- vuln_type="weak_authentication",
71
- severity="medium",
72
- title="Backdoor static token for login",
73
- rationale="Fixed token bypasses proper authentication controls.",
74
- ),
75
- ],
76
- ),
77
- "medium": TaskSpec(
78
- id="medium",
79
- title="Payment Webhook Service",
80
- difficulty="medium",
81
- objective=(
82
- "Review a webhook and export pipeline used by operations. Catch logic and "
83
- "input-handling flaws that can be exploited in production."
84
- ),
85
- max_steps=14,
86
- repository={
87
- "service/webhook.py": """import hmac\nimport hashlib\nfrom flask import request, jsonify\n\nSHARED_SECRET = 'whsec_test_123'\n\ndef verify_signature(body: bytes, signature: str) -> bool:\n digest = hmac.new(SHARED_SECRET.encode(), body, hashlib.sha256).hexdigest()\n return digest == signature\n\ndef process_event(event: dict):\n return {'processed': event.get('id')}\n\ndef webhook_handler():\n body = request.data\n signature = request.headers.get('X-Signature', '')\n if not verify_signature(body, signature):\n return jsonify({'error': 'invalid signature'}), 401\n\n event = request.json or {}\n if event.get('debug'):\n # temporary bypass while partner migrates\n return jsonify({'ok': True, 'bypass': True})\n\n return jsonify(process_event(event))\n""",
88
- "service/export.py": """import os\n\ndef build_export_cmd(job_name: str, output_dir: str) -> str:\n return f\"tar -czf {output_dir}/{job_name}.tgz /srv/data/{job_name}\"\n\ndef run_export(job_name: str, output_dir: str):\n cmd = build_export_cmd(job_name, output_dir)\n os.system(cmd)\n return {'status': 'ok', 'cmd': cmd}\n""",
89
- "service/serializers.py": """import yaml\n\ndef load_template(raw: str):\n return yaml.load(raw, Loader=yaml.Loader)\n""",
90
- },
91
- vulnerabilities=[
92
- VulnerabilitySpec(
93
- id="M-01",
94
- filename="service/webhook.py",
95
- line=10,
96
- vuln_type="weak_authentication",
97
- severity="medium",
98
- title="Timing-unsafe signature comparison",
99
- rationale="Using == for secrets can leak comparison timing signal.",
100
- ),
101
- VulnerabilitySpec(
102
- id="M-02",
103
- filename="service/webhook.py",
104
- line=22,
105
- vuln_type="weak_authentication",
106
- severity="high",
107
- title="Debug flag bypasses signature verification outcome",
108
- rationale="Attacker-controlled debug field returns success without processing guards.",
109
- ),
110
- VulnerabilitySpec(
111
- id="M-03",
112
- filename="service/export.py",
113
- line=8,
114
- vuln_type="command_injection",
115
- severity="critical",
116
- title="Unsanitized shell command execution",
117
- rationale="User-controlled job_name/output_dir flow into os.system command string.",
118
- ),
119
- VulnerabilitySpec(
120
- id="M-04",
121
- filename="service/serializers.py",
122
- line=4,
123
- vuln_type="insecure_deserialization",
124
- severity="high",
125
- title="Unsafe YAML loader",
126
- rationale="yaml.Loader can construct arbitrary Python objects from attacker input.",
127
- ),
128
- ],
129
- ),
130
- "hard": TaskSpec(
131
- id="hard",
132
- title="Enterprise Multi-Tenant API",
133
- difficulty="hard",
134
- objective=(
135
- "Audit an API gateway handling tenants, files, and callback fetches. "
136
- "Find high-impact vulnerabilities without flooding false positives."
137
- ),
138
- max_steps=16,
139
- repository={
140
- "api/auth.py": """import base64\nimport json\nimport jwt\n\ndef issue_token(user_id: str, tenant_id: str):\n payload = {'sub': user_id, 'tenant': tenant_id, 'role': 'member'}\n return jwt.encode(payload, 'dev-key', algorithm='HS256')\n\ndef parse_token(token: str):\n header_b64 = token.split('.')[0] + '=='\n header = json.loads(base64.urlsafe_b64decode(header_b64).decode())\n if header.get('alg') == 'none':\n return json.loads(base64.urlsafe_b64decode(token.split('.')[1] + '==').decode())\n return jwt.decode(token, 'dev-key', algorithms=['HS256'])\n""",
141
- "api/files.py": """from flask import request, jsonify\n\nFILES = {\n 'tenant-a': {'1': 'a-private-doc'},\n 'tenant-b': {'2': 'b-private-doc'},\n}\n\ndef get_file(user):\n file_id = request.args.get('file_id')\n tenant = request.args.get('tenant')\n data = FILES.get(tenant, {}).get(file_id)\n if not data:\n return jsonify({'error': 'not found'}), 404\n return jsonify({'file': data, 'tenant': tenant, 'user': user['sub']})\n""",
142
- "api/fetcher.py": """import requests\n\ndef fetch_preview(url: str):\n response = requests.get(url, timeout=3)\n return {'status': response.status_code, 'body': response.text[:120]}\n""",
143
- "api/storage.py": """from pathlib import Path\n\nBASE = Path('/srv/uploads')\n\ndef read_attachment(path_fragment: str) -> bytes:\n final_path = BASE / path_fragment\n return final_path.read_bytes()\n""",
144
- },
145
- vulnerabilities=[
146
- VulnerabilitySpec(
147
- id="H-01",
148
- filename="api/auth.py",
149
- line=12,
150
- vuln_type="weak_authentication",
151
- severity="critical",
152
- title="Accepts unsigned JWT tokens when alg=none",
153
- rationale="Token parser trusts attacker-controlled header and bypasses signature checks.",
154
- ),
155
- VulnerabilitySpec(
156
- id="H-02",
157
- filename="api/files.py",
158
- line=11,
159
- vuln_type="weak_authentication",
160
- severity="high",
161
- title="Tenant access controlled by request parameter",
162
- rationale="Requester can switch tenant query parameter and read cross-tenant data (IDOR).",
163
- ),
164
- VulnerabilitySpec(
165
- id="H-03",
166
- filename="api/fetcher.py",
167
- line=4,
168
- vuln_type="ssrf",
169
- severity="high",
170
- title="Server-side fetch of arbitrary URL",
171
- rationale="Attacker can query internal metadata endpoints through backend network path.",
172
- ),
173
- VulnerabilitySpec(
174
- id="H-04",
175
- filename="api/storage.py",
176
- line=6,
177
- vuln_type="path_traversal",
178
- severity="critical",
179
- title="Unvalidated path join for file reads",
180
- rationale="Path fragments containing .. can escape upload directory.",
181
- ),
182
- ],
183
- ),
184
- }
185
-
186
- SEVERITY_WEIGHTS = {
187
- "low": 1.0,
188
- "medium": 2.0,
189
- "high": 3.0,
190
- "critical": 4.0,
191
- }
192
-
193
- TARGET_CONFIDENCE = {
194
- "low": 0.55,
195
- "medium": 0.65,
196
- "high": 0.8,
197
- "critical": 0.9,
198
- }
199
-
200
-
201
- def get_task(task_id: str) -> TaskSpec:
202
- if task_id not in TASKS:
203
- raise KeyError(f"Unknown task_id '{task_id}'. Available: {', '.join(sorted(TASKS))}")
204
- return TASKS[task_id]
205
-
206
-
207
- def list_task_ids() -> List[str]:
208
- return sorted(TASKS.keys())