dmarcator 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e13144ffc39b0f47db1c26f65201c05db51af4701cf51ffd2398dcab8011a791
4
- data.tar.gz: 7560c54eb55c97de2a841e22f5c1ff4469741b23a2bd3f593934eb24b21c3582
3
+ metadata.gz: bdd3aafe769b2b0da2829b7247b4c65cd5cb15a28f41f1703e4b198938f43a58
4
+ data.tar.gz: 22cd8f8dfed8f93c89b329af83b8b057288b15087033f2ee3646f17dbc631a06
5
5
  SHA512:
6
- metadata.gz: f7966f69b4b9741fad6a84c6f502631137f32891955d81a3f94e3b83fe01810fca1a8a94544dbb4f81f6adbc361b6d6d89374377d435e46f8df44a82e247a37a
7
- data.tar.gz: 016b57445827a1c297870e079a148be4c0ce786dabc3f0cb2574a09beb00c8d91742a117b0d03e8095c3b4f4aeeb46a22a88431c3c4b781df948021a77f1bab4
6
+ metadata.gz: 18710be1b05e3d8c6301654a0ed9a9957d8d67eafa2e873e83cc3d43b0d1d4d2f2551a808b180177d3140cabc2bf3460be61578ae0ecba6d9b9e2ac4a0ad4a29
7
+ data.tar.gz: e03b01fa23c6981ca858da830e73f08ddc6ca522dc28849d7efc076dbb0f6d319e41e8687522c5ecbe10bfeeda1e8726e9289ed8566711e9abc672e294ffdc54
data/ChangeLog.md CHANGED
@@ -1,3 +1,8 @@
1
+ ### 0.2.0 / 2026-10-13
2
+
3
+ * Added DMARC XML report parsing, ported from the `dmarc_parser` gem by D Salko.
4
+ * Added `nokogiri` and `bigdecimal` runtime dependencies.
5
+
1
6
  ### 0.1.0 / 2026-10-13
2
7
 
3
8
  * Forked from the Trail of Bits `dmarc` gem and published as `dmarcator`.
data/LICENSE.txt CHANGED
@@ -1,6 +1,7 @@
1
1
  The MIT License (MIT)
2
2
 
3
3
  Copyright (c) 2014-2016 Trail of Bits
4
+ Copyright (c) 2020 D Salko
4
5
  Copyright (c) 2026 Noah Berman
5
6
 
6
7
  Permission is hereby granted, free of charge, to any person obtaining a copy of
data/README.md CHANGED
@@ -11,8 +11,9 @@ DMARCator is a Ruby library for parsing and querying [DMARC] records.
11
11
  > a stricter parser. See [Acknowledgments](#acknowledgments) below.
12
12
 
13
13
  [DMARC] policies are published as DNS TXT records at `_dmarc.<domain>`. This
14
- library parses those records into a structured `DMARCator::Record` and can query
15
- them directly through Ruby's `Resolv` DNS client.
14
+ library parses those records into a structured `DMARCator::Record`, queries them
15
+ directly through Ruby's `Resolv` DNS client, and parses the XML aggregate
16
+ reports those policies generate.
16
17
 
17
18
  ## Installation
18
19
 
@@ -92,10 +93,32 @@ record = DMARCator::Record.query("twitter.com")
92
93
  # => #<DMARCator::Record ... @p=:reject, @rua=[...], ...>
93
94
  ```
94
95
 
96
+ ## Parsing XML reports
97
+
98
+ Parse a DMARC aggregate/forensic report:
99
+
100
+ ```ruby
101
+ report = DMARCator::Report.new(xml_string)
102
+
103
+ report.version
104
+ # => 0.1e0
105
+
106
+ report.metadata.org_name
107
+ # => "google.com"
108
+
109
+ report.policy.domain
110
+ # => "opserv.io"
111
+
112
+ report.records.first.source_ip
113
+ # => "209.85.220.69"
114
+ ```
115
+
95
116
  ## Requirements
96
117
 
97
118
  * [Ruby] >= 3.0.0
98
119
  * [parslet] >= 1.0.0, < 3.0.0
120
+ * [nokogiri] >= 1.10
121
+ * [bigdecimal] >= 1.0
99
122
 
100
123
  ## Testing
101
124
 
@@ -129,9 +152,15 @@ This fork renames the `DMARC` module to `DMARCator` (loaded via
129
152
  * a DNS query path that selects the `v=DMARC1` TXT record per RFC 7489,
130
153
  * hardened packaging and test fixtures.
131
154
 
155
+ The DMARC XML report parser is ported from [`dmarc_parser`][dmarc-parser-gem],
156
+ created by D Salko and released under the MIT License.
157
+
132
158
  [DMARC]: https://www.rfc-editor.org/rfc/rfc7489
133
159
  [dmarc-gem]: https://rubygems.org/gems/dmarc
160
+ [dmarc-parser-gem]: https://rubygems.org/gems/dmarc_parser
134
161
  [Trail of Bits]: https://www.trailofbits.com
135
162
  [Ruby]: https://www.ruby-lang.org
136
163
  [parslet]: https://kschiess.github.io/parslet/
164
+ [nokogiri]: https://nokogiri.org
165
+ [bigdecimal]: https://github.com/ruby/bigdecimal
137
166
  [LICENSE.txt]: LICENSE.txt
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../xml_helper'
4
+
5
+ module DMARCator
6
+ class Report
7
+ #
8
+ # Represents an `auth_results` entry (DKIM/SPF) in a DMARC XML report.
9
+ #
10
+ class AuthResult
11
+ include XmlHelper
12
+
13
+ ATTRIBUTES = [:type, :domain, :result, :selector, :scope]
14
+
15
+ attr_accessor(*ATTRIBUTES)
16
+
17
+ def initialize(node)
18
+ @node = node
19
+ self.type = node.name
20
+ self.domain = get_text('domain')
21
+ self.result = get_text('result')
22
+ self.selector = get_text('selector')
23
+ self.scope = get_text('scope')
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../xml_helper'
4
+
5
+ module DMARCator
6
+ class Report
7
+ #
8
+ # Represents the `report_metadata` element of a DMARC XML report.
9
+ #
10
+ class Metadata
11
+ include XmlHelper
12
+
13
+ ATTRIBUTES = [
14
+ :org_name, :email, :extra_contact_info, :report_id,
15
+ :begin_at, :end_at, :errors
16
+ ]
17
+
18
+ attr_accessor(*ATTRIBUTES)
19
+
20
+ def initialize(node)
21
+ @node = node
22
+ self.org_name = get_text('feedback/report_metadata/org_name')
23
+ self.email = get_text('feedback/report_metadata/email')
24
+ self.extra_contact_info = get_text('feedback/report_metadata/extra_contact_info')
25
+ self.report_id = get_text('feedback/report_metadata/report_id')
26
+ self.begin_at = get_timestamp('feedback/report_metadata/date_range/begin')
27
+ self.end_at = get_timestamp('feedback/report_metadata/date_range/end')
28
+ self.errors = @node.xpath('feedback/report_metadata/error').map(&:text)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../xml_helper'
4
+
5
+ module DMARCator
6
+ class Report
7
+ #
8
+ # Represents the `policy_published` element of a DMARC XML report.
9
+ #
10
+ class Policy
11
+ include XmlHelper
12
+
13
+ ATTRIBUTES = [:domain, :adkim, :aspf, :p, :sp, :pct, :fo]
14
+
15
+ attr_accessor(*ATTRIBUTES)
16
+
17
+ def initialize(node)
18
+ @node = node
19
+
20
+ ATTRIBUTES.each do |attr|
21
+ self.public_send("#{attr}=", get_text("feedback/policy_published/#{attr}"))
22
+ end
23
+
24
+ self.pct = self.pct && !self.pct.empty? ? self.pct.to_i : nil
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../xml_helper'
4
+
5
+ module DMARCator
6
+ class Report
7
+ #
8
+ # Represents a `reason` element of a DMARC XML report record.
9
+ #
10
+ class Reason
11
+ include XmlHelper
12
+
13
+ ATTRIBUTES = [:type, :comment]
14
+
15
+ attr_accessor(*ATTRIBUTES)
16
+
17
+ def initialize(node)
18
+ @node = node
19
+ self.type = get_text('type')
20
+ self.comment = get_text('comment')
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../xml_helper'
4
+ require_relative 'auth_result'
5
+ require_relative 'reason'
6
+
7
+ module DMARCator
8
+ class Report
9
+ #
10
+ # Represents a single `record` element of a DMARC XML report.
11
+ #
12
+ class Record
13
+ include XmlHelper
14
+
15
+ ATTRIBUTES = [
16
+ :source_ip, :count, :disposition, :dkim, :spf,
17
+ :envelope_to, :envelope_from, :header_from,
18
+ :auth_results, :reasons
19
+ ]
20
+
21
+ attr_accessor(*ATTRIBUTES)
22
+
23
+ def initialize(node)
24
+ @node = node
25
+ self.source_ip = get_text('row/source_ip')
26
+ self.count = get_int('row/count')
27
+ self.disposition = get_text('row/policy_evaluated/disposition')
28
+ self.dkim = get_text('row/policy_evaluated/dkim')
29
+ self.spf = get_text('row/policy_evaluated/spf')
30
+ self.reasons = node.xpath('row/policy_evaluated/reason').map do |reason_node|
31
+ Reason.new(reason_node)
32
+ end
33
+ self.envelope_to = get_text('identifiers/envelope_to')
34
+ self.envelope_from = get_text('identifiers/envelope_from')
35
+ self.header_from = get_text('identifiers/header_from')
36
+ self.auth_results = node.xpath('auth_results/*').map do |auth_results_node|
37
+ AuthResult.new(auth_results_node)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+ require 'bigdecimal'
5
+ require 'bigdecimal/util'
6
+
7
+ require_relative 'xml_helper'
8
+ require_relative 'report/metadata'
9
+ require_relative 'report/policy'
10
+ require_relative 'report/record'
11
+
12
+ module DMARCator
13
+ #
14
+ # Represents a parsed DMARC aggregate/forensic XML report.
15
+ #
16
+ # Nokogiri's default parse options are used, which do not resolve external
17
+ # entities or fetch network resources; treat report XML as untrusted input.
18
+ #
19
+ class Report
20
+ include XmlHelper
21
+
22
+ #
23
+ # Initializes the report from an XML string.
24
+ #
25
+ # @param [String] xml
26
+ # The raw DMARC XML report.
27
+ #
28
+ def initialize(xml)
29
+ @xml = xml
30
+ @node = Nokogiri::XML(xml)
31
+ @node.remove_namespaces!
32
+ end
33
+
34
+ #
35
+ # The report version.
36
+ #
37
+ # @return [BigDecimal, nil]
38
+ #
39
+ def version
40
+ @version ||= begin
41
+ value = get_text('feedback/version')
42
+ BigDecimal(value) if value && !value.empty?
43
+ end
44
+ end
45
+
46
+ #
47
+ # The report metadata.
48
+ #
49
+ # @return [Report::Metadata]
50
+ #
51
+ def metadata
52
+ @metadata ||= Metadata.new(@node)
53
+ end
54
+
55
+ #
56
+ # The published DMARC policy.
57
+ #
58
+ # @return [Report::Policy]
59
+ #
60
+ def policy
61
+ @policy ||= Policy.new(@node)
62
+ end
63
+
64
+ #
65
+ # The report records.
66
+ #
67
+ # @return [Array<Report::Record>]
68
+ #
69
+ def records
70
+ @records ||= @node.xpath('feedback/record').map do |node|
71
+ Record.new(node)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -1,4 +1,4 @@
1
1
  module DMARCator
2
2
  # dmarcator version
3
- VERSION = '0.1.0'
3
+ VERSION = '0.2.0'
4
4
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+
5
+ module DMARCator
6
+ #
7
+ # Internal helper for extracting values from parsed DMARC XML reports.
8
+ #
9
+ module XmlHelper
10
+ #
11
+ # Returns the text of the first node matching the XPath expression.
12
+ #
13
+ # @param [String] xpath
14
+ # The XPath expression to match.
15
+ #
16
+ # @return [String, nil]
17
+ # The node text, or `nil` if no node matched.
18
+ #
19
+ def get_text(xpath)
20
+ @node.at_xpath(xpath)&.text
21
+ end
22
+
23
+ #
24
+ # Returns a Unix timestamp as a `Time`.
25
+ #
26
+ # @param [String] xpath
27
+ # The XPath expression to match.
28
+ #
29
+ # @return [Time, nil]
30
+ #
31
+ def get_timestamp(xpath)
32
+ value = get_text(xpath)
33
+ value && !value.empty? ? Time.at(value.to_i) : nil
34
+ end
35
+
36
+ #
37
+ # Returns an integer value.
38
+ #
39
+ # @param [String] xpath
40
+ # The XPath expression to match.
41
+ #
42
+ # @return [Integer, nil]
43
+ #
44
+ def get_int(xpath)
45
+ value = get_text(xpath)
46
+ value && !value.empty? ? value.to_i : nil
47
+ end
48
+ end
49
+ end
data/lib/dmarcator.rb CHANGED
@@ -2,4 +2,5 @@ require 'dmarcator/exceptions'
2
2
  require 'dmarcator/query'
3
3
  require 'dmarcator/record'
4
4
  require 'dmarcator/parser'
5
+ require 'dmarcator/report'
5
6
  require 'dmarcator/version'
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'dmarcator/report'
5
+ require 'bigdecimal'
6
+
7
+ RSpec.describe DMARCator::Report do
8
+ let(:example_path) { File.join(__dir__, 'support', 'complete.xml') }
9
+ let(:google_path) { File.join(__dir__, 'support', 'google.xml') }
10
+
11
+ let(:xml_report) { File.read(example_path) }
12
+ let(:google_report) { File.read(google_path) }
13
+
14
+ it 'has a version number' do
15
+ expect(DMARCator::VERSION).not_to be_nil
16
+ end
17
+
18
+ it 'reads report version if available' do
19
+ report = described_class.new(xml_report)
20
+ expect(report.version).to eq(BigDecimal('0.1'))
21
+
22
+ google = described_class.new(google_report)
23
+ expect(google.version).to be_nil
24
+ end
25
+
26
+ it 'reads report metadata' do
27
+ report = described_class.new(xml_report)
28
+
29
+ expect(report.metadata.org_name).to eq('google.com')
30
+ expect(report.metadata.email).to eq('noreply-dmarc-support@google.com')
31
+ expect(report.metadata.extra_contact_info).to eq('https://support.google.com/a/answer/2466580')
32
+ expect(report.metadata.report_id).to eq('3114726731964606297')
33
+ expect(report.metadata.begin_at).to eq(Time.at(1_593_648_000))
34
+ expect(report.metadata.end_at).to eq(Time.at(1_593_734_399))
35
+ expect(report.metadata.errors).to eq(['some error', 'another error'])
36
+ end
37
+
38
+ it 'reads policy published' do
39
+ report = described_class.new(xml_report)
40
+
41
+ expect(report.policy.domain).to eq('opserv.io')
42
+ expect(report.policy.adkim).to eq('r')
43
+ expect(report.policy.aspf).to eq('r')
44
+ expect(report.policy.p).to eq('none')
45
+ expect(report.policy.sp).to eq('none')
46
+ expect(report.policy.pct).to eq(100)
47
+ expect(report.policy.fo).to eq('1')
48
+ end
49
+
50
+ it 'reads report records' do
51
+ report = described_class.new(xml_report)
52
+ record = report.records.first
53
+
54
+ expect(record.source_ip).to eq('209.85.220.69')
55
+ expect(record.count).to eq(96)
56
+ expect(record.disposition).to eq('none')
57
+ expect(record.dkim).to eq('fail')
58
+ expect(record.spf).to eq('fail')
59
+
60
+ expect(record.reasons.count).to eq(2)
61
+ expect(record.reasons.first.type).to eq('local_policy')
62
+ expect(record.reasons.first.comment).to eq('arc=pass')
63
+ expect(record.reasons.last.type).to eq('other')
64
+ expect(record.reasons.last.comment).to eq('dkim=pass')
65
+
66
+ expect(record.envelope_to).to eq('envelope-to.io')
67
+ expect(record.envelope_from).to eq('envelope-from.io')
68
+ expect(record.header_from).to eq('opserv.io')
69
+
70
+ expect(record.auth_results.count).to eq(2)
71
+ expect(record.auth_results.first.type).to eq('dkim')
72
+ expect(record.auth_results.first.domain).to eq('condenast-com-tw.20150623.gappssmtp.com')
73
+ expect(record.auth_results.first.result).to eq('pass')
74
+ expect(record.auth_results.first.selector).to eq('20150623')
75
+ expect(record.auth_results.last.type).to eq('spf')
76
+ expect(record.auth_results.last.domain).to eq('condenast.com.tw')
77
+ expect(record.auth_results.last.result).to eq('neutral')
78
+ expect(record.auth_results.last.scope).to eq('mfrom')
79
+ end
80
+ end
@@ -0,0 +1,364 @@
1
+ <?xml version="1.0" encoding="UTF-8" ?>
2
+ <feedback xmlns="urn:ietf:params:xml:ns:dmarc-2.0">
3
+ <version>0.1</version>
4
+ <report_metadata>
5
+ <org_name>google.com</org_name>
6
+ <email>noreply-dmarc-support@google.com</email>
7
+ <extra_contact_info>https://support.google.com/a/answer/2466580</extra_contact_info>
8
+ <report_id>3114726731964606297</report_id>
9
+ <date_range>
10
+ <begin>1593648000</begin>
11
+ <end>1593734399</end>
12
+ </date_range>
13
+ <error>some error</error>
14
+ <error>another error</error>
15
+ </report_metadata>
16
+ <policy_published>
17
+ <domain>opserv.io</domain>
18
+ <adkim>r</adkim>
19
+ <aspf>r</aspf>
20
+ <p>none</p>
21
+ <sp>none</sp>
22
+ <pct>100</pct>
23
+ <fo>1</fo>
24
+ </policy_published>
25
+ <record>
26
+ <row>
27
+ <source_ip>209.85.220.69</source_ip>
28
+ <count>96</count>
29
+ <policy_evaluated>
30
+ <disposition>none</disposition>
31
+ <dkim>fail</dkim>
32
+ <spf>fail</spf>
33
+ <reason>
34
+ <type>local_policy</type>
35
+ <comment>arc=pass</comment>
36
+ </reason>
37
+ <reason>
38
+ <type>other</type>
39
+ <comment>dkim=pass</comment>
40
+ </reason>
41
+ </policy_evaluated>
42
+ </row>
43
+ <identifiers>
44
+ <envelope_to>envelope-to.io</envelope_to>
45
+ <envelope_from>envelope-from.io</envelope_from>
46
+ <header_from>opserv.io</header_from>
47
+ </identifiers>
48
+ <auth_results>
49
+ <dkim>
50
+ <domain>condenast-com-tw.20150623.gappssmtp.com</domain>
51
+ <result>pass</result>
52
+ <selector>20150623</selector>
53
+ </dkim>
54
+ <spf>
55
+ <domain>condenast.com.tw</domain>
56
+ <result>neutral</result>
57
+ <scope>mfrom</scope>
58
+ </spf>
59
+ </auth_results>
60
+ </record>
61
+ <record>
62
+ <row>
63
+ <source_ip>167.89.100.132</source_ip>
64
+ <count>11</count>
65
+ <policy_evaluated>
66
+ <disposition>none</disposition>
67
+ <dkim>pass</dkim>
68
+ <spf>pass</spf>
69
+ </policy_evaluated>
70
+ </row>
71
+ <identifiers>
72
+ <header_from>opserv.io</header_from>
73
+ </identifiers>
74
+ <auth_results>
75
+ <dkim>
76
+ <domain>opserv.io</domain>
77
+ <result>pass</result>
78
+ <selector>s1</selector>
79
+ </dkim>
80
+ <dkim>
81
+ <domain>sendgrid.info</domain>
82
+ <result>pass</result>
83
+ <selector>smtpapi</selector>
84
+ </dkim>
85
+ <spf>
86
+ <domain>alert.opserv.io</domain>
87
+ <result>pass</result>
88
+ </spf>
89
+ </auth_results>
90
+ </record>
91
+ <record>
92
+ <row>
93
+ <source_ip>167.89.100.13</source_ip>
94
+ <count>96</count>
95
+ <policy_evaluated>
96
+ <disposition>none</disposition>
97
+ <dkim>pass</dkim>
98
+ <spf>pass</spf>
99
+ </policy_evaluated>
100
+ </row>
101
+ <identifiers>
102
+ <header_from>opserv.io</header_from>
103
+ </identifiers>
104
+ <auth_results>
105
+ <dkim>
106
+ <domain>opserv.io</domain>
107
+ <result>pass</result>
108
+ <selector>s1</selector>
109
+ </dkim>
110
+ <dkim>
111
+ <domain>sendgrid.info</domain>
112
+ <result>pass</result>
113
+ <selector>smtpapi</selector>
114
+ </dkim>
115
+ <spf>
116
+ <domain>alert.opserv.io</domain>
117
+ <result>pass</result>
118
+ </spf>
119
+ </auth_results>
120
+ </record>
121
+ <record>
122
+ <row>
123
+ <source_ip>167.89.100.13</source_ip>
124
+ <count>161</count>
125
+ <policy_evaluated>
126
+ <disposition>none</disposition>
127
+ <dkim>pass</dkim>
128
+ <spf>pass</spf>
129
+ </policy_evaluated>
130
+ </row>
131
+ <identifiers>
132
+ <header_from>opserv.io</header_from>
133
+ </identifiers>
134
+ <auth_results>
135
+ <dkim>
136
+ <domain>opserv.io</domain>
137
+ <result>pass</result>
138
+ <selector>s1</selector>
139
+ </dkim>
140
+ <spf>
141
+ <domain>alert.opserv.io</domain>
142
+ <result>pass</result>
143
+ </spf>
144
+ </auth_results>
145
+ </record>
146
+ <record>
147
+ <row>
148
+ <source_ip>50.31.60.236</source_ip>
149
+ <count>28</count>
150
+ <policy_evaluated>
151
+ <disposition>none</disposition>
152
+ <dkim>pass</dkim>
153
+ <spf>pass</spf>
154
+ </policy_evaluated>
155
+ </row>
156
+ <identifiers>
157
+ <header_from>opserv.io</header_from>
158
+ </identifiers>
159
+ <auth_results>
160
+ <dkim>
161
+ <domain>opserv.io</domain>
162
+ <result>pass</result>
163
+ <selector>s1</selector>
164
+ </dkim>
165
+ <spf>
166
+ <domain>alert.opserv.io</domain>
167
+ <result>pass</result>
168
+ </spf>
169
+ </auth_results>
170
+ </record>
171
+ <record>
172
+ <row>
173
+ <source_ip>50.31.60.236</source_ip>
174
+ <count>13</count>
175
+ <policy_evaluated>
176
+ <disposition>none</disposition>
177
+ <dkim>pass</dkim>
178
+ <spf>pass</spf>
179
+ </policy_evaluated>
180
+ </row>
181
+ <identifiers>
182
+ <header_from>opserv.io</header_from>
183
+ </identifiers>
184
+ <auth_results>
185
+ <dkim>
186
+ <domain>opserv.io</domain>
187
+ <result>pass</result>
188
+ <selector>s1</selector>
189
+ </dkim>
190
+ <dkim>
191
+ <domain>sendgrid.info</domain>
192
+ <result>pass</result>
193
+ <selector>smtpapi</selector>
194
+ </dkim>
195
+ <spf>
196
+ <domain>alert.opserv.io</domain>
197
+ <result>pass</result>
198
+ </spf>
199
+ </auth_results>
200
+ </record>
201
+ <record>
202
+ <row>
203
+ <source_ip>209.85.220.41</source_ip>
204
+ <count>3</count>
205
+ <policy_evaluated>
206
+ <disposition>none</disposition>
207
+ <dkim>pass</dkim>
208
+ <spf>fail</spf>
209
+ </policy_evaluated>
210
+ </row>
211
+ <identifiers>
212
+ <header_from>opserv.io</header_from>
213
+ </identifiers>
214
+ <auth_results>
215
+ <dkim>
216
+ <domain>opserv.io</domain>
217
+ <result>pass</result>
218
+ <selector>s1</selector>
219
+ </dkim>
220
+ <spf>
221
+ <domain>hta.gr</domain>
222
+ <result>pass</result>
223
+ </spf>
224
+ </auth_results>
225
+ </record>
226
+ <record>
227
+ <row>
228
+ <source_ip>209.85.220.69</source_ip>
229
+ <count>10</count>
230
+ <policy_evaluated>
231
+ <disposition>none</disposition>
232
+ <dkim>fail</dkim>
233
+ <spf>fail</spf>
234
+ <reason>
235
+ <type>local_policy</type>
236
+ <comment>arc=pass</comment>
237
+ </reason>
238
+ </policy_evaluated>
239
+ </row>
240
+ <identifiers>
241
+ <header_from>opserv.io</header_from>
242
+ </identifiers>
243
+ <auth_results>
244
+ <dkim>
245
+ <domain>unicstay-com.20150623.gappssmtp.com</domain>
246
+ <result>pass</result>
247
+ <selector>20150623</selector>
248
+ </dkim>
249
+ <spf>
250
+ <domain>unicstay.com</domain>
251
+ <result>pass</result>
252
+ </spf>
253
+ </auth_results>
254
+ </record>
255
+ <record>
256
+ <row>
257
+ <source_ip>209.85.220.41</source_ip>
258
+ <count>3</count>
259
+ <policy_evaluated>
260
+ <disposition>none</disposition>
261
+ <dkim>pass</dkim>
262
+ <spf>fail</spf>
263
+ </policy_evaluated>
264
+ </row>
265
+ <identifiers>
266
+ <header_from>opserv.io</header_from>
267
+ </identifiers>
268
+ <auth_results>
269
+ <dkim>
270
+ <domain>opserv.io</domain>
271
+ <result>pass</result>
272
+ <selector>s1</selector>
273
+ </dkim>
274
+ <dkim>
275
+ <domain>sendgrid.info</domain>
276
+ <result>pass</result>
277
+ <selector>smtpapi</selector>
278
+ </dkim>
279
+ <spf>
280
+ <domain>gmail.com</domain>
281
+ <result>pass</result>
282
+ </spf>
283
+ </auth_results>
284
+ </record>
285
+ <record>
286
+ <row>
287
+ <source_ip>167.89.100.132</source_ip>
288
+ <count>31</count>
289
+ <policy_evaluated>
290
+ <disposition>none</disposition>
291
+ <dkim>pass</dkim>
292
+ <spf>pass</spf>
293
+ </policy_evaluated>
294
+ </row>
295
+ <identifiers>
296
+ <header_from>opserv.io</header_from>
297
+ </identifiers>
298
+ <auth_results>
299
+ <dkim>
300
+ <domain>opserv.io</domain>
301
+ <result>pass</result>
302
+ <selector>s1</selector>
303
+ </dkim>
304
+ <spf>
305
+ <domain>alert.opserv.io</domain>
306
+ <result>pass</result>
307
+ </spf>
308
+ </auth_results>
309
+ </record>
310
+ <record>
311
+ <row>
312
+ <source_ip>209.85.220.69</source_ip>
313
+ <count>6</count>
314
+ <policy_evaluated>
315
+ <disposition>none</disposition>
316
+ <dkim>fail</dkim>
317
+ <spf>fail</spf>
318
+ <reason>
319
+ <type>local_policy</type>
320
+ <comment>arc=pass</comment>
321
+ </reason>
322
+ </policy_evaluated>
323
+ </row>
324
+ <identifiers>
325
+ <header_from>opserv.io</header_from>
326
+ </identifiers>
327
+ <auth_results>
328
+ <dkim>
329
+ <domain>sportskeeda.com</domain>
330
+ <result>pass</result>
331
+ <selector>google</selector>
332
+ </dkim>
333
+ <spf>
334
+ <domain>sportskeeda.com</domain>
335
+ <result>pass</result>
336
+ </spf>
337
+ </auth_results>
338
+ </record>
339
+ <record>
340
+ <row>
341
+ <source_ip>209.85.220.41</source_ip>
342
+ <count>66</count>
343
+ <policy_evaluated>
344
+ <disposition>none</disposition>
345
+ <dkim>pass</dkim>
346
+ <spf>fail</spf>
347
+ </policy_evaluated>
348
+ </row>
349
+ <identifiers>
350
+ <header_from>opserv.io</header_from>
351
+ </identifiers>
352
+ <auth_results>
353
+ <dkim>
354
+ <domain>opserv.io</domain>
355
+ <result>pass</result>
356
+ <selector>s1</selector>
357
+ </dkim>
358
+ <spf>
359
+ <domain>mmprojects.co.uk</domain>
360
+ <result>pass</result>
361
+ </spf>
362
+ </auth_results>
363
+ </record>
364
+ </feedback>
@@ -0,0 +1,353 @@
1
+ <?xml version="1.0" encoding="UTF-8" ?>
2
+ <feedback>
3
+ <report_metadata>
4
+ <org_name>google.com</org_name>
5
+ <email>noreply-dmarc-support@google.com</email>
6
+ <extra_contact_info>https://support.google.com/a/answer/2466580</extra_contact_info>
7
+ <report_id>3114726731964606297</report_id>
8
+ <date_range>
9
+ <begin>1593648000</begin>
10
+ <end>1593734399</end>
11
+ </date_range>
12
+ </report_metadata>
13
+ <policy_published>
14
+ <domain>opserv.io</domain>
15
+ <adkim>r</adkim>
16
+ <aspf>r</aspf>
17
+ <p>none</p>
18
+ <sp>none</sp>
19
+ <pct>100</pct>
20
+ </policy_published>
21
+ <record>
22
+ <row>
23
+ <source_ip>209.85.220.69</source_ip>
24
+ <count>96</count>
25
+ <policy_evaluated>
26
+ <disposition>none</disposition>
27
+ <dkim>fail</dkim>
28
+ <spf>fail</spf>
29
+ <reason>
30
+ <type>local_policy</type>
31
+ <comment>arc=pass</comment>
32
+ </reason>
33
+ </policy_evaluated>
34
+ </row>
35
+ <identifiers>
36
+ <header_from>opserv.io</header_from>
37
+ </identifiers>
38
+ <auth_results>
39
+ <dkim>
40
+ <domain>condenast-com-tw.20150623.gappssmtp.com</domain>
41
+ <result>pass</result>
42
+ <selector>20150623</selector>
43
+ </dkim>
44
+ <spf>
45
+ <domain>condenast.com.tw</domain>
46
+ <result>neutral</result>
47
+ </spf>
48
+ </auth_results>
49
+ </record>
50
+ <record>
51
+ <row>
52
+ <source_ip>167.89.100.132</source_ip>
53
+ <count>11</count>
54
+ <policy_evaluated>
55
+ <disposition>none</disposition>
56
+ <dkim>pass</dkim>
57
+ <spf>pass</spf>
58
+ </policy_evaluated>
59
+ </row>
60
+ <identifiers>
61
+ <header_from>opserv.io</header_from>
62
+ </identifiers>
63
+ <auth_results>
64
+ <dkim>
65
+ <domain>opserv.io</domain>
66
+ <result>pass</result>
67
+ <selector>s1</selector>
68
+ </dkim>
69
+ <dkim>
70
+ <domain>sendgrid.info</domain>
71
+ <result>pass</result>
72
+ <selector>smtpapi</selector>
73
+ </dkim>
74
+ <spf>
75
+ <domain>alert.opserv.io</domain>
76
+ <result>pass</result>
77
+ </spf>
78
+ </auth_results>
79
+ </record>
80
+ <record>
81
+ <row>
82
+ <source_ip>167.89.100.13</source_ip>
83
+ <count>96</count>
84
+ <policy_evaluated>
85
+ <disposition>none</disposition>
86
+ <dkim>pass</dkim>
87
+ <spf>pass</spf>
88
+ </policy_evaluated>
89
+ </row>
90
+ <identifiers>
91
+ <header_from>opserv.io</header_from>
92
+ </identifiers>
93
+ <auth_results>
94
+ <dkim>
95
+ <domain>opserv.io</domain>
96
+ <result>pass</result>
97
+ <selector>s1</selector>
98
+ </dkim>
99
+ <dkim>
100
+ <domain>sendgrid.info</domain>
101
+ <result>pass</result>
102
+ <selector>smtpapi</selector>
103
+ </dkim>
104
+ <spf>
105
+ <domain>alert.opserv.io</domain>
106
+ <result>pass</result>
107
+ </spf>
108
+ </auth_results>
109
+ </record>
110
+ <record>
111
+ <row>
112
+ <source_ip>167.89.100.13</source_ip>
113
+ <count>161</count>
114
+ <policy_evaluated>
115
+ <disposition>none</disposition>
116
+ <dkim>pass</dkim>
117
+ <spf>pass</spf>
118
+ </policy_evaluated>
119
+ </row>
120
+ <identifiers>
121
+ <header_from>opserv.io</header_from>
122
+ </identifiers>
123
+ <auth_results>
124
+ <dkim>
125
+ <domain>opserv.io</domain>
126
+ <result>pass</result>
127
+ <selector>s1</selector>
128
+ </dkim>
129
+ <spf>
130
+ <domain>alert.opserv.io</domain>
131
+ <result>pass</result>
132
+ </spf>
133
+ </auth_results>
134
+ </record>
135
+ <record>
136
+ <row>
137
+ <source_ip>50.31.60.236</source_ip>
138
+ <count>28</count>
139
+ <policy_evaluated>
140
+ <disposition>none</disposition>
141
+ <dkim>pass</dkim>
142
+ <spf>pass</spf>
143
+ </policy_evaluated>
144
+ </row>
145
+ <identifiers>
146
+ <header_from>opserv.io</header_from>
147
+ </identifiers>
148
+ <auth_results>
149
+ <dkim>
150
+ <domain>opserv.io</domain>
151
+ <result>pass</result>
152
+ <selector>s1</selector>
153
+ </dkim>
154
+ <spf>
155
+ <domain>alert.opserv.io</domain>
156
+ <result>pass</result>
157
+ </spf>
158
+ </auth_results>
159
+ </record>
160
+ <record>
161
+ <row>
162
+ <source_ip>50.31.60.236</source_ip>
163
+ <count>13</count>
164
+ <policy_evaluated>
165
+ <disposition>none</disposition>
166
+ <dkim>pass</dkim>
167
+ <spf>pass</spf>
168
+ </policy_evaluated>
169
+ </row>
170
+ <identifiers>
171
+ <header_from>opserv.io</header_from>
172
+ </identifiers>
173
+ <auth_results>
174
+ <dkim>
175
+ <domain>opserv.io</domain>
176
+ <result>pass</result>
177
+ <selector>s1</selector>
178
+ </dkim>
179
+ <dkim>
180
+ <domain>sendgrid.info</domain>
181
+ <result>pass</result>
182
+ <selector>smtpapi</selector>
183
+ </dkim>
184
+ <spf>
185
+ <domain>alert.opserv.io</domain>
186
+ <result>pass</result>
187
+ </spf>
188
+ </auth_results>
189
+ </record>
190
+ <record>
191
+ <row>
192
+ <source_ip>209.85.220.41</source_ip>
193
+ <count>3</count>
194
+ <policy_evaluated>
195
+ <disposition>none</disposition>
196
+ <dkim>pass</dkim>
197
+ <spf>fail</spf>
198
+ </policy_evaluated>
199
+ </row>
200
+ <identifiers>
201
+ <header_from>opserv.io</header_from>
202
+ </identifiers>
203
+ <auth_results>
204
+ <dkim>
205
+ <domain>opserv.io</domain>
206
+ <result>pass</result>
207
+ <selector>s1</selector>
208
+ </dkim>
209
+ <spf>
210
+ <domain>hta.gr</domain>
211
+ <result>pass</result>
212
+ </spf>
213
+ </auth_results>
214
+ </record>
215
+ <record>
216
+ <row>
217
+ <source_ip>209.85.220.69</source_ip>
218
+ <count>10</count>
219
+ <policy_evaluated>
220
+ <disposition>none</disposition>
221
+ <dkim>fail</dkim>
222
+ <spf>fail</spf>
223
+ <reason>
224
+ <type>local_policy</type>
225
+ <comment>arc=pass</comment>
226
+ </reason>
227
+ </policy_evaluated>
228
+ </row>
229
+ <identifiers>
230
+ <header_from>opserv.io</header_from>
231
+ </identifiers>
232
+ <auth_results>
233
+ <dkim>
234
+ <domain>unicstay-com.20150623.gappssmtp.com</domain>
235
+ <result>pass</result>
236
+ <selector>20150623</selector>
237
+ </dkim>
238
+ <spf>
239
+ <domain>unicstay.com</domain>
240
+ <result>pass</result>
241
+ </spf>
242
+ </auth_results>
243
+ </record>
244
+ <record>
245
+ <row>
246
+ <source_ip>209.85.220.41</source_ip>
247
+ <count>3</count>
248
+ <policy_evaluated>
249
+ <disposition>none</disposition>
250
+ <dkim>pass</dkim>
251
+ <spf>fail</spf>
252
+ </policy_evaluated>
253
+ </row>
254
+ <identifiers>
255
+ <header_from>opserv.io</header_from>
256
+ </identifiers>
257
+ <auth_results>
258
+ <dkim>
259
+ <domain>opserv.io</domain>
260
+ <result>pass</result>
261
+ <selector>s1</selector>
262
+ </dkim>
263
+ <dkim>
264
+ <domain>sendgrid.info</domain>
265
+ <result>pass</result>
266
+ <selector>smtpapi</selector>
267
+ </dkim>
268
+ <spf>
269
+ <domain>gmail.com</domain>
270
+ <result>pass</result>
271
+ </spf>
272
+ </auth_results>
273
+ </record>
274
+ <record>
275
+ <row>
276
+ <source_ip>167.89.100.132</source_ip>
277
+ <count>31</count>
278
+ <policy_evaluated>
279
+ <disposition>none</disposition>
280
+ <dkim>pass</dkim>
281
+ <spf>pass</spf>
282
+ </policy_evaluated>
283
+ </row>
284
+ <identifiers>
285
+ <header_from>opserv.io</header_from>
286
+ </identifiers>
287
+ <auth_results>
288
+ <dkim>
289
+ <domain>opserv.io</domain>
290
+ <result>pass</result>
291
+ <selector>s1</selector>
292
+ </dkim>
293
+ <spf>
294
+ <domain>alert.opserv.io</domain>
295
+ <result>pass</result>
296
+ </spf>
297
+ </auth_results>
298
+ </record>
299
+ <record>
300
+ <row>
301
+ <source_ip>209.85.220.69</source_ip>
302
+ <count>6</count>
303
+ <policy_evaluated>
304
+ <disposition>none</disposition>
305
+ <dkim>fail</dkim>
306
+ <spf>fail</spf>
307
+ <reason>
308
+ <type>local_policy</type>
309
+ <comment>arc=pass</comment>
310
+ </reason>
311
+ </policy_evaluated>
312
+ </row>
313
+ <identifiers>
314
+ <header_from>opserv.io</header_from>
315
+ </identifiers>
316
+ <auth_results>
317
+ <dkim>
318
+ <domain>sportskeeda.com</domain>
319
+ <result>pass</result>
320
+ <selector>google</selector>
321
+ </dkim>
322
+ <spf>
323
+ <domain>sportskeeda.com</domain>
324
+ <result>pass</result>
325
+ </spf>
326
+ </auth_results>
327
+ </record>
328
+ <record>
329
+ <row>
330
+ <source_ip>209.85.220.41</source_ip>
331
+ <count>66</count>
332
+ <policy_evaluated>
333
+ <disposition>none</disposition>
334
+ <dkim>pass</dkim>
335
+ <spf>fail</spf>
336
+ </policy_evaluated>
337
+ </row>
338
+ <identifiers>
339
+ <header_from>opserv.io</header_from>
340
+ </identifiers>
341
+ <auth_results>
342
+ <dkim>
343
+ <domain>opserv.io</domain>
344
+ <result>pass</result>
345
+ <selector>s1</selector>
346
+ </dkim>
347
+ <spf>
348
+ <domain>mmprojects.co.uk</domain>
349
+ <result>pass</result>
350
+ </spf>
351
+ </auth_results>
352
+ </record>
353
+ </feedback>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dmarcator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Noah Berman
@@ -30,6 +30,34 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: 3.0.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: nokogiri
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '1.10'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '1.10'
47
+ - !ruby/object:Gem::Dependency
48
+ name: bigdecimal
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '1.0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '1.0'
33
61
  description: A hardened DMARC record parser and query client, forked from the Trail
34
62
  of Bits 'dmarc' gem.
35
63
  email:
@@ -47,13 +75,23 @@ files:
47
75
  - lib/dmarcator/parser.rb
48
76
  - lib/dmarcator/query.rb
49
77
  - lib/dmarcator/record.rb
78
+ - lib/dmarcator/report.rb
79
+ - lib/dmarcator/report/auth_result.rb
80
+ - lib/dmarcator/report/metadata.rb
81
+ - lib/dmarcator/report/policy.rb
82
+ - lib/dmarcator/report/reason.rb
83
+ - lib/dmarcator/report/record.rb
50
84
  - lib/dmarcator/uri.rb
51
85
  - lib/dmarcator/version.rb
86
+ - lib/dmarcator/xml_helper.rb
52
87
  - spec/data/alexa.csv
53
88
  - spec/dmarcator_spec.rb
54
89
  - spec/parser_spec.rb
55
90
  - spec/record_spec.rb
91
+ - spec/report_spec.rb
56
92
  - spec/spec_helper.rb
93
+ - spec/support/complete.xml
94
+ - spec/support/google.xml
57
95
  - spec/uri_spec.rb
58
96
  homepage: https://github.com/bermannoah/dmarc
59
97
  licenses:
@@ -84,5 +122,8 @@ test_files:
84
122
  - spec/dmarcator_spec.rb
85
123
  - spec/parser_spec.rb
86
124
  - spec/record_spec.rb
125
+ - spec/report_spec.rb
87
126
  - spec/spec_helper.rb
127
+ - spec/support/complete.xml
128
+ - spec/support/google.xml
88
129
  - spec/uri_spec.rb