Files
awoooi/scripts/security/github-target-probe.py

139 lines
4.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""GitHub target repo 只讀存在性探測。
此工具使用 `git ls-remote --heads` 檢查候選 GitHub repo 是否可讀。
它不 clone、不 fetch、不 push也不寫入任何 remote。
"""
from __future__ import annotations
import argparse
import json
import subprocess
from pathlib import Path
def probe_candidate(candidate: str, timeout: int) -> dict[str, object]:
url = f"https://github.com/{candidate}.git"
try:
result = subprocess.run(
["git", "ls-remote", "--heads", url],
check=False,
capture_output=True,
text=True,
timeout=timeout,
)
except subprocess.TimeoutExpired:
return {
"github_repo": candidate,
"url_redacted": url,
"status": "timeout",
"head_count": 0,
"heads": [],
"error_summary": "git ls-remote timeout",
}
heads = []
if result.returncode == 0:
for line in result.stdout.splitlines():
parts = line.split()
if len(parts) != 2 or not parts[1].startswith("refs/heads/"):
continue
heads.append(
{
"name": parts[1].removeprefix("refs/heads/"),
"sha": parts[0],
}
)
status = "exists" if heads else "exists_empty_or_no_heads"
error_summary = ""
else:
stderr = result.stderr.strip()
if "Repository not found" in stderr:
status = "not_found_or_private"
error_summary = "GitHub 回應 repository not found可能未建立或為 private 且未授權"
else:
status = "error"
error_summary = stderr.splitlines()[-1] if stderr else "git ls-remote failed"
return {
"github_repo": candidate,
"url_redacted": url,
"status": status,
"head_count": len(heads),
"heads": heads,
"error_summary": error_summary,
}
def write_markdown(payload: dict[str, object], path: Path) -> None:
lines = [
"# GitHub Target Probe 快照",
"",
"| 項目 | 值 |",
"|------|----|",
f"| 狀態 | `{payload['status']}` |",
f"| 候選 repo 數 | `{payload['candidate_count']}` |",
f"| exists | `{payload['exists_count']}` |",
f"| not found or private | `{payload['not_found_or_private_count']}` |",
"",
"## 候選 Repo",
"",
"| GitHub repo | status | heads | error |",
"|-------------|--------|-------|-------|",
]
for candidate in payload.get("candidates", []):
if not isinstance(candidate, dict):
continue
lines.append(
"| "
+ " | ".join(
[
f"`{candidate.get('github_repo', '')}`",
f"`{candidate.get('status', '')}`",
f"`{candidate.get('head_count', 0)}`",
str(candidate.get("error_summary", "") or ""),
]
)
+ " |"
)
lines.extend(
[
"",
"> 注意:`not_found_or_private` 只代表未授權 read-only probe 看不到,不等同確認不存在。",
"",
]
)
path.write_text("\n".join(lines), encoding="utf-8")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--candidate", action="append", required=True)
parser.add_argument("--output-json", required=True)
parser.add_argument("--output-md", required=True)
parser.add_argument("--timeout", type=int, default=10)
args = parser.parse_args()
candidates = [probe_candidate(candidate, args.timeout) for candidate in args.candidate]
exists_count = sum(1 for item in candidates if item["status"].startswith("exists"))
not_found_count = sum(1 for item in candidates if item["status"] == "not_found_or_private")
payload = {
"schema_version": "github_target_probe_v1",
"status": "ok",
"candidate_count": len(candidates),
"exists_count": exists_count,
"not_found_or_private_count": not_found_count,
"candidates": candidates,
}
Path(args.output_json).write_text(
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
write_markdown(payload, Path(args.output_md))
return 0
if __name__ == "__main__":
raise SystemExit(main())