#!/usr/bin/env ruby # Implement CIS Benchmarks for AWS Section 3.x # Details on each benchmark from https://benchmarks.cisecurity.org/downloads/show-single/?file=awsfoundations.100 # name should be in camelcase since we'll use it for filter and alarm names filters = [ { benchmark: '3.1', description: 'Ensure a log metric filter and alarm exist for unauthorized API calls', level: 1, name: 'UnauthorizedApiCalls', pattern: '{ ($.errorCode = "*UnauthorizedOperation") || ($.errorCode = "AccessDenied*") }' }, { benchmark: '3.2', description: 'Ensure a log metric filter and alarm exist for Management Console sign-in without MFA', level: 1, name: 'NoMfaConsoleLogins', pattern: '{ $.userIdentity.sessionContext.attributes.mfaAuthenticated != "true" && $.userIdentity.invokedBy = "signin.amazonaws.com" }' }, { benchmark: '3.3', description: 'Ensure a log metric filter and alarm exist for usage of "root" account', level: 1, name: 'RootAccountLogins', pattern: '{ $.userIdentity.type = "Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType != "AwsServiceEvent" }' }, { benchmark: '3.4', description: 'Ensure a log metric filter and alarm exist for IAM policy changes', level: 1, name: 'IamPolicyChanges', pattern: '{($.eventName=DeleteGroupPolicy)||($.eventName=DeleteRolePolicy)||($.eventName=DeleteUserPolicy)||($.eventName=PutGroupPolicy)||($.eventName=PutRolePolicy)||($.eventName=PutUserPolicy)||($.eventName=CreatePolicy)||($.eventName=DeletePolicy)||($.eventName=CreatePolicyVersion)||($.eventName=DeletePolicyVersion)||($.eventName=AttachRolePolicy)||($.eventName=DetachRolePolicy)||($.eventName=AttachUserPolicy)||($.eventName=DetachUserPolicy)||($.eventName=AttachGroupPolicy)||($.eventName=DetachGroupPolicy)}' }, { benchmark: '3.5', description: 'Ensure a log metric filter and alarm exist for CloudTrail configuration changes', level: 1, name: 'CloudTrailConfigurationChanges', pattern: '{ ($.eventName = CreateTrail) ||($.eventName = UpdateTrail) || ($.eventName = DeleteTrail) || ($.eventName = StartLogging) || ($.eventName = StopLogging) }' }, { benchmark: '3.6', description: 'Ensure a log metric filter and alarm exist for AWS Management Console authentication failures', level: 2, name: 'FailedConsoleLogins', pattern: '{ ($.eventName = ConsoleLogin) && ($.errorMessage = "Failed authentication") }' }, { benchmark: '3.7', description: 'Ensure a log metric filter and alarm exist for disabling or scheduled deletion of customer created CMKs', level: 2, name: 'DisabledOrDeletedCmks', pattern: '{($.eventSource = kms.amazonaws.com) && (($.eventName=DisableKey)||($.eventName=ScheduleKeyDeletion))}' }, { benchmark: '3.8', description: 'Ensure a log metric filter and alarm exist for S3 bucket policy changes', level: 1, name: 'S3BucketPolicyChanges', pattern: '{ ($.eventSource = s3.amazonaws.com) && (($.eventName = PutBucketAcl) || ($.eventName = PutBucketPolicy) || ($.eventName = PutBucketCors) || ($.eventName = PutBucketLifecycle) || ($.eventName = PutBucketReplication) || ($.eventName = DeleteBucketPolicy) || ($.eventName = DeleteBucketCors) || ($.eventName = DeleteBucketLifecycle) || ($.eventName = DeleteBucketReplication)) }' }, { benchmark: '3.9', description: 'Ensure a log metric filter and alarm exist for AWS Config configuration changes', level: 2, name: 'AwsConfigChanges', pattern: '{($.eventSource = config.amazonaws.com) && (($.eventName=StopConfigurationRecorder)||($.eventName=DeleteDeliveryChannel)||($.eventName=PutDeliveryChannel)||($.eventName=PutConfigurationRecorder))}' }, { benchmark: '3.10', description: 'Ensure a log metric filter and alarm exist for security group changes', level: 2, name: 'SecurityGroupChanges', pattern: '{ ($.eventName = AuthorizeSecurityGroupIngress) || ($.eventName = AuthorizeSecurityGroupEgress) || ($.eventName = RevokeSecurityGroupIngress) || ($.eventName = RevokeSecurityGroupEgress) || ($.eventName = CreateSecurityGroup) || ($.eventName = DeleteSecurityGroup)}' }, { benchmark: '3.11', description: 'Ensure a log metric filter and alarm exist for changes to Network Access Control Lists', level: 2, name: 'NetworkAccessControlListChanges', pattern: '{ ($.eventName = CreateNetworkAcl) || ($.eventName = CreateNetworkAclEntry) || ($.eventName = DeleteNetworkAcl) || ($.eventName = DeleteNetworkAclEntry) || ($.eventName = ReplaceNetworkAclEntry) || ($.eventName = ReplaceNetworkAclAssociation) }' }, { benchmark: '3.12', description: 'Ensure a log metric filter and alarm exist for changes to network gateways', level: 1, name: 'NetworkGatewayChanges', pattern: '{ ($.eventName = CreateCustomerGateway) || ($.eventName = DeleteCustomerGateway) || ($.eventName = AttachInternetGateway) || ($.eventName = CreateInternetGateway) || ($.eventName = DeleteInternetGateway) || ($.eventName = DetachInternetGateway) }' } ] require 'aws-sdk' # The name (not ARN) of your CloudTrail Log Group CLOUDTRAIL_LOG_GROUP = ENV['CLOUDTRAIL_LOG_GROUP'] # The ARN of an SNS topic the alarm will post to ALARM_SNS_TOPIC = ENV['ALARM_SNS_TOPIC'] # Namespace used for metrics created by metric filters METRIC_NAMESPACE = 'CisBenchmarks' def filter_name(filter) "#{filter[:name]}Filter" end def alarm_name(filter) "#{filter[:name]}Alarm" end def metric_filter_exists?(filter) cwl = Aws::CloudWatchLogs::Client.new(region: 'us-east-1') resp = cwl.describe_metric_filters({ log_group_name: CLOUDTRAIL_LOG_GROUP, filter_name_prefix: filter_name(filter) }) resp.metric_filters.each do |metric_filter| if metric_filter.filter_name == filter_name(filter) return true end end return false end # filter: a record from the hash above def create_metric_filter(filter) filter_name = filter_name(filter) cwl = Aws::CloudWatchLogs::Client.new(region: 'us-east-1') resp = cwl.put_metric_filter({ log_group_name: CLOUDTRAIL_LOG_GROUP, filter_name: filter_name, filter_pattern: filter[:pattern], metric_transformations: [ { metric_name: filter[:name], metric_namespace: METRIC_NAMESPACE, metric_value: '1' } ] }) end def metric_alarm_exists?(filter) cw = Aws::CloudWatch::Resource.new(region: 'us-east-1') alarms = cw.alarms({ alarm_names: [ alarm_name(filter) ] }) alarms.each do |alarm| if alarm.alarm_name == alarm_name(filter) return true end end return false end # filter: a record from the hash above def create_metric_alarm(filter) alarm_name = alarm_name(filter) alarm_description = "CIS Benchmark Alarm for #{filter[:benchmark]} #{filter[:name]}" cw = Aws::CloudWatch::Client.new(region: 'us-east-1') resp = cw.put_metric_alarm({ alarm_name: alarm_name, actions_enabled: true, alarm_actions: [ ALARM_SNS_TOPIC ], metric_name: filter[:name], namespace: METRIC_NAMESPACE, statistic: 'Sum', period: 300, evaluation_periods: 1, threshold: 1.0, comparison_operator: 'GreaterThanOrEqualToThreshold' }) end ## MAIN filters.each do |filter| puts "Checking #{filter[:benchmark]}: #{filter[:description]}..." if !metric_filter_exists?(filter) puts "\tCreating #{filter_name(filter)}." create_metric_filter(filter) else puts "\t#{filter_name(filter)} exists." end if !metric_alarm_exists?(filter) puts "\tCreating #{alarm_name(filter)}." create_metric_alarm(filter) else puts "\t#{alarm_name(filter)} exists." end puts "" end