122 lines
5.1 KiB
Bash
Executable File
122 lines
5.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 外部事实登记表逐条重验:对 policy/facts.md 每条事实执行其验证命令并报告 PASS/FAIL。
|
||
# 规则依据:AGENTS.md「证据与验证门禁」(外部事实零信任);verification.md「声明与证据来源」。
|
||
# 用法:make facts-verify [ARGS=--update] (--update:把 PASS 项 last_verified 刷为当天并写回)
|
||
# 表内验证命令约定不含竖线;含 "\|" 转义时按字面还原后执行。
|
||
# 登记表引用的探针子命令(单独运行时在 stderr 说明判定,不输出凭据):
|
||
# api-token URL URL 须逐字等于登记端点 https://git-home.u1680832.nyat.app:30319/api/v1/user,
|
||
# 校验通过后才进程内读 ~/.secrets/gitea HOME 段 ACCESS_TOKEN 并 GET(不跟随重定向);
|
||
# 0=接受 3=缺失 4=格式不符 5=请求失败/被拒/重定向 6=非登记端点
|
||
# tls-handshake-fails HOST PORT 仅当 TCP 已连通且 TLS 层握手报错时 0;
|
||
# 3=DNS 失败 4=超时 5=连接失败 6=握手成功(端点仍可用)
|
||
set -uo pipefail
|
||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||
|
||
case "${1:-}" in
|
||
api-token)
|
||
[ $# -eq 2 ] || { echo "usage: facts-verify.sh api-token URL" >&2; exit 2; }
|
||
exec python3 - "$SCRIPT_DIR" "$2" <<'PY'
|
||
import sys, urllib.request
|
||
sys.path.insert(0, sys.argv[1])
|
||
import task_triage
|
||
|
||
ENDPOINT = "https://git-home.u1680832.nyat.app:30319/api/v1/user"
|
||
if sys.argv[2] != ENDPOINT:
|
||
print("api-token: URL 不是 policy/facts.md F2 登记端点,拒绝发送凭据", file=sys.stderr)
|
||
sys.exit(6)
|
||
|
||
|
||
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
||
def redirect_request(self, *args, **kwargs):
|
||
return None
|
||
|
||
|
||
urllib.request.install_opener(urllib.request.build_opener(NoRedirect))
|
||
url = ENDPOINT
|
||
token = task_triage.secrets_file_token()
|
||
if not token:
|
||
print("api-token: ~/.secrets/gitea HOME 段 ACCESS_TOKEN 缺失", file=sys.stderr)
|
||
sys.exit(3)
|
||
if not task_triage.TOKEN_RE.fullmatch(token):
|
||
print("api-token: ~/.secrets/gitea HOME 段 ACCESS_TOKEN 格式不符(应为单个无空白值)", file=sys.stderr)
|
||
sys.exit(4)
|
||
try:
|
||
task_triage.http_get_json(url, token)
|
||
except task_triage.Stop as stop:
|
||
print("api-token: " + stop.message, file=sys.stderr)
|
||
sys.exit(5)
|
||
print("api-token: accepted by " + url, file=sys.stderr)
|
||
PY
|
||
;;
|
||
tls-handshake-fails)
|
||
[ $# -eq 3 ] || { echo "usage: facts-verify.sh tls-handshake-fails HOST PORT" >&2; exit 2; }
|
||
exec python3 - "$2" "$3" <<'PY'
|
||
import socket, ssl, sys
|
||
|
||
host, port = sys.argv[1], int(sys.argv[2])
|
||
|
||
|
||
def verdict(code, text):
|
||
print(f"tls-handshake-fails {host}:{port}: {text}", file=sys.stderr)
|
||
sys.exit(code)
|
||
|
||
|
||
try:
|
||
sock = socket.create_connection((host, port), timeout=8)
|
||
except socket.gaierror:
|
||
verdict(3, "DNS resolution failed (not a TLS observation)")
|
||
except TimeoutError:
|
||
verdict(4, "TCP connect timed out (not a TLS observation)")
|
||
except OSError as exc:
|
||
verdict(5, f"TCP connect failed ({type(exc).__name__}; not a TLS observation)")
|
||
try:
|
||
with sock, ssl.create_default_context().wrap_socket(sock, server_hostname=host) as tls:
|
||
version = tls.version()
|
||
except ssl.SSLError as exc:
|
||
verdict(0, f"TLS handshake failed ({type(exc).__name__}: {getattr(exc, 'reason', None) or 'unknown'})")
|
||
except TimeoutError:
|
||
verdict(4, "TLS handshake timed out (not a TLS-layer error)")
|
||
except OSError as exc:
|
||
verdict(5, f"connection failed during handshake ({type(exc).__name__}; not a TLS-layer error)")
|
||
verdict(6, f"TLS handshake succeeded ({version}); endpoint still answers")
|
||
PY
|
||
;;
|
||
esac
|
||
|
||
F="${FACTS:-policy/facts.md}"
|
||
[ -f "$F" ] || { echo "ERROR: 事实登记表 $F 不存在" >&2; exit 1; }
|
||
python3 - "$F" "${1:-}" <<'PY'
|
||
import re, subprocess, sys, datetime
|
||
path, update = sys.argv[1], sys.argv[2] == "--update"
|
||
lines = open(path, encoding="utf-8").read().splitlines(keepends=True)
|
||
today = datetime.date.today().isoformat()
|
||
total = fails = 0
|
||
for i, line in enumerate(lines):
|
||
if not re.match(r"^\|\s*F\d+\s*\|", line):
|
||
continue
|
||
raw = line.strip().strip("|").replace("\\|", "\x00")
|
||
cells = [c.strip().replace("\x00", "|") for c in raw.split("|")]
|
||
if len(cells) < 4:
|
||
print(f"[row {i+1}] SKIP 格式不符(列数<4)", file=sys.stderr)
|
||
continue
|
||
fid, cmd = cells[0], cells[2].strip("`")
|
||
total += 1
|
||
try:
|
||
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=90)
|
||
ok = r.returncode == 0
|
||
except subprocess.TimeoutExpired:
|
||
ok = False
|
||
print(f"[{fid}] TIMEOUT >90s", file=sys.stderr)
|
||
print(f"[{fid}] {'PASS' if ok else 'FAIL'} :: {cmd[:80]}")
|
||
if not ok:
|
||
fails += 1
|
||
elif update and len(cells) >= 4 and re.match(r"\d{4}-\d{2}-\d{2}", cells[3]):
|
||
cells[3] = today
|
||
lines[i] = "| " + " | ".join(c.replace("|", "\\|") for c in cells) + " |\n"
|
||
if update:
|
||
open(path, "w", encoding="utf-8").writelines(lines)
|
||
print(f"[facts-verify] total={total} pass={total - fails} fail={fails}"
|
||
+ (";last_verified 已刷新为 " + today if update and total else ""))
|
||
sys.exit(1 if fails else 0)
|
||
PY
|