cfn-guardian 0.1.0 → 0.3.3

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.dockerignore +1 -0
  3. data/Dockerfile +19 -0
  4. data/Gemfile.lock +31 -13
  5. data/README.md +441 -42
  6. data/cfn-guardian.gemspec +6 -2
  7. data/lib/cfnguardian.rb +301 -27
  8. data/lib/cfnguardian/cloudwatch.rb +121 -0
  9. data/lib/cfnguardian/codecommit.rb +54 -0
  10. data/lib/cfnguardian/codepipeline.rb +138 -0
  11. data/lib/cfnguardian/compile.rb +58 -17
  12. data/lib/cfnguardian/config/defaults.yaml +94 -0
  13. data/lib/cfnguardian/display_formatter.rb +164 -0
  14. data/lib/cfnguardian/drift.rb +79 -0
  15. data/lib/cfnguardian/log.rb +0 -1
  16. data/lib/cfnguardian/models/alarm.rb +98 -36
  17. data/lib/cfnguardian/models/check.rb +103 -26
  18. data/lib/cfnguardian/models/composite.rb +21 -0
  19. data/lib/cfnguardian/models/event.rb +164 -40
  20. data/lib/cfnguardian/models/metric_filter.rb +28 -0
  21. data/lib/cfnguardian/resources/application_targetgroup.rb +2 -0
  22. data/lib/cfnguardian/resources/base.rb +38 -16
  23. data/lib/cfnguardian/resources/ecs_service.rb +2 -2
  24. data/lib/cfnguardian/resources/http.rb +16 -1
  25. data/lib/cfnguardian/resources/internal_http.rb +74 -0
  26. data/lib/cfnguardian/resources/internal_port.rb +33 -0
  27. data/lib/cfnguardian/resources/internal_sftp.rb +58 -0
  28. data/lib/cfnguardian/resources/log_group.rb +26 -0
  29. data/lib/cfnguardian/resources/network_targetgroup.rb +1 -0
  30. data/lib/cfnguardian/resources/port.rb +25 -0
  31. data/lib/cfnguardian/resources/rds_instance.rb +2 -0
  32. data/lib/cfnguardian/resources/sftp.rb +50 -0
  33. data/lib/cfnguardian/resources/sql.rb +1 -1
  34. data/lib/cfnguardian/resources/tls.rb +66 -0
  35. data/lib/cfnguardian/s3.rb +3 -2
  36. data/lib/cfnguardian/stacks/main.rb +86 -65
  37. data/lib/cfnguardian/stacks/resources.rb +81 -42
  38. data/lib/cfnguardian/string.rb +12 -0
  39. data/lib/cfnguardian/version.rb +1 -1
  40. metadata +102 -5
@@ -0,0 +1,54 @@
1
+ require 'aws-sdk-codecommit'
2
+ require 'time'
3
+ require 'cfnguardian/log'
4
+
5
+ module CfnGuardian
6
+ class CodeCommit
7
+ include Logging
8
+
9
+ def initialize(repo_name)
10
+ @repo_name = repo_name
11
+ @client = Aws::CodeCommit::Client.new()
12
+ end
13
+
14
+ def get_last_commit(branch='master')
15
+ resp = @client.get_branch({
16
+ repository_name: @repo_name,
17
+ branch_name: branch,
18
+ })
19
+ return resp.branch.commit_id
20
+ end
21
+
22
+ def get_commit_history(branch='master',count=10)
23
+ history = []
24
+ commit = get_last_commit(branch)
25
+
26
+ count.times do
27
+
28
+ resp = @client.get_commit({
29
+ repository_name: @repo_name,
30
+ commit_id: commit
31
+ })
32
+
33
+ time = Time.strptime(resp.commit.committer.date,'%s')
34
+
35
+ history << {
36
+ message: resp.commit.message,
37
+ author: resp.commit.author.name,
38
+ date: time.localtime.strftime("%d/%m/%Y %I:%M %p"),
39
+ id: resp.commit.commit_id
40
+ }
41
+
42
+ if resp.commit.parents.any?
43
+ commit = resp.commit.parents.first
44
+ else
45
+ break
46
+ end
47
+
48
+ end
49
+
50
+ return history
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,138 @@
1
+ require 'aws-sdk-codepipeline'
2
+ require 'time'
3
+ require 'cfnguardian/log'
4
+
5
+ module CfnGuardian
6
+ class CodePipeline
7
+ include Logging
8
+
9
+ def initialize(pipeline_name)
10
+ @pipeline_name = pipeline_name
11
+ client = Aws::CodePipeline::Client.new()
12
+ @pipeline = client.get_pipeline_state({
13
+ name: @pipeline_name
14
+ })
15
+ end
16
+
17
+ def retry()
18
+ resp = client.start_pipeline_execution({
19
+ name: @pipeline_name,
20
+ client_request_token: "ClientRequestToken",
21
+ })
22
+ end
23
+
24
+ def get_stage(stage_name)
25
+ return @pipeline.stage_states.find {|stage| stage.stage_name == stage_name}
26
+ end
27
+
28
+ def colour_rows(rows, status)
29
+ if status == 'Failed'
30
+ rows.map! {|row| row.map! {|r| r.red } }
31
+ elsif status == 'Succeeded'
32
+ rows.map! {|row| row.map! {|r| r.green } }
33
+ elsif status == 'InProgress'
34
+ rows.map! {|row| row.map! {|r| r.blue } }
35
+ elsif ["Stopped", "Stopping"].include? status
36
+ rows.map! {|row| row.map! {|r| r.yellow } }
37
+ end
38
+ end
39
+
40
+ def get_source()
41
+ source_stage = get_stage("Source")
42
+ action = source_stage.action_states.first
43
+ status = source_stage.latest_execution.status
44
+ state = {
45
+ stage: action.action_name,
46
+ rows: [
47
+ ['Status', status],
48
+ ['Commit', action.current_revision.revision_id],
49
+ ['Last Status Change', action.latest_execution.last_status_change.localtime.strftime("%d/%m/%Y %I:%M %p")]
50
+ ]
51
+ }
52
+
53
+ unless action.latest_execution.error_details.nil?
54
+ state[:rows].push(
55
+ ['Error Message', action.latest_execution.error_details.message]
56
+ )
57
+ end
58
+
59
+ colour_rows(state[:rows],status)
60
+
61
+ return state
62
+ end
63
+
64
+ def get_build()
65
+ source_stage = get_stage("Build")
66
+ action = source_stage.action_states.first
67
+ status = source_stage.latest_execution.status
68
+ state = {
69
+ stage: action.action_name,
70
+ rows: [
71
+ ['Status', status],
72
+ ['Build Id', action.latest_execution.external_execution_id],
73
+ ['Last Status Change', action.latest_execution.last_status_change.localtime.strftime("%d/%m/%Y %I:%M %p")],
74
+ ['Logs', action.latest_execution.external_execution_url]
75
+ ]
76
+ }
77
+
78
+ unless action.latest_execution.error_details.nil?
79
+ state[:rows].push(
80
+ ['Error Message', action.latest_execution.error_details.message]
81
+ )
82
+ end
83
+
84
+ colour_rows(state[:rows],status)
85
+
86
+ return state
87
+ end
88
+
89
+ def get_create_changeset()
90
+ source_stage = get_stage("Deploy")
91
+ action = source_stage.action_states.find {|action| action.action_name == "CreateChangeSet"}
92
+ status = source_stage.latest_execution.status
93
+ state = {
94
+ stage: action.action_name,
95
+ rows: [
96
+ ['Status', status],
97
+ ['Summary', action.latest_execution.summary],
98
+ ['Last Status Change', action.latest_execution.last_status_change.localtime.strftime("%d/%m/%Y %I:%M %p")],
99
+ ]
100
+ }
101
+
102
+ unless action.latest_execution.error_details.nil?
103
+ state[:rows].push(
104
+ ['Error Message', action.latest_execution.error_details.message]
105
+ )
106
+ end
107
+
108
+ colour_rows(state[:rows],status)
109
+
110
+ return state
111
+ end
112
+
113
+ def get_deploy_changeset()
114
+ source_stage = get_stage("Deploy")
115
+ action = source_stage.action_states.find {|action| action.action_name == "DeployChangeSet"}
116
+ status = source_stage.latest_execution.status
117
+ state = {
118
+ stage: action.action_name,
119
+ rows: [
120
+ ['Status', status],
121
+ ['Summary', action.latest_execution.summary],
122
+ ['Last Status Change', action.latest_execution.last_status_change.localtime.strftime("%d/%m/%Y %I:%M %p")],
123
+ ]
124
+ }
125
+
126
+ unless action.latest_execution.error_details.nil?
127
+ state[:rows].push(
128
+ ['Error Message', action.latest_execution.error_details.message]
129
+ )
130
+ end
131
+
132
+ colour_rows(state[:rows],status)
133
+
134
+ return state
135
+ end
136
+
137
+ end
138
+ end
@@ -1,7 +1,9 @@
1
1
  require 'yaml'
2
+ require 'fileutils'
2
3
  require 'cfnguardian/string'
3
4
  require 'cfnguardian/stacks/resources'
4
5
  require 'cfnguardian/stacks/main'
6
+ require 'cfnguardian/models/composite'
5
7
  require 'cfnguardian/resources/base'
6
8
  require 'cfnguardian/resources/apigateway'
7
9
  require 'cfnguardian/resources/application_targetgroup'
@@ -18,6 +20,9 @@ require 'cfnguardian/resources/elastic_file_system'
18
20
  require 'cfnguardian/resources/elasticache_replication_group'
19
21
  require 'cfnguardian/resources/elastic_loadbalancer'
20
22
  require 'cfnguardian/resources/http'
23
+ require 'cfnguardian/resources/internal_http'
24
+ require 'cfnguardian/resources/port'
25
+ require 'cfnguardian/resources/internal_port'
21
26
  require 'cfnguardian/resources/nrpe'
22
27
  require 'cfnguardian/resources/lambda'
23
28
  require 'cfnguardian/resources/network_targetgroup'
@@ -26,24 +31,31 @@ require 'cfnguardian/resources/rds_instance'
26
31
  require 'cfnguardian/resources/redshift_cluster'
27
32
  require 'cfnguardian/resources/sql'
28
33
  require 'cfnguardian/resources/sqs_queue'
34
+ require 'cfnguardian/resources/log_group'
35
+ require 'cfnguardian/resources/sftp'
36
+ require 'cfnguardian/resources/internal_sftp'
37
+ require 'cfnguardian/resources/tls'
29
38
 
30
39
  module CfnGuardian
31
40
  class Compile
32
41
  include Logging
33
42
 
34
- attr_reader :cost, :resources
43
+ attr_reader :cost, :resources, :topics
35
44
 
36
- def initialize(opts,bucket)
37
- @prefix = opts.fetch(:stack_name,'guardian')
38
- @bucket = bucket
39
-
40
- config = YAML.load_file(opts.fetch(:config))
45
+ def initialize(config_file)
46
+ config = YAML.load_file(config_file)
47
+
41
48
  @resource_groups = config.fetch('Resources',{})
49
+ @composites = config.fetch('Composites',{})
42
50
  @templates = config.fetch('Templates',{})
51
+ @topics = config.fetch('Topics',{})
52
+ @maintenance_groups = config.fetch('MaintenaceGroups', {})
43
53
 
54
+ @maintenance_group_list = @maintenance_groups.keys.map {|group| "#{group}MaintenanceGroup"}
44
55
  @resources = []
45
56
  @stacks = []
46
57
  @checks = []
58
+ @ssm_parameters = []
47
59
 
48
60
  @cost = 0
49
61
  end
@@ -57,7 +69,7 @@ module CfnGuardian
57
69
  rescue NameError => e
58
70
  if @templates.has_key?(group) && @templates[group].has_key?('Inherit')
59
71
  begin
60
- resource_class = Kernel.const_get("CfnGuardian::Resource::#{@templates[group]['Inherit']}").new(resource)
72
+ resource_class = Kernel.const_get("CfnGuardian::Resource::#{@templates[group]['Inherit']}").new(resource, group)
61
73
  logger.debug "Inheritited resource group #{@templates[group]['Inherit']} for group #{group}"
62
74
  rescue NameError => e
63
75
  logger.warn "'#{@templates[group]['Inherit']}' resource group doesn't exist and is unable to be inherited from"
@@ -70,40 +82,69 @@ module CfnGuardian
70
82
  end
71
83
 
72
84
  overides = @templates.has_key?(group) ? @templates[group] : {}
73
- @resources.concat resource_class.get_alarms(overides)
85
+ @resources.concat resource_class.get_alarms(overides,resource)
86
+ @resources.concat resource_class.get_metric_filters()
74
87
  @resources.concat resource_class.get_events()
75
88
  @checks.concat resource_class.get_checks()
76
89
 
77
90
  @cost += resource_class.get_cost
78
91
  end
79
92
  end
93
+
94
+ @maintenance_groups.each do |maintenance_group,resource_groups|
95
+ resource_groups.each do |group, alarms|
96
+ alarms.each do |alarm, resources|
97
+ resources.each do |resource|
98
+ res = @resources.find {|r|
99
+ (r.type == 'Alarm') &&
100
+ (r.class == group && r.name == alarm) &&
101
+ (r.resource_id == resource['Id'] || r.resource_name == resource['Name'])}
102
+ unless res.nil?
103
+ res.maintenance_groups.append("#{maintenance_group}MaintenanceGroup")
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ @composites.each do |name,params|
111
+ @resources.push CfnGuardian::Models::Composite.new(name,params)
112
+ @cost += 0.50
113
+ end
114
+
115
+ @ssm_parameters = @resources.select {|resource| resource.type == 'Event'}.map {|event| event.ssm_parameters}.flatten.uniq
116
+ end
117
+
118
+ def alarms
119
+ @resources.select {|resource| resource.type == 'Alarm'}
80
120
  end
81
121
 
82
- def split_resources
122
+ def split_resources(bucket,path)
83
123
  split = @resources.each_slice(200).to_a
84
124
  split.each_with_index do |resources,index|
85
125
  @stacks.push({
86
126
  'Name' => "GuardianStack#{index}",
87
- 'TemplateURL' => "https://#{@bucket}.s3.amazonaws.com/#{@prefix}/guardian-stack-#{index}.compiled.yaml",
127
+ 'TemplateURL' => "https://#{bucket}.s3.amazonaws.com/#{path}/guardian-stack-#{index}.compiled.yaml",
88
128
  'Reference' => index
89
129
  })
90
130
  end
91
131
  return split
92
132
  end
93
133
 
94
- def compile_templates
134
+ def compile_templates(bucket,path)
95
135
  clean_out_directory()
96
- resources = split_resources()
136
+ resources = split_resources(bucket,path)
97
137
 
98
138
  main_stack = CfnGuardian::Stacks::Main.new()
99
- template = main_stack.build_template(@stacks,@checks)
100
- valid = template.validate
139
+ main_stack.build_template(@stacks,@checks,@topics,@maintenance_group_list,@ssm_parameters)
140
+ valid = main_stack.template.validate
141
+ FileUtils.mkdir_p 'out'
101
142
  File.write("out/guardian.compiled.yaml", JSON.parse(valid.to_json).to_yaml)
102
143
 
103
144
  resources.each_with_index do |resources,index|
104
- stack = CfnGuardian::Stacks::Resources.new()
105
- template = stack.build_template(resources)
106
- valid = template.validate
145
+ stack = CfnGuardian::Stacks::Resources.new(main_stack.parameters)
146
+ stack.build_template(resources)
147
+ valid = stack.template.validate
107
148
  File.write("out/guardian-stack-#{index}.compiled.yaml", JSON.parse(valid.to_json).to_yaml)
108
149
  end
109
150
  end
@@ -0,0 +1,94 @@
1
+ Resources:
2
+ AmazonMQBroker:
3
+ - Id: Default
4
+ ApiGateway:
5
+ - Id: Default
6
+ ApplicationTargetGroup:
7
+ - Id: Default
8
+ LoadBalancer: Default
9
+ AutoScalingGroup:
10
+ - Id: Default
11
+ CloudFrontDistribution:
12
+ - Id: Default
13
+ DomainExpiry:
14
+ - Id: Default
15
+ DynamoDBTable:
16
+ - Id: Default
17
+ Ec2Instance:
18
+ - Id: Default
19
+ ECSCluster:
20
+ - Id: Default
21
+ ECSService:
22
+ - Id: Default
23
+ Cluster: Default
24
+ ElasticFileSystem:
25
+ - Id: Default
26
+ ElasticLoadBalancer:
27
+ - Id: Default
28
+ ElastiCacheReplicationGroup:
29
+ - Id: Default
30
+ Http:
31
+ - Id: Default
32
+ StatusCode: 200
33
+ Ssl: true
34
+ BodyRegex: Default
35
+ InternalHttp:
36
+ - Environment: Default
37
+ VpcId: vpc-default
38
+ Subnets:
39
+ - subnet-default
40
+ Hosts:
41
+ - Id: Default
42
+ StatusCode: 200
43
+ BodyRegex: Default
44
+ Port:
45
+ - Id: Default
46
+ Port: 0
47
+ InternalPort:
48
+ - Environment: Default
49
+ VpcId: vpc-default
50
+ Subnets:
51
+ - subnet-default
52
+ Hosts:
53
+ - Id: Default
54
+ Port: 0
55
+ Lambda:
56
+ - Id: Default
57
+ LogGroup:
58
+ - Id: Default
59
+ MetricFilters:
60
+ - MetricName: Default
61
+ Pattern: Default
62
+ NetworkTargetGroup:
63
+ - Id: Default
64
+ LoadBalancer: Default
65
+ Nrpe:
66
+ - Environment: Default
67
+ VpcId: vpc-default
68
+ Subnets:
69
+ - subnet-default
70
+ Hosts:
71
+ - Id: Default
72
+ Commands:
73
+ - default
74
+ RDSClusterInstance:
75
+ - Id: Default
76
+ RDSInstance:
77
+ - Id: Default
78
+ RedshiftCluster:
79
+ - Id: Default
80
+ Sql:
81
+ - Environment: Default
82
+ VpcId: vpc-default
83
+ Subnets:
84
+ - subnet-default
85
+ Hosts:
86
+ - Id: Default
87
+ SecretId: Default
88
+ Engine: default
89
+ Queries:
90
+ - MetricName: Default
91
+ Query: Default
92
+ SQSQueue:
93
+ - Id: Default
94
+
@@ -0,0 +1,164 @@
1
+ require 'cfnguardian/cloudwatch'
2
+ require 'cfnguardian/string'
3
+ require 'time'
4
+
5
+ module CfnGuardian
6
+ class DisplayFormatter
7
+
8
+ def initialize(alarms=[])
9
+ @alarms = alarms
10
+ end
11
+
12
+ def alarms()
13
+ resp = []
14
+
15
+ @alarms.each do |alarm|
16
+ alarm_name = CfnGuardian::CloudWatch.get_alarm_name(alarm)
17
+ puts alarm_name
18
+ rows = [
19
+ ['ResourceId', alarm.resource_id],
20
+ ['ResourceHash', alarm.resource_hash],
21
+ ['ResourceName', alarm.resource_name],
22
+ ['Enabled', alarm.enabled],
23
+ ['MetricName', alarm.metric_name],
24
+ ['Dimensions', alarm.dimensions],
25
+ ['Threshold', alarm.threshold],
26
+ ['Period', alarm.period],
27
+ ['EvaluationPeriods', alarm.evaluation_periods],
28
+ ['ComparisonOperator', alarm.comparison_operator],
29
+ ['Statistic', alarm.statistic],
30
+ ['ActionsEnabled', alarm.actions_enabled],
31
+ ['DatapointsToAlarm', alarm.datapoints_to_alarm],
32
+ ['ExtendedStatistic', alarm.extended_statistic],
33
+ ['EvaluateLowSampleCountPercentile', alarm.evaluate_low_sample_count_percentile],
34
+ ['Unit', alarm.unit],
35
+ ['AlarmAction', alarm.alarm_action],
36
+ ['TreatMissingData', alarm.treat_missing_data]
37
+ ]
38
+
39
+ rows.select! {|row| !row[1].nil?}
40
+
41
+ resp << {
42
+ title: "#{alarm.group}::#{alarm.name}".green + "\n" + alarm_name.green,
43
+ rows: rows
44
+ }
45
+ end
46
+
47
+ return resp
48
+ end
49
+
50
+ def compare_alarms(metric_alarms)
51
+ resp = []
52
+
53
+ @alarms.each do |alarm|
54
+ alarm_name = CfnGuardian::CloudWatch.get_alarm_name(alarm)
55
+ metric_alarm = metric_alarms.find {|ma| ma.alarm_name == alarm_name}
56
+ dimensions = metric_alarm.dimensions.map {|dim| {dim.name.to_sym => dim.value}}.inject(:merge)
57
+
58
+ rows = [
59
+ ['ResourceId', alarm.resource_id, alarm.resource_id],
60
+ ['ResourceHash', alarm.resource_hash, alarm.resource_hash],
61
+ ['ResourceName', alarm.resource_name, alarm.resource_name],
62
+ ['Enabled', alarm.enabled, true],
63
+ ['MetricName', alarm.metric_name, metric_alarm.metric_name],
64
+ ['Dimensions', alarm.dimensions, dimensions],
65
+ ['Threshold', alarm.threshold.to_f, metric_alarm.threshold],
66
+ ['Period', alarm.period, metric_alarm.period],
67
+ ['EvaluationPeriods', alarm.evaluation_periods, metric_alarm.evaluation_periods],
68
+ ['ComparisonOperator', alarm.comparison_operator, metric_alarm.comparison_operator],
69
+ ['Statistic', alarm.statistic, metric_alarm.statistic],
70
+ ['ActionsEnabled', alarm.actions_enabled, metric_alarm.actions_enabled],
71
+ ['DatapointsToAlarm', alarm.datapoints_to_alarm, metric_alarm.datapoints_to_alarm],
72
+ ['ExtendedStatistic', alarm.extended_statistic, metric_alarm.extended_statistic],
73
+ ['EvaluateLowSampleCountPercentile', alarm.evaluate_low_sample_count_percentile, metric_alarm.evaluate_low_sample_count_percentile],
74
+ ['Unit', alarm.unit, metric_alarm.unit],
75
+ ['TreatMissingData', alarm.treat_missing_data, metric_alarm.treat_missing_data],
76
+ ['AlarmAction', alarm.alarm_action, alarm.alarm_action]
77
+ ]
78
+
79
+ rows.select! {|row| !row[1].nil?}.each {|row| colour_compare_row(row)}
80
+
81
+ if has_config_difference?(rows)
82
+ resp << {
83
+ title: "#{alarm.group}::#{alarm.name}".green + "\n" + alarm_name.green,
84
+ rows: rows
85
+ }
86
+ end
87
+ end
88
+
89
+ return resp
90
+ end
91
+
92
+ def alarm_state(metric_alarms)
93
+ rows = []
94
+
95
+ metric_alarms.each do |ma|
96
+ if ma.state_value == 'ALARM'
97
+ state_value = ma.state_value.to_s.red
98
+ elsif ma.state_value == 'INSUFFICIENT_DATA'
99
+ state_value = ma.state_value.to_s.yellow
100
+ else
101
+ state_value = ma.state_value.to_s.green
102
+ end
103
+
104
+ rows << [
105
+ ma.alarm_name,
106
+ state_value,
107
+ ma.state_updated_timestamp.localtime,
108
+ ma.actions_enabled ? 'ENABLED'.green : 'DISABLED'.red
109
+ ]
110
+ end
111
+ # sort by state_value
112
+ return rows.sort_by {|r| r[3]}
113
+ end
114
+
115
+ def alarm_history(history,type)
116
+ rows = []
117
+ line_width = 100
118
+
119
+ history.each do |item|
120
+ data = JSON.load(item.history_data)
121
+
122
+ case type
123
+ when "StateUpdate"
124
+ rows << [
125
+ item.timestamp.localtime,
126
+ item.history_summary,
127
+ data['newState']['stateReason'].word_wrap
128
+ ]
129
+ when "ConfigurationUpdate"
130
+ updated = []
131
+ if data['type'] == 'Update'
132
+ data['originalUpdatedFields'].each do |k,v|
133
+ unless k == 'alarmConfigurationUpdatedTimestamp'
134
+ updated << "#{k}: #{v} -> #{data['updatedAlarm'][k]}"
135
+ end
136
+ end
137
+ end
138
+ rows << [
139
+ item.timestamp.localtime,
140
+ data['type'],
141
+ updated.join("\n").word_wrap
142
+ ]
143
+ end
144
+ end
145
+
146
+ return rows
147
+ end
148
+
149
+ private
150
+
151
+ def has_config_difference?(rows)
152
+ rows.each do |row|
153
+ unless row[1].eql?(row[2])
154
+ return true
155
+ end
156
+ end
157
+ return false
158
+ end
159
+
160
+ def colour_compare_row(row)
161
+ return row[1].eql?(row[2]) ? row.map! {|r| r.to_s.green} : row.map! {|r| r.to_s.red}
162
+ end
163
+ end
164
+ end