import subprocess import json import time import tempfile import os import logging import traceback from fastapi import FastAPI app = FastAPI() logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) SEMGREP_TIMEOUT = 300.0 SEMGREP_RULES_DIR = "/app/rules/" def generate_tree_string(path: str, max_depth: int = 3) -> str: """Génère une représentation visuelle de l'arborescence (exclut le bruit).""" lines = [] exclude_dirs = {".git", "__pycache__", "node_modules", "venv", ".venv", "env"} def _walk(current_path, depth): if depth > max_depth: return try: entries = sorted(os.listdir(current_path)) except Exception: return for entry in entries: if entry in exclude_dirs or entry.startswith('.'): continue full_path = os.path.join(current_path, entry) indent = " " * depth if os.path.isdir(full_path): lines.append(f"{indent}📁 {entry}/") _walk(full_path, depth + 1) else: lines.append(f"{indent}📄 {entry}") _walk(path, 0) return "\n".join(lines) @app.post("/scan") async def scan_repository(repo_url: str): start_time = time.time() issues_list = [] try: # 1. CRÉATION DU DOSSIER TEMPORAIRE ET CLONAGE DU PROJET À SCANNER with tempfile.TemporaryDirectory() as tmp_dir: repo_path = os.path.join(tmp_dir, "repo") logger.info(f"[Sandbox] Clonage dynamique du projet {repo_url} dans {repo_path}...") clone_cmd = f"git clone --depth 1 {repo_url} {repo_path}" clone_result = subprocess.run(clone_cmd, shell=True, capture_output=True, text=True) if clone_result.returncode != 0: logger.error(f"[Sandbox] Échec du git clone du projet : {clone_result.stderr}") return { "results": { "is_executable": False, "runtime": { "exit_code": clone_result.returncode, "stdout": "", "stderr": f"Impossible de cloner le projet : {clone_result.stderr}", "duration_seconds": round(time.time() - start_time, 2), "timeout_triggered": False }, "issues": [] } } repo_tree = generate_tree_string(repo_path) try: cloned_files = os.listdir(repo_path) logger.info(f"[Sandbox] 📁 Fichiers récupérés après clone : {cloned_files}") if not cloned_files or cloned_files == ['.git']: logger.warning("[Sandbox] ⚠️ Le dossier cloné est vide (ou ne contient que .git) !") except Exception as dir_err: logger.error(f"[Sandbox] Impossible de lister le contenu du dossier cloné : {dir_err}") # 2. EXÉCUTION DE SEMGREP AVEC LA RÈGLE UNIQUE try: if os.path.exists(SEMGREP_RULES_DIR): logger.info(f"[Sandbox] Répertoire de règles détecté : {SEMGREP_RULES_DIR}") else: logger.error(f"[Sandbox] ❌ Répertoire de règles introuvable à l'emplacement : {SEMGREP_RULES_DIR}") logger.info(f"[Sandbox] Lancement du scan Semgrep avec l'audit unique ({SEMGREP_RULES_DIR})...") secure_command = f"semgrep scan --config={SEMGREP_RULES_DIR} --json --quiet ." result = subprocess.run( secure_command, shell=True, cwd=repo_path, capture_output=True, text=True, timeout=SEMGREP_TIMEOUT ) duration = round(time.time() - start_time, 2) exit_code = result.returncode stderr_output = result.stderr timeout_triggered = False except subprocess.TimeoutExpired: logger.error("[Sandbox] Timeout Semgrep dépassé.") return { "results": { "is_executable": False, "runtime": { "exit_code": -1, "stdout": "", "stderr": "L'analyse statique a dépassé le timeout global.", "duration_seconds": round(time.time() - start_time, 2), "timeout_triggered": True }, "issues": [] } } # 3. PARSING DES RÉSULTATS try: if result.returncode == 0: is_executable = True stdout_msg = f"Scan statique réussi.\n\n[STRUCTURE DETECTEE] :\n{repo_tree}" if result.stdout.strip(): scan_data = json.loads(result.stdout) for item in scan_data.get("results", []): issues_list.append({ "tool": "Semgrep", "file": item.get("path", "").replace(f"{repo_path}/", ""), "line": item.get("start", {}).get("line"), "code": item.get("check_id"), "severity": item.get("extra", {}).get("severity", "MEDIUM").upper(), "message": item.get("extra", {}).get("message", "") }) else: is_executable = False stdout_msg = f"Échec du moteur de scan Semgrep (Exit code {result.returncode})." logger.error(f"[Sandbox] Semgrep a échoué ! Code: {result.returncode}\nSTDOUT: {result.stdout}\nSTDERR: {result.stderr}") except Exception as e: is_executable = False stdout_msg = "Erreur lors du traitement des résultats du scan." stderr_output = f"Erreur parsing JSON Semgrep: {str(e)}\n{result.stderr}" return { "results": { "is_executable": is_executable, "runtime": { "exit_code": exit_code, "stdout": stdout_msg, "stderr": stderr_output, "duration_seconds": duration, "timeout_triggered": timeout_triggered }, "issues": issues_list } } except Exception as global_e: error_trace = traceback.format_exc() logger.error(f"[Sandbox] CRASH INTERNE CRITIQUE :\n{error_trace}") return { "results": { "is_executable": False, "runtime": { "exit_code": -99, "stdout": "Crash critique du conteneur de la Sandbox.", "stderr": f"Exception Python : {str(global_e)}\n\nTraceback complet :\n{error_trace}", "duration_seconds": round(time.time() - start_time, 2), "timeout_triggered": False }, "issues": [] } }