dmarcator 0.3.1 → 0.4.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 +4 -4
- data/ChangeLog.md +9 -0
- data/README.md +23 -1
- data/lib/dmarcator/exceptions.rb +2 -0
- data/lib/dmarcator/parser.rb +50 -7
- data/lib/dmarcator/query.rb +11 -2
- data/lib/dmarcator/record.rb +13 -10
- data/lib/dmarcator/report/policy.rb +1 -1
- data/lib/dmarcator/report.rb +11 -3
- data/lib/dmarcator/uri.rb +3 -1
- data/lib/dmarcator/version.rb +3 -1
- data/lib/dmarcator/xml_helper.rb +6 -1
- data/lib/dmarcator.rb +2 -0
- metadata +2 -20
- data/spec/data/alexa.csv +0 -500
- data/spec/dmarcator_spec.rb +0 -128
- data/spec/parser_spec.rb +0 -413
- data/spec/record_spec.rb +0 -229
- data/spec/report_spec.rb +0 -108
- data/spec/spec_helper.rb +0 -11
- data/spec/support/complete.xml +0 -364
- data/spec/support/google.xml +0 -353
- data/spec/uri_spec.rb +0 -187
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c4ed90f1362c3a69141cadeb2e7360b71a7f86c18f74f433990d0760d2bab0d
|
|
4
|
+
data.tar.gz: 63d27b1fc2acbf70627757d7a5604c1cf471e2d2f7dc71c80e8e221f663f3a70
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 92f2851179be4e2ee8eaaa542afa87ab6efcb2aed546161001778efda5f41579a78a552b1a30010ff625f877f94cbf08c7b7506ffbdacc06fc1d68c9753ef415
|
|
7
|
+
data.tar.gz: 10ad6dc240c07a14705ac06c83388c0981b4476ae03c980323b0d288f69693ea552218b94615524c9c7b8692b521a316157fd0c3fed87f96726acf2dfc7ce4b8
|
data/ChangeLog.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
### 0.4.0 / 2026-10-26
|
|
2
|
+
|
|
3
|
+
* Add `strict: true` parsing mode to reject unknown tags and invalid tag values.
|
|
4
|
+
* Return `nil` for non-numeric report integers (`count`, `pct`) instead of coercing them to `0`.
|
|
5
|
+
* Disable network access (`NONET`) explicitly when parsing XML reports.
|
|
6
|
+
* Reject DNS labels with leading or trailing hyphens.
|
|
7
|
+
* Set explicit DNS query timeouts.
|
|
8
|
+
* Reject XML reports larger than 25 MiB.
|
|
9
|
+
|
|
1
10
|
### 0.3.1 / 2026-10-26
|
|
2
11
|
|
|
3
12
|
* Update the gemspec maintainer metadata to list only the fork's author.
|
data/README.md
CHANGED
|
@@ -65,7 +65,7 @@ record.aspf
|
|
|
65
65
|
# => :r
|
|
66
66
|
|
|
67
67
|
record.fo
|
|
68
|
-
# => ["
|
|
68
|
+
# => ["1"]
|
|
69
69
|
|
|
70
70
|
record.pct
|
|
71
71
|
# => 100
|
|
@@ -93,6 +93,16 @@ record = DMARCator::Record.query("twitter.com")
|
|
|
93
93
|
# => #<DMARCator::Record ... @p=:reject, @rua=[...], ...>
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
## Strict parsing
|
|
97
|
+
|
|
98
|
+
By default, unknown tags and tags with invalid values are ignored. Pass
|
|
99
|
+
`strict: true` to reject them instead:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
DMARCator::Record.parse("v=DMARC1; p=none; pct=abc", strict: true)
|
|
103
|
+
# => DMARCator::InvalidRecord: invalid DMARC tag value: "pct=abc"
|
|
104
|
+
```
|
|
105
|
+
|
|
96
106
|
## Parsing XML reports
|
|
97
107
|
|
|
98
108
|
Parse a DMARC aggregate/forensic report:
|
|
@@ -113,6 +123,18 @@ report.records.first.source_ip
|
|
|
113
123
|
# => "209.85.220.69"
|
|
114
124
|
```
|
|
115
125
|
|
|
126
|
+
## Security
|
|
127
|
+
|
|
128
|
+
DMARC records and XML reports are treated as untrusted input:
|
|
129
|
+
|
|
130
|
+
* XML reports are parsed with network access disabled (`NONET`), so external
|
|
131
|
+
entities are never resolved or fetched.
|
|
132
|
+
* Reports larger than 25 MiB are rejected before parsing.
|
|
133
|
+
* A warning is emitted at load time if the bundled `resolv` gem is affected by
|
|
134
|
+
CVE-2026-80212/CVE-2026-80213.
|
|
135
|
+
* Domain names are validated before resolution (labels ≤ 63 octets, total ≤
|
|
136
|
+
248 octets, no leading/trailing hyphens).
|
|
137
|
+
|
|
116
138
|
## Requirements
|
|
117
139
|
|
|
118
140
|
* [Ruby] >= 3.3.13
|
data/lib/dmarcator/exceptions.rb
CHANGED
data/lib/dmarcator/parser.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'dmarcator/exceptions'
|
|
2
4
|
require 'dmarcator/uri'
|
|
3
5
|
|
|
@@ -45,8 +47,14 @@ module DMARCator
|
|
|
45
47
|
|
|
46
48
|
def self.tag_rule(name,tag,&block)
|
|
47
49
|
rule(:"dmarc_#{name}") do
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
(
|
|
51
|
+
str(tag) >> wsp? >> str('=') >> wsp? >>
|
|
52
|
+
instance_eval(&block).as(tag.to_sym)
|
|
53
|
+
) |
|
|
54
|
+
(
|
|
55
|
+
str(tag) >> wsp? >> str('=') >> wsp? >>
|
|
56
|
+
match["^; \t"].repeat(1)
|
|
57
|
+
).as(:invalid_tag)
|
|
50
58
|
end
|
|
51
59
|
end
|
|
52
60
|
|
|
@@ -81,8 +89,7 @@ module DMARCator
|
|
|
81
89
|
tag_rule(:adkim, 'adkim') { match['rs'] }
|
|
82
90
|
tag_rule(:aspf, 'aspf') { match['rs'] }
|
|
83
91
|
|
|
84
|
-
rule(:unknown_tag) { match["^; \t"].repeat(1) }
|
|
85
|
-
rule(:unknown_value) { match["^; \t"].repeat(1) }
|
|
92
|
+
rule(:unknown_tag) { match["^; \t"].repeat(1).as(:unknown_tag) }
|
|
86
93
|
|
|
87
94
|
rule(:dmarc_uri) do
|
|
88
95
|
mailto_uri.as(:uri) >> (
|
|
@@ -93,7 +100,8 @@ module DMARCator
|
|
|
93
100
|
end
|
|
94
101
|
|
|
95
102
|
rule(:mailto_uri) do
|
|
96
|
-
|
|
103
|
+
match['mM'] >> match['aA'] >> match['iI'] >> match['lL'] >> match['tT'] >> match['oO'] >>
|
|
104
|
+
str(':') >> mailto_address
|
|
97
105
|
end
|
|
98
106
|
|
|
99
107
|
rule(:mailto_address) do
|
|
@@ -176,11 +184,33 @@ module DMARCator
|
|
|
176
184
|
# @param [String] record
|
|
177
185
|
# The raw DMARC record to parse.
|
|
178
186
|
#
|
|
187
|
+
# @param [Boolean] strict (false)
|
|
188
|
+
# When `true`, raise on unknown tags or invalid tag values instead of
|
|
189
|
+
# silently ignoring them.
|
|
190
|
+
#
|
|
179
191
|
# @return [Hash{Symbol => Object}]
|
|
180
192
|
# The Hash of tags within the record.
|
|
181
193
|
#
|
|
182
|
-
|
|
183
|
-
|
|
194
|
+
# @raise [InvalidRecord]
|
|
195
|
+
# The record is malformed, or (in strict mode) contains an unknown tag
|
|
196
|
+
# or invalid tag value.
|
|
197
|
+
#
|
|
198
|
+
def self.parse(record, strict: false)
|
|
199
|
+
tree = new.parse(record)
|
|
200
|
+
|
|
201
|
+
if strict
|
|
202
|
+
tree.each do |node|
|
|
203
|
+
next unless ignored?(node)
|
|
204
|
+
|
|
205
|
+
if node.key?(:unknown_tag)
|
|
206
|
+
raise InvalidRecord, "unknown DMARC tag: #{Transform.truncate(node[:unknown_tag].to_s).inspect}"
|
|
207
|
+
else
|
|
208
|
+
raise InvalidRecord, "invalid DMARC tag value: #{Transform.truncate(node[:invalid_tag].to_s).inspect}"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
tags = Transform.new.apply(tree.reject { |node| ignored?(node) })
|
|
184
214
|
hash = {}
|
|
185
215
|
|
|
186
216
|
tags.each do |tag|
|
|
@@ -198,6 +228,19 @@ module DMARCator
|
|
|
198
228
|
return hash
|
|
199
229
|
end
|
|
200
230
|
|
|
231
|
+
#
|
|
232
|
+
# Determines whether a parse-tree node represents a tag that the lenient
|
|
233
|
+
# parser dropped because it was unknown or had an invalid value.
|
|
234
|
+
#
|
|
235
|
+
# @param [Object] node
|
|
236
|
+
# The parse-tree node.
|
|
237
|
+
#
|
|
238
|
+
# @return [Boolean]
|
|
239
|
+
#
|
|
240
|
+
def self.ignored?(node)
|
|
241
|
+
node.is_a?(Hash) && (node.key?(:unknown_tag) || node.key?(:invalid_tag))
|
|
242
|
+
end
|
|
243
|
+
|
|
201
244
|
#
|
|
202
245
|
# Validates the parsed tags.
|
|
203
246
|
#
|
data/lib/dmarcator/query.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'resolv'
|
|
2
4
|
require 'rubygems'
|
|
3
5
|
|
|
@@ -10,6 +12,11 @@ module DMARCator
|
|
|
10
12
|
Gem::Requirement.new('>= 0.4.0', '< 0.7.2')
|
|
11
13
|
].freeze
|
|
12
14
|
|
|
15
|
+
#
|
|
16
|
+
# DNS query timeouts in seconds, tried in order.
|
|
17
|
+
#
|
|
18
|
+
DNS_TIMEOUTS = [2, 4, 8].freeze
|
|
19
|
+
|
|
13
20
|
#
|
|
14
21
|
# Determines the version of the loaded `resolv` gem, if available.
|
|
15
22
|
#
|
|
@@ -56,7 +63,7 @@ module DMARCator
|
|
|
56
63
|
!domain.start_with?('.') &&
|
|
57
64
|
!domain.include?('..') &&
|
|
58
65
|
!domain.match?(/[^A-Za-z0-9._-]/) &&
|
|
59
|
-
domain.split('.').all? { |label| label.length <= 63 }
|
|
66
|
+
domain.split('.').all? { |label| label.length <= 63 && !label.start_with?('-') && !label.end_with?('-') }
|
|
60
67
|
raise ArgumentError, "invalid domain: #{domain.inspect}"
|
|
61
68
|
end
|
|
62
69
|
|
|
@@ -70,7 +77,7 @@ module DMARCator
|
|
|
70
77
|
# The domain to query DMARC for.
|
|
71
78
|
#
|
|
72
79
|
# @param [Resolv::DNS] resolver
|
|
73
|
-
# The resolver to use.
|
|
80
|
+
# The resolver to use. Its timeouts are set to {DNS_TIMEOUTS}.
|
|
74
81
|
#
|
|
75
82
|
# @return [String, nil]
|
|
76
83
|
# The domain's DMARC record or `nil` if none exists.
|
|
@@ -82,6 +89,8 @@ module DMARCator
|
|
|
82
89
|
def self.query(domain,resolver=Resolv::DNS.new)
|
|
83
90
|
validate_domain(domain)
|
|
84
91
|
|
|
92
|
+
resolver.timeouts = DNS_TIMEOUTS
|
|
93
|
+
|
|
85
94
|
host = "_dmarc.#{domain}"
|
|
86
95
|
|
|
87
96
|
begin
|
data/lib/dmarcator/record.rb
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'dmarcator/query'
|
|
2
4
|
require 'dmarcator/parser'
|
|
3
5
|
require 'dmarcator/exceptions'
|
|
4
6
|
|
|
5
7
|
require 'resolv'
|
|
6
|
-
require 'uri'
|
|
7
8
|
|
|
8
9
|
module DMARCator
|
|
9
10
|
class Record
|
|
@@ -18,7 +19,7 @@ module DMARCator
|
|
|
18
19
|
# @return [Array<Uri>]
|
|
19
20
|
attr_reader :rua
|
|
20
21
|
|
|
21
|
-
# `
|
|
22
|
+
# `ruf` field.
|
|
22
23
|
#
|
|
23
24
|
# @return [Array<Uri>]
|
|
24
25
|
attr_reader :ruf
|
|
@@ -76,7 +77,7 @@ module DMARCator
|
|
|
76
77
|
end
|
|
77
78
|
|
|
78
79
|
#
|
|
79
|
-
# Determines if the `sp=` field was specified
|
|
80
|
+
# Determines if the `sp=` field was specified.
|
|
80
81
|
#
|
|
81
82
|
# @return [Boolean]
|
|
82
83
|
#
|
|
@@ -160,7 +161,7 @@ module DMARCator
|
|
|
160
161
|
end
|
|
161
162
|
|
|
162
163
|
#
|
|
163
|
-
# Determines if the `p=` field was specified
|
|
164
|
+
# Determines if the `p=` field was specified.
|
|
164
165
|
#
|
|
165
166
|
# @return [Boolean]
|
|
166
167
|
#
|
|
@@ -234,7 +235,7 @@ module DMARCator
|
|
|
234
235
|
end
|
|
235
236
|
|
|
236
237
|
#
|
|
237
|
-
# Determines if the `rua=` field was specified
|
|
238
|
+
# Determines if the `rua=` field was specified.
|
|
238
239
|
#
|
|
239
240
|
# @return [Boolean]
|
|
240
241
|
#
|
|
@@ -245,7 +246,7 @@ module DMARCator
|
|
|
245
246
|
end
|
|
246
247
|
|
|
247
248
|
#
|
|
248
|
-
# Determines if the `ruf=` field was specified
|
|
249
|
+
# Determines if the `ruf=` field was specified.
|
|
249
250
|
#
|
|
250
251
|
# @return [Boolean]
|
|
251
252
|
#
|
|
@@ -261,6 +262,10 @@ module DMARCator
|
|
|
261
262
|
# @param [String] record
|
|
262
263
|
# The raw DMARC record.
|
|
263
264
|
#
|
|
265
|
+
# @param [Boolean] strict (false)
|
|
266
|
+
# When `true`, raise on unknown tags or invalid tag values instead of
|
|
267
|
+
# silently ignoring them.
|
|
268
|
+
#
|
|
264
269
|
# @return [Record]
|
|
265
270
|
# The parsed DMARC record.
|
|
266
271
|
#
|
|
@@ -271,8 +276,8 @@ module DMARCator
|
|
|
271
276
|
#
|
|
272
277
|
# @api public
|
|
273
278
|
#
|
|
274
|
-
def self.parse(record)
|
|
275
|
-
attributes = Parser.parse(record)
|
|
279
|
+
def self.parse(record, strict: false)
|
|
280
|
+
attributes = Parser.parse(record, strict: strict)
|
|
276
281
|
|
|
277
282
|
unless attributes[:p]
|
|
278
283
|
raise InvalidRecord, "DMARC record is missing the required 'p' tag"
|
|
@@ -281,8 +286,6 @@ module DMARCator
|
|
|
281
286
|
new(attributes)
|
|
282
287
|
rescue Parslet::ParseFailed => error
|
|
283
288
|
raise InvalidRecord.new(truncate_error_message(error.message), error.cause)
|
|
284
|
-
rescue URI::Error => error
|
|
285
|
-
raise InvalidRecord.new(truncate_error_message(error.message), error)
|
|
286
289
|
end
|
|
287
290
|
|
|
288
291
|
#
|
data/lib/dmarcator/report.rb
CHANGED
|
@@ -14,12 +14,15 @@ module DMARCator
|
|
|
14
14
|
#
|
|
15
15
|
# Represents a parsed DMARC aggregate/forensic XML report.
|
|
16
16
|
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
17
|
+
# Parsing disables network access (NONET) so external entities are never
|
|
18
|
+
# resolved or fetched; treat report XML as untrusted input.
|
|
19
19
|
#
|
|
20
20
|
class Report
|
|
21
21
|
include XmlHelper
|
|
22
22
|
|
|
23
|
+
# Maximum accepted report size in bytes (25 MiB).
|
|
24
|
+
MAX_REPORT_BYTES = 25 * 1024 * 1024
|
|
25
|
+
|
|
23
26
|
#
|
|
24
27
|
# Initializes the report from an XML string.
|
|
25
28
|
#
|
|
@@ -28,7 +31,12 @@ module DMARCator
|
|
|
28
31
|
#
|
|
29
32
|
def initialize(xml)
|
|
30
33
|
@xml = xml
|
|
31
|
-
|
|
34
|
+
|
|
35
|
+
if xml.is_a?(String) && xml.bytesize > MAX_REPORT_BYTES
|
|
36
|
+
raise InvalidReport, "DMARC XML report exceeds maximum size of #{MAX_REPORT_BYTES} bytes"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
@node = Nokogiri::XML(xml) { |config| config.nonet }
|
|
32
40
|
|
|
33
41
|
unless @node.errors.empty?
|
|
34
42
|
raise InvalidReport, "invalid DMARC XML report: #{@node.errors.first}"
|
data/lib/dmarcator/uri.rb
CHANGED
data/lib/dmarcator/version.rb
CHANGED
data/lib/dmarcator/xml_helper.rb
CHANGED
|
@@ -48,7 +48,12 @@ module DMARCator
|
|
|
48
48
|
#
|
|
49
49
|
def get_int(xpath)
|
|
50
50
|
value = get_text(xpath)
|
|
51
|
-
|
|
51
|
+
return nil if value.nil? || value.empty?
|
|
52
|
+
|
|
53
|
+
digits = value.strip
|
|
54
|
+
return nil unless digits.match?(/\A\d+\z/)
|
|
55
|
+
|
|
56
|
+
digits.to_i
|
|
52
57
|
end
|
|
53
58
|
end
|
|
54
59
|
end
|
data/lib/dmarcator.rb
CHANGED
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.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Noah Berman
|
|
@@ -82,15 +82,6 @@ files:
|
|
|
82
82
|
- lib/dmarcator/uri.rb
|
|
83
83
|
- lib/dmarcator/version.rb
|
|
84
84
|
- lib/dmarcator/xml_helper.rb
|
|
85
|
-
- spec/data/alexa.csv
|
|
86
|
-
- spec/dmarcator_spec.rb
|
|
87
|
-
- spec/parser_spec.rb
|
|
88
|
-
- spec/record_spec.rb
|
|
89
|
-
- spec/report_spec.rb
|
|
90
|
-
- spec/spec_helper.rb
|
|
91
|
-
- spec/support/complete.xml
|
|
92
|
-
- spec/support/google.xml
|
|
93
|
-
- spec/uri_spec.rb
|
|
94
85
|
homepage: https://github.com/bermannoah/dmarc
|
|
95
86
|
licenses:
|
|
96
87
|
- MIT
|
|
@@ -115,13 +106,4 @@ requirements: []
|
|
|
115
106
|
rubygems_version: 4.0.23
|
|
116
107
|
specification_version: 4
|
|
117
108
|
summary: DMARC record parser and query client
|
|
118
|
-
test_files:
|
|
119
|
-
- spec/data/alexa.csv
|
|
120
|
-
- spec/dmarcator_spec.rb
|
|
121
|
-
- spec/parser_spec.rb
|
|
122
|
-
- spec/record_spec.rb
|
|
123
|
-
- spec/report_spec.rb
|
|
124
|
-
- spec/spec_helper.rb
|
|
125
|
-
- spec/support/complete.xml
|
|
126
|
-
- spec/support/google.xml
|
|
127
|
-
- spec/uri_spec.rb
|
|
109
|
+
test_files: []
|