Received reports: read the report lists as the server sends them #89

Merged
jcoffey-dev merged 1 commits from fix/report-lists-are-maps into main 2026-10-06 16:23:32 +00:00
5 changed files with 76 additions and 19 deletions

No files matched your search

@@ -216,7 +216,14 @@ export function DmarcSummaryCard() {
};
}, []);
const summaries = useMemo(() => (reports ? summarizeDmarc(reports) : []), [reports]);
// A report shaped in a way we don't expect shows as an error here, not a broken page.
const [summaries, summaryError] = useMemo(() => {
try {
return [reports ? summarizeDmarc(reports) : [], null] as const;
} catch (e) {
return [[], e instanceof Error ? e.message : String(e)] as const;
}
}, [reports]);
return (
<section className="space-y-3 rounded-xl border p-4">
@@ -232,8 +239,8 @@ export function DmarcSummaryCard() {
</p>
</div>
</div>
{error ? (
<p className="text-sm text-destructive">{error}</p>
{error || summaryError ? (
<p className="text-sm text-destructive">{error ?? summaryError}</p>
) : !reports ? (
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
) : summaries.length === 0 ? (
@@ -70,7 +70,14 @@ export function TlsSummaryCard() {
};
}, []);
const summaries = useMemo(() => (reports ? summarizeTls(reports) : []), [reports]);
// A report shaped in a way we don't expect shows as an error here, not a broken page.
const [summaries, summaryError] = useMemo(() => {
try {
return [reports ? summarizeTls(reports) : [], null] as const;
} catch (e) {
return [[], e instanceof Error ? e.message : String(e)] as const;
}
}, [reports]);
return (
<section className="space-y-3 rounded-xl border p-4">
@@ -83,8 +90,8 @@ export function TlsSummaryCard() {
</p>
</div>
</div>
{error ? (
<p className="text-sm text-destructive">{error}</p>
{error || summaryError ? (
<p className="text-sm text-destructive">{error ?? summaryError}</p>
) : !reports ? (
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
) : summaries.length === 0 ? (
+8 -5
View File
@@ -16,6 +16,9 @@ import { getAccountId, jmapRequest } from '@/services/jmap/client';
/** At most this many of each kind are read; the newest ones win. */
export const REPORT_LIMIT = 500;
/** The server sends an objectList as a map keyed by index ("0", "1", …); read it with jmapMapToArray. */
export type JmapList<T> = T[] | Record<string, T>;
export type DmarcResult = 'pass' | 'fail' | 'unspecified';
export type Disposition = 'none' | 'pass' | 'quarantine' | 'reject' | 'unspecified';
@@ -27,8 +30,8 @@ export interface DmarcRecord {
evaluatedDisposition: Disposition;
evaluatedDkim: DmarcResult;
evaluatedSpf: DmarcResult;
dkimResults?: { domain: string; selector?: string; result: string }[];
spfResults?: { domain: string; result: string }[];
dkimResults?: JmapList<{ domain: string; selector?: string; result: string }>;
spfResults?: JmapList<{ domain: string; result: string }>;
}
export interface DmarcReport {
@@ -38,7 +41,7 @@ export interface DmarcReport {
policyDomain: string;
policyDisposition?: string;
policyTestingMode?: boolean;
records: DmarcRecord[];
records: JmapList<DmarcRecord>;
}
export interface DmarcExternalReport {
@@ -59,7 +62,7 @@ export interface TlsPolicy {
policyType?: string;
totalSuccessfulSessions: number;
totalFailedSessions: number;
failureDetails?: TlsFailure[];
failureDetails?: JmapList<TlsFailure>;
}
export interface TlsExternalReport {
@@ -69,7 +72,7 @@ export interface TlsExternalReport {
organizationName?: string | null;
dateRangeStart: string;
dateRangeEnd: string;
policies: TlsPolicy[];
policies: JmapList<TlsPolicy>;
};
}
+34 -2
View File
@@ -5,7 +5,7 @@
*/
import { describe, expect, it } from 'vitest';
import type { DmarcExternalReport, DmarcRecord, DmarcReport, TlsExternalReport, TlsPolicy } from './api';
import type { DmarcExternalReport, DmarcRecord, DmarcReport, JmapList, TlsExternalReport, TlsPolicy } from './api';
import { summarizeDmarc, summarizeTls } from './summarize';
function record(over: Partial<DmarcRecord> = {}): DmarcRecord {
@@ -39,6 +39,23 @@ function dmarc(records: DmarcRecord[], over: Partial<DmarcReport> = {}, id = 'a'
}
describe('summarizeDmarc', () => {
it('reads lists the way the server sends them: maps keyed by index', () => {
const wire = dmarc([]);
wire.report.records = {
'0': record({ dkimResults: { '0': { domain: 'example.org', result: 'pass' } }, spfResults: {} }),
'1': record({
sourceIp: '203.0.113.5',
evaluatedDkim: 'fail',
evaluatedSpf: 'fail',
dkimResults: {},
spfResults: {},
}),
};
const [d] = summarizeDmarc([wire]);
expect(d).toMatchObject({ messages: 20, passed: 10, failed: 10 });
expect(d.sources.map((s) => s.label)).toEqual(['203.0.113.5', 'example.org']);
});
it('counts passing mail under the domain that proved it', () => {
const [d] = summarizeDmarc([
dmarc([record(), record({ sourceIp: '192.0.2.11', count: 5 })]),
@@ -102,7 +119,7 @@ describe('summarizeDmarc', () => {
});
});
function tls(policies: TlsPolicy[], org = 'google.com'): TlsExternalReport {
function tls(policies: JmapList<TlsPolicy>, org = 'google.com'): TlsExternalReport {
return {
id: org,
receivedAt: '2026-10-02T00:00:00Z',
@@ -116,6 +133,21 @@ function tls(policies: TlsPolicy[], org = 'google.com'): TlsExternalReport {
}
describe('summarizeTls', () => {
it('reads policies and failures sent as maps keyed by index', () => {
const [d] = summarizeTls([
tls({
'0': {
policyDomain: 'example.org',
totalSuccessfulSessions: 5,
totalFailedSessions: 2,
failureDetails: { '0': { resultType: 'certificateExpired', failedSessionCount: 2 } },
},
}),
]);
expect(d).toMatchObject({ domain: 'example.org', succeeded: 5, failed: 2 });
expect(d.failures).toEqual([{ kind: 'certificateExpired', sessions: 2, hosts: [] }]);
});
it('adds up sessions per domain and groups failures by kind', () => {
const [d] = summarizeTls([
tls([
+14 -6
View File
@@ -10,7 +10,8 @@
* tested without a server.
*/
import type { Disposition, DmarcExternalReport, DmarcRecord, TlsExternalReport } from './api';
import { jmapMapToArray } from '@/lib/jmapUtils';
import type { Disposition, DmarcExternalReport, DmarcRecord, TlsExternalReport, TlsFailure, TlsPolicy } from './api';
/** One server (or service) that sent mail with one of our domains in From. */
export interface Source {
@@ -52,9 +53,13 @@ function handling(d: Disposition): keyof Source['failedHandling'] {
/** Who the sender proved to be. A pass without a passing domain falls back to the IP. */
function identify(r: DmarcRecord): Pick<Source, 'label' | 'provedBy'> {
const dkim = r.dkimResults?.find((d) => d.result === 'pass' && d.domain);
const dkim = jmapMapToArray<{ domain: string; result: string }>(r.dkimResults).find(
(d) => d.result === 'pass' && d.domain,
);
if (dkim) return { label: dkim.domain.toLowerCase(), provedBy: 'dkim' };
const spf = r.spfResults?.find((s) => s.result === 'pass' && s.domain);
const spf = jmapMapToArray<{ domain: string; result: string }>(r.spfResults).find(
(s) => s.result === 'pass' && s.domain,
);
if (spf) return { label: spf.domain.toLowerCase(), provedBy: 'spf' };
return { label: r.sourceIp ?? '?', provedBy: null };
}
@@ -64,6 +69,7 @@ export function summarizeDmarc(reports: DmarcExternalReport[]): DomainSummary[]
/** Per domain, the end of the newest report, whose policy wins. */
const newest = new Map<string, string>();
for (const { report } of reports) {
if (!report?.policyDomain) continue;
const name = report.policyDomain.toLowerCase();
let d = domains.get(name);
if (!d) {
@@ -91,7 +97,7 @@ export function summarizeDmarc(reports: DmarcExternalReport[]): DomainSummary[]
d.policy = p === 'none' || p === 'quarantine' || p === 'reject' ? p : 'unknown';
d.testing = report.policyTestingMode === true;
}
for (const r of report.records ?? []) {
for (const r of jmapMapToArray<DmarcRecord>(report.records)) {
const count = r.count ?? 0;
const ok = passes(r);
const who = identify(r);
@@ -173,8 +179,10 @@ const KINDS = new Set<string>([
export function summarizeTls(reports: TlsExternalReport[]): TlsSummary[] {
const domains = new Map<string, TlsSummary & { kinds: Map<TlsFailureKind, { sessions: number; hosts: string[] }> }>();
for (const { report } of reports) {
if (!report) continue;
const counted = new Set<string>();
for (const p of report.policies ?? []) {
for (const p of jmapMapToArray<TlsPolicy>(report.policies)) {
if (!p.policyDomain) continue;
const name = p.policyDomain.toLowerCase();
let d = domains.get(name);
if (!d) {
@@ -189,7 +197,7 @@ export function summarizeTls(reports: TlsExternalReport[]): TlsSummary[] {
d.failed += p.totalFailedSessions ?? 0;
const org = report.organizationName;
if (org && !d.reporters.includes(org)) d.reporters.push(org);
for (const f of p.failureDetails ?? []) {
for (const f of jmapMapToArray<TlsFailure>(p.failureDetails)) {
const kind = (f.resultType && KINDS.has(f.resultType) ? f.resultType : 'other') as TlsFailureKind;
const k = d.kinds.get(kind) ?? { sessions: 0, hosts: [] };
k.sessions += f.failedSessionCount ?? 0;