audit: +kb-audit-npm/creds/dns — расширение karpathy-style

- kb-audit-npm.py: NPM API → сверка с npm-proxy-hosts.md
  детектит новые/удалённые hosts + смену backend/SSL
- kb-audit-creds.py: HEAD/GET-ping всех URL из credentials.md
  с fallback на GET при 501/405, skip embedded-creds URLs
- kb-audit-dns.py: dig @8.8.8.8 и @10.0.0.1 для всех доменов NPM
  детектит NXDOMAIN + split-horizon

Первый прогон нашёл:
- NPM: 2 новых host (router/vpn.dttb.ru), 2 изменения (bitrix24 backend, git SSL)
- Creds: все 12 URL reachable ✓
- DNS: itilegent.ru не резолвится (публичные записи протухли)
This commit is contained in:
dttb
2026-04-18 12:35:21 +03:00
parent c4791dc7d5
commit 1f7d265f16
6 changed files with 621 additions and 0 deletions

136
scripts/kb-audit-dns.py Executable file
View File

@@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""
kb-audit-dns — резолвит все домены из NPM API через публичный DNS (8.8.8.8),
сравнивает с локальным резолвом, детектит NXDOMAIN и расхождения.
Пишет audit/YYYY-MM-DD-dns-drift.md.
"""
import json
import ssl
import subprocess
import sys
import urllib.request
from datetime import date
from pathlib import Path
NPM_URL = "https://npm.dttb.ru"
NPM_USER = "it5870@yandex.ru"
NPM_PASS = "1qaz!QAZ"
VAULT = Path(__file__).resolve().parent.parent
OUT_DIR = VAULT / "audit"
_CTX = ssl.create_default_context()
_CTX.check_hostname = False
_CTX.verify_mode = ssl.CERT_NONE
def fetch_npm_domains():
"""Берём домены из NPM API (вместо парсинга markdown — свежее)."""
req = urllib.request.Request(
f"{NPM_URL}/api/tokens",
data=json.dumps({"identity": NPM_USER, "secret": NPM_PASS}).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
token = json.loads(urllib.request.urlopen(req, context=_CTX, timeout=10).read())["token"]
req2 = urllib.request.Request(
f"{NPM_URL}/api/nginx/proxy-hosts",
headers={"Authorization": f"Bearer {token}"},
)
hosts = json.loads(urllib.request.urlopen(req2, context=_CTX, timeout=10).read())
domains = set()
for h in hosts:
for d in h.get("domain_names", []):
domains.add(d)
return sorted(domains)
def dig(domain: str, server: str = "8.8.8.8") -> list[str]:
"""dig +short @server domain → список IP (пусто если NXDOMAIN/недоступен)."""
try:
r = subprocess.run(
["dig", f"@{server}", "+short", "+time=3", "+tries=1", domain, "A"],
capture_output=True, text=True, timeout=8,
)
return [l.strip() for l in r.stdout.splitlines() if l.strip() and not l.startswith(";")]
except (subprocess.TimeoutExpired, FileNotFoundError):
return []
def main():
today = date.today().isoformat()
OUT_DIR.mkdir(parents=True, exist_ok=True)
out = OUT_DIR / f"{today}-dns-drift.md"
try:
domains = fetch_npm_domains()
except Exception as e:
print(f"NPM fetch fail: {e}", file=sys.stderr)
sys.exit(1)
if len(domains) < 3:
print(f"safety abort: domains {len(domains)} < 3", file=sys.stderr)
sys.exit(2)
results = []
for d in domains:
public = dig(d, "8.8.8.8")
local = dig(d, "10.0.0.1") # OpenWrt router
results.append((d, public, local))
no_public = [r for r in results if not r[1]]
split = [r for r in results if r[1] and r[2] and set(r[1]) != set(r[2])]
no_local = [r for r in results if not r[2]]
lines = [
"---",
f"date: {today}",
"type: audit",
"source: kb-audit-dns.py",
"tags: [audit, dns]",
"---",
"",
f"# DNS resolve audit — {today}",
"",
f"Резолвим все домены из NPM через публичный DNS (8.8.8.8) и локальный роутер (10.0.0.1).",
"",
f"- Всего доменов: **{len(domains)}**",
f"- NXDOMAIN на 8.8.8.8: {len(no_public)} / пустой ответ локально: {len(no_local)} / split-horizon: {len(split)}",
"",
]
if no_public:
lines += ["## ❌ NXDOMAIN / не резолвится на 8.8.8.8 (публичный DNS)", ""]
lines += ["| Домен | Локальный IP |", "|---|---|"]
for d, _, loc in no_public:
lines.append(f"| `{d}` | {','.join(loc) or '(тоже нет)'} |")
lines += [""]
if split:
lines += ["## ⚠ Split-horizon — разные IP снаружи и внутри", "",
"Это нормально для *.dttb.ru (внешний Let's Encrypt IP vs локальный 10.0.0.195). Но неожиданный split может быть багом.", ""]
lines += ["| Домен | Публичный (8.8.8.8) | Локальный (10.0.0.1) |", "|---|---|---|"]
for d, pub, loc in split:
lines.append(f"| `{d}` | {','.join(pub)} | {','.join(loc)} |")
lines += [""]
if no_local:
lines += ["## ⚠ Пустой локальный резолв (роутер не знает)", ""]
for d, pub, _ in no_local:
lines.append(f"- `{d}` (публичный: {','.join(pub) or '-'})")
lines += [""]
lines += ["## Полная таблица резолва", "",
"| Домен | 8.8.8.8 | 10.0.0.1 |", "|---|---|---|"]
for d, pub, loc in results:
lines.append(f"| `{d}` | {','.join(pub) or ''} | {','.join(loc) or ''} |")
lines += ["", "---", "*Автоматически через `scripts/kb-audit-dns.py`.*"]
out.write_text("\n".join(lines))
print(f"dns drift: {out}")
print(f" no_public: {len(no_public)} / split: {len(split)} / no_local: {len(no_local)}")
if __name__ == "__main__":
main()