Spaces:
Sleeping
Sleeping
- .gitignore +20 -0
- main.py +71 -53
- 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 |
-
|
| 1820 |
-
|
| 1821 |
-
|
| 1822 |
-
|
| 1823 |
-
|
| 1824 |
-
|
| 1825 |
-
2. Whether the code is syntactically correct
|
| 1826 |
-
3. Whether the fix follows best practices
|
| 1827 |
"""
|
| 1828 |
-
|
| 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 {"
|
| 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]
|
| 1883 |
|
| 1884 |
-
|
| 1885 |
-
|
| 1886 |
-
|
| 1887 |
-
|
| 1888 |
-
|
| 1889 |
-
|
| 1890 |
-
|
| 1891 |
-
|
| 1892 |
-
|
| 1893 |
-
|
| 1894 |
-
|
| 1895 |
-
|
| 1896 |
-
|
|
|
|
|
|
|
|
|
|
| 1897 |
|
| 1898 |
if resp.status_code != 200:
|
| 1899 |
-
|
| 1900 |
-
try:
|
| 1901 |
-
error_detail += f" - {resp.text[:200]}"
|
| 1902 |
-
except:
|
| 1903 |
-
pass
|
| 1904 |
-
raise HTTPException(status_code=500, detail=error_detail)
|
| 1905 |
|
| 1906 |
-
|
| 1907 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1908 |
|
| 1909 |
if evaluation.get("secured") is True:
|
| 1910 |
-
# Consume the challenge from pool
|
| 1911 |
-
|
| 1912 |
-
|
| 1913 |
-
|
| 1914 |
-
|
| 1915 |
-
|
| 1916 |
-
|
| 1917 |
-
|
| 1918 |
-
|
| 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 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
)
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 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 |
-
|
| 619 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|