dmarc-report-parser 0.2.0

An RFC 7489-compliant DMARC aggregate report parser
Documentation

dmarc-report-parser

Crates.io docs.rs License: MIT

An RFC 7489-compliant DMARC aggregate report parser written in Rust. It can be used both as a library in your own Rust projects and as a standalone CLI tool for viewing reports in the terminal, as HTML, or as Markdown.

Features

  • Parses DMARC aggregate feedback XML as defined in RFC 7489 Appendix C
  • Zero-copy deserialization into strongly-typed Rust structs
  • FromStr, TryFrom<&str>, and TryFrom<&[u8]> trait implementations for ergonomic usage
  • Optional CLI with colorized terminal output, HTML, and Markdown rendering
  • CLI supports .xml, .xml.gz, .gz, and .zip input files
  • CLI accepts multiple files and renders them as a single aggregate view

Installation

As a library

Add the crate to your project:

cargo add dmarc-report-parser

As a CLI tool

Install with the cli feature enabled:

cargo install dmarc-report-parser --features cli

This installs the dmarc-report binary.

Library usage

Parsing from a string

use dmarc_report_parser::{parse, Report};

let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<feedback>
  <report_metadata>
    <org_name>Example Corp</org_name>
    <email>dmarc@example.com</email>
    <report_id>abc123</report_id>
    <date_range>
      <begin>1609459200</begin>
      <end>1609545600</end>
    </date_range>
  </report_metadata>
  <policy_published>
    <domain>example.com</domain>
    <adkim>r</adkim>
    <aspf>r</aspf>
    <p>none</p>
    <sp>none</sp>
    <pct>100</pct>
  </policy_published>
  <record>
    <row>
      <source_ip>192.0.2.1</source_ip>
      <count>5</count>
      <policy_evaluated>
        <disposition>none</disposition>
        <dkim>pass</dkim>
        <spf>pass</spf>
      </policy_evaluated>
    </row>
    <identifiers>
      <envelope_from>example.com</envelope_from>
      <header_from>example.com</header_from>
    </identifiers>
    <auth_results>
      <spf>
        <domain>example.com</domain>
        <result>pass</result>
      </spf>
    </auth_results>
  </record>
</feedback>"#;

let report: Report = parse(xml).unwrap();
assert_eq!(report.report_metadata.org_name, "Example Corp");
assert_eq!(report.records.len(), 1);

Parsing from bytes

let xml_bytes: &[u8] = b"<?xml version=\"1.0\"?>
<feedback>
  <report_metadata>
    <org_name>Test</org_name>
    <email>test@example.com</email>
    <report_id>1</report_id>
    <date_range><begin>0</begin><end>0</end></date_range>
  </report_metadata>
  <policy_published>
    <domain>example.com</domain><p>none</p><sp>none</sp><pct>100</pct>
  </policy_published>
  <record>
    <row>
      <source_ip>127.0.0.1</source_ip><count>1</count>
      <policy_evaluated><disposition>none</disposition><dkim>pass</dkim><spf>pass</spf></policy_evaluated>
    </row>
    <identifiers><envelope_from>example.com</envelope_from><header_from>example.com</header_from></identifiers>
    <auth_results><spf><domain>example.com</domain><result>pass</result></spf></auth_results>
  </record>
</feedback>";

let report = dmarc_report_parser::parse_bytes(xml_bytes).unwrap();
assert_eq!(report.report_metadata.org_name, "Test");

Using FromStr / TryFrom

use std::str::FromStr;
use dmarc_report_parser::Report;

# let xml = r#"<?xml version="1.0"?>
# <feedback>
#   <report_metadata>
#     <org_name>Test</org_name><email>t@e.com</email><report_id>1</report_id>
#     <date_range><begin>0</begin><end>0</end></date_range>
#   </report_metadata>
#   <policy_published><domain>e.com</domain><p>none</p><sp>none</sp><pct>100</pct></policy_published>
#   <record>
#     <row><source_ip>127.0.0.1</source_ip><count>1</count>
#       <policy_evaluated><disposition>none</disposition><dkim>pass</dkim><spf>pass</spf></policy_evaluated>
#     </row>
#     <identifiers><envelope_from>e.com</envelope_from><header_from>e.com</header_from></identifiers>
#     <auth_results><spf><domain>e.com</domain><result>pass</result></spf></auth_results>
#   </record>
# </feedback>"#;
let report = Report::from_str(xml).unwrap();
let report: Report = xml.parse().unwrap();
let report = Report::try_from(xml).unwrap();

CLI usage

The dmarc-report CLI reads one or more DMARC aggregate report files and renders them in the chosen format. When multiple files are passed, the output is an aggregate view containing an overview, a per-report summary, and a combined records table.

Usage: dmarc-report [OPTIONS] <FILES>...

Arguments:
  <FILES>...  One or more DMARC report files (.xml, .xml.gz, .zip, or .gz)

Options:
  -f, --format <FORMAT>  Output format [default: terminal]
                         [possible values: terminal, html, markdown]
  -o, --output <FILE>    Write output to a file instead of stdout
  -h, --help             Print help
  -V, --version          Print version

Examples

# Display a report in the terminal with colors
dmarc-report report.xml

# Render as HTML and save to a file
dmarc-report report.xml --format html --output report.html

# Render as Markdown from a gzip-compressed report
dmarc-report report.xml.gz --format markdown

# Parse a report inside a zip archive
dmarc-report report.zip

# Aggregate multiple reports into a single Markdown view
dmarc-report *.xml.gz --format markdown

Supported types

The library exposes the full RFC 7489 Appendix C schema as Rust types:

Type Description
Report Top-level aggregate feedback report
Aggregate Combined view across multiple reports
ReportMetadata Report generator metadata
DateRange UTC time range (Unix timestamps)
PolicyPublished Published DMARC policy for the domain
Record A single message record
Row Per-message data row
PolicyEvaluated DMARC evaluation results
PolicyOverrideReason Reason for policy override
Identifiers Message identifiers
AuthResults Authentication results
DkimAuthResult DKIM signature evaluation result
SpfAuthResult SPF check result

And enums: AlignmentMode, Disposition, DmarcResult, DkimResult, SpfResult, SpfDomainScope, PolicyOverride.

License

MIT — see LICENSE for details.