mirror of
https://github.com/domainaware/parsedmarc.git
synced 2026-08-03 14:12:20 +00:00
Guarantee non-empty xml_schema: <feedback> identifies the report, not <version>
The <feedback> XML root element (anchored at the top of parse_aggregate_report_xml) is what identifies an aggregate report; <version> is optional metadata per RFC 7489 Appendix C. The "draft" fallback only covered a fully absent <version>: an empty <version/> (which xmltodict parses as None) or a whitespace-only value produced xml_schema=None, which the flat-row serializer coerced to "" — and any consumer detecting aggregate rows by a non-empty xml_schema, such as the Google SecOps CBN parser's report-type cascade, silently dropped the row. The fallback now applies unless <version> carries non-empty text (attribute-wrapped values are unwrapped via _text like other elements). The regression test fails on the previous code with xml_schema=None != "draft". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
a131fa937b
commit
571bbf741b
@@ -1497,6 +1497,73 @@ class Test(unittest.TestCase):
|
||||
self.assertIsNone(pp["testing"])
|
||||
self.assertIsNone(pp["discovery_method"])
|
||||
|
||||
def testEmptyVersionElementFallsBackToDraftSchema(self):
|
||||
"""An empty <version/> must not blank out xml_schema.
|
||||
|
||||
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. xmltodict parses an empty
|
||||
<version/> as None, which previously became xml_schema=None and an
|
||||
empty string in the flat rows — breaking downstream consumers that
|
||||
detect aggregate rows by a non-empty xml_schema (the Google SecOps
|
||||
parser's report-type cascade).
|
||||
"""
|
||||
xml = """<?xml version="1.0"?>
|
||||
<feedback>
|
||||
<version/>
|
||||
<report_metadata>
|
||||
<org_name>TestOrg</org_name>
|
||||
<email>test@example.com</email>
|
||||
<report_id>test-empty-version</report_id>
|
||||
<date_range><begin>1704067200</begin><end>1704153599</end></date_range>
|
||||
</report_metadata>
|
||||
<policy_published>
|
||||
<domain>example.com</domain>
|
||||
<p>none</p>
|
||||
</policy_published>
|
||||
<record>
|
||||
<row>
|
||||
<source_ip>192.0.2.1</source_ip>
|
||||
<count>1</count>
|
||||
<policy_evaluated><disposition>none</disposition><dkim>pass</dkim><spf>pass</spf></policy_evaluated>
|
||||
</row>
|
||||
<identifiers><header_from>example.com</header_from></identifiers>
|
||||
<auth_results><spf><domain>example.com</domain><result>pass</result></spf></auth_results>
|
||||
</record>
|
||||
</feedback>"""
|
||||
report = parsedmarc.parse_aggregate_report_xml(xml, offline=True)
|
||||
self.assertEqual(report["xml_schema"], "draft")
|
||||
rows = parsedmarc.parsed_aggregate_reports_to_csv_rows(report)
|
||||
self.assertEqual(rows[0]["xml_schema"], "draft")
|
||||
|
||||
def testWhitespaceVersionElementFallsBackToDraftSchema(self):
|
||||
"""A whitespace-only <version> value also falls back to 'draft'."""
|
||||
xml = """<?xml version="1.0"?>
|
||||
<feedback>
|
||||
<version> </version>
|
||||
<report_metadata>
|
||||
<org_name>TestOrg</org_name>
|
||||
<email>test@example.com</email>
|
||||
<report_id>test-ws-version</report_id>
|
||||
<date_range><begin>1704067200</begin><end>1704153599</end></date_range>
|
||||
</report_metadata>
|
||||
<policy_published>
|
||||
<domain>example.com</domain>
|
||||
<p>none</p>
|
||||
</policy_published>
|
||||
<record>
|
||||
<row>
|
||||
<source_ip>192.0.2.1</source_ip>
|
||||
<count>1</count>
|
||||
<policy_evaluated><disposition>none</disposition><dkim>pass</dkim><spf>pass</spf></policy_evaluated>
|
||||
</row>
|
||||
<identifiers><header_from>example.com</header_from></identifiers>
|
||||
<auth_results><spf><domain>example.com</domain><result>pass</result></spf></auth_results>
|
||||
</record>
|
||||
</feedback>"""
|
||||
report = parsedmarc.parse_aggregate_report_xml(xml, offline=True)
|
||||
self.assertEqual(report["xml_schema"], "draft")
|
||||
|
||||
def testMagicXmlTagDetection(self):
|
||||
"""XML without declaration (starting with '<') is extracted"""
|
||||
xml_no_decl = b"<feedback><report_metadata><org_name>T</org_name><email>a@b.com</email><report_id>r1</report_id><date_range><begin>1704067200</begin><end>1704153599</end></date_range></report_metadata><policy_published><domain>example.com</domain><p>none</p></policy_published><record><row><source_ip>192.0.2.1</source_ip><count>1</count><policy_evaluated><disposition>none</disposition><dkim>pass</dkim><spf>pass</spf></policy_evaluated></row><identifiers><header_from>example.com</header_from></identifiers><auth_results><spf><domain>example.com</domain><result>pass</result></spf></auth_results></record></feedback>"
|
||||
|
||||
Reference in New Issue
Block a user