import subprocess # nosec B404 from typing import Any from app.tools.semgrep_scanner import run_semgrep def run_ruff(local_path: str) -> dict[str, Any]: result: dict[str, Any] = {"output": "", "issues": [], "success": False} try: process = subprocess.run( # nosec ["ruff", "check", ".", "--output-format=text"], cwd=local_path, capture_output=True, text=True, timeout=60, ) result["output"] = process.stdout + process.stderr result["issues"] = [ line for line in str(result["output"]).splitlines() if line.strip() ] result["success"] = True except Exception as e: result["output"] = f"Ruff error: {str(e)}" return result def run_bandit(local_path: str) -> dict[str, Any]: result: dict[str, Any] = {"output": "", "issues": [], "success": False} try: process = subprocess.run( # nosec ["bandit", "-r", ".", "-f", "txt", "-q"], cwd=local_path, capture_output=True, text=True, timeout=60, ) result["output"] = process.stdout + process.stderr result["issues"] = [ line for line in str(result["output"]).splitlines() if line.strip() ] result["success"] = True except Exception as e: result["output"] = f"Bandit error: {str(e)}" return result def run_full_analysis(local_path: str) -> dict[str, Any]: ruff_results = run_ruff(local_path) bandit_results = run_bandit(local_path) semgrep_result = run_semgrep(local_path) # Normalise Semgrep findings into the same flat issues list format # used by ruff/bandit so callers don't need to know the difference semgrep_issues = [ f"[{f['severity'].upper()}] {f['rule_id']} {f['file']}:{f['line']} — {f['message']}" for f in semgrep_result.get("findings", []) ] return { "ruff": ruff_results, "bandit": bandit_results, "semgrep": { "issues": semgrep_issues, "findings": semgrep_result.get("findings", []), # structured form for downstream use "success": semgrep_result.get("success", False), "error": semgrep_result.get("error"), }, "total_issues": ( len(ruff_results["issues"]) + len(bandit_results["issues"]) + len(semgrep_issues) ), }