add suppression list for outgoing dmarc and tls reports

for reporting addresses that cause DSNs to be returned. that just adds noise.
the admin can add/remove/extend addresses through the webadmin.

in the future, we could send reports with a smtp mail from of
"postmaster+<signed-encoded-recipient>@...", and add the reporting recipient
on the suppression list automatically when a DSN comes in on that address, but
for now this will probably do.
This commit is contained in:
Mechiel Lukkien
2023-11-13 13:48:52 +01:00
parent 6ce69d5425
commit e24e1bee19
12 changed files with 697 additions and 17 deletions

View File

@ -2031,6 +2031,36 @@ func (Admin) DMARCRemoveEvaluations(ctx context.Context, domain string) {
xcheckf(ctx, err, "removing evaluations for domain")
}
// DMARCSuppressAdd adds a reporting address to the suppress list. Outgoing
// reports will be suppressed for a period.
func (Admin) DMARCSuppressAdd(ctx context.Context, reportingAddress string, until time.Time, comment string) {
addr, err := smtp.ParseAddress(reportingAddress)
xcheckuserf(ctx, err, "parsing reporting address")
ba := dmarcdb.SuppressAddress{ReportingAddress: addr.String(), Until: until, Comment: comment}
err = dmarcdb.SuppressAdd(ctx, &ba)
xcheckf(ctx, err, "adding address to suppresslist")
}
// DMARCSuppressList returns all reporting addresses on the suppress list.
func (Admin) DMARCSuppressList(ctx context.Context) []dmarcdb.SuppressAddress {
l, err := dmarcdb.SuppressList(ctx)
xcheckf(ctx, err, "listing reporting addresses in suppresslist")
return l
}
// DMARCSuppressRemove removes a reporting address record from the suppress list.
func (Admin) DMARCSuppressRemove(ctx context.Context, id int64) {
err := dmarcdb.SuppressRemove(ctx, id)
xcheckf(ctx, err, "removing reporting address from suppresslist")
}
// DMARCSuppressExtend updates the until field of a suppressed reporting address record.
func (Admin) DMARCSuppressExtend(ctx context.Context, id int64, until time.Time) {
err := dmarcdb.SuppressUpdate(ctx, id, until)
xcheckf(ctx, err, "updating reporting address in suppresslist")
}
// TLSRPTResults returns all TLSRPT results in the database.
func (Admin) TLSRPTResults(ctx context.Context) []tlsrptdb.TLSResult {
results, err := tlsrptdb.Results(ctx)
@ -2078,3 +2108,33 @@ func (Admin) TLSRPTRemoveResults(ctx context.Context, domain string, day string)
err = tlsrptdb.RemoveResultsPolicyDomain(ctx, dom, day)
xcheckf(ctx, err, "removing tls results")
}
// TLSRPTSuppressAdd adds a reporting address to the suppress list. Outgoing
// reports will be suppressed for a period.
func (Admin) TLSRPTSuppressAdd(ctx context.Context, reportingAddress string, until time.Time, comment string) {
addr, err := smtp.ParseAddress(reportingAddress)
xcheckuserf(ctx, err, "parsing reporting address")
ba := tlsrptdb.TLSRPTSuppressAddress{ReportingAddress: addr.String(), Until: until, Comment: comment}
err = tlsrptdb.SuppressAdd(ctx, &ba)
xcheckf(ctx, err, "adding address to suppresslist")
}
// TLSRPTSuppressList returns all reporting addresses on the suppress list.
func (Admin) TLSRPTSuppressList(ctx context.Context) []tlsrptdb.TLSRPTSuppressAddress {
l, err := tlsrptdb.SuppressList(ctx)
xcheckf(ctx, err, "listing reporting addresses in suppresslist")
return l
}
// TLSRPTSuppressRemove removes a reporting address record from the suppress list.
func (Admin) TLSRPTSuppressRemove(ctx context.Context, id int64) {
err := tlsrptdb.SuppressRemove(ctx, id)
xcheckf(ctx, err, "removing reporting address from suppresslist")
}
// TLSRPTSuppressExtend updates the until field of a suppressed reporting address record.
func (Admin) TLSRPTSuppressExtend(ctx context.Context, id int64, until time.Time) {
err := tlsrptdb.SuppressUpdate(ctx, id, until)
xcheckf(ctx, err, "updating reporting address in suppresslist")
}

View File

@ -1023,7 +1023,7 @@ const dmarcIndex = async () => {
dom._kids(page,
crumbs(
crumblink('Mox Admin', '#'),
'DMARC reports and evaluations',
'DMARC',
),
dom.ul(
dom.li(
@ -1085,7 +1085,10 @@ const renderDMARCSummaries = (summaries) => {
}
const dmarcEvaluations = async () => {
const evalStats = await api.DMARCEvaluationStats()
const [evalStats, suppressAddresses] = await Promise.all([
api.DMARCEvaluationStats(),
api.DMARCSuppressList(),
])
const isEmpty = (o) => {
for (const e in o) {
@ -1094,6 +1097,9 @@ const dmarcEvaluations = async () => {
return true
}
let fieldset, reportingAddress, until, comment
const nextmonth = new Date(new Date().getTime()+31*24*3600*1000)
const page = document.getElementById('page')
dom._kids(page,
crumbs(
@ -1121,6 +1127,101 @@ const dmarcEvaluations = async () => {
isEmpty(evalStats) ? dom.tr(dom.td(attr({colspan: '3'}), 'No evaluations.')) : [],
),
),
dom.br(),
dom.br(),
dom.h2('Suppressed reporting addresses'),
dom.p('In practice, sending a DMARC report to a reporting address can cause DSN to be sent back. Such addresses can be added to a supression list for a period, to reduce noise in the postmaster mailbox.'),
dom.form(
async function submit(e) {
e.stopPropagation()
e.preventDefault()
try {
fieldset.disabled = true
await api.DMARCSuppressAdd(reportingAddress.value, new Date(until.value), comment.value)
} catch (err) {
console.log({err})
window.alert('Error: ' + err.message)
return
} finally {
fieldset.disabled = false
}
window.location.reload() // todo: add the address to the list, or only reload the list
},
fieldset=dom.fieldset(
dom.label(
style({display: 'inline-block'}),
'Reporting address',
dom.br(),
reportingAddress=dom.input(attr({required: ''})),
),
' ',
dom.label(
style({display: 'inline-block'}),
'Until',
dom.br(),
until=dom.input(attr({type: 'date', required: '', value: nextmonth.getFullYear()+'-'+(1+nextmonth.getMonth())+'-'+nextmonth.getDate()})),
),
' ',
dom.label(
style({display: 'inline-block'}),
dom.span('Comment (optional)'),
dom.br(),
comment=dom.input(),
),
' ',
dom.button('Add', attr({title: 'Outgoing reports to this reporting address will be suppressed until the end time.'})),
),
),
dom.br(),
dom('table.hover',
dom.thead(
dom.tr(
dom.th('Reporting address'),
dom.th('Until'),
dom.th('Comment'),
dom.th('Action'),
),
),
dom.tbody(
(suppressAddresses || []).length === 0 ? dom.tr(dom.td(attr({colspan: '4'}), 'No suppressed reporting addresses.')) : [],
(suppressAddresses || []).map(ba =>
dom.tr(
dom.td(ba.ReportingAddress),
dom.td(ba.Until),
dom.td(ba.Comment),
dom.td(
dom.button('Remove', attr({type: 'button'}), async function click(e) {
try {
e.target.disabled = true
await api.DMARCSuppressRemove(ba.ID)
} catch (err) {
console.log({err})
window.alert('Error: ' + err.message)
return
} finally {
e.target.disabled = false
}
window.location.reload() // todo: only reload the list
}),
' ',
dom.button('Extend for 1 month', attr({type: 'button'}), async function click(e) {
try {
e.target.disabled = true
await api.DMARCSuppressExtend(ba.ID, new Date(new Date().getTime() + 31*24*3600*1000))
} catch (err) {
console.log({err})
window.alert('Error: ' + err.message)
return
} finally {
e.target.disabled = false
}
window.location.reload() // todo: only reload the list
}),
),
)
),
),
),
)
}
@ -1479,10 +1580,16 @@ const tlsrptIndex = async () => {
}
const tlsrptResults = async () => {
const results = await api.TLSRPTResults()
const [results, suppressAddresses] = await Promise.all([
api.TLSRPTResults(),
api.TLSRPTSuppressList(),
])
// todo: add a view where results are grouped by policy domain+dayutc. now each recipient domain gets a row.
let fieldset, reportingAddress, until, comment
const nextmonth = new Date(new Date().getTime()+31*24*3600*1000)
const page = document.getElementById('page')
dom._kids(page,
crumbs(
@ -1545,6 +1652,101 @@ const tlsrptResults = async () => {
results.length === 0 ? dom.tr(dom.td(attr({colspan: '9'}), 'No results.')) : [],
),
),
dom.br(),
dom.br(),
dom.h2('Suppressed reporting addresses'),
dom.p('In practice, sending a TLS report to a reporting address can cause DSN to be sent back. Such addresses can be added to a suppress list for a period, to reduce noise in the postmaster mailbox.'),
dom.form(
async function submit(e) {
e.stopPropagation()
e.preventDefault()
try {
fieldset.disabled = true
await api.TLSRPTSuppressAdd(reportingAddress.value, new Date(until.value), comment.value)
} catch (err) {
console.log({err})
window.alert('Error: ' + err.message)
return
} finally {
fieldset.disabled = false
}
window.location.reload() // todo: add the address to the list, or only reload the list
},
fieldset=dom.fieldset(
dom.label(
style({display: 'inline-block'}),
'Reporting address',
dom.br(),
reportingAddress=dom.input(attr({required: ''})),
),
' ',
dom.label(
style({display: 'inline-block'}),
'Until',
dom.br(),
until=dom.input(attr({type: 'date', required: '', value: nextmonth.getFullYear()+'-'+(1+nextmonth.getMonth())+'-'+nextmonth.getDate()})),
),
' ',
dom.label(
style({display: 'inline-block'}),
dom.span('Comment (optional)'),
dom.br(),
comment=dom.input(),
),
' ',
dom.button('Add', attr({title: 'Outgoing reports to this reporting address will be suppressed until the end time.'})),
),
),
dom.br(),
dom('table.hover',
dom.thead(
dom.tr(
dom.th('Reporting address'),
dom.th('Until'),
dom.th('Comment'),
dom.th('Action'),
),
),
dom.tbody(
(suppressAddresses || []).length === 0 ? dom.tr(dom.td(attr({colspan: '4'}), 'No suppressed reporting addresses.')) : [],
(suppressAddresses || []).map(ba =>
dom.tr(
dom.td(ba.ReportingAddress),
dom.td(ba.Until),
dom.td(ba.Comment),
dom.td(
dom.button('Remove', attr({type: 'button'}), async function click(e) {
try {
e.target.disabled = true
await api.TLSRPTSuppressRemove(ba.ID)
} catch (err) {
console.log({err})
window.alert('Error: ' + err.message)
return
} finally {
e.target.disabled = false
}
window.location.reload() // todo: only reload the list
}),
' ',
dom.button('Extend for 1 month', attr({type: 'button'}), async function click(e) {
try {
e.target.disabled = true
await api.TLSRPTSuppressExtend(ba.ID, new Date(new Date().getTime() + 31*24*3600*1000))
} catch (err) {
console.log({err})
window.alert('Error: ' + err.message)
return
} finally {
e.target.disabled = false
}
window.location.reload() // todo: only reload the list
}),
),
)
),
),
),
)
}

View File

@ -828,6 +828,77 @@
],
"Returns": []
},
{
"Name": "DMARCSuppressAdd",
"Docs": "DMARCSuppressAdd adds a reporting address to the suppress list. Outgoing\nreports will be suppressed for a period.",
"Params": [
{
"Name": "reportingAddress",
"Typewords": [
"string"
]
},
{
"Name": "until",
"Typewords": [
"timestamp"
]
},
{
"Name": "comment",
"Typewords": [
"string"
]
}
],
"Returns": []
},
{
"Name": "DMARCSuppressList",
"Docs": "DMARCSuppressList returns all reporting addresses on the suppress list.",
"Params": [],
"Returns": [
{
"Name": "r0",
"Typewords": [
"[]",
"SuppressAddress"
]
}
]
},
{
"Name": "DMARCSuppressRemove",
"Docs": "DMARCSuppressRemove removes a reporting address record from the suppress list.",
"Params": [
{
"Name": "id",
"Typewords": [
"int64"
]
}
],
"Returns": []
},
{
"Name": "DMARCSuppressExtend",
"Docs": "DMARCSuppressExtend updates the until field of a suppressed reporting address record.",
"Params": [
{
"Name": "id",
"Typewords": [
"int64"
]
},
{
"Name": "until",
"Typewords": [
"timestamp"
]
}
],
"Returns": []
},
{
"Name": "TLSRPTResults",
"Docs": "TLSRPTResults returns all TLSRPT results in the database.",
@ -920,6 +991,77 @@
}
],
"Returns": []
},
{
"Name": "TLSRPTSuppressAdd",
"Docs": "TLSRPTSuppressAdd adds a reporting address to the suppress list. Outgoing\nreports will be suppressed for a period.",
"Params": [
{
"Name": "reportingAddress",
"Typewords": [
"string"
]
},
{
"Name": "until",
"Typewords": [
"timestamp"
]
},
{
"Name": "comment",
"Typewords": [
"string"
]
}
],
"Returns": []
},
{
"Name": "TLSRPTSuppressList",
"Docs": "TLSRPTSuppressList returns all reporting addresses on the suppress list.",
"Params": [],
"Returns": [
{
"Name": "r0",
"Typewords": [
"[]",
"TLSRPTSuppressAddress"
]
}
]
},
{
"Name": "TLSRPTSuppressRemove",
"Docs": "TLSRPTSuppressRemove removes a reporting address record from the suppress list.",
"Params": [
{
"Name": "id",
"Typewords": [
"int64"
]
}
],
"Returns": []
},
{
"Name": "TLSRPTSuppressExtend",
"Docs": "TLSRPTSuppressExtend updates the until field of a suppressed reporting address record.",
"Params": [
{
"Name": "id",
"Typewords": [
"int64"
]
},
{
"Name": "until",
"Typewords": [
"timestamp"
]
}
],
"Returns": []
}
],
"Sections": [],
@ -3808,6 +3950,47 @@
}
]
},
{
"Name": "SuppressAddress",
"Docs": "SuppressAddress is a reporting address for which outgoing DMARC reports\nwill be suppressed for a period.",
"Fields": [
{
"Name": "ID",
"Docs": "",
"Typewords": [
"int64"
]
},
{
"Name": "Inserted",
"Docs": "",
"Typewords": [
"timestamp"
]
},
{
"Name": "ReportingAddress",
"Docs": "",
"Typewords": [
"string"
]
},
{
"Name": "Until",
"Docs": "",
"Typewords": [
"timestamp"
]
},
{
"Name": "Comment",
"Docs": "",
"Typewords": [
"string"
]
}
]
},
{
"Name": "TLSResult",
"Docs": "TLSResult is stored in the database to track TLS results per policy domain, day\nand recipient domain. These records will be included in TLS reports.",
@ -3877,6 +4060,47 @@
]
}
]
},
{
"Name": "TLSRPTSuppressAddress",
"Docs": "TLSRPTSuppressAddress is a reporting address for which outgoing TLS reports\nwill be suppressed for a period.",
"Fields": [
{
"Name": "ID",
"Docs": "",
"Typewords": [
"int64"
]
},
{
"Name": "Inserted",
"Docs": "",
"Typewords": [
"timestamp"
]
},
{
"Name": "ReportingAddress",
"Docs": "",
"Typewords": [
"string"
]
},
{
"Name": "Until",
"Docs": "",
"Typewords": [
"timestamp"
]
},
{
"Name": "Comment",
"Docs": "",
"Typewords": [
"string"
]
}
]
}
],
"Ints": [],