Files
knowledge-base/scripts/kb-audit-dns.py
dttb 1f7d265f16 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 не резолвится (публичные записи протухли)
2026-04-18 12:35:21 +03:00

137 lines
5.0 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()