sensu-plugins/sensu-plugins-aws

View on GitHub
bin/check-s3-bucket-visibility.rb

Summary

Maintainability
B
4 hrs
Test Coverage
#! /usr/bin/env ruby
#
# check-s3-bucket-visibility
#
# DESCRIPTION:
#   This plugin checks a bucket for website configuration and bucket policy.
#   It alerts if the bucket has a website configuration, or a policy that has
#   Get or List actions.
#
# OUTPUT:
#   plain-text
#
# PLATFORMS:
#   Linux
#
# DEPENDENCIES:
#   gem: aws-sdk
#   gem: sensu-plugin
#
# USAGE:
#   ./check-s3-bucket-visibility.rb --bucket-name mybucket --aws-region eu-west-1
#
# NOTES:
#
# LICENSE:
#   Copyright (c) 2015, Olivier Bazoud and Ricky Hussmann,
#     olivier.bazoud@gmail.com, ricky.hussmann@gmail.com
#   Released under the same terms as Sensu (the MIT license); see LICENSE
#   for details.
#

require 'aws-sdk-s3'
require 'sensu-plugin/check/cli'
require 'sensu-plugins-aws'

class CheckS3Bucket < Sensu::Plugin::Check::CLI
  include Common
  option :aws_region,
         short: '-r AWS_REGION',
         long: '--aws-region REGION',
         description: 'AWS Region (defaults to us-east-1).',
         default: 'us-east-1'

  option :bucket_names,
         short: '-b BUCKET_NAMES',
         long: '--bucket-names',
         description: 'A comma seperated list of S3 buckets to check',
         proc: proc { |b| b.split(',') }

  option :all_buckets,
         short: '-a BOOL',
         long: '--all-buckets BOOL',
         description: 'If all buckets are true it will look at any buckets that we have access to in the region',
         boolean: true,
         default: false

  option :exclude_buckets,
         short: '-e EXCLUDED_BUCKETS_COMMA_SEPERATED',
         long: '--excluded-buckets EXCLUDED_BUCKETS_COMMA_SEPERATED',
         description: 'A comma seperated list of buckets to ignore that are expected to have loose permissions',
         proc: proc { |b| b.split(',') }

  option :exclude_regex_filter,
         long: '--exclude-regex-filter MY_REGEX',
         description: 'A regex to filter out bucket names'

  option :critical_on_missing,
         short: '-m ',
         long: '--critical-on-missing',
         description: 'The check will fail with CRITICAL rather than WARN when a bucket is not found',
         default: 'false'

  def true?(obj)
    !obj.nil? && obj.to_s.casecmp('true') != -1
  end

  def s3_client
    @s3_client ||= Aws::S3::Client.new
  end

  def s3_resource
    @s3_resource || Aws::S3::Resource.new
  end

  def list_buckets
    buckets = []
    s3_resource.buckets.each do |bucket|
      if s3_resource.client.get_bucket_location(bucket: bucket.name).location_constraint == config[:aws_region]
        buckets << bucket.name
      else
        p "skipping bucket: #{bucket.name} as is not in the region specified: #{config[:aws_region]}"
      end
    end
    buckets
  end

  def excluded_bucket?(bucket_name)
    return false if config[:exclude_buckets].nil?
    config[:exclude_buckets].include?(bucket_name)
  end

  def excluded_bucket_regex?(bucket_name)
    return false if config[:exclude_regex_filter].nil?
    if bucket_name.match(Regexp.new(Regexp.escape(config[:exclude_regex_filter])))
      true
    else
      false
    end
  end

  def website_configuration?(bucket_name)
    s3_client.get_bucket_website(bucket: bucket_name)
    true
  rescue Aws::S3::Errors::NoSuchWebsiteConfiguration
    false
  end

  def get_bucket_policy(bucket_name)
    JSON.parse(s3_client.get_bucket_policy(bucket: bucket_name).policy.string)
  rescue Aws::S3::Errors::NoSuchBucketPolicy
    { 'Statement' => [] }
  end

  def policy_too_permissive?(policy)
    policy['Statement'].any? { |s| statement_too_permissive? s }
  end

  def statement_too_permissive?(s)
    actions_contain_get_or_list? Array(s['Action'])
  end

  def actions_contain_get_or_list?(actions)
    actions.any? { |a| !Array(a).grep(/^s3:Get|s3:List|s3:\*/).empty? }
  end

  def run
    errors = []
    warnings = []
    buckets = if config[:all_buckets]
                list_buckets
              elsif config[:bucket_names] && !config[:bucket_names].empty?
                config[:bucket_names]
              else
                unknown 'you must specify either all buckets or provide list of buckets'
              end

    buckets.each do |bucket_name|
      if excluded_bucket?(bucket_name)
        p "bucket_name: #{bucket_name} was ignored as it matched excluded_buckets"
        next
      elsif excluded_bucket_regex?(bucket_name)
        p "bucket_name: #{bucket_name} was ignored as it matched exclude_regex_filter: #{Regexp.new(Regexp.escape(config[:exclude_regex_filter]))}"
        next
      end
      begin
        if website_configuration?(bucket_name)
          errors.push "#{bucket_name}: website configuration found"
        end
        if policy_too_permissive?(get_bucket_policy(bucket_name))
          errors.push "#{bucket_name}: bucket policy too permissive"
        end
      rescue Aws::S3::Errors::NoSuchBucket
        mesg = "Bucket #{bucket_name} not found"
        true?(config[:critical_on_missing]) ? errors.push(mesg) : warnings.push(mesg)
      end
    end

    if !errors.empty?
      critical errors.join '; '
    elsif !warnings.empty?
      warning warnings.join '; '
    else
      ok "#{buckets.join ','} not exposed via website or bucket policy"
    end
  end
end