metasploit_data_models 0.18.1 → 0.19.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- 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
|