dmarcator 0.1.0 → 0.3.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 +15 -0
- data/LICENSE.txt +1 -0
- data/README.md +32 -3
- data/lib/dmarcator/exceptions.rb +6 -0
- data/lib/dmarcator/query.rb +46 -2
- data/lib/dmarcator/report/auth_result.rb +27 -0
- data/lib/dmarcator/report/metadata.rb +32 -0
- data/lib/dmarcator/report/policy.rb +28 -0
- data/lib/dmarcator/report/reason.rb +24 -0
- data/lib/dmarcator/report/record.rb +42 -0
- data/lib/dmarcator/report.rb +88 -0
- data/lib/dmarcator/uri.rb +21 -0
- data/lib/dmarcator/version.rb +1 -1
- data/lib/dmarcator/xml_helper.rb +54 -0
- data/lib/dmarcator.rb +1 -0
- data/spec/dmarcator_spec.rb +66 -0
- data/spec/report_spec.rb +108 -0
- data/spec/support/complete.xml +364 -0
- data/spec/support/google.xml +353 -0
- data/spec/uri_spec.rb +46 -0
- metadata +44 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '03348cb4b245b8e35dde8ba708559df27d8fd40a6e52d1e15bd73db8732e49d8'
|
|
4
|
+
data.tar.gz: fac2e24464f16d080f27e7fd1fede61720947dffda32a792d5b5b20d4f507373
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2c5c33baf620a3520d927f4eb1f7b3078703201f3c314779fd42aef5f97d844280a5d25e82103f2a6e6aae963779a9f8f25962ea92758b44e4a4054844363c50
|
|
7
|
+
data.tar.gz: bc9e4584af2e2a158c918f855e80e76d590f539d69d3571b1edcac12a036bca01ee269cc88c3f3d63f670b65c753dc068cce3d1f9e7d7a4a38b0783a41a7ad2d
|
data/ChangeLog.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
### 0.3.0 / 2026-10-26
|
|
2
|
+
|
|
3
|
+
* Raise `DMARCator::InvalidReport` for malformed XML reports instead of returning an empty report.
|
|
4
|
+
* Return `nil` for a non-numeric report `version` instead of raising `ArgumentError`.
|
|
5
|
+
* Return `nil` for a non-numeric report timestamp instead of coercing it to the Unix epoch.
|
|
6
|
+
* Add `DMARCator::Uri#eql?` and `#hash` for correct Hash/Set semantics.
|
|
7
|
+
* Harden `DMARCator.validate_domain` against CVE-2026-80213 (63-octet label limit and 248-octet total limit).
|
|
8
|
+
* Warn at load time when the `resolv` gem is affected by CVE-2026-80212/CVE-2026-80213.
|
|
9
|
+
* 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.
|
|
10
|
+
|
|
11
|
+
### 0.2.0 / 2026-10-13
|
|
12
|
+
|
|
13
|
+
* Added DMARC XML report parsing, ported from the `dmarc_parser` gem by D Salko.
|
|
14
|
+
* Added `nokogiri` and `bigdecimal` runtime dependencies.
|
|
15
|
+
|
|
1
16
|
### 0.1.0 / 2026-10-13
|
|
2
17
|
|
|
3
18
|
* Forked from the Trail of Bits `dmarc` gem and published as `dmarcator`.
|
data/LICENSE.txt
CHANGED
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
|
|
15
|
-
|
|
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
|
-
* [Ruby] >= 3.
|
|
118
|
+
* [Ruby] >= 3.3.13
|
|
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
|
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
|
|
@@ -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,88 @@
|
|
|
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 'exceptions'
|
|
9
|
+
require_relative 'report/metadata'
|
|
10
|
+
require_relative 'report/policy'
|
|
11
|
+
require_relative 'report/record'
|
|
12
|
+
|
|
13
|
+
module DMARCator
|
|
14
|
+
#
|
|
15
|
+
# Represents a parsed DMARC aggregate/forensic XML report.
|
|
16
|
+
#
|
|
17
|
+
# Nokogiri's default parse options are used, which do not resolve external
|
|
18
|
+
# entities or fetch network resources; treat report XML as untrusted input.
|
|
19
|
+
#
|
|
20
|
+
class Report
|
|
21
|
+
include XmlHelper
|
|
22
|
+
|
|
23
|
+
#
|
|
24
|
+
# Initializes the report from an XML string.
|
|
25
|
+
#
|
|
26
|
+
# @param [String] xml
|
|
27
|
+
# The raw DMARC XML report.
|
|
28
|
+
#
|
|
29
|
+
def initialize(xml)
|
|
30
|
+
@xml = xml
|
|
31
|
+
@node = Nokogiri::XML(xml)
|
|
32
|
+
|
|
33
|
+
unless @node.errors.empty?
|
|
34
|
+
raise InvalidReport, "invalid DMARC XML report: #{@node.errors.first}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
@node.remove_namespaces!
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
#
|
|
41
|
+
# The report version.
|
|
42
|
+
#
|
|
43
|
+
# @return [BigDecimal, nil]
|
|
44
|
+
#
|
|
45
|
+
def version
|
|
46
|
+
@version ||= begin
|
|
47
|
+
value = get_text('feedback/version')
|
|
48
|
+
|
|
49
|
+
if value && !value.empty?
|
|
50
|
+
begin
|
|
51
|
+
BigDecimal(value)
|
|
52
|
+
rescue ArgumentError
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
#
|
|
60
|
+
# The report metadata.
|
|
61
|
+
#
|
|
62
|
+
# @return [Report::Metadata]
|
|
63
|
+
#
|
|
64
|
+
def metadata
|
|
65
|
+
@metadata ||= Metadata.new(@node)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
#
|
|
69
|
+
# The published DMARC policy.
|
|
70
|
+
#
|
|
71
|
+
# @return [Report::Policy]
|
|
72
|
+
#
|
|
73
|
+
def policy
|
|
74
|
+
@policy ||= Policy.new(@node)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
#
|
|
78
|
+
# The report records.
|
|
79
|
+
#
|
|
80
|
+
# @return [Array<Report::Record>]
|
|
81
|
+
#
|
|
82
|
+
def records
|
|
83
|
+
@records ||= @node.xpath('feedback/record').map do |node|
|
|
84
|
+
Record.new(node)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
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
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
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)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
#
|
|
42
|
+
# Returns an integer value.
|
|
43
|
+
#
|
|
44
|
+
# @param [String] xpath
|
|
45
|
+
# The XPath expression to match.
|
|
46
|
+
#
|
|
47
|
+
# @return [Integer, nil]
|
|
48
|
+
#
|
|
49
|
+
def get_int(xpath)
|
|
50
|
+
value = get_text(xpath)
|
|
51
|
+
value && !value.empty? ? value.to_i : nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/dmarcator.rb
CHANGED
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
|