From 84848418f940beb59a81b44dc0da4de9aacd6e5f Mon Sep 17 00:00:00 2001 From: Sean Whalen <44679+seanthegeek@users.noreply.github.com> Date: Sat, 11 Jul 2026 16:04:03 -0400 Subject: [PATCH] Disambiguate "aggregate rows" in the xml_schema changelog entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In DMARC vocabulary "row" reads as the 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 --- CHANGELOG.md | 18 +++++++++++------- google_secops_parser/parsedmarc.conf | 5 +++-- parsedmarc/__init__.py | 7 ++++--- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99668115..61521713 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `` - root element is what identifies an aggregate report; `` is optional - metadata (RFC 7489 Appendix C) that reporters omit or leave empty in the - wild. An empty or whitespace-only `` 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 `` 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 + `` element (RFC 7489 Appendix C). That default did not cover an + empty `` 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 `` contains + non-empty text. ## 10.2.1 diff --git a/google_secops_parser/parsedmarc.conf b/google_secops_parser/parsedmarc.conf index a2304a84..0ea3065b 100644 --- a/google_secops_parser/parsedmarc.conf +++ b/google_secops_parser/parsedmarc.conf @@ -244,8 +244,9 @@ filter { # xml_schema is aggregate-only and guaranteed non-empty: the # XML root element is what identifies an aggregate report, and parsedmarc # falls back to "draft" whenever 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). diff --git a/parsedmarc/__init__.py b/parsedmarc/__init__.py index e9a6109c..ee8d0819 100644 --- a/parsedmarc/__init__.py +++ b/parsedmarc/__init__.py @@ -853,9 +853,10 @@ def parse_aggregate_report_xml( # revision. Reports in the wild omit entirely or send an # empty (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"])