From a8cd6fb940d974a9437402a2001afb152ed646ca Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Tue, 14 Nov 2023 09:32:02 -0500 Subject: [PATCH 01/93] recreateing #977 on a fresh pull from main Signed-off-by: Aaron Lippold --- libraries/aws_account.rb | 249 +++++++++++++++++++++++++ libraries/aws_backend.rb | 158 +++++++++++----- libraries/aws_iam_access_keys.rb | 128 ++++++++----- libraries/aws_iam_credential_report.rb | 79 ++++++++ 4 files changed, 521 insertions(+), 93 deletions(-) create mode 100644 libraries/aws_account.rb create mode 100644 libraries/aws_iam_credential_report.rb diff --git a/libraries/aws_account.rb b/libraries/aws_account.rb new file mode 100644 index 000000000..e0491072f --- /dev/null +++ b/libraries/aws_account.rb @@ -0,0 +1,249 @@ +require "aws_backend" +require "pry" + +class AwsPrimaryAccount < AwsResourceBase + name "aws_primary_contact" + desc "Verifies the primary contact information for an AWS Account." + example <<~EXAMPLE + describe aws_primary_account do + it { should be_configured } + its('full_name') { should cmp 'John Smith' } + its('address_line_1') { should cmp '42 Wallaby Way' } + end + EXAMPLE + + attr_reader :table, :raw_data + + FilterTable.create + .register_column(:address_line_1, field: :address_line_1, style: :simple) + .register_column(:adress_line_2, field: :adress_line_2, style: :simple) + .register_column(:address_line_3, field: :address_line_3, style: :simple) + .register_column(:city, field: :city, style: :simple) + .register_column(:company_name, field: :company_name, style: :simple) + .register_column(:country_code, field: :country_code, style: :simple) + .register_column(:district_or_county, field: :district_or_county, style: :simple) + .register_column(:full_name, field: :full_name, style: :simple) + .register_column(:phone_number, field: :phone_number, style: :simple) + .register_column(:postal_code, field: :postal_code, style: :simple) + .register_column(:state_or_region, field: :state_or_region, style: :simple) + .register_column(:website_url, field: :website_url, style: :simple) + .register_custom_matcher(:configured?) { |x| x.entries.any? } + .install_filter_methods_on_resource(self, :table) + + def initialize(opts = {}) + super(opts) + validate_parameters + # binding.pry + @table = fetch_data + end + + def resource_id + "AWS Account for #{@table[0][:full_name]}" || "AWS Account Contact Information" + end + + def to_s + "AWS Account Primary Contact" + end + + def fetch_data + @raw_data = [] + loop do + catch_aws_errors do + @api_response = @aws.account_client.get_contact_information.contact_information + end + return [] if !@api_response || @api_response.empty? + + @raw_data << { + address_line_1: @api_response.address_line_1, + address_line_2: @api_response.address_line_2, + address_line_3: @api_response.address_line_3, + city: @api_response.city, + company_name: @api_response.company_name, + country_code: @api_response.country_code, + district_or_county: @api_response.district_or_county, + full_name: @api_response.full_name, + phone_number: @api_response.phone_number, + postal_code: @api_response.postal_code, + state_or_region: @api_response.state_or_region, + website_url: @api_response.website_url, + } + break + end + @raw_data + end + + # @aws.account_client.get_alternate_contact({alternate_contact_type: "BILLING"}).alternate_contact.to_h.transform_keys(&:to_s) + # resp.alternate_contact.alternate_contact_type #=> String, one of "BILLING", "OPERATIONS", "SECURITY" + # resp.alternate_contact.email_address #=> String + # resp.alternate_contact.name #=> String + # resp.alternate_contact.phone_number #=> String + # resp.alternate_contact.title #=> String + + class AwsBillingAccount < AwsResourceBase + name "aws_billing_contact" + desc "Verifies the billing contact information for an AWS Account." + example <<~EXAMPLE + describe aws_billing_account do + it { should be_configured } + its('name') { should cmp 'John Smith' } + its('email_address') { should cmp 'jsmith@acme.com' } + end + EXAMPLE + + attr_reader :table, :raw_data + + FilterTable.create + .register_column(:email_address, field: :email_address, style: :simple) + .register_column(:name, field: :name, style: :simple) + .register_column(:phone_number, field: :phone_number, style: :simple) + .register_column(:title, field: :title, style: :simple) + .register_custom_matcher(:configured?) { |x| x.entries.any? } + .install_filter_methods_on_resource(self, :table) + + def initialize(opts = {}) + super(opts) + validate_parameters + @table = fetch_data + end + + def resource_id + "AWS Billing for #{@table[0][:name]}" || "AWS Account Billing Information" + end + + def to_s + "AWS Account Billing Contact" + end + + def fetch_data + @raw_data = [] + loop do + catch_aws_errors do + @api_response = @aws.account_client.get_alternate_contact({ alternate_contact_type: "BILLING" }).alternate_contact + end + return [] if !@api_response || @api_response.empty? + + @raw_data << { + email_address: @api_response.email_address, + name: @api_response.name, + phone_number: @api_response.phone_number, + title: @api_response.title, + } + break + end + @raw_data + end + end + + # @aws.account_client.get_alternate_contact({alternate_contact_type: "OPERATIONS"}).alternate_contact.to_h.transform_keys(&:to_s) + + class AwsAccountOperationsContact < AwsResourceBase + name "aws_operations_contact" + desc "Verifies the operations contact information for an AWS Account." + example <<~EXAMPLE + describe aws_operations_account do + it { should be_configured } + its('name') { should cmp 'John Smith' } + its('email_address') { should cmp 'jsmith@acme.com' } + end + EXAMPLE + + attr_reader :table, :raw_data + + FilterTable.create + .register_column(:email_address, field: :email_address, style: :simple) + .register_column(:name, field: :name, style: :simple) + .register_column(:phone_number, field: :phone_number, style: :simple) + .register_column(:title, field: :title, style: :simple) + .register_custom_matcher(:configured?) { |x| x.entries.any? } + .install_filter_methods_on_resource(self, :table) + + def initialize(opts = {}) + super(opts) + validate_parameters + @table = fetch_data + end + + def resource_id + "AWS Operations Contact for #{@table[0][:name]}" || "AWS Account Operations Contact Information" + end + + def to_s + "AWS Account Operations Contact Information" + end + + def fetch_data + @raw_data = [] + loop do + catch_aws_errors do + @api_response = @aws.account_client.get_alternate_contact({ alternate_contact_type: "OPERATIONS" }).alternate_contact + end + return [] if !@api_response || @api_response.empty? + + @raw_data << { + email_address: @api_response.email_address, + name: @api_response.name, + phone_number: @api_response.phone_number, + title: @api_response.title, + } + break + end + @raw_data + end + end + + # @aws.account_client.get_alternate_contact({alternate_contact_type: "SECURITY"}).alternate_contact.to_h.transform_keys(&:to_s) + class AwsAccountSecurityContact < AwsResourceBase + name "aws_security_contact" + desc "Verifies the security contact information for an AWS Account." + example <<~EXAMPLE + describe aws_security_account do + it { should be_configured } + its('name') { should cmp 'John Smith' } + its('email_address') { should cmp 'jsmith@acme.com' } + end + EXAMPLE + + attr_reader :table, :raw_data + + FilterTable.create + .register_column(:email_address, field: :email_address, style: :simple) + .register_column(:name, field: :name, style: :simple) + .register_column(:phone_number, field: :phone_number, style: :simple) + .register_column(:title, field: :title, style: :simple) + .register_custom_matcher(:configured?) { |x| x.entries.any? } + .install_filter_methods_on_resource(self, :table) + + def initialize(opts = {}) + super(opts) + validate_parameters + @table = fetch_data + end + + def resource_id + "AWS Security Contact for #{@table[0][:name]}" || "AWS Account Security Contact Information" + end + + def to_s + "AWS Account Security Contact" + end + + def fetch_data + @raw_data = [] + loop do + catch_aws_errors do + @api_response = @aws.account_client.get_alternate_contact({ alternate_contact_type: "SECURITY" }).alternate_contact + end + return [] if !@api_response || @api_response.empty? + + @raw_data << { + email_address: @api_response.email_address, + name: @api_response.name, + phone_number: @api_response.phone_number, + title: @api_response.title, + } + break + end + @raw_data + end + end +end diff --git a/libraries/aws_backend.rb b/libraries/aws_backend.rb index 217ba5a83..81e52ea5d 100644 --- a/libraries/aws_backend.rb +++ b/libraries/aws_backend.rb @@ -74,9 +74,7 @@ def initialize(params) # This can be useful for e.g. # https://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/stubbing.html # https://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/setup-config.html#aws-ruby-sdk-setting-non-standard-endpoint - if params.is_a?(Hash) - @client_args = params.fetch(:client_args, nil) - end + @client_args = params.fetch(:client_args, nil) if params.is_a?(Hash) @cache = {} end @@ -338,6 +336,10 @@ def synthetics_client def apigatewayv2_client aws_client(Aws::ApiGatewayV2::Client) end + + def account_client + aws_client(Aws::Account::Client) + end end # Base class for AWS resources @@ -353,15 +355,31 @@ def initialize(opts) client_args = { client_args: {} } if opts.is_a?(Hash) # below allows each resource to optionally and conveniently set a region - client_args[:client_args][:region] = opts[:aws_region] if opts[:aws_region] + client_args[:client_args][:region] = opts[:aws_region] if opts[ + :aws_region + ] # below allows each resource to optionally and conveniently set an endpoint - client_args[:client_args][:endpoint] = opts[:aws_endpoint] if opts[:aws_endpoint] + client_args[:client_args][:endpoint] = opts[:aws_endpoint] if opts[ + :aws_endpoint + ] # below allows each resource to optionally and conveniently set max_retries and retry_backoff env_hash = ENV.map { |k, v| [k.downcase, v] }.to_h - opts[:aws_retry_limit]= env_hash["aws_retry_limit"].to_i if !opts[:aws_retry_limit] && env_hash["aws_retry_limit"] - opts[:aws_retry_backoff]= env_hash["aws_retry_backoff"].to_i if !opts[:aws_retry_backoff] && env_hash["aws_retry_backoff"] - client_args[:client_args][:retry_limit] = opts[:aws_retry_limit] if opts[:aws_retry_limit] - client_args[:client_args][:retry_backoff] = "lambda { |c| sleep(#{opts[:aws_retry_backoff]}) }" if opts[:aws_retry_backoff] + opts[:aws_retry_limit] = env_hash["aws_retry_limit"].to_i if !opts[ + :aws_retry_limit + ] && env_hash["aws_retry_limit"] + opts[:aws_retry_backoff] = env_hash["aws_retry_backoff"].to_i if !opts[ + :aws_retry_backoff + ] && env_hash["aws_retry_backoff"] + client_args[:client_args][:retry_limit] = opts[:aws_retry_limit] if opts[ + :aws_retry_limit + ] + if opts[ + :aws_retry_backoff + ] + client_args[:client_args][ + :retry_backoff + ] = "lambda { |c| sleep(#{opts[:aws_retry_backoff]}) }" + end # this catches the stub_data true option for unit testing - and others that could be useful for consumers client_args[:client_args].update(opts[:client_args]) if opts[:client_args] @@ -375,9 +393,14 @@ def initialize(opts) # here we might want to inject stub data for testing, let's use an option for that return if !defined?(@opts.keys) || !@opts.include?(:stub_data) - raise ArgumentError, "Expected stub data to be an array" if !opts[:stub_data].is_a?(Array) + if !opts[:stub_data].is_a?(Array) + raise ArgumentError, "Expected stub data to be an array" + end opts[:stub_data].each do |stub| - raise ArgumentError, "Expect each stub_data hash to have :client, :method and :data keys" if !stub.keys.all? { |a| %i(method data client).include?(a) } + if !stub.keys.all? { |a| %i(method data client).include?(a) } + raise ArgumentError, + "Expect each stub_data hash to have :client, :method and :data keys" + end @aws.aws_client(stub[:client]).stub_responses(stub[:method], stub[:data]) end end @@ -389,24 +412,57 @@ def initialize(opts) # If a parameter is entirely optional, use `allow` def validate_parameters(allow: [], required: nil, require_any_of: nil) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity if required - raise ArgumentError, "Expected required parameters as Array of Symbols, got #{required}" unless required.is_a?(Array) && required.all? { |r| r.is_a?(Symbol) } - raise ArgumentError, "#{@__resource_name__}: `#{required}` must be provided" unless @opts.is_a?(Hash) && required.all? { |req| @opts.key?(req) && !@opts[req].nil? && @opts[req] != "" } + unless required.is_a?(Array) && required.all? { |r| r.is_a?(Symbol) } + raise ArgumentError, + "Expected required parameters as Array of Symbols, got #{required}" + end + unless @opts.is_a?(Hash) && + required.all? { |req| + @opts.key?(req) && !@opts[req].nil? && @opts[req] != "" + } + raise ArgumentError, + "#{@__resource_name__}: `#{required}` must be provided" + end allow += required end if require_any_of - raise ArgumentError, "Expected required parameters as Array of Symbols, got #{require_any_of}" unless require_any_of.is_a?(Array) && require_any_of.all? { |r| r.is_a?(Symbol) } - raise ArgumentError, "#{@__resource_name__}: One of `#{require_any_of}` must be provided." unless @opts.is_a?(Hash) && require_any_of.any? { |req| @opts.key?(req) && !@opts[req].nil? && @opts[req] != "" } + unless require_any_of.is_a?(Array) && + require_any_of.all? { |r| r.is_a?(Symbol) } + raise ArgumentError, + "Expected required parameters as Array of Symbols, got #{require_any_of}" + end + unless @opts.is_a?(Hash) && + require_any_of.any? { |req| + @opts.key?(req) && !@opts[req].nil? && @opts[req] != "" + } + raise ArgumentError, + "#{@__resource_name__}: One of `#{require_any_of}` must be provided." + end allow += require_any_of end - allow += %i(client_args stub_data aws_region aws_endpoint aws_retry_limit aws_retry_backoff resource_data) - raise ArgumentError, "Scalar arguments not supported" unless defined?(@opts.keys) - raise ArgumentError, "Unexpected arguments found" unless @opts.keys.all? { |a| allow.include?(a) } - raise ArgumentError, "Provided parameter should not be empty" unless @opts.values.all? do |a| + allow += %i( + client_args + stub_data + aws_region + aws_endpoint + aws_retry_limit + aws_retry_backoff + resource_data + ) + unless defined?(@opts.keys) + raise ArgumentError, "Scalar arguments not supported" + end + unless @opts.keys.all? { |a| allow.include?(a) } + raise ArgumentError, "Unexpected arguments found" + end + unless @opts.values.all? { |a| return true if a.instance_of?(Integer) return true if [TrueClass, FalseClass].include?(a.class) !a.empty? + } + raise ArgumentError, "Provided parameter should not be empty" end true end @@ -436,6 +492,10 @@ def catch_aws_errors Inspec::Log.error "It appears that you have not set your AWS credentials. See https://www.inspec.io/docs/reference/platforms for details." fail_resource("No AWS credentials available") nil + rescue Aws::Account::Errors::ResourceNotFoundException => e + Inspec::Log.warn "#{e.message}" + skip_resource("#{e.message}") + nil rescue Aws::Errors::NoSuchEndpointError Inspec::Log.error "The endpoint that is trying to be accessed does not exist." fail_resource("Invalid Endpoint error") @@ -448,18 +508,20 @@ def catch_aws_errors when "InvalidAccessKeyId" advice = "Please ensure your AWS Access Key ID is set correctly." when "InvalidClientTokenId" - advice = "Please ensure that the aws access key, aws secret access key, and the aws session token are correct." + advice = + "Please ensure that the aws access key, aws secret access key, and the aws session token are correct." when "AccessDenied" - advice = "Please check the IAM permissions required for this Resource in the documentation, " \ - "and ensure your Service Principal has these permissions set." + advice = + "Please check the IAM permissions required for this Resource in the documentation, " \ + "and ensure your Service Principal has these permissions set." end error_message = "#{e.message}: #{advice}" raise Inspec::Exceptions::ResourceFailed, error_message else Inspec::Log.warn "AWS Service Error encountered running a control with Resource #{@__resource_name__}. " \ - "Error message: #{e.message}. You should address this error to ensure your controls are " \ - "behaving as expected." + "Error message: #{e.message}. You should address this error to ensure your controls are " \ + "behaving as expected." @failed_resource = true end nil @@ -479,9 +541,7 @@ def is_permissions_error(error) def map_tags(tag_list) return {} if tag_list.nil? || tag_list.empty? tags = {} - tag_list.each do |tag| - tags[tag[:key]] = tag[:value] - end + tag_list.each { |tag| tags[tag[:key]] = tag[:value] } tags end @@ -509,9 +569,10 @@ def respond_to_missing?(*several_variants) # This method should be used when AWS API returns multiple resources for the provided criteria. def resource_fail(message = nil) - message ||= "#{@__resource_name__}: #{@display_name}. Multiple AWS resources were returned for the provided criteria. "\ - "If you wish to test multiple entities, please use the plural resource. "\ - "Otherwise, please provide more specific criteria to lookup the resource." + message ||= + "#{@__resource_name__}: #{@display_name}. Multiple AWS resources were returned for the provided criteria. " \ + "If you wish to test multiple entities, please use the plural resource. " \ + "Otherwise, please provide more specific criteria to lookup the resource." # Fail resource in resource pack. `exists?` method will return `false`. @failed_resource = true # Fail resource in InSpec core. Tests in InSpec profile will return the message. @@ -546,14 +607,16 @@ def self.populate_filter_table(raw_data, table_scheme) end def fetch(client:, operation:, kwargs: {}) - raise ArgumentError, "Valid Client not found!" unless @aws.respond_to?(client) + unless @aws.respond_to?(client) + raise ArgumentError, "Valid Client not found!" + end client_obj = @aws.send(client) - raise ArgumentError, "#{client} does not support #{operation}" unless client_obj.respond_to?(operation) - - catch_aws_errors do - client_obj.send(operation, **kwargs) + unless client_obj.respond_to?(operation) + raise ArgumentError, "#{client} does not support #{operation}" end + + catch_aws_errors { client_obj.send(operation, **kwargs) } end private @@ -561,7 +624,10 @@ def fetch(client:, operation:, kwargs: {}) def populate_filter_table_from_response return unless @table.present? - table_schema = @table.first.keys.map { |key| { column: key.to_s.pluralize.to_sym, field: key, style: :simple } } + table_schema = + @table.first.keys.map do |key| + { column: key.to_s.pluralize.to_sym, field: key, style: :simple } + end AwsCollectionResourceBase.populate_filter_table(:table, table_schema) end end @@ -583,14 +649,16 @@ def create_methods(object, data) when /Aws::.*/ # iterate around the instance variables data.instance_variables.each do |var| - create_method(object, var.to_s.delete("@"), data.instance_variable_get(var)) + create_method( + object, + var.to_s.delete("@"), + data.instance_variable_get(var), + ) end # When the data is a Hash object iterate around each of the key value pairs and # create a method for each one. when "Hash" - data.each do |key, value| - create_method(object, key, value) - end + data.each { |key, value| create_method(object, key, value) } end end @@ -610,7 +678,11 @@ def create_method(object, name, value) value end when "Hash" - value.count == 0 ? return_value = value : return_value = AwsResourceProbe.new(value) + if value.count == 0 + return_value = value + else + return_value = AwsResourceProbe.new(value) + end object.define_singleton_method name do return_value end @@ -633,9 +705,7 @@ def create_method(object, name, value) else if name.eql?(:tags) probes = {} - value.each do |tag| - probes[tag[:key]] = tag[:value] - end + value.each { |tag| probes[tag[:key]] = tag[:value] } else probes = [] value.each do |value_item| diff --git a/libraries/aws_iam_access_keys.rb b/libraries/aws_iam_access_keys.rb index ba4c4b441..4571a6b21 100644 --- a/libraries/aws_iam_access_keys.rb +++ b/libraries/aws_iam_access_keys.rb @@ -11,21 +11,42 @@ class AwsIamAccessKeys < AwsCollectionResourceBase attr_reader :table - FilterTable.create - .register_column(:usernames, field: :username) - .register_column(:access_key_ids, field: :access_key_id) - .register_column(:created_date, field: :create_date) - .register_column(:created_days_ago, field: :created_days_ago) - .register_column(:created_with_user, field: :created_with_user) - .register_column(:created_hours_ago, field: :created_hours_ago) - .register_column(:active, field: :active) - .register_column(:inactive, field: :inactive) - .register_column(:last_used_date, field: :last_used_date, lazy_instance: :lazy_load_last_used_date) - .register_column(:last_used_hours_ago, field: :last_used_hours_ago, lazy_instance: :lazy_load_last_used_hours_ago) - .register_column(:last_used_days_ago, field: :last_used_days_ago, lazy_instance: :lazy_load_last_used_days_ago) - .register_column(:ever_used, field: :ever_used, lazy_instance: :lazy_load_ever_used) - .register_column(:never_used, field: :never_used, lazy_instance: :lazy_load_never_used_time) - .register_column(:user_created_date, field: :user_created_date) + FilterTable + .create + .register_column(:usernames, field: :username) + .register_column(:access_key_ids, field: :access_key_id) + .register_column(:created_date, field: :create_date) + .register_column(:created_days_ago, field: :created_days_ago) + .register_column(:created_with_user, field: :created_with_user) + .register_column(:created_hours_ago, field: :created_hours_ago) + .register_column(:active, field: :active) + .register_column(:inactive, field: :inactive) + .register_column( + :last_used_date, + field: :last_used_date, + lazy_instance: :lazy_load_last_used_date, + ) + .register_column( + :last_used_hours_ago, + field: :last_used_hours_ago, + lazy_instance: :lazy_load_last_used_hours_ago, + ) + .register_column( + :last_used_days_ago, + field: :last_used_days_ago, + lazy_instance: :lazy_load_last_used_days_ago, + ) + .register_column( + :ever_used, + field: :ever_used, + lazy_instance: :lazy_load_ever_used, + ) + .register_column( + :never_used, + field: :never_used, + lazy_instance: :lazy_load_never_used_time, + ) + .register_column(:user_created_date, field: :user_created_date) .register_custom_matcher(:exists?) { |x| !x.entries.empty? } .install_filter_methods_on_resource(self, :table) @@ -51,13 +72,7 @@ def fetch_data(username) # Otherwise, get details of all users. # Returns a map (K,V) of (username: user_details) def get_users(username = nil) - catch_aws_errors do - if username - [fetch_user(username)] - else - collect_all_users - end - end + catch_aws_errors { username ? [fetch_user(username)] : collect_all_users } end def fetch_user(username) @@ -69,24 +84,21 @@ def fetch_user(username) end def collect_all_users - catch_aws_errors do - iam_client.list_users.flat_map(&:users) - end + catch_aws_errors { iam_client.list_users.flat_map(&:users) } end # Given a Hash of Users, build Access Key details for each. def get_keys - @_users.flat_map do |user| - fetch_keys(user.user_name) - end + @_users.flat_map { |user| fetch_keys(user.user_name) } end def fetch_keys(username) - access_keys = catch_aws_errors do - iam_client.list_access_keys(user_name: username) - rescue Aws::IAM::Errors::NoSuchEntity - # Swallow - a miss on search results should return an empty table - end + access_keys = + catch_aws_errors do + iam_client.list_access_keys({ user_name: username }) + rescue Aws::IAM::Errors::NoSuchEntity + # Swallow - a miss on search results should return an empty table + end access_keys&.flat_map do |response| response.access_key_metadata.flat_map do |access_key| access_key_hash = access_key.to_h @@ -94,19 +106,28 @@ def fetch_keys(username) access_key_hash[:id] = access_key_hash[:access_key_id] access_key_hash[:active] = access_key_hash[:status] == "Active" access_key_hash[:inactive] = access_key_hash[:status] != "Active" - access_key_hash[:created_hours_ago] = ((Time.now - access_key_hash[:create_date]) / (60*60)).to_i - access_key_hash[:created_days_ago] = (access_key_hash[:created_hours_ago] / 24).to_i - access_key_hash[:user_created_date] = access_key_hash[:create_date] - access_key_hash[:created_with_user] = (access_key_hash[:create_date] - access_key_hash[:user_created_date]).abs < 1.0/24.0 + access_key_hash[:created_hours_ago] = ( + (Time.now - access_key_hash[:create_date]) / (60 * 60) + ).to_i + access_key_hash[:created_days_ago] = ( + access_key_hash[:created_hours_ago] / 24 + ).to_i + access_key_hash[:user_created_date] = @_users + .find { |user| user.user_name == access_key_hash[:username] } + .create_date + access_key_hash[:created_with_user] = ( + access_key_hash[:create_date] - access_key_hash[:user_created_date] + ).abs < 1.0 / 24.0 access_key_hash end end end def last_used(row, _condition, _table) - @last_used ||= catch_aws_errors do - iam_client.get_access_key_last_used(access_key_id: row[:access_key_id]) - .access_key_last_used + catch_aws_errors do + iam_client.get_access_key_last_used( + { access_key_id: row[:access_key_id] }, + ).access_key_last_used end end @@ -114,26 +135,35 @@ def lazy_load_last_used_date(row, condition, table) row[:last_used_date] ||= last_used(row, condition, table).last_used_date end - def lazy_load_ever_used(row, condition, table) - row[:ever_used] = !lazy_load_never_used_time(row, condition, table) + def lazy_load_never_used_time(row, condition, table) + row[:never_used] ||= lazy_load_last_used_date(row, condition, table).nil? end - def lazy_load_never_used_time(row, condition, table) - row[:never_used] = lazy_load_last_used_date(row, condition, table).nil? + def lazy_load_ever_used(row, condition, table) + row[:ever_used] ||= !lazy_load_never_used_time(row, condition, table) end def lazy_load_last_used_hours_ago(row, condition, table) - return if lazy_load_never_used_time(row, condition, table) - - row[:last_used_hours_ago] = ((Time.now - row[:last_used_date]) / (60*60)).to_i + return row[:last_used_hours_ago] = nil if lazy_load_never_used_time( + row, + condition, + table, + ) + row[:last_used_hours_ago] = ( + (Time.now - row[:last_used_date]) / (60 * 60) + ).to_i end def lazy_load_last_used_days_ago(row, condition, table) - return if lazy_load_never_used_time(row, condition, table) + return row[:last_used_days_ago] = nil if lazy_load_never_used_time( + row, + condition, + table, + ) if row[:last_used_hours_ago].nil? lazy_load_last_used_hours_ago(row, condition, table) end - row[:last_used_days_ago] = (row[:last_used_hours_ago]/24).to_i + row[:last_used_days_ago] = (row[:last_used_hours_ago] / 24).to_i end def iam_client diff --git a/libraries/aws_iam_credential_report.rb b/libraries/aws_iam_credential_report.rb new file mode 100644 index 000000000..29fc4d044 --- /dev/null +++ b/libraries/aws_iam_credential_report.rb @@ -0,0 +1,79 @@ +require "csv" +require "aws_backend" + +class AwsIamCredentialReport < AwsCollectionResourceBase + name "aws_iam_credential_report" + desc "Lists all users in the AWS account and the status of their credentials." + + example " + describe aws_iam_credential_report.where(mfa_active: false) do + it { should_not exist } + end + " + + attr_reader :table + + FilterTable.create + .register_column(:user, field: :user) + .register_column(:arn, field: :arn) + .register_column(:user_creation_time, field: :user_creation_time) + .register_column(:password_enabled, field: :password_enabled) + .register_column(:password_last_used, field: :password_last_used) + .register_column(:password_last_changed, field: :password_last_changed) + .register_column(:password_next_rotation, field: :password_next_rotation) + .register_column(:mfa_active, field: :mfa_active) + .register_column(:access_key_1_active, field: :access_key_1_active) + .register_column(:access_key_1_last_rotated, field: :access_key_1_last_rotated) + .register_column(:access_key_1_last_used_date, field: :access_key_1_last_used_date) + .register_column(:access_key_1_last_used_region, field: :access_key_1_last_used_region) + .register_column(:access_key_1_last_used_service, field: :access_key_1_last_used_service) + .register_column(:access_key_2_active, field: :access_key_2_active) + .register_column(:access_key_2_last_rotated, field: :access_key_2_last_rotated) + .register_column(:access_key_2_last_used_date, field: :access_key_2_last_used_date) + .register_column(:access_key_2_last_used_region, field: :access_key_2_last_used_region) + .register_column(:access_key_2_last_used_service, field: :access_key_2_last_used_service) + .register_column(:cert_1_active, field: :cert_1_active) + .register_column(:cert_1_last_rotated, field: :cert_1_last_rotated) + .register_column(:cert_2_active, field: :cert_2_active) + .register_column(:cert_2_last_rotated, field: :cert_2_last_rotated) + .install_filter_methods_on_resource(self, :table) + + def initialize(opts = {}) + super(opts) + validate_parameters + @table = fetch_data + end + + def to_s + "IAM Credential Report" + end + + private + + def fetch_data + catch_aws_errors do + @aws.iam_client.generate_credential_report + begin + attempts ||= 0 + response = @aws.iam_client.get_credential_report + rescue Aws::IAM::Errors::ReportInProgress => e + if (attempts += 1) <= 5 + Inspec::Log.warn "AWS IAM Credential Report still being generated - attempt #{attempts}/5." + sleep 5 + retry + else + Inspec::Log.warn "AWS IAM Credential Report was not generated quickly enough." + raise e + end + end + report = CSV.parse(response.content, headers: true, header_converters: :symbol, converters: [:date_time, lambda { |field| + if field == "true" + true + else + field == "false" ? false : field + end + }]) + report.map(&:to_h) + end + end +end From 4766dce5b98caab38790b0079b3a8cb0bee2c40f Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Tue, 14 Nov 2023 11:20:23 -0500 Subject: [PATCH 02/93] adding missing library Signed-off-by: Aaron Lippold --- libraries/aws_backend.rb | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/libraries/aws_backend.rb b/libraries/aws_backend.rb index 81e52ea5d..340d60b4d 100644 --- a/libraries/aws_backend.rb +++ b/libraries/aws_backend.rb @@ -62,6 +62,7 @@ require "aws-sdk-waf" require "aws-sdk-synthetics" require "aws-sdk-apigatewayv2" +require "aws-sdk-account" # AWS Inspec Backend Classes # @@ -373,9 +374,7 @@ def initialize(opts) client_args[:client_args][:retry_limit] = opts[:aws_retry_limit] if opts[ :aws_retry_limit ] - if opts[ - :aws_retry_backoff - ] + if opts[:aws_retry_backoff] client_args[:client_args][ :retry_backoff ] = "lambda { |c| sleep(#{opts[:aws_retry_backoff]}) }" @@ -397,7 +396,7 @@ def initialize(opts) raise ArgumentError, "Expected stub data to be an array" end opts[:stub_data].each do |stub| - if !stub.keys.all? { |a| %i(method data client).include?(a) } + if !stub.keys.all? { |a| %i[method data client].include?(a) } raise ArgumentError, "Expect each stub_data hash to have :client, :method and :data keys" end @@ -417,9 +416,9 @@ def validate_parameters(allow: [], required: nil, require_any_of: nil) # rubocop "Expected required parameters as Array of Symbols, got #{required}" end unless @opts.is_a?(Hash) && - required.all? { |req| - @opts.key?(req) && !@opts[req].nil? && @opts[req] != "" - } + required.all? { |req| + @opts.key?(req) && !@opts[req].nil? && @opts[req] != "" + } raise ArgumentError, "#{@__resource_name__}: `#{required}` must be provided" end @@ -428,21 +427,21 @@ def validate_parameters(allow: [], required: nil, require_any_of: nil) # rubocop if require_any_of unless require_any_of.is_a?(Array) && - require_any_of.all? { |r| r.is_a?(Symbol) } + require_any_of.all? { |r| r.is_a?(Symbol) } raise ArgumentError, "Expected required parameters as Array of Symbols, got #{require_any_of}" end unless @opts.is_a?(Hash) && - require_any_of.any? { |req| - @opts.key?(req) && !@opts[req].nil? && @opts[req] != "" - } + require_any_of.any? { |req| + @opts.key?(req) && !@opts[req].nil? && @opts[req] != "" + } raise ArgumentError, "#{@__resource_name__}: One of `#{require_any_of}` must be provided." end allow += require_any_of end - allow += %i( + allow += %i[ client_args stub_data aws_region @@ -450,7 +449,7 @@ def validate_parameters(allow: [], required: nil, require_any_of: nil) # rubocop aws_retry_limit aws_retry_backoff resource_data - ) + ] unless defined?(@opts.keys) raise ArgumentError, "Scalar arguments not supported" end @@ -458,10 +457,10 @@ def validate_parameters(allow: [], required: nil, require_any_of: nil) # rubocop raise ArgumentError, "Unexpected arguments found" end unless @opts.values.all? { |a| - return true if a.instance_of?(Integer) - return true if [TrueClass, FalseClass].include?(a.class) - !a.empty? - } + return true if a.instance_of?(Integer) + return true if [TrueClass, FalseClass].include?(a.class) + !a.empty? + } raise ArgumentError, "Provided parameter should not be empty" end true @@ -652,7 +651,7 @@ def create_methods(object, data) create_method( object, var.to_s.delete("@"), - data.instance_variable_get(var), + data.instance_variable_get(var) ) end # When the data is a Hash object iterate around each of the key value pairs and From 1ce0e5a132e8d1c0f5f0756c9034742e0261962c Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Tue, 14 Nov 2023 11:50:08 -0500 Subject: [PATCH 03/93] Fixes to AWS Account and Backend * Fixed error collection in constructor to not incorrectly fail * Updated warning message to not add extra '.' in outputs Signed-off-by: Aaron Lippold --- libraries/aws_account.rb | 148 ++++++++++++++++++++++++++------------- libraries/aws_backend.rb | 2 +- 2 files changed, 102 insertions(+), 48 deletions(-) diff --git a/libraries/aws_account.rb b/libraries/aws_account.rb index e0491072f..36e6ec47b 100644 --- a/libraries/aws_account.rb +++ b/libraries/aws_account.rb @@ -1,5 +1,4 @@ require "aws_backend" -require "pry" class AwsPrimaryAccount < AwsResourceBase name "aws_primary_contact" @@ -14,14 +13,19 @@ class AwsPrimaryAccount < AwsResourceBase attr_reader :table, :raw_data - FilterTable.create + FilterTable + .create .register_column(:address_line_1, field: :address_line_1, style: :simple) .register_column(:adress_line_2, field: :adress_line_2, style: :simple) .register_column(:address_line_3, field: :address_line_3, style: :simple) .register_column(:city, field: :city, style: :simple) .register_column(:company_name, field: :company_name, style: :simple) .register_column(:country_code, field: :country_code, style: :simple) - .register_column(:district_or_county, field: :district_or_county, style: :simple) + .register_column( + :district_or_county, + field: :district_or_county, + style: :simple + ) .register_column(:full_name, field: :full_name, style: :simple) .register_column(:phone_number, field: :phone_number, style: :simple) .register_column(:postal_code, field: :postal_code, style: :simple) @@ -33,12 +37,21 @@ class AwsPrimaryAccount < AwsResourceBase def initialize(opts = {}) super(opts) validate_parameters - # binding.pry + begin + @aws.account_client.get_contact_information + rescue Aws::Account::Errors::ResourceNotFoundException + skip_resource( + "The Primary contact has not been configured for this AWS Account." + ) + @failed_resource = true + return + end @table = fetch_data end def resource_id - "AWS Account for #{@table[0][:full_name]}" || "AWS Account Contact Information" + "AWS Account for #{@table[0][:full_name]}" || + "AWS Account Contact Information" end def to_s @@ -49,36 +62,30 @@ def fetch_data @raw_data = [] loop do catch_aws_errors do - @api_response = @aws.account_client.get_contact_information.contact_information + @api_response = + @aws.account_client.get_contact_information.contact_information end return [] if !@api_response || @api_response.empty? @raw_data << { - address_line_1: @api_response.address_line_1, - address_line_2: @api_response.address_line_2, - address_line_3: @api_response.address_line_3, - city: @api_response.city, - company_name: @api_response.company_name, - country_code: @api_response.country_code, + address_line_1: @api_response.address_line_1, + address_line_2: @api_response.address_line_2, + address_line_3: @api_response.address_line_3, + city: @api_response.city, + company_name: @api_response.company_name, + country_code: @api_response.country_code, district_or_county: @api_response.district_or_county, - full_name: @api_response.full_name, - phone_number: @api_response.phone_number, - postal_code: @api_response.postal_code, - state_or_region: @api_response.state_or_region, - website_url: @api_response.website_url, + full_name: @api_response.full_name, + phone_number: @api_response.phone_number, + postal_code: @api_response.postal_code, + state_or_region: @api_response.state_or_region, + website_url: @api_response.website_url } break end @raw_data end - # @aws.account_client.get_alternate_contact({alternate_contact_type: "BILLING"}).alternate_contact.to_h.transform_keys(&:to_s) - # resp.alternate_contact.alternate_contact_type #=> String, one of "BILLING", "OPERATIONS", "SECURITY" - # resp.alternate_contact.email_address #=> String - # resp.alternate_contact.name #=> String - # resp.alternate_contact.phone_number #=> String - # resp.alternate_contact.title #=> String - class AwsBillingAccount < AwsResourceBase name "aws_billing_contact" desc "Verifies the billing contact information for an AWS Account." @@ -92,7 +99,8 @@ class AwsBillingAccount < AwsResourceBase attr_reader :table, :raw_data - FilterTable.create + FilterTable + .create .register_column(:email_address, field: :email_address, style: :simple) .register_column(:name, field: :name, style: :simple) .register_column(:phone_number, field: :phone_number, style: :simple) @@ -103,6 +111,17 @@ class AwsBillingAccount < AwsResourceBase def initialize(opts = {}) super(opts) validate_parameters + begin + @aws.account_client.get_alternate_contact( + { alternate_contact_type: "BILLING" } + ) + rescue Aws::Account::Errors::ResourceNotFoundException + skip_resource( + "The BILLING contact has not been configured for this AWS Account." + ) + @failed_resource = true + return + end @table = fetch_data end @@ -118,15 +137,19 @@ def fetch_data @raw_data = [] loop do catch_aws_errors do - @api_response = @aws.account_client.get_alternate_contact({ alternate_contact_type: "BILLING" }).alternate_contact + @api_response = + @aws + .account_client + .get_alternate_contact({ alternate_contact_type: "BILLING" }) + .alternate_contact end return [] if !@api_response || @api_response.empty? @raw_data << { - email_address: @api_response.email_address, - name: @api_response.name, - phone_number: @api_response.phone_number, - title: @api_response.title, + email_address: @api_response.email_address, + name: @api_response.name, + phone_number: @api_response.phone_number, + title: @api_response.title } break end @@ -134,8 +157,6 @@ def fetch_data end end - # @aws.account_client.get_alternate_contact({alternate_contact_type: "OPERATIONS"}).alternate_contact.to_h.transform_keys(&:to_s) - class AwsAccountOperationsContact < AwsResourceBase name "aws_operations_contact" desc "Verifies the operations contact information for an AWS Account." @@ -149,7 +170,8 @@ class AwsAccountOperationsContact < AwsResourceBase attr_reader :table, :raw_data - FilterTable.create + FilterTable + .create .register_column(:email_address, field: :email_address, style: :simple) .register_column(:name, field: :name, style: :simple) .register_column(:phone_number, field: :phone_number, style: :simple) @@ -160,11 +182,23 @@ class AwsAccountOperationsContact < AwsResourceBase def initialize(opts = {}) super(opts) validate_parameters + begin + @aws.account_client.get_alternate_contact( + { alternate_contact_type: "OPERATIONS" } + ) + rescue Aws::Account::Errors::ResourceNotFoundException + skip_resource( + "The Operations contact has not been configured for this AWS Account." + ) + @failed_resource = true + return + end @table = fetch_data end def resource_id - "AWS Operations Contact for #{@table[0][:name]}" || "AWS Account Operations Contact Information" + "AWS Operations Contact for #{@table[0][:name]}" || + "AWS Account Operations Contact Information" end def to_s @@ -175,15 +209,19 @@ def fetch_data @raw_data = [] loop do catch_aws_errors do - @api_response = @aws.account_client.get_alternate_contact({ alternate_contact_type: "OPERATIONS" }).alternate_contact + @api_response = + @aws + .account_client + .get_alternate_contact({ alternate_contact_type: "OPERATIONS" }) + .alternate_contact end return [] if !@api_response || @api_response.empty? @raw_data << { - email_address: @api_response.email_address, - name: @api_response.name, - phone_number: @api_response.phone_number, - title: @api_response.title, + email_address: @api_response.email_address, + name: @api_response.name, + phone_number: @api_response.phone_number, + title: @api_response.title } break end @@ -191,7 +229,6 @@ def fetch_data end end - # @aws.account_client.get_alternate_contact({alternate_contact_type: "SECURITY"}).alternate_contact.to_h.transform_keys(&:to_s) class AwsAccountSecurityContact < AwsResourceBase name "aws_security_contact" desc "Verifies the security contact information for an AWS Account." @@ -205,7 +242,8 @@ class AwsAccountSecurityContact < AwsResourceBase attr_reader :table, :raw_data - FilterTable.create + FilterTable + .create .register_column(:email_address, field: :email_address, style: :simple) .register_column(:name, field: :name, style: :simple) .register_column(:phone_number, field: :phone_number, style: :simple) @@ -216,11 +254,23 @@ class AwsAccountSecurityContact < AwsResourceBase def initialize(opts = {}) super(opts) validate_parameters + begin + @aws.account_client.get_alternate_contact( + { alternate_contact_type: "SECURITY" } + ) + rescue Aws::Account::Errors::ResourceNotFoundException + skip_resource( + "The Security contact has not been configured for this AWS Account." + ) + @failed_resource = true + return + end @table = fetch_data end def resource_id - "AWS Security Contact for #{@table[0][:name]}" || "AWS Account Security Contact Information" + "AWS Security Contact for #{@table[0][:name]}" || + "AWS Account Security Contact Information" end def to_s @@ -231,15 +281,19 @@ def fetch_data @raw_data = [] loop do catch_aws_errors do - @api_response = @aws.account_client.get_alternate_contact({ alternate_contact_type: "SECURITY" }).alternate_contact + @api_response = + @aws + .account_client + .get_alternate_contact({ alternate_contact_type: "SECURITY" }) + .alternate_contact end return [] if !@api_response || @api_response.empty? @raw_data << { - email_address: @api_response.email_address, - name: @api_response.name, - phone_number: @api_response.phone_number, - title: @api_response.title, + email_address: @api_response.email_address, + name: @api_response.name, + phone_number: @api_response.phone_number, + title: @api_response.title } break end diff --git a/libraries/aws_backend.rb b/libraries/aws_backend.rb index 340d60b4d..b9b423692 100644 --- a/libraries/aws_backend.rb +++ b/libraries/aws_backend.rb @@ -519,7 +519,7 @@ def catch_aws_errors raise Inspec::Exceptions::ResourceFailed, error_message else Inspec::Log.warn "AWS Service Error encountered running a control with Resource #{@__resource_name__}. " \ - "Error message: #{e.message}. You should address this error to ensure your controls are " \ + "Error message: #{e.message} You should address this error to ensure your controls are " \ "behaving as expected." @failed_resource = true end From fa773f97de79c5ef096b0218c4202de812d63282 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Fri, 17 Nov 2023 20:50:48 -0500 Subject: [PATCH 04/93] standardized classes and removed filter tables Signed-off-by: Aaron Lippold --- libraries/aws_account.rb | 387 +++++++++++++++++++++------------------ 1 file changed, 212 insertions(+), 175 deletions(-) diff --git a/libraries/aws_account.rb b/libraries/aws_account.rb index 36e6ec47b..6f18f1941 100644 --- a/libraries/aws_account.rb +++ b/libraries/aws_account.rb @@ -1,4 +1,5 @@ require "aws_backend" +# require "pry" class AwsPrimaryAccount < AwsResourceBase name "aws_primary_contact" @@ -11,81 +12,81 @@ class AwsPrimaryAccount < AwsResourceBase end EXAMPLE - attr_reader :table, :raw_data - - FilterTable - .create - .register_column(:address_line_1, field: :address_line_1, style: :simple) - .register_column(:adress_line_2, field: :adress_line_2, style: :simple) - .register_column(:address_line_3, field: :address_line_3, style: :simple) - .register_column(:city, field: :city, style: :simple) - .register_column(:company_name, field: :company_name, style: :simple) - .register_column(:country_code, field: :country_code, style: :simple) - .register_column( - :district_or_county, - field: :district_or_county, - style: :simple - ) - .register_column(:full_name, field: :full_name, style: :simple) - .register_column(:phone_number, field: :phone_number, style: :simple) - .register_column(:postal_code, field: :postal_code, style: :simple) - .register_column(:state_or_region, field: :state_or_region, style: :simple) - .register_column(:website_url, field: :website_url, style: :simple) - .register_custom_matcher(:configured?) { |x| x.entries.any? } - .install_filter_methods_on_resource(self, :table) + attr_reader :raw_data, + :api_response, + :address_line_1, + :address_line_2, + :address_line_3, + :city, + :country_code, + :company_name, + :district_or_county, + :full_name, + :phone_number, + :postal_code, + :state_or_region, + :website_url, + :aws_account_id def initialize(opts = {}) + @raw_data = {} super(opts) validate_parameters begin - @aws.account_client.get_contact_information + catch_aws_errors { @aws_account_id = fetch_aws_account } + @api_response = + @aws.account_client.get_contact_information.contact_information || nil rescue Aws::Account::Errors::ResourceNotFoundException skip_resource( "The Primary contact has not been configured for this AWS Account." ) - @failed_resource = true - return + return [] if !@api_response || @api_response.empty? end - @table = fetch_data - end - def resource_id - "AWS Account for #{@table[0][:full_name]}" || - "AWS Account Contact Information" + unless @api_response.nil? + @api_response + .members + .map(&:to_s) + .each do |key| + instance_variable_set("@#{key}", @api_response.send(key)) + end + @raw_data = @api_response.to_h.transform_keys(&:to_s) + else + @address_line_1, + @address_line_2, + @address_line_3, + @city, + @country_code, + @company_name, + @district_or_county, + @full_name, + @phone_number, + @postal_code, + @state_or_region, + @website_url = + nil + end end - def to_s - "AWS Account Primary Contact" + def configured? + !@api_response.nil? || !@raw_data end - def fetch_data - @raw_data = [] - loop do - catch_aws_errors do - @api_response = - @aws.account_client.get_contact_information.contact_information - end - return [] if !@api_response || @api_response.empty? - - @raw_data << { - address_line_1: @api_response.address_line_1, - address_line_2: @api_response.address_line_2, - address_line_3: @api_response.address_line_3, - city: @api_response.city, - company_name: @api_response.company_name, - country_code: @api_response.country_code, - district_or_county: @api_response.district_or_county, - full_name: @api_response.full_name, - phone_number: @api_response.phone_number, - postal_code: @api_response.postal_code, - state_or_region: @api_response.state_or_region, - website_url: @api_response.website_url - } - break + def resource_id + if @aws_account_id + "AWS Primary Contact for account: #{@aws_account_id}" + else + "AWS Account Primary Contact Information" end - @raw_data end + def to_s + if @aws_account_id + "AWS Primary Contact for account: #{@aws_account_id}" + else + "AWS Account Primary Contact" + end + end class AwsBillingAccount < AwsResourceBase name "aws_billing_contact" desc "Verifies the billing contact information for an AWS Account." @@ -97,66 +98,76 @@ class AwsBillingAccount < AwsResourceBase end EXAMPLE - attr_reader :table, :raw_data - - FilterTable - .create - .register_column(:email_address, field: :email_address, style: :simple) - .register_column(:name, field: :name, style: :simple) - .register_column(:phone_number, field: :phone_number, style: :simple) - .register_column(:title, field: :title, style: :simple) - .register_custom_matcher(:configured?) { |x| x.entries.any? } - .install_filter_methods_on_resource(self, :table) + attr_reader :raw_data, + :api, + :api_response, + :email_address, + :name, + :phone_number, + :title, + :aws_account_id def initialize(opts = {}) super(opts) + @raw_data = {} validate_parameters begin - @aws.account_client.get_alternate_contact( - { alternate_contact_type: "BILLING" } - ) + catch_aws_errors { @aws_account_id = fetch_aws_account } + @api_response = fetch_aws_alternate_contact(@aws, "billing") rescue Aws::Account::Errors::ResourceNotFoundException skip_resource( "The BILLING contact has not been configured for this AWS Account." ) - @failed_resource = true - return + return [] if !@api_response || @api_response.empty? + end + + unless @api_response.nil? + @api_response + .members + .map(&:to_s) + .each do |key| + instance_variable_set("@#{key}", @api_response.send(key)) + end + @raw_data = @api_response.to_h.transform_keys(&:to_s) + else + @name, @email_address, @phone_number, @title = nil end - @table = fetch_data + end + + def configured? + !@api_response.nil? || !@raw_data end def resource_id - "AWS Billing for #{@table[0][:name]}" || "AWS Account Billing Information" + if @aws_account_id + "AWS Billing Contact for account: #{@aws_account_id}" + else + "AWS Billing Contact Information" + end end def to_s - "AWS Account Billing Contact" + if @aws_account_id + "AWS Billing Contact for account: #{@aws_account_id}" + else + "AWS Account Primary Contact" + end end - def fetch_data - @raw_data = [] - loop do - catch_aws_errors do - @api_response = - @aws - .account_client - .get_alternate_contact({ alternate_contact_type: "BILLING" }) - .alternate_contact - end - return [] if !@api_response || @api_response.empty? + private - @raw_data << { - email_address: @api_response.email_address, - name: @api_response.name, - phone_number: @api_response.phone_number, - title: @api_response.title - } - break - end - @raw_data + def fetch_aws_account + arn = @aws.sts_client.get_caller_identity({}).arn + arn.split(":")[4] end - end + def fetch_aws_alternate_contact(aws_client, type) + aws_client + .account_client + .get_alternate_contact({ alternate_contact_type: "#{type.uppercase}" }) + .alternate_contact + end + end class AwsAccountOperationsContact < AwsResourceBase name "aws_operations_contact" desc "Verifies the operations contact information for an AWS Account." @@ -168,67 +179,76 @@ class AwsAccountOperationsContact < AwsResourceBase end EXAMPLE - attr_reader :table, :raw_data - - FilterTable - .create - .register_column(:email_address, field: :email_address, style: :simple) - .register_column(:name, field: :name, style: :simple) - .register_column(:phone_number, field: :phone_number, style: :simple) - .register_column(:title, field: :title, style: :simple) - .register_custom_matcher(:configured?) { |x| x.entries.any? } - .install_filter_methods_on_resource(self, :table) + attr_reader :raw_data, + :api, + :api_response, + :email_address, + :name, + :phone_number, + :title, + :aws_account_id def initialize(opts = {}) super(opts) + @raw_data = {} validate_parameters begin - @aws.account_client.get_alternate_contact( - { alternate_contact_type: "OPERATIONS" } - ) + catch_aws_errors { @aws_account_id = fetch_aws_account } + @api_response = fetch_aws_alternate_contact(@aws, "operations") rescue Aws::Account::Errors::ResourceNotFoundException skip_resource( "The Operations contact has not been configured for this AWS Account." ) - @failed_resource = true - return + return [] if !@api_response || @api_response.empty? + end + + unless @api_response.nil? + @api_response + .members + .map(&:to_s) + .each do |key| + instance_variable_set("@#{key}", @api_response.send(key)) + end + @raw_data = @api_response.to_h.transform_keys(&:to_s) + else + @name, @email_address, @phone_number, @title = nil end - @table = fetch_data + end + + def configured? + !@api_response.nil? || !@raw_data end def resource_id - "AWS Operations Contact for #{@table[0][:name]}" || - "AWS Account Operations Contact Information" + if @aws_account_id + "AWS Operations Contact for #{@aws_account_id}" + else + "AWS Operations Contact Information" + end end def to_s - "AWS Account Operations Contact Information" + if @aws_account_id + "AWS Account Operations Contact for #{@aws_account_id}" + else + "AWS Account Operations Contact" + end end - def fetch_data - @raw_data = [] - loop do - catch_aws_errors do - @api_response = - @aws - .account_client - .get_alternate_contact({ alternate_contact_type: "OPERATIONS" }) - .alternate_contact - end - return [] if !@api_response || @api_response.empty? + private - @raw_data << { - email_address: @api_response.email_address, - name: @api_response.name, - phone_number: @api_response.phone_number, - title: @api_response.title - } - break - end - @raw_data + def fetch_aws_account + arn = @aws.sts_client.get_caller_identity({}).arn + arn.split(":")[4] end - end + def fetch_aws_alternate_contact(aws_client, type) + aws_client + .account_client + .get_alternate_contact({ alternate_contact_type: "#{type.uppercase}" }) + .alternate_contact + end + end class AwsAccountSecurityContact < AwsResourceBase name "aws_security_contact" desc "Verifies the security contact information for an AWS Account." @@ -240,64 +260,81 @@ class AwsAccountSecurityContact < AwsResourceBase end EXAMPLE - attr_reader :table, :raw_data - - FilterTable - .create - .register_column(:email_address, field: :email_address, style: :simple) - .register_column(:name, field: :name, style: :simple) - .register_column(:phone_number, field: :phone_number, style: :simple) - .register_column(:title, field: :title, style: :simple) - .register_custom_matcher(:configured?) { |x| x.entries.any? } - .install_filter_methods_on_resource(self, :table) + attr_reader :raw_data, + :api, + :api_response, + :email_address, + :name, + :phone_number, + :title, + :aws_account_id def initialize(opts = {}) super(opts) + @raw_data = {} validate_parameters begin - @aws.account_client.get_alternate_contact( - { alternate_contact_type: "SECURITY" } - ) + catch_aws_errors { @aws_account_id = fetch_aws_account } + @api_response = fetch_aws_alternate_contact(@aws, "security") rescue Aws::Account::Errors::ResourceNotFoundException skip_resource( "The Security contact has not been configured for this AWS Account." ) - @failed_resource = true - return + return [] if !@api_response || @api_response.empty? + end + + unless @api_response.nil? + @api_response + .members + .map(&:to_s) + .each do |key| + instance_variable_set("@#{key}", @api_response.send(key)) + end + @raw_data = @api_response.to_h.transform_keys(&:to_s) + else + @name, @email_address, @phone_number, @title = nil end - @table = fetch_data + end + + def configured? + !@api_response.nil? || !@raw_data end def resource_id - "AWS Security Contact for #{@table[0][:name]}" || - "AWS Account Security Contact Information" + if @aws_account_id + "AWS Security Contact for account: #{@aws_account_id}" + else + "AWS Security Contact Information" + end end def to_s - "AWS Account Security Contact" + if @aws_account_id + "AWS Security Contact for account: #{@aws_account_id}" + else + "AWS Account Security Contact" + end end - def fetch_data - @raw_data = [] - loop do - catch_aws_errors do - @api_response = - @aws - .account_client - .get_alternate_contact({ alternate_contact_type: "SECURITY" }) - .alternate_contact - end - return [] if !@api_response || @api_response.empty? + private - @raw_data << { - email_address: @api_response.email_address, - name: @api_response.name, - phone_number: @api_response.phone_number, - title: @api_response.title - } - break - end - @raw_data + def fetch_aws_account + arn = @aws.sts_client.get_caller_identity({}).arn + arn.split(":")[4] end end + + def fetch_aws_alternate_contact(aws_client, type) + aws_client + .account_client + .get_alternate_contact({ alternate_contact_type: "#{type.uppercase}" }) + .alternate_contact + end + + private + + def fetch_aws_account + arn = @aws.sts_client.get_caller_identity({}).arn + arn.split(":")[4] + end end From 2899832b0f5154227a2c4bf3b6d2ab75b48914ed Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Fri, 17 Nov 2023 20:58:33 -0500 Subject: [PATCH 05/93] moved account singular resources into seperate files Signed-off-by: Aaron Lippold --- libraries/aws_account.rb | 243 ---------------------------- libraries/aws_billing_account.rb | 84 ++++++++++ libraries/aws_operations_account.rb | 84 ++++++++++ libraries/aws_security_account.rb | 84 ++++++++++ 4 files changed, 252 insertions(+), 243 deletions(-) create mode 100644 libraries/aws_billing_account.rb create mode 100644 libraries/aws_operations_account.rb create mode 100644 libraries/aws_security_account.rb diff --git a/libraries/aws_account.rb b/libraries/aws_account.rb index 6f18f1941..18ce7f99e 100644 --- a/libraries/aws_account.rb +++ b/libraries/aws_account.rb @@ -87,249 +87,6 @@ def to_s "AWS Account Primary Contact" end end - class AwsBillingAccount < AwsResourceBase - name "aws_billing_contact" - desc "Verifies the billing contact information for an AWS Account." - example <<~EXAMPLE - describe aws_billing_account do - it { should be_configured } - its('name') { should cmp 'John Smith' } - its('email_address') { should cmp 'jsmith@acme.com' } - end - EXAMPLE - - attr_reader :raw_data, - :api, - :api_response, - :email_address, - :name, - :phone_number, - :title, - :aws_account_id - - def initialize(opts = {}) - super(opts) - @raw_data = {} - validate_parameters - begin - catch_aws_errors { @aws_account_id = fetch_aws_account } - @api_response = fetch_aws_alternate_contact(@aws, "billing") - rescue Aws::Account::Errors::ResourceNotFoundException - skip_resource( - "The BILLING contact has not been configured for this AWS Account." - ) - return [] if !@api_response || @api_response.empty? - end - - unless @api_response.nil? - @api_response - .members - .map(&:to_s) - .each do |key| - instance_variable_set("@#{key}", @api_response.send(key)) - end - @raw_data = @api_response.to_h.transform_keys(&:to_s) - else - @name, @email_address, @phone_number, @title = nil - end - end - - def configured? - !@api_response.nil? || !@raw_data - end - - def resource_id - if @aws_account_id - "AWS Billing Contact for account: #{@aws_account_id}" - else - "AWS Billing Contact Information" - end - end - - def to_s - if @aws_account_id - "AWS Billing Contact for account: #{@aws_account_id}" - else - "AWS Account Primary Contact" - end - end - - private - - def fetch_aws_account - arn = @aws.sts_client.get_caller_identity({}).arn - arn.split(":")[4] - end - - def fetch_aws_alternate_contact(aws_client, type) - aws_client - .account_client - .get_alternate_contact({ alternate_contact_type: "#{type.uppercase}" }) - .alternate_contact - end - end - class AwsAccountOperationsContact < AwsResourceBase - name "aws_operations_contact" - desc "Verifies the operations contact information for an AWS Account." - example <<~EXAMPLE - describe aws_operations_account do - it { should be_configured } - its('name') { should cmp 'John Smith' } - its('email_address') { should cmp 'jsmith@acme.com' } - end - EXAMPLE - - attr_reader :raw_data, - :api, - :api_response, - :email_address, - :name, - :phone_number, - :title, - :aws_account_id - - def initialize(opts = {}) - super(opts) - @raw_data = {} - validate_parameters - begin - catch_aws_errors { @aws_account_id = fetch_aws_account } - @api_response = fetch_aws_alternate_contact(@aws, "operations") - rescue Aws::Account::Errors::ResourceNotFoundException - skip_resource( - "The Operations contact has not been configured for this AWS Account." - ) - return [] if !@api_response || @api_response.empty? - end - - unless @api_response.nil? - @api_response - .members - .map(&:to_s) - .each do |key| - instance_variable_set("@#{key}", @api_response.send(key)) - end - @raw_data = @api_response.to_h.transform_keys(&:to_s) - else - @name, @email_address, @phone_number, @title = nil - end - end - - def configured? - !@api_response.nil? || !@raw_data - end - - def resource_id - if @aws_account_id - "AWS Operations Contact for #{@aws_account_id}" - else - "AWS Operations Contact Information" - end - end - - def to_s - if @aws_account_id - "AWS Account Operations Contact for #{@aws_account_id}" - else - "AWS Account Operations Contact" - end - end - - private - - def fetch_aws_account - arn = @aws.sts_client.get_caller_identity({}).arn - arn.split(":")[4] - end - - def fetch_aws_alternate_contact(aws_client, type) - aws_client - .account_client - .get_alternate_contact({ alternate_contact_type: "#{type.uppercase}" }) - .alternate_contact - end - end - class AwsAccountSecurityContact < AwsResourceBase - name "aws_security_contact" - desc "Verifies the security contact information for an AWS Account." - example <<~EXAMPLE - describe aws_security_account do - it { should be_configured } - its('name') { should cmp 'John Smith' } - its('email_address') { should cmp 'jsmith@acme.com' } - end - EXAMPLE - - attr_reader :raw_data, - :api, - :api_response, - :email_address, - :name, - :phone_number, - :title, - :aws_account_id - - def initialize(opts = {}) - super(opts) - @raw_data = {} - validate_parameters - begin - catch_aws_errors { @aws_account_id = fetch_aws_account } - @api_response = fetch_aws_alternate_contact(@aws, "security") - rescue Aws::Account::Errors::ResourceNotFoundException - skip_resource( - "The Security contact has not been configured for this AWS Account." - ) - return [] if !@api_response || @api_response.empty? - end - - unless @api_response.nil? - @api_response - .members - .map(&:to_s) - .each do |key| - instance_variable_set("@#{key}", @api_response.send(key)) - end - @raw_data = @api_response.to_h.transform_keys(&:to_s) - else - @name, @email_address, @phone_number, @title = nil - end - end - - def configured? - !@api_response.nil? || !@raw_data - end - - def resource_id - if @aws_account_id - "AWS Security Contact for account: #{@aws_account_id}" - else - "AWS Security Contact Information" - end - end - - def to_s - if @aws_account_id - "AWS Security Contact for account: #{@aws_account_id}" - else - "AWS Account Security Contact" - end - end - - private - - def fetch_aws_account - arn = @aws.sts_client.get_caller_identity({}).arn - arn.split(":")[4] - end - end - - def fetch_aws_alternate_contact(aws_client, type) - aws_client - .account_client - .get_alternate_contact({ alternate_contact_type: "#{type.uppercase}" }) - .alternate_contact - end private diff --git a/libraries/aws_billing_account.rb b/libraries/aws_billing_account.rb new file mode 100644 index 000000000..2ab8ceee7 --- /dev/null +++ b/libraries/aws_billing_account.rb @@ -0,0 +1,84 @@ +require "aws_backend" +# require "pry" + +class AwsBillingAccount < AwsResourceBase + name "aws_billing_contact" + desc "Verifies the billing contact information for an AWS Account." + example <<~EXAMPLE + describe aws_billing_account do + it { should be_configured } + its('name') { should cmp 'John Smith' } + its('email_address') { should cmp 'jsmith@acme.com' } + end + EXAMPLE + + attr_reader :raw_data, + :api, + :api_response, + :email_address, + :name, + :phone_number, + :title, + :aws_account_id + + def initialize(opts = {}) + super(opts) + @raw_data = {} + validate_parameters + begin + catch_aws_errors { @aws_account_id = fetch_aws_account } + @api_response = fetch_aws_alternate_contact(@aws, "billing") + rescue Aws::Account::Errors::ResourceNotFoundException + skip_resource( + "The BILLING contact has not been configured for this AWS Account." + ) + return [] if !@api_response || @api_response.empty? + end + + unless @api_response.nil? + @api_response + .members + .map(&:to_s) + .each do |key| + instance_variable_set("@#{key}", @api_response.send(key)) + end + @raw_data = @api_response.to_h.transform_keys(&:to_s) + else + @name, @email_address, @phone_number, @title = nil + end + end + + def configured? + !@api_response.nil? || !@raw_data + end + + def resource_id + if @aws_account_id + "AWS Billing Contact for account: #{@aws_account_id}" + else + "AWS Billing Contact Information" + end + end + + def to_s + if @aws_account_id + "AWS Billing Contact for account: #{@aws_account_id}" + else + "AWS Account Primary Contact" + end + end + + private + + def fetch_aws_account + arn = @aws.sts_client.get_caller_identity({}).arn + arn.split(":")[4] + end + + def fetch_aws_alternate_contact(aws_client, type) + aws_client + .account_client + .get_alternate_contact({ alternate_contact_type: "#{type.uppercase}" }) + .alternate_contact + end +end diff --git a/libraries/aws_operations_account.rb b/libraries/aws_operations_account.rb new file mode 100644 index 000000000..874f92122 --- /dev/null +++ b/libraries/aws_operations_account.rb @@ -0,0 +1,84 @@ +require "aws_backend" +# require "pry" + +class AwsAccountOperationsContact < AwsResourceBase + name "aws_operations_contact" + desc "Verifies the operations contact information for an AWS Account." + example <<~EXAMPLE + describe aws_operations_account do + it { should be_configured } + its('name') { should cmp 'John Smith' } + its('email_address') { should cmp 'jsmith@acme.com' } + end + EXAMPLE + + attr_reader :raw_data, + :api, + :api_response, + :email_address, + :name, + :phone_number, + :title, + :aws_account_id + + def initialize(opts = {}) + super(opts) + @raw_data = {} + validate_parameters + begin + catch_aws_errors { @aws_account_id = fetch_aws_account } + @api_response = fetch_aws_alternate_contact(@aws, "operations") + rescue Aws::Account::Errors::ResourceNotFoundException + skip_resource( + "The Operations contact has not been configured for this AWS Account." + ) + return [] if !@api_response || @api_response.empty? + end + + unless @api_response.nil? + @api_response + .members + .map(&:to_s) + .each do |key| + instance_variable_set("@#{key}", @api_response.send(key)) + end + @raw_data = @api_response.to_h.transform_keys(&:to_s) + else + @name, @email_address, @phone_number, @title = nil + end + end + + def configured? + !@api_response.nil? || !@raw_data + end + + def resource_id + if @aws_account_id + "AWS Operations Contact for #{@aws_account_id}" + else + "AWS Operations Contact Information" + end + end + + def to_s + if @aws_account_id + "AWS Account Operations Contact for #{@aws_account_id}" + else + "AWS Account Operations Contact" + end + end + + private + + def fetch_aws_account + arn = @aws.sts_client.get_caller_identity({}).arn + arn.split(":")[4] + end + + def fetch_aws_alternate_contact(aws_client, type) + aws_client + .account_client + .get_alternate_contact({ alternate_contact_type: "#{type.uppercase}" }) + .alternate_contact + end +end diff --git a/libraries/aws_security_account.rb b/libraries/aws_security_account.rb new file mode 100644 index 000000000..798a57a1f --- /dev/null +++ b/libraries/aws_security_account.rb @@ -0,0 +1,84 @@ +require "aws_backend" +# require "pry" + +class AwsAccountSecurityContact < AwsResourceBase + name "aws_security_contact" + desc "Verifies the security contact information for an AWS Account." + example <<~EXAMPLE + describe aws_security_account do + it { should be_configured } + its('name') { should cmp 'John Smith' } + its('email_address') { should cmp 'jsmith@acme.com' } + end + EXAMPLE + + attr_reader :raw_data, + :api, + :api_response, + :email_address, + :name, + :phone_number, + :title, + :aws_account_id + + def initialize(opts = {}) + super(opts) + @raw_data = {} + validate_parameters + begin + catch_aws_errors { @aws_account_id = fetch_aws_account } + @api_response = fetch_aws_alternate_contact(@aws, "security") + rescue Aws::Account::Errors::ResourceNotFoundException + skip_resource( + "The Security contact has not been configured for this AWS Account." + ) + return [] if !@api_response || @api_response.empty? + end + + unless @api_response.nil? + @api_response + .members + .map(&:to_s) + .each do |key| + instance_variable_set("@#{key}", @api_response.send(key)) + end + @raw_data = @api_response.to_h.transform_keys(&:to_s) + else + @name, @email_address, @phone_number, @title = nil + end + end + + def configured? + !@api_response.nil? || !@raw_data + end + + def resource_id + if @aws_account_id + "AWS Security Contact for account: #{@aws_account_id}" + else + "AWS Security Contact Information" + end + end + + def to_s + if @aws_account_id + "AWS Security Contact for account: #{@aws_account_id}" + else + "AWS Account Security Contact" + end + end + + private + + def fetch_aws_account + arn = @aws.sts_client.get_caller_identity({}).arn + arn.split(":")[4] + end +end + +def fetch_aws_alternate_contact(aws_client, type) + aws_client + .account_client + .get_alternate_contact({ alternate_contact_type: "#{type.uppercase}" }) + .alternate_contact +end From 04bc284183c0c75a40d7dbfb2eb4f00f0166f6cd Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Fri, 17 Nov 2023 21:46:27 -0500 Subject: [PATCH 06/93] fixed typo in resource_id function Signed-off-by: Aaron Lippold --- libraries/aws_operations_account.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/aws_operations_account.rb b/libraries/aws_operations_account.rb index 874f92122..ad4779df5 100644 --- a/libraries/aws_operations_account.rb +++ b/libraries/aws_operations_account.rb @@ -54,7 +54,7 @@ def configured? def resource_id if @aws_account_id - "AWS Operations Contact for #{@aws_account_id}" + "AWS Operations Contact for account: #{@aws_account_id}" else "AWS Operations Contact Information" end @@ -62,7 +62,7 @@ def resource_id def to_s if @aws_account_id - "AWS Account Operations Contact for #{@aws_account_id}" + "AWS Operations Contact for account: #{@aws_account_id}" else "AWS Account Operations Contact" end From ff7fffd7594df5bd46d334727c1d7c6748b102db Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Fri, 17 Nov 2023 21:48:34 -0500 Subject: [PATCH 07/93] updated aws-account file name to match resource name Signed-off-by: Aaron Lippold --- libraries/{aws_account.rb => aws_primary_contact.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename libraries/{aws_account.rb => aws_primary_contact.rb} (100%) diff --git a/libraries/aws_account.rb b/libraries/aws_primary_contact.rb similarity index 100% rename from libraries/aws_account.rb rename to libraries/aws_primary_contact.rb From 39afb10e5ec741f69d4da3dd648590aef3c81760 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Fri, 17 Nov 2023 21:51:35 -0500 Subject: [PATCH 08/93] updated aws alternate account file names to match resources Signed-off-by: Aaron Lippold --- libraries/{aws_billing_account.rb => aws_billing_contact.rb} | 0 .../{aws_operations_account.rb => aws_operations_contact.rb} | 0 libraries/{aws_security_account.rb => aws_security_contact.rb} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename libraries/{aws_billing_account.rb => aws_billing_contact.rb} (100%) rename libraries/{aws_operations_account.rb => aws_operations_contact.rb} (100%) rename libraries/{aws_security_account.rb => aws_security_contact.rb} (100%) diff --git a/libraries/aws_billing_account.rb b/libraries/aws_billing_contact.rb similarity index 100% rename from libraries/aws_billing_account.rb rename to libraries/aws_billing_contact.rb diff --git a/libraries/aws_operations_account.rb b/libraries/aws_operations_contact.rb similarity index 100% rename from libraries/aws_operations_account.rb rename to libraries/aws_operations_contact.rb diff --git a/libraries/aws_security_account.rb b/libraries/aws_security_contact.rb similarity index 100% rename from libraries/aws_security_account.rb rename to libraries/aws_security_contact.rb From 16ba41cf92b586fd734d480751723ca30f35b969 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Sat, 18 Nov 2023 11:01:43 -0500 Subject: [PATCH 09/93] Added documentation and alias for resources - added documenation for all four resources - added an alias for `configured?` to point to `exist?` Signed-off-by: Aaron Lippold --- .../inspec/resources/aws_billing_contact.md | 103 ++++++++++++++ .../inspec/resources/aws_iam_access_key.md | 16 +-- .../resources/aws_operations_contact.md | 102 ++++++++++++++ .../inspec/resources/aws_primary_contact.md | 126 ++++++++++++++++++ .../inspec/resources/aws_security_contact.md | 102 ++++++++++++++ libraries/aws_billing_contact.rb | 2 + libraries/aws_operations_contact.rb | 2 + libraries/aws_primary_contact.rb | 4 +- libraries/aws_security_contact.rb | 2 + 9 files changed, 450 insertions(+), 9 deletions(-) create mode 100644 docs-chef-io/content/inspec/resources/aws_billing_contact.md create mode 100644 docs-chef-io/content/inspec/resources/aws_operations_contact.md create mode 100644 docs-chef-io/content/inspec/resources/aws_primary_contact.md create mode 100644 docs-chef-io/content/inspec/resources/aws_security_contact.md diff --git a/docs-chef-io/content/inspec/resources/aws_billing_contact.md b/docs-chef-io/content/inspec/resources/aws_billing_contact.md new file mode 100644 index 000000000..36d3662e9 --- /dev/null +++ b/docs-chef-io/content/inspec/resources/aws_billing_contact.md @@ -0,0 +1,103 @@ ++++ +title = "aws_billing_contact Resource" +platform = "aws" +draft = false +gh_repo = "inspec-aws" + +[menu.inspec] +title = "aws_billing_contact" +identifier = "inspec/resources/aws/aws_billing_contact Resource" +parent = "inspec/resources/aws" ++++ + +Use the `aws_billing_contact` InSpec audit resource to test properties of the billing contact information associated with your account. + +For additional information, including details on parameters and properties, see the [AWS documentation on the billing contact information associated with your account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact-billing.html). Technical details on the data structure can be found for the [api documentation.](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/Account/Client.html#get_contact_information-instance_method) + +## Installation + +{{% inspec_aws_install %}} + +## Syntax + +The `aws_billing_contact` resource allows the testing of the billing contact information associated with your account. + +```ruby +describe aws_billing_contact do + it { should exist } +end +``` + +## Parameters + +This resources does not take any parameters at this time. + +## Properties + + +`api_response` (Struct) +: Returns the api response from our call to the aws api as a struct. + +`raw_data` (Hash) +: Returns a transformed Hash of Strings of the data associated with the billing contact. + +`aws_account_id` (String) +: 12-digit account ID number of the Amazon Web Services account associated with the billing contact. + +`name` (String) +: Specifies the full name of the billing contact. + +`title` (String) +: Specifies the full name of the billing contact. + +`email_address` (String) +: Specifies the full name of the billing contact. + +`phone_number` (String) +: Specifies the phone number associated with the billing contact. + +## Examples + +The following examples show how to use this InSpec audit resource. + +**Test that a billing contact exists for the aws account.** + +```ruby +describe aws_billing_contact do + it { should exist } +end +``` + +**Test that the billing contact is set and the values for its full name and first address line are set as expected.** + +```ruby +describe aws_billing_contact do + it { should be_configured } + its('name') { should cmp 'John Smith' } + its('title') { should cmp 'Money Guy' } +end +``` + +## Matchers + +{{% inspec_matchers_link %}} + +### exist (alias of configured) + +Use `should` to test if the aws account has a billing contact configured. + +```ruby +it { should exist } +``` + +### configured + +The `configured` matcher tests if the described billing contact is set and configured for the aws account by returning `true` if the api response is not null or data exists in the raw data. + +```ruby +it { should be_configured } +``` + +## AWS Permissions + +{{% aws_permissions_principal action="Aws::Account::Types::GetAlternateContactResponse" %}} diff --git a/docs-chef-io/content/inspec/resources/aws_iam_access_key.md b/docs-chef-io/content/inspec/resources/aws_iam_access_key.md index 6104bc1a1..edde7a275 100644 --- a/docs-chef-io/content/inspec/resources/aws_iam_access_key.md +++ b/docs-chef-io/content/inspec/resources/aws_iam_access_key.md @@ -1,16 +1,16 @@ +++ -title = "aws_iam_access_key Resource" +title = " Resource" platform = "aws" draft = false gh_repo = "inspec-aws" [menu.inspec] -title = "aws_iam_access_key" -identifier = "inspec/resources/aws/aws_iam_access_key Resource" +title = "" +identifier = "inspec/resources/aws/ Resource" parent = "inspec/resources/aws" +++ -Use the `aws_iam_access_key` InSpec audit resource to test properties of a single AWS IAM Access Key. +Use the `` InSpec audit resource to test properties of a single AWS IAM Access Key. For additional information, including details on parameters and properties, see the [AWS documentation on IAM Access Keys](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html). @@ -20,10 +20,10 @@ For additional information, including details on parameters and properties, see ## Syntax -An `aws_iam_access_key` resource allows the testing of a single AWS IAM Access Key. +An `` resource allows the testing of a single AWS IAM Access Key. ```ruby -describe aws_iam_access_key(access_key_id: 'AKIA1111111111111111') do +describe (access_key_id: 'AKIA1111111111111111') do it { should exist } end ``` @@ -63,7 +63,7 @@ The following examples show how to use this InSpec audit resource. **Test that an IAM Access Key has been used in the last 90 days.** ```ruby -describe aws_iam_access_key(access_key_id: 'AKIA1111111111111111') do +describe (access_key_id: 'AKIA1111111111111111') do it { should exist } its('last_used_date') { should be > Time.now - 90 * 86400 } end @@ -72,7 +72,7 @@ end **Test that an IAM Access Key for a specific user exists.** ```ruby -describe aws_iam_access_key(username: 'psmith', id: 'AKIA1111111111111111') do +describe (username: 'psmith', id: 'AKIA1111111111111111') do it { should exist } end ``` diff --git a/docs-chef-io/content/inspec/resources/aws_operations_contact.md b/docs-chef-io/content/inspec/resources/aws_operations_contact.md new file mode 100644 index 000000000..c36d70998 --- /dev/null +++ b/docs-chef-io/content/inspec/resources/aws_operations_contact.md @@ -0,0 +1,102 @@ ++++ +title = "aws_operations_contact Resource" +platform = "aws" +draft = false +gh_repo = "inspec-aws" + +[menu.inspec] +title = "aws_operations_contact" +identifier = "inspec/resources/aws/aws_operations_contact Resource" +parent = "inspec/resources/aws" ++++ + +Use the `aws_operations_contact` InSpec audit resource to test properties of the operations contact information associated with your account. + +For additional information, including details on parameters and properties, see the [AWS documentation on the operations contact information associated with your account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact-operations.html). Technical details on the data structure can be found for the [api documentation.](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/Account/Client.html#get_contact_information-instance_method) + +## Installation + +{{% inspec_aws_install %}} + +## Syntax + +The `aws_operations_contact` resource allows the testing of the operations contact information associated with your account. + +```ruby +describe aws_operations_contact do + it { should exist } +end +``` + +## Parameters + +This resources does not take any parameters at this time. + +## Properties + +`api_response` (Struct) +: Returns the api response from our call to the aws api as a struct. + +`raw_data` (Hash) +: Returns a transformed Hash of Strings of the data associated with the operations contact. + +`aws_account_id` (String) +: 12-digit account ID number of the Amazon Web Services account associated with the operations contact. + +`name` (String) +: Specifies the full name of the operations contact. + +`title` (String) +: Specifies the full name of the operations contact. + +`email_address` (String) +: Specifies the full name of the operations contact. + +`phone_number` (String) +: Specifies the phone number associated with the operations contact. + +## Examples + +The following examples show how to use this InSpec audit resource. + +**Test that a operations contact exists for the aws account.** + +```ruby +describe aws_operations_contact do + it { should exist } +end +``` + +**Test that the operations contact is set and the values for its full name and first address line are set as expected.** + +```ruby +describe aws_operations_contact do + it { should be_configured } + its('name') { should cmp 'John Smith' } + its('title') { should cmp 'Ops Guy' } +end +``` + +## Matchers + +{{% inspec_matchers_link %}} + +### exist (alias of configured) + +Use `should` to test if the aws account has a operations contact configured. + +```ruby +it { should exist } +``` + +### configured + +The `configured` matcher tests if the described operations contact is set and configured for the aws account by returning `true` if the api response is not null or data exists in the raw data. + +```ruby +it { should be_configured } +``` + +## AWS Permissions + +{{% aws_permissions_principal action="Aws::Account::Types::GetAlternateContactResponse" %}} diff --git a/docs-chef-io/content/inspec/resources/aws_primary_contact.md b/docs-chef-io/content/inspec/resources/aws_primary_contact.md new file mode 100644 index 000000000..d1024869c --- /dev/null +++ b/docs-chef-io/content/inspec/resources/aws_primary_contact.md @@ -0,0 +1,126 @@ ++++ +title = "aws_primary_contact Resource" +platform = "aws" +draft = false +gh_repo = "inspec-aws" + +[menu.inspec] +title = "aws_primary_contact" +identifier = "inspec/resources/aws/aws_primary_contact Resource" +parent = "inspec/resources/aws" ++++ + +Use the `aws_primary_contact` InSpec audit resource to test properties of the primary contact information associated with your account. + +For additional information, including details on parameters and properties, see the [AWS documentation on primary contact information associated with your account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact-primary.html). Technical details on the data structure can be found for the [api documentation.](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/Account/Client.html#get_contact_information-instance_method) + +## Installation + +{{% inspec_aws_install %}} + +## Syntax + +An `aws_primary_contact` resource allows the testing of the primary contact information associated with your account. + +```ruby +describe aws_primary_contact do + it { should exist } +end +``` + +## Parameters + +This resources does not take any parameters at this time. + +## Properties + +`aws_account_id` (String) +: 12-digit account ID number of the Amazon Web Services account. + +`address_line_1` (String) +: Specifies the first address line for the primary contact's address. + +`address_line_2` (String) +: Specifies the second address line for the primary contact's address. + +`address_line_3` (String) +: Specifies the third address line for the primary contact's address. + +`city` (String) +: Specifies the city for the primary contact's address. + +`state_or_region` (String) +: Specifies the state or region for the primary contact's address. + +`postal_code` (String) +: Specifies the postal code for the primary contact's address. + +`country_code` (String) +: Specifies the country code of the primary contact. + +`company_name` (String) +: Specifies the company name associated with the primary contact. + +`full_name` (String) +: Specifies the full name of the primary contact. + +`phone_number` (String) +: Specifies the phone number associated with the primary contact. + +`website_url` (String) +: Specifies the website url associated with the primary contact. + +`district_or_county` (String) +: Specifies the district or county associated with the primary contact. + +`api_response` (Struct) +: Returns the api response from our call to the aws api as a struct. + +`raw_data` (Hash) +: Returns a transformed Hash of Strings of the data associated with the primary contact. + +## Examples + +The following examples show how to use this InSpec audit resource. + +**Test that a primary contact exists for the aws account.** + +```ruby +describe aws_primary_contact do + it { should exist } +end +``` + +**Test that an the primary contact is set and the values for its full name and first address line are set as expected.** + +```ruby +describe aws_primary_contact do + it { should be_configured } + its('full_name') { should cmp 'John Smith' } + its('address_line_1') { should cmp '42 Wallaby Way' } +end +``` + +## Matchers + +{{% inspec_matchers_link %}} + +### exist (alias of configured) + +Use `should` to test the if the aws account has a primary contact configured. + +```ruby +it { should exist } +``` + +### configured + +The `configured` matcher tests if the primary contact is set and configured for the aws account by returning `true` if the api response is not null or data exists in the raw data. + +```ruby +it { should be_configured } +``` + +## AWS Permissions + +{{% aws_permissions_principal action="Aws::Account::Types::GetContactInformationResponse" %}} \ No newline at end of file diff --git a/docs-chef-io/content/inspec/resources/aws_security_contact.md b/docs-chef-io/content/inspec/resources/aws_security_contact.md new file mode 100644 index 000000000..37f70d4dc --- /dev/null +++ b/docs-chef-io/content/inspec/resources/aws_security_contact.md @@ -0,0 +1,102 @@ ++++ +title = "aws_security_contact Resource" +platform = "aws" +draft = false +gh_repo = "inspec-aws" + +[menu.inspec] +title = "aws_security_contact" +identifier = "inspec/resources/aws/aws_security_contact Resource" +parent = "inspec/resources/aws" ++++ + +Use the `aws_security_contact` InSpec audit resource to test properties of the security contact information associated with your account. + +For additional information, including details on parameters and properties, see the [AWS documentation on the security contact information associated with your account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact-security.html). Technical details on the data structure can be found for the [api documentation.](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/Account/Client.html#get_contact_information-instance_method) + +## Installation + +{{% inspec_aws_install %}} + +## Syntax + +The `aws_security_contact` resource allows the testing of the security contact information associated with your account. + +```ruby +describe aws_security_contact do + it { should exist } +end +``` + +## Parameters + +This resources does not take any parameters at this time. + +## Properties + +`api_response` (Struct) +: Returns the api response from our call to the aws api as a struct. + +`raw_data` (Hash) +: Returns a transformed Hash of Strings of the data associated with the security contact. + +`aws_account_id` (String) +: 12-digit account ID number of the Amazon Web Services account associated with the security contact. + +`name` (String) +: Specifies the full name of the security contact. + +`title` (String) +: Specifies the full name of the security contact. + +`email_address` (String) +: Specifies the full name of the security contact. + +`phone_number` (String) +: Specifies the phone number associated with the security contact. + +## Examples + +The following examples show how to use this InSpec audit resource. + +**Test that a security contact exists for the aws account.** + +```ruby +describe aws_security_contact do + it { should exist } +end +``` + +**Test that the security contact is set and the values for its full name and first address line are set as expected.** + +```ruby +describe aws_security_contact do + it { should be_configured } + its('name') { should cmp 'John Smith' } + its('title') { should cmp 'Ops Guy' } +end +``` + +## Matchers + +{{% inspec_matchers_link %}} + +### exist (alias of configured) + +Use `should` to test if the aws account has a security contact configured. + +```ruby +it { should exist } +``` + +### configured + +The `configured` matcher tests if the described security contact is set and configured for the aws account by returning `true` if the api response is not null or data exists in the raw data. + +```ruby +it { should be_configured } +``` + +## AWS Permissions + +{{% aws_permissions_principal action="Aws::Account::Types::GetAlternateContactResponse" %}} diff --git a/libraries/aws_billing_contact.rb b/libraries/aws_billing_contact.rb index 2ab8ceee7..02a3634b1 100644 --- a/libraries/aws_billing_contact.rb +++ b/libraries/aws_billing_contact.rb @@ -52,6 +52,8 @@ def configured? !@api_response.nil? || !@raw_data end + alias exist? configured? + def resource_id if @aws_account_id "AWS Billing Contact for account: #{@aws_account_id}" diff --git a/libraries/aws_operations_contact.rb b/libraries/aws_operations_contact.rb index ad4779df5..93f4a9997 100644 --- a/libraries/aws_operations_contact.rb +++ b/libraries/aws_operations_contact.rb @@ -52,6 +52,8 @@ def configured? !@api_response.nil? || !@raw_data end + alias exist? configured? + def resource_id if @aws_account_id "AWS Operations Contact for account: #{@aws_account_id}" diff --git a/libraries/aws_primary_contact.rb b/libraries/aws_primary_contact.rb index 18ce7f99e..498373b5d 100644 --- a/libraries/aws_primary_contact.rb +++ b/libraries/aws_primary_contact.rb @@ -5,7 +5,7 @@ class AwsPrimaryAccount < AwsResourceBase name "aws_primary_contact" desc "Verifies the primary contact information for an AWS Account." example <<~EXAMPLE - describe aws_primary_account do + describe aws_primary_contact do it { should be_configured } its('full_name') { should cmp 'John Smith' } its('address_line_1') { should cmp '42 Wallaby Way' } @@ -72,6 +72,8 @@ def configured? !@api_response.nil? || !@raw_data end + alias exist? configured? + def resource_id if @aws_account_id "AWS Primary Contact for account: #{@aws_account_id}" diff --git a/libraries/aws_security_contact.rb b/libraries/aws_security_contact.rb index 798a57a1f..d9cbe82e5 100644 --- a/libraries/aws_security_contact.rb +++ b/libraries/aws_security_contact.rb @@ -52,6 +52,8 @@ def configured? !@api_response.nil? || !@raw_data end + alias exist? configured? + def resource_id if @aws_account_id "AWS Security Contact for account: #{@aws_account_id}" From 3ec62d0bd66961f20b83038414d12ec059e825f0 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Sat, 18 Nov 2023 11:08:05 -0500 Subject: [PATCH 10/93] Removed pry from resources Signed-off-by: Aaron Lippold --- libraries/aws_billing_contact.rb | 1 - libraries/aws_operations_contact.rb | 1 - libraries/aws_primary_contact.rb | 1 - libraries/aws_security_contact.rb | 1 - 4 files changed, 4 deletions(-) diff --git a/libraries/aws_billing_contact.rb b/libraries/aws_billing_contact.rb index 02a3634b1..b73cb2e04 100644 --- a/libraries/aws_billing_contact.rb +++ b/libraries/aws_billing_contact.rb @@ -1,5 +1,4 @@ require "aws_backend" -# require "pry" class AwsBillingAccount < AwsResourceBase name "aws_billing_contact" diff --git a/libraries/aws_operations_contact.rb b/libraries/aws_operations_contact.rb index 93f4a9997..5d76d169e 100644 --- a/libraries/aws_operations_contact.rb +++ b/libraries/aws_operations_contact.rb @@ -1,5 +1,4 @@ require "aws_backend" -# require "pry" class AwsAccountOperationsContact < AwsResourceBase name "aws_operations_contact" diff --git a/libraries/aws_primary_contact.rb b/libraries/aws_primary_contact.rb index 498373b5d..ff595710a 100644 --- a/libraries/aws_primary_contact.rb +++ b/libraries/aws_primary_contact.rb @@ -1,5 +1,4 @@ require "aws_backend" -# require "pry" class AwsPrimaryAccount < AwsResourceBase name "aws_primary_contact" diff --git a/libraries/aws_security_contact.rb b/libraries/aws_security_contact.rb index d9cbe82e5..85a1da70f 100644 --- a/libraries/aws_security_contact.rb +++ b/libraries/aws_security_contact.rb @@ -1,5 +1,4 @@ require "aws_backend" -# require "pry" class AwsAccountSecurityContact < AwsResourceBase name "aws_security_contact" From 018f3638ee1abd6acbc37ccc48fa0c601c4dab4b Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Sat, 18 Nov 2023 11:44:32 -0500 Subject: [PATCH 11/93] repulled upsteam copy of docs Signed-off-by: Aaron Lippold --- .../inspec/resources/aws_iam_access_key.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs-chef-io/content/inspec/resources/aws_iam_access_key.md b/docs-chef-io/content/inspec/resources/aws_iam_access_key.md index edde7a275..6104bc1a1 100644 --- a/docs-chef-io/content/inspec/resources/aws_iam_access_key.md +++ b/docs-chef-io/content/inspec/resources/aws_iam_access_key.md @@ -1,16 +1,16 @@ +++ -title = " Resource" +title = "aws_iam_access_key Resource" platform = "aws" draft = false gh_repo = "inspec-aws" [menu.inspec] -title = "" -identifier = "inspec/resources/aws/ Resource" +title = "aws_iam_access_key" +identifier = "inspec/resources/aws/aws_iam_access_key Resource" parent = "inspec/resources/aws" +++ -Use the `` InSpec audit resource to test properties of a single AWS IAM Access Key. +Use the `aws_iam_access_key` InSpec audit resource to test properties of a single AWS IAM Access Key. For additional information, including details on parameters and properties, see the [AWS documentation on IAM Access Keys](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html). @@ -20,10 +20,10 @@ For additional information, including details on parameters and properties, see ## Syntax -An `` resource allows the testing of a single AWS IAM Access Key. +An `aws_iam_access_key` resource allows the testing of a single AWS IAM Access Key. ```ruby -describe (access_key_id: 'AKIA1111111111111111') do +describe aws_iam_access_key(access_key_id: 'AKIA1111111111111111') do it { should exist } end ``` @@ -63,7 +63,7 @@ The following examples show how to use this InSpec audit resource. **Test that an IAM Access Key has been used in the last 90 days.** ```ruby -describe (access_key_id: 'AKIA1111111111111111') do +describe aws_iam_access_key(access_key_id: 'AKIA1111111111111111') do it { should exist } its('last_used_date') { should be > Time.now - 90 * 86400 } end @@ -72,7 +72,7 @@ end **Test that an IAM Access Key for a specific user exists.** ```ruby -describe (username: 'psmith', id: 'AKIA1111111111111111') do +describe aws_iam_access_key(username: 'psmith', id: 'AKIA1111111111111111') do it { should exist } end ``` From 972c118cd137268a9023e6514c40dfee46f76dad Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Sun, 19 Nov 2023 13:07:01 -0500 Subject: [PATCH 12/93] reusing updated aws-billing-contact resource Signed-off-by: Aaron Lippold --- libraries/aws_billing_contact.rb | 17 ++++++++--------- libraries/aws_security_contact.rb | 21 +++++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/libraries/aws_billing_contact.rb b/libraries/aws_billing_contact.rb index b73cb2e04..8ab6c8124 100644 --- a/libraries/aws_billing_contact.rb +++ b/libraries/aws_billing_contact.rb @@ -23,28 +23,27 @@ class AwsBillingAccount < AwsResourceBase def initialize(opts = {}) super(opts) @raw_data = {} + @title, @name, @email_address, @phone_number = String.new validate_parameters begin catch_aws_errors { @aws_account_id = fetch_aws_account } - @api_response = fetch_aws_alternate_contact(@aws, "billing") + @api_response = fetch_aws_alternate_contact("billing") rescue Aws::Account::Errors::ResourceNotFoundException skip_resource( - "The BILLING contact has not been configured for this AWS Account." + "The Billing contact has not been configured for this AWS Account." ) return [] if !@api_response || @api_response.empty? end - unless @api_response.nil? + if @api_response @api_response .members .map(&:to_s) .each do |key| instance_variable_set("@#{key}", @api_response.send(key)) end - @raw_data = @api_response.to_h.transform_keys(&:to_s) - else - @name, @email_address, @phone_number, @title = nil end + @raw_data = @api_response.to_h.transform_keys(&:to_s) end def configured? @@ -76,10 +75,10 @@ def fetch_aws_account arn.split(":")[4] end - def fetch_aws_alternate_contact(aws_client, type) - aws_client + def fetch_aws_alternate_contact(type) + @aws .account_client - .get_alternate_contact({ alternate_contact_type: "#{type.uppercase}" }) + .get_alternate_contact({ alternate_contact_type: "#{type.upcase}" }) .alternate_contact end end diff --git a/libraries/aws_security_contact.rb b/libraries/aws_security_contact.rb index 85a1da70f..e5bcfe7c5 100644 --- a/libraries/aws_security_contact.rb +++ b/libraries/aws_security_contact.rb @@ -1,5 +1,5 @@ require "aws_backend" - +# require "pry-byebug" class AwsAccountSecurityContact < AwsResourceBase name "aws_security_contact" desc "Verifies the security contact information for an AWS Account." @@ -26,7 +26,8 @@ def initialize(opts = {}) validate_parameters begin catch_aws_errors { @aws_account_id = fetch_aws_account } - @api_response = fetch_aws_alternate_contact(@aws, "security") + type = "security" + @api_response = fetch_aws_alternate_contact(type) rescue Aws::Account::Errors::ResourceNotFoundException skip_resource( "The Security contact has not been configured for this AWS Account." @@ -43,7 +44,7 @@ def initialize(opts = {}) end @raw_data = @api_response.to_h.transform_keys(&:to_s) else - @name, @email_address, @phone_number, @title = nil + @name, @email_address, @phone_number, @title = "" end end @@ -69,17 +70,17 @@ def to_s end end - private + # private def fetch_aws_account arn = @aws.sts_client.get_caller_identity({}).arn arn.split(":")[4] end -end -def fetch_aws_alternate_contact(aws_client, type) - aws_client - .account_client - .get_alternate_contact({ alternate_contact_type: "#{type.uppercase}" }) - .alternate_contact + def fetch_aws_alternate_contact(type) + @aws + .account_client + .get_alternate_contact({ alternate_contact_type: "#{type.upcase}" }) + .alternate_contact + end end From c4d9933e5eac9b519b969f06171262cd446473f1 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Sun, 19 Nov 2023 14:16:36 -0500 Subject: [PATCH 13/93] Updated individual resources and added alternate * added the aws-alternate-contact resource * updated and standardized coding for security, billing and operations resources * added documentation for the aws-alternate-contact resource Signed-off-by: Aaron Lippold --- .../inspec/resources/aws_alternate_contact.md | 113 +++++++++++++++++ libraries/aws_alternate_contact.rb | 115 ++++++++++++++++++ libraries/aws_operations_contact.rb | 19 ++- libraries/aws_primary_contact.rb | 34 +++--- libraries/aws_security_contact.rb | 18 ++- 5 files changed, 262 insertions(+), 37 deletions(-) create mode 100644 docs-chef-io/content/inspec/resources/aws_alternate_contact.md create mode 100644 libraries/aws_alternate_contact.rb diff --git a/docs-chef-io/content/inspec/resources/aws_alternate_contact.md b/docs-chef-io/content/inspec/resources/aws_alternate_contact.md new file mode 100644 index 000000000..9493fb1c7 --- /dev/null +++ b/docs-chef-io/content/inspec/resources/aws_alternate_contact.md @@ -0,0 +1,113 @@ ++++ +title = "aws_alternate_contact Resource" +platform = "aws" +draft = false +gh_repo = "inspec-aws" + +[menu.inspec] +title = "aws_alternate_contact" +identifier = "inspec/resources/aws/aws_alternate_contact Resource" +parent = "inspec/resources/aws" ++++ + +Use the `aws_alternate_contact` InSpec audit resource to test properties of the alternate contact information associated with your account. + +For additional information, including details on parameters and properties, see the [AWS documentation on the alternate contact information associated with your account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact-alternate.html). Technical details on the data structure can be found for the [api documentation.](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/Account/Client.html#get_contact_information-instance_method) + +## Installation + +{{% inspec_aws_install %}} + +## Syntax + +The `aws_alternate_contact` resource allows the testing of the alternate contact information associated with your account. + +```ruby +describe aws_alternate_contact do + it { should exist } +end +``` + +## Parameters + +`type` _(required)_ + +: This resource accepts a single parameter, the type of the alternate contact type. + This can be passed either as a string or as a `type: 'value'` key-value entry in a hash. Valid types are 'billing', 'operations' and 'security' + +## Properties + + +`api_response` (Struct) +: Returns the api response from our call to the aws api as a struct. + +`raw_data` (Hash) +: Returns a transformed Hash of Strings of the data associated with the alternate contact. + +`aws_account_id` (String) +: 12-digit account ID number of the Amazon Web Services account associated with the alternate contact. + +`name` (String) +: Specifies the full name of the alternate contact. + +`title` (String) +: Specifies the full name of the alternate contact. + +`email_address` (String) +: Specifies the full name of the alternate contact. + +`phone_number` (String) +: Specifies the phone number associated with the alternate contact. + +## Examples + +The following examples show how to use this InSpec audit resource. + +**Test that a alternate contact exists for the aws account.** + +```ruby +describe aws_alternate_contact do + it { should exist } +end +``` + +**Test that the alternate contact is set and the values for its full name and first address line are set as expected.** + +```ruby +describe aws_alternate_contact(type: 'billing') do + it { should be_configured } + its('name') { should cmp 'John Smith' } + its('title') { should cmp 'Money Guy' } +end +``` +```ruby +describe aws_alternate_contact('security') do + it { should exist } + its('name') { should cmp 'Jane Smith' } + its('title') { should cmp 'Security Gal' } +end +``` + +## Matchers + +{{% inspec_matchers_link %}} + +### exist (alias of configured) + +Use `should` to test if the aws account has a alternate contact configured. + +```ruby +it { should exist } +``` + +### configured + +The `configured` matcher tests if the described alternate contact is set and configured for the aws account by returning `true` if the api response is not null or data exists in the raw data. + +```ruby +it { should be_configured } +``` + +## AWS Permissions + +{{% aws_permissions_principal action="Aws::Account::Types::GetAlternateContactResponse" %}} diff --git a/libraries/aws_alternate_contact.rb b/libraries/aws_alternate_contact.rb new file mode 100644 index 000000000..ddc2f42f2 --- /dev/null +++ b/libraries/aws_alternate_contact.rb @@ -0,0 +1,115 @@ +require "aws_backend" +require "pry" + +class AwsAlternateAccount < AwsResourceBase + name "aws_alternate_contact" + desc "Verifies the billing contact information for an AWS Account." + example <<~EXAMPLE1 + describe aws_alternate_account(type: 'billing') do + it { should be_configured } + its('name') { should cmp 'John Smith' } + its('email_address') { should cmp 'jsmith@acme.com' } + end + EXAMPLE1 + + example <<~EXAMPLE2 + describe aws_alternate_account('security') do + it { should be_configured } + its('name') { should cmp 'Jane Smith' } + its('email_address') { should cmp 'janesmith@acme.com' } + end + EXAMPLE2 + + attr_reader :raw_data, + :api, + :api_response, + :email_address, + :name, + :phone_number, + :title, + :aws_account_id + + def initialize(opts = {}) # rubocop:disable Metrics/MethodLength + @raw_data = {} + supported_opt_keys = %i[type] + supported_opts_values = %w[billing operations security] + opts = { type: opts } if opts.is_a?(String) + + unless opts.respond_to?(:keys) + raise ArgumentError, + "Invalid aws_alternate_contact param '#{opts}'. Please pass a hash with these supported key(s): #{supported_opt_keys}" + end + unless (opts.keys - supported_opt_keys).empty? + raise ArgumentError, + "Unsupported aws_alternate_contact options '#{opts.keys - supported_opt_keys}'. Supported key(s): #{supported_opt_keys}" + end + unless opts.keys && (opts.keys & supported_opt_keys).length == 1 + raise ArgumentError, + "Specifying more than one of :type for aws_alternate_account is not supported" + end + unless supported_opts_values.any? { |val| opts.values.include?(val) } + raise ArgumentError, + "You may only pass a value of type: #{supported_opts_values} as the ':type' for aws_alternate_account" + end + super(opts) + validate_parameters(required: [:type]) + + begin + catch_aws_errors { @aws_account_id = fetch_aws_account } + @api_response = fetch_aws_alternate_contact(opts[:type]) + rescue Aws::Account::Errors::ResourceNotFoundException + skip_resource( + "The #{opts[:type].uppercase} contact has not been configured for this AWS Account." + ) + return [] if !@api_response || @api_response.empty? + end + + unless @api_response.nil? + @api_response + .members + .map(&:to_s) + .each do |key| + instance_variable_set("@#{key}", @api_response.send(key)) + end + @raw_data = @api_response.to_h.transform_keys(&:to_s) + else + @name, @email_address, @phone_number, @title = "" + end + end + + def configured? + !@api_response.nil? || !@raw_data + end + + alias exist? configured? + + def resource_id + if @aws_account_id + "AWS #{opts[:type].capitalize} Contact for account: #{@aws_account_id}" + else + "AWS #{opts[:type].capitalize} Contact Information" + end + end + + def to_s + if @aws_account_id + "AWS #{opts[:type].capitalize} Contact for account: #{@aws_account_id}" + else + "AWS Account #{opts[:type].capitalize} Contact" + end + end + + private + + def fetch_aws_account + arn = @aws.sts_client.get_caller_identity({}).arn + arn.split(":")[4] + end + + def fetch_aws_alternate_contact(type) + @aws + .account_client + .get_alternate_contact({ alternate_contact_type: "#{type.upcase}" }) + .alternate_contact + end +end diff --git a/libraries/aws_operations_contact.rb b/libraries/aws_operations_contact.rb index 5d76d169e..07b98791a 100644 --- a/libraries/aws_operations_contact.rb +++ b/libraries/aws_operations_contact.rb @@ -1,6 +1,6 @@ require "aws_backend" -class AwsAccountOperationsContact < AwsResourceBase +class AwsOperationsAccount < AwsResourceBase name "aws_operations_contact" desc "Verifies the operations contact information for an AWS Account." example <<~EXAMPLE @@ -23,10 +23,11 @@ class AwsAccountOperationsContact < AwsResourceBase def initialize(opts = {}) super(opts) @raw_data = {} + @title, @name, @email_address, @phone_number = String.new validate_parameters begin catch_aws_errors { @aws_account_id = fetch_aws_account } - @api_response = fetch_aws_alternate_contact(@aws, "operations") + @api_response = fetch_aws_alternate_contact("operations") rescue Aws::Account::Errors::ResourceNotFoundException skip_resource( "The Operations contact has not been configured for this AWS Account." @@ -34,17 +35,15 @@ def initialize(opts = {}) return [] if !@api_response || @api_response.empty? end - unless @api_response.nil? + if @api_response @api_response .members .map(&:to_s) .each do |key| instance_variable_set("@#{key}", @api_response.send(key)) end - @raw_data = @api_response.to_h.transform_keys(&:to_s) - else - @name, @email_address, @phone_number, @title = nil end + @raw_data = @api_response.to_h.transform_keys(&:to_s) end def configured? @@ -65,7 +64,7 @@ def to_s if @aws_account_id "AWS Operations Contact for account: #{@aws_account_id}" else - "AWS Account Operations Contact" + "AWS Account Primary Contact" end end @@ -76,10 +75,10 @@ def fetch_aws_account arn.split(":")[4] end - def fetch_aws_alternate_contact(aws_client, type) - aws_client + def fetch_aws_alternate_contact(type) + @aws .account_client - .get_alternate_contact({ alternate_contact_type: "#{type.uppercase}" }) + .get_alternate_contact({ alternate_contact_type: "#{type.upcase}" }) .alternate_contact end end diff --git a/libraries/aws_primary_contact.rb b/libraries/aws_primary_contact.rb index ff595710a..3e8332b8e 100644 --- a/libraries/aws_primary_contact.rb +++ b/libraries/aws_primary_contact.rb @@ -1,4 +1,5 @@ require "aws_backend" +require "pry-byebug" class AwsPrimaryAccount < AwsResourceBase name "aws_primary_contact" @@ -29,12 +30,25 @@ class AwsPrimaryAccount < AwsResourceBase def initialize(opts = {}) @raw_data = {} + @address_line_1, + @address_line_2, + @address_line_3, + @city, + @country_code, + @company_name, + @district_or_county, + @full_name, + @phone_number, + @postal_code, + @state_or_region, + @website_url = + String.new super(opts) validate_parameters begin catch_aws_errors { @aws_account_id = fetch_aws_account } @api_response = - @aws.account_client.get_contact_information.contact_information || nil + @aws.account_client.get_contact_information.contact_information rescue Aws::Account::Errors::ResourceNotFoundException skip_resource( "The Primary contact has not been configured for this AWS Account." @@ -42,29 +56,15 @@ def initialize(opts = {}) return [] if !@api_response || @api_response.empty? end - unless @api_response.nil? + if @api_response @api_response .members .map(&:to_s) .each do |key| instance_variable_set("@#{key}", @api_response.send(key)) end - @raw_data = @api_response.to_h.transform_keys(&:to_s) - else - @address_line_1, - @address_line_2, - @address_line_3, - @city, - @country_code, - @company_name, - @district_or_county, - @full_name, - @phone_number, - @postal_code, - @state_or_region, - @website_url = - nil end + @raw_data = @api_response.to_h.transform_keys(&:to_s) end def configured? diff --git a/libraries/aws_security_contact.rb b/libraries/aws_security_contact.rb index e5bcfe7c5..bae3472a8 100644 --- a/libraries/aws_security_contact.rb +++ b/libraries/aws_security_contact.rb @@ -1,6 +1,6 @@ require "aws_backend" -# require "pry-byebug" -class AwsAccountSecurityContact < AwsResourceBase + +class AwsSecurityAccount < AwsResourceBase name "aws_security_contact" desc "Verifies the security contact information for an AWS Account." example <<~EXAMPLE @@ -23,11 +23,11 @@ class AwsAccountSecurityContact < AwsResourceBase def initialize(opts = {}) super(opts) @raw_data = {} + @title, @name, @email_address, @phone_number = String.new validate_parameters begin catch_aws_errors { @aws_account_id = fetch_aws_account } - type = "security" - @api_response = fetch_aws_alternate_contact(type) + @api_response = fetch_aws_alternate_contact("security") rescue Aws::Account::Errors::ResourceNotFoundException skip_resource( "The Security contact has not been configured for this AWS Account." @@ -35,17 +35,15 @@ def initialize(opts = {}) return [] if !@api_response || @api_response.empty? end - unless @api_response.nil? + if @api_response @api_response .members .map(&:to_s) .each do |key| instance_variable_set("@#{key}", @api_response.send(key)) end - @raw_data = @api_response.to_h.transform_keys(&:to_s) - else - @name, @email_address, @phone_number, @title = "" end + @raw_data = @api_response.to_h.transform_keys(&:to_s) end def configured? @@ -66,11 +64,11 @@ def to_s if @aws_account_id "AWS Security Contact for account: #{@aws_account_id}" else - "AWS Account Security Contact" + "AWS Account Primary Contact" end end - # private + private def fetch_aws_account arn = @aws.sts_client.get_caller_identity({}).arn From 7d55c44cf4d0649c5167cc052bfda45e78b38c40 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Sun, 19 Nov 2023 15:36:17 -0500 Subject: [PATCH 14/93] exposed the instance data for easier use by end user Signed-off-by: Aaron Lippold --- libraries/aws_ec2_instance.rb | 52 +++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/libraries/aws_ec2_instance.rb b/libraries/aws_ec2_instance.rb index 4a511ed4b..df15cb5e8 100644 --- a/libraries/aws_ec2_instance.rb +++ b/libraries/aws_ec2_instance.rb @@ -1,4 +1,5 @@ require "aws_backend" +require "pry" class AwsEc2Instance < AwsResourceBase name "aws_ec2_instance" @@ -16,31 +17,40 @@ class AwsEc2Instance < AwsResourceBase end " + attr_reader :instance + def initialize(opts = {}) opts = { instance_id: opts } if opts.is_a?(String) super(opts) - validate_parameters(require_any_of: %i(instance_id name)) + validate_parameters(require_any_of: %i[instance_id name]) if opts[:instance_id] && !opts[:instance_id].empty? # Use instance_id, if provided - if !opts[:instance_id].is_a?(String) || opts[:instance_id] !~ /(^i-[0-9a-f]{8})|(^i-[0-9a-f]{17})$/ - raise ArgumentError, "#{@__resource_name__}: `instance_id` must be a string in the format of 'i-' followed by 8 or 17 hexadecimal characters." + if !opts[:instance_id].is_a?(String) || + opts[:instance_id] !~ /(^i-[0-9a-f]{8})|(^i-[0-9a-f]{17})$/ + raise ArgumentError, + "#{@__resource_name__}: `instance_id` must be a string in the format of 'i-' followed by 8 or 17 hexadecimal characters." end @display_name = opts[:instance_id] instance_arguments = { instance_ids: [opts[:instance_id]] } elsif opts[:name] && !opts[:name].empty? # Otherwise use name, if provided @display_name = opts[:name] - instance_arguments = { filters: [{ name: "tag:Name", values: [opts[:name]] }] } + instance_arguments = { + filters: [{ name: "tag:Name", values: [opts[:name]] }] + } else - raise ArgumentError, "#{@__resource_name__}: either instance_id or name must be provided." + raise ArgumentError, + "#{@__resource_name__}: either instance_id or name must be provided." end catch_aws_errors do resp = @aws.compute_client.describe_instances(instance_arguments) - if resp.reservations.first.nil? || resp.reservations.first.instances.first.nil? + if resp.reservations.first.nil? || + resp.reservations.first.instances.first.nil? empty_response_warn return end - if resp.reservations.count > 1 || resp.reservations.first.instances.count > 1 + if resp.reservations.count > 1 || + resp.reservations.first.instances.count > 1 resource_fail return else @@ -57,7 +67,9 @@ def state end def security_groups - @instance[:security_groups].map { |sg| { id: sg[:group_id], name: sg[:group_name] } } + @instance[:security_groups].map do |sg| + { id: sg[:group_id], name: sg[:group_name] } + end end def tags @@ -86,7 +98,9 @@ def availability_zone def ebs_volumes return nil unless @instance[:block_device_mappings] return nil if @instance[:block_device_mappings].count == 0 - @instance[:block_device_mappings].map { |vol| { id: vol[:ebs][:volume_id], name: vol[:device_name] } } + @instance[:block_device_mappings].map do |vol| + { id: vol[:ebs][:volume_id], name: vol[:device_name] } + end end def network_interface_ids @@ -96,12 +110,18 @@ def network_interface_ids end def has_roles? - return false unless @instance[:iam_instance_profile] && @instance[:iam_instance_profile][:arn] + unless @instance[:iam_instance_profile] && + @instance[:iam_instance_profile][:arn] + return false + end instance_profile = @instance[:iam_instance_profile][:arn].split("/").last @returned_roles = nil # Check if there is a role created at the attached profile catch_aws_errors do - resp = @aws.iam_client.get_instance_profile({ instance_profile_name: instance_profile }) + resp = + @aws.iam_client.get_instance_profile( + { instance_profile_name: instance_profile } + ) @returned_roles = resp.instance_profile.roles end @returned_roles && !@returned_roles.empty? @@ -117,7 +137,15 @@ def role end # Generate a matcher for each state - %w{pending running shutting-down terminated stopping stopped unknown}.each do |state_name| + %w[ + pending + running + shutting-down + terminated + stopping + stopped + unknown + ].each do |state_name| define_method "#{state_name.tr("-", "_")}?" do state == state_name end From 7b5ea639367055b751178cfcc468e668c52014f3 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Sun, 19 Nov 2023 17:41:09 -0500 Subject: [PATCH 15/93] updated docs on aws-ec2-instance to include the instance properity Signed-off-by: Aaron Lippold --- docs-chef-io/content/inspec/resources/aws_ec2_instance.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs-chef-io/content/inspec/resources/aws_ec2_instance.md b/docs-chef-io/content/inspec/resources/aws_ec2_instance.md index 5650f4a1b..81b57ca31 100644 --- a/docs-chef-io/content/inspec/resources/aws_ec2_instance.md +++ b/docs-chef-io/content/inspec/resources/aws_ec2_instance.md @@ -80,6 +80,9 @@ One of either the EC2 instance's ID or name must be be provided. There are also additional properties available. For a comprehensive list, see [the API reference documentation](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_Instance.html) +`instance` +: A hash containing all the data collected about the EC2 + ## Examples **Test that an EC2 instance is running.** From 06652c82fcbe13f29de86c0ffd2b84e2cb689f63 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Sun, 19 Nov 2023 17:41:09 -0500 Subject: [PATCH 16/93] updated docs on aws-ec2-instance to include the instance properity Signed-off-by: Aaron Lippold --- docs-chef-io/content/inspec/resources/aws_ec2_instance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-chef-io/content/inspec/resources/aws_ec2_instance.md b/docs-chef-io/content/inspec/resources/aws_ec2_instance.md index 81b57ca31..d9b5b3f1b 100644 --- a/docs-chef-io/content/inspec/resources/aws_ec2_instance.md +++ b/docs-chef-io/content/inspec/resources/aws_ec2_instance.md @@ -81,7 +81,7 @@ One of either the EC2 instance's ID or name must be be provided. There are also additional properties available. For a comprehensive list, see [the API reference documentation](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_Instance.html) `instance` -: A hash containing all the data collected about the EC2 +: A hash containing all the data collected about the EC2. ## Examples From d1a54e8f0e7bdc699fcac676dc8b209d43c769ef Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Sun, 19 Nov 2023 17:48:48 -0500 Subject: [PATCH 17/93] removed pry from requires Signed-off-by: Aaron Lippold --- libraries/aws_alternate_contact.rb | 1 - libraries/aws_ec2_instance.rb | 1 - libraries/aws_primary_contact.rb | 1 - 3 files changed, 3 deletions(-) diff --git a/libraries/aws_alternate_contact.rb b/libraries/aws_alternate_contact.rb index ddc2f42f2..f57b66892 100644 --- a/libraries/aws_alternate_contact.rb +++ b/libraries/aws_alternate_contact.rb @@ -1,5 +1,4 @@ require "aws_backend" -require "pry" class AwsAlternateAccount < AwsResourceBase name "aws_alternate_contact" diff --git a/libraries/aws_ec2_instance.rb b/libraries/aws_ec2_instance.rb index df15cb5e8..f25a469f0 100644 --- a/libraries/aws_ec2_instance.rb +++ b/libraries/aws_ec2_instance.rb @@ -1,5 +1,4 @@ require "aws_backend" -require "pry" class AwsEc2Instance < AwsResourceBase name "aws_ec2_instance" diff --git a/libraries/aws_primary_contact.rb b/libraries/aws_primary_contact.rb index 3e8332b8e..a07ef9e18 100644 --- a/libraries/aws_primary_contact.rb +++ b/libraries/aws_primary_contact.rb @@ -1,5 +1,4 @@ require "aws_backend" -require "pry-byebug" class AwsPrimaryAccount < AwsResourceBase name "aws_primary_contact" From c27b2e88a038ae210eb02005ef54e096fe197174 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Tue, 21 Nov 2023 22:30:43 -0500 Subject: [PATCH 18/93] updated examples Signed-off-by: Aaron Lippold --- libraries/aws_alternate_contact.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/libraries/aws_alternate_contact.rb b/libraries/aws_alternate_contact.rb index f57b66892..4a760255a 100644 --- a/libraries/aws_alternate_contact.rb +++ b/libraries/aws_alternate_contact.rb @@ -4,20 +4,20 @@ class AwsAlternateAccount < AwsResourceBase name "aws_alternate_contact" desc "Verifies the billing contact information for an AWS Account." example <<~EXAMPLE1 - describe aws_alternate_account(type: 'billing') do + describe aws_alternate_account(type: 'billing') do it { should be_configured } its('name') { should cmp 'John Smith' } its('email_address') { should cmp 'jsmith@acme.com' } end - EXAMPLE1 + EXAMPLE1 example <<~EXAMPLE2 - describe aws_alternate_account('security') do - it { should be_configured } - its('name') { should cmp 'Jane Smith' } - its('email_address') { should cmp 'janesmith@acme.com' } - end - EXAMPLE2 + describe aws_alternate_account('security') do + it { should be_configured } + its('name') { should cmp 'Jane Smith' } + its('email_address') { should cmp 'janesmith@acme.com' } + end + EXAMPLE2 attr_reader :raw_data, :api, From 36729573ead94405fc35ea13be8b3140f4ace24c Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Wed, 22 Nov 2023 16:07:31 -0500 Subject: [PATCH 19/93] Start of Access Analyzer Plural Resource Need to add region as a optional pram to the constructor Signed-off-by: Aaron Lippold --- Gemfile | 16 ++-- libraries/aws_backend.rb | 13 +++ libraries/aws_iam_access_analyzers.rb | 121 ++++++++++++++++++++++++++ libraries/aws_regions.rb | 12 +-- 4 files changed, 150 insertions(+), 12 deletions(-) create mode 100644 libraries/aws_iam_access_analyzers.rb diff --git a/Gemfile b/Gemfile index fee6f685c..98538b59a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ -source 'https://rubygems.org' +source "https://rubygems.org" -gem 'bundle' +gem "bundle" # Note that 'aws-sdk' pulls in a large number of libraries, choose explicitly those to include instead # gem 'aws-sdk', '~> 3' # @@ -11,9 +11,11 @@ gem 'bundle' # In the mean time the gem can be added here for local development # Use Latest Inspec -gem 'inspec-bin' +gem "inspec-bin" +gem "aws-sdk-accessanalyzer" +gem "aws-partitions" -gem 'rubocop', '~> 1.25.1', require: false +gem "rubocop", "~> 1.25.1", require: false group :test do gem "chefstyle", "~> 2.2.2" @@ -22,7 +24,7 @@ group :test do end group :development do - gem 'rake' - gem 'minitest' - gem 'pry-byebug' + gem "rake" + gem "minitest" + gem "pry-byebug" end diff --git a/libraries/aws_backend.rb b/libraries/aws_backend.rb index b9b423692..20a7c05c0 100644 --- a/libraries/aws_backend.rb +++ b/libraries/aws_backend.rb @@ -63,6 +63,7 @@ require "aws-sdk-synthetics" require "aws-sdk-apigatewayv2" require "aws-sdk-account" +require "aws-sdk-accessanalyzer" # AWS Inspec Backend Classes # @@ -341,6 +342,14 @@ def apigatewayv2_client def account_client aws_client(Aws::Account::Client) end + + def access_analyzer_client + aws_client(Aws::AccessAnalyzer::Client) + end + + def partitions_region_client + aws_client(Aws::Partitions::Region::Client) + end end # Base class for AWS resources @@ -495,6 +504,10 @@ def catch_aws_errors Inspec::Log.warn "#{e.message}" skip_resource("#{e.message}") nil + rescue Aws::AccessAnalyzer::Errors => e + Inspec::Log.warn "#{e.message}" + skip_resource("#{e.message}") + nil rescue Aws::Errors::NoSuchEndpointError Inspec::Log.error "The endpoint that is trying to be accessed does not exist." fail_resource("Invalid Endpoint error") diff --git a/libraries/aws_iam_access_analyzers.rb b/libraries/aws_iam_access_analyzers.rb new file mode 100644 index 000000000..d0cb36a6f --- /dev/null +++ b/libraries/aws_iam_access_analyzers.rb @@ -0,0 +1,121 @@ +require "aws_backend" +require "pry" + +class AwsIamAccessAnalyzer < AwsResourceBase + name "aws_iam_access_analyzers" + desc "Verifies settings for a collection AWS IAM Access Analyzers." + example <<~EXAMPLE1 + # retrieve both 'account' and 'organization' analyzers + describe aws_iam_access_analyzers do + it { should exist } + end + EXAMPLE1 + + example <<~EXAMPLE2 + # retrieve only 'account' analyzers + describe aws_iam_access_analyzers('account') do + it { should exist } + end + EXAMPLE2 + + example <<~EXAMPLE3 + # retrieve only 'account' analyzers + describe aws_iam_access_analyzers(type: 'account') do + it { should exist } + end + EXAMPLE3 + + attr_reader :table, :raw_data, :api_response, :aws_account_id, :parameters + + FilterTable + .create + .register_column(:analyzer_names, field: :name) + .register_column(:analyzer_type, field: :type) + .register_column(:arns, field: :arn) + .register_column(:created_date, field: :created_at) + .register_column(:last_resource_analyzed, field: :last_resource_analyzed) + .register_column(:last_analyzed_date, field: :last_resource_analyzed_at) + .register_column(:tags, field: :tags) + .register_column(:status, field: :status) + .register_column(:status_reason, filed: :status_reason) + .install_filter_methods_on_resource(self, :table) + + def initialize(opts = {}) + @raw_data = [] + @api_response = nil + @supported_opts_values = %w[account organization all] + + opts = { type: opts } if opts.is_a?(String) + unless (opts.values - @supported_opts_values).empty? || opts.nil? + raise ArgumentError, + "Unsupported options '#{opts.values - @supported_opts_values}'. Supported key(s): #{@supported_opts_values}" + end + super(opts) + validate_parameters(allow: %i[type]) + parameters = {} + parameters[:type] = opts[:type].upcase if opts[:type] + puts parameters + @table = fetch_data(parameters) + puts @table.empty? + end + + def fetch_data(parameters) + analyzer_rows = [] + catch_aws_errors do + catch_aws_errors { @aws_account_id = fetch_aws_account } + if parameters.empty? || parameters[:type] == "ALL" + @api_response = @aws.access_analyzer_client.list_analyzers + elsif parameters[:type] == "ACCOUNT" || + parameters[:type] == "ORGANIZATION" + @api_response = @aws.access_analyzer_client.list_analyzers(parameters) + end + + @api_response.analyzers.each do |aa| + analyzer_rows += [ + { + arn: aa.arn, + name: aa.name, + type: aa.type, + created_at: aa.created_at, + last_resource_analyzed: aa.last_resource_analyzed, + last_resource_analyzed_at: aa.last_resource_analyzed_at, + #TODO: Flatten the hash of tags? + tags: aa.tags, + status: aa.status, + status_reason: aa.status_reason + } + ] + end + @raw_data = + @api_response[:analyzers].empty? ? [] : @api_response.to_h[:analyzers] + end + @table = analyzer_rows + end + + def resource_id + response = "AWS IAM " + opts[:type] ? response += "#{opts[:type].capitalize} " : "" + if @aws_account_id + response += "Account Analyzer for #{@aws_account_id}" + else + response += "Account Analyzer Information" + end + end + + def to_s + response = "AWS IAM " + opts[:type] ? response += "#{opts[:type].capitalize} " : "" + if @aws_account_id + response += "Account Analyzer for #{@aws_account_id}" + else + response += "Account Analyzer Information" + end + end + + private + + def fetch_aws_account + arn = @aws.sts_client.get_caller_identity({}).arn + arn.split(":")[4] + end +end diff --git a/libraries/aws_regions.rb b/libraries/aws_regions.rb index 4f527177c..dd0187d40 100644 --- a/libraries/aws_regions.rb +++ b/libraries/aws_regions.rb @@ -10,11 +10,12 @@ class AwsRegions < AwsResourceBase end " - attr_reader :table + attr_reader :table, :regions - FilterTable.create + FilterTable + .create .register_column(:region_names, field: :region_name) - .register_column(:endpoints, field: :endpoint) + .register_column(:endpoints, field: :endpoint) .install_filter_methods_on_resource(self, :table) def initialize(opts = {}) @@ -30,8 +31,9 @@ def fetch_data end return [] if !@regions || @regions.empty? @regions.each do |region| - region_rows += [{ region_name: region[:region_name], - endpoint: region[:endpoint] }] + region_rows += [ + { region_name: region[:region_name], endpoint: region[:endpoint] } + ] end @table = region_rows end From 574dd55696bfbfaf2d474527e61fe982b2b9f104 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Wed, 22 Nov 2023 22:59:28 -0500 Subject: [PATCH 20/93] Updates to support CIS AWS Foundations 1.20 - added aws_iam_access_analyzers plural resource - updated aws_regions and aws_region to expose opt_in data - update aws_regions(s) docs Signed-off-by: Aaron Lippold --- .../content/inspec/resources/aws_region.md | 4 ++++ .../content/inspec/resources/aws_regions.md | 6 +++++ libraries/aws_iam_access_analyzers.rb | 23 +++++++++++++------ libraries/aws_region.rb | 10 ++++---- libraries/aws_regions.rb | 22 +++++++++++++++++- 5 files changed, 53 insertions(+), 12 deletions(-) diff --git a/docs-chef-io/content/inspec/resources/aws_region.md b/docs-chef-io/content/inspec/resources/aws_region.md index 0940e2b8f..ba35420bc 100644 --- a/docs-chef-io/content/inspec/resources/aws_region.md +++ b/docs-chef-io/content/inspec/resources/aws_region.md @@ -31,6 +31,7 @@ end ```ruby describe aws_region(region_name: 'us-east-1') do it { should exist } + its('opt_in_status') { should cmp 'opt-in-not-required' } end ``` @@ -49,6 +50,9 @@ end `endpoint` : The resolved endpoint of the region. +`opt_in_status` +: The opt-in status of the Region (opt-in-not-required | opted-in | not-opted-in). + ## Examples **Test whether a region exists.** diff --git a/docs-chef-io/content/inspec/resources/aws_regions.md b/docs-chef-io/content/inspec/resources/aws_regions.md index 52f2c24c2..5cb815faa 100644 --- a/docs-chef-io/content/inspec/resources/aws_regions.md +++ b/docs-chef-io/content/inspec/resources/aws_regions.md @@ -44,6 +44,12 @@ end `endpoints` : The resolved endpoints of the regions. +`opt_in_status` +: The opt-in status of the Region. Possible values are: `opt-in-not-required`, `opted-in` and `not-opted-in`. + +`region_opt_status` +: One of the potential statuses a Region can undergo. Possible values are: `Enabled`, `Enabling`, `Disabled`, `Disabling` and `Enabled_By_Default`. + ## Examples The following examples show how to use this InSpec audit resource. diff --git a/libraries/aws_iam_access_analyzers.rb b/libraries/aws_iam_access_analyzers.rb index d0cb36a6f..878d462a7 100644 --- a/libraries/aws_iam_access_analyzers.rb +++ b/libraries/aws_iam_access_analyzers.rb @@ -51,12 +51,13 @@ def initialize(opts = {}) "Unsupported options '#{opts.values - @supported_opts_values}'. Supported key(s): #{@supported_opts_values}" end super(opts) - validate_parameters(allow: %i[type]) + validate_parameters(allow: %i[type aws_region]) parameters = {} parameters[:type] = opts[:type].upcase if opts[:type] - puts parameters + @aws.access_analyzer_client.config.region = opts[:aws_region] if opts[ + :aws_region + ] @table = fetch_data(parameters) - puts @table.empty? end def fetch_data(parameters) @@ -96,20 +97,24 @@ def resource_id response = "AWS IAM " opts[:type] ? response += "#{opts[:type].capitalize} " : "" if @aws_account_id - response += "Account Analyzer for #{@aws_account_id}" + response += + "Account Analyzer for #{@aws_account_id} in #{get_current_region}" else - response += "Account Analyzer Information" + response += "Account Analyzer Information in #{get_current_region}" end + response end def to_s response = "AWS IAM " opts[:type] ? response += "#{opts[:type].capitalize} " : "" if @aws_account_id - response += "Account Analyzer for #{@aws_account_id}" + response += + "Account Analyzer for #{@aws_account_id} in #{get_current_region}" else - response += "Account Analyzer Information" + response += "Account Analyzer Information in #{get_current_region}" end + response end private @@ -118,4 +123,8 @@ def fetch_aws_account arn = @aws.sts_client.get_caller_identity({}).arn arn.split(":")[4] end + + def get_current_region + @aws.access_analyzer_client.config.region + end end diff --git a/libraries/aws_region.rb b/libraries/aws_region.rb index e3091a727..725a353c0 100644 --- a/libraries/aws_region.rb +++ b/libraries/aws_region.rb @@ -1,4 +1,5 @@ require "aws_backend" +require "pry" class AwsRegion < AwsResourceBase name "aws_region" @@ -9,7 +10,7 @@ class AwsRegion < AwsResourceBase it { should exist } end " - attr_reader :region_name, :endpoint + attr_reader :region_name, :endpoint, :resp, :opt_in_status def initialize(opts = {}) opts = { region_name: opts } if opts.is_a?(String) @@ -19,9 +20,10 @@ def initialize(opts = {}) @region_name = opts[:region_name] catch_aws_errors do - resp = @aws.compute_client.describe_regions(region_names: [@region_name]) - return if resp.regions.empty? - @endpoint = resp.regions[0].endpoint + @resp = @aws.compute_client.describe_regions(region_names: [@region_name]) + return if @resp.regions.empty? + @opt_in_status = @resp.regions[0].opt_in_status + @endpoint = @resp.regions[0].endpoint end end diff --git a/libraries/aws_regions.rb b/libraries/aws_regions.rb index dd0187d40..bc837194c 100644 --- a/libraries/aws_regions.rb +++ b/libraries/aws_regions.rb @@ -16,6 +16,8 @@ class AwsRegions < AwsResourceBase .create .register_column(:region_names, field: :region_name) .register_column(:endpoints, field: :endpoint) + .register_column(:opt_in_status, field: :opt_in_status) + .register_column(:region_opt_status, field: :region_opt_status) .install_filter_methods_on_resource(self, :table) def initialize(opts = {}) @@ -30,11 +32,29 @@ def fetch_data @regions = @aws.compute_client.describe_regions.to_h[:regions] end return [] if !@regions || @regions.empty? + region_opt_status = "" @regions.each do |region| + catch_aws_errors do + region_opt_status = fetch_region_opt_status(region[:region_name]) + end region_rows += [ - { region_name: region[:region_name], endpoint: region[:endpoint] } + { + region_name: region[:region_name], + endpoint: region[:endpoint], + opt_in_status: region[:opt_in_status], + region_opt_status: region_opt_status + } ] end @table = region_rows end + + private + + def fetch_region_opt_status(region) + @aws + .account_client + .get_region_opt_status({ region_name: region }) + .region_opt_status + end end From f1f6eed0d9440cfa0033b18ed03101fc6237320d Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Wed, 22 Nov 2023 23:23:35 -0500 Subject: [PATCH 21/93] minor updates for string interpolation fixes Signed-off-by: Aaron Lippold --- libraries/aws_alternate_contact.rb | 22 +-- libraries/aws_backend.rb | 234 ++++++++++++++--------------- libraries/aws_iam_root_user.rb | 14 +- 3 files changed, 135 insertions(+), 135 deletions(-) diff --git a/libraries/aws_alternate_contact.rb b/libraries/aws_alternate_contact.rb index 4a760255a..a6d4088c3 100644 --- a/libraries/aws_alternate_contact.rb +++ b/libraries/aws_alternate_contact.rb @@ -1,8 +1,8 @@ -require "aws_backend" +require 'aws_backend' class AwsAlternateAccount < AwsResourceBase - name "aws_alternate_contact" - desc "Verifies the billing contact information for an AWS Account." + name 'aws_alternate_contact' + desc 'Verifies the billing contact information for an AWS Account.' example <<~EXAMPLE1 describe aws_alternate_account(type: 'billing') do it { should be_configured } @@ -28,10 +28,10 @@ class AwsAlternateAccount < AwsResourceBase :title, :aws_account_id - def initialize(opts = {}) # rubocop:disable Metrics/MethodLength + def initialize(opts = {}) @raw_data = {} - supported_opt_keys = %i[type] - supported_opts_values = %w[billing operations security] + supported_opt_keys = %i(type) + supported_opts_values = %w{billing operations security} opts = { type: opts } if opts.is_a?(String) unless opts.respond_to?(:keys) @@ -44,7 +44,7 @@ def initialize(opts = {}) # rubocop:disable Metrics/MethodLength end unless opts.keys && (opts.keys & supported_opt_keys).length == 1 raise ArgumentError, - "Specifying more than one of :type for aws_alternate_account is not supported" + 'Specifying more than one of :type for aws_alternate_account is not supported' end unless supported_opts_values.any? { |val| opts.values.include?(val) } raise ArgumentError, @@ -58,7 +58,7 @@ def initialize(opts = {}) # rubocop:disable Metrics/MethodLength @api_response = fetch_aws_alternate_contact(opts[:type]) rescue Aws::Account::Errors::ResourceNotFoundException skip_resource( - "The #{opts[:type].uppercase} contact has not been configured for this AWS Account." + "The #{opts[:type].uppercase} contact has not been configured for this AWS Account.", ) return [] if !@api_response || @api_response.empty? end @@ -72,7 +72,7 @@ def initialize(opts = {}) # rubocop:disable Metrics/MethodLength end @raw_data = @api_response.to_h.transform_keys(&:to_s) else - @name, @email_address, @phone_number, @title = "" + @name, @email_address, @phone_number, @title = '' end end @@ -102,13 +102,13 @@ def to_s def fetch_aws_account arn = @aws.sts_client.get_caller_identity({}).arn - arn.split(":")[4] + arn.split(':')[4] end def fetch_aws_alternate_contact(type) @aws .account_client - .get_alternate_contact({ alternate_contact_type: "#{type.upcase}" }) + .get_alternate_contact({ alternate_contact_type: type.upcase.to_s }) .alternate_contact end end diff --git a/libraries/aws_backend.rb b/libraries/aws_backend.rb index 20a7c05c0..1fd6eca6e 100644 --- a/libraries/aws_backend.rb +++ b/libraries/aws_backend.rb @@ -1,69 +1,69 @@ -require "active_support" -require "active_support/core_ext" -require "active_support/core_ext/string" - -require "aws-sdk-autoscaling" -require "aws-sdk-batch" -require "aws-sdk-cloudformation" -require "aws-sdk-cloudfront" -require "aws-sdk-cloudtrail" -require "aws-sdk-cloudwatch" -require "aws-sdk-cloudwatchlogs" -require "aws-sdk-configservice" -require "aws-sdk-core" -require "aws-sdk-dynamodb" -require "aws-sdk-ec2" -require "aws-sdk-ecr" -require "aws-sdk-ecrpublic" -require "aws-sdk-ecs" -require "aws-sdk-eks" -require "aws-sdk-elasticache" -require "aws-sdk-elasticloadbalancing" -require "aws-sdk-elasticloadbalancingv2" -require "aws-sdk-guardduty" -require "aws-sdk-iam" -require "aws-sdk-kms" -require "aws-sdk-lambda" -require "aws-sdk-organizations" -require "aws-sdk-rds" -require "aws-sdk-route53" -require "aws-sdk-s3" -require "aws-sdk-shield" -require "aws-sdk-sns" -require "aws-sdk-sqs" -require "aws-sdk-efs" -require "aws-sdk-ssm" -require "rspec/expectations" -require "aws-sdk-transfer" -require "aws-sdk-elasticsearchservice" -require "aws-sdk-cognitoidentity" -require "aws-sdk-redshift" -require "aws-sdk-athena" -require "aws-sdk-applicationautoscaling" -require "aws-sdk-cognitoidentityprovider" -require "aws-sdk-apigateway" -require "aws-sdk-databasemigrationservice" -require "aws-sdk-route53resolver" -require "aws-sdk-servicecatalog" -require "aws-sdk-glue" -require "aws-sdk-eventbridge" -require "aws-sdk-states" -require "aws-sdk-ram" -require "aws-sdk-secretsmanager" -require "aws-sdk-networkfirewall" -require "aws-sdk-mq" -require "aws-sdk-networkmanager" -require "aws-sdk-signer" -require "aws-sdk-amplify" -require "aws-sdk-simpledb" -require "aws-sdk-emr" -require "aws-sdk-securityhub" -require "aws-sdk-ses" -require "aws-sdk-waf" -require "aws-sdk-synthetics" -require "aws-sdk-apigatewayv2" -require "aws-sdk-account" -require "aws-sdk-accessanalyzer" +require 'active_support' +require 'active_support/core_ext' +require 'active_support/core_ext/string' + +require 'aws-sdk-autoscaling' +require 'aws-sdk-batch' +require 'aws-sdk-cloudformation' +require 'aws-sdk-cloudfront' +require 'aws-sdk-cloudtrail' +require 'aws-sdk-cloudwatch' +require 'aws-sdk-cloudwatchlogs' +require 'aws-sdk-configservice' +require 'aws-sdk-core' +require 'aws-sdk-dynamodb' +require 'aws-sdk-ec2' +require 'aws-sdk-ecr' +require 'aws-sdk-ecrpublic' +require 'aws-sdk-ecs' +require 'aws-sdk-eks' +require 'aws-sdk-elasticache' +require 'aws-sdk-elasticloadbalancing' +require 'aws-sdk-elasticloadbalancingv2' +require 'aws-sdk-guardduty' +require 'aws-sdk-iam' +require 'aws-sdk-kms' +require 'aws-sdk-lambda' +require 'aws-sdk-organizations' +require 'aws-sdk-rds' +require 'aws-sdk-route53' +require 'aws-sdk-s3' +require 'aws-sdk-shield' +require 'aws-sdk-sns' +require 'aws-sdk-sqs' +require 'aws-sdk-efs' +require 'aws-sdk-ssm' +require 'rspec/expectations' +require 'aws-sdk-transfer' +require 'aws-sdk-elasticsearchservice' +require 'aws-sdk-cognitoidentity' +require 'aws-sdk-redshift' +require 'aws-sdk-athena' +require 'aws-sdk-applicationautoscaling' +require 'aws-sdk-cognitoidentityprovider' +require 'aws-sdk-apigateway' +require 'aws-sdk-databasemigrationservice' +require 'aws-sdk-route53resolver' +require 'aws-sdk-servicecatalog' +require 'aws-sdk-glue' +require 'aws-sdk-eventbridge' +require 'aws-sdk-states' +require 'aws-sdk-ram' +require 'aws-sdk-secretsmanager' +require 'aws-sdk-networkfirewall' +require 'aws-sdk-mq' +require 'aws-sdk-networkmanager' +require 'aws-sdk-signer' +require 'aws-sdk-amplify' +require 'aws-sdk-simpledb' +require 'aws-sdk-emr' +require 'aws-sdk-securityhub' +require 'aws-sdk-ses' +require 'aws-sdk-waf' +require 'aws-sdk-synthetics' +require 'aws-sdk-apigatewayv2' +require 'aws-sdk-account' +require 'aws-sdk-accessanalyzer' # AWS Inspec Backend Classes # @@ -374,12 +374,12 @@ def initialize(opts) ] # below allows each resource to optionally and conveniently set max_retries and retry_backoff env_hash = ENV.map { |k, v| [k.downcase, v] }.to_h - opts[:aws_retry_limit] = env_hash["aws_retry_limit"].to_i if !opts[ + opts[:aws_retry_limit] = env_hash['aws_retry_limit'].to_i if !opts[ :aws_retry_limit - ] && env_hash["aws_retry_limit"] - opts[:aws_retry_backoff] = env_hash["aws_retry_backoff"].to_i if !opts[ + ] && env_hash['aws_retry_limit'] + opts[:aws_retry_backoff] = env_hash['aws_retry_backoff'].to_i if !opts[ :aws_retry_backoff - ] && env_hash["aws_retry_backoff"] + ] && env_hash['aws_retry_backoff'] client_args[:client_args][:retry_limit] = opts[:aws_retry_limit] if opts[ :aws_retry_limit ] @@ -402,12 +402,12 @@ def initialize(opts) # here we might want to inject stub data for testing, let's use an option for that return if !defined?(@opts.keys) || !@opts.include?(:stub_data) if !opts[:stub_data].is_a?(Array) - raise ArgumentError, "Expected stub data to be an array" + raise ArgumentError, 'Expected stub data to be an array' end opts[:stub_data].each do |stub| - if !stub.keys.all? { |a| %i[method data client].include?(a) } + if !stub.keys.all? { |a| %i(method data client).include?(a) } raise ArgumentError, - "Expect each stub_data hash to have :client, :method and :data keys" + 'Expect each stub_data hash to have :client, :method and :data keys' end @aws.aws_client(stub[:client]).stub_responses(stub[:method], stub[:data]) end @@ -425,9 +425,9 @@ def validate_parameters(allow: [], required: nil, require_any_of: nil) # rubocop "Expected required parameters as Array of Symbols, got #{required}" end unless @opts.is_a?(Hash) && - required.all? { |req| - @opts.key?(req) && !@opts[req].nil? && @opts[req] != "" - } + required.all? { |req| + @opts.key?(req) && !@opts[req].nil? && @opts[req] != '' + } raise ArgumentError, "#{@__resource_name__}: `#{required}` must be provided" end @@ -436,21 +436,21 @@ def validate_parameters(allow: [], required: nil, require_any_of: nil) # rubocop if require_any_of unless require_any_of.is_a?(Array) && - require_any_of.all? { |r| r.is_a?(Symbol) } + require_any_of.all? { |r| r.is_a?(Symbol) } raise ArgumentError, "Expected required parameters as Array of Symbols, got #{require_any_of}" end unless @opts.is_a?(Hash) && - require_any_of.any? { |req| - @opts.key?(req) && !@opts[req].nil? && @opts[req] != "" - } + require_any_of.any? { |req| + @opts.key?(req) && !@opts[req].nil? && @opts[req] != '' + } raise ArgumentError, "#{@__resource_name__}: One of `#{require_any_of}` must be provided." end allow += require_any_of end - allow += %i[ + allow += %i( client_args stub_data aws_region @@ -458,19 +458,19 @@ def validate_parameters(allow: [], required: nil, require_any_of: nil) # rubocop aws_retry_limit aws_retry_backoff resource_data - ] + ) unless defined?(@opts.keys) - raise ArgumentError, "Scalar arguments not supported" + raise ArgumentError, 'Scalar arguments not supported' end unless @opts.keys.all? { |a| allow.include?(a) } - raise ArgumentError, "Unexpected arguments found" + raise ArgumentError, 'Unexpected arguments found' end unless @opts.values.all? { |a| return true if a.instance_of?(Integer) return true if [TrueClass, FalseClass].include?(a.class) !a.empty? } - raise ArgumentError, "Provided parameter should not be empty" + raise ArgumentError, 'Provided parameter should not be empty' end true end @@ -488,44 +488,44 @@ def tags def name return unless tags - return tags["Name"] if tags.is_a?(Hash) + return tags['Name'] if tags.is_a?(Hash) # tags might be in the original format: [{:key=>"Name", :value=>"aws-linux-ubuntu-vm"}], e.g in EC2 - tags.select { |tag| tag[:key] == "Name" }.first&.dig(:value) + tags.select { |tag| tag[:key] == 'Name' }.first&.dig(:value) end # Intercept AWS exceptions def catch_aws_errors yield # Catch and create custom messages as needed rescue Aws::Errors::MissingCredentialsError - Inspec::Log.error "It appears that you have not set your AWS credentials. See https://www.inspec.io/docs/reference/platforms for details." - fail_resource("No AWS credentials available") + Inspec::Log.error 'It appears that you have not set your AWS credentials. See https://www.inspec.io/docs/reference/platforms for details.' + fail_resource('No AWS credentials available') nil rescue Aws::Account::Errors::ResourceNotFoundException => e - Inspec::Log.warn "#{e.message}" - skip_resource("#{e.message}") + Inspec::Log.warn e.message.to_s + skip_resource(e.message.to_s) nil rescue Aws::AccessAnalyzer::Errors => e - Inspec::Log.warn "#{e.message}" - skip_resource("#{e.message}") + Inspec::Log.warn e.message.to_s + skip_resource(e.message.to_s) nil rescue Aws::Errors::NoSuchEndpointError - Inspec::Log.error "The endpoint that is trying to be accessed does not exist." - fail_resource("Invalid Endpoint error") + Inspec::Log.error 'The endpoint that is trying to be accessed does not exist.' + fail_resource('Invalid Endpoint error') nil rescue Aws::Errors::ServiceError => e if is_permissions_error(e) - advice = "" - error_type = e.class.to_s.split("::").last + advice = '' + error_type = e.class.to_s.split('::').last case error_type - when "InvalidAccessKeyId" - advice = "Please ensure your AWS Access Key ID is set correctly." - when "InvalidClientTokenId" + when 'InvalidAccessKeyId' + advice = 'Please ensure your AWS Access Key ID is set correctly.' + when 'InvalidClientTokenId' advice = - "Please ensure that the aws access key, aws secret access key, and the aws session token are correct." - when "AccessDenied" + 'Please ensure that the aws access key, aws secret access key, and the aws session token are correct.' + when 'AccessDenied' advice = - "Please check the IAM permissions required for this Resource in the documentation, " \ - "and ensure your Service Principal has these permissions set." + 'Please check the IAM permissions required for this Resource in the documentation, ' \ + 'and ensure your Service Principal has these permissions set.' end error_message = "#{e.message}: #{advice}" @@ -533,7 +533,7 @@ def catch_aws_errors else Inspec::Log.warn "AWS Service Error encountered running a control with Resource #{@__resource_name__}. " \ "Error message: #{e.message} You should address this error to ensure your controls are " \ - "behaving as expected." + 'behaving as expected.' @failed_resource = true end nil @@ -583,8 +583,8 @@ def respond_to_missing?(*several_variants) def resource_fail(message = nil) message ||= "#{@__resource_name__}: #{@display_name}. Multiple AWS resources were returned for the provided criteria. " \ - "If you wish to test multiple entities, please use the plural resource. " \ - "Otherwise, please provide more specific criteria to lookup the resource." + 'If you wish to test multiple entities, please use the plural resource. ' \ + 'Otherwise, please provide more specific criteria to lookup the resource.' # Fail resource in resource pack. `exists?` method will return `false`. @failed_resource = true # Fail resource in InSpec core. Tests in InSpec profile will return the message. @@ -620,7 +620,7 @@ def self.populate_filter_table(raw_data, table_scheme) def fetch(client:, operation:, kwargs: {}) unless @aws.respond_to?(client) - raise ArgumentError, "Valid Client not found!" + raise ArgumentError, 'Valid Client not found!' end client_obj = @aws.send(client) @@ -663,13 +663,13 @@ def create_methods(object, data) data.instance_variables.each do |var| create_method( object, - var.to_s.delete("@"), - data.instance_variable_get(var) + var.to_s.delete('@'), + data.instance_variable_get(var), ) end # When the data is a Hash object iterate around each of the key value pairs and # create a method for each one. - when "Hash" + when 'Hash' data.each { |key, value| create_method(object, key, value) } end end @@ -685,11 +685,11 @@ def create_method(object, name, value) # Create the necessary method based on the var that has been passed # Test the value for its type so that the method can be setup correctly case value.class.to_s - when "String", "Integer", "TrueClass", "FalseClass", "Fixnum", "Time" + when 'String', 'Integer', 'TrueClass', 'FalseClass', 'Fixnum', 'Time' object.define_singleton_method name do value end - when "Hash" + when 'Hash' if value.count == 0 return_value = value else @@ -704,7 +704,7 @@ def create_method(object, name, value) value = value.to_h if value.respond_to? :to_h AwsResourceProbe.new(value) end - when "Array" + when 'Array' # Some things are just string or integer arrays # Check this by seeing if the first element is a string / integer / boolean or # a hashtable @@ -712,7 +712,7 @@ def create_method(object, name, value) # the quickest test # p value[0].class.to_s case value[0].class.to_s - when "String", "Integer", "TrueClass", "FalseClass", "Fixnum", "Time" + when 'String', 'Integer', 'TrueClass', 'FalseClass', 'Fixnum', 'Time' probes = value else if name.eql?(:tags) @@ -773,10 +773,10 @@ def initialize(item) # Hash: Key=>Value pair to look for in the @item property def include?(opt) unless opt.is_a?(Symbol) || opt.is_a?(Hash) || opt.is_a?(String) - raise ArgumentError, "Key or Key:Value pair should be provided." + raise ArgumentError, 'Key or Key:Value pair should be provided.' end if opt.is_a?(Hash) - raise ArgumentError, "Only one item can be provided" if opt.keys.size > 1 + raise ArgumentError, 'Only one item can be provided' if opt.keys.size > 1 return @item[opt.keys.first] == opt.values.first end @item.key?(opt.to_sym) diff --git a/libraries/aws_iam_root_user.rb b/libraries/aws_iam_root_user.rb index b012eb5df..1cd721394 100644 --- a/libraries/aws_iam_root_user.rb +++ b/libraries/aws_iam_root_user.rb @@ -1,8 +1,8 @@ -require "aws_backend" +require 'aws_backend' class AwsIamRootUser < AwsResourceBase - name "aws_iam_root_user" - desc "Verifies settings for AWS Root Account." + name 'aws_iam_root_user' + desc 'Verifies settings for AWS Root Account.' example " describe aws_iam_root_user do it { should have_access_key } @@ -27,11 +27,11 @@ def resource_id end def has_access_key? - @summary_account["AccountAccessKeysPresent"] == 1 + @summary_account['AccountAccessKeysPresent'] == 1 end def has_mfa_enabled? - @summary_account["AccountMFAEnabled"] == 1 + @summary_account['AccountMFAEnabled'] == 1 end def has_hardware_mfa_enabled? @@ -40,7 +40,7 @@ def has_hardware_mfa_enabled? # Virtual MFA devices have suffix 'root-account-mfa-device' def has_virtual_mfa_enabled? - virtual_mfa_suffix = "root-account-mfa-device" + virtual_mfa_suffix = 'root-account-mfa-device' @virtual_devices.any? { |device| device[:serial_number].end_with?(virtual_mfa_suffix) } end @@ -49,6 +49,6 @@ def exists? end def to_s - "AWS Root-User" + 'AWS Root-User' end end From e732d6f38825fe288fa21ffb9254b03d9a5f2416 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Wed, 22 Nov 2023 23:28:26 -0500 Subject: [PATCH 22/93] fixed formating of non-interpolated strings Signed-off-by: Aaron Lippold --- libraries/aws_billing_contact.rb | 30 +++++++++++------------ libraries/aws_iam_access_analyzers.rb | 34 +++++++++++++-------------- libraries/aws_operations_contact.rb | 30 +++++++++++------------ libraries/aws_primary_contact.rb | 14 +++++------ libraries/aws_region.rb | 8 +++---- libraries/aws_regions.rb | 12 +++++----- libraries/aws_security_contact.rb | 30 +++++++++++------------ 7 files changed, 79 insertions(+), 79 deletions(-) diff --git a/libraries/aws_billing_contact.rb b/libraries/aws_billing_contact.rb index 8ab6c8124..f9919e770 100644 --- a/libraries/aws_billing_contact.rb +++ b/libraries/aws_billing_contact.rb @@ -1,15 +1,15 @@ -require "aws_backend" +require 'aws_backend' class AwsBillingAccount < AwsResourceBase - name "aws_billing_contact" - desc "Verifies the billing contact information for an AWS Account." + name 'aws_billing_contact' + desc 'Verifies the billing contact information for an AWS Account.' example <<~EXAMPLE - describe aws_billing_account do - it { should be_configured } - its('name') { should cmp 'John Smith' } - its('email_address') { should cmp 'jsmith@acme.com' } - end - EXAMPLE + describe aws_billing_account do + it { should be_configured } + its('name') { should cmp 'John Smith' } + its('email_address') { should cmp 'jsmith@acme.com' } + end + EXAMPLE attr_reader :raw_data, :api, @@ -27,10 +27,10 @@ def initialize(opts = {}) validate_parameters begin catch_aws_errors { @aws_account_id = fetch_aws_account } - @api_response = fetch_aws_alternate_contact("billing") + @api_response = fetch_aws_alternate_contact('billing') rescue Aws::Account::Errors::ResourceNotFoundException skip_resource( - "The Billing contact has not been configured for this AWS Account." + 'The Billing contact has not been configured for this AWS Account.', ) return [] if !@api_response || @api_response.empty? end @@ -56,7 +56,7 @@ def resource_id if @aws_account_id "AWS Billing Contact for account: #{@aws_account_id}" else - "AWS Billing Contact Information" + 'AWS Billing Contact Information' end end @@ -64,7 +64,7 @@ def to_s if @aws_account_id "AWS Billing Contact for account: #{@aws_account_id}" else - "AWS Account Primary Contact" + 'AWS Account Primary Contact' end end @@ -72,13 +72,13 @@ def to_s def fetch_aws_account arn = @aws.sts_client.get_caller_identity({}).arn - arn.split(":")[4] + arn.split(':')[4] end def fetch_aws_alternate_contact(type) @aws .account_client - .get_alternate_contact({ alternate_contact_type: "#{type.upcase}" }) + .get_alternate_contact({ alternate_contact_type: type.upcase.to_s }) .alternate_contact end end diff --git a/libraries/aws_iam_access_analyzers.rb b/libraries/aws_iam_access_analyzers.rb index 878d462a7..828b488a3 100644 --- a/libraries/aws_iam_access_analyzers.rb +++ b/libraries/aws_iam_access_analyzers.rb @@ -1,9 +1,9 @@ -require "aws_backend" -require "pry" +require 'aws_backend' +require 'pry' class AwsIamAccessAnalyzer < AwsResourceBase - name "aws_iam_access_analyzers" - desc "Verifies settings for a collection AWS IAM Access Analyzers." + name 'aws_iam_access_analyzers' + desc 'Verifies settings for a collection AWS IAM Access Analyzers.' example <<~EXAMPLE1 # retrieve both 'account' and 'organization' analyzers describe aws_iam_access_analyzers do @@ -43,7 +43,7 @@ class AwsIamAccessAnalyzer < AwsResourceBase def initialize(opts = {}) @raw_data = [] @api_response = nil - @supported_opts_values = %w[account organization all] + @supported_opts_values = %w{account organization all} opts = { type: opts } if opts.is_a?(String) unless (opts.values - @supported_opts_values).empty? || opts.nil? @@ -51,7 +51,7 @@ def initialize(opts = {}) "Unsupported options '#{opts.values - @supported_opts_values}'. Supported key(s): #{@supported_opts_values}" end super(opts) - validate_parameters(allow: %i[type aws_region]) + validate_parameters(allow: %i(type aws_region)) parameters = {} parameters[:type] = opts[:type].upcase if opts[:type] @aws.access_analyzer_client.config.region = opts[:aws_region] if opts[ @@ -64,10 +64,10 @@ def fetch_data(parameters) analyzer_rows = [] catch_aws_errors do catch_aws_errors { @aws_account_id = fetch_aws_account } - if parameters.empty? || parameters[:type] == "ALL" + if parameters.empty? || parameters[:type] == 'ALL' @api_response = @aws.access_analyzer_client.list_analyzers - elsif parameters[:type] == "ACCOUNT" || - parameters[:type] == "ORGANIZATION" + elsif parameters[:type] == 'ACCOUNT' || + parameters[:type] == 'ORGANIZATION' @api_response = @aws.access_analyzer_client.list_analyzers(parameters) end @@ -80,11 +80,11 @@ def fetch_data(parameters) created_at: aa.created_at, last_resource_analyzed: aa.last_resource_analyzed, last_resource_analyzed_at: aa.last_resource_analyzed_at, - #TODO: Flatten the hash of tags? + # TODO: Flatten the hash of tags? tags: aa.tags, status: aa.status, - status_reason: aa.status_reason - } + status_reason: aa.status_reason, + }, ] end @raw_data = @@ -94,8 +94,8 @@ def fetch_data(parameters) end def resource_id - response = "AWS IAM " - opts[:type] ? response += "#{opts[:type].capitalize} " : "" + response = 'AWS IAM ' + opts[:type] ? response += "#{opts[:type].capitalize} " : '' if @aws_account_id response += "Account Analyzer for #{@aws_account_id} in #{get_current_region}" @@ -106,8 +106,8 @@ def resource_id end def to_s - response = "AWS IAM " - opts[:type] ? response += "#{opts[:type].capitalize} " : "" + response = 'AWS IAM ' + opts[:type] ? response += "#{opts[:type].capitalize} " : '' if @aws_account_id response += "Account Analyzer for #{@aws_account_id} in #{get_current_region}" @@ -121,7 +121,7 @@ def to_s def fetch_aws_account arn = @aws.sts_client.get_caller_identity({}).arn - arn.split(":")[4] + arn.split(':')[4] end def get_current_region diff --git a/libraries/aws_operations_contact.rb b/libraries/aws_operations_contact.rb index 07b98791a..6d93c5ef9 100644 --- a/libraries/aws_operations_contact.rb +++ b/libraries/aws_operations_contact.rb @@ -1,15 +1,15 @@ -require "aws_backend" +require 'aws_backend' class AwsOperationsAccount < AwsResourceBase - name "aws_operations_contact" - desc "Verifies the operations contact information for an AWS Account." + name 'aws_operations_contact' + desc 'Verifies the operations contact information for an AWS Account.' example <<~EXAMPLE - describe aws_operations_account do - it { should be_configured } - its('name') { should cmp 'John Smith' } - its('email_address') { should cmp 'jsmith@acme.com' } - end - EXAMPLE + describe aws_operations_account do + it { should be_configured } + its('name') { should cmp 'John Smith' } + its('email_address') { should cmp 'jsmith@acme.com' } + end + EXAMPLE attr_reader :raw_data, :api, @@ -27,10 +27,10 @@ def initialize(opts = {}) validate_parameters begin catch_aws_errors { @aws_account_id = fetch_aws_account } - @api_response = fetch_aws_alternate_contact("operations") + @api_response = fetch_aws_alternate_contact('operations') rescue Aws::Account::Errors::ResourceNotFoundException skip_resource( - "The Operations contact has not been configured for this AWS Account." + 'The Operations contact has not been configured for this AWS Account.', ) return [] if !@api_response || @api_response.empty? end @@ -56,7 +56,7 @@ def resource_id if @aws_account_id "AWS Operations Contact for account: #{@aws_account_id}" else - "AWS Operations Contact Information" + 'AWS Operations Contact Information' end end @@ -64,7 +64,7 @@ def to_s if @aws_account_id "AWS Operations Contact for account: #{@aws_account_id}" else - "AWS Account Primary Contact" + 'AWS Account Primary Contact' end end @@ -72,13 +72,13 @@ def to_s def fetch_aws_account arn = @aws.sts_client.get_caller_identity({}).arn - arn.split(":")[4] + arn.split(':')[4] end def fetch_aws_alternate_contact(type) @aws .account_client - .get_alternate_contact({ alternate_contact_type: "#{type.upcase}" }) + .get_alternate_contact({ alternate_contact_type: type.upcase.to_s }) .alternate_contact end end diff --git a/libraries/aws_primary_contact.rb b/libraries/aws_primary_contact.rb index a07ef9e18..a6cab3130 100644 --- a/libraries/aws_primary_contact.rb +++ b/libraries/aws_primary_contact.rb @@ -1,8 +1,8 @@ -require "aws_backend" +require 'aws_backend' class AwsPrimaryAccount < AwsResourceBase - name "aws_primary_contact" - desc "Verifies the primary contact information for an AWS Account." + name 'aws_primary_contact' + desc 'Verifies the primary contact information for an AWS Account.' example <<~EXAMPLE describe aws_primary_contact do it { should be_configured } @@ -50,7 +50,7 @@ def initialize(opts = {}) @aws.account_client.get_contact_information.contact_information rescue Aws::Account::Errors::ResourceNotFoundException skip_resource( - "The Primary contact has not been configured for this AWS Account." + 'The Primary contact has not been configured for this AWS Account.', ) return [] if !@api_response || @api_response.empty? end @@ -76,7 +76,7 @@ def resource_id if @aws_account_id "AWS Primary Contact for account: #{@aws_account_id}" else - "AWS Account Primary Contact Information" + 'AWS Account Primary Contact Information' end end @@ -84,7 +84,7 @@ def to_s if @aws_account_id "AWS Primary Contact for account: #{@aws_account_id}" else - "AWS Account Primary Contact" + 'AWS Account Primary Contact' end end @@ -92,6 +92,6 @@ def to_s def fetch_aws_account arn = @aws.sts_client.get_caller_identity({}).arn - arn.split(":")[4] + arn.split(':')[4] end end diff --git a/libraries/aws_region.rb b/libraries/aws_region.rb index 725a353c0..e145d2685 100644 --- a/libraries/aws_region.rb +++ b/libraries/aws_region.rb @@ -1,9 +1,9 @@ -require "aws_backend" -require "pry" +require 'aws_backend' +require 'pry' class AwsRegion < AwsResourceBase - name "aws_region" - desc "Verifies settings for an AWS region." + name 'aws_region' + desc 'Verifies settings for an AWS region.' example " describe aws_region('eu-west-2') do diff --git a/libraries/aws_regions.rb b/libraries/aws_regions.rb index bc837194c..61bc972ce 100644 --- a/libraries/aws_regions.rb +++ b/libraries/aws_regions.rb @@ -1,8 +1,8 @@ -require "aws_backend" +require 'aws_backend' class AwsRegions < AwsResourceBase - name "aws_regions" - desc "Verifies settings for AWS Regions in bulk." + name 'aws_regions' + desc 'Verifies settings for AWS Regions in bulk.' example " describe aws_regions do @@ -32,7 +32,7 @@ def fetch_data @regions = @aws.compute_client.describe_regions.to_h[:regions] end return [] if !@regions || @regions.empty? - region_opt_status = "" + region_opt_status = '' @regions.each do |region| catch_aws_errors do region_opt_status = fetch_region_opt_status(region[:region_name]) @@ -42,8 +42,8 @@ def fetch_data region_name: region[:region_name], endpoint: region[:endpoint], opt_in_status: region[:opt_in_status], - region_opt_status: region_opt_status - } + region_opt_status: region_opt_status, + }, ] end @table = region_rows diff --git a/libraries/aws_security_contact.rb b/libraries/aws_security_contact.rb index bae3472a8..c92cfde7d 100644 --- a/libraries/aws_security_contact.rb +++ b/libraries/aws_security_contact.rb @@ -1,15 +1,15 @@ -require "aws_backend" +require 'aws_backend' class AwsSecurityAccount < AwsResourceBase - name "aws_security_contact" - desc "Verifies the security contact information for an AWS Account." + name 'aws_security_contact' + desc 'Verifies the security contact information for an AWS Account.' example <<~EXAMPLE - describe aws_security_account do - it { should be_configured } - its('name') { should cmp 'John Smith' } - its('email_address') { should cmp 'jsmith@acme.com' } - end - EXAMPLE + describe aws_security_account do + it { should be_configured } + its('name') { should cmp 'John Smith' } + its('email_address') { should cmp 'jsmith@acme.com' } + end + EXAMPLE attr_reader :raw_data, :api, @@ -27,10 +27,10 @@ def initialize(opts = {}) validate_parameters begin catch_aws_errors { @aws_account_id = fetch_aws_account } - @api_response = fetch_aws_alternate_contact("security") + @api_response = fetch_aws_alternate_contact('security') rescue Aws::Account::Errors::ResourceNotFoundException skip_resource( - "The Security contact has not been configured for this AWS Account." + 'The Security contact has not been configured for this AWS Account.', ) return [] if !@api_response || @api_response.empty? end @@ -56,7 +56,7 @@ def resource_id if @aws_account_id "AWS Security Contact for account: #{@aws_account_id}" else - "AWS Security Contact Information" + 'AWS Security Contact Information' end end @@ -64,7 +64,7 @@ def to_s if @aws_account_id "AWS Security Contact for account: #{@aws_account_id}" else - "AWS Account Primary Contact" + 'AWS Account Primary Contact' end end @@ -72,13 +72,13 @@ def to_s def fetch_aws_account arn = @aws.sts_client.get_caller_identity({}).arn - arn.split(":")[4] + arn.split(':')[4] end def fetch_aws_alternate_contact(type) @aws .account_client - .get_alternate_contact({ alternate_contact_type: "#{type.upcase}" }) + .get_alternate_contact({ alternate_contact_type: type.upcase.to_s }) .alternate_contact end end From 8a987986fe73879ab1c8c5051ee8410ca415f4ef Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Fri, 24 Nov 2023 14:13:59 -0500 Subject: [PATCH 23/93] Minor fixes - removed unneeded aws_region update of clint args - made feedback on allowed account types more direct - failed fast on param errors Signed-off-by: Aaron Lippold --- libraries/aws_iam_access_analyzers.rb | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/libraries/aws_iam_access_analyzers.rb b/libraries/aws_iam_access_analyzers.rb index 828b488a3..034fb4c78 100644 --- a/libraries/aws_iam_access_analyzers.rb +++ b/libraries/aws_iam_access_analyzers.rb @@ -42,26 +42,22 @@ class AwsIamAccessAnalyzer < AwsResourceBase def initialize(opts = {}) @raw_data = [] + parameters = {} @api_response = nil @supported_opts_values = %w{account organization all} - opts = { type: opts } if opts.is_a?(String) - unless (opts.values - @supported_opts_values).empty? || opts.nil? - raise ArgumentError, - "Unsupported options '#{opts.values - @supported_opts_values}'. Supported key(s): #{@supported_opts_values}" - end super(opts) validate_parameters(allow: %i(type aws_region)) - parameters = {} parameters[:type] = opts[:type].upcase if opts[:type] - @aws.access_analyzer_client.config.region = opts[:aws_region] if opts[ - :aws_region - ] + unless @supported_opts_values.map(&:upcase).include?(parameters[:type]) || parameters[:type].nil? + raise ArgumentError, "Unsupported Account Type: '#{parameters[:type].downcase}'. Supported account types: #{@supported_opts_values}" + end @table = fetch_data(parameters) end def fetch_data(parameters) analyzer_rows = [] + catch_aws_errors do catch_aws_errors { @aws_account_id = fetch_aws_account } if parameters.empty? || parameters[:type] == 'ALL' From 7106b0728cede790da49a68c68b048bb43423325 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Fri, 24 Nov 2023 14:17:48 -0500 Subject: [PATCH 24/93] removed commented lines Signed-off-by: Aaron Lippold --- libraries/aws_iam_access_analyzers.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/aws_iam_access_analyzers.rb b/libraries/aws_iam_access_analyzers.rb index 034fb4c78..73bf4daac 100644 --- a/libraries/aws_iam_access_analyzers.rb +++ b/libraries/aws_iam_access_analyzers.rb @@ -76,7 +76,6 @@ def fetch_data(parameters) created_at: aa.created_at, last_resource_analyzed: aa.last_resource_analyzed, last_resource_analyzed_at: aa.last_resource_analyzed_at, - # TODO: Flatten the hash of tags? tags: aa.tags, status: aa.status, status_reason: aa.status_reason, From e0778ce65c0c96d6562ce84a7769fed1c298c7f9 Mon Sep 17 00:00:00 2001 From: wdower Date: Tue, 28 Nov 2023 13:41:12 -0500 Subject: [PATCH 25/93] adding versioning hashie mash to aws_s3_bucket for convenience Signed-off-by: wdower --- libraries/aws_s3_bucket.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libraries/aws_s3_bucket.rb b/libraries/aws_s3_bucket.rb index 53b0ae6a0..82705a9d8 100644 --- a/libraries/aws_s3_bucket.rb +++ b/libraries/aws_s3_bucket.rb @@ -101,6 +101,13 @@ def has_versioning_enabled? end end + def versioning + return [] unless exists? # exists? would throw the same NoSuchBucket error if the bucket name was not valid + catch_aws_errors do + @versioning ||= Hashie::Mash.new(@aws.storage_client.get_bucket_versioning(bucket: @bucket_name)) + end + end + def has_secure_transport_enabled? bucket_policy.any? { |s| s.effect == "Deny" && s.condition && s.condition["Bool"] && s.condition["Bool"]["aws:SecureTransport"] && s.condition["Bool"]["aws:SecureTransport"] == "false" } end From 74b57482c1180b33c0c865992ab1fafadb4bea47 Mon Sep 17 00:00:00 2001 From: wdower Date: Tue, 28 Nov 2023 13:41:37 -0500 Subject: [PATCH 26/93] adding missing hashie require Signed-off-by: wdower --- libraries/aws_s3_bucket.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/aws_s3_bucket.rb b/libraries/aws_s3_bucket.rb index 82705a9d8..38f944459 100644 --- a/libraries/aws_s3_bucket.rb +++ b/libraries/aws_s3_bucket.rb @@ -1,4 +1,5 @@ require "aws_backend" +require "hashie/mash" class AwsS3Bucket < AwsResourceBase name "aws_s3_bucket" From c0d4916552e3cfd64c6b70a1f2a1196f04fef8d9 Mon Sep 17 00:00:00 2001 From: wdower Date: Tue, 28 Nov 2023 15:44:34 -0500 Subject: [PATCH 27/93] exposing versioning as an attr Signed-off-by: wdower --- libraries/aws_s3_bucket.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/aws_s3_bucket.rb b/libraries/aws_s3_bucket.rb index 38f944459..45f45bc90 100644 --- a/libraries/aws_s3_bucket.rb +++ b/libraries/aws_s3_bucket.rb @@ -10,7 +10,7 @@ class AwsS3Bucket < AwsResourceBase end " - attr_reader :region, :bucket_name + attr_reader :region, :bucket_name, :versioning def initialize(opts = {}) opts = { bucket_name: opts } if opts.is_a?(String) From 3bb87dff62fd0f432749f69def04b80865787d9f Mon Sep 17 00:00:00 2001 From: wdower Date: Wed, 29 Nov 2023 12:05:53 -0500 Subject: [PATCH 28/93] fixing gemfile, adding a public? method to rds resource Signed-off-by: wdower --- Gemfile | 1 + libraries/aws_rds_instance.rb | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/Gemfile b/Gemfile index 98538b59a..1db5f9788 100644 --- a/Gemfile +++ b/Gemfile @@ -14,6 +14,7 @@ gem "bundle" gem "inspec-bin" gem "aws-sdk-accessanalyzer" gem "aws-partitions" +gem "train-kubernetes" gem "rubocop", "~> 1.25.1", require: false diff --git a/libraries/aws_rds_instance.rb b/libraries/aws_rds_instance.rb index b233dec82..30c8b8871 100644 --- a/libraries/aws_rds_instance.rb +++ b/libraries/aws_rds_instance.rb @@ -26,6 +26,10 @@ def resource_id "#{@rds_instance? @rds_instance[:db_instance_identifier]: ""}_#{@rds_instance? @rds_instance[:db_name]: ""}_#{@rds_instance? @rds_instance[:master_username]: ""}" end + def public? + @rds_instance[:publicly_accessible] + end + def has_encrypted_storage? @rds_instance[:storage_encrypted] end From 5660ba59044c2ef5f99833152f5d7075932264ff Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Wed, 29 Nov 2023 17:50:45 -0500 Subject: [PATCH 29/93] debugging null case for aws return with no rds instances Signed-off-by: Aaron Lippold --- libraries/aws_backend.rb | 4 +++- libraries/aws_rds_instances.rb | 13 ++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/libraries/aws_backend.rb b/libraries/aws_backend.rb index 1fd6eca6e..85d20669b 100644 --- a/libraries/aws_backend.rb +++ b/libraries/aws_backend.rb @@ -634,7 +634,9 @@ def fetch(client:, operation:, kwargs: {}) private def populate_filter_table_from_response - return unless @table.present? + require 'pry-byebug' + # return unless @table.present? + return [] if @table.empty? table_schema = @table.first.keys.map do |key| diff --git a/libraries/aws_rds_instances.rb b/libraries/aws_rds_instances.rb index 0b8fa576a..55b707220 100644 --- a/libraries/aws_rds_instances.rb +++ b/libraries/aws_rds_instances.rb @@ -1,8 +1,8 @@ -require "aws_backend" +require 'aws_backend' class AwsRdsInstances < AwsCollectionResourceBase - name "aws_rds_instances" - desc "Verifies settings for AWS RDS instances in bulk." + name 'aws_rds_instances' + desc 'Verifies settings for AWS RDS instances in bulk.' example " describe aws_rds_instances do it { should exist } @@ -24,8 +24,11 @@ class AwsRdsInstances < AwsCollectionResourceBase def initialize(opts = {}) super(opts) validate_parameters - @table = fetch(client: :rds_client, operation: :describe_db_instances).db_instances.map(&:to_h) - + catch_aws_errors do + @table = fetch(client: :rds_client, operation: :describe_db_instances).db_instances.map(&:to_h) + end + return [] unless @table.present? + populate_filter_table_from_response end end From 5af3282ec0515ffe92a2052cb0395918669b6245 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Wed, 29 Nov 2023 19:22:45 -0500 Subject: [PATCH 30/93] returning the resource to standard sans the updated exist function Signed-off-by: Aaron Lippold --- libraries/aws_backend.rb | 4 +--- libraries/aws_rds_instances.rb | 13 ++++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/libraries/aws_backend.rb b/libraries/aws_backend.rb index 85d20669b..1fd6eca6e 100644 --- a/libraries/aws_backend.rb +++ b/libraries/aws_backend.rb @@ -634,9 +634,7 @@ def fetch(client:, operation:, kwargs: {}) private def populate_filter_table_from_response - require 'pry-byebug' - # return unless @table.present? - return [] if @table.empty? + return unless @table.present? table_schema = @table.first.keys.map do |key| diff --git a/libraries/aws_rds_instances.rb b/libraries/aws_rds_instances.rb index 55b707220..a3f47cdd3 100644 --- a/libraries/aws_rds_instances.rb +++ b/libraries/aws_rds_instances.rb @@ -21,14 +21,17 @@ class AwsRdsInstances < AwsCollectionResourceBase end " + attr_reader :table + def initialize(opts = {}) super(opts) validate_parameters - catch_aws_errors do - @table = fetch(client: :rds_client, operation: :describe_db_instances).db_instances.map(&:to_h) - end - return [] unless @table.present? - + @table = fetch(client: :rds_client, operation: :describe_db_instances).db_instances.map(&:to_h) + populate_filter_table_from_response end + + def exist? + !@table.empty? + end end From 1ec68a9e31d3f3a0ea9cbea5a539b06e20bc9fc0 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Fri, 1 Dec 2023 16:26:49 -0500 Subject: [PATCH 31/93] added general securityhub resource Signed-off-by: Aaron Lippold --- libraries/aws_backend.rb | 4 ++++ libraries/aws_securityhub.rb | 41 ++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 libraries/aws_securityhub.rb diff --git a/libraries/aws_backend.rb b/libraries/aws_backend.rb index 1fd6eca6e..4e326b1d8 100644 --- a/libraries/aws_backend.rb +++ b/libraries/aws_backend.rb @@ -508,6 +508,10 @@ def catch_aws_errors Inspec::Log.warn e.message.to_s skip_resource(e.message.to_s) nil + rescue Aws::SecurityHub::Errors::InvalidAccessException => e + Inspec::Log.warn("#{e.message} in region: #{opts[:aws_region]}") + #skip_resource(e.message.to_s) + nil rescue Aws::Errors::NoSuchEndpointError Inspec::Log.error 'The endpoint that is trying to be accessed does not exist.' fail_resource('Invalid Endpoint error') diff --git a/libraries/aws_securityhub.rb b/libraries/aws_securityhub.rb new file mode 100644 index 000000000..8338e5e9f --- /dev/null +++ b/libraries/aws_securityhub.rb @@ -0,0 +1,41 @@ +require "aws_backend" + +class AWSSecurityHub < AwsResourceBase + name "aws_securityhub" + desc "Gets information about the Security Hub." + + example " + describe aws_securityhub do + it { should be_subscribed } + end + " + + attr_reader :describe_hub, :res, :hub_arn, :subscribed_at, :auto_enable_controls, :control_finding_generator + + def initialize(opts = {}) + @raw_data = {} + @res = {} + @describe_hub = [] + super(opts) + validate_parameters() + catch_aws_errors do + @describe_hub = @aws.securityhub_client.describe_hub + @res = @describe_hub.to_h.presence || {} + create_resource_methods(@res) unless @res.nil? + end + end + + def subscribed? + @res[:subscribed_at].present? || !@res.empty? + end + + alias exists? subscribed? + + def resource_id + @res[:hub_arn].presence || '' + end + + def to_s + "Security Hub: #{resource_id}" + end +end From 6e68bc0d2ea09904b7f85441123f5d63cfe61d1d Mon Sep 17 00:00:00 2001 From: wdower Date: Fri, 1 Dec 2023 16:31:12 -0500 Subject: [PATCH 32/93] adding event_selectors attr to cloudtrail Signed-off-by: wdower --- libraries/aws_cloudtrail_trail.rb | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/libraries/aws_cloudtrail_trail.rb b/libraries/aws_cloudtrail_trail.rb index bb55764c8..2452cb4d8 100644 --- a/libraries/aws_cloudtrail_trail.rb +++ b/libraries/aws_cloudtrail_trail.rb @@ -11,7 +11,7 @@ class AwsCloudTrailTrail < AwsResourceBase attr_reader :cloud_watch_logs_log_group_arn, :cloud_watch_logs_role_arn, :home_region, :trail_name, :kms_key_id, :s3_bucket_name, :s3_key_prefix, :trail_arn, :is_multi_region_trail, - :log_file_validation_enabled, :is_organization_trail + :log_file_validation_enabled, :is_organization_trail, :event_selectors alias multi_region_trail? is_multi_region_trail alias log_file_validation_enabled? log_file_validation_enabled @@ -77,12 +77,18 @@ def get_log_group_for_multi_region_active_mgmt_rw_all return @cloud_watch_logs_log_group_arn.split(":")[6] if has_event_selector_mgmt_events_rw_type_all? && logging? end + # TODO: see what happens when running against nil event selectors + def event_selectors + catch_aws_errors do + @event_selectors = @aws.cloudtrail_client.get_event_selectors(trail_name: @trail_name) + end + end + def has_event_selector_mgmt_events_rw_type_all? return nil unless exists? event_selector_found = false begin - event_selectors = @aws.cloudtrail_client.get_event_selectors(trail_name: @trail_name) - event_selectors.event_selectors.each do |es| + @event_selectors.event_selectors.each do |es| event_selector_found = true if es.read_write_type == "All" && es.include_management_events == true end rescue Aws::CloudTrail::Errors::TrailNotFoundException From 317dac0aec5640b8010693adef06244c10f31e07 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Fri, 1 Dec 2023 16:36:59 -0500 Subject: [PATCH 33/93] added docs for the aws_securityhub resource and added extra alias' Signed-off-by: Aaron Lippold --- .../inspec/resources/aws_securityhub.md | 114 ++++++++++++++++++ libraries/aws_securityhub.rb | 1 + 2 files changed, 115 insertions(+) create mode 100644 docs-chef-io/content/inspec/resources/aws_securityhub.md diff --git a/docs-chef-io/content/inspec/resources/aws_securityhub.md b/docs-chef-io/content/inspec/resources/aws_securityhub.md new file mode 100644 index 000000000..61741f682 --- /dev/null +++ b/docs-chef-io/content/inspec/resources/aws_securityhub.md @@ -0,0 +1,114 @@ ++++ +title = "aws_securityhub Resource" +platform = "aws" +draft = false +gh_repo = "inspec-aws" + +[menu.inspec] +title = "aws_securityhub" +identifier = "inspec/resources/aws/aws_securityhub Resource" +parent = "inspec/resources/aws" ++++ + +Use the `aws_securityhub` InSpec audit resource to test properties of a single AWS Security Hub. + +For additional information, including details on parameters and properties, see the [AWS documentation on AWS Security Hub](https://docs.aws.amazon.com/securityhub/1.0/APIReference/API_DescribeHub.html). + +## Installation + +{{% inspec_aws_install %}} + +## Syntax + +Ensure that the hub exists. + +```ruby +describe aws_securityhub_hub do + it { should be_subscribed } +end +``` + +## Parameters + +`aws_region` _(required)_ + +: The region of the Hub resource that was retrieved. + +## Properties + +`hub_arn` +: The ARN of the Hub resource that was retrieved. + +`subscribed_at` +: The date and time when Security Hub was enabled in the account. + +`auto_enable_controls` +: Whether to automatically enable new controls when they are added to standards that are enabled. + +## Examples + +**Ensure an auto enable controls is true.** + +```ruby +describe aws_securityhub do + it { should exist } + its('auto_enable_controls') { should eq true } +end +``` + +**Ensure a hub ARN is available.** + +```ruby +describe aws_securityhub_hub do + it { should be_subscribed } + its('hub_arn') { should eq 'HUB_ARN' } +end +``` + +## Matchers + +{{% inspec_matchers_link %}} + +The controls will pass if the `describe` method returns at least one result. + +### exist + +Use `should` to test that the entity exists. + +```ruby +describe aws_securityhub_hub(hub_arn: 'HUB_ARN') do + it { should exist } +end +``` + +Use `should_not` to test the entity does not exist. + +```ruby +describe aws_securityhub_hub(hub_arn: 'HUB_ARN') do + it { should_not exist } +end +``` + +### subscribed + +Use `should` to test that security hub is scribed in us-east-1. + +```ruby +describe aws_securityhub_hub(aws_region: 'us-east-1' do + it { should be_subscribed } +end +``` + +### be_available + +Use `should` to check if the entity is available. + +```ruby +describe aws_securityhub_hub(hub_arn: 'HUB_ARN') do + it { should be_available } +end +``` + +## AWS Permissions + +{{% aws_permissions_principal action="SecurityHub:Client:DescribeHubResponse" %}} diff --git a/libraries/aws_securityhub.rb b/libraries/aws_securityhub.rb index 8338e5e9f..e7707e4ca 100644 --- a/libraries/aws_securityhub.rb +++ b/libraries/aws_securityhub.rb @@ -30,6 +30,7 @@ def subscribed? end alias exists? subscribed? + alias exist? subscribed? def resource_id @res[:hub_arn].presence || '' From ec13a0c1a5baf9f81765dc42ad10864d651a0045 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Sun, 3 Dec 2023 00:48:28 -0500 Subject: [PATCH 34/93] - added gemspec-util.sh - added new `prevent_public_access_by_account?` * will throw URI parse errors until s3control gem is updated in train-aws - fixed typo in Makefile - kept needed gems in Gemfile until other upstream PRs are merged Signed-off-by: Aaron Lippold --- Gemfile | 1 + Makefile | 3 ++- gemspec-util.sh | 10 ++++++++++ libraries/aws_backend.rb | 6 +++++- libraries/aws_s3_bucket.rb | 37 ++++++++++++++++++++++++++++++++++--- libraries/aws_s3_buckets.rb | 6 +++--- 6 files changed, 55 insertions(+), 8 deletions(-) create mode 100755 gemspec-util.sh diff --git a/Gemfile b/Gemfile index 1db5f9788..02a3afc25 100644 --- a/Gemfile +++ b/Gemfile @@ -13,6 +13,7 @@ gem "bundle" # Use Latest Inspec gem "inspec-bin" gem "aws-sdk-accessanalyzer" +gem "aws-sdk-s3control" gem "aws-partitions" gem "train-kubernetes" diff --git a/Makefile b/Makefile index c37e2f38d..a3a510e18 100644 --- a/Makefile +++ b/Makefile @@ -25,4 +25,5 @@ shell_tester: docker-compose run --rm --entrypoint bash tester logout: - docker-compose run --rm aws rm -rf /app/.aws \ No newline at end of file + docker-compose run --rm aws rm -rf /app/.aws + diff --git a/gemspec-util.sh b/gemspec-util.sh new file mode 100755 index 000000000..14af8ed8c --- /dev/null +++ b/gemspec-util.sh @@ -0,0 +1,10 @@ +#!/bin/zsh + +echo -e 'deps and their versions in gemspec\n' +cat *.gemspec | ack -o '(?<=dependency).*(?<=[<>=]\s)(?>\d+\.?\d+)' | tr -d "',><=~" + +echo -e 'Deps and their version on RubyGems\n\n' +cat *.gemspec | ack -o '(?<=dependency).*(?<=[<>=]\s)(?>\d+\.?\d+)' | tr -d "',><=~" | awk '{ print $1 }' | xargs gem info -r | ack '\(\d+\.\d+\.\d+\)' + +echo -e 'gemspec and current remote versions side-by-side\n\n' +paste <(cat $(ls | ack gemspec) | ack -o '(?<=dependency).*(?<=[<>=]\s)(?>\d+\.?\d+)' | tr -d "',><=~") <(cat $(ls | ack gemspec) | ack -o '(?<=dependency).*(?<=[<>=]\s)(?>\d+\.?\d+)' | tr -d "',><=~" | awk '{ print $1 }' | xargs gem info -r | ack -o '(?<=\()(\d+\.\d+\.\d+)(?!>\))') diff --git a/libraries/aws_backend.rb b/libraries/aws_backend.rb index 4e326b1d8..9c075d77e 100644 --- a/libraries/aws_backend.rb +++ b/libraries/aws_backend.rb @@ -28,6 +28,7 @@ require 'aws-sdk-rds' require 'aws-sdk-route53' require 'aws-sdk-s3' +require 'aws-sdk-s3control' require 'aws-sdk-shield' require 'aws-sdk-sns' require 'aws-sdk-sqs' @@ -215,6 +216,10 @@ def storage_client aws_client(Aws::S3::Client) end + def storage_control_clientexit + aws_client(Aws::S3Control::Client) + end + def sts_client aws_client(Aws::STS::Client) end @@ -510,7 +515,6 @@ def catch_aws_errors nil rescue Aws::SecurityHub::Errors::InvalidAccessException => e Inspec::Log.warn("#{e.message} in region: #{opts[:aws_region]}") - #skip_resource(e.message.to_s) nil rescue Aws::Errors::NoSuchEndpointError Inspec::Log.error 'The endpoint that is trying to be accessed does not exist.' diff --git a/libraries/aws_s3_bucket.rb b/libraries/aws_s3_bucket.rb index 45f45bc90..3233977df 100644 --- a/libraries/aws_s3_bucket.rb +++ b/libraries/aws_s3_bucket.rb @@ -59,7 +59,7 @@ def public? begin @bucket_policy_status_public = @aws.storage_client.get_bucket_policy_status(bucket: @bucket_name).policy_status.is_public rescue Aws::S3::Errors::NoSuchBucketPolicy - @bucket_policy_status_public = false # preserves the original behaviour + @bucket_policy_status_public = false # preserves the original behavior end @bucket_policy_status_public || \ bucket_acl.any? { |g| g.grantee.type == "Group" && g.grantee.uri =~ /AllUsers/ } || \ @@ -77,8 +77,32 @@ def has_access_logging_enabled? def prevent_public_access? return false unless exists? @prevent_public_access ||= catch_aws_errors do - public_access_config = @aws.storage_client.get_public_access_block(bucket: @bucket_name).public_access_block_configuration - public_access_config.block_public_acls == true && public_access_config.ignore_public_acls == true && public_access_config.block_public_policy == true && public_access_config.restrict_public_buckets == true + begin + @public_access_config = @aws.storage_client.get_public_access_block(bucket: @bucket_name).public_access_block_configuration + # API throws an error if no public block access is configured + rescue Aws::S3::Errors::NoSuchPublicAccessBlockConfiguration + return @public_access_config = false + end + @public_access_config.block_public_acls == true && public_access_config.ignore_public_acls == true && public_access_config.block_public_policy == true && public_access_config.restrict_public_buckets == true + end + end + + def prevent_public_access_by_account? + return false unless exists? + @prevent_public_access_by_account ||= catch_aws_errors do + begin + @account_id = fetch_aws_account + require 'pry' ; binding.pry + @public_access_account_config = @aws.storage_control_client.get_public_access_block(account_id: @account_id).public_access_block_configuration + rescue Aws::S3::Errors::NoSuchPublicAccessBlockConfiguration + return @public_access_account_config = false + # not sure if this is standard behavior, expect above error for no config found + # rescue Aws::S3Control::Errors::InvalidURI => e + # TODO: Caused by outdated gem in train-aws https://github.com/inspec/train-aws/pull/519 + # raise "#{e.message}" + # return @public_access_account_config = false + end + @public_access_account_config.block_public_acls == true && public_access_config.ignore_public_acls == true && public_access_config.block_public_policy == true && public_access_config.restrict_public_buckets == true end end @@ -165,4 +189,11 @@ def resource_id def to_s "S3 Bucket #{@bucket_name}" end + + private + + def fetch_aws_account + arn = @aws.sts_client.get_caller_identity({}).arn + arn.split(':')[4] + end end diff --git a/libraries/aws_s3_buckets.rb b/libraries/aws_s3_buckets.rb index fcd1e8f3f..20d39f57e 100644 --- a/libraries/aws_s3_buckets.rb +++ b/libraries/aws_s3_buckets.rb @@ -4,8 +4,8 @@ class AwsS3Buckets < AwsResourceBase name "aws_s3_buckets" desc "Verifies settings for AWS S3 Buckets in bulk." example " - describe aws_s3_bucket do - its('bucket_names') { should eq ['my_bucket'] } + describe aws_s3_buckets do + its('bucket_names') { should include 'my_bucket' } end " @@ -38,7 +38,7 @@ def fetch_data end @api_response.each do |resp| resp.buckets.each do |bucket| - bucket_rows += [{ bucket_name: bucket[:name] }] + bucket_rows += [{ bucket_name: bucket[:name]}] end end @table = bucket_rows From ec6b2e59b0d3cd962522df394b57e34a7fbecdb4 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Sun, 3 Dec 2023 00:48:28 -0500 Subject: [PATCH 35/93] - added gemspec-util.sh - added new `prevent_public_access_by_account?` * will throw URI parse errors until s3control gem is updated in train-aws - fixed typo in Makefile - kept needed gems in Gemfile until other upstream PRs are merged Signed-off-by: Aaron Lippold --- Gemfile | 1 - libraries/aws_s3_bucket.rb | 1 - 2 files changed, 2 deletions(-) diff --git a/Gemfile b/Gemfile index 02a3afc25..9de3ee47b 100644 --- a/Gemfile +++ b/Gemfile @@ -15,7 +15,6 @@ gem "inspec-bin" gem "aws-sdk-accessanalyzer" gem "aws-sdk-s3control" gem "aws-partitions" -gem "train-kubernetes" gem "rubocop", "~> 1.25.1", require: false diff --git a/libraries/aws_s3_bucket.rb b/libraries/aws_s3_bucket.rb index 3233977df..383ab36b7 100644 --- a/libraries/aws_s3_bucket.rb +++ b/libraries/aws_s3_bucket.rb @@ -92,7 +92,6 @@ def prevent_public_access_by_account? @prevent_public_access_by_account ||= catch_aws_errors do begin @account_id = fetch_aws_account - require 'pry' ; binding.pry @public_access_account_config = @aws.storage_control_client.get_public_access_block(account_id: @account_id).public_access_block_configuration rescue Aws::S3::Errors::NoSuchPublicAccessBlockConfiguration return @public_access_account_config = false From 6f4aa8c34bda0334cbae16296742127c59fbff05 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Sun, 3 Dec 2023 02:40:59 -0500 Subject: [PATCH 36/93] Pulled in all gem updates from our train-aws PR Fixed typo in client name for storage_control_client Simplified the aws_s3_bucket `prevent_` methods Tested that the updated gems fix the Parse URI error Signed-off-by: Aaron Lippold --- Gemfile | 4 +--- libraries/aws_backend.rb | 2 +- libraries/aws_s3_bucket.rb | 11 +++-------- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/Gemfile b/Gemfile index 9de3ee47b..bc943b782 100644 --- a/Gemfile +++ b/Gemfile @@ -12,9 +12,7 @@ gem "bundle" # Use Latest Inspec gem "inspec-bin" -gem "aws-sdk-accessanalyzer" -gem "aws-sdk-s3control" -gem "aws-partitions" +gem "train-aws", git: 'https://github.com/mitre/train-aws.git', branch: 'al/dep-updates' gem "rubocop", "~> 1.25.1", require: false diff --git a/libraries/aws_backend.rb b/libraries/aws_backend.rb index 9c075d77e..61990df1c 100644 --- a/libraries/aws_backend.rb +++ b/libraries/aws_backend.rb @@ -216,7 +216,7 @@ def storage_client aws_client(Aws::S3::Client) end - def storage_control_clientexit + def storage_control_client aws_client(Aws::S3Control::Client) end diff --git a/libraries/aws_s3_bucket.rb b/libraries/aws_s3_bucket.rb index 383ab36b7..0dce422f8 100644 --- a/libraries/aws_s3_bucket.rb +++ b/libraries/aws_s3_bucket.rb @@ -10,7 +10,7 @@ class AwsS3Bucket < AwsResourceBase end " - attr_reader :region, :bucket_name, :versioning + attr_reader :region, :bucket_name, :versioning, :public_access_account_config, :public_access_config def initialize(opts = {}) opts = { bucket_name: opts } if opts.is_a?(String) @@ -83,7 +83,7 @@ def prevent_public_access? rescue Aws::S3::Errors::NoSuchPublicAccessBlockConfiguration return @public_access_config = false end - @public_access_config.block_public_acls == true && public_access_config.ignore_public_acls == true && public_access_config.block_public_policy == true && public_access_config.restrict_public_buckets == true + @public_access_config.all? end end @@ -95,13 +95,8 @@ def prevent_public_access_by_account? @public_access_account_config = @aws.storage_control_client.get_public_access_block(account_id: @account_id).public_access_block_configuration rescue Aws::S3::Errors::NoSuchPublicAccessBlockConfiguration return @public_access_account_config = false - # not sure if this is standard behavior, expect above error for no config found - # rescue Aws::S3Control::Errors::InvalidURI => e - # TODO: Caused by outdated gem in train-aws https://github.com/inspec/train-aws/pull/519 - # raise "#{e.message}" - # return @public_access_account_config = false end - @public_access_account_config.block_public_acls == true && public_access_config.ignore_public_acls == true && public_access_config.block_public_policy == true && public_access_config.restrict_public_buckets == true + @public_access_account_config.all? end end From cf2c0f6a47ee50933469ec3c1c9514965dce802d Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Sun, 3 Dec 2023 04:17:36 -0500 Subject: [PATCH 37/93] updated methods Signed-off-by: Aaron Lippold --- libraries/aws_s3_bucket.rb | 39 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/libraries/aws_s3_bucket.rb b/libraries/aws_s3_bucket.rb index 0dce422f8..29fd91e6a 100644 --- a/libraries/aws_s3_bucket.rb +++ b/libraries/aws_s3_bucket.rb @@ -1,9 +1,9 @@ -require "aws_backend" -require "hashie/mash" +require 'aws_backend' +require 'hashie/mash' class AwsS3Bucket < AwsResourceBase - name "aws_s3_bucket" - desc "Verifies settings for a s3 bucket." + name 'aws_s3_bucket' + desc 'Verifies settings for a s3 bucket.' example " describe aws_s3_bucket(bucket_name: 'test_bucket') do it { should exist } @@ -24,10 +24,10 @@ def initialize(opts = {}) @region = @aws.storage_client.get_bucket_location(bucket: @bucket_name).location_constraint # LocationConstraint "EU" correlates to the region "eu-west-1", but region "EU" does not exist as a "region", only a LocationConstraint # this currently is the only Location constraint that can have either of 2 values "EU" or "eu-west-1". But only "eu-west-1" is a region - @region = "eu-west-1" if @region == "EU" + @region = 'eu-west-1' if @region == 'EU' # Forcing bucket region for future bucket calls to avoid warnings about multiple unnecessary # redirects and signing attempts. - opts[:aws_region] = @region.empty? ? "us-east-1" : @region + opts[:aws_region] = @region.empty? ? 'us-east-1' : @region super(opts) rescue Aws::S3::Errors::NoSuchBucket @region = nil @@ -62,8 +62,8 @@ def public? @bucket_policy_status_public = false # preserves the original behavior end @bucket_policy_status_public || \ - bucket_acl.any? { |g| g.grantee.type == "Group" && g.grantee.uri =~ /AllUsers/ } || \ - bucket_acl.any? { |g| g.grantee.type == "Group" && g.grantee.uri =~ /AuthenticatedUsers/ } + bucket_acl.any? { |g| g.grantee.type == 'Group' && g.grantee.uri =~ /AllUsers/ } || \ + bucket_acl.any? { |g| g.grantee.type == 'Group' && g.grantee.uri =~ /AuthenticatedUsers/ } end end @@ -78,25 +78,24 @@ def prevent_public_access? return false unless exists? @prevent_public_access ||= catch_aws_errors do begin - @public_access_config = @aws.storage_client.get_public_access_block(bucket: @bucket_name).public_access_block_configuration - # API throws an error if no public block access is configured + public_access_config = @aws.storage_client.get_public_access_block(bucket: @bucket_name).public_access_block_configuration rescue Aws::S3::Errors::NoSuchPublicAccessBlockConfiguration - return @public_access_config = false + return false end - @public_access_config.all? + public_access_config.values.all? end end def prevent_public_access_by_account? return false unless exists? + @account_id = fetch_aws_account @prevent_public_access_by_account ||= catch_aws_errors do begin - @account_id = fetch_aws_account - @public_access_account_config = @aws.storage_control_client.get_public_access_block(account_id: @account_id).public_access_block_configuration + public_access_account_config = @aws.storage_control_client.get_public_access_block(account_id: @account_id).public_access_block_configuration rescue Aws::S3::Errors::NoSuchPublicAccessBlockConfiguration - return @public_access_account_config = false + return false end - @public_access_account_config.all? + public_access_account_config.values.all? end end @@ -116,7 +115,7 @@ def has_default_encryption_enabled? def has_versioning_enabled? return false unless exists? catch_aws_errors do - @has_versioning_enabled = @aws.storage_client.get_bucket_versioning(bucket: @bucket_name).status == "Enabled" + @has_versioning_enabled = @aws.storage_client.get_bucket_versioning(bucket: @bucket_name).status == 'Enabled' end end @@ -128,7 +127,7 @@ def versioning end def has_secure_transport_enabled? - bucket_policy.any? { |s| s.effect == "Deny" && s.condition && s.condition["Bool"] && s.condition["Bool"]["aws:SecureTransport"] && s.condition["Bool"]["aws:SecureTransport"] == "false" } + bucket_policy.any? { |s| s.effect == 'Deny' && s.condition && s.condition['Bool'] && s.condition['Bool']['aws:SecureTransport'] && s.condition['Bool']['aws:SecureTransport'] == 'false' } end # below is to preserve the original 'unsupported' function but isn't used in the above @@ -143,7 +142,7 @@ def fetch_bucket_policy # AWS SDK returns a StringIO, we have to read() raw_policy = @aws.storage_client.get_bucket_policy(bucket: @bucket_name).to_h return [] if !raw_policy.key?(:policy) - JSON.parse(raw_policy[:policy].read)["Statement"].map do |statement| + JSON.parse(raw_policy[:policy].read)['Statement'].map do |statement| lowercase_hash = {} statement.each_key { |k| lowercase_hash[k.downcase] = statement[k] } policy_list += [OpenStruct.new(lowercase_hash)] @@ -183,7 +182,7 @@ def resource_id def to_s "S3 Bucket #{@bucket_name}" end - + private def fetch_aws_account From b407565d6c14abb2de19308143e246b3f244a2f5 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Sun, 3 Dec 2023 15:56:24 -0500 Subject: [PATCH 38/93] prevent-public-access-by-bucket and ...-by-account should be working now and capture API errors correctly Signed-off-by: Aaron Lippold --- libraries/aws_backend.rb | 8 ++++++-- libraries/aws_s3_bucket.rb | 26 +++++++++++++++----------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/libraries/aws_backend.rb b/libraries/aws_backend.rb index 61990df1c..7711b9b02 100644 --- a/libraries/aws_backend.rb +++ b/libraries/aws_backend.rb @@ -501,7 +501,7 @@ def name # Intercept AWS exceptions def catch_aws_errors yield # Catch and create custom messages as needed - rescue Aws::Errors::MissingCredentialsError + rescue Aws::Errors::MissingCredentialsError => e Inspec::Log.error 'It appears that you have not set your AWS credentials. See https://www.inspec.io/docs/reference/platforms for details.' fail_resource('No AWS credentials available') nil @@ -516,10 +516,14 @@ def catch_aws_errors rescue Aws::SecurityHub::Errors::InvalidAccessException => e Inspec::Log.warn("#{e.message} in region: #{opts[:aws_region]}") nil - rescue Aws::Errors::NoSuchEndpointError + rescue Aws::Errors::NoSuchEndpointError => e Inspec::Log.error 'The endpoint that is trying to be accessed does not exist.' fail_resource('Invalid Endpoint error') nil + rescue Aws::S3::Errors::NoSuchPublicAccessBlockConfiguration => e + Inspec::Log.error 'No public access block configuration was found' + skip_resource('No public access block configuration was found') + nil rescue Aws::Errors::ServiceError => e if is_permissions_error(e) advice = '' diff --git a/libraries/aws_s3_bucket.rb b/libraries/aws_s3_bucket.rb index 29fd91e6a..5678deedc 100644 --- a/libraries/aws_s3_bucket.rb +++ b/libraries/aws_s3_bucket.rb @@ -10,7 +10,7 @@ class AwsS3Bucket < AwsResourceBase end " - attr_reader :region, :bucket_name, :versioning, :public_access_account_config, :public_access_config + attr_reader :region, :bucket_name, :versioning def initialize(opts = {}) opts = { bucket_name: opts } if opts.is_a?(String) @@ -76,29 +76,33 @@ def has_access_logging_enabled? def prevent_public_access? return false unless exists? - @prevent_public_access ||= catch_aws_errors do + @prevent_public_access = begin public_access_config = @aws.storage_client.get_public_access_block(bucket: @bucket_name).public_access_block_configuration - rescue Aws::S3::Errors::NoSuchPublicAccessBlockConfiguration - return false + rescue Aws::S3::Errors::NoSuchPublicAccessBlockConfiguration => e + @prevent_public_access = false end - public_access_config.values.all? - end + return false unless @prevent_public_access + public_access_config.block_public_acls == true && public_access_config.ignore_public_acls == true && public_access_config.block_public_policy == true && public_access_config.restrict_public_buckets == true end + alias preventing_public_access_via_bucket? prevent_public_access? + def prevent_public_access_by_account? return false unless exists? @account_id = fetch_aws_account - @prevent_public_access_by_account ||= catch_aws_errors do + @prevent_public_access_by_account = begin public_access_account_config = @aws.storage_control_client.get_public_access_block(account_id: @account_id).public_access_block_configuration - rescue Aws::S3::Errors::NoSuchPublicAccessBlockConfiguration - return false + rescue Aws::S3::Errors::NoSuchPublicAccessBlockConfiguration => e + @prevent_public_access_by_account = false end - public_access_account_config.values.all? - end + return false unless @prevent_public_access_by_account + public_access_account_config.block_public_acls == true && public_access_account_config.ignore_public_acls == true && public_access_account_config.block_public_policy == true && public_access_account_config.restrict_public_buckets == true end + alias preventing_public_access_via_account? prevent_public_access_by_account? + def has_default_encryption_enabled? return false unless exists? @has_default_encryption_enabled ||= catch_aws_errors do From 08c393c9fb81aa3ebcf0364c90fd678951175e3c Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Sun, 3 Dec 2023 17:14:19 -0500 Subject: [PATCH 39/93] Docs for `prevent_public_access` and 'prevent_public_access_via_account` - added missing documetaion for `prevent_public_access` - added documentation for `prevent_public_access_via_account` - documented alias function for more readable tests Signed-off-by: Aaron Lippold --- .../content/inspec/resources/aws_s3_bucket.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs-chef-io/content/inspec/resources/aws_s3_bucket.md b/docs-chef-io/content/inspec/resources/aws_s3_bucket.md index d918ae16f..2f7c35793 100644 --- a/docs-chef-io/content/inspec/resources/aws_s3_bucket.md +++ b/docs-chef-io/content/inspec/resources/aws_s3_bucket.md @@ -192,6 +192,30 @@ The `have_secure_transport_enabled` matcher tests if a bucket policy that explic it { should have_secure_transport_enabled } +#### prevent_public_access + +The `prevent_public_access` matcher tests if the buckets public access is restricted via the bucket access block of the given bucket via the AWS S3 API. + + it { should be_prevent_public_access } + +#### preventing_public_access_via_bucket + +Alias of `prevent_public_access`. + + it { should be_preventing_public_public_access_via_bucket } + +#### prevent_public_access_by_account + +The `prevent_public_access_by_account` matcher tests if the buckets public access is restricted via current aws account via the AWS S3 Control API. + + it { should be_prevent_public_access_by_account } + +#### preventing_public_access_by_account + +Alias of `prevent_public_access_by_account`. + + it { should be_preventing_public_access_by_account } + ## AWS Permissions Your [Principal](https://docs.aws.amazon.com/IAM/latest/UserGuide/intro-structure.html#intro-structure-principal) will need the `S3:Client:GetBucketAclOutput`, `S3:Client:GetBucketLocationOutput`, `S3:Client:GetBucketLoggingOutput`, `S3:Client:GetBucketPolicyOutput`, and `S3:Client:GetBucketEncryptionOutput` actions set to allow. From 85082089227c1c6b4fff766403f8c46ab90d0d02 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Sun, 3 Dec 2023 18:14:04 -0500 Subject: [PATCH 40/93] minor linting and some unneeded assignments Signed-off-by: Aaron Lippold --- libraries/aws_alternate_contact.rb | 12 +- libraries/aws_backend.rb | 244 +++++++++++++------------- libraries/aws_billing_contact.rb | 18 +- libraries/aws_cloudtrail_trail.rb | 2 +- libraries/aws_ec2_instance.rb | 18 +- libraries/aws_iam_access_analyzers.rb | 24 +-- libraries/aws_iam_root_user.rb | 14 +- libraries/aws_operations_contact.rb | 18 +- libraries/aws_primary_contact.rb | 16 +- libraries/aws_rds_instances.rb | 6 +- libraries/aws_region.rb | 8 +- libraries/aws_regions.rb | 8 +- libraries/aws_s3_bucket.rb | 30 ++-- libraries/aws_s3_buckets.rb | 2 +- libraries/aws_security_contact.rb | 18 +- libraries/aws_securityhub.rb | 4 +- 16 files changed, 221 insertions(+), 221 deletions(-) diff --git a/libraries/aws_alternate_contact.rb b/libraries/aws_alternate_contact.rb index a6d4088c3..e95f07f8d 100644 --- a/libraries/aws_alternate_contact.rb +++ b/libraries/aws_alternate_contact.rb @@ -1,8 +1,8 @@ -require 'aws_backend' +require "aws_backend" class AwsAlternateAccount < AwsResourceBase - name 'aws_alternate_contact' - desc 'Verifies the billing contact information for an AWS Account.' + name "aws_alternate_contact" + desc "Verifies the billing contact information for an AWS Account." example <<~EXAMPLE1 describe aws_alternate_account(type: 'billing') do it { should be_configured } @@ -44,7 +44,7 @@ def initialize(opts = {}) end unless opts.keys && (opts.keys & supported_opt_keys).length == 1 raise ArgumentError, - 'Specifying more than one of :type for aws_alternate_account is not supported' + "Specifying more than one of :type for aws_alternate_account is not supported" end unless supported_opts_values.any? { |val| opts.values.include?(val) } raise ArgumentError, @@ -72,7 +72,7 @@ def initialize(opts = {}) end @raw_data = @api_response.to_h.transform_keys(&:to_s) else - @name, @email_address, @phone_number, @title = '' + @name, @email_address, @phone_number, @title = "" end end @@ -102,7 +102,7 @@ def to_s def fetch_aws_account arn = @aws.sts_client.get_caller_identity({}).arn - arn.split(':')[4] + arn.split(":")[4] end def fetch_aws_alternate_contact(type) diff --git a/libraries/aws_backend.rb b/libraries/aws_backend.rb index 7711b9b02..6823cec6c 100644 --- a/libraries/aws_backend.rb +++ b/libraries/aws_backend.rb @@ -1,70 +1,70 @@ -require 'active_support' -require 'active_support/core_ext' -require 'active_support/core_ext/string' - -require 'aws-sdk-autoscaling' -require 'aws-sdk-batch' -require 'aws-sdk-cloudformation' -require 'aws-sdk-cloudfront' -require 'aws-sdk-cloudtrail' -require 'aws-sdk-cloudwatch' -require 'aws-sdk-cloudwatchlogs' -require 'aws-sdk-configservice' -require 'aws-sdk-core' -require 'aws-sdk-dynamodb' -require 'aws-sdk-ec2' -require 'aws-sdk-ecr' -require 'aws-sdk-ecrpublic' -require 'aws-sdk-ecs' -require 'aws-sdk-eks' -require 'aws-sdk-elasticache' -require 'aws-sdk-elasticloadbalancing' -require 'aws-sdk-elasticloadbalancingv2' -require 'aws-sdk-guardduty' -require 'aws-sdk-iam' -require 'aws-sdk-kms' -require 'aws-sdk-lambda' -require 'aws-sdk-organizations' -require 'aws-sdk-rds' -require 'aws-sdk-route53' -require 'aws-sdk-s3' -require 'aws-sdk-s3control' -require 'aws-sdk-shield' -require 'aws-sdk-sns' -require 'aws-sdk-sqs' -require 'aws-sdk-efs' -require 'aws-sdk-ssm' -require 'rspec/expectations' -require 'aws-sdk-transfer' -require 'aws-sdk-elasticsearchservice' -require 'aws-sdk-cognitoidentity' -require 'aws-sdk-redshift' -require 'aws-sdk-athena' -require 'aws-sdk-applicationautoscaling' -require 'aws-sdk-cognitoidentityprovider' -require 'aws-sdk-apigateway' -require 'aws-sdk-databasemigrationservice' -require 'aws-sdk-route53resolver' -require 'aws-sdk-servicecatalog' -require 'aws-sdk-glue' -require 'aws-sdk-eventbridge' -require 'aws-sdk-states' -require 'aws-sdk-ram' -require 'aws-sdk-secretsmanager' -require 'aws-sdk-networkfirewall' -require 'aws-sdk-mq' -require 'aws-sdk-networkmanager' -require 'aws-sdk-signer' -require 'aws-sdk-amplify' -require 'aws-sdk-simpledb' -require 'aws-sdk-emr' -require 'aws-sdk-securityhub' -require 'aws-sdk-ses' -require 'aws-sdk-waf' -require 'aws-sdk-synthetics' -require 'aws-sdk-apigatewayv2' -require 'aws-sdk-account' -require 'aws-sdk-accessanalyzer' +require "active_support" +require "active_support/core_ext" +require "active_support/core_ext/string" + +require "aws-sdk-autoscaling" +require "aws-sdk-batch" +require "aws-sdk-cloudformation" +require "aws-sdk-cloudfront" +require "aws-sdk-cloudtrail" +require "aws-sdk-cloudwatch" +require "aws-sdk-cloudwatchlogs" +require "aws-sdk-configservice" +require "aws-sdk-core" +require "aws-sdk-dynamodb" +require "aws-sdk-ec2" +require "aws-sdk-ecr" +require "aws-sdk-ecrpublic" +require "aws-sdk-ecs" +require "aws-sdk-eks" +require "aws-sdk-elasticache" +require "aws-sdk-elasticloadbalancing" +require "aws-sdk-elasticloadbalancingv2" +require "aws-sdk-guardduty" +require "aws-sdk-iam" +require "aws-sdk-kms" +require "aws-sdk-lambda" +require "aws-sdk-organizations" +require "aws-sdk-rds" +require "aws-sdk-route53" +require "aws-sdk-s3" +require "aws-sdk-s3control" +require "aws-sdk-shield" +require "aws-sdk-sns" +require "aws-sdk-sqs" +require "aws-sdk-efs" +require "aws-sdk-ssm" +require "rspec/expectations" +require "aws-sdk-transfer" +require "aws-sdk-elasticsearchservice" +require "aws-sdk-cognitoidentity" +require "aws-sdk-redshift" +require "aws-sdk-athena" +require "aws-sdk-applicationautoscaling" +require "aws-sdk-cognitoidentityprovider" +require "aws-sdk-apigateway" +require "aws-sdk-databasemigrationservice" +require "aws-sdk-route53resolver" +require "aws-sdk-servicecatalog" +require "aws-sdk-glue" +require "aws-sdk-eventbridge" +require "aws-sdk-states" +require "aws-sdk-ram" +require "aws-sdk-secretsmanager" +require "aws-sdk-networkfirewall" +require "aws-sdk-mq" +require "aws-sdk-networkmanager" +require "aws-sdk-signer" +require "aws-sdk-amplify" +require "aws-sdk-simpledb" +require "aws-sdk-emr" +require "aws-sdk-securityhub" +require "aws-sdk-ses" +require "aws-sdk-waf" +require "aws-sdk-synthetics" +require "aws-sdk-apigatewayv2" +require "aws-sdk-account" +require "aws-sdk-accessanalyzer" # AWS Inspec Backend Classes # @@ -379,12 +379,12 @@ def initialize(opts) ] # below allows each resource to optionally and conveniently set max_retries and retry_backoff env_hash = ENV.map { |k, v| [k.downcase, v] }.to_h - opts[:aws_retry_limit] = env_hash['aws_retry_limit'].to_i if !opts[ + opts[:aws_retry_limit] = env_hash["aws_retry_limit"].to_i if !opts[ :aws_retry_limit - ] && env_hash['aws_retry_limit'] - opts[:aws_retry_backoff] = env_hash['aws_retry_backoff'].to_i if !opts[ + ] && env_hash["aws_retry_limit"] + opts[:aws_retry_backoff] = env_hash["aws_retry_backoff"].to_i if !opts[ :aws_retry_backoff - ] && env_hash['aws_retry_backoff'] + ] && env_hash["aws_retry_backoff"] client_args[:client_args][:retry_limit] = opts[:aws_retry_limit] if opts[ :aws_retry_limit ] @@ -407,12 +407,12 @@ def initialize(opts) # here we might want to inject stub data for testing, let's use an option for that return if !defined?(@opts.keys) || !@opts.include?(:stub_data) if !opts[:stub_data].is_a?(Array) - raise ArgumentError, 'Expected stub data to be an array' + raise ArgumentError, "Expected stub data to be an array" end opts[:stub_data].each do |stub| if !stub.keys.all? { |a| %i(method data client).include?(a) } raise ArgumentError, - 'Expect each stub_data hash to have :client, :method and :data keys' + "Expect each stub_data hash to have :client, :method and :data keys" end @aws.aws_client(stub[:client]).stub_responses(stub[:method], stub[:data]) end @@ -430,9 +430,9 @@ def validate_parameters(allow: [], required: nil, require_any_of: nil) # rubocop "Expected required parameters as Array of Symbols, got #{required}" end unless @opts.is_a?(Hash) && - required.all? { |req| - @opts.key?(req) && !@opts[req].nil? && @opts[req] != '' - } + required.all? { |req| + @opts.key?(req) && !@opts[req].nil? && @opts[req] != "" + } raise ArgumentError, "#{@__resource_name__}: `#{required}` must be provided" end @@ -441,14 +441,14 @@ def validate_parameters(allow: [], required: nil, require_any_of: nil) # rubocop if require_any_of unless require_any_of.is_a?(Array) && - require_any_of.all? { |r| r.is_a?(Symbol) } + require_any_of.all? { |r| r.is_a?(Symbol) } raise ArgumentError, "Expected required parameters as Array of Symbols, got #{require_any_of}" end unless @opts.is_a?(Hash) && - require_any_of.any? { |req| - @opts.key?(req) && !@opts[req].nil? && @opts[req] != '' - } + require_any_of.any? { |req| + @opts.key?(req) && !@opts[req].nil? && @opts[req] != "" + } raise ArgumentError, "#{@__resource_name__}: One of `#{require_any_of}` must be provided." end @@ -465,17 +465,17 @@ def validate_parameters(allow: [], required: nil, require_any_of: nil) # rubocop resource_data ) unless defined?(@opts.keys) - raise ArgumentError, 'Scalar arguments not supported' + raise ArgumentError, "Scalar arguments not supported" end unless @opts.keys.all? { |a| allow.include?(a) } - raise ArgumentError, 'Unexpected arguments found' + raise ArgumentError, "Unexpected arguments found" end unless @opts.values.all? { |a| - return true if a.instance_of?(Integer) - return true if [TrueClass, FalseClass].include?(a.class) - !a.empty? - } - raise ArgumentError, 'Provided parameter should not be empty' + return true if a.instance_of?(Integer) + return true if [TrueClass, FalseClass].include?(a.class) + !a.empty? + } + raise ArgumentError, "Provided parameter should not be empty" end true end @@ -493,51 +493,51 @@ def tags def name return unless tags - return tags['Name'] if tags.is_a?(Hash) + return tags["Name"] if tags.is_a?(Hash) # tags might be in the original format: [{:key=>"Name", :value=>"aws-linux-ubuntu-vm"}], e.g in EC2 - tags.select { |tag| tag[:key] == 'Name' }.first&.dig(:value) + tags.select { |tag| tag[:key] == "Name" }.first&.dig(:value) end # Intercept AWS exceptions def catch_aws_errors yield # Catch and create custom messages as needed - rescue Aws::Errors::MissingCredentialsError => e - Inspec::Log.error 'It appears that you have not set your AWS credentials. See https://www.inspec.io/docs/reference/platforms for details.' - fail_resource('No AWS credentials available') - nil rescue Aws::Account::Errors::ResourceNotFoundException => e - Inspec::Log.warn e.message.to_s + Inspec::Log.warn(e.message.to_s) skip_resource(e.message.to_s) nil rescue Aws::AccessAnalyzer::Errors => e - Inspec::Log.warn e.message.to_s + Inspec::Log.warn(e.message.to_s) skip_resource(e.message.to_s) nil + rescue Aws::Errors::MissingCredentialsError + Inspec::Log.error("It appears that you have not set your AWS credentials. See https://www.inspec.io/docs/reference/platforms for details.") + fail_resource("No AWS credentials available") + nil rescue Aws::SecurityHub::Errors::InvalidAccessException => e Inspec::Log.warn("#{e.message} in region: #{opts[:aws_region]}") nil - rescue Aws::Errors::NoSuchEndpointError => e - Inspec::Log.error 'The endpoint that is trying to be accessed does not exist.' - fail_resource('Invalid Endpoint error') + rescue Aws::Errors::NoSuchEndpointError + Inspec::Log.error("The endpoint that is trying to be accessed does not exist.") + fail_resource("Invalid Endpoint error") nil - rescue Aws::S3::Errors::NoSuchPublicAccessBlockConfiguration => e - Inspec::Log.error 'No public access block configuration was found' - skip_resource('No public access block configuration was found') + rescue Aws::S3::Errors::NoSuchPublicAccessBlockConfiguration + Inspec::Log.error("No public access block configuration was found") + skip_resource("No public access block configuration was found") nil rescue Aws::Errors::ServiceError => e if is_permissions_error(e) - advice = '' - error_type = e.class.to_s.split('::').last + advice = "" + error_type = e.class.to_s.split("::").last case error_type - when 'InvalidAccessKeyId' - advice = 'Please ensure your AWS Access Key ID is set correctly.' - when 'InvalidClientTokenId' + when "InvalidAccessKeyId" + advice = "Please ensure your AWS Access Key ID is set correctly." + when "InvalidClientTokenId" advice = - 'Please ensure that the aws access key, aws secret access key, and the aws session token are correct.' - when 'AccessDenied' + "Please ensure that the aws access key, aws secret access key, and the aws session token are correct." + when "AccessDenied" advice = - 'Please check the IAM permissions required for this Resource in the documentation, ' \ - 'and ensure your Service Principal has these permissions set.' + "Please check the IAM permissions required for this Resource in the documentation, " \ + "and ensure your Service Principal has these permissions set." end error_message = "#{e.message}: #{advice}" @@ -545,7 +545,7 @@ def catch_aws_errors else Inspec::Log.warn "AWS Service Error encountered running a control with Resource #{@__resource_name__}. " \ "Error message: #{e.message} You should address this error to ensure your controls are " \ - 'behaving as expected.' + "behaving as expected." @failed_resource = true end nil @@ -595,8 +595,8 @@ def respond_to_missing?(*several_variants) def resource_fail(message = nil) message ||= "#{@__resource_name__}: #{@display_name}. Multiple AWS resources were returned for the provided criteria. " \ - 'If you wish to test multiple entities, please use the plural resource. ' \ - 'Otherwise, please provide more specific criteria to lookup the resource.' + "If you wish to test multiple entities, please use the plural resource. " \ + "Otherwise, please provide more specific criteria to lookup the resource." # Fail resource in resource pack. `exists?` method will return `false`. @failed_resource = true # Fail resource in InSpec core. Tests in InSpec profile will return the message. @@ -632,7 +632,7 @@ def self.populate_filter_table(raw_data, table_scheme) def fetch(client:, operation:, kwargs: {}) unless @aws.respond_to?(client) - raise ArgumentError, 'Valid Client not found!' + raise ArgumentError, "Valid Client not found!" end client_obj = @aws.send(client) @@ -675,13 +675,13 @@ def create_methods(object, data) data.instance_variables.each do |var| create_method( object, - var.to_s.delete('@'), + var.to_s.delete("@"), data.instance_variable_get(var), ) end # When the data is a Hash object iterate around each of the key value pairs and # create a method for each one. - when 'Hash' + when "Hash" data.each { |key, value| create_method(object, key, value) } end end @@ -697,11 +697,11 @@ def create_method(object, name, value) # Create the necessary method based on the var that has been passed # Test the value for its type so that the method can be setup correctly case value.class.to_s - when 'String', 'Integer', 'TrueClass', 'FalseClass', 'Fixnum', 'Time' + when "String", "Integer", "TrueClass", "FalseClass", "Fixnum", "Time" object.define_singleton_method name do value end - when 'Hash' + when "Hash" if value.count == 0 return_value = value else @@ -716,7 +716,7 @@ def create_method(object, name, value) value = value.to_h if value.respond_to? :to_h AwsResourceProbe.new(value) end - when 'Array' + when "Array" # Some things are just string or integer arrays # Check this by seeing if the first element is a string / integer / boolean or # a hashtable @@ -724,7 +724,7 @@ def create_method(object, name, value) # the quickest test # p value[0].class.to_s case value[0].class.to_s - when 'String', 'Integer', 'TrueClass', 'FalseClass', 'Fixnum', 'Time' + when "String", "Integer", "TrueClass", "FalseClass", "Fixnum", "Time" probes = value else if name.eql?(:tags) @@ -785,10 +785,10 @@ def initialize(item) # Hash: Key=>Value pair to look for in the @item property def include?(opt) unless opt.is_a?(Symbol) || opt.is_a?(Hash) || opt.is_a?(String) - raise ArgumentError, 'Key or Key:Value pair should be provided.' + raise ArgumentError, "Key or Key:Value pair should be provided." end if opt.is_a?(Hash) - raise ArgumentError, 'Only one item can be provided' if opt.keys.size > 1 + raise ArgumentError, "Only one item can be provided" if opt.keys.size > 1 return @item[opt.keys.first] == opt.values.first end @item.key?(opt.to_sym) diff --git a/libraries/aws_billing_contact.rb b/libraries/aws_billing_contact.rb index f9919e770..f0e3ed7d9 100644 --- a/libraries/aws_billing_contact.rb +++ b/libraries/aws_billing_contact.rb @@ -1,8 +1,8 @@ -require 'aws_backend' +require "aws_backend" class AwsBillingAccount < AwsResourceBase - name 'aws_billing_contact' - desc 'Verifies the billing contact information for an AWS Account.' + name "aws_billing_contact" + desc "Verifies the billing contact information for an AWS Account." example <<~EXAMPLE describe aws_billing_account do it { should be_configured } @@ -23,14 +23,14 @@ class AwsBillingAccount < AwsResourceBase def initialize(opts = {}) super(opts) @raw_data = {} - @title, @name, @email_address, @phone_number = String.new + @title, @name, @email_address, @phone_number = "" validate_parameters begin catch_aws_errors { @aws_account_id = fetch_aws_account } - @api_response = fetch_aws_alternate_contact('billing') + @api_response = fetch_aws_alternate_contact("billing") rescue Aws::Account::Errors::ResourceNotFoundException skip_resource( - 'The Billing contact has not been configured for this AWS Account.', + "The Billing contact has not been configured for this AWS Account.", ) return [] if !@api_response || @api_response.empty? end @@ -56,7 +56,7 @@ def resource_id if @aws_account_id "AWS Billing Contact for account: #{@aws_account_id}" else - 'AWS Billing Contact Information' + "AWS Billing Contact Information" end end @@ -64,7 +64,7 @@ def to_s if @aws_account_id "AWS Billing Contact for account: #{@aws_account_id}" else - 'AWS Account Primary Contact' + "AWS Account Primary Contact" end end @@ -72,7 +72,7 @@ def to_s def fetch_aws_account arn = @aws.sts_client.get_caller_identity({}).arn - arn.split(':')[4] + arn.split(":")[4] end def fetch_aws_alternate_contact(type) diff --git a/libraries/aws_cloudtrail_trail.rb b/libraries/aws_cloudtrail_trail.rb index 2452cb4d8..b6fdc64f1 100644 --- a/libraries/aws_cloudtrail_trail.rb +++ b/libraries/aws_cloudtrail_trail.rb @@ -11,7 +11,7 @@ class AwsCloudTrailTrail < AwsResourceBase attr_reader :cloud_watch_logs_log_group_arn, :cloud_watch_logs_role_arn, :home_region, :trail_name, :kms_key_id, :s3_bucket_name, :s3_key_prefix, :trail_arn, :is_multi_region_trail, - :log_file_validation_enabled, :is_organization_trail, :event_selectors + :log_file_validation_enabled, :is_organization_trail alias multi_region_trail? is_multi_region_trail alias log_file_validation_enabled? log_file_validation_enabled diff --git a/libraries/aws_ec2_instance.rb b/libraries/aws_ec2_instance.rb index f25a469f0..feaa7f616 100644 --- a/libraries/aws_ec2_instance.rb +++ b/libraries/aws_ec2_instance.rb @@ -21,11 +21,11 @@ class AwsEc2Instance < AwsResourceBase def initialize(opts = {}) opts = { instance_id: opts } if opts.is_a?(String) super(opts) - validate_parameters(require_any_of: %i[instance_id name]) + validate_parameters(require_any_of: %i(instance_id name)) if opts[:instance_id] && !opts[:instance_id].empty? # Use instance_id, if provided if !opts[:instance_id].is_a?(String) || - opts[:instance_id] !~ /(^i-[0-9a-f]{8})|(^i-[0-9a-f]{17})$/ + opts[:instance_id] !~ /(^i-[0-9a-f]{8})|(^i-[0-9a-f]{17})$/ raise ArgumentError, "#{@__resource_name__}: `instance_id` must be a string in the format of 'i-' followed by 8 or 17 hexadecimal characters." end @@ -34,7 +34,7 @@ def initialize(opts = {}) elsif opts[:name] && !opts[:name].empty? # Otherwise use name, if provided @display_name = opts[:name] instance_arguments = { - filters: [{ name: "tag:Name", values: [opts[:name]] }] + filters: [{ name: "tag:Name", values: [opts[:name]] }], } else raise ArgumentError, @@ -44,12 +44,12 @@ def initialize(opts = {}) catch_aws_errors do resp = @aws.compute_client.describe_instances(instance_arguments) if resp.reservations.first.nil? || - resp.reservations.first.instances.first.nil? + resp.reservations.first.instances.first.nil? empty_response_warn return end if resp.reservations.count > 1 || - resp.reservations.first.instances.count > 1 + resp.reservations.first.instances.count > 1 resource_fail return else @@ -110,7 +110,7 @@ def network_interface_ids def has_roles? unless @instance[:iam_instance_profile] && - @instance[:iam_instance_profile][:arn] + @instance[:iam_instance_profile][:arn] return false end instance_profile = @instance[:iam_instance_profile][:arn].split("/").last @@ -119,7 +119,7 @@ def has_roles? catch_aws_errors do resp = @aws.iam_client.get_instance_profile( - { instance_profile_name: instance_profile } + { instance_profile_name: instance_profile }, ) @returned_roles = resp.instance_profile.roles end @@ -136,7 +136,7 @@ def role end # Generate a matcher for each state - %w[ + %w{ pending running shutting-down @@ -144,7 +144,7 @@ def role stopping stopped unknown - ].each do |state_name| + }.each do |state_name| define_method "#{state_name.tr("-", "_")}?" do state == state_name end diff --git a/libraries/aws_iam_access_analyzers.rb b/libraries/aws_iam_access_analyzers.rb index 73bf4daac..bfcdec0b3 100644 --- a/libraries/aws_iam_access_analyzers.rb +++ b/libraries/aws_iam_access_analyzers.rb @@ -1,9 +1,9 @@ -require 'aws_backend' -require 'pry' +require "aws_backend" +require "pry" class AwsIamAccessAnalyzer < AwsResourceBase - name 'aws_iam_access_analyzers' - desc 'Verifies settings for a collection AWS IAM Access Analyzers.' + name "aws_iam_access_analyzers" + desc "Verifies settings for a collection AWS IAM Access Analyzers." example <<~EXAMPLE1 # retrieve both 'account' and 'organization' analyzers describe aws_iam_access_analyzers do @@ -60,10 +60,10 @@ def fetch_data(parameters) catch_aws_errors do catch_aws_errors { @aws_account_id = fetch_aws_account } - if parameters.empty? || parameters[:type] == 'ALL' + if parameters.empty? || parameters[:type] == "ALL" @api_response = @aws.access_analyzer_client.list_analyzers - elsif parameters[:type] == 'ACCOUNT' || - parameters[:type] == 'ORGANIZATION' + elsif parameters[:type] == "ACCOUNT" || + parameters[:type] == "ORGANIZATION" @api_response = @aws.access_analyzer_client.list_analyzers(parameters) end @@ -89,8 +89,8 @@ def fetch_data(parameters) end def resource_id - response = 'AWS IAM ' - opts[:type] ? response += "#{opts[:type].capitalize} " : '' + response = "AWS IAM " + opts[:type] ? response += "#{opts[:type].capitalize} " : "" if @aws_account_id response += "Account Analyzer for #{@aws_account_id} in #{get_current_region}" @@ -101,8 +101,8 @@ def resource_id end def to_s - response = 'AWS IAM ' - opts[:type] ? response += "#{opts[:type].capitalize} " : '' + response = "AWS IAM " + opts[:type] ? response += "#{opts[:type].capitalize} " : "" if @aws_account_id response += "Account Analyzer for #{@aws_account_id} in #{get_current_region}" @@ -116,7 +116,7 @@ def to_s def fetch_aws_account arn = @aws.sts_client.get_caller_identity({}).arn - arn.split(':')[4] + arn.split(":")[4] end def get_current_region diff --git a/libraries/aws_iam_root_user.rb b/libraries/aws_iam_root_user.rb index 1cd721394..b012eb5df 100644 --- a/libraries/aws_iam_root_user.rb +++ b/libraries/aws_iam_root_user.rb @@ -1,8 +1,8 @@ -require 'aws_backend' +require "aws_backend" class AwsIamRootUser < AwsResourceBase - name 'aws_iam_root_user' - desc 'Verifies settings for AWS Root Account.' + name "aws_iam_root_user" + desc "Verifies settings for AWS Root Account." example " describe aws_iam_root_user do it { should have_access_key } @@ -27,11 +27,11 @@ def resource_id end def has_access_key? - @summary_account['AccountAccessKeysPresent'] == 1 + @summary_account["AccountAccessKeysPresent"] == 1 end def has_mfa_enabled? - @summary_account['AccountMFAEnabled'] == 1 + @summary_account["AccountMFAEnabled"] == 1 end def has_hardware_mfa_enabled? @@ -40,7 +40,7 @@ def has_hardware_mfa_enabled? # Virtual MFA devices have suffix 'root-account-mfa-device' def has_virtual_mfa_enabled? - virtual_mfa_suffix = 'root-account-mfa-device' + virtual_mfa_suffix = "root-account-mfa-device" @virtual_devices.any? { |device| device[:serial_number].end_with?(virtual_mfa_suffix) } end @@ -49,6 +49,6 @@ def exists? end def to_s - 'AWS Root-User' + "AWS Root-User" end end diff --git a/libraries/aws_operations_contact.rb b/libraries/aws_operations_contact.rb index 6d93c5ef9..be6316d20 100644 --- a/libraries/aws_operations_contact.rb +++ b/libraries/aws_operations_contact.rb @@ -1,8 +1,8 @@ -require 'aws_backend' +require "aws_backend" class AwsOperationsAccount < AwsResourceBase - name 'aws_operations_contact' - desc 'Verifies the operations contact information for an AWS Account.' + name "aws_operations_contact" + desc "Verifies the operations contact information for an AWS Account." example <<~EXAMPLE describe aws_operations_account do it { should be_configured } @@ -23,14 +23,14 @@ class AwsOperationsAccount < AwsResourceBase def initialize(opts = {}) super(opts) @raw_data = {} - @title, @name, @email_address, @phone_number = String.new + @title, @name, @email_address, @phone_number = "" validate_parameters begin catch_aws_errors { @aws_account_id = fetch_aws_account } - @api_response = fetch_aws_alternate_contact('operations') + @api_response = fetch_aws_alternate_contact("operations") rescue Aws::Account::Errors::ResourceNotFoundException skip_resource( - 'The Operations contact has not been configured for this AWS Account.', + "The Operations contact has not been configured for this AWS Account.", ) return [] if !@api_response || @api_response.empty? end @@ -56,7 +56,7 @@ def resource_id if @aws_account_id "AWS Operations Contact for account: #{@aws_account_id}" else - 'AWS Operations Contact Information' + "AWS Operations Contact Information" end end @@ -64,7 +64,7 @@ def to_s if @aws_account_id "AWS Operations Contact for account: #{@aws_account_id}" else - 'AWS Account Primary Contact' + "AWS Account Primary Contact" end end @@ -72,7 +72,7 @@ def to_s def fetch_aws_account arn = @aws.sts_client.get_caller_identity({}).arn - arn.split(':')[4] + arn.split(":")[4] end def fetch_aws_alternate_contact(type) diff --git a/libraries/aws_primary_contact.rb b/libraries/aws_primary_contact.rb index a6cab3130..eb5a07f3b 100644 --- a/libraries/aws_primary_contact.rb +++ b/libraries/aws_primary_contact.rb @@ -1,8 +1,8 @@ -require 'aws_backend' +require "aws_backend" class AwsPrimaryAccount < AwsResourceBase - name 'aws_primary_contact' - desc 'Verifies the primary contact information for an AWS Account.' + name "aws_primary_contact" + desc "Verifies the primary contact information for an AWS Account." example <<~EXAMPLE describe aws_primary_contact do it { should be_configured } @@ -41,7 +41,7 @@ def initialize(opts = {}) @postal_code, @state_or_region, @website_url = - String.new + "" super(opts) validate_parameters begin @@ -50,7 +50,7 @@ def initialize(opts = {}) @aws.account_client.get_contact_information.contact_information rescue Aws::Account::Errors::ResourceNotFoundException skip_resource( - 'The Primary contact has not been configured for this AWS Account.', + "The Primary contact has not been configured for this AWS Account.", ) return [] if !@api_response || @api_response.empty? end @@ -76,7 +76,7 @@ def resource_id if @aws_account_id "AWS Primary Contact for account: #{@aws_account_id}" else - 'AWS Account Primary Contact Information' + "AWS Account Primary Contact Information" end end @@ -84,7 +84,7 @@ def to_s if @aws_account_id "AWS Primary Contact for account: #{@aws_account_id}" else - 'AWS Account Primary Contact' + "AWS Account Primary Contact" end end @@ -92,6 +92,6 @@ def to_s def fetch_aws_account arn = @aws.sts_client.get_caller_identity({}).arn - arn.split(':')[4] + arn.split(":")[4] end end diff --git a/libraries/aws_rds_instances.rb b/libraries/aws_rds_instances.rb index a3f47cdd3..bcf77567d 100644 --- a/libraries/aws_rds_instances.rb +++ b/libraries/aws_rds_instances.rb @@ -1,8 +1,8 @@ -require 'aws_backend' +require "aws_backend" class AwsRdsInstances < AwsCollectionResourceBase - name 'aws_rds_instances' - desc 'Verifies settings for AWS RDS instances in bulk.' + name "aws_rds_instances" + desc "Verifies settings for AWS RDS instances in bulk." example " describe aws_rds_instances do it { should exist } diff --git a/libraries/aws_region.rb b/libraries/aws_region.rb index e145d2685..725a353c0 100644 --- a/libraries/aws_region.rb +++ b/libraries/aws_region.rb @@ -1,9 +1,9 @@ -require 'aws_backend' -require 'pry' +require "aws_backend" +require "pry" class AwsRegion < AwsResourceBase - name 'aws_region' - desc 'Verifies settings for an AWS region.' + name "aws_region" + desc "Verifies settings for an AWS region." example " describe aws_region('eu-west-2') do diff --git a/libraries/aws_regions.rb b/libraries/aws_regions.rb index 61bc972ce..d4a7988da 100644 --- a/libraries/aws_regions.rb +++ b/libraries/aws_regions.rb @@ -1,8 +1,8 @@ -require 'aws_backend' +require "aws_backend" class AwsRegions < AwsResourceBase - name 'aws_regions' - desc 'Verifies settings for AWS Regions in bulk.' + name "aws_regions" + desc "Verifies settings for AWS Regions in bulk." example " describe aws_regions do @@ -32,7 +32,7 @@ def fetch_data @regions = @aws.compute_client.describe_regions.to_h[:regions] end return [] if !@regions || @regions.empty? - region_opt_status = '' + region_opt_status = "" @regions.each do |region| catch_aws_errors do region_opt_status = fetch_region_opt_status(region[:region_name]) diff --git a/libraries/aws_s3_bucket.rb b/libraries/aws_s3_bucket.rb index 5678deedc..07e98513f 100644 --- a/libraries/aws_s3_bucket.rb +++ b/libraries/aws_s3_bucket.rb @@ -1,16 +1,16 @@ -require 'aws_backend' -require 'hashie/mash' +require "aws_backend" +require "hashie/mash" class AwsS3Bucket < AwsResourceBase - name 'aws_s3_bucket' - desc 'Verifies settings for a s3 bucket.' + name "aws_s3_bucket" + desc "Verifies settings for a s3 bucket." example " describe aws_s3_bucket(bucket_name: 'test_bucket') do it { should exist } end " - attr_reader :region, :bucket_name, :versioning + attr_reader :region, :bucket_name def initialize(opts = {}) opts = { bucket_name: opts } if opts.is_a?(String) @@ -24,10 +24,10 @@ def initialize(opts = {}) @region = @aws.storage_client.get_bucket_location(bucket: @bucket_name).location_constraint # LocationConstraint "EU" correlates to the region "eu-west-1", but region "EU" does not exist as a "region", only a LocationConstraint # this currently is the only Location constraint that can have either of 2 values "EU" or "eu-west-1". But only "eu-west-1" is a region - @region = 'eu-west-1' if @region == 'EU' + @region = "eu-west-1" if @region == "EU" # Forcing bucket region for future bucket calls to avoid warnings about multiple unnecessary # redirects and signing attempts. - opts[:aws_region] = @region.empty? ? 'us-east-1' : @region + opts[:aws_region] = @region.empty? ? "us-east-1" : @region super(opts) rescue Aws::S3::Errors::NoSuchBucket @region = nil @@ -62,8 +62,8 @@ def public? @bucket_policy_status_public = false # preserves the original behavior end @bucket_policy_status_public || \ - bucket_acl.any? { |g| g.grantee.type == 'Group' && g.grantee.uri =~ /AllUsers/ } || \ - bucket_acl.any? { |g| g.grantee.type == 'Group' && g.grantee.uri =~ /AuthenticatedUsers/ } + bucket_acl.any? { |g| g.grantee.type == "Group" && g.grantee.uri =~ /AllUsers/ } || \ + bucket_acl.any? { |g| g.grantee.type == "Group" && g.grantee.uri =~ /AuthenticatedUsers/ } end end @@ -79,7 +79,7 @@ def prevent_public_access? @prevent_public_access = begin public_access_config = @aws.storage_client.get_public_access_block(bucket: @bucket_name).public_access_block_configuration - rescue Aws::S3::Errors::NoSuchPublicAccessBlockConfiguration => e + rescue Aws::S3::Errors::NoSuchPublicAccessBlockConfiguration @prevent_public_access = false end return false unless @prevent_public_access @@ -94,7 +94,7 @@ def prevent_public_access_by_account? @prevent_public_access_by_account = begin public_access_account_config = @aws.storage_control_client.get_public_access_block(account_id: @account_id).public_access_block_configuration - rescue Aws::S3::Errors::NoSuchPublicAccessBlockConfiguration => e + rescue Aws::S3::Errors::NoSuchPublicAccessBlockConfiguration @prevent_public_access_by_account = false end return false unless @prevent_public_access_by_account @@ -119,7 +119,7 @@ def has_default_encryption_enabled? def has_versioning_enabled? return false unless exists? catch_aws_errors do - @has_versioning_enabled = @aws.storage_client.get_bucket_versioning(bucket: @bucket_name).status == 'Enabled' + @has_versioning_enabled = @aws.storage_client.get_bucket_versioning(bucket: @bucket_name).status == "Enabled" end end @@ -131,7 +131,7 @@ def versioning end def has_secure_transport_enabled? - bucket_policy.any? { |s| s.effect == 'Deny' && s.condition && s.condition['Bool'] && s.condition['Bool']['aws:SecureTransport'] && s.condition['Bool']['aws:SecureTransport'] == 'false' } + bucket_policy.any? { |s| s.effect == "Deny" && s.condition && s.condition["Bool"] && s.condition["Bool"]["aws:SecureTransport"] && s.condition["Bool"]["aws:SecureTransport"] == "false" } end # below is to preserve the original 'unsupported' function but isn't used in the above @@ -146,7 +146,7 @@ def fetch_bucket_policy # AWS SDK returns a StringIO, we have to read() raw_policy = @aws.storage_client.get_bucket_policy(bucket: @bucket_name).to_h return [] if !raw_policy.key?(:policy) - JSON.parse(raw_policy[:policy].read)['Statement'].map do |statement| + JSON.parse(raw_policy[:policy].read)["Statement"].map do |statement| lowercase_hash = {} statement.each_key { |k| lowercase_hash[k.downcase] = statement[k] } policy_list += [OpenStruct.new(lowercase_hash)] @@ -191,6 +191,6 @@ def to_s def fetch_aws_account arn = @aws.sts_client.get_caller_identity({}).arn - arn.split(':')[4] + arn.split(":")[4] end end diff --git a/libraries/aws_s3_buckets.rb b/libraries/aws_s3_buckets.rb index 20d39f57e..371810fa7 100644 --- a/libraries/aws_s3_buckets.rb +++ b/libraries/aws_s3_buckets.rb @@ -38,7 +38,7 @@ def fetch_data end @api_response.each do |resp| resp.buckets.each do |bucket| - bucket_rows += [{ bucket_name: bucket[:name]}] + bucket_rows += [{ bucket_name: bucket[:name] }] end end @table = bucket_rows diff --git a/libraries/aws_security_contact.rb b/libraries/aws_security_contact.rb index c92cfde7d..afb095f9a 100644 --- a/libraries/aws_security_contact.rb +++ b/libraries/aws_security_contact.rb @@ -1,8 +1,8 @@ -require 'aws_backend' +require "aws_backend" class AwsSecurityAccount < AwsResourceBase - name 'aws_security_contact' - desc 'Verifies the security contact information for an AWS Account.' + name "aws_security_contact" + desc "Verifies the security contact information for an AWS Account." example <<~EXAMPLE describe aws_security_account do it { should be_configured } @@ -23,14 +23,14 @@ class AwsSecurityAccount < AwsResourceBase def initialize(opts = {}) super(opts) @raw_data = {} - @title, @name, @email_address, @phone_number = String.new + @title, @name, @email_address, @phone_number = "" validate_parameters begin catch_aws_errors { @aws_account_id = fetch_aws_account } - @api_response = fetch_aws_alternate_contact('security') + @api_response = fetch_aws_alternate_contact("security") rescue Aws::Account::Errors::ResourceNotFoundException skip_resource( - 'The Security contact has not been configured for this AWS Account.', + "The Security contact has not been configured for this AWS Account.", ) return [] if !@api_response || @api_response.empty? end @@ -56,7 +56,7 @@ def resource_id if @aws_account_id "AWS Security Contact for account: #{@aws_account_id}" else - 'AWS Security Contact Information' + "AWS Security Contact Information" end end @@ -64,7 +64,7 @@ def to_s if @aws_account_id "AWS Security Contact for account: #{@aws_account_id}" else - 'AWS Account Primary Contact' + "AWS Account Primary Contact" end end @@ -72,7 +72,7 @@ def to_s def fetch_aws_account arn = @aws.sts_client.get_caller_identity({}).arn - arn.split(':')[4] + arn.split(":")[4] end def fetch_aws_alternate_contact(type) diff --git a/libraries/aws_securityhub.rb b/libraries/aws_securityhub.rb index e7707e4ca..1c17c06b0 100644 --- a/libraries/aws_securityhub.rb +++ b/libraries/aws_securityhub.rb @@ -17,7 +17,7 @@ def initialize(opts = {}) @res = {} @describe_hub = [] super(opts) - validate_parameters() + validate_parameters catch_aws_errors do @describe_hub = @aws.securityhub_client.describe_hub @res = @describe_hub.to_h.presence || {} @@ -33,7 +33,7 @@ def subscribed? alias exist? subscribed? def resource_id - @res[:hub_arn].presence || '' + @res[:hub_arn].presence || "" end def to_s From 96238e479a364f5cc07bd2386bdcc5a6133df3a4 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Sun, 3 Dec 2023 18:45:20 -0500 Subject: [PATCH 41/93] fixed rubocop error - W: Lint/DuplicateBranch: Duplicate branch body etected. rescue Aws::AccessAnalyzer::Errors => e ... ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Signed-off-by: Aaron Lippold --- libraries/aws_backend.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/aws_backend.rb b/libraries/aws_backend.rb index 6823cec6c..3ddaa6597 100644 --- a/libraries/aws_backend.rb +++ b/libraries/aws_backend.rb @@ -505,10 +505,6 @@ def catch_aws_errors Inspec::Log.warn(e.message.to_s) skip_resource(e.message.to_s) nil - rescue Aws::AccessAnalyzer::Errors => e - Inspec::Log.warn(e.message.to_s) - skip_resource(e.message.to_s) - nil rescue Aws::Errors::MissingCredentialsError Inspec::Log.error("It appears that you have not set your AWS credentials. See https://www.inspec.io/docs/reference/platforms for details.") fail_resource("No AWS credentials available") @@ -520,6 +516,10 @@ def catch_aws_errors Inspec::Log.error("The endpoint that is trying to be accessed does not exist.") fail_resource("Invalid Endpoint error") nil + rescue Aws::AccessAnalyzer::Errors::ServiceError => e + Inspec::Log.warn(e.message) + skip_resource(e.message) + nil rescue Aws::S3::Errors::NoSuchPublicAccessBlockConfiguration Inspec::Log.error("No public access block configuration was found") skip_resource("No public access block configuration was found") From 2951f87c31bc82261eafc46e93b1dcdb5f312639 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Sun, 3 Dec 2023 22:26:09 -0500 Subject: [PATCH 42/93] reverted cloudtrail resource for now to keep ci passing Signed-off-by: Aaron Lippold --- .gitignore | 1 + libraries/aws_cloudtrail_trail.rb | 10 +++------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 979c01916..7f786ed6d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ Gemfile.lock inspec.lock .kitchen +*.code-workspace *.plan *.tfstate* local diff --git a/libraries/aws_cloudtrail_trail.rb b/libraries/aws_cloudtrail_trail.rb index b6fdc64f1..9b557d74c 100644 --- a/libraries/aws_cloudtrail_trail.rb +++ b/libraries/aws_cloudtrail_trail.rb @@ -11,7 +11,7 @@ class AwsCloudTrailTrail < AwsResourceBase attr_reader :cloud_watch_logs_log_group_arn, :cloud_watch_logs_role_arn, :home_region, :trail_name, :kms_key_id, :s3_bucket_name, :s3_key_prefix, :trail_arn, :is_multi_region_trail, - :log_file_validation_enabled, :is_organization_trail + :log_file_validation_enabled, :is_organization_trail, :event_selectors alias multi_region_trail? is_multi_region_trail alias log_file_validation_enabled? log_file_validation_enabled @@ -24,6 +24,7 @@ def initialize(opts = {}) validate_parameters(required: [:trail_name]) @trail_name = opts[:trail_name] + @event_selectors = [] catch_aws_errors do resp = @aws.cloudtrail_client.describe_trails({ trail_name_list: [@trail_name] }) @trail = resp.trail_list[0].to_h @@ -78,16 +79,11 @@ def get_log_group_for_multi_region_active_mgmt_rw_all end # TODO: see what happens when running against nil event selectors - def event_selectors - catch_aws_errors do - @event_selectors = @aws.cloudtrail_client.get_event_selectors(trail_name: @trail_name) - end - end - def has_event_selector_mgmt_events_rw_type_all? return nil unless exists? event_selector_found = false begin + @event_selectors = @aws.cloudtrail_client.get_event_selectors(trail_name: @trail_name) @event_selectors.event_selectors.each do |es| event_selector_found = true if es.read_write_type == "All" && es.include_management_events == true end From 12e38b16ec8c925e3b66d1869373f5d8865c6483 Mon Sep 17 00:00:00 2001 From: wdower Date: Mon, 4 Dec 2023 11:47:40 -0500 Subject: [PATCH 43/93] wip on cloudtrail saner event selector functions Signed-off-by: wdower --- libraries/aws_cloudtrail_trail.rb | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/libraries/aws_cloudtrail_trail.rb b/libraries/aws_cloudtrail_trail.rb index 9b557d74c..758272346 100644 --- a/libraries/aws_cloudtrail_trail.rb +++ b/libraries/aws_cloudtrail_trail.rb @@ -38,6 +38,7 @@ def initialize(opts = {}) @cloud_watch_logs_role_arn = @trail[:cloud_watch_logs_role_arn] @log_file_validation_enabled = @trail[:log_file_validation_enabled] @cloud_watch_logs_log_group_arn = @trail[:cloud_watch_logs_log_group_arn] + @event_selectors = @aws.cloudtrail_client.get_event_selectors(trail_name: @trail_name) end end @@ -83,7 +84,6 @@ def has_event_selector_mgmt_events_rw_type_all? return nil unless exists? event_selector_found = false begin - @event_selectors = @aws.cloudtrail_client.get_event_selectors(trail_name: @trail_name) @event_selectors.event_selectors.each do |es| event_selector_found = true if es.read_write_type == "All" && es.include_management_events == true end @@ -93,6 +93,31 @@ def has_event_selector_mgmt_events_rw_type_all? event_selector_found end + + # describe aws_cloudtrail_trail(x) do + # it { should be_monitoring_read("arn::whatever::s3") } + # it { should be_monitoring_write("arn::whatever::s3") } + # it { should be_using_advanced_event_selectors } + # it { should be_using_basic_event_selectors } + # it { should be_multi_region_trail } + # end + + def monitoring_read?(aws_object) + # TODO + end + + def monitoring_write?(aws_object) + # TODO + end + + def using_advanced_event_selectors? + @event_selectors.advanced_event_selectors.present? + end + + def using_basic_event_selectors? + @event_selectors.event_selectors.present? + end + def exists? !@trail.nil? && !@trail.empty? end From 1580b88c533f653bac95c508cb54fae7a43a64b4 Mon Sep 17 00:00:00 2001 From: wdower Date: Mon, 4 Dec 2023 22:01:05 -0500 Subject: [PATCH 44/93] wip cloudtrail Signed-off-by: wdower --- libraries/aws_cloudtrail_trail.rb | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/libraries/aws_cloudtrail_trail.rb b/libraries/aws_cloudtrail_trail.rb index 758272346..ac465045c 100644 --- a/libraries/aws_cloudtrail_trail.rb +++ b/libraries/aws_cloudtrail_trail.rb @@ -93,7 +93,6 @@ def has_event_selector_mgmt_events_rw_type_all? event_selector_found end - # describe aws_cloudtrail_trail(x) do # it { should be_monitoring_read("arn::whatever::s3") } # it { should be_monitoring_write("arn::whatever::s3") } @@ -102,12 +101,31 @@ def has_event_selector_mgmt_events_rw_type_all? # it { should be_multi_region_trail } # end - def monitoring_read?(aws_object) - # TODO + def monitoring?(aws_resource_type, mode) + if using_basic_event_selectors? + basic_mode = mode == 'r' ? "ReadOnly" : "WriteOnly" + @event_selectors.event_selectors.any? { |es| + es.read_write_type.match?(/All|#{basic_mode}/) && + es.data_resources.any? { |dr| + dr.values.include?(aws_resource_type) + } + } + else + advanced_mode = mode == 'r' + @event_selectors.advanced_event_selectors.any? { |es| + es.field_selectors.any? { |fs| + + } + } + end + end + + def monitoring_read?(aws_resource_type) + monitoring?(aws_resource_type, 'r') end - def monitoring_write?(aws_object) - # TODO + def monitoring_write?(aws_resource_type) + monitoring?(aws_resource_type, 'w') end def using_advanced_event_selectors? From e4ec3b89da54963e039297ad5971696afb229a6e Mon Sep 17 00:00:00 2001 From: wdower Date: Mon, 4 Dec 2023 22:56:30 -0500 Subject: [PATCH 45/93] working functions for monitoring reads and writes of resource types Signed-off-by: wdower --- libraries/aws_cloudtrail_trail.rb | 40 +++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/libraries/aws_cloudtrail_trail.rb b/libraries/aws_cloudtrail_trail.rb index ac465045c..2051df411 100644 --- a/libraries/aws_cloudtrail_trail.rb +++ b/libraries/aws_cloudtrail_trail.rb @@ -6,6 +6,9 @@ class AwsCloudTrailTrail < AwsResourceBase example <<-EXAMPLE describe aws_cloudtrail_trail('TRIAL_NAME') do it { should exist } + it { should be_monitoring_read("AWS::S3::Object") } + it { should be_monitoring_write("AWS::S3::Object") } + it { should be_multi_region_trail } end EXAMPLE @@ -93,28 +96,39 @@ def has_event_selector_mgmt_events_rw_type_all? event_selector_found end - # describe aws_cloudtrail_trail(x) do - # it { should be_monitoring_read("arn::whatever::s3") } - # it { should be_monitoring_write("arn::whatever::s3") } - # it { should be_using_advanced_event_selectors } - # it { should be_using_basic_event_selectors } - # it { should be_multi_region_trail } - # end - def monitoring?(aws_resource_type, mode) + # basic event selectors have a simpler structure than the advanced ones - check basic first if using_basic_event_selectors? + puts "BASIC" + puts aws_resource_type basic_mode = mode == 'r' ? "ReadOnly" : "WriteOnly" @event_selectors.event_selectors.any? { |es| es.read_write_type.match?(/All|#{basic_mode}/) && es.data_resources.any? { |dr| - dr.values.include?(aws_resource_type) + dr.type.include?(aws_resource_type) && + dr.values.all? { |val| # make sure the values do not indicate individual resources + val.split(/[:\/]/).count <= 3 # can be of the form 'arn:aws:s3' but not + # 'arn:aws:s3:::/' + } } } - else - advanced_mode = mode == 'r' + else + readOnly = mode == 'r' @event_selectors.advanced_event_selectors.any? { |es| - es.field_selectors.any? { |fs| - + (es.field_selectors.any? { |fs| # check if readOnly is explicitly set to true + fs.field == "readOnly" && fs.equals == [readOnly.to_s] # note that AdvancedFieldSelector has a field named "equals" + # also note that designating an AFS as writeOnly means setting + # the readOnly field to 'false' + } || + es.field_selectors.none? { |fs| # or check if readOnly is unset entirely (means both read and write are logged) + fs.field == "readOnly" + }) && + es.field_selectors.any? { |fs| # check if some other field selector is set to the right resource type + fs.field == "resources.type" && fs.equals == [aws_resource_type] + } && + es.field_selectors.none? { |fs| # check that no other event selector is tracking an individual arn + # if no arn field is set, cloudtrail is tracking the whole type + fs.field.downcase == "resources.arn" } } end From 6a8e6ccfdc9e87194e69894cecb12eab2168f955 Mon Sep 17 00:00:00 2001 From: wdower Date: Mon, 4 Dec 2023 23:04:56 -0500 Subject: [PATCH 46/93] removing errant puts statement Signed-off-by: wdower --- libraries/aws_cloudtrail_trail.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/libraries/aws_cloudtrail_trail.rb b/libraries/aws_cloudtrail_trail.rb index 2051df411..178f88012 100644 --- a/libraries/aws_cloudtrail_trail.rb +++ b/libraries/aws_cloudtrail_trail.rb @@ -99,8 +99,6 @@ def has_event_selector_mgmt_events_rw_type_all? def monitoring?(aws_resource_type, mode) # basic event selectors have a simpler structure than the advanced ones - check basic first if using_basic_event_selectors? - puts "BASIC" - puts aws_resource_type basic_mode = mode == 'r' ? "ReadOnly" : "WriteOnly" @event_selectors.event_selectors.any? { |es| es.read_write_type.match?(/All|#{basic_mode}/) && From b8c4d667d50d0adee153115ad5a0dc52f87ebb53 Mon Sep 17 00:00:00 2001 From: wdower Date: Tue, 5 Dec 2023 16:16:44 -0500 Subject: [PATCH 47/93] linting cloudtrail resource, adding macie resource first try Signed-off-by: wdower --- libraries/aws_backend.rb | 23 +++++++++++----- libraries/aws_cloudtrail_trail.rb | 44 +++++++++++++++---------------- libraries/aws_macie.rb | 43 ++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 29 deletions(-) create mode 100644 libraries/aws_macie.rb diff --git a/libraries/aws_backend.rb b/libraries/aws_backend.rb index 3ddaa6597..301f394a2 100644 --- a/libraries/aws_backend.rb +++ b/libraries/aws_backend.rb @@ -65,6 +65,8 @@ require "aws-sdk-apigatewayv2" require "aws-sdk-account" require "aws-sdk-accessanalyzer" +require "aws-sdk-macie2" +require "aws-sdk-wafv2" # AWS Inspec Backend Classes # @@ -355,6 +357,10 @@ def access_analyzer_client def partitions_region_client aws_client(Aws::Partitions::Region::Client) end + + def macie_client + aws_client(Aws::Macie2::Client) + end end # Base class for AWS resources @@ -501,7 +507,14 @@ def name # Intercept AWS exceptions def catch_aws_errors yield # Catch and create custom messages as needed - rescue Aws::Account::Errors::ResourceNotFoundException => e + + basic_exceptions = [ + Aws::Account::Errors::ResourceNotFoundException, + Aws::AccessAnalyzer::Errors::ServiceError, + Aws::Macie2::Errors::ServiceError, + ] + + rescue *basic_exceptions => e Inspec::Log.warn(e.message.to_s) skip_resource(e.message.to_s) nil @@ -516,10 +529,6 @@ def catch_aws_errors Inspec::Log.error("The endpoint that is trying to be accessed does not exist.") fail_resource("Invalid Endpoint error") nil - rescue Aws::AccessAnalyzer::Errors::ServiceError => e - Inspec::Log.warn(e.message) - skip_resource(e.message) - nil rescue Aws::S3::Errors::NoSuchPublicAccessBlockConfiguration Inspec::Log.error("No public access block configuration was found") skip_resource("No public access block configuration was found") @@ -544,8 +553,8 @@ def catch_aws_errors raise Inspec::Exceptions::ResourceFailed, error_message else Inspec::Log.warn "AWS Service Error encountered running a control with Resource #{@__resource_name__}. " \ - "Error message: #{e.message} You should address this error to ensure your controls are " \ - "behaving as expected." + "Error message: #{e.message} You should address this error to ensure your controls are " \ + "behaving as expected." @failed_resource = true end nil diff --git a/libraries/aws_cloudtrail_trail.rb b/libraries/aws_cloudtrail_trail.rb index 178f88012..5b4b297a4 100644 --- a/libraries/aws_cloudtrail_trail.rb +++ b/libraries/aws_cloudtrail_trail.rb @@ -99,45 +99,45 @@ def has_event_selector_mgmt_events_rw_type_all? def monitoring?(aws_resource_type, mode) # basic event selectors have a simpler structure than the advanced ones - check basic first if using_basic_event_selectors? - basic_mode = mode == 'r' ? "ReadOnly" : "WriteOnly" + basic_mode = mode == "r" ? "ReadOnly" : "WriteOnly" @event_selectors.event_selectors.any? { |es| es.read_write_type.match?(/All|#{basic_mode}/) && - es.data_resources.any? { |dr| - dr.type.include?(aws_resource_type) && - dr.values.all? { |val| # make sure the values do not indicate individual resources - val.split(/[:\/]/).count <= 3 # can be of the form 'arn:aws:s3' but not - # 'arn:aws:s3:::/' - } - } + es.data_resources.any? { |dr| + dr.type.include?(aws_resource_type) && + dr.values.all? { |val| # make sure the values do not indicate individual resources + val.split(%r{[:/]}).count <= 3 # can be of the form 'arn:aws:s3' but not + # 'arn:aws:s3:::/' + } + } } - else - readOnly = mode == 'r' + else + read_only = mode == "r" @event_selectors.advanced_event_selectors.any? { |es| (es.field_selectors.any? { |fs| # check if readOnly is explicitly set to true - fs.field == "readOnly" && fs.equals == [readOnly.to_s] # note that AdvancedFieldSelector has a field named "equals" - # also note that designating an AFS as writeOnly means setting - # the readOnly field to 'false' + fs.field == "readOnly" && fs.equals == [read_only.to_s] # note that AdvancedFieldSelector has a field named "equals" + # also note that designating an AFS as writeOnly means setting + # the readOnly field to 'false' } || es.field_selectors.none? { |fs| # or check if readOnly is unset entirely (means both read and write are logged) fs.field == "readOnly" }) && - es.field_selectors.any? { |fs| # check if some other field selector is set to the right resource type - fs.field == "resources.type" && fs.equals == [aws_resource_type] - } && - es.field_selectors.none? { |fs| # check that no other event selector is tracking an individual arn - # if no arn field is set, cloudtrail is tracking the whole type - fs.field.downcase == "resources.arn" - } + es.field_selectors.any? { |fs| # check if some other field selector is set to the right resource type + fs.field == "resources.type" && fs.equals == [aws_resource_type] + } && + es.field_selectors.none? { |fs| # check that no other event selector is tracking an individual arn + # if no arn field is set, cloudtrail is tracking the whole type + fs.field.downcase == "resources.arn" + } } end end def monitoring_read?(aws_resource_type) - monitoring?(aws_resource_type, 'r') + monitoring?(aws_resource_type, "r") end def monitoring_write?(aws_resource_type) - monitoring?(aws_resource_type, 'w') + monitoring?(aws_resource_type, "w") end def using_advanced_event_selectors? diff --git a/libraries/aws_macie.rb b/libraries/aws_macie.rb new file mode 100644 index 000000000..623ca215b --- /dev/null +++ b/libraries/aws_macie.rb @@ -0,0 +1,43 @@ +require "aws_backend" + +class AWSMacie < AwsResourceBase + name "aws_macie" + desc "Gets information about Macie status and configuration." + + example " + describe aws_macie do + it { should be_enabled } + it { should be_monitoring(['arn1', 'arn2', 'arn3']) } + end + " + + attr_reader :monitoring_list, :enabled, :jobs + + def initialize(opts = {}) + @raw_data = {} + @res = {} + @describe_hub = [] + super(opts) + validate_parameters + catch_aws_errors do + @session = @aws.macie_client.get_macie_session + @jobs = @aws.macie_client.list_classification_jobs + @buckets = @aws.macie_client.describe_buckets + @organization_configuration = @aws.macie_client.describe_organization_configuration + end + end + + def enabled? + @session.status == "ENABLED" + end + + # def monitoring?(bucket_list) + # @jobs.any? { |job| + # job. + # } + # end + + def to_s + "AWS Macie" + end +end From 9a99e2671170e70118de7443d6f4b2de31010169 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Tue, 5 Dec 2023 16:58:16 -0500 Subject: [PATCH 48/93] small fix Signed-off-by: Aaron Lippold --- libraries/aws_alb.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libraries/aws_alb.rb b/libraries/aws_alb.rb index 315b24b35..d64f8e6b2 100644 --- a/libraries/aws_alb.rb +++ b/libraries/aws_alb.rb @@ -1,8 +1,8 @@ -require "aws_backend" +require 'aws_backend' class AwsAlb < AwsResourceBase - name "aws_alb" - desc "Verifies settings for an Application Load Balancer." + name 'aws_alb' + desc 'Verifies settings for an Application Load Balancer.' example <<-EXAMPLE describe aws_alb('arn:aws:elasticloadbalancing') do it { should exist } @@ -45,8 +45,8 @@ def resource_id def access_log_enabled return unless alb_attributes - s3_enabled_attr = alb_attributes.find { |attr| attr.key.eql?("access_logs.s3.enabled") } - @access_log_enabled = s3_enabled_attr&.value == "true" + s3_enabled_attr = alb_attributes.find { |attr| attr.key.eql?('access_logs.s3.enabled') } + @access_log_enabled = s3_enabled_attr&.value == 'true' end def listeners @@ -56,7 +56,7 @@ def listeners end def ssl_policies - @ssl_policies ||= listeners.filter_map { |listener| listener.ssl_policy if listener.protocol == "HTTPS" }.uniq + @ssl_policies ||= listeners.filter_map { |listener| listener.ssl_policy if listener.protocol == 'HTTPS' }.uniq end def external_ports From 88a2a58f7fded08613e7a86484e77d065fcfb320 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Tue, 5 Dec 2023 20:50:05 -0500 Subject: [PATCH 49/93] small changes to the start of the aws_macie base resource Signed-off-by: Aaron Lippold --- libraries/aws_backend.rb | 1 + libraries/aws_macie.rb | 43 +++++++++++++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/libraries/aws_backend.rb b/libraries/aws_backend.rb index 301f394a2..c5876a991 100644 --- a/libraries/aws_backend.rb +++ b/libraries/aws_backend.rb @@ -512,6 +512,7 @@ def catch_aws_errors Aws::Account::Errors::ResourceNotFoundException, Aws::AccessAnalyzer::Errors::ServiceError, Aws::Macie2::Errors::ServiceError, + Aws::Macie2::Errors::ResourceNotFoundException ] rescue *basic_exceptions => e diff --git a/libraries/aws_macie.rb b/libraries/aws_macie.rb index 623ca215b..1128404fa 100644 --- a/libraries/aws_macie.rb +++ b/libraries/aws_macie.rb @@ -1,8 +1,8 @@ -require "aws_backend" +require 'aws_backend' class AWSMacie < AwsResourceBase - name "aws_macie" - desc "Gets information about Macie status and configuration." + name 'aws_macie' + desc 'Gets information about Macie status and configuration.' example " describe aws_macie do @@ -11,7 +11,7 @@ class AWSMacie < AwsResourceBase end " - attr_reader :monitoring_list, :enabled, :jobs + attr_reader :session, :jobs, :buckets, :organization_configuration def initialize(opts = {}) @raw_data = {} @@ -19,16 +19,45 @@ def initialize(opts = {}) @describe_hub = [] super(opts) validate_parameters + + fetch_data + end + + def fetch_data + require 'pry'; binding.pry catch_aws_errors do @session = @aws.macie_client.get_macie_session @jobs = @aws.macie_client.list_classification_jobs @buckets = @aws.macie_client.describe_buckets - @organization_configuration = @aws.macie_client.describe_organization_configuration + # Can't do this until we setup the user to be a Macie Admin? + # @organization_configuration = @aws.macie_client.describe_organization_configuration end end + # jobs.items.first.bucket_definitions.first.to_h[:buckets] + # aws.macie_client.describe_buckets.buckets.count + # aws.macie_client.list_classification_jobs.items.first['bucket_definitions'] + + # it may be more straitforward to have multiple small resources so this one doesn't get hug + + # aws_macie base resource + # - this may have 'session' method via get_macie_session + # - members via list_members + # - list_organization_admin_accounts ? + # - perhaps owner etc. + # - usuage_totals - via get_usage_totals + # aws_macie_jobs ... + # aws_macie_job(job_id) ... + # aws_macie_buckets + # aws_macie_buckets(bucket_name or arn) + # aws_macie_findings + # - list_findings + # aws_macie_finding + # - get_finding_statistics(finding_id) + + def enabled? - @session.status == "ENABLED" + @session.status == 'ENABLED' end # def monitoring?(bucket_list) @@ -38,6 +67,6 @@ def enabled? # end def to_s - "AWS Macie" + 'AWS Macie' end end From cf3345f9ef2fb386516f3602f35f29ce7c44e483 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Tue, 5 Dec 2023 23:06:40 -0500 Subject: [PATCH 50/93] - fixed aws-backend catch errros by putting back to old style - ran bundle exec rubocop:auto_correct Signed-off-by: Aaron Lippold --- libraries/aws_alb.rb | 12 ++++++------ libraries/aws_backend.rb | 26 +++++++++++++++----------- libraries/aws_iam_users.rb | 2 +- libraries/aws_macie.rb | 14 ++++++-------- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/libraries/aws_alb.rb b/libraries/aws_alb.rb index d64f8e6b2..315b24b35 100644 --- a/libraries/aws_alb.rb +++ b/libraries/aws_alb.rb @@ -1,8 +1,8 @@ -require 'aws_backend' +require "aws_backend" class AwsAlb < AwsResourceBase - name 'aws_alb' - desc 'Verifies settings for an Application Load Balancer.' + name "aws_alb" + desc "Verifies settings for an Application Load Balancer." example <<-EXAMPLE describe aws_alb('arn:aws:elasticloadbalancing') do it { should exist } @@ -45,8 +45,8 @@ def resource_id def access_log_enabled return unless alb_attributes - s3_enabled_attr = alb_attributes.find { |attr| attr.key.eql?('access_logs.s3.enabled') } - @access_log_enabled = s3_enabled_attr&.value == 'true' + s3_enabled_attr = alb_attributes.find { |attr| attr.key.eql?("access_logs.s3.enabled") } + @access_log_enabled = s3_enabled_attr&.value == "true" end def listeners @@ -56,7 +56,7 @@ def listeners end def ssl_policies - @ssl_policies ||= listeners.filter_map { |listener| listener.ssl_policy if listener.protocol == 'HTTPS' }.uniq + @ssl_policies ||= listeners.filter_map { |listener| listener.ssl_policy if listener.protocol == "HTTPS" }.uniq end def external_ports diff --git a/libraries/aws_backend.rb b/libraries/aws_backend.rb index c5876a991..575677c97 100644 --- a/libraries/aws_backend.rb +++ b/libraries/aws_backend.rb @@ -507,15 +507,7 @@ def name # Intercept AWS exceptions def catch_aws_errors yield # Catch and create custom messages as needed - - basic_exceptions = [ - Aws::Account::Errors::ResourceNotFoundException, - Aws::AccessAnalyzer::Errors::ServiceError, - Aws::Macie2::Errors::ServiceError, - Aws::Macie2::Errors::ResourceNotFoundException - ] - - rescue *basic_exceptions => e + rescue Aws::Account::Errors::ResourceNotFoundException => e Inspec::Log.warn(e.message.to_s) skip_resource(e.message.to_s) nil @@ -530,10 +522,22 @@ def catch_aws_errors Inspec::Log.error("The endpoint that is trying to be accessed does not exist.") fail_resource("Invalid Endpoint error") nil + rescue Aws::AccessAnalyzer::Errors::ServiceError => e + Inspec::Log.warn(e.message) + skip_resource(e.message) + nil rescue Aws::S3::Errors::NoSuchPublicAccessBlockConfiguration Inspec::Log.error("No public access block configuration was found") skip_resource("No public access block configuration was found") nil + rescue Aws::Macie2::Errors::ServiceError => e + Inspec::Log.error("Macie Service Error: #{e.message}") + skip_resource("Macie Service Error: #{e.message}") + nil + rescue Aws::Macie2::Errors::ResourceNotFoundException => e + Inspec::Log.error("Macie Resource: #{e.message}") + skip_resource("Macie Resource Error: #{e.message}") + nil rescue Aws::Errors::ServiceError => e if is_permissions_error(e) advice = "" @@ -554,8 +558,8 @@ def catch_aws_errors raise Inspec::Exceptions::ResourceFailed, error_message else Inspec::Log.warn "AWS Service Error encountered running a control with Resource #{@__resource_name__}. " \ - "Error message: #{e.message} You should address this error to ensure your controls are " \ - "behaving as expected." + "Error message: #{e.message} You should address this error to ensure your controls are " \ + "behaving as expected." @failed_resource = true end nil diff --git a/libraries/aws_iam_users.rb b/libraries/aws_iam_users.rb index 10f87d931..d6830c4be 100644 --- a/libraries/aws_iam_users.rb +++ b/libraries/aws_iam_users.rb @@ -21,7 +21,7 @@ class AwsIamUsers < AwsCollectionResourceBase .register_column(:attached_policy_names, field: :attached_policy_names, lazy_instance: :lazy_load_attached_policy_names) .register_column(:attached_policy_arns, field: :attached_policy_arns, lazy_instance: :lazy_load_attached_policy_arns) .register_column(:has_console_password, field: :has_console_password, lazy_instance: :lazy_load_has_console_password) - .register_column(:has_inline_policies, field: :has_inline_policies, lazy_instance: :lazy_load_has_inline_policies) + .register_column(:has_inline_policies, field: :has_inline_policies, lazy_instance: :lazy_load_has_inline_policies) .register_column(:inline_policy_names, field: :inline_policy_names, lazy_instance: :lazy_load_inline_policies) .register_column(:has_mfa_enabled, field: :has_mfa_enabled, lazy_instance: :lazy_load_has_mfa_enabled) .register_column(:password_ever_used?, field: :password_ever_used?) diff --git a/libraries/aws_macie.rb b/libraries/aws_macie.rb index 1128404fa..19ade3ccf 100644 --- a/libraries/aws_macie.rb +++ b/libraries/aws_macie.rb @@ -1,8 +1,8 @@ -require 'aws_backend' +require "aws_backend" class AWSMacie < AwsResourceBase - name 'aws_macie' - desc 'Gets information about Macie status and configuration.' + name "aws_macie" + desc "Gets information about Macie status and configuration." example " describe aws_macie do @@ -24,7 +24,6 @@ def initialize(opts = {}) end def fetch_data - require 'pry'; binding.pry catch_aws_errors do @session = @aws.macie_client.get_macie_session @jobs = @aws.macie_client.list_classification_jobs @@ -38,7 +37,7 @@ def fetch_data # aws.macie_client.describe_buckets.buckets.count # aws.macie_client.list_classification_jobs.items.first['bucket_definitions'] - # it may be more straitforward to have multiple small resources so this one doesn't get hug + # it may be more straitforward to have multiple small resources so this one doesn't get hug # aws_macie base resource # - this may have 'session' method via get_macie_session @@ -55,9 +54,8 @@ def fetch_data # aws_macie_finding # - get_finding_statistics(finding_id) - def enabled? - @session.status == 'ENABLED' + @session.status == "ENABLED" end # def monitoring?(bucket_list) @@ -67,6 +65,6 @@ def enabled? # end def to_s - 'AWS Macie' + "AWS Macie" end end From a05637a6fbea9c45f47a1adf54da00ec7ce07000 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Tue, 5 Dec 2023 23:19:49 -0500 Subject: [PATCH 51/93] linting Signed-off-by: Aaron Lippold --- libraries/aws_macie.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libraries/aws_macie.rb b/libraries/aws_macie.rb index 19ade3ccf..7ddb02123 100644 --- a/libraries/aws_macie.rb +++ b/libraries/aws_macie.rb @@ -1,8 +1,8 @@ -require "aws_backend" +require 'aws_backend' class AWSMacie < AwsResourceBase - name "aws_macie" - desc "Gets information about Macie status and configuration." + name 'aws_macie' + desc 'Gets information about Macie status and configuration.' example " describe aws_macie do @@ -37,7 +37,7 @@ def fetch_data # aws.macie_client.describe_buckets.buckets.count # aws.macie_client.list_classification_jobs.items.first['bucket_definitions'] - # it may be more straitforward to have multiple small resources so this one doesn't get hug + # it may be more strait forward to have multiple small resources so this one doesn't get hug # aws_macie base resource # - this may have 'session' method via get_macie_session @@ -55,7 +55,7 @@ def fetch_data # - get_finding_statistics(finding_id) def enabled? - @session.status == "ENABLED" + @session.status == 'ENABLED' end # def monitoring?(bucket_list) @@ -65,6 +65,6 @@ def enabled? # end def to_s - "AWS Macie" + 'AWS Macie' end end From 7a8a7bbefec321fba6f90f2351368150c2cbf0b8 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Wed, 6 Dec 2023 00:58:06 -0500 Subject: [PATCH 52/93] fixed the param being passed to get_events Signed-off-by: Aaron Lippold --- libraries/aws_cloudtrail_trail.rb | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/libraries/aws_cloudtrail_trail.rb b/libraries/aws_cloudtrail_trail.rb index 5b4b297a4..955434e9f 100644 --- a/libraries/aws_cloudtrail_trail.rb +++ b/libraries/aws_cloudtrail_trail.rb @@ -1,8 +1,8 @@ -require "aws_backend" +require 'aws_backend' class AwsCloudTrailTrail < AwsResourceBase - name "aws_cloudtrail_trail" - desc "Verifies settings for an individual AWS CloudTrail Trail." + name 'aws_cloudtrail_trail' + desc 'Verifies settings for an individual AWS CloudTrail Trail.' example <<-EXAMPLE describe aws_cloudtrail_trail('TRIAL_NAME') do it { should exist } @@ -41,7 +41,7 @@ def initialize(opts = {}) @cloud_watch_logs_role_arn = @trail[:cloud_watch_logs_role_arn] @log_file_validation_enabled = @trail[:log_file_validation_enabled] @cloud_watch_logs_log_group_arn = @trail[:cloud_watch_logs_log_group_arn] - @event_selectors = @aws.cloudtrail_client.get_event_selectors(trail_name: @trail_name) + @event_selectors = @aws.cloudtrail_client.get_event_selectors({ trail_name: @trail_name }) end end @@ -78,8 +78,8 @@ def encrypted? def get_log_group_for_multi_region_active_mgmt_rw_all return nil unless exists? return nil unless @cloud_watch_logs_log_group_arn - return nil if @cloud_watch_logs_log_group_arn.split(":").count < 6 - return @cloud_watch_logs_log_group_arn.split(":")[6] if has_event_selector_mgmt_events_rw_type_all? && logging? + return nil if @cloud_watch_logs_log_group_arn.split(':').count < 6 + return @cloud_watch_logs_log_group_arn.split(':')[6] if has_event_selector_mgmt_events_rw_type_all? && logging? end # TODO: see what happens when running against nil event selectors @@ -88,7 +88,7 @@ def has_event_selector_mgmt_events_rw_type_all? event_selector_found = false begin @event_selectors.event_selectors.each do |es| - event_selector_found = true if es.read_write_type == "All" && es.include_management_events == true + event_selector_found = true if es.read_write_type == 'All' && es.include_management_events == true end rescue Aws::CloudTrail::Errors::TrailNotFoundException event_selector_found @@ -99,7 +99,7 @@ def has_event_selector_mgmt_events_rw_type_all? def monitoring?(aws_resource_type, mode) # basic event selectors have a simpler structure than the advanced ones - check basic first if using_basic_event_selectors? - basic_mode = mode == "r" ? "ReadOnly" : "WriteOnly" + basic_mode = mode == 'r' ? 'ReadOnly' : 'WriteOnly' @event_selectors.event_selectors.any? { |es| es.read_write_type.match?(/All|#{basic_mode}/) && es.data_resources.any? { |dr| @@ -111,33 +111,33 @@ def monitoring?(aws_resource_type, mode) } } else - read_only = mode == "r" + read_only = mode == 'r' @event_selectors.advanced_event_selectors.any? { |es| (es.field_selectors.any? { |fs| # check if readOnly is explicitly set to true - fs.field == "readOnly" && fs.equals == [read_only.to_s] # note that AdvancedFieldSelector has a field named "equals" + fs.field == 'readOnly' && fs.equals == [read_only.to_s] # NOTE: that AdvancedFieldSelector has a field named "equals" # also note that designating an AFS as writeOnly means setting # the readOnly field to 'false' } || es.field_selectors.none? { |fs| # or check if readOnly is unset entirely (means both read and write are logged) - fs.field == "readOnly" + fs.field == 'readOnly' }) && es.field_selectors.any? { |fs| # check if some other field selector is set to the right resource type - fs.field == "resources.type" && fs.equals == [aws_resource_type] + fs.field == 'resources.type' && fs.equals == [aws_resource_type] } && es.field_selectors.none? { |fs| # check that no other event selector is tracking an individual arn # if no arn field is set, cloudtrail is tracking the whole type - fs.field.downcase == "resources.arn" + fs.field.downcase == 'resources.arn' } } end end def monitoring_read?(aws_resource_type) - monitoring?(aws_resource_type, "r") + monitoring?(aws_resource_type, 'r') end def monitoring_write?(aws_resource_type) - monitoring?(aws_resource_type, "w") + monitoring?(aws_resource_type, 'w') end def using_advanced_event_selectors? From db8981ff58575cc8f01a2fbf9e82c4d72def09c1 Mon Sep 17 00:00:00 2001 From: wdower Date: Wed, 6 Dec 2023 15:11:58 -0500 Subject: [PATCH 53/93] made to_s an alias to avoid duplicate code Signed-off-by: wdower --- libraries/aws_primary_contact.rb | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/libraries/aws_primary_contact.rb b/libraries/aws_primary_contact.rb index eb5a07f3b..3f48e7f8a 100644 --- a/libraries/aws_primary_contact.rb +++ b/libraries/aws_primary_contact.rb @@ -80,13 +80,7 @@ def resource_id end end - def to_s - if @aws_account_id - "AWS Primary Contact for account: #{@aws_account_id}" - else - "AWS Account Primary Contact" - end - end + alias to_s private From 4fa53dcd790a45d613c1020cf060d01305189787 Mon Sep 17 00:00:00 2001 From: wdower Date: Wed, 6 Dec 2023 15:16:52 -0500 Subject: [PATCH 54/93] fixing alias in primary contact, fixing resource_id and to_s in aws_iam_password_policy Signed-off-by: wdower --- libraries/aws_iam_password_policy.rb | 16 +++++++++++++--- libraries/aws_primary_contact.rb | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/libraries/aws_iam_password_policy.rb b/libraries/aws_iam_password_policy.rb index 1ede00f6c..10f04fbc7 100644 --- a/libraries/aws_iam_password_policy.rb +++ b/libraries/aws_iam_password_policy.rb @@ -20,6 +20,7 @@ def initialize(opts = {}) catch_aws_errors do @policy = @aws.iam_client.get_account_password_policy.password_policy + @aws_account_id = fetch_aws_account end end @@ -95,10 +96,19 @@ def exists? end def resource_id - @policy + if @aws_account_id + "AWS Password Policy for account: #{@aws_account_id}" + else + "AWS Account Password Policy" + end end - def to_s - "AWS IAM Password Policy" + alias to_s resource_id + + private + + def fetch_aws_account + arn = @aws.sts_client.get_caller_identity({}).arn + arn.split(":")[4] end end diff --git a/libraries/aws_primary_contact.rb b/libraries/aws_primary_contact.rb index 3f48e7f8a..27b818400 100644 --- a/libraries/aws_primary_contact.rb +++ b/libraries/aws_primary_contact.rb @@ -80,7 +80,7 @@ def resource_id end end - alias to_s + alias to_s resource_id private From a44b72ca77b603b693948fc8c84c78ec0f4223b1 Mon Sep 17 00:00:00 2001 From: wdower Date: Wed, 6 Dec 2023 16:06:08 -0500 Subject: [PATCH 55/93] catching a new AWS error for IAM, initializing policy to nil in aws_iam_password_policy resource to make sure it responds to a missing password policy correctly Signed-off-by: wdower --- libraries/aws_backend.rb | 4 ++++ libraries/aws_iam_password_policy.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/aws_backend.rb b/libraries/aws_backend.rb index 575677c97..4f6909c03 100644 --- a/libraries/aws_backend.rb +++ b/libraries/aws_backend.rb @@ -511,6 +511,10 @@ def catch_aws_errors Inspec::Log.warn(e.message.to_s) skip_resource(e.message.to_s) nil + rescue Aws::IAM::Errors::NoSuchEntity => e + Inspec::Log.error("IAM Service Error: #{e.message}") + skip_resource("IAM Service Error: #{e.message}") + nil rescue Aws::Errors::MissingCredentialsError Inspec::Log.error("It appears that you have not set your AWS credentials. See https://www.inspec.io/docs/reference/platforms for details.") fail_resource("No AWS credentials available") diff --git a/libraries/aws_iam_password_policy.rb b/libraries/aws_iam_password_policy.rb index 10f04fbc7..814454813 100644 --- a/libraries/aws_iam_password_policy.rb +++ b/libraries/aws_iam_password_policy.rb @@ -17,7 +17,7 @@ class AwsIamPasswordPolicy < AwsResourceBase def initialize(opts = {}) super(opts) validate_parameters - + @policy = nil catch_aws_errors do @policy = @aws.iam_client.get_account_password_policy.password_policy @aws_account_id = fetch_aws_account From 8645735c871906c1884fee843d927999914c3069 Mon Sep 17 00:00:00 2001 From: wdower Date: Wed, 6 Dec 2023 17:48:43 -0500 Subject: [PATCH 56/93] updating kms display_name to expose it and set it equal to a human-friendly value Signed-off-by: wdower --- libraries/aws_kms_key.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/aws_kms_key.rb b/libraries/aws_kms_key.rb index bf4f03eb2..6ebc71225 100644 --- a/libraries/aws_kms_key.rb +++ b/libraries/aws_kms_key.rb @@ -9,6 +9,8 @@ class AwsKmsKey < AwsResourceBase end " + attr_reader :display_name, :arn, :alias + def initialize(opts = {}) # SDK permits key_id to hold either an ID or an ARN opts = { key_id: opts } if opts.is_a?(String) @@ -18,7 +20,7 @@ def initialize(opts = {}) @alias = opts[:alias] opts[:key_id] = fetch_key_id end - @display_name = opts[:key_id] + @display_name = key_metadata[:key_id] @arn = key_metadata[:arn] create_resource_methods(key_metadata) From 46b1949b8b26839ea7a042334097f593ac6da456 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Thu, 7 Dec 2023 09:11:56 -0500 Subject: [PATCH 57/93] updated resources to account for govcloud api access Signed-off-by: Aaron Lippold --- libraries/aws_alternate_contact.rb | 28 ++++++++++++++++++---------- libraries/aws_billing_contact.rb | 26 +++++++++++++++++++------- libraries/aws_cloudtrail_trail.rb | 28 ++++++++++++++-------------- libraries/aws_macie.rb | 10 +++++----- libraries/aws_operations_contact.rb | 20 ++++++++++++++------ libraries/aws_primary_contact.rb | 22 +++++++++++++++------- libraries/aws_security_contact.rb | 20 ++++++++++++++------ 7 files changed, 99 insertions(+), 55 deletions(-) diff --git a/libraries/aws_alternate_contact.rb b/libraries/aws_alternate_contact.rb index e95f07f8d..7bf6e0366 100644 --- a/libraries/aws_alternate_contact.rb +++ b/libraries/aws_alternate_contact.rb @@ -30,8 +30,8 @@ class AwsAlternateAccount < AwsResourceBase def initialize(opts = {}) @raw_data = {} - supported_opt_keys = %i(type) - supported_opts_values = %w{billing operations security} + supported_opt_keys = %i[type] + supported_opts_values = %w[billing operations security] opts = { type: opts } if opts.is_a?(String) unless opts.respond_to?(:keys) @@ -52,14 +52,22 @@ def initialize(opts = {}) end super(opts) validate_parameters(required: [:type]) - - begin - catch_aws_errors { @aws_account_id = fetch_aws_account } - @api_response = fetch_aws_alternate_contact(opts[:type]) - rescue Aws::Account::Errors::ResourceNotFoundException - skip_resource( - "The #{opts[:type].uppercase} contact has not been configured for this AWS Account.", - ) + catch_aws_errors do + begin + @aws_account_id = fetch_aws_account + @api_response = fetch_aws_alternate_contact(opts[:type]) + rescue Aws::Account::Errors::ResourceNotFoundException + @api_response = nil + skip_resource( + "The #{opts[:type].uppercase} contact has not been configured for this AWS Account." + ) + return + rescue Aws::Errors::NoSuchEndpointError + @api_response = nil + skip_resource( + "The account contact endpoint is not available in this segment, please review this via the AWS Management Console." + ) + end return [] if !@api_response || @api_response.empty? end diff --git a/libraries/aws_billing_contact.rb b/libraries/aws_billing_contact.rb index f0e3ed7d9..6fa573fda 100644 --- a/libraries/aws_billing_contact.rb +++ b/libraries/aws_billing_contact.rb @@ -25,13 +25,25 @@ def initialize(opts = {}) @raw_data = {} @title, @name, @email_address, @phone_number = "" validate_parameters - begin - catch_aws_errors { @aws_account_id = fetch_aws_account } - @api_response = fetch_aws_alternate_contact("billing") - rescue Aws::Account::Errors::ResourceNotFoundException - skip_resource( - "The Billing contact has not been configured for this AWS Account.", - ) + catch_aws_errors do + # require "pry" + # binding.pry + begin + catch_aws_errors do + @aws_account_id = fetch_aws_account + @api_response = fetch_aws_alternate_contact("billing") + rescue Aws::Account::Errors::ResourceNotFoundException + @api_response = nil + skip_resource( + "The Billing contact has not been configured for this AWS Account.", + ) + rescue Aws::Errors::NoSuchEndpointError + @api_response = nil + skip_resource( + "The account contact endpoint is not available in this segment, please review this via the AWS Management Console.", + ) + end + end return [] if !@api_response || @api_response.empty? end diff --git a/libraries/aws_cloudtrail_trail.rb b/libraries/aws_cloudtrail_trail.rb index 955434e9f..564121e1a 100644 --- a/libraries/aws_cloudtrail_trail.rb +++ b/libraries/aws_cloudtrail_trail.rb @@ -1,8 +1,8 @@ -require 'aws_backend' +require "aws_backend" class AwsCloudTrailTrail < AwsResourceBase - name 'aws_cloudtrail_trail' - desc 'Verifies settings for an individual AWS CloudTrail Trail.' + name "aws_cloudtrail_trail" + desc "Verifies settings for an individual AWS CloudTrail Trail." example <<-EXAMPLE describe aws_cloudtrail_trail('TRIAL_NAME') do it { should exist } @@ -78,8 +78,8 @@ def encrypted? def get_log_group_for_multi_region_active_mgmt_rw_all return nil unless exists? return nil unless @cloud_watch_logs_log_group_arn - return nil if @cloud_watch_logs_log_group_arn.split(':').count < 6 - return @cloud_watch_logs_log_group_arn.split(':')[6] if has_event_selector_mgmt_events_rw_type_all? && logging? + return nil if @cloud_watch_logs_log_group_arn.split(":").count < 6 + return @cloud_watch_logs_log_group_arn.split(":")[6] if has_event_selector_mgmt_events_rw_type_all? && logging? end # TODO: see what happens when running against nil event selectors @@ -88,7 +88,7 @@ def has_event_selector_mgmt_events_rw_type_all? event_selector_found = false begin @event_selectors.event_selectors.each do |es| - event_selector_found = true if es.read_write_type == 'All' && es.include_management_events == true + event_selector_found = true if es.read_write_type == "All" && es.include_management_events == true end rescue Aws::CloudTrail::Errors::TrailNotFoundException event_selector_found @@ -99,7 +99,7 @@ def has_event_selector_mgmt_events_rw_type_all? def monitoring?(aws_resource_type, mode) # basic event selectors have a simpler structure than the advanced ones - check basic first if using_basic_event_selectors? - basic_mode = mode == 'r' ? 'ReadOnly' : 'WriteOnly' + basic_mode = mode == "r" ? "ReadOnly" : "WriteOnly" @event_selectors.event_selectors.any? { |es| es.read_write_type.match?(/All|#{basic_mode}/) && es.data_resources.any? { |dr| @@ -111,33 +111,33 @@ def monitoring?(aws_resource_type, mode) } } else - read_only = mode == 'r' + read_only = mode == "r" @event_selectors.advanced_event_selectors.any? { |es| (es.field_selectors.any? { |fs| # check if readOnly is explicitly set to true - fs.field == 'readOnly' && fs.equals == [read_only.to_s] # NOTE: that AdvancedFieldSelector has a field named "equals" + fs.field == "readOnly" && fs.equals == [read_only.to_s] # NOTE: that AdvancedFieldSelector has a field named "equals" # also note that designating an AFS as writeOnly means setting # the readOnly field to 'false' } || es.field_selectors.none? { |fs| # or check if readOnly is unset entirely (means both read and write are logged) - fs.field == 'readOnly' + fs.field == "readOnly" }) && es.field_selectors.any? { |fs| # check if some other field selector is set to the right resource type - fs.field == 'resources.type' && fs.equals == [aws_resource_type] + fs.field == "resources.type" && fs.equals == [aws_resource_type] } && es.field_selectors.none? { |fs| # check that no other event selector is tracking an individual arn # if no arn field is set, cloudtrail is tracking the whole type - fs.field.downcase == 'resources.arn' + fs.field.downcase == "resources.arn" } } end end def monitoring_read?(aws_resource_type) - monitoring?(aws_resource_type, 'r') + monitoring?(aws_resource_type, "r") end def monitoring_write?(aws_resource_type) - monitoring?(aws_resource_type, 'w') + monitoring?(aws_resource_type, "w") end def using_advanced_event_selectors? diff --git a/libraries/aws_macie.rb b/libraries/aws_macie.rb index 7ddb02123..95eda8295 100644 --- a/libraries/aws_macie.rb +++ b/libraries/aws_macie.rb @@ -1,8 +1,8 @@ -require 'aws_backend' +require "aws_backend" class AWSMacie < AwsResourceBase - name 'aws_macie' - desc 'Gets information about Macie status and configuration.' + name "aws_macie" + desc "Gets information about Macie status and configuration." example " describe aws_macie do @@ -55,7 +55,7 @@ def fetch_data # - get_finding_statistics(finding_id) def enabled? - @session.status == 'ENABLED' + @session.status == "ENABLED" end # def monitoring?(bucket_list) @@ -65,6 +65,6 @@ def enabled? # end def to_s - 'AWS Macie' + "AWS Macie" end end diff --git a/libraries/aws_operations_contact.rb b/libraries/aws_operations_contact.rb index be6316d20..ad09e9427 100644 --- a/libraries/aws_operations_contact.rb +++ b/libraries/aws_operations_contact.rb @@ -26,12 +26,20 @@ def initialize(opts = {}) @title, @name, @email_address, @phone_number = "" validate_parameters begin - catch_aws_errors { @aws_account_id = fetch_aws_account } - @api_response = fetch_aws_alternate_contact("operations") - rescue Aws::Account::Errors::ResourceNotFoundException - skip_resource( - "The Operations contact has not been configured for this AWS Account.", - ) + catch_aws_errors do + @aws_account_id = fetch_aws_account + @api_response = fetch_aws_alternate_contact("operations") + rescue Aws::Account::Errors::ResourceNotFoundException + @api_response = nil + skip_resource( + "The Operations contact has not been configured for this AWS Account.", + ) + rescue Aws::Errors::NoSuchEndpointError + @api_response = nil + skip_resource( + "The account contact endpoint is not available in this segment, please review this via the AWS Management Console.", + ) + end return [] if !@api_response || @api_response.empty? end diff --git a/libraries/aws_primary_contact.rb b/libraries/aws_primary_contact.rb index 27b818400..e7daf5910 100644 --- a/libraries/aws_primary_contact.rb +++ b/libraries/aws_primary_contact.rb @@ -45,13 +45,21 @@ def initialize(opts = {}) super(opts) validate_parameters begin - catch_aws_errors { @aws_account_id = fetch_aws_account } - @api_response = - @aws.account_client.get_contact_information.contact_information - rescue Aws::Account::Errors::ResourceNotFoundException - skip_resource( - "The Primary contact has not been configured for this AWS Account.", - ) + catch_aws_errors do + @aws_account_id = fetch_aws_account + @api_response = + @aws.account_client.get_contact_information.contact_information + rescue Aws::Account::Errors::ResourceNotFoundException + @api_response = nil + skip_resource( + "The Primary contact has not been configured for this AWS Account.", + ) + rescue Aws::Errors::NoSuchEndpointError + @api_response = nil + skip_resource( + "The account contact endpoint is not available in this segment, please review this via the AWS Management Console.", + ) + end return [] if !@api_response || @api_response.empty? end diff --git a/libraries/aws_security_contact.rb b/libraries/aws_security_contact.rb index afb095f9a..137900702 100644 --- a/libraries/aws_security_contact.rb +++ b/libraries/aws_security_contact.rb @@ -26,12 +26,20 @@ def initialize(opts = {}) @title, @name, @email_address, @phone_number = "" validate_parameters begin - catch_aws_errors { @aws_account_id = fetch_aws_account } - @api_response = fetch_aws_alternate_contact("security") - rescue Aws::Account::Errors::ResourceNotFoundException - skip_resource( - "The Security contact has not been configured for this AWS Account.", - ) + catch_aws_errors do + @aws_account_id = fetch_aws_account + @api_response = fetch_aws_alternate_contact("security") + rescue Aws::Account::Errors::ResourceNotFoundException + @api_response = nil + skip_resource( + "The Security contact has not been configured for this AWS Account.", + ) + rescue Aws::Errors::NoSuchEndpointError + @api_response = nil + skip_resource( + "The account contact endpoint is not available in this segment, please review this via the AWS Management Console.", + ) + end return [] if !@api_response || @api_response.empty? end From 5cbd6d731203690f158804c517b3d389b2c78d8c Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Thu, 7 Dec 2023 09:13:02 -0500 Subject: [PATCH 58/93] rubocop:lint Signed-off-by: Aaron Lippold --- libraries/aws_alternate_contact.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/aws_alternate_contact.rb b/libraries/aws_alternate_contact.rb index 7bf6e0366..f6094a539 100644 --- a/libraries/aws_alternate_contact.rb +++ b/libraries/aws_alternate_contact.rb @@ -30,8 +30,8 @@ class AwsAlternateAccount < AwsResourceBase def initialize(opts = {}) @raw_data = {} - supported_opt_keys = %i[type] - supported_opts_values = %w[billing operations security] + supported_opt_keys = %i(type) + supported_opts_values = %w{billing operations security} opts = { type: opts } if opts.is_a?(String) unless opts.respond_to?(:keys) @@ -59,13 +59,13 @@ def initialize(opts = {}) rescue Aws::Account::Errors::ResourceNotFoundException @api_response = nil skip_resource( - "The #{opts[:type].uppercase} contact has not been configured for this AWS Account." + "The #{opts[:type].uppercase} contact has not been configured for this AWS Account.", ) return rescue Aws::Errors::NoSuchEndpointError @api_response = nil skip_resource( - "The account contact endpoint is not available in this segment, please review this via the AWS Management Console." + "The account contact endpoint is not available in this segment, please review this via the AWS Management Console.", ) end return [] if !@api_response || @api_response.empty? From 6327bf8af23742b5dcd6cf6e7de0a78c16677803 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Thu, 7 Dec 2023 10:16:19 -0500 Subject: [PATCH 59/93] added notes file Signed-off-by: Aaron Lippold --- notes | 1 + 1 file changed, 1 insertion(+) create mode 100644 notes diff --git a/notes b/notes new file mode 100644 index 000000000..15123a59d --- /dev/null +++ b/notes @@ -0,0 +1 @@ +https://aws.amazon.com/compliance/fips/#FIPS_Endpoints_by_Service From 6713e851851ebca81ad1ecae93af66449660e1f4 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Thu, 7 Dec 2023 16:52:50 -0500 Subject: [PATCH 60/93] rubocop:lint Signed-off-by: Aaron Lippold --- libraries/aws_cloudtrail_trail.rb | 85 ++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 30 deletions(-) diff --git a/libraries/aws_cloudtrail_trail.rb b/libraries/aws_cloudtrail_trail.rb index 564121e1a..9bb567a66 100644 --- a/libraries/aws_cloudtrail_trail.rb +++ b/libraries/aws_cloudtrail_trail.rb @@ -12,9 +12,18 @@ class AwsCloudTrailTrail < AwsResourceBase end EXAMPLE - attr_reader :cloud_watch_logs_log_group_arn, :cloud_watch_logs_role_arn, :home_region, :trail_name, - :kms_key_id, :s3_bucket_name, :s3_key_prefix, :trail_arn, :is_multi_region_trail, - :log_file_validation_enabled, :is_organization_trail, :event_selectors + attr_reader :cloud_watch_logs_log_group_arn, + :cloud_watch_logs_role_arn, + :home_region, + :trail_name, + :kms_key_id, + :s3_bucket_name, + :s3_key_prefix, + :trail_arn, + :is_multi_region_trail, + :log_file_validation_enabled, + :is_organization_trail, + :event_selectors alias multi_region_trail? is_multi_region_trail alias log_file_validation_enabled? log_file_validation_enabled @@ -29,7 +38,11 @@ def initialize(opts = {}) @trail_name = opts[:trail_name] @event_selectors = [] catch_aws_errors do - resp = @aws.cloudtrail_client.describe_trails({ trail_name_list: [@trail_name] }) + resp = + @aws.cloudtrail_client.describe_trails( + { trail_name_list: [@trail_name] }, + ) + @event_selectors = @aws.cloudtrail_client.get_event_selectors({ trail_name: @trail_name }) @trail = resp.trail_list[0].to_h @trail_arn = @trail[:trail_arn] @kms_key_id = @trail[:kms_key_id] @@ -41,7 +54,6 @@ def initialize(opts = {}) @cloud_watch_logs_role_arn = @trail[:cloud_watch_logs_role_arn] @log_file_validation_enabled = @trail[:log_file_validation_enabled] @cloud_watch_logs_log_group_arn = @trail[:cloud_watch_logs_log_group_arn] - @event_selectors = @aws.cloudtrail_client.get_event_selectors({ trail_name: @trail_name }) end end @@ -53,8 +65,14 @@ def delivered_logs_days_ago return nil unless exists? catch_aws_errors do begin - trail_status = @aws.cloudtrail_client.get_trail_status({ name: @trail_name }).to_h - ((Time.now - trail_status[:latest_cloud_watch_logs_delivery_time]) / (24 * 60 * 60)).to_i unless trail_status[:latest_cloud_watch_logs_delivery_time].nil? + trail_status = + @aws.cloudtrail_client.get_trail_status({ name: @trail_name }).to_h + unless trail_status[:latest_cloud_watch_logs_delivery_time].nil? + ( + (Time.now - trail_status[:latest_cloud_watch_logs_delivery_time]) / + (24 * 60 * 60) + ).to_i + end rescue Aws::CloudTrail::Errors::TrailNotFoundException nil end @@ -64,7 +82,9 @@ def delivered_logs_days_ago def logging? catch_aws_errors do begin - @aws.cloudtrail_client.get_trail_status({ name: @trail_name }).to_h[:is_logging] + @aws.cloudtrail_client.get_trail_status({ name: @trail_name }).to_h[ + :is_logging + ] rescue Aws::CloudTrail::Errors::TrailNotFoundException nil end @@ -79,7 +99,9 @@ def get_log_group_for_multi_region_active_mgmt_rw_all return nil unless exists? return nil unless @cloud_watch_logs_log_group_arn return nil if @cloud_watch_logs_log_group_arn.split(":").count < 6 - return @cloud_watch_logs_log_group_arn.split(":")[6] if has_event_selector_mgmt_events_rw_type_all? && logging? + if has_event_selector_mgmt_events_rw_type_all? && logging? + @cloud_watch_logs_log_group_arn.split(":")[6] + end end # TODO: see what happens when running against nil event selectors @@ -88,7 +110,8 @@ def has_event_selector_mgmt_events_rw_type_all? event_selector_found = false begin @event_selectors.event_selectors.each do |es| - event_selector_found = true if es.read_write_type == "All" && es.include_management_events == true + event_selector_found = true if es.read_write_type == "All" && + es.include_management_events == true end rescue Aws::CloudTrail::Errors::TrailNotFoundException event_selector_found @@ -100,35 +123,37 @@ def monitoring?(aws_resource_type, mode) # basic event selectors have a simpler structure than the advanced ones - check basic first if using_basic_event_selectors? basic_mode = mode == "r" ? "ReadOnly" : "WriteOnly" - @event_selectors.event_selectors.any? { |es| + @event_selectors.event_selectors.any? do |es| es.read_write_type.match?(/All|#{basic_mode}/) && - es.data_resources.any? { |dr| + es.data_resources.any? do |dr| dr.type.include?(aws_resource_type) && - dr.values.all? { |val| # make sure the values do not indicate individual resources + dr.values.all? do |val| # make sure the values do not indicate individual resources val.split(%r{[:/]}).count <= 3 # can be of the form 'arn:aws:s3' but not # 'arn:aws:s3:::/' - } - } - } + end + end + end else read_only = mode == "r" - @event_selectors.advanced_event_selectors.any? { |es| - (es.field_selectors.any? { |fs| # check if readOnly is explicitly set to true - fs.field == "readOnly" && fs.equals == [read_only.to_s] # NOTE: that AdvancedFieldSelector has a field named "equals" - # also note that designating an AFS as writeOnly means setting - # the readOnly field to 'false' - } || - es.field_selectors.none? { |fs| # or check if readOnly is unset entirely (means both read and write are logged) - fs.field == "readOnly" - }) && - es.field_selectors.any? { |fs| # check if some other field selector is set to the right resource type + @event_selectors.advanced_event_selectors.any? do |es| + ( + es.field_selectors.any? do |fs| # check if readOnly is explicitly set to true + fs.field == "readOnly" && fs.equals == [read_only.to_s] # NOTE: that AdvancedFieldSelector has a field named "equals" + # also note that designating an AFS as writeOnly means setting + # the readOnly field to 'false' + end || + es.field_selectors.none? do |fs| # or check if readOnly is unset entirely (means both read and write are logged) + fs.field == "readOnly" + end + ) && + es.field_selectors.any? do |fs| # check if some other field selector is set to the right resource type fs.field == "resources.type" && fs.equals == [aws_resource_type] - } && - es.field_selectors.none? { |fs| # check that no other event selector is tracking an individual arn + end && + es.field_selectors.none? do |fs| # check that no other event selector is tracking an individual arn # if no arn field is set, cloudtrail is tracking the whole type fs.field.downcase == "resources.arn" - } - } + end + end end end From 664b18ac4b1e0c740e895cc6cdc7aeaa1f6f0b1d Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Thu, 7 Dec 2023 17:32:27 -0500 Subject: [PATCH 61/93] errors in macie Signed-off-by: Aaron Lippold --- libraries/aws_macie.rb | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/libraries/aws_macie.rb b/libraries/aws_macie.rb index 95eda8295..ad9dcfc30 100644 --- a/libraries/aws_macie.rb +++ b/libraries/aws_macie.rb @@ -19,15 +19,21 @@ def initialize(opts = {}) @describe_hub = [] super(opts) validate_parameters - fetch_data end def fetch_data catch_aws_errors do - @session = @aws.macie_client.get_macie_session - @jobs = @aws.macie_client.list_classification_jobs - @buckets = @aws.macie_client.describe_buckets + begin + @session = @aws.macie_client.get_macie_session + @jobs = @aws.macie_client.list_classification_jobs + @buckets = @aws.macie_client.describe_buckets + rescue Aws::Errors::NoSuchEndpointError + skip_resource( + "The account contact endpoint is not available in this segment, please review this via the AWS Management Console.", + ) + end + return [] if (!@session or @session.empty?) || (!@jobs or @jobs.empty?) || (!@bucket or @bucket.empty?) # Can't do this until we setup the user to be a Macie Admin? # @organization_configuration = @aws.macie_client.describe_organization_configuration end From 91157d1effcca4cf5edd8b8382cea254f4a84ab8 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Fri, 8 Dec 2023 14:45:14 -0500 Subject: [PATCH 62/93] adding the Network Seahorse Error to backend and contact resources Signed-off-by: Aaron Lippold --- libraries/aws_alternate_contact.rb | 4 +--- libraries/aws_backend.rb | 4 ++++ libraries/aws_operations_contact.rb | 4 +--- libraries/aws_primary_contact.rb | 4 +--- libraries/aws_security_contact.rb | 4 +--- 5 files changed, 8 insertions(+), 12 deletions(-) diff --git a/libraries/aws_alternate_contact.rb b/libraries/aws_alternate_contact.rb index f6094a539..b93437bbf 100644 --- a/libraries/aws_alternate_contact.rb +++ b/libraries/aws_alternate_contact.rb @@ -57,13 +57,11 @@ def initialize(opts = {}) @aws_account_id = fetch_aws_account @api_response = fetch_aws_alternate_contact(opts[:type]) rescue Aws::Account::Errors::ResourceNotFoundException - @api_response = nil skip_resource( "The #{opts[:type].uppercase} contact has not been configured for this AWS Account.", ) return - rescue Aws::Errors::NoSuchEndpointError - @api_response = nil + rescue Aws::Errors::NoSuchEndpointError, Seahorse::Client::NetworkingError skip_resource( "The account contact endpoint is not available in this segment, please review this via the AWS Management Console.", ) diff --git a/libraries/aws_backend.rb b/libraries/aws_backend.rb index 4f6909c03..c83cfb429 100644 --- a/libraries/aws_backend.rb +++ b/libraries/aws_backend.rb @@ -542,6 +542,10 @@ def catch_aws_errors Inspec::Log.error("Macie Resource: #{e.message}") skip_resource("Macie Resource Error: #{e.message}") nil + rescue Seahorse::Client::NetworkingError => e + Inspec::Log.error("Seahorse Error: #{e.message}") + skip_resource("Seahorse Error: #{e.message}") + nil rescue Aws::Errors::ServiceError => e if is_permissions_error(e) advice = "" diff --git a/libraries/aws_operations_contact.rb b/libraries/aws_operations_contact.rb index ad09e9427..9371abb45 100644 --- a/libraries/aws_operations_contact.rb +++ b/libraries/aws_operations_contact.rb @@ -30,12 +30,10 @@ def initialize(opts = {}) @aws_account_id = fetch_aws_account @api_response = fetch_aws_alternate_contact("operations") rescue Aws::Account::Errors::ResourceNotFoundException - @api_response = nil skip_resource( "The Operations contact has not been configured for this AWS Account.", ) - rescue Aws::Errors::NoSuchEndpointError - @api_response = nil + rescue Aws::Errors::NoSuchEndpointError, Seahorse::Client::NetworkingError skip_resource( "The account contact endpoint is not available in this segment, please review this via the AWS Management Console.", ) diff --git a/libraries/aws_primary_contact.rb b/libraries/aws_primary_contact.rb index e7daf5910..66c8b45b2 100644 --- a/libraries/aws_primary_contact.rb +++ b/libraries/aws_primary_contact.rb @@ -50,12 +50,10 @@ def initialize(opts = {}) @api_response = @aws.account_client.get_contact_information.contact_information rescue Aws::Account::Errors::ResourceNotFoundException - @api_response = nil skip_resource( "The Primary contact has not been configured for this AWS Account.", ) - rescue Aws::Errors::NoSuchEndpointError - @api_response = nil + rescue Aws::Errors::NoSuchEndpointError, Seahorse::Client::NetworkingError skip_resource( "The account contact endpoint is not available in this segment, please review this via the AWS Management Console.", ) diff --git a/libraries/aws_security_contact.rb b/libraries/aws_security_contact.rb index 137900702..414eefb8f 100644 --- a/libraries/aws_security_contact.rb +++ b/libraries/aws_security_contact.rb @@ -30,12 +30,10 @@ def initialize(opts = {}) @aws_account_id = fetch_aws_account @api_response = fetch_aws_alternate_contact("security") rescue Aws::Account::Errors::ResourceNotFoundException - @api_response = nil skip_resource( "The Security contact has not been configured for this AWS Account.", ) - rescue Aws::Errors::NoSuchEndpointError - @api_response = nil + rescue Aws::Errors::NoSuchEndpointError, Seahorse::Client::NetworkingError skip_resource( "The account contact endpoint is not available in this segment, please review this via the AWS Management Console.", ) From fb950c3ff056143ad26e2cc9f75f22efc82c4cb9 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Fri, 8 Dec 2023 15:30:32 -0500 Subject: [PATCH 63/93] updating error catch for seahorse Signed-off-by: Aaron Lippold --- libraries/aws_backend.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/aws_backend.rb b/libraries/aws_backend.rb index c83cfb429..efc69ff44 100644 --- a/libraries/aws_backend.rb +++ b/libraries/aws_backend.rb @@ -505,7 +505,7 @@ def name end # Intercept AWS exceptions - def catch_aws_errors + def catch_aws_errors # rubocop:disable Metrics/MethodLength yield # Catch and create custom messages as needed rescue Aws::Account::Errors::ResourceNotFoundException => e Inspec::Log.warn(e.message.to_s) From a4223c0229b9e9d3073455ec73d66c747285ed2e Mon Sep 17 00:00:00 2001 From: wdower Date: Fri, 8 Dec 2023 17:07:42 -0500 Subject: [PATCH 64/93] adding ebs encryption matcher to aws_region Signed-off-by: wdower --- docs-chef-io/content/inspec/resources/aws_region.md | 8 ++++++++ libraries/aws_region.rb | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/docs-chef-io/content/inspec/resources/aws_region.md b/docs-chef-io/content/inspec/resources/aws_region.md index ba35420bc..b77aaaccf 100644 --- a/docs-chef-io/content/inspec/resources/aws_region.md +++ b/docs-chef-io/content/inspec/resources/aws_region.md @@ -83,6 +83,14 @@ The control will pass if the describe returns at least one result. it { should exist } ``` +### ebs_encryption_enabled + +The control will pass if the region has EBS volume encryption enabled by default. + +```ruby +it { should have_ebs_encryption_enabled } +``` + ## AWS Permissions {{% aws_permissions_principal action="EC2:Client:DescribeRegionsResult" %}} diff --git a/libraries/aws_region.rb b/libraries/aws_region.rb index 725a353c0..5788c4932 100644 --- a/libraries/aws_region.rb +++ b/libraries/aws_region.rb @@ -8,6 +8,7 @@ class AwsRegion < AwsResourceBase example " describe aws_region('eu-west-2') do it { should exist } + it { should have_ebs_encryption_enabled } end " attr_reader :region_name, :endpoint, :resp, :opt_in_status @@ -21,6 +22,7 @@ def initialize(opts = {}) @region_name = opts[:region_name] catch_aws_errors do @resp = @aws.compute_client.describe_regions(region_names: [@region_name]) + @ebs_encryption_enabled = @aws.compute_client.get_ebs_encryption_by_default(region_names: [@region_name]) return if @resp.regions.empty? @opt_in_status = @resp.regions[0].opt_in_status @endpoint = @resp.regions[0].endpoint @@ -35,6 +37,10 @@ def exists? !@endpoint.nil? end + def ebs_encryption_enabled? + @ebs_encryption_enabled + end + def to_s "Region #{@region_name}" end From fda831a6e66bf8f00cd0e2e90bfbb5668934660e Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Sat, 9 Dec 2023 15:05:47 -0500 Subject: [PATCH 65/93] removing unneeded private function Signed-off-by: Aaron Lippold --- libraries/aws_region.rb | 23 +++++++++++++++++------ libraries/aws_regions.rb | 11 ----------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/libraries/aws_region.rb b/libraries/aws_region.rb index 5788c4932..63df03e90 100644 --- a/libraries/aws_region.rb +++ b/libraries/aws_region.rb @@ -11,7 +11,7 @@ class AwsRegion < AwsResourceBase it { should have_ebs_encryption_enabled } end " - attr_reader :region_name, :endpoint, :resp, :opt_in_status + attr_reader :region_name, :endpoint, :resp, :opt_in_status, :ebs_encryption_enabled def initialize(opts = {}) opts = { region_name: opts } if opts.is_a?(String) @@ -21,11 +21,11 @@ def initialize(opts = {}) @region_name = opts[:region_name] catch_aws_errors do - @resp = @aws.compute_client.describe_regions(region_names: [@region_name]) - @ebs_encryption_enabled = @aws.compute_client.get_ebs_encryption_by_default(region_names: [@region_name]) + @resp = @aws.compute_client.describe_regions({ region_names: [@region_name] }) return if @resp.regions.empty? - @opt_in_status = @resp.regions[0].opt_in_status - @endpoint = @resp.regions[0].endpoint + @ebs_encryption_enabled = fetch_ebs_status_by_region(@region_name) + @opt_in_status = @resp.regions.first.opt_in_status + @endpoint = @resp.regions.first.endpoint end end @@ -37,11 +37,22 @@ def exists? !@endpoint.nil? end - def ebs_encryption_enabled? + def has_ebs_encryption_enabled? @ebs_encryption_enabled end def to_s "Region #{@region_name}" end + + private + + def fetch_ebs_status_by_region(region) + catch_aws_errors do + new_client = @aws.compute_client + new_client.config.region = region + new_client.get_ebs_encryption_by_default[:ebs_encryption_by_default] + end + end + end diff --git a/libraries/aws_regions.rb b/libraries/aws_regions.rb index d4a7988da..fbc38b512 100644 --- a/libraries/aws_regions.rb +++ b/libraries/aws_regions.rb @@ -17,7 +17,6 @@ class AwsRegions < AwsResourceBase .register_column(:region_names, field: :region_name) .register_column(:endpoints, field: :endpoint) .register_column(:opt_in_status, field: :opt_in_status) - .register_column(:region_opt_status, field: :region_opt_status) .install_filter_methods_on_resource(self, :table) def initialize(opts = {}) @@ -42,19 +41,9 @@ def fetch_data region_name: region[:region_name], endpoint: region[:endpoint], opt_in_status: region[:opt_in_status], - region_opt_status: region_opt_status, }, ] end @table = region_rows end - - private - - def fetch_region_opt_status(region) - @aws - .account_client - .get_region_opt_status({ region_name: region }) - .region_opt_status - end end From 67010a0b90df02a084cdc775b50325f4a6d40ed0 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Sat, 9 Dec 2023 21:12:27 -0500 Subject: [PATCH 66/93] fixing proc processors on csv parser in aws_iam_credential_report Signed-off-by: Aaron Lippold --- libraries/aws_iam_credential_report.rb | 21 ++++++++++++++------- libraries/aws_regions.rb | 4 ---- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/libraries/aws_iam_credential_report.rb b/libraries/aws_iam_credential_report.rb index 29fc4d044..05a0fa3dd 100644 --- a/libraries/aws_iam_credential_report.rb +++ b/libraries/aws_iam_credential_report.rb @@ -1,5 +1,5 @@ -require "csv" require "aws_backend" +require "csv" class AwsIamCredentialReport < AwsCollectionResourceBase name "aws_iam_credential_report" @@ -11,7 +11,7 @@ class AwsIamCredentialReport < AwsCollectionResourceBase end " - attr_reader :table + attr_reader :table, :response, :test FilterTable.create .register_column(:user, field: :user) @@ -55,7 +55,7 @@ def fetch_data @aws.iam_client.generate_credential_report begin attempts ||= 0 - response = @aws.iam_client.get_credential_report + @response = @aws.iam_client.get_credential_report rescue Aws::IAM::Errors::ReportInProgress => e if (attempts += 1) <= 5 Inspec::Log.warn "AWS IAM Credential Report still being generated - attempt #{attempts}/5." @@ -66,13 +66,20 @@ def fetch_data raise e end end - report = CSV.parse(response.content, headers: true, header_converters: :symbol, converters: [:date_time, lambda { |field| - if field == "true" + + bool_converter = proc do |field| + case field.downcase + when "true" true + when "false" + false else - field == "false" ? false : field + field end - }]) + end + + no_info = proc { |field| field == "no_information" ? "N/A" : field } + report = CSV.parse(response.content, headers: true, header_converters: :symbol, converters: [:date_time, bool_converter, no_info]) report.map(&:to_h) end end diff --git a/libraries/aws_regions.rb b/libraries/aws_regions.rb index fbc38b512..00413e899 100644 --- a/libraries/aws_regions.rb +++ b/libraries/aws_regions.rb @@ -31,11 +31,7 @@ def fetch_data @regions = @aws.compute_client.describe_regions.to_h[:regions] end return [] if !@regions || @regions.empty? - region_opt_status = "" @regions.each do |region| - catch_aws_errors do - region_opt_status = fetch_region_opt_status(region[:region_name]) - end region_rows += [ { region_name: region[:region_name], From 6ba3c10ba4467ba8c85fcf4bc9fca4892ba5bc1a Mon Sep 17 00:00:00 2001 From: wdower Date: Mon, 11 Dec 2023 10:54:39 -0500 Subject: [PATCH 67/93] added rescue for NoSuchEntity in aws_iam_password_policy Signed-off-by: wdower --- libraries/aws_iam_password_policy.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/aws_iam_password_policy.rb b/libraries/aws_iam_password_policy.rb index 814454813..7448ebbda 100644 --- a/libraries/aws_iam_password_policy.rb +++ b/libraries/aws_iam_password_policy.rb @@ -18,6 +18,8 @@ def initialize(opts = {}) super(opts) validate_parameters @policy = nil + require "pry" + pry catch_aws_errors do @policy = @aws.iam_client.get_account_password_policy.password_policy @aws_account_id = fetch_aws_account From 3fb10d76d1373c6624e327349e4eaf53a276db1e Mon Sep 17 00:00:00 2001 From: wdower Date: Mon, 11 Dec 2023 14:18:33 -0500 Subject: [PATCH 68/93] forgot to save prior edits to aws_iam_password_policy Signed-off-by: wdower --- libraries/aws_iam_password_policy.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libraries/aws_iam_password_policy.rb b/libraries/aws_iam_password_policy.rb index 7448ebbda..aa23dc8e6 100644 --- a/libraries/aws_iam_password_policy.rb +++ b/libraries/aws_iam_password_policy.rb @@ -23,6 +23,10 @@ def initialize(opts = {}) catch_aws_errors do @policy = @aws.iam_client.get_account_password_policy.password_policy @aws_account_id = fetch_aws_account + rescue Aws::IAM::Errors::NoSuchEntity + skip_resource( + "The account password policy either does not exist or is set to default settings; please review via the AWS Management Console.", + ) end end From 052f8f2bc6e7a46bc15ca618dae787eae680c86e Mon Sep 17 00:00:00 2001 From: wdower Date: Mon, 11 Dec 2023 14:19:55 -0500 Subject: [PATCH 69/93] built out better has_acl_entry_value? for aws_network_acl Signed-off-by: wdower --- libraries/aws_network_acl.rb | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/libraries/aws_network_acl.rb b/libraries/aws_network_acl.rb index 1ed0e6155..8b10300de 100644 --- a/libraries/aws_network_acl.rb +++ b/libraries/aws_network_acl.rb @@ -1,4 +1,5 @@ require "aws_backend" +require "pry" class AwsNetworkACL < AwsResourceBase EGRESS = "egress".freeze @@ -52,12 +53,24 @@ def has_associations?(subnet_id: nil) associated_subnet_ids.any? { |associated_subnet_id| associated_subnet_id == subnet_id } end - def has_acl_entry_value?(cidr_block:, egress:, rule_action:) - invalid_args = method(__method__).parameters.select { |param| param.nil? || param.empty? } - raise ArgumentError, "params #{invalid_args.map { |i| "`#{i}`" }.join(",")} cannot be blank" if cidr_block.nil? || cidr_block.empty? + def has_acl_entry_value?(cidr_block: nil, egress: nil, icmp_type_code: nil, ipv_6_cidr_block: nil, port_range: nil, protocol: nil, rule_action: nil, rule_number: nil) return false unless acl_entries - - acl_entries.any? { |entry| entry.egress == egress && entry.cidr_block == cidr_block && entry.rule_action == rule_action } + + # rules for all protocols are recorded as rules with protocol == -1 + protocol = "-1" if protocol.to_s.downcase == "all" + + # check for acl entries matching any combination of fields + # iff a field was passed to the matcher, then it is included as part of the test + acl_entries.any? { |entry| + entry.cidr_block == cidr_block && + egress.to_s.present? ? entry.egress == egress : true && + icmp_type_code.present? ? entry.icmp_type_code == icmp_type_code : true && + ipv_6_cidr_block.present? ? entry.ipv_6_cidr_block == ipv_6_cidr_block : true && + port_range.present? ? port_within_range?(entry.port_range, port_range) : true && + protocol.present? ? entry.protocol == protocol.to_s : true && + rule_action ? entry.rule_action == rule_action : true && + rule_number ? entry.rule_number == rule_number : true + } end def has_egress?(cidr_block: nil, rule_action: nil) @@ -103,6 +116,7 @@ def fetch end create_resource_methods(network_acl_hash) create_rule_number_methods + pry end def network_acl @@ -154,4 +168,10 @@ def cidr_block_and_rule_action_exists_for?(collection, cidr_block, rule_action) collection.any? { |entry| entry.cidr_block == cidr_block || entry.rule_action == rule_action } end + + def port_within_range?(acl_port_range, expected_port) + return false if acl_port_range.nil? || expected_port.nil? + # compare an Aws::EC2::Types::PortRange to a standard Range + (acl_port_range.from..acl_port_range.to).include?(expected_port) + end end From d0cb116dcb84903f0caf282eb63b80fc7a5f35b6 Mon Sep 17 00:00:00 2001 From: wdower Date: Mon, 11 Dec 2023 14:20:24 -0500 Subject: [PATCH 70/93] forgot to save prior edits to aws_network_acl Signed-off-by: wdower --- libraries/aws_network_acl.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/libraries/aws_network_acl.rb b/libraries/aws_network_acl.rb index 8b10300de..c95318968 100644 --- a/libraries/aws_network_acl.rb +++ b/libraries/aws_network_acl.rb @@ -1,5 +1,4 @@ require "aws_backend" -require "pry" class AwsNetworkACL < AwsResourceBase EGRESS = "egress".freeze @@ -116,7 +115,6 @@ def fetch end create_resource_methods(network_acl_hash) create_rule_number_methods - pry end def network_acl From aac9edde73a626523a56b1b608b5529b1ca070ca Mon Sep 17 00:00:00 2001 From: wdower Date: Tue, 12 Dec 2023 10:33:06 -0500 Subject: [PATCH 71/93] linting Signed-off-by: wdower --- libraries/aws_iam_password_policy.rb | 2 +- libraries/aws_network_acl.rb | 48 ++++++++++++++++++++++------ 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/libraries/aws_iam_password_policy.rb b/libraries/aws_iam_password_policy.rb index aa23dc8e6..a79671510 100644 --- a/libraries/aws_iam_password_policy.rb +++ b/libraries/aws_iam_password_policy.rb @@ -23,7 +23,7 @@ def initialize(opts = {}) catch_aws_errors do @policy = @aws.iam_client.get_account_password_policy.password_policy @aws_account_id = fetch_aws_account - rescue Aws::IAM::Errors::NoSuchEntity + rescue Aws::IAM::Errors::NoSuchEntity skip_resource( "The account password policy either does not exist or is set to default settings; please review via the AWS Management Console.", ) diff --git a/libraries/aws_network_acl.rb b/libraries/aws_network_acl.rb index c95318968..dd9bbe6e7 100644 --- a/libraries/aws_network_acl.rb +++ b/libraries/aws_network_acl.rb @@ -54,21 +54,49 @@ def has_associations?(subnet_id: nil) def has_acl_entry_value?(cidr_block: nil, egress: nil, icmp_type_code: nil, ipv_6_cidr_block: nil, port_range: nil, protocol: nil, rule_action: nil, rule_number: nil) return false unless acl_entries - + # rules for all protocols are recorded as rules with protocol == -1 protocol = "-1" if protocol.to_s.downcase == "all" # check for acl entries matching any combination of fields # iff a field was passed to the matcher, then it is included as part of the test - acl_entries.any? { |entry| - entry.cidr_block == cidr_block && - egress.to_s.present? ? entry.egress == egress : true && - icmp_type_code.present? ? entry.icmp_type_code == icmp_type_code : true && - ipv_6_cidr_block.present? ? entry.ipv_6_cidr_block == ipv_6_cidr_block : true && - port_range.present? ? port_within_range?(entry.port_range, port_range) : true && - protocol.present? ? entry.protocol == protocol.to_s : true && - rule_action ? entry.rule_action == rule_action : true && - rule_number ? entry.rule_number == rule_number : true + acl_entries.any? { |entry| + if entry.cidr_block == cidr_block && + egress.to_s.present? + entry.egress == egress + else + if true && + icmp_type_code.present? + entry.icmp_type_code == icmp_type_code + else + if true && + ipv_6_cidr_block.present? + entry.ipv_6_cidr_block == ipv_6_cidr_block + else + if true && + port_range.present? + port_within_range?(entry.port_range, port_range) + else + if true && + protocol.present? + entry.protocol == protocol.to_s + else + if true && + rule_action + entry.rule_action == rule_action + else + if true && + rule_number + entry.rule_number == rule_number + else + true + end + end + end + end + end + end + end } end From f2f28563917400b798f555917ddaf476f07ca7a5 Mon Sep 17 00:00:00 2001 From: wdower Date: Tue, 12 Dec 2023 11:58:39 -0500 Subject: [PATCH 72/93] refactoring network_acl to use a filtertable for the acl rules for way easier test writing Signed-off-by: wdower --- libraries/aws_network_acl.rb | 68 ++++++++++-------------------------- 1 file changed, 18 insertions(+), 50 deletions(-) diff --git a/libraries/aws_network_acl.rb b/libraries/aws_network_acl.rb index dd9bbe6e7..6d99e32b7 100644 --- a/libraries/aws_network_acl.rb +++ b/libraries/aws_network_acl.rb @@ -15,6 +15,19 @@ class AwsNetworkACL < AwsResourceBase end " + attr_reader :acl_table + + FilterTable.create + .register_column(:cidr_block, field: :cidr_block) + .register_column(:egress, field: :egress) + .register_column(:icmp_type_code, field: :icmp_type_code) + .register_column(:ipv_6_cidr_block, field: :ipv_6_cidr_block) + .register_column(:port_range, field: :port_range) + .register_column(:protocol, field: :protocol) + .register_column(:rule_action, field: :rule_action) + .register_column(:rule_number, field: :rule_number) + .install_filter_methods_on_resource(self, :acl_table) + def initialize(opts = {}) opts = { network_acl_id: opts } if opts.is_a?(String) super @@ -52,52 +65,12 @@ def has_associations?(subnet_id: nil) associated_subnet_ids.any? { |associated_subnet_id| associated_subnet_id == subnet_id } end - def has_acl_entry_value?(cidr_block: nil, egress: nil, icmp_type_code: nil, ipv_6_cidr_block: nil, port_range: nil, protocol: nil, rule_action: nil, rule_number: nil) + def has_acl_entry_value?(cidr_block:, egress:, rule_action:) + invalid_args = method(__method__).parameters.select { |param| param.nil? || param.empty? } + raise ArgumentError, "params #{invalid_args.map { |i| "`#{i}`" }.join(",")} cannot be blank" if cidr_block.nil? || cidr_block.empty? return false unless acl_entries - # rules for all protocols are recorded as rules with protocol == -1 - protocol = "-1" if protocol.to_s.downcase == "all" - - # check for acl entries matching any combination of fields - # iff a field was passed to the matcher, then it is included as part of the test - acl_entries.any? { |entry| - if entry.cidr_block == cidr_block && - egress.to_s.present? - entry.egress == egress - else - if true && - icmp_type_code.present? - entry.icmp_type_code == icmp_type_code - else - if true && - ipv_6_cidr_block.present? - entry.ipv_6_cidr_block == ipv_6_cidr_block - else - if true && - port_range.present? - port_within_range?(entry.port_range, port_range) - else - if true && - protocol.present? - entry.protocol == protocol.to_s - else - if true && - rule_action - entry.rule_action == rule_action - else - if true && - rule_number - entry.rule_number == rule_number - else - true - end - end - end - end - end - end - end - } + acl_entries.any? { |entry| entry.egress == egress && entry.cidr_block == cidr_block && entry.rule_action == rule_action } end def has_egress?(cidr_block: nil, rule_action: nil) @@ -143,6 +116,7 @@ def fetch end create_resource_methods(network_acl_hash) create_rule_number_methods + @acl_table = network_acl.entries.map { |e| e.to_h } end def network_acl @@ -194,10 +168,4 @@ def cidr_block_and_rule_action_exists_for?(collection, cidr_block, rule_action) collection.any? { |entry| entry.cidr_block == cidr_block || entry.rule_action == rule_action } end - - def port_within_range?(acl_port_range, expected_port) - return false if acl_port_range.nil? || expected_port.nil? - # compare an Aws::EC2::Types::PortRange to a standard Range - (acl_port_range.from..acl_port_range.to).include?(expected_port) - end end From 5dabefd3f3376e8cf22baf0be86d6d24308ea73d Mon Sep 17 00:00:00 2001 From: wdower Date: Tue, 12 Dec 2023 16:40:41 -0500 Subject: [PATCH 73/93] adding better example for aws_network_acl Signed-off-by: wdower --- libraries/aws_network_acl.rb | 42 +++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/libraries/aws_network_acl.rb b/libraries/aws_network_acl.rb index 6d99e32b7..bf8139c44 100644 --- a/libraries/aws_network_acl.rb +++ b/libraries/aws_network_acl.rb @@ -1,5 +1,25 @@ require "aws_backend" +class AwsNetworkACLTable + + FilterTable.create + .register_column(:cidr_block, field: :cidr_block) + .register_column(:egress, field: :egress) + .register_column(:icmp_type_code, field: :icmp_type_code) + .register_column(:ipv_6_cidr_block, field: :ipv_6_cidr_block) + .register_column(:port_range, field: :port_range) + .register_column(:protocol, field: :protocol) + .register_column(:rule_action, field: :rule_action) + .register_column(:rule_number, field: :rule_number) + .install_filter_methods_on_resource(self, :acl_table) + + attr_reader :acl_table + + def initialize(acl_table) + @acl_table = acl_table + end +end + class AwsNetworkACL < AwsResourceBase EGRESS = "egress".freeze INGRESS = "ingress".freeze @@ -13,20 +33,12 @@ class AwsNetworkACL < AwsResourceBase describe aws_network_acl('014aef8a0689b8f43') do it { should exist } end - " - attr_reader :acl_table + describe aws_network_acl('014aef8a0689b8f43').acls.where(cidr_block: '0.0.0.0/0', rule_action: 'allow', protocol: '-1') do + it { should_not exist } + end - FilterTable.create - .register_column(:cidr_block, field: :cidr_block) - .register_column(:egress, field: :egress) - .register_column(:icmp_type_code, field: :icmp_type_code) - .register_column(:ipv_6_cidr_block, field: :ipv_6_cidr_block) - .register_column(:port_range, field: :port_range) - .register_column(:protocol, field: :protocol) - .register_column(:rule_action, field: :rule_action) - .register_column(:rule_number, field: :rule_number) - .install_filter_methods_on_resource(self, :acl_table) + " def initialize(opts = {}) opts = { network_acl_id: opts } if opts.is_a?(String) @@ -103,6 +115,11 @@ def to_s "Network ACL ID: #{@opts[:network_acl_id]}" end + def acls + return [] unless network_acl + AwsNetworkACLTable.new(network_acl.entries.map { |e| e.to_h }) + end + private def fetch @@ -116,7 +133,6 @@ def fetch end create_resource_methods(network_acl_hash) create_rule_number_methods - @acl_table = network_acl.entries.map { |e| e.to_h } end def network_acl From c6349668cbb727c5204989d3bb1f887a3d065211 Mon Sep 17 00:00:00 2001 From: wdower Date: Tue, 12 Dec 2023 16:42:58 -0500 Subject: [PATCH 74/93] better example formatting Signed-off-by: wdower --- libraries/aws_network_acl.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/aws_network_acl.rb b/libraries/aws_network_acl.rb index bf8139c44..e764ec5a5 100644 --- a/libraries/aws_network_acl.rb +++ b/libraries/aws_network_acl.rb @@ -25,7 +25,7 @@ class AwsNetworkACL < AwsResourceBase INGRESS = "ingress".freeze name "aws_network_acl" desc "Verifies settings for a single AWS Network ACL" - example " + example <<~EXAMPLE1 describe aws_network_acl(network_acl_id: '014aef8a0689b8f43') do it { should exist } end @@ -33,12 +33,12 @@ class AwsNetworkACL < AwsResourceBase describe aws_network_acl('014aef8a0689b8f43') do it { should exist } end - + EXAMPLE1 + example <<~EXAMPLE2 describe aws_network_acl('014aef8a0689b8f43').acls.where(cidr_block: '0.0.0.0/0', rule_action: 'allow', protocol: '-1') do it { should_not exist } end - - " + EXAMPLE2 def initialize(opts = {}) opts = { network_acl_id: opts } if opts.is_a?(String) From 0b93fc45dc25b76bc84b8d8d21862fb74d8ed6a8 Mon Sep 17 00:00:00 2001 From: wdower Date: Tue, 12 Dec 2023 16:44:38 -0500 Subject: [PATCH 75/93] linting Signed-off-by: wdower --- libraries/aws_network_acl.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/libraries/aws_network_acl.rb b/libraries/aws_network_acl.rb index e764ec5a5..cb5d8fb56 100644 --- a/libraries/aws_network_acl.rb +++ b/libraries/aws_network_acl.rb @@ -2,7 +2,7 @@ class AwsNetworkACLTable - FilterTable.create + FilterTable.create .register_column(:cidr_block, field: :cidr_block) .register_column(:egress, field: :egress) .register_column(:icmp_type_code, field: :icmp_type_code) @@ -13,11 +13,11 @@ class AwsNetworkACLTable .register_column(:rule_number, field: :rule_number) .install_filter_methods_on_resource(self, :acl_table) - attr_reader :acl_table + attr_reader :acl_table - def initialize(acl_table) - @acl_table = acl_table - end + def initialize(acl_table) + @acl_table = acl_table + end end class AwsNetworkACL < AwsResourceBase @@ -33,7 +33,7 @@ class AwsNetworkACL < AwsResourceBase describe aws_network_acl('014aef8a0689b8f43') do it { should exist } end - EXAMPLE1 + EXAMPLE1 example <<~EXAMPLE2 describe aws_network_acl('014aef8a0689b8f43').acls.where(cidr_block: '0.0.0.0/0', rule_action: 'allow', protocol: '-1') do it { should_not exist } @@ -117,7 +117,7 @@ def to_s def acls return [] unless network_acl - AwsNetworkACLTable.new(network_acl.entries.map { |e| e.to_h }) + AwsNetworkACLTable.new(network_acl.entries.map(&:to_h)) end private From 9963bc4930bdc40cdeefffa6795c1342f7462dbf Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Tue, 12 Dec 2023 17:09:37 -0500 Subject: [PATCH 76/93] cleaned up the of our filterTable helper Signed-off-by: Aaron Lippold --- libraries/aws_network_acl.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/libraries/aws_network_acl.rb b/libraries/aws_network_acl.rb index cb5d8fb56..6f09c15c5 100644 --- a/libraries/aws_network_acl.rb +++ b/libraries/aws_network_acl.rb @@ -13,10 +13,15 @@ class AwsNetworkACLTable .register_column(:rule_number, field: :rule_number) .install_filter_methods_on_resource(self, :acl_table) - attr_reader :acl_table + attr_reader :acl_table, :acl_name - def initialize(acl_table) + def initialize(acl_table, acl_name = nil) @acl_table = acl_table + @acl_name = acl_name + end + + def to_s + @acl_name.present? ? "ACL #{acl_name}" : "ACL: " end end @@ -117,7 +122,7 @@ def to_s def acls return [] unless network_acl - AwsNetworkACLTable.new(network_acl.entries.map(&:to_h)) + AwsNetworkACLTable.new(network_acl.entries.map(&:to_h), @opts[:network_acl_id]) end private From b45eddc4d2d0ee88e959972292160945dce9cf11 Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Tue, 12 Dec 2023 18:04:52 -0500 Subject: [PATCH 77/93] removed pry from testing Signed-off-by: Aaron Lippold --- libraries/aws_billing_contact.rb | 2 -- libraries/aws_iam_password_policy.rb | 2 -- libraries/aws_region.rb | 1 - 3 files changed, 5 deletions(-) diff --git a/libraries/aws_billing_contact.rb b/libraries/aws_billing_contact.rb index 6fa573fda..46384452d 100644 --- a/libraries/aws_billing_contact.rb +++ b/libraries/aws_billing_contact.rb @@ -26,8 +26,6 @@ def initialize(opts = {}) @title, @name, @email_address, @phone_number = "" validate_parameters catch_aws_errors do - # require "pry" - # binding.pry begin catch_aws_errors do @aws_account_id = fetch_aws_account diff --git a/libraries/aws_iam_password_policy.rb b/libraries/aws_iam_password_policy.rb index a79671510..e36bb5a78 100644 --- a/libraries/aws_iam_password_policy.rb +++ b/libraries/aws_iam_password_policy.rb @@ -18,8 +18,6 @@ def initialize(opts = {}) super(opts) validate_parameters @policy = nil - require "pry" - pry catch_aws_errors do @policy = @aws.iam_client.get_account_password_policy.password_policy @aws_account_id = fetch_aws_account diff --git a/libraries/aws_region.rb b/libraries/aws_region.rb index 63df03e90..6a3f760f0 100644 --- a/libraries/aws_region.rb +++ b/libraries/aws_region.rb @@ -1,5 +1,4 @@ require "aws_backend" -require "pry" class AwsRegion < AwsResourceBase name "aws_region" From b2e99850e4154d735f4599b2ffeaffc338914053 Mon Sep 17 00:00:00 2001 From: Will Dower Date: Thu, 14 Dec 2023 22:55:07 -0500 Subject: [PATCH 78/93] updating cloudtrail docs Signed-off-by: Will Dower --- .../inspec/resources/aws_cloudtrail_trail.md | 29 +++++++++++++++++++ libraries/aws_cloudtrail_trail.rb | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/docs-chef-io/content/inspec/resources/aws_cloudtrail_trail.md b/docs-chef-io/content/inspec/resources/aws_cloudtrail_trail.md index 3a520aa4d..5c918f767 100644 --- a/docs-chef-io/content/inspec/resources/aws_cloudtrail_trail.md +++ b/docs-chef-io/content/inspec/resources/aws_cloudtrail_trail.md @@ -130,6 +130,15 @@ describe aws_cloudtrail_trail('TRAIL_NAME') do end ``` +**Test if a trail is monitoring an AWS object type:** + +```ruby +describe aws_cloudtrail_trail('TRAIL_NAME') do + it { should be_monitoring_read("AWS::S3::Object") } + it { should be_monitoring_write("AWS::S3::Object") } +end +``` + ## Matchers {{% inspec_matchers_link %}} @@ -192,6 +201,26 @@ describe aws_cloudtrail_trail('TRAIL_NAME') do end ``` +### be_monitoring_read + +The test will pass if the identified trail is monitoring read events on the given AWS object type (if the trail is only monitoring one ARN of that object type, the test will fail). + +```ruby +describe aws_cloudtrail_trail('TRAIL_NAME') do + it { should be_monitoring_read("AWS::S3::Object") } +end +``` + +### be_monitoring_write + +The test will pass if the identified trail is monitoring write events on the given AWS object type (if the trail is only monitoring one ARN of that object type, the test will fail). + +```ruby +describe aws_cloudtrail_trail('TRAIL_NAME') do + it { should be_monitoring_write("AWS::S3::Object") } +end +``` + ## AWS Permissions {{% aws_permissions_principal action="CloudTrail:Client:DescribeTrailsResponse" %}} diff --git a/libraries/aws_cloudtrail_trail.rb b/libraries/aws_cloudtrail_trail.rb index 9bb567a66..db44dd6d3 100644 --- a/libraries/aws_cloudtrail_trail.rb +++ b/libraries/aws_cloudtrail_trail.rb @@ -4,7 +4,7 @@ class AwsCloudTrailTrail < AwsResourceBase name "aws_cloudtrail_trail" desc "Verifies settings for an individual AWS CloudTrail Trail." example <<-EXAMPLE - describe aws_cloudtrail_trail('TRIAL_NAME') do + describe aws_cloudtrail_trail('TRAIL_NAME') do it { should exist } it { should be_monitoring_read("AWS::S3::Object") } it { should be_monitoring_write("AWS::S3::Object") } From e47ebdb17e7849d473a3450f598036fbb3623332 Mon Sep 17 00:00:00 2001 From: wdower <57142072+wdower@users.noreply.github.com> Date: Fri, 15 Dec 2023 17:41:10 +0000 Subject: [PATCH 79/93] adding docs for aws_iam_access_analyzer Signed-off-by: wdower <57142072+wdower@users.noreply.github.com> --- .../resources/aws_iam_access_analyzers.md | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 docs-chef-io/content/inspec/resources/aws_iam_access_analyzers.md diff --git a/docs-chef-io/content/inspec/resources/aws_iam_access_analyzers.md b/docs-chef-io/content/inspec/resources/aws_iam_access_analyzers.md new file mode 100644 index 000000000..bc37a7e2c --- /dev/null +++ b/docs-chef-io/content/inspec/resources/aws_iam_access_analyzers.md @@ -0,0 +1,104 @@ ++++ +title = "aws_iam_access_analyzers Resource" +platform = "aws" +draft = false +gh_repo = "inspec-aws" + +[menu.inspec] +title = "aws_iam_access_analyzers" +identifier = "inspec/resources/aws/aws_iam_access_analyzers Resource" +parent = "inspec/resources/aws" ++++ + +Use the `aws_iam_access_analyzers` InSpec audit resource to verify settings for multiple AWS IAM Access Analyzers. + +For additional information, including details on parameters and properties, see the [AWS documentation on Access Analyzers](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-accessanalyzer-analyzer.html). + +## Installation + +{{% inspec_aws_install %}} + +## Syntax + +Ensure that an access analyzer (of either `account` or `organization` type) exists. + +```ruby +describe aws_iam_access_analyzers do + it { should exist } +end +``` + +## Parameters + +`type` _(optional)_ + +: The type of access analyzers to be tested. Must be one of `account` or `organization`. If this parameter is not given, the resource will return data for both types. + +## Properties + +`analyzer_names` +: List of names of returned analyzers. + +`analyzer_types` +: List of types of returned analyzers (`account` or `organization`). + +`arns` +: List of ARNs of returned analyzers. + +`created_date` +: List of creation dates of returned analyzers. + +`last_resource_analyzed` +: List of resources that were most recently analyzed by the returned analyzers. + +`last_analyzed_date` +: List of timestamps representing the times at which the returned analyzers last analyzed a resource. + +`tags` +: List of hashes of tags for each returned analyzer. + +`status` +: List of statuses for the returned analyzers (`Active`, `Disabled`, `Creating`, or `Failed`). + +`status_reason` +: List of details about the current status for each returned analyzer. + +## Examples + +Determine if an access analyzer for the AWS account (as opposed to the entire organization) exists: + +```ruby +describe aws_iam_access_analyzers('account') do + it { should exist } +end + +describe aws_iam_access_analyzers(type: 'account') do + it { should exist } +end +``` + +## Matchers + +{{% inspec_matchers_link %}} + +### exist + +Use `should` to test that the entity exists. + +```ruby +describe aws_iam_access_analyzers + it { should exist } +end +``` + +Use `should_not` to test the entity does not exist. + +```ruby +describe aws_iam_access_analyzers + it { should_not exist } +end +``` + +## AWS Permissions + +TODO From 87b6fb79b14433f1dcbd206112773c9ec573c869 Mon Sep 17 00:00:00 2001 From: wdower <57142072+wdower@users.noreply.github.com> Date: Fri, 15 Dec 2023 21:14:36 +0000 Subject: [PATCH 80/93] fixing incorrect action in the permissions section of aw_iam_group Signed-off-by: wdower <57142072+wdower@users.noreply.github.com> --- docs-chef-io/content/inspec/resources/aws_iam_group.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs-chef-io/content/inspec/resources/aws_iam_group.md b/docs-chef-io/content/inspec/resources/aws_iam_group.md index bb426d0d0..9e224e508 100644 --- a/docs-chef-io/content/inspec/resources/aws_iam_group.md +++ b/docs-chef-io/content/inspec/resources/aws_iam_group.md @@ -91,6 +91,6 @@ end ## AWS Permissions -{{% aws_permissions_principal action="IAM:Client:GetGroupResponse" %}} +{{% aws_permissions_principal action="iam:GetGroup" %}} -You can find detailed documentation at [Actions, Resources, and Condition Keys for Identity And Access Management](https://docs.aws.amazon.com/IAM/latest/UserGuide/list_identityandaccessmanagement.html). +You can find detailed documentation on this action in the [AWS API documentation](https://docs.aws.amazon.com/IAM/latest/APIReference/API_GetGroup.html). From 2f68fbc83b35a391fab70b723f49e4059b2cf1ff Mon Sep 17 00:00:00 2001 From: wdower <57142072+wdower@users.noreply.github.com> Date: Fri, 15 Dec 2023 21:15:15 +0000 Subject: [PATCH 81/93] adding permissions section to aws_iam_access_analyzers Signed-off-by: wdower <57142072+wdower@users.noreply.github.com> --- .../content/inspec/resources/aws_iam_access_analyzers.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs-chef-io/content/inspec/resources/aws_iam_access_analyzers.md b/docs-chef-io/content/inspec/resources/aws_iam_access_analyzers.md index bc37a7e2c..d34b46927 100644 --- a/docs-chef-io/content/inspec/resources/aws_iam_access_analyzers.md +++ b/docs-chef-io/content/inspec/resources/aws_iam_access_analyzers.md @@ -101,4 +101,6 @@ end ## AWS Permissions -TODO +{{% aws_permissions_principal action="access-analyzer:ListAnalyzers" %}} + +You can find detailed documentation on this action in the [AWS API documentation](https://docs.aws.amazon.com/access-analyzer/latest/APIReference/API_ListAnalyzers.html). From 2e24061edb1bcbc37024e87adaec2c7a5b47caa1 Mon Sep 17 00:00:00 2001 From: wdower <57142072+wdower@users.noreply.github.com> Date: Fri, 15 Dec 2023 22:11:03 +0000 Subject: [PATCH 82/93] adding docs for aws_iam_credential_report Signed-off-by: wdower <57142072+wdower@users.noreply.github.com> --- .../resources/aws_iam_credential_report.md | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 docs-chef-io/content/inspec/resources/aws_iam_credential_report.md diff --git a/docs-chef-io/content/inspec/resources/aws_iam_credential_report.md b/docs-chef-io/content/inspec/resources/aws_iam_credential_report.md new file mode 100644 index 000000000..94b4e75db --- /dev/null +++ b/docs-chef-io/content/inspec/resources/aws_iam_credential_report.md @@ -0,0 +1,145 @@ ++++ +title = "aws_iam_credential_report Resource" +platform = "aws" +draft = false +gh_repo = "inspec-aws" + +[menu.inspec] +title = "aws_iam_credential_report" +identifier = "inspec/resources/aws/aws_iam_credential_report Resource" +parent = "inspec/resources/aws" ++++ + +Use the `aws_iam_credential_report` InSpec audit resource to list all users in the AWS account and the status of their credentials. + +For additional information, including details on parameters and properties, see the [AWS documentation on Credential Reports](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_getting-report.html). + +## Installation + +{{% inspec_aws_install %}} + +## Syntax + +Use the AWS Credential Report to query data about users' access credential configurations and the timestamps at which credentials were last used. + +```ruby +describe aws_iam_credential_report.where(user: username) do + its('mfa_active') { should eq true } +end +``` + +## Parameters + +This resource does not require any parameters. + +## Properties + +`user` +: List of the usernames (not full ARNs) associated with the account. + +`arn` +: List of the full ARNs of users associated with the account. + +`user_creation_time` +: List of the timestamps when the user was created, in ISO 8601 date-time format. + +`password_enabled` +: List of booleans for whether each user has a password enabled for the AWS console. The value for the AWS account root user is always "not_supported". + +`password_last_used` +: List of the timestamps when the user last logged in using the password, in ISO 8601 date-time format (value will be 'N/A' for a user with no password or a user who has never logged in with their password). + +`password_last_changed` +: List of the timestamps when the user last changed their password, in ISO 8601 date-time format (value will be 'N/A' for a user with no password or a user who has never logged in with their password). The value for the AWS account root user is always "not_supported". + +`password_next_rotation` +: List of the dates and times (in ISO 8601 date-time format) at which each user will be forced to change the password, if the user is required to rotate passwords (value will be 'N/A' for a user with no password or a user who has never logged in with their password). The value for the AWS account root user is always "not_supported". + +`mfa_active` +: List of booleans for whether each user has a multi-factor authentication (MFA) device enabled. + +`access_key_1_active` +: List of booleans for whether each user has an active access key in their first key slot. + +`access_key_1_last_rotated` +: List of dates and times (in ISO 8601 date-time format) for when each user's first access key was last rotated (value will be 'N/A' for users without an access key in the first slot, if the key has never been used). + +`access_key_1_last_used_date` +: List of dates and times (in ISO 8601 date-time format) for when each user's first access key was last used to sign an AWS API request (value will be 'N/A' for users without an access key in the first slot, or if the key has never been used). + +`access_key_1_last_used_region` +: List of AWS regions in which each user's first access key was last used to sign an AWS API request (value will be 'N/A' for users without an access key in the first slot, if the key has never been used, or if the last service this key was used for is not region-specific). + +`access_key_1_last_used_service` +: List of AWS services for which each user's first access key was last used to sign an AWS API request (value will be 'N/A' for users without an access key in the first slot, or if the key has never been used). + +`access_key_2_active` +: List of booleans for whether each user has an active access key in their second key slot. + +`access_key_2_last_rotated` +: List of dates and times (in ISO 8601 date-time format) for when each user's second access key was last rotated (value will be 'N/A' for users without an access key in the second slot, if the key has never been used). + +`access_key_2_last_used_date` +: List of dates and times (in ISO 8601 date-time format) for when each user's second access key was last used to sign an AWS API request (value will be 'N/A' for users without an access key in the second slot, or if the key has never been used). + +`access_key_2_last_used_region` +: List of AWS regions in which each user's second access key was last used to sign an AWS API request (value will be 'N/A' for users without an access key in the second slot, if the key has never been used, or if the last service this key was used for is not region-specific). + +`access_key_2_last_used_service` +: List of AWS services for which each user's second access key was last used to sign an AWS API request (value will be 'N/A' for users without an access key in the second slot, or if the key has never been used). + +`cert_1_active` +: List of booleans for whether each user has an X.509 signing certificate and the certificate is active. + +`cert_1_last_rotated` +: List of dates and times (in ISO 8601 date-time format) for when each user's signing certificate was created or last changed (value will be 'N/A' for users with no active certificate). + +`cert_2_active` +: List of booleans for whether each user has a second X.509 signing certificate and the certificate is active. + +`cert_2_last_rotated` +: List of dates and times (in ISO 8601 date-time format) for when each user's second signing certificate was created or last changed (value will be 'N/A' for users with no active certificate or only one active certificate). + +## Examples + +Determine if the root user has MFA enabled: + +```ruby +describe aws_iam_credential_report.where(user: '').entries.first do + its('mfa_active') { should eq true } +end +``` + +Ensuring that all users with passwords have used them within the last month: +```ruby +aws_iam_credential_report.where(password_enabled: true).entries.each do |user| + describe "The user (#{user.user})" do + subject { ((Time.current - user.password_last_used) / (24 * 60 * 60)).to_i } + it 'must have used their password within the last 30 days.' do + expect(subject).to be < 30 + end + end +end +``` + +Check if access keys for all users have been rotated within the last month: +```ruby +aws_iam_credential_report.where(access_key_1_active: true).entries.each do |user| + describe "The user (#{user.user})" do + subject { ((Time.current - user.access_key_1_last_used_date) / (24 * 60 * 60)).to_i } + it 'must have used access key 1 within the last 90 days.' do + expect(subject).to be < 90 + end + end +end +``` +## Matchers + +{{% inspec_matchers_link %}} + +## AWS Permissions + +Your [Principal](https://docs.aws.amazon.com/IAM/latest/UserGuide/intro-structure.html#intro-structure-principal) will need the `{{ .Get "iam:GenerateCredentialReport" }}` action and the `{{ .Get "iam:GetCredentialReport" }}` with `Effect` set to `Allow`. + +You can find detailed documentation on these actions in the AWS API documentation: [GenerateCredentialReport](https://docs.aws.amazon.com/IAM/latest/APIReference/API_GenerateCredentialReport.html), [GetCredentialReport](https://docs.aws.amazon.com/IAM/latest/APIReference/API_GetCredentialReport.html). + From 75aa2a86db8715780aabceadfd1d94ecf035eec4 Mon Sep 17 00:00:00 2001 From: wdower <57142072+wdower@users.noreply.github.com> Date: Mon, 18 Dec 2023 23:50:24 +0000 Subject: [PATCH 83/93] added a jobs filtertable to macie resource Signed-off-by: wdower <57142072+wdower@users.noreply.github.com> --- libraries/aws_macie.rb | 61 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/libraries/aws_macie.rb b/libraries/aws_macie.rb index ad9dcfc30..d164e592e 100644 --- a/libraries/aws_macie.rb +++ b/libraries/aws_macie.rb @@ -1,5 +1,40 @@ require "aws_backend" +class AwsMacieJobTable + + FilterTable.create + .register_column(:bucket_criteria, field: :bucket_criteria) + .register_column(:bucket_definitions, field: :bucket_definitions) + .register_column(:created_at, field: :created_at) + .register_column(:job_id, field: :job_id) + .register_column(:job_status, field: :job_status) + .register_column(:job_type, field: :job_type) + .register_column(:last_run_error_status, field: :last_run_error_status) + .register_column(:name, field: :name) + .register_column(:user_paused_details, field: :user_paused_details) + .install_filter_methods_on_resource(self, :job_table) + + attr_reader :job_table, :job_name + + def monitoring?(buckets) + self.bucket_definitions.any? { |bd| + bd.any? { |account| + (buckets - account[:buckets]).empty? + } + } + end + + def initialize(job_table, job_name = nil) + @job_table = job_table + @job_name = job_name + end + + def to_s + @job_name + end +end + + class AWSMacie < AwsResourceBase name "aws_macie" desc "Gets information about Macie status and configuration." @@ -11,7 +46,7 @@ class AWSMacie < AwsResourceBase end " - attr_reader :session, :jobs, :buckets, :organization_configuration + attr_reader:buckets, :organization_configuration def initialize(opts = {}) @raw_data = {} @@ -27,7 +62,9 @@ def fetch_data begin @session = @aws.macie_client.get_macie_session @jobs = @aws.macie_client.list_classification_jobs - @buckets = @aws.macie_client.describe_buckets + @jobs_table = [] + @buckets = @aws.macie_client.describe_buckets.buckets + @buckets_table = [] rescue Aws::Errors::NoSuchEndpointError skip_resource( "The account contact endpoint is not available in this segment, please review this via the AWS Management Console.", @@ -39,6 +76,19 @@ def fetch_data end end + def jobs + return [] unless @jobs + AwsMacieJobTable.new(@jobs.items.map(&:to_h)) + end + + def monitoring_buckets?(buckets) + return false unless @jobs + b = [buckets] unless buckets.is_a?(Array) + jobs.monitoring?(b) + end + + alias monitoring_bucket? monitoring_buckets? + # jobs.items.first.bucket_definitions.first.to_h[:buckets] # aws.macie_client.describe_buckets.buckets.count # aws.macie_client.list_classification_jobs.items.first['bucket_definitions'] @@ -73,4 +123,11 @@ def enabled? def to_s "AWS Macie" end + + private + + def fetch_aws_region + arn = @aws.sts_client.get_caller_identity({}).arn + arn.split(":")[1] + end end From 6bf7ec2b9e56187142358d52a8aa34f5e6decb41 Mon Sep 17 00:00:00 2001 From: wdower <57142072+wdower@users.noreply.github.com> Date: Tue, 19 Dec 2023 17:50:40 +0000 Subject: [PATCH 84/93] rounding out macie with more filtertables for findings and buckets -- updated monitored? method to work better with lists of buckets Signed-off-by: wdower <57142072+wdower@users.noreply.github.com> --- libraries/aws_macie.rb | 122 +++++++++++++++++++++++++++++++++++------ 1 file changed, 104 insertions(+), 18 deletions(-) diff --git a/libraries/aws_macie.rb b/libraries/aws_macie.rb index d164e592e..348145abe 100644 --- a/libraries/aws_macie.rb +++ b/libraries/aws_macie.rb @@ -4,7 +4,7 @@ class AwsMacieJobTable FilterTable.create .register_column(:bucket_criteria, field: :bucket_criteria) - .register_column(:bucket_definitions, field: :bucket_definitions) + .register_column(:bucket_definitions, field: :bucket_definitions) .register_column(:created_at, field: :created_at) .register_column(:job_id, field: :job_id) .register_column(:job_status, field: :job_status) @@ -17,11 +17,12 @@ class AwsMacieJobTable attr_reader :job_table, :job_name def monitoring?(buckets) - self.bucket_definitions.any? { |bd| - bd.any? { |account| - (buckets - account[:buckets]).empty? - } - } + self.entries.each do |job| + job[:bucket_definitions].each do |bd| + buckets = buckets - bd[:buckets] + end + end + buckets.empty? end def initialize(job_table, job_name = nil) @@ -30,10 +31,87 @@ def initialize(job_table, job_name = nil) end def to_s - @job_name + @job_name.present? ? @job_name : "AWS Macie Jobs" end end +class AwsMacieBucketTable + + FilterTable.create + .register_column(:account_id, field: :account_id) + .register_column(:allows_unencrypted_object_uploads, field: :allows_unencrypted_object_uploads) + .register_column(:bucket_arn, field: :bucket_arn) + .register_column(:bucket_created_at, field: :bucket_created_at) + .register_column(:bucket_name, field: :bucket_name) + .register_column(:classifiable_object_count, field: :classifiable_object_count) + .register_column(:classifiable_size_in_bytes, field: :classifiable_size_in_bytes) + .register_column(:error_code, field: :error_code) + .register_column(:error_message, field: :error_message) + .register_column(:job_details, field: :job_details) + .register_column(:last_automated_discovery_time, field: :last_automated_discovery_time) + .register_column(:last_updated, field: :last_updated) + .register_column(:object_count, field: :object_count) + .register_column(:object_count_by_encryption_type, field: :object_count_by_encryption_type) + .register_column(:public_access, field: :public_access) + .register_column(:region, field: :region) + .register_column(:replication_details, field: :replication_details) + .register_column(:sensitivity_score, field: :sensitivity_score) + .register_column(:server_side_encryption, field: :server_side_encryption) + .register_column(:shared_access, field: :shared_access) + .register_column(:size_in_bytes, field: :size_in_bytes) + .register_column(:size_in_bytes_compressed, field: :size_in_bytes_compressed) + .register_column(:tags, field: :tags) + .register_column(:unclassifiable_object_count, field: :unclassifiable_object_count) + .register_column(:unclassifiable_object_size_in_bytes, field: :unclassifiable_object_size_in_bytes) + .register_column(:versioning, field: :versioning) + .install_filter_methods_on_resource(self, :buckets_table) + + attr_reader :buckets_table, :buckets_name + + def initialize(buckets_table, buckets_name = nil) + @buckets_table = buckets_table + @buckets_name = buckets_name + end + + def to_s + @buckets_name.present? ? @buckets_name : "AWS Macie Buckets" + end +end + +# class AwsMacieFindingTable + +# FilterTable.create +# .register_column(:account_id, field: :account_id) +# .register_column(:archived, field: :archived) +# .register_column(:category, field: :category) +# .register_column(:classification_details, field: :classification_details) +# .register_column(:count, field: :count) +# .register_column(:created_at, field: :created_at) +# .register_column(:description, field: :description) +# .register_column(:id, field: :id) +# .register_column(:partition, field: :partition) +# .register_column(:policy_details, field: :policy_details) +# .register_column(:region, field: :region) +# .register_column(:resources_affected, field: :resources_affected) +# .register_column(:sample, field: :sample) +# .register_column(:schema_version, field: :schema_version) +# .register_column(:severity, field: :severity) +# .register_column(:title, field: :title) +# .register_column(:type, field: :type) +# .register_column(:updated_at, field: :updated_at) +# .install_filter_methods_on_resource(self, :job_table) + +# attr_reader :findings_table, :findings_name + +# def initialize(findings_table, findings_name = nil) +# @findings_table = findings_table +# @findings_name = findings_name +# end + +# def to_s +# @findings_name.present? ? @findings_name : "AWS Macie Findings" +# end +# end class AWSMacie < AwsResourceBase name "aws_macie" @@ -62,9 +140,11 @@ def fetch_data begin @session = @aws.macie_client.get_macie_session @jobs = @aws.macie_client.list_classification_jobs - @jobs_table = [] - @buckets = @aws.macie_client.describe_buckets.buckets - @buckets_table = [] + @jobs.present? ? @jobs_table = AwsMacieJobTable.new(@jobs.items.map(&:to_h)) : @jobs_table = [] + @buckets = @aws.macie_client.describe_buckets + @buckets.present? ? @buckets_table = AwsMacieBucketTable.new(@buckets.buckets.map(&:to_h)) : @buckets_table = [] + # @findings = @aws.macie_client.get_findings + # @findings.present? ? @findings_table = AwsMacieFindingTable.new(@findings.findings.map(&:to_h)) : @findings_table = [] rescue Aws::Errors::NoSuchEndpointError skip_resource( "The account contact endpoint is not available in this segment, please review this via the AWS Management Console.", @@ -76,9 +156,21 @@ def fetch_data end end + def session + return [] unless @session.present? + @session + end + def jobs - return [] unless @jobs - AwsMacieJobTable.new(@jobs.items.map(&:to_h)) + @jobs_table + end + + def buckets + @buckets_table + end + + def findings + @findings_table end def monitoring_buckets?(buckets) @@ -114,12 +206,6 @@ def enabled? @session.status == "ENABLED" end - # def monitoring?(bucket_list) - # @jobs.any? { |job| - # job. - # } - # end - def to_s "AWS Macie" end From e2844d0e48d7e30dd9312cc9f173c9404c844aee Mon Sep 17 00:00:00 2001 From: wdower <57142072+wdower@users.noreply.github.com> Date: Tue, 19 Dec 2023 21:08:01 +0000 Subject: [PATCH 85/93] updating cloudtrail function for mgmt events to understand advanced event selectors Signed-off-by: wdower <57142072+wdower@users.noreply.github.com> --- libraries/aws_cloudtrail_trail.rb | 37 +++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/libraries/aws_cloudtrail_trail.rb b/libraries/aws_cloudtrail_trail.rb index db44dd6d3..f027ca9e1 100644 --- a/libraries/aws_cloudtrail_trail.rb +++ b/libraries/aws_cloudtrail_trail.rb @@ -108,13 +108,31 @@ def get_log_group_for_multi_region_active_mgmt_rw_all def has_event_selector_mgmt_events_rw_type_all? return nil unless exists? event_selector_found = false - begin - @event_selectors.event_selectors.each do |es| - event_selector_found = true if es.read_write_type == "All" && - es.include_management_events == true + require pry + pry + if using_basic_event_selectors? + begin + @event_selectors.event_selectors.each do |es| + event_selector_found = true if es.read_write_type == "All" && + es.include_management_events == true + end + rescue Aws::CloudTrail::Errors::TrailNotFoundException + event_selector_found end - rescue Aws::CloudTrail::Errors::TrailNotFoundException - event_selector_found + else + event_selector_found = @event_selectors.advanced_event_selectors.any? { |es| + ( + # check if readOnly is unset entirely (means both read and write are logged) + es.field_selectors.none? do |fs| + fs.field == "readOnly" + end + ) && ( + # check if a field selector is set to track management events + es.field_selectors.any? do |fs| + fs.field == "eventCategory" && fs.equals == ["Management"] + end + ) + } end event_selector_found end @@ -127,9 +145,10 @@ def monitoring?(aws_resource_type, mode) es.read_write_type.match?(/All|#{basic_mode}/) && es.data_resources.any? do |dr| dr.type.include?(aws_resource_type) && - dr.values.all? do |val| # make sure the values do not indicate individual resources - val.split(%r{[:/]}).count <= 3 # can be of the form 'arn:aws:s3' but not - # 'arn:aws:s3:::/' + # make sure the values do not indicate individual resources + dr.values.all? do |val| + # val can be of the form 'arn:aws:s3' but not 'arn:aws:s3:::/' + val.split(%r{[:/]}).count <= 3 end end end From 7486e2403fa8ab604bee3fc07c7177706645afd1 Mon Sep 17 00:00:00 2001 From: wdower <57142072+wdower@users.noreply.github.com> Date: Tue, 19 Dec 2023 21:08:42 +0000 Subject: [PATCH 86/93] working out findings table in macie Signed-off-by: wdower <57142072+wdower@users.noreply.github.com> --- libraries/aws_macie.rb | 133 ++++++++++++++++++----------------------- 1 file changed, 58 insertions(+), 75 deletions(-) diff --git a/libraries/aws_macie.rb b/libraries/aws_macie.rb index 348145abe..0dcdfb461 100644 --- a/libraries/aws_macie.rb +++ b/libraries/aws_macie.rb @@ -3,23 +3,23 @@ class AwsMacieJobTable FilterTable.create - .register_column(:bucket_criteria, field: :bucket_criteria) + .register_column(:bucket_criteria, field: :bucket_criteria) .register_column(:bucket_definitions, field: :bucket_definitions) - .register_column(:created_at, field: :created_at) - .register_column(:job_id, field: :job_id) + .register_column(:created_at, field: :created_at) + .register_column(:job_id, field: :job_id) .register_column(:job_status, field: :job_status) .register_column(:job_type, field: :job_type) - .register_column(:last_run_error_status, field: :last_run_error_status) - .register_column(:name, field: :name) + .register_column(:last_run_error_status, field: :last_run_error_status) + .register_column(:name, field: :name) .register_column(:user_paused_details, field: :user_paused_details) .install_filter_methods_on_resource(self, :job_table) attr_reader :job_table, :job_name def monitoring?(buckets) - self.entries.each do |job| + entries.each do |job| job[:bucket_definitions].each do |bd| - buckets = buckets - bd[:buckets] + buckets -= bd[:buckets] end end buckets.empty? @@ -40,12 +40,12 @@ class AwsMacieBucketTable FilterTable.create .register_column(:account_id, field: :account_id) .register_column(:allows_unencrypted_object_uploads, field: :allows_unencrypted_object_uploads) - .register_column(:bucket_arn, field: :bucket_arn) + .register_column(:bucket_arn, field: :bucket_arn) .register_column(:bucket_created_at, field: :bucket_created_at) .register_column(:bucket_name, field: :bucket_name) - .register_column(:classifiable_object_count, field: :classifiable_object_count) - .register_column(:classifiable_size_in_bytes, field: :classifiable_size_in_bytes) - .register_column(:error_code, field: :error_code) + .register_column(:classifiable_object_count, field: :classifiable_object_count) + .register_column(:classifiable_size_in_bytes, field: :classifiable_size_in_bytes) + .register_column(:error_code, field: :error_code) .register_column(:error_message, field: :error_message) .register_column(:job_details, field: :job_details) .register_column(:last_automated_discovery_time, field: :last_automated_discovery_time) @@ -78,40 +78,40 @@ def to_s end end -# class AwsMacieFindingTable - -# FilterTable.create -# .register_column(:account_id, field: :account_id) -# .register_column(:archived, field: :archived) -# .register_column(:category, field: :category) -# .register_column(:classification_details, field: :classification_details) -# .register_column(:count, field: :count) -# .register_column(:created_at, field: :created_at) -# .register_column(:description, field: :description) -# .register_column(:id, field: :id) -# .register_column(:partition, field: :partition) -# .register_column(:policy_details, field: :policy_details) -# .register_column(:region, field: :region) -# .register_column(:resources_affected, field: :resources_affected) -# .register_column(:sample, field: :sample) -# .register_column(:schema_version, field: :schema_version) -# .register_column(:severity, field: :severity) -# .register_column(:title, field: :title) -# .register_column(:type, field: :type) -# .register_column(:updated_at, field: :updated_at) -# .install_filter_methods_on_resource(self, :job_table) - -# attr_reader :findings_table, :findings_name - -# def initialize(findings_table, findings_name = nil) -# @findings_table = findings_table -# @findings_name = findings_name -# end - -# def to_s -# @findings_name.present? ? @findings_name : "AWS Macie Findings" -# end -# end +class AwsMacieFindingTable + + FilterTable.create + .register_column(:account_id, field: :account_id) + .register_column(:archived, field: :archived) + .register_column(:category, field: :category) + .register_column(:classification_details, field: :classification_details) + .register_column(:count, field: :count) + .register_column(:created_at, field: :created_at) + .register_column(:description, field: :description) + .register_column(:id, field: :id) + .register_column(:partition, field: :partition) + .register_column(:policy_details, field: :policy_details) + .register_column(:region, field: :region) + .register_column(:resources_affected, field: :resources_affected) + .register_column(:sample, field: :sample) + .register_column(:schema_version, field: :schema_version) + .register_column(:severity, field: :severity) + .register_column(:title, field: :title) + .register_column(:type, field: :type) + .register_column(:updated_at, field: :updated_at) + .install_filter_methods_on_resource(self, :job_table) + + attr_reader :findings_table, :findings_name + + def initialize(findings_table, findings_name = nil) + @findings_table = findings_table + @findings_name = findings_name + end + + def to_s + @findings_name.present? ? @findings_name : "AWS Macie Findings" + end +end class AWSMacie < AwsResourceBase name "aws_macie" @@ -124,8 +124,6 @@ class AWSMacie < AwsResourceBase end " - attr_reader:buckets, :organization_configuration - def initialize(opts = {}) @raw_data = {} @res = {} @@ -143,16 +141,14 @@ def fetch_data @jobs.present? ? @jobs_table = AwsMacieJobTable.new(@jobs.items.map(&:to_h)) : @jobs_table = [] @buckets = @aws.macie_client.describe_buckets @buckets.present? ? @buckets_table = AwsMacieBucketTable.new(@buckets.buckets.map(&:to_h)) : @buckets_table = [] - # @findings = @aws.macie_client.get_findings - # @findings.present? ? @findings_table = AwsMacieFindingTable.new(@findings.findings.map(&:to_h)) : @findings_table = [] + @findings = [] + @findings_table = [] rescue Aws::Errors::NoSuchEndpointError skip_resource( "The account contact endpoint is not available in this segment, please review this via the AWS Management Console.", ) end return [] if (!@session or @session.empty?) || (!@jobs or @jobs.empty?) || (!@bucket or @bucket.empty?) - # Can't do this until we setup the user to be a Macie Admin? - # @organization_configuration = @aws.macie_client.describe_organization_configuration end end @@ -169,8 +165,16 @@ def buckets @buckets_table end - def findings - @findings_table + def findings(finding_ids, sort_criteria = nil) + catch_aws_errors do + begin + require "pry" + pry + findings = @aws.macie_client.get_findings(finding_ids, sort_criteria) + + findings.present? ? AwsMacieFindingTable.new(@findings.findings.map(&:to_h)) : [] + end + end end def monitoring_buckets?(buckets) @@ -179,28 +183,7 @@ def monitoring_buckets?(buckets) jobs.monitoring?(b) end - alias monitoring_bucket? monitoring_buckets? - - # jobs.items.first.bucket_definitions.first.to_h[:buckets] - # aws.macie_client.describe_buckets.buckets.count - # aws.macie_client.list_classification_jobs.items.first['bucket_definitions'] - - # it may be more strait forward to have multiple small resources so this one doesn't get hug - - # aws_macie base resource - # - this may have 'session' method via get_macie_session - # - members via list_members - # - list_organization_admin_accounts ? - # - perhaps owner etc. - # - usuage_totals - via get_usage_totals - # aws_macie_jobs ... - # aws_macie_job(job_id) ... - # aws_macie_buckets - # aws_macie_buckets(bucket_name or arn) - # aws_macie_findings - # - list_findings - # aws_macie_finding - # - get_finding_statistics(finding_id) + alias monitoring_bucket? monitoring_buckets? def enabled? @session.status == "ENABLED" From d0c4f65d518654c9ec06d95242c3a1d377a5769f Mon Sep 17 00:00:00 2001 From: wdower <57142072+wdower@users.noreply.github.com> Date: Tue, 19 Dec 2023 21:30:26 +0000 Subject: [PATCH 87/93] updating cloudtrail mgmt events function Signed-off-by: wdower <57142072+wdower@users.noreply.github.com> --- libraries/aws_cloudtrail_trail.rb | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/libraries/aws_cloudtrail_trail.rb b/libraries/aws_cloudtrail_trail.rb index f027ca9e1..e6ad9fd33 100644 --- a/libraries/aws_cloudtrail_trail.rb +++ b/libraries/aws_cloudtrail_trail.rb @@ -108,8 +108,6 @@ def get_log_group_for_multi_region_active_mgmt_rw_all def has_event_selector_mgmt_events_rw_type_all? return nil unless exists? event_selector_found = false - require pry - pry if using_basic_event_selectors? begin @event_selectors.event_selectors.each do |es| @@ -121,17 +119,10 @@ def has_event_selector_mgmt_events_rw_type_all? end else event_selector_found = @event_selectors.advanced_event_selectors.any? { |es| - ( - # check if readOnly is unset entirely (means both read and write are logged) - es.field_selectors.none? do |fs| - fs.field == "readOnly" - end - ) && ( - # check if a field selector is set to track management events - es.field_selectors.any? do |fs| - fs.field == "eventCategory" && fs.equals == ["Management"] - end - ) + # check if readOnly is unset entirely (means both read and write are logged) + es.field_selectors.none? { |fs| fs.field == "readOnly" } && \ + # check if a field selector is set to track management events + es.field_selectors.any? { |fs| fs.field == "eventCategory" && fs.equals == ["foobar"] } } end event_selector_found From 4444c6b7188b805886bf7d85e47c4c990411f178 Mon Sep 17 00:00:00 2001 From: wdower <57142072+wdower@users.noreply.github.com> Date: Tue, 19 Dec 2023 21:31:58 +0000 Subject: [PATCH 88/93] removing errant foobar Signed-off-by: wdower <57142072+wdower@users.noreply.github.com> --- libraries/aws_cloudtrail_trail.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/aws_cloudtrail_trail.rb b/libraries/aws_cloudtrail_trail.rb index e6ad9fd33..8acd959f1 100644 --- a/libraries/aws_cloudtrail_trail.rb +++ b/libraries/aws_cloudtrail_trail.rb @@ -122,7 +122,7 @@ def has_event_selector_mgmt_events_rw_type_all? # check if readOnly is unset entirely (means both read and write are logged) es.field_selectors.none? { |fs| fs.field == "readOnly" } && \ # check if a field selector is set to track management events - es.field_selectors.any? { |fs| fs.field == "eventCategory" && fs.equals == ["foobar"] } + es.field_selectors.any? { |fs| fs.field == "eventCategory" && fs.equals == ["Management"] } } end event_selector_found From 13b6cba144b793404cd05ee187d0b28d7bb29432 Mon Sep 17 00:00:00 2001 From: wdower <57142072+wdower@users.noreply.github.com> Date: Thu, 21 Dec 2023 16:38:52 +0000 Subject: [PATCH 89/93] full macie resource Signed-off-by: wdower <57142072+wdower@users.noreply.github.com> --- libraries/aws_macie.rb | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/libraries/aws_macie.rb b/libraries/aws_macie.rb index 0dcdfb461..d3844e959 100644 --- a/libraries/aws_macie.rb +++ b/libraries/aws_macie.rb @@ -99,7 +99,7 @@ class AwsMacieFindingTable .register_column(:title, field: :title) .register_column(:type, field: :type) .register_column(:updated_at, field: :updated_at) - .install_filter_methods_on_resource(self, :job_table) + .install_filter_methods_on_resource(self, :findings_table) attr_reader :findings_table, :findings_name @@ -120,7 +120,11 @@ class AWSMacie < AwsResourceBase example " describe aws_macie do it { should be_enabled } - it { should be_monitoring(['arn1', 'arn2', 'arn3']) } + it { should be_monitoring_buckets(['arn1', 'arn2', 'arn3']) } + end + + describe aws_macie.findings do + its('count') { should eq 0 } end " @@ -142,7 +146,6 @@ def fetch_data @buckets = @aws.macie_client.describe_buckets @buckets.present? ? @buckets_table = AwsMacieBucketTable.new(@buckets.buckets.map(&:to_h)) : @buckets_table = [] @findings = [] - @findings_table = [] rescue Aws::Errors::NoSuchEndpointError skip_resource( "The account contact endpoint is not available in this segment, please review this via the AWS Management Console.", @@ -165,14 +168,18 @@ def buckets @buckets_table end - def findings(finding_ids, sort_criteria = nil) + def findings(finding_ids=[], sort_criteria: nil) catch_aws_errors do begin - require "pry" - pry - findings = @aws.macie_client.get_findings(finding_ids, sort_criteria) - - findings.present? ? AwsMacieFindingTable.new(@findings.findings.map(&:to_h)) : [] + if finding_ids.blank? + # if the user didn't pass a parameter for specific finding ids, or a single id, then fetch them all + finding_ids = @aws.macie_client.list_findings.finding_ids + end + # catch if the user passed in a single ID + finding_ids = [finding_ids] unless finding_ids.is_a?(Array) + + findings = @aws.macie_client.get_findings(finding_ids: finding_ids, sort_criteria: sort_criteria) + findings.present? ? AwsMacieFindingTable.new(findings.findings.map(&:to_h)) : [] end end end From 428c2a54cd4b51acb0024a18944b77c1803fc1b4 Mon Sep 17 00:00:00 2001 From: wdower <57142072+wdower@users.noreply.github.com> Date: Thu, 21 Dec 2023 17:09:57 +0000 Subject: [PATCH 90/93] removing a bunch of complexity from macie because the filtertable basically does it for us anywhay Signed-off-by: wdower <57142072+wdower@users.noreply.github.com> --- libraries/aws_macie.rb | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/libraries/aws_macie.rb b/libraries/aws_macie.rb index d3844e959..426fe81fe 100644 --- a/libraries/aws_macie.rb +++ b/libraries/aws_macie.rb @@ -145,7 +145,10 @@ def fetch_data @jobs.present? ? @jobs_table = AwsMacieJobTable.new(@jobs.items.map(&:to_h)) : @jobs_table = [] @buckets = @aws.macie_client.describe_buckets @buckets.present? ? @buckets_table = AwsMacieBucketTable.new(@buckets.buckets.map(&:to_h)) : @buckets_table = [] - @findings = [] + @findings = @aws.macie_client.list_findings.finding_ids + @findings.present? ? @findings_table = AwsMacieFindingTable.new( + @aws.macie_client.get_findings(finding_ids: @findings).findings.map(&:to_h) + ) : @findings_table = [] rescue Aws::Errors::NoSuchEndpointError skip_resource( "The account contact endpoint is not available in this segment, please review this via the AWS Management Console.", @@ -168,20 +171,8 @@ def buckets @buckets_table end - def findings(finding_ids=[], sort_criteria: nil) - catch_aws_errors do - begin - if finding_ids.blank? - # if the user didn't pass a parameter for specific finding ids, or a single id, then fetch them all - finding_ids = @aws.macie_client.list_findings.finding_ids - end - # catch if the user passed in a single ID - finding_ids = [finding_ids] unless finding_ids.is_a?(Array) - - findings = @aws.macie_client.get_findings(finding_ids: finding_ids, sort_criteria: sort_criteria) - findings.present? ? AwsMacieFindingTable.new(findings.findings.map(&:to_h)) : [] - end - end + def findings + @findings_table end def monitoring_buckets?(buckets) From c02ed57604829c214a9ae9496803fae2d623be6d Mon Sep 17 00:00:00 2001 From: wdower <57142072+wdower@users.noreply.github.com> Date: Thu, 21 Dec 2023 17:11:24 +0000 Subject: [PATCH 91/93] removing unused function from macie, privating the data fetcher Signed-off-by: wdower <57142072+wdower@users.noreply.github.com> --- libraries/aws_macie.rb | 43 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/libraries/aws_macie.rb b/libraries/aws_macie.rb index 426fe81fe..e4236e51a 100644 --- a/libraries/aws_macie.rb +++ b/libraries/aws_macie.rb @@ -137,27 +137,6 @@ def initialize(opts = {}) fetch_data end - def fetch_data - catch_aws_errors do - begin - @session = @aws.macie_client.get_macie_session - @jobs = @aws.macie_client.list_classification_jobs - @jobs.present? ? @jobs_table = AwsMacieJobTable.new(@jobs.items.map(&:to_h)) : @jobs_table = [] - @buckets = @aws.macie_client.describe_buckets - @buckets.present? ? @buckets_table = AwsMacieBucketTable.new(@buckets.buckets.map(&:to_h)) : @buckets_table = [] - @findings = @aws.macie_client.list_findings.finding_ids - @findings.present? ? @findings_table = AwsMacieFindingTable.new( - @aws.macie_client.get_findings(finding_ids: @findings).findings.map(&:to_h) - ) : @findings_table = [] - rescue Aws::Errors::NoSuchEndpointError - skip_resource( - "The account contact endpoint is not available in this segment, please review this via the AWS Management Console.", - ) - end - return [] if (!@session or @session.empty?) || (!@jobs or @jobs.empty?) || (!@bucket or @bucket.empty?) - end - end - def session return [] unless @session.present? @session @@ -193,8 +172,24 @@ def to_s private - def fetch_aws_region - arn = @aws.sts_client.get_caller_identity({}).arn - arn.split(":")[1] + def fetch_data + catch_aws_errors do + begin + @session = @aws.macie_client.get_macie_session + @jobs = @aws.macie_client.list_classification_jobs + @jobs.present? ? @jobs_table = AwsMacieJobTable.new(@jobs.items.map(&:to_h)) : @jobs_table = [] + @buckets = @aws.macie_client.describe_buckets + @buckets.present? ? @buckets_table = AwsMacieBucketTable.new(@buckets.buckets.map(&:to_h)) : @buckets_table = [] + @findings = @aws.macie_client.list_findings.finding_ids + @findings.present? ? @findings_table = AwsMacieFindingTable.new( + @aws.macie_client.get_findings(finding_ids: @findings).findings.map(&:to_h) + ) : @findings_table = [] + rescue Aws::Errors::NoSuchEndpointError + skip_resource( + "The account contact endpoint is not available in this segment, please review this via the AWS Management Console.", + ) + end + return [] if (!@session or @session.empty?) || (!@jobs or @jobs.empty?) || (!@bucket or @bucket.empty?) + end end end From 4fafc0656c61f593e1dfd5314996c0752ff4ae24 Mon Sep 17 00:00:00 2001 From: wdower <57142072+wdower@users.noreply.github.com> Date: Thu, 21 Dec 2023 17:27:40 +0000 Subject: [PATCH 92/93] doc for macie Signed-off-by: wdower <57142072+wdower@users.noreply.github.com> --- .../content/inspec/resources/aws_macie.md | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 docs-chef-io/content/inspec/resources/aws_macie.md diff --git a/docs-chef-io/content/inspec/resources/aws_macie.md b/docs-chef-io/content/inspec/resources/aws_macie.md new file mode 100644 index 000000000..d3c6bfefd --- /dev/null +++ b/docs-chef-io/content/inspec/resources/aws_macie.md @@ -0,0 +1,125 @@ ++++ +title = "aws_macie Resource" +platform = "aws" +draft = false +gh_repo = "inspec-aws" + +[menu.inspec] +title = "aws_macie" +identifier = "inspec/resources/aws/aws_macie Resource" +parent = "inspec/resources/aws" ++++ + +Use the `aws_macie` InSpec audit resource to query the configuration and findings of Amazon Macie. See the [Amazon Macie API docs](https://docs.aws.amazon.com/macie/latest/APIReference/welcome.html) for details on information available from Macie. + +## Installation + +{{% inspec_aws_install %}} + +## Syntax + +An `aws_macie` resource declares the tests for the Amazon Macie instance active for the organization. + +```ruby +describe aws_macie do + it { should be_enabled } +end +``` + +The `aws_macie` resource has three properties which behave as [Filter Tables](https://github.com/inspec/inspec/blob/main/dev-docs/filtertable-usage.md): +- `jobs` +- `buckets` +- `findings` + +```ruby +describe aws_macie.jobs.where(name: "expected-job-name") do + its('job_status') { should_not cmp "CANCELLED" } +end +``` + +## Parameters + +This resource does not require any parameters. + +## Properties + +`session` +: Returns the status and configuration settings for an Amazon Macie account. + +`jobs` +: Returns a FilterTable of all jobs defined for Amazon Macie. See all columns inside the table in the [AWS docs](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/Macie2/Client.html#list_classification_jobs-instance_method) + +`buckets` +: Returns a FilterTable contianing statistical data and other information on all buckets monitored by Amazon Macie. See all columns inside the table in the [AWS docs](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/Macie2/Client.html#describe_buckets-instance_method) + +`findings` +: Returns a FilterTable of all findings discovered by Aamzon Macie. See all columns inside the table in the [AWS docs](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/Macie2/Client.html#get_findings-instance_method). + +## Examples + +**Test if Macie is enabled.** + +```ruby +describe aws_macie do + it { should be_enabled } +end +``` + +**Test that a given job is active.** + +```ruby +describe aws_macie.jobs.where(name: "expected-job-name") do + its('job_status') { should_not cmp "CANCELLED" } +end +``` + +**Test that there are no active findings.** + +```ruby +describe aws_macie.findings do + its('count') { should eq 0 } +end +``` + +**Test that a given S3 bucket is being monitored by Macie.** + +```ruby +describe aws_macie do + it { should be_monitoring("my-sample-s3-name") } +end +``` + +## Matchers + +This InSpec audit resource has the following special matchers. For a full list of available matchers, please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/). + + +### enabled? + +```ruby +describe aws_macie do + it { should be_enabled } +end +``` + +### monitored? + +Tests theat a particular S3 or list of S3 buckets is monitored by Macie's jobs. + +```ruby +describe aws_macie do + it { should be_monitoring(["my-sample-s3-name-1", "my-sample-s3-name-2"]) } +end +``` + +## AWS Permissions + +Your [Principal](https://docs.aws.amazon.com/IAM/latest/UserGuide/intro-structure.html#intro-structure-principal) will need several action permissions to use each feature of the Macie resource. Your role will need: + +- the `{{ .Get "macie2:GetFindings" }}` action +- the `{{ .Get "ListClassificationJobs" }}` action +- the `{{ .Get "macie2:DescribeBuckets" }}` action +- the `{{ .Get "macie2:ListFindings" }}` action +- the `{{ .Get "macie2:GetMacieSession" }}` action + +All with `Effect` set to `Allow`. \ No newline at end of file From 0c0280cab388c69d2e332abed8818741d471cdf2 Mon Sep 17 00:00:00 2001 From: wdower Date: Fri, 12 Jan 2024 09:09:13 -0500 Subject: [PATCH 93/93] ran linter Signed-off-by: wdower --- libraries/aws_cloudtrail_trail.rb | 4 ++-- libraries/aws_macie.rb | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/libraries/aws_cloudtrail_trail.rb b/libraries/aws_cloudtrail_trail.rb index 8acd959f1..f9e15ec52 100644 --- a/libraries/aws_cloudtrail_trail.rb +++ b/libraries/aws_cloudtrail_trail.rb @@ -121,8 +121,8 @@ def has_event_selector_mgmt_events_rw_type_all? event_selector_found = @event_selectors.advanced_event_selectors.any? { |es| # check if readOnly is unset entirely (means both read and write are logged) es.field_selectors.none? { |fs| fs.field == "readOnly" } && \ - # check if a field selector is set to track management events - es.field_selectors.any? { |fs| fs.field == "eventCategory" && fs.equals == ["Management"] } + # check if a field selector is set to track management events + es.field_selectors.any? { |fs| fs.field == "eventCategory" && fs.equals == ["Management"] } } end event_selector_found diff --git a/libraries/aws_macie.rb b/libraries/aws_macie.rb index e4236e51a..8bfbf742b 100644 --- a/libraries/aws_macie.rb +++ b/libraries/aws_macie.rb @@ -181,9 +181,13 @@ def fetch_data @buckets = @aws.macie_client.describe_buckets @buckets.present? ? @buckets_table = AwsMacieBucketTable.new(@buckets.buckets.map(&:to_h)) : @buckets_table = [] @findings = @aws.macie_client.list_findings.finding_ids - @findings.present? ? @findings_table = AwsMacieFindingTable.new( - @aws.macie_client.get_findings(finding_ids: @findings).findings.map(&:to_h) - ) : @findings_table = [] + if @findings.present? + @findings_table = AwsMacieFindingTable.new( + @aws.macie_client.get_findings(finding_ids: @findings).findings.map(&:to_h), + ) + else + @findings_table = [] + end rescue Aws::Errors::NoSuchEndpointError skip_resource( "The account contact endpoint is not available in this segment, please review this via the AWS Management Console.",