cfn-nag 0.0.44 → 0.1.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/bin/cfn_nag +27 -11
- data/bin/cfn_nag_rules +5 -4
- data/bin/cfn_nag_scan +29 -0
- data/lib/cfn-nag.rb +3 -0
- data/lib/cfn-nag/cfn_nag.rb +115 -0
- data/lib/cfn-nag/custom_rule_loader.rb +72 -0
- data/lib/cfn-nag/custom_rules/CloudFormationAuthenticationRule.rb +28 -0
- data/lib/cfn-nag/custom_rules/CloudFrontDistributionAccessLoggingRule.rb +24 -0
- data/lib/cfn-nag/custom_rules/EbsVolumeHasSseRule.rb +24 -0
- data/lib/cfn-nag/custom_rules/ElasticLoadBalancerAccessLoggingRule.rb +24 -0
- data/lib/cfn-nag/custom_rules/IamManagedPolicyNotActionRule.rb +25 -0
- data/lib/cfn-nag/custom_rules/IamManagedPolicyNotResourceRule.rb +25 -0
- data/lib/cfn-nag/custom_rules/IamManagedPolicyWildcardActionRule.rb +25 -0
- data/lib/cfn-nag/custom_rules/IamManagedPolicyWildcardResourceRule.rb +25 -0
- data/lib/cfn-nag/custom_rules/IamPolicyNotActionRule.rb +25 -0
- data/lib/cfn-nag/custom_rules/IamPolicyNotResourceRule.rb +25 -0
- data/lib/cfn-nag/custom_rules/IamPolicyWildcardActionRule.rb +25 -0
- data/lib/cfn-nag/custom_rules/IamPolicyWildcardResourceRule.rb +25 -0
- data/lib/cfn-nag/custom_rules/IamRoleNotActionOnPermissionsPolicyRule.rb +28 -0
- data/lib/cfn-nag/custom_rules/IamRoleNotActionOnTrustPolicyRule.rb +25 -0
- data/lib/cfn-nag/custom_rules/IamRoleNotPrincipalOnTrustPolicyRule.rb +25 -0
- data/lib/cfn-nag/custom_rules/IamRoleNotResourceOnPermissionsPolicyRule.rb +28 -0
- data/lib/cfn-nag/custom_rules/IamRoleWildcardActionOnPermissionsPolicyRule.rb +28 -0
- data/lib/cfn-nag/custom_rules/IamRoleWildcardActionOnTrustPolicyRule.rb +27 -0
- data/lib/cfn-nag/custom_rules/IamRoleWildcardResourceOnPermissionsPolicyRule.rb +28 -0
- data/lib/cfn-nag/custom_rules/LambdaPermissionInvokeFunctionActionRule.rb +24 -0
- data/lib/cfn-nag/custom_rules/LambdaPermissionWildcardPrincipalRule.rb +24 -0
- data/lib/cfn-nag/custom_rules/ManagedPolicyOnUserRule.rb +24 -0
- data/lib/cfn-nag/custom_rules/PolicyOnUserRule.rb +24 -0
- data/lib/cfn-nag/custom_rules/S3BucketPolicyNotActionRule.rb +25 -0
- data/lib/cfn-nag/custom_rules/S3BucketPolicyNotPrincipalRule.rb +25 -0
- data/lib/cfn-nag/custom_rules/S3BucketPolicyWildcardActionRule.rb +30 -0
- data/lib/cfn-nag/custom_rules/S3BucketPolicyWildcardPrincipalRule.rb +29 -0
- data/lib/cfn-nag/custom_rules/S3BucketPublicReadAclRule.rb +29 -0
- data/lib/cfn-nag/custom_rules/S3BucketPublicReadWriteAclRule.rb +29 -0
- data/lib/cfn-nag/custom_rules/SecurityGroupEgressOpenToWorldRule.rb +39 -0
- data/lib/cfn-nag/custom_rules/SecurityGroupEgressPortRangeRule.rb +38 -0
- data/lib/cfn-nag/custom_rules/SecurityGroupIngressCidrNon32Rule.rb +40 -0
- data/lib/cfn-nag/custom_rules/SecurityGroupIngressOpenToWorldRule.rb +39 -0
- data/lib/cfn-nag/custom_rules/SecurityGroupIngressPortRangeRule.rb +38 -0
- data/lib/{custom_rules/security_group_missing_egress.rb → cfn-nag/custom_rules/SecurityGroupMissingEgressRule.rb} +6 -12
- data/lib/cfn-nag/custom_rules/SnsTopicPolicyNotActionRule.rb +25 -0
- data/lib/cfn-nag/custom_rules/SnsTopicPolicyNotPrincipalRule.rb +26 -0
- data/lib/cfn-nag/custom_rules/SnsTopicPolicyWildcardPrincipalRule.rb +29 -0
- data/lib/cfn-nag/custom_rules/SqsQueuePolicyNotActionRule.rb +25 -0
- data/lib/cfn-nag/custom_rules/SqsQueuePolicyNotPrincipalRule.rb +25 -0
- data/lib/cfn-nag/custom_rules/SqsQueuePolicyWildcardActionRule.rb +30 -0
- data/lib/cfn-nag/custom_rules/SqsQueuePolicyWildcardPrincipalRule.rb +29 -0
- data/lib/cfn-nag/custom_rules/UserHasInlinePolicyRule.rb +25 -0
- data/lib/cfn-nag/custom_rules/UserMissingGroupRule.rb +28 -0
- data/lib/cfn-nag/custom_rules/WafWebAclDefaultActionRule.rb +34 -0
- data/lib/cfn-nag/custom_rules/base.rb +28 -0
- data/lib/cfn-nag/custom_rules/unencrypted_s3_put_allowed.rb +58 -0
- data/lib/{profile.rb → cfn-nag/profile.rb} +0 -1
- data/lib/{profile_loader.rb → cfn-nag/profile_loader.rb} +2 -2
- data/lib/{result_view → cfn-nag/result_view}/json_results.rb +0 -0
- data/lib/{result_view → cfn-nag/result_view}/rules_view.rb +0 -0
- data/lib/{result_view → cfn-nag/result_view}/simple_stdout_results.rb +5 -12
- data/lib/cfn-nag/rule_definition.rb +36 -0
- data/lib/cfn-nag/rule_dumper.rb +23 -0
- data/lib/cfn-nag/rule_registry.rb +43 -0
- data/lib/cfn-nag/template_discovery.rb +24 -0
- data/lib/cfn-nag/violation.rb +58 -0
- metadata +79 -36
- data/lib/cfn_nag.rb +0 -219
- data/lib/custom_rule_loader.rb +0 -64
- data/lib/custom_rules/unencrypted_s3_put_allowed.rb +0 -58
- data/lib/custom_rules/user_missing_group.rb +0 -34
- data/lib/json_rules/basic_rules.rb +0 -49
- data/lib/json_rules/cfn_rules.rb +0 -4
- data/lib/json_rules/cidr_rules.rb +0 -77
- data/lib/json_rules/cloudfront_rules.rb +0 -4
- data/lib/json_rules/ebs_rules.rb +0 -4
- data/lib/json_rules/iam_policy_rules.rb +0 -153
- data/lib/json_rules/iam_user_rules.rb +0 -15
- data/lib/json_rules/lambda_rules.rb +0 -9
- data/lib/json_rules/loadbalancer_rules.rb +0 -9
- data/lib/json_rules/port_rules.rb +0 -33
- data/lib/json_rules/s3_bucket_rules.rb +0 -51
- data/lib/json_rules/sns_rules.rb +0 -29
- data/lib/json_rules/sqs_rules.rb +0 -25
- data/lib/model/action_parser.rb +0 -27
- data/lib/model/cfn_model.rb +0 -182
- data/lib/model/iam_user_parser.rb +0 -34
- data/lib/model/parser_registry.rb +0 -31
- data/lib/model/s3_bucket_policy.rb +0 -25
- data/lib/model/s3_bucket_policy_parser.rb +0 -28
- data/lib/model/security_group_parser.rb +0 -59
- data/lib/rule.rb +0 -208
- data/lib/rule_registry.rb +0 -45
- data/lib/violation.rb +0 -41
@@ -1,25 +0,0 @@
|
|
1
|
-
class S3BucketPolicy
|
2
|
-
attr_accessor :logical_resource_id
|
3
|
-
|
4
|
-
attr_reader :statements
|
5
|
-
|
6
|
-
def initialize
|
7
|
-
@statements = []
|
8
|
-
end
|
9
|
-
|
10
|
-
def add_statement(statement_hash)
|
11
|
-
statements << statement_hash
|
12
|
-
end
|
13
|
-
|
14
|
-
def self.condition_includes?(statement, condition_hash)
|
15
|
-
if statement['Condition'].nil?
|
16
|
-
false
|
17
|
-
else
|
18
|
-
if statement['Condition'].is_a? Hash
|
19
|
-
statement['Condition'] == condition_hash
|
20
|
-
else
|
21
|
-
statement['Condition'].include? condition_hash
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
@@ -1,28 +0,0 @@
|
|
1
|
-
require_relative 's3_bucket_policy'
|
2
|
-
|
3
|
-
|
4
|
-
class S3BucketPolicyParser
|
5
|
-
|
6
|
-
def parse(resource_name, resource_json)
|
7
|
-
properties = resource_json['Properties']
|
8
|
-
bucket_policy = S3BucketPolicy.new
|
9
|
-
|
10
|
-
bucket_policy.logical_resource_id = resource_name
|
11
|
-
|
12
|
-
unless properties.nil?
|
13
|
-
policy_document = properties['PolicyDocument']
|
14
|
-
unless policy_document.nil?
|
15
|
-
unless policy_document['Statement'].nil?
|
16
|
-
if policy_document['Statement'].is_a? Array
|
17
|
-
policy_document['Statement'].each { |statement | bucket_policy.add_statement(statement)}
|
18
|
-
else
|
19
|
-
bucket_policy.add_statement(policy_document['Statement'])
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
bucket_policy
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
@@ -1,59 +0,0 @@
|
|
1
|
-
require_relative 'cfn_model'
|
2
|
-
|
3
|
-
class SecurityGroupParser
|
4
|
-
|
5
|
-
# precondition: properties are actually there... other validator takes care
|
6
|
-
def parse(resource_name, resource_json)
|
7
|
-
properties = resource_json['Properties']
|
8
|
-
security_group = SecurityGroup.new
|
9
|
-
|
10
|
-
parse_ingress_rules(security_group, properties)
|
11
|
-
|
12
|
-
parse_egress_rules(security_group, properties)
|
13
|
-
|
14
|
-
security_group.vpc_id = properties['VpcId']
|
15
|
-
security_group.group_description = properties['GroupDescription']
|
16
|
-
security_group.logical_resource_id = resource_name
|
17
|
-
|
18
|
-
security_group
|
19
|
-
end
|
20
|
-
|
21
|
-
private
|
22
|
-
|
23
|
-
def parse_ingress_rules(security_group, properties)
|
24
|
-
unless properties['SecurityGroupIngress'].nil?
|
25
|
-
if properties['SecurityGroupIngress'].is_a? Array
|
26
|
-
properties['SecurityGroupIngress'].each do |ingress_json|
|
27
|
-
security_group.add_ingress_rule ingress_json
|
28
|
-
end
|
29
|
-
elsif properties['SecurityGroupIngress'].is_a? Hash
|
30
|
-
security_group.add_ingress_rule properties['SecurityGroupIngress']
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def parse_egress_rules(security_group, properties)
|
36
|
-
unless properties['SecurityGroupEgress'].nil?
|
37
|
-
if properties['SecurityGroupEgress'].is_a? Array
|
38
|
-
properties['SecurityGroupEgress'].each do |egress_json|
|
39
|
-
security_group.add_egress_rule egress_json
|
40
|
-
end
|
41
|
-
elsif properties['SecurityGroupEgress'].is_a? Hash
|
42
|
-
security_group.add_egress_rule properties['SecurityGroupEgress']
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
end
|
48
|
-
|
49
|
-
class SecurityGroupXgressParser
|
50
|
-
|
51
|
-
def parse(resource_name, resource_json)
|
52
|
-
xgress = {}
|
53
|
-
xgress['logical_resource_id'] = resource_name
|
54
|
-
resource_json['Properties'].each_pair do |key, value|
|
55
|
-
xgress[key] = value
|
56
|
-
end
|
57
|
-
xgress
|
58
|
-
end
|
59
|
-
end
|
data/lib/rule.rb
DELETED
@@ -1,208 +0,0 @@
|
|
1
|
-
require 'logging'
|
2
|
-
require_relative 'violation'
|
3
|
-
|
4
|
-
module Rule
|
5
|
-
attr_accessor :input_json
|
6
|
-
|
7
|
-
# jq preamble to spit out Resources but as an array of key-value pairs
|
8
|
-
# can be used in jq rule definition but... this is probably reducing replication at the cost of opaqueness
|
9
|
-
def resources
|
10
|
-
'.Resources|with_entries(.value.LogicalResourceId = .key)[]'
|
11
|
-
end
|
12
|
-
|
13
|
-
# jq to filter CloudFormation resources by Type
|
14
|
-
# can be used in jq rule definition but... this is probably reducing replication at the cost of opaqueness
|
15
|
-
def resources_by_type(resource)
|
16
|
-
"#{resources}| select(.Type == \"#{resource}\")"
|
17
|
-
end
|
18
|
-
|
19
|
-
def warning(id:, jq:, message:)
|
20
|
-
warning_def = @rule_registry.definition(id: id,
|
21
|
-
type: Violation::WARNING,
|
22
|
-
message: message)
|
23
|
-
|
24
|
-
return if @stop_processing
|
25
|
-
|
26
|
-
Logging.logger['log'].debug jq
|
27
|
-
|
28
|
-
stdout = jq_command(@input_json, jq)
|
29
|
-
result = $?.exitstatus
|
30
|
-
scrape_jq_output_for_error(jq, stdout)
|
31
|
-
|
32
|
-
resource_ids = parse_logical_resource_ids(stdout)
|
33
|
-
new_warnings = resource_ids.size
|
34
|
-
if result == 0 and new_warnings > 0
|
35
|
-
add_violation(id: warning_def.id,
|
36
|
-
type: Violation::WARNING,
|
37
|
-
message: message,
|
38
|
-
logical_resource_ids: resource_ids)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
def raw_fatal_assertion(id:, jq:, message:)
|
43
|
-
failing_rule(id: id,
|
44
|
-
jq_expression: jq,
|
45
|
-
fail_if_found: false,
|
46
|
-
fatal: true,
|
47
|
-
message: message,
|
48
|
-
raw: true)
|
49
|
-
end
|
50
|
-
|
51
|
-
def fatal_assertion(id:, jq:, message:)
|
52
|
-
failing_rule(id: id,
|
53
|
-
jq_expression: jq,
|
54
|
-
fail_if_found: false,
|
55
|
-
fatal: true,
|
56
|
-
message: message)
|
57
|
-
end
|
58
|
-
|
59
|
-
def raw_fatal_violation(id:, jq:, message:)
|
60
|
-
failing_rule(id: id,
|
61
|
-
jq_expression: jq,
|
62
|
-
fail_if_found: true,
|
63
|
-
fatal: true,
|
64
|
-
message: message,
|
65
|
-
raw: true)
|
66
|
-
end
|
67
|
-
|
68
|
-
def fatal_violation(id:, jq:, message:)
|
69
|
-
failing_rule(id: id,
|
70
|
-
jq_expression: jq,
|
71
|
-
fail_if_found: true,
|
72
|
-
fatal: true,
|
73
|
-
message: message)
|
74
|
-
end
|
75
|
-
|
76
|
-
def violation(id:, jq:, message:)
|
77
|
-
failing_rule(id: id,
|
78
|
-
jq_expression: jq,
|
79
|
-
fail_if_found: true,
|
80
|
-
message: message)
|
81
|
-
end
|
82
|
-
|
83
|
-
def assertion(id:, jq:, message:)
|
84
|
-
failing_rule(id: id,
|
85
|
-
jq_expression: jq,
|
86
|
-
fail_if_found: false,
|
87
|
-
message: message)
|
88
|
-
end
|
89
|
-
|
90
|
-
def self.empty?(array)
|
91
|
-
array.nil? or array.size ==0
|
92
|
-
end
|
93
|
-
|
94
|
-
def self.count_warnings(violations)
|
95
|
-
violations.inject(0) do |count, violation|
|
96
|
-
if violation.type == Violation::WARNING
|
97
|
-
if empty?(violation.logical_resource_ids)
|
98
|
-
count += 1
|
99
|
-
else
|
100
|
-
count += violation.logical_resource_ids.size
|
101
|
-
end
|
102
|
-
end
|
103
|
-
count
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
def self.count_failures(violations)
|
108
|
-
violations.inject(0) do |count, violation|
|
109
|
-
if violation.type == Violation::FAILING_VIOLATION
|
110
|
-
if empty?(violation.logical_resource_ids)
|
111
|
-
count += 1
|
112
|
-
else
|
113
|
-
count += violation.logical_resource_ids.size
|
114
|
-
end
|
115
|
-
end
|
116
|
-
count
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
# this is to record a violation as opposed to "registering" a violation
|
121
|
-
#
|
122
|
-
# not super keen on this looking at it after the fact.... @violations
|
123
|
-
# will have to live in the object this Rule is mixed into i.e. CfnNag
|
124
|
-
def add_violation(id:,
|
125
|
-
type:,
|
126
|
-
message:,
|
127
|
-
logical_resource_ids: nil,
|
128
|
-
violating_code: nil)
|
129
|
-
violation = Violation.new(id: id,
|
130
|
-
type: type,
|
131
|
-
message: message,
|
132
|
-
logical_resource_ids: logical_resource_ids,
|
133
|
-
violating_code: violating_code)
|
134
|
-
@violations << violation
|
135
|
-
end
|
136
|
-
|
137
|
-
private
|
138
|
-
|
139
|
-
def parse_logical_resource_ids(stdout)
|
140
|
-
JSON.load(stdout)
|
141
|
-
end
|
142
|
-
|
143
|
-
def scrape_jq_output_for_error(command, stdout)
|
144
|
-
fail "jq rule is likely not correct: #{command}\n\n#{stdout}" if stdout.include? 'jq: error'
|
145
|
-
end
|
146
|
-
|
147
|
-
# fail_if_found: this is false for an assertion, true for a violation. either way this rule ups the "failure" count
|
148
|
-
#
|
149
|
-
# raw: don't try to parse the output in any way. the rule is some kind of oddball so just show what matched and up
|
150
|
-
# failure count by 1
|
151
|
-
#
|
152
|
-
# fatal: if true, any match of the rule causes immediate shutdown to avoid more complicated downstream error checking
|
153
|
-
def failing_rule(id:,
|
154
|
-
jq_expression:,
|
155
|
-
fail_if_found:,
|
156
|
-
message:,
|
157
|
-
fatal: false,
|
158
|
-
raw: false)
|
159
|
-
|
160
|
-
fail_def = @rule_registry.definition(id: id,
|
161
|
-
type: Violation::FAILING_VIOLATION,
|
162
|
-
message: message)
|
163
|
-
|
164
|
-
return if @stop_processing
|
165
|
-
|
166
|
-
Logging.logger['log'].debug jq_expression
|
167
|
-
|
168
|
-
stdout = jq_command(@input_json, jq_expression)
|
169
|
-
result = $?.exitstatus
|
170
|
-
scrape_jq_output_for_error(jq_expression, stdout)
|
171
|
-
if (fail_if_found and result == 0) or
|
172
|
-
(not fail_if_found and result != 0)
|
173
|
-
|
174
|
-
if raw
|
175
|
-
add_violation(id: fail_def.id,
|
176
|
-
type: Violation::FAILING_VIOLATION,
|
177
|
-
message: message,
|
178
|
-
violating_code: stdout)
|
179
|
-
|
180
|
-
if fatal
|
181
|
-
@stop_processing = true
|
182
|
-
end
|
183
|
-
else
|
184
|
-
resource_ids = parse_logical_resource_ids(stdout)
|
185
|
-
|
186
|
-
if resource_ids.size > 0
|
187
|
-
add_violation(id: fail_def.id,
|
188
|
-
type: Violation::FAILING_VIOLATION,
|
189
|
-
message: message,
|
190
|
-
logical_resource_ids: resource_ids)
|
191
|
-
|
192
|
-
if fatal
|
193
|
-
@stop_processing = true
|
194
|
-
end
|
195
|
-
end
|
196
|
-
end
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
# the -e will return an exit code
|
201
|
-
def jq_command(input_json, jq_expression)
|
202
|
-
IO.popen(['jq', jq_expression, '-e'], 'r+', {:err=>[:child, :out]}) do |pipe|
|
203
|
-
pipe << input_json
|
204
|
-
pipe.close_write
|
205
|
-
pipe.readlines.join
|
206
|
-
end
|
207
|
-
end
|
208
|
-
end
|
data/lib/rule_registry.rb
DELETED
@@ -1,45 +0,0 @@
|
|
1
|
-
require_relative 'violation'
|
2
|
-
|
3
|
-
class RuleRegistry
|
4
|
-
|
5
|
-
attr_reader :rules
|
6
|
-
|
7
|
-
def initialize
|
8
|
-
@rules = []
|
9
|
-
end
|
10
|
-
|
11
|
-
def definition(id:,
|
12
|
-
type:,
|
13
|
-
message:)
|
14
|
-
violation_def = Violation.new(id: id,
|
15
|
-
type: type,
|
16
|
-
message: message)
|
17
|
-
existing_def = @rules.find { |definition| definition == violation_def }
|
18
|
-
|
19
|
-
if existing_def.nil?
|
20
|
-
add_rule violation_def
|
21
|
-
else
|
22
|
-
existing_def
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
# FATAL applies to multiple rules
|
27
|
-
def by_id(id)
|
28
|
-
@rules.select { |rule| rule.id == id }
|
29
|
-
end
|
30
|
-
|
31
|
-
def warnings
|
32
|
-
@rules.select { |rule| rule.type == Violation::WARNING }
|
33
|
-
end
|
34
|
-
|
35
|
-
def failings
|
36
|
-
@rules.select { |rule| rule.type == Violation::FAILING_VIOLATION }
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
def add_rule(violation_def)
|
42
|
-
@rules << violation_def
|
43
|
-
violation_def
|
44
|
-
end
|
45
|
-
end
|
data/lib/violation.rb
DELETED
@@ -1,41 +0,0 @@
|
|
1
|
-
require 'json'
|
2
|
-
|
3
|
-
class Violation
|
4
|
-
WARNING = 'WARN'
|
5
|
-
FAILING_VIOLATION = 'FAIL'
|
6
|
-
|
7
|
-
attr_reader :id, :type, :message, :logical_resource_ids, :violating_code
|
8
|
-
|
9
|
-
def initialize(id:,
|
10
|
-
type:,
|
11
|
-
message:,
|
12
|
-
logical_resource_ids: nil,
|
13
|
-
violating_code: nil)
|
14
|
-
@id = id
|
15
|
-
@type = type
|
16
|
-
@message = message
|
17
|
-
@logical_resource_ids = logical_resource_ids
|
18
|
-
@violating_code = violating_code
|
19
|
-
|
20
|
-
fail if @type.nil?
|
21
|
-
fail if @message.nil?
|
22
|
-
end
|
23
|
-
|
24
|
-
def to_s
|
25
|
-
puts "#{@id} #{@type} #{@message} #{@logical_resource_ids} #{@violating_code}"
|
26
|
-
end
|
27
|
-
|
28
|
-
def to_h
|
29
|
-
{
|
30
|
-
id: @id,
|
31
|
-
type: @type,
|
32
|
-
message: @message,
|
33
|
-
logical_resource_ids: @logical_resource_ids,
|
34
|
-
violating_code: @violating_code
|
35
|
-
}
|
36
|
-
end
|
37
|
-
|
38
|
-
def ==(other_violation)
|
39
|
-
other_violation.class == self.class && other_violation.to_h == to_h
|
40
|
-
end
|
41
|
-
end
|