Specs stay at repo root (cross-VM). Move deploy and code into logical projects with README per domain, updated manifest.yaml, and symlinks at legacy paths for VM122 backward compatibility.
86 lines
2.4 KiB
Python
86 lines
2.4 KiB
Python
"""Public DNS checks via dig (read-only)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
from typing import Any
|
|
|
|
|
|
def _dig(*args: str) -> list[str]:
|
|
try:
|
|
proc = subprocess.run(
|
|
["dig", "+short", *args],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=8,
|
|
)
|
|
if proc.returncode != 0:
|
|
return []
|
|
lines = [ln.strip().strip('"') for ln in proc.stdout.splitlines() if ln.strip()]
|
|
return lines
|
|
except Exception:
|
|
return []
|
|
|
|
|
|
def _result(check_id: str, label: str, status: str, message: str, evidence: dict | None = None) -> dict[str, Any]:
|
|
return {
|
|
"check_id": check_id,
|
|
"label": label,
|
|
"status": status,
|
|
"message": message,
|
|
"evidence": evidence or {},
|
|
}
|
|
|
|
|
|
def collect(domain: str, mail_public_ip: str | None = None) -> dict[str, dict[str, Any]]:
|
|
domain = domain.lower().strip()
|
|
mail_host = f"mail.{domain}"
|
|
results: dict[str, dict[str, Any]] = {}
|
|
|
|
mx = _dig(domain, "MX")
|
|
mx_ok = any(mail_host in line or domain in line for line in mx)
|
|
results["dns_mx"] = _result(
|
|
"dns_mx",
|
|
"MX record",
|
|
"pass" if mx_ok else "fail",
|
|
f"MX: {', '.join(mx[:3]) or 'none'}",
|
|
{"records": mx},
|
|
)
|
|
|
|
txt_root = _dig(domain, "TXT")
|
|
spf = [t for t in txt_root if t.lower().startswith("v=spf1")]
|
|
results["dns_spf"] = _result(
|
|
"dns_spf",
|
|
"SPF",
|
|
"pass" if spf else "fail",
|
|
spf[0][:120] if spf else "SPF TXT not found",
|
|
{"records": spf},
|
|
)
|
|
|
|
dkim_name = f"default._domainkey.{domain}"
|
|
dkim = _dig(dkim_name, "TXT")
|
|
results["dns_dkim"] = _result(
|
|
"dns_dkim",
|
|
"DKIM",
|
|
"pass" if dkim else "fail",
|
|
"DKIM TXT present" if dkim else f"{dkim_name} not found",
|
|
{"records": dkim[:2]},
|
|
)
|
|
|
|
dmarc_name = f"_dmarc.{domain}"
|
|
dmarc = _dig(dmarc_name, "TXT")
|
|
results["dns_dmarc"] = _result(
|
|
"dns_dmarc",
|
|
"DMARC",
|
|
"pass" if dmarc else "warn",
|
|
dmarc[0][:120] if dmarc else "DMARC TXT not found",
|
|
{"records": dmarc},
|
|
)
|
|
|
|
if mail_public_ip:
|
|
a_mail = _dig(mail_host, "A")
|
|
if mail_public_ip not in a_mail and results["dns_mx"]["status"] == "pass":
|
|
results["dns_mx"]["status"] = "warn"
|
|
results["dns_mx"]["message"] += f" (A {mail_host}: {a_mail or 'none'})"
|
|
|
|
return results
|