metasploit_data_models 0.18.1-java → 0.19.0-java
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/app/models/mdm/host.rb +7 -0
- data/app/models/mdm/service.rb +30 -1
- data/app/models/mdm/tag.rb +10 -0
- data/app/models/metasploit_data_models/ip_address/v4/cidr.rb +14 -0
- data/app/models/metasploit_data_models/ip_address/v4/nmap.rb +14 -0
- data/app/models/metasploit_data_models/ip_address/v4/range.rb +12 -0
- data/app/models/metasploit_data_models/ip_address/v4/segment/nmap/list.rb +126 -0
- data/app/models/metasploit_data_models/ip_address/v4/segment/nmap/range.rb +12 -0
- data/app/models/metasploit_data_models/ip_address/v4/segment/single.rb +123 -0
- data/app/models/metasploit_data_models/ip_address/v4/segmented.rb +200 -0
- data/app/models/metasploit_data_models/ip_address/v4/single.rb +53 -0
- data/app/models/metasploit_data_models/search/operation/ip_address.rb +60 -0
- data/app/models/metasploit_data_models/search/operator/ip_address.rb +33 -0
- data/app/models/metasploit_data_models/search/visitor/attribute.rb +1 -0
- data/app/models/metasploit_data_models/search/visitor/includes.rb +1 -0
- data/app/models/metasploit_data_models/search/visitor/joins.rb +1 -0
- data/app/models/metasploit_data_models/search/visitor/where.rb +51 -0
- data/config/locales/en.yml +35 -4
- data/lib/metasploit_data_models/ip_address.rb +5 -0
- data/lib/metasploit_data_models/ip_address/cidr.rb +174 -0
- data/lib/metasploit_data_models/ip_address/range.rb +181 -0
- data/lib/metasploit_data_models/match/child.rb +48 -0
- data/lib/metasploit_data_models/match/parent.rb +103 -0
- data/lib/metasploit_data_models/version.rb +4 -4
- data/metasploit_data_models.gemspec +2 -1
- data/spec/app/models/mdm/cred_spec.rb +164 -31
- data/spec/app/models/mdm/service_spec.rb +33 -44
- data/spec/app/models/metasploit_data_models/ip_address/v4/cidr_spec.rb +121 -0
- data/spec/app/models/metasploit_data_models/ip_address/v4/nmap_spec.rb +151 -0
- data/spec/app/models/metasploit_data_models/ip_address/v4/range_spec.rb +300 -0
- data/spec/app/models/metasploit_data_models/ip_address/v4/segment/nmap/list_spec.rb +278 -0
- data/spec/app/models/metasploit_data_models/ip_address/v4/segment/nmap/range_spec.rb +304 -0
- data/spec/app/models/metasploit_data_models/ip_address/v4/segment/segmented_spec.rb +29 -0
- data/spec/app/models/metasploit_data_models/ip_address/v4/segment/single_spec.rb +315 -0
- data/spec/app/models/metasploit_data_models/ip_address/v4/single_spec.rb +183 -0
- data/spec/app/models/metasploit_data_models/search/operation/ip_address_spec.rb +182 -0
- data/spec/app/models/metasploit_data_models/search/operator/ip_address_spec.rb +19 -0
- data/spec/app/models/metasploit_data_models/search/visitor/relation_spec.rb +229 -36
- data/spec/dummy/config/application.rb +1 -1
- data/spec/dummy/db/structure.sql +3011 -0
- data/spec/factories/mdm/services.rb +3 -1
- data/spec/lib/metasploit_data_models/ip_address/cidr_spec.rb +350 -0
- data/spec/lib/metasploit_data_models/ip_address/range_spec.rb +77 -0
- data/spec/lib/metasploit_data_models/match/child_spec.rb +61 -0
- data/spec/lib/metasploit_data_models/match/parent_spec.rb +155 -0
- data/spec/support/matchers/match_regex_exactly.rb +28 -0
- data/spec/support/shared/contexts/rex/text.rb +15 -0
- data/spec/support/shared/examples/metasploit_data_models/search/operation/ipaddress/match.rb +109 -0
- metadata +58 -9
- data/spec/dummy/db/schema.rb +0 -609
@@ -0,0 +1,53 @@
|
|
1
|
+
# A single IPv4 address, in standard, dotted decimal notation.
|
2
|
+
#
|
3
|
+
# @example Dotted Decimal Notation
|
4
|
+
# '1.2.3.4'
|
5
|
+
#
|
6
|
+
class MetasploitDataModels::IPAddress::V4::Single < MetasploitDataModels::IPAddress::V4::Segmented
|
7
|
+
#
|
8
|
+
# Segments
|
9
|
+
#
|
10
|
+
|
11
|
+
segment class_name: 'MetasploitDataModels::IPAddress::V4::Segment::Single'
|
12
|
+
|
13
|
+
#
|
14
|
+
# Instance Methods
|
15
|
+
#
|
16
|
+
|
17
|
+
# Adds `other` IPv4 address to this IPv4 address.
|
18
|
+
#
|
19
|
+
# @return [MetasploitDataModels::IPAddress::V4::Single] a new IPv4 address contain the sum of the two addresses
|
20
|
+
# segments with carries from lower to higher segments.
|
21
|
+
# @raise [TypeError] if `other` isn't the same class.
|
22
|
+
# @raise [ArgmentError] if `self` plus `other` yields an IP address greater than 255.255.255.255.
|
23
|
+
# @see succ
|
24
|
+
def +(other)
|
25
|
+
unless other.is_a? self.class
|
26
|
+
raise TypeError, "Cannot add #{other.class} to #{self.class}"
|
27
|
+
end
|
28
|
+
|
29
|
+
carry = 0
|
30
|
+
sum_segments = []
|
31
|
+
low_to_high_segments = segments.zip(other.segments).reverse
|
32
|
+
|
33
|
+
low_to_high_segments.each do |self_segment, other_segment|
|
34
|
+
segment, carry = self_segment.add_with_carry(other_segment, carry)
|
35
|
+
sum_segments.unshift segment
|
36
|
+
end
|
37
|
+
|
38
|
+
unless carry == 0
|
39
|
+
raise ArgumentError,
|
40
|
+
"#{self} + #{other} is not a valid IP address. It is #{sum_segments.join('.')} with a carry (#{carry})"
|
41
|
+
end
|
42
|
+
|
43
|
+
self.class.new(segments: sum_segments)
|
44
|
+
end
|
45
|
+
|
46
|
+
# The succeeding IPv4 address.
|
47
|
+
#
|
48
|
+
# @see #+
|
49
|
+
# @raise (see #+)
|
50
|
+
def succ
|
51
|
+
self + self.class.new(value: '0.0.0.1')
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# Searches an `inet` column in a PostgreSQL database using
|
2
|
+
# {MetasploitDataModels::IPAddress::V4::Single a standard IPv4 address},
|
3
|
+
# {MetasploitDataModels::IPAddress::V4::CIDR an IPv4 CIDR block}, or
|
4
|
+
# {MetasploitDataModels::IPAddress::V4::Range an IPv4 address range}.
|
5
|
+
class MetasploitDataModels::Search::Operation::IPAddress < Metasploit::Model::Search::Operation::Base
|
6
|
+
include MetasploitDataModels::Match::Parent
|
7
|
+
|
8
|
+
#
|
9
|
+
# Match Children
|
10
|
+
#
|
11
|
+
|
12
|
+
# in order of precedence, so simpler single IPv4 addresses are matched before the more complex ranges which may
|
13
|
+
# degenerate to equivalent formatted value
|
14
|
+
match_children_named %w{
|
15
|
+
MetasploitDataModels::IPAddress::V4::Single
|
16
|
+
MetasploitDataModels::IPAddress::V4::CIDR
|
17
|
+
MetasploitDataModels::IPAddress::V4::Range
|
18
|
+
}
|
19
|
+
|
20
|
+
#
|
21
|
+
#
|
22
|
+
# Validations
|
23
|
+
#
|
24
|
+
#
|
25
|
+
|
26
|
+
#
|
27
|
+
# Validation Methods
|
28
|
+
#
|
29
|
+
|
30
|
+
validate :value_valid
|
31
|
+
|
32
|
+
#
|
33
|
+
# Attribute Validations
|
34
|
+
#
|
35
|
+
|
36
|
+
validates :value,
|
37
|
+
presence: true
|
38
|
+
|
39
|
+
#
|
40
|
+
# Instance Method
|
41
|
+
#
|
42
|
+
|
43
|
+
# @param formatted_value [#to_s]
|
44
|
+
def value=(formatted_value)
|
45
|
+
@value = match_child(formatted_value) || formatted_value
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# Validates that `#value` is valid.
|
51
|
+
#
|
52
|
+
# @return [void]
|
53
|
+
def value_valid
|
54
|
+
if value.present?
|
55
|
+
unless value.respond_to?(:valid?) && value.valid?
|
56
|
+
errors.add(:value, :invalid)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Operator for `inet` columns in a PostgreSQL database, which operates on formatted values using
|
2
|
+
# {MetasploitDataModels::Search::Operation::IPAddress}.
|
3
|
+
class MetasploitDataModels::Search::Operator::IPAddress < Metasploit::Model::Search::Operator::Single
|
4
|
+
#
|
5
|
+
# Attributes
|
6
|
+
#
|
7
|
+
|
8
|
+
# @!attribute [r] attribute
|
9
|
+
# The attribute on `Metasploit::Model::Search::Operator::Base#klass` that is searchable.
|
10
|
+
#
|
11
|
+
# @return [Symbol] the attribute name
|
12
|
+
attr_accessor :attribute
|
13
|
+
|
14
|
+
#
|
15
|
+
# Validations
|
16
|
+
#
|
17
|
+
|
18
|
+
validates :attribute,
|
19
|
+
presence: true
|
20
|
+
|
21
|
+
#
|
22
|
+
# Instance Methods
|
23
|
+
#
|
24
|
+
|
25
|
+
alias_method :name, :attribute
|
26
|
+
|
27
|
+
# The class used for `Metasploit::Model::Search::Operator::Single#operate_on`.
|
28
|
+
#
|
29
|
+
# @return [String] `'MetasploitDataModels::Search::Operation::IPAddress'`
|
30
|
+
def operation_class_name
|
31
|
+
@operation_class_name ||= 'MetasploitDataModels::Search::Operation::IPAddress'
|
32
|
+
end
|
33
|
+
end
|
@@ -7,6 +7,7 @@ class MetasploitDataModels::Search::Visitor::Attribute
|
|
7
7
|
end
|
8
8
|
|
9
9
|
visit 'Metasploit::Model::Search::Operator::Attribute',
|
10
|
+
'MetasploitDataModels::Search::Operator::IPAddress',
|
10
11
|
'MetasploitDataModels::Search::Operator::Port::List' do |operator|
|
11
12
|
table = operator.klass.arel_table
|
12
13
|
table[operator.attribute]
|
@@ -23,6 +23,7 @@ class MetasploitDataModels::Search::Visitor::Includes
|
|
23
23
|
end
|
24
24
|
|
25
25
|
visit 'Metasploit::Model::Search::Operator::Attribute',
|
26
|
+
'MetasploitDataModels::Search::Operator::IPAddress',
|
26
27
|
'MetasploitDataModels::Search::Operator::Port::List' do |_operator|
|
27
28
|
[]
|
28
29
|
end
|
@@ -45,6 +45,45 @@ class MetasploitDataModels::Search::Visitor::Where
|
|
45
45
|
attribute.matches(match_value)
|
46
46
|
end
|
47
47
|
|
48
|
+
visit 'MetasploitDataModels::IPAddress::CIDR' do |cidr|
|
49
|
+
cast_to_inet "#{cidr.address}/#{cidr.prefix_length}"
|
50
|
+
end
|
51
|
+
|
52
|
+
visit 'MetasploitDataModels::IPAddress::Range' do |ip_address_range|
|
53
|
+
range = ip_address_range.value
|
54
|
+
|
55
|
+
begin_node = visit range.begin
|
56
|
+
end_node = visit range.end
|
57
|
+
|
58
|
+
# AND nodes should be created with a list
|
59
|
+
Arel::Nodes::And.new([begin_node, end_node])
|
60
|
+
end
|
61
|
+
|
62
|
+
visit 'MetasploitDataModels::IPAddress::V4::Single' do |ip_address|
|
63
|
+
cast_to_inet(ip_address.to_s)
|
64
|
+
end
|
65
|
+
|
66
|
+
visit 'MetasploitDataModels::Search::Operation::IPAddress' do |operation|
|
67
|
+
attribute = attribute_visitor.visit operation.operator
|
68
|
+
value = operation.value
|
69
|
+
value_node = visit value
|
70
|
+
|
71
|
+
case value
|
72
|
+
when MetasploitDataModels::IPAddress::CIDR
|
73
|
+
Arel::Nodes::InfixOperation.new(
|
74
|
+
'<<',
|
75
|
+
attribute,
|
76
|
+
value_node
|
77
|
+
)
|
78
|
+
when MetasploitDataModels::IPAddress::Range
|
79
|
+
Arel::Nodes::Between.new(attribute, value_node)
|
80
|
+
when MetasploitDataModels::IPAddress::V4::Single
|
81
|
+
Arel::Nodes::Equality.new(attribute, value_node)
|
82
|
+
else
|
83
|
+
raise TypeError, "Don't know how to handle #{value.class}"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
48
87
|
visit 'MetasploitDataModels::Search::Operation::Port::Range' do |range_operation|
|
49
88
|
attribute = attribute_visitor.visit range_operation.operator
|
50
89
|
|
@@ -69,5 +108,17 @@ class MetasploitDataModels::Search::Visitor::Where
|
|
69
108
|
@method_visitor ||= MetasploitDataModels::Search::Visitor::Method.new
|
70
109
|
end
|
71
110
|
|
111
|
+
private
|
112
|
+
|
113
|
+
# Casts a literal string to INET in AREL.
|
114
|
+
#
|
115
|
+
# @return [Arel::Nodes::NamedFunction]
|
116
|
+
def cast_to_inet(string)
|
117
|
+
cast_argument = Arel::Nodes::As.new(string, Arel::Nodes::SqlLiteral.new('INET'))
|
118
|
+
Arel::Nodes::NamedFunction.new('CAST', [cast_argument])
|
119
|
+
end
|
120
|
+
|
121
|
+
public
|
122
|
+
|
72
123
|
Metasploit::Concern.run(self)
|
73
124
|
end
|
data/config/locales/en.yml
CHANGED
@@ -5,12 +5,39 @@ en:
|
|
5
5
|
messages:
|
6
6
|
# have to duplicate activerecord.model.errors.message.taken because of the different i18n_scope
|
7
7
|
taken: "has already been taken"
|
8
|
-
|
9
8
|
models:
|
10
|
-
metasploit_data_models/
|
9
|
+
metasploit_data_models/ip_address/range:
|
11
10
|
attributes:
|
12
|
-
|
13
|
-
|
11
|
+
value:
|
12
|
+
order: "beginning of range (%{begin}) cannot be after end of range (%{end})"
|
13
|
+
metasploit_data_models/ip_address/v4/segmented:
|
14
|
+
attributes:
|
15
|
+
segments:
|
16
|
+
segment_invalid: "has invalid segment (%{segment}) at index %{index}"
|
17
|
+
wrong_length: "is the wrong length (should be %{count} segments)"
|
18
|
+
metasploit_data_models/ip_address/v4/segment/nmap/list:
|
19
|
+
attributes:
|
20
|
+
value:
|
21
|
+
array: "is not an Array"
|
22
|
+
element: "has invalid element (%{element}) at index %{index}"
|
23
|
+
metasploit_data_models/ip_address/v4/segment/nmap/range:
|
24
|
+
attributes:
|
25
|
+
value:
|
26
|
+
order: "beginning of range (%{begin}) cannot be after end of range (%{end})"
|
27
|
+
metasploit_data_models/search/operation/ip_address/invalid_range:
|
28
|
+
attributes:
|
29
|
+
value:
|
30
|
+
format: "does not match any known formats (IPv4, IPv4 CIDR, IPv4 NMAP, IPv4 Range, IPv6, IPv6 CIDR, IPv6 Range)"
|
31
|
+
metasploit_data_models/search/operation/ip_address/v4/range:
|
32
|
+
attributes:
|
33
|
+
value:
|
34
|
+
extreme: "%{extreme} (%{extreme_value}) is not an IPAddr"
|
35
|
+
order: "beginning of range (%{begin}) cannot be after end of range (%{end})"
|
36
|
+
type: "is not a range"
|
37
|
+
metasploit_data_models/search/operation/ip_address/v4/single:
|
38
|
+
attributes:
|
39
|
+
value:
|
40
|
+
format: "does not match IPv4 dotted decimal format"
|
14
41
|
metasploit_data_models/search/operation/port/range:
|
15
42
|
attributes:
|
16
43
|
value:
|
@@ -21,3 +48,7 @@ en:
|
|
21
48
|
value:
|
22
49
|
order: "is not in order: begin (%{begin} is greater than end (%{end})."
|
23
50
|
range: "is not a range"
|
51
|
+
metasploit_data_models/search/operator/multitext:
|
52
|
+
attributes:
|
53
|
+
operator_names:
|
54
|
+
too_short: "is too short (minimum is %{count} operator names)"
|
@@ -0,0 +1,174 @@
|
|
1
|
+
# Common behavior for Class-InterDomain Routing (`<address>/<prefix-length>`) notation under
|
2
|
+
# {MetasploitDataModels::IPAddress},
|
3
|
+
module MetasploitDataModels::IPAddress::CIDR
|
4
|
+
# so that translations for errors messages can be filed under metasploit_data_models/ip_address/cidr
|
5
|
+
extend ActiveModel::Naming
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
#
|
9
|
+
# CONSTANTS
|
10
|
+
#
|
11
|
+
|
12
|
+
# Separator between the {#address} and {#prefix_length}
|
13
|
+
SEPARATOR = '/'
|
14
|
+
|
15
|
+
#
|
16
|
+
# Attributes
|
17
|
+
#
|
18
|
+
|
19
|
+
# @!attribute address
|
20
|
+
# The IP address being masked by {#prefix_length} `1` bits.
|
21
|
+
#
|
22
|
+
# @return [Object] an instance of {address_class}
|
23
|
+
attr_reader :address
|
24
|
+
|
25
|
+
# @!attribute prefix_length
|
26
|
+
# The significant number of bits in {#address}.
|
27
|
+
#
|
28
|
+
# @return [Integer] number of `1` bits in the netmask of {#address}
|
29
|
+
attr_reader :prefix_length
|
30
|
+
|
31
|
+
included do
|
32
|
+
include ActiveModel::Validations
|
33
|
+
|
34
|
+
#
|
35
|
+
#
|
36
|
+
# Validations
|
37
|
+
#
|
38
|
+
#
|
39
|
+
|
40
|
+
#
|
41
|
+
# Validation Methods
|
42
|
+
#
|
43
|
+
|
44
|
+
validate :address_valid
|
45
|
+
|
46
|
+
#
|
47
|
+
# Attribute Validations
|
48
|
+
#
|
49
|
+
|
50
|
+
validates :address,
|
51
|
+
presence: true
|
52
|
+
end
|
53
|
+
|
54
|
+
# Class methods added to the including `Class`.
|
55
|
+
module ClassMethods
|
56
|
+
include MetasploitDataModels::Match::Child
|
57
|
+
|
58
|
+
#
|
59
|
+
# Attributes
|
60
|
+
#
|
61
|
+
|
62
|
+
# @!attribute address_class
|
63
|
+
# The Class` whose instance are usd for {MetasploitDataModels::IPAddress::CIDR#address}.
|
64
|
+
#
|
65
|
+
# @return [Class]
|
66
|
+
attr_reader :address_class
|
67
|
+
|
68
|
+
#
|
69
|
+
# Methods
|
70
|
+
#
|
71
|
+
|
72
|
+
# @note `address_class` must respond to `#segment_class` and `#segment_count` so {#maximum_prefix_length} can be
|
73
|
+
# calculated.
|
74
|
+
#
|
75
|
+
# Sets up the address class and allowed {#maximum_prefix_length} for the including `Class`.
|
76
|
+
#
|
77
|
+
# @param options [Hash{Symbol => Class}]
|
78
|
+
# @option options [Class, #segment_class, #segment_count] :address_class The `Class` whose instances will be used
|
79
|
+
# for {#address}.
|
80
|
+
def cidr(options={})
|
81
|
+
options.assert_valid_keys(:address_class)
|
82
|
+
|
83
|
+
@address_class = options.fetch(:address_class)
|
84
|
+
|
85
|
+
#
|
86
|
+
# Validations
|
87
|
+
#
|
88
|
+
|
89
|
+
validates :prefix_length,
|
90
|
+
numericality: {
|
91
|
+
only_integer: true,
|
92
|
+
greater_than_or_equal_to: 0,
|
93
|
+
less_than_or_equal_to: maximum_prefix_length
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
# Regular expression that matches a string that contains only a CIDR IP address.
|
98
|
+
#
|
99
|
+
# @return [Regexp]
|
100
|
+
def match_regexp
|
101
|
+
@match_regexp ||= /\A#{regexp}\z/
|
102
|
+
end
|
103
|
+
|
104
|
+
# The maximum number of bits in a prefix for the {#address_class}.
|
105
|
+
#
|
106
|
+
# @return [Integer] the number of bits across all segments of {#address_class}.
|
107
|
+
def maximum_prefix_length
|
108
|
+
@maximum_prefix_length ||= address_class.segment_count * address_class.segment_class.bits
|
109
|
+
end
|
110
|
+
|
111
|
+
# Regular expression that matches a portion of string that contains a CIDR IP address.
|
112
|
+
#
|
113
|
+
# @return [Regexp]
|
114
|
+
def regexp
|
115
|
+
@regexp ||= /(?<address>#{address_class.regexp})#{Regexp.escape(SEPARATOR)}(?<prefix_length>\d+)/
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
#
|
120
|
+
# Instance Methods
|
121
|
+
#
|
122
|
+
|
123
|
+
# Set {#address}.
|
124
|
+
#
|
125
|
+
# @param formatted_address [#to_s]
|
126
|
+
def address=(formatted_address)
|
127
|
+
@address = self.class.address_class.new(value: formatted_address)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Set {#prefix_length}.
|
131
|
+
#
|
132
|
+
# @param formatted_prefix_length [#to_s]
|
133
|
+
def prefix_length=(formatted_prefix_length)
|
134
|
+
@prefix_length_before_type_cast = formatted_prefix_length
|
135
|
+
|
136
|
+
begin
|
137
|
+
# use Integer() instead of String#to_i as String#to_i will ignore trailing letters (i.e. '1two' -> 1) and turn all
|
138
|
+
# string without an integer in it to 0.
|
139
|
+
@prefix_length = Integer(formatted_prefix_length.to_s)
|
140
|
+
rescue ArgumentError
|
141
|
+
@prefix_length = formatted_prefix_length
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# The formatted_prefix_length passed to {#prefix_length=}
|
146
|
+
#
|
147
|
+
# @return [#to_s]
|
148
|
+
def prefix_length_before_type_cast
|
149
|
+
@prefix_length_before_type_cast
|
150
|
+
end
|
151
|
+
|
152
|
+
# Parses the `formatted_value` into an {#address} and {#prefix_length}.
|
153
|
+
#
|
154
|
+
# @param formatted_value [#to_s]
|
155
|
+
def value=(formatted_value)
|
156
|
+
formatted_address, formatted_prefix_length = formatted_value.to_s.split(SEPARATOR, 2)
|
157
|
+
|
158
|
+
self.address = formatted_address
|
159
|
+
self.prefix_length = formatted_prefix_length
|
160
|
+
|
161
|
+
[address, prefix_length]
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
# Validates that {#address} is valid.
|
167
|
+
#
|
168
|
+
# @return [void]
|
169
|
+
def address_valid
|
170
|
+
if address && !address.valid?
|
171
|
+
errors.add(:address, :invalid)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|