mox/tlsrptdb/report.go
Mechiel Lukkien bf8cfd9724
add debug logging about bstore db schema upgrades
bstore was updated to v0.0.6 to add this logging.
this simplifies some of the db-handling code in mtastsdb,tlsrptdb,dmarcdb. we
now call the package-level Init() and Close() in all tests properly.
2024-05-10 14:44:37 +02:00

137 lines
4.3 KiB
Go

// Package tlsrptdb stores reports from "SMTP TLS Reporting" in its database.
package tlsrptdb
import (
"context"
"fmt"
"log/slog"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/mjl-/bstore"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/mlog"
"github.com/mjl-/mox/mox-"
"github.com/mjl-/mox/tlsrpt"
)
var (
metricSession = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "mox_tlsrptdb_session_total",
Help: "Number of sessions, both success and known result types.",
},
[]string{"type"}, // Known result types, and "success"
)
knownResultTypes = map[tlsrpt.ResultType]struct{}{
tlsrpt.ResultSTARTTLSNotSupported: {},
tlsrpt.ResultCertificateHostMismatch: {},
tlsrpt.ResultCertificateExpired: {},
tlsrpt.ResultTLSAInvalid: {},
tlsrpt.ResultDNSSECInvalid: {},
tlsrpt.ResultDANERequired: {},
tlsrpt.ResultCertificateNotTrusted: {},
tlsrpt.ResultSTSPolicyInvalid: {},
tlsrpt.ResultSTSWebPKIInvalid: {},
tlsrpt.ResultValidationFailure: {},
tlsrpt.ResultSTSPolicyFetch: {},
}
)
// Record is a TLS report as a database record, including information
// about the sender.
type Record struct {
ID int64
Domain string `bstore:"index"` // Policy domain to which the TLS report applies. Unicode.
FromDomain string
MailFrom string
HostReport bool // Report for host TLSRPT record, as opposed to domain TLSRPT record.
Report tlsrpt.Report
}
// AddReport adds a TLS report to the database.
//
// The report should have come in over SMTP, with a DKIM-validated
// verifiedFromDomain. Using HTTPS for reports is not recommended as there is no
// authentication on the reports origin.
//
// Only reports for known domains are added to the database. Unknown domains are
// ignored without causing an error, unless no known domain was found in the report
// at all.
//
// Prometheus metrics are updated only for configured domains.
func AddReport(ctx context.Context, log mlog.Log, verifiedFromDomain dns.Domain, mailFrom string, hostReport bool, r *tlsrpt.Report) error {
if len(r.Policies) == 0 {
return fmt.Errorf("no policies in report")
}
var inserted int
return ReportDB.Write(ctx, func(tx *bstore.Tx) error {
for _, p := range r.Policies {
pp := p.Policy
d, err := dns.ParseDomain(pp.Domain)
if err != nil {
return fmt.Errorf("invalid domain %v in tls report: %v", d, err)
}
if _, ok := mox.Conf.Domain(d); !ok && d != mox.Conf.Static.HostnameDomain {
log.Info("unknown host/recipient policy domain in tls report, not storing", slog.Any("domain", d), slog.String("mailfrom", mailFrom))
continue
}
metricSession.WithLabelValues("success").Add(float64(p.Summary.TotalSuccessfulSessionCount))
for _, f := range p.FailureDetails {
var result string
if _, ok := knownResultTypes[f.ResultType]; ok {
result = string(f.ResultType)
} else {
result = "other"
}
metricSession.WithLabelValues(result).Add(float64(f.FailedSessionCount))
}
record := Record{0, d.Name(), verifiedFromDomain.Name(), mailFrom, d == mox.Conf.Static.HostnameDomain, *r}
if err := tx.Insert(&record); err != nil {
return fmt.Errorf("inserting report for domain: %w", err)
}
inserted++
}
if inserted == 0 {
return fmt.Errorf("no domains in report recognized")
}
return nil
})
}
// Records returns all TLS reports in the database.
func Records(ctx context.Context) ([]Record, error) {
return bstore.QueryDB[Record](ctx, ReportDB).List()
}
// RecordID returns the report for the ID.
func RecordID(ctx context.Context, id int64) (Record, error) {
e := Record{ID: id}
err := ReportDB.Get(ctx, &e)
return e, err
}
// RecordsPeriodPolicyDomain returns the reports overlapping start and end, for the
// given policy domain. If policy domain is empty, records for all domains are
// returned.
func RecordsPeriodDomain(ctx context.Context, start, end time.Time, policyDomain dns.Domain) ([]Record, error) {
q := bstore.QueryDB[Record](ctx, ReportDB)
var zerodom dns.Domain
if policyDomain != zerodom {
q.FilterNonzero(Record{Domain: policyDomain.Name()})
}
q.FilterFn(func(r Record) bool {
dr := r.Report.DateRange
return !dr.Start.Before(start) && dr.Start.Before(end) || dr.End.After(start) && !dr.End.After(end)
})
return q.List()
}