From c91c3f1023c08cbd30aa50396928c800f4ef0c6b Mon Sep 17 00:00:00 2001 From: Beda Schmid Date: Wed, 20 May 2026 13:41:56 -0300 Subject: [PATCH] DKIM selectors where queried separatedly --- app/dns_policy.py | 33 ++++++++++++++++++++++++++------- app/main.py | 25 ++++++++++++++++++------- app/static/app.css | 20 ++++++++++++++++++++ app/templates/domain.html | 20 +++++++++++++++----- tests/test_dns_policy.py | 6 ++++-- 5 files changed, 83 insertions(+), 21 deletions(-) diff --git a/app/dns_policy.py b/app/dns_policy.py index ec4b1e4..f9b8e28 100644 --- a/app/dns_policy.py +++ b/app/dns_policy.py @@ -32,6 +32,7 @@ class ParsedSpfRecord: @dataclass class DkimRecord: selector: str + domain: str query_name: str record: str | None = None error: str | None = None @@ -138,7 +139,7 @@ def parse_spf_records(records: list[str]) -> tuple[ParsedSpfRecord, list[str]]: def collect_domain_dns_policy( domain: str, *, - selectors: list[str] | None = None, + selectors: list[str | tuple[str, str]] | None = None, txt_lookup: TxtLookup | None = None, mx_lookup: MxLookup | None = None, ) -> DomainDnsPolicy: @@ -164,16 +165,34 @@ def collect_domain_dns_policy( except Exception as exc: policy.errors.append(f"MX lookup failed: {exc}") - for selector in sorted({item.strip().lower() for item in selectors or [] if item and item.strip()}): - query_name = f"{selector}._domainkey.{domain}" + selector_domains: set[tuple[str, str]] = set() + for item in selectors or []: + if isinstance(item, tuple): + selector, dkim_domain = item + else: + selector, dkim_domain = item, domain + selector = (selector or "").strip().lower() + dkim_domain = (dkim_domain or domain).strip().lower().rstrip(".") + if selector and dkim_domain: + selector_domains.add((selector, dkim_domain)) + + for selector, dkim_domain in sorted(selector_domains): + query_name = f"{selector}._domainkey.{dkim_domain}" try: records = txt_lookup(query_name) dkim_records = [record for record in records if record.strip().lower().startswith("v=dkim1")] - policy.dkim.append(DkimRecord(selector=selector, query_name=query_name, record=dkim_records[0] if dkim_records else None)) + policy.dkim.append( + DkimRecord( + selector=selector, + domain=dkim_domain, + query_name=query_name, + record=dkim_records[0] if dkim_records else None, + ) + ) if not dkim_records: - policy.errors.append(f"DKIM record not found for selector {selector}") + policy.errors.append(f"DKIM record not found for selector {selector} on {dkim_domain}") except Exception as exc: - policy.dkim.append(DkimRecord(selector=selector, query_name=query_name, error=str(exc))) - policy.errors.append(f"DKIM lookup failed for selector {selector}: {exc}") + policy.dkim.append(DkimRecord(selector=selector, domain=dkim_domain, query_name=query_name, error=str(exc))) + policy.errors.append(f"DKIM lookup failed for selector {selector} on {dkim_domain}: {exc}") return policy diff --git a/app/main.py b/app/main.py index bafb2e9..d79fdb3 100644 --- a/app/main.py +++ b/app/main.py @@ -400,9 +400,9 @@ def _json_list(value: str | None) -> list: return data if isinstance(data, list) else [] -def _observed_dkim_selectors(session: Session, domain: str) -> list[str]: +def _observed_dkim_selectors(session: Session, domain: str) -> list[tuple[str, str]]: rows = session.execute( - select(AuthResult.selector) + select(AuthResult.selector, AuthResult.domain) .select_from(AuthResult) .join(Record) .join(Report) @@ -410,11 +410,12 @@ def _observed_dkim_selectors(session: Session, domain: str) -> list[str]: Report.domain == domain, AuthResult.auth_type == "dkim", AuthResult.selector.is_not(None), + AuthResult.domain.is_not(None), ) .distinct() - .order_by(AuthResult.selector) - ).scalars().all() - return [row for row in rows if row] + .order_by(AuthResult.domain, AuthResult.selector) + ).all() + return [(selector, auth_domain) for selector, auth_domain in rows if selector and auth_domain] def _snapshot_model(domain: str, policy: DomainDnsPolicy) -> DomainDnsSnapshot: @@ -447,6 +448,14 @@ def _latest_dns_snapshot(session: Session, domain: str) -> SimpleNamespace | Non ) if not snapshot: return None + dkim_records = _json_list(snapshot.dkim_records_json) + dkim_found = [item for item in dkim_records if isinstance(item, dict) and item.get("record")] + dkim_missing = [item for item in dkim_records if isinstance(item, dict) and not item.get("record")] + dns_errors = [ + item + for item in _json_list(snapshot.errors_json) + if isinstance(item, str) and not item.startswith("DKIM lookup failed") and not item.startswith("DKIM record not found") + ] return SimpleNamespace( id=snapshot.id, domain=snapshot.domain, @@ -463,9 +472,11 @@ def _latest_dns_snapshot(session: Session, domain: str) -> SimpleNamespace | Non spf_record=snapshot.spf_record, spf_all=snapshot.spf_all, spf_includes=_json_list(snapshot.spf_includes_json), - dkim_records=_json_list(snapshot.dkim_records_json), + dkim_records=dkim_records, + dkim_found=dkim_found, + dkim_missing=dkim_missing, mx_records=_json_list(snapshot.mx_records_json), - errors=_json_list(snapshot.errors_json), + errors=dns_errors, ) diff --git a/app/static/app.css b/app/static/app.css index 21fd85e..b2ff801 100644 --- a/app/static/app.css +++ b/app/static/app.css @@ -1212,6 +1212,26 @@ button { padding: 8px 10px; } +.dw-dns-missing { + margin-top: 12px; +} + +.dw-dns-missing summary { + color: var(--dw-muted); + cursor: pointer; + font-size: 12px; + font-weight: 700; + line-height: 18px; +} + +.dw-dns-missing-list { + display: grid; + gap: 6px; + margin-top: 8px; + max-height: 220px; + overflow: auto; +} + .dw-panel-title { color: var(--dw-text); font-size: 20px; diff --git a/app/templates/domain.html b/app/templates/domain.html index 18444ce..acec7b0 100644 --- a/app/templates/domain.html +++ b/app/templates/domain.html @@ -67,18 +67,28 @@
DKIM - {{ dns_snapshot.dkim_records | length }} selectors + {{ dns_snapshot.dkim_found | length }} found{% if dns_snapshot.dkim_missing %}, {{ dns_snapshot.dkim_missing | length }} missing{% endif %}
- {% for item in dns_snapshot.dkim_records %} + {% for item in dns_snapshot.dkim_found %}
- {{ item.selector }} - {{ item.record or item.error or "No DKIM record found." }} + {{ item.selector }}._domainkey.{{ item.domain or domain }} + {{ item.record }}
{% else %} - No observed DKIM selectors yet. + No currently resolvable observed DKIM selectors. {% endfor %}
+ {% if dns_snapshot.dkim_missing %} +
+ {{ dns_snapshot.dkim_missing | length }} observed selector lookups did not resolve +
+ {% for item in dns_snapshot.dkim_missing %} + {{ item.query_name }}{% if item.error %}: {{ item.error }}{% endif %} + {% endfor %} +
+
+ {% endif %}
diff --git a/tests/test_dns_policy.py b/tests/test_dns_policy.py index 08fdf6d..a635800 100644 --- a/tests/test_dns_policy.py +++ b/tests/test_dns_policy.py @@ -24,7 +24,7 @@ def test_collect_domain_dns_policy_uses_observed_dkim_selectors(): txt_records = { "_dmarc.example.com": ["v=DMARC1; p=reject; pct=100"], "example.com": ["v=spf1 include:_spf.example.net -all"], - "s1._domainkey.example.com": ["v=DKIM1; k=rsa; p=abc"], + "s1._domainkey.mail.example.net": ["v=DKIM1; k=rsa; p=abc"], } def txt_lookup(name: str) -> list[str]: @@ -34,7 +34,7 @@ def test_collect_domain_dns_policy_uses_observed_dkim_selectors(): policy = collect_domain_dns_policy( "example.com", - selectors=["s1"], + selectors=[("s1", "mail.example.net")], txt_lookup=txt_lookup, mx_lookup=lambda name: ["10 mail.example.com"], ) @@ -43,5 +43,7 @@ def test_collect_domain_dns_policy_uses_observed_dkim_selectors(): assert policy.spf.all_mechanism == "-all" assert policy.mx_records == ["10 mail.example.com"] assert policy.dkim[0].selector == "s1" + assert policy.dkim[0].domain == "mail.example.net" + assert policy.dkim[0].query_name == "s1._domainkey.mail.example.net" assert policy.dkim[0].record == "v=DKIM1; k=rsa; p=abc" assert policy.errors == []