docs(security): add supply chain contract manifest [skip ci]
This commit is contained in:
138
scripts/security/github-target-probe.py
Normal file
138
scripts/security/github-target-probe.py
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user