inspec 4.22.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +63 -0
  3. data/inspec.gemspec +36 -0
  4. data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/Gemfile +11 -0
  5. data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/inspec-plugin-template.gemspec +43 -0
  6. data/lib/plugins/inspec-init/templates/profiles/aws/README.md +192 -0
  7. data/lib/plugins/inspec-init/templates/profiles/aws/attributes.yml +2 -0
  8. data/lib/plugins/inspec-init/templates/profiles/aws/controls/example.rb +39 -0
  9. data/lib/plugins/inspec-init/templates/profiles/aws/inspec.yml +22 -0
  10. data/lib/plugins/inspec-init/templates/profiles/azure/README.md +56 -0
  11. data/lib/plugins/inspec-init/templates/profiles/azure/controls/example.rb +14 -0
  12. data/lib/plugins/inspec-init/templates/profiles/azure/inspec.yml +14 -0
  13. data/lib/plugins/inspec-init/templates/profiles/gcp/README.md +66 -0
  14. data/lib/plugins/inspec-init/templates/profiles/gcp/attributes.yml +2 -0
  15. data/lib/plugins/inspec-init/templates/profiles/gcp/controls/example.rb +27 -0
  16. data/lib/plugins/inspec-init/templates/profiles/gcp/inspec.yml +19 -0
  17. data/lib/resource_support/aws.rb +76 -0
  18. data/lib/resource_support/aws/aws_backend_base.rb +12 -0
  19. data/lib/resource_support/aws/aws_backend_factory_mixin.rb +12 -0
  20. data/lib/resource_support/aws/aws_plural_resource_mixin.rb +24 -0
  21. data/lib/resource_support/aws/aws_resource_mixin.rb +69 -0
  22. data/lib/resource_support/aws/aws_singular_resource_mixin.rb +27 -0
  23. data/lib/resources/aws/aws_billing_report.rb +107 -0
  24. data/lib/resources/aws/aws_billing_reports.rb +74 -0
  25. data/lib/resources/aws/aws_cloudtrail_trail.rb +97 -0
  26. data/lib/resources/aws/aws_cloudtrail_trails.rb +51 -0
  27. data/lib/resources/aws/aws_cloudwatch_alarm.rb +67 -0
  28. data/lib/resources/aws/aws_cloudwatch_log_metric_filter.rb +105 -0
  29. data/lib/resources/aws/aws_config_delivery_channel.rb +74 -0
  30. data/lib/resources/aws/aws_config_recorder.rb +99 -0
  31. data/lib/resources/aws/aws_ebs_volume.rb +127 -0
  32. data/lib/resources/aws/aws_ebs_volumes.rb +69 -0
  33. data/lib/resources/aws/aws_ec2_instance.rb +162 -0
  34. data/lib/resources/aws/aws_ec2_instances.rb +69 -0
  35. data/lib/resources/aws/aws_ecs_cluster.rb +88 -0
  36. data/lib/resources/aws/aws_eks_cluster.rb +105 -0
  37. data/lib/resources/aws/aws_elb.rb +85 -0
  38. data/lib/resources/aws/aws_elbs.rb +84 -0
  39. data/lib/resources/aws/aws_flow_log.rb +106 -0
  40. data/lib/resources/aws/aws_iam_access_key.rb +112 -0
  41. data/lib/resources/aws/aws_iam_access_keys.rb +153 -0
  42. data/lib/resources/aws/aws_iam_group.rb +62 -0
  43. data/lib/resources/aws/aws_iam_groups.rb +56 -0
  44. data/lib/resources/aws/aws_iam_password_policy.rb +121 -0
  45. data/lib/resources/aws/aws_iam_policies.rb +57 -0
  46. data/lib/resources/aws/aws_iam_policy.rb +311 -0
  47. data/lib/resources/aws/aws_iam_role.rb +60 -0
  48. data/lib/resources/aws/aws_iam_root_user.rb +82 -0
  49. data/lib/resources/aws/aws_iam_user.rb +145 -0
  50. data/lib/resources/aws/aws_iam_users.rb +160 -0
  51. data/lib/resources/aws/aws_kms_key.rb +100 -0
  52. data/lib/resources/aws/aws_kms_keys.rb +58 -0
  53. data/lib/resources/aws/aws_rds_instance.rb +74 -0
  54. data/lib/resources/aws/aws_route_table.rb +67 -0
  55. data/lib/resources/aws/aws_route_tables.rb +64 -0
  56. data/lib/resources/aws/aws_s3_bucket.rb +142 -0
  57. data/lib/resources/aws/aws_s3_bucket_object.rb +87 -0
  58. data/lib/resources/aws/aws_s3_buckets.rb +52 -0
  59. data/lib/resources/aws/aws_security_group.rb +314 -0
  60. data/lib/resources/aws/aws_security_groups.rb +71 -0
  61. data/lib/resources/aws/aws_sns_subscription.rb +82 -0
  62. data/lib/resources/aws/aws_sns_topic.rb +57 -0
  63. data/lib/resources/aws/aws_sns_topics.rb +60 -0
  64. data/lib/resources/aws/aws_sqs_queue.rb +66 -0
  65. data/lib/resources/aws/aws_subnet.rb +92 -0
  66. data/lib/resources/aws/aws_subnets.rb +56 -0
  67. data/lib/resources/aws/aws_vpc.rb +77 -0
  68. data/lib/resources/aws/aws_vpcs.rb +55 -0
  69. data/lib/resources/azure/azure_backend.rb +379 -0
  70. data/lib/resources/azure/azure_generic_resource.rb +55 -0
  71. data/lib/resources/azure/azure_resource_group.rb +151 -0
  72. data/lib/resources/azure/azure_virtual_machine.rb +262 -0
  73. data/lib/resources/azure/azure_virtual_machine_data_disk.rb +131 -0
  74. metadata +202 -0
@@ -0,0 +1,74 @@
1
+ require "inspec/utils/filter"
2
+ require "resource_support/aws/aws_plural_resource_mixin"
3
+ require "resource_support/aws/aws_backend_base"
4
+ require "aws-sdk-costandusagereportservice"
5
+
6
+ class AwsBillingReports < Inspec.resource(1)
7
+ name "aws_billing_reports"
8
+ supports platform: "aws"
9
+ desc "Verifies settings for AWS Cost and Billing Reports."
10
+ example <<~EXAMPLE
11
+ describe aws_billing_reports do
12
+ its('report_names') { should include 'inspec1' }
13
+ its('s3_buckets') { should include 'inspec1-s3-bucket' }
14
+ end
15
+
16
+ describe aws_billing_reports.where { report_name =~ /inspec.*/ } do
17
+ its ('report_names') { should include ['inspec1'] }
18
+ its ('time_units') { should include ['DAILY'] }
19
+ its ('s3_buckets') { should include ['inspec1-s3-bucket'] }
20
+ end
21
+ EXAMPLE
22
+
23
+ include AwsPluralResourceMixin
24
+
25
+ filtertable = FilterTable.create
26
+ filtertable.register_custom_matcher(:exists?) { |x| !x.entries.empty? }
27
+ .register_column(:report_names, field: :report_name)
28
+ .register_column(:time_units, field: :time_unit, style: :simple)
29
+ .register_column(:formats, field: :format, style: :simple)
30
+ .register_column(:compressions, field: :compression, style: :simple)
31
+ .register_column(:s3_buckets, field: :s3_bucket, style: :simple)
32
+ .register_column(:s3_prefixes, field: :s3_prefix, style: :simple)
33
+ .register_column(:s3_regions, field: :s3_region, style: :simple)
34
+ filtertable.install_filter_methods_on_resource(self, :table)
35
+
36
+ def validate_params(resource_params)
37
+ unless resource_params.empty?
38
+ raise ArgumentError, "aws_billing_reports does not accept resource parameters."
39
+ end
40
+
41
+ resource_params
42
+ end
43
+
44
+ def to_s
45
+ "AWS Billing Reports"
46
+ end
47
+
48
+ def fetch_from_api
49
+ @table = []
50
+ pagination_opts = {}
51
+ backend = BackendFactory.create(inspec_runner)
52
+ loop do
53
+ api_result = backend.describe_report_definitions(pagination_opts)
54
+ api_result.report_definitions.each do |raw_report|
55
+ report = raw_report.to_h
56
+ %i{time_unit compression}.each { |field| report[field].downcase! }
57
+ @table << report
58
+ end
59
+ pagination_opts = { next_token: api_result.next_token }
60
+ break unless api_result.next_token
61
+ end
62
+ end
63
+
64
+ class Backend
65
+ class AwsClientApi < AwsBackendBase
66
+ AwsBillingReports::BackendFactory.set_default_backend(self)
67
+ self.aws_client_class = Aws::CostandUsageReportService::Client
68
+
69
+ def describe_report_definitions(options = {})
70
+ aws_service_client.describe_report_definitions(options)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,97 @@
1
+ require "resource_support/aws/aws_singular_resource_mixin"
2
+ require "resource_support/aws/aws_backend_base"
3
+ require "aws-sdk-cloudtrail"
4
+
5
+ class AwsCloudTrailTrail < Inspec.resource(1)
6
+ name "aws_cloudtrail_trail"
7
+ desc "Verifies settings for an individual AWS CloudTrail Trail"
8
+ example <<~EXAMPLE
9
+ describe aws_cloudtrail_trail('trail-name') do
10
+ it { should exist }
11
+ end
12
+ EXAMPLE
13
+
14
+ supports platform: "aws"
15
+
16
+ include AwsSingularResourceMixin
17
+ attr_reader :cloud_watch_logs_log_group_arn, :cloud_watch_logs_role_arn, :home_region,
18
+ :kms_key_id, :s3_bucket_name, :trail_arn
19
+
20
+ def to_s
21
+ "CloudTrail #{@trail_name}"
22
+ end
23
+
24
+ def multi_region_trail?
25
+ @is_multi_region_trail
26
+ end
27
+
28
+ def log_file_validation_enabled?
29
+ @log_file_validation_enabled
30
+ end
31
+
32
+ def encrypted?
33
+ !kms_key_id.nil?
34
+ end
35
+
36
+ def delivered_logs_days_ago
37
+ query = { name: @trail_name }
38
+ catch_aws_errors do
39
+ begin
40
+ resp = BackendFactory.create(inspec_runner).get_trail_status(query).to_h
41
+ ((Time.now - resp[:latest_cloud_watch_logs_delivery_time]) / (24 * 60 * 60)).to_i unless resp[:latest_cloud_watch_logs_delivery_time].nil?
42
+ rescue Aws::CloudTrail::Errors::TrailNotFoundException
43
+ nil
44
+ end
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def validate_params(raw_params)
51
+ validated_params = check_resource_param_names(
52
+ raw_params: raw_params,
53
+ allowed_params: [:trail_name],
54
+ allowed_scalar_name: :trail_name,
55
+ allowed_scalar_type: String
56
+ )
57
+
58
+ if validated_params.empty?
59
+ raise ArgumentError, "You must provide the parameter 'trail_name' to aws_cloudtrail_trail."
60
+ end
61
+
62
+ validated_params
63
+ end
64
+
65
+ def fetch_from_api
66
+ backend = BackendFactory.create(inspec_runner)
67
+
68
+ query = { trail_name_list: [@trail_name] }
69
+ resp = backend.describe_trails(query)
70
+
71
+ @trail = resp.trail_list[0].to_h
72
+ @exists = !@trail.empty?
73
+ @s3_bucket_name = @trail[:s3_bucket_name]
74
+ @is_multi_region_trail = @trail[:is_multi_region_trail]
75
+ @trail_arn = @trail[:trail_arn]
76
+ @log_file_validation_enabled = @trail[:log_file_validation_enabled]
77
+ @cloud_watch_logs_role_arn = @trail[:cloud_watch_logs_role_arn]
78
+ @cloud_watch_logs_log_group_arn = @trail[:cloud_watch_logs_log_group_arn]
79
+ @kms_key_id = @trail[:kms_key_id]
80
+ @home_region = @trail[:home_region]
81
+ end
82
+
83
+ class Backend
84
+ class AwsClientApi < AwsBackendBase
85
+ AwsCloudTrailTrail::BackendFactory.set_default_backend(self)
86
+ self.aws_client_class = Aws::CloudTrail::Client
87
+
88
+ def describe_trails(query)
89
+ aws_service_client.describe_trails(query)
90
+ end
91
+
92
+ def get_trail_status(query)
93
+ aws_service_client.get_trail_status(query)
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,51 @@
1
+ require "resource_support/aws/aws_plural_resource_mixin"
2
+ require "resource_support/aws/aws_backend_base"
3
+ require "aws-sdk-cloudtrail"
4
+
5
+ class AwsCloudTrailTrails < Inspec.resource(1)
6
+ name "aws_cloudtrail_trails"
7
+ desc "Verifies settings for AWS CloudTrail Trails in bulk"
8
+ example <<~EXAMPLE
9
+ describe aws_cloudtrail_trails do
10
+ it { should exist }
11
+ end
12
+ EXAMPLE
13
+ supports platform: "aws"
14
+
15
+ include AwsPluralResourceMixin
16
+
17
+ def validate_params(resource_params)
18
+ unless resource_params.empty?
19
+ raise ArgumentError, "aws_cloudtrail_trails does not accept resource parameters."
20
+ end
21
+
22
+ resource_params
23
+ end
24
+
25
+ # Underlying FilterTable implementation.
26
+ filter = FilterTable.create
27
+ filter.register_custom_matcher(:exists?) { |x| !x.entries.empty? }
28
+ filter.register_column(:trail_arns, field: :trail_arn)
29
+ filter.register_column(:names, field: :name)
30
+ filter.install_filter_methods_on_resource(self, :table)
31
+
32
+ def to_s
33
+ "CloudTrail Trails"
34
+ end
35
+
36
+ def fetch_from_api
37
+ backend = BackendFactory.create(inspec_runner)
38
+ @table = backend.describe_trails({}).to_h[:trail_list]
39
+ end
40
+
41
+ class Backend
42
+ class AwsClientApi < AwsBackendBase
43
+ AwsCloudTrailTrails::BackendFactory.set_default_backend(self)
44
+ self.aws_client_class = Aws::CloudTrail::Client
45
+
46
+ def describe_trails(query)
47
+ aws_service_client.describe_trails(query)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,67 @@
1
+ require "resource_support/aws/aws_singular_resource_mixin"
2
+ require "resource_support/aws/aws_backend_base"
3
+ require "aws-sdk-cloudwatch"
4
+
5
+ class AwsCloudwatchAlarm < Inspec.resource(1)
6
+ name "aws_cloudwatch_alarm"
7
+ desc <<~EXAMPLE
8
+ # Look for a specific alarm
9
+ aws_cloudwatch_alarm(
10
+ metric_name: 'my-metric-name',
11
+ metric_namespace: 'my-metric-namespace',
12
+ ) do
13
+ it { should exist }
14
+ end
15
+ EXAMPLE
16
+ supports platform: "aws"
17
+
18
+ include AwsSingularResourceMixin
19
+ attr_reader :alarm_actions, :alarm_name, :metric_name, :metric_namespace
20
+
21
+ private
22
+
23
+ def validate_params(raw_params)
24
+ recognized_params = check_resource_param_names(
25
+ raw_params: raw_params,
26
+ allowed_params: %i{metric_name metric_namespace}
27
+ )
28
+ validated_params = {}
29
+ # Currently you must specify exactly metric_name and metric_namespace
30
+ %i{metric_name metric_namespace}.each do |param|
31
+ raise ArgumentError, "Missing resource param #{param}" unless recognized_params.key?(param)
32
+
33
+ validated_params[param] = recognized_params.delete(param)
34
+ end
35
+
36
+ validated_params
37
+ end
38
+
39
+ def fetch_from_api
40
+ aws_alarms = BackendFactory.create(inspec_runner).describe_alarms_for_metric(
41
+ metric_name: @metric_name,
42
+ namespace: @metric_namespace
43
+ )
44
+ if aws_alarms.metric_alarms.empty?
45
+ @exists = false
46
+ elsif aws_alarms.metric_alarms.count > 1
47
+ alarms = aws_alarms.metric_alarms.map(&:alarm_name)
48
+ raise "More than one Cloudwatch Alarm was matched. Try using " \
49
+ "more specific resource parameters. Alarms matched: #{alarms.join(", ")}"
50
+ else
51
+ @alarm_actions = aws_alarms.metric_alarms.first.alarm_actions
52
+ @alarm_name = aws_alarms.metric_alarms.first.alarm_name
53
+ @exists = true
54
+ end
55
+ end
56
+
57
+ class Backend
58
+ class AwsClientApi < AwsBackendBase
59
+ AwsCloudwatchAlarm::BackendFactory.set_default_backend(self)
60
+ self.aws_client_class = Aws::CloudWatch::Client
61
+
62
+ def describe_alarms_for_metric(query)
63
+ aws_service_client.describe_alarms_for_metric(query)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,105 @@
1
+ require "resource_support/aws/aws_singular_resource_mixin"
2
+ require "resource_support/aws/aws_backend_base"
3
+ require "aws-sdk-cloudwatchlogs"
4
+
5
+ class AwsCloudwatchLogMetricFilter < Inspec.resource(1)
6
+ name "aws_cloudwatch_log_metric_filter"
7
+ desc "Verifies individual Cloudwatch Log Metric Filters"
8
+ example <<~EXAMPLE
9
+ # Look for a LMF by its filter name and log group name. This combination
10
+ # will always either find at most one LMF - no duplicates.
11
+ describe aws_cloudwatch_log_metric_filter(
12
+ filter_name: 'my-filter',
13
+ log_group_name: 'my-log-group'
14
+ ) do
15
+ it { should exist }
16
+ end
17
+
18
+ # Search for an LMF by pattern and log group.
19
+ # This could result in an error if the results are not unique.
20
+ describe aws_cloudwatch_log_metric_filter(
21
+ log_group_name: 'my-log-group',
22
+ pattern: 'my-filter'
23
+ ) do
24
+ it { should exist }
25
+ end
26
+ EXAMPLE
27
+ supports platform: "aws"
28
+ include AwsSingularResourceMixin
29
+ attr_reader :filter_name, :log_group_name, :metric_name, :metric_namespace, :pattern
30
+
31
+ private
32
+
33
+ def validate_params(raw_params)
34
+ validated_params = check_resource_param_names(
35
+ raw_params: raw_params,
36
+ allowed_params: %i{filter_name log_group_name pattern}
37
+ )
38
+ if validated_params.empty?
39
+ raise ArgumentError, "You must provide either filter_name, log_group, or pattern to aws_cloudwatch_log_metric_filter."
40
+ end
41
+
42
+ validated_params
43
+ end
44
+
45
+ def fetch_from_api
46
+ # get a backend
47
+ backend = BackendFactory.create(inspec_runner)
48
+
49
+ # Perform query with remote filtering
50
+ aws_search_criteria = {}
51
+ aws_search_criteria[:filter_name] = filter_name if filter_name
52
+ aws_search_criteria[:log_group_name] = log_group_name if log_group_name
53
+ begin
54
+ aws_results = backend.describe_metric_filters(aws_search_criteria)
55
+ rescue Aws::CloudWatchLogs::Errors::ResourceNotFoundException
56
+ @exists = false
57
+ return
58
+ end
59
+
60
+ # Then perform local filtering
61
+ if pattern
62
+ aws_results.select! { |lmf| lmf.filter_pattern == pattern }
63
+ end
64
+
65
+ # Check result count. We're a singular resource and can tolerate
66
+ # 0 or 1 results, not multiple.
67
+ if aws_results.count > 1
68
+ raise "More than one result was returned, but aws_cloudwatch_log_metric_filter "\
69
+ "can only handle a single AWS resource. Consider passing more resource "\
70
+ "parameters to narrow down the search."
71
+ elsif aws_results.empty?
72
+ @exists = false
73
+ else
74
+ @exists = true
75
+ # Unpack the funny-shaped object we got back from AWS into our instance vars
76
+ lmf = aws_results.first
77
+ @filter_name = lmf.filter_name
78
+ @log_group_name = lmf.log_group_name
79
+ @pattern = lmf.filter_pattern # Note inconsistent name
80
+ # AWS SDK returns an array of metric transformations
81
+ # but only allows one (mandatory) entry, let's flatten that
82
+ @metric_name = lmf.metric_transformations.first.metric_name
83
+ @metric_namespace = lmf.metric_transformations.first.metric_namespace
84
+ end
85
+ end
86
+
87
+ class Backend
88
+ # Uses the cloudwatch API to really talk to AWS
89
+ class AwsClientApi < AwsBackendBase
90
+ BackendFactory.set_default_backend(self)
91
+ self.aws_client_class = Aws::CloudWatchLogs::Client
92
+
93
+ def describe_metric_filters(criteria)
94
+ query = {}
95
+ query[:filter_name_prefix] = criteria[:filter_name] if criteria[:filter_name]
96
+ query[:log_group_name] = criteria[:log_group_name] if criteria[:log_group_name]
97
+ # 'pattern' is not available as a remote filter,
98
+ # we filter it after the fact locally
99
+ # TODO: handle pagination? Max 50/page. Maybe you want a plural resource?
100
+ aws_response = aws_service_client.describe_metric_filters(query)
101
+ aws_response.metric_filters
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,74 @@
1
+ require "resource_support/aws/aws_singular_resource_mixin"
2
+ require "resource_support/aws/aws_backend_base"
3
+ require "aws-sdk-configservice"
4
+
5
+ class AwsConfigDeliveryChannel < Inspec.resource(1)
6
+ name "aws_config_delivery_channel"
7
+ desc "Verifies settings for AWS Config Delivery Channel"
8
+ example <<~EXAMPLE
9
+ describe aws_config_delivery_channel do
10
+ it { should exist }
11
+ its('s3_bucket_name') { should eq 'my_bucket' }
12
+ its('sns_topic_arn') { should eq arn:aws:sns:us-east-1:721741954427:sns_topic' }
13
+ end
14
+ EXAMPLE
15
+ supports platform: "aws"
16
+
17
+ include AwsSingularResourceMixin
18
+ attr_reader :channel_name, :s3_bucket_name, :s3_key_prefix, :sns_topic_arn,
19
+ :delivery_frequency_in_hours
20
+
21
+ def to_s
22
+ "Config_Delivery_Channel: #{@channel_name}"
23
+ end
24
+
25
+ private
26
+
27
+ def validate_params(raw_params)
28
+ validated_params = check_resource_param_names(
29
+ raw_params: raw_params,
30
+ allowed_params: [:channel_name],
31
+ allowed_scalar_name: :channel_name,
32
+ allowed_scalar_type: String
33
+ )
34
+
35
+ validated_params
36
+ end
37
+
38
+ def fetch_from_api
39
+ backend = BackendFactory.create(inspec_runner)
40
+ query = @channel_name ? { delivery_channel_names: [@channel_name] } : {}
41
+ response = backend.describe_delivery_channels(query)
42
+
43
+ @exists = !response.delivery_channels.empty?
44
+ return unless exists?
45
+
46
+ channel = response.delivery_channels.first.to_h
47
+ @channel_name = channel[:name]
48
+ @s3_bucket_name = channel[:s3_bucket_name]
49
+ @s3_key_prefix = channel[:s3_key_prefix]
50
+ @sns_topic_arn = channel[:sns_topic_arn]
51
+ @delivery_frequency_in_hours = channel.dig(:config_snapshot_delivery_properties, :delivery_frequency)
52
+ frequencies = {
53
+ "One_Hour" => 1,
54
+ "TwentyFour_Hours" => 24,
55
+ "Three_Hours" => 3,
56
+ "Six_Hours" => 6,
57
+ "Twelve_Hours" => 12,
58
+ }
59
+ @delivery_frequency_in_hours = frequencies[@delivery_frequency_in_hours]
60
+ rescue Aws::ConfigService::Errors::NoSuchDeliveryChannelException
61
+ @exists = false
62
+ end
63
+
64
+ class Backend
65
+ class AwsClientApi < AwsBackendBase
66
+ BackendFactory.set_default_backend(self)
67
+ self.aws_client_class = Aws::ConfigService::Client
68
+
69
+ def describe_delivery_channels(query = {})
70
+ aws_service_client.describe_delivery_channels(query)
71
+ end
72
+ end
73
+ end
74
+ end