- 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 не резолвится (публичные записи протухли)
137 lines
5.0 KiB
Python
Executable File
137 lines
5.0 KiB
Python
Executable File
#!/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()
|