Detect aggregate reports by "xml_schema" instead of "domain"

xml_schema is aggregate-only (failure/SMTP TLS rows don't carry it) and a
distinctive, non-generic field name, addressing the concern that "domain"
could be confused with other logs. parsedmarc defaults xml_schema to "draft"
when the report omits <version> (parsedmarc/__init__.py:832), so it survives a
missing version element -- unlike a field with no default.

It is also a native JSON string straight out of the json{} filter, so unlike
dmarc_aligned it needs no convert step to be testable, keeping detection
independent of the type-conversion in step 1b. xml_schema is added to the
pre-json init block (required for any if-tested field); domain stays
initialized since it is still mapped to target.hostname.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sean Whalen
2026-06-04 10:36:49 -04:00
parent 2d9a2a2a8f
commit 46e694502d
2 changed files with 12 additions and 8 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ detects them by a field unique to each and maps them as follows:
| parsedmarc report | Detected by | UDM `metadata.event_type` |
|---|---|---|
| DMARC aggregate | `domain` | `EMAIL_TRANSACTION` |
| DMARC aggregate | `xml_schema` | `EMAIL_TRANSACTION` |
| DMARC failure | `feedback_type` | `EMAIL_TRANSACTION` |
| SMTP TLS (RFC 8460) | `policy_type` | `GENERIC_EVENT` |
+11 -7
View File
@@ -10,7 +10,7 @@ filter {
#
# parsedmarc emits three flat JSON shapes, one object per syslog line, via the
# CSV-row serializers (parsed_aggregate/failure/smtp_tls_reports_to_csv_rows):
# * DMARC aggregate report record -> detected by "domain"
# * DMARC aggregate report record -> detected by "xml_schema"
# * DMARC failure report record -> detected by "feedback_type"
# * SMTP TLS report record -> detected by "policy_type"
#
@@ -81,9 +81,10 @@ filter {
# report-type detection
"feedback_type" => ""
"policy_type" => ""
"domain" => ""
"xml_schema" => ""
# aggregate
"domain" => ""
"report_id" => ""
"org_name" => ""
"org_email" => ""
@@ -207,16 +208,19 @@ filter {
# and unique to each shape:
# feedback_type -> failure
# policy_type -> smtp_tls
# domain -> aggregate (the reported From-domain; a required
# element of every aggregate record)
# domain is preferred over header_from (which can be empty when a record
# carries no identifiers) and over adkim (a defaulted policy field).
# xml_schema -> aggregate
# xml_schema is aggregate-only and parsedmarc defaults it to "draft" when
# the report omits <version> (parsedmarc/__init__.py), so it survives a
# missing version. It is preferred over: header_from (can be empty when a
# record carries no identifiers), adkim (a defaulted policy field), domain
# (a generic name), and dmarc_aligned (a boolean that only becomes testable
# after the convert in step 1b -- detection should not depend on that).
# ---------------------------------------------------------------------------
if [feedback_type] {
mutate { replace => { "report_type" => "failure" } }
} else if [policy_type] {
mutate { replace => { "report_type" => "smtp_tls" } }
} else if [domain] {
} else if [xml_schema] {
mutate { replace => { "report_type" => "aggregate" } }
}