dmarcator 0.2.0 → 0.3.1
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 +14 -0
- data/README.md +1 -1
- data/lib/dmarcator/exceptions.rb +6 -0
- data/lib/dmarcator/query.rb +46 -2
- data/lib/dmarcator/report.rb +14 -1
- data/lib/dmarcator/uri.rb +21 -0
- data/lib/dmarcator/version.rb +1 -1
- data/lib/dmarcator/xml_helper.rb +6 -1
- data/spec/dmarcator_spec.rb +66 -0
- data/spec/report_spec.rb +28 -0
- data/spec/uri_spec.rb +46 -0
- metadata +3 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 00c852a4815b31a240c4c24071c523ff0af57f6d87b447fbcec7515f8b502e55
|
|
4
|
+
data.tar.gz: b325e8eb13e3ee9f91215800492a9d81142338ad71992c998f1d0976756b4214
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d7b2dd3cbcbd800cfc3b1de240200ec0493fa6d82794282f00aa4c23d209f4269f8335f55468091e4f69dad70206b56c6dcac33c5da578f385507a1bd3eb531a
|
|
7
|
+
data.tar.gz: 3faa47a127473ce206dc30d983772f5189d4d7cec302394ab96dcf64afe481463bfdaad645bf99e2c38cd14bd133d1c7c637744646dfed2d62662a5b105578f7
|
data/ChangeLog.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
### 0.3.1 / 2026-10-26
|
|
2
|
+
|
|
3
|
+
* Update the gemspec maintainer metadata to list only the fork's author.
|
|
4
|
+
|
|
5
|
+
### 0.3.0 / 2026-10-26
|
|
6
|
+
|
|
7
|
+
* Raise `DMARCator::InvalidReport` for malformed XML reports instead of returning an empty report.
|
|
8
|
+
* Return `nil` for a non-numeric report `version` instead of raising `ArgumentError`.
|
|
9
|
+
* Return `nil` for a non-numeric report timestamp instead of coercing it to the Unix epoch.
|
|
10
|
+
* Add `DMARCator::Uri#eql?` and `#hash` for correct Hash/Set semantics.
|
|
11
|
+
* Harden `DMARCator.validate_domain` against CVE-2026-80213 (63-octet label limit and 248-octet total limit).
|
|
12
|
+
* Warn at load time when the `resolv` gem is affected by CVE-2026-80212/CVE-2026-80213.
|
|
13
|
+
* Require Ruby >= 3.3.13 to pick up the Regexp compilation fixes behind GitLab's CVE-2026-89078, CVE-2026-93577, CVE-2026-97229 and CVE-2026-110264.
|
|
14
|
+
|
|
1
15
|
### 0.2.0 / 2026-10-13
|
|
2
16
|
|
|
3
17
|
* Added DMARC XML report parsing, ported from the `dmarc_parser` gem by D Salko.
|
data/README.md
CHANGED
data/lib/dmarcator/exceptions.rb
CHANGED
data/lib/dmarcator/query.rb
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
require 'resolv'
|
|
2
|
+
require 'rubygems'
|
|
2
3
|
|
|
3
4
|
module DMARCator
|
|
5
|
+
#
|
|
6
|
+
# Resolv gem versions affected by CVE-2026-80212 and CVE-2026-80213.
|
|
7
|
+
#
|
|
8
|
+
VULNERABLE_RESOLV = [
|
|
9
|
+
Gem::Requirement.new('< 0.3.2'),
|
|
10
|
+
Gem::Requirement.new('>= 0.4.0', '< 0.7.2')
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
#
|
|
14
|
+
# Determines the version of the loaded `resolv` gem, if available.
|
|
15
|
+
#
|
|
16
|
+
# @return [String, nil]
|
|
17
|
+
#
|
|
18
|
+
def self.resolv_version
|
|
19
|
+
if defined?(Resolv::VERSION)
|
|
20
|
+
Resolv::VERSION
|
|
21
|
+
elsif (spec = Gem.loaded_specs['resolv'])
|
|
22
|
+
spec.version.to_s
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#
|
|
27
|
+
# Determines whether the loaded `resolv` gem is affected by
|
|
28
|
+
# CVE-2026-80212 or CVE-2026-80213.
|
|
29
|
+
#
|
|
30
|
+
# @return [Boolean]
|
|
31
|
+
#
|
|
32
|
+
def self.vulnerable_resolv?
|
|
33
|
+
version = resolv_version
|
|
34
|
+
return false unless version
|
|
35
|
+
|
|
36
|
+
VULNERABLE_RESOLV.any? do |requirement|
|
|
37
|
+
requirement.satisfied_by?(Gem::Version.new(version))
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
4
41
|
#
|
|
5
42
|
# Validates a domain before using it in a DNS query.
|
|
6
43
|
#
|
|
@@ -15,10 +52,11 @@ module DMARCator
|
|
|
15
52
|
#
|
|
16
53
|
def self.validate_domain(domain)
|
|
17
54
|
unless domain.is_a?(String) && !domain.empty? &&
|
|
18
|
-
domain.length <=
|
|
55
|
+
domain.length <= 248 &&
|
|
19
56
|
!domain.start_with?('.') &&
|
|
20
57
|
!domain.include?('..') &&
|
|
21
|
-
!domain.match?(/[^A-Za-z0-9._-]/)
|
|
58
|
+
!domain.match?(/[^A-Za-z0-9._-]/) &&
|
|
59
|
+
domain.split('.').all? { |label| label.length <= 63 }
|
|
22
60
|
raise ArgumentError, "invalid domain: #{domain.inspect}"
|
|
23
61
|
end
|
|
24
62
|
|
|
@@ -61,3 +99,9 @@ module DMARCator
|
|
|
61
99
|
nil
|
|
62
100
|
end
|
|
63
101
|
end
|
|
102
|
+
|
|
103
|
+
if DMARCator.vulnerable_resolv?
|
|
104
|
+
warn "dmarcator: resolv #{DMARCator.resolv_version} is affected by " \
|
|
105
|
+
"CVE-2026-80212 and CVE-2026-80213; upgrade to resolv >= 0.7.2 " \
|
|
106
|
+
"(or >= 0.3.2 on Ruby 3.3)"
|
|
107
|
+
end
|
data/lib/dmarcator/report.rb
CHANGED
|
@@ -5,6 +5,7 @@ require 'bigdecimal'
|
|
|
5
5
|
require 'bigdecimal/util'
|
|
6
6
|
|
|
7
7
|
require_relative 'xml_helper'
|
|
8
|
+
require_relative 'exceptions'
|
|
8
9
|
require_relative 'report/metadata'
|
|
9
10
|
require_relative 'report/policy'
|
|
10
11
|
require_relative 'report/record'
|
|
@@ -28,6 +29,11 @@ module DMARCator
|
|
|
28
29
|
def initialize(xml)
|
|
29
30
|
@xml = xml
|
|
30
31
|
@node = Nokogiri::XML(xml)
|
|
32
|
+
|
|
33
|
+
unless @node.errors.empty?
|
|
34
|
+
raise InvalidReport, "invalid DMARC XML report: #{@node.errors.first}"
|
|
35
|
+
end
|
|
36
|
+
|
|
31
37
|
@node.remove_namespaces!
|
|
32
38
|
end
|
|
33
39
|
|
|
@@ -39,7 +45,14 @@ module DMARCator
|
|
|
39
45
|
def version
|
|
40
46
|
@version ||= begin
|
|
41
47
|
value = get_text('feedback/version')
|
|
42
|
-
|
|
48
|
+
|
|
49
|
+
if value && !value.empty?
|
|
50
|
+
begin
|
|
51
|
+
BigDecimal(value)
|
|
52
|
+
rescue ArgumentError
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
end
|
|
43
56
|
end
|
|
44
57
|
end
|
|
45
58
|
|
data/lib/dmarcator/uri.rb
CHANGED
|
@@ -75,6 +75,27 @@ module DMARCator
|
|
|
75
75
|
(@unit == other.unit)
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
#
|
|
79
|
+
# Determines if the DMARC URI is equivalent to the other.
|
|
80
|
+
#
|
|
81
|
+
# @param [Object] other
|
|
82
|
+
# the other DMARC URI to compare against.
|
|
83
|
+
#
|
|
84
|
+
# @return [Boolean]
|
|
85
|
+
#
|
|
86
|
+
def eql?(other)
|
|
87
|
+
self == other
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
#
|
|
91
|
+
# Computes a hash value consistent with {#eql?}.
|
|
92
|
+
#
|
|
93
|
+
# @return [Integer]
|
|
94
|
+
#
|
|
95
|
+
def hash
|
|
96
|
+
[self.class, @uri, @size, @unit].hash
|
|
97
|
+
end
|
|
98
|
+
|
|
78
99
|
#
|
|
79
100
|
# Converts the DMARC URI back into a String.
|
|
80
101
|
#
|
data/lib/dmarcator/version.rb
CHANGED
data/lib/dmarcator/xml_helper.rb
CHANGED
|
@@ -30,7 +30,12 @@ module DMARCator
|
|
|
30
30
|
#
|
|
31
31
|
def get_timestamp(xpath)
|
|
32
32
|
value = get_text(xpath)
|
|
33
|
-
|
|
33
|
+
return nil if value.nil? || value.empty?
|
|
34
|
+
|
|
35
|
+
digits = value.strip
|
|
36
|
+
return nil unless digits.match?(/\A\d+\z/)
|
|
37
|
+
|
|
38
|
+
Time.at(digits.to_i)
|
|
34
39
|
end
|
|
35
40
|
|
|
36
41
|
#
|
data/spec/dmarcator_spec.rb
CHANGED
|
@@ -59,4 +59,70 @@ describe DMARCator do
|
|
|
59
59
|
expect { subject.query("foo bar") }.to raise_error(ArgumentError)
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
|
+
|
|
63
|
+
describe ".validate_domain" do
|
|
64
|
+
it "accepts a valid domain" do
|
|
65
|
+
expect(subject.validate_domain("example.com")).to eq("example.com")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "rejects a label longer than 63 octets" do
|
|
69
|
+
expect {
|
|
70
|
+
subject.validate_domain("#{'a' * 64}.com")
|
|
71
|
+
}.to raise_error(ArgumentError)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "accepts a 63-octet label" do
|
|
75
|
+
domain = "#{'a' * 63}.com"
|
|
76
|
+
|
|
77
|
+
expect(subject.validate_domain(domain)).to eq(domain)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "rejects a domain longer than 248 octets" do
|
|
81
|
+
domain = "#{'a' * 62}.#{'a' * 62}.#{'a' * 62}.#{'a' * 60}"
|
|
82
|
+
|
|
83
|
+
expect { subject.validate_domain(domain) }.to raise_error(ArgumentError)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "accepts a domain at the 248-octet limit" do
|
|
87
|
+
domain = "#{'a' * 62}.#{'a' * 62}.#{'a' * 62}.#{'a' * 59}"
|
|
88
|
+
|
|
89
|
+
expect(subject.validate_domain(domain)).to eq(domain)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "rejects a non-string domain" do
|
|
93
|
+
expect { subject.validate_domain(123) }.to raise_error(ArgumentError)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
describe ".vulnerable_resolv?" do
|
|
98
|
+
it "is true for resolv 0.3.1 and earlier" do
|
|
99
|
+
allow(subject).to receive(:resolv_version).and_return("0.3.1")
|
|
100
|
+
|
|
101
|
+
expect(subject.vulnerable_resolv?).to be(true)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "is false for resolv 0.3.2" do
|
|
105
|
+
allow(subject).to receive(:resolv_version).and_return("0.3.2")
|
|
106
|
+
|
|
107
|
+
expect(subject.vulnerable_resolv?).to be(false)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "is true for resolv 0.4.0 through 0.7.1" do
|
|
111
|
+
allow(subject).to receive(:resolv_version).and_return("0.7.1")
|
|
112
|
+
|
|
113
|
+
expect(subject.vulnerable_resolv?).to be(true)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "is false for resolv 0.7.2 and later" do
|
|
117
|
+
allow(subject).to receive(:resolv_version).and_return("0.7.2")
|
|
118
|
+
|
|
119
|
+
expect(subject.vulnerable_resolv?).to be(false)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "is false when the version is unknown" do
|
|
123
|
+
allow(subject).to receive(:resolv_version).and_return(nil)
|
|
124
|
+
|
|
125
|
+
expect(subject.vulnerable_resolv?).to be(false)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
62
128
|
end
|
data/spec/report_spec.rb
CHANGED
|
@@ -23,6 +23,34 @@ RSpec.describe DMARCator::Report do
|
|
|
23
23
|
expect(google.version).to be_nil
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
it 'returns nil for a non-numeric report version' do
|
|
27
|
+
report = described_class.new('<feedback><version>abc</version></feedback>')
|
|
28
|
+
|
|
29
|
+
expect(report.version).to be_nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'raises InvalidReport for malformed XML' do
|
|
33
|
+
expect {
|
|
34
|
+
described_class.new('this is not xml')
|
|
35
|
+
}.to raise_error(DMARCator::InvalidReport)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'returns nil for a non-numeric timestamp' do
|
|
39
|
+
report = described_class.new(
|
|
40
|
+
'<feedback><report_metadata><date_range><begin>notanumber</begin></date_range></report_metadata></feedback>'
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
expect(report.metadata.begin_at).to be_nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'parses timestamps with surrounding whitespace' do
|
|
47
|
+
report = described_class.new(
|
|
48
|
+
'<feedback><report_metadata><date_range><begin> 1593648000 </begin></date_range></report_metadata></feedback>'
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
expect(report.metadata.begin_at).to eq(Time.at(1_593_648_000))
|
|
52
|
+
end
|
|
53
|
+
|
|
26
54
|
it 'reads report metadata' do
|
|
27
55
|
report = described_class.new(xml_report)
|
|
28
56
|
|
data/spec/uri_spec.rb
CHANGED
|
@@ -109,6 +109,52 @@ describe DMARCator::Uri do
|
|
|
109
109
|
end
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
+
describe "#eql?" do
|
|
113
|
+
it "is true when all fields match" do
|
|
114
|
+
other = described_class.new(uri.dup, size, unit)
|
|
115
|
+
|
|
116
|
+
expect(subject.eql?(other)).to be(true)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "is false when the uri differs" do
|
|
120
|
+
other = described_class.new(URI("mailto:other@example.com"), size, unit)
|
|
121
|
+
|
|
122
|
+
expect(subject.eql?(other)).to be(false)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "is false for a different class" do
|
|
126
|
+
expect(subject.eql?(Object.new)).to be(false)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
describe "#hash" do
|
|
131
|
+
it "is equal for equivalent URIs" do
|
|
132
|
+
other = described_class.new(uri.dup, size, unit)
|
|
133
|
+
|
|
134
|
+
expect(subject.hash).to eq(other.hash)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it "differs for non-equivalent URIs" do
|
|
138
|
+
other = described_class.new(URI("mailto:other@example.com"), size, unit)
|
|
139
|
+
|
|
140
|
+
expect(subject.hash).not_to eq(other.hash)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
describe "Hash/Set semantics" do
|
|
145
|
+
it "deduplicates equivalent URIs" do
|
|
146
|
+
other = described_class.new(uri.dup, size, unit)
|
|
147
|
+
|
|
148
|
+
expect([subject, other].uniq.size).to eq(1)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it "treats equivalent URIs as the same Hash key" do
|
|
152
|
+
other = described_class.new(uri.dup, size, unit)
|
|
153
|
+
|
|
154
|
+
expect({ subject => 1 }.key?(other)).to be(true)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
112
158
|
describe "#to_s" do
|
|
113
159
|
context "when only the uri is set" do
|
|
114
160
|
subject { described_class.new(uri) }
|
metadata
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dmarcator
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Noah Berman
|
|
8
|
-
- Davis Gallinghouse
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
@@ -62,7 +61,6 @@ description: A hardened DMARC record parser and query client, forked from the Tr
|
|
|
62
61
|
of Bits 'dmarc' gem.
|
|
63
62
|
email:
|
|
64
63
|
- noah@noahberman.org
|
|
65
|
-
- davis@trailofbits.com
|
|
66
64
|
executables: []
|
|
67
65
|
extensions: []
|
|
68
66
|
extra_rdoc_files: []
|
|
@@ -107,14 +105,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
107
105
|
requirements:
|
|
108
106
|
- - ">="
|
|
109
107
|
- !ruby/object:Gem::Version
|
|
110
|
-
version: 3.
|
|
108
|
+
version: 3.3.13
|
|
111
109
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
112
110
|
requirements:
|
|
113
111
|
- - ">="
|
|
114
112
|
- !ruby/object:Gem::Version
|
|
115
113
|
version: '0'
|
|
116
114
|
requirements: []
|
|
117
|
-
rubygems_version: 4.0.
|
|
115
|
+
rubygems_version: 4.0.23
|
|
118
116
|
specification_version: 4
|
|
119
117
|
summary: DMARC record parser and query client
|
|
120
118
|
test_files:
|