Disambiguate "aggregate rows" in the xml_schema changelog entry

In DMARC vocabulary "row" reads as the <row> element inside an
aggregate report record, which carries no schema information — the
changelog entry was about the flattened JSON/CSV output lines, where
xml_schema is a synthesized per-record field. Reword the entry and the
two related comments to say which kind of row is meant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sean Whalen
2026-07-11 16:04:03 -04:00
co-authored by Claude Fable 5
parent cd2785d440
commit 84848418f9
3 changed files with 18 additions and 12 deletions
+11 -7
View File
@@ -34,13 +34,17 @@
`policy_strings` / `mx_host_patterns` from an earlier policy were reused for
a later policy that did not define them, because the row template dict was
built once per report instead of once per policy.
- **`xml_schema` can no longer be empty on aggregate rows.** The `<feedback>`
root element is what identifies an aggregate report; `<version>` is optional
metadata (RFC 7489 Appendix C) that reporters omit or leave empty in the
wild. An empty or whitespace-only `<version/>` previously produced
`xml_schema=None` (an empty string in the flat CSV/JSON rows), breaking
consumers that detect aggregate rows by a non-empty `xml_schema`. The
"draft" fallback now applies unless `<version>` carries non-empty text.
- **The `xml_schema` field of a parsed aggregate report can no longer be
empty.** parsedmarc reports the schema revision of an aggregate report in a
synthesized `xml_schema` field (surfaced in the JSON/CSV output for every
record), defaulting to `"draft"` when the report omits the optional
`<version>` element (RFC 7489 Appendix C). That default did not cover an
empty `<version/>` or a whitespace-only value, which produced
`xml_schema=None` — an empty string in the flat JSON/CSV output — so
consumers that identify aggregate records by a non-empty `xml_schema`
(such as the Google SecOps parser's report-type detection) dropped those
records. The `"draft"` fallback now applies unless `<version>` contains
non-empty text.
## 10.2.1
+3 -2
View File
@@ -244,8 +244,9 @@ filter {
# xml_schema is aggregate-only and guaranteed non-empty: the <feedback>
# XML root element is what identifies an aggregate report, and parsedmarc
# falls back to "draft" whenever <version> is missing, empty, or
# whitespace (parsedmarc/__init__.py), so every aggregate row carries a
# usable value. It is preferred over: header_from (can be empty when a
# whitespace (parsedmarc/__init__.py), so every JSON line serialized
# from an aggregate report carries a usable value. 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).
+4 -3
View File
@@ -853,9 +853,10 @@ def parse_aggregate_report_xml(
# revision. Reports in the wild omit <version> entirely or send an
# empty <version/> (which xmltodict parses as None) or one carrying
# attributes (a dict). Only a non-empty text value may override the
# "draft" default: xml_schema must never be empty, because flat-row
# consumers (e.g. the Google SecOps parser's report-type detection)
# rely on it being present on every aggregate row.
# "draft" default: xml_schema must never be empty, because consumers
# of the flattened JSON/CSV output (e.g. the Google SecOps parser's
# report-type detection) rely on it being non-empty on every line
# serialized from an aggregate report.
schema = "draft"
if "version" in report:
version = _text(report["version"])