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:
136
scripts/kb-audit-dns.py
Executable file
136
scripts/kb-audit-dns.py
Executable 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()
|
||||
Reference in New Issue
Block a user