""" git_exposure_scanner.py — Git Repository Exposure Scanner ========================================================== Checks for publicly accessible .git directories and version control artifacts that could expose source code, commit history, credentials, and configuration. Checks: - .git/HEAD, .git/config, .git/COMMIT_EDITMSG, .git/index - .gitignore, .gitmodules, .gitattributes - Common VCS metadata files (SVN, Mercurial) - Source code backup files - CI/CD configuration leaks """ import re, urllib.request, urllib.error from scanners.base_scanner import BaseScanner from scanners.core.signatures import matches_signature # Format: (path, expected_content_regex, display_name, severity, cvss) GIT_PROBES = [ ("/.git/HEAD", r"ref:\s*refs/heads/", "Git HEAD reference", "Critical", 9.8), ("/.git/config", r"\[core\]|\[remote", "Git repository config", "Critical", 9.8), ("/.git/COMMIT_EDITMSG",r".*", "Git commit message", "High", 8.5), ("/.git/index", r"DIRC", "Git index (binary)", "Critical", 9.8), ("/.git/FETCH_HEAD", r".*", "Git FETCH_HEAD", "High", 8.0), ("/.git/packed-refs", r"refs/", "Git packed refs", "High", 8.0), ("/.git/logs/HEAD", r"commit|checkout", "Git reflog", "High", 8.0), ("/.gitignore", r".*", ".gitignore file", "Medium", 5.3), ("/.gitmodules", r"\[submodule", ".gitmodules (submodule list)","High", 7.5), ("/.gitattributes", r".*", ".gitattributes", "Low", 3.1), # SVN ("/.svn/entries", r"https?://|svn://", "SVN entries file", "High", 8.0), ("/.svn/wc.db", r"SQLite", "SVN working copy DB", "Critical", 9.0), # Mercurial ("/.hg/hgrc", r"\[paths\]", "Mercurial config", "High", 8.0), # CI / CD secrets ("/.travis.yml", r"language:|script:", "Travis CI config", "Medium", 5.3), ("/.env", r"[A-Z_]+=", ".env file (env variables)", "Critical", 9.9), ("/.env.local", r"[A-Z_]+=", ".env.local file", "Critical", 9.9), ("/.env.production", r"[A-Z_]+=", ".env.production file", "Critical", 9.9), ("/Dockerfile", r"FROM |RUN |CMD ", "Dockerfile exposed", "Medium", 5.3), ("/docker-compose.yml", r"services:|version:", "docker-compose config", "Medium", 5.8), ("/Jenkinsfile", r"pipeline|stage", "Jenkinsfile exposed", "Medium", 5.3), ("/.github/workflows/", r"on:|jobs:", "GitHub Actions workflows", "Low", 3.5), # Backup / source files ("/backup.sql", r"INSERT INTO|CREATE TABLE","SQL dump exposed", "Critical", 9.9), ("/dump.sql", r"INSERT INTO|CREATE TABLE","SQL dump exposed", "Critical", 9.9), ("/config.php.bak", r".*", "PHP config backup", "High", 8.5), ("/wp-config.php.bak", r".*", "WordPress config backup", "Critical", 9.8), ] class GitExposureScanner(BaseScanner): SCANNER_NAME = "Git / VCS Exposure Scanner" _SCANNER_KEY = "git_exposure" def __init__(self, scan_id, target, domain, **kwargs): super().__init__(scan_id, target, domain, **kwargs) # ------------------------------------------------------------------ def run(self) -> list: self.log("INFO", f"[GitExposure] Probing {len(GIT_PROBES)} VCS/backup paths on {self.target}...") base = self.target.rstrip("/") found = 0 for path, pattern, display, severity, cvss in GIT_PROBES: url = f"{base}{path}" body, status = self._probe(url) if body is None: continue # PHASE 1: Suppress if response is the site's SPA/404 catch-all if self._is_baseline(status, body): self.log("INFO", f"[GitExposure] SUPPRESSED (baseline match): {url}") continue if status == 200 and re.search(pattern, body, re.S | re.I): found += 1 self.log("CRITICAL" if severity == "Critical" else "WARNING", f"[GitExposure] EXPOSED: {display} at {url}") masked = body[:300].replace("\n", " ") self.add_vuln( title=f"Exposed VCS/Config File: {display}", severity=severity, category="Information Disclosure", cvss_score=cvss, description=( f"The file `{url}` is publicly accessible (HTTP 200).\n\n" f"**Resource type:** {display}\n\n" f"**Preview (first 300 chars):**\n```\n{masked}\n```\n\n" "Exposed version control metadata can reveal:\n" "- Full source code reconstruction from pack files\n" "- Hardcoded credentials and API keys in commit history\n" "- Internal infrastructure hostnames and paths\n" "- Business logic and proprietary algorithms" ), remediation=( f"1. Immediately block access to `{path}` in your web server:\n" " Nginx: location ~ /\\.git { deny all; return 404; }\n" " Apache: RedirectMatch 404 /\\.git\n" "2. Rotate any credentials found in git history immediately.\n" "3. Use `git filter-branch` or BFG Repo Cleaner to purge secrets.\n" "4. Add VCS directories to your web server deny rules globally.\n" "5. Use a WAF rule to block /.git, /.svn, /.env paths." ), ) elif status == 200: self.log("INFO", f"[GitExposure] {url} returned 200 but content mismatch") else: self.log("INFO", f"[GitExposure] {url} -> HTTP {status}") if found == 0: self.log("SUCCESS", "[GitExposure] No exposed VCS/backup files detected") else: self.log("WARNING", f"[GitExposure] {found} exposed resource(s) found!") return self.vulns # ------------------------------------------------------------------ def _probe(self, url: str) -> tuple: try: headers = {"User-Agent": "LarShield/2.0 GitExposure-Probe"} headers.update(self.auth_headers or {}) req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req, timeout=6, context=self.get_ssl_context()) as r: return r.read().decode("utf-8", errors="ignore"), r.status except urllib.error.HTTPError as e: return "", e.code except Exception as e: self.log("ERROR", f"[GitExposure] Fetch error: {e}") return None, 0