SentinelScan-WSS / backend /scanners /git_exposure_scanner.py
larxius's picture
Deploy SentinelScan WSS to HF Spaces
d543fc1 verified
Raw
History Blame Contribute Delete
7.41 kB
"""
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