cfn-guardian 0.1.0 → 0.6.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.dockerignore +1 -0
  3. data/.github/workflows/build-gem.yml +25 -0
  4. data/.github/workflows/release-gem.yml +25 -0
  5. data/.github/workflows/release-image.yml +33 -0
  6. data/.rspec +1 -0
  7. data/Dockerfile +19 -0
  8. data/Gemfile.lock +39 -21
  9. data/README.md +9 -378
  10. data/cfn-guardian.gemspec +7 -5
  11. data/docs/alarm_templates.md +130 -0
  12. data/docs/cli.md +182 -0
  13. data/docs/composite_alarms.md +24 -0
  14. data/docs/custom_checks/azure_file_check.md +28 -0
  15. data/docs/custom_checks/domain_expiry.md +10 -0
  16. data/docs/custom_checks/http.md +59 -0
  17. data/docs/custom_checks/log_group_metric_filters.md +27 -0
  18. data/docs/custom_checks/nrpe.md +29 -0
  19. data/docs/custom_checks/port.md +40 -0
  20. data/docs/custom_checks/sftp.md +73 -0
  21. data/docs/custom_checks/sql.md +44 -0
  22. data/docs/custom_checks/tls.md +25 -0
  23. data/docs/custom_metrics.md +71 -0
  24. data/docs/event_subscriptions.md +67 -0
  25. data/docs/maintenance_mode.md +85 -0
  26. data/docs/notifiers.md +33 -0
  27. data/docs/overview.md +22 -0
  28. data/docs/resources.md +93 -0
  29. data/docs/variables.md +58 -0
  30. data/lib/cfnguardian.rb +325 -37
  31. data/lib/cfnguardian/cloudwatch.rb +132 -0
  32. data/lib/cfnguardian/codecommit.rb +54 -0
  33. data/lib/cfnguardian/codepipeline.rb +138 -0
  34. data/lib/cfnguardian/compile.rb +142 -18
  35. data/lib/cfnguardian/config/defaults.yaml +103 -0
  36. data/lib/cfnguardian/deploy.rb +2 -16
  37. data/lib/cfnguardian/display_formatter.rb +163 -0
  38. data/lib/cfnguardian/drift.rb +79 -0
  39. data/lib/cfnguardian/error.rb +4 -0
  40. data/lib/cfnguardian/log.rb +0 -1
  41. data/lib/cfnguardian/models/alarm.rb +193 -59
  42. data/lib/cfnguardian/models/check.rb +128 -33
  43. data/lib/cfnguardian/models/composite.rb +21 -0
  44. data/lib/cfnguardian/models/event.rb +201 -49
  45. data/lib/cfnguardian/models/event_subscription.rb +96 -0
  46. data/lib/cfnguardian/models/metric_filter.rb +28 -0
  47. data/lib/cfnguardian/resources/amazonmq_rabbitmq.rb +136 -0
  48. data/lib/cfnguardian/resources/application_targetgroup.rb +2 -0
  49. data/lib/cfnguardian/resources/azure_file.rb +20 -0
  50. data/lib/cfnguardian/resources/base.rb +155 -33
  51. data/lib/cfnguardian/resources/ec2_instance.rb +11 -0
  52. data/lib/cfnguardian/resources/ecs_service.rb +2 -2
  53. data/lib/cfnguardian/resources/http.rb +17 -1
  54. data/lib/cfnguardian/resources/internal_http.rb +74 -0
  55. data/lib/cfnguardian/resources/internal_port.rb +33 -0
  56. data/lib/cfnguardian/resources/internal_sftp.rb +58 -0
  57. data/lib/cfnguardian/resources/log_group.rb +26 -0
  58. data/lib/cfnguardian/resources/network_targetgroup.rb +1 -0
  59. data/lib/cfnguardian/resources/port.rb +25 -0
  60. data/lib/cfnguardian/resources/rds_cluster.rb +14 -0
  61. data/lib/cfnguardian/resources/rds_instance.rb +73 -0
  62. data/lib/cfnguardian/resources/redshift_cluster.rb +2 -2
  63. data/lib/cfnguardian/resources/sftp.rb +50 -0
  64. data/lib/cfnguardian/resources/sql.rb +3 -3
  65. data/lib/cfnguardian/resources/tls.rb +66 -0
  66. data/lib/cfnguardian/s3.rb +3 -2
  67. data/lib/cfnguardian/stacks/main.rb +94 -72
  68. data/lib/cfnguardian/stacks/resources.rb +111 -43
  69. data/lib/cfnguardian/string.rb +12 -0
  70. data/lib/cfnguardian/version.rb +1 -1
  71. metadata +133 -10
data/docs/notifiers.md ADDED
@@ -0,0 +1,33 @@
1
+ # Guardian Notifiers
2
+
3
+ ## SNS Notification
4
+
5
+ There are 4 default notification levels used by Guardian Critical, Warning, Task, Informational. If you wish to recieve notifications for each of these you need to supply an sns topic arn in the alarms.yaml
6
+
7
+ ```yaml
8
+ Topics:
9
+ Critical: arn:aws:sns:ap-southeast-2:123456789012:Critical
10
+ Warning: arn:aws:sns:ap-southeast-2:123456789012:Warning
11
+ Task: arn:aws:sns:ap-southeast-2:123456789012:Task
12
+ Informational: arn:aws:sns:ap-southeast-2:123456789012:Informational
13
+ ```
14
+
15
+ Each alarm has a default notification level but can be overriden in the config using the `AlarmAction` property at either the alarm group or alarm level. See the [Overriding Defaults](#overriding-defaults) section on how to do that.
16
+
17
+ You can add your own notification topics to the topics section and combine them with the existing topics. `AlarmAction` property will accept both a string and array of notication topics.
18
+
19
+ ```yaml
20
+ Topics:
21
+ Critical: arn:aws:sns:ap-southeast-2:123456789012:Critical
22
+ Warning: arn:aws:sns:ap-southeast-2:123456789012:Warning
23
+ Task: arn:aws:sns:ap-southeast-2:123456789012:Task
24
+ Informational: arn:aws:sns:ap-southeast-2:123456789012:Informational
25
+ Custom: arn:aws:sns:ap-southeast-2:123456789012:Custom
26
+
27
+ Template:
28
+ Ec2Instance:
29
+ GroupOverrides:
30
+ AlarmActions:
31
+ - Critical
32
+ - Custom
33
+ ```
data/docs/overview.md ADDED
@@ -0,0 +1,22 @@
1
+ # Guardian Documentation
2
+
3
+ ## Table of Contents
4
+ 1. [CLI Commands](cli.md)
5
+ 2. [Resources](resources.md)
6
+ 3. [Alarm Templates](alarm_templates.md)
7
+ 4. Custom Checks
8
+ 1. [HTTP](custom_checks/http.md)
9
+ 2. [Domain Expirey](custom_checks/domain_expirey.md)
10
+ 3. [LogGroup Metric Filters](custom_checks/log_group_metric_filters.md)
11
+ 4. [NRPE](custom_checks/nrpe.md)
12
+ 5. [Port](custom_checks/port.md)
13
+ 6. [SFTP](custom_checks/sftp.md)
14
+ 7. [SQL](custom_checks/sql.md)
15
+ 8. [TLS](custom_checks/tls.md)
16
+ 9. [Azure File Check](custom_checks/azure_file_check.md)
17
+ 5. [Event Subscriptions](event_subscriptions.md)
18
+ 6. [Notifiers](notifiers.md)
19
+ 7. [Maintenance Mode](maintenance_mode.md)
20
+ 8. [Composite Alarms](composite_alarms.md)
21
+ 9. [Alarms for Custom Metrics](custom_metrics.md)
22
+ 10. [Dimension Variables](variables.md)
data/docs/resources.md ADDED
@@ -0,0 +1,93 @@
1
+ # Resources
2
+
3
+ Resources are AWS resources grouped by the resource type such as `RDSInstance`, `Ec2Instance`, `ApplicationTargetGroup` etc. These are defined under the top level key `Resources` in the yaml config file. The resource group is then used to generate standard set of alarms.
4
+
5
+ Custom resource groups can be created however matching alarm templates must be created to create alarms.
6
+
7
+ ## Resource lookup
8
+
9
+ Resources can be looked up within an account using the tool [monitorable](https://github.com/base2Services/monitorable). This tool will scan every region within an account for AWS resources that can be monitored and return a valid Guardian yaml config using the `--format cfn-guardian` flag.
10
+
11
+ ```sh
12
+ ./monitorable.py --format cfn-guardian
13
+ ```
14
+
15
+ ## YAML Configuration
16
+
17
+ The resources key is where the resources are defined.
18
+
19
+ ```yaml
20
+ Resources:
21
+ # resource group
22
+ Ec2Instance:
23
+ # Array of resources defining the resource id with the Id: key
24
+ - Id: i-1a2b3c4d5e
25
+ ```
26
+
27
+ There are some resources that require more that the resource id to generate the alarm, for these cases addition key:values are required.
28
+
29
+ ```yaml
30
+ Resources:
31
+ ApplicationTargetGroup:
32
+ - Id: target-group-id
33
+ # Target group requires the loadbalancer id for the alarm
34
+ Loadbalancer: app/application-loadbalancer-id
35
+ ```
36
+
37
+ | Resource Group | Require Keys |
38
+ | --------------------------- | ---------------- |
39
+ | ApiGateway | Id |
40
+ | AmazonMQBroker | Id |
41
+ | AutoScalingGroup | Id |
42
+ | DynamoDBTable | Id |
43
+ | ElastiCacheReplicationGroup | Id |
44
+ | ElasticFileSystem | Id |
45
+ | Ec2Instance | Id |
46
+ | EcsCluster | Id |
47
+ | EcsService | Id, Cluster |
48
+ | NetworkTargetGroup | Id, LoadBalancer |
49
+ | ApplicationTargetGroup | Id, LoadBalancer |
50
+ | ElasticLoadBalancer | Id |
51
+ | RDSInstance | Id |
52
+ | RDSClusterInstance | Id |
53
+ | RedshiftCluster | Id |
54
+ | Lambda | Id |
55
+ | CloudFrontDistribution | Id |
56
+ | SQSQueue | Id |
57
+
58
+
59
+ ## Custom Resource Groups
60
+
61
+ You may want to create a custom resource group if some of the resources require differewnt alarm configurations. To create a custom resource group create new name for the group and add the resources, then create the alarm template and inherit the desired alarms.
62
+
63
+ ```yaml
64
+ Resources:
65
+ # default resource group
66
+ Ec2Instance:
67
+ - Id: i-1a2b3c4d5e
68
+ - Id: i-9z8y7x6w5v
69
+ # custom resource group
70
+ CustomEc2Instance:
71
+ - Id: i-6fefg5qe4e
72
+
73
+ Templates:
74
+ # create a new alarm template with the same group name
75
+ CustomEc2Instance:
76
+ # inherit the ec2 alarms
77
+ Inherit: Ec2Instance
78
+ # alter the alarms
79
+ CPUUtilizationHigh: false
80
+ ```
81
+
82
+ ## Friendly Resource Names
83
+
84
+ You can set a friendly name which will replace the resource id in the alarm name.
85
+ The resource id will still be available in the alarm description.
86
+
87
+ ```yaml
88
+ Resources:
89
+ ApplicationTargetGroup:
90
+ - Id: target-group-id
91
+ Loadbalancer: app/application-loadbalancer-id
92
+ Name: webapp
93
+ ```
data/docs/variables.md ADDED
@@ -0,0 +1,58 @@
1
+ ## Dimension Variables
2
+
3
+ variables can be used to reference resource group values such as the resource Id within the dimensions section of an alarm template.
4
+
5
+ For example here we are creating an alarm for a disk usage metric for a group of EC2 instances.
6
+
7
+ ```yaml
8
+ Templates:
9
+ Ec2Instance:
10
+ LowDiskSpaceRootVolume:
11
+ Namespace: CWAgent
12
+ MetricName: DiskSpaceUsedPercent
13
+ Dimensions:
14
+ path: '/'
15
+ # Reference the resource Id from the resource group
16
+ host: ${Resource::Id}
17
+ device: 'xvda1'
18
+ fstype: 'ext4'
19
+ Statistic: Maximum
20
+ Threshold: 85
21
+ Period: 60
22
+ EvaluationPeriods: 1
23
+ TreatMissingData: breaching
24
+
25
+ Resources:
26
+ Ec2Instance:
27
+ - Id: i-12345678
28
+ - Id: i-abcdefgh
29
+ ```
30
+
31
+ custom variables can be referenced if you have different dimensions for each resource. using the example above, you may have different file system types on each instance.
32
+
33
+ ```yaml
34
+ Templates:
35
+ Ec2Instance:
36
+ LowDiskSpaceRootVolume:
37
+ Namespace: CWAgent
38
+ MetricName: DiskSpaceUsedPercent
39
+ Dimensions:
40
+ path: '/'
41
+ # Reference the resource Id from the resource group
42
+ host: ${Resource::Id}
43
+ device: 'xvda1'
44
+ # Reference the resource FileSystemType from the resource group
45
+ fstype: ${Resource::FileSystemType}
46
+ Statistic: Maximum
47
+ Threshold: 85
48
+ Period: 60
49
+ EvaluationPeriods: 1
50
+ TreatMissingData: breaching
51
+
52
+ Resources:
53
+ Ec2Instance:
54
+ - Id: i-12345678
55
+ FileSystemType: ext4
56
+ - Id: i-abcdefgh
57
+ FileSystemType: ext4
58
+ ```
data/lib/cfnguardian.rb CHANGED
@@ -1,10 +1,16 @@
1
1
  require 'thor'
2
2
  require 'terminal-table'
3
+ require 'term/ansicolor'
3
4
  require "cfnguardian/log"
4
5
  require "cfnguardian/version"
5
6
  require "cfnguardian/compile"
6
7
  require "cfnguardian/validate"
7
8
  require "cfnguardian/deploy"
9
+ require "cfnguardian/cloudwatch"
10
+ require "cfnguardian/display_formatter"
11
+ require "cfnguardian/drift"
12
+ require "cfnguardian/codecommit"
13
+ require "cfnguardian/codepipeline"
8
14
 
9
15
  module CfnGuardian
10
16
  class Cli < Thor
@@ -16,6 +22,8 @@ module CfnGuardian
16
22
  puts CfnGuardian::VERSION
17
23
  end
18
24
 
25
+ class_option :debug, type: :boolean, default: false, desc: "enable debug logging"
26
+
19
27
  desc "compile", "Generate monitoring CloudFormation templates"
20
28
  long_desc <<-LONG
21
29
  Generates CloudFormation templates from the alarm configuration and output to the out/ directory.
@@ -23,16 +31,26 @@ module CfnGuardian
23
31
  method_option :config, aliases: :c, type: :string, desc: "yaml config file", required: true
24
32
  method_option :validate, type: :boolean, default: true, desc: "validate cfn templates"
25
33
  method_option :bucket, type: :string, desc: "provide custom bucket name, will create a default bucket if not provided"
34
+ method_option :path, type: :string, default: "guardian", desc: "S3 path location for nested stacks"
26
35
  method_option :region, aliases: :r, type: :string, desc: "set the AWS region"
36
+ method_option :template_config, type: :boolean, default: false, desc: "Genrates an AWS CodePipeline cloudformation template configuration file to override parameters"
37
+ method_option :sns_critical, type: :string, desc: "sns topic arn for the critical alarms"
38
+ method_option :sns_warning, type: :string, desc: "sns topic arn for the warning alarms"
39
+ method_option :sns_task, type: :string, desc: "sns topic arn for the task alarms"
40
+ method_option :sns_informational, type: :string, desc: "sns topic arn for the informational alarms"
41
+ method_option :sns_events, type: :string, desc: "sns topic arn for the informational alarms"
42
+
27
43
 
28
44
  def compile
45
+ set_log_level(options[:debug])
46
+
29
47
  set_region(options[:region],options[:validate])
30
- s3 = CfnGuardian::S3.new(options[:bucket])
48
+ s3 = CfnGuardian::S3.new(options[:bucket],options[:path])
31
49
 
32
- compiler = CfnGuardian::Compile.new(options,s3.bucket)
50
+ compiler = CfnGuardian::Compile.new(options[:config])
33
51
  compiler.get_resources
34
- compiler.compile_templates
35
- logger.info "Clouformation templates compiled successfully in out/ directory"
52
+ compiler.compile_templates(s3.bucket,s3.path)
53
+ logger.info "Cloudformation templates compiled successfully in out/ directory"
36
54
  if options[:validate]
37
55
  s3.create_bucket_if_not_exists()
38
56
  validator = CfnGuardian::Validate.new(s3.bucket)
@@ -40,10 +58,16 @@ module CfnGuardian
40
58
  logger.error("One or more templates failed to validate")
41
59
  exit(1)
42
60
  else
43
- logger.info "Clouformation templates were validated successfully"
61
+ logger.info "Cloudformation templates were validated successfully"
44
62
  end
45
63
  end
46
64
  logger.warn "AWS cloudwatch alarms defined in the templates will cost roughly $#{'%.2f' % compiler.cost} per month"
65
+
66
+ if options[:template_config]
67
+ logger.info "Generating a AWS CodePipeline template configuration file template-config.guardian.json"
68
+ parameters = compiler.load_parameters(options)
69
+ compiler.genrate_template_config(parameters)
70
+ end
47
71
  end
48
72
 
49
73
  desc "deploy", "Generates and deploys monitoring CloudFormation templates"
@@ -53,21 +77,26 @@ module CfnGuardian
53
77
  LONG
54
78
  method_option :config, aliases: :c, type: :string, desc: "yaml config file", required: true
55
79
  method_option :bucket, type: :string, desc: "provide custom bucket name, will create a default bucket if not provided"
80
+ method_option :path, type: :string, default: "guardian", desc: "S3 path location for nested stacks"
56
81
  method_option :region, aliases: :r, type: :string, desc: "set the AWS region"
57
- method_option :stack_name, aliases: :r, type: :string, desc: "set the Cloudformation stack name. Defaults to `guardian`"
58
- method_option :sns_critical, type: :string, desc: "sns topic arn for the critical alamrs"
59
- method_option :sns_warning, type: :string, desc: "sns topic arn for the warning alamrs"
60
- method_option :sns_task, type: :string, desc: "sns topic arn for the task alamrs"
61
- method_option :sns_informational, type: :string, desc: "sns topic arn for the informational alamrs"
82
+ method_option :stack_name, aliases: :s, type: :string, desc: "set the Cloudformation stack name. Defaults to `guardian`"
83
+ method_option :sns_critical, type: :string, desc: "sns topic arn for the critical alarms"
84
+ method_option :sns_warning, type: :string, desc: "sns topic arn for the warning alarms"
85
+ method_option :sns_task, type: :string, desc: "sns topic arn for the task alarms"
86
+ method_option :sns_informational, type: :string, desc: "sns topic arn for the informational alarms"
87
+ method_option :sns_events, type: :string, desc: "sns topic arn for the informational alarms"
62
88
 
63
89
  def deploy
90
+ set_log_level(options[:debug])
91
+
64
92
  set_region(options[:region],true)
65
- s3 = CfnGuardian::S3.new(options[:bucket])
93
+ s3 = CfnGuardian::S3.new(options[:bucket],options[:path])
66
94
 
67
- compiler = CfnGuardian::Compile.new(options,s3.bucket)
95
+ compiler = CfnGuardian::Compile.new(options[:config])
68
96
  compiler.get_resources
69
- compiler.compile_templates
70
- logger.info "Clouformation templates compiled successfully in out/ directory"
97
+ compiler.compile_templates(s3.bucket,s3.path)
98
+ parameters = compiler.load_parameters(options)
99
+ logger.info "Cloudformation templates compiled successfully in out/ directory"
71
100
 
72
101
  s3.create_bucket_if_not_exists
73
102
  validator = CfnGuardian::Validate.new(s3.bucket)
@@ -75,56 +104,289 @@ module CfnGuardian
75
104
  logger.error("One or more templates failed to validate")
76
105
  exit(1)
77
106
  else
78
- logger.info "Clouformation templates were validated successfully"
107
+ logger.info "Cloudformation templates were validated successfully"
79
108
  end
80
109
 
81
- deployer = CfnGuardian::Deploy.new(options,s3.bucket)
110
+ deployer = CfnGuardian::Deploy.new(options,s3.bucket,parameters)
82
111
  deployer.upload_templates
83
112
  change_set, change_set_type = deployer.create_change_set()
84
113
  deployer.wait_for_changeset(change_set.id)
85
114
  deployer.execute_change_set(change_set.id)
86
115
  deployer.wait_for_execute(change_set_type)
87
116
  end
117
+
118
+ desc "show-drift", "Cloudformation drift detection"
119
+ long_desc <<-LONG
120
+ Displays any cloudformation drift detection in the cloudwatch alarms from the deployed stacks
121
+ LONG
122
+ method_option :stack_name, aliases: :s, type: :string, default: 'guardian', desc: "set the Cloudformation stack name"
123
+ method_option :region, aliases: :r, type: :string, desc: "set the AWS region"
124
+
125
+ def show_drift
126
+ set_region(options[:region],true)
127
+
128
+ rows = []
129
+ drift = CfnGuardian::Drift.new(options[:stack_name])
130
+ nested_stacks = drift.find_nested_stacks
131
+ nested_stacks.each do |stack|
132
+ drift.detect_drift(stack)
133
+ rows << drift.get_drift(stack)
134
+ end
135
+
136
+ if rows.any?
137
+ puts Terminal::Table.new(
138
+ :title => "Guardian Alarm Drift".green,
139
+ :headings => ['Alarm Name', 'Property', 'Expected', 'Actual', 'Type'],
140
+ :rows => rows.flatten(1))
141
+ exit(1)
142
+ end
143
+ end
88
144
 
89
145
  desc "show-alarms", "Shows alarm settings"
90
146
  long_desc <<-LONG
91
147
  Displays the configured settings for each alarm. Can be filtered by resource group and alarm name.
92
148
  Defaults to show all configured alarms.
93
149
  LONG
94
- method_option :config, aliases: :c, type: :string, desc: "yaml config file", required: true
95
- method_option :group, aliases: :g, type: :string, desc: "resource group"
96
- method_option :name, aliases: :n, type: :string, desc: "alarm name"
97
- method_option :resource, aliases: :r, type: :string, desc: "resource id"
150
+ method_option :config, aliases: :c, type: :string, desc: "yaml config file"
151
+ method_option :defaults, type: :boolean, desc: "display default alarms and properties"
152
+ method_option :region, aliases: :r, type: :string, desc: "set the AWS region"
153
+ method_option :filter, type: :hash, default: {}, desc: "filter the displayed alarms by [group, resource-id, alarm, stack-id, topic, maintenance-group]"
154
+ method_option :compare, type: :boolean, default: false, desc: "compare config to deployed alarms"
155
+
98
156
  def show_alarms
99
- compiler = CfnGuardian::Compile.new(options,'no-bucket')
157
+ set_log_level(options[:debug])
158
+ set_region(options[:region],options[:compare])
159
+
160
+ if options[:config]
161
+ config_file = options[:config]
162
+ elsif options[:defaults]
163
+ config_file = default_config()
164
+ else
165
+ logger.error('one of `--config YAML` or `--defaults` must be supplied')
166
+ exit -1
167
+ end
168
+
169
+ compiler = CfnGuardian::Compile.new(config_file)
100
170
  compiler.get_resources
171
+ alarms = filter_compiled_alarms(compiler.alarms,options[:filter])
172
+
173
+ if alarms.empty?
174
+ logger.error "No matches found"
175
+ exit 1
176
+ end
101
177
 
102
- alarms = compiler.resources.select{|h| h[:type] == 'Alarm'}
103
- groups = alarms.group_by{|h| h[:class]}
178
+ headings = ['Property', 'Config']
179
+ formatter = CfnGuardian::DisplayFormatter.new(alarms)
104
180
 
105
- if options[:group]
106
- groups = groups.fetch(options[:group],{}).group_by{|h| h[:class]}
107
- if options[:resource]
108
- groups = groups[options[:group]].select{|h| h[:resource] == options[:resource]}.group_by{|h| h[:class]}
181
+ if options[:compare] && !options[:defaults]
182
+ metric_alarms = CfnGuardian::CloudWatch.get_alarms_by_prefix(prefix: 'guardian')
183
+ metric_alarms = CfnGuardian::CloudWatch.filter_alarms(filters: options[:filter], alarms: metric_alarms)
184
+
185
+ formatted_alarms = formatter.compare_alarms(metric_alarms)
186
+ headings.push('Deployed')
187
+ else
188
+ formatted_alarms = formatter.alarms()
189
+ end
190
+
191
+ if formatted_alarms.any?
192
+ formatted_alarms.each do |fa|
193
+ puts Terminal::Table.new(
194
+ :title => fa[:title],
195
+ :headings => headings,
196
+ :rows => fa[:rows])
109
197
  end
110
- if options[:name]
111
- groups = groups[options[:group]].select{|h| h[:name] == options[:name]}.group_by{|h| h[:class]}
198
+ else
199
+ if options[:compare] && !options[:defaults]
200
+ logger.info "No difference found between you config and alarms in deployed AWS"
201
+ else
202
+ logger.warn "No alarms found"
112
203
  end
113
204
  end
205
+ end
206
+
207
+ desc "show-state", "Shows alarm state in cloudwatch"
208
+ long_desc <<-LONG
209
+ Displays the current cloudwatch alarm state. By default it will return all the guardian alarms.
210
+ LONG
211
+ method_option :region, aliases: :r, type: :string, desc: "set the AWS region"
212
+ method_option :state, aliases: :s, type: :string, enum: %w(OK ALARM INSUFFICIENT_DATA), desc: "filter by alarm state"
213
+ method_option :alarm_names, type: :array, desc: "list of cloudwatch alarm names"
214
+ method_option :alarm_prefix, type: :string, default: "guardian", desc: "cloudwatch alarm name prefix"
215
+ method_option :filter, type: :hash, default: {}, desc: "filter the displayed alarms by [group, resource-id, alarm, stack-id, topic, maintenance-group]"
216
+
217
+ def show_state
218
+ set_log_level(options[:debug])
219
+ set_region(options[:region],true)
220
+ action_prefix = nil
221
+
222
+ if options[:filter].has_key?('topic')
223
+ action_prefix = get_topic_arn_from_stack(options[:filter]['topic'])
224
+ elsif options[:filter].has_key?('maintenance-group')
225
+ action_prefix = "arn:aws:sns:#{Aws.config[:region]}:#{CfnGuardian::CloudWatch.aws_account_id()}:#{options[:filter]['maintenance-group']}MaintenanceGroup"
226
+ end
227
+
228
+ if options[:alarm_names]
229
+ metric_alarms = CfnGuardian::CloudWatch.get_alarms_by_name(alarm_names: options[:alarm_names], state: options[:state], action_prefix: action_prefix)
230
+ else
231
+ metric_alarms = CfnGuardian::CloudWatch.get_alarms_by_prefix(prefix: options[:alarm_prefix], state: options[:state], action_prefix: action_prefix)
232
+ end
233
+
234
+ metric_alarms = CfnGuardian::CloudWatch.filter_alarms(filters: options[:filter], alarms: metric_alarms)
235
+
236
+ formatter = CfnGuardian::DisplayFormatter.new()
237
+ rows = formatter.alarm_state(metric_alarms)
114
238
 
115
- groups.each do |grp,alarms|
116
- puts "\n\s\s#{grp}\n"
117
- alarms.each do |alarm|
118
- rows = alarm.reject {|k,v| [:type,:class,:name].include?(k)}
119
- .sort_by {|k,v| k}
239
+ if rows.any?
240
+ puts Terminal::Table.new(
241
+ :title => "Alarm State",
242
+ :headings => ['Alarm Name', 'State', 'Changed', 'Notifications'],
243
+ :rows => rows)
244
+ else
245
+ logger.warn "No alarms found"
246
+ end
247
+ end
248
+
249
+ desc "show-history", "Shows alarm history for the last 7 days"
250
+ long_desc <<-LONG
251
+ Displays the alarm state or config history for the last 7 days
252
+ LONG
253
+ method_option :region, aliases: :r, type: :string, desc: "set the AWS region"
254
+ method_option :alarm_names, type: :array, desc: "list of cloudwatch alarm names"
255
+ method_option :type, aliases: :t, type: :string,
256
+ enum: %w(state config), default: 'state', desc: "filter by alarm state"
257
+ method_option :alarm_prefix, type: :string, default: "guardian", desc: "cloudwatch alarm name prefix"
258
+ method_option :filter, type: :hash, desc: "filter the displayed alarms by [group, resource-id, alarm, stack-id]"
259
+
260
+ def show_history
261
+ set_log_level(options[:debug])
262
+ set_region(options[:region],true)
263
+
264
+ if options[:alarm_names]
265
+ metric_alarms = CfnGuardian::CloudWatch.get_alarms_by_name(alarm_names: options[:alarm_names], state: options[:state])
266
+ else
267
+ metric_alarms = CfnGuardian::CloudWatch.get_alarms_by_prefix(prefix: options[:alarm_prefix], state: options[:state])
268
+ end
269
+
270
+ metric_alarms = CfnGuardian::CloudWatch.filter_alarms(filters: options[:filter], alarms: metric_alarms)
271
+
272
+ case options[:type]
273
+ when 'state'
274
+ type = 'StateUpdate'
275
+ headings = ['Date', 'Summary', 'Reason']
276
+ when 'config'
277
+ type = 'ConfigurationUpdate'
278
+ headings = ['Date', 'Summary', 'Type']
279
+ end
280
+
281
+ formatter = CfnGuardian::DisplayFormatter.new()
282
+
283
+ metric_alarms.each do |alarm|
284
+ history = CfnGuardian::CloudWatch.get_alarm_history(alarm.alarm_name,type)
285
+ rows = formatter.alarm_history(history,type)
286
+ if rows.any?
120
287
  puts Terminal::Table.new(
121
- :title => alarm[:name],
122
- :headings => ['property', 'Value'],
123
- :rows => rows.map! {|k,v| [k,v.to_s]})
288
+ :title => alarm.alarm_name.green,
289
+ :headings => headings,
290
+ :rows => rows)
291
+ puts "\n"
124
292
  end
125
293
  end
126
294
  end
127
295
 
296
+ desc "show-config-history", "Shows the last 10 commits made to the codecommit repo"
297
+ long_desc <<-LONG
298
+ Shows the last 10 commits made to the codecommit repo
299
+ LONG
300
+ method_option :region, aliases: :r, type: :string, desc: "set the AWS region"
301
+ method_option :repository, type: :string, default: 'guardian', desc: "codecommit repository name"
302
+
303
+ def show_config_history
304
+ set_region(options[:region],true)
305
+
306
+ history = CfnGuardian::CodeCommit.new(options[:repository]).get_commit_history()
307
+ puts Terminal::Table.new(
308
+ :headings => history.first.keys.map{|h| h.to_s.to_heading},
309
+ :rows => history.map(&:values))
310
+ end
311
+
312
+ desc "show-pipeline", "Shows the current state of the AWS code pipeline"
313
+ long_desc <<-LONG
314
+ Shows the current state of the AWS code pipeline
315
+ LONG
316
+ method_option :region, aliases: :r, type: :string, desc: "set the AWS region"
317
+ method_option :pipeline, aliases: :p, type: :string, default: 'guardian', desc: "codepipeline name"
318
+
319
+ def show_pipeline
320
+ set_region(options[:region],true)
321
+ pipeline = CfnGuardian::CodePipeline.new(options[:pipeline])
322
+ source = pipeline.get_source()
323
+ build = pipeline.get_build()
324
+ create = pipeline.get_create_changeset()
325
+ deploy = pipeline.get_deploy_changeset()
326
+
327
+ puts Terminal::Table.new(
328
+ :title => "Stage: #{source[:stage]}",
329
+ :rows => source[:rows])
330
+
331
+ puts "\t|"
332
+ puts "\t|"
333
+
334
+ puts Terminal::Table.new(
335
+ :title => "Stage: #{build[:stage]}",
336
+ :rows => build[:rows])
337
+
338
+ puts "\t|"
339
+ puts "\t|"
340
+
341
+ puts Terminal::Table.new(
342
+ :title => "Stage: #{create[:stage]}",
343
+ :rows => create[:rows])
344
+
345
+ puts "\t|"
346
+ puts "\t|"
347
+
348
+ puts Terminal::Table.new(
349
+ :title => "Stage: #{deploy[:stage]}",
350
+ :rows => deploy[:rows])
351
+ end
352
+
353
+ desc "disable-alarms", "Disable cloudwatch alarm notifications"
354
+ long_desc <<-LONG
355
+ Disable cloudwatch alarm notifications for a maintenance group or for specific alarms.
356
+ LONG
357
+ method_option :region, aliases: :r, type: :string, desc: "set the AWS region"
358
+ method_option :group, aliases: :g, type: :string, desc: "name of the maintenance group defined in the config"
359
+ method_option :alarm_prefix, type: :string, desc: "cloud watch alarm name prefix"
360
+ method_option :alarms, type: :array, desc: "List of cloudwatch alarm names"
361
+
362
+ def disable_alarms
363
+ set_region(options[:region],true)
364
+
365
+ alarm_names = CfnGuardian::CloudWatch.get_alarm_names(options[:group],options[:alarm_prefix])
366
+ CfnGuardian::CloudWatch.disable_alarms(alarm_names)
367
+
368
+ logger.info "Disabled #{alarm_names.length} alarms"
369
+ end
370
+
371
+ desc "enable-alarms", "Enable cloudwatch alarm notifications"
372
+ long_desc <<-LONG
373
+ Enable cloudwatch alarm notifications for a maintenance group or for specific alarms.
374
+ Once alarms are enable the state is set back to OK to re send notifications of any failed alarms.
375
+ LONG
376
+ method_option :region, aliases: :r, type: :string, desc: "set the AWS region"
377
+ method_option :group, aliases: :g, type: :string, desc: "name of the maintenance group defined in the config"
378
+ method_option :alarm_prefix, type: :string, desc: "cloud watch alarm name prefix"
379
+ method_option :alarms, type: :array, desc: "List of cloudwatch alarm names"
380
+
381
+ def enable_alarms
382
+ set_region(options[:region],true)
383
+
384
+ alarm_names = CfnGuardian::CloudWatch.get_alarm_names(options[:group],options[:alarm_prefix])
385
+ CfnGuardian::CloudWatch.enable_alarms(alarm_names)
386
+
387
+ logger.info "#{alarm_names.length} alarms enabled"
388
+ end
389
+
128
390
  private
129
391
 
130
392
  def set_region(region,required)
@@ -142,5 +404,31 @@ module CfnGuardian
142
404
  end
143
405
  end
144
406
 
407
+ def set_log_level(debug)
408
+ logger.level = debug ? Logger::DEBUG : Logger::INFO
409
+ end
410
+
411
+ def filter_compiled_alarms(alarms,filters)
412
+ filters = filters.slice('group', 'resource', 'alarm', 'topic', 'maintenance-group')
413
+ alarms.select! {|alarm| alarm.group.downcase == filters['group'].downcase} if filters.has_key?('group')
414
+ alarms.select! {|alarm| alarm.resource_id.downcase == filters['resource'].downcase} if filters.has_key?('resource')
415
+ alarms.select! {|alarm| alarm.name.downcase.include? filters['alarm'].downcase} if filters.has_key?('alarm')
416
+ alarms.select! {|alarm| alarm.alarm_action.include? filters['topic']} if filters.has_key?('topic')
417
+ alarms.select! {|alarm| alarm.maintenance_groups.include? "#{filters['maintenance-group']}MaintenanceGroup"} if filters.has_key?('maintenance-group')
418
+ return alarms
419
+ end
420
+
421
+ def default_config()
422
+ return "#{File.expand_path(File.dirname(__FILE__))}/cfnguardian/config/defaults.yaml"
423
+ end
424
+
425
+ def get_topic_arn_from_stack(topic)
426
+ client = Aws::CloudFormation::Client.new()
427
+ resp = client.describe_stacks({ stack_name: @stack_name })
428
+ stack = resp.stacks.first
429
+ parameter = stack.parameters.find {|p| p.parameter_key == topic}
430
+ return !parameter.nil? ? parameter.parameter_value : nil
431
+ end
432
+
145
433
  end
146
434
  end