dmarcator 0.10.0 → 0.12.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 +28 -0
- data/README.md +56 -6
- data/lib/dmarcator/config.rb +105 -0
- data/lib/dmarcator/parser.rb +4 -0
- data/lib/dmarcator/query.rb +11 -2
- data/lib/dmarcator/record.rb +2 -0
- data/lib/dmarcator/report/auth_result.rb +11 -11
- data/lib/dmarcator/report/metadata.rb +15 -15
- data/lib/dmarcator/report/policy.rb +15 -15
- data/lib/dmarcator/report/reason.rb +5 -5
- data/lib/dmarcator/report/row.rb +17 -18
- data/lib/dmarcator/report.rb +32 -21
- data/lib/dmarcator/version.rb +1 -1
- data/lib/dmarcator/xml_helper.rb +2 -0
- metadata +2 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b6c289ca56b406c5f591f414ada16407028e732310ad9b4c784e2f231469828a
|
|
4
|
+
data.tar.gz: 24f260da56f98e045d262bd5fcbe3ee9d3f8be1b1bdd310e26e8b9b4b74f1021
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f88871eab61d67fcccbe7ac0e6f0dfc8f97160592d97e69e58a69d9d99b069ec02e70b9126a0dc7ae114c9e50d599a0ca014ba9b02fc2514fab7db97c4bd4a08
|
|
7
|
+
data.tar.gz: 2d10f64bd4cd096f54ba2a6372a07c582dc3aad581fe556a3228be4378ba09bd9e3e5ba57c94a4cd98acc4b87899ded063ab9b886b73390267659754713297bb
|
data/ChangeLog.md
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
### 0.12.0 / 2026-10-29
|
|
2
|
+
|
|
3
|
+
Breaking changes, taken before 1.0:
|
|
4
|
+
|
|
5
|
+
* `Report#version` returns the published value as a String instead of a `BigDecimal`, and the `bigdecimal` runtime dependency is gone. A non-numeric version still yields `nil`.
|
|
6
|
+
* Parsed report objects (`Report::Metadata`, `Policy`, `Row`, `AuthResult`, `Reason`) are read-only. Their attributes were previously writable, which allowed a parsed report to be mutated into a state that never came from the document.
|
|
7
|
+
* `Report.max_report_bytes` now delegates to `DMARCator.config.max_report_bytes`. The class-level accessors remain, so existing callers keep working; `Config#max_report_bytes` defaults to 25 MiB and rejects values that are not a positive Integer or nil.
|
|
8
|
+
|
|
9
|
+
Other changes:
|
|
10
|
+
|
|
11
|
+
* `DMARCator::XmlHelper` methods are private to including classes, `Record.truncate_error_message` is private, and `DMARCator::Parser` is documented as `@api private`.
|
|
12
|
+
* Add an API-contract spec that pins the public surface of every public class, so additions and removals are deliberate.
|
|
13
|
+
* Add `SECURITY.md` and document the stability, thread-safety and deprecation policies in the README.
|
|
14
|
+
|
|
15
|
+
Migration:
|
|
16
|
+
|
|
17
|
+
| Before | After |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `report.version == BigDecimal("0.1")` | `report.version == "0.1"` |
|
|
20
|
+
| `report.policy.pct = 50` | assign nothing; parsed values are read-only |
|
|
21
|
+
| `Report.max_report_bytes = n` | `DMARCator.configure { |c| c.max_report_bytes = n }` |
|
|
22
|
+
|
|
23
|
+
### 0.11.0 / 2026-10-29
|
|
24
|
+
|
|
25
|
+
* Add `DMARCator.configure` for process-wide defaults, so a resolver can be set from code instead of only through the `DMARCATOR_DNS` environment variables. A configured resolver takes precedence over the environment, and `DMARCator.reset_configuration!` discards it.
|
|
26
|
+
* A configured resolver may be a callable, in which case it is invoked for each lookup. This lets threaded applications use one resolver per lookup instead of sharing a `Resolv::DNS`, which does not serialise concurrent lookups.
|
|
27
|
+
* Run CI on macOS 15 as well as Ubuntu 24.04, covering the Socket/OpenSSL and Net::HTTP paths used by the DoT and DoH transports.
|
|
28
|
+
|
|
1
29
|
### 0.10.0 / 2026-10-29
|
|
2
30
|
|
|
3
31
|
Breaking changes, taken before 1.0 so the release does not carry them:
|
data/README.md
CHANGED
|
@@ -193,6 +193,32 @@ helper returning "the" record would hide exactly those conditions. DKIM is also
|
|
|
193
193
|
per-selector and selectors cannot be enumerated, so any convenience method would
|
|
194
194
|
have to take a selector and would only build the name above.
|
|
195
195
|
|
|
196
|
+
## Configuration
|
|
197
|
+
|
|
198
|
+
Process-wide defaults can be set from code, and take precedence over the
|
|
199
|
+
`DMARCATOR_DNS` environment variables:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
DMARCator.configure do |config|
|
|
203
|
+
config.resolver = DMARCator::Resolver::DoH.new(url: "https://dns.example/dns-query")
|
|
204
|
+
config.max_report_bytes = 5 * 1024 * 1024
|
|
205
|
+
end
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
A resolver object is shared by every thread, and `Resolv::DNS` does not
|
|
209
|
+
serialise concurrent lookups, so threaded applications should configure a
|
|
210
|
+
factory instead:
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
DMARCator.configure do |config|
|
|
214
|
+
config.resolver = -> { Resolv::DNS.new }
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`DMARCator.reset_configuration!` discards configured values, which is useful in
|
|
219
|
+
tests. Passing `resolver:` to a lookup still overrides both the configured
|
|
220
|
+
default and the environment.
|
|
221
|
+
|
|
196
222
|
## Strict parsing
|
|
197
223
|
|
|
198
224
|
By default, unknown tags and tags with invalid values are ignored. Pass
|
|
@@ -211,7 +237,7 @@ Parse a DMARC aggregate/forensic report:
|
|
|
211
237
|
report = DMARCator::Report.new(xml_string)
|
|
212
238
|
|
|
213
239
|
report.version
|
|
214
|
-
# => 0.
|
|
240
|
+
# => "0.1"
|
|
215
241
|
|
|
216
242
|
report.metadata.org_name
|
|
217
243
|
# => "google.com"
|
|
@@ -262,18 +288,44 @@ resolver = DMARCator::Resolver::DoH.new(url: "https://dns.example/dns-query")
|
|
|
262
288
|
record = DMARCator::Record.lookup("example.com", resolver: resolver)
|
|
263
289
|
```
|
|
264
290
|
|
|
291
|
+
## Stability
|
|
292
|
+
|
|
293
|
+
The public API is pinned by a contract spec in the test suite: the module
|
|
294
|
+
methods, constants and per-class public methods it asserts are the supported
|
|
295
|
+
surface. Anything marked `@api private` in the documentation — the parser, the
|
|
296
|
+
XML helper, the internal resolver plumbing — may change in a minor release.
|
|
297
|
+
|
|
298
|
+
### Thread safety
|
|
299
|
+
|
|
300
|
+
* `DMARCator::Record`, `DMARCator::Report` and the parsed value objects are
|
|
301
|
+
read-only after construction and safe to share between threads.
|
|
302
|
+
* The memoized parser and transform are shared, and safe for concurrent use.
|
|
303
|
+
* Resolvers are not necessarily safe for concurrent lookups: `Resolv::DNS` does
|
|
304
|
+
not serialise queries. Use one resolver per thread, or configure a factory
|
|
305
|
+
with `DMARCator.configure` so each lookup builds its own.
|
|
306
|
+
* `DMARCator.configure` mutates process-wide state. Call it during boot rather
|
|
307
|
+
than per request.
|
|
308
|
+
|
|
309
|
+
### Deprecations
|
|
310
|
+
|
|
311
|
+
A method is deprecated in a minor release by documentation only — no runtime
|
|
312
|
+
warnings, so nothing is added to application logs. Deprecated methods are
|
|
313
|
+
removed in the next major release.
|
|
314
|
+
|
|
265
315
|
## Security
|
|
266
316
|
|
|
267
|
-
DMARC records and XML reports are treated as untrusted input
|
|
317
|
+
DMARC records and XML reports are treated as untrusted input. See
|
|
318
|
+
[SECURITY.md](https://github.com/bermannoah/dmarcator/blob/main/SECURITY.md)
|
|
319
|
+
for the reporting policy and scope.
|
|
268
320
|
|
|
269
321
|
* XML reports are parsed with network access disabled (`NONET`), so external
|
|
270
322
|
entities are never resolved or fetched.
|
|
271
|
-
* Reports larger than `DMARCator
|
|
323
|
+
* Reports larger than `DMARCator.config.max_report_bytes` (25 MiB by
|
|
272
324
|
default) are rejected before parsing. Parsing builds a DOM roughly 13× the
|
|
273
325
|
input size, so lower the limit to bound memory:
|
|
274
326
|
|
|
275
327
|
```ruby
|
|
276
|
-
DMARCator
|
|
328
|
+
DMARCator.configure { |config| config.max_report_bytes = 5 * 1024 * 1024 }
|
|
277
329
|
```
|
|
278
330
|
* A warning is emitted at load time if the bundled `resolv` gem is affected by
|
|
279
331
|
CVE-2026-80212/CVE-2026-80213.
|
|
@@ -291,7 +343,6 @@ DMARC records and XML reports are treated as untrusted input:
|
|
|
291
343
|
* [Ruby] >= 3.3.13
|
|
292
344
|
* [parslet] >= 1.0.0, < 3.0.0
|
|
293
345
|
* [nokogiri] >= 1.10
|
|
294
|
-
* [bigdecimal] >= 1.0
|
|
295
346
|
|
|
296
347
|
## Testing
|
|
297
348
|
|
|
@@ -361,5 +412,4 @@ created by D Salko and released under the MIT License.
|
|
|
361
412
|
[Ruby]: https://www.ruby-lang.org
|
|
362
413
|
[parslet]: https://kschiess.github.io/parslet/
|
|
363
414
|
[nokogiri]: https://nokogiri.org
|
|
364
|
-
[bigdecimal]: https://github.com/ruby/bigdecimal
|
|
365
415
|
[LICENSE.txt]: LICENSE.txt
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DMARCator
|
|
4
|
+
#
|
|
5
|
+
# Process-wide defaults.
|
|
6
|
+
#
|
|
7
|
+
# @since 0.11.0
|
|
8
|
+
#
|
|
9
|
+
class Config
|
|
10
|
+
# Default maximum accepted report size in bytes (25 MiB).
|
|
11
|
+
#
|
|
12
|
+
# Parsing a report builds a DOM roughly 13x the input size, so this bounds
|
|
13
|
+
# the memory a hostile report can cost.
|
|
14
|
+
#
|
|
15
|
+
# @since 0.12.0
|
|
16
|
+
#
|
|
17
|
+
DEFAULT_MAX_REPORT_BYTES = 25 * 1024 * 1024
|
|
18
|
+
|
|
19
|
+
#
|
|
20
|
+
# The default resolver used when a lookup is not given one.
|
|
21
|
+
#
|
|
22
|
+
# Either a resolver ({Resolv::DNS}, {Resolver::DoH}, {Resolver::DoT}) or a
|
|
23
|
+
# callable returning one. Prefer a callable in threaded applications: a
|
|
24
|
+
# resolver object is shared by every thread, and `Resolv::DNS` does not
|
|
25
|
+
# serialise concurrent lookups.
|
|
26
|
+
#
|
|
27
|
+
# @return [Object, #call, nil]
|
|
28
|
+
#
|
|
29
|
+
attr_accessor :resolver
|
|
30
|
+
|
|
31
|
+
#
|
|
32
|
+
# The maximum accepted report size in bytes.
|
|
33
|
+
#
|
|
34
|
+
# Defaults to {DEFAULT_MAX_REPORT_BYTES}. Lower it to bound memory for
|
|
35
|
+
# reports from untrusted sources.
|
|
36
|
+
#
|
|
37
|
+
# @return [Integer]
|
|
38
|
+
#
|
|
39
|
+
def max_report_bytes
|
|
40
|
+
@max_report_bytes || DEFAULT_MAX_REPORT_BYTES
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
#
|
|
44
|
+
# Sets the maximum accepted report size in bytes.
|
|
45
|
+
#
|
|
46
|
+
# @param [Integer, nil] value
|
|
47
|
+
# A positive number of bytes, or `nil` to restore the default.
|
|
48
|
+
#
|
|
49
|
+
# @raise [ArgumentError]
|
|
50
|
+
# The value is not a positive Integer or nil.
|
|
51
|
+
#
|
|
52
|
+
def max_report_bytes=(value)
|
|
53
|
+
unless value.nil? || (value.is_a?(Integer) && value.positive?)
|
|
54
|
+
raise ArgumentError,
|
|
55
|
+
"max_report_bytes must be a positive Integer or nil, got #{value.inspect}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
@max_report_bytes = value
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
#
|
|
63
|
+
# The process-wide configuration.
|
|
64
|
+
#
|
|
65
|
+
# @return [Config]
|
|
66
|
+
#
|
|
67
|
+
# @since 0.11.0
|
|
68
|
+
#
|
|
69
|
+
def self.config
|
|
70
|
+
@config ||= Config.new
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
#
|
|
74
|
+
# Configures process-wide defaults.
|
|
75
|
+
#
|
|
76
|
+
# DMARCator.configure do |config|
|
|
77
|
+
# config.resolver = -> { Resolv::DNS.new }
|
|
78
|
+
# end
|
|
79
|
+
#
|
|
80
|
+
# When set, a configured resolver takes precedence over `DMARCATOR_DNS` and
|
|
81
|
+
# is used by {txt}, {txt_records}, {dmarc} and {Record.lookup} whenever no
|
|
82
|
+
# resolver is passed explicitly.
|
|
83
|
+
#
|
|
84
|
+
# @yieldparam [Config] config
|
|
85
|
+
#
|
|
86
|
+
# @return [Config]
|
|
87
|
+
#
|
|
88
|
+
# @since 0.11.0
|
|
89
|
+
#
|
|
90
|
+
def self.configure
|
|
91
|
+
yield config if block_given?
|
|
92
|
+
config
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
#
|
|
96
|
+
# Discards any configured defaults.
|
|
97
|
+
#
|
|
98
|
+
# @return [Config]
|
|
99
|
+
#
|
|
100
|
+
# @since 0.11.0
|
|
101
|
+
#
|
|
102
|
+
def self.reset_configuration!
|
|
103
|
+
@config = Config.new
|
|
104
|
+
end
|
|
105
|
+
end
|
data/lib/dmarcator/parser.rb
CHANGED
data/lib/dmarcator/query.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'resolv'
|
|
4
4
|
require 'rubygems'
|
|
5
5
|
|
|
6
|
+
require_relative 'config'
|
|
6
7
|
require_relative 'resolver'
|
|
7
8
|
|
|
8
9
|
module DMARCator
|
|
@@ -125,7 +126,11 @@ module DMARCator
|
|
|
125
126
|
end
|
|
126
127
|
|
|
127
128
|
#
|
|
128
|
-
#
|
|
129
|
+
# The default resolver: a {configure}d one, or one built from the
|
|
130
|
+
# environment.
|
|
131
|
+
#
|
|
132
|
+
# A configured resolver may be an object or a callable; a callable is called
|
|
133
|
+
# for each lookup, which is what threaded applications should use.
|
|
129
134
|
#
|
|
130
135
|
# @return [Resolv::DNS, Resolver::DoH, Resolver::DoT]
|
|
131
136
|
#
|
|
@@ -135,7 +140,11 @@ module DMARCator
|
|
|
135
140
|
# @since 0.6.0
|
|
136
141
|
#
|
|
137
142
|
def self.default_resolver
|
|
138
|
-
|
|
143
|
+
configured = config.resolver
|
|
144
|
+
|
|
145
|
+
return configured.call if configured.respond_to?(:call)
|
|
146
|
+
|
|
147
|
+
configured || Resolver.build
|
|
139
148
|
end
|
|
140
149
|
|
|
141
150
|
#
|
data/lib/dmarcator/record.rb
CHANGED
|
@@ -13,32 +13,32 @@ module DMARCator
|
|
|
13
13
|
# Attribute names exposed by this class.
|
|
14
14
|
ATTRIBUTES = [:type, :domain, :result, :selector, :scope]
|
|
15
15
|
|
|
16
|
-
# @!attribute [
|
|
16
|
+
# @!attribute [r] type
|
|
17
17
|
# @return [String] the entry name (`dkim` or `spf`).
|
|
18
|
-
# @!attribute [
|
|
18
|
+
# @!attribute [r] domain
|
|
19
19
|
# @return [String, nil] the authenticated domain.
|
|
20
|
-
# @!attribute [
|
|
20
|
+
# @!attribute [r] result
|
|
21
21
|
# @return [String, nil] the authentication result.
|
|
22
|
-
# @!attribute [
|
|
22
|
+
# @!attribute [r] selector
|
|
23
23
|
# @return [String, nil] the DKIM selector.
|
|
24
|
-
# @!attribute [
|
|
24
|
+
# @!attribute [r] scope
|
|
25
25
|
# @return [String, nil] the SPF scope.
|
|
26
|
-
ATTRIBUTES.each { |attribute|
|
|
26
|
+
ATTRIBUTES.each { |attribute| attr_reader(attribute) }
|
|
27
27
|
|
|
28
28
|
def initialize(node)
|
|
29
29
|
@node = node
|
|
30
|
-
|
|
30
|
+
@type = node.name
|
|
31
31
|
|
|
32
32
|
node.element_children.each do |element|
|
|
33
33
|
case element.name
|
|
34
34
|
when 'domain'
|
|
35
|
-
|
|
35
|
+
@domain ||= element.text
|
|
36
36
|
when 'result'
|
|
37
|
-
|
|
37
|
+
@result ||= element.text
|
|
38
38
|
when 'selector'
|
|
39
|
-
|
|
39
|
+
@selector ||= element.text
|
|
40
40
|
when 'scope'
|
|
41
|
-
|
|
41
|
+
@scope ||= element.text
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
44
|
end
|
|
@@ -16,25 +16,25 @@ module DMARCator
|
|
|
16
16
|
:begin_at, :end_at, :errors
|
|
17
17
|
]
|
|
18
18
|
|
|
19
|
-
# @!attribute [
|
|
19
|
+
# @!attribute [r] org_name
|
|
20
20
|
# @return [String, nil] the reporting organization.
|
|
21
|
-
# @!attribute [
|
|
21
|
+
# @!attribute [r] email
|
|
22
22
|
# @return [String, nil] the reporting organization's contact address.
|
|
23
|
-
# @!attribute [
|
|
23
|
+
# @!attribute [r] extra_contact_info
|
|
24
24
|
# @return [String, nil] additional contact information.
|
|
25
|
-
# @!attribute [
|
|
25
|
+
# @!attribute [r] report_id
|
|
26
26
|
# @return [String, nil] the unique report identifier.
|
|
27
|
-
# @!attribute [
|
|
27
|
+
# @!attribute [r] begin_at
|
|
28
28
|
# @return [Time, nil] the start of the reporting interval.
|
|
29
|
-
# @!attribute [
|
|
29
|
+
# @!attribute [r] end_at
|
|
30
30
|
# @return [Time, nil] the end of the reporting interval.
|
|
31
|
-
# @!attribute [
|
|
31
|
+
# @!attribute [r] errors
|
|
32
32
|
# @return [Array<String>] errors reported by the reporting organization.
|
|
33
|
-
ATTRIBUTES.each { |attribute|
|
|
33
|
+
ATTRIBUTES.each { |attribute| attr_reader(attribute) }
|
|
34
34
|
|
|
35
35
|
def initialize(node)
|
|
36
36
|
@node = node
|
|
37
|
-
|
|
37
|
+
@errors = []
|
|
38
38
|
|
|
39
39
|
metadata = node.at_xpath('feedback/report_metadata')
|
|
40
40
|
return unless metadata
|
|
@@ -42,13 +42,13 @@ module DMARCator
|
|
|
42
42
|
metadata.element_children.each do |element|
|
|
43
43
|
case element.name
|
|
44
44
|
when 'org_name'
|
|
45
|
-
|
|
45
|
+
@org_name ||= element.text
|
|
46
46
|
when 'email'
|
|
47
|
-
|
|
47
|
+
@email ||= element.text
|
|
48
48
|
when 'extra_contact_info'
|
|
49
|
-
|
|
49
|
+
@extra_contact_info ||= element.text
|
|
50
50
|
when 'report_id'
|
|
51
|
-
|
|
51
|
+
@report_id ||= element.text
|
|
52
52
|
when 'error'
|
|
53
53
|
errors << element.text
|
|
54
54
|
when 'date_range'
|
|
@@ -63,9 +63,9 @@ module DMARCator
|
|
|
63
63
|
date_range.element_children.each do |element|
|
|
64
64
|
case element.name
|
|
65
65
|
when 'begin'
|
|
66
|
-
|
|
66
|
+
@begin_at ||= parse_timestamp(element.text)
|
|
67
67
|
when 'end'
|
|
68
|
-
|
|
68
|
+
@end_at ||= parse_timestamp(element.text)
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
end
|
|
@@ -13,21 +13,21 @@ module DMARCator
|
|
|
13
13
|
# Attribute names exposed by this class.
|
|
14
14
|
ATTRIBUTES = [:domain, :adkim, :aspf, :p, :sp, :pct, :fo]
|
|
15
15
|
|
|
16
|
-
# @!attribute [
|
|
16
|
+
# @!attribute [r] domain
|
|
17
17
|
# @return [String, nil] the domain the policy applies to.
|
|
18
|
-
# @!attribute [
|
|
18
|
+
# @!attribute [r] adkim
|
|
19
19
|
# @return [String, nil] the DKIM alignment mode (`r` or `s`).
|
|
20
|
-
# @!attribute [
|
|
20
|
+
# @!attribute [r] aspf
|
|
21
21
|
# @return [String, nil] the SPF alignment mode (`r` or `s`).
|
|
22
|
-
# @!attribute [
|
|
22
|
+
# @!attribute [r] p
|
|
23
23
|
# @return [String, nil] the requested policy.
|
|
24
|
-
# @!attribute [
|
|
24
|
+
# @!attribute [r] sp
|
|
25
25
|
# @return [String, nil] the requested subdomain policy.
|
|
26
|
-
# @!attribute [
|
|
26
|
+
# @!attribute [r] pct
|
|
27
27
|
# @return [Integer, nil] the percentage of messages the policy applied to.
|
|
28
|
-
# @!attribute [
|
|
28
|
+
# @!attribute [r] fo
|
|
29
29
|
# @return [String, nil] the requested failure-reporting options.
|
|
30
|
-
ATTRIBUTES.each { |attribute|
|
|
30
|
+
ATTRIBUTES.each { |attribute| attr_reader(attribute) }
|
|
31
31
|
|
|
32
32
|
def initialize(node)
|
|
33
33
|
@node = node
|
|
@@ -38,19 +38,19 @@ module DMARCator
|
|
|
38
38
|
policy.element_children.each do |element|
|
|
39
39
|
case element.name
|
|
40
40
|
when 'pct'
|
|
41
|
-
|
|
41
|
+
@pct ||= parse_integer(element.text)
|
|
42
42
|
when 'domain'
|
|
43
|
-
|
|
43
|
+
@domain ||= element.text
|
|
44
44
|
when 'adkim'
|
|
45
|
-
|
|
45
|
+
@adkim ||= element.text
|
|
46
46
|
when 'aspf'
|
|
47
|
-
|
|
47
|
+
@aspf ||= element.text
|
|
48
48
|
when 'p'
|
|
49
|
-
|
|
49
|
+
@p ||= element.text
|
|
50
50
|
when 'sp'
|
|
51
|
-
|
|
51
|
+
@sp ||= element.text
|
|
52
52
|
when 'fo'
|
|
53
|
-
|
|
53
|
+
@fo ||= element.text
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
end
|
|
@@ -13,11 +13,11 @@ module DMARCator
|
|
|
13
13
|
# Attribute names exposed by this class.
|
|
14
14
|
ATTRIBUTES = [:type, :comment]
|
|
15
15
|
|
|
16
|
-
# @!attribute [
|
|
16
|
+
# @!attribute [r] type
|
|
17
17
|
# @return [String, nil] the reason type.
|
|
18
|
-
# @!attribute [
|
|
18
|
+
# @!attribute [r] comment
|
|
19
19
|
# @return [String, nil] an optional explanatory comment.
|
|
20
|
-
ATTRIBUTES.each { |attribute|
|
|
20
|
+
ATTRIBUTES.each { |attribute| attr_reader(attribute) }
|
|
21
21
|
|
|
22
22
|
def initialize(node)
|
|
23
23
|
@node = node
|
|
@@ -25,9 +25,9 @@ module DMARCator
|
|
|
25
25
|
node.element_children.each do |element|
|
|
26
26
|
case element.name
|
|
27
27
|
when 'type'
|
|
28
|
-
|
|
28
|
+
@type ||= element.text
|
|
29
29
|
when 'comment'
|
|
30
|
-
|
|
30
|
+
@comment ||= element.text
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
end
|
data/lib/dmarcator/report/row.rb
CHANGED
|
@@ -21,24 +21,23 @@ module DMARCator
|
|
|
21
21
|
:envelope_to, :envelope_from, :header_from
|
|
22
22
|
]
|
|
23
23
|
|
|
24
|
-
# @!attribute [
|
|
24
|
+
# @!attribute [r] source_ip
|
|
25
25
|
# @return [String, nil] the sending IP address.
|
|
26
|
-
# @!attribute [
|
|
26
|
+
# @!attribute [r] count
|
|
27
27
|
# @return [Integer, nil] the number of messages from this source.
|
|
28
|
-
# @!attribute [
|
|
28
|
+
# @!attribute [r] disposition
|
|
29
29
|
# @return [String, nil] the action applied to the messages.
|
|
30
|
-
# @!attribute [
|
|
30
|
+
# @!attribute [r] dkim
|
|
31
31
|
# @return [String, nil] the DKIM evaluation result.
|
|
32
|
-
# @!attribute [
|
|
32
|
+
# @!attribute [r] spf
|
|
33
33
|
# @return [String, nil] the SPF evaluation result.
|
|
34
|
-
# @!attribute [
|
|
34
|
+
# @!attribute [r] envelope_to
|
|
35
35
|
# @return [String, nil] the envelope recipient domain.
|
|
36
|
-
# @!attribute [
|
|
36
|
+
# @!attribute [r] envelope_from
|
|
37
37
|
# @return [String, nil] the envelope sender domain.
|
|
38
|
-
# @!attribute [
|
|
38
|
+
# @!attribute [r] header_from
|
|
39
39
|
# @return [String, nil] the `From` header domain.
|
|
40
|
-
ATTRIBUTES.each { |attribute|
|
|
41
|
-
attr_writer :auth_results, :reasons
|
|
40
|
+
ATTRIBUTES.each { |attribute| attr_reader(attribute) }
|
|
42
41
|
|
|
43
42
|
def initialize(node)
|
|
44
43
|
@node = node
|
|
@@ -87,9 +86,9 @@ module DMARCator
|
|
|
87
86
|
row.element_children.each do |element|
|
|
88
87
|
case element.name
|
|
89
88
|
when 'source_ip'
|
|
90
|
-
|
|
89
|
+
@source_ip ||= element.text
|
|
91
90
|
when 'count'
|
|
92
|
-
|
|
91
|
+
@count ||= parse_integer(element.text)
|
|
93
92
|
when 'policy_evaluated'
|
|
94
93
|
parse_policy_evaluated(element)
|
|
95
94
|
end
|
|
@@ -100,11 +99,11 @@ module DMARCator
|
|
|
100
99
|
policy_evaluated.element_children.each do |element|
|
|
101
100
|
case element.name
|
|
102
101
|
when 'disposition'
|
|
103
|
-
|
|
102
|
+
@disposition ||= element.text
|
|
104
103
|
when 'dkim'
|
|
105
|
-
|
|
104
|
+
@dkim ||= element.text
|
|
106
105
|
when 'spf'
|
|
107
|
-
|
|
106
|
+
@spf ||= element.text
|
|
108
107
|
when 'reason'
|
|
109
108
|
@reason_nodes << element
|
|
110
109
|
end
|
|
@@ -115,11 +114,11 @@ module DMARCator
|
|
|
115
114
|
identifiers.element_children.each do |element|
|
|
116
115
|
case element.name
|
|
117
116
|
when 'envelope_to'
|
|
118
|
-
|
|
117
|
+
@envelope_to ||= element.text
|
|
119
118
|
when 'envelope_from'
|
|
120
|
-
|
|
119
|
+
@envelope_from ||= element.text
|
|
121
120
|
when 'header_from'
|
|
122
|
-
|
|
121
|
+
@header_from ||= element.text
|
|
123
122
|
end
|
|
124
123
|
end
|
|
125
124
|
end
|
data/lib/dmarcator/report.rb
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'nokogiri'
|
|
4
|
-
require 'bigdecimal'
|
|
5
|
-
require 'bigdecimal/util'
|
|
6
4
|
|
|
5
|
+
require_relative 'config'
|
|
7
6
|
require_relative 'xml_helper'
|
|
8
7
|
require_relative 'exceptions'
|
|
9
8
|
require_relative 'report/metadata'
|
|
@@ -21,22 +20,35 @@ module DMARCator
|
|
|
21
20
|
include XmlHelper
|
|
22
21
|
|
|
23
22
|
# Default maximum accepted report size in bytes (25 MiB).
|
|
24
|
-
|
|
23
|
+
#
|
|
24
|
+
# @deprecated Use {Config::DEFAULT_MAX_REPORT_BYTES}.
|
|
25
|
+
#
|
|
26
|
+
MAX_REPORT_BYTES = Config::DEFAULT_MAX_REPORT_BYTES
|
|
25
27
|
|
|
26
28
|
class << self
|
|
27
29
|
#
|
|
28
30
|
# The maximum accepted report size in bytes.
|
|
29
31
|
#
|
|
30
|
-
#
|
|
31
|
-
# roughly 13x the input size, so lower this to bound memory for reports
|
|
32
|
-
# from untrusted sources.
|
|
32
|
+
# @deprecated Use {DMARCator.configure} with `config.max_report_bytes`.
|
|
33
33
|
#
|
|
34
34
|
# @return [Integer]
|
|
35
35
|
#
|
|
36
|
-
attr_writer :max_report_bytes
|
|
37
|
-
|
|
38
36
|
def max_report_bytes
|
|
39
|
-
|
|
37
|
+
DMARCator.config.max_report_bytes
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
#
|
|
41
|
+
# Sets the maximum accepted report size in bytes.
|
|
42
|
+
#
|
|
43
|
+
# @deprecated Use {DMARCator.configure} with `config.max_report_bytes`.
|
|
44
|
+
#
|
|
45
|
+
# @param [Integer, nil] value
|
|
46
|
+
#
|
|
47
|
+
# @raise [ArgumentError]
|
|
48
|
+
# The value is not a positive Integer or nil.
|
|
49
|
+
#
|
|
50
|
+
def max_report_bytes=(value)
|
|
51
|
+
DMARCator.config.max_report_bytes = value
|
|
40
52
|
end
|
|
41
53
|
end
|
|
42
54
|
|
|
@@ -47,8 +59,8 @@ module DMARCator
|
|
|
47
59
|
# The raw DMARC XML report.
|
|
48
60
|
#
|
|
49
61
|
# @raise [InvalidReport]
|
|
50
|
-
# The report is not a String, exceeds
|
|
51
|
-
# well-formed XML.
|
|
62
|
+
# The report is not a String, exceeds
|
|
63
|
+
# {Config#max_report_bytes}, or is not well-formed XML.
|
|
52
64
|
#
|
|
53
65
|
def initialize(xml)
|
|
54
66
|
unless xml.is_a?(String)
|
|
@@ -57,7 +69,7 @@ module DMARCator
|
|
|
57
69
|
|
|
58
70
|
@xml = xml
|
|
59
71
|
|
|
60
|
-
limit =
|
|
72
|
+
limit = DMARCator.config.max_report_bytes
|
|
61
73
|
|
|
62
74
|
if xml.bytesize > limit
|
|
63
75
|
raise InvalidReport, "DMARC XML report exceeds maximum size of #{limit} bytes"
|
|
@@ -73,23 +85,22 @@ module DMARCator
|
|
|
73
85
|
end
|
|
74
86
|
|
|
75
87
|
#
|
|
76
|
-
# The report version.
|
|
88
|
+
# The report version, as published.
|
|
89
|
+
#
|
|
90
|
+
# Returned as a String because versions are opaque identifiers, not
|
|
91
|
+
# numbers to compute with.
|
|
77
92
|
#
|
|
78
|
-
# @return [
|
|
93
|
+
# @return [String, nil]
|
|
94
|
+
# The version, or `nil` when it is absent or not numeric.
|
|
79
95
|
#
|
|
80
96
|
def version
|
|
81
97
|
return @version if defined?(@version)
|
|
82
98
|
|
|
83
99
|
@version = begin
|
|
84
100
|
value = get_text('feedback/version')
|
|
101
|
+
value = value.strip if value
|
|
85
102
|
|
|
86
|
-
if value &&
|
|
87
|
-
begin
|
|
88
|
-
BigDecimal(value)
|
|
89
|
-
rescue ArgumentError
|
|
90
|
-
nil
|
|
91
|
-
end
|
|
92
|
-
end
|
|
103
|
+
value if value && value.match?(/\A\d+(?:\.\d+)*\z/)
|
|
93
104
|
end
|
|
94
105
|
end
|
|
95
106
|
|
data/lib/dmarcator/version.rb
CHANGED
data/lib/dmarcator/xml_helper.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.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Noah Berman
|
|
@@ -43,20 +43,6 @@ dependencies:
|
|
|
43
43
|
- - ">="
|
|
44
44
|
- !ruby/object:Gem::Version
|
|
45
45
|
version: '1.10'
|
|
46
|
-
- !ruby/object:Gem::Dependency
|
|
47
|
-
name: bigdecimal
|
|
48
|
-
requirement: !ruby/object:Gem::Requirement
|
|
49
|
-
requirements:
|
|
50
|
-
- - ">="
|
|
51
|
-
- !ruby/object:Gem::Version
|
|
52
|
-
version: '1.0'
|
|
53
|
-
type: :runtime
|
|
54
|
-
prerelease: false
|
|
55
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
-
requirements:
|
|
57
|
-
- - ">="
|
|
58
|
-
- !ruby/object:Gem::Version
|
|
59
|
-
version: '1.0'
|
|
60
46
|
description: A hardened DMARC record parser and query client, forked from the Trail
|
|
61
47
|
of Bits 'dmarc' gem.
|
|
62
48
|
email:
|
|
@@ -69,6 +55,7 @@ files:
|
|
|
69
55
|
- LICENSE.txt
|
|
70
56
|
- README.md
|
|
71
57
|
- lib/dmarcator.rb
|
|
58
|
+
- lib/dmarcator/config.rb
|
|
72
59
|
- lib/dmarcator/exceptions.rb
|
|
73
60
|
- lib/dmarcator/parser.rb
|
|
74
61
|
- lib/dmarcator/query.rb
|