-
Notifications
You must be signed in to change notification settings - Fork 0
Add dns command and failure pattern breakdown to check #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0a3644e
91d298e
b688d89
2aa63e5
d786884
bf7deba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -56,6 +56,8 @@ pub fn main() !void { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try cmdShow(allocator, args[2], format orelse "table", enrich); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (std.mem.eql(u8, command, "dns")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try cmdDns(allocator, domain, format orelse "text"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (std.mem.eql(u8, command, "domains")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try cmdDomains(allocator, format orelse "table", account); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (std.mem.eql(u8, command, "summary")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -277,6 +279,211 @@ fn fetchForAccount(allocator: std.mem.Allocator, acct: *const Config.Account, da | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return .{ .dmarc = counts.dmarc, .tls = counts.tls }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn cmdDns(allocator: std.mem.Allocator, domain_filter: ?[]const u8, format: []const u8) !void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cfg = try Config.load(allocator); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer cfg.deinit(allocator); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Collect domains from reports (or use filter) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var domains: std.ArrayList([]const u8) = .empty; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (domains.items) |d| allocator.free(d); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| domains.deinit(allocator); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (domain_filter) |d| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| domains.append(allocator, allocator.dupe(u8, d) catch return) catch {}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const entries = try loadEntries(allocator, &cfg, null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer reports.store.freeReportEntries(allocator, entries); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var domain_set = std.StringHashMap(void).init(allocator); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer domain_set.deinit(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (entries) |e| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (e.domain.len > 0) domain_set.put(e.domain, {}) catch {}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var it = domain_set.iterator(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| while (it.next()) |kv| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| domains.append(allocator, allocator.dupe(u8, kv.key_ptr.*) catch continue) catch {}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const is_json = std.mem.eql(u8, format, "json"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const icon_green = "\x1b[38;2;194;255;38m●\x1b[0m"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const icon_yellow = "\x1b[38;2;255;200;0m●\x1b[0m"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const icon_red = "\x1b[38;2;255;51;102m●\x1b[0m"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const check_green = "\x1b[38;2;194;255;38m✓\x1b[0m "; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const check_yellow = "\x1b[38;2;255;200;0m△\x1b[0m "; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const check_red = "\x1b[38;2;255;51;102m✗\x1b[0m "; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const not_found = "\x1b[2m(not found)\x1b[0m"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var json_buf: std.ArrayList(u8) = .empty; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer json_buf.deinit(allocator); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var json_first = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (is_json) json_buf.appendSlice(allocator, "[") catch {}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (domains.items) |domain| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var buf: [2048]u8 = undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Query all records first to determine domain status | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var dmarc_txt: ?[]const u8 = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer if (dmarc_txt) |t| allocator.free(t); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var spf_txt: ?[]const u8 = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer if (spf_txt) |t| allocator.free(t); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var dkim_txt: ?[]const u8 = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer if (dkim_txt) |t| allocator.free(t); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var dkim_selector: []const u8 = ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var mta_sts_txt: ?[]const u8 = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer if (mta_sts_txt) |t| allocator.free(t); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var tls_rpt_txt: ?[]const u8 = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer if (tls_rpt_txt) |t| allocator.free(t); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // DMARC | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const qname = std.fmt.allocPrint(allocator, "_dmarc.{s}", .{domain}) catch continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer allocator.free(qname); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dmarc_txt = reports.ipinfo.queryTxt(allocator, qname) catch null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // SPF | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (reports.ipinfo.queryTxt(allocator, domain)) |txt| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (std.mem.indexOf(u8, txt, "v=spf1") != null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| spf_txt = txt; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| allocator.free(txt); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else |_| {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+348
to
+357
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // DKIM | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for ([_][]const u8{ "default", "google", "selector1", "selector2", "s1", "s2", "dkim", "mail" }) |selector| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const qname = std.fmt.allocPrint(allocator, "{s}._domainkey.{s}", .{ selector, domain }) catch continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer allocator.free(qname); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (reports.ipinfo.queryTxt(allocator, qname)) |txt| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (std.mem.indexOf(u8, txt, "DKIM1") != null or std.mem.indexOf(u8, txt, "p=") != null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dkim_txt = txt; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dkim_selector = selector; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| allocator.free(txt); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else |_| {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+360
to
+372
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // MTA-STS | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const qname = std.fmt.allocPrint(allocator, "_mta-sts.{s}", .{domain}) catch continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer allocator.free(qname); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mta_sts_txt = reports.ipinfo.queryTxt(allocator, qname) catch null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // TLS-RPT | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const qname = std.fmt.allocPrint(allocator, "_smtp._tls.{s}", .{domain}) catch continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer allocator.free(qname); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tls_rpt_txt = reports.ipinfo.queryTxt(allocator, qname) catch null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const dmarc_policy_weak = if (dmarc_txt) |t| reports.stats.isDmarcPolicyWeak(t) else false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const spf_weak = if (spf_txt) |t| reports.stats.isSpfWeak(t) else false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const has_dmarc = dmarc_txt != null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const has_spf = spf_txt != null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const has_dkim = dkim_txt != null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const dns_status = reports.stats.evaluateDnsStatus(has_dmarc, has_spf, has_dkim, dmarc_policy_weak, spf_weak); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (is_json) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!json_first) json_buf.appendSlice(allocator, ",") catch continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| json_first = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const status_str = dns_status.label(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const esc = reports.stats.jsonEscape; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const dmarc_s = if (dmarc_txt) |t| esc(allocator, t) else ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer if (dmarc_txt != null and dmarc_s.ptr != dmarc_txt.?.ptr) allocator.free(dmarc_s); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const spf_s = if (spf_txt) |t| esc(allocator, t) else ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer if (spf_txt != null and spf_s.ptr != spf_txt.?.ptr) allocator.free(spf_s); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const dkim_s = if (dkim_txt) |t| esc(allocator, t) else ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer if (dkim_txt != null and dkim_s.ptr != dkim_txt.?.ptr) allocator.free(dkim_s); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const mta_s = if (mta_sts_txt) |t| esc(allocator, t) else ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer if (mta_sts_txt != null and mta_s.ptr != mta_sts_txt.?.ptr) allocator.free(mta_s); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const tls_s = if (tls_rpt_txt) |t| esc(allocator, t) else ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer if (tls_rpt_txt != null and tls_s.ptr != tls_rpt_txt.?.ptr) allocator.free(tls_s); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const line = std.fmt.allocPrint(allocator, "{{\"domain\":\"{s}\",\"status\":\"{s}\",\"dmarc\":\"{s}\",\"spf\":\"{s}\",\"dkim\":\"{s}\",\"dkim_selector\":\"{s}\",\"mta_sts\":\"{s}\",\"tls_rpt\":\"{s}\"}}", .{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| domain, status_str, dmarc_s, spf_s, dkim_s, dkim_selector, mta_s, tls_s, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) catch continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer allocator.free(line); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| json_buf.appendSlice(allocator, line) catch continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+412
to
+416
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const line = std.fmt.allocPrint(allocator, "{{\"domain\":\"{s}\",\"status\":\"{s}\",\"dmarc\":\"{s}\",\"spf\":\"{s}\",\"dkim\":\"{s}\",\"dkim_selector\":\"{s}\",\"mta_sts\":\"{s}\",\"tls_rpt\":\"{s}\"}}", .{ | |
| domain, status_str, dmarc_s, spf_s, dkim_s, dkim_selector, mta_s, tls_s, | |
| }) catch continue; | |
| defer allocator.free(line); | |
| json_buf.appendSlice(allocator, line) catch continue; | |
| var line_buf = std.ArrayList(u8){}; | |
| defer line_buf.deinit(allocator); | |
| const writer = line_buf.writer(allocator); | |
| writer.writeAll("{\"domain\":") catch continue; | |
| std.json.stringify(domain, .{}, writer) catch continue; | |
| writer.writeAll(",\"status\":") catch continue; | |
| std.json.stringify(status_str, .{}, writer) catch continue; | |
| writer.writeAll(",\"dmarc\":") catch continue; | |
| std.json.stringify(dmarc_s, .{}, writer) catch continue; | |
| writer.writeAll(",\"spf\":") catch continue; | |
| std.json.stringify(spf_s, .{}, writer) catch continue; | |
| writer.writeAll(",\"dkim\":") catch continue; | |
| std.json.stringify(dkim_s, .{}, writer) catch continue; | |
| writer.writeAll(",\"dkim_selector\":") catch continue; | |
| std.json.stringify(dkim_selector, .{}, writer) catch continue; | |
| writer.writeAll(",\"mta_sts\":") catch continue; | |
| std.json.stringify(mta_s, .{}, writer) catch continue; | |
| writer.writeAll(",\"tls_rpt\":") catch continue; | |
| std.json.stringify(tls_s, .{}, writer) catch continue; | |
| writer.writeAll("}") catch continue; | |
| json_buf.appendSlice(allocator, line_buf.items) catch continue; |
Copilot
AI
Apr 17, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The section header "Failure breakdown" is misleading given the logic above: dkim_only_fail / spf_only_fail are explicitly counted as DMARC passes (result.dmarc_pass += rec.count). This makes it easy to misread the output as a breakdown of DMARC failures when it is really an auth-mechanism breakdown. Consider renaming the header/labels (e.g., "Auth results breakdown") or explicitly stating that single-mechanism fails still pass DMARC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that
queryTxtis public and used for general DNS lookups, returning only the first TXT RR (viaparseTxtFromResponseparsing the first answer) is a surprising/incorrect contract for domains with multiple TXT records (common at the apex). Consider either updating it to collect all TXT answers (and possibly return an array/slice) or renaming/documenting it asqueryFirstTxtto avoid callers assuming it searches all TXT records.