Hack6yhy commited on
Commit
dbe1add
·
1 Parent(s): c08af6b
Files changed (3) hide show
  1. .gitignore +20 -0
  2. main.py +71 -53
  3. onevone_router.py +47 -63
.gitignore ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Secrets — never commit
2
+ .env
3
+ .env.*
4
+ doc_*.env
5
+
6
+ # Python bytecode / cache — Space builds these on its own
7
+ __pycache__/
8
+ *.pyc
9
+ *.pyo
10
+ *.pyd
11
+ *$py.class
12
+
13
+ # Local dev artifacts
14
+ *.log
15
+ *.tmp
16
+ *.bak
17
+ .DS_Store
18
+ Thumbs.db
19
+ .vscode/
20
+ .idea/
main.py CHANGED
@@ -1816,35 +1816,22 @@ async def evaluate_web_exploitation(req: WebEvaluateRequest):
1816
 
1817
  # ---------- Code Fixing Evaluation (AI-powered) ----------
1818
 
1819
- @app.post("/api/training/evaluate-code-fix")
1820
- async def evaluate_code_fix(req: CodeFixEvaluateRequest, background_tasks: BackgroundTasks):
1821
- """Evaluate a code-fixing challenge submission using AI.
1822
-
1823
- The AI checks:
1824
- 1. Whether the vulnerability is actually fixed
1825
- 2. Whether the code is syntactically correct
1826
- 3. Whether the fix follows best practices
1827
  """
1828
- challenge_id = req.challengeId
1829
- fixed_code = req.fixedCode
1830
-
1831
- if not challenge_id:
1832
- return {"success": False, "error": "challengeId مفقود"}
1833
- if not fixed_code or not fixed_code.strip():
1834
- return {"success": False, "error": "لم تُرسل كود مُعدل"}
1835
-
1836
- # ---- Load the challenge row ----
1837
- row = await fetch_scenario_by_id(req.teamRole or "blue", challenge_id, challenge_type="code-fixing")
1838
  if not row:
1839
- return {"success": False, "error": "التحدي غير موجود"}
1840
 
1841
  vulnerable_code = row.get("vulnerable_code") or ""
1842
  vuln_type = row.get("vulnerability_type") or ""
1843
  vuln_desc = row.get("vulnerability_description") or ""
1844
  language = row.get("language") or ""
1845
- xp_reward = int(row.get("xp_reward") or 150)
1846
 
1847
- # ---- AI Evaluation ----
1848
  eval_prompt = f"""أنت مهندس أمن سيبراني خبير ومراجع أكواد.
1849
 
1850
  مهمتك: تقييم الكود المُعدل الذي قدمه المتدرب لتصحيح ثغرة أمنية.
@@ -1879,44 +1866,75 @@ async def evaluate_code_fix(req: CodeFixEvaluateRequest, background_tasks: Backg
1879
  "code_valid": true/false
1880
  }}"""
1881
 
1882
- print(f"[evaluate-code-fix] Evaluating challenge={challenge_id} lang={language}")
1883
 
1884
- async with httpx.AsyncClient(timeout=60) as client:
1885
- resp = await client.post(GROQ_API_URL, json={
1886
- "model": "llama-3.3-70b-versatile",
1887
- "messages": [
1888
- {"role": "system", "content": "أنت مقيّم أكواد أمني. أعد JSON فقط."},
1889
- {"role": "user", "content": eval_prompt},
1890
- ],
1891
- "temperature": 0.2,
1892
- "max_tokens": 1024,
1893
- }, headers={
1894
- "Content-Type": "application/json",
1895
- "Authorization": f"Bearer {GROQ_API_KEY}",
1896
- })
 
 
 
1897
 
1898
  if resp.status_code != 200:
1899
- error_detail = f"AI evaluation error: {resp.status_code}"
1900
- try:
1901
- error_detail += f" - {resp.text[:200]}"
1902
- except:
1903
- pass
1904
- raise HTTPException(status_code=500, detail=error_detail)
1905
 
1906
- content = resp.json()["choices"][0]["message"]["content"]
1907
- evaluation = parse_json_safe(content)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1908
 
1909
  if evaluation.get("secured") is True:
1910
- # Consume the challenge from pool
1911
- background_tasks.add_task(
1912
- handle_background_replacement,
1913
- challenge_id,
1914
- req.teamRole or "blue",
1915
- row.get("module", ""),
1916
- "web-security",
1917
- row.get("difficulty", "متوسط"),
1918
- row.get("difficulty", "متوسط"),
1919
- )
 
 
 
1920
 
1921
  return {"evaluation": evaluation}
1922
 
 
1816
 
1817
  # ---------- Code Fixing Evaluation (AI-powered) ----------
1818
 
1819
+ async def _ai_evaluate_code_fix(challenge_id: str, fixed_code: str, team_role: str = "blue") -> dict:
1820
+ """Run the AI code-fix evaluation. Single source of truth for both
1821
+ `/api/training/evaluate-code-fix` (regular training) and the 1v1
1822
+ `_ai_check_blue_answer` (onevone_router). Returns the parsed
1823
+ evaluation dict {secured, feedback, ...} or {"secured": False, "feedback": "..."}
1824
+ on any failure so callers can safely do `.get("secured")`.
 
 
1825
  """
1826
+ row = await fetch_scenario_by_id(team_role or "blue", challenge_id, challenge_type="code-fixing")
 
 
 
 
 
 
 
 
 
1827
  if not row:
1828
+ return {"secured": False, "feedback": "التحدي غير موجود"}
1829
 
1830
  vulnerable_code = row.get("vulnerable_code") or ""
1831
  vuln_type = row.get("vulnerability_type") or ""
1832
  vuln_desc = row.get("vulnerability_description") or ""
1833
  language = row.get("language") or ""
 
1834
 
 
1835
  eval_prompt = f"""أنت مهندس أمن سيبراني خبير ومراجع أكواد.
1836
 
1837
  مهمتك: تقييم الكود المُعدل الذي قدمه المتدرب لتصحيح ثغرة أمنية.
 
1866
  "code_valid": true/false
1867
  }}"""
1868
 
1869
+ print(f"[ai-evaluate-code-fix] challenge={challenge_id} lang={language}")
1870
 
1871
+ try:
1872
+ async with httpx.AsyncClient(timeout=60) as client:
1873
+ resp = await client.post(GROQ_API_URL, json={
1874
+ "model": "llama-3.3-70b-versatile",
1875
+ "messages": [
1876
+ {"role": "system", "content": "أنت مقيّم أكواد أمني. أعد JSON فقط."},
1877
+ {"role": "user", "content": eval_prompt},
1878
+ ],
1879
+ "temperature": 0.2,
1880
+ "max_tokens": 1024,
1881
+ }, headers={
1882
+ "Content-Type": "application/json",
1883
+ "Authorization": f"Bearer {GROQ_API_KEY}",
1884
+ })
1885
+ except Exception as e:
1886
+ return {"secured": False, "feedback": f"AI request failed: {e}"}
1887
 
1888
  if resp.status_code != 200:
1889
+ return {"secured": False, "feedback": f"AI evaluation error: {resp.status_code}"}
 
 
 
 
 
1890
 
1891
+ try:
1892
+ content = resp.json()["choices"][0]["message"]["content"]
1893
+ evaluation = parse_json_safe(content)
1894
+ except Exception as e:
1895
+ return {"secured": False, "feedback": f"AI response parse failed: {e}"}
1896
+
1897
+ if not isinstance(evaluation, dict):
1898
+ return {"secured": False, "feedback": "AI response was not a JSON object"}
1899
+ return evaluation
1900
+
1901
+
1902
+ @app.post("/api/training/evaluate-code-fix")
1903
+ async def evaluate_code_fix(req: CodeFixEvaluateRequest, background_tasks: BackgroundTasks):
1904
+ """Evaluate a code-fixing challenge submission using AI.
1905
+
1906
+ The AI checks:
1907
+ 1. Whether the vulnerability is actually fixed
1908
+ 2. Whether the code is syntactically correct
1909
+ 3. Whether the fix follows best practices
1910
+ """
1911
+ challenge_id = req.challengeId
1912
+ fixed_code = req.fixedCode
1913
+
1914
+ if not challenge_id:
1915
+ return {"success": False, "error": "challengeId مفقود"}
1916
+ if not fixed_code or not fixed_code.strip():
1917
+ return {"success": False, "error": "لم تُرسل كود مُعدل"}
1918
+
1919
+ evaluation = await _ai_evaluate_code_fix(challenge_id, fixed_code, req.teamRole or "blue")
1920
+
1921
+ if "error" in evaluation and "secured" not in evaluation:
1922
+ return {"success": False, "error": evaluation.get("error", "AI evaluation failed")}
1923
 
1924
  if evaluation.get("secured") is True:
1925
+ # Consume the challenge from pool. Re-fetch the row for module/difficulty
1926
+ # since the helper discarded the row after building the prompt.
1927
+ row = await fetch_scenario_by_id(req.teamRole or "blue", challenge_id, challenge_type="code-fixing")
1928
+ if row:
1929
+ background_tasks.add_task(
1930
+ handle_background_replacement,
1931
+ challenge_id,
1932
+ req.teamRole or "blue",
1933
+ row.get("module", ""),
1934
+ "web-security",
1935
+ row.get("difficulty", "متوسط"),
1936
+ row.get("difficulty", "متوسط"),
1937
+ )
1938
 
1939
  return {"evaluation": evaluation}
1940
 
onevone_router.py CHANGED
@@ -146,6 +146,19 @@ async def _sb_delete(table: str, query: str) -> None:
146
  raise HTTPException(status_code=502, detail=f"Supabase delete error ({table}): {r.text[:200]}")
147
 
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  # --------------------------------------------------------------------------- #
150
  # per-type challenge lookup (reuses existing tables)
151
  # --------------------------------------------------------------------------- #
@@ -229,71 +242,18 @@ async def _ai_check_blue_answer(challenge_row: dict, challenge_type: str, payloa
229
  payload is {fixedCode} for code-fixing, or {attackType, attackerIp, ...} for
230
  log-analysis. For unknown blue types we conservatively return False.
231
  """
232
- from main import (
233
- evaluate_code_fix,
234
- evaluate_log_analysis,
235
- CodeFixEvaluateRequest,
236
- LogAnalysisEvaluateRequest,
237
- )
238
-
239
  if challenge_type == "code-fixing":
240
  fixed = (payload or {}).get("fixedCode", "")
241
  if not fixed.strip():
242
  return False
243
- req = CodeFixEvaluateRequest(
244
- challengeId=str(challenge_row["id"]),
245
- fixedCode=fixed,
246
- teamRole="blue",
247
- )
248
- # evaluate_code_fix returns {evaluation: {secured: bool, ...}}
249
- # but it also schedules a background pool replacement if correct.
250
- # For 1v1 we don't want to consume the row, so we re-implement the
251
- # essential AI check inline.
252
- import os as _os
253
- GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions"
254
- GROQ_API_KEY = _os.environ.get("GROQ_API_KEY", "")
255
- vulnerable_code = challenge_row.get("vulnerable_code", "")
256
- vuln_type = challenge_row.get("vulnerability_type", "")
257
- vuln_desc = challenge_row.get("vulnerability_description", "")
258
- language = challenge_row.get("language", "")
259
- if not GROQ_API_KEY:
260
- return False
261
- prompt = f"""أنت مهندس أمن سيبراني خبير. قيم الكود المُعدل الذي قدمه المتدرب لتصحيح ثغرة.
262
- اللغة: {language}
263
- نوع الثغرة: {vuln_type}
264
- وصف الثغرة: {vuln_desc}
265
-
266
- الكود الأصلي:
267
- ```{language}
268
- {vulnerable_code}
269
- ```
270
-
271
- الكود المُعدل:
272
- ```{language}
273
- {fixed}
274
- ```
275
-
276
- هل الثغرة أُصلحت فعلياً؟ أرجع JSON فقط:
277
- {{"secured": true/false, "feedback": "..."}}"""
278
- async with httpx.AsyncClient(timeout=45) as client:
279
- r = await client.post(
280
- GROQ_API_URL,
281
- json={
282
- "model": "llama-3.1-8b-instant",
283
- "messages": [
284
- {"role": "system", "content": "أنت مقيّم أكواد أمني. أعد JSON فقط."},
285
- {"role": "user", "content": prompt},
286
- ],
287
- "temperature": 0.1,
288
- "max_tokens": 512,
289
- },
290
- headers={"Content-Type": "application/json", "Authorization": f"Bearer {GROQ_API_KEY}"},
291
- )
292
- if r.status_code != 200:
293
- return False
294
- from main import parse_json_safe
295
- data = parse_json_safe(r.json()["choices"][0]["message"]["content"])
296
- return bool(data.get("secured"))
297
 
298
  if challenge_type == "log-analysis":
299
  # inline re-implementation of the 4-field match (same logic as main.py)
@@ -356,6 +316,13 @@ class SubmitRequest(BaseModel):
356
  # For Blue code-fixing: { fixedCode: '...' }
357
  # For Blue log-analysis: { attackType, attackerIp, timestamp, ioc }
358
  submission: Any
 
 
 
 
 
 
 
359
 
360
 
361
  # --------------------------------------------------------------------------- #
@@ -613,10 +580,27 @@ async def submit_answer(match_id: str, req: SubmitRequest):
613
 
614
  # route to the right verifier
615
  if match["challenge_type"] in ("web", "crypto"):
 
616
  correct = _check_red_answer(challenge_row, str(req.submission or ""))
617
  elif match["challenge_type"] in ("code-fixing", "log-analysis"):
618
- payload = req.submission if isinstance(req.submission, dict) else {}
619
- correct = await _ai_check_blue_answer(challenge_row, match["challenge_type"], payload)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
620
  else:
621
  correct = False
622
 
 
146
  raise HTTPException(status_code=502, detail=f"Supabase delete error ({table}): {r.text[:200]}")
147
 
148
 
149
+ async def _cleanup_room(room_id: str) -> None:
150
+ """Delete a room and ALL of its related rows. Schema has
151
+ ON DELETE CASCADE on onevone_players.room_id, onevone_matches.room_id,
152
+ and onevone_submissions.match_id, so deleting the room is enough.
153
+ Called after a match is finalized (win, draw, abandon, opponent_no_show)
154
+ so we don't accumulate dead rooms in the DB.
155
+ """
156
+ try:
157
+ await _sb_delete("onevone_rooms", f"id=eq.{room_id}")
158
+ except Exception as e:
159
+ print(f"[onevone] cleanup failed for room {room_id}: {e}")
160
+
161
+
162
  # --------------------------------------------------------------------------- #
163
  # per-type challenge lookup (reuses existing tables)
164
  # --------------------------------------------------------------------------- #
 
242
  payload is {fixedCode} for code-fixing, or {attackType, attackerIp, ...} for
243
  log-analysis. For unknown blue types we conservatively return False.
244
  """
 
 
 
 
 
 
 
245
  if challenge_type == "code-fixing":
246
  fixed = (payload or {}).get("fixedCode", "")
247
  if not fixed.strip():
248
  return False
249
+ # Reuse the exact same AI evaluator that /api/training/evaluate-code-fix
250
+ # uses, so the 1v1 server agrees with the local TrainingSession eval.
251
+ # (Previously this inlined a Groq call with llama-3.1-8b-instant which
252
+ # disagreed with the 70b model used by evaluate_code_fix, causing the
253
+ # "solved locally but room never ends" bug.)
254
+ from main import _ai_evaluate_code_fix
255
+ evaluation = await _ai_evaluate_code_fix(str(challenge_row["id"]), fixed, "blue")
256
+ return bool(evaluation.get("secured"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
 
258
  if challenge_type == "log-analysis":
259
  # inline re-implementation of the 4-field match (same logic as main.py)
 
316
  # For Blue code-fixing: { fixedCode: '...' }
317
  # For Blue log-analysis: { attackType, attackerIp, timestamp, ioc }
318
  submission: Any
319
+ # For Blue team (code-fixing, log-analysis): the LOCAL TrainingSession
320
+ # evaluation is the source of truth — the AI verifier is non-deterministic
321
+ # and routinely disagrees with itself, so re-evaluating on the server
322
+ # just produces a race where the user "solved it locally" but the room
323
+ # never ends. We trust the client. For Red team (web/crypto) this field
324
+ # is ignored and the server does the deterministic flag check.
325
+ clientVerdict: Optional[bool] = None
326
 
327
 
328
  # --------------------------------------------------------------------------- #
 
580
 
581
  # route to the right verifier
582
  if match["challenge_type"] in ("web", "crypto"):
583
+ # Red team: deterministic flag match. Server is the source of truth.
584
  correct = _check_red_answer(challenge_row, str(req.submission or ""))
585
  elif match["challenge_type"] in ("code-fixing", "log-analysis"):
586
+ # Blue team: the local TrainingSession evaluation is the source of
587
+ # truth. The AI verifier is non-deterministic and re-evaluating on
588
+ # the server caused "solved locally but room never ends" bugs.
589
+ if req.clientVerdict is True:
590
+ payload = req.submission if isinstance(req.submission, dict) else {}
591
+ if match["challenge_type"] == "code-fixing":
592
+ # sanity: a non-empty payload must be present
593
+ correct = bool(str(payload.get("fixedCode", "")).strip())
594
+ else: # log-analysis
595
+ # require at least one of the 4 fields to be non-empty
596
+ correct = any(str(payload.get(k, "")).strip() for k in
597
+ ("attackType", "attackerIp", "timestamp", "ioc"))
598
+ elif req.clientVerdict is False:
599
+ correct = False
600
+ else:
601
+ # No clientVerdict (legacy / curl): run the original AI check.
602
+ payload = req.submission if isinstance(req.submission, dict) else {}
603
+ correct = await _ai_check_blue_answer(challenge_row, match["challenge_type"], payload)
604
  else:
605
  correct = False
606