cfn-guardian 0.1.0 → 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.dockerignore +1 -0
- data/Dockerfile +19 -0
- data/Gemfile.lock +31 -13
- data/README.md +441 -42
- data/cfn-guardian.gemspec +6 -2
- data/lib/cfnguardian.rb +301 -27
- data/lib/cfnguardian/cloudwatch.rb +121 -0
- data/lib/cfnguardian/codecommit.rb +54 -0
- data/lib/cfnguardian/codepipeline.rb +138 -0
- data/lib/cfnguardian/compile.rb +58 -17
- data/lib/cfnguardian/config/defaults.yaml +94 -0
- data/lib/cfnguardian/display_formatter.rb +164 -0
- data/lib/cfnguardian/drift.rb +79 -0
- data/lib/cfnguardian/log.rb +0 -1
- data/lib/cfnguardian/models/alarm.rb +98 -36
- data/lib/cfnguardian/models/check.rb +103 -26
- data/lib/cfnguardian/models/composite.rb +21 -0
- data/lib/cfnguardian/models/event.rb +164 -40
- data/lib/cfnguardian/models/metric_filter.rb +28 -0
- data/lib/cfnguardian/resources/application_targetgroup.rb +2 -0
- data/lib/cfnguardian/resources/base.rb +38 -16
- data/lib/cfnguardian/resources/ecs_service.rb +2 -2
- data/lib/cfnguardian/resources/http.rb +16 -1
- data/lib/cfnguardian/resources/internal_http.rb +74 -0
- data/lib/cfnguardian/resources/internal_port.rb +33 -0
- data/lib/cfnguardian/resources/internal_sftp.rb +58 -0
- data/lib/cfnguardian/resources/log_group.rb +26 -0
- data/lib/cfnguardian/resources/network_targetgroup.rb +1 -0
- data/lib/cfnguardian/resources/port.rb +25 -0
- data/lib/cfnguardian/resources/rds_instance.rb +2 -0
- data/lib/cfnguardian/resources/sftp.rb +50 -0
- data/lib/cfnguardian/resources/sql.rb +1 -1
- data/lib/cfnguardian/resources/tls.rb +66 -0
- data/lib/cfnguardian/s3.rb +3 -2
- data/lib/cfnguardian/stacks/main.rb +86 -65
- data/lib/cfnguardian/stacks/resources.rb +81 -42
- data/lib/cfnguardian/string.rb +12 -0
- data/lib/cfnguardian/version.rb +1 -1
- metadata +102 -5
@@ -0,0 +1,21 @@
|
|
1
|
+
module CfnGuardian
|
2
|
+
module Models
|
3
|
+
class Composite
|
4
|
+
|
5
|
+
attr_reader :type
|
6
|
+
attr_accessor :name,
|
7
|
+
:description,
|
8
|
+
:rule,
|
9
|
+
:alarm_action
|
10
|
+
|
11
|
+
def initialize(name,params = {})
|
12
|
+
@type = 'Composite'
|
13
|
+
@name = name
|
14
|
+
@description = params.fetch('Description', '')
|
15
|
+
@rule = params.fetch('Rule', 'FALSE')
|
16
|
+
@alarm_action = params.fetch('Action', nil)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -5,43 +5,30 @@ module CfnGuardian
|
|
5
5
|
class Event
|
6
6
|
|
7
7
|
attr_reader :type
|
8
|
-
attr_accessor :
|
8
|
+
attr_accessor :group,
|
9
9
|
:target,
|
10
10
|
:hash,
|
11
11
|
:name,
|
12
12
|
:cron,
|
13
13
|
:enabled,
|
14
|
-
:resource
|
14
|
+
:resource,
|
15
|
+
:environment,
|
16
|
+
:payload,
|
17
|
+
:ssm_parameters
|
15
18
|
|
16
19
|
def initialize(resource)
|
17
20
|
@type = 'Event'
|
18
|
-
@
|
21
|
+
@group = nil
|
19
22
|
@target = nil
|
20
23
|
@hash = Digest::MD5.hexdigest resource['Id']
|
21
24
|
@name = @hash
|
22
25
|
@cron = "* * * * ? *"
|
23
26
|
@enabled = true
|
24
27
|
@resource = resource['Id'].to_resource_name
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
type: @type,
|
30
|
-
class: @class,
|
31
|
-
target: @target,
|
32
|
-
hash: @hash,
|
33
|
-
name: @name,
|
34
|
-
cron: @cron,
|
35
|
-
enabled: @enabled,
|
36
|
-
resource: @resource,
|
37
|
-
payload: event_payload()
|
38
|
-
}
|
39
|
-
end
|
40
|
-
|
41
|
-
def event_payload
|
42
|
-
{}.to_json
|
43
|
-
end
|
44
|
-
|
28
|
+
@environment = ""
|
29
|
+
@payload = {}.to_json
|
30
|
+
@ssm_parameters = []
|
31
|
+
end
|
45
32
|
end
|
46
33
|
|
47
34
|
class HttpEvent < Event
|
@@ -56,7 +43,7 @@ module CfnGuardian
|
|
56
43
|
|
57
44
|
def initialize(resource)
|
58
45
|
super(resource)
|
59
|
-
@
|
46
|
+
@group = 'Http'
|
60
47
|
@name = 'HttpEvent'
|
61
48
|
@target = 'HttpCheckFunction'
|
62
49
|
@endpoint = resource['Id']
|
@@ -66,9 +53,10 @@ module CfnGuardian
|
|
66
53
|
@body_regex = resource.fetch('BodyRegex',nil)
|
67
54
|
@headers = resource.fetch('Headers',nil)
|
68
55
|
@payload = resource.fetch('Payload',nil)
|
56
|
+
@compressed = resource.fetch('Compressed',false)
|
69
57
|
end
|
70
58
|
|
71
|
-
def
|
59
|
+
def payload
|
72
60
|
payload = {
|
73
61
|
'ENDPOINT' => @endpoint,
|
74
62
|
'METHOD' => @method,
|
@@ -78,14 +66,56 @@ module CfnGuardian
|
|
78
66
|
payload['BODY_REGEX_MATCH'] = @body_regex unless @body_regex.nil?
|
79
67
|
payload['HEADERS'] = @headers unless @headers.nil?
|
80
68
|
payload['PAYLOAD'] = @payload unless @payload.nil?
|
69
|
+
payload['COMPRESSED'] = '1' if @compressed
|
81
70
|
return payload.to_json
|
82
71
|
end
|
83
72
|
end
|
84
73
|
|
85
|
-
class
|
74
|
+
class InternalHttpEvent < HttpEvent
|
75
|
+
def initialize(resource,environment)
|
76
|
+
super(resource)
|
77
|
+
@group = 'InternalHttp'
|
78
|
+
@name = 'InternalHttpEvent'
|
79
|
+
@target = "InternalHttpCheckFunction#{environment}"
|
80
|
+
@environment = environment
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class PortEvent < Event
|
85
|
+
def initialize(resource)
|
86
|
+
super(resource)
|
87
|
+
@group = 'Port'
|
88
|
+
@name = 'PortEvent'
|
89
|
+
@target = 'PortCheckFunction'
|
90
|
+
@hostname = resource['Id']
|
91
|
+
@port = resource['Port']
|
92
|
+
@timeout = resource.fetch('Timeout',120)
|
93
|
+
end
|
94
|
+
|
95
|
+
def payload
|
96
|
+
return {
|
97
|
+
'HOSTNAME' => @hostname,
|
98
|
+
'PORT' => @port,
|
99
|
+
'TIMEOUT' => @timeout,
|
100
|
+
'STATUS_CODE_MATCH' => @status_code
|
101
|
+
}.to_json
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class InternalPortEvent < PortEvent
|
106
|
+
def initialize(resource,environment)
|
107
|
+
super(resource)
|
108
|
+
@group = 'InternalPort'
|
109
|
+
@name = 'InternalPortEvent'
|
110
|
+
@target = "InternalPortCheckFunction#{environment}"
|
111
|
+
@environment = environment
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class NrpeEvent < Event
|
86
116
|
def initialize(resource,environment,command)
|
87
117
|
super(resource)
|
88
|
-
@
|
118
|
+
@group = 'Nrpe'
|
89
119
|
@name = 'NrpeEvent'
|
90
120
|
@target = "NrpeCheckFunction#{environment}"
|
91
121
|
@host = resource['Id']
|
@@ -94,7 +124,7 @@ module CfnGuardian
|
|
94
124
|
@command = command
|
95
125
|
end
|
96
126
|
|
97
|
-
def
|
127
|
+
def payload
|
98
128
|
return {
|
99
129
|
'host' => @host,
|
100
130
|
'environment' => @environment,
|
@@ -107,7 +137,7 @@ module CfnGuardian
|
|
107
137
|
class SslEvent < Event
|
108
138
|
def initialize(resource)
|
109
139
|
super(resource)
|
110
|
-
@
|
140
|
+
@group = 'Ssl'
|
111
141
|
@name = 'SslEvent'
|
112
142
|
@target = 'SslCheckFunction'
|
113
143
|
@cron = "0 12 * * ? *"
|
@@ -115,7 +145,7 @@ module CfnGuardian
|
|
115
145
|
@region = resource.fetch('Region',"${AWS::Region}")
|
116
146
|
end
|
117
147
|
|
118
|
-
def
|
148
|
+
def payload
|
119
149
|
return {
|
120
150
|
'Url' => @url,
|
121
151
|
'Region' => @region
|
@@ -123,6 +153,16 @@ module CfnGuardian
|
|
123
153
|
end
|
124
154
|
end
|
125
155
|
|
156
|
+
class InternalSslEvent < SslEvent
|
157
|
+
def initialize(resource,environment)
|
158
|
+
super(resource)
|
159
|
+
@group = 'InternalSsl'
|
160
|
+
@name = 'InternalSslEvent'
|
161
|
+
@target = "InternalSslCheckFunction#{environment}"
|
162
|
+
@environment = environment
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
126
166
|
class DomainExpiryEvent < Event
|
127
167
|
|
128
168
|
attr_accessor :domain,
|
@@ -130,7 +170,7 @@ module CfnGuardian
|
|
130
170
|
|
131
171
|
def initialize(resource)
|
132
172
|
super(resource)
|
133
|
-
@
|
173
|
+
@group = 'DomainExpiry'
|
134
174
|
@name = 'DomainExpiryEvent'
|
135
175
|
@target = 'DomainExpiryCheckFunction'
|
136
176
|
@cron = "0 12 * * ? *"
|
@@ -138,17 +178,17 @@ module CfnGuardian
|
|
138
178
|
@region = resource.fetch('Region',"${AWS::Region}")
|
139
179
|
end
|
140
180
|
|
141
|
-
def
|
142
|
-
{'Domain' => @domain}.to_json
|
181
|
+
def payload
|
182
|
+
return {'Domain' => @domain}.to_json
|
143
183
|
end
|
144
184
|
end
|
145
185
|
|
146
186
|
class SqlEvent < Event
|
147
|
-
def initialize(resource,query)
|
187
|
+
def initialize(resource,query,environment)
|
148
188
|
super(resource)
|
149
|
-
@
|
189
|
+
@group = 'Sql'
|
150
190
|
@name = 'SqlEvent'
|
151
|
-
@target =
|
191
|
+
@target = "SqlCheckFunction#{environment}"
|
152
192
|
@host = resource['Id']
|
153
193
|
@engine = resource['Engine']
|
154
194
|
@port = resource['Port']
|
@@ -157,9 +197,10 @@ module CfnGuardian
|
|
157
197
|
@query = query
|
158
198
|
@region = resource.fetch('Region',"${AWS::Region}")
|
159
199
|
@test_type = '1-row-1-value-zero-is-good'
|
200
|
+
@environment = environment
|
160
201
|
end
|
161
202
|
|
162
|
-
def
|
203
|
+
def payload
|
163
204
|
return {
|
164
205
|
'Host' => @host,
|
165
206
|
'Engine' => @engine,
|
@@ -171,20 +212,103 @@ module CfnGuardian
|
|
171
212
|
'TestType' => @test_type
|
172
213
|
}.to_json
|
173
214
|
end
|
215
|
+
|
216
|
+
def ssm_parameters
|
217
|
+
params = []
|
218
|
+
params << @ssm_username
|
219
|
+
params << @ssm_password
|
220
|
+
return params
|
221
|
+
end
|
174
222
|
end
|
175
223
|
|
176
224
|
class ContainerInstanceEvent < Event
|
177
225
|
def initialize(resource)
|
178
226
|
super(resource)
|
179
|
-
@
|
227
|
+
@group = 'ContainerInstance'
|
180
228
|
@name = 'ContainerInstanceEvent'
|
181
229
|
@target = 'ContainerInstanceCheckFunction'
|
182
230
|
@cron = "0/5 * * * ? *"
|
183
231
|
@cluster = resource['Id']
|
184
232
|
end
|
185
233
|
|
186
|
-
def
|
187
|
-
{'CLUSTER' => @cluster}.to_json
|
234
|
+
def payload
|
235
|
+
return {'CLUSTER' => @cluster}.to_json
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
class SFTPEvent < Event
|
240
|
+
def initialize(resource)
|
241
|
+
super(resource)
|
242
|
+
@group = 'SFTP'
|
243
|
+
@name = 'SFTPEvent'
|
244
|
+
@target = 'SFTPCheckFunction'
|
245
|
+
@cron = "0/5 * * * ? *"
|
246
|
+
@host = resource['Id']
|
247
|
+
@user = resource['User']
|
248
|
+
@port = resource.fetch('Port', nil)
|
249
|
+
@server_key = resource.fetch('ServerKey', nil)
|
250
|
+
@password = resource.fetch('Password', nil)
|
251
|
+
@private_key = resource.fetch('PrivateKey', nil)
|
252
|
+
@private_key_pass = resource.fetch('PrivateKeyPass', nil)
|
253
|
+
@file = resource.fetch('File', nil)
|
254
|
+
@file_regex_match = resource.fetch('FileRegexMatch', nil)
|
255
|
+
end
|
256
|
+
|
257
|
+
def payload
|
258
|
+
payload = {
|
259
|
+
'HOSTNAME' => @host,
|
260
|
+
'USERNAME' => @user
|
261
|
+
}
|
262
|
+
payload['PORT'] = @port unless @port.nil?
|
263
|
+
payload['SERVER_KEY'] = @server_key unless @server_key.nil?
|
264
|
+
payload['PASSWORD'] = @password unless @password.nil?
|
265
|
+
payload['PRIVATEKEY'] = @private_key unless @private_key.nil?
|
266
|
+
payload['PRIVATEKEY_PASSWORD'] = @private_key_pass unless @private_key_pass.nil?
|
267
|
+
payload['FILE'] = @file unless @file.nil?
|
268
|
+
payload['FILE_REGEX_MATCH'] = @file_regex_match unless @file_regex_match.nil?
|
269
|
+
return payload.to_json
|
270
|
+
end
|
271
|
+
|
272
|
+
def ssm_parameters
|
273
|
+
params = []
|
274
|
+
params << @password unless @password.nil?
|
275
|
+
params << @private_key unless @private_key.nil?
|
276
|
+
params << @private_key_pass unless @private_key_pass.nil?
|
277
|
+
return params
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
class InternalSFTPEvent < SFTPEvent
|
282
|
+
def initialize(resource,environment)
|
283
|
+
super(resource)
|
284
|
+
@group = 'InternalSFTP'
|
285
|
+
@name = 'InternalSFTPEvent'
|
286
|
+
@target = "InternalSFTPCheckFunction#{environment}"
|
287
|
+
@environment = environment
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
class TLSEvent < Event
|
292
|
+
def initialize(resource)
|
293
|
+
super(resource)
|
294
|
+
@group = 'TLS'
|
295
|
+
@name = 'TLSEvent'
|
296
|
+
@target = 'TLSCheckFunction'
|
297
|
+
@cron = "0/5 * * * ? *"
|
298
|
+
@host = resource['Id']
|
299
|
+
@port = resource.fetch('Port', 443)
|
300
|
+
@check_max = resource.fetch('MaxSupported', nil)
|
301
|
+
@versions = resource.fetch('Versions', ['SSLv2','SSLv3','TLSv1','TLSv1.1','TLSv1.2'])
|
302
|
+
end
|
303
|
+
|
304
|
+
def payload
|
305
|
+
payload = {
|
306
|
+
'HOSTNAME' => @host,
|
307
|
+
'PORT' => @port
|
308
|
+
}
|
309
|
+
payload['CHECK_MAX_SUPPORTED'] = @check_max.nil?
|
310
|
+
payload['PROTOCOLS'] = @versions unless @versions.nil?
|
311
|
+
return payload.to_json
|
188
312
|
end
|
189
313
|
end
|
190
314
|
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'cfnguardian/string'
|
2
|
+
require 'digest/md5'
|
3
|
+
|
4
|
+
module CfnGuardian
|
5
|
+
module Models
|
6
|
+
class MetricFilter
|
7
|
+
|
8
|
+
attr_reader :type,
|
9
|
+
:metric_namespace,
|
10
|
+
:name
|
11
|
+
attr_accessor :log_group,
|
12
|
+
:pattern,
|
13
|
+
:metric_value,
|
14
|
+
:metric_name
|
15
|
+
|
16
|
+
def initialize(log_group,filter)
|
17
|
+
@type = 'MetricFilter'
|
18
|
+
@name = Digest::MD5.hexdigest(log_group + filter['MetricName'])
|
19
|
+
@log_group = log_group
|
20
|
+
@pattern = filter['Pattern']
|
21
|
+
@metric_value = filter.fetch('MetricValue', '1').to_s
|
22
|
+
@metric_name = filter['MetricName']
|
23
|
+
@metric_namespace = "MetricFilters"
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -5,9 +5,11 @@ module CfnGuardian::Resource
|
|
5
5
|
alarm = CfnGuardian::Models::ApplicationTargetGroupAlarm.new(@resource)
|
6
6
|
alarm.name = 'HealthyHosts'
|
7
7
|
alarm.metric_name = 'HealthyHostCount'
|
8
|
+
alarm.comparison_operator = 'LessThanThreshold'
|
8
9
|
alarm.statistic = 'Minimum'
|
9
10
|
alarm.threshold = 2
|
10
11
|
alarm.evaluation_periods = 1
|
12
|
+
alarm.comparison_operator = 'LessThanThreshold'
|
11
13
|
@alarms.push(alarm)
|
12
14
|
|
13
15
|
alarm = CfnGuardian::Models::ApplicationTargetGroupAlarm.new(@resource)
|
@@ -1,24 +1,29 @@
|
|
1
1
|
require 'cfnguardian/string'
|
2
|
+
require 'cfnguardian/cloudwatch'
|
2
3
|
require 'cfnguardian/models/alarm'
|
3
4
|
require 'cfnguardian/models/event'
|
4
5
|
require 'cfnguardian/models/check'
|
6
|
+
require 'cfnguardian/models/metric_filter'
|
5
7
|
|
6
8
|
module CfnGuardian::Resource
|
7
9
|
class Base
|
8
10
|
include Logging
|
9
11
|
|
10
|
-
def initialize(resource)
|
12
|
+
def initialize(resource, override_group = nil)
|
11
13
|
@resource = resource
|
14
|
+
@override_group = override_group
|
12
15
|
@alarms = []
|
13
16
|
@events = []
|
14
17
|
@checks = []
|
18
|
+
@metric_filters = []
|
15
19
|
end
|
16
20
|
|
21
|
+
# Overidden by inheritted classes to define default alarms
|
17
22
|
def default_alarms()
|
18
23
|
return @alarms
|
19
24
|
end
|
20
25
|
|
21
|
-
def get_alarms(overides={})
|
26
|
+
def get_alarms(overides={},resource={})
|
22
27
|
# generate default alarms
|
23
28
|
default_alarms()
|
24
29
|
|
@@ -31,18 +36,20 @@ module CfnGuardian::Resource
|
|
31
36
|
|
32
37
|
if !alarm.nil?
|
33
38
|
alarm.enabled = false
|
34
|
-
logger.debug "Disabling alarm '#{name}' for resource #{alarm.
|
39
|
+
logger.debug "Disabling alarm '#{name}' for resource #{alarm.resource_id}"
|
35
40
|
next
|
36
41
|
end
|
37
42
|
end
|
38
43
|
|
44
|
+
# continue if the override is in the incorrect format
|
39
45
|
unless properties.is_a?(Hash)
|
40
46
|
if name != 'Inherit'
|
41
|
-
logger.warn "Incorrect format for alarm '#{name}'. Should be of type 'Hash', instead got type '#{properties.
|
47
|
+
logger.warn "Incorrect format for alarm '#{name}'. Should be of type 'Hash', instead got type '#{properties.group}'"
|
42
48
|
end
|
43
49
|
next
|
44
50
|
end
|
45
51
|
|
52
|
+
# Create a new alarm inheriting the defaults of an existing alarm
|
46
53
|
if properties.has_key?('Inherit')
|
47
54
|
alarm = find_alarm(properties['Inherit'])
|
48
55
|
if !alarm.nil?
|
@@ -59,37 +66,52 @@ module CfnGuardian::Resource
|
|
59
66
|
alarm = find_alarm(name)
|
60
67
|
|
61
68
|
if alarm.nil?
|
62
|
-
alarm
|
63
|
-
|
64
|
-
|
65
|
-
if alarm.name.nil?
|
69
|
+
# if alarm doesn't exist create a new one
|
70
|
+
alarm = Kernel.const_get("CfnGuardian::Models::#{self.class.to_s.split('::').last}Alarm").new(resource)
|
71
|
+
properties.each {|attr,value| update_alarm(alarm,attr,value)}
|
66
72
|
alarm.name = name
|
73
|
+
@alarms.push(alarm)
|
74
|
+
else
|
75
|
+
# if there is an existing alarm update the properties
|
76
|
+
properties.each {|attr,value| update_alarm(alarm,attr,value)}
|
67
77
|
end
|
68
|
-
|
69
|
-
properties.each {|attr,value| update_alarm(alarm,attr,value)}
|
70
|
-
@alarms.push(alarm)
|
71
|
-
|
72
78
|
end
|
73
79
|
|
74
|
-
|
80
|
+
unless @override_group.nil?
|
81
|
+
@alarms.each {|a| a.group = @override_group}
|
82
|
+
end
|
83
|
+
|
84
|
+
return @alarms.select{|a| a.enabled}
|
75
85
|
end
|
76
86
|
|
87
|
+
# Overidden by inheritted classes to define default events
|
77
88
|
def default_events()
|
78
89
|
return @events
|
79
90
|
end
|
80
91
|
|
81
92
|
def get_events()
|
82
93
|
default_events()
|
83
|
-
return @events.select{|e| e.enabled}
|
94
|
+
return @events.select{|e| e.enabled}
|
84
95
|
end
|
85
96
|
|
97
|
+
# Overidden by inheritted classes to define default checks
|
86
98
|
def default_checks()
|
87
99
|
return @checks
|
88
100
|
end
|
89
101
|
|
90
102
|
def get_checks()
|
91
103
|
default_checks()
|
92
|
-
return @checks
|
104
|
+
return @checks
|
105
|
+
end
|
106
|
+
|
107
|
+
# Overidden by inheritted classes to define default checks
|
108
|
+
def default_metric_filters()
|
109
|
+
return @metric_filters
|
110
|
+
end
|
111
|
+
|
112
|
+
def get_metric_filters()
|
113
|
+
default_metric_filters()
|
114
|
+
return @metric_filters
|
93
115
|
end
|
94
116
|
|
95
117
|
def get_cost()
|
@@ -107,7 +129,7 @@ module CfnGuardian::Resource
|
|
107
129
|
alarm.send("#{attr.to_underscore}=",value)
|
108
130
|
rescue NoMethodError => e
|
109
131
|
if !e.message.match?(/inherit/)
|
110
|
-
logger.warn "Unknown key '#{attr}' for #{alarm.
|
132
|
+
logger.warn "Unknown key '#{attr}' for #{alarm.resource_id} alarm #{alarm.name}"
|
111
133
|
end
|
112
134
|
end
|
113
135
|
end
|