#!/usr/bin/env bash set -euo pipefail root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P) if [[ $# -gt 0 ]]; then [[ $# -eq 2 && $1 == --root && -d $2 ]] || { echo 'usage: verify-dwh-auth-docs.sh [--root DIRECTORY]' >&2; exit 2; } root=$(cd "$2" && pwd -P) fi python3 - "$root" <<'PY' import pathlib, re, sys root = pathlib.Path(sys.argv[1]) docs = { "server": "docs/install/dwh-auth-server.md", "client": "docs/install/dwh-auth-client-enrollment.md", "tls": "docs/install/dwh-auth-tls.md", "rollout": "docs/operations/psd-dwh-auth-rollout.md", "manual": "docs/testing/dwh-auth-manual-acceptance.md", "evidence": "docs/testing/evidence/psd-dwh-auth-rollout-report-template.md", "local": "docs/install/local-workspace-registry.md", "server_registry": "docs/install/server-workspace-registry.md", "psd": "docs/install/psd-workspace-setup.md", "guide": "docs/guida-utente.md", "index": "docs/index.md", "nav": "mkdocs.yml", "psd_template": "deploy/psd/workspace-bindings.env.example", } text = {} for label, relative in docs.items(): path = root / relative if not path.is_file(): raise SystemExit(f"dwh-auth docs: missing {relative}") text[label] = path.read_text(encoding="utf-8") requirements = { "server": ["key_output=/root/dwh-auth-provision/psd-mac-primary.key", "legacy_key_id=legacy-shared", "v1_header_file", "legacy_header_file", "random_header_file", "--header \"@", "registry_staging", "registry_previous", "registry_restore_rollback", "journal_actual_key_scan=PASS", "sys.argv[2:]", "read_bytes()", "subprocess.Popen", "stderr=subprocess.DEVNULL", "returncode != 0", "max_journal_bytes = 1_048_576", "max_journal_lines = 10_000", "stdout=subprocess.PIPE", "max_chunk_bytes = 65_536", "process.stdout.read1(", "remaining = max_journal_bytes - bytes_seen", "searchable = carry + chunk", "max_needle_length", "process.kill()", "process.wait()", "socket_v1=PASS", "socket_legacy=PASS", "https_v1_post_revoke=PASS", "https_legacy_post_revoke=PASS", "key_file_bytes=PASS", "dd if=\"$1\" bs=65536 status=none", "manifest", "journalctl", "systemctl disable --now", "trap", "/var/lib/dwh-auth", "root:dwh-auth", "2750", ".writer.lock", "0640", "/run/dwh-auth/verify.sock", "0660", "systemd", "key create", "key list", "key status", "key revoke", "check", "backup", "rollback", "disinstallazione", "rest_api", "postgres_direct", "ssh_tunnel"], "client": ["Workspace management", "Validate workspace source", "Test workspace connections", "Save entered secrets", "Forget stored value", "API_KEY_FILE", "THT_WS_PSD_CLINICAL_DWH_API_KEY_FILE", "TLS_CA_FILE", "/rpc/ping", "rest_api", "postgres_direct", "ssh_tunnel", "401", "503", "rotazione", "revoca"], "tls": ["self-issued", ".it", ".com", "SAN", "TLS_CA_FILE", "openssl x509 -noout -fingerprint -sha256", "fuori banda", "rinnovo", "curl -k"], "rollout": ["v1=2xx", "legacy=2xx", "v1=2xx post-revoca", "legacy=401 post-revoca", "file header curl protetti 0600", "PASS/FAIL", "no raw diff", "Task 9", "Task 10", "IN_DISCUSSION", "postgres_direct", "rest_api", "legacy-shared", "nginx -t", "204", "401", "503", "Qdrant", "Ollama", "rollback"], "manual": ["credenziali reali", "sintetici", "/rpc/ping", "204", "401", "503", "TLS", "registry", "postgres_direct", "ssh_tunnel"], "evidence": ["ID pubblici", "owner", "mode", "timestamp", "checksum", "approvazione"], } for label, tokens in requirements.items(): lowered = text[label].lower() for token in tokens: if token.lower() not in lowered: raise SystemExit(f"dwh-auth docs: {docs[label]} lacks required topic: {token}") for path in [root / docs[k] for k in ("server", "client", "tls", "rollout", "manual", "evidence", "local", "server_registry", "psd", "guide", "index")]: source = path.read_text(encoding="utf-8") for target in re.findall(r"(? {target}") nav = text["nav"] for relative in (docs["server"], docs["client"], docs["tls"], docs["rollout"], docs["manual"], docs["evidence"]): nav_relative = relative.removeprefix("docs/") if nav.count(nav_relative) != 1: raise SystemExit(f"dwh-auth docs: navigation must include once: {nav_relative}") corpus = "\n".join(text.values()) for pattern, label in [ (r"thtdwh_v1\.[A-Za-z0-9_-]{16}\.[A-Za-z0-9_-]{43}", "credential literal"), (r"(?mi)^\s*[\"']?secret_sha256[\"']?\s*[:=]\s*[\"']?[A-Za-z0-9_-]{16,}", "credential digest literal"), (r"(?mi)^\s*(?:export\s+)?[A-Z][A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD)\s*=\s*(?!/|<)[^\s#]+", "secret in environment"), (r"(?i)(?:curl|dwh-auth)[^\n]{0,240}(?:(?:-H|--header)\s+['\"][^'\"]*X-API-Key\s*:|--(?:api-key|token|password)\b)", "secret in argv"), (r"(?im)^(?!.*(?:non usare|mai usare)).*curl\s+(?:[^\n]*\s)?(?:-k|--insecure)\b|verify_tls\s*=\s*false|insecure_skip_verify", "TLS bypass"), (r"(?i)chmod\s+0?[0-7][0-7][4-7]\s+[^\n]*(?:\.key|secret|provision)", "world-readable secret"), (r"(?m)^\s*(?:sudo\s+)?nginx\s+-T\b", "raw Nginx capture"), (r"(?m)^\s*(?:sudo\s+)?(?:diff\b|git\s+diff\b)", "raw diff capture"), (r"(?m)^\s*(?:sudo\s+)?curl[^\n]*--config\b", "curl config artifact"), (r"(?i)docker\s+compose[^\n]*\bdwh-auth\b", "Compose coupling"), ]: if re.search(pattern, corpus): raise SystemExit(f"dwh-auth docs: forbidden {label}") server_direct = re.search(r"server PSD[^\n]{0,100}postgres_direct", text["rollout"], re.I) psd_direct = re.search(r"server PSD[^\n]{0,100}postgres_direct", text["psd"], re.I) if not server_direct or not psd_direct: raise SystemExit("dwh-auth docs: PSD server must remain postgres_direct") template = text["psd_template"] if "tht_ws_psd_clinical_dwh_transport=rest_api" not in template.lower() or "mac/local/remota" not in template.lower() or not re.search(r"mai .*server psd", template, re.I): raise SystemExit("dwh-auth docs: PSD REST template must be explicitly Mac/local/remota, never server PSD") if "TLS_CA_FILE" not in text["psd"] or re.search(r"(?i)nessuna CA|HTTPS pubblico", text["psd"]): raise SystemExit("dwh-auth docs: PSD setup contradicts private CA TLS requirement") restore_steps = ( 'registry_staging="/var/lib/.dwh-auth-restore-$run_id"', 'registry_candidate="$registry_staging/dwh-auth"', 'sudo tar --acls --xattrs -C "$registry_staging" -xf "$registry_backup"', 'if ! sudo /usr/local/sbin/dwh-auth --registry-root "$registry_candidate" check; then', 'if ! sudo mv -T -- "$registry_root" "$registry_previous"; then', 'if ! sudo mv -T -- "$registry_candidate" "$registry_root"; then', 'if ! sudo mv -T -- "$registry_previous" "$registry_root"; then', 'if ! sudo mv -T -- "$registry_root" "$registry_staging/failed-dwh-auth"; then', ) restore_positions = [text["server"].find(step) for step in restore_steps] if any(position < 0 for position in restore_positions) or restore_positions != sorted(restore_positions): raise SystemExit("dwh-auth docs: restore must stage/check then use guarded same-filesystem renames") if text["server"].count('if ! sudo mv -T -- "$registry_previous" "$registry_root"; then') != 2: raise SystemExit("dwh-auth docs: every restore rollback rename must use guarded mv -T --") if re.search(r"sudo tar[^\n]*-C /var/lib[^\n]*(?:-x|--extract)[^\n]*registry_backup", text["server"]): raise SystemExit("dwh-auth docs: restore must not overlay the live registry") journal_steps = ( "subprocess.Popen(", "[\"journalctl\", \"-u\", \"dwh-auth\", \"--since\", sys.argv[1]", "sys.argv[2:]", "stderr=subprocess.DEVNULL", "stdout=subprocess.PIPE", "max_journal_bytes = 1_048_576", "max_journal_lines = 10_000", "max_chunk_bytes = 65_536", "process.stdout.read1(", "remaining = max_journal_bytes - bytes_seen", "searchable = carry + chunk", "max_needle_length", "process.kill()", "process.wait()", "if process.returncode != 0:", "any(needle in searchable for needle in needles)", ) if any(step not in text["server"] for step in journal_steps): raise SystemExit("dwh-auth docs: journal scan must fail closed and check the actual key bytes") print("dwh-auth documentation contract passed") PY