Correction QA: Validation arborescence + qualité/sécurité, ajout logique de fin complète, suppression repo non conforme
This commit is contained in:
@@ -3,7 +3,7 @@ FROM semgrep/semgrep:latest
|
||||
USER root
|
||||
|
||||
RUN sed -i 's/https/http/g' /etc/apk/repositories && \
|
||||
apk update && apk add --no-cache python3 py3-pip
|
||||
apk update && apk add --no-cache python3 py3-pip git
|
||||
|
||||
RUN pip install --no-cache-dir \
|
||||
--trusted-host pypi.org \
|
||||
@@ -13,6 +13,10 @@ RUN pip install --no-cache-dir \
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# COPY r2c-security-audit.yaml /app/r2c-security-audit.yaml
|
||||
COPY rules/ /app/rules/
|
||||
RUN sed -i 's/adjust_for_docker()/pass/g' /usr/lib/python3.12/site-packages/semgrep/commands/scan.py
|
||||
|
||||
COPY main.py .
|
||||
|
||||
EXPOSE 8004
|
||||
|
||||
@@ -1,85 +1,179 @@
|
||||
import subprocess
|
||||
import json
|
||||
import time
|
||||
from fastapi import FastAPI
|
||||
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
|
||||
|
||||
@app.post("/scan/{repo_name}")
|
||||
async def scan_repository(repo_name: str):
|
||||
repo_path = f"/src/{repo_name}"
|
||||
issues_list = []
|
||||
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:
|
||||
secure_command = f"ulimit -f 10240 && semgrep scan --config=p/r2c-security-audit --json --quiet {repo_path}"
|
||||
|
||||
result = subprocess.run(
|
||||
secure_command,
|
||||
shell=True,
|
||||
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
|
||||
is_executable = True
|
||||
# 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}")
|
||||
|
||||
except subprocess.TimeoutExpired as te:
|
||||
logger.error(f"[Sandbox] Semgrep a dépassé le timeout sur {repo_name}")
|
||||
duration = round(time.time() - start_time, 2)
|
||||
exit_code = -1
|
||||
stderr_output = f"L'analyse statique a été coupée : Timeout de {SEMGREP_TIMEOUT}s dépassé."
|
||||
timeout_triggered = True
|
||||
is_executable = False
|
||||
# 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": is_executable,
|
||||
"is_executable": False,
|
||||
"runtime": {
|
||||
"exit_code": exit_code,
|
||||
"stdout": "",
|
||||
"stderr": stderr_output,
|
||||
"duration_seconds": duration,
|
||||
"timeout_triggered": timeout_triggered
|
||||
"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": []
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
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", "")
|
||||
})
|
||||
is_executable = True
|
||||
stderr_output = result.stderr
|
||||
except Exception as e:
|
||||
is_executable = False
|
||||
stderr_output = f"Erreur parsing JSON Semgrep: {str(e)}\n{result.stderr}"
|
||||
|
||||
return {
|
||||
"results": {
|
||||
"is_executable": is_executable,
|
||||
"runtime": {
|
||||
"exit_code": exit_code,
|
||||
"stdout": "Scan statique universel effectué avec succès.",
|
||||
"stderr": stderr_output,
|
||||
"duration_seconds": duration,
|
||||
"timeout_triggered": timeout_triggered
|
||||
},
|
||||
"issues": issues_list
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,23 +4,42 @@ from app.core.config import settings
|
||||
from app.schemas.qa_report import QARawResults, RuntimeOutput
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async_client = httpx.AsyncClient(verify=False)
|
||||
|
||||
async def run_project_qa(repo_name: str) -> QARawResults:
|
||||
async def run_project_qa(repo_url: str) -> QARawResults:
|
||||
"""
|
||||
Sollicite l'API du conteneur persistant arc-sandbox pour effectuer
|
||||
une analyse de qualité et sécurité statique (Semgrep).
|
||||
Sollicite l'API du conteneur arc-sandbox pour effectuer
|
||||
une analyse de qualité et sécurité statique (Semgrep) en clonant le dépôt.
|
||||
"""
|
||||
sandbox_url = f"http://arc-sandbox:8004/scan/{repo_name}"
|
||||
logger.info(f"[QA Client] Envoi de la requête de scan à la sandbox : {sandbox_url}")
|
||||
sandbox_url = "http://arc-sandbox:8004/scan"
|
||||
logger.info(f"[QA Client] Envoi de la requête de scan pour le dépôt : {repo_url}")
|
||||
|
||||
try:
|
||||
response = await async_client.post(sandbox_url, timeout=120.0)
|
||||
response = await async_client.post(sandbox_url, params={"repo_url": repo_url}, timeout=150.0)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return QARawResults.model_validate(data.get("results"))
|
||||
|
||||
if data is None:
|
||||
logger.error("[QA Client] ❌ La Sandbox a répondu 200 OK mais a renvoyé une réponse vide (null).")
|
||||
return QARawResults(
|
||||
is_executable=False,
|
||||
runtime=RuntimeOutput(
|
||||
exit_code=-1,
|
||||
stdout="",
|
||||
stderr="Erreur interne Sandbox : Réponse JSON vide.",
|
||||
duration_seconds=0,
|
||||
timeout_triggered=False
|
||||
),
|
||||
issues=[]
|
||||
)
|
||||
|
||||
results = data.get("results", {})
|
||||
if not results.get("is_executable"):
|
||||
logger.error(f"[QA Client] ❌ Erreur interne Sandbox (Semgrep STDERR) : {results.get('runtime', {}).get('stderr')}")
|
||||
|
||||
return QARawResults.model_validate(results)
|
||||
|
||||
else:
|
||||
logger.error(f"[QA Client] Erreur de la Sandbox (Status {response.status_code}): {response.text}")
|
||||
return QARawResults(
|
||||
|
||||
10094
backend/app/sandbox/rules/ci.yaml
Normal file
10094
backend/app/sandbox/rules/ci.yaml
Normal file
File diff suppressed because it is too large
Load Diff
20054
backend/app/sandbox/rules/cwe-top-25.yaml
Normal file
20054
backend/app/sandbox/rules/cwe-top-25.yaml
Normal file
File diff suppressed because it is too large
Load Diff
70779
backend/app/sandbox/rules/default.yaml
Normal file
70779
backend/app/sandbox/rules/default.yaml
Normal file
File diff suppressed because it is too large
Load Diff
41421
backend/app/sandbox/rules/owasp-top-ten.yaml
Normal file
41421
backend/app/sandbox/rules/owasp-top-ten.yaml
Normal file
File diff suppressed because it is too large
Load Diff
13698
backend/app/sandbox/rules/security-audit.yaml
Normal file
13698
backend/app/sandbox/rules/security-audit.yaml
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user