CyberArena / app /services /dedup.py
Hussien Haider
H
5c14256
Raw
History Blame Contribute Delete
10.5 kB
"""
Challenge deduplication utilities.
Before inserting a new challenge into any of the four tables,
we check whether a sufficiently similar challenge already exists
to prevent the pool from filling with near-duplicates.
"""
from __future__ import annotations
import os
import re
from difflib import SequenceMatcher
from typing import Dict, List, Optional, Set
import httpx
SUPABASE_URL = os.getenv("SUPABASE_URL", "")
SUPABASE_ANON_KEY = os.getenv("SUPABASE_ANON_KEY", "")
# ---------------------------------------------------------------------------
# Text normalisation helpers
# ---------------------------------------------------------------------------
_AR_STOP_WORDS: Set[str] = {
"في", "من", "على", "إلى", "عن", "مع", "هذا", "هذه", "التي", "الذي",
"أن", "إن", "لا", "ما", "لا", "كل", "بها", "به", "فيها", "فيه",
"أو", "ثم", "لكن", "بل", "حتى", "قد", "لقد", "كان", "يجب", "يتم",
"يمكن", "حيث", "كما", "أيضاً", "ذلك", "تلك", "اللذين", "اللتين",
}
def _normalise(text: str) -> str:
"""Lower-case, strip punctuation, collapse whitespace."""
text = text.lower()
text = re.sub(r"[^\w\s]", " ", text)
text = re.sub(r"\s+", " ", text).strip()
return text
def _token_set(text: str) -> Set[str]:
"""Return de-duplicated token set, Arabic stop-words removed."""
tokens = _normalise(text).split()
return {t for t in tokens if t not in _AR_STOP_WORDS and len(t) > 1}
def _title_similarity(a: str, b: str) -> float:
"""
Title similarity = max of:
- token overlap (Jaccard-ish)
- SequenceMatcher ratio on normalised strings
"""
sa, sb = _normalise(a), _normalise(b)
if not sa or not sb:
return 0.0
# SequenceMatcher on normalised strings
seq = SequenceMatcher(None, sa, sb).ratio()
# Token overlap
ta, tb = _token_set(a), _token_set(b)
if not ta or not tb:
return seq
jaccard = len(ta & tb) / max(len(ta | tb), 1)
return max(seq, jaccard)
def _code_similarity(a: str, b: str) -> float:
"""
For code: use SequenceMatcher on stripped lines.
Code tends to have boilerplate; line-level matching works well.
"""
def _strip(lines: str) -> List[str]:
return [ln.strip() for ln in lines.splitlines() if ln.strip()]
la, lb = _strip(a), _strip(b)
if not la or not lb:
return 0.0
return SequenceMatcher(None, la, lb).ratio()
# ---------------------------------------------------------------------------
# DB fetch helpers
# ---------------------------------------------------------------------------
_HEADERS = {
"apikey": SUPABASE_ANON_KEY,
"Authorization": f"Bearer {SUPABASE_ANON_KEY}",
"Content-Type": "application/json",
}
def _fetch_existing_titles(table: str, role_filter: Optional[str] = None,
limit: int = 200) -> List[str]:
"""Fetch recent titles from a challenge table."""
url = f"{SUPABASE_URL}/rest/v1/{table}"
params: Dict[str, str] = {
"select": "title",
"order": "created_at.desc",
"limit": str(limit),
}
if role_filter:
params["team_role"] = f"eq.{role_filter}"
try:
r = httpx.get(url, headers=_HEADERS, params=params, timeout=10)
r.raise_for_status()
return [row.get("title", "") for row in r.json()]
except Exception:
return []
def _fetch_existing_codes(table: str, code_col: str,
role_filter: Optional[str] = None,
limit: int = 100) -> List[str]:
"""Fetch recent code/story/log_content from a challenge table."""
url = f"{SUPABASE_URL}/rest/v1/{table}"
params: Dict[str, str] = {
"select": f"title,{code_col}",
"order": "created_at.desc",
"limit": str(limit),
}
if role_filter:
params["team_role"] = f"eq.{role_filter}"
try:
r = httpx.get(url, headers=_HEADERS, params=params, timeout=10)
r.raise_for_status()
return [row.get(code_col, "") for row in r.json() if row.get(code_col)]
except Exception:
return []
def fetch_existing_titles(table: str, role_filter: Optional[str] = None,
limit: int = 200) -> List[str]:
"""Fetch recent titles from a challenge table (public wrapper)."""
return _fetch_existing_titles(table, role_filter, limit)
# ---------------------------------------------------------------------------
# Public API — one function per generator
# ---------------------------------------------------------------------------
def is_duplicate_encryption(title: str, story: str,
task_outline: str = "",
role_filter: Optional[str] = None) -> bool:
"""Check if an encryption challenge is too similar to existing ones."""
existing_titles = _fetch_existing_titles("encryption_challenges", role_filter)
for t in existing_titles:
if _title_similarity(title, t) > 0.55:
return True
existing_stories = _fetch_existing_codes(
"encryption_challenges", "story", role_filter)
for s in existing_stories:
if _title_similarity(story, s) > 0.55:
return True
if task_outline:
existing_tasks = _fetch_existing_codes(
"encryption_challenges", "task_outline", role_filter)
for t in existing_tasks:
if _title_similarity(task_outline, t) > 0.55:
return True
return False
def is_duplicate_code_fixing(title: str, vulnerable_code: str,
task_outline: str = "",
role_filter: Optional[str] = None) -> bool:
"""Check if a code-fixing challenge is too similar to existing ones."""
existing_titles = _fetch_existing_titles(
"code_fixing_challenges", role_filter, limit=300)
for t in existing_titles:
if _title_similarity(title, t) > 0.55:
return True
existing_codes = _fetch_existing_codes(
"code_fixing_challenges", "vulnerable_code", role_filter, limit=100)
for c in existing_codes:
if _code_similarity(vulnerable_code, c) > 0.65:
return True
if task_outline:
existing_tasks = _fetch_existing_codes(
"code_fixing_challenges", "task_outline", role_filter, limit=200)
for t in existing_tasks:
if _title_similarity(task_outline, t) > 0.55:
return True
return False
def is_duplicate_log_analysis(title: str, log_content: str,
task_outline: str = "",
role_filter: Optional[str] = None) -> bool:
"""Check if a log-analysis challenge is too similar to existing ones."""
existing_titles = _fetch_existing_titles(
"log_analysis_challenges", role_filter, limit=300)
for t in existing_titles:
if _title_similarity(title, t) > 0.55:
return True
existing_logs = _fetch_existing_codes(
"log_analysis_challenges", "log_content", role_filter, limit=100)
for lc in existing_logs:
if _code_similarity(log_content, lc) > 0.60:
return True
if task_outline:
existing_tasks = _fetch_existing_codes(
"log_analysis_challenges", "task_outline", role_filter, limit=200)
for t in existing_tasks:
if _title_similarity(task_outline, t) > 0.55:
return True
return False
def is_duplicate_vuln_hunter(title: str, vulnerable_code: str,
task_outline: str = "",
role_filter: Optional[str] = None) -> bool:
"""Check if a vulnerability-hunter challenge is too similar to existing ones."""
existing_titles = _fetch_existing_titles(
"vulnerability_hunter_challenges", role_filter, limit=300)
for t in existing_titles:
if _title_similarity(title, t) > 0.55:
return True
existing_codes = _fetch_existing_codes(
"vulnerability_hunter_challenges", "vulnerable_code", role_filter, limit=100)
for c in existing_codes:
if _code_similarity(vulnerable_code, c) > 0.60:
return True
if task_outline:
existing_tasks = _fetch_existing_codes(
"vulnerability_hunter_challenges", "task_outline", role_filter, limit=200)
for t in existing_tasks:
if _title_similarity(task_outline, t) > 0.55:
return True
return False
def is_duplicate_web_exploit(title: str, http_request: str,
task_outline: str = "",
role_filter: Optional[str] = None) -> bool:
"""Check if a web-exploitation challenge is too similar to existing ones."""
existing_titles = _fetch_existing_titles(
"web_exploitation_challenges", role_filter, limit=300)
for t in existing_titles:
if _title_similarity(title, t) > 0.55:
return True
existing_requests = _fetch_existing_codes(
"web_exploitation_challenges", "http_request", role_filter, limit=100)
for r in existing_requests:
if _code_similarity(http_request, r) > 0.60:
return True
if task_outline:
existing_tasks = _fetch_existing_codes(
"web_exploitation_challenges", "task_outline", role_filter, limit=200)
for t in existing_tasks:
if _title_similarity(task_outline, t) > 0.55:
return True
return False
def is_duplicate_steganography(title: str, story: str,
task_outline: str = "",
role_filter: Optional[str] = None) -> bool:
"""Check if a steganography challenge is too similar to existing ones."""
existing_titles = _fetch_existing_titles(
"steganography_challenges", role_filter, limit=300)
for t in existing_titles:
if _title_similarity(title, t) > 0.55:
return True
existing_stories = _fetch_existing_codes(
"steganography_challenges", "story", role_filter, limit=100)
for s in existing_stories:
if _title_similarity(story, s) > 0.55:
return True
if task_outline:
existing_tasks = _fetch_existing_codes(
"steganography_challenges", "task_outline", role_filter, limit=200)
for t in existing_tasks:
if _title_similarity(task_outline, t) > 0.55:
return True
return False