diff --git a/.gitignore b/.gitignore index c4614989..2d4c9e0c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ # Swap files *.swp + +# Steampipe variable files +*.spvars +*.auto.spvars \ No newline at end of file diff --git a/README.md b/README.md index 5f103299..c60518d8 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,31 @@ This mod uses the credentials configured in the [Steampipe AWS plugin](https://h No extra configuration is required. +### Common and Tag Dimensions + +The benchmark queries use common properties (like `account_id`, `connection_name` and `region`) and tags that are defined in the form of a default list of strings in the `mod.sp` file. These properties can be overwritten in several ways: + +- Copy and rename the `steampipe.spvars.example` file to `steampipe.spvars`, and then modify the variable values inside that file +- Pass in a value on the command line: + + ```shell + steampipe check benchmark.cis_v150 --var 'common_dimensions=["account_id", "connection_name", "region"]' + ``` + + ```shell + steampipe check benchmark.cis_v150 --var 'tag_dimensions=["Environment", "Owner"]' + ``` + +- Set an environment variable: + + ```shell + SP_VAR_common_dimensions='["account_id", "connection_name", "region"]' steampipe check control.cis_v150_5_1 + ``` + + ```shell + SP_VAR_tag_dimensions='["Environment", "Owner"]' steampipe check control.cis_v150_5_1 + ``` + ## Contributing If you have an idea for additional controls or just want to help maintain and extend this mod ([or others](https://github.com/topics/steampipe-mod)) we would love you to join the community and start contributing. diff --git a/conformance_pack/account.sp b/conformance_pack/account.sp new file mode 100644 index 00000000..cf37ade3 --- /dev/null +++ b/conformance_pack/account.sp @@ -0,0 +1,32 @@ +# Non-Config rule query + +query "account_alternate_contact_security_registered" { + sql = <<-EOQ + with alternate_security_contact as ( + select + name, + account_id + from + aws_account_alternate_contact + where + contact_type = 'SECURITY' + ) + select + arn as resource, + case + when a.partition = 'aws-us-gov' then 'info' + -- Name is a required field if setting a security contact + when c.name is not null then 'ok' + else 'alarm' + end as status, + case + when a.partition = 'aws-us-gov' then a.title || ' in GovCloud, manual verification required.' + when c.name is not null then a.title || ' has security contact ' || c.name || ' registered.' + else a.title || ' security contact not registered.' + end as reason + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join alternate_security_contact as c on c.account_id = a.account_id; + EOQ +} diff --git a/conformance_pack/acm.sp b/conformance_pack/acm.sp index 3725ca4b..061a34ce 100644 --- a/conformance_pack/acm.sp +++ b/conformance_pack/acm.sp @@ -44,3 +44,64 @@ control "acm_certificate_no_wildcard_domain_name" { other_checks = "true" }) } + +query "acm_certificate_expires_30_days" { + sql = <<-EOQ + select + certificate_arn as resource, + case + when renewal_eligibility = 'INELIGIBLE' then 'skip' + when date(not_after) - date(current_date) >= 30 then 'ok' + else 'alarm' + end as status, + case + when renewal_eligibility = 'INELIGIBLE' then title || ' not eligible for renewal.' + else title || ' expires ' || to_char(not_after, 'DD-Mon-YYYY') || + ' (' || extract(day from not_after - current_date) || ' days).' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_acm_certificate; + EOQ +} + +query "acm_certificate_no_wildcard_domain_name" { + sql = <<-EOQ + select + certificate_arn as resource, + case + when domain_name like '*%' then 'alarm' + else 'ok' + end as status, + case + when domain_name like '*%' then title || ' uses wildcard domain name.' + else title || ' does not use wildcard domain name.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_acm_certificate; + EOQ +} + +query "acm_certificate_transparency_logging_enabled" { + sql = <<-EOQ + select + certificate_arn as resource, + case + when type = 'IMPORTED' then 'skip' + when certificate_transparency_logging_preference = 'ENABLED' then 'ok' + else 'alarm' + end as status, + case + when type = 'IMPORTED' then title || ' is imported.' + when certificate_transparency_logging_preference = 'ENABLED' then title || ' transparency logging enabled.' + else title || ' transparency logging disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_acm_certificate; + EOQ +} diff --git a/conformance_pack/apigateway.sp b/conformance_pack/apigateway.sp index d8454cd1..d58b5428 100644 --- a/conformance_pack/apigateway.sp +++ b/conformance_pack/apigateway.sp @@ -85,3 +85,185 @@ control "apigateway_rest_api_authorizers_configured" { other_checks = "true" }) } + +query "apigateway_stage_cache_encryption_at_rest_enabled" { + sql = <<-EOQ + select + 'arn:' || partition || ':apigateway:' || region || '::/apis/' || rest_api_id || '/stages/' || name as resource, + case + when method_settings -> '*/*' ->> 'CachingEnabled' = 'true' + and method_settings -> '*/*' ->> 'CacheDataEncrypted' = 'true' then 'ok' + else 'alarm' + end as status, + case + when method_settings -> '*/*' ->> 'CachingEnabled' = 'true' + and method_settings -> '*/*' ->> 'CacheDataEncrypted' = 'true' + then title || ' API cache and encryption enabled.' + else title || ' API cache and encryption not enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_api_gateway_stage; + EOQ +} + +query "apigateway_stage_logging_enabled" { + sql = <<-EOQ + with all_stages as ( + select + name as stage_name, + 'arn:' || partition || ':apigateway:' || region || '::/apis/' || rest_api_id || '/stages/' || name as arn, + method_settings -> '*/*' ->> 'LoggingLevel' as log_level, + title, + region, + account_id, + tags + from + aws_api_gateway_stage + union + select + stage_name, + 'arn:' || partition || ':apigateway:' || region || '::/apis/' || api_id || '/stages/' || stage_name as arn, + default_route_logging_level as log_level, + title, + region, + account_id, + tags + from + aws_api_gatewayv2_stage + ) + select + arn as resource, + case + when log_level is null or log_level = '' or log_level = 'OFF' then 'alarm' + else 'ok' + end as status, + case + when log_level is null or log_level = '' or log_level = 'OFF' then title || ' logging not enabled.' + else title || ' logging enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + all_stages; + EOQ +} + +query "apigateway_rest_api_stage_use_ssl_certificate" { + sql = <<-EOQ + select + arn as resource, + case + when client_certificate_id is null then 'alarm' + else 'ok' + end as status, + case + when client_certificate_id is null then title || ' does not use SSL certificate.' + else title || ' uses SSL certificate.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_api_gateway_stage; + EOQ +} + +query "apigateway_stage_use_waf_web_acl" { + sql = <<-EOQ + select + arn as resource, + case + when web_acl_arn is not null then 'ok' + else 'alarm' + end as status, + case + when web_acl_arn is not null then title || ' associated with WAF web ACL.' + else title || ' not associated with WAF web ACL.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_api_gateway_stage; + EOQ +} + +query "apigateway_rest_api_authorizers_configured" { + sql = <<-EOQ + select + p.name as resource, + case + when jsonb_array_length(a.provider_arns) > 0 then 'ok' + else 'alarm' + end as status, + case + when jsonb_array_length(a.provider_arns) > 0 then p.name || ' authorizers configured.' + else p.name || ' authorizers not configured.' + end as reason + + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "p.")} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "p.")} + from + aws_api_gateway_rest_api as p + left join aws_api_gateway_authorizer as a on p.api_id = a.rest_api_id; + EOQ +} + +# Non-Config rule query + +query "api_gatewayv2_route_authorization_type_configured" { + sql = <<-EOQ + select + 'arn:' || partition || ':apigateway:' || region || '::/apis/' || api_id || '/routes/' || route_id as resource, + case + when authorization_type is null then 'alarm' + else 'ok' + end as status, + case + when authorization_type is null then route_id || ' authorization type not configured.' + else route_id || ' authorization type ' || authorization_type || ' configured.' + end as reason + + ${local.common_dimensions_sql} + from + aws_api_gatewayv2_route; + EOQ +} + +query "apigateway_rest_api_stage_xray_tracing_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when tracing_enabled then 'ok' + else 'alarm' + end as status, + case + when tracing_enabled then title || ' X-Ray tracing enabled.' + else title || ' X-Ray tracing disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_api_gateway_stage; + EOQ +} + +query "gatewayv2_stage_access_logging_enabled" { + sql = <<-EOQ + select + 'arn:' || partition || ':apigateway:' || region || '::/apis/' || api_id || '/stages/' || stage_name as resource, + case + when access_log_settings is null then 'alarm' + else 'ok' + end as status, + case + when access_log_settings is null then title || ' access logging disabled.' + else title || ' access logging enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_api_gatewayv2_stage; + EOQ +} diff --git a/conformance_pack/autoscaling.sp b/conformance_pack/autoscaling.sp index c05d59d7..4f993d4b 100644 --- a/conformance_pack/autoscaling.sp +++ b/conformance_pack/autoscaling.sp @@ -41,10 +41,186 @@ control "autoscaling_launch_config_public_ip_disabled" { control "autoscaling_group_no_suspended_process" { title = "Auto Scaling groups should not have any suspended processes" description = "Ensure that there are no Auto Scaling Groups (ASGs) with suspended processes provisioned in your AWS account in order to avoid disrupting the auto scaling workflow." - query = query.autoscaling_group_no_suspended_processe + query = query.autoscaling_group_no_suspended_process tags = merge(local.conformance_pack_autoscaling_common_tags, { other_checks = "true" }) } +query "autoscaling_group_with_lb_use_health_check" { + sql = <<-EOQ + select + autoscaling_group_arn as resource, + case + when load_balancer_names is null and target_group_arns is null then 'alarm' + when health_check_type != 'ELB' then 'alarm' + else 'ok' + end as status, + case + when load_balancer_names is null and target_group_arns is null then title || ' not associated with a load balancer.' + when health_check_type != 'ELB' then title || ' does not use ELB health check.' + else title || ' uses ELB health check.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_autoscaling_group; + EOQ +} + +query "autoscaling_launch_config_public_ip_disabled" { + sql = <<-EOQ + select + launch_configuration_arn as resource, + case + when associate_public_ip_address then 'alarm' + else 'ok' + end as status, + case + when associate_public_ip_address then title || ' public IP enabled.' + else title || ' public IP disabled.' + end as reason + ${local.common_dimensions_sql} + from + aws_ec2_launch_configuration; + EOQ +} + +query "autoscaling_group_no_suspended_process" { + sql = <<-EOQ + select + autoscaling_group_arn as resource, + case + when suspended_processes is null then 'ok' + else 'alarm' + end as status, + case + when suspended_processes is null then title || ' has no suspended process.' + else title || ' has suspended process.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_autoscaling_group; + EOQ +} + +# Non-Config rule query + +query "autoscaling_group_multiple_az_configured" { + sql = <<-EOQ + select + autoscaling_group_arn as resource, + case + when jsonb_array_length(availability_zones) > 1 then 'ok' + else 'alarm' + end as status, + title || ' has ' || jsonb_array_length(availability_zones) || ' availability zone(s).' as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_autoscaling_group; + EOQ +} + +query "autoscaling_group_uses_ec2_launch_template" { + sql = <<-EOQ + select + autoscaling_group_arn as resource, + case + when launch_template_id is not null then 'ok' + else 'alarm' + end as status, + case + when launch_template_id is not null then title || ' using an EC2 launch template.' + else title || ' not using an EC2 launch template.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_autoscaling_group; + EOQ +} + +query "autoscaling_launch_config_hop_limit" { + sql = <<-EOQ + select + launch_configuration_arn as resource, + case + when metadata_options_put_response_hop_limit is null then 'ok' + when metadata_options_put_response_hop_limit > 1 then 'alarm' + else 'ok' + end as status, + case + --If you do not specify a value, the hop limit default is 1. + when metadata_options_put_response_hop_limit is null then title || ' metadata response hop limit set to default.' + else title || ' has a metadata response hop limit of ' || metadata_options_put_response_hop_limit || '.' + end as reason + ${local.common_dimensions_sql} + from + aws_ec2_launch_configuration; + EOQ +} + +query "autoscaling_launch_config_requires_imdsv2" { + sql = <<-EOQ + select + launch_configuration_arn as resource, + case + when metadata_options_http_tokens = 'required' then 'ok' + else 'alarm' + end as status, + case + when metadata_options_http_tokens = 'required' then title || ' configured to use Instance Metadata Service Version 2 (IMDSv2).' + else title || ' not configured to use Instance Metadata Service Version 2 (IMDSv2).' + end as reason + ${local.common_dimensions_sql} + from + aws_ec2_launch_configuration; + EOQ +} + +query "autoscaling_use_multiple_instance_types_in_multiple_az" { + sql = <<-EOQ + with autoscaling_groups as ( + select + autoscaling_group_arn, + title, + mixed_instances_policy_launch_template_overrides, + region, + tags, + _ctx, + account_id + from + aws_ec2_autoscaling_group + ), + distinct_instance_types_count as ( + select + autoscaling_group_arn, + count(distinct(e -> 'InstanceType')) as distinct_instance_types + from + autoscaling_groups, + jsonb_array_elements(mixed_instances_policy_launch_template_overrides) as e + group by + autoscaling_group_arn, + title, + mixed_instances_policy_launch_template_overrides + ) + select + a.autoscaling_group_arn as resource, + case + when b.distinct_instance_types > 1 then 'ok' + else 'alarm' + end as status, + case + when b.distinct_instance_types > 1 then title || ' uses ' || b.distinct_instance_types || ' instance types.' + else title || ' does not use multiple instance types.' + end as reason + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "a.")} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "a.")} + from + autoscaling_groups as a + left join distinct_instance_types_count as b on a.autoscaling_group_arn = b.autoscaling_group_arn; + EOQ +} diff --git a/conformance_pack/backup.sp b/conformance_pack/backup.sp index da3d7f6a..05ea37fa 100644 --- a/conformance_pack/backup.sp +++ b/conformance_pack/backup.sp @@ -65,3 +65,108 @@ control "backup_recovery_point_min_retention_35_days" { nist_800_171_rev_2 = "true" }) } + +query "backup_recovery_point_manual_deletion_disabled" { + sql = <<-EOQ + with recovery_point_manual_deletion_disabled as ( + select + arn + from + aws_backup_vault, + jsonb_array_elements(policy -> 'Statement') as s + where + s ->> 'Effect' = 'Deny' and + s -> 'Action' @> '["backup:DeleteRecoveryPoint","backup:UpdateRecoveryPointLifecycle","backup:PutBackupVaultAccessPolicy"]' + and s ->> 'Resource' = '*' + group by + arn + ) + select + v.arn as resource, + case + when d.arn is not null then 'ok' + else 'alarm' + end as status, + case + when d.arn is not null then v.title || ' recovery point manual deletion disabled.' + else v.title || ' recovery point manual deletion not disabled.' + end as reason + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "v.")} + from + aws_backup_vault as v + left join recovery_point_manual_deletion_disabled as d on v.arn = d.arn; + EOQ +} + +query "backup_plan_min_retention_35_days" { + sql = <<-EOQ + with all_plans as ( + select + arn, + r as Rules, + title, + region, + account_id, + _ctx + from + aws_backup_plan, + jsonb_array_elements(backup_plan -> 'Rules') as r + ) + select + -- The resource ARN can be duplicate as we are checking all the associated rules to the backup-plan + -- Backup plans are composed of one or more backup rules. + -- https://docs.aws.amazon.com/aws-backup/latest/devguide/creating-a-backup-plan.html + r.arn as resource, + case + when r.Rules is null then 'alarm' + when r.Rules ->> 'Lifecycle' is null then 'ok' + when (r.Rules -> 'Lifecycle' ->> 'DeleteAfterDays')::int >= 35 then 'ok' + else 'alarm' + end as status, + case + when r.Rules is null then r.title || ' retention period not set.' + when r.Rules ->> 'Lifecycle' is null then (r.Rules ->> 'RuleName') || ' retention period set to never expire.' + else (r.Rules ->> 'RuleName') || ' retention period set to ' || (r.Rules -> 'Lifecycle' ->> 'DeleteAfterDays') || ' days.' + end as reason + ${local.common_dimensions_sql} + from + all_plans as r; + EOQ +} + +query "backup_recovery_point_encryption_enabled" { + sql = <<-EOQ + select + recovery_point_arn as resource, + case + when is_encrypted then 'ok' + else 'alarm' + end as status, + case + when is_encrypted then recovery_point_arn || ' encryption enabled.' + else recovery_point_arn || ' encryption disabled.' + end as reason + ${local.common_dimensions_sql} + from + aws_backup_recovery_point; + EOQ +} + +query "backup_recovery_point_min_retention_35_days" { + sql = <<-EOQ + select + recovery_point_arn as resource, + case + when (lifecycle -> 'DeleteAfterDays') is null then 'ok' + when (lifecycle -> 'DeleteAfterDays')::int >= 35 then 'ok' + else 'alarm' + end as status, + case + when (lifecycle -> 'DeleteAfterDays') is null then split_part(recovery_point_arn, ':', -1) || ' retention period set to never expire.' + else split_part(recovery_point_arn, ':', -1) || ' recovery point has a retention period of ' || (lifecycle -> 'DeleteAfterDays')::int || ' days.' + end as reason + ${local.common_dimensions_sql} + from + aws_backup_recovery_point; + EOQ +} diff --git a/conformance_pack/cloudformation.sp b/conformance_pack/cloudformation.sp index d0fe2133..491d112f 100644 --- a/conformance_pack/cloudformation.sp +++ b/conformance_pack/cloudformation.sp @@ -43,3 +43,101 @@ control "cloudformation_stack_termination_protection_enabled" { other_checks = "true" }) } + +query "cloudformation_stack_output_no_secrets" { + sql = <<-EOQ + with stack_output as ( + select + id, + jsonb_array_elements(outputs) -> 'OutputKey' as k, + jsonb_array_elements(outputs) -> 'OutputValue' as v, + region, + account_id + from + aws_cloudformation_stack + ), + stack_with_secrets as ( + select + distinct id + from + stack_output + where + lower(k::text) like any (array ['%pass%', '%secret%','%token%','%key%']) + or k::text ~ '(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]' or lower(v::text) like any (array ['%pass%', '%secret%','%token%','%key%']) or v::text ~ '(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]' + ) + select + c.id as resource, + case + when c.outputs is null then 'ok' + when s.id is null then 'ok' + else 'alarm' + end as status, + case + when c.outputs is null then title || ' has no outputs.' + when s.id is null then title || ' no secrets found in outputs.' + else title || ' has secrets in outputs.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "c.")} + from + aws_cloudformation_stack as c + left join stack_with_secrets as s on c.id = s.id; + EOQ +} + +query "cloudformation_stack_notifications_enabled" { + sql = <<-EOQ + select + id as resource, + case + when jsonb_array_length(notification_arns) > 0 then 'ok' + else 'alarm' + end as status, + case + when jsonb_array_length(notification_arns) > 0 then title || ' notifications enabled.' + else title || ' notifications disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_cloudformation_stack; + EOQ +} + +query "cloudformation_stack_rollback_enabled" { + sql = <<-EOQ + select + id as resource, + case + when not disable_rollback then 'ok' + else 'alarm' + end as status, + case + when not disable_rollback then title || ' rollback enabled.' + else title || ' rollback disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_cloudformation_stack; + EOQ +} + +query "cloudformation_stack_termination_protection_enabled" { + sql = <<-EOQ + select + id as resource, + case + when enable_termination_protection then 'ok' + else 'alarm' + end as status, + case + when enable_termination_protection then title || ' termination protection enabled.' + else title || ' termination protection disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_cloudformation_stack; + EOQ +} diff --git a/conformance_pack/cloudfront.sp b/conformance_pack/cloudfront.sp index 37025729..e953adbf 100644 --- a/conformance_pack/cloudfront.sp +++ b/conformance_pack/cloudfront.sp @@ -54,3 +54,392 @@ control "cloudfront_distribution_logging_enabled" { cis_controls_v8_ig1 = "true" }) } + +query "cloudfront_distribution_encryption_in_transit_enabled" { + sql = <<-EOQ + with data as ( + select + distinct arn + from + aws_cloudfront_distribution, + jsonb_array_elements( + case jsonb_typeof(cache_behaviors -> 'Items') + when 'array' then (cache_behaviors -> 'Items') + else null end + ) as cb + where + cb ->> 'ViewerProtocolPolicy' = 'allow-all' + ) + select + b.arn as resource, + case + when d.arn is not null or (default_cache_behavior ->> 'ViewerProtocolPolicy' = 'allow-all') then 'alarm' + else 'ok' + end as status, + case + when d.arn is not null or (default_cache_behavior ->> 'ViewerProtocolPolicy' = 'allow-all') then title || ' data not encrypted in transit.' + else title || ' data encrypted in transit.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "b.")} + from + aws_cloudfront_distribution as b + left join data as d on b.arn = d.arn; + EOQ +} + +query "cloudfront_distribution_geo_restrictions_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when restrictions -> 'GeoRestriction' ->> 'RestrictionType' = 'none' then 'alarm' + else 'ok' + end as status, + case + when restrictions -> 'GeoRestriction' ->> 'RestrictionType' = 'none' then title || ' Geo Restriction disabled.' + else title || ' Geo Restriction enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_cloudfront_distribution; + EOQ +} + +query "cloudfront_distribution_use_secure_cipher" { + sql = <<-EOQ + with origin_protocols as ( + select + distinct arn, + o -> 'CustomOriginConfig' ->> 'OriginSslProtocols' as origin_ssl_policy + from + aws_cloudfront_distribution, + jsonb_array_elements(origins) as o + where + o -> 'CustomOriginConfig' -> 'OriginSslProtocols' -> 'Items' @> '["TLSv1.2%", "TLSv1.1%"]' + ) + select + b.arn as resource, + case + when o.arn is not null then 'ok' + else 'alarm' + end as status, + case + when o.arn is not null then title || ' use secure cipher.' + else title || ' does not use secure cipher.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "b.")} + from + aws_cloudfront_distribution as b + left join origin_protocols as o on b.arn = o.arn; + EOQ +} + +query "cloudfront_distribution_non_s3_origins_encryption_in_transit_enabled" { + sql = <<-EOQ + with viewer_protocol_policy_value as ( + select + distinct arn + from + aws_cloudfront_distribution, + jsonb_array_elements( + case jsonb_typeof(cache_behaviors -> 'Items') + when 'array' then (cache_behaviors -> 'Items') + else null end + ) as cb + where + cb ->> 'ViewerProtocolPolicy' = 'allow-all' + ), + origin_protocol_policy_value as ( + select + distinct arn, + o -> 'CustomOriginConfig' ->> 'OriginProtocolPolicy' as origin_protocol_policy + from + aws_cloudfront_distribution, + jsonb_array_elements(origins) as o + where + o -> 'CustomOriginConfig' ->> 'OriginProtocolPolicy' = 'http-only' + or o -> 'CustomOriginConfig' ->> 'OriginProtocolPolicy' = 'match-viewer' + and o -> 'S3OriginConfig' is null + ) + select + b.arn as resource, + case + when o.arn is not null and o.origin_protocol_policy = 'http-only' then 'alarm' + when o.arn is not null and o.origin_protocol_policy = 'match-viewer' and ( v.arn is not null or (default_cache_behavior ->> 'ViewerProtocolPolicy' = 'allow-all') ) then 'alarm' + else 'ok' + end as status, + case + when o.arn is not null and o.origin_protocol_policy = 'http-only' then title || ' origins traffic not encrypted in transit.' + when o.arn is not null and o.origin_protocol_policy = 'match-viewer' and ( v.arn is not null or (default_cache_behavior ->> 'ViewerProtocolPolicy' = 'allow-all') ) then title || ' origins traffic not encrypted in transit.' + else title || ' origins traffic encrypted in transit.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "b.")} + from + aws_cloudfront_distribution as b + left join origin_protocol_policy_value as o on b.arn = o.arn + left join viewer_protocol_policy_value as v on b.arn = v.arn; + EOQ +} + +query "cloudfront_distribution_logging_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when logging ->> 'Enabled' = 'true' then 'ok' + else 'alarm' + end as status, + case + when logging ->> 'Enabled' = 'true' then title || ' logging enabled.' + else title || ' logging disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_cloudfront_distribution; + EOQ +} + +# Non-Config rule query + +query "cloudfront_distribution_configured_with_origin_failover" { + sql = <<-EOQ + select + arn as resource, + case + when origin_groups ->> 'Items' is not null then 'ok' + else 'alarm' + end as status, + case + when origin_groups ->> 'Items' is not null then title || ' origin group is configured.' + else title || ' origin group not configured.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_cloudfront_distribution; + EOQ +} + +query "cloudfront_distribution_custom_origins_encryption_in_transit_enabled" { + sql = <<-EOQ + with viewer_protocol_policy_value as ( + select + distinct arn + from + aws_cloudfront_distribution, + jsonb_array_elements( + case jsonb_typeof(cache_behaviors -> 'Items') + when 'array' then (cache_behaviors -> 'Items') + else null end + ) as cb + where + cb ->> 'ViewerProtocolPolicy' = 'allow-all' + ), + origin_protocol_policy_value as ( + select + distinct arn, + o -> 'CustomOriginConfig' ->> 'OriginProtocolPolicy' as origin_protocol_policy + from + aws_cloudfront_distribution, + jsonb_array_elements(origins) as o + where + o -> 'CustomOriginConfig' ->> 'OriginProtocolPolicy' = 'http-only' + or o -> 'CustomOriginConfig' ->> 'OriginProtocolPolicy' = 'match-viewer' + ) + select + b.arn as resource, + case + when o.arn is not null and o.origin_protocol_policy = 'http-only' then 'alarm' + when o.arn is not null and o.origin_protocol_policy = 'match-viewer' and ( v.arn is not null or (default_cache_behavior ->> 'ViewerProtocolPolicy' = 'allow-all') ) then 'alarm' + else 'ok' + end as status, + case + when o.arn is not null and o.origin_protocol_policy = 'http-only' then title || ' custom origins traffic not encrypted in transit.' + when o.arn is not null and o.origin_protocol_policy = 'match-viewer' and ( v.arn is not null or (default_cache_behavior ->> 'ViewerProtocolPolicy' = 'allow-all') ) then title || ' custom origins traffic not encrypted in transit.' + else title || ' custom origins traffic encrypted in transit.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_cloudfront_distribution as b + left join origin_protocol_policy_value as o on b.arn = o.arn + left join viewer_protocol_policy_value as v on b.arn = v.arn; + EOQ +} + +query "cloudfront_distribution_default_root_object_configured" { + sql = <<-EOQ + select + arn as resource, + case + when default_root_object = '' then 'alarm' + else 'ok' + end as status, + case + when default_root_object = '' then title || ' default root object not configured.' + else title || ' default root object configured.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_cloudfront_distribution; + EOQ +} + +query "cloudfront_distribution_no_deprecated_ssl_protocol" { + sql = <<-EOQ + with origin_ssl_protocols as ( + select + distinct arn, + o -> 'CustomOriginConfig' ->> 'OriginProtocolPolicy' as origin_protocol_policy + from + aws_cloudfront_distribution, + jsonb_array_elements(origins) as o + where + o -> 'CustomOriginConfig' -> 'OriginSslProtocols' -> 'Items' @> '["SSLv3"]' + ) + select + b.arn as resource, + case + when o.arn is null then 'ok' + else 'alarm' + end as status, + case + when o.arn is null then title || ' does not have deprecated SSL protocols.' + else title || ' has deprecated SSL protocols.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_cloudfront_distribution as b + left join origin_ssl_protocols as o on b.arn = o.arn; + EOQ +} + +query "cloudfront_distribution_no_non_existent_s3_origin" { + sql = <<-EOQ + with distribution_with_non_existent_bucket as ( + select + distinct d.arn as arn, + to_jsonb(string_to_array((string_agg(split_part(o ->> 'Id', '.s3', 1), ',')),',')) as bucket_name_list + from + aws_cloudfront_distribution as d, + jsonb_array_elements(d.origins) as o + left join aws_s3_bucket as b on b.name = split_part(o ->> 'Id', '.s3', 1) + where + b.name is null + and o ->> 'DomainName' like '%.s3.%' + group by + d.arn + ) + select + distinct b.arn as resource, + case + when b.arn is null then 'ok' + else 'alarm' + end as status, + case + when b.arn is null then title || ' does not point to any non-existent S3 origins.' + when jsonb_array_length(b.bucket_name_list) > 0 + then title || + case + when jsonb_array_length(b.bucket_name_list) > 2 + then concat(' point to non-existent S3 origins ', b.bucket_name_list #>> '{0}', ', ', b.bucket_name_list #>> '{1}', ' and ' || (jsonb_array_length(b.bucket_name_list) - 2)::text || ' more.' ) + when jsonb_array_length(b.bucket_name_list) = 2 + then concat(' point to non-existent S3 origins ', b.bucket_name_list #>> '{0}', ' and ', b.bucket_name_list #>> '{1}', '.') + else concat(' point to non-existent S3 origin ', b.bucket_name_list #>> '{0}', '.') + end + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_cloudfront_distribution as d + left join distribution_with_non_existent_bucket as b on b.arn = d.arn; + EOQ +} + +query "cloudfront_distribution_origin_access_identity_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when o ->> 'DomainName' not like '%s3.amazonaws.com' then 'skip' + when o ->> 'DomainName' like '%s3.amazonaws.com' + and o -> 'S3OriginConfig' ->> 'OriginAccessIdentity' = '' then 'alarm' + else 'ok' + end as status, + case + when o ->> 'DomainName' not like '%s3.amazonaws.com' then title || ' origin type is not s3.' + when o ->> 'DomainName' like '%s3.amazonaws.com' + and o -> 'S3OriginConfig' ->> 'OriginAccessIdentity' = '' then title || ' origin access identity not configured.' + else title || ' origin access identity configured.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_cloudfront_distribution, + jsonb_array_elements(origins) as o; + EOQ +} + +query "cloudfront_distribution_sni_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when viewer_certificate ->> 'SSLSupportMethod' = 'sni-only' then 'ok' + else 'alarm' + end as status, + case + when viewer_certificate ->> 'SSLSupportMethod' = 'sni-only' then title || ' SNI enabled.' + else title || ' SNI disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_cloudfront_distribution; + EOQ +} + +query "cloudfront_distribution_use_custom_ssl_certificate" { + sql = <<-EOQ + select + arn as resource, + case + when viewer_certificate ->> 'ACMCertificateArn' is not null and viewer_certificate ->> 'Certificate' is not null then 'ok' + else 'alarm' + end as status, + case + when viewer_certificate ->> 'ACMCertificateArn' is not null and viewer_certificate ->> 'Certificate' is not null then title || ' uses custom SSL certificate.' + else title || ' does not use custom SSL certificate.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_cloudfront_distribution; + EOQ +} + +query "cloudfront_distribution_waf_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when web_acl_id <> '' then 'ok' + else 'alarm' + end as status, + case + when web_acl_id <> '' then title || ' associated with WAF.' + else title || ' not associated with WAF.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_cloudfront_distribution; + EOQ +} diff --git a/conformance_pack/cloudtrail.sp b/conformance_pack/cloudtrail.sp index 6611a0b5..081dd6d5 100644 --- a/conformance_pack/cloudtrail.sp +++ b/conformance_pack/cloudtrail.sp @@ -172,3 +172,467 @@ control "cloudtrail_bucket_not_public" { gdpr = "true" }) } + +query "cloudtrail_trail_integrated_with_logs" { + sql = <<-EOQ + select + arn as resource, + case + when log_group_arn != 'null' and ((latest_delivery_time) > current_date - 1) then 'ok' + else 'alarm' + end as status, + case + when log_group_arn != 'null' and ((latest_delivery_time) > current_date - 1) then title || ' integrated with CloudWatch logs.' + else title || ' not integrated with CloudWatch logs.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_cloudtrail_trail + where + region = home_region; + EOQ +} + +query "cloudtrail_s3_data_events_enabled" { + sql = <<-EOQ + with s3_selectors as ( + select + name as trail_name, + is_multi_region_trail, + bucket_selector + from + aws_cloudtrail_trail, + jsonb_array_elements(event_selectors) as event_selector, + jsonb_array_elements(event_selector -> 'DataResources') as data_resource, + jsonb_array_elements_text(data_resource -> 'Values') as bucket_selector + where + is_multi_region_trail + and data_resource ->> 'Type' = 'AWS::S3::Object' + and event_selector ->> 'ReadWriteType' = 'All' + ) + select + b.arn as resource, + case + when count(bucket_selector) > 0 then 'ok' + else 'alarm' + end as status, + case + when count(bucket_selector) > 0 then b.name || ' object-level data events logging enabled.' + else b.name || ' object-level data events logging disabled.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "b.")} + from + aws_s3_bucket as b + left join s3_selectors on bucket_selector like (b.arn || '%') or bucket_selector = 'arn:aws:s3' + group by + b.account_id, b.region, b.arn, b.name, b.tags; + EOQ +} + +query "cloudtrail_trail_logs_encrypted_with_kms_cmk" { + sql = <<-EOQ + select + arn as resource, + case + when kms_key_id is null then 'alarm' + else 'ok' + end as status, + case + when kms_key_id is null then title || ' logs are not encrypted at rest.' + else title || ' logs are encrypted at rest.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_cloudtrail_trail + where + region = home_region; + EOQ +} + +query "cloudtrail_multi_region_trail_enabled" { + sql = <<-EOQ + with multi_region_trails as ( + select + account_id, + count(account_id) as num_multregion_trails + from + aws_cloudtrail_trail + where + is_multi_region_trail and region = home_region + and is_logging + group by + account_id, + is_multi_region_trail + ), organization_trails as ( + select + is_organization_trail, + is_logging, + is_multi_region_trail, + account_id + from + aws_cloudtrail_trail + where + is_organization_trail + ) + select + distinct a.arn as resource, + case + when coalesce(num_multregion_trails, 0) >= 1 then 'ok' + when o.is_organization_trail and o.is_logging and o.is_multi_region_trail then 'ok' + when o.is_organization_trail and o.is_multi_region_trail and o.is_logging is null then 'info' + else 'alarm' + end as status, + case + when coalesce(num_multregion_trails, 0) >= 1 then a.title || ' has ' || coalesce(num_multregion_trails, 0) || ' multi-region trail(s).' + when o.is_organization_trail and o.is_logging and o.is_multi_region_trail then a.title || ' has multi-region trail(s).' + when o.is_organization_trail and o.is_multi_region_trail and o.is_logging is null then a.title || ' has organization trail, check organization account for cloudtrail logging status.' + else a.title || ' does not have multi-region trail(s).' + end as reason + + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join multi_region_trails as b on a.account_id = b.account_id + left join organization_trails as o on a.account_id = o.account_id; + EOQ +} + +query "cloudtrail_trail_validation_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when log_file_validation_enabled then 'ok' + else 'alarm' + end as status, + case + when log_file_validation_enabled then title || ' log file validation enabled.' + else title || ' log file validation disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_cloudtrail_trail + where + region = home_region; + EOQ +} + +query "cloudtrail_trail_enabled" { + sql = <<-EOQ + with trails_enabled as ( + select + arn, + is_logging + from + aws_cloudtrail_trail + where + home_region = region + ) + select + a.arn as resource, + case + when b.is_logging is null and a.is_logging then 'ok' + when b.is_logging then 'ok' + else 'alarm' + end as status, + case + when b.is_logging is null and a.is_logging then a.title || ' enabled.' + when b.is_logging then a.title || ' enabled.' + else a.title || ' disabled.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "a.")} + from + aws_cloudtrail_trail as a + left join trails_enabled b on a.arn = b.arn; + EOQ +} + +query "cloudtrail_security_trail_enabled" { + sql = <<-EOQ + with trails_enabled as ( + select + distinct arn, + is_logging, + event_selectors, + coalesce( + jsonb_agg(g) filter ( where not (g = 'null') ), + $$[]$$::jsonb + ) as excludeManagementEventSources + from + aws_cloudtrail_trail + left join jsonb_array_elements(event_selectors) as e on true + left join jsonb_array_elements_text(e -> 'ExcludeManagementEventSources') as g on true + where + home_region = region + group by arn, is_logging, event_selectors + ), + all_trails as ( + select + a.arn as arn, + tags, + _ctx, + case + when a.is_logging is null then b.is_logging + else a.is_logging + end as is_logging, + case + when a.event_selectors is null then b.event_selectors + else a.event_selectors + end as event_selectors, + b.excludeManagementEventSources, + a.include_global_service_events, + a.is_multi_region_trail, + a.log_file_validation_enabled, + a.kms_key_id, + a.region, + a.account_id, + a.title + from + aws_cloudtrail_trail as a + left join trails_enabled as b on a.arn = b.arn + ) + select + arn as resource, + case + when not is_logging then 'alarm' + when not include_global_service_events then 'alarm' + when not is_multi_region_trail then 'alarm' + when not log_file_validation_enabled then 'alarm' + when kms_key_id is null then 'alarm' + when not (jsonb_array_length(event_selectors) = 1 and event_selectors @> '[{"ReadWriteType":"All"}]') then 'alarm' + when not (event_selectors @> '[{"IncludeManagementEvents":true}]') then 'alarm' + when jsonb_array_length(excludeManagementEventSources) > 0 then 'alarm' + else 'ok' + end as status, + case + when not is_logging then title || ' disabled.' + when not include_global_service_events then title || ' not recording global service events.' + when not is_multi_region_trail then title || ' not a muti-region trail.' + when not log_file_validation_enabled then title || ' log file validation disabled.' + when kms_key_id is null then title || ' not encrypted with a KMS key.' + when not (jsonb_array_length(event_selectors) = 1 and event_selectors @> '[{"ReadWriteType":"All"}]') then title || ' not recording events for both reads and writes.' + when not (event_selectors @> '[{"IncludeManagementEvents":true}]') then title || ' not recording management events.' + when jsonb_array_length(excludeManagementEventSources) > 0 then title || ' excludes management events for ' || trim(excludeManagementEventSources::text, '[]') || '.' + else title || ' meets all security best practices.' + end as reason + + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + all_trails; + EOQ +} + +query "cloudtrail_s3_logging_enabled" { + sql = <<-EOQ + select + t.arn as resource, + case + when b.logging is not null then 'ok' + else 'alarm' + end as status, + case + when b.logging is not null then t.title || '''s logging bucket ' || t.s3_bucket_name || ' has access logging enabled.' + else t.title || '''s logging bucket ' || t.s3_bucket_name || ' has access logging disabled.' + end as reason + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "t.")} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "t.")} + from + aws_cloudtrail_trail t + inner join aws_s3_bucket b on t.s3_bucket_name = b.name + where + t.region = t.home_region; + EOQ +} + +query "cloudtrail_bucket_not_public" { + sql = <<-EOQ + with public_bucket_data as ( + -- note the counts are not exactly CORRECT because of the jsonb_array_elements joins, + -- but will be non-zero if any matches are found + select + t.s3_bucket_name as name, + b.arn, + t.region, + t.account_id, + t.tags, + t._ctx, + count(acl_grant) filter (where acl_grant -> 'Grantee' ->> 'URI' like '%acs.amazonaws.com/groups/global/AllUsers') as all_user_grants, + count(acl_grant) filter (where acl_grant -> 'Grantee' ->> 'URI' like '%acs.amazonaws.com/groups/global/AuthenticatedUsers') as auth_user_grants, + count(s) filter (where s ->> 'Effect' = 'Allow' and p = '*' ) as anon_statements + from + aws_cloudtrail_trail as t + left join aws_s3_bucket as b on t.s3_bucket_name = b.name + left join jsonb_array_elements(acl -> 'Grants') as acl_grant on true + left join jsonb_array_elements(policy_std -> 'Statement') as s on true + left join jsonb_array_elements_text(s -> 'Principal' -> 'AWS') as p on true + group by + t.s3_bucket_name, + b.arn, + t.region, + t.account_id, + t.tags, + t._ctx + ) + select + case + when arn is null then 'arn:aws:s3::' || name + else arn + end as resource, + case + when arn is null then 'skip' + when all_user_grants > 0 then 'alarm' + when auth_user_grants > 0 then 'alarm' + when anon_statements > 0 then 'alarm' + else 'ok' + end as status, + case + when arn is null then name || ' not found in account ' || account_id || '.' + when all_user_grants > 0 then name || ' grants access to AllUsers in ACL.' + when auth_user_grants > 0 then name || ' grants access to AuthenticatedUsers in ACL.' + when anon_statements > 0 then name || ' grants access to AWS:*" in bucket policy.' + else name || ' does not grant anonymous access in ACL or bucket policy.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + public_bucket_data; + EOQ +} + +# Non-Config rule query + +query "cloudtrail_multi_region_read_write_enabled" { + sql = <<-EOQ + with event_selectors_trail_details as ( + select + distinct account_id + from + aws_cloudtrail_trail, + jsonb_array_elements(event_selectors) as e + where + (is_logging and is_multi_region_trail and e ->> 'ReadWriteType' = 'All') + ), + advanced_event_selectors_trail_details as ( + select + distinct account_id + from + aws_cloudtrail_trail, + jsonb_array_elements_text(advanced_event_selectors) as a + where + -- when readOnly = true, then it is readOnly, when readOnly = false then it is writeOnly, if advanced_event_selectors is not null then it is both ReadWriteType + (is_logging and is_multi_region_trail and advanced_event_selectors is not null and (not a like '%readOnly%')) + ) + select + a.title as resource, + case + when d.account_id is null and ad.account_id is null then 'alarm' + else 'ok' + end as status, + case + when d.account_id is null and ad.account_id is null then 'cloudtrail disabled.' + else 'cloudtrail enabled.' + end as reason + + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join event_selectors_trail_details as d on d.account_id = a.account_id + left join advanced_event_selectors_trail_details as ad on ad.account_id = a.account_id; + EOQ +} + +query "cloudtrail_s3_object_read_events_audit_enabled" { + sql = <<-EOQ + with s3_selectors as + ( + select + name as trail_name, + is_multi_region_trail, + bucket_selector + from + aws_cloudtrail_trail, + jsonb_array_elements(event_selectors) as event_selector, + jsonb_array_elements(event_selector -> 'DataResources') as data_resource, + jsonb_array_elements_text(data_resource -> 'Values') as bucket_selector + where + is_multi_region_trail + and data_resource ->> 'Type' = 'AWS::S3::Object' + and event_selector ->> 'ReadWriteType' in + ( + 'ReadOnly', + 'All' + ) + ) + select + b.arn as resource, + case + when count(bucket_selector) > 0 then 'ok' + else 'alarm' + end as status, + case + when count(bucket_selector) > 0 then b.name || ' object-level read events logging enabled.' + else b.name || ' object-level read events logging disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_s3_bucket as b + left join + s3_selectors + on bucket_selector like (b.arn || '%') + or bucket_selector = 'arn:aws:s3' + group by + b.account_id, b.region, b.arn, b.name, b.tags, b._ctx; + EOQ +} + +query "cloudtrail_s3_object_write_events_audit_enabled" { + sql = <<-EOQ + with s3_selectors as + ( + select + name as trail_name, + is_multi_region_trail, + bucket_selector + from + aws_cloudtrail_trail, + jsonb_array_elements(event_selectors) as event_selector, + jsonb_array_elements(event_selector -> 'DataResources') as data_resource, + jsonb_array_elements_text(data_resource -> 'Values') as bucket_selector + where + is_multi_region_trail + and data_resource ->> 'Type' = 'AWS::S3::Object' + and event_selector ->> 'ReadWriteType' in + ( + 'WriteOnly', + 'All' + ) + ) + select + b.arn as resource, + case + when count(bucket_selector) > 0 then 'ok' + else 'alarm' + end as status, + case + when count(bucket_selector) > 0 then b.name || ' object-level write events logging enabled.' + else b.name || ' object-level write events logging disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_s3_bucket as b + left join + s3_selectors + on bucket_selector like (b.arn || '%') + or bucket_selector = 'arn:aws:s3' + group by + b.account_id, b.region, b.arn, b.name, b.tags, b._ctx; + EOQ +} diff --git a/conformance_pack/cloudwatch.sp b/conformance_pack/cloudwatch.sp index f7e62b87..8af55c82 100644 --- a/conformance_pack/cloudwatch.sp +++ b/conformance_pack/cloudwatch.sp @@ -231,4 +231,823 @@ control "log_metric_filter_cloudtrail_configuration" { }) } +query "cloudwatch_alarm_action_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when jsonb_array_length(alarm_actions) = 0 + and jsonb_array_length(insufficient_data_actions) = 0 + and jsonb_array_length(ok_actions) = 0 then 'alarm' + else 'ok' + end as status, + case + when jsonb_array_length(alarm_actions) = 0 + and jsonb_array_length(insufficient_data_actions) = 0 + and jsonb_array_length(ok_actions) = 0 then title || ' no action enabled.' + when jsonb_array_length(alarm_actions) != 0 then title || ' alarm action enabled.' + when jsonb_array_length(insufficient_data_actions) != 0 then title || ' insufficient data action enabled.' + when jsonb_array_length(ok_actions) != 0 then title || ' ok action enabled.' + else 'ok' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_cloudwatch_alarm; + EOQ +} + +query "log_group_encryption_at_rest_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when kms_key_id is null then 'alarm' + else 'ok' + end as status, + case + when kms_key_id is null then title || ' not encrypted at rest.' + else title || ' encrypted at rest.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_cloudwatch_log_group; + EOQ +} + +query "cloudwatch_log_group_retention_period_365" { + sql = <<-EOQ + select + arn as resource, + case + when retention_in_days is null or retention_in_days < 365 then 'alarm' + else 'ok' + end as status, + case + when retention_in_days is null then title || ' retention period not set.' + when retention_in_days < 365 then title || ' retention period less than 365 days.' + else title || ' retention period 365 days or above.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_cloudwatch_log_group; + EOQ +} + +query "log_metric_filter_unauthorized_api" { + sql = <<-EOQ + with filter_data as ( + select + trail.account_id, + trail.name as trail_name, + trail.is_logging, + split_part(trail.log_group_arn, ':', 7) as log_group_name, + filter.name as filter_name, + action_arn as topic_arn, + alarm.metric_name, + subscription.subscription_arn, + filter.filter_pattern + from + aws_cloudtrail_trail as trail, + jsonb_array_elements(trail.event_selectors) as se, + aws_cloudwatch_log_metric_filter as filter, + aws_cloudwatch_alarm as alarm, + jsonb_array_elements_text(alarm.alarm_actions) as action_arn, + aws_sns_topic_subscription as subscription + where + trail.is_multi_region_trail is true + and trail.is_logging + and se ->> 'ReadWriteType' = 'All' + and trail.log_group_arn is not null + and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) + -- As per cis recommended exact pattern order + -- {($.errorCode = "*UnauthorizedOperation") || ($.errorCode = "AccessDenied*") || ($.sourceIPAddress!="delivery.logs.amazonaws.com") || ($.eventName!="HeadBucket") } + and filter.filter_pattern ~ '\$\.errorCode\s*=\s*"\*UnauthorizedOperation".+\$\.errorCode\s*=\s*"AccessDenied\*".+\$\.sourceIPAddress\s*!=\s*"delivery.logs.amazonaws.com".+\$\.eventName\s*!=\s*"HeadBucket"' + and alarm.metric_name = filter.metric_transformation_name + and subscription.topic_arn = action_arn + ) + select + distinct 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when f.trail_name is null then 'alarm' + else 'ok' + end as status, + case + when f.trail_name is null then 'No log metric filter and alarm exist for unauthorized API calls.' + else filter_name || ' forwards events for unauthorized API calls.' + end as reason + + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join filter_data as f on a.account_id = f.account_id; + EOQ +} + +query "log_metric_filter_console_login_mfa" { + sql = <<-EOQ + with filter_data as ( + select + trail.account_id, + trail.name as trail_name, + trail.is_logging, + split_part(trail.log_group_arn, ':', 7) as log_group_name, + filter.name as filter_name, + action_arn as topic_arn, + alarm.metric_name, + subscription.subscription_arn, + filter.filter_pattern + from + aws_cloudtrail_trail as trail, + jsonb_array_elements(trail.event_selectors) as se, + aws_cloudwatch_log_metric_filter as filter, + aws_cloudwatch_alarm as alarm, + jsonb_array_elements_text(alarm.alarm_actions) as action_arn, + aws_sns_topic_subscription as subscription + where + trail.is_multi_region_trail is true + and trail.is_logging + and se ->> 'ReadWriteType' = 'All' + and trail.log_group_arn is not null + and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) + and filter.filter_pattern ~ '\(\s*\$\.eventName\s*=\s*"ConsoleLogin"\)\s+&&\s+\(\s*\$.additionalEventData\.MFAUsed\s*!=\s*"Yes"' + and alarm.metric_name = filter.metric_transformation_name + and subscription.topic_arn = action_arn + ) + select + distinct 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when f.trail_name is null then 'alarm' + else 'ok' + end as status, + case + when f.trail_name is null then 'No log metric filter and alarm exist for console sign-in without MFA.' + else filter_name || ' forwards events for console sign-in without MFA.' + end as reason + + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join filter_data as f on a.account_id = f.account_id; + EOQ +} + +query "log_metric_filter_root_login" { + sql = <<-EOQ + with filter_data as ( + select + trail.account_id, + trail.name as trail_name, + trail.is_logging, + split_part(trail.log_group_arn, ':', 7) as log_group_name, + filter.name as filter_name, + action_arn as topic_arn, + alarm.metric_name, + subscription.subscription_arn, + filter.filter_pattern + from + aws_cloudtrail_trail as trail, + jsonb_array_elements(trail.event_selectors) as se, + aws_cloudwatch_log_metric_filter as filter, + aws_cloudwatch_alarm as alarm, + jsonb_array_elements_text(alarm.alarm_actions) as action_arn, + aws_sns_topic_subscription as subscription + where + trail.is_multi_region_trail is true + and trail.is_logging + and se ->> 'ReadWriteType' = 'All' + and trail.log_group_arn is not null + and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) + and filter.filter_pattern ~ '\s*\$\.userIdentity\.type\s*=\s*"Root".+\$\.userIdentity\.invokedBy NOT EXISTS.+\$\.eventType\s*!=\s*"AwsServiceEvent"' + and alarm.metric_name = filter.metric_transformation_name + and subscription.topic_arn = action_arn + ) + select + distinct 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when f.trail_name is null then 'alarm' + else 'ok' + end as status, + case + when f.trail_name is null then 'No log metric filter and alarm exist for usage of "root" account.' + else filter_name || ' forwards events for usage of "root" account.' + end as reason + + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join filter_data as f on a.account_id = f.account_id; + EOQ +} + +query "log_metric_filter_iam_policy" { + sql = <<-EOQ + with filter_data as ( + select + trail.account_id, + trail.name as trail_name, + trail.is_logging as is_logging, + split_part(trail.log_group_arn, ':', 7) as log_group_name, + filter.name as filter_name, + action_arn as topic_arn, + alarm.metric_name, + subscription.subscription_arn, + filter.filter_pattern, + filter.metric_transformation_name + from + aws_cloudtrail_trail as trail, + jsonb_array_elements(trail.event_selectors) as se, + aws_cloudwatch_log_metric_filter as filter, + aws_cloudwatch_alarm as alarm, + jsonb_array_elements_text(alarm.alarm_actions) as action_arn, + aws_sns_topic_subscription as subscription + where + trail.is_multi_region_trail is true + and trail.is_logging + and se ->> 'ReadWriteType' = 'All' + and trail.log_group_arn is not null + and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) + and filter.filter_pattern ~ '\s*\$\.eventName\s*=\s*DeleteGroupPolicy.+\$\.eventName\s*=\s*DeleteRolePolicy.+\$\.eventName\s*=\s*DeleteUserPolicy.+\$\.eventName\s*=\s*PutGroupPolicy.+\$\.eventName\s*=\s*PutRolePolicy.+\$\.eventName\s*=\s*PutUserPolicy.+\$\.eventName\s*=\s*CreatePolicy.+\$\.eventName\s*=\s*DeletePolicy.+\$\.eventName\s*=\s*CreatePolicyVersion.+\$\.eventName\s*=\s*DeletePolicyVersion.+\$\.eventName\s*=\s*AttachRolePolicy.+\$\.eventName\s*=\s*DetachRolePolicy.+\$\.eventName\s*=\s*AttachUserPolicy.+\$\.eventName\s*=\s*DetachUserPolicy.+\$\.eventName\s*=\s*AttachGroupPolicy.+\$\.eventName\s*=\s*DetachGroupPolicy' + and alarm.metric_name = filter.metric_transformation_name + and subscription.topic_arn = action_arn + ) + select + distinct 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when f.trail_name is null then 'alarm' + else 'ok' + end as status, + case + when f.trail_name is null then 'No log metric filter and alarm exist for IAM policy changes.' + else filter_name || ' forwards events for IAM policy changes.' + end as reason + + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join filter_data as f on a.account_id = f.account_id; + EOQ +} + +query "log_metric_filter_vpc" { + sql = <<-EOQ + with filter_data as ( + select + trail.account_id, + trail.name as trail_name, + trail.is_logging, + split_part(trail.log_group_arn, ':', 7) as log_group_name, + filter.name as filter_name, + action_arn as topic_arn, + alarm.metric_name, + alarm.name as alarm_name, + subscription.subscription_arn, + filter.filter_pattern + from + aws_cloudtrail_trail as trail, + jsonb_array_elements(trail.event_selectors) as se, + aws_cloudwatch_log_metric_filter as filter, + aws_cloudwatch_alarm as alarm, + jsonb_array_elements_text(alarm.alarm_actions) as action_arn, + aws_sns_topic_subscription as subscription + where + trail.is_multi_region_trail is true + and trail.is_logging + and se ->> 'ReadWriteType' = 'All' + and trail.log_group_arn is not null + and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) + and filter.filter_pattern ~ '\s*\$\.eventName\s*=\s*CreateVpc.+\$\.eventName\s*=\s*DeleteVpc.+\$\.eventName\s*=\s*ModifyVpcAttribute.+\$\.eventName\s*=\s*AcceptVpcPeeringConnection.+\$\.eventName\s*=\s*CreateVpcPeeringConnection.+\$\.eventName\s*=\s*DeleteVpcPeeringConnection.+\$\.eventName\s*=\s*RejectVpcPeeringConnection.+\$\.eventName\s*=\s*AttachClassicLinkVpc.+\$\.eventName\s*=\s*DetachClassicLinkVpc.+\$\.eventName\s*=\s*DisableVpcClassicLink.+\$\.eventName\s*=\s*EnableVpcClassicLink' + and alarm.metric_name = filter.metric_transformation_name + and subscription.topic_arn = action_arn + ) + select + distinct 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when f.trail_name is null then 'alarm' + else 'ok' + end as status, + case + when f.trail_name is null then 'No log metric filter and alarm exist for VPC changes.' + else filter_name || ' forwards events for VPC changes.' + end as reason + + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join filter_data as f on a.account_id = f.account_id; + EOQ +} + +query "log_metric_filter_route_table" { + sql = <<-EOQ + with filter_data as ( + select + trail.account_id, + trail.name as trail_name, + trail.is_logging, + split_part(trail.log_group_arn, ':', 7) as log_group_name, + filter.name as filter_name, + action_arn as topic_arn, + alarm.metric_name, + alarm.name as alarm_name, + subscription.subscription_arn, + filter.filter_pattern + from + aws_cloudtrail_trail as trail, + jsonb_array_elements(trail.event_selectors) as se, + aws_cloudwatch_log_metric_filter as filter, + aws_cloudwatch_alarm as alarm, + jsonb_array_elements_text(alarm.alarm_actions) as action_arn, + aws_sns_topic_subscription as subscription + where + trail.is_multi_region_trail is true + and trail.is_logging + and se ->> 'ReadWriteType' = 'All' + and trail.log_group_arn is not null + and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) + and filter.filter_pattern ~ '\s*\$\.eventName\s*=\s*CreateRoute.+\$\.eventName\s*=\s*CreateRouteTable.+\$\.eventName\s*=\s*ReplaceRoute.+\$\.eventName\s*=\s*ReplaceRouteTableAssociation.+\$\.eventName\s*=\s*DeleteRouteTable.+\$\.eventName\s*=\s*DeleteRoute.+\$\.eventName\s*=\s*DisassociateRouteTable' + and alarm.metric_name = filter.metric_transformation_name + and subscription.topic_arn = action_arn + ) + select + distinct 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when f.trail_name is null then 'alarm' + else 'ok' + end as status, + case + when f.trail_name is null then 'No log metric filter and alarm exist for route table changes.' + else filter_name || ' forwards events for route table changes.' + end as reason + + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join filter_data as f on a.account_id = f.account_id; + EOQ +} + +query "log_metric_filter_network_gateway" { + sql = <<-EOQ + with filter_data as ( + select + trail.account_id, + trail.name as trail_name, + trail.is_logging, + split_part(trail.log_group_arn, ':', 7) as log_group_name, + filter.name as filter_name, + action_arn as topic_arn, + alarm.metric_name, + alarm.name as alarm_name, + subscription.subscription_arn, + filter.filter_pattern + from + aws_cloudtrail_trail as trail, + jsonb_array_elements(trail.event_selectors) as se, + aws_cloudwatch_log_metric_filter as filter, + aws_cloudwatch_alarm as alarm, + jsonb_array_elements_text(alarm.alarm_actions) as action_arn, + aws_sns_topic_subscription as subscription + where + trail.is_multi_region_trail is true + and trail.is_logging + and se ->> 'ReadWriteType' = 'All' + and trail.log_group_arn is not null + and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) + and filter.filter_pattern ~ '\s*\$\.eventName\s*=\s*CreateCustomerGateway.+\$\.eventName\s*=\s*DeleteCustomerGateway.+\$\.eventName\s*=\s*AttachInternetGateway.+\$\.eventName\s*=\s*CreateInternetGateway.+\$\.eventName\s*=\s*DeleteInternetGateway.+\$\.eventName\s*=\s*DetachInternetGateway' + and alarm.metric_name = filter.metric_transformation_name + and subscription.topic_arn = action_arn + ) + select + distinct 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when f.trail_name is null then 'alarm' + else 'ok' + end as status, + case + when f.trail_name is null then 'No log metric filter and alarm exist for changes to network gateways.' + else filter_name || ' forwards events for changes to network gateways.' + end as reason + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join filter_data as f on a.account_id = f.account_id; + EOQ +} + +query "log_metric_filter_network_acl" { + sql = <<-EOQ + with filter_data as ( + select + trail.account_id, + trail.name as trail_name, + trail.is_logging, + split_part(trail.log_group_arn, ':', 7) as log_group_name, + filter.name as filter_name, + action_arn as topic_arn, + alarm.metric_name, + subscription.subscription_arn, + filter.filter_pattern + from + aws_cloudtrail_trail as trail, + jsonb_array_elements(trail.event_selectors) as se, + aws_cloudwatch_log_metric_filter as filter, + aws_cloudwatch_alarm as alarm, + jsonb_array_elements_text(alarm.alarm_actions) as action_arn, + aws_sns_topic_subscription as subscription + where + trail.is_multi_region_trail is true + and trail.is_logging + and se ->> 'ReadWriteType' = 'All' + and trail.log_group_arn is not null + and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) + and filter.filter_pattern ~ '\s*\$\.eventName\s*=\s*CreateNetworkAcl.+\$\.eventName\s*=\s*CreateNetworkAclEntry.+\$\.eventName\s*=\s*DeleteNetworkAcl.+\$\.eventName\s*=\s*DeleteNetworkAclEntry.+\$\.eventName\s*=\s*ReplaceNetworkAclEntry.+\$\.eventName\s*=\s*ReplaceNetworkAclAssociation' + and alarm.metric_name = filter.metric_transformation_name + and subscription.topic_arn = action_arn + ) + select + distinct 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when f.trail_name is null then 'alarm' + else 'ok' + end as status, + case + when f.trail_name is null then 'No log metric filter and alarm exist for changes to NACLs.' + else filter_name || ' forwards events for changes to NACLs.' + end as reason + + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join filter_data as f on a.account_id = f.account_id; + EOQ +} + +query "log_metric_filter_security_group" { + sql = <<-EOQ + with filter_data as ( + select + trail.account_id, + trail.name as trail_name, + trail.is_logging, + split_part(trail.log_group_arn, ':', 7) as log_group_name, + filter.name as filter_name, + action_arn as topic_arn, + alarm.metric_name, + subscription.subscription_arn, + filter.filter_pattern + from + aws_cloudtrail_trail as trail, + jsonb_array_elements(trail.event_selectors) as se, + aws_cloudwatch_log_metric_filter as filter, + aws_cloudwatch_alarm as alarm, + jsonb_array_elements_text(alarm.alarm_actions) as action_arn, + aws_sns_topic_subscription as subscription + where + trail.is_multi_region_trail is true + and trail.is_logging + and se ->> 'ReadWriteType' = 'All' + and trail.log_group_arn is not null + and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) + and filter.filter_pattern ~ '\s*\$\.eventName\s*=\s*AuthorizeSecurityGroupIngress.+\$\.eventName\s*=\s*AuthorizeSecurityGroupEgress.+\$\.eventName\s*=\s*RevokeSecurityGroupIngress.+\$\.eventName\s*=\s*RevokeSecurityGroupEgress.+\$\.eventName\s*=\s*CreateSecurityGroup.+\$\.eventName\s*=\s*DeleteSecurityGroup' + and alarm.metric_name = filter.metric_transformation_name + and subscription.topic_arn = action_arn + ) + select + distinct 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when f.trail_name is null then 'alarm' + else 'ok' + end as status, + case + when f.trail_name is null then 'No log metric filter and alarm exist for security group changes.' + else filter_name || ' forwards events for security group changes.' + end as reason + + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join filter_data as f on a.account_id = f.account_id; + EOQ +} + +query "log_metric_filter_config_configuration" { + sql = <<-EOQ + with filter_data as ( + select + trail.account_id, + trail.name as trail_name, + trail.is_logging, + split_part(trail.log_group_arn, ':', 7) as log_group_name, + filter.name as filter_name, + action_arn as topic_arn, + alarm.metric_name, + subscription.subscription_arn, + filter.filter_pattern + from + aws_cloudtrail_trail as trail, + jsonb_array_elements(trail.event_selectors) as se, + aws_cloudwatch_log_metric_filter as filter, + aws_cloudwatch_alarm as alarm, + jsonb_array_elements_text(alarm.alarm_actions) as action_arn, + aws_sns_topic_subscription as subscription + where + trail.is_multi_region_trail is true + and trail.is_logging + and se ->> 'ReadWriteType' = 'All' + and trail.log_group_arn is not null + and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) + and filter.filter_pattern ~ '\s*\$\.eventSource\s*=\s*config.amazonaws.com.+\$\.eventName\s*=\s*StopConfigurationRecorder.+\$\.eventName\s*=\s*DeleteDeliveryChannel.+\$\.eventName\s*=\s*PutDeliveryChannel.+\$\.eventName\s*=\s*PutConfigurationRecorder' + and alarm.metric_name = filter.metric_transformation_name + and subscription.topic_arn = action_arn + ) + select + distinct 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when f.trail_name is null then 'alarm' + else 'ok' + end as status, + case + when f.trail_name is null then 'No log metric filter and alarm exist for AWS Config configuration changes.' + else filter_name || ' forwards events for AWS Config configuration changes.' + end as reason + + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join filter_data as f on a.account_id = f.account_id; + EOQ +} + +query "log_metric_filter_bucket_policy" { + sql = <<-EOQ + with filter_data as ( + select + trail.account_id, + trail.name as trail_name, + trail.is_logging as is_logging, + split_part(trail.log_group_arn, ':', 7) as log_group_name, + filter.name as filter_name, + action_arn as topic_arn, + alarm.metric_name, + subscription.subscription_arn, + filter.filter_pattern, + filter.metric_transformation_name + from + aws_cloudtrail_trail as trail, + jsonb_array_elements(trail.event_selectors) as se, + aws_cloudwatch_log_metric_filter as filter, + aws_cloudwatch_alarm as alarm, + jsonb_array_elements_text(alarm.alarm_actions) as action_arn, + aws_sns_topic_subscription as subscription + where + trail.is_multi_region_trail is true + and trail.is_logging + and se ->> 'ReadWriteType' = 'All' + and trail.log_group_arn is not null + and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) + and filter.filter_pattern ~ '\s*\$\.eventSource\s*=\s*s3.amazonaws.com.+\$\.eventName\s*=\s*PutBucketAcl.+\$\.eventName\s*=\s*PutBucketPolicy.+\$\.eventName\s*=\s*PutBucketCors.+\$\.eventName\s*=\s*PutBucketLifecycle.+\$\.eventName\s*=\s*PutBucketReplication.+\$\.eventName\s*=\s*DeleteBucketPolicy.+\$\.eventName\s*=\s*DeleteBucketCors.+\$\.eventName\s*=\s*DeleteBucketLifecycle.+\$\.eventName\s*=\s*DeleteBucketReplication' + and alarm.metric_name = filter.metric_transformation_name + and subscription.topic_arn = action_arn + ) + select + distinct 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when f.trail_name is null then 'alarm' + else 'ok' + end as status, + case + when f.trail_name is null then 'No log metric filter and alarm exist for S3 bucket policy changes.' + else filter_name || ' forwards events for S3 bucket policy changes.' + end as reason + + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join filter_data as f on a.account_id = f.account_id; + EOQ +} + +query "log_metric_filter_disable_or_delete_cmk" { + sql = <<-EOQ + with filter_data as ( + select + trail.account_id, + trail.name as trail_name, + trail.is_logging, + split_part(trail.log_group_arn, ':', 7) as log_group_name, + filter.name as filter_name, + action_arn as topic_arn, + alarm.metric_name, + subscription.subscription_arn, + filter.filter_pattern + from + aws_cloudtrail_trail as trail, + jsonb_array_elements(trail.event_selectors) as se, + aws_cloudwatch_log_metric_filter as filter, + aws_cloudwatch_alarm as alarm, + jsonb_array_elements_text(alarm.alarm_actions) as action_arn, + aws_sns_topic_subscription as subscription + where + trail.is_multi_region_trail is true + and trail.is_logging + and se ->> 'ReadWriteType' = 'All' + and trail.log_group_arn is not null + and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) + and filter.filter_pattern ~ '\s*\$\.eventSource\s*=\s*kms.amazonaws.com.+\$\.eventName\s*=\s*DisableKey.+\$\.eventName\s*=\s*ScheduleKeyDeletion' + and alarm.metric_name = filter.metric_transformation_name + and subscription.topic_arn = action_arn + ) + select + distinct 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when f.trail_name is null then 'alarm' + else 'ok' + end as status, + case + when f.trail_name is null then 'No log metric filter and alarm exist for disabling/deletion of CMKs.' + else filter_name || ' forwards events for disabling/deletion of CMKs.' + end as reason + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join filter_data as f on a.account_id = f.account_id; + EOQ +} + +query "log_metric_filter_console_authentication_failure" { + sql = <<-EOQ + with filter_data as ( + select + trail.account_id, + trail.name as trail_name, + trail.is_logging, + split_part(trail.log_group_arn, ':', 7) as log_group_name, + filter.name as filter_name, + action_arn as topic_arn, + alarm.metric_name, + subscription.subscription_arn, + filter.filter_pattern + from + aws_cloudtrail_trail as trail, + jsonb_array_elements(trail.event_selectors) as se, + aws_cloudwatch_log_metric_filter as filter, + aws_cloudwatch_alarm as alarm, + jsonb_array_elements_text(alarm.alarm_actions) as action_arn, + aws_sns_topic_subscription as subscription + where + trail.is_multi_region_trail is true + and trail.is_logging + and se ->> 'ReadWriteType' = 'All' + and trail.log_group_arn is not null + and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) + and filter.filter_pattern ~ '\s*\$\.eventName\s*=\s*ConsoleLogin.+\$\.errorMessage\s*=\s*"Failed authentication"' + and alarm.metric_name = filter.metric_transformation_name + and subscription.topic_arn = action_arn + ) + select + distinct 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when f.trail_name is null then 'alarm' + else 'ok' + end as status, + case + when f.trail_name is null then 'No log metric filter and alarm exist for console authentication failures.' + else filter_name || ' forwards events for console authentication failures.' + end as reason + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join filter_data as f on a.account_id = f.account_id; + EOQ +} + +query "log_metric_filter_cloudtrail_configuration" { + sql = <<-EOQ + with filter_data as ( + select + trail.account_id, + trail.name as trail_name, + trail.is_logging, + split_part(trail.log_group_arn, ':', 7) as log_group_name, + filter.name as filter_name, + action_arn as topic_arn, + alarm.metric_name, + subscription.subscription_arn, + filter.filter_pattern + from + aws_cloudtrail_trail as trail, + jsonb_array_elements(trail.event_selectors) as se, + aws_cloudwatch_log_metric_filter as filter, + aws_cloudwatch_alarm as alarm, + jsonb_array_elements_text(alarm.alarm_actions) as action_arn, + aws_sns_topic_subscription as subscription + where + trail.is_multi_region_trail is true + and trail.is_logging + and se ->> 'ReadWriteType' = 'All' + and trail.log_group_arn is not null + and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) + and filter.filter_pattern ~ '\s*\$\.eventName\s*=\s*CreateTrail.+\$\.eventName\s*=\s*UpdateTrail.+\$\.eventName\s*=\s*DeleteTrail.+\$\.eventName\s*=\s*StartLogging.+\$\.eventName\s*=\s*StopLogging' + and alarm.metric_name = filter.metric_transformation_name + and subscription.topic_arn = action_arn + ) + select + distinct 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when f.trail_name is null then 'alarm' + else 'ok' + end as status, + case + when f.trail_name is null then 'No log metric filter and alarm exist for CloudTrail configuration changes.' + else filter_name || ' forwards events for CloudTrail configuration changes.' + end as reason + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join filter_data as f on a.account_id = f.account_id; + EOQ +} + +query "cloudwatch_cross_account_sharing" { + sql = <<-EOQ + with iam_role_cross_account_sharing_count as ( + select + arn, + replace(replace(replace((a -> 'Principal' ->> 'AWS'), '[',''), ']', ''), '"', '') as cross_account_details, + account_id + from + aws_iam_role, + jsonb_array_elements(assume_role_policy_std -> 'Statement') as a + where + name = 'CloudWatch-CrossAccountSharingRole' + ) + select + a.arn as resource, + case + when c.arn is null then 'ok' + else 'info' + end as status, + case + when c.arn is null then 'CloudWatch does not allow cross-account sharing.' + else 'CloudWatch allow cross-account sharing with '|| cross_account_details || '.' + end as reason + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join iam_role_cross_account_sharing_count as c on c.account_id = a.account_id; + EOQ +} + +# Non-Config rule query + +query "log_metric_filter_organization" { + sql = <<-EOQ + with filter_data as ( + select + trail.account_id, + trail.name as trail_name, + trail.is_logging, + split_part(trail.log_group_arn, ':', 7) as log_group_name, + filter.name as filter_name, + action_arn as topic_arn, + alarm.metric_name, + alarm.name as alarm_name, + subscription.subscription_arn, + filter.filter_pattern + from + aws_cloudtrail_trail as trail, + jsonb_array_elements(trail.event_selectors) as se, + aws_cloudwatch_log_metric_filter as filter, + aws_cloudwatch_alarm as alarm, + jsonb_array_elements_text(alarm.alarm_actions) as action_arn, + aws_sns_topic_subscription as subscription + where + trail.is_multi_region_trail is true + and trail.is_logging + and se ->> 'ReadWriteType' = 'All' + and trail.log_group_arn is not null + and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) + and filter.filter_pattern ~ '\s*\$\.eventSource\s*=\s*organizations.amazonaws.com.+\$\.eventName\s*=\s*"AcceptHandshake".+\$\.eventName\s*=\s*"AttachPolicy".+\$\.eventName\s*=\s*"CreateAccount".+\$\.eventName\s*=\s*"CreateOrganizationalUnit".+\$\.eventName\s*=\s*"CreatePolicy".+\$\.eventName\s*=\s*"DeclineHandshake".+\$\.eventName\s*=\s*"DeleteOrganization".+\$\.eventName\s*=\s*"DeleteOrganizationalUnit".+\$\.eventName\s*=\s*"DeletePolicy".+\$\.eventName\s*=\s*"DetachPolicy".+\$\.eventName\s*=\s*"DisablePolicyType".+\$\.eventName\s*=\s*"EnablePolicyType".+\$\.eventName\s*=\s*"InviteAccountToOrganization".+\$\.eventName\s*=\s*"LeaveOrganization".+\$\.eventName\s*=\s*"MoveAccount".+\$\.eventName\s*=\s*"RemoveAccountFromOrganization".+\$\.eventName\s*=\s*"UpdatePolicy".+\$\.eventName\s*=\s*"UpdateOrganizationalUnit"' + and alarm.metric_name = filter.metric_transformation_name + and subscription.topic_arn = action_arn + ) + select + distinct 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when f.trail_name is null then 'alarm' + else 'ok' + end as status, + case + when f.trail_name is null then 'No log metric filter and alarm exists for AWS Organizations changes.' + else filter_name || ' forwards relevant events for AWS Organizations changes.' + end as reason + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join filter_data as f on a.account_id = f.account_id; + EOQ +} diff --git a/conformance_pack/codebuild.sp b/conformance_pack/codebuild.sp index ab7eccf6..1d5883ba 100644 --- a/conformance_pack/codebuild.sp +++ b/conformance_pack/codebuild.sp @@ -72,7 +72,7 @@ control "codebuild_project_logging_enabled" { control "codebuild_project_environment_privileged_mode_disabled" { title = "CodeBuild project environment privileged mode should be disabled" - description = "This control checks if an AWS CodeBuild project environment has privileged mode enabled. The rule is non compliant for a CodeBuild project if ‘privilegedMode’ is set to ‘true’." + description = "This control checks if an AWS CodeBuild project environment has privileged mode enabled. The rule is non compliant for a CodeBuild project if 'privilegedMode' is set to 'true'." query = query.codebuild_project_environment_privileged_mode_disabled tags = merge(local.conformance_pack_codebuild_common_tags, { @@ -89,3 +89,201 @@ control "codebuild_project_artifact_encryption_enabled" { cis_controls_v8_ig1 = "true" }) } + +query "codebuild_project_plaintext_env_variables_no_sensitive_aws_values" { + sql = <<-EOQ + with invalid_key_name as ( + select + distinct arn, + name + from + aws_codebuild_project, + jsonb_array_elements(environment -> 'EnvironmentVariables') as env + where + env ->> 'Name' ilike any(array['%AWS_ACCESS_KEY_ID%', '%AWS_SECRET_ACCESS_KEY%', '%PASSWORD%']) + and env ->> 'Type' = 'PLAINTEXT' + ) + select + a.arn as resource, + case + when b.arn is null then 'ok' + else 'alarm' + end as status, + case + when b.arn is null then a.title || ' has no plaintext environment variables with sensitive AWS values.' + else a.title || ' has plaintext environment variables with sensitive AWS values.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "a.")} + from + aws_codebuild_project as a + left join invalid_key_name b on a.arn = b.arn; + EOQ +} + +query "codebuild_project_source_repo_oauth_configured" { + sql = <<-EOQ + select + p.arn as resource, + case + when p.source ->> 'Type' not in ('GITHUB', 'BITBUCKET') then 'skip' + when c.auth_type = 'OAUTH' then 'ok' + else 'alarm' + end as status, + case + when p.source ->> 'Type' = 'NO_SOURCE' then p.title || ' doesn''t have input source code.' + when p.source ->> 'Type' not in ('GITHUB', 'BITBUCKET') then p.title || ' source code isn''t in GitHub/Bitbucket repository.' + when c.auth_type = 'OAUTH' then p.title || ' using OAuth to connect source repository.' + else p.title || ' not using OAuth to connect source repository.' + end as reason + + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "p.")} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "p.")} + from + aws_codebuild_project as p + left join aws_codebuild_source_credential as c on (p.region = c.region and p.source ->> 'Type' = c.server_type); + EOQ +} + +query "codebuild_project_with_user_controlled_buildspec" { + sql = <<-EOQ + select + arn as resource, + case + when split_part(source ->> 'Buildspec', '.', -1) = 'yml' then 'alarm' + else 'ok' + end as status, + case + when split_part(source ->> 'Buildspec', '.', -1) = 'yml' then title || ' uses a user controlled buildspec.' + else title || ' does not uses a user controlled buildspec.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_codebuild_project; + EOQ +} + +query "codebuild_project_logging_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when logs_config -> 'CloudWatchLogs' ->> 'Status' = 'ENABLED' or logs_config -> 'S3Logs' ->> 'Status' = 'ENABLED' then 'ok' + else 'alarm' + end as status, + case + when logs_config -> 'CloudWatchLogs' ->> 'Status' = 'ENABLED' or logs_config -> 'S3Logs' ->> 'Status' = 'ENABLED' then title || ' logging enabled.' + else title || ' logging disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_codebuild_project; + EOQ +} + +query "codebuild_project_environment_privileged_mode_disabled" { + sql = <<-EOQ + select + arn as resource, + case + when environment ->> 'PrivilegedMode' = 'true' then 'alarm' + else 'ok' + end as status, + case + when environment ->> 'PrivilegedMode' = 'true' then title || ' environment privileged mode enabled.' + else title || ' environment privileged mode disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_codebuild_project; + EOQ +} + +query "codebuild_project_artifact_encryption_enabled" { + sql = <<-EOQ + with secondary_artifact as ( + select + distinct arn + from + aws_codebuild_project, + jsonb_array_elements(secondary_artifacts) as a + where + a -> 'EncryptionDisabled' = 'true' + ) + select + a.arn as resource, + case + when p.artifacts ->> 'EncryptionDisabled' = 'false' + and (p.secondary_artifacts is null or a.arn is null) then 'ok' + else 'alarm' + end as status, + case + when p.artifacts ->> 'EncryptionDisabled' = 'false' + and (p.secondary_artifacts is null or a.arn is null) then p.title || ' all artifacts encryption enabled.' + else p.title || ' all artifacts encryption not enabled.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "p.")} + from + aws_codebuild_project as p + left join secondary_artifact as a on a.arn = p.arn; + EOQ +} + +query "codebuild_project_build_greater_then_90_days" { + sql = <<-EOQ + with latest_codebuild_build as ( + select + project_name, + region, + account_id, + min(date_part('day', now() - end_time)) as build_time + from + aws_codebuild_build + group by + project_name, + region, + account_id + ) + select + p.arn as resource, + case + when b.build_time is null then 'alarm' + when b.build_time < 90 then 'ok' + else 'alarm' + end as status, + case + when b.build_time is null then p.title || ' was never build.' + else p.title || ' was build ' || build_time || ' day(s) before.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "p.")} + from + aws_codebuild_project as p + left join latest_codebuild_build as b on p.name = b.project_name and p.region = b.region and p.account_id = b.account_id; + EOQ +} + +# Non-Config rule query + +query "codebuild_project_s3_logs_encryption_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when not (logs_config -> 'S3Logs' ->> 'EncryptionDisabled')::bool then 'ok' + else 'alarm' + end as status, + case + when not (logs_config -> 'S3Logs' ->> 'EncryptionDisabled')::bool then title || ' S3Logs encryption enabled.' + else title || ' S3Logs encryption disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_codebuild_project; + EOQ +} diff --git a/conformance_pack/config.sp b/conformance_pack/config.sp index 2da4abfc..a1373a78 100644 --- a/conformance_pack/config.sp +++ b/conformance_pack/config.sp @@ -17,3 +17,62 @@ control "config_enabled_all_regions" { soc_2 = "true" }) } + +query "config_enabled_all_regions" { + sql = <<-EOQ + -- pgFormatter-ignore + -- Get count for any region with all matching criteria + with global_recorders as ( + select + count(*) as global_config_recorders + from + aws_config_configuration_recorder + where + recording_group -> 'IncludeGlobalResourceTypes' = 'true' + and recording_group -> 'AllSupported' = 'true' + and status ->> 'Recording' = 'true' + and status ->> 'LastStatus' = 'SUCCESS' + ) + select + 'arn:aws::' || a.region || ':' || a.account_id as resource, + case + -- When any of the region satisfies with above CTE + -- In left join of table, regions now having + -- 'Recording' and 'LastStatus' matching criteria can be considered as OK + when + g.global_config_recorders >= 1 + and status ->> 'Recording' = 'true' + and status ->> 'LastStatus' = 'SUCCESS' + then 'ok' + -- Skip any regions that are disabled in the account. + when a.opt_in_status = 'not-opted-in' then 'skip' + else 'alarm' + end as status, + -- Below cases are for citing respective reasons for control state + case + when a.opt_in_status = 'not-opted-in' then a.region || ' region is disabled.' + else + case + when recording_group -> 'IncludeGlobalResourceTypes' = 'true' then a.region || ' IncludeGlobalResourceTypes enabled,' + else a.region || ' IncludeGlobalResourceTypes disabled,' + end || + case + when recording_group -> 'AllSupported' = 'true' then ' AllSupported enabled,' + else ' AllSupported disabled,' + end || + case + when status ->> 'Recording' = 'true' then ' Recording enabled' + else ' Recording disabled' + end || + case + when status ->> 'LastStatus' = 'SUCCESS' then ' and LastStatus is SUCCESS.' + else ' and LastStatus is not SUCCESS.' + end + end as reason + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "a.")} + from + global_recorders as g, + aws_region as a + left join aws_config_configuration_recorder as r on r.account_id = a.account_id and r.region = a.name; + EOQ +} \ No newline at end of file diff --git a/conformance_pack/dax.sp b/conformance_pack/dax.sp index 5de02ea2..3523701b 100644 --- a/conformance_pack/dax.sp +++ b/conformance_pack/dax.sp @@ -15,3 +15,22 @@ control "dax_cluster_encryption_at_rest_enabled" { hipaa = "true" }) } + +query "dax_cluster_encryption_at_rest_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when sse_description ->> 'Status' = 'ENABLED' then 'ok' + else 'alarm' + end as status, + case + when sse_description ->> 'Status' = 'ENABLED' then title || ' encryption at rest enabled.' + else title || ' encryption at rest not enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_dax_cluster; + EOQ +} diff --git a/conformance_pack/dms.sp b/conformance_pack/dms.sp index 25dc70d1..f47c2ff8 100644 --- a/conformance_pack/dms.sp +++ b/conformance_pack/dms.sp @@ -24,3 +24,23 @@ control "dms_replication_instance_not_publicly_accessible" { rbi_cyber_security = "true" }) } + +query "dms_replication_instance_not_publicly_accessible" { + sql = <<-EOQ + select + arn as resource, + case + when publicly_accessible then 'alarm' + else 'ok' + end status, + case + when publicly_accessible then title || ' publicly accessible.' + else title || ' not publicly accessible.' + end reason + + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_dms_replication_instance; + EOQ +} diff --git a/conformance_pack/dynamodb.sp b/conformance_pack/dynamodb.sp index 75f6563d..fecc8ff3 100644 --- a/conformance_pack/dynamodb.sp +++ b/conformance_pack/dynamodb.sp @@ -113,3 +113,172 @@ control "dynamodb_table_protected_by_backup_plan" { soc_2 = "true" }) } + +query "dynamodb_table_auto_scaling_enabled" { + sql = <<-EOQ + with table_with_autocaling as ( + select + t.resource_id as resource_id, + count(t.resource_id) as count + from + aws_appautoscaling_target as t where service_namespace = 'dynamodb' + group by t.resource_id + ) + select + d.arn as resource, + case + when d.billing_mode = 'PAY_PER_REQUEST' then 'ok' + when t.resource_id is null then 'alarm' + when t.count < 2 then 'alarm' + else 'ok' + end as status, + case + when d.billing_mode = 'PAY_PER_REQUEST' then d.title || ' on-demand mode enabled.' + when t.resource_id is null then d.title || ' autoscaling not enabled.' + when t.count < 2 then d.title || ' auto scaling not enabled for both read and write capacity.' + else d.title || ' autoscaling enabled for both read and write capacity.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "d.")} + from + aws_dynamodb_table as d + left join table_with_autocaling as t on concat('table/', d.name) = t.resource_id; + EOQ +} + +query "dynamodb_table_point_in_time_recovery_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when lower( point_in_time_recovery_description ->> 'PointInTimeRecoveryStatus' ) = 'disabled' then 'alarm' + else 'ok' + end as status, + case + when lower( point_in_time_recovery_description ->> 'PointInTimeRecoveryStatus' ) = 'disabled' then title || ' point-in-time recovery not enabled.' + else title || ' point-in-time recovery enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_dynamodb_table; + EOQ +} + +query "dynamodb_table_encrypted_with_kms" { + sql = <<-EOQ + select + arn as resource, + case + when sse_description is null then 'alarm' + else 'ok' + end as status, + case + when sse_description is null then title || ' not encrypted with KMS.' + else title || ' encrypted with KMS.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_dynamodb_table; + EOQ +} + +query "dynamodb_table_in_backup_plan" { + sql = <<-EOQ + with mapped_with_id as ( + select + jsonb_agg(elems) as mapped_ids + from + aws_backup_selection, + jsonb_array_elements(resources) as elems + group by backup_plan_id + ), + mapped_with_tags as ( + select + jsonb_agg(elems ->> 'ConditionKey') as mapped_tags + from + aws_backup_selection, + jsonb_array_elements(list_of_tags) as elems + group by backup_plan_id + ), + backed_up_table as ( + select + t.name + from + aws_dynamodb_table as t + join mapped_with_id as m on m.mapped_ids ?| array[t.arn] + union + select + t.name + from + aws_dynamodb_table as t + join mapped_with_tags as m on m.mapped_tags ?| array(select jsonb_object_keys(tags)) + ) + select + t.arn as resource, + case + when b.name is null then 'alarm' + else 'ok' + end as status, + case + when b.name is null then t.title || ' not in backup plan.' + else t.title || ' in backup plan.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "t.")} + from + aws_dynamodb_table as t + left join backed_up_table as b on t.name = b.name; + EOQ +} + +query "dynamodb_table_encryption_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when sse_description is not null and sse_description ->> 'SSEType' = 'KMS' then 'ok' + when sse_description is null then 'ok' + else 'alarm' + end as status, + case + when sse_description is not null and sse_description ->> 'SSEType' = 'KMS' + then title || ' encrypted with AWS KMS.' + when sse_description is null then title || ' encrypted with DynamoDB managed CMK.' + else title || ' not encrypted with CMK.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_dynamodb_table; + EOQ +} + +query "dynamodb_table_protected_by_backup_plan" { + sql = <<-EOQ + with backup_protected_table as ( + select + resource_arn as arn + from + aws_backup_protected_resource as b + where + resource_type = 'DynamoDB' + ) + select + t.arn as resource, + case + when b.arn is not null then 'ok' + else 'alarm' + end as status, + case + when b.arn is not null then t.title || ' is protected by backup plan.' + else t.title || ' is not protected by backup plan.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "t.")} + from + aws_dynamodb_table as t + left join backup_protected_table as b on t.arn = b.arn; + EOQ +} diff --git a/conformance_pack/ebs.sp b/conformance_pack/ebs.sp index 6abfed02..acf51825 100644 --- a/conformance_pack/ebs.sp +++ b/conformance_pack/ebs.sp @@ -132,3 +132,182 @@ control "ebs_volume_unused" { nist_800_53_rev_5 = "true" }) } + +query "ebs_snapshot_not_publicly_restorable" { + sql = <<-EOQ + select + 'arn:' || partition || ':ec2:' || region || ':' || account_id || ':snapshot/' || snapshot_id as resource, + case + when create_volume_permissions @> '[{"Group": "all", "UserId": null}]' then 'alarm' + else 'ok' + end status, + case + when create_volume_permissions @> '[{"Group": "all", "UserId": null}]' then title || ' is publicly restorable.' + else title || ' is not publicly restorable.' + end reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ebs_snapshot; + EOQ +} + +query "ebs_volume_encryption_at_rest_enabled" { + sql = <<-EOQ + select + + arn as resource, + case + when encrypted then 'ok' + else 'alarm' + end status, + case + when encrypted then volume_id || ' encrypted.' + else volume_id || ' not encrypted.' + end reason + + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ebs_volume; + EOQ +} + +query "ebs_attached_volume_encryption_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when state != 'in-use' then 'skip' + when encrypted then 'ok' + else 'alarm' + end as status, + case + when state != 'in-use' then volume_id || ' not attached.' + when encrypted then volume_id || ' encrypted.' + else volume_id || ' not encrypted.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ebs_volume; + EOQ +} + +query "ebs_volume_in_backup_plan" { + sql = <<-EOQ + with mapped_with_id as ( + select + jsonb_agg(elems) as mapped_ids + from + aws_backup_selection, + jsonb_array_elements(resources) as elems + group by backup_plan_id + ), + mapped_with_tags as ( + select + jsonb_agg(elems ->> 'ConditionKey') as mapped_tags + from + aws_backup_selection, + jsonb_array_elements(list_of_tags) as elems + group by backup_plan_id + ), + backed_up_volume as ( + select + v.volume_id + from + aws_ebs_volume as v + join mapped_with_id as t on t.mapped_ids ?| array[v.arn] + union + select + v.volume_id + from + aws_ebs_volume as v + join mapped_with_tags as t on t.mapped_tags ?| array(select jsonb_object_keys(tags)) + ) + select + v.arn as resource, + case + when b.volume_id is null then 'alarm' + else 'ok' + end as status, + case + when b.volume_id is null then v.title || ' not in backup plan.' + else v.title || ' in backup plan.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "v.")} + from + aws_ebs_volume as v + left join backed_up_volume as b on v.volume_id = b.volume_id; + EOQ +} + +query "ebs_attached_volume_delete_on_termination_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when state != 'in-use' then 'skip' + when attachment ->> 'DeleteOnTermination' = 'true' then 'ok' + else 'alarm' + end as status, + case + when state != 'in-use' then title || ' not attached to EC2 instance.' + when attachment ->> 'DeleteOnTermination' = 'true' then title || ' attached to ' || (attachment ->> 'InstanceId') || ', delete on termination enabled.' + else title || ' attached to ' || (attachment ->> 'InstanceId') || ', delete on termination disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ebs_volume + left join jsonb_array_elements(attachments) as attachment on true; + EOQ +} + +query "ebs_volume_protected_by_backup_plan" { + sql = <<-EOQ + with backup_protected_volume as ( + select + resource_arn as arn + from + aws_backup_protected_resource as b + where + resource_type = 'EBS' + ) + select + v.arn as resource, + case + when b.arn is not null then 'ok' + else 'alarm' + end as status, + case + when b.arn is not null then v.title || ' is protected by backup plan.' + else v.title || ' is not protected by backup plan.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "v.")} + from + aws_ebs_volume as v + left join backup_protected_volume as b on v.arn = b.arn; + EOQ +} + +query "ebs_volume_unused" { + sql = <<-EOQ + select + arn as resource, + case + when state = 'in-use' then 'ok' + else 'alarm' + end as status, + case + when state = 'in-use' then title || ' attached to EC2 instance.' + else title || ' not attached to EC2 instance.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ebs_volume; + EOQ +} diff --git a/conformance_pack/ec2.sp b/conformance_pack/ec2.sp index 62cf4ee7..bd4745d6 100644 --- a/conformance_pack/ec2.sp +++ b/conformance_pack/ec2.sp @@ -215,3 +215,408 @@ control "ec2_instance_no_launch_wizard_security_group" { other_checks = "true" }) } + +query "ec2_ebs_default_encryption_enabled" { + sql = <<-EOQ + select + 'arn:' || partition || '::' || region || ':' || account_id as resource, + case + when not default_ebs_encryption_enabled then 'alarm' + else 'ok' + end as status, + case + when not default_ebs_encryption_enabled then region || ' default EBS encryption disabled.' + else region || ' default EBS encryption enabled.' + end as reason + ${local.common_dimensions_sql} + from + aws_ec2_regional_settings; + EOQ +} + +query "ec2_instance_detailed_monitoring_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when monitoring_state = 'enabled' then 'ok' + else 'alarm' + end status, + case + when monitoring_state = 'enabled' then instance_id || ' detailed monitoring enabled.' + else instance_id || ' detailed monitoring disabled.' + end reason + + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_instance; + EOQ +} + +query "ec2_instance_in_vpc" { + sql = <<-EOQ + select + arn as resource, + case + when vpc_id is null then 'alarm' + else 'ok' + end as status, + case + when vpc_id is null then title || ' not in VPC.' + else title || ' in VPC.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_instance; + EOQ +} + +query "ec2_instance_not_publicly_accessible" { + sql = <<-EOQ + select + arn as resource, + case + when public_ip_address is null then 'ok' + else 'alarm' + end as status, + case + when public_ip_address is null then instance_id || ' not publicly accessible.' + else instance_id || ' publicly accessible.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_instance; + EOQ +} + +query "ec2_stopped_instance_30_days" { + sql = <<-EOQ + select + arn as resource, + case + when instance_state not in ('stopped', 'stopping') then 'skip' + when state_transition_time <= (current_date - interval '30' day) then 'alarm' + else 'ok' + end as status, + case + when instance_state not in ('stopped', 'stopping') then title || ' is in ' || instance_state || ' state.' + else title || ' stopped since ' || to_char(state_transition_time , 'DD-Mon-YYYY') || ' (' || extract(day from current_timestamp - state_transition_time) || ' days).' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_instance; + EOQ +} + +query "ec2_instance_ebs_optimized" { + sql = <<-EOQ + select + arn as resource, + case + when ebs_optimized then 'ok' + else 'alarm' + end as status, + case + when ebs_optimized then title || ' EBS optimization enabled.' + else title || ' EBS optimization disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_instance; + EOQ +} + +query "ec2_instance_uses_imdsv2" { + sql = <<-EOQ + select + arn as resource, + case + when metadata_options ->> 'HttpTokens' = 'optional' then 'alarm' + else 'ok' + end as status, + case + when metadata_options ->> 'HttpTokens' = 'optional' then title || ' not configured to use Instance Metadata Service Version 2 (IMDSv2).' + else title || ' configured to use Instance Metadata Service Version 2 (IMDSv2).' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_instance; + EOQ +} + +query "ec2_instance_protected_by_backup_plan" { + sql = <<-EOQ + with backup_protected_instance as ( + select + resource_arn as arn + from + aws_backup_protected_resource as b + where + resource_type = 'EC2' + ) + select + i.arn as resource, + case + when b.arn is not null then 'ok' + else 'alarm' + end as status, + case + when b.arn is not null then i.title || ' is protected by backup plan.' + else i.title || ' is not protected by backup plan.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "i.")} + from + aws_ec2_instance as i + left join backup_protected_instance as b on i.arn = b.arn; + EOQ +} + +query "ec2_instance_iam_profile_attached" { + sql = <<-EOQ + select + arn as resource, + case + when iam_instance_profile_id is not null then 'ok' + else 'alarm' + end as status, + case + when iam_instance_profile_id is not null then title || ' IAM profile attached.' + else title || ' IAM profile not attached.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_instance; + EOQ +} + +query "ec2_instance_publicly_accessible_iam_profile_attached" { + sql = <<-EOQ + select + arn as resource, + case + when iam_instance_profile_id is not null then 'ok' + else 'alarm' + end as status, + case + when iam_instance_profile_id is not null then title || ' IAM profile attached.' + else title || ' IAM profile not attached.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_instance + where + public_ip_address is not null; + EOQ +} + +query "ec2_instance_user_data_no_secrets" { + sql = <<-EOQ + select + arn as resource, + case + when user_data like any (array ['%pass%', '%secret%','%token%','%key%']) + or user_data ~ '(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]' then 'alarm' + else 'ok' + end as status, + case + when user_data like any (array ['%pass%', '%secret%','%token%','%key%']) + or user_data ~ '(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]' then instance_id ||' potential secret found in user data.' + else instance_id || ' no secrets found in user data.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_instance; + EOQ +} + +query "ec2_transit_gateway_auto_cross_account_attachment_disabled" { + sql = <<-EOQ + select + transit_gateway_arn as resource, + case + when auto_accept_shared_attachments = 'enable' then 'alarm' + else 'ok' + end as status, + case + when auto_accept_shared_attachments = 'enable' then title || ' automatic shared account attachment enabled.' + else title || ' automatic shared account attachment disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_transit_gateway; + EOQ +} + +query "ec2_instance_no_launch_wizard_security_group" { + sql = <<-EOQ + with launch_wizard_sg_attached_instance as ( + select + distinct arn as arn + from + aws_ec2_instance, + jsonb_array_elements(security_groups) as sg + where + sg ->> 'GroupName' like 'launch-wizard%' + ) + select + i.arn as resource, + case + when sg.arn is null then 'ok' + else 'alarm' + end as status, + case + when sg.arn is null then i.title || ' not associated with launch-wizard security group.' + else i.title || ' associated with launch-wizard security group.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "i.")} + from + aws_ec2_instance as i + left join launch_wizard_sg_attached_instance as sg on i.arn = sg.arn; + EOQ +} + +query "ec2_instance_no_high_level_finding_in_inspector_scan" { + sql = <<-EOQ + with severity_list as ( + select + distinct title , + a ->> 'Value' as instance_id + from + aws_inspector_finding, + jsonb_array_elements(attributes) as a + where + severity = 'High' + and asset_type = 'ec2-instance' + and a ->> 'Key' = 'INSTANCE_ID' + group by + a ->> 'Value', + title + ), ec2_istance_list as ( + select + distinct instance_id + from + severity_list + ) + select + arn as resource, + case + when l.instance_id is null then 'ok' + else 'alarm' + end as status, + case + when l.instance_id is null then i.title || ' has no high level finding in inspector scans.' + else i.title || ' has ' || (select count(*) from severity_list where instance_id = i.instance_id) || ' high level findings in inspector scans.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "i.")} + from + aws_ec2_instance as i + left join ec2_istance_list as l on i.instance_id = l.instance_id; + EOQ +} + +# Non-Config rule query + +query "ec2_classic_lb_connection_draining_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when connection_draining_enabled then 'ok' + else 'alarm' + end as status, + case + when connection_draining_enabled then title || ' connection draining enabled.' + else title || ' connection draining disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_classic_load_balancer; + EOQ +} + +query "ec2_instance_no_amazon_key_pair" { + sql = <<-EOQ + select + arn as resource, + case + when instance_state <> 'running' then 'skip' + when key_name is null then 'ok' + else 'alarm' + end as status, + case + when instance_state <> 'running' then title || ' is in ' || instance_state || ' state.' + when key_name is null then title || ' not launched using amazon key pairs.' + else title || ' launched using amazon key pairs.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_instance; + EOQ +} + +query "ec2_instance_not_use_multiple_enis" { + sql = <<-EOQ + select + arn as resource, + case + when jsonb_array_length(network_interfaces) = 1 then 'ok' + else 'alarm' + end status, + title || ' has ' || jsonb_array_length(network_interfaces) || ' ENI(s) attached.' + as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_instance; + EOQ +} + +query "ec2_instance_termination_protection_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when disable_api_termination then 'ok' + else 'alarm' + end status, + case + when disable_api_termination then instance_id || ' termination protection enabled.' + else instance_id || ' termination protection disabled.' + end reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_instance; + EOQ +} + +query "ec2_instance_virtualization_type_no_paravirtual" { + sql = <<-EOQ + select + arn as resource, + case + when virtualization_type = 'paravirtual' then 'alarm' + else 'ok' + end as status, + title || ' virtualization type is ' || virtualization_type || '.' as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_instance; + EOQ +} diff --git a/conformance_pack/ecr.sp b/conformance_pack/ecr.sp index 672363f5..1393c7ec 100644 --- a/conformance_pack/ecr.sp +++ b/conformance_pack/ecr.sp @@ -23,3 +23,99 @@ control "ecr_repository_prohibit_public_access" { other_checks = "true" }) } + +query "ecr_repository_image_scan_on_push_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when image_scanning_configuration ->> 'ScanOnPush' = 'true' then 'ok' + else 'alarm' + end as status, + case + when image_scanning_configuration ->> 'ScanOnPush' = 'true' then title || ' scan on push enabled.' + else title || ' scan on push disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ecr_repository; + EOQ +} + +query "ecr_repository_prohibit_public_access" { + sql = <<-EOQ + with open_access_ecr_repo as( + select + distinct arn + from + aws_ecr_repository, + jsonb_array_elements(policy_std -> 'Statement') as s, + jsonb_array_elements_text(s -> 'Principal' -> 'AWS') as p, + string_to_array(p, ':') as pa, + jsonb_array_elements_text(s -> 'Action') as a + where + s ->> 'Effect' = 'Allow' + and ( + p = '*' + ) + ) + select + r.arn as resource, + case + when o.arn is not null then 'alarm' + else 'ok' + end as status, + case + when o.arn is not null then r.title || ' allows public access.' + else r.title || ' does not allow public access.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "r.")} + from + aws_ecr_repository as r + left join open_access_ecr_repo as o on r.arn = o.arn + group by + resource, status, reason, r.region, r.account_id, r.tags, r._ctx; + EOQ +} + +# Non-Config rule query + +query "ecr_repository_lifecycle_policy_configured" { + sql = <<-EOQ + select + arn as resource, + case + when lifecycle_policy -> 'rules' is not null then 'ok' + else 'alarm' + end as status, + case + when lifecycle_policy -> 'rules' is not null then title || ' lifecycle policy configured.' + else title || ' lifecycle policy not configured.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ecr_repository; + EOQ +} + +query "ecr_repository_tag_immutability_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when image_tag_mutability = 'IMMUTABLE' then 'ok' + else 'alarm' + end as status, + case + when image_tag_mutability = 'IMMUTABLE' then title || ' tag immutability enabled.' + else title || ' tag immutability disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ecr_repository; + EOQ +} diff --git a/conformance_pack/ecs.sp b/conformance_pack/ecs.sp index bc4a35f4..2031bf86 100644 --- a/conformance_pack/ecs.sp +++ b/conformance_pack/ecs.sp @@ -70,3 +70,382 @@ control "ecs_task_definition_logging_enabled" { other_checks = "true" }) } + +query "ecs_task_definition_user_for_host_mode_check" { + sql = <<-EOQ + with host_network_task_definition as ( + select + distinct task_definition_arn as arn + from + aws_ecs_task_definition, + jsonb_array_elements(container_definitions) as c + where + network_mode = 'host' + and + (c ->> 'Privileged' is not null + and c ->> 'Privileged' <> 'false' + ) + and + ( c ->> 'User' is not null + and c ->> 'User' <> 'root' + ) + ) + select + a.task_definition_arn as resource, + case + when a.network_mode is null or a.network_mode <> 'host' then 'skip' + when b.arn is not null then 'ok' + else 'alarm' + end as status, + case + when a.network_mode is null or a.network_mode <> 'host' then a.title || ' not host network mode.' + when b.arn is not null then a.title || ' have secure host network mode.' + else a.title || ' not have secure host network mode.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ecs_task_definition as a + left join host_network_task_definition as b on a.task_definition_arn = b.arn; + EOQ +} + +query "ecs_task_definition_logging_enabled" { + sql = <<-EOQ + with task_definitions_logging_enabled as ( + select + distinct task_definition_arn as arn + from + aws_ecs_task_definition, + jsonb_array_elements(container_definitions) as c + where + c ->> 'LogConfiguration' is not null + ) + select + a.task_definition_arn as resource, + case + when b.arn is not null then 'ok' + else 'alarm' + end as status, + case + when b.arn is not null then a.title || ' logging enabled.' + else a.title || ' logging disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ecs_task_definition as a + left join task_definitions_logging_enabled as b on a.task_definition_arn = b.arn; + EOQ +} + +query "ecs_cluster_encryption_at_rest_enabled" { + sql = <<-EOQ + with unencrypted_volumes as ( + select + distinct cluster_arn + from + aws_ecs_container_instance as i, + aws_ec2_instance as e, + jsonb_array_elements(block_device_mappings) as b, + aws_ebs_volume as v + where + i.ec2_instance_id = e.instance_id + and b -> 'Ebs' ->> 'VolumeId' = v.volume_id + and not v.encrypted + ) + select + c.cluster_arn as resource, + case + when c.registered_container_instances_count = 0 then 'skip' + when v.cluster_arn is not null then 'alarm' + else 'ok' + end as status, + case + when c.registered_container_instances_count = 0 then title || ' has no container instance registered.' + when v.cluster_arn is not null then c.title || ' encryption at rest disabled.' + else c.title || ' encryption at rest enabled.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "c.")} + from + aws_ecs_cluster as c + left join unencrypted_volumes as v on v.cluster_arn = c.cluster_arn; + EOQ +} + +query "ecs_cluster_instance_in_vpc" { + sql = <<-EOQ + select + c.arn as resource, + case + when i.vpc_id is null then 'alarm' + else 'ok' + end as status, + case + when i.vpc_id is null then c.title || ' not in VPC.' + else c.title || ' in VPC.' + end as reason + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "c.")} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "c.")} + from + aws_ecs_container_instance as c + left join aws_ec2_instance as i on c.ec2_instance_id = i.instance_id; + EOQ +} + +query "ecs_cluster_no_registered_container_instance" { + sql = <<-EOQ + select + cluster_arn as resource, + case + when registered_container_instances_count = 0 then 'alarm' + else 'ok' + end as status, + case + when registered_container_instances_count = 0 then title || ' has no container instance registered.' + else title || ' has ' || registered_container_instances_count || ' container instance(s) registered.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ecs_cluster; + EOQ +} + +query "ecs_service_load_balancer_attached" { + sql = <<-EOQ + select + arn as resource, + case + when jsonb_array_length(load_balancers) = 0 then 'alarm' + else 'ok' + end as status, + case + when jsonb_array_length(load_balancers) = 0 then title || ' has no load balancer attached.' + else title || ' has ' || jsonb_array_length(load_balancers) || ' load balancer(s) attached.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ecs_service; + EOQ +} + +# Non-Config rule query + +query "ecs_cluster_container_insights_enabled" { + sql = <<-EOQ + select + cluster_arn as resource, + case + when s ->> 'Name' = 'containerInsights' and s ->> 'Value' = 'enabled' then 'ok' + else 'alarm' + end as status, + case + when s ->> 'Name' = 'containerInsights' and s ->> 'Value' = 'enabled' then title || ' Container Insights enabled.' + else title || ' Container Insights disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ecs_cluster as c, + jsonb_array_elements(settings) as s; + EOQ +} + +query "ecs_cluster_container_instance_agent_connected" { + sql = <<-EOQ + with unconnected_agent_instance as ( + select + distinct cluster_arn + from + aws_ecs_container_instance + where + agent_connected = false and status = 'ACTIVE' + ) + select + c.cluster_arn as resource, + case + when c.registered_container_instances_count = 0 then 'skip' + when i.cluster_arn is null then 'ok' + else 'alarm' + end as status, + case + when c.registered_container_instances_count = 0 then title || ' has no container instance registered.' + when i.cluster_arn is null then title || ' container instance has connected agent.' + else title || ' container instance is either draining or has unconnected agents.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ecs_cluster as c + left join unconnected_agent_instance as i on c.cluster_arn = i.cluster_arn; + EOQ +} + +query "ecs_service_fargate_using_latest_platform_version" { + sql = <<-EOQ + select + arn as resource, + case + when launch_type <> 'FARGATE' then 'skip' + when platform_version = 'LATEST' then 'ok' + else 'alarm' + end as status, + case + when launch_type <> 'FARGATE' then title || ' is ' || launch_type || ' service.' + when platform_version = 'LATEST' then title || ' running on the latest fargate platform version.' + else title || ' not running on the latest fargate platform version.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ecs_service; + EOQ +} + +query "ecs_service_not_publicly_accessible" { + sql = <<-EOQ + with service_awsvpc_mode_task_definition as ( + select + a.service_name as service_name, + b.task_definition_arn as task_definition + from + aws_ecs_service as a + left join aws_ecs_task_definition as b on a.task_definition = b.task_definition_arn + where + b.network_mode = 'awsvpc' + ) + select + a.arn as resource, + case + when b.service_name is null then 'skip' + when network_configuration -> 'AwsvpcConfiguration' ->> 'AssignPublicIp' = 'DISABLED' then 'ok' + else 'alarm' + end as status, + case + when b.service_name is null then a.title || ' task definition not host network mode.' + when network_configuration -> 'AwsvpcConfiguration' ->> 'AssignPublicIp' = 'DISABLED' then a.title || ' not publicly accessible.' + else a.title || ' publicly accessible.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ecs_service as a + left join service_awsvpc_mode_task_definition as b on a.service_name = b.service_name; + EOQ +} + +query "ecs_task_definition_container_environment_no_secret" { + sql = <<-EOQ + with definitions_with_secret_environment_variable as ( + select + distinct task_definition_arn as arn + from + aws_ecs_task_definition, + jsonb_array_elements(container_definitions) as c, + jsonb_array_elements( c -> 'Environment') as e, + jsonb_array_elements( + case jsonb_typeof(c -> 'Secrets') + when 'array' then (c -> 'Secrets') + else null end + ) as s + where + e ->> 'Name' like any (array ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY','ECS_ENGINE_AUTH_DATA']) + or s ->> 'Name' like any (array ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY','ECS_ENGINE_AUTH_DATA']) + ) + select + d.task_definition_arn as resource, + case + when e.arn is null then 'ok' + else 'alarm' + end as status, + case + when e.arn is null then d.title || ' container environment variables does not have secrets.' + else d.title || ' container environment variables have secrets.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ecs_task_definition as d + left join definitions_with_secret_environment_variable as e on d.task_definition_arn = e.arn; + EOQ +} + +query "ecs_task_definition_container_non_privileged" { + sql = <<-EOQ + with privileged_container_definition as ( + select + distinct task_definition_arn as arn + from + aws_ecs_task_definition, + jsonb_array_elements(container_definitions) as c + where + c ->> 'Privileged' = 'true' + ) + select + d.task_definition_arn as resource, + case + when c.arn is null then 'ok' + else 'alarm' + end as status, + case + when c.arn is null then d.title || ' does not have elevated privileges.' + else d.title || ' has elevated privileges.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ecs_task_definition as d + left join privileged_container_definition as c on d.task_definition_arn = c.arn; + EOQ +} + +query "ecs_task_definition_container_readonly_root_filesystem" { + sql = <<-EOQ + with privileged_container_definition as ( + select + distinct task_definition_arn as arn + from + aws_ecs_task_definition, + jsonb_array_elements(container_definitions) as c + where + c ->> 'ReadonlyRootFilesystem' = 'true' + ) + select + d.task_definition_arn as resource, + case + when c.arn is not null then 'ok' + else 'alarm' + end as status, + case + when c.arn is not null then d.title || ' containers limited to read-only access to root filesystems.' + else d.title || ' containers not limited to read-only access to root filesystems.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ecs_task_definition as d + left join privileged_container_definition as c on d.task_definition_arn = c.arn; + EOQ +} + +query "ecs_task_definition_no_host_pid_mode" { + sql = <<-EOQ + select + task_definition_arn as resource, + case + when pid_mode = 'host' then 'alarm' + else 'ok' + end as status, + case + when pid_mode = 'host' then title || ' shares the host process namespace.' + else title || ' does not share the host process namespace.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ecs_task_definition; + EOQ +} diff --git a/conformance_pack/efs.sp b/conformance_pack/efs.sp index ac88e23b..dc71a171 100644 --- a/conformance_pack/efs.sp +++ b/conformance_pack/efs.sp @@ -80,3 +80,179 @@ control "efs_file_system_enforces_ssl" { other_checks = "true" }) } + +query "efs_file_system_encrypt_data_at_rest" { + sql = <<-EOQ + select + arn as resource, + case + when encrypted then 'ok' + else 'alarm' + end as status, + case + when encrypted then title || ' encrypted at rest.' + else title || ' not encrypted at rest.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_efs_file_system; + EOQ +} + +query "efs_file_system_automatic_backups_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when automatic_backups = 'enabled' then 'ok' + else 'alarm' + end as status, + case + when automatic_backups = 'enabled' then title || ' automatic backups enabled.' + else title || ' automatic backups not enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_efs_file_system; + EOQ +} + +query "efs_file_system_protected_by_backup_plan" { + sql = <<-EOQ + with backup_protected_file_system as ( + select + resource_arn as arn + from + aws_backup_protected_resource as b + where + resource_type = 'EFS' + ) + select + f.arn as resource, + case + when b.arn is not null then 'ok' + else 'alarm' + end as status, + case + when b.arn is not null then f.title || ' is protected by backup plan.' + else f.title || ' is not protected by backup plan.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "f.")} + from + aws_efs_file_system as f + left join backup_protected_file_system as b on f.arn = b.arn; + EOQ +} + +query "efs_file_system_encrypted_with_cmk" { + sql = <<-EOQ + with encrypted_fs as ( + select + fs.arn as arn, + key_manager + from + aws_efs_file_system as fs + left join aws_kms_key as k on fs.kms_key_id = k.arn + where + enabled + ) + select + f.arn as resource, + case + when not encrypted then 'alarm' + when encrypted and e.key_manager = 'CUSTOMER' then 'ok' + else 'alarm' + end as status, + case + when not encrypted then title || ' not encrypted.' + when encrypted and e.key_manager = 'CUSTOMER' then title || ' encrypted with CMK.' + else title || ' not encrypted with CMK.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_efs_file_system as f + left join encrypted_fs as e on f.arn = e.arn; + EOQ +} + +query "efs_file_system_enforces_ssl" { + sql = <<-EOQ + with ssl_ok as ( + select + distinct name, + arn, + 'ok' as status + from + aws_efs_file_system, + jsonb_array_elements(policy_std -> 'Statement') as s, + jsonb_array_elements_text(s -> 'Principal' -> 'AWS') as p, + jsonb_array_elements_text(s -> 'Action') as a, + jsonb_array_elements_text( + s -> 'Condition' -> 'Bool' -> 'aws:securetransport' + ) as ssl + where + p = '*' + and s ->> 'Effect' = 'Deny' + and ssl :: bool = false + ) + select + f.arn as resource, + case + when ok.status = 'ok' then 'ok' + else 'alarm' + end status, + case + when ok.status = 'ok' then f.title || ' policy enforces HTTPS.' + else f.title || ' policy does not enforce HTTPS.' + end reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "f.")} + from + aws_efs_file_system as f + left join ssl_ok as ok on ok.name = f.name; + EOQ +} + +# Non-Config rule query + +query "efs_access_point_enforce_root_directory" { + sql = <<-EOQ + select + access_point_arn as resource, + case + when root_directory ->> 'Path'= '/' then 'alarm' + else 'ok' + end as status, + case + when root_directory ->> 'Path'= '/' then title || ' not configured to enforce a root directory.' + else title || ' configured to enforce a root directory.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_efs_access_point; + EOQ +} + +query "efs_access_point_enforce_user_identity" { + sql = <<-EOQ + select + access_point_arn as resource, + case + when posix_user is null then 'alarm' + else 'ok' + end as status, + case + when posix_user is null then title || ' does not enforce a user identity.' + else title || ' enforces a user identity.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_efs_access_point; + EOQ +} diff --git a/conformance_pack/eks.sp b/conformance_pack/eks.sp index 32fccca6..b87db856 100644 --- a/conformance_pack/eks.sp +++ b/conformance_pack/eks.sp @@ -46,3 +46,138 @@ control "eks_cluster_no_default_vpc" { other_checks = "true" }) } + +query "eks_cluster_secrets_encrypted" { + sql = <<-EOQ + with eks_secrets_encrypted as ( + select + distinct arn as arn + from + aws_eks_cluster, + jsonb_array_elements(encryption_config) as e + where + e -> 'Resources' @> '["secrets"]' + ) + select + a.arn as resource, + case + when encryption_config is null then 'alarm' + when b.arn is not null then 'ok' + else 'alarm' + end as status, + case + when encryption_config is null then a.title || ' encryption not enabled.' + when b.arn is not null then a.title || ' encrypted with EKS secrets.' + else a.title || ' not encrypted with EKS secrets.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_eks_cluster as a + left join eks_secrets_encrypted as b on a.arn = b.arn; + EOQ +} + +query "eks_cluster_endpoint_restrict_public_access" { + sql = <<-EOQ + select + arn as resource, + case + when resources_vpc_config ->> 'EndpointPublicAccess' = 'true' then 'alarm' + else 'ok' + end as status, + case + when resources_vpc_config ->> 'EndpointPublicAccess' = 'true' then title || ' endpoint publicly accessible.' + else title || ' endpoint not publicly accessible.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_eks_cluster; + EOQ +} + +query "eks_cluster_control_plane_audit_logging_enabled" { + sql = <<-EOQ + with control_panel_audit_logging as ( + select + distinct arn, + log -> 'Types' as log_type + from + aws_eks_cluster, + jsonb_array_elements(logging -> 'ClusterLogging') as log + where + log ->> 'Enabled' = 'true' + and (log -> 'Types') @> '["api", "audit", "authenticator", "controllerManager", "scheduler"]' + ) + select + c.arn as resource, + case + when l.arn is not null then 'ok' + else 'alarm' + end as status, + case + when l.arn is not null then c.title || ' control plane audit logging enabled for all log types.' + else + case when logging -> 'ClusterLogging' @> '[{"Enabled": true}]' then c.title || ' control plane audit logging not enabled for all log types.' + else c.title || ' control plane audit logging not enabled.' + end + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "c.")} + from + aws_eks_cluster as c + left join control_panel_audit_logging as l on l.arn = c.arn; + EOQ +} + +query "eks_cluster_no_default_vpc" { + sql = <<-EOQ + with default_vpc_cluster as ( + select + distinct c.arn + from + aws_eks_cluster as c + left join aws_vpc as v on v.vpc_id = c.resources_vpc_config ->> 'VpcId' + where + v.is_default + ) + select + c.arn as resource, + case + when v.arn is not null then 'alarm' + else 'ok' + end as status, + case + when v.arn is not null then title || ' uses default VPC.' + else title || ' does not use default VPC.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "c.")} + from + aws_eks_cluster as c + left join default_vpc_cluster as v on v.arn = c.arn; + EOQ +} + +# Non-Config rule query + +query "eks_cluster_with_latest_kubernetes_version" { + sql = <<-EOQ + select + arn as resource, + case + -- eks:oldestVersionSupported (Current oldest supported version is 1.19) + when (version)::decimal >= 1.19 then 'ok' + else 'alarm' + end as status, + case + when (version)::decimal >= 1.19 then title || ' runs on a supported kubernetes version.' + else title || ' does not run on a supported kubernetes version.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_eks_cluster; + EOQ +} diff --git a/conformance_pack/elasticache.sp b/conformance_pack/elasticache.sp index 0b8f96e5..58c79869 100644 --- a/conformance_pack/elasticache.sp +++ b/conformance_pack/elasticache.sp @@ -25,3 +25,22 @@ control "elasticache_redis_cluster_automatic_backup_retention_15_days" { soc_2 = "true" }) } + +query "elasticache_redis_cluster_automatic_backup_retention_15_days" { + sql = <<-EOQ + select + arn as resource, + case + when snapshot_retention_limit < 15 then 'alarm' + else 'ok' + end as status, + case + when snapshot_retention_limit = 0 then title || ' automatic backups not enabled.' + when snapshot_retention_limit < 15 then title || ' automatic backup retention period is less than 15 days.' + else title || ' automatic backup retention period is more than 15 days.' + end as reason + ${local.common_dimensions_sql} + from + aws_elasticache_replication_group; + EOQ +} \ No newline at end of file diff --git a/conformance_pack/elasticbeanstalk.sp b/conformance_pack/elasticbeanstalk.sp index 6a597a06..b6b02426 100644 --- a/conformance_pack/elasticbeanstalk.sp +++ b/conformance_pack/elasticbeanstalk.sp @@ -15,3 +15,22 @@ control "elastic_beanstalk_enhanced_health_reporting_enabled" { nist_800_53_rev_5 = "true" }) } + +query "elastic_beanstalk_enhanced_health_reporting_enabled" { + sql = <<-EOQ + select + application_name as resource, + case + when health_status is not null and health is not null then 'ok' + else 'alarm' + end as status, + case + when health_status is not null and health is not null then application_name || ' enhanced health check enabled.' + else application_name || ' enhanced health check disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_elastic_beanstalk_environment; + EOQ +} diff --git a/conformance_pack/elb.sp b/conformance_pack/elb.sp index 6b4be60a..ea17adf5 100644 --- a/conformance_pack/elb.sp +++ b/conformance_pack/elb.sp @@ -253,4 +253,687 @@ control "elb_tls_listener_protocol_version" { tags = merge(local.conformance_pack_elb_common_tags, { other_checks = "true" }) -} \ No newline at end of file +} + +query "elb_application_classic_lb_logging_enabled" { + sql = <<-EOQ + ( + select + arn as resource, + case + when load_balancer_attributes @> '[{"Key": "access_logs.s3.enabled", "Value": "true"}]' then 'ok' + else 'alarm' + end as status, + case + when load_balancer_attributes @> '[{"Key": "access_logs.s3.enabled", "Value": "true"}]' then title || ' logging enabled.' + else title || ' logging disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_application_load_balancer + ) + union + ( + select + 'arn:' || partition || ':elasticloadbalancing:' || region || ':' || account_id || ':loadbalancer/' || title as resource, + case + when access_log_enabled = 'true' then 'ok' + else 'alarm' + end as status, + case + when access_log_enabled = 'true' then title || ' logging enabled.' + else title || ' logging disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_classic_load_balancer + ); + EOQ +} + +query "elb_application_lb_deletion_protection_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when load_balancer_attributes @> '[{"Key": "deletion_protection.enabled", "Value": "true"}]' then 'ok' + else 'alarm' + end as status, + case + when load_balancer_attributes @> '[{"Key": "deletion_protection.enabled", "Value": "true"}]' then title || ' deletion protection enabled.' + else title || ' deletion protection disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_application_load_balancer; + EOQ +} + +query "elb_application_lb_redirect_http_request_to_https" { + sql = <<-EOQ + with detailed_listeners as ( + select + arn, + load_balancer_arn, + protocol + from + aws_ec2_load_balancer_listener, + jsonb_array_elements(default_actions) as ac + where + split_part(arn,'/',2) = 'app' + and protocol = 'HTTP' + and ac ->> 'Type' = 'redirect' + and ac -> 'RedirectConfig' ->> 'Protocol' = 'HTTPS' + ) + select + a.arn as resource, + case + when b.load_balancer_arn is null then 'alarm' + else 'ok' + end as status, + case + when b.load_balancer_arn is not null then a.title || ' associated with HTTP redirection.' + else a.title || ' not associated with HTTP redirection.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "a.")} + from + aws_ec2_application_load_balancer a + left join detailed_listeners b on a.arn = b.load_balancer_arn; + EOQ +} + +query "elb_application_lb_waf_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when load_balancer_attributes @> '[{"Key":"waf.fail_open.enabled","Value":"true"}]' then 'ok' + else 'alarm' + end as status, + case + when load_balancer_attributes @> '[{"Key":"waf.fail_open.enabled","Value":"true"}]' then title || ' WAF enabled.' + else title || ' WAF disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_application_load_balancer; + EOQ +} + +query "elb_classic_lb_use_ssl_certificate" { + sql = <<-EOQ + with detailed_classic_listeners as ( + select + name + from + aws_ec2_classic_load_balancer, + jsonb_array_elements(listener_descriptions) as listener_description + where + listener_description -> 'Listener' ->> 'Protocol' in ('HTTPS', 'SSL', 'TLS') + and listener_description -> 'Listener' ->> 'SSLCertificateId' like 'arn:aws:acm%' + ) + select + 'arn:' || a.partition || ':elasticloadbalancing:' || a.region || ':' || a.account_id || ':loadbalancer/' || a.name as resource, + case + when a.listener_descriptions is null then 'skip' + when b.name is not null then 'alarm' + else 'ok' + end as status, + case + when a.listener_descriptions is null then a.title || ' has no listener.' + when b.name is not null then a.title || ' does not use certificates provided by ACM.' + else a.title || ' uses certificates provided by ACM.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_classic_load_balancer as a + left join detailed_classic_listeners as b on a.name = b.name; + EOQ +} + +query "elb_application_lb_drop_http_headers" { + sql = <<-EOQ + select + arn as resource, + case + when load_balancer_attributes @> '[{"Key": "routing.http.drop_invalid_header_fields.enabled", "Value": "true"}]' then 'ok' + else 'alarm' + end as status, + case + when load_balancer_attributes @> '[{"Key": "routing.http.drop_invalid_header_fields.enabled", "Value": "true"}]' then title || ' configured to drop http headers.' + else title || ' not configured to drop http headers.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_application_load_balancer; + EOQ +} + +query "elb_classic_lb_use_tls_https_listeners" { + sql = <<-EOQ + select + 'arn:' || partition || ':elasticloadbalancing:' || region || ':' || account_id || ':loadbalancer/' || title as resource, + case + when listener_description -> 'Listener' ->> 'Protocol' in ('HTTPS', 'SSL', 'TLS') then 'ok' + else 'alarm' + end as status, + case + when listener_description -> 'Listener' ->> 'Protocol' = 'HTTPS' then title || ' configured with HTTPS protocol.' + when listener_description -> 'Listener' ->> 'Protocol' = 'SSL' then title || ' configured with TLS protocol.' + else title || ' configured with ' || (listener_description -> 'Listener' ->> 'Protocol') || ' protocol.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_classic_load_balancer, + jsonb_array_elements(listener_descriptions) as listener_description; + EOQ +} + +query "elb_classic_lb_cross_zone_load_balancing_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when cross_zone_load_balancing_enabled then 'ok' + else 'alarm' + end as status, + case + when cross_zone_load_balancing_enabled then title || ' cross-zone load balancing enabled.' + else title || ' cross-zone load balancing disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_classic_load_balancer; + EOQ +} + +query "elb_application_network_lb_use_ssl_certificate" { + sql = <<-EOQ + with listeners_without_certificate as ( + select + load_balancer_arn, + count(*) as count + from + aws_ec2_load_balancer_listener + where arn not in + ( select arn from aws_ec2_load_balancer_listener, jsonb_array_elements(certificates) as c + where c ->> 'CertificateArn' like 'arn:aws:acm%' ) + group by load_balancer_arn + ), + all_application_network_load_balacer as ( + select + arn, + account_id, + region, + title, + _ctx + from + aws_ec2_application_load_balancer + union + select + arn, + account_id, + region, + title, + _ctx + from + aws_ec2_network_load_balancer + ) + select + a.arn as resource, + case + when b.load_balancer_arn is null then 'ok' + else 'alarm' + end as status, + case + when b.load_balancer_arn is null then a.title || ' uses certificates provided by ACM.' + else a.title || ' has ' || b.count || ' listeners which do not use certificates provided by ACM.' + end as reason + + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "a.")} + from + all_application_network_load_balacer as a + left join listeners_without_certificate as b on a.arn = b.load_balancer_arn; + EOQ +} + +query "elb_listener_use_secure_ssl_cipher" { + sql = <<-EOQ + select + load_balancer_arn as resource, + case + when ssl_policy like any(array['ELBSecurityPolicy-TLS-1-2-2017-01', 'ELBSecurityPolicy-TLS-1-1-2017-01']) then 'ok' + else 'alarm' + end as status, + case + when ssl_policy like any (array['ELBSecurityPolicy-TLS-1-2-2017-01', 'ELBSecurityPolicy-TLS-1-1-2017-01']) then title || ' uses secure SSL cipher.' + else title || ' uses insecure SSL cipher.' + end as reason + ${local.common_dimensions_sql} + from + aws_ec2_load_balancer_listener; + EOQ +} + +query "elb_application_classic_network_lb_prohibit_public_access" { + sql = <<-EOQ + with all_lb_details as ( + select + arn, + scheme, + title, + region, + account_id, + tags, + _ctx + from + aws_ec2_application_load_balancer + union + select + arn, + scheme, + title, + region, + account_id, + tags, + _ctx + from + aws_ec2_network_load_balancer + union + select + arn, + scheme, + title, + region, + account_id, + tags, + _ctx + from + aws_ec2_classic_load_balancer + ) + select + arn as resource, + case + when scheme = 'internet-facing' then 'alarm' + else 'ok' + end as status, + case + when scheme = 'internet-facing' then title || ' publicly accessible.' + else title|| ' not publicly accessible.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + all_lb_details; + EOQ +} + +query "elb_application_lb_with_outbound_rule" { + sql = <<-EOQ + with sg_with_outbound as ( + select + arn, + sg + from + aws_ec2_application_load_balancer, + jsonb_array_elements_text(security_groups) as sg + left join aws_vpc_security_group_rule as sgr on sg = sgr.group_id + where + sgr.type = 'egress' + group by + sg, arn + ), application_lb_without_outbound as ( + select + distinct arn + from + aws_ec2_application_load_balancer, + jsonb_array_elements_text(security_groups) as s + where + s not in ( select sg from sg_with_outbound) + ) + select + distinct a.arn as resource, + case + when a.security_groups is null then 'alarm' + when o.arn is not null then 'alarm' + else 'ok' + end as status, + case + when a.security_groups is null then a.title || ' does not have security group attached.' + when o.arn is not null then a.title || ' all attached security groups does not have outbound rule(s).' + else a.title || ' all attached security groups have outbound rule(s).' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "a.")} + from + aws_ec2_application_load_balancer as a + left join application_lb_without_outbound as o on a.arn = o.arn; + EOQ +} + +query "elb_classic_lb_with_outbound_rule" { + sql = <<-EOQ + with sg_with_outbound as ( + select + arn, + sg + from + aws_ec2_classic_load_balancer, + jsonb_array_elements_text(security_groups) as sg + left join aws_vpc_security_group_rule as sgr on sg = sgr.group_id + where + sgr.type = 'egress' + group by + sg, arn + ), classic_lb_without_outbound as ( + select + distinct arn + from + aws_ec2_classic_load_balancer, + jsonb_array_elements_text(security_groups) as s + where + s not in ( select sg from sg_with_outbound) + ) + select + distinct c.arn as resource, + case + when c.security_groups is null then 'alarm' + when o.arn is not null then 'alarm' + else 'ok' + end as status, + case + when c.security_groups is null then c.title || ' does not have security group attached.' + when o.arn is not null then c.title || ' all attached security groups does not have outbound rule(s).' + else c.title || ' all attached security groups have outbound rule(s).' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "c.")} + from + aws_ec2_classic_load_balancer as c + left join classic_lb_without_outbound as o on c.arn = o.arn; + EOQ +} + +query "elb_classic_lb_with_outbound_rule" { + sql = <<-EOQ + select + load_balancer_arn as resource, + case + when protocol <> 'HTTPS' then 'skip' + when protocol = 'HTTPS' and ssl_policy like any(array['Protocol-SSLv3', 'Protocol-TLSv1']) then 'alarm' + else 'ok' + end as status, + case + when protocol <> 'HTTPS' then title || ' uses protocol ' || protocol || '.' + when ssl_policy like any (array['Protocol-SSLv3', 'Protocol-TLSv1']) then title || ' uses insecure SSL or TLS cipher.' + else title || ' uses secure SSL or TLS cipher.' + end as reason + ${local.common_dimensions_sql} + from + aws_ec2_load_balancer_listener; + EOQ +} + +query "elb_application_lb_listener_certificate_expire_7_days" { + sql = <<-EOQ + select + load_balancer_arn as resource, + case + when date(not_after) - date(current_date) >= 7 then 'ok' + else 'alarm' + end as status, + l.title || ' certificate set to expire in ' || extract(day from not_after - current_date) || ' days.' as reason + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "l.")} + from + aws_ec2_load_balancer_listener as l, + jsonb_array_elements(certificates) as c + left join aws_acm_certificate as a on c ->> 'CertificateArn' = a.certificate_arn; + EOQ +} + +query "elb_application_lb_listener_certificate_expire_30_days" { + sql = <<-EOQ + select + load_balancer_arn as resource, + case + when date(not_after) - date(current_date) >= 30 then 'ok' + else 'alarm' + end as status, + l.title || ' certificate set to expire in ' || extract(day from not_after - current_date) || ' days.' as reason + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "l.")} + from + aws_ec2_load_balancer_listener as l, + jsonb_array_elements(certificates) as c + left join aws_acm_certificate as a on c ->> 'CertificateArn' = a.certificate_arn; + EOQ +} + +query "elb_application_network_lb_use_listeners" { + sql = <<-EOQ + with load_balancers as ( + select + n.arn, + n.title, + n.region, + n.account_id, + tags, + _ctx + from + aws_ec2_network_load_balancer as n + union + select + a.arn, + a.title, + a.region, + a.account_id, + tags, + _ctx + from + aws_ec2_application_load_balancer as a + ) + select + distinct lb.arn as resource, + case + when l.load_balancer_arn is not null then 'ok' + else 'alarm' + end as status, + case + when l.load_balancer_arn is not null then lb.title || ' uses listener.' + else lb.title || ' does not uses listener.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "lb.")} + from + load_balancers as lb + left join aws_ec2_load_balancer_listener as l on lb.arn = l.load_balancer_arn; + EOQ +} + +query "elb_tls_listener_protocol_version" { + sql = <<-EOQ + select + load_balancer_arn as resource, + case + when protocol <> 'HTTPS' then 'skip' + when protocol = 'HTTPS' and ssl_policy like any(array['Protocol-SSLv3', 'Protocol-TLSv1']) then 'alarm' + else 'ok' + end as status, + case + when protocol <> 'HTTPS' then title || ' uses protocol ' || protocol || '.' + when ssl_policy like any (array['Protocol-SSLv3', 'Protocol-TLSv1']) then title || ' uses insecure SSL or TLS cipher.' + else title || ' uses secure SSL or TLS cipher.' + end as reason + ${local.common_dimensions_sql} + from + aws_ec2_load_balancer_listener; + EOQ +} + +# Non-Config rule query + +query "elb_application_gateway_network_lb_multiple_az_configured" { + sql = <<-EOQ + select + arn as resource, + case + when jsonb_array_length(availability_zones) < 2 then 'alarm' + else 'ok' + end as status, + title || ' has ' || jsonb_array_length(availability_zones) || ' availability zone(s).' as reason + + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_application_load_balancer + union + select + arn as resource, + case + when jsonb_array_length(availability_zones) < 2 then 'alarm' + else 'ok' + end as status, + title || ' has ' || jsonb_array_length(availability_zones) || ' availability zone(s).' as reason + + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_network_load_balancer + union + select + arn as resource, + case + when jsonb_array_length(availability_zones) < 2 then 'alarm' + else 'ok' + end as status, + title || ' has ' || jsonb_array_length(availability_zones) || ' availability zone(s).' as reason + + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_gateway_load_balancer; + EOQ +} + +query "elb_application_lb_desync_mitigation_mode" { + sql = <<-EOQ + with app_lb_desync_mitigation_mode as ( + select + arn, + l ->> 'Key', + l ->> 'Value' as v + from + aws_ec2_application_load_balancer, + jsonb_array_elements(load_balancer_attributes) as l + where + l ->> 'Key' = 'routing.http.desync_mitigation_mode' + ) + select + a.arn as resource, + case + when m.v = any(array['defensive', 'strictest']) then 'ok' + else 'alarm' + end as status, + title || ' has ' || m.v || ' desync mitigation mode.' as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_application_load_balancer as a + left join app_lb_desync_mitigation_mode as m on a.arn = m.arn; + EOQ +} + +query "elb_classic_lb_desync_mitigation_mode" { + sql = <<-EOQ + with app_lb_desync_mitigation_mode as ( + select + arn, + a ->> 'Key', + a ->> 'Value' as v + from + aws_ec2_classic_load_balancer, + jsonb_array_elements(additional_attributes) as a + where + a ->> 'Key' = 'elb.http.desyncmitigationmode' + ) + select + c.arn as resource, + case + when m.v = any(array['defensive', 'strictest']) then 'ok' + else 'alarm' + end as status, + title || ' has ' || m.v || ' desync mitigation mode.' as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_classic_load_balancer as c + left join app_lb_desync_mitigation_mode as m on c.arn = m.arn; + EOQ +} + +query "elb_classic_lb_multiple_az_configured" { + sql = <<-EOQ + select + arn as resource, + case + when jsonb_array_length(availability_zones) < 2 then 'alarm' + else 'ok' + end as status, + title || ' has ' || jsonb_array_length(availability_zones) || ' availability zone(s).' as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_ec2_classic_load_balancer; + EOQ +} + +query "elb_network_lb_tls_listener_security_policy_configured" { + sql = <<-EOQ + with tls_listeners as ( + select + distinct load_balancer_arn + from + aws_ec2_load_balancer_listener + where + protocol = 'TLS' + and ssl_policy not in ('ELBSecurityPolicy-2016-08', 'ELBSecurityPolicy-FS-2018-0', 'ELBSecurityPolicy-TLS13-1-2-Ext1-2021-06', 'ELBSecurityPolicy-TLS13-1-2-2021-06') + group by + load_balancer_arn + ), nwl_without_tls_listener as ( + select + load_balancer_arn, + count(*) + from + aws_ec2_load_balancer_listener + where + protocol = 'TLS' + group by + load_balancer_arn + ) + select + lb.arn as resource, + case + when l.load_balancer_arn is not null and lb.arn in (select load_balancer_arn from tls_listeners) then 'alarm' + when l.load_balancer_arn is not null then 'ok' + else 'info' + end as status, + case + when l.load_balancer_arn is not null and lb.arn in (select load_balancer_arn from tls_listeners) then lb.title || ' TLS listener security policy not updated.' + when l.load_balancer_arn is not null then lb.title || ' TLS listener security policy updated.' + else lb.title || ' does not use TLS listener.' + end as reason + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "lb.")} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "lb.")} + from + aws_ec2_network_load_balancer as lb + left join nwl_without_tls_listener as l on l.load_balancer_arn = lb.arn; + EOQ +} diff --git a/conformance_pack/emr.sp b/conformance_pack/emr.sp index 628348e8..1403db5a 100644 --- a/conformance_pack/emr.sp +++ b/conformance_pack/emr.sp @@ -50,3 +50,62 @@ control "emr_cluster_master_nodes_no_public_ip" { rbi_cyber_security = "true" }) } + +query "emr_cluster_kerberos_enabled" { + sql = <<-EOQ + select + cluster_arn as resource, + case + when kerberos_attributes is null then 'alarm' + else 'ok' + end as status, + case + when kerberos_attributes is null then title || ' Kerberos not enabled.' + else title || ' Kerberos enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_emr_cluster; + EOQ +} + +query "emr_cluster_master_nodes_no_public_ip" { + sql = <<-EOQ + select + c.cluster_arn as resource, + case + when c.status ->> 'State' not in ('RUNNING', 'WAITING') then 'skip' + when s.map_public_ip_on_launch then 'alarm' + else 'ok' + end as status, + case + when c.status ->> 'State' not in ('RUNNING', 'WAITING') then c.title || ' is in ' || (c.status ->> 'State') || ' state.' + when s.map_public_ip_on_launch then c.title || ' master nodes assigned with public IP.' + else c.title || ' master nodes not assigned with public IP.' + end as reason + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "c.")} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "c.")} + from + aws_emr_cluster as c + left join aws_vpc_subnet as s on c.ec2_instance_attributes ->> 'Ec2SubnetId' = s.subnet_id; + EOQ +} + +query "emr_account_public_access_blocked" { + sql = <<-EOQ + select + 'arn:' || partition || '::' || region || ':' || account_id as resource, + case + when block_public_security_group_rules then 'ok' + else 'alarm' + end as status, + case + when block_public_security_group_rules then region || ' EMR block public access enabled.' + else region || ' EMR block public access disabled.' + end as reason + ${local.common_dimensions_sql} + from + aws_emr_block_public_access_configuration; + EOQ +} diff --git a/conformance_pack/es.sp b/conformance_pack/es.sp index 38853982..d4c7b951 100644 --- a/conformance_pack/es.sp +++ b/conformance_pack/es.sp @@ -102,3 +102,253 @@ control "es_domain_internal_user_database_enabled" { other_checks = "true" }) } + +query "es_domain_encryption_at_rest_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when encryption_at_rest_options ->> 'Enabled' = 'false' then 'alarm' + else 'ok' + end status, + case + when encryption_at_rest_options ->> 'Enabled' = 'false' then title || ' encryption at rest not enabled.' + else title || ' encryption at rest enabled.' + end reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_elasticsearch_domain; + EOQ +} + +query "es_domain_in_vpc" { + sql = <<-EOQ + select + arn as resource, + case + when vpc_options ->> 'VPCId' is null then 'alarm' + else 'ok' + end status, + case + when vpc_options ->> 'VPCId' is null then title || ' not in VPC.' + else title || ' in VPC ' || (vpc_options ->> 'VPCId') || '.' + end reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_elasticsearch_domain; + EOQ +} + +query "es_domain_node_to_node_encryption_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when region = any(array['af-south-1', 'eu-south-1', 'cn-north-1', 'cn-northwest-1']) then 'skip' + when not enabled then 'alarm' + else 'ok' + end as status, + case + when region = any(array['af-south-1', 'eu-south-1', 'cn-north-1', 'cn-northwest-1']) then title || ' node-to-node encryption not supported in ' || region || '.' + when not enabled then title || ' node-to-node encryption disabled.' + else title || ' node-to-node encryption enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_elasticsearch_domain; + EOQ +} + +query "es_domain_logs_to_cloudwatch" { + sql = <<-EOQ + select + arn as resource, + case + when + ( log_publishing_options -> 'ES_APPLICATION_LOGS' -> 'Enabled' = 'true' + and log_publishing_options -> 'ES_APPLICATION_LOGS' -> 'CloudWatchLogsLogGroupArn' is not null + ) + and + ( log_publishing_options -> 'SEARCH_SLOW_LOGS' -> 'Enabled' = 'true' + and log_publishing_options -> 'SEARCH_SLOW_LOGS' -> 'CloudWatchLogsLogGroupArn' is not null + ) + and + ( log_publishing_options -> 'INDEX_SLOW_LOGS' -> 'Enabled' = 'true' + and log_publishing_options -> 'INDEX_SLOW_LOGS' -> 'CloudWatchLogsLogGroupArn' is not null + ) + then 'ok' + else 'alarm' + end as status, + case + when + ( log_publishing_options -> 'ES_APPLICATION_LOGS' -> 'Enabled' = 'true' + and log_publishing_options -> 'ES_APPLICATION_LOGS' -> 'CloudWatchLogsLogGroupArn' is not null + ) + and + ( log_publishing_options -> 'SEARCH_SLOW_LOGS' -> 'Enabled' = 'true' + and log_publishing_options -> 'SEARCH_SLOW_LOGS' -> 'CloudWatchLogsLogGroupArn' is not null + ) + and + ( log_publishing_options -> 'INDEX_SLOW_LOGS' -> 'Enabled' = 'true' + and log_publishing_options -> 'INDEX_SLOW_LOGS' -> 'CloudWatchLogsLogGroupArn' is not null + ) then title || ' logging enabled for search , index and error.' + else title || ' logging not enabled for all search, index and error.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_elasticsearch_domain; + EOQ +} + +query "es_domain_cognito_authentication_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when cognito_options ->> 'Enabled' = 'true' then 'ok' + else 'alarm' + end as status, + case + when cognito_options ->> 'Enabled' = 'true' then title || ' Amazon Cognito authentication for Kibana enabled.' + else title || ' Amazon Cognito authentication for Kibana disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_elasticsearch_domain; + EOQ +} + +query "es_domain_internal_user_database_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when advanced_security_options ->> 'InternalUserDatabaseEnabled' = 'true' then 'ok' + else 'alarm' + end as status, + case + when advanced_security_options ->> 'InternalUserDatabaseEnabled' = 'true' then title || ' internal user database enabled.' + else title || ' internal user database disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_elasticsearch_domain; + EOQ +} + +# Non-Config rule query + +query "es_domain_audit_logging_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when + log_publishing_options -> 'AUDIT_LOGS' -> 'Enabled' = 'true' + and log_publishing_options -> 'AUDIT_LOGS' -> 'CloudWatchLogsLogGroupArn' is not null then 'ok' + else 'alarm' + end as status, + case + when + log_publishing_options -> 'AUDIT_LOGS' -> 'Enabled' = 'true' + and log_publishing_options -> 'AUDIT_LOGS' -> 'CloudWatchLogsLogGroupArn' is not null then title || ' audit logging enabled.' + else title || ' audit logging disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_elasticsearch_domain; + EOQ +} + +query "es_domain_data_nodes_min_3" { + sql = <<-EOQ + select + arn as resource, + case + when elasticsearch_cluster_config ->> 'ZoneAwarenessEnabled' = 'false' then 'alarm' + when + elasticsearch_cluster_config ->> 'ZoneAwarenessEnabled' = 'true' + and (elasticsearch_cluster_config ->> 'InstanceCount')::integer >= 3 then 'ok' + else 'alarm' + end status, + case + when elasticsearch_cluster_config ->> 'ZoneAwarenessEnabled' = 'false' then title || ' zone awareness disabled.' + else title || ' has ' || (elasticsearch_cluster_config ->> 'InstanceCount') || ' data node(s).' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_elasticsearch_domain; + EOQ +} + +query "es_domain_dedicated_master_nodes_min_3" { + sql = <<-EOQ + select + arn as resource, + case + when elasticsearch_cluster_config ->> 'DedicatedMasterEnabled' = 'false' then 'alarm' + when + elasticsearch_cluster_config ->> 'DedicatedMasterEnabled' = 'true' + and (elasticsearch_cluster_config ->> 'DedicatedMasterCount')::integer >= 3 then 'ok' + else 'alarm' + end status, + case + when elasticsearch_cluster_config ->> 'DedicatedMasterEnabled' = 'false' then title || ' dedicated master nodes disabled.' + else title || ' has ' || (elasticsearch_cluster_config ->> 'DedicatedMasterCount') || ' dedicated master node(s).' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_elasticsearch_domain; + EOQ +} + +query "es_domain_encrypted_using_tls_1_2" { + sql = <<-EOQ + select + arn as resource, + case + when domain_endpoint_options ->> 'TLSSecurityPolicy' = 'Policy-Min-TLS-1-2-2019-07' then 'ok' + else 'alarm' + end status, + case + when domain_endpoint_options ->> 'TLSSecurityPolicy' = 'Policy-Min-TLS-1-2-2019-07' then title || ' encrypted using TLS 1.2.' + else title || ' not encrypted using TLS 1.2.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_elasticsearch_domain; + EOQ +} + +query "es_domain_error_logging_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when + log_publishing_options -> 'ES_APPLICATION_LOGS' -> 'Enabled' = 'true' + and log_publishing_options -> 'ES_APPLICATION_LOGS' -> 'CloudWatchLogsLogGroupArn' is not null then 'ok' + else 'alarm' + end as status, + case + when + log_publishing_options -> 'ES_APPLICATION_LOGS' -> 'Enabled' = 'true' + and log_publishing_options -> 'ES_APPLICATION_LOGS' -> 'CloudWatchLogsLogGroupArn' is not null then title || ' error logging enabled.' + else title || ' error logging disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_elasticsearch_domain; + EOQ +} diff --git a/conformance_pack/fsx.sp b/conformance_pack/fsx.sp index 17c3ed81..8f4e7bd3 100644 --- a/conformance_pack/fsx.sp +++ b/conformance_pack/fsx.sp @@ -19,3 +19,31 @@ control "fsx_file_system_protected_by_backup_plan" { soc_2 = "true" }) } + +query "fsx_file_system_protected_by_backup_plan" { + sql = <<-EOQ + with backup_protected_fsx_file_system as ( + select + resource_arn as arn + from + aws_backup_protected_resource as b + where + resource_type = 'FSx' + ) + select + f.arn as resource, + case + when b.arn is not null then 'ok' + else 'alarm' + end as status, + case + when b.arn is not null then f.title || ' is protected by backup plan.' + else f.title || ' is not protected by backup plan.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "f.")} + from + aws_fsx_file_system as f + left join backup_protected_fsx_file_system as b on f.arn = b.arn; + EOQ +} diff --git a/conformance_pack/glue.sp b/conformance_pack/glue.sp index 369d4aca..ff33a1ea 100644 --- a/conformance_pack/glue.sp +++ b/conformance_pack/glue.sp @@ -63,3 +63,155 @@ control "glue_job_s3_encryption_enabled" { other_checks = "true" }) } + +query "glue_dev_endpoint_cloudwatch_logs_encryption_enabled" { + sql = <<-EOQ + select + e.arn as resource, + case + when cloud_watch_encryption is not null and cloud_watch_encryption ->> 'CloudWatchEncryptionMode' != 'DISABLED' then 'ok' + else 'alarm' + end as status, + case + when cloud_watch_encryption is not null and cloud_watch_encryption ->> 'CloudWatchEncryptionMode' != 'DISABLED' then e.title || ' CloudWatch logs encryption enabled.' + else e.title || ' CloudWatch logs encryption disabled.' + end as reason + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "e.")} + from + aws_glue_dev_endpoint as e + left join aws_glue_security_configuration as c on e.security_configuration = c.name; + EOQ +} + +query "glue_dev_endpoint_job_bookmark_encryption_enabled" { + sql = <<-EOQ + select + e.arn as resource, + case + when job_bookmarks_encryption is not null and job_bookmarks_encryption ->> 'JobBookmarksEncryptionMode' != 'DISABLED' then 'ok' + else 'alarm' + end as status, + case + when job_bookmarks_encryption is not null and job_bookmarks_encryption ->> 'JobBookmarksEncryptionMode' != 'DISABLED' then e.title || ' job bookmark encryption enabled.' + else e.title || ' job bookmark encryption disabled.' + end as reason + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "e.")} + from + aws_glue_dev_endpoint as e + left join aws_glue_security_configuration as c on e.security_configuration = c.name; + EOQ +} + +query "glue_dev_endpoint_s3_encryption_enabled" { + sql = <<-EOQ + select + d.arn as resource, + case + when e is not null and e ->> 'S3EncryptionMode' != 'DISABLED' then 'ok' + else 'alarm' + end as status, + case + when e is not null and e ->> 'S3EncryptionMode' != 'DISABLED' then d.title || ' S3 encryption enabled.' + else d.title || ' S3 encryption disabled.' + end as reason + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "d.")} + from + aws_glue_dev_endpoint as d + left join aws_glue_security_configuration s on d.security_configuration = s.name, + jsonb_array_elements(s.s3_encryption) e; + EOQ +} + +query "glue_job_cloudwatch_logs_encryption_enabled" { + sql = <<-EOQ + select + j.arn as resource, + case + when cloud_watch_encryption is not null and cloud_watch_encryption ->> 'CloudWatchEncryptionMode' != 'DISABLED' then 'ok' + else 'alarm' + end as status, + case + when cloud_watch_encryption is not null and cloud_watch_encryption ->> 'CloudWatchEncryptionMode' != 'DISABLED' then j.title || ' CloudWatch logs encryption enabled.' + else j.title || ' CloudWatch logs encryption disabled.' + end as reason + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "j.")} + from + aws_glue_job as j + left join aws_glue_security_configuration as c on j.security_configuration = c.name; + EOQ +} + +query "glue_job_bookmarks_encryption_enabled" { + sql = <<-EOQ + select + j.arn as resource, + case + when job_bookmarks_encryption is not null and job_bookmarks_encryption ->> 'JobBookmarksEncryptionMode' != 'DISABLED' then 'ok' + else 'alarm' + end as status, + case + when job_bookmarks_encryption is not null and job_bookmarks_encryption ->> 'JobBookmarksEncryptionMode' != 'DISABLED' then j.title || ' job bookmarks encryption enabled.' + else j.title || ' job bookmarks encryption disabled.' + end as reason + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "j.")} + from + aws_glue_job as j + left join aws_glue_security_configuration as c on j.security_configuration = c.name; + EOQ +} + +query "glue_job_s3_encryption_enabled" { + sql = <<-EOQ + select + j.arn as resource, + case + when e is not null and e ->> 'S3EncryptionMode' != 'DISABLED' then 'ok' + else 'alarm' + end as status, + case + when e is not null and e ->> 'S3EncryptionMode' != 'DISABLED' then j.title || ' S3 encryption enabled.' + else j.title || ' S3 encryption disabled.' + end as reason + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "j.")} + from + aws_glue_job as j + left join aws_glue_security_configuration as s on j.security_configuration = s.name, + jsonb_array_elements(s.s3_encryption) e; + EOQ +} + +# Non-Config rule query + +query "glue_data_catalog_encryption_settings_metadata_encryption_enabled" { + sql = <<-EOQ + select + case + when encryption_at_rest is not null and encryption_at_rest ->> 'CatalogEncryptionMode' != 'DISABLED' then 'ok' + else 'alarm' + end as status, + case + when encryption_at_rest is not null and encryption_at_rest ->> 'CatalogEncryptionMode' != 'DISABLED' then 'Glue data catalog metadata encryption is enabled in ' || region || '.' + else 'Glue data catalog metadata encryption is disabled in ' || region || '.' + end as reason + ${local.common_dimensions_sql} + from + aws_glue_data_catalog_encryption_settings; + EOQ +} + +query "glue_data_catalog_encryption_settings_password_encryption_enabled" { + sql = <<-EOQ + select + case + when connection_password_encryption is not null and connection_password_encryption ->> 'ReturnConnectionPasswordEncrypted' != 'false' then 'ok' + else 'alarm' + end as status, + case + when connection_password_encryption is not null and connection_password_encryption ->> 'ReturnConnectionPasswordEncrypted' != 'false' then 'Glue data catalog connection password encryption enabled in ' || region || '.' + else 'Glue data catalog connection password encryption disabled in ' || region || '.' + end as reason + ${local.common_dimensions_sql} + from + aws_glue_data_catalog_encryption_settings; + EOQ +} diff --git a/conformance_pack/guardduty.sp b/conformance_pack/guardduty.sp index aeeab690..ff25ccb4 100644 --- a/conformance_pack/guardduty.sp +++ b/conformance_pack/guardduty.sp @@ -43,3 +43,48 @@ control "guardduty_finding_archived" { soc_2 = "true" }) } + +query "guardduty_enabled" { + sql = <<-EOQ + select + 'arn:' || r.partition || '::' || r.region || ':' || r.account_id as resource, + case + when r.region = any(array['af-south-1', 'ap-northeast-3', 'ap-southeast-3', 'eu-south-1', 'cn-north-1', 'cn-northwest-1', 'me-south-1', 'us-gov-east-1']) then 'skip' + -- Skip any regions that are disabled in the account. + when r.opt_in_status = 'not-opted-in' then 'skip' + when status = 'ENABLED' and master_account ->> 'AccountId' is null then 'ok' + when status = 'ENABLED' and master_account ->> 'AccountId' is not null then 'info' + else 'alarm' + end as status, + case + when r.region = any(array['af-south-1', 'ap-northeast-3', 'ap-southeast-3', 'eu-south-1', 'cn-north-1', 'cn-northwest-1', 'me-south-1', 'us-gov-east-1']) then r.region || ' region not supported.' + when r.opt_in_status = 'not-opted-in' then r.region || ' region is disabled.' + when status is null then 'No GuardDuty detector found in ' || r.region || '.' + when status = 'ENABLED' and master_account ->> 'AccountId' is null then r.region || ' detector ' || d.title || ' enabled.' + when status = 'ENABLED' and master_account ->> 'AccountId' is not null then r.region || ' detector ' || d.title || ' is managed by account ' || (master_account ->> 'AccountId') || ' via delegated admin.' + else r.region || ' detector ' || d.title || ' disabled.' + end as reason + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "r.")} + from + aws_region as r + left join aws_guardduty_detector d on r.account_id = d.account_id and r.name = d.region; + EOQ +} + +query "guardduty_finding_archived" { + sql = <<-EOQ + select + arn as resource, + case + when service ->> 'Archived' = 'false' then 'alarm' + else 'ok' + end as status, + case + when service ->> 'Archived' = 'false' then title || ' not archived.' + else title || ' archived.' + end as reason + ${local.common_dimensions_sql} + from + aws_guardduty_finding; + EOQ +} diff --git a/conformance_pack/iam.sp b/conformance_pack/iam.sp index ad23fefc..d18ffb01 100644 --- a/conformance_pack/iam.sp +++ b/conformance_pack/iam.sp @@ -463,36 +463,6 @@ control "iam_user_with_administrator_access_mfa_enabled" { }) } -control "iam_policy_custom_no_assume_role" { - title = "IAM roles should not have any assume role policies attached" - description = "Role assume policies can provide access to roles in external AWS accounts." - query = query.iam_policy_custom_no_assume_role - - tags = merge(local.conformance_pack_iam_common_tags, { - other_checks = "true" - }) -} - -control "iam_user_hardware_mfa_enabled" { - title = "IAM users should have hardware MFA enabled" - description = "Manage access to resources in the AWS Cloud by ensuring hardware MFA is enabled for the user." - query = query.iam_user_hardware_mfa_enabled - - tags = merge(local.conformance_pack_iam_common_tags, { - other_checks = "true" - }) -} - -control "iam_user_with_administrator_access_mfa_enabled" { - title = "IAM administrator users should have MFA enabled" - description = "Manage access to resources in the AWS Cloud by ensuring MFA is enabled for users with administrative privileges." - query = query.iam_user_with_administrator_access_mfa_enabled - - tags = merge(local.conformance_pack_iam_common_tags, { - other_checks = "true" - }) -} - control "iam_managed_policy_attached_to_role" { title = "IAM AWS managed policies should be attached to IAM role" description = "This control checks if all AWS managed policies specified in the list of managed policies are attached to the AWS Identity and Access Management (IAM) role. The rule is non compliant if an AWS managed policy is not attached to the IAM role." @@ -512,3 +482,1181 @@ control "iam_policy_unused" { cis_controls_v8_ig1 = "true" }) } + + +query "iam_account_password_policy_strong_min_reuse_24" { + sql = <<-EOQ + select + 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when + minimum_password_length >= 14 + and password_reuse_prevention >= 24 + and require_lowercase_characters = 'true' + and require_uppercase_characters = 'true' + and require_numbers = 'true' + and require_symbols = 'true' + and max_password_age <= 90 + then 'ok' + else 'alarm' + end as status, + case + when minimum_password_length is null then 'No password policy set.' + when + minimum_password_length >= 14 + and password_reuse_prevention >= 24 + and require_lowercase_characters = 'true' + and require_uppercase_characters = 'true' + and require_numbers = 'true' + and require_symbols = 'true' + and max_password_age <= 90 + then 'Strong password policies configured.' + else 'Strong password policies not configured.' + end as reason + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join aws_iam_account_password_policy as pol on a.account_id = pol.account_id; + EOQ +} + +query "iam_group_not_empty" { + sql = <<-EOQ + select + arn as resource, + case + when users is null then 'alarm' + else 'ok' + end as status, + case + when users is null then title || ' not associated with any IAM user.' + else title || ' associated with IAM user.' + end as reason + ${local.common_dimensions_global_sql} + from + aws_iam_group; + EOQ +} + +query "iam_policy_custom_no_star_star" { + sql = <<-EOQ + with bad_policies as ( + select + arn, + count(*) as num_bad_statements + from + aws_iam_policy, + jsonb_array_elements(policy_std -> 'Statement') as s, + jsonb_array_elements_text(s -> 'Resource') as resource, + jsonb_array_elements_text(s -> 'Action') as action + where + not is_aws_managed + and s ->> 'Effect' = 'Allow' + and resource = '*' + and ( + (action = '*' + or action = '*:*' + ) + ) + group by + arn + ) + select + p.arn as resource, + case + when bad.arn is null then 'ok' + else 'alarm' + end status, + p.name || ' contains ' || coalesce(bad.num_bad_statements,0) || + ' statements that allow action "*" on resource "*".' as reason + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "p.")} + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "p.")} + from + aws_iam_policy as p + left join bad_policies as bad on p.arn = bad.arn + where + not p.is_aws_managed; + EOQ +} + +query "iam_root_user_no_access_keys" { + sql = <<-EOQ + select + 'arn:' || partition || ':::' || account_id as resource, + case + when account_access_keys_present > 0 then 'alarm' + else 'ok' + end status, + case + when account_access_keys_present > 0 then 'Root user access keys exist.' + else 'No root user access keys exist.' + end reason + ${local.common_dimensions_global_sql} + from + aws_iam_account_summary; + EOQ +} + +query "iam_root_user_hardware_mfa_enabled" { + sql = <<-EOQ + select + 'arn:' || s.partition || ':::' || s.account_id as resource, + case + when account_mfa_enabled and serial_number is null then 'ok' + else 'alarm' + end status, + case + when account_mfa_enabled = false then 'MFA not enabled for root account.' + when serial_number is not null then 'MFA enabled for root account, but the MFA associated is a virtual device.' + else 'Hardware MFA device enabled for root account.' + end reason + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "s.")} + from + aws_iam_account_summary as s + left join aws_iam_virtual_mfa_device on serial_number = 'arn:' || s.partition || ':iam::' || s.account_id || ':mfa/root-account-mfa-device'; + EOQ +} + +query "iam_root_user_mfa_enabled" { + sql = <<-EOQ + select + 'arn:' || partition || ':::' || account_id as resource, + case + when account_mfa_enabled then 'ok' + else 'alarm' + end status, + case + when account_mfa_enabled then 'MFA enabled for root account.' + else 'MFA not enabled for root account.' + end reason + ${local.common_dimensions_global_sql} + from + aws_iam_account_summary; + EOQ +} + +query "iam_user_access_key_age_90" { + sql = <<-EOQ + select + 'arn:' || partition || ':iam::' || account_id || ':user/' || user_name || '/accesskey/' || access_key_id as resource, + case + when create_date <= (current_date - interval '90' day) then 'alarm' + else 'ok' + end status, + user_name || ' ' || access_key_id || ' created ' || to_char(create_date , 'DD-Mon-YYYY') || + ' (' || extract(day from current_timestamp - create_date) || ' days).' + as reason + ${local.common_dimensions_global_sql} + from + aws_iam_access_key; + + EOQ +} + +query "iam_user_console_access_mfa_enabled" { + sql = <<-EOQ + select + user_arn as resource, + case + when password_enabled and not mfa_active then 'alarm' + else 'ok' + end as status, + case + when not password_enabled then user_name || ' password login disabled.' + when password_enabled and not mfa_active then user_name || ' password login enabled but no MFA device configured.' + else user_name || ' password login enabled and MFA device configured.' + end as reason + ${local.common_dimensions_global_sql} + from + aws_iam_credential_report; + EOQ +} + +query "iam_user_mfa_enabled" { + sql = <<-EOQ + select + user_arn as resource, + case + when not mfa_active then 'alarm' + else 'ok' + end as status, + case + when not mfa_active then user_name || ' MFA device not configured.' + else user_name || ' MFA device configured.' + end as reason + ${local.common_dimensions_global_sql} + from + aws_iam_credential_report; + EOQ +} + +query "iam_user_no_inline_attached_policies" { + sql = <<-EOQ + select + arn as resource, + case + when inline_policies is null and attached_policy_arns is null then 'ok' + else 'alarm' + end status, + name || ' has ' || coalesce(jsonb_array_length(inline_policies),0) || ' inline and ' || + coalesce(jsonb_array_length(attached_policy_arns),0) || ' directly attached policies.' as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_global_sql} + from + aws_iam_user; + EOQ +} + +query "iam_user_unused_credentials_90" { + sql = <<-EOQ + select + user_arn as resource, + case + when user_name = '' + then 'info' + when password_enabled and password_last_used is null and password_last_changed < (current_date - interval '90' day) + then 'alarm' + when password_enabled and password_last_used < (current_date - interval '90' day) + then 'alarm' + when access_key_1_active and access_key_1_last_used_date is null and access_key_1_last_rotated < (current_date - interval '90' day) + then 'alarm' + when access_key_1_active and access_key_1_last_used_date < (current_date - interval '90' day) + then 'alarm' + when access_key_2_active and access_key_2_last_used_date is null and access_key_2_last_rotated < (current_date - interval '90' day) + then 'alarm' + when access_key_2_active and access_key_2_last_used_date < (current_date - interval '90' day) + then 'alarm' + else 'ok' + end status, + user_name || + case + when not password_enabled + then ' password not enabled,' + when password_enabled and password_last_used is null + then ' password created ' || to_char(password_last_changed, 'DD-Mon-YYYY') || ' never used,' + else + ' password used ' || to_char(password_last_used, 'DD-Mon-YYYY') || ',' + end || + case + when not access_key_1_active + then ' key 1 not enabled,' + when access_key_1_active and access_key_1_last_used_date is null + then ' key 1 created ' || to_char(access_key_1_last_rotated, 'DD-Mon-YYYY') || ' never used,' + else + ' key 1 used ' || to_char(access_key_1_last_used_date, 'DD-Mon-YYYY') || ',' + end || + case + when not access_key_2_active + then ' key 2 not enabled.' + when access_key_2_active and access_key_2_last_used_date is null + then ' key 2 created ' || to_char(access_key_2_last_rotated, 'DD-Mon-YYYY') || ' never used.' + else + ' key 2 used ' || to_char(access_key_2_last_used_date, 'DD-Mon-YYYY') || '.' + end + as reason + ${local.common_dimensions_global_sql} + from + aws_iam_credential_report; + EOQ +} + +query "iam_user_in_group" { + sql = <<-EOQ + select + arn as resource, + case + when jsonb_array_length(groups) = 0 then 'alarm' + else 'ok' + end as status, + case + when jsonb_array_length(groups) = 0 then title || ' not associated with any IAM group.' + else title || ' associated with IAM group.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_global_sql} + from + aws_iam_user; + EOQ +} + +query "iam_group_user_role_no_inline_policies" { + sql = <<-EOQ + select + arn as resource, + case + when inline_policies is null then 'ok' + else 'alarm' + end status, + 'User ' || title || ' has ' || coalesce(jsonb_array_length(inline_policies), 0) || ' inline policies.' as reason + ${local.common_dimensions_global_sql} + from + aws_iam_user + union + select + arn as resource, + case + when inline_policies is null then 'ok' + else 'alarm' + end status, + 'Role ' || title || ' has ' || coalesce(jsonb_array_length(inline_policies), 0) || ' inline policies.' as reason + ${local.common_dimensions_global_sql} + from + aws_iam_role + where + arn not like '%service-role/%' + union + select + arn as resource, + case + when inline_policies is null then 'ok' + else 'alarm' + end status, + 'Group ' || title || ' has ' || coalesce(jsonb_array_length(inline_policies), 0) || ' inline policies.' as reason + ${local.common_dimensions_global_sql} + from + aws_iam_group; + EOQ +} + +query "iam_support_role" { + sql = <<-EOQ + -- pgFormatter-ignore + with support_role_count as + ( + select + 'arn:' || a.partition || ':::' || a.account_id as resource, + count(policy_arn), + a.account_id, + a._ctx + from + aws_account as a + left join aws_iam_role as r on r.account_id = a.account_id + left join jsonb_array_elements_text(attached_policy_arns) as policy_arn on true + where + split_part(policy_arn, '/', 2) = 'AWSSupportAccess' + or policy_arn is null + group by + a.account_id, + a.partition, + a._ctx + ) + select + resource, + case + when count > 0 then 'ok' + else 'alarm' + end as status, + case + when count = 1 then 'AWSSupportAccess policy attached to 1 role.' + when count > 1 then 'AWSSupportAccess policy attached to ' || count || ' roles.' + else 'AWSSupportAccess policy not attached to any role.' + end as reason + ${local.common_dimensions_global_sql} + from + support_role_count; + EOQ +} + +query "iam_account_password_policy_min_length_14" { + sql = <<-EOQ + select + 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when minimum_password_length >= 14 then 'ok' + else 'alarm' + end as status, + case + when minimum_password_length is null then 'No password policy set.' + else 'Minimum password length set to ' || minimum_password_length || '.' + end as reason + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join aws_iam_account_password_policy as pol on a.account_id = pol.account_id; + EOQ +} + +query "iam_account_password_policy_reuse_24" { + sql = <<-EOQ + select + 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when password_reuse_prevention >= 24 then 'ok' + else 'alarm' + end as status, + case + when minimum_password_length is null then 'No password policy set.' + when password_reuse_prevention is null then 'Password reuse prevention not set.' + else 'Password reuse prevention set to ' || password_reuse_prevention || '.' + end as reason + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join aws_iam_account_password_policy as pol on a.account_id = pol.account_id; + EOQ +} + +query "iam_account_password_policy_strong" { + sql = <<-EOQ + select + 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when + minimum_password_length >= 14 + and password_reuse_prevention >= 5 + and require_lowercase_characters = 'true' + and require_uppercase_characters = 'true' + and require_numbers = 'true' + and max_password_age <= 90 + then 'ok' + else 'alarm' + end status, + case + when minimum_password_length is null then 'No password policy set.' + when + minimum_password_length >= 14 + and password_reuse_prevention >= 5 + and require_lowercase_characters = 'true' + and require_uppercase_characters = 'true' + and require_numbers = 'true' + and max_password_age <= 90 + then 'Strong password policies configured.' + else 'Strong password policies not configured.' + end reason + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join aws_iam_account_password_policy as pol on a.account_id = pol.account_id; + EOQ +} + +query "iam_account_password_policy_one_lowercase_letter" { + sql = <<-EOQ + select + 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when require_lowercase_characters then 'ok' + else 'alarm' + end as status, + case + when minimum_password_length is null then 'No password policy set.' + when require_lowercase_characters then 'Lowercase character required.' + else 'Lowercase character not required.' + end as reason + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join aws_iam_account_password_policy as pol on a.account_id = pol.account_id; + EOQ +} + +query "iam_account_password_policy_one_uppercase_letter" { + sql = <<-EOQ + select + 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when require_uppercase_characters then 'ok' + else 'alarm' + end as status, + case + when minimum_password_length is null then 'No password policy set.' + when require_uppercase_characters then 'Uppercase character required.' + else 'Uppercase character not required.' + end as reason + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join aws_iam_account_password_policy as pol on a.account_id = pol.account_id; + EOQ +} + +query "iam_account_password_policy_one_number" { + sql = <<-EOQ + select + 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when require_numbers then 'ok' + else 'alarm' + end as status, + case + when minimum_password_length is null then 'No password policy set.' + when require_numbers then 'Number required.' + else 'Number not required.' + end as reason + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join aws_iam_account_password_policy as pol on a.account_id = pol.account_id; + EOQ +} + +query "iam_account_password_policy_expire_90" { + sql = <<-EOQ + select + 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when max_password_age <= 90 then 'ok' + else 'alarm' + end as status, + case + when max_password_age is null then 'Password expiration not set.' + else 'Password expiration set to ' || max_password_age || ' days.' + end as reason + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join aws_iam_account_password_policy as pol on a.account_id = pol.account_id; + EOQ +} + +query "iam_account_password_policy_one_symbol" { + sql = <<-EOQ + select + 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when require_symbols then 'ok' + else 'alarm' + end as status, + case + when minimum_password_length is null then 'No password policy set.' + when require_symbols then 'Symbol required.' + else 'Symbol not required.' + end as reason + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join aws_iam_account_password_policy as pol on a.account_id = pol.account_id; + EOQ +} + +query "iam_policy_custom_no_service_wildcard" { + sql = <<-EOQ + with wildcard_action_policies as ( + select + arn, + count(*) as statements_num + from + aws_iam_policy, + jsonb_array_elements(policy_std -> 'Statement') as s, + jsonb_array_elements_text(s -> 'Resource') as resource, + jsonb_array_elements_text(s -> 'Action') as action + where + not is_aws_managed + and s ->> 'Effect' = 'Allow' + and resource = '*' + and ( + action like '%:*' + or action = '*' + ) + group by + arn + ) + select + p.arn as resource, + case + when w.arn is null then 'ok' + else 'alarm' + end status, + p.name || ' contains ' || coalesce(w.statements_num,0) || + ' statements that allow action "*" on at least 1 AWS service on resource "*".' as reason + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "p.")} + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "p.")} + from + aws_iam_policy as p + left join wildcard_action_policies as w on p.arn = w.arn + where + not p.is_aws_managed; + EOQ +} + +query "iam_policy_custom_no_blocked_kms_actions" { + sql = <<-EOQ + with kms_blocked_actions as ( + select + arn, + count(*) as statements_num + from + aws_iam_policy, + jsonb_array_elements(policy_std -> 'Statement') as s, + jsonb_array_elements_text(s -> 'Resource') as resource, + jsonb_array_elements_text(s -> 'Action') as action + where + not is_aws_managed + and s ->> 'Effect' = 'Allow' + and action like any(array['kms:decrypt', 'kms:reencryptfrom']) + group by + arn + ) + select + p.arn as resource, + case + when w.arn is null then 'ok' + else 'alarm' + end status, + p.name || ' contains ' || coalesce(w.statements_num,0) || ' statements that allow blocked actions on AWS KMS keys.' as reason + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "p.")} + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "p.")} + from + aws_iam_policy as p + left join kms_blocked_actions as w on p.arn = w.arn + where + not p.is_aws_managed; + EOQ +} + +query "iam_policy_inline_no_blocked_kms_actions" { + sql = <<-EOQ + with iam_resource_types as ( + select + arn, + inline_policies_std, + name, + account_id, + region, + _ctx + from + aws_iam_user + union + select + arn, + inline_policies_std, + name, + account_id, + region, + _ctx + from + aws_iam_role + union + select + arn, + inline_policies_std, + name, + account_id, + region, + _ctx + from + aws_iam_group + ), + kms_blocked_actions as ( + select + arn, + count(*) as statements_num + from + iam_resource_types, + jsonb_array_elements(inline_policies_std) as policy_std, + jsonb_array_elements(policy_std -> 'PolicyDocument' -> 'Statement') as s, + jsonb_array_elements_text(s -> 'Resource') as resource, + jsonb_array_elements_text(s -> 'Action') as action + where + s ->> 'Effect' = 'Allow' + and action like any(array['kms:decrypt','kms:decrypt*', 'kms:reencryptfrom', 'kms:*', 'kms:reencrypt*']) + group by + arn + ) + select + u.arn as resource, + case + when w.arn is null then 'ok' + else 'alarm' + end status, + u.name || ' contains ' || coalesce(w.statements_num,0) || ' inline policy statement(s) that allow blocked actions on AWS KMS keys.' as reason + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "u.")} + from + iam_resource_types as u + left join kms_blocked_actions as w on u.arn = w.arn; + EOQ +} + +query "account_part_of_organizations" { + sql = <<-EOQ + select + arn as resource, + case + when organization_id is not null then 'ok' + else 'alarm' + end as status, + case + when organization_id is not null then title || ' is part of organization(s).' + else title || ' is not part of organization.' + end as reason + ${local.common_dimensions_sql} + from + aws_account; + EOQ +} + +query "iam_policy_custom_no_assume_role" { + sql = <<-EOQ + with filter_users as ( + select + user_id, + name, + policies + from + aws_iam_user, + jsonb_array_elements_text(inline_policies) as policies + where + policies like '%AssumeRole%' + ) + select + u.arn as resource, + case + when fu.user_id is not null then 'alarm' + else 'ok' + end as status, + case + when fu.user_id is not null then u.name || ' custom policies allow STS Role assumption.' + else u.name || ' custom policies does not allow STS Role assumption.' + end as reason + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "u.")} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "u.")} + from + aws_iam_user as u + left join filter_users as fu on u.user_id = fu.user_id + order by + u.name; + EOQ +} + +query "iam_user_hardware_mfa_enabled" { + sql = <<-EOQ + select + u.arn as resource, + case + when serial_number is null then 'alarm' + when serial_number like any(array['%mfa%','%sms-mfa%']) then 'info' + else 'ok' + end as status, + case + when serial_number is null then u.name || ' MFA device not configured.' + when serial_number like any(array['%mfa%','%sms-mfa%']) then u.name || ' MFA enabled, but the MFA associated is a virtual device.' + else u.name || ' hardware MFA device enabled.' + end as reason + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "u.")} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "u.")} + from + aws_iam_virtual_mfa_device as m + right join aws_iam_user as u on m.user_id = u.user_id; + EOQ +} + +query "iam_user_with_administrator_access_mfa_enabled" { + sql = <<-EOQ + with admin_users as ( + select + user_id, + name, + attachments + from + aws_iam_user, + jsonb_array_elements_text(attached_policy_arns) as attachments + where + split_part(attachments, '/', 2) = 'AdministratorAccess' + ) + select + u.arn as resource, + case + when au.user_id is null then 'skip' + when au.user_id is not null and u.mfa_enabled then 'ok' + else 'alarm' + end as status, + case + when au.user_id is null then u.name || ' does not have administrator access.' + when au.user_id is not null and u.mfa_enabled then u.name || ' has MFA token enabled.' + else u.name || ' has MFA token disabled.' + end as reason + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "u.")} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "u.")} + from + aws_iam_user as u + left join admin_users au on u.user_id = au.user_id + order by + u.name; + EOQ +} + +query "iam_managed_policy_attached_to_role" { + sql = <<-EOQ + with role_attached_policies as ( + select + jsonb_array_elements_text(attached_policy_arns) as policy_arn + from + aws_iam_role + ) + select + arn as resource, + case + when arn in (select policy_arn from role_attached_policies) then 'ok' + else 'alarm' + end as status, + case + when arn in (select policy_arn from role_attached_policies) then title || ' attached to IAM role.' + else title || ' not attached to IAM role.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_global_sql} + from + aws_iam_policy + where + is_aws_managed; + EOQ +} + +query "iam_policy_unused" { + sql = <<-EOQ + with in_use_policies as ( + select + attached_policy_arns + from + aws_iam_user + union + select + attached_policy_arns + from + aws_iam_group + where + jsonb_array_length(users) > 0 + union + select + attached_policy_arns + from + aws_iam_role + ) + select + arn as resource, + case + when arn in (select jsonb_array_elements_text(attached_policy_arns) from in_use_policies) then 'ok' + else 'alarm' + end as status, + case + when arn in (select jsonb_array_elements_text(attached_policy_arns) from in_use_policies) then title || ' in use.' + else title || ' not in use.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_global_sql} + from + aws_iam_policy; + EOQ +} + +# Non-Config rule query + +query "iam_access_analyzer_enabled" { + sql = <<-EOQ + select + 'arn:' || r.partition || '::' || r.region || ':' || r.account_id as resource, + case + -- Skip any regions that are disabled in the account. + when r.opt_in_status = 'not-opted-in' then 'skip' + when aa.arn is not null then 'ok' + else 'alarm' + end as status, + case + when r.opt_in_status = 'not-opted-in' then r.region || ' region is disabled.' + when aa.arn is not null then aa.name || ' enabled in ' || r.region || '.' + else 'Access Analyzer not enabled in ' || r.region || '.' + end as reason + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "r.")} + from + aws_region as r + left join aws_accessanalyzer_analyzer as aa on r.account_id = aa.account_id and r.region = aa.region; + EOQ +} + +query "iam_account_password_policy_strong_min_length_8" { + sql = <<-EOQ + select + 'arn:' || a.partition || ':::' || a.account_id as resource, + case + when + minimum_password_length >= 8 + and require_lowercase_characters = 'true' + and require_uppercase_characters = 'true' + and require_numbers = 'true' + and require_symbols = 'true' + then 'ok' + else 'alarm' + end as status, + case + when minimum_password_length is null then 'No password policy set.' + when + minimum_password_length >= 8 + and require_lowercase_characters = 'true' + and require_uppercase_characters = 'true' + and require_numbers = 'true' + and require_symbols = 'true' + then 'Strong password policies configured.' + else 'Strong password policies not configured.' + end as reason + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "a.")} + from + aws_account as a + left join aws_iam_account_password_policy as pol on a.account_id = pol.account_id; + EOQ +} + +query "iam_policy_all_attached_no_star_star" { + sql = <<-EOQ + with star_access_policies as ( + select + arn, + count(*) as num_bad_statements + from + aws_iam_policy, + jsonb_array_elements(policy_std -> 'Statement') as s, + jsonb_array_elements_text(s -> 'Resource') as resource, + jsonb_array_elements_text(s -> 'Action') as action + where + s ->> 'Effect' = 'Allow' + and resource = '*' + and ( + (action = '*' + or action = '*:*' + ) + ) + and is_attached + group by arn + ) + select + p.arn as resource, + case + when s.arn is null then 'ok' + else 'alarm' + end status, + p.name || ' contains ' || coalesce(s.num_bad_statements,0) || ' statements that allow action "*" on resource "*".' as reason + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "p.")} + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "p.")} + from + aws_iam_policy as p + left join star_access_policies as s on p.arn = s.arn + where + p.is_attached; + EOQ +} + +query "iam_policy_custom_attached_no_star_star" { + sql = <<-EOQ + -- This query checks the customer managed policies having * access and attached to IAM resource(s) + with star_access_policies as ( + select + arn, + count(*) as num_bad_statements + from + aws_iam_policy, + jsonb_array_elements(policy_std -> 'Statement') as s, + jsonb_array_elements_text(s -> 'Resource') as resource, + jsonb_array_elements_text(s -> 'Action') as action + where + not is_aws_managed + and s ->> 'Effect' = 'Allow' + and resource = '*' + and ( + (action = '*' + or action = '*:*' + ) + ) + and is_attached + group by arn + ) + select + p.arn as resource, + case + when s.arn is null then 'ok' + else 'alarm' + end status, + p.name || ' contains ' || coalesce(s.num_bad_statements,0) || ' statements that allow action "*" on resource "*".' as reason + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "p.")} + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "p.")} + from + aws_iam_policy as p + left join star_access_policies as s on p.arn = s.arn + where + not p.is_aws_managed; + EOQ +} + +query "iam_root_last_used" { + sql = <<-EOQ + select + user_arn as resource, + case + when password_last_used >= (current_date - interval '90' day) then 'alarm' + when access_key_1_last_used_date <= (current_date - interval '90' day) then 'alarm' + when access_key_2_last_used_date <= (current_date - interval '90' day) then 'alarm' + else 'ok' + end as status, + case + when password_last_used is null then 'Root never logged in with password.' + else 'Root password used ' || to_char(password_last_used , 'DD-Mon-YYYY') || ' (' || extract(day from current_timestamp - password_last_used) || ' days).' + end || + case + when access_key_1_last_used_date is null then ' Access Key 1 never used.' + else ' Access Key 1 used ' || to_char(access_key_1_last_used_date , 'DD-Mon-YYYY') || ' (' || extract(day from current_timestamp - access_key_1_last_used_date) || ' days).' + end || + case + when access_key_2_last_used_date is null then ' Access Key 2 never used.' + else ' Access Key 2 used ' || to_char(access_key_2_last_used_date , 'DD-Mon-YYYY') || ' (' || extract(day from current_timestamp - access_key_2_last_used_date) || ' days).' + end as reason + ${local.common_dimensions_global_sql} + from + aws_iam_credential_report + where + user_name = ''; + EOQ +} + +query "iam_root_user_virtual_mfa" { + sql = <<-EOQ + select + 'arn:' || s.partition || ':::' || s.account_id as resource, + case + when account_mfa_enabled and serial_number is not null then 'ok' + else 'alarm' + end status, + case + when account_mfa_enabled = false then 'MFA is not enabled for the root user.' + when serial_number is null then 'MFA is enabled for the root user, but the MFA associated with the root user is a hardware device.' + else 'Virtual MFA enabled for the root user.' + end reason + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "s.")} + from + aws_iam_account_summary as s + left join aws_iam_virtual_mfa_device on serial_number = 'arn:' || s.partition || ':iam::' || s.account_id || ':mfa/root-account-mfa-device'; + EOQ +} + +query "iam_server_certificate_not_expired" { + sql = <<-EOQ + select + arn as resource, + case when expiration < (current_date - interval '1' second) then 'alarm' + else 'ok' + end as status, + case when expiration < (current_date - interval '1' second) then + name || ' expired ' || to_char(expiration, 'DD-Mon-YYYY') || '.' + else + name || ' valid until ' || to_char(expiration, 'DD-Mon-YYYY') || '.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_global_sql} + from + aws_iam_server_certificate; + EOQ +} + +query "iam_user_access_keys_and_password_at_setup" { + sql = <<-EOQ + select + user_arn as resource, + case + -- alarm when password is enabled and the key was created within 10 seconds of the user + when password_enabled and (extract(epoch from (access_key_1_last_rotated - user_creation_time)) < 10) then 'alarm' + else 'ok' + end as status, + case + when not password_enabled then user_name || ' password login disabled.' + when access_key_1_last_rotated is null then user_name || ' has no access keys.' + when password_enabled and (extract(epoch from (access_key_1_last_rotated - user_creation_time)) < 10) + then user_name || ' has access key created during user creation and password login enabled.' + else user_name || ' has access key not created during user creation.' + end as reason + ${local.common_dimensions_global_sql} + from + aws_iam_credential_report; + EOQ +} + +query "iam_user_no_policies" { + sql = <<-EOQ + select + arn as resource, + case + when attached_policy_arns is null then 'ok' + else 'alarm' + end status, + name || ' has ' || coalesce(jsonb_array_length(attached_policy_arns),0) || ' attached policies.' as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_global_sql} + from + aws_iam_user; + EOQ +} + +query "iam_user_one_active_key" { + sql = <<-EOQ + select + u.arn as resource, + case + when count(k.*) > 1 then 'alarm' + else 'ok' + end as status, + u.name || ' has ' || count(k.*) || ' active access key(s).' as reason + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "u.")} + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "u.")} + from + aws_iam_user as u + left join aws_iam_access_key as k on u.name = k.user_name and u.account_id = k.account_id + where + k.status = 'Active' or k.status is null + group by + u.arn, + u.name, + u.account_id, + u.tags, + u._ctx; + EOQ +} + +query "iam_user_unused_credentials_45" { + sql = <<-EOQ + select + user_arn as resource, + case + --root_account will have always password associated even though AWS credential report returns 'not_supported' for password_enabled + when user_name = '' + then 'info' + when password_enabled and password_last_used is null and password_last_changed < (current_date - interval '45' day) + then 'alarm' + when password_enabled and password_last_used < (current_date - interval '45' day) + then 'alarm' + when access_key_1_active and access_key_1_last_used_date is null and access_key_1_last_rotated < (current_date - interval '45' day) + then 'alarm' + when access_key_1_active and access_key_1_last_used_date < (current_date - interval '45' day) + then 'alarm' + when access_key_2_active and access_key_2_last_used_date is null and access_key_2_last_rotated < (current_date - interval '45' day) + then 'alarm' + when access_key_2_active and access_key_2_last_used_date < (current_date - interval '45' day) + then 'alarm' + else 'ok' + end status, + user_name || + case + when not password_enabled + then ' password not enabled,' + when password_enabled and password_last_used is null + then ' password created ' || to_char(password_last_changed, 'DD-Mon-YYYY') || ' never used,' + else + ' password used ' || to_char(password_last_used, 'DD-Mon-YYYY') || ',' + end || + case + when not access_key_1_active + then ' key 1 not enabled,' + when access_key_1_active and access_key_1_last_used_date is null + then ' key 1 created ' || to_char(access_key_1_last_rotated, 'DD-Mon-YYYY') || ' never used,' + else + ' key 1 used ' || to_char(access_key_1_last_used_date, 'DD-Mon-YYYY') || ',' + end || + case + when not access_key_2_active + then ' key 2 not enabled.' + when access_key_2_active and access_key_2_last_used_date is null + then ' key 2 created ' || to_char(access_key_2_last_rotated, 'DD-Mon-YYYY') || ' never used.' + else + ' key 2 used ' || to_char(access_key_2_last_used_date, 'DD-Mon-YYYY') || '.' + end + as reason + ${local.common_dimensions_global_sql} + from + aws_iam_credential_report; + EOQ +} diff --git a/conformance_pack/kinesis.sp b/conformance_pack/kinesis.sp index 910623ff..7b3635a8 100644 --- a/conformance_pack/kinesis.sp +++ b/conformance_pack/kinesis.sp @@ -23,3 +23,41 @@ control "kinesis_stream_encrypted_with_kms_cmk" { other_checks = "true" }) } + +query "kinesis_stream_server_side_encryption_enabled" { + sql = <<-EOQ + select + stream_arn as resource, + case + when encryption_type = 'KMS' then 'ok' + else 'alarm' + end as status, + case + when encryption_type = 'KMS' then title || ' server side encryption enabled.' + else title || ' server side encryption disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_kinesis_stream; + EOQ +} + +query "kinesis_stream_encrypted_with_kms_cmk" { + sql = <<-EOQ + select + stream_arn as resource, + case + when encryption_type = 'KMS' and key_id <> 'alias/aws/kinesis' then 'ok' + else 'alarm' + end as status, + case + when encryption_type = 'KMS' and key_id <> 'alias/aws/kinesis' then title || ' encrypted with CMK.' + else title || ' not encrypted with CMK.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_kinesis_stream; + EOQ +} diff --git a/conformance_pack/kms.sp b/conformance_pack/kms.sp index 80dfaf03..d76032ef 100644 --- a/conformance_pack/kms.sp +++ b/conformance_pack/kms.sp @@ -70,3 +70,218 @@ control "kms_cmk_policy_prohibit_public_access" { other_checks = "true" }) } + +query "kms_key_not_pending_deletion" { + sql = <<-EOQ + select + + arn as resource, + case + when key_state = 'PendingDeletion' then 'alarm' + else 'ok' + end as status, + case + when key_state = 'PendingDeletion' then title || ' scheduled for deletion and will be deleted in ' || extract(day from deletion_date - current_timestamp) || ' day(s).' + else title || ' not scheduled for deletion.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_kms_key + where + key_manager = 'CUSTOMER'; + EOQ +} + +query "kms_cmk_rotation_enabled" { + sql = <<-EOQ + select + + arn as resource, + case + when origin = 'EXTERNAL' then 'skip' + when key_state = 'PendingDeletion' then 'skip' + when key_state = 'Disabled' then 'skip' + when not key_rotation_enabled then 'alarm' + else 'ok' + end as status, + case + when origin = 'EXTERNAL' then title || ' has imported key material.' + when key_state = 'PendingDeletion' then title || ' is pending deletion.' + when key_state = 'Disabled' then title || ' is disabled.' + when not key_rotation_enabled then title || ' key rotation disabled.' + else title || ' key rotation enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_kms_key + where + key_manager = 'CUSTOMER'; + EOQ +} + +query "kms_key_decryption_restricted_in_iam_customer_managed_policy" { + sql = <<-EOQ + with policy_with_decrypt_grant as ( + select + distinct arn + from + aws_iam_policy, + jsonb_array_elements(policy_std -> 'Statement') as statement + where + not is_aws_managed + and statement ->> 'Effect' = 'Allow' + and statement -> 'Resource' ?| array['*', 'arn:aws:kms:*:' || account_id || ':key/*', 'arn:aws:kms:*:' || account_id || ':alias/*'] + and statement -> 'Action' ?| array['*', 'kms:*', 'kms:decrypt', 'kms:reencryptfrom', 'kms:reencrypt*'] + ) + select + + i.arn as resource, + case + when d.arn is null then 'ok' + else 'alarm' + end as status, + case + when d.arn is null then i.title || ' doesn''t allow decryption actions on all keys.' + else i.title || ' allows decryption actions on all keys.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "i.")} + from + aws_iam_policy i + left join policy_with_decrypt_grant d on i.arn = d.arn + where + not is_aws_managed; + EOQ +} + +query "kms_key_decryption_restricted_in_iam_inline_policy" { + sql = <<-EOQ + with user_with_decrypt_grant as ( + select + distinct arn + from + aws_iam_user, + jsonb_array_elements(inline_policies_std) as inline_policy, + jsonb_array_elements(inline_policy -> 'PolicyDocument' -> 'Statement') as statement + where + statement ->> 'Effect' = 'Allow' + and statement -> 'Resource' ?| array['*', 'arn:aws:kms:*:' || account_id || ':key/*', 'arn:aws:kms:*:' || account_id || ':alias/*'] + and statement -> 'Action' ?| array['*', 'kms:*', 'kms:decrypt', 'kms:deencrypt*', 'kms:reencryptfrom'] + ), + role_with_decrypt_grant as ( + select + distinct arn + from + aws_iam_role, + jsonb_array_elements(inline_policies_std) as inline_policy, + jsonb_array_elements(inline_policy -> 'PolicyDocument' -> 'Statement') as statement + where + statement ->> 'Effect' = 'Allow' + and statement -> 'Resource' ?| array['*', 'arn:aws:kms:*:' || account_id || ':key/*', 'arn:aws:kms:*:' || account_id || ':alias/*'] + and statement -> 'Action' ?| array['*', 'kms:*', 'kms:decrypt', 'kms:deencrypt*', 'kms:reencryptfrom'] + ), + group_with_decrypt_grant as ( + select + distinct arn + from + aws_iam_group, + jsonb_array_elements(inline_policies_std) as inline_policy, + jsonb_array_elements(inline_policy -> 'PolicyDocument' -> 'Statement') as statement + where + statement ->> 'Effect' = 'Allow' + and statement -> 'Resource' ?| array['*', 'arn:aws:kms:*:' || account_id || ':key/*', 'arn:aws:kms:*:' || account_id || ':alias/*'] + and statement -> 'Action' ?| array['*', 'kms:*', 'kms:decrypt', 'kms:deencrypt*', 'kms:reencryptfrom'] + ) + select + + i.arn as resource, + case + when d.arn is null then 'ok' + else 'alarm' + end as status, + case + when d.arn is null then 'User ' || i.title || ' not allowed to perform decryption actions on all keys.' + else 'User ' || i.title || ' allowed to perform decryption actions on all keys.' + end as reason + + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "i.")} + from + aws_iam_user i + left join user_with_decrypt_grant d on i.arn = d.arn + union + select + r.arn as resource, + case + when d.arn is null then 'ok' + else 'alarm' + end as status, + case + when d.arn is null then 'Role ' || r.title || ' not allowed to perform decryption actions on all keys.' + else 'Role ' || r.title || ' allowed to perform decryption actions on all keys.' + end as reason + + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "r.")} + from + aws_iam_role r + left join role_with_decrypt_grant d on r.arn = d.arn + where + r.arn not like '%service-role/%' + union + select + g.arn as resource, + case + when d.arn is null then 'ok' + else 'alarm' + end as status, + case + when d.arn is null then 'Role ' || g.title || ' not allowed to perform decryption actions on all keys.' + else 'Group ' || g.title || ' allowed to perform decryption actions on all keys.' + end as reason + ${replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "g.")} + from + aws_iam_group g + left join group_with_decrypt_grant d on g.arn = d.arn; + EOQ +} + +query "kms_cmk_policy_prohibit_public_access" { + sql = <<-EOQ + with wildcard_action_policies as ( + select + arn, + count(*) as statements_num + from + aws_kms_key, + jsonb_array_elements(policy_std -> 'Statement') as s + where + s ->> 'Effect' = 'Allow' + and ( + ( s -> 'Principal' -> 'AWS') = '["*"]' + or s ->> 'Principal' = '*' + ) + and key_manager = 'CUSTOMER' + group by + arn + ) + select + k.arn as resource, + case + when p.arn is null then 'ok' + else 'alarm' + end status, + case + when p.arn is null then title || ' does not allow public access.' + else title || ' contains ' || coalesce(p.statements_num,0) || + ' statements that allows public access.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "k.")} + from + aws_kms_key as k + left join wildcard_action_policies as p on p.arn = k.arn + where + key_manager = 'CUSTOMER'; + EOQ +} diff --git a/conformance_pack/lambda.sp b/conformance_pack/lambda.sp index 49294909..3b9c2bda 100644 --- a/conformance_pack/lambda.sp +++ b/conformance_pack/lambda.sp @@ -97,3 +97,265 @@ control "lambda_function_tracing_enabled" { other_checks = "true" }) } + +query "lambda_function_dead_letter_queue_configured" { + sql = <<-EOQ + select + + arn as resource, + case + when dead_letter_config_target_arn is null then 'alarm' + else 'ok' + end as status, + case + when dead_letter_config_target_arn is null then title || ' configured with dead-letter queue.' + else title || ' not configured with dead-letter queue.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_lambda_function; + EOQ +} + +query "lambda_function_in_vpc" { + sql = <<-EOQ + select + + arn as resource, + case + when vpc_id is null then 'alarm' + else 'ok' + end status, + case + when vpc_id is null then title || ' is not in VPC.' + else title || ' is in VPC ' || vpc_id || '.' + end reason + + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_lambda_function; + EOQ +} + +query "lambda_function_restrict_public_access" { + sql = <<-EOQ + with wildcard_action_policies as ( + select + arn, + count(*) as statements_num + from + aws_lambda_function, + jsonb_array_elements(policy_std -> 'Statement') as s + where + s ->> 'Effect' = 'Allow' + and ( + ( s -> 'Principal' -> 'AWS') = '["*"]' + or s ->> 'Principal' = '*' + ) + group by + arn + ) + select + + f.arn as resource, + case + when p.arn is null then 'ok' + else 'alarm' + end as status, + case + when p.arn is null then title || ' does not allow public access.' + else title || ' contains ' || coalesce(p.statements_num,0) || + ' statements that allows public access.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "f.")} + from + aws_lambda_function as f + left join wildcard_action_policies as p on p.arn = f.arn; + EOQ +} + +query "lambda_function_concurrent_execution_limit_configured" { + sql = <<-EOQ + select + + arn as resource, + case + when reserved_concurrent_executions is null then 'alarm' + else 'ok' + end as status, + case + when reserved_concurrent_executions is null then title || ' function-level concurrent execution limit not configured.' + else title || ' function-level concurrent execution limit configured.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_lambda_function; + EOQ +} + +query "lambda_function_cloudtrail_logging_enabled" { + sql = <<-EOQ + with function_logging_cloudtrails as ( + select + distinct replace(replace(v::text,'"',''),'/','') as lambda_arn, + d ->> 'Type' as type + from + aws_cloudtrail_trail, + jsonb_array_elements(event_selectors) e, + jsonb_array_elements(e -> 'DataResources') as d, + jsonb_array_elements(d -> 'Values') v + where + d ->> 'Type' = 'AWS::Lambda::Function' + and replace(replace(v::text,'"',''),'/','') <> 'arn:aws:lambda' + ), function_logging_region as ( + select + region as cloudtrail_region, + replace(replace(v::text,'"',''),'/','') as lambda_arn + from + aws_cloudtrail_trail, + jsonb_array_elements(event_selectors) e, + jsonb_array_elements(e -> 'DataResources') as d, + jsonb_array_elements(d -> 'Values') v + where + d ->> 'Type' = 'AWS::Lambda::Function' + and replace(replace(v::text,'"',''),'/','') = 'arn:aws:lambda' + group by + region, + lambda_arn + ), + function_logging_region_advance_es as ( + select + region as cloudtrail_region + from + aws_cloudtrail_trail, + jsonb_array_elements(advanced_event_selectors) a, + jsonb_array_elements(a -> 'FieldSelectors') as f, + jsonb_array_elements_text(f -> 'Equals') e + where + e = 'AWS::Lambda::Function' + and f ->> 'Field' != 'eventCategory' + group by + region + ) + select + distinct l.arn as resource, + case + when (l.arn = c.lambda_arn) + or (r.lambda_arn = 'arn:aws:lambda' and r.cloudtrail_region = l.region ) + or a.cloudtrail_region = l.region then 'ok' + else 'alarm' + end as status, + case + when (l.arn = c.lambda_arn) + or (r.lambda_arn = 'arn:aws:s3' and r.cloudtrail_region = l.region ) + or a.cloudtrail_region = l.region then l.name || ' logging enabled.' + else l.name || ' logging not enabled.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "l.")} + from + aws_lambda_function as l + left join function_logging_cloudtrails as c on l.arn = c.lambda_arn + left join function_logging_region as r on r.cloudtrail_region = l.region + left join function_logging_region_advance_es as a on a.cloudtrail_region = l.region; + EOQ +} + +query "lambda_function_tracing_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when tracing_config ->> 'Mode' = 'PassThrough' then 'alarm' + else 'ok' + end as status, + case + when tracing_config ->> 'Mode' = 'PassThrough' then title || ' has tracing disabled.' + else title || ' has tracing enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_lambda_function; + EOQ +} + +# Non-Config rule query + +query "lambda_function_cors_configuration" { + sql = <<-EOQ + select + arn as resource, + case + when url_config is null then 'info' + when url_config -> 'Cors' ->> 'AllowOrigins' = '["*"]' then 'alarm' + else 'ok' + end as status, + case + when url_config is null then title || ' does not has a URL config.' + when url_config -> 'Cors' ->> 'AllowOrigins' = '["*"]' then title || ' CORS configuration allow all origins.' + else title || ' CORS configuration does not allow all origins.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_lambda_function; + EOQ +} + +query "lambda_function_multiple_az_configured" { + sql = <<-EOQ + select + arn as resource, + case + when vpc_id is null then 'skip' + else case + when + ( + select + count(distinct availability_zone_id) + from + aws_vpc_subnet + where + subnet_id in (select jsonb_array_elements_text(vpc_subnet_ids) ) + ) >= 2 + then 'ok' + else 'alarm' + end + end as status, + case + when vpc_id is null then title || ' is not in VPC.' + else title || ' has ' || jsonb_array_length(vpc_subnet_ids) || ' availability zone(s).' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_lambda_function; + EOQ +} + +query "lambda_function_use_latest_runtime" { + sql = <<-EOQ + select + arn as resource, + case + when package_type <> 'Zip' then 'skip' + when runtime in ('nodejs16.x', 'nodejs14.x', 'nodejs12.x', 'nodejs10.x', 'python3.9', 'python3.8', 'python3.7', 'python3.6', 'ruby2.5', 'ruby2.7', 'java11', 'java8', 'java8.al2', 'go1.x', 'dotnetcore2.1', 'dotnetcore3.1', 'dotnet6') then 'ok' + else 'alarm' + end as status, + case + when package_type <> 'Zip' then title || ' package type is ' || package_type || '.' + when runtime in ('nodejs16.x', 'nodejs14.x', 'nodejs12.x', 'nodejs10.x', 'python3.9', 'python3.8', 'python3.7', 'python3.6', 'ruby2.5', 'ruby2.7', 'java11', 'java8', 'java8.al2', 'go1.x', 'dotnetcore2.1', 'dotnetcore3.1', 'dotnet6') then title || ' uses latest runtime - ' || runtime || '.' + else title || ' uses ' || runtime || ' which is not the latest version.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_lambda_function; + EOQ +} + diff --git a/conformance_pack/manual_control.sp b/conformance_pack/manual_control.sp index 72045ac9..c73c1e4b 100644 --- a/conformance_pack/manual_control.sp +++ b/conformance_pack/manual_control.sp @@ -3,3 +3,15 @@ control "manual_control" { description = "Manual verification is required." query = query.manual_control } + +query "manual_control" { + sql = <<-EOQ + select + 'arn:' || partition || ':::' || account_id as resource, + 'info' as status, + 'Manual verification required.' as reason + ${local.common_dimensions_global_sql} + from + aws_account; + EOQ +} \ No newline at end of file diff --git a/conformance_pack/networkfirewall.sp b/conformance_pack/networkfirewall.sp new file mode 100644 index 00000000..3545432f --- /dev/null +++ b/conformance_pack/networkfirewall.sp @@ -0,0 +1,84 @@ +# Non-Config rule query + +query "networkfirewall_firewall_policy_default_stateless_action_check_fragmented_packets" { + sql = <<-EOQ + select + arn as resource, + case + when (not (firewall_policy -> 'StatelessFragmentDefaultActions') ? 'aws:drop' + and not (firewall_policy -> 'StatelessFragmentDefaultActions') ? 'aws:forward_to_sfe') then 'alarm' + else 'ok' + end as status, + case + when (not (firewall_policy -> 'StatelessFragmentDefaultActions') ? 'aws:drop' + and not (firewall_policy -> 'StatelessFragmentDefaultActions') ? 'aws:forward_to_sfe') then title || ' stateless action is neither drop nor forward for fragmented packets.' + else title || ' stateless action is either drop or forward for fragmented packets.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_networkfirewall_firewall_policy; + EOQ +} + +query "networkfirewall_firewall_policy_default_stateless_action_check_full_packets" { + sql = <<-EOQ + select + arn as resource, + case + when (not (firewall_policy -> 'StatelessDefaultActions') ? 'aws:drop' + and not (firewall_policy -> 'StatelessDefaultActions') ? 'aws:forward_to_sfe') then 'alarm' + else 'ok' + end as status, + case + when (not (firewall_policy -> 'StatelessDefaultActions') ? 'aws:drop' + and not (firewall_policy -> 'StatelessDefaultActions') ? 'aws:forward_to_sfe') then title || ' stateless action is neither drop nor forward for full packets.' + else title || ' stateless action is either drop or forward for full packets.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_networkfirewall_firewall_policy; + EOQ +} + +query "networkfirewall_firewall_policy_rule_group_not_empty" { + sql = <<-EOQ + select + arn as resource, + case + when (firewall_policy ->> 'StatefulRuleGroupReferences' is null or jsonb_array_length(firewall_policy -> 'StatefulRuleGroupReferences') = 0) + and (firewall_policy ->> 'StatelessRuleGroupReferences' is null or jsonb_array_length(firewall_policy -> 'StatelessRuleGroupReferences') = 0) then 'alarm' + else 'ok' + end as status, + case + when (firewall_policy ->> 'StatefulRuleGroupReferences' is null or jsonb_array_length(firewall_policy -> 'StatefulRuleGroupReferences') = 0) + and (firewall_policy ->> 'StatelessRuleGroupReferences' is null or jsonb_array_length(firewall_policy -> 'StatelessRuleGroupReferences') = 0) then title || ' has no associated rule groups.' + else title || ' has associated rule groups.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_networkfirewall_firewall_policy; + EOQ +} + +query "networkfirewall_stateless_rule_group_not_empty" { + sql = <<-EOQ + select + arn as resource, + case + when type = 'STATEFUL' then 'skip' + when jsonb_array_length(rules_source -> 'StatelessRulesAndCustomActions' -> 'StatelessRules') > 0 then 'ok' + else 'alarm' + end as status, + case + when type = 'STATEFUL' then title || ' is a stateful rule group.' + else title || ' has ' || jsonb_array_length(rules_source -> 'StatelessRulesAndCustomActions' -> 'StatelessRules') || ' rule(s).' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_networkfirewall_rule_group; + EOQ +} diff --git a/conformance_pack/opensearch.sp b/conformance_pack/opensearch.sp new file mode 100644 index 00000000..9d79a023 --- /dev/null +++ b/conformance_pack/opensearch.sp @@ -0,0 +1,82 @@ +# Non-Config rule query + +query "opensearch_domain_encryption_at_rest_enabled" { + sql = <<-EOQ + select + + arn as resource, + case + when encryption_at_rest_options ->> 'Enabled' = 'false' then 'alarm' + else 'ok' + end status, + case + when encryption_at_rest_options ->> 'Enabled' = 'false' then title || ' encryption at rest not enabled.' + else title || ' encryption at rest enabled.' + end reason + + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_opensearch_domain; + EOQ +} + +query "opensearch_domain_fine_grained_access_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when advanced_security_options is null or not (advanced_security_options -> 'Enabled')::boolean then 'alarm' + else 'ok' + end as status, + case + when advanced_security_options is null or not (advanced_security_options -> 'Enabled')::boolean then title || ' having fine-grained access control disabled.' + else title || ' having fine-grained access control enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_opensearch_domain; + EOQ +} + +query "opensearch_domain_in_vpc" { + sql = <<-EOQ + with public_subnets as ( + select + distinct a -> 'SubnetId' as SubnetId + from + aws_vpc_route_table as t, + jsonb_array_elements(associations) as a, + jsonb_array_elements(routes) as r + where + r ->> 'DestinationCidrBlock' = '0.0.0.0/0' + and r ->> 'GatewayId' like 'igw-%' + ), opensearch_domain_with_public_subnet as ( + select + arn + from + aws_opensearch_domain , + jsonb_array_elements(vpc_options -> 'SubnetIds') as s + where + s in (select SubnetId from public_subnets) + ) + select + d.arn as resource, + case + when d.vpc_options ->> 'VPCId' is null then 'alarm' + when d.vpc_options ->> 'VPCId' is not null and p.arn is not null then 'alarm' + else 'ok' + end status, + case + when vpc_options ->> 'VPCId' is null then title || ' not in VPC.' + when d.vpc_options ->> 'VPCId' is not null and p.arn is not null then title || ' attached to public subnet.' + else title || ' in VPC ' || (vpc_options ->> 'VPCId') || '.' + end reason + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "d.")} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "d.")} + from + aws_opensearch_domain as d left join opensearch_domain_with_public_subnet as p + on d.arn = p.arn; + EOQ +} diff --git a/conformance_pack/rds.sp b/conformance_pack/rds.sp index 634aedcc..9765483b 100644 --- a/conformance_pack/rds.sp +++ b/conformance_pack/rds.sp @@ -306,3 +306,768 @@ control "rds_db_instance_ca_certificate_expires_7_days" { other_checks = "true" }) } + +query "rds_db_instance_backup_enabled" { + sql = <<-EOQ + select + + arn as resource, + case + when backup_retention_period < 1 then 'alarm' + else 'ok' + end as status, + case + when backup_retention_period < 1 then title || ' backups not enabled.' + else title || ' backups enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_instance; + EOQ +} + +query "rds_db_instance_encryption_at_rest_enabled" { + sql = <<-EOQ + select + + arn as resource, + case + when storage_encrypted then 'ok' + else 'alarm' + end as status, + case + when storage_encrypted then title || ' encrypted at rest.' + else title || ' not encrypted at rest.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_instance; + EOQ +} + +query "rds_db_instance_multiple_az_enabled" { + sql = <<-EOQ + select + + arn as resource, + case + when engine ilike any (array ['%aurora-mysql%', '%aurora-postgres%']) then 'skip' + when multi_az then 'ok' + else 'alarm' + end as status, + case + when engine ilike any (array ['%aurora-mysql%', '%aurora-postgres%']) then title || ' cluster instance.' + when multi_az then title || ' Multi-AZ enabled.' + else title || ' Multi-AZ disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_instance; + EOQ +} + +query "rds_db_instance_prohibit_public_access" { + sql = <<-EOQ + select + + arn as resource, + case + when publicly_accessible then 'alarm' + else 'ok' + end status, + case + when publicly_accessible then title || ' publicly accessible.' + else title || ' not publicly accessible.' + end reason + + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_instance; + EOQ +} + +query "rds_db_snapshot_encrypted_at_rest" { + sql = <<-EOQ + ( + select + + arn as resource, + case + when storage_encrypted then 'ok' + else 'alarm' + end as status, + case + when storage_encrypted then title || ' encrypted at rest.' + else title || ' not encrypted at rest.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_cluster_snapshot + ) + union + ( + select + + arn as resource, + case + when encrypted then 'ok' + else 'alarm' + end as status, + case + when encrypted then title || ' encrypted at rest.' + else title || ' not encrypted at rest.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_snapshot + ); + EOQ +} + +query "rds_db_snapshot_prohibit_public_access" { + sql = <<-EOQ + ( + select + + arn as resource, + case + when cluster_snapshot -> 'AttributeValues' = '["all"]' then 'alarm' + else 'ok' + end status, + case + when cluster_snapshot -> 'AttributeValues' = '["all"]' then title || ' publicly restorable.' + else title || ' not publicly restorable.' + end reason + + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_cluster_snapshot, + jsonb_array_elements(db_cluster_snapshot_attributes) as cluster_snapshot + ) + union + ( + select + + arn as resource, + case + when database_snapshot -> 'AttributeValues' = '["all"]' then 'alarm' + else 'ok' + end status, + case + when database_snapshot -> 'AttributeValues' = '["all"]' then title || ' publicly restorable.' + else title || ' not publicly restorable.' + end reason + + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_snapshot, + jsonb_array_elements(db_snapshot_attributes) as database_snapshot + ); + EOQ +} + +query "rds_db_instance_logging_enabled" { + sql = <<-EOQ + select + + arn as resource, + engine, + case + when engine like any (array ['mariadb', '%mysql']) and enabled_cloudwatch_logs_exports ?& array ['audit','error','general','slowquery'] then 'ok' + when engine like any (array['%postgres%']) and enabled_cloudwatch_logs_exports ?& array ['postgresql','upgrade'] then 'ok' + when engine like 'oracle%' and enabled_cloudwatch_logs_exports ?& array ['alert','audit', 'trace','listener'] then 'ok' + when engine = 'sqlserver-ex' and enabled_cloudwatch_logs_exports ?& array ['error'] then 'ok' + when engine like 'sqlserver%' and enabled_cloudwatch_logs_exports ?& array ['error','agent'] then 'ok' + else 'alarm' + end as status, + case + when engine like any (array ['mariadb', '%mysql']) and enabled_cloudwatch_logs_exports ?& array ['audit','error','general','slowquery'] + then title || ' ' || engine || ' logging enabled.' + when engine like any (array['%postgres%']) and enabled_cloudwatch_logs_exports ?& array ['postgresql','upgrade'] + then title || ' ' || engine || ' logging enabled.' + when engine like 'oracle%' and enabled_cloudwatch_logs_exports ?& array ['alert','audit', 'trace','listener'] + then title || ' ' || engine || ' logging enabled.' + when engine = 'sqlserver-ex' and enabled_cloudwatch_logs_exports ?& array ['error'] + then title || ' ' || engine || ' logging enabled.' + when engine like 'sqlserver%' and enabled_cloudwatch_logs_exports ?& array ['error','agent'] + then title || ' ' || engine || ' logging enabled.' + else title || ' logging not enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_instance; + EOQ +} + +query "rds_db_instance_in_backup_plan" { + sql = <<-EOQ + with mapped_with_id as ( + select + jsonb_agg(elems) as mapped_ids + from + aws_backup_selection, + jsonb_array_elements(resources) as elems + group by backup_plan_id + ), + mapped_with_tags as ( + select + jsonb_agg(elems ->> 'ConditionKey') as mapped_tags + from + aws_backup_selection, + jsonb_array_elements(list_of_tags) as elems + group by backup_plan_id + ), + backed_up_instance as ( + select + i.db_instance_identifier + from + aws_rds_db_instance as i + join mapped_with_id as t on t.mapped_ids ?| array[i.arn] + union + select + i.db_instance_identifier + from + aws_rds_db_instance as i + join mapped_with_tags as t on t.mapped_tags ?| array(select jsonb_object_keys(tags)) + ) + select + + i.arn as resource, + case + when b.db_instance_identifier is null then 'alarm' + else 'ok' + end as status, + case + when b.db_instance_identifier is null then i.title || ' not in backup plan.' + else i.title || ' in backup plan.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "i.")} + from + aws_rds_db_instance as i + left join backed_up_instance as b on i.db_instance_identifier = b.db_instance_identifier; + EOQ +} + +query "rds_db_instance_and_cluster_enhanced_monitoring_enabled" { + sql = <<-EOQ + ( + select + + arn as resource, + case + when enabled_cloudwatch_logs_exports is not null then 'ok' + else 'alarm' + end as status, + case + when enabled_cloudwatch_logs_exports is not null then title || ' enhanced monitoring enabled.' + else title || ' enhanced monitoring not enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_cluster + ) + union + ( + select + + arn as resource, + case + when class = 'db.m1.small' then 'skip' + when enhanced_monitoring_resource_arn is not null then 'ok' + else 'alarm' + end as status, + case + when class = 'db.m1.small' then title || ' enhanced monitoring not supported.' + when enhanced_monitoring_resource_arn is not null then title || ' enhanced monitoring enabled.' + else title || ' enhanced monitoring not enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_instance + ); + EOQ +} + +query "rds_db_instance_deletion_protection_enabled" { + sql = <<-EOQ + select + + arn as resource, + case + when engine like any(array['aurora%', 'docdb', 'neptune']) then 'skip' + when deletion_protection then 'ok' + else 'alarm' + end status, + case + when engine like any(array['aurora%', 'docdb', 'neptune']) then title || ' has engine ' || engine || ' cluster, deletion protection is set at cluster level.' + when deletion_protection then title || ' deletion protection enabled.' + else title || ' deletion protection not enabled.' + end reason + + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_instance; + EOQ +} + +query "rds_db_instance_iam_authentication_enabled" { + sql = <<-EOQ + select + + arn as resource, + case + when iam_database_authentication_enabled then 'ok' + else 'alarm' + end as status, + case + when iam_database_authentication_enabled then title || ' IAM authentication enabled.' + else title || ' IAM authentication not enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_instance; + EOQ +} + +query "rds_db_cluster_iam_authentication_enabled" { + sql = <<-EOQ + select + + arn as resource, + case + when iam_database_authentication_enabled then 'ok' + else 'alarm' + end as status, + case + when iam_database_authentication_enabled then title || ' IAM authentication enabled.' + else title || ' IAM authentication not enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_cluster; + EOQ +} + +query "rds_db_cluster_aurora_protected_by_backup_plan" { + sql = <<-EOQ + with backup_protected_cluster as ( + select + resource_arn as arn + from + aws_backup_protected_resource as b + where + resource_type = 'Aurora' + ) + select + + c.arn as resource, + case + when c.engine not like '%aurora%' then 'skip' + when b.arn is not null then 'ok' + else 'alarm' + end as status, + case + when c.engine not like '%aurora%' then c.title || ' not Aurora resources.' + when b.arn is not null then c.title || ' is protected by backup plan.' + else c.title || ' is not protected by backup plan.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "c.")} + from + aws_rds_db_cluster as c + left join backup_protected_cluster as b on c.arn = b.arn; + EOQ +} + +query "rds_db_instance_protected_by_backup_plan" { + sql = <<-EOQ + with backup_protected_rds_isntance as ( + select + resource_arn as arn + from + aws_backup_protected_resource as b + where + resource_type = 'RDS' + ) + select + + r.arn as resource, + case + when b.arn is not null then 'ok' + else 'alarm' + end as status, + case + when b.arn is not null then r.title || ' is protected by backup plan.' + else r.title || ' is not protected by backup plan.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "r.")} + from + aws_rds_db_instance as r + left join backup_protected_rds_isntance as b on r.arn = b.arn; + EOQ +} + +query "rds_db_instance_automatic_minor_version_upgrade_enabled" { + sql = <<-EOQ + select + + arn as resource, + case + when auto_minor_version_upgrade then 'ok' + else 'alarm' + end as status, + case + when auto_minor_version_upgrade then title || ' automatic minor version upgrades enabled.' + else title || ' automatic minor version upgrades not enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_instance; + EOQ +} + +query "rds_db_cluster_deletion_protection_enabled" { + sql = <<-EOQ + select + + db_cluster_identifier as resource, + case + when deletion_protection then 'ok' + else 'alarm' + end as status, + case + when deletion_protection then title || ' deletion protection enabled.' + else title || ' deletion protection not enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_cluster; + EOQ +} + +query "rds_db_instance_cloudwatch_logs_enabled" { + sql = <<-EOQ + select + + arn as resource, + case + when enabled_cloudwatch_logs_exports is not null then 'ok' + else 'alarm' + end as status, + case + when enabled_cloudwatch_logs_exports is not null then title || ' integrated with CloudWatch logs.' + else title || ' not integrated with CloudWatch logs.' + end reason + + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_instance; + EOQ +} + +query "rds_db_instance_ca_certificate_expires_7_days" { + sql = <<-EOQ + select + + arn as resource, + case + when extract(day from (to_timestamp(certificate ->> 'ValidTill','YYYY-MM-DDTHH:MI:SS')) - current_timestamp) <= '7' then 'alarm' + else 'ok' + end as status, + title || ' expires ' || to_char(to_timestamp(certificate ->> 'ValidTill','YYYY-MM-DDTHH:MI:SS'), 'DD-Mon-YYYY') || + ' (' || extract(day from (to_timestamp(certificate ->> 'ValidTill','YYYY-MM-DDTHH:MI:SS')) - current_timestamp) || ' days).' + as reason + + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_instance; + EOQ +} + +# Non-Config rule query + +query "rds_db_cluster_aurora_backtracking_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when engine not ilike '%aurora-mysql%' then 'skip' + when backtrack_window is not null then 'ok' + else 'alarm' + end as status, + case + when engine not ilike '%aurora-mysql%' then title || ' not Aurora MySQL-compatible edition.' + when backtrack_window is not null then title || ' backtracking enabled.' + else title || ' backtracking not enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_cluster; + EOQ +} + +query "rds_db_cluster_copy_tags_to_snapshot_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when copy_tags_to_snapshot then 'ok' + else 'alarm' + end as status, + case + when copy_tags_to_snapshot then title || ' copy tags to snapshot enabled.' + else title || ' copy tags to snapshot disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_cluster; + EOQ +} + +query "rds_db_cluster_events_subscription" { + sql = <<-EOQ + select + arn as resource, + case + when source_type <> 'db-cluster' then 'skip' + when source_type = 'db-cluster' and enabled and event_categories_list @> '["failure", "maintenance"]' then 'ok' + else 'alarm' + end as status, + case + when source_type <> 'db-cluster' then cust_subscription_id || ' event subscription of ' || source_type || ' type.' + when source_type = 'db-cluster' and enabled and event_categories_list @> '["failure", "maintenance"]' then cust_subscription_id || ' event subscription enabled for critical db cluster events.' + else cust_subscription_id || ' event subscription missing critical db cluster events.' + end as reason + ${local.common_dimensions_sql} + from + aws_rds_db_event_subscription; + EOQ +} + +query "rds_db_cluster_multiple_az_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when multi_az then 'ok' + else 'alarm' + end as status, + case + when multi_az then title || ' Multi-AZ enabled.' + else title || ' Multi-AZ disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_cluster; + EOQ +} + +query "rds_db_cluster_no_default_admin_name" { + sql = <<-EOQ + select + arn as resource, + case + when master_user_name in ('admin','postgres') then 'alarm' + else 'ok' + end status, + case + when master_user_name in ('admin','postgres') then title || ' using default master user name.' + else title || ' not using default master user name.' + end reason + + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_cluster; + EOQ +} + +query "rds_db_instance_and_cluster_no_default_port" { + sql = <<-EOQ + ( + select + arn as resource, + case + when engine similar to '%(aurora|mysql|mariadb)%' and port = '3306' then 'alarm' + when engine like '%postgres%' and port = '5432' then 'alarm' + when engine like 'oracle%' and port = '1521' then 'alarm' + when engine like 'sqlserver%' and port = '1433' then 'alarm' + else 'ok' + end as status, + case + when engine similar to '%(aurora|mysql|mariadb)%' and port = '3306' then title || ' ' || engine || ' uses a default port.' + when engine like '%postgres%' and port = '5432' then title || ' ' || engine || ' uses a default port.' + when engine like 'oracle%' and port = '1521' then title || ' ' || engine || ' uses a default port.' + when engine like 'sqlserver%' and port = '1433' then title || ' ' || engine || ' uses a default port.' + else title || ' doesnt use a default port.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_cluster + ) + union + ( + select + arn as resource, + case + when engine similar to '%(aurora|mysql|mariadb)%' and port = '3306' then 'alarm' + when engine like '%postgres%' and port = '5432' then 'alarm' + when engine like 'oracle%' and port = '1521' then 'alarm' + when engine like 'sqlserver%' and port = '1433' then 'alarm' + else 'ok' + end as status, + case + when engine similar to '%(aurora|mysql|mariadb)%' and port = '3306' then title || ' ' || engine || ' uses a default port.' + when engine like '%postgres%' and port = '5432' then title || ' ' || engine || ' uses a default port.' + when engine like 'oracle%' and port = '1521' then title || ' ' || engine || ' uses a default port.' + when engine like 'sqlserver%' and port = '1433' then title || ' ' || engine || ' uses a default port.' + else title || ' doesnt use a default port.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_instance + ); + EOQ +} + +query "rds_db_instance_copy_tags_to_snapshot_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when copy_tags_to_snapshot then 'ok' + else 'alarm' + end as status, + case + when copy_tags_to_snapshot then title || ' copy tags to snapshot enabled.' + else title || ' copy tags to snapshot disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_instance; + EOQ +} + +query "rds_db_instance_events_subscription" { + sql = <<-EOQ + select + arn as resource, + case + when source_type <> 'db-instance' then 'skip' + when source_type = 'db-instance' and enabled and event_categories_list @> '["failure", "maintenance", "configuration change"]' then 'ok' + else 'alarm' + end as status, + case + when source_type <> 'db-instance' then cust_subscription_id || ' event subscription of ' || source_type || ' type.' + when source_type like 'db-instance' and enabled and event_categories_list @> '["failure", "maintenance", "configuration change"]' then cust_subscription_id || ' event subscription enabled for critical instance events.' + else cust_subscription_id || ' event subscription missing critical instance events.' + end as reason + ${local.common_dimensions_sql} + from + aws_rds_db_event_subscription; + EOQ +} + +query "rds_db_instance_in_vpc" { + sql = <<-EOQ + select + arn as resource, + case + when vpc_id is null then 'alarm' + else 'ok' + end as status, + case + when vpc_id is null then title || ' is not in VPC.' + else title || ' is in VPC ' || vpc_id || '.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_instance; + EOQ +} + +query "rds_db_instance_no_default_admin_name" { + sql = <<-EOQ + select + arn as resource, + case + when master_user_name in ('admin','postgres') then 'alarm' + else 'ok' + end status, + case + when master_user_name in ('admin','postgres') then title || ' using default master user name.' + else title || ' not using default master user name.' + end reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_rds_db_instance; + EOQ +} + +query "rds_db_parameter_group_events_subscription" { + sql = <<-EOQ + select + arn as resource, + case + when source_type <> 'db-parameter-group' then 'skip' + when source_type = 'db-parameter-group' and enabled and event_categories_list @> '["maintenance", "failure"]' then 'ok' + else 'alarm' + end as status, + case + when source_type <> 'db-parameter-group' then cust_subscription_id || ' event subscription of ' || source_type || ' type.' + when source_type = 'db-parameter-group' and enabled and event_categories_list @> '["configuration change"]' then cust_subscription_id || ' event subscription enabled for critical database parameter group events.' + else cust_subscription_id || ' event subscription missing critical database parameter group events.' + end as reason + ${local.common_dimensions_sql} + from + aws_rds_db_event_subscription; + EOQ +} + +query "rds_db_security_group_events_subscription" { + sql = <<-EOQ + select + arn as resource, + case + when source_type <> 'db-security-group' then 'skip' + when source_type = 'db-security-group' and enabled and event_categories_list @> '["failure", "configuration change"]' then 'ok' + else 'alarm' + end as status, + case + when source_type <> 'db-security-group' then cust_subscription_id || ' event subscription of ' || source_type || ' type.' + when source_type = 'db-security-group' and enabled and event_categories_list @> '["failure", "configuration change"]' then cust_subscription_id || ' event subscription enabled for critical database security group events.' + else cust_subscription_id || ' event subscription missing critical database security group events.' + end as reason + ${local.common_dimensions_sql} + from + aws_rds_db_event_subscription; + EOQ +} diff --git a/conformance_pack/redshift.sp b/conformance_pack/redshift.sp index 67a5bd45..a978a059 100644 --- a/conformance_pack/redshift.sp +++ b/conformance_pack/redshift.sp @@ -134,3 +134,234 @@ control "redshift_cluster_enhanced_vpc_routing_enabled" { nist_800_53_rev_5 = "true" }) } + +query "redshift_cluster_encryption_in_transit_enabled" { + sql = <<-EOQ + with pg_with_ssl as ( + select + name as pg_name, + p ->> 'ParameterName' as parameter_name, + p ->> 'ParameterValue' as parameter_value + from + aws_redshift_parameter_group, + jsonb_array_elements(parameters) as p + where + p ->> 'ParameterName' = 'require_ssl' + and p ->> 'ParameterValue' = 'true' + ) + select + + 'arn:aws:redshift:' || region || ':' || account_id || ':' || 'cluster' || ':' || cluster_identifier as resource, + case + when cpg ->> 'ParameterGroupName' in (select pg_name from pg_with_ssl ) then 'ok' + else 'alarm' + end as status, + case + when cpg ->> 'ParameterGroupName' in (select pg_name from pg_with_ssl ) then title || ' encryption in transit enabled.' + else title || ' encryption in transit disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_redshift_cluster, + jsonb_array_elements(cluster_parameter_groups) as cpg; + EOQ +} + +query "redshift_cluster_encryption_logging_enabled" { + sql = <<-EOQ + select + + arn as resource, + case + when not encrypted then 'alarm' + when not (logging_status ->> 'LoggingEnabled') :: boolean then 'alarm' + else 'ok' + end as status, + case + when not encrypted then title || ' not encrypted.' + when not (logging_status ->> 'LoggingEnabled') :: boolean then title || ' audit logging not enabled.' + else title || ' audit logging and encryption enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_redshift_cluster; + EOQ +} + +query "redshift_cluster_prohibit_public_access" { + sql = <<-EOQ + select + + cluster_namespace_arn as resource, + case + when publicly_accessible then 'alarm' + else 'ok' + end status, + case + when publicly_accessible then title || ' publicly accessible.' + else title || ' not publicly accessible.' + end reason + + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_redshift_cluster; + EOQ +} + +query "redshift_cluster_automatic_snapshots_min_7_days" { + sql = <<-EOQ + select + 'arn:aws:redshift:' || region || ':' || account_id || ':' || 'cluster' || ':' || cluster_identifier as resource, + case + when automated_snapshot_retention_period >= 7 then 'ok' + else 'alarm' + end as status, + case + when automated_snapshot_retention_period >= 7 then title || ' automatic snapshots enabled with retention period greater than equals 7 days.' + else title || ' automatic snapshots not enabled with retention period greater than equals 7 days.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_redshift_cluster; + EOQ +} + +query "redshift_cluster_kms_enabled" { + sql = <<-EOQ + select + 'arn:aws:redshift:' || region || ':' || account_id || ':' || 'cluster' || ':' || cluster_identifier as resource, + case + when encrypted and kms_key_id is not null then 'ok' + else 'alarm' + end as status, + case + when encrypted and kms_key_id is not null then title || ' encrypted with KMS.' + else title || ' not encrypted with KMS' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_redshift_cluster; + EOQ +} + +query "redshift_cluster_maintenance_settings_check" { + sql = <<-EOQ + select + 'arn:aws:redshift:' || region || ':' || account_id || ':' || 'cluster' || ':' || cluster_identifier as resource, + case + when allow_version_upgrade and automated_snapshot_retention_period >= 7 then 'ok' + else 'alarm' + end as status, + case + when allow_version_upgrade and automated_snapshot_retention_period >= 7 then title || ' has the required maintenance settings.' + else title || ' does not have required maintenance settings.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_redshift_cluster; + EOQ +} + +query "redshift_cluster_enhanced_vpc_routing_enabled" { + sql = <<-EOQ + select + 'arn:aws:redshift:' || region || ':' || account_id || ':' || 'cluster' || ':' || cluster_identifier as resource, + case + when enhanced_vpc_routing then 'ok' + else 'alarm' + end as status, + case + when enhanced_vpc_routing then title || ' enhanced VPC routing enabled.' + else title || ' enhanced VPC routing disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_redshift_cluster; + EOQ +} + +# Non-Config rule query + +query "redshift_cluster_automatic_upgrade_major_versions_enabled" { + sql = <<-EOQ + select + 'arn:aws:redshift:' || region || ':' || account_id || ':' || 'cluster' || ':' || cluster_identifier as resource, + case + when allow_version_upgrade then 'ok' + else 'alarm' + end as status, + case + when allow_version_upgrade then title || ' automatic upgrades to major versions enabled.' + else title || ' automatic upgrades to major versions disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_redshift_cluster; + EOQ +} + +query "redshift_cluster_logging_enabled" { + sql = <<-EOQ + select + 'arn:aws:redshift:' || region || ':' || account_id || ':' || 'cluster' || ':' || cluster_identifier as resource, + case + when logging_status ->> 'LoggingEnabled' = 'true' then 'ok' + else 'alarm' + end as status, + case + when logging_status ->> 'LoggingEnabled' = 'true' then title || ' logging enabled.' + else title || ' logging disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_redshift_cluster; + EOQ +} + +query "redshift_cluster_no_default_admin_name" { + sql = <<-EOQ + select + arn as resource, + case + when master_username = 'awsuser' then 'alarm' + else 'ok' + end status, + case + when master_username = 'awsuser' then title || ' using default master user name.' + else title || ' not using default master user name.' + end reason + + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_redshift_cluster; + EOQ +} + +query "redshift_cluster_no_default_database_name" { + sql = <<-EOQ + select + arn as resource, + case + when db_name = 'dev' then 'alarm' + else 'ok' + end as status, + case + when db_name = 'dev' then title || ' using default database name.' + else title || ' not using default database name.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_redshift_cluster; + EOQ +} diff --git a/conformance_pack/route53.sp b/conformance_pack/route53.sp index c54fcaa6..23965e01 100644 --- a/conformance_pack/route53.sp +++ b/conformance_pack/route53.sp @@ -73,3 +73,136 @@ control "route53_domain_transfer_lock_enabled" { other_checks = "true" }) } + +query "route53_zone_query_logging_enabled" { + sql = <<-EOQ + select + id as resource, + case + when private_zone then 'skip' + when query_logging_configs is not null or jsonb_array_length(query_logging_configs) > 0 then 'ok' + else 'alarm' + end as status, + case + when private_zone then title || ' is private hosted zone.' + when query_logging_configs is not null or jsonb_array_length(query_logging_configs) > 0 then title || ' query logging to CloudWatch enabled.' + else title || ' query logging to CloudWatch disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_route53_zone; + EOQ +} + +query "route53_domain_transfer_lock_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when transfer_lock then 'ok' + else 'alarm' + end as status, + case + when transfer_lock then title || ' transfer lock enabled.' + else title || ' transfer lock disabled.' + end reason + + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_route53_domain; + EOQ +} + +query "route53_domain_expires_30_days" { + sql = <<-EOQ + select + arn as resource, + case + when date(expiration_date) - date(current_date) >= 30 then 'ok' + else 'alarm' + end as status, + title || ' set to expire in ' || extract(day from expiration_date - current_date) || ' days.' as reason + + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_route53_domain; + EOQ +} + +query "route53_domain_expires_7_days" { + sql = <<-EOQ + select + arn as resource, + case + when date(expiration_date) - date(current_date) >= 7 then 'ok' + else 'alarm' + end as status, + title || ' set to expire in ' || extract(day from expiration_date - current_date) || ' days.' as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_route53_domain; + EOQ +} + +query "route53_domain_not_expired" { + sql = <<-EOQ + select + arn as resource, + case + when expiration_date < (current_date - interval '1' minute) then 'alarm' + else 'ok' + end as status, + case + when expiration_date < (current_date - interval '1' minute) then title || ' expired on ' || to_char(expiration_date, 'DD-Mon-YYYY') || '.' + else title || ' set to expire in ' || extract(day from expiration_date - current_date) || ' days.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_route53_domain; + EOQ +} + +query "route53_domain_privacy_protection_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when admin_privacy then 'ok' + else 'alarm' + end as status, + case + when admin_privacy then title || ' privacy protection enabled.' + else title || ' privacy protection disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_route53_domain; + EOQ +} + +# Non-Config rule query + +query "route53_domain_auto_renew_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when auto_renew then 'ok' + else 'alarm' + end as status, + case + when auto_renew then title || ' auto renew enabled.' + else title || ' auto renew disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_route53_domain; + EOQ +} diff --git a/conformance_pack/s3.sp b/conformance_pack/s3.sp index 46ae2921..f97c5353 100644 --- a/conformance_pack/s3.sp +++ b/conformance_pack/s3.sp @@ -280,3 +280,730 @@ control "s3_bucket_object_logging_enabled" { other_checks = "true" }) } + +query "s3_bucket_cross_region_replication_enabled" { + sql = <<-EOQ + with bucket_with_replication as ( + select + name, + r ->> 'Status' as rep_status + from + aws_s3_bucket, + jsonb_array_elements(replication -> 'Rules' ) as r + ) + select + b.arn as resource, + case + when b.name = r.name and r.rep_status = 'Enabled' then 'ok' + else 'alarm' + end as status, + case + when b.name = r.name and r.rep_status = 'Enabled' then b.title || ' enabled with cross-region replication.' + else b.title || ' not enabled with cross-region replication.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "b.")} + from + aws_s3_bucket b + left join bucket_with_replication r on b.name = r.name; + EOQ +} + +query "s3_bucket_default_encryption_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when server_side_encryption_configuration is not null then 'ok' + else 'alarm' + end status, + case + when server_side_encryption_configuration is not null then name || ' default encryption enabled.' + else name || ' default encryption disabled.' + end reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_s3_bucket; + EOQ +} + +query "s3_bucket_enforces_ssl" { + sql = <<-EOQ + with ssl_ok as ( + select + distinct name, + arn, + 'ok' as status + from + aws_s3_bucket, + jsonb_array_elements(policy_std -> 'Statement') as s, + jsonb_array_elements_text(s -> 'Principal' -> 'AWS') as p, + jsonb_array_elements_text(s -> 'Action') as a, + jsonb_array_elements_text(s -> 'Resource') as r, + jsonb_array_elements_text( + s -> 'Condition' -> 'Bool' -> 'aws:securetransport' + ) as ssl + where + p = '*' + and s ->> 'Effect' = 'Deny' + and ssl :: bool = false + ) + select + b.arn as resource, + case + when ok.status = 'ok' then 'ok' + else 'alarm' + end status, + case + when ok.status = 'ok' then b.name || ' bucket policy enforces HTTPS.' + else b.name || ' bucket policy does not enforce HTTPS.' + end reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "b.")} + from + aws_s3_bucket as b + left join ssl_ok as ok on ok.name = b.name; + EOQ +} + +query "s3_bucket_logging_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when logging -> 'TargetBucket' is null then 'alarm' + else 'ok' + end as status, + case + when logging -> 'TargetBucket' is null then title || ' logging disabled.' + else title || ' logging enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_s3_bucket; + EOQ +} + +query "s3_bucket_object_lock_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when object_lock_configuration is null then 'alarm' + else 'ok' + end as status, + case + when object_lock_configuration is null then title || ' object lock not enabled.' + else title || ' object lock enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_s3_bucket; + EOQ +} + +query "s3_bucket_restrict_public_read_access" { + sql = <<-EOQ + with public_acl as ( + select + distinct name + from + aws_s3_bucket, + jsonb_array_elements(acl -> 'Grants') as grants + where + (grants -> 'Grantee' ->> 'URI' = 'http://acs.amazonaws.com/groups/global/AllUsers' + or grants -> 'Grantee' ->> 'URI' = 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers') + and ( + grants ->> 'Permission' = 'FULL_CONTROL' + or grants ->> 'Permission' = 'READ_ACP' + or grants ->> 'Permission' = 'READ' + ) + ),read_access_policy as ( + select + distinct name + from + aws_s3_bucket, + jsonb_array_elements(policy_std -> 'Statement') as s, + jsonb_array_elements_text(s -> 'Action') as action + where + s ->> 'Effect' = 'Allow' + and ( + s -> 'Principal' -> 'AWS' = '["*"]' + or s ->> 'Principal' = '*' + ) + and ( + action = '*' + or action = '*:*' + or action = 's3:*' + or action ilike 's3:get%' + or action ilike 's3:list%' + ) + ) + select + b.arn as resource, + case + when (block_public_acls or a.name is null) and not bucket_policy_is_public then 'ok' + when (block_public_acls or a.name is null) and (bucket_policy_is_public and block_public_policy) then 'ok' + when (block_public_acls or a.name is null) and (bucket_policy_is_public and p.name is null) then 'ok' + else 'alarm' + end status, + case + when (block_public_acls or a.name is null) and not bucket_policy_is_public then b.title || ' not publicly readable.' + when (block_public_acls or a.name is null) and (bucket_policy_is_public and block_public_policy) then b.title || ' not publicly readable.' + when (block_public_acls or a.name is null) and (bucket_policy_is_public and p.name is null) then b.title || ' not publicly readable.' + else b.title || ' publicly readable.' + end reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "b.")} + from + aws_s3_bucket as b + left join public_acl as a on b.name = a.name + left join read_access_policy as p on b.name = p.name; + EOQ +} + +query "s3_bucket_restrict_public_write_access" { + sql = <<-EOQ + with public_acl as ( + select + distinct name + from + aws_s3_bucket, + jsonb_array_elements(acl -> 'Grants') as grants + where + (grants -> 'Grantee' ->> 'URI' = 'http://acs.amazonaws.com/groups/global/AllUsers' + or grants -> 'Grantee' ->> 'URI' = 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers') + and ( + grants ->> 'Permission' = 'FULL_CONTROL' + or grants ->> 'Permission' = 'WRITE_ACP' + or grants ->> 'Permission' = 'WRITE' + ) + ), write_access_policy as ( + select + distinct name + from + aws_s3_bucket, + jsonb_array_elements(policy_std -> 'Statement') as s, + jsonb_array_elements_text(s -> 'Action') as action + where + s ->> 'Effect' = 'Allow' + and ( + s -> 'Principal' -> 'AWS' = '["*"]' + or s ->> 'Principal' = '*' + ) + and ( + action = '*' + or action = '*:*' + or action = 's3:*' + or action ilike 's3:put%' + or action ilike 's3:delete%' + or action ilike 's3:create%' + or action ilike 's3:update%' + or action ilike 's3:replicate%' + or action ilike 's3:restore%' + ) + ) + select + b.arn as resource, + case + when (block_public_acls or a.name is null) and not bucket_policy_is_public then 'ok' + when (block_public_acls or a.name is null) and (bucket_policy_is_public and block_public_policy) then 'ok' + when bucket_policy_is_public and p.name is null then 'ok' + else 'alarm' + end status, + case + when (block_public_acls or a.name is null ) and not bucket_policy_is_public then b.title || ' not publicly writable.' + when (block_public_acls or a.name is null) and (bucket_policy_is_public and block_public_policy) then b.title || ' not publicly writable.' + when (block_public_acls or a.name is null) and (bucket_policy_is_public and p.name is null) then b.title || ' not publicly writable.' + else b.title || ' publicly writable.' + end reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "b.")} + from + aws_s3_bucket as b + left join public_acl as a on b.name = a.name + left join write_access_policy as p on b.name = p.name; + EOQ +} + +query "s3_bucket_versioning_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when versioning_enabled then 'ok' + else 'alarm' + end as status, + case + when versioning_enabled then name || ' versioning enabled.' + else name || ' versioning disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_s3_bucket; + EOQ +} + +query "s3_public_access_block_account" { + sql = <<-EOQ + select + 'arn' || ':' || 'aws' || ':::' || account_id as resource, + case + when block_public_acls + and block_public_policy + and ignore_public_acls + and restrict_public_buckets + then 'ok' + else 'alarm' + end as status, + case + when block_public_acls + and block_public_policy + and ignore_public_acls + and restrict_public_buckets + then 'Account level public access blocks enabled.' + else 'Account level public access blocks not enabled for: ' || + concat_ws(', ', + case when not (block_public_acls ) then 'block_public_acls' end, + case when not (block_public_policy) then 'block_public_policy' end, + case when not (ignore_public_acls ) then 'ignore_public_acls' end, + case when not (restrict_public_buckets) then 'restrict_public_buckets' end + ) || '.' + end as reason + ${local.common_dimensions_global_sql} + from + aws_s3_account_settings; + EOQ +} + +query "s3_public_access_block_bucket_account" { + sql = <<-EOQ + select + arn as resource, + case + when (bucket.block_public_acls or s3account.block_public_acls) + and (bucket.block_public_policy or s3account.block_public_policy) + and (bucket.ignore_public_acls or s3account.ignore_public_acls) + and (bucket.restrict_public_buckets or s3account.restrict_public_buckets) + then 'ok' + else 'alarm' + end as status, + case + when (bucket.block_public_acls or s3account.block_public_acls) + and (bucket.block_public_policy or s3account.block_public_policy) + and (bucket.ignore_public_acls or s3account.ignore_public_acls) + and (bucket.restrict_public_buckets or s3account.restrict_public_buckets) + then name || ' all public access blocks enabled.' + else name || ' not enabled for: ' || + concat_ws(', ', + case when not (bucket.block_public_acls or s3account.block_public_acls) then 'block_public_acls' end, + case when not (bucket.block_public_policy or s3account.block_public_policy) then 'block_public_policy' end, + case when not (bucket.ignore_public_acls or s3account.ignore_public_acls) then 'ignore_public_acls' end, + case when not (bucket.restrict_public_buckets or s3account.restrict_public_buckets) then 'restrict_public_buckets' end + ) || '.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "bucket.")} + from + aws_s3_bucket as bucket, + aws_s3_account_settings as s3account + where + s3account.account_id = bucket.account_id; + + EOQ +} + +query "s3_bucket_default_encryption_enabled_kms" { + sql = <<-EOQ + with data as ( + select + distinct name + from + aws_s3_bucket, + jsonb_array_elements(server_side_encryption_configuration -> 'Rules') as rules + where + rules -> 'ApplyServerSideEncryptionByDefault' ->> 'KMSMasterKeyID' is not null + ) + select + b.arn as resource, + case + when d.name is not null then 'ok' + else 'alarm' + end status, + case + when d.name is not null then b.name || ' default encryption with KMS enabled.' + else b.name || ' default encryption with KMS disabled.' + end reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "b.")} + from + aws_s3_bucket as b + left join data as d on b.name = d.name; + + EOQ +} + +query "s3_public_access_block_bucket" { + sql = <<-EOQ + select + arn as resource, + case + when block_public_acls + and block_public_policy + and ignore_public_acls + and restrict_public_buckets + then 'ok' + else 'alarm' + end as status, + case + when block_public_acls + and block_public_policy + and ignore_public_acls + and restrict_public_buckets + then name || ' all public access blocks enabled.' + else name || ' not enabled for: ' || + concat_ws(', ', + case when not block_public_acls then 'block_public_acls' end, + case when not block_public_policy then 'block_public_policy' end, + case when not ignore_public_acls then 'ignore_public_acls' end, + case when not restrict_public_buckets then 'restrict_public_buckets' end + ) || '.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_s3_bucket; + EOQ +} + +query "s3_bucket_policy_restricts_cross_account_permission_changes" { + sql = <<-EOQ + with cross_account_buckets as ( + select + distinct arn + from + aws_s3_bucket, + jsonb_array_elements(policy_std -> 'Statement') as s, + jsonb_array_elements_text(s -> 'Principal' -> 'AWS') as p, + string_to_array(p, ':') as pa, + jsonb_array_elements_text(s -> 'Action') as a + where + s ->> 'Effect' = 'Allow' + and ( + pa [5] != account_id + or p = '*' + ) + and a in ( + 's3:deletebucketpolicy', + 's3:putbucketacl', + 's3:putbucketpolicy', + 's3:putencryptionconfiguration', + 's3:putobjectacl' + ) + ) + select + a.arn as resource, + case + when b.arn is null then 'ok' + else 'alarm' + end as status, + case + when b.arn is null then title || ' restricts cross-account bucket access.' + else title || ' allows cross-account bucket access.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "a.")} + from + aws_s3_bucket a + left join cross_account_buckets b on a.arn = b.arn; + EOQ +} + +query "s3_bucket_object_logging_enabled" { + sql = <<-EOQ + with object_logging_cloudtrails as ( + select + d ->> 'Type' as type, + replace(replace(v::text,'"',''),'/','') as bucket_arn + from + aws_cloudtrail_trail, + jsonb_array_elements(event_selectors) e, + jsonb_array_elements(e -> 'DataResources') as d, + jsonb_array_elements(d -> 'Values') v + where + d ->> 'Type' = 'AWS::S3::Object' + ), object_logging_region as ( + select + region as cloudtrail_region, + replace(replace(v::text,'"',''),'/','') as bucket_arn + from + aws_cloudtrail_trail, + jsonb_array_elements(event_selectors) e, + jsonb_array_elements(e -> 'DataResources') as d, + jsonb_array_elements(d -> 'Values') v + where + d ->> 'Type' = 'AWS::S3::Object' + and replace(replace(v::text,'"',''),'/','') = 'arn:aws:s3' + group by + region, + bucket_arn + ), + object_logging_region_advance_es as ( + select + region as cloudtrail_region + from + aws_cloudtrail_trail, + jsonb_array_elements(advanced_event_selectors) a, + jsonb_array_elements(a -> 'FieldSelectors') as f, + jsonb_array_elements_text(f -> 'Equals') e + where + e = 'AWS::S3::Object' + and f ->> 'Field' != 'eventCategory' + group by + region + ) + select + distinct s.arn as resource, + case + when (s.arn = c.bucket_arn) + or (r.bucket_arn = 'arn:aws:s3' and r. cloudtrail_region = s.region ) + or a. cloudtrail_region = s.region then 'ok' + else 'alarm' + end as status, + case + when (s.arn = c.bucket_arn) + or (r.bucket_arn = 'arn:aws:s3' and r. cloudtrail_region = s.region ) + or a. cloudtrail_region = s.region then s.name || ' object logging enabled.' + else s.name || ' object logging not enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_s3_bucket as s + left join object_logging_cloudtrails as c on s.arn = c.bucket_arn + left join object_logging_region as r on r. cloudtrail_region = s.region + left join object_logging_region_advance_es as a on a. cloudtrail_region = s.region; + EOQ +} + +query "s3_bucket_static_website_hosting_disabled" { + sql = <<-EOQ + select + arn as resource, + case + when website_configuration -> 'IndexDocument' ->> 'Suffix' is not null then 'alarm' + else 'ok' + end status, + case + when website_configuration -> 'IndexDocument' ->> 'Suffix' is not null then name || ' static website hosting enabled.' + else name || ' static website hosting disabled.' + end reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_s3_bucket; + EOQ +} + +# Non-Config rule query + +query "s3_bucket_acls_should_prohibit_user_access" { + sql = <<-EOQ + with bucket_acl_details as ( + select + arn, + title, + array[acl -> 'Owner' ->> 'ID'] as bucket_owner, + array_agg(grantee_id) as bucket_acl_permissions, + object_ownership_controls, + region, + account_id, + _ctx, + tags + from + aws_s3_bucket, + jsonb_path_query(acl, '$.Grants.Grantee.ID') as grantee_id + group by + arn, + title, + acl, + region, + account_id, + object_ownership_controls, + _ctx, + tags + ), + bucket_acl_checks as ( + select + arn, + title, + to_jsonb(bucket_acl_permissions) - bucket_owner as additional_permissions, + object_ownership_controls, + region, + account_id, + _ctx, + tags + from + bucket_acl_details + ) + select + arn as resource, + case + when object_ownership_controls -> 'Rules' @> '[{"ObjectOwnership": "BucketOwnerEnforced"} ]' then 'ok' + when jsonb_array_length(additional_permissions) = 0 then 'ok' + else 'alarm' + end status, + case + when object_ownership_controls -> 'Rules' @> '[{"ObjectOwnership": "BucketOwnerEnforced"} ]' then title || ' ACLs are disabled.' + when jsonb_array_length(additional_permissions) = 0 then title || ' does not have ACLs for user access.' + else title || ' has ACLs for user access.' + end reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + bucket_acl_checks; + EOQ +} + +query "s3_bucket_event_notifications_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when + event_notification_configuration ->> 'EventBridgeConfiguration' is null + and event_notification_configuration ->> 'LambdaFunctionConfigurations' is null + and event_notification_configuration ->> 'QueueConfigurations' is null + and event_notification_configuration ->> 'TopicConfigurations' is null then 'alarm' + else 'ok' + end as status, + case + when + event_notification_configuration ->> 'EventBridgeConfiguration' is null + and event_notification_configuration ->> 'LambdaFunctionConfigurations' is null + and event_notification_configuration ->> 'QueueConfigurations' is null + and event_notification_configuration ->> 'TopicConfigurations' is null then title || ' event notifications disabled.' + else title || ' event notifications enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_s3_bucket; + EOQ +} + +query "s3_bucket_mfa_delete_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when versioning_mfa_delete then 'ok' + else 'alarm' + end status, + case + when versioning_mfa_delete then name || ' MFA delete enabled.' + else name || ' MFA delete disabled.' + end reason + + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_s3_bucket; + EOQ +} + +query "s3_bucket_protected_by_macie" { + sql = <<-EOQ + with bucket_list as ( + select + trim(b::text, '"' ) as bucket_name + from + aws_macie2_classification_job, + jsonb_array_elements(s3_job_definition -> 'BucketDefinitions') as d, + jsonb_array_elements(d -> 'Buckets') as b + ) + select + b.arn as resource, + case + when b.region = any(array['us-gov-east-1', 'us-gov-west-1']) then 'skip' + when l.bucket_name is not null then 'ok' + else 'alarm' + end status, + case + when b.region = any(array['us-gov-east-1', 'us-gov-west-1']) then b.title || ' not protected by Macie as Macie is not supported in ' || b.region || '.' + when l.bucket_name is not null then b.title || ' protected by Macie.' + else b.title || ' not protected by Macie.' + end reason + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "b.")} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "b.")} + from + aws_s3_bucket as b + left join bucket_list as l on b.name = l.bucket_name; + EOQ +} + +query "s3_bucket_public_access_blocked" { + sql = <<-EOQ + select + arn as resource, + case + when + block_public_acls + and block_public_policy + and ignore_public_acls + and restrict_public_buckets + then + 'ok' + else + 'alarm' + end status, + case + when + block_public_acls + and block_public_policy + and ignore_public_acls + and restrict_public_buckets + then name || ' blocks public access.' + else name || ' does not block public access.' + end reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_s3_bucket; + EOQ +} + +query "s3_bucket_versioning_and_lifecycle_policy_enabled" { + sql = <<-EOQ + with lifecycle_rules_enabled as ( + select + arn + from + aws_s3_bucket, + jsonb_array_elements(lifecycle_rules) as r + where + r ->> 'Status' = 'Enabled' + ) + select + b.arn as resource, + case + when not versioning_enabled then 'alarm' + when versioning_enabled and r.arn is not null then 'ok' + else 'alarm' + end status, + case + when not versioning_enabled then name || ' versioning diabled.' + when versioning_enabled and r.arn is not null then ' lifecycle policy configured.' + else name || ' lifecycle policy not configured.' + end reason + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "b.")} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "b.")} + from + aws_s3_bucket as b + left join lifecycle_rules_enabled as r on r.arn = b.arn; + EOQ +} diff --git a/conformance_pack/sagemaker.sp b/conformance_pack/sagemaker.sp index d39fadf2..ae456f19 100644 --- a/conformance_pack/sagemaker.sp +++ b/conformance_pack/sagemaker.sp @@ -155,3 +155,234 @@ control "sagemaker_training_job_volume_and_data_encryption_enabled" { other_checks = "true" }) } + +query "sagemaker_notebook_instance_direct_internet_access_disabled" { + sql = <<-EOQ + select + arn as resource, + case + when direct_internet_access = 'Enabled' then 'alarm' + else 'ok' + end status, + case + when direct_internet_access = 'Enabled' then title || ' direct internet access enabled.' + else title || ' direct internet access disabled.' + end reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_sagemaker_notebook_instance; + EOQ +} + +query "sagemaker_notebook_instance_encryption_at_rest_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when kms_key_id is null then 'alarm' + else 'ok' + end as status, + case + when kms_key_id is null then title || ' encryption at rest enabled' + else title || ' encryption at rest not enabled' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_sagemaker_notebook_instance; + EOQ +} + +query "sagemaker_endpoint_configuration_encryption_at_rest_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when kms_key_id is null then 'alarm' + else 'ok' + end as status, + case + when kms_key_id is null then title || ' encryption at rest disabled.' + else title || ' encryption at rest enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_sagemaker_endpoint_configuration; + EOQ +} + +query "sagemaker_model_in_vpc" { + sql = <<-EOQ + select + arn as resource, + case + when vpc_config is not null then 'ok' + else 'alarm' + end as status, + case + when vpc_config is not null then title || ' in VPC.' + else title || ' not in VPC.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_sagemaker_model; + EOQ +} + +query "sagemaker_model_network_isolation_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when enable_network_isolation then 'ok' + else 'alarm' + end as status, + case + when enable_network_isolation then title || ' network isolation enabled.' + else title || ' network isolation disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_sagemaker_model; + EOQ +} + +query "sagemaker_notebook_instance_in_vpc" { + sql = <<-EOQ + select + arn as resource, + case + when subnet_id is not null then 'ok' + else 'alarm' + end as status, + case + when subnet_id is not null then title || ' in VPC.' + else title || ' not in VPC.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_sagemaker_notebook_instance; + EOQ +} + +query "sagemaker_notebook_instance_root_access_disabled" { + sql = <<-EOQ + select + arn as resource, + case + when root_access = 'Disabled' then 'ok' + else 'alarm' + end as status, + case + when root_access = 'Disabled' then title || ' root access disabled.' + else title || ' root access enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_sagemaker_notebook_instance; + EOQ +} + +query "sagemaker_training_job_in_vpc" { + sql = <<-EOQ + select + arn as resource, + case + when vpc_config is not null then 'ok' + else 'alarm' + end as status, + case + when vpc_config is not null then title || ' in VPC.' + else title || ' not in VPC.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_sagemaker_training_job; + EOQ +} + +query "sagemaker_training_job_inter_container_traffic_encryption_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when enable_inter_container_traffic_encryption then 'ok' + else 'alarm' + end as status, + case + when enable_inter_container_traffic_encryption then title || ' inter-container traffic encryption enabled.' + else title || ' inter-container traffic encryption disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_sagemaker_training_job; + EOQ +} + +query "sagemaker_training_job_network_isolation_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when enable_network_isolation then 'ok' + else 'alarm' + end as status, + case + when enable_network_isolation then title || ' network isolation enabled.' + else title || ' network isolation disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_sagemaker_training_job; + EOQ +} + +query "sagemaker_training_job_volume_and_data_encryption_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when output_data_config ->> 'KmsKeyId' is null or output_data_config ->> 'KmsKeyId' = '' then 'alarm' + else 'ok' + end as status, + case + when output_data_config ->> 'KmsKeyId' is null or output_data_config ->> 'KmsKeyId' = '' then title || ' volume and output data encryption disabled.' + else title || ' volume and output data encryption enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_sagemaker_training_job; + EOQ +} + +query "sagemaker_notebook_instance_encrypted_with_kms_cmk" { + sql = <<-EOQ + select + i.arn as resource, + case + when kms_key_id is null then 'alarm' + when k.key_manager = 'CUSTOMER' then 'ok' + else 'alarm' + end as status, + case + when kms_key_id is null then i.title || ' encryption disabled.' + when k.key_manager = 'CUSTOMER' then i.title || ' encryption at rest with CMK enabled.' + else i.title || ' encryption at rest with CMK disabled.' + end as reason + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "i.")} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "i.")} + from + aws_sagemaker_notebook_instance as i + left join aws_kms_key as k on k.arn = i.kms_key_id; + EOQ +} diff --git a/conformance_pack/secretsmanager.sp b/conformance_pack/secretsmanager.sp index a3ad149b..3c03e6b3 100644 --- a/conformance_pack/secretsmanager.sp +++ b/conformance_pack/secretsmanager.sp @@ -62,3 +62,170 @@ control "secretsmanager_secret_last_changed_90_day" { cisa_cyber_essentials = "true" }) } + +query "secretsmanager_secret_automatic_rotation_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when rotation_rules is null then 'alarm' + else 'ok' + end as status, + case + when rotation_rules is null then title || ' automatic rotation not enabled.' + else title || ' automatic rotation enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_secretsmanager_secret; + EOQ +} + +query "secretsmanager_secret_rotated_as_scheduled" { + sql = <<-EOQ + select + arn as resource, + case + when primary_region is not null and region != primary_region then 'skip' -- Replica secret + when rotation_rules is null then 'alarm' -- Rotation not enabled + when last_rotated_date is null + and (date(current_date) - date(created_date)) <= (rotation_rules -> 'AutomaticallyAfterDays')::integer then 'ok' -- New secret not due for rotation yet + when last_rotated_date is null + and (date(current_date) - date(created_date)) > (rotation_rules -> 'AutomaticallyAfterDays')::integer then 'alarm' -- New secret overdue for rotation + when last_rotated_date is not null + and (date(current_date) - date(last_rotated_date)) > (rotation_rules -> 'AutomaticallyAfterDays')::integer then 'alarm' -- Secret has been rotated before but is overdue for another rotation + end as status, + case + when primary_region is not null and region != primary_region then title || ' is a replica.' + when rotation_rules is null then title || ' rotation not enabled.' + when last_rotated_date is null + and (date(current_date) - date(created_date)) <= (rotation_rules -> 'AutomaticallyAfterDays')::integer then title || ' scheduled for rotation.' + when last_rotated_date is null + and (date(current_date) - date(created_date)) > (rotation_rules -> 'AutomaticallyAfterDays')::integer then title || ' not rotated as per schedule.' + when last_rotated_date is not null + and (date(current_date) - date(last_rotated_date)) > (rotation_rules -> 'AutomaticallyAfterDays')::integer then title || ' not rotated as per schedule.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_secretsmanager_secret; + EOQ +} + +query "secretsmanager_secret_unused_90_day" { + sql = <<-EOQ + select + arn as resource, + case + when last_accessed_date is null then 'alarm' + when date(current_date) - date(last_accessed_date) <= 90 then 'ok' + else 'alarm' + end as status, + case + when last_accessed_date is null then title || ' never accessed.' + else + title || ' last used ' || extract(day from current_timestamp - last_accessed_date) || ' day(s) ago.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_secretsmanager_secret; + EOQ +} + +query "secretsmanager_secret_encrypted_with_kms_cmk" { + sql = <<-EOQ + with encryption_keys as ( + select + distinct s.arn, + k.aliases as alias + from + aws_secretsmanager_secret as s + left join aws_kms_key as k on k.arn = s.kms_key_id + where + jsonb_array_length(k.aliases) > 0 + ) + select + s.arn as resource, + case + when kms_key_id is null + or kms_key_id = 'alias/aws/secretsmanager' + or k.alias @> '[{"AliasName":"alias/aws/secretsmanager"}]'then 'alarm' + else 'ok' + end as status, + case + when kms_key_id is null then title || ' not encrypted with KMS.' + when kms_key_id = 'alias/aws/secretsmanager' or k.alias @> '[{"AliasName":"alias/aws/secretsmanager"}]' then title || ' encrypted with AWS managed key.' + else title || ' encrypted with CMK.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_secretsmanager_secret as s + left join encryption_keys as k on s.arn = k.arn; + + EOQ +} + +query "secretsmanager_secret_last_changed_90_day" { + sql = <<-EOQ + select + arn as resource, + case + when last_changed_date is null then 'alarm' + when date(current_date) - date(last_changed_date) <= 90 then 'ok' + else 'alarm' + end as status, + case + when last_changed_date is null then title || ' never rotated.' + else + title || ' last rotated ' || extract(day from current_timestamp - last_changed_date) || ' day(s) ago.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_secretsmanager_secret; + EOQ +} + +# Non-Config rule query + +query "secretsmanager_secret_automatic_rotation_lambda_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when rotation_rules is not null and rotation_lambda_arn is not null then 'ok' + else 'alarm' + end as status, + case + when rotation_rules is not null and rotation_lambda_arn is not null then title || ' scheduled for rotation using Lambda function.' + else title || ' automatic rotation using Lambda function disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_secretsmanager_secret; + + EOQ +} + +query "secretsmanager_secret_last_used_1_day" { + sql = <<-EOQ + select + arn as resource, + case + when date(last_accessed_date) - date(created_date) >= 1 then 'ok' + else 'alarm' + end as status, + case + when date(last_accessed_date)- date(created_date) >= 1 then title || ' recently used.' + else title || ' not used recently.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_secretsmanager_secret; + EOQ +} diff --git a/conformance_pack/securityhub.sp b/conformance_pack/securityhub.sp index 4d9efb9d..97c0d464 100644 --- a/conformance_pack/securityhub.sp +++ b/conformance_pack/securityhub.sp @@ -25,3 +25,27 @@ control "securityhub_enabled" { soc_2 = "true" }) } + +query "securityhub_enabled" { + sql = <<-EOQ + select + 'arn:' || r.partition || '::' || r.region || ':' || r.account_id as resource, + case + when r.region = any(array['af-south-1', 'eu-south-1', 'cn-north-1', 'cn-northwest-1', 'ap-northeast-3']) then 'skip' + -- Skip any regions that are disabled in the account. + when r.opt_in_status = 'not-opted-in' then 'skip' + when h.hub_arn is not null then 'ok' + else 'alarm' + end as status, + case + when r.region = any(array['af-south-1', 'eu-south-1', 'cn-north-1', 'cn-northwest-1', 'ap-northeast-3']) then r.region || ' region not supported.' + when r.opt_in_status = 'not-opted-in' then r.region || ' region is disabled.' + when h.hub_arn is not null then 'Security Hub enabled in ' || r.region || '.' + else 'Security Hub disabled in ' || r.region || '.' + end as reason + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "r.")} + from + aws_region as r + left join aws_securityhub_hub as h on r.account_id = h.account_id and r.name = h.region; + EOQ +} diff --git a/conformance_pack/sns.sp b/conformance_pack/sns.sp index 2245f1ad..6a25a6f3 100644 --- a/conformance_pack/sns.sp +++ b/conformance_pack/sns.sp @@ -34,3 +34,88 @@ control "sns_topic_policy_prohibit_public_access" { other_checks = "true" }) } + +query "sns_topic_encrypted_at_rest" { + sql = <<-EOQ + select + topic_arn as resource, + case + when kms_master_key_id is null then 'alarm' + else 'ok' + end as status, + case + when kms_master_key_id is null then title || ' encryption at rest disabled.' + else title || ' encryption at rest enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_sns_topic; + EOQ +} + +query "sns_topic_policy_prohibit_public_access" { + sql = <<-EOQ + with wildcard_action_policies as ( + select + topic_arn, + count(*) as statements_num + from + aws_sns_topic, + jsonb_array_elements(policy_std -> 'Statement') as s + where + s ->> 'Effect' = 'Allow' + and ( + ( s -> 'Principal' -> 'AWS') = '["*"]' + or s ->> 'Principal' = '*' + ) + group by + topic_arn + ) + select + t.topic_arn as resource, + case + when p.topic_arn is null then 'ok' + else 'alarm' + end as status, + case + when p.topic_arn is null then title || ' does not allow public access.' + else title || ' contains ' || coalesce(p.statements_num,0) || + ' statements that allows public access.' + end as reason + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "t.")} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "t.")} + from + aws_sns_topic as t + left join wildcard_action_policies as p on p.topic_arn = t.topic_arn; + EOQ +} + +# Non-Config rule query + +query "sns_topic_notification_delivery_status_enabled" { + sql = <<-EOQ + select + topic_arn as resource, + case + when application_failure_feedback_role_arn is null + and firehose_failure_feedback_role_arn is null + and http_failure_feedback_role_arn is null + and lambda_failure_feedback_role_arn is null + and sqs_failure_feedback_role_arn is null then 'alarm' + else 'ok' + end as status, + case + when application_failure_feedback_role_arn is null + and firehose_failure_feedback_role_arn is null + and http_failure_feedback_role_arn is null + and lambda_failure_feedback_role_arn is null + and sqs_failure_feedback_role_arn is null then title || ' has delivery status logging for notification messages disabled.' + else title || ' has delivery status logging for notification messages enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_sns_topic; + EOQ +} diff --git a/conformance_pack/sqs.sp b/conformance_pack/sqs.sp index 97dc8cb3..993455f0 100644 --- a/conformance_pack/sqs.sp +++ b/conformance_pack/sqs.sp @@ -24,3 +24,80 @@ control "sqs_queue_dead_letter_queue_configured" { }) } +query "sqs_queue_policy_prohibit_public_access" { + sql = <<-EOQ + with wildcard_action_policies as ( + select + queue_arn, + count(*) as statements_num + from + aws_sqs_queue, + jsonb_array_elements(policy_std -> 'Statement') as s + where + s ->> 'Effect' = 'Allow' + and ( + ( s -> 'Principal' -> 'AWS') = '["*"]' + or s ->> 'Principal' = '*' + ) + group by + queue_arn + ) + select + q.queue_arn as resource, + case + when p.queue_arn is null then 'ok' + else 'alarm' + end as status, + case + when p.queue_arn is null then title || ' does not allow public access.' + else title || ' contains ' || coalesce(p.statements_num,0) || + ' statements that allows public access.' + end as reason + + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "q.")} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "q.")} + from + aws_sqs_queue as q + left join wildcard_action_policies as p on q.queue_arn = p.queue_arn; + EOQ +} + +query "sqs_queue_dead_letter_queue_configured" { + sql = <<-EOQ + select + queue_arn as resource, + case + when redrive_policy is not null then 'ok' + else 'alarm' + end as status, + case + when redrive_policy is not null then title || ' configured with dead-letter queue.' + else title || ' not configured with dead-letter queue.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_sqs_queue; + EOQ +} + +# Non-Config rule query + +query "sqs_queue_encrypted_at_rest" { + sql = <<-EOQ + select + queue_arn as resource, + case + when kms_master_key_id is null then 'alarm' + else 'ok' + end as status, + case + when kms_master_key_id is null then title || ' encryption at rest disabled.' + else title || ' encryption at rest enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_sqs_queue; + EOQ +} diff --git a/conformance_pack/ssm.sp b/conformance_pack/ssm.sp index c43162ae..b42804f6 100644 --- a/conformance_pack/ssm.sp +++ b/conformance_pack/ssm.sp @@ -68,3 +68,70 @@ control "ssm_managed_instance_compliance_patch_compliant" { soc_2 = "true" }) } + +query "ec2_instance_ssm_managed" { + sql = <<-EOQ + select + i.arn as resource, + case + when i.instance_state = 'stopped' then 'info' + when m.instance_id is null then 'alarm' + else 'ok' + end as status, + case + when i.instance_state = 'stopped' then i.title || ' is in stopped state.' + when m.instance_id is null then i.title || ' not managed by AWS SSM.' + else i.title || ' managed by AWS SSM.' + end as reason + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "i.")} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "i.")} + from + aws_ec2_instance i + left join aws_ssm_managed_instance m on m.instance_id = i.instance_id; + EOQ +} + +query "ssm_managed_instance_compliance_association_compliant" { + sql = <<-EOQ + select + id as resource, + case + when c.status = 'COMPLIANT' then 'ok' + else 'alarm' + end as status, + case + when c.status = 'COMPLIANT' then c.resource_id || ' association ' || c.title || ' is compliant.' + else c.resource_id || ' association ' || c.title || ' is non-compliant.' + end as reason + + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "c.")} + from + aws_ssm_managed_instance as i, + aws_ssm_managed_instance_compliance as c + where + c.resource_id = i.instance_id + and c.compliance_type = 'Association'; + EOQ +} + +query "ssm_managed_instance_compliance_patch_compliant" { + sql = <<-EOQ + select + id as resource, + case + when c.status = 'COMPLIANT' then 'ok' + else 'alarm' + end as status, + case + when c.status = 'COMPLIANT' then c.resource_id || ' patch ' || c.title || ' is compliant.' + else c.resource_id || ' patch ' || c.title || ' is non-compliant.' + end as reason + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "c.")} + from + aws_ssm_managed_instance as i, + aws_ssm_managed_instance_compliance as c + where + c.resource_id = i.instance_id + and c.compliance_type = 'Patch'; + EOQ +} diff --git a/conformance_pack/vpc.sp b/conformance_pack/vpc.sp index 5cb45c3c..a42121d9 100644 --- a/conformance_pack/vpc.sp +++ b/conformance_pack/vpc.sp @@ -274,3 +274,1087 @@ control "vpc_network_acl_unused" { cisa_cyber_essentials = "true" }) } + +query "vpc_flow_logs_enabled" { + sql = <<-EOQ + select + + distinct arn as resource, + case + when v.account_id <> v.owner_id then 'skip' + when f.resource_id is not null then 'ok' + else 'alarm' + end as status, + case + when v.account_id <> v.owner_id then vpc_id || ' is a shared VPC.' + when f.resource_id is not null then vpc_id || ' flow logging enabled.' + else vpc_id || ' flow logging disabled.' + end as reason + ${replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "v.")} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "v.")} + from + aws_vpc as v + left join aws_vpc_flow_log as f on v.vpc_id = f.resource_id; + EOQ +} + +query "vpc_igw_attached_to_authorized_vpc" { + sql = <<-EOQ + select + 'arn:' || partition || ':ec2:' || region || ':' || account_id || ':internet-gateway/' || title as resource, + case + when jsonb_array_length(attachments) = 0 then 'alarm' + else 'ok' + end as status, + case + when jsonb_array_length(attachments) = 0 then title || ' not attached to VPC.' + else title || ' attached to ' || split_part( + substring(attachments :: text, 3, length(attachments :: text) -6), + '"VpcId": "', + 2 + ) || '.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_vpc_internet_gateway; + EOQ +} + +query "vpc_security_group_restrict_ingress_tcp_udp_all" { + sql = <<-EOQ + with bad_rules as ( + select + group_id, + count(*) as num_bad_rules + from + aws_vpc_security_group_rule + where + type = 'ingress' + and cidr_ipv4 = '0.0.0.0/0' + and ( + ip_protocol in ('tcp', 'udp') + or ( + ip_protocol = '-1' + and from_port is null + ) + ) + group by + group_id + ) + select + arn as resource, + case + when bad_rules.group_id is null then 'ok' + else 'alarm' + end as status, + case + when bad_rules.group_id is null then sg.group_id || ' does not allow ingress to TCP or UDP ports from 0.0.0.0/0.' + else sg.group_id || ' contains ' || bad_rules.num_bad_rules || ' rule(s) that allow ingress to TCP or UDP ports from 0.0.0.0/0.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_vpc_security_group as sg + left join bad_rules on bad_rules.group_id = sg.group_id; + EOQ +} + +query "vpc_security_group_restrict_ingress_common_ports_all" { + sql = <<-EOQ + with ingress_ssh_rules as ( + select + group_id, + count(*) as num_ssh_rules + from + aws_vpc_security_group_rule + where + type = 'ingress' + and cidr_ipv4 = '0.0.0.0/0' + and ( + ( ip_protocol = '-1' + and from_port is null + ) + or ( + from_port >= 22 + and to_port <= 22 + ) + or ( + from_port >= 3389 + and to_port <= 3389 + ) + or ( + from_port >= 21 + and to_port <= 21 + ) + or ( + from_port >= 20 + and to_port <= 20 + ) + or ( + from_port >= 3306 + and to_port <= 3306 + ) + or ( + from_port >= 4333 + and to_port <= 4333 + ) + ) + group by + group_id + ) + select + arn as resource, + case + when ingress_ssh_rules.group_id is null then 'ok' + else 'alarm' + end as status, + case + when ingress_ssh_rules.group_id is null then sg.group_id || ' ingress restricted for ports 20, 21, 22, 3306, 3389, 4333 from 0.0.0.0/0.' + else sg.group_id || ' contains ' || ingress_ssh_rules.num_ssh_rules || ' ingress rule(s) allowing access on ports 20, 21, 22, 3306, 3389, 4333 from 0.0.0.0/0.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_vpc_security_group as sg + left join ingress_ssh_rules on ingress_ssh_rules.group_id = sg.group_id; + EOQ +} + +query "vpc_security_group_restrict_ingress_ssh_all" { + sql = <<-EOQ + with ingress_ssh_rules as ( + select + group_id, + count(*) as num_ssh_rules + from + aws_vpc_security_group_rule + where + type = 'ingress' + and cidr_ipv4 = '0.0.0.0/0' + and ( + ( ip_protocol = '-1' + and from_port is null + ) + or ( + from_port >= 22 + and to_port <= 22 + ) + ) + group by + group_id + ) + select + arn as resource, + case + when ingress_ssh_rules.group_id is null then 'ok' + else 'alarm' + end as status, + case + when ingress_ssh_rules.group_id is null then sg.group_id || ' ingress restricted for SSH from 0.0.0.0/0.' + else sg.group_id || ' contains ' || ingress_ssh_rules.num_ssh_rules || ' ingress rule(s) allowing SSH from 0.0.0.0/0.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_vpc_security_group as sg + left join ingress_ssh_rules on ingress_ssh_rules.group_id = sg.group_id; + EOQ +} + +query "vpc_default_security_group_restricts_all_traffic" { + sql = <<-EOQ + select + arn resource, + case + when jsonb_array_length(ip_permissions) = 0 and jsonb_array_length(ip_permissions_egress) = 0 then 'ok' + else 'alarm' + end status, + case + when jsonb_array_length(ip_permissions) > 0 and jsonb_array_length(ip_permissions_egress) > 0 + then 'Default security group ' || group_id || ' has inbound and outbound rules.' + when jsonb_array_length(ip_permissions) > 0 and jsonb_array_length(ip_permissions_egress) = 0 + then 'Default security group ' || group_id || ' has inbound rules.' + when jsonb_array_length(ip_permissions) = 0 and jsonb_array_length(ip_permissions_egress) > 0 + then 'Default security group ' || group_id || ' has outbound rules.' + else 'Default security group ' || group_id || ' has no inbound or outbound rules.' + end reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_vpc_security_group + where + group_name = 'default'; + EOQ +} + +query "vpc_vpn_tunnel_up" { + sql = <<-EOQ + with filter_data as ( + select + arn, + count(t ->> 'Status') + from + aws_vpc_vpn_connection, + jsonb_array_elements(vgw_telemetry) as t + where t ->> 'Status' = 'UP' + group by arn + ) + select + a.arn as resource, + case + when b.count is null or b.count < 2 then 'alarm' + else 'ok' + end as status, + case + when b.count is null then a.title || ' has both tunnels offline.' + when b.count = 1 then a.title || ' has one tunnel offline.' + else a.title || ' has both tunnels online.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_vpc_vpn_connection as a + left join filter_data as b on a.arn = b.arn; + EOQ +} + +query "vpc_eip_associated" { + sql = <<-EOQ + select + 'arn:' || partition || ':ec2:' || region || ':' || account_id || ':eip/' || allocation_id as resource, + case + when association_id is null then 'alarm' + else 'ok' + end status, + case + when association_id is null then title || ' is not associated with any resource.' + else title || ' is associated with a resource.' + end reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_vpc_eip; + EOQ +} + +query "vpc_security_group_associated_to_eni" { + sql = <<-EOQ + with associated_sg as ( + select + count(sg ->> 'GroupId'), + sg ->> 'GroupId' as secgrp_id + from + aws_ec2_network_interface, + jsonb_array_elements(groups) as sg + group by sg ->> 'GroupId' + ) + select + distinct s.arn as resource, + case + when a.secgrp_id = s.group_id then 'ok' + else 'alarm' + end as status, + case + when a.secgrp_id = s.group_id then s.title || ' is associated with ' || a.count || ' ENI(s).' + else s.title || ' not associated to any ENI.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_vpc_security_group as s + left join associated_sg as a on s.group_id = a.secgrp_id; + EOQ +} + +query "vpc_subnet_auto_assign_public_ip_disabled" { + sql = <<-EOQ + select + subnet_id as resource, + case + when map_public_ip_on_launch = 'false' then 'ok' + else 'alarm' + end as status, + case + when map_public_ip_on_launch = 'false' then title || ' auto assign public IP disabled.' + else title || ' auto assign public IP enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_vpc_subnet; + EOQ +} + +query "vpc_route_table_restrict_public_access_to_igw" { + sql = <<-EOQ + with route_with_public_access as ( + select + route_table_id, + count(*) as num + from + aws_vpc_route_table, + jsonb_array_elements(routes) as r + where + ( r ->> 'DestinationCidrBlock' = '0.0.0.0/0' + or r ->> 'DestinationCidrBlock' = '::/0' + ) + and r ->> 'GatewayId' like 'igw%' + group by + route_table_id + ) + select + a.route_table_id as resource, + case + when b.route_table_id is null then 'ok' + else 'alarm' + end as status, + case + when b.route_table_id is null then a.title || ' does not have public routes to an Internet Gateway (IGW)' + else a.title || ' contains ' || b.num || ' rule(s) which have public routes to an Internet Gateway (IGW)' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_vpc_route_table as a + left join route_with_public_access as b on b.route_table_id = a.route_table_id; + EOQ +} + +query "vpc_security_group_restricted_common_ports" { + sql = <<-EOQ + with ingress_ssh_rules as ( + select + group_id, + count(*) as num_ssh_rules + from + aws_vpc_security_group_rule + where + type = 'ingress' + and cidr_ipv4 = '0.0.0.0/0' + and ( + ( ip_protocol = '-1' + and from_port is null + ) + or ( + from_port >= 22 + and to_port <= 22 + ) + or ( + from_port >= 3389 + and to_port <= 3389 + ) + or ( + from_port >= 21 + and to_port <= 21 + ) + or ( + from_port >= 20 + and to_port <= 20 + ) + or ( + from_port >= 3306 + and to_port <= 3306 + ) + or ( + from_port >= 4333 + and to_port <= 4333 + ) + or ( + from_port >= 23 + and to_port <= 23 + ) + or ( + from_port >= 25 + and to_port <= 25 + ) + or ( + from_port >= 445 + and to_port <= 445 + ) + or ( + from_port >= 110 + and to_port <= 110 + ) + or ( + from_port >= 135 + and to_port <= 135 + ) + or ( + from_port >= 143 + and to_port <= 143 + ) + or ( + from_port >= 1433 + and to_port <= 3389 + ) + or ( + from_port >= 3389 + and to_port <= 1434 + ) + or ( + from_port >= 5432 + and to_port <= 5432 + ) + or ( + from_port >= 5500 + and to_port <= 5500 + ) + or ( + from_port >= 5601 + and to_port <= 5601 + ) + or ( + from_port >= 9200 + and to_port <= 9300 + ) + or ( + from_port >= 8080 + and to_port <= 8080 + ) + ) + group by + group_id + ) + select + arn as resource, + case + when ingress_ssh_rules.group_id is null then 'ok' + else 'alarm' + end as status, + case + when ingress_ssh_rules.group_id is null then sg.group_id || ' ingress restricted for common ports from 0.0.0.0/0..' + else sg.group_id || ' contains ' || ingress_ssh_rules.num_ssh_rules || ' ingress rule(s) allowing access for common ports from 0.0.0.0/0.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "sg.")} + from + aws_vpc_security_group as sg + left join ingress_ssh_rules on ingress_ssh_rules.group_id = sg.group_id; + EOQ +} + +query "vpc_security_group_restrict_ingress_redis_port" { + sql = <<-EOQ + with ingress_redis_port as ( + select + group_id, + count(*) as num_redis_rules + from + aws_vpc_security_group_rule + where + type = 'ingress' + and + (cidr_ipv4 = '0.0.0.0/0' + or cidr_ipv6 = '::/0') + and + ( + ( ip_protocol = '-1' + and from_port is null + ) + or ( + from_port >= 6379 + and to_port <= 6379 + ) + ) + group by + group_id + ) + select + arn as resource, + case + when ingress_redis_port.group_id is null then 'ok' + else 'alarm' + end as status, + case + when ingress_redis_port.group_id is null then sg.group_id || ' restricted ingress from 0.0.0.0/0 or ::/0 to Redis port 6379.' + else sg.group_id || ' contains ' || ingress_redis_port.num_redis_rules || ' ingress rule(s) from 0.0.0.0/0 or ::/0 to Redis port 6379.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_vpc_security_group as sg + left join ingress_redis_port on ingress_redis_port.group_id = sg.group_id; + EOQ +} + +query "vpc_security_group_restrict_ingress_kibana_port" { + sql = <<-EOQ + with ingress_kibana_port as ( + select + group_id, + count(*) as num_ssh_rules + from + aws_vpc_security_group_rule + where + type = 'ingress' + and ( + cidr_ipv4 = '0.0.0.0/0' + or cidr_ipv6 = '::/0' + ) + and ( + ( ip_protocol = '-1' + and from_port is null + ) + or ( + from_port >= 9200 + and to_port <= 9200 + ) + or ( + from_port >= 5601 + and to_port <= 5601 + ) + ) + group by + group_id + ) + select + arn as resource, + case + when k.group_id is null then 'ok' + else 'alarm' + end as status, + case + when k.group_id is null then sg.group_id || ' ingress restricted for kibana port from 0.0.0.0/0.' + else sg.group_id || ' contains ' || k.num_ssh_rules || ' ingress rule(s) allowing kibana port from 0.0.0.0/0.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "sg.")} + from + aws_vpc_security_group as sg + left join ingress_kibana_port as k on k.group_id = sg.group_id; + EOQ +} + +query "vpc_security_group_not_uses_launch_wizard_sg" { + sql = <<-EOQ + with associated_sg as ( + select + distinct (sg ->> 'GroupName') as sg_name + from + aws_ec2_network_interface, + jsonb_array_elements(groups) as sg + where + (sg ->> 'GroupName') like 'launch-wizard%' + ) + select + arn as resource, + case + when a.sg_name is null then 'ok' + else 'alarm' + end as status, + case + when a.sg_name is null then title || ' not in use.' + else title || ' in use.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_vpc_security_group as s + left join associated_sg as a on a.sg_name = s.group_name + where + group_name like 'launch-wizard%'; + EOQ +} + +query "vpc_endpoint_service_acceptance_required_enabled" { + sql = <<-EOQ + select + service_id as resource, + case + when acceptance_required then 'ok' + else 'alarm' + end as status, + case + when acceptance_required then title || ' acceptance_required enabled.' + else title || ' acceptance_required disabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_vpc_endpoint_service; + EOQ +} + +query "vpc_network_acl_unused" { + sql = <<-EOQ + select + network_acl_id as resource, + case + when jsonb_array_length(associations) >= 1 then 'ok' + else 'alarm' + end status, + case + when jsonb_array_length(associations) >= 1 then title || ' associated with subnet.' + else title || ' not associated with subnet.' + end reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_vpc_network_acl; + EOQ +} + +query "vpc_security_group_restrict_ingress_kafka_port" { + sql = <<-EOQ + with ingress_kafka_port as ( + select + group_id, + count(*) as num_ssh_rules + from + aws_vpc_security_group_rule + where + type = 'ingress' + and ( + cidr_ipv4 = '0.0.0.0/0' + or cidr_ipv6 = '::/0' + ) + and ( + ( ip_protocol = '-1' + and from_port is null + ) + or ( + from_port >= 9092 + and to_port <= 9092 + ) + ) + group by + group_id + ) + select + arn as resource, + case + when k.group_id is null then 'ok' + else 'alarm' + end as status, + case + when k.group_id is null then sg.group_id || ' ingress restricted for kafka port from 0.0.0.0/0.' + else sg.group_id || ' contains ' || k.num_ssh_rules || ' ingress rule(s) allowing kafka port from 0.0.0.0/0.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "sg.")} + from + aws_vpc_security_group as sg + left join ingress_kafka_port as k on k.group_id = sg.group_id; + EOQ +} + +# Non-Config rule query + +query "vpc_configured_to_use_vpc_endpoints" { + sql = <<-EOQ + select + arn as resource, + case + when vpc_id not in ( + select + vpc_id + from + aws_vpc_endpoint + where + service_name like 'com.amazonaws.' || region || '.ec2' + ) then 'alarm' + else 'ok' + end as status, + case + when vpc_id not in ( + select + vpc_id + from + aws_vpc_endpoint + where + service_name like 'com.amazonaws.' || region || '.ec2' + ) then title || ' not configured to use VPC endpoints.' + else title || ' configured to use VPC endpoints.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_vpc; + EOQ +} + +query "vpc_network_acl_remote_administration" { + sql = <<-EOQ + with bad_rules as ( + select + network_acl_id, + count(*) as num_bad_rules + from + aws_vpc_network_acl, + jsonb_array_elements(entries) as att + where + att ->> 'Egress' = 'false' -- as per aws egress = false indicates the ingress + and ( + att ->> 'CidrBlock' = '0.0.0.0/0' + or att ->> 'Ipv6CidrBlock' = '::/0' + ) + and att ->> 'RuleAction' = 'allow' + and ( + ( + att ->> 'Protocol' = '-1' -- all traffic + and att ->> 'PortRange' is null + ) + or ( + (att -> 'PortRange' ->> 'From') :: int <= 22 + and (att -> 'PortRange' ->> 'To') :: int >= 22 + and att ->> 'Protocol' in('6', '17') -- TCP or UDP + ) + or ( + (att -> 'PortRange' ->> 'From') :: int <= 3389 + and (att -> 'PortRange' ->> 'To') :: int >= 3389 + and att ->> 'Protocol' in('6', '17') -- TCP or UDP + ) + ) + group by + network_acl_id + ) + select + 'arn:' || acl.partition || ':ec2:' || acl.region || ':' || acl.account_id || ':network-acl/' || acl.network_acl_id as resource, + case + when bad_rules.network_acl_id is null then 'ok' + else 'alarm' + end as status, + case + when bad_rules.network_acl_id is null then acl.network_acl_id || ' does not allow ingress to port 22 or 3389 from 0.0.0.0/0 or ::/0.' + else acl.network_acl_id || ' contains ' || bad_rules.num_bad_rules || ' rule(s) allowing ingress to port 22 or 3389 from 0.0.0.0/0 or ::/0.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "acl.")} + from + aws_vpc_network_acl as acl + left join bad_rules on bad_rules.network_acl_id = acl.network_acl_id; + EOQ +} + +query "vpc_network_acl_remote_administration" { + sql = <<-EOQ + with bad_rules as ( + select + network_acl_id, + count(*) as num_bad_rules + from + aws_vpc_network_acl, + jsonb_array_elements(entries) as att + where + att ->> 'Egress' = 'false' -- as per aws egress = false indicates the ingress + and ( + att ->> 'CidrBlock' = '0.0.0.0/0' + or att ->> 'Ipv6CidrBlock' = '::/0' + ) + and att ->> 'RuleAction' = 'allow' + and ( + ( + att ->> 'Protocol' = '-1' -- all traffic + and att ->> 'PortRange' is null + ) + or ( + (att -> 'PortRange' ->> 'From') :: int <= 22 + and (att -> 'PortRange' ->> 'To') :: int >= 22 + and att ->> 'Protocol' in('6', '17') -- TCP or UDP + ) + or ( + (att -> 'PortRange' ->> 'From') :: int <= 3389 + and (att -> 'PortRange' ->> 'To') :: int >= 3389 + and att ->> 'Protocol' in('6', '17') -- TCP or UDP + ) + ) + group by + network_acl_id + ) + select + 'arn:' || acl.partition || ':ec2:' || acl.region || ':' || acl.account_id || ':network-acl/' || acl.network_acl_id as resource, + case + when bad_rules.network_acl_id is null then 'ok' + else 'alarm' + end as status, + case + when bad_rules.network_acl_id is null then acl.network_acl_id || ' does not allow ingress to port 22 or 3389 from 0.0.0.0/0 or ::/0.' + else acl.network_acl_id || ' contains ' || bad_rules.num_bad_rules || ' rule(s) allowing ingress to port 22 or 3389 from 0.0.0.0/0 or ::/0.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "acl.")} + from + aws_vpc_network_acl as acl + left join bad_rules on bad_rules.network_acl_id = acl.network_acl_id; + EOQ +} + +query "vpc_security_group_allows_ingress_authorized_ports" { + sql = <<-EOQ + with ingress_unauthorized_ports as ( + select + group_id, + count(*) + from + aws_vpc_security_group_rule + where + type = 'ingress' + and cidr_ipv4 = '0.0.0.0/0' + and (from_port is null or from_port not in (80,443)) + group by group_id + ) + select + sg.arn as resource, + case + when ingress_unauthorized_ports.count > 0 then 'alarm' + else 'ok' + end as status, + case + when ingress_unauthorized_ports.count > 0 then sg.title || ' having unrestricted incoming traffic other than default ports from 0.0.0.0/0 ' + else sg.title || ' allows unrestricted incoming traffic for authorized default ports (80,443).' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "sg.")} + from + aws_vpc_security_group as sg + left join ingress_unauthorized_ports on ingress_unauthorized_ports.group_id = sg.group_id; + EOQ +} + +query "vpc_security_group_associated" { + sql = <<-EOQ + with associated_sg as ( + select + sg ->> 'GroupId' as secgrp_id, + sg ->> 'GroupName' as secgrp_name + from + aws_ec2_network_interface, + jsonb_array_elements(groups) as sg + ) + select + distinct s.arn as resource, + case + when a.secgrp_id = s.group_id then 'ok' + else 'alarm' + end as status, + case + when a.secgrp_id = s.group_id then s.title || ' is associated.' + else s.title || ' not associated.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "s.")} + from + aws_vpc_security_group s + left join associated_sg a on s.group_id = a.secgrp_id; + EOQ +} + +query "vpc_security_group_remote_administration_ipv4" { + sql = <<-EOQ + with bad_rules as ( + select + group_id, + count(*) as num_bad_rules + from + aws_vpc_security_group_rule + where + type = 'ingress' + and ( + cidr_ipv4 = '0.0.0.0/0' + ) + and ( + ( ip_protocol = '-1' -- all traffic + and from_port is null + ) + or ( + from_port >= 22 + and to_port <= 22 + ) + or ( + from_port >= 3389 + and to_port <= 3389 + ) + ) + group by + group_id + ) + select + arn as resource, + case + when bad_rules.group_id is null then 'ok' + else 'alarm' + end as status, + case + when bad_rules.group_id is null then sg.group_id || ' does not allow ingress to port 22 or 3389 from 0.0.0.0/0.' + else sg.group_id || ' contains ' || bad_rules.num_bad_rules || ' rule(s) that allow ingress to port 22 or 3389 from 0.0.0.0/0.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "sg.")} + from + aws_vpc_security_group as sg + left join bad_rules on bad_rules.group_id = sg.group_id; + EOQ +} + +query "vpc_security_group_remote_administration_ipv6" { + sql = <<-EOQ + with bad_rules as ( + select + group_id, + count(*) as num_bad_rules + from + aws_vpc_security_group_rule + where + type = 'ingress' + and ( + cidr_ipv6 = '::/0' + ) + and ( + ( ip_protocol = '-1' -- all traffic + and from_port is null + ) + or ( + from_port >= 22 + and to_port <= 22 + ) + or ( + from_port >= 3389 + and to_port <= 3389 + ) + ) + group by + group_id + ) + select + arn as resource, + case + when bad_rules.group_id is null then 'ok' + else 'alarm' + end as status, + case + when bad_rules.group_id is null then sg.group_id || ' does not allow ingress to port 22 or 3389 from ::/0.' + else sg.group_id || ' contains ' || bad_rules.num_bad_rules || ' rule(s) that allow ingress to port 22 or 3389 from ::/0.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "sg.")} + from + aws_vpc_security_group as sg + left join bad_rules on bad_rules.group_id = sg.group_id; + EOQ +} + +query "vpc_security_group_remote_administration" { + sql = <<-EOQ + with bad_rules as ( + select + group_id, + count(*) as num_bad_rules + from + aws_vpc_security_group_rule + where + type = 'ingress' + and ( + cidr_ipv4 = '0.0.0.0/0' + or cidr_ipv6 = '::/0' + ) + and ( + ( ip_protocol = '-1' -- all traffic + and from_port is null + ) + or ( + from_port >= 22 + and to_port <= 22 + ) + or ( + from_port >= 3389 + and to_port <= 3389 + ) + ) + group by + group_id + ) + select + arn as resource, + case + when bad_rules.group_id is null then 'ok' + else 'alarm' + end as status, + case + when bad_rules.group_id is null then sg.group_id || ' does not allow ingress to port 22 or 3389 from 0.0.0.0/0 or ::/0.' + else sg.group_id || ' contains ' || bad_rules.num_bad_rules || ' rule(s) that allow ingress to port 22 or 3389 from 0.0.0.0/0 or ::/0.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "sg.")} + from + aws_vpc_security_group as sg + left join bad_rules on bad_rules.group_id = sg.group_id; + EOQ +} + +query "vpc_security_group_restrict_ingress_rdp_all" { + sql = <<-EOQ + with ingress_rdp_rules as ( + select + group_id, + count(*) as num_rdp_rules + from + aws_vpc_security_group_rule + where + type = 'ingress' + and cidr_ipv4 = '0.0.0.0/0' + and ( + ( ip_protocol = '-1' + and from_port is null + ) + or ( + from_port >= 3389 + and to_port <= 3389 + ) + ) + group by + group_id + ) + select + arn as resource, + case + when ingress_rdp_rules.group_id is null then 'ok' + else 'alarm' + end as status, + case + when ingress_rdp_rules.group_id is null then sg.group_id || ' ingress restricted for RDP from 0.0.0.0/0.' + else sg.group_id || ' contains ' || ingress_rdp_rules.num_rdp_rules || ' ingress rule(s) allowing RDP from 0.0.0.0/0.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "sg.")} + from + aws_vpc_security_group as sg + left join ingress_rdp_rules on ingress_rdp_rules.group_id = sg.group_id; + EOQ +} + +query "vpc_security_group_unsued" { + sql = <<-EOQ + with associated_sg as ( + select + sg ->> 'GroupId' as secgrp_id + from + aws_ec2_network_interface, + jsonb_array_elements(groups) as sg + group by sg ->> 'GroupId' + union + select + sg ->> 'GroupId' as secgrp_id + from + aws_ec2_instance, + jsonb_array_elements(security_groups) as sg + group by sg ->> 'GroupId' + + ) + select + distinct s.arn as resource, + case + when a.secgrp_id is not null then 'ok' + else 'alarm' + end as status, + case + when a.secgrp_id is not null then s.title || ' is in use.' + else s.title || ' not in use.' + end as reason + ${local.tag_dimensions_sql} + ${replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "s.")} + from + aws_vpc_security_group as s + left join associated_sg as a on s.group_id = a.secgrp_id; + EOQ +} diff --git a/conformance_pack/waf.sp b/conformance_pack/waf.sp new file mode 100644 index 00000000..1e0c613d --- /dev/null +++ b/conformance_pack/waf.sp @@ -0,0 +1,58 @@ +# Non-Config rule query + +query "waf_rule_condition_attached" { + sql = <<-EOQ + select + akas as resource, + case + when predicates is null or jsonb_array_length(predicates) = 0 then 'alarm' + else 'ok' + end as status, + case + when predicates is null or jsonb_array_length(predicates) = 0 then title || ' has no attached conditions.' + else title || ' has attached conditions.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_waf_rule; + EOQ +} + +query "waf_rule_group_rule_attached" { + sql = <<-EOQ + select + arn as resource, + case + when activated_rules is null or jsonb_array_length(activated_rules) = 0 then 'alarm' + else 'ok' + end as status, + case + when activated_rules is null or jsonb_array_length(activated_rules) = 0 then title || ' has no attached rules.' + else title || ' has attached rules.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_waf_rule_group; + EOQ +} + +query "waf_web_acl_rule_attached" { + sql = <<-EOQ + select + arn as resource, + case + when rules is null or jsonb_array_length(rules) = 0 then 'alarm' + else 'ok' + end as status, + case + when rules is null or jsonb_array_length(rules) = 0 then title || ' has no attached rules.' + else title || ' has attached rules.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_waf_web_acl; + EOQ +} diff --git a/conformance_pack/wafv2.sp b/conformance_pack/wafv2.sp index f16f409c..2dbd5f7e 100644 --- a/conformance_pack/wafv2.sp +++ b/conformance_pack/wafv2.sp @@ -26,3 +26,54 @@ control "wafv2_web_acl_logging_enabled" { soc_2 = "true" }) } + +query "wafv2_web_acl_logging_enabled" { + sql = <<-EOQ + select + arn as resource, + case + when logging_configuration is null then 'alarm' + else 'ok' + end as status, + case + when logging_configuration is null then title || ' logging disabled.' + else title || ' logging enabled.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_wafv2_web_acl; + EOQ +} + +# Non-Config rule query + +query "wafv2_web_acl_rule_attached" { + sql = <<-EOQ + with rule_group_count as ( + select + count(*) as rule_group_count + from + aws_wafv2_web_acl, + jsonb_array_elements(rules) as r + where + r -> 'Statement' -> 'RuleGroupReferenceStatement' ->> 'ARN' is not null + group by + arn + ) + select + arn as resource, + case + when rules is null or jsonb_array_length(rules) = 0 then 'alarm' + else 'ok' + end as status, + case + when rules is null or jsonb_array_length(rules) = 0 then title || ' has no attached rules.' + else title || ' has ' || (select rule_group_count from rule_group_count ) || ' rule group(s) and ' || (jsonb_array_length(rules) - (select rule_group_count from rule_group_count )) || ' rule(s) attached.' + end as reason + ${local.tag_dimensions_sql} + ${local.common_dimensions_sql} + from + aws_wafv2_web_acl; + EOQ +} diff --git a/docs/index.md b/docs/index.md index 86029b11..2f65b9ea 100644 --- a/docs/index.md +++ b/docs/index.md @@ -130,6 +130,31 @@ This mod uses the credentials configured in the [Steampipe AWS plugin](https://h No extra configuration is required. +### Common and Tag Dimensions + +The benchmark queries use common properties (like `account_id`, `connection_name` and `region`) and tags that are defined in the form of a default list of strings in the `mod.sp` file. These properties can be overwritten in several ways: + +- Copy and rename the `steampipe.spvars.example` file to `steampipe.spvars`, and then modify the variable values inside that file +- Pass in a value on the command line: + + ```shell + steampipe check benchmark.cis_v150 --var 'common_dimensions=["account_id", "connection_name", "region"]' + ``` + + ```shell + steampipe check benchmark.cis_v150 --var 'tag_dimensions=["Environment", "Owner"]' + ``` + +- Set an environment variable: + + ```shell + SP_VAR_common_dimensions='["account_id", "connection_name", "region"]' steampipe check control.cis_v150_5_1 + ``` + + ```shell + SP_VAR_tag_dimensions='["Environment", "Owner"]' steampipe check control.cis_v150_5_1 + ``` + ## Contributing If you have an idea for additional controls or just want to help maintain and extend this mod ([or others](https://github.com/topics/steampipe-mod)) we would love you to join the community and start contributing. diff --git a/mod.sp b/mod.sp index 1fffd503..07864a9b 100644 --- a/mod.sp +++ b/mod.sp @@ -7,6 +7,59 @@ locals { } } +variable "common_dimensions" { + type = list(string) + description = "A list of common dimensions to add to each control." + # Define which common dimensions should be added to each control. + # - account_id + # - connection_name (_ctx ->> 'connection_name') + # - region + default = [ "account_id", "region" ] +} + +variable "tag_dimensions" { + type = list(string) + description = "A list of tags to add as dimensions to each control." + # A list of tag names to include as dimensions for resources that support + # tags (e.g. "Owner", "Environment"). Default to empty since tag names are + # a personal choice - for commonly used tag names see + # https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html#tag-categories + default = [] +} + +locals { + + # Local internal variable to build the SQL select clause for common + # dimensions using a table name qualifier if required. Do not edit directly. + common_dimensions_qualifier_sql = <<-EOQ + %{~ if contains(var.common_dimensions, "connection_name") }, __QUALIFIER___ctx ->> 'connection_name' as connection_name%{ endif ~} + %{~ if contains(var.common_dimensions, "region") }, __QUALIFIER__region%{ endif ~} + %{~ if contains(var.common_dimensions, "account_id") }, __QUALIFIER__account_id%{ endif ~} + EOQ + + common_dimensions_qualifier_global_sql = <<-EOQ + %{~ if contains(var.common_dimensions, "connection_name") }, __QUALIFIER___ctx ->> 'connection_name' as connection_name%{ endif ~} + %{~ if contains(var.common_dimensions, "account_id") }, __QUALIFIER__account_id%{ endif ~} + EOQ + + # Local internal variable to build the SQL select clause for tag + # dimensions. Do not edit directly. + tag_dimensions_qualifier_sql = <<-EOQ + %{~ for dim in var.tag_dimensions }, __QUALIFIER__tags ->> '${dim}' as "${replace(dim, "\"", "\"\"")}"%{ endfor ~} + EOQ + +} + +locals { + + # Local internal variable with the full SQL select clause for common + # dimensions. Do not edit directly. + common_dimensions_sql = replace(local.common_dimensions_qualifier_sql, "__QUALIFIER__", "") + common_dimensions_global_sql = replace(local.common_dimensions_qualifier_global_sql, "__QUALIFIER__", "") + tag_dimensions_sql = replace(local.tag_dimensions_qualifier_sql, "__QUALIFIER__", "") + +} + mod "aws_compliance" { # hub metadata title = "AWS Compliance" diff --git a/query/account/account_alternate_contact_security_registered.sql b/query/account/account_alternate_contact_security_registered.sql deleted file mode 100644 index dfa96094..00000000 --- a/query/account/account_alternate_contact_security_registered.sql +++ /dev/null @@ -1,28 +0,0 @@ -with alternate_security_contact as ( - select - name, - account_id - from - aws_account_alternate_contact - where - contact_type = 'SECURITY' -) -select - -- Required Columns - arn as resource, - case - when a.partition = 'aws-us-gov' then 'info' - -- Name is a required field if setting a security contact - when c.name is not null then 'ok' - else 'alarm' - end as status, - case - when a.partition = 'aws-us-gov' then a.title || ' in GovCloud, manual verification required.' - when c.name is not null then a.title || ' has security contact ' || c.name || ' registered.' - else a.title || ' security contact not registered.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join alternate_security_contact as c on c.account_id = a.account_id; diff --git a/query/acm/acm_certificate_expires_30_days.sql b/query/acm/acm_certificate_expires_30_days.sql deleted file mode 100644 index a4123ef2..00000000 --- a/query/acm/acm_certificate_expires_30_days.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - certificate_arn as resource, - case - when renewal_eligibility = 'INELIGIBLE' then 'skip' - when date(not_after) - date(current_date) >= 30 then 'ok' - else 'alarm' - end as status, - case - when renewal_eligibility = 'INELIGIBLE' then title || ' not eligible for renewal.' - else title || ' expires ' || to_char(not_after, 'DD-Mon-YYYY') || - ' (' || extract(day from not_after - current_date) || ' days).' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_acm_certificate; \ No newline at end of file diff --git a/query/acm/acm_certificate_no_wildcard_domain_name.sql b/query/acm/acm_certificate_no_wildcard_domain_name.sql deleted file mode 100644 index 9185544e..00000000 --- a/query/acm/acm_certificate_no_wildcard_domain_name.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - certificate_arn as resource, - case - when domain_name like '*%' then 'alarm' - else 'ok' - end as status, - case - when domain_name like '*%' then title || ' uses wildcard domain name.' - else title || ' does not use wildcard domain name.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_acm_certificate; diff --git a/query/acm/acm_certificate_transparency_logging_enabled.sql b/query/acm/acm_certificate_transparency_logging_enabled.sql deleted file mode 100644 index 899cb68d..00000000 --- a/query/acm/acm_certificate_transparency_logging_enabled.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - certificate_arn as resource, - case - when type = 'IMPORTED' then 'skip' - when certificate_transparency_logging_preference = 'ENABLED' then 'ok' - else 'alarm' - end as status, - case - when type = 'IMPORTED' then title || ' is imported.' - when certificate_transparency_logging_preference = 'ENABLED' then title || ' transparency logging enabled.' - else title || ' transparency logging disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_acm_certificate; diff --git a/query/apigateway/api_gatewayv2_route_authorization_type_configured.sql b/query/apigateway/api_gatewayv2_route_authorization_type_configured.sql deleted file mode 100644 index d7ca45f8..00000000 --- a/query/apigateway/api_gatewayv2_route_authorization_type_configured.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - 'arn:' || partition || ':apigateway:' || region || '::/apis/' || api_id || '/routes/' || route_id as resource, - case - when authorization_type is null then 'alarm' - else 'ok' - end as status, - case - when authorization_type is null then route_id || ' authorization type not configured.' - else route_id || ' authorization type ' || authorization_type || ' configured.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_api_gatewayv2_route; \ No newline at end of file diff --git a/query/apigateway/apigateway_rest_api_authorizers_configured.sql b/query/apigateway/apigateway_rest_api_authorizers_configured.sql deleted file mode 100644 index 0f9e1138..00000000 --- a/query/apigateway/apigateway_rest_api_authorizers_configured.sql +++ /dev/null @@ -1,17 +0,0 @@ -select - -- Required Columns - p.name as resource, - case - when jsonb_array_length(a.provider_arns) > 0 then 'ok' - else 'alarm' - end as status, - case - when jsonb_array_length(a.provider_arns) > 0 then p.name || ' authorizers configured.' - else p.name || ' authorizers not configured.' - end as reason, - -- Additional Dimensions - p.region, - p.account_id -from - aws_api_gateway_rest_api as p - left join aws_api_gateway_authorizer as a on p.api_id = a.rest_api_id; \ No newline at end of file diff --git a/query/apigateway/apigateway_rest_api_stage_use_ssl_certificate.sql b/query/apigateway/apigateway_rest_api_stage_use_ssl_certificate.sql deleted file mode 100644 index 5c4e4417..00000000 --- a/query/apigateway/apigateway_rest_api_stage_use_ssl_certificate.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when client_certificate_id is null then 'alarm' - else 'ok' - end as status, - case - when client_certificate_id is null then title || ' does not use SSL certificate.' - else title || ' uses SSL certificate.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_api_gateway_stage; \ No newline at end of file diff --git a/query/apigateway/apigateway_rest_api_stage_xray_tracing_enabled.sql b/query/apigateway/apigateway_rest_api_stage_xray_tracing_enabled.sql deleted file mode 100644 index a5529122..00000000 --- a/query/apigateway/apigateway_rest_api_stage_xray_tracing_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when tracing_enabled then 'ok' - else 'alarm' - end as status, - case - when tracing_enabled then title || ' X-Ray tracing enabled.' - else title || ' X-Ray tracing disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_api_gateway_stage; \ No newline at end of file diff --git a/query/apigateway/apigateway_stage_cache_encryption_at_rest_enabled.sql b/query/apigateway/apigateway_stage_cache_encryption_at_rest_enabled.sql deleted file mode 100644 index 6b106e80..00000000 --- a/query/apigateway/apigateway_stage_cache_encryption_at_rest_enabled.sql +++ /dev/null @@ -1,19 +0,0 @@ -select - -- Required Columns - 'arn:' || partition || ':apigateway:' || region || '::/apis/' || rest_api_id || '/stages/' || name as resource, - case - when method_settings -> '*/*' ->> 'CachingEnabled' = 'true' - and method_settings -> '*/*' ->> 'CacheDataEncrypted' = 'true' then 'ok' - else 'alarm' - end as status, - case - when method_settings -> '*/*' ->> 'CachingEnabled' = 'true' - and method_settings -> '*/*' ->> 'CacheDataEncrypted' = 'true' - then title || ' API cache and encryption enabled.' - else title || ' API cache and encryption not enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_api_gateway_stage; \ No newline at end of file diff --git a/query/apigateway/apigateway_stage_logging_enabled.sql b/query/apigateway/apigateway_stage_logging_enabled.sql deleted file mode 100644 index 7f982804..00000000 --- a/query/apigateway/apigateway_stage_logging_enabled.sql +++ /dev/null @@ -1,37 +0,0 @@ -with all_stages as ( - select - name as stage_name, - 'arn:' || partition || ':apigateway:' || region || '::/apis/' || rest_api_id || '/stages/' || name as arn, - method_settings -> '*/*' ->> 'LoggingLevel' as log_level, - title, - region, - account_id - from - aws_api_gateway_stage - union - select - stage_name, - 'arn:' || partition || ':apigateway:' || region || '::/apis/' || api_id || '/stages/' || stage_name as arn, - default_route_logging_level as log_level, - title, - region, - account_id - from - aws_api_gatewayv2_stage -) -select - -- Required Columns - arn as resource, - case - when log_level is null or log_level = '' or log_level = 'OFF' then 'alarm' - else 'ok' - end as status, - case - when log_level is null or log_level = '' or log_level = 'OFF' then title || ' logging not enabled.' - else title || ' logging enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - all_stages; \ No newline at end of file diff --git a/query/apigateway/apigateway_stage_use_waf_web_acl.sql b/query/apigateway/apigateway_stage_use_waf_web_acl.sql deleted file mode 100644 index 0704a95e..00000000 --- a/query/apigateway/apigateway_stage_use_waf_web_acl.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when web_acl_arn is not null then 'ok' - else 'alarm' - end as status, - case - when web_acl_arn is not null then title || ' associated with WAF web ACL.' - else title || ' not associated with WAF web ACL.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_api_gateway_stage; \ No newline at end of file diff --git a/query/apigateway/gatewayv2_stage_access_logging_enabled.sql b/query/apigateway/gatewayv2_stage_access_logging_enabled.sql deleted file mode 100644 index 571bbb7a..00000000 --- a/query/apigateway/gatewayv2_stage_access_logging_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - 'arn:' || partition || ':apigateway:' || region || '::/apis/' || api_id || '/stages/' || stage_name as resource, - case - when access_log_settings is null then 'alarm' - else 'ok' - end as status, - case - when access_log_settings is null then title || ' access logging disabled.' - else title || ' access logging enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_api_gatewayv2_stage; \ No newline at end of file diff --git a/query/autoscaling/autoscaling_group_multiple_az_configured.sql b/query/autoscaling/autoscaling_group_multiple_az_configured.sql deleted file mode 100644 index cd64d239..00000000 --- a/query/autoscaling/autoscaling_group_multiple_az_configured.sql +++ /dev/null @@ -1,13 +0,0 @@ -select - -- Required Columns - autoscaling_group_arn as resource, - case - when jsonb_array_length(availability_zones) > 1 then 'ok' - else 'alarm' - end as status, - title || ' has ' || jsonb_array_length(availability_zones) || ' availability zone(s).' as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_autoscaling_group; diff --git a/query/autoscaling/autoscaling_group_no_suspended_processe.sql b/query/autoscaling/autoscaling_group_no_suspended_processe.sql deleted file mode 100644 index 1a8747f8..00000000 --- a/query/autoscaling/autoscaling_group_no_suspended_processe.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - autoscaling_group_arn as resource, - case - when suspended_processes is null then 'ok' - else 'alarm' - end as status, - case - when suspended_processes is null then title || ' has no suspended process.' - else title || ' has suspended process.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_autoscaling_group; \ No newline at end of file diff --git a/query/autoscaling/autoscaling_group_uses_ec2_launch_template.sql b/query/autoscaling/autoscaling_group_uses_ec2_launch_template.sql deleted file mode 100644 index 61f42d52..00000000 --- a/query/autoscaling/autoscaling_group_uses_ec2_launch_template.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - autoscaling_group_arn as resource, - case - when launch_template_id is not null then 'ok' - else 'alarm' - end as status, - case - when launch_template_id is not null then title || ' using an EC2 launch template.' - else title || ' not using an EC2 launch template.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_autoscaling_group; \ No newline at end of file diff --git a/query/autoscaling/autoscaling_group_with_lb_use_health_check.sql b/query/autoscaling/autoscaling_group_with_lb_use_health_check.sql deleted file mode 100644 index e30e6124..00000000 --- a/query/autoscaling/autoscaling_group_with_lb_use_health_check.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - autoscaling_group_arn as resource, - case - when load_balancer_names is null and target_group_arns is null then 'alarm' - when health_check_type != 'ELB' then 'alarm' - else 'ok' - end as status, - case - when load_balancer_names is null and target_group_arns is null then title || ' not associated with a load balancer.' - when health_check_type != 'ELB' then title || ' does not use ELB health check.' - else title || ' uses ELB health check.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_autoscaling_group; diff --git a/query/autoscaling/autoscaling_launch_config_hop_limit.sql b/query/autoscaling/autoscaling_launch_config_hop_limit.sql deleted file mode 100644 index bc5745f1..00000000 --- a/query/autoscaling/autoscaling_launch_config_hop_limit.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - launch_configuration_arn as resource, - case - when metadata_options_put_response_hop_limit is null then 'ok' - when metadata_options_put_response_hop_limit > 1 then 'alarm' - else 'ok' - end as status, - case - --If you do not specify a value, the hop limit default is 1. - when metadata_options_put_response_hop_limit is null then title || ' metadata response hop limit set to default.' - else title || ' has a metadata response hop limit of ' || metadata_options_put_response_hop_limit || '.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_launch_configuration; \ No newline at end of file diff --git a/query/autoscaling/autoscaling_launch_config_public_ip_disabled.sql b/query/autoscaling/autoscaling_launch_config_public_ip_disabled.sql deleted file mode 100644 index 93cd5f8c..00000000 --- a/query/autoscaling/autoscaling_launch_config_public_ip_disabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - launch_configuration_arn as resource, - case - when associate_public_ip_address then 'alarm' - else 'ok' - end as status, - case - when associate_public_ip_address then title || ' public IP enabled.' - else title || ' public IP disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_launch_configuration; \ No newline at end of file diff --git a/query/autoscaling/autoscaling_launch_config_requires_imdsv2.sql b/query/autoscaling/autoscaling_launch_config_requires_imdsv2.sql deleted file mode 100644 index 3b98cede..00000000 --- a/query/autoscaling/autoscaling_launch_config_requires_imdsv2.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - launch_configuration_arn as resource, - case - when metadata_options_http_tokens = 'required' then 'ok' - else 'alarm' - end as status, - case - when metadata_options_http_tokens = 'required' then title || ' configured to use Instance Metadata Service Version 2 (IMDSv2).' - else title || ' not configured to use Instance Metadata Service Version 2 (IMDSv2).' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_launch_configuration; \ No newline at end of file diff --git a/query/autoscaling/autoscaling_use_multiple_instance_types_in_multiple_az.sql b/query/autoscaling/autoscaling_use_multiple_instance_types_in_multiple_az.sql deleted file mode 100644 index bfad9e53..00000000 --- a/query/autoscaling/autoscaling_use_multiple_instance_types_in_multiple_az.sql +++ /dev/null @@ -1,39 +0,0 @@ -with autoscaling_groups as ( - select - autoscaling_group_arn, - title, - mixed_instances_policy_launch_template_overrides, - region, - account_id - from - aws_ec2_autoscaling_group -), -distinct_instance_types_count as ( - select - autoscaling_group_arn, - count(distinct(e -> 'InstanceType')) as distinct_instance_types - from - autoscaling_groups, - jsonb_array_elements(mixed_instances_policy_launch_template_overrides) as e - group by - autoscaling_group_arn, - title, - mixed_instances_policy_launch_template_overrides -) -select - -- Required Columns - a.autoscaling_group_arn as resource, - case - when b.distinct_instance_types > 1 then 'ok' - else 'alarm' - end as status, - case - when b.distinct_instance_types > 1 then title || ' uses ' || b.distinct_instance_types || ' instance types.' - else title || ' does not use multiple instance types.' - end as reason, - -- Additional Dimensions - region, - account_id -from - autoscaling_groups as a - left join distinct_instance_types_count as b on a.autoscaling_group_arn = b.autoscaling_group_arn; \ No newline at end of file diff --git a/query/backup/backup_plan_min_retention_35_days.sql b/query/backup/backup_plan_min_retention_35_days.sql deleted file mode 100644 index 528b0bde..00000000 --- a/query/backup/backup_plan_min_retention_35_days.sql +++ /dev/null @@ -1,33 +0,0 @@ -with all_plans as ( - select - arn, - r as Rules, - title, - region, - account_id - from - aws_backup_plan, - jsonb_array_elements(backup_plan -> 'Rules') as r -) -select - -- Required Columns - -- The resource ARN can be duplicate as we are checking all the associated rules to the backup-plan - -- Backup plans are composed of one or more backup rules. - -- https://docs.aws.amazon.com/aws-backup/latest/devguide/creating-a-backup-plan.html - r.arn as resource, - case - when r.Rules is null then 'alarm' - when r.Rules ->> 'Lifecycle' is null then 'ok' - when (r.Rules -> 'Lifecycle' ->> 'DeleteAfterDays')::int >= 35 then 'ok' - else 'alarm' - end as status, - case - when r.Rules is null then r.title || ' retention period not set.' - when r.Rules ->> 'Lifecycle' is null then (r.Rules ->> 'RuleName') || ' retention period set to never expire.' - else (r.Rules ->> 'RuleName') || ' retention period set to ' || (r.Rules -> 'Lifecycle' ->> 'DeleteAfterDays') || ' days.' - end as reason, - -- Additional Dimensions - r.region, - r.account_id -from - all_plans as r; \ No newline at end of file diff --git a/query/backup/backup_recovery_point_encryption_enabled.sql b/query/backup/backup_recovery_point_encryption_enabled.sql deleted file mode 100644 index a4bda09d..00000000 --- a/query/backup/backup_recovery_point_encryption_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - recovery_point_arn as resource, - case - when is_encrypted then 'ok' - else 'alarm' - end as status, - case - when is_encrypted then recovery_point_arn || ' encryption enabled.' - else recovery_point_arn || ' encryption disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_backup_recovery_point; \ No newline at end of file diff --git a/query/backup/backup_recovery_point_manual_deletion_disabled.sql b/query/backup/backup_recovery_point_manual_deletion_disabled.sql deleted file mode 100644 index 92a6e376..00000000 --- a/query/backup/backup_recovery_point_manual_deletion_disabled.sql +++ /dev/null @@ -1,30 +0,0 @@ -with recovery_point_manual_deletion_disabled as ( - select - arn - from - aws_backup_vault, - jsonb_array_elements(policy -> 'Statement') as s - where - s ->> 'Effect' = 'Deny' and - s -> 'Action' @> '["backup:DeleteRecoveryPoint","backup:UpdateRecoveryPointLifecycle","backup:PutBackupVaultAccessPolicy"]' - and s ->> 'Resource' = '*' - group by - arn -) -select - -- Required Columns - v.arn as resource, - case - when d.arn is not null then 'ok' - else 'alarm' - end as status, - case - when d.arn is not null then v.title || ' recovery point manual deletion disabled.' - else v.title || ' recovery point manual deletion not disabled.' - end as reason, - -- Additional Dimensions - v.region, - v.account_id -from - aws_backup_vault as v - left join recovery_point_manual_deletion_disabled as d on v.arn = d.arn; \ No newline at end of file diff --git a/query/backup/backup_recovery_point_min_retention_35_days.sql b/query/backup/backup_recovery_point_min_retention_35_days.sql deleted file mode 100644 index 3b49af37..00000000 --- a/query/backup/backup_recovery_point_min_retention_35_days.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - recovery_point_arn as resource, - case - when (lifecycle -> 'DeleteAfterDays') is null then 'ok' - when (lifecycle -> 'DeleteAfterDays')::int >= 35 then 'ok' - else 'alarm' - end as status, - case - when (lifecycle -> 'DeleteAfterDays') is null then split_part(recovery_point_arn, ':', -1) || ' retention period set to never expire.' - else split_part(recovery_point_arn, ':', -1) || ' recovery point has a retention period of ' || (lifecycle -> 'DeleteAfterDays')::int || ' days.' - end as reason, - -- Additional Dimensions - region, - account_id - from aws_backup_recovery_point; \ No newline at end of file diff --git a/query/cloudformation/cloudformation_stack_notifications_enabled.sql b/query/cloudformation/cloudformation_stack_notifications_enabled.sql deleted file mode 100644 index 4e8ebc56..00000000 --- a/query/cloudformation/cloudformation_stack_notifications_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - id as resource, - case - when jsonb_array_length(notification_arns) > 0 then 'ok' - else 'alarm' - end as status, - case - when jsonb_array_length(notification_arns) > 0 then title || ' notifications enabled.' - else title || ' notifications disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudformation_stack; \ No newline at end of file diff --git a/query/cloudformation/cloudformation_stack_output_no_secrets.sql b/query/cloudformation/cloudformation_stack_output_no_secrets.sql deleted file mode 100644 index e2e618a6..00000000 --- a/query/cloudformation/cloudformation_stack_output_no_secrets.sql +++ /dev/null @@ -1,37 +0,0 @@ -with stack_output as ( - select - id, - jsonb_array_elements(outputs) -> 'OutputKey' as k, - jsonb_array_elements(outputs) -> 'OutputValue' as v, - region, - account_id - from - aws_cloudformation_stack -),stack_with_secrets as ( - select - distinct id - from - stack_output - where - lower(k::text) like any (array ['%pass%', '%secret%','%token%','%key%']) - or k::text ~ '(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]' or lower(v::text) like any (array ['%pass%', '%secret%','%token%','%key%']) or v::text ~ '(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]' -) -select - -- Required Columns - c.id as resource, - case - when c.outputs is null then 'ok' - when s.id is null then 'ok' - else 'alarm' - end as status, - case - when c.outputs is null then title || ' has no outputs.' - when s.id is null then title || ' no secrets found in outputs.' - else title || ' has secrets in outputs.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudformation_stack as c - left join stack_with_secrets as s on c.id = s.id; \ No newline at end of file diff --git a/query/cloudformation/cloudformation_stack_rollback_enabled.sql b/query/cloudformation/cloudformation_stack_rollback_enabled.sql deleted file mode 100644 index bb6245b0..00000000 --- a/query/cloudformation/cloudformation_stack_rollback_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - id as resource, - case - when not disable_rollback then 'ok' - else 'alarm' - end as status, - case - when not disable_rollback then title || ' rollback enabled.' - else title || ' rollback disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudformation_stack; \ No newline at end of file diff --git a/query/cloudformation/cloudformation_stack_termination_protection_enabled.sql b/query/cloudformation/cloudformation_stack_termination_protection_enabled.sql deleted file mode 100644 index 94190d5a..00000000 --- a/query/cloudformation/cloudformation_stack_termination_protection_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - id as resource, - case - when enable_termination_protection then 'ok' - else 'alarm' - end as status, - case - when enable_termination_protection then title || ' termination protection enabled.' - else title || ' termination protection disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudformation_stack; diff --git a/query/cloudfront/cloudfront_distribution_configured_with_origin_failover.sql b/query/cloudfront/cloudfront_distribution_configured_with_origin_failover.sql deleted file mode 100644 index 2da516a6..00000000 --- a/query/cloudfront/cloudfront_distribution_configured_with_origin_failover.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when origin_groups ->> 'Items' is not null then 'ok' - else 'alarm' - end as status, - case - when origin_groups ->> 'Items' is not null then title || ' origin group is configured.' - else title || ' origin group not configured.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudfront_distribution; \ No newline at end of file diff --git a/query/cloudfront/cloudfront_distribution_custom_origins_encryption_in_transit_enabled.sql b/query/cloudfront/cloudfront_distribution_custom_origins_encryption_in_transit_enabled.sql deleted file mode 100644 index 61506c0a..00000000 --- a/query/cloudfront/cloudfront_distribution_custom_origins_encryption_in_transit_enabled.sql +++ /dev/null @@ -1,44 +0,0 @@ -with viewer_protocol_policy_value as ( - select - distinct arn - from - aws_cloudfront_distribution, - jsonb_array_elements( - case jsonb_typeof(cache_behaviors -> 'Items') - when 'array' then (cache_behaviors -> 'Items') - else null end - ) as cb - where - cb ->> 'ViewerProtocolPolicy' = 'allow-all' -), - origin_protocol_policy_value as ( - select - distinct arn, - o -> 'CustomOriginConfig' ->> 'OriginProtocolPolicy' as origin_protocol_policy - from - aws_cloudfront_distribution, - jsonb_array_elements(origins) as o - where - o -> 'CustomOriginConfig' ->> 'OriginProtocolPolicy' = 'http-only' - or o -> 'CustomOriginConfig' ->> 'OriginProtocolPolicy' = 'match-viewer' -) -select - -- Required Columns - b.arn as resource, - case - when o.arn is not null and o.origin_protocol_policy = 'http-only' then 'alarm' - when o.arn is not null and o.origin_protocol_policy = 'match-viewer' and ( v.arn is not null or (default_cache_behavior ->> 'ViewerProtocolPolicy' = 'allow-all') ) then 'alarm' - else 'ok' - end as status, - case - when o.arn is not null and o.origin_protocol_policy = 'http-only' then title || ' custom origins traffic not encrypted in transit.' - when o.arn is not null and o.origin_protocol_policy = 'match-viewer' and ( v.arn is not null or (default_cache_behavior ->> 'ViewerProtocolPolicy' = 'allow-all') ) then title || ' custom origins traffic not encrypted in transit.' - else title || ' custom origins traffic encrypted in transit.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudfront_distribution as b - left join origin_protocol_policy_value as o on b.arn = o.arn - left join viewer_protocol_policy_value as v on b.arn = v.arn; \ No newline at end of file diff --git a/query/cloudfront/cloudfront_distribution_default_root_object_configured.sql b/query/cloudfront/cloudfront_distribution_default_root_object_configured.sql deleted file mode 100644 index 79d3ed21..00000000 --- a/query/cloudfront/cloudfront_distribution_default_root_object_configured.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when default_root_object = '' then 'alarm' - else 'ok' - end as status, - case - when default_root_object = '' then title || ' default root object not configured.' - else title || ' default root object configured.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudfront_distribution; \ No newline at end of file diff --git a/query/cloudfront/cloudfront_distribution_encryption_in_transit_enabled.sql b/query/cloudfront/cloudfront_distribution_encryption_in_transit_enabled.sql deleted file mode 100644 index bd01607e..00000000 --- a/query/cloudfront/cloudfront_distribution_encryption_in_transit_enabled.sql +++ /dev/null @@ -1,30 +0,0 @@ -with data as ( - select - distinct arn - from - aws_cloudfront_distribution, - jsonb_array_elements( - case jsonb_typeof(cache_behaviors -> 'Items') - when 'array' then (cache_behaviors -> 'Items') - else null end - ) as cb - where - cb ->> 'ViewerProtocolPolicy' = 'allow-all' -) -select - -- Required Columns - b.arn as resource, - case - when d.arn is not null or (default_cache_behavior ->> 'ViewerProtocolPolicy' = 'allow-all') then 'alarm' - else 'ok' - end as status, - case - when d.arn is not null or (default_cache_behavior ->> 'ViewerProtocolPolicy' = 'allow-all') then title || ' data not encrypted in transit.' - else title || ' data encrypted in transit.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudfront_distribution as b - left join data as d on b.arn = d.arn; \ No newline at end of file diff --git a/query/cloudfront/cloudfront_distribution_geo_restrictions_enabled.sql b/query/cloudfront/cloudfront_distribution_geo_restrictions_enabled.sql deleted file mode 100644 index 366f2448..00000000 --- a/query/cloudfront/cloudfront_distribution_geo_restrictions_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when restrictions -> 'GeoRestriction' ->> 'RestrictionType' = 'none' then 'alarm' - else 'ok' - end as status, - case - when restrictions -> 'GeoRestriction' ->> 'RestrictionType' = 'none' then title || ' Geo Restriction disabled.' - else title || ' Geo Restriction enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudfront_distribution; \ No newline at end of file diff --git a/query/cloudfront/cloudfront_distribution_logging_enabled.sql b/query/cloudfront/cloudfront_distribution_logging_enabled.sql deleted file mode 100644 index 1a07100c..00000000 --- a/query/cloudfront/cloudfront_distribution_logging_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when logging ->> 'Enabled' = 'true' then 'ok' - else 'alarm' - end as status, - case - when logging ->> 'Enabled' = 'true' then title || ' logging enabled.' - else title || ' logging disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudfront_distribution; \ No newline at end of file diff --git a/query/cloudfront/cloudfront_distribution_no_deprecated_ssl_protocol.sql b/query/cloudfront/cloudfront_distribution_no_deprecated_ssl_protocol.sql deleted file mode 100644 index b7d1d094..00000000 --- a/query/cloudfront/cloudfront_distribution_no_deprecated_ssl_protocol.sql +++ /dev/null @@ -1,27 +0,0 @@ -with origin_ssl_protocols as ( - select - distinct arn, - o -> 'CustomOriginConfig' ->> 'OriginProtocolPolicy' as origin_protocol_policy - from - aws_cloudfront_distribution, - jsonb_array_elements(origins) as o - where - o -> 'CustomOriginConfig' -> 'OriginSslProtocols' -> 'Items' @> '["SSLv3"]' -) -select - -- Required Columns - b.arn as resource, - case - when o.arn is null then 'ok' - else 'alarm' - end as status, - case - when o.arn is null then title || ' does not have deprecated SSL protocols.' - else title || ' has deprecated SSL protocols.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudfront_distribution as b - left join origin_ssl_protocols as o on b.arn = o.arn; \ No newline at end of file diff --git a/query/cloudfront/cloudfront_distribution_no_non_existent_s3_origin.sql b/query/cloudfront/cloudfront_distribution_no_non_existent_s3_origin.sql deleted file mode 100644 index 023bbb9c..00000000 --- a/query/cloudfront/cloudfront_distribution_no_non_existent_s3_origin.sql +++ /dev/null @@ -1,39 +0,0 @@ -with distribution_with_non_existent_bucket as ( - select - distinct d.arn as arn, - to_jsonb(string_to_array((string_agg(split_part(o ->> 'Id', '.s3', 1), ',')),',')) as bucket_name_list - from - aws_cloudfront_distribution as d, - jsonb_array_elements(d.origins) as o - left join aws_s3_bucket as b on b.name = split_part(o ->> 'Id', '.s3', 1) - where - b.name is null - and o ->> 'DomainName' like '%.s3.%' - group by - d.arn -) -select - -- Required Columns - distinct b.arn as resource, - case - when b.arn is null then 'ok' - else 'alarm' - end as status, - case - when b.arn is null then title || ' does not point to any non-existent S3 origins.' - when jsonb_array_length(b.bucket_name_list) > 0 - then title || - case - when jsonb_array_length(b.bucket_name_list) > 2 - then concat(' point to non-existent S3 origins ', b.bucket_name_list #>> '{0}', ', ', b.bucket_name_list #>> '{1}', ' and ' || (jsonb_array_length(b.bucket_name_list) - 2)::text || ' more.' ) - when jsonb_array_length(b.bucket_name_list) = 2 - then concat(' point to non-existent S3 origins ', b.bucket_name_list #>> '{0}', ' and ', b.bucket_name_list #>> '{1}', '.') - else concat(' point to non-existent S3 origin ', b.bucket_name_list #>> '{0}', '.') - end - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudfront_distribution as d - left join distribution_with_non_existent_bucket as b on b.arn = d.arn diff --git a/query/cloudfront/cloudfront_distribution_non_s3_origins_encryption_in_transit_enabled.sql b/query/cloudfront/cloudfront_distribution_non_s3_origins_encryption_in_transit_enabled.sql deleted file mode 100644 index d8e34753..00000000 --- a/query/cloudfront/cloudfront_distribution_non_s3_origins_encryption_in_transit_enabled.sql +++ /dev/null @@ -1,45 +0,0 @@ -with viewer_protocol_policy_value as ( - select - distinct arn - from - aws_cloudfront_distribution, - jsonb_array_elements( - case jsonb_typeof(cache_behaviors -> 'Items') - when 'array' then (cache_behaviors -> 'Items') - else null end - ) as cb - where - cb ->> 'ViewerProtocolPolicy' = 'allow-all' -), -origin_protocol_policy_value as ( - select - distinct arn, - o -> 'CustomOriginConfig' ->> 'OriginProtocolPolicy' as origin_protocol_policy - from - aws_cloudfront_distribution, - jsonb_array_elements(origins) as o - where - o -> 'CustomOriginConfig' ->> 'OriginProtocolPolicy' = 'http-only' - or o -> 'CustomOriginConfig' ->> 'OriginProtocolPolicy' = 'match-viewer' - and o -> 'S3OriginConfig' is null -) -select - -- Required Columns - b.arn as resource, - case - when o.arn is not null and o.origin_protocol_policy = 'http-only' then 'alarm' - when o.arn is not null and o.origin_protocol_policy = 'match-viewer' and ( v.arn is not null or (default_cache_behavior ->> 'ViewerProtocolPolicy' = 'allow-all') ) then 'alarm' - else 'ok' - end as status, - case - when o.arn is not null and o.origin_protocol_policy = 'http-only' then title || ' origins traffic not encrypted in transit.' - when o.arn is not null and o.origin_protocol_policy = 'match-viewer' and ( v.arn is not null or (default_cache_behavior ->> 'ViewerProtocolPolicy' = 'allow-all') ) then title || ' origins traffic not encrypted in transit.' - else title || ' origins traffic encrypted in transit.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudfront_distribution as b - left join origin_protocol_policy_value as o on b.arn = o.arn - left join viewer_protocol_policy_value as v on b.arn = v.arn; \ No newline at end of file diff --git a/query/cloudfront/cloudfront_distribution_origin_access_identity_enabled.sql b/query/cloudfront/cloudfront_distribution_origin_access_identity_enabled.sql deleted file mode 100644 index d8807dc0..00000000 --- a/query/cloudfront/cloudfront_distribution_origin_access_identity_enabled.sql +++ /dev/null @@ -1,21 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when o ->> 'DomainName' not like '%s3.amazonaws.com' then 'skip' - when o ->> 'DomainName' like '%s3.amazonaws.com' - and o -> 'S3OriginConfig' ->> 'OriginAccessIdentity' = '' then 'alarm' - else 'ok' - end as status, - case - when o ->> 'DomainName' not like '%s3.amazonaws.com' then title || ' origin type is not s3.' - when o ->> 'DomainName' like '%s3.amazonaws.com' - and o -> 'S3OriginConfig' ->> 'OriginAccessIdentity' = '' then title || ' origin access identity not configured.' - else title || ' origin access identity configured.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudfront_distribution, - jsonb_array_elements(origins) as o; diff --git a/query/cloudfront/cloudfront_distribution_sni_enabled.sql b/query/cloudfront/cloudfront_distribution_sni_enabled.sql deleted file mode 100644 index 93a35a58..00000000 --- a/query/cloudfront/cloudfront_distribution_sni_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when viewer_certificate ->> 'SSLSupportMethod' = 'sni-only' then 'ok' - else 'alarm' - end as status, - case - when viewer_certificate ->> 'SSLSupportMethod' = 'sni-only' then title || ' SNI enabled.' - else title || ' SNI disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudfront_distribution; \ No newline at end of file diff --git a/query/cloudfront/cloudfront_distribution_use_custom_ssl_certificate.sql b/query/cloudfront/cloudfront_distribution_use_custom_ssl_certificate.sql deleted file mode 100644 index 1af5e021..00000000 --- a/query/cloudfront/cloudfront_distribution_use_custom_ssl_certificate.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when viewer_certificate ->> 'ACMCertificateArn' is not null and viewer_certificate ->> 'Certificate' is not null then 'ok' - else 'alarm' - end as status, - case - when viewer_certificate ->> 'ACMCertificateArn' is not null and viewer_certificate ->> 'Certificate' is not null then title || ' uses custom SSL certificate.' - else title || ' does not use custom SSL certificate.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudfront_distribution; \ No newline at end of file diff --git a/query/cloudfront/cloudfront_distribution_use_secure_cipher.sql b/query/cloudfront/cloudfront_distribution_use_secure_cipher.sql deleted file mode 100644 index 35876eab..00000000 --- a/query/cloudfront/cloudfront_distribution_use_secure_cipher.sql +++ /dev/null @@ -1,27 +0,0 @@ -with origin_protocols as ( - select - distinct arn, - o -> 'CustomOriginConfig' ->> 'OriginSslProtocols' as origin_ssl_policy - from - aws_cloudfront_distribution, - jsonb_array_elements(origins) as o - where - o -> 'CustomOriginConfig' -> 'OriginSslProtocols' -> 'Items' @> '["TLSv1.2%", "TLSv1.1%"]' -) -select - -- Required Columns - b.arn as resource, - case - when o.arn is not null then 'ok' - else 'alarm' - end as status, - case - when o.arn is not null then title || ' use secure cipher.' - else title || ' does not use secure cipher.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudfront_distribution as b - left join origin_protocols as o on b.arn = o.arn; \ No newline at end of file diff --git a/query/cloudfront/cloudfront_distribution_waf_enabled.sql b/query/cloudfront/cloudfront_distribution_waf_enabled.sql deleted file mode 100644 index 69ee7b81..00000000 --- a/query/cloudfront/cloudfront_distribution_waf_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when web_acl_id <> '' then 'ok' - else 'alarm' - end as status, - case - when web_acl_id <> '' then title || ' associated with WAF.' - else title || ' not associated with WAF.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudfront_distribution; \ No newline at end of file diff --git a/query/cloudtrail/cloudtrail_bucket_not_public.sql b/query/cloudtrail/cloudtrail_bucket_not_public.sql deleted file mode 100644 index 62abfdf5..00000000 --- a/query/cloudtrail/cloudtrail_bucket_not_public.sql +++ /dev/null @@ -1,49 +0,0 @@ -with public_bucket_data as ( --- note the counts are not exactly CORRECT because of the jsonb_array_elements joins, --- but will be non-zero if any matches are found - select - t.s3_bucket_name as name, - b.arn, - t.region, - t.account_id, - count(acl_grant) filter (where acl_grant -> 'Grantee' ->> 'URI' like '%acs.amazonaws.com/groups/global/AllUsers') as all_user_grants, - count(acl_grant) filter (where acl_grant -> 'Grantee' ->> 'URI' like '%acs.amazonaws.com/groups/global/AuthenticatedUsers') as auth_user_grants, - count(s) filter (where s ->> 'Effect' = 'Allow' and p = '*' ) as anon_statements - from - aws_cloudtrail_trail as t - left join aws_s3_bucket as b on t.s3_bucket_name = b.name - left join jsonb_array_elements(acl -> 'Grants') as acl_grant on true - left join jsonb_array_elements(policy_std -> 'Statement') as s on true - left join jsonb_array_elements_text(s -> 'Principal' -> 'AWS') as p on true - group by - t.s3_bucket_name, - b.arn, - t.region, - t.account_id -) - -select - -- Required Columns - case - when arn is null then 'arn:aws:s3::' || name - else arn - end as resource, - case - when arn is null then 'skip' - when all_user_grants > 0 then 'alarm' - when auth_user_grants > 0 then 'alarm' - when anon_statements > 0 then 'alarm' - else 'ok' - end as status, - case - when arn is null then name || ' not found in account ' || account_id || '.' - when all_user_grants > 0 then name || ' grants access to AllUsers in ACL.' - when auth_user_grants > 0 then name || ' grants access to AuthenticatedUsers in ACL.' - when anon_statements > 0 then name || ' grants access to AWS:*" in bucket policy.' - else name || ' does not grant anonymous access in ACL or bucket policy.' - end as reason, - -- Additional Dimensions - region, - account_id -from - public_bucket_data diff --git a/query/cloudtrail/cloudtrail_multi_region_read_write_enabled.sql b/query/cloudtrail/cloudtrail_multi_region_read_write_enabled.sql deleted file mode 100644 index ba4dd3cf..00000000 --- a/query/cloudtrail/cloudtrail_multi_region_read_write_enabled.sql +++ /dev/null @@ -1,36 +0,0 @@ -with event_selectors_trail_details as ( - select - distinct account_id - from - aws_cloudtrail_trail, - jsonb_array_elements(event_selectors) as e - where - (is_logging and is_multi_region_trail and e ->> 'ReadWriteType' = 'All') -), -advanced_event_selectors_trail_details as ( - select - distinct account_id - from - aws_cloudtrail_trail, - jsonb_array_elements_text(advanced_event_selectors) as a - where - -- when readOnly = true, then it is readOnly, when readOnly = false then it is writeOnly, if advanced_event_selectors is not null then it is both ReadWriteType - (is_logging and is_multi_region_trail and advanced_event_selectors is not null and (not a like '%readOnly%')) -) -select - -- Required Columns - a.title as resource, - case - when d.account_id is null and ad.account_id is null then 'alarm' - else 'ok' - end as status, - case - when d.account_id is null and ad.account_id is null then 'cloudtrail disabled.' - else 'cloudtrail enabled.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join event_selectors_trail_details as d on d.account_id = a.account_id - left join advanced_event_selectors_trail_details as ad on ad.account_id = a.account_id; \ No newline at end of file diff --git a/query/cloudtrail/cloudtrail_multi_region_trail_enabled.sql b/query/cloudtrail/cloudtrail_multi_region_trail_enabled.sql deleted file mode 100644 index 0269efcc..00000000 --- a/query/cloudtrail/cloudtrail_multi_region_trail_enabled.sql +++ /dev/null @@ -1,44 +0,0 @@ -with multi_region_trails as ( - select - account_id, - count(account_id) as num_multregion_trails - from - aws_cloudtrail_trail - where - is_multi_region_trail and region = home_region - and is_logging - group by - account_id, - is_multi_region_trail -), organization_trails as ( - select - is_organization_trail, - is_logging, - is_multi_region_trail, - account_id - from - aws_cloudtrail_trail - where - is_organization_trail -) -select - -- Required Columns - distinct a.arn as resource, - case - when coalesce(num_multregion_trails, 0) >= 1 then 'ok' - when o.is_organization_trail and o.is_logging and o.is_multi_region_trail then 'ok' - when o.is_organization_trail and o.is_multi_region_trail and o.is_logging is null then 'info' - else 'alarm' - end as status, - case - when coalesce(num_multregion_trails, 0) >= 1 then a.title || ' has ' || coalesce(num_multregion_trails, 0) || ' multi-region trail(s).' - when o.is_organization_trail and o.is_logging and o.is_multi_region_trail then a.title || ' has multi-region trail(s).' - when o.is_organization_trail and o.is_multi_region_trail and o.is_logging is null then a.title || ' has organization trail, check organization account for cloudtrail logging status.' - else a.title || ' does not have multi-region trail(s).' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join multi_region_trails as b on a.account_id = b.account_id - left join organization_trails as o on a.account_id = o.account_id \ No newline at end of file diff --git a/query/cloudtrail/cloudtrail_s3_data_events_enabled.sql b/query/cloudtrail/cloudtrail_s3_data_events_enabled.sql deleted file mode 100644 index 08bdd690..00000000 --- a/query/cloudtrail/cloudtrail_s3_data_events_enabled.sql +++ /dev/null @@ -1,38 +0,0 @@ -with s3_selectors as -( - select - name as trail_name, - is_multi_region_trail, - bucket_selector - from - aws_cloudtrail_trail, - jsonb_array_elements(event_selectors) as event_selector, - jsonb_array_elements(event_selector -> 'DataResources') as data_resource, - jsonb_array_elements_text(data_resource -> 'Values') as bucket_selector - where - is_multi_region_trail - and data_resource ->> 'Type' = 'AWS::S3::Object' - and event_selector ->> 'ReadWriteType' = 'All' -) -select - -- Required columns - b.arn as resource, - case - when count(bucket_selector) > 0 then 'ok' - else 'alarm' - end as status, - case - when count(bucket_selector) > 0 then b.name || ' object-level data events logging enabled.' - else b.name || ' object-level data events logging disabled.' - end as reason, - -- Additional columns - region, - account_id -from - aws_s3_bucket as b - left join - s3_selectors - on bucket_selector like (b.arn || '%') - or bucket_selector = 'arn:aws:s3' -group by - b.account_id, b.region, b.arn, b.name; \ No newline at end of file diff --git a/query/cloudtrail/cloudtrail_s3_logging_enabled.sql b/query/cloudtrail/cloudtrail_s3_logging_enabled.sql deleted file mode 100644 index e9e6fa6b..00000000 --- a/query/cloudtrail/cloudtrail_s3_logging_enabled.sql +++ /dev/null @@ -1,19 +0,0 @@ -select - -- Required columns - t.arn as resource, - case - when b.logging is not null then 'ok' - else 'alarm' - end as status, - case - when b.logging is not null then t.title || '''s logging bucket ' || t.s3_bucket_name || ' has access logging enabled.' - else t.title || '''s logging bucket ' || t.s3_bucket_name || ' has access logging disabled.' - end as reason, - -- Additional columns - t.region, - t.account_id -from - aws_cloudtrail_trail t - inner join aws_s3_bucket b on t.s3_bucket_name = b.name -where - t.region = t.home_region; \ No newline at end of file diff --git a/query/cloudtrail/cloudtrail_s3_object_read_events_audit_enabled.sql b/query/cloudtrail/cloudtrail_s3_object_read_events_audit_enabled.sql deleted file mode 100644 index 66e1b43e..00000000 --- a/query/cloudtrail/cloudtrail_s3_object_read_events_audit_enabled.sql +++ /dev/null @@ -1,42 +0,0 @@ -with s3_selectors as -( - select - name as trail_name, - is_multi_region_trail, - bucket_selector - from - aws_cloudtrail_trail, - jsonb_array_elements(event_selectors) as event_selector, - jsonb_array_elements(event_selector -> 'DataResources') as data_resource, - jsonb_array_elements_text(data_resource -> 'Values') as bucket_selector - where - is_multi_region_trail - and data_resource ->> 'Type' = 'AWS::S3::Object' - and event_selector ->> 'ReadWriteType' in - ( - 'ReadOnly', - 'All' - ) -) -select - -- Required columns - b.arn as resource, - case - when count(bucket_selector) > 0 then 'ok' - else 'alarm' - end as status, - case - when count(bucket_selector) > 0 then b.name || ' object-level read events logging enabled.' - else b.name || ' object-level read events logging disabled.' - end as reason, - -- Additional columns - region, - account_id -from - aws_s3_bucket as b - left join - s3_selectors - on bucket_selector like (b.arn || '%') - or bucket_selector = 'arn:aws:s3' -group by - b.account_id, b.region, b.arn, b.name; \ No newline at end of file diff --git a/query/cloudtrail/cloudtrail_s3_object_write_events_audit_enabled.sql b/query/cloudtrail/cloudtrail_s3_object_write_events_audit_enabled.sql deleted file mode 100644 index 7ec09eb4..00000000 --- a/query/cloudtrail/cloudtrail_s3_object_write_events_audit_enabled.sql +++ /dev/null @@ -1,42 +0,0 @@ -with s3_selectors as -( - select - name as trail_name, - is_multi_region_trail, - bucket_selector - from - aws_cloudtrail_trail, - jsonb_array_elements(event_selectors) as event_selector, - jsonb_array_elements(event_selector -> 'DataResources') as data_resource, - jsonb_array_elements_text(data_resource -> 'Values') as bucket_selector - where - is_multi_region_trail - and data_resource ->> 'Type' = 'AWS::S3::Object' - and event_selector ->> 'ReadWriteType' in - ( - 'WriteOnly', - 'All' - ) -) -select - -- Required columns - b.arn as resource, - case - when count(bucket_selector) > 0 then 'ok' - else 'alarm' - end as status, - case - when count(bucket_selector) > 0 then b.name || ' object-level write events logging enabled.' - else b.name || ' object-level write events logging disabled.' - end as reason, - -- Additional columns - region, - account_id -from - aws_s3_bucket as b - left join - s3_selectors - on bucket_selector like (b.arn || '%') - or bucket_selector = 'arn:aws:s3' -group by - b.account_id, b.region, b.arn, b.name; \ No newline at end of file diff --git a/query/cloudtrail/cloudtrail_security_trail_enabled.sql b/query/cloudtrail/cloudtrail_security_trail_enabled.sql deleted file mode 100644 index 0e3d0beb..00000000 --- a/query/cloudtrail/cloudtrail_security_trail_enabled.sql +++ /dev/null @@ -1,70 +0,0 @@ -with trails_enabled as ( - select - distinct arn, - is_logging, - event_selectors, - coalesce( - jsonb_agg(g) filter ( where not (g = 'null') ), - $$[]$$::jsonb - ) as excludeManagementEventSources - from - aws_cloudtrail_trail - left join jsonb_array_elements(event_selectors) as e on true - left join jsonb_array_elements_text(e -> 'ExcludeManagementEventSources') as g on true - where - home_region = region - group by arn, is_logging, event_selectors -), -all_trails as ( - select - a.arn as arn, - case - when a.is_logging is null then b.is_logging - else a.is_logging - end as is_logging, - case - when a.event_selectors is null then b.event_selectors - else a.event_selectors - end as event_selectors, - b.excludeManagementEventSources, - a.include_global_service_events, - a.is_multi_region_trail, - a.log_file_validation_enabled, - a.kms_key_id, - a.region, - a.account_id, - a.title - from - aws_cloudtrail_trail as a - left join trails_enabled as b on a.arn = b.arn -) -select - -- Required Columns - arn as resource, - case - when not is_logging then 'alarm' - when not include_global_service_events then 'alarm' - when not is_multi_region_trail then 'alarm' - when not log_file_validation_enabled then 'alarm' - when kms_key_id is null then 'alarm' - when not (jsonb_array_length(event_selectors) = 1 and event_selectors @> '[{"ReadWriteType":"All"}]') then 'alarm' - when not (event_selectors @> '[{"IncludeManagementEvents":true}]') then 'alarm' - when jsonb_array_length(excludeManagementEventSources) > 0 then 'alarm' - else 'ok' - end as status, - case - when not is_logging then title || ' disabled.' - when not include_global_service_events then title || ' not recording global service events.' - when not is_multi_region_trail then title || ' not a muti-region trail.' - when not log_file_validation_enabled then title || ' log file validation disabled.' - when kms_key_id is null then title || ' not encrypted with a KMS key.' - when not (jsonb_array_length(event_selectors) = 1 and event_selectors @> '[{"ReadWriteType":"All"}]') then title || ' not recording events for both reads and writes.' - when not (event_selectors @> '[{"IncludeManagementEvents":true}]') then title || ' not recording management events.' - when jsonb_array_length(excludeManagementEventSources) > 0 then title || ' excludes management events for ' || trim(excludeManagementEventSources::text, '[]') || '.' - else title || ' meets all security best practices.' - end as reason, - -- Additional Dimensions - region, - account_id -from - all_trails; diff --git a/query/cloudtrail/cloudtrail_trail_enabled.sql b/query/cloudtrail/cloudtrail_trail_enabled.sql deleted file mode 100644 index c8b0616a..00000000 --- a/query/cloudtrail/cloudtrail_trail_enabled.sql +++ /dev/null @@ -1,28 +0,0 @@ -with trails_enabled as ( - select - arn, - is_logging - from - aws_cloudtrail_trail - where - home_region = region -) -select - -- Required Columns - a.arn as resource, - case - when b.is_logging is null and a.is_logging then 'ok' - when b.is_logging then 'ok' - else 'alarm' - end as status, - case - when b.is_logging is null and a.is_logging then a.title || ' enabled.' - when b.is_logging then a.title || ' enabled.' - else a.title || ' disabled.' - end as reason, - -- Additional Dimensions - a.region, - a.account_id -from - aws_cloudtrail_trail as a -left join trails_enabled b on a.arn = b.arn; \ No newline at end of file diff --git a/query/cloudtrail/cloudtrail_trail_integrated_with_logs.sql b/query/cloudtrail/cloudtrail_trail_integrated_with_logs.sql deleted file mode 100644 index cc32ed16..00000000 --- a/query/cloudtrail/cloudtrail_trail_integrated_with_logs.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when log_group_arn != 'null' and ((latest_delivery_time) > current_date - 1) then 'ok' - else 'alarm' - end as status, - case - when log_group_arn != 'null' and ((latest_delivery_time) > current_date - 1) then title || ' integrated with CloudWatch logs.' - else title || ' not integrated with CloudWatch logs.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudtrail_trail -where - region = home_region; \ No newline at end of file diff --git a/query/cloudtrail/cloudtrail_trail_logs_encrypted_with_kms_cmk.sql b/query/cloudtrail/cloudtrail_trail_logs_encrypted_with_kms_cmk.sql deleted file mode 100644 index f372c334..00000000 --- a/query/cloudtrail/cloudtrail_trail_logs_encrypted_with_kms_cmk.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required columns - arn as resource, - case - when kms_key_id is null then 'alarm' - else 'ok' - end as status, - case - when kms_key_id is null then title || ' logs are not encrypted at rest.' - else title || ' logs are encrypted at rest.' - end as reason, - -- Additional columns - region, - account_id -from - aws_cloudtrail_trail -where - region = home_region; \ No newline at end of file diff --git a/query/cloudtrail/cloudtrail_trail_validation_enabled.sql b/query/cloudtrail/cloudtrail_trail_validation_enabled.sql deleted file mode 100644 index b2a8deeb..00000000 --- a/query/cloudtrail/cloudtrail_trail_validation_enabled.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when log_file_validation_enabled then 'ok' - else 'alarm' - end as status, - case - when log_file_validation_enabled then title || ' log file validation enabled.' - else title || ' log file validation disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudtrail_trail -where - region = home_region; \ No newline at end of file diff --git a/query/cloudwatch/cloudwatch_alarm_action_enabled.sql b/query/cloudwatch/cloudwatch_alarm_action_enabled.sql deleted file mode 100644 index 8feafc73..00000000 --- a/query/cloudwatch/cloudwatch_alarm_action_enabled.sql +++ /dev/null @@ -1,23 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when jsonb_array_length(alarm_actions) = 0 - and jsonb_array_length(insufficient_data_actions) = 0 - and jsonb_array_length(ok_actions) = 0 then 'alarm' - else 'ok' - end as status, - case - when jsonb_array_length(alarm_actions) = 0 - and jsonb_array_length(insufficient_data_actions) = 0 - and jsonb_array_length(ok_actions) = 0 then title || ' no action enabled.' - when jsonb_array_length(alarm_actions) != 0 then title || ' alarm action enabled.' - when jsonb_array_length(insufficient_data_actions) != 0 then title || ' insufficient data action enabled.' - when jsonb_array_length(ok_actions) != 0 then title || ' ok action enabled.' - else 'ok' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudwatch_alarm; diff --git a/query/cloudwatch/cloudwatch_cross_account_sharing.sql b/query/cloudwatch/cloudwatch_cross_account_sharing.sql deleted file mode 100644 index 76c1120e..00000000 --- a/query/cloudwatch/cloudwatch_cross_account_sharing.sql +++ /dev/null @@ -1,27 +0,0 @@ -with iam_role_cross_account_sharing_count as ( - select - arn, - replace(replace(replace((a -> 'Principal' ->> 'AWS'), '[',''), ']', ''), '"', '') as cross_account_details, - account_id - from - aws_iam_role, - jsonb_array_elements(assume_role_policy_std -> 'Statement') as a - where - name = 'CloudWatch-CrossAccountSharingRole' -) -select - -- Required Columns - a.arn as resource, - case - when c.arn is null then 'ok' - else 'info' - end as status, - case - when c.arn is null then 'CloudWatch does not allow cross-account sharing.' - else 'CloudWatch allow cross-account sharing with '|| cross_account_details || '.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join iam_role_cross_account_sharing_count as c on c.account_id = a.account_id; \ No newline at end of file diff --git a/query/cloudwatch/cloudwatch_log_group_retention_period_365.sql b/query/cloudwatch/cloudwatch_log_group_retention_period_365.sql deleted file mode 100644 index d31e534d..00000000 --- a/query/cloudwatch/cloudwatch_log_group_retention_period_365.sql +++ /dev/null @@ -1,17 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when retention_in_days is null or retention_in_days < 365 then 'alarm' - else 'ok' - end as status, - case - when retention_in_days is null then title || ' retention period not set.' - when retention_in_days < 365 then title || ' retention period less than 365 days.' - else title || ' retention period 365 days or above.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudwatch_log_group; \ No newline at end of file diff --git a/query/cloudwatch/log_group_encryption_at_rest_enabled.sql b/query/cloudwatch/log_group_encryption_at_rest_enabled.sql deleted file mode 100644 index 08a62741..00000000 --- a/query/cloudwatch/log_group_encryption_at_rest_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when kms_key_id is null then 'alarm' - else 'ok' - end as status, - case - when kms_key_id is null then title || ' not encrypted at rest.' - else title || ' encrypted at rest.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_cloudwatch_log_group; \ No newline at end of file diff --git a/query/cloudwatch/log_metric_filter_bucket_policy.sql b/query/cloudwatch/log_metric_filter_bucket_policy.sql deleted file mode 100644 index 712c5fe6..00000000 --- a/query/cloudwatch/log_metric_filter_bucket_policy.sql +++ /dev/null @@ -1,45 +0,0 @@ -with filter_data as ( - select - trail.account_id, - trail.name as trail_name, - trail.is_logging as is_logging, - split_part(trail.log_group_arn, ':', 7) as log_group_name, - filter.name as filter_name, - action_arn as topic_arn, - alarm.metric_name, - subscription.subscription_arn, - filter.filter_pattern, - filter.metric_transformation_name - from - aws_cloudtrail_trail as trail, - jsonb_array_elements(trail.event_selectors) as se, - aws_cloudwatch_log_metric_filter as filter, - aws_cloudwatch_alarm as alarm, - jsonb_array_elements_text(alarm.alarm_actions) as action_arn, - aws_sns_topic_subscription as subscription - where - trail.is_multi_region_trail is true - and trail.is_logging - and se ->> 'ReadWriteType' = 'All' - and trail.log_group_arn is not null - and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) - and filter.filter_pattern ~ '\s*\$\.eventSource\s*=\s*s3.amazonaws.com.+\$\.eventName\s*=\s*PutBucketAcl.+\$\.eventName\s*=\s*PutBucketPolicy.+\$\.eventName\s*=\s*PutBucketCors.+\$\.eventName\s*=\s*PutBucketLifecycle.+\$\.eventName\s*=\s*PutBucketReplication.+\$\.eventName\s*=\s*DeleteBucketPolicy.+\$\.eventName\s*=\s*DeleteBucketCors.+\$\.eventName\s*=\s*DeleteBucketLifecycle.+\$\.eventName\s*=\s*DeleteBucketReplication' - and alarm.metric_name = filter.metric_transformation_name - and subscription.topic_arn = action_arn -) -select - -- Required Columns - distinct 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when f.trail_name is null then 'alarm' - else 'ok' - end as status, - case - when f.trail_name is null then 'No log metric filter and alarm exist for S3 bucket policy changes.' - else filter_name || ' forwards events for S3 bucket policy changes.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join filter_data as f on a.account_id = f.account_id diff --git a/query/cloudwatch/log_metric_filter_cloudtrail_configuration.sql b/query/cloudwatch/log_metric_filter_cloudtrail_configuration.sql deleted file mode 100644 index 6959450b..00000000 --- a/query/cloudwatch/log_metric_filter_cloudtrail_configuration.sql +++ /dev/null @@ -1,44 +0,0 @@ -with filter_data as ( - select - trail.account_id, - trail.name as trail_name, - trail.is_logging, - split_part(trail.log_group_arn, ':', 7) as log_group_name, - filter.name as filter_name, - action_arn as topic_arn, - alarm.metric_name, - subscription.subscription_arn, - filter.filter_pattern - from - aws_cloudtrail_trail as trail, - jsonb_array_elements(trail.event_selectors) as se, - aws_cloudwatch_log_metric_filter as filter, - aws_cloudwatch_alarm as alarm, - jsonb_array_elements_text(alarm.alarm_actions) as action_arn, - aws_sns_topic_subscription as subscription - where - trail.is_multi_region_trail is true - and trail.is_logging - and se ->> 'ReadWriteType' = 'All' - and trail.log_group_arn is not null - and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) - and filter.filter_pattern ~ '\s*\$\.eventName\s*=\s*CreateTrail.+\$\.eventName\s*=\s*UpdateTrail.+\$\.eventName\s*=\s*DeleteTrail.+\$\.eventName\s*=\s*StartLogging.+\$\.eventName\s*=\s*StopLogging' - and alarm.metric_name = filter.metric_transformation_name - and subscription.topic_arn = action_arn -) -select - -- Required Columns - distinct 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when f.trail_name is null then 'alarm' - else 'ok' - end as status, - case - when f.trail_name is null then 'No log metric filter and alarm exist for CloudTrail configuration changes.' - else filter_name || ' forwards events for CloudTrail configuration changes.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join filter_data as f on a.account_id = f.account_id diff --git a/query/cloudwatch/log_metric_filter_config_configuration.sql b/query/cloudwatch/log_metric_filter_config_configuration.sql deleted file mode 100644 index 51f10f5b..00000000 --- a/query/cloudwatch/log_metric_filter_config_configuration.sql +++ /dev/null @@ -1,44 +0,0 @@ -with filter_data as ( - select - trail.account_id, - trail.name as trail_name, - trail.is_logging, - split_part(trail.log_group_arn, ':', 7) as log_group_name, - filter.name as filter_name, - action_arn as topic_arn, - alarm.metric_name, - subscription.subscription_arn, - filter.filter_pattern - from - aws_cloudtrail_trail as trail, - jsonb_array_elements(trail.event_selectors) as se, - aws_cloudwatch_log_metric_filter as filter, - aws_cloudwatch_alarm as alarm, - jsonb_array_elements_text(alarm.alarm_actions) as action_arn, - aws_sns_topic_subscription as subscription - where - trail.is_multi_region_trail is true - and trail.is_logging - and se ->> 'ReadWriteType' = 'All' - and trail.log_group_arn is not null - and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) - and filter.filter_pattern ~ '\s*\$\.eventSource\s*=\s*config.amazonaws.com.+\$\.eventName\s*=\s*StopConfigurationRecorder.+\$\.eventName\s*=\s*DeleteDeliveryChannel.+\$\.eventName\s*=\s*PutDeliveryChannel.+\$\.eventName\s*=\s*PutConfigurationRecorder' - and alarm.metric_name = filter.metric_transformation_name - and subscription.topic_arn = action_arn -) -select - -- Required Columns - distinct 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when f.trail_name is null then 'alarm' - else 'ok' - end as status, - case - when f.trail_name is null then 'No log metric filter and alarm exist for AWS Config configuration changes.' - else filter_name || ' forwards events for AWS Config configuration changes.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join filter_data as f on a.account_id = f.account_id diff --git a/query/cloudwatch/log_metric_filter_console_authentication_failure.sql b/query/cloudwatch/log_metric_filter_console_authentication_failure.sql deleted file mode 100644 index a1a05571..00000000 --- a/query/cloudwatch/log_metric_filter_console_authentication_failure.sql +++ /dev/null @@ -1,44 +0,0 @@ -with filter_data as ( - select - trail.account_id, - trail.name as trail_name, - trail.is_logging, - split_part(trail.log_group_arn, ':', 7) as log_group_name, - filter.name as filter_name, - action_arn as topic_arn, - alarm.metric_name, - subscription.subscription_arn, - filter.filter_pattern - from - aws_cloudtrail_trail as trail, - jsonb_array_elements(trail.event_selectors) as se, - aws_cloudwatch_log_metric_filter as filter, - aws_cloudwatch_alarm as alarm, - jsonb_array_elements_text(alarm.alarm_actions) as action_arn, - aws_sns_topic_subscription as subscription - where - trail.is_multi_region_trail is true - and trail.is_logging - and se ->> 'ReadWriteType' = 'All' - and trail.log_group_arn is not null - and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) - and filter.filter_pattern ~ '\s*\$\.eventName\s*=\s*ConsoleLogin.+\$\.errorMessage\s*=\s*"Failed authentication"' - and alarm.metric_name = filter.metric_transformation_name - and subscription.topic_arn = action_arn -) -select - -- Required Columns - distinct 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when f.trail_name is null then 'alarm' - else 'ok' - end as status, - case - when f.trail_name is null then 'No log metric filter and alarm exist for console authentication failures.' - else filter_name || ' forwards events for console authentication failures.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join filter_data as f on a.account_id = f.account_id diff --git a/query/cloudwatch/log_metric_filter_console_login_mfa.sql b/query/cloudwatch/log_metric_filter_console_login_mfa.sql deleted file mode 100644 index f07bd983..00000000 --- a/query/cloudwatch/log_metric_filter_console_login_mfa.sql +++ /dev/null @@ -1,44 +0,0 @@ -with filter_data as ( - select - trail.account_id, - trail.name as trail_name, - trail.is_logging, - split_part(trail.log_group_arn, ':', 7) as log_group_name, - filter.name as filter_name, - action_arn as topic_arn, - alarm.metric_name, - subscription.subscription_arn, - filter.filter_pattern - from - aws_cloudtrail_trail as trail, - jsonb_array_elements(trail.event_selectors) as se, - aws_cloudwatch_log_metric_filter as filter, - aws_cloudwatch_alarm as alarm, - jsonb_array_elements_text(alarm.alarm_actions) as action_arn, - aws_sns_topic_subscription as subscription - where - trail.is_multi_region_trail is true - and trail.is_logging - and se ->> 'ReadWriteType' = 'All' - and trail.log_group_arn is not null - and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) - and filter.filter_pattern ~ '\(\s*\$\.eventName\s*=\s*"ConsoleLogin"\)\s+&&\s+\(\s*\$.additionalEventData\.MFAUsed\s*!=\s*"Yes"' - and alarm.metric_name = filter.metric_transformation_name - and subscription.topic_arn = action_arn -) -select - -- Required Columns - distinct 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when f.trail_name is null then 'alarm' - else 'ok' - end as status, - case - when f.trail_name is null then 'No log metric filter and alarm exist for console sign-in without MFA.' - else filter_name || ' forwards events for console sign-in without MFA.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join filter_data as f on a.account_id = f.account_id diff --git a/query/cloudwatch/log_metric_filter_disable_or_delete_cmk.sql b/query/cloudwatch/log_metric_filter_disable_or_delete_cmk.sql deleted file mode 100644 index 2a02f74a..00000000 --- a/query/cloudwatch/log_metric_filter_disable_or_delete_cmk.sql +++ /dev/null @@ -1,44 +0,0 @@ -with filter_data as ( - select - trail.account_id, - trail.name as trail_name, - trail.is_logging, - split_part(trail.log_group_arn, ':', 7) as log_group_name, - filter.name as filter_name, - action_arn as topic_arn, - alarm.metric_name, - subscription.subscription_arn, - filter.filter_pattern - from - aws_cloudtrail_trail as trail, - jsonb_array_elements(trail.event_selectors) as se, - aws_cloudwatch_log_metric_filter as filter, - aws_cloudwatch_alarm as alarm, - jsonb_array_elements_text(alarm.alarm_actions) as action_arn, - aws_sns_topic_subscription as subscription - where - trail.is_multi_region_trail is true - and trail.is_logging - and se ->> 'ReadWriteType' = 'All' - and trail.log_group_arn is not null - and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) - and filter.filter_pattern ~ '\s*\$\.eventSource\s*=\s*kms.amazonaws.com.+\$\.eventName\s*=\s*DisableKey.+\$\.eventName\s*=\s*ScheduleKeyDeletion' - and alarm.metric_name = filter.metric_transformation_name - and subscription.topic_arn = action_arn -) -select - -- Required Columns - distinct 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when f.trail_name is null then 'alarm' - else 'ok' - end as status, - case - when f.trail_name is null then 'No log metric filter and alarm exist for disabling/deletion of CMKs.' - else filter_name || ' forwards events for disabling/deletion of CMKs.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join filter_data as f on a.account_id = f.account_id diff --git a/query/cloudwatch/log_metric_filter_iam_policy.sql b/query/cloudwatch/log_metric_filter_iam_policy.sql deleted file mode 100644 index fd30b177..00000000 --- a/query/cloudwatch/log_metric_filter_iam_policy.sql +++ /dev/null @@ -1,45 +0,0 @@ -with filter_data as ( - select - trail.account_id, - trail.name as trail_name, - trail.is_logging as is_logging, - split_part(trail.log_group_arn, ':', 7) as log_group_name, - filter.name as filter_name, - action_arn as topic_arn, - alarm.metric_name, - subscription.subscription_arn, - filter.filter_pattern, - filter.metric_transformation_name - from - aws_cloudtrail_trail as trail, - jsonb_array_elements(trail.event_selectors) as se, - aws_cloudwatch_log_metric_filter as filter, - aws_cloudwatch_alarm as alarm, - jsonb_array_elements_text(alarm.alarm_actions) as action_arn, - aws_sns_topic_subscription as subscription - where - trail.is_multi_region_trail is true - and trail.is_logging - and se ->> 'ReadWriteType' = 'All' - and trail.log_group_arn is not null - and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) - and filter.filter_pattern ~ '\s*\$\.eventName\s*=\s*DeleteGroupPolicy.+\$\.eventName\s*=\s*DeleteRolePolicy.+\$\.eventName\s*=\s*DeleteUserPolicy.+\$\.eventName\s*=\s*PutGroupPolicy.+\$\.eventName\s*=\s*PutRolePolicy.+\$\.eventName\s*=\s*PutUserPolicy.+\$\.eventName\s*=\s*CreatePolicy.+\$\.eventName\s*=\s*DeletePolicy.+\$\.eventName\s*=\s*CreatePolicyVersion.+\$\.eventName\s*=\s*DeletePolicyVersion.+\$\.eventName\s*=\s*AttachRolePolicy.+\$\.eventName\s*=\s*DetachRolePolicy.+\$\.eventName\s*=\s*AttachUserPolicy.+\$\.eventName\s*=\s*DetachUserPolicy.+\$\.eventName\s*=\s*AttachGroupPolicy.+\$\.eventName\s*=\s*DetachGroupPolicy' - and alarm.metric_name = filter.metric_transformation_name - and subscription.topic_arn = action_arn -) -select - -- Required Columns - distinct 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when f.trail_name is null then 'alarm' - else 'ok' - end as status, - case - when f.trail_name is null then 'No log metric filter and alarm exist for IAM policy changes.' - else filter_name || ' forwards events for IAM policy changes.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join filter_data as f on a.account_id = f.account_id diff --git a/query/cloudwatch/log_metric_filter_network_acl.sql b/query/cloudwatch/log_metric_filter_network_acl.sql deleted file mode 100644 index 129a6f32..00000000 --- a/query/cloudwatch/log_metric_filter_network_acl.sql +++ /dev/null @@ -1,44 +0,0 @@ -with filter_data as ( - select - trail.account_id, - trail.name as trail_name, - trail.is_logging, - split_part(trail.log_group_arn, ':', 7) as log_group_name, - filter.name as filter_name, - action_arn as topic_arn, - alarm.metric_name, - subscription.subscription_arn, - filter.filter_pattern - from - aws_cloudtrail_trail as trail, - jsonb_array_elements(trail.event_selectors) as se, - aws_cloudwatch_log_metric_filter as filter, - aws_cloudwatch_alarm as alarm, - jsonb_array_elements_text(alarm.alarm_actions) as action_arn, - aws_sns_topic_subscription as subscription - where - trail.is_multi_region_trail is true - and trail.is_logging - and se ->> 'ReadWriteType' = 'All' - and trail.log_group_arn is not null - and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) - and filter.filter_pattern ~ '\s*\$\.eventName\s*=\s*CreateNetworkAcl.+\$\.eventName\s*=\s*CreateNetworkAclEntry.+\$\.eventName\s*=\s*DeleteNetworkAcl.+\$\.eventName\s*=\s*DeleteNetworkAclEntry.+\$\.eventName\s*=\s*ReplaceNetworkAclEntry.+\$\.eventName\s*=\s*ReplaceNetworkAclAssociation' - and alarm.metric_name = filter.metric_transformation_name - and subscription.topic_arn = action_arn -) -select - -- Required Columns - distinct 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when f.trail_name is null then 'alarm' - else 'ok' - end as status, - case - when f.trail_name is null then 'No log metric filter and alarm exist for changes to NACLs.' - else filter_name || ' forwards events for changes to NACLs.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join filter_data as f on a.account_id = f.account_id diff --git a/query/cloudwatch/log_metric_filter_network_gateway.sql b/query/cloudwatch/log_metric_filter_network_gateway.sql deleted file mode 100644 index 7e9819f6..00000000 --- a/query/cloudwatch/log_metric_filter_network_gateway.sql +++ /dev/null @@ -1,45 +0,0 @@ -with filter_data as ( - select - trail.account_id, - trail.name as trail_name, - trail.is_logging, - split_part(trail.log_group_arn, ':', 7) as log_group_name, - filter.name as filter_name, - action_arn as topic_arn, - alarm.metric_name, - alarm.name as alarm_name, - subscription.subscription_arn, - filter.filter_pattern - from - aws_cloudtrail_trail as trail, - jsonb_array_elements(trail.event_selectors) as se, - aws_cloudwatch_log_metric_filter as filter, - aws_cloudwatch_alarm as alarm, - jsonb_array_elements_text(alarm.alarm_actions) as action_arn, - aws_sns_topic_subscription as subscription - where - trail.is_multi_region_trail is true - and trail.is_logging - and se ->> 'ReadWriteType' = 'All' - and trail.log_group_arn is not null - and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) - and filter.filter_pattern ~ '\s*\$\.eventName\s*=\s*CreateCustomerGateway.+\$\.eventName\s*=\s*DeleteCustomerGateway.+\$\.eventName\s*=\s*AttachInternetGateway.+\$\.eventName\s*=\s*CreateInternetGateway.+\$\.eventName\s*=\s*DeleteInternetGateway.+\$\.eventName\s*=\s*DetachInternetGateway' - and alarm.metric_name = filter.metric_transformation_name - and subscription.topic_arn = action_arn -) -select - -- Required Columns - distinct 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when f.trail_name is null then 'alarm' - else 'ok' - end as status, - case - when f.trail_name is null then 'No log metric filter and alarm exist for changes to network gateways.' - else filter_name || ' forwards events for changes to network gateways.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join filter_data as f on a.account_id = f.account_id diff --git a/query/cloudwatch/log_metric_filter_organization.sql b/query/cloudwatch/log_metric_filter_organization.sql deleted file mode 100644 index fb132a20..00000000 --- a/query/cloudwatch/log_metric_filter_organization.sql +++ /dev/null @@ -1,45 +0,0 @@ -with filter_data as ( - select - trail.account_id, - trail.name as trail_name, - trail.is_logging, - split_part(trail.log_group_arn, ':', 7) as log_group_name, - filter.name as filter_name, - action_arn as topic_arn, - alarm.metric_name, - alarm.name as alarm_name, - subscription.subscription_arn, - filter.filter_pattern - from - aws_cloudtrail_trail as trail, - jsonb_array_elements(trail.event_selectors) as se, - aws_cloudwatch_log_metric_filter as filter, - aws_cloudwatch_alarm as alarm, - jsonb_array_elements_text(alarm.alarm_actions) as action_arn, - aws_sns_topic_subscription as subscription - where - trail.is_multi_region_trail is true - and trail.is_logging - and se ->> 'ReadWriteType' = 'All' - and trail.log_group_arn is not null - and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) - and filter.filter_pattern ~ '\s*\$\.eventSource\s*=\s*organizations.amazonaws.com.+\$\.eventName\s*=\s*"AcceptHandshake".+\$\.eventName\s*=\s*"AttachPolicy".+\$\.eventName\s*=\s*"CreateAccount".+\$\.eventName\s*=\s*"CreateOrganizationalUnit".+\$\.eventName\s*=\s*"CreatePolicy".+\$\.eventName\s*=\s*"DeclineHandshake".+\$\.eventName\s*=\s*"DeleteOrganization".+\$\.eventName\s*=\s*"DeleteOrganizationalUnit".+\$\.eventName\s*=\s*"DeletePolicy".+\$\.eventName\s*=\s*"DetachPolicy".+\$\.eventName\s*=\s*"DisablePolicyType".+\$\.eventName\s*=\s*"EnablePolicyType".+\$\.eventName\s*=\s*"InviteAccountToOrganization".+\$\.eventName\s*=\s*"LeaveOrganization".+\$\.eventName\s*=\s*"MoveAccount".+\$\.eventName\s*=\s*"RemoveAccountFromOrganization".+\$\.eventName\s*=\s*"UpdatePolicy".+\$\.eventName\s*=\s*"UpdateOrganizationalUnit"' - and alarm.metric_name = filter.metric_transformation_name - and subscription.topic_arn = action_arn -) -select - -- Required Columns - distinct 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when f.trail_name is null then 'alarm' - else 'ok' - end as status, - case - when f.trail_name is null then 'No log metric filter and alarm exists for AWS Organizations changes.' - else filter_name || ' forwards relevant events for AWS Organizations changes.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join filter_data as f on a.account_id = f.account_id; \ No newline at end of file diff --git a/query/cloudwatch/log_metric_filter_root_login.sql b/query/cloudwatch/log_metric_filter_root_login.sql deleted file mode 100644 index 574c28cb..00000000 --- a/query/cloudwatch/log_metric_filter_root_login.sql +++ /dev/null @@ -1,44 +0,0 @@ -with filter_data as ( - select - trail.account_id, - trail.name as trail_name, - trail.is_logging, - split_part(trail.log_group_arn, ':', 7) as log_group_name, - filter.name as filter_name, - action_arn as topic_arn, - alarm.metric_name, - subscription.subscription_arn, - filter.filter_pattern - from - aws_cloudtrail_trail as trail, - jsonb_array_elements(trail.event_selectors) as se, - aws_cloudwatch_log_metric_filter as filter, - aws_cloudwatch_alarm as alarm, - jsonb_array_elements_text(alarm.alarm_actions) as action_arn, - aws_sns_topic_subscription as subscription - where - trail.is_multi_region_trail is true - and trail.is_logging - and se ->> 'ReadWriteType' = 'All' - and trail.log_group_arn is not null - and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) - and filter.filter_pattern ~ '\s*\$\.userIdentity\.type\s*=\s*"Root".+\$\.userIdentity\.invokedBy NOT EXISTS.+\$\.eventType\s*!=\s*"AwsServiceEvent"' - and alarm.metric_name = filter.metric_transformation_name - and subscription.topic_arn = action_arn -) -select - -- Required Columns - distinct 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when f.trail_name is null then 'alarm' - else 'ok' - end as status, - case - when f.trail_name is null then 'No log metric filter and alarm exist for usage of "root" account.' - else filter_name || ' forwards events for usage of "root" account.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join filter_data as f on a.account_id = f.account_id \ No newline at end of file diff --git a/query/cloudwatch/log_metric_filter_route_table.sql b/query/cloudwatch/log_metric_filter_route_table.sql deleted file mode 100644 index 622710de..00000000 --- a/query/cloudwatch/log_metric_filter_route_table.sql +++ /dev/null @@ -1,45 +0,0 @@ -with filter_data as ( - select - trail.account_id, - trail.name as trail_name, - trail.is_logging, - split_part(trail.log_group_arn, ':', 7) as log_group_name, - filter.name as filter_name, - action_arn as topic_arn, - alarm.metric_name, - alarm.name as alarm_name, - subscription.subscription_arn, - filter.filter_pattern - from - aws_cloudtrail_trail as trail, - jsonb_array_elements(trail.event_selectors) as se, - aws_cloudwatch_log_metric_filter as filter, - aws_cloudwatch_alarm as alarm, - jsonb_array_elements_text(alarm.alarm_actions) as action_arn, - aws_sns_topic_subscription as subscription - where - trail.is_multi_region_trail is true - and trail.is_logging - and se ->> 'ReadWriteType' = 'All' - and trail.log_group_arn is not null - and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) - and filter.filter_pattern ~ '\s*\$\.eventName\s*=\s*CreateRoute.+\$\.eventName\s*=\s*CreateRouteTable.+\$\.eventName\s*=\s*ReplaceRoute.+\$\.eventName\s*=\s*ReplaceRouteTableAssociation.+\$\.eventName\s*=\s*DeleteRouteTable.+\$\.eventName\s*=\s*DeleteRoute.+\$\.eventName\s*=\s*DisassociateRouteTable' - and alarm.metric_name = filter.metric_transformation_name - and subscription.topic_arn = action_arn -) -select - -- Required Columns - distinct 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when f.trail_name is null then 'alarm' - else 'ok' - end as status, - case - when f.trail_name is null then 'No log metric filter and alarm exist for route table changes.' - else filter_name || ' forwards events for route table changes.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join filter_data as f on a.account_id = f.account_id diff --git a/query/cloudwatch/log_metric_filter_security_group.sql b/query/cloudwatch/log_metric_filter_security_group.sql deleted file mode 100644 index 8e40bf8a..00000000 --- a/query/cloudwatch/log_metric_filter_security_group.sql +++ /dev/null @@ -1,44 +0,0 @@ -with filter_data as ( - select - trail.account_id, - trail.name as trail_name, - trail.is_logging, - split_part(trail.log_group_arn, ':', 7) as log_group_name, - filter.name as filter_name, - action_arn as topic_arn, - alarm.metric_name, - subscription.subscription_arn, - filter.filter_pattern - from - aws_cloudtrail_trail as trail, - jsonb_array_elements(trail.event_selectors) as se, - aws_cloudwatch_log_metric_filter as filter, - aws_cloudwatch_alarm as alarm, - jsonb_array_elements_text(alarm.alarm_actions) as action_arn, - aws_sns_topic_subscription as subscription - where - trail.is_multi_region_trail is true - and trail.is_logging - and se ->> 'ReadWriteType' = 'All' - and trail.log_group_arn is not null - and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) - and filter.filter_pattern ~ '\s*\$\.eventName\s*=\s*AuthorizeSecurityGroupIngress.+\$\.eventName\s*=\s*AuthorizeSecurityGroupEgress.+\$\.eventName\s*=\s*RevokeSecurityGroupIngress.+\$\.eventName\s*=\s*RevokeSecurityGroupEgress.+\$\.eventName\s*=\s*CreateSecurityGroup.+\$\.eventName\s*=\s*DeleteSecurityGroup' - and alarm.metric_name = filter.metric_transformation_name - and subscription.topic_arn = action_arn -) -select - -- Required Columns - distinct 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when f.trail_name is null then 'alarm' - else 'ok' - end as status, - case - when f.trail_name is null then 'No log metric filter and alarm exist for security group changes.' - else filter_name || ' forwards events for security group changes.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join filter_data as f on a.account_id = f.account_id diff --git a/query/cloudwatch/log_metric_filter_unauthorized_api.sql b/query/cloudwatch/log_metric_filter_unauthorized_api.sql deleted file mode 100644 index 6fb2cd9c..00000000 --- a/query/cloudwatch/log_metric_filter_unauthorized_api.sql +++ /dev/null @@ -1,46 +0,0 @@ -with filter_data as ( - select - trail.account_id, - trail.name as trail_name, - trail.is_logging, - split_part(trail.log_group_arn, ':', 7) as log_group_name, - filter.name as filter_name, - action_arn as topic_arn, - alarm.metric_name, - subscription.subscription_arn, - filter.filter_pattern - from - aws_cloudtrail_trail as trail, - jsonb_array_elements(trail.event_selectors) as se, - aws_cloudwatch_log_metric_filter as filter, - aws_cloudwatch_alarm as alarm, - jsonb_array_elements_text(alarm.alarm_actions) as action_arn, - aws_sns_topic_subscription as subscription - where - trail.is_multi_region_trail is true - and trail.is_logging - and se ->> 'ReadWriteType' = 'All' - and trail.log_group_arn is not null - and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) - -- As per cis recommended exact pattern order - -- {($.errorCode = "*UnauthorizedOperation") || ($.errorCode = "AccessDenied*") || ($.sourceIPAddress!="delivery.logs.amazonaws.com") || ($.eventName!="HeadBucket") } - and filter.filter_pattern ~ '\$\.errorCode\s*=\s*"\*UnauthorizedOperation".+\$\.errorCode\s*=\s*"AccessDenied\*".+\$\.sourceIPAddress\s*!=\s*"delivery.logs.amazonaws.com".+\$\.eventName\s*!=\s*"HeadBucket"' - and alarm.metric_name = filter.metric_transformation_name - and subscription.topic_arn = action_arn -) -select - -- Required Columns - distinct 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when f.trail_name is null then 'alarm' - else 'ok' - end as status, - case - when f.trail_name is null then 'No log metric filter and alarm exist for unauthorized API calls.' - else filter_name || ' forwards events for unauthorized API calls.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join filter_data as f on a.account_id = f.account_id; \ No newline at end of file diff --git a/query/cloudwatch/log_metric_filter_vpc.sql b/query/cloudwatch/log_metric_filter_vpc.sql deleted file mode 100644 index 14faf178..00000000 --- a/query/cloudwatch/log_metric_filter_vpc.sql +++ /dev/null @@ -1,45 +0,0 @@ -with filter_data as ( - select - trail.account_id, - trail.name as trail_name, - trail.is_logging, - split_part(trail.log_group_arn, ':', 7) as log_group_name, - filter.name as filter_name, - action_arn as topic_arn, - alarm.metric_name, - alarm.name as alarm_name, - subscription.subscription_arn, - filter.filter_pattern - from - aws_cloudtrail_trail as trail, - jsonb_array_elements(trail.event_selectors) as se, - aws_cloudwatch_log_metric_filter as filter, - aws_cloudwatch_alarm as alarm, - jsonb_array_elements_text(alarm.alarm_actions) as action_arn, - aws_sns_topic_subscription as subscription - where - trail.is_multi_region_trail is true - and trail.is_logging - and se ->> 'ReadWriteType' = 'All' - and trail.log_group_arn is not null - and filter.log_group_name = split_part(trail.log_group_arn, ':', 7) - and filter.filter_pattern ~ '\s*\$\.eventName\s*=\s*CreateVpc.+\$\.eventName\s*=\s*DeleteVpc.+\$\.eventName\s*=\s*ModifyVpcAttribute.+\$\.eventName\s*=\s*AcceptVpcPeeringConnection.+\$\.eventName\s*=\s*CreateVpcPeeringConnection.+\$\.eventName\s*=\s*DeleteVpcPeeringConnection.+\$\.eventName\s*=\s*RejectVpcPeeringConnection.+\$\.eventName\s*=\s*AttachClassicLinkVpc.+\$\.eventName\s*=\s*DetachClassicLinkVpc.+\$\.eventName\s*=\s*DisableVpcClassicLink.+\$\.eventName\s*=\s*EnableVpcClassicLink' - and alarm.metric_name = filter.metric_transformation_name - and subscription.topic_arn = action_arn -) -select - -- Required Columns - distinct 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when f.trail_name is null then 'alarm' - else 'ok' - end as status, - case - when f.trail_name is null then 'No log metric filter and alarm exist for VPC changes.' - else filter_name || ' forwards events for VPC changes.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join filter_data as f on a.account_id = f.account_id diff --git a/query/codebuild/codebuild_project_artifact_encryption_enabled.sql b/query/codebuild/codebuild_project_artifact_encryption_enabled.sql deleted file mode 100644 index fd3cfc5b..00000000 --- a/query/codebuild/codebuild_project_artifact_encryption_enabled.sql +++ /dev/null @@ -1,28 +0,0 @@ -with secondary_artifact as ( - select - distinct arn - from - aws_codebuild_project, - jsonb_array_elements(secondary_artifacts) as a - where - a -> 'EncryptionDisabled' = 'true' -) -select - -- Required Columns - a.arn as resource, - case - when p.artifacts ->> 'EncryptionDisabled' = 'false' - and (p.secondary_artifacts is null or a.arn is null) then 'ok' - else 'alarm' - end as status, - case - when p.artifacts ->> 'EncryptionDisabled' = 'false' - and (p.secondary_artifacts is null or a.arn is null) then p.title || ' all artifacts encryption enabled.' - else p.title || ' all artifacts encryption not enabled.' - end as reason, - -- Additional Dimensions - p.region, - p.account_id -from - aws_codebuild_project as p - left join secondary_artifact as a on a.arn = p.arn; diff --git a/query/codebuild/codebuild_project_build_greater_then_90_days.sql b/query/codebuild/codebuild_project_build_greater_then_90_days.sql deleted file mode 100644 index ea4223ef..00000000 --- a/query/codebuild/codebuild_project_build_greater_then_90_days.sql +++ /dev/null @@ -1,31 +0,0 @@ -with latest_codebuild_build as ( - select - project_name, - region, - account_id, - min(date_part('day', now() - end_time)) as build_time - from - aws_codebuild_build - group by - project_name, - region, - account_id -) -select - -- Required Columns - p.arn as resource, - case - when b.build_time is null then 'alarm' - when b.build_time < 90 then 'ok' - else 'alarm' - end as status, - case - when b.build_time is null then p.title || ' was never build.' - else p.title || ' was build ' || build_time || ' day(s) before.' - end as reason, - -- Additional Dimensions - p.account_id, - p.region -from - aws_codebuild_project as p - left join latest_codebuild_build as b on p.name = b.project_name and p.region = b.region and p.account_id = b.account_id; \ No newline at end of file diff --git a/query/codebuild/codebuild_project_environment_privileged_mode_disabled.sql b/query/codebuild/codebuild_project_environment_privileged_mode_disabled.sql deleted file mode 100644 index aac5c9a8..00000000 --- a/query/codebuild/codebuild_project_environment_privileged_mode_disabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when environment ->> 'PrivilegedMode' = 'true' then 'alarm' - else 'ok' - end as status, - case - when environment ->> 'PrivilegedMode' = 'true' then title || ' environment privileged mode enabled.' - else title || ' environment privileged mode disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_codebuild_project; \ No newline at end of file diff --git a/query/codebuild/codebuild_project_logging_enabled.sql b/query/codebuild/codebuild_project_logging_enabled.sql deleted file mode 100644 index 24fe11d3..00000000 --- a/query/codebuild/codebuild_project_logging_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when logs_config -> 'CloudWatchLogs' ->> 'Status' = 'ENABLED' or logs_config -> 'S3Logs' ->> 'Status' = 'ENABLED' then 'ok' - else 'alarm' - end as status, - case - when logs_config -> 'CloudWatchLogs' ->> 'Status' = 'ENABLED' or logs_config -> 'S3Logs' ->> 'Status' = 'ENABLED' then title || ' logging enabled.' - else title || ' logging disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_codebuild_project; \ No newline at end of file diff --git a/query/codebuild/codebuild_project_plaintext_env_variables_no_sensitive_aws_values.sql b/query/codebuild/codebuild_project_plaintext_env_variables_no_sensitive_aws_values.sql deleted file mode 100644 index 2a6dda77..00000000 --- a/query/codebuild/codebuild_project_plaintext_env_variables_no_sensitive_aws_values.sql +++ /dev/null @@ -1,28 +0,0 @@ -with invalid_key_name as ( - select - distinct arn, - name - from - aws_codebuild_project, - jsonb_array_elements(environment -> 'EnvironmentVariables') as env - where - env ->> 'Name' ilike any(array['%AWS_ACCESS_KEY_ID%', '%AWS_SECRET_ACCESS_KEY%', '%PASSWORD%']) - and env ->> 'Type' = 'PLAINTEXT' -) -select - -- Required Columns - a.arn as resource, - case - when b.arn is null then 'ok' - else 'alarm' - end as status, - case - when b.arn is null then a.title || ' has no plaintext environment variables with sensitive AWS values.' - else a.title || ' has plaintext environment variables with sensitive AWS values.' - end as reason, - -- Additional Dimensions - a.region, - a.account_id -from - aws_codebuild_project a - left join invalid_key_name b on a.arn = b.arn; diff --git a/query/codebuild/codebuild_project_s3_logs_encryption_enabled.sql b/query/codebuild/codebuild_project_s3_logs_encryption_enabled.sql deleted file mode 100644 index 2aa6f8ca..00000000 --- a/query/codebuild/codebuild_project_s3_logs_encryption_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when not (logs_config -> 'S3Logs' ->> 'EncryptionDisabled')::bool then 'ok' - else 'alarm' - end as status, - case - when not (logs_config -> 'S3Logs' ->> 'EncryptionDisabled')::bool then title || ' S3Logs encryption enabled.' - else title || ' S3Logs encryption disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_codebuild_project; \ No newline at end of file diff --git a/query/codebuild/codebuild_project_source_repo_oauth_configured.sql b/query/codebuild/codebuild_project_source_repo_oauth_configured.sql deleted file mode 100644 index 8aef7732..00000000 --- a/query/codebuild/codebuild_project_source_repo_oauth_configured.sql +++ /dev/null @@ -1,20 +0,0 @@ -select - -- Required Columns - p.arn as resource, - case - when p.source ->> 'Type' not in ('GITHUB', 'BITBUCKET') then 'skip' - when c.auth_type = 'OAUTH' then 'ok' - else 'alarm' - end as status, - case - when p.source ->> 'Type' = 'NO_SOURCE' then p.title || ' doesn''t have input source code.' - when p.source ->> 'Type' not in ('GITHUB', 'BITBUCKET') then p.title || ' source code isn''t in GitHub/Bitbucket repository.' - when c.auth_type = 'OAUTH' then p.title || ' using OAuth to connect source repository.' - else p.title || ' not using OAuth to connect source repository.' - end as reason, - -- Additional Dimensions - p.region, - p.account_id -from - aws_codebuild_project as p - left join aws_codebuild_source_credential as c on (p.region = c.region and p.source ->> 'Type' = c.server_type); \ No newline at end of file diff --git a/query/codebuild/codebuild_project_with_user_controlled_buildspec.sql b/query/codebuild/codebuild_project_with_user_controlled_buildspec.sql deleted file mode 100644 index d3465104..00000000 --- a/query/codebuild/codebuild_project_with_user_controlled_buildspec.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when split_part(source ->> 'Buildspec', '.', -1) = 'yml' then 'alarm' - else 'ok' - end as status, - case - when split_part(source ->> 'Buildspec', '.', -1) = 'yml' then title || ' uses a user controlled buildspec.' - else title || ' does not uses a user controlled buildspec.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_codebuild_project; \ No newline at end of file diff --git a/query/config/config_enabled_all_regions.sql b/query/config/config_enabled_all_regions.sql deleted file mode 100644 index 4cbecacc..00000000 --- a/query/config/config_enabled_all_regions.sql +++ /dev/null @@ -1,58 +0,0 @@ --- pgFormatter-ignore --- Get count for any region with all matching criteria -with global_recorders as ( - select - count(*) as global_config_recorders - from - aws_config_configuration_recorder - where - recording_group -> 'IncludeGlobalResourceTypes' = 'true' - and recording_group -> 'AllSupported' = 'true' - and status ->> 'Recording' = 'true' - and status ->> 'LastStatus' = 'SUCCESS' - ) -select - -- Required columns - 'arn:aws::' || a.region || ':' || a.account_id as resource, - case - -- When any of the region satisfies with above CTE - -- In left join of table, regions now having - -- 'Recording' and 'LastStatus' matching criteria can be considered as OK - when - g.global_config_recorders >= 1 - and status ->> 'Recording' = 'true' - and status ->> 'LastStatus' = 'SUCCESS' - then 'ok' - -- Skip any regions that are disabled in the account. - when a.opt_in_status = 'not-opted-in' then 'skip' - else 'alarm' - end as status, - -- Below cases are for citing respective reasons for control state - case - when a.opt_in_status = 'not-opted-in' then a.region || ' region is disabled.' - else - case - when recording_group -> 'IncludeGlobalResourceTypes' = 'true' then a.region || ' IncludeGlobalResourceTypes enabled,' - else a.region || ' IncludeGlobalResourceTypes disabled,' - end || - case - when recording_group -> 'AllSupported' = 'true' then ' AllSupported enabled,' - else ' AllSupported disabled,' - end || - case - when status ->> 'Recording' = 'true' then ' Recording enabled' - else ' Recording disabled' - end || - case - when status ->> 'LastStatus' = 'SUCCESS' then ' and LastStatus is SUCCESS.' - else ' and LastStatus is not SUCCESS.' - end - end as reason, - -- Additional columns - a.region, - a.account_id -from - global_recorders as g, - aws_region as a - left join aws_config_configuration_recorder as r - on r.account_id = a.account_id and r.region = a.name; diff --git a/query/dax/dax_cluster_encryption_at_rest_enabled.sql b/query/dax/dax_cluster_encryption_at_rest_enabled.sql deleted file mode 100644 index 0cabc183..00000000 --- a/query/dax/dax_cluster_encryption_at_rest_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when sse_description ->> 'Status' = 'ENABLED' then 'ok' - else 'alarm' - end as status, - case - when sse_description ->> 'Status' = 'ENABLED' then title || ' encryption at rest enabled.' - else title || ' encryption at rest not enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_dax_cluster; \ No newline at end of file diff --git a/query/dms/dms_replication_instance_not_publicly_accessible.sql b/query/dms/dms_replication_instance_not_publicly_accessible.sql deleted file mode 100644 index 526090fe..00000000 --- a/query/dms/dms_replication_instance_not_publicly_accessible.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when publicly_accessible then 'alarm' - else 'ok' - end status, - case - when publicly_accessible then title || ' publicly accessible.' - else title || ' not publicly accessible.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_dms_replication_instance; diff --git a/query/dynamodb/dynamodb_table_auto_scaling_enabled.sql b/query/dynamodb/dynamodb_table_auto_scaling_enabled.sql deleted file mode 100644 index a041e9bd..00000000 --- a/query/dynamodb/dynamodb_table_auto_scaling_enabled.sql +++ /dev/null @@ -1,29 +0,0 @@ -with table_with_autocaling as ( - select - t.resource_id as resource_id, - count(t.resource_id) as count - from - aws_appautoscaling_target as t where service_namespace = 'dynamodb' - group by t.resource_id -) -select - -- Required Columns - d.arn as resource, - case - when d.billing_mode = 'PAY_PER_REQUEST' then 'ok' - when t.resource_id is null then 'alarm' - when t.count < 2 then 'alarm' - else 'ok' - end as status, - case - when d.billing_mode = 'PAY_PER_REQUEST' then d.title || ' on-demand mode enabled.' - when t.resource_id is null then d.title || ' autoscaling not enabled.' - when t.count < 2 then d.title || ' auto scaling not enabled for both read and write capacity.' - else d.title || ' autoscaling enabled for both read and write capacity.' - end as reason, - -- Additional Dimensions - d.region, - d.account_id -from - aws_dynamodb_table as d - left join table_with_autocaling as t on concat('table/', d.name) = t.resource_id; diff --git a/query/dynamodb/dynamodb_table_encrypted_with_kms.sql b/query/dynamodb/dynamodb_table_encrypted_with_kms.sql deleted file mode 100644 index 34b4a30a..00000000 --- a/query/dynamodb/dynamodb_table_encrypted_with_kms.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when sse_description is null then 'alarm' - else 'ok' - end as status, - case - when sse_description is null then title || ' not encrypted with KMS.' - else title || ' encrypted with KMS.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_dynamodb_table; diff --git a/query/dynamodb/dynamodb_table_encryption_enabled.sql b/query/dynamodb/dynamodb_table_encryption_enabled.sql deleted file mode 100644 index 033ba6b0..00000000 --- a/query/dynamodb/dynamodb_table_encryption_enabled.sql +++ /dev/null @@ -1,19 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when sse_description is not null and sse_description ->> 'SSEType' = 'KMS' then 'ok' - when sse_description is null then 'ok' - else 'alarm' - end as status, - case - when sse_description is not null and sse_description ->> 'SSEType' = 'KMS' - then title || ' encrypted with AWS KMS.' - when sse_description is null then title || ' encrypted with DynamoDB managed CMK.' - else title || ' not encrypted with CMK.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_dynamodb_table; diff --git a/query/dynamodb/dynamodb_table_in_backup_plan.sql b/query/dynamodb/dynamodb_table_in_backup_plan.sql deleted file mode 100644 index 90d6c988..00000000 --- a/query/dynamodb/dynamodb_table_in_backup_plan.sql +++ /dev/null @@ -1,46 +0,0 @@ -with mapped_with_id as ( - select - jsonb_agg(elems) as mapped_ids - from - aws_backup_selection, - jsonb_array_elements(resources) as elems - group by backup_plan_id -), -mapped_with_tags as ( - select - jsonb_agg(elems ->> 'ConditionKey') as mapped_tags - from - aws_backup_selection, - jsonb_array_elements(list_of_tags) as elems - group by backup_plan_id -), -backed_up_table as ( - select - t.name - from - aws_dynamodb_table as t - join mapped_with_id as m on m.mapped_ids ?| array[t.arn] - union - select - t.name - from - aws_dynamodb_table as t - join mapped_with_tags as m on m.mapped_tags ?| array(select jsonb_object_keys(tags)) -) -select - -- Required Columns - t.arn as resource, - case - when b.name is null then 'alarm' - else 'ok' - end as status, - case - when b.name is null then t.title || ' not in backup plan.' - else t.title || ' in backup plan.' - end as reason, - -- Additional Dimensions - t.region, - t.account_id -from - aws_dynamodb_table as t - left join backed_up_table as b on t.name = b.name; diff --git a/query/dynamodb/dynamodb_table_point_in_time_recovery_enabled.sql b/query/dynamodb/dynamodb_table_point_in_time_recovery_enabled.sql deleted file mode 100644 index 73bf9738..00000000 --- a/query/dynamodb/dynamodb_table_point_in_time_recovery_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when lower( point_in_time_recovery_description ->> 'PointInTimeRecoveryStatus' ) = 'disabled' then 'alarm' - else 'ok' - end as status, - case - when lower( point_in_time_recovery_description ->> 'PointInTimeRecoveryStatus' ) = 'disabled' then title || ' point-in-time recovery not enabled.' - else title || ' point-in-time recovery enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_dynamodb_table; diff --git a/query/dynamodb/dynamodb_table_protected_by_backup_plan.sql b/query/dynamodb/dynamodb_table_protected_by_backup_plan.sql deleted file mode 100644 index 84862acf..00000000 --- a/query/dynamodb/dynamodb_table_protected_by_backup_plan.sql +++ /dev/null @@ -1,25 +0,0 @@ -with backup_protected_table as ( - select - resource_arn as arn - from - aws_backup_protected_resource as b - where - resource_type = 'DynamoDB' -) -select - -- Required Columns - t.arn as resource, - case - when b.arn is not null then 'ok' - else 'alarm' - end as status, - case - when b.arn is not null then t.title || ' is protected by backup plan.' - else t.title || ' is not protected by backup plan.' - end as reason, - -- Additional Dimensions - t.region, - t.account_id -from - aws_dynamodb_table as t - left join backup_protected_table as b on t.arn = b.arn; diff --git a/query/ebs/ebs_attached_volume_delete_on_termination_enabled.sql b/query/ebs/ebs_attached_volume_delete_on_termination_enabled.sql deleted file mode 100644 index 2d13df2b..00000000 --- a/query/ebs/ebs_attached_volume_delete_on_termination_enabled.sql +++ /dev/null @@ -1,19 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when state != 'in-use' then 'skip' - when attachment ->> 'DeleteOnTermination' = 'true' then 'ok' - else 'alarm' - end as status, - case - when state != 'in-use' then title || ' not attached to EC2 instance.' - when attachment ->> 'DeleteOnTermination' = 'true' then title || ' attached to ' || (attachment ->> 'InstanceId') || ', delete on termination enabled.' - else title || ' attached to ' || (attachment ->> 'InstanceId') || ', delete on termination disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ebs_volume - left join jsonb_array_elements(attachments) as attachment on true; diff --git a/query/ebs/ebs_attached_volume_encryption_enabled.sql b/query/ebs/ebs_attached_volume_encryption_enabled.sql deleted file mode 100644 index 12005b03..00000000 --- a/query/ebs/ebs_attached_volume_encryption_enabled.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when state != 'in-use' then 'skip' - when encrypted then 'ok' - else 'alarm' - end as status, - case - when state != 'in-use' then volume_id || ' not attached.' - when encrypted then volume_id || ' encrypted.' - else volume_id || ' not encrypted.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ebs_volume; \ No newline at end of file diff --git a/query/ebs/ebs_snapshot_not_publicly_restorable.sql b/query/ebs/ebs_snapshot_not_publicly_restorable.sql deleted file mode 100644 index f9121a8a..00000000 --- a/query/ebs/ebs_snapshot_not_publicly_restorable.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - 'arn:' || partition || ':ec2:' || region || ':' || account_id || ':snapshot/' || snapshot_id as resource, - case - when create_volume_permissions @> '[{"Group": "all", "UserId": null}]' then 'alarm' - else 'ok' - end status, - case - when create_volume_permissions @> '[{"Group": "all", "UserId": null}]' then title || ' is publicly restorable.' - else title || ' is not publicly restorable.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_ebs_snapshot; \ No newline at end of file diff --git a/query/ebs/ebs_volume_encryption_at_rest_enabled.sql b/query/ebs/ebs_volume_encryption_at_rest_enabled.sql deleted file mode 100644 index c16a37f0..00000000 --- a/query/ebs/ebs_volume_encryption_at_rest_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when encrypted then 'ok' - else 'alarm' - end status, - case - when encrypted then volume_id || ' encrypted.' - else volume_id || ' not encrypted.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_ebs_volume; \ No newline at end of file diff --git a/query/ebs/ebs_volume_in_backup_plan.sql b/query/ebs/ebs_volume_in_backup_plan.sql deleted file mode 100644 index ae0f8988..00000000 --- a/query/ebs/ebs_volume_in_backup_plan.sql +++ /dev/null @@ -1,46 +0,0 @@ -with mapped_with_id as ( - select - jsonb_agg(elems) as mapped_ids - from - aws_backup_selection, - jsonb_array_elements(resources) as elems - group by backup_plan_id -), -mapped_with_tags as ( - select - jsonb_agg(elems ->> 'ConditionKey') as mapped_tags - from - aws_backup_selection, - jsonb_array_elements(list_of_tags) as elems - group by backup_plan_id -), -backed_up_volume as ( - select - v.volume_id - from - aws_ebs_volume as v - join mapped_with_id as t on t.mapped_ids ?| array[v.arn] - union - select - v.volume_id - from - aws_ebs_volume as v - join mapped_with_tags as t on t.mapped_tags ?| array(select jsonb_object_keys(tags)) -) -select - -- Required Columns - v.arn as resource, - case - when b.volume_id is null then 'alarm' - else 'ok' - end as status, - case - when b.volume_id is null then v.title || ' not in backup plan.' - else v.title || ' in backup plan.' - end as reason, - -- Additional Dimensions - v.region, - v.account_id -from - aws_ebs_volume as v - left join backed_up_volume as b on v.volume_id = b.volume_id; \ No newline at end of file diff --git a/query/ebs/ebs_volume_protected_by_backup_plan.sql b/query/ebs/ebs_volume_protected_by_backup_plan.sql deleted file mode 100644 index ad3b5c0b..00000000 --- a/query/ebs/ebs_volume_protected_by_backup_plan.sql +++ /dev/null @@ -1,25 +0,0 @@ -with backup_protected_volume as ( - select - resource_arn as arn - from - aws_backup_protected_resource as b - where - resource_type = 'EBS' -) -select - -- Required Columns - v.arn as resource, - case - when b.arn is not null then 'ok' - else 'alarm' - end as status, - case - when b.arn is not null then v.title || ' is protected by backup plan.' - else v.title || ' is not protected by backup plan.' - end as reason, - -- Additional Dimensions - v.region, - v.account_id -from - aws_ebs_volume as v - left join backup_protected_volume as b on v.arn = b.arn; \ No newline at end of file diff --git a/query/ebs/ebs_volume_unused.sql b/query/ebs/ebs_volume_unused.sql deleted file mode 100644 index 2e71010c..00000000 --- a/query/ebs/ebs_volume_unused.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when state = 'in-use' then 'ok' - else 'alarm' - end as status, - case - when state = 'in-use' then title || ' attached to EC2 instance.' - else title || ' not attached to EC2 instance.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ebs_volume; \ No newline at end of file diff --git a/query/ec2/ec2_classic_lb_connection_draining_enabled.sql b/query/ec2/ec2_classic_lb_connection_draining_enabled.sql deleted file mode 100644 index a8471778..00000000 --- a/query/ec2/ec2_classic_lb_connection_draining_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when connection_draining_enabled then 'ok' - else 'alarm' - end as status, - case - when connection_draining_enabled then title || ' connection draining enabled.' - else title || ' connection draining disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_classic_load_balancer; \ No newline at end of file diff --git a/query/ec2/ec2_ebs_default_encryption_enabled.sql b/query/ec2/ec2_ebs_default_encryption_enabled.sql deleted file mode 100644 index 4a162cdd..00000000 --- a/query/ec2/ec2_ebs_default_encryption_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - 'arn:' || partition || '::' || region || ':' || account_id as resource, - case - when not default_ebs_encryption_enabled then 'alarm' - else 'ok' - end as status, - case - when not default_ebs_encryption_enabled then region || ' default EBS encryption disabled.' - else region || ' default EBS encryption enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_regional_settings; \ No newline at end of file diff --git a/query/ec2/ec2_instance_detailed_monitoring_enabled.sql b/query/ec2/ec2_instance_detailed_monitoring_enabled.sql deleted file mode 100644 index 63caede9..00000000 --- a/query/ec2/ec2_instance_detailed_monitoring_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when monitoring_state = 'enabled' then 'ok' - else 'alarm' - end status, - case - when monitoring_state = 'enabled' then instance_id || ' detailed monitoring enabled.' - else instance_id || ' detailed monitoring disabled.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_instance; \ No newline at end of file diff --git a/query/ec2/ec2_instance_ebs_optimized.sql b/query/ec2/ec2_instance_ebs_optimized.sql deleted file mode 100644 index 37f049f8..00000000 --- a/query/ec2/ec2_instance_ebs_optimized.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when ebs_optimized then 'ok' - else 'alarm' - end as status, - case - when ebs_optimized then title || ' EBS optimization enabled.' - else title || ' EBS optimization disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_instance; \ No newline at end of file diff --git a/query/ec2/ec2_instance_iam_profile_attached.sql b/query/ec2/ec2_instance_iam_profile_attached.sql deleted file mode 100644 index 7e32e2c0..00000000 --- a/query/ec2/ec2_instance_iam_profile_attached.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when iam_instance_profile_id is not null then 'ok' - else 'alarm' - end as status, - case - when iam_instance_profile_id is not null then title || ' IAM profile attached.' - else title || ' IAM profile not attached.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_instance; \ No newline at end of file diff --git a/query/ec2/ec2_instance_in_vpc.sql b/query/ec2/ec2_instance_in_vpc.sql deleted file mode 100644 index 2441a2ef..00000000 --- a/query/ec2/ec2_instance_in_vpc.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when vpc_id is null then 'alarm' - else 'ok' - end as status, - case - when vpc_id is null then title || ' not in VPC.' - else title || ' in VPC.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_instance; \ No newline at end of file diff --git a/query/ec2/ec2_instance_no_amazon_key_pair.sql b/query/ec2/ec2_instance_no_amazon_key_pair.sql deleted file mode 100644 index 4c6fa666..00000000 --- a/query/ec2/ec2_instance_no_amazon_key_pair.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when instance_state <> 'running' then 'skip' - when key_name is null then 'ok' - else 'alarm' - end as status, - case - when instance_state <> 'running' then title || ' is in ' || instance_state || ' state.' - when key_name is null then title || ' not launched using amazon key pairs.' - else title || ' launched using amazon key pairs.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_instance; diff --git a/query/ec2/ec2_instance_no_high_level_finding_in_inspector_scan.sql b/query/ec2/ec2_instance_no_high_level_finding_in_inspector_scan.sql deleted file mode 100644 index de9d1028..00000000 --- a/query/ec2/ec2_instance_no_high_level_finding_in_inspector_scan.sql +++ /dev/null @@ -1,37 +0,0 @@ -with severity_list as ( - select - distinct title , - a ->> 'Value' as instance_id - from - aws_inspector_finding, - jsonb_array_elements(attributes) as a - where - severity = 'High' - and asset_type = 'ec2-instance' - and a ->> 'Key' = 'INSTANCE_ID' - group by - a ->> 'Value', - title -), ec2_istance_list as ( - select - distinct instance_id - from - severity_list -) -select - -- Required Columns - arn as resource, - case - when l.instance_id is null then 'ok' - else 'alarm' - end as status, - case - when l.instance_id is null then i.title || ' has no high level finding in inspector scans.' - else i.title || ' has ' || (select count(*) from severity_list where instance_id = i.instance_id) || ' high level findings in inspector scans.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_instance as i - left join ec2_istance_list as l on i.instance_id = l.instance_id; \ No newline at end of file diff --git a/query/ec2/ec2_instance_no_launch_wizard_security_group.sql b/query/ec2/ec2_instance_no_launch_wizard_security_group.sql deleted file mode 100644 index 9a5eb524..00000000 --- a/query/ec2/ec2_instance_no_launch_wizard_security_group.sql +++ /dev/null @@ -1,26 +0,0 @@ -with launch_wizard_sg_attached_instance as ( - select - distinct arn as arn - from - aws_ec2_instance, - jsonb_array_elements(security_groups) as sg - where - sg ->> 'GroupName' like 'launch-wizard%' -) -select - -- Required Columns - i.arn as resource, - case - when sg.arn is null then 'ok' - else 'alarm' - end as status, - case - when sg.arn is null then i.title || ' not associated with launch-wizard security group.' - else i.title || ' associated with launch-wizard security group.' - end as reason, - -- Additional Dimensions - i.region, - i.account_id -from - aws_ec2_instance as i - left join launch_wizard_sg_attached_instance as sg on i.arn = sg.arn; \ No newline at end of file diff --git a/query/ec2/ec2_instance_not_publicly_accessible.sql b/query/ec2/ec2_instance_not_publicly_accessible.sql deleted file mode 100644 index 41e60337..00000000 --- a/query/ec2/ec2_instance_not_publicly_accessible.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when public_ip_address is null then 'ok' - else 'alarm' - end status, - case - when public_ip_address is null then instance_id || ' not publicly accessible.' - else instance_id || ' publicly accessible.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_instance; \ No newline at end of file diff --git a/query/ec2/ec2_instance_not_use_multiple_enis.sql b/query/ec2/ec2_instance_not_use_multiple_enis.sql deleted file mode 100644 index 90dc8adc..00000000 --- a/query/ec2/ec2_instance_not_use_multiple_enis.sql +++ /dev/null @@ -1,14 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when jsonb_array_length(network_interfaces) = 1 then 'ok' - else 'alarm' - end status, - title || ' has ' || jsonb_array_length(network_interfaces) || ' ENI(s) attached.' - as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_instance; \ No newline at end of file diff --git a/query/ec2/ec2_instance_protected_by_backup_plan.sql b/query/ec2/ec2_instance_protected_by_backup_plan.sql deleted file mode 100644 index 3b94a875..00000000 --- a/query/ec2/ec2_instance_protected_by_backup_plan.sql +++ /dev/null @@ -1,25 +0,0 @@ -with backup_protected_instance as ( - select - resource_arn as arn - from - aws_backup_protected_resource as b - where - resource_type = 'EC2' -) -select - -- Required Columns - i.arn as resource, - case - when b.arn is not null then 'ok' - else 'alarm' - end as status, - case - when b.arn is not null then i.title || ' is protected by backup plan.' - else i.title || ' is not protected by backup plan.' - end as reason, - -- Additional Dimensions - i.region, - i.account_id -from - aws_ec2_instance as i - left join backup_protected_instance as b on i.arn = b.arn; \ No newline at end of file diff --git a/query/ec2/ec2_instance_publicly_accessible_iam_profile_attached.sql b/query/ec2/ec2_instance_publicly_accessible_iam_profile_attached.sql deleted file mode 100644 index 05e97cdb..00000000 --- a/query/ec2/ec2_instance_publicly_accessible_iam_profile_attached.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when iam_instance_profile_id is not null then 'ok' - else 'alarm' - end as status, - case - when iam_instance_profile_id is not null then title || ' IAM profile attached.' - else title || ' IAM profile not attached.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_instance -where - public_ip_address is not null; \ No newline at end of file diff --git a/query/ec2/ec2_instance_termination_protection_enabled.sql b/query/ec2/ec2_instance_termination_protection_enabled.sql deleted file mode 100644 index d520e8b0..00000000 --- a/query/ec2/ec2_instance_termination_protection_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when disable_api_termination then 'ok' - else 'alarm' - end status, - case - when disable_api_termination then instance_id || ' termination protection enabled.' - else instance_id || ' termination protection disabled.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_instance; \ No newline at end of file diff --git a/query/ec2/ec2_instance_user_data_no_secrets.sql b/query/ec2/ec2_instance_user_data_no_secrets.sql deleted file mode 100644 index 6a1ddc3c..00000000 --- a/query/ec2/ec2_instance_user_data_no_secrets.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when user_data like any (array ['%pass%', '%secret%','%token%','%key%']) - or user_data ~ '(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]' then 'alarm' - else 'ok' - end as status, - case - when user_data like any (array ['%pass%', '%secret%','%token%','%key%']) - or user_data ~ '(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]' then instance_id ||' potential secret found in user data.' - else instance_id || ' no secrets found in user data.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_instance; \ No newline at end of file diff --git a/query/ec2/ec2_instance_uses_imdsv2.sql b/query/ec2/ec2_instance_uses_imdsv2.sql deleted file mode 100644 index f76712fe..00000000 --- a/query/ec2/ec2_instance_uses_imdsv2.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when metadata_options ->> 'HttpTokens' = 'optional' then 'alarm' - else 'ok' - end as status, - case - when metadata_options ->> 'HttpTokens' = 'optional' then title || ' not configured to use Instance Metadata Service Version 2 (IMDSv2).' - else title || ' configured to use Instance Metadata Service Version 2 (IMDSv2).' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_instance; \ No newline at end of file diff --git a/query/ec2/ec2_instance_virtualization_type_no_paravirtual.sql b/query/ec2/ec2_instance_virtualization_type_no_paravirtual.sql deleted file mode 100644 index b97dbc08..00000000 --- a/query/ec2/ec2_instance_virtualization_type_no_paravirtual.sql +++ /dev/null @@ -1,13 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when virtualization_type = 'paravirtual' then 'alarm' - else 'ok' - end as status, - title || ' virtualization type is ' || virtualization_type || '.' as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_instance; diff --git a/query/ec2/ec2_stopped_instance_30_days.sql b/query/ec2/ec2_stopped_instance_30_days.sql deleted file mode 100644 index 0c3eaf12..00000000 --- a/query/ec2/ec2_stopped_instance_30_days.sql +++ /dev/null @@ -1,17 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when instance_state not in ('stopped', 'stopping') then 'skip' - when state_transition_time <= (current_date - interval '30' day) then 'alarm' - else 'ok' - end as status, - case - when instance_state not in ('stopped', 'stopping') then title || ' is in ' || instance_state || ' state.' - else title || ' stopped since ' || to_char(state_transition_time , 'DD-Mon-YYYY') || ' (' || extract(day from current_timestamp - state_transition_time) || ' days).' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_instance; \ No newline at end of file diff --git a/query/ec2/ec2_transit_gateway_auto_cross_account_attachment_disabled.sql b/query/ec2/ec2_transit_gateway_auto_cross_account_attachment_disabled.sql deleted file mode 100644 index 6ce7e51e..00000000 --- a/query/ec2/ec2_transit_gateway_auto_cross_account_attachment_disabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - transit_gateway_arn as resource, - case - when auto_accept_shared_attachments = 'enable' then 'alarm' - else 'ok' - end status, - case - when auto_accept_shared_attachments = 'enable' then title || ' automatic shared account attachment enabled.' - else title || ' automatic shared account attachment disabled.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_transit_gateway; \ No newline at end of file diff --git a/query/ecr/ecr_repository_image_scan_on_push_enabled.sql b/query/ecr/ecr_repository_image_scan_on_push_enabled.sql deleted file mode 100644 index 1b0fa352..00000000 --- a/query/ecr/ecr_repository_image_scan_on_push_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when image_scanning_configuration ->> 'ScanOnPush' = 'true' then 'ok' - else 'alarm' - end as status, - case - when image_scanning_configuration ->> 'ScanOnPush' = 'true' then title || ' scan on push enabled.' - else title || ' scan on push disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ecr_repository; \ No newline at end of file diff --git a/query/ecr/ecr_repository_lifecycle_policy_configured.sql b/query/ecr/ecr_repository_lifecycle_policy_configured.sql deleted file mode 100644 index 11eb1e76..00000000 --- a/query/ecr/ecr_repository_lifecycle_policy_configured.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when lifecycle_policy -> 'rules' is not null then 'ok' - else 'alarm' - end as status, - case - when lifecycle_policy -> 'rules' is not null then title || ' lifecycle policy configured.' - else title || ' lifecycle policy not configured.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ecr_repository; \ No newline at end of file diff --git a/query/ecr/ecr_repository_prohibit_public_access.sql b/query/ecr/ecr_repository_prohibit_public_access.sql deleted file mode 100644 index 856fda93..00000000 --- a/query/ecr/ecr_repository_prohibit_public_access.sql +++ /dev/null @@ -1,34 +0,0 @@ -with open_access_ecr_repo as( - select - distinct arn - from - aws_ecr_repository, - jsonb_array_elements(policy_std -> 'Statement') as s, - jsonb_array_elements_text(s -> 'Principal' -> 'AWS') as p, - string_to_array(p, ':') as pa, - jsonb_array_elements_text(s -> 'Action') as a - where - s ->> 'Effect' = 'Allow' - and ( - p = '*' - ) -) -select - -- Required Columns - r.arn as resource, - case - when o.arn is not null then 'alarm' - else 'ok' - end as status, - case - when o.arn is not null then r.title || ' allows public access.' - else r.title || ' does not allow public access.' - end as reason, - -- Additional Dimensions - r.region, - r.account_id -from - aws_ecr_repository as r - left join open_access_ecr_repo as o on r.arn = o.arn -group by - resource, status, reason, r.region, r.account_id; \ No newline at end of file diff --git a/query/ecr/ecr_repository_tag_immutability_enabled.sql b/query/ecr/ecr_repository_tag_immutability_enabled.sql deleted file mode 100644 index 999e9aff..00000000 --- a/query/ecr/ecr_repository_tag_immutability_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when image_tag_mutability = 'IMMUTABLE' then 'ok' - else 'alarm' - end as status, - case - when image_tag_mutability = 'IMMUTABLE' then title || ' tag immutability enabled.' - else title || ' tag immutability disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ecr_repository; \ No newline at end of file diff --git a/query/ecs/ecs_cluster_container_insights_enabled.sql b/query/ecs/ecs_cluster_container_insights_enabled.sql deleted file mode 100644 index 71724ab2..00000000 --- a/query/ecs/ecs_cluster_container_insights_enabled.sql +++ /dev/null @@ -1,17 +0,0 @@ -select - -- Required Columns - cluster_arn as resource, - case - when s ->> 'Name' = 'containerInsights' and s ->> 'Value' = 'enabled' then 'ok' - else 'alarm' - end as status, - case - when s ->> 'Name' = 'containerInsights' and s ->> 'Value' = 'enabled' then title || ' Container Insights enabled.' - else title || ' Container Insights disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ecs_cluster as c, - jsonb_array_elements(settings) as s; \ No newline at end of file diff --git a/query/ecs/ecs_cluster_container_instance_agent_connected.sql b/query/ecs/ecs_cluster_container_instance_agent_connected.sql deleted file mode 100644 index 90ab359e..00000000 --- a/query/ecs/ecs_cluster_container_instance_agent_connected.sql +++ /dev/null @@ -1,27 +0,0 @@ -with unconnected_agent_instance as ( - select - distinct cluster_arn - from - aws_ecs_container_instance - where - agent_connected = false and status = 'ACTIVE' -) -select - -- Required Columns - c.cluster_arn as resource, - case - when c.registered_container_instances_count = 0 then 'skip' - when i.cluster_arn is null then 'ok' - else 'alarm' - end as status, - case - when c.registered_container_instances_count = 0 then title || ' has no container instance registered.' - when i.cluster_arn is null then title || ' container instance has connected agent.' - else title || ' container instance is either draining or has unconnected agents.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ecs_cluster as c - left join unconnected_agent_instance as i on c.cluster_arn = i.cluster_arn; \ No newline at end of file diff --git a/query/ecs/ecs_cluster_encryption_at_rest_enabled.sql b/query/ecs/ecs_cluster_encryption_at_rest_enabled.sql deleted file mode 100644 index b1806a10..00000000 --- a/query/ecs/ecs_cluster_encryption_at_rest_enabled.sql +++ /dev/null @@ -1,32 +0,0 @@ -with unencrypted_volumes as ( - select - distinct cluster_arn - from - aws_ecs_container_instance as i, - aws_ec2_instance as e, - jsonb_array_elements(block_device_mappings) as b, - aws_ebs_volume as v - where - i.ec2_instance_id = e.instance_id - and b -> 'Ebs' ->> 'VolumeId' = v.volume_id - and not v.encrypted -) -select - -- Required Columns - c.cluster_arn as resource, - case - when c.registered_container_instances_count = 0 then 'skip' - when v.cluster_arn is not null then 'alarm' - else 'ok' - end as status, - case - when c.registered_container_instances_count = 0 then title || ' has no container instance registered.' - when v.cluster_arn is not null then c.title || ' encryption at rest disabled.' - else c.title || ' encryption at rest enabled.' - end as reason, - -- Additional Dimensions - c.region, - c.account_id -from - aws_ecs_cluster as c - left join unencrypted_volumes as v on v.cluster_arn = c.cluster_arn; \ No newline at end of file diff --git a/query/ecs/ecs_cluster_instance_in_vpc.sql b/query/ecs/ecs_cluster_instance_in_vpc.sql deleted file mode 100644 index 420c6162..00000000 --- a/query/ecs/ecs_cluster_instance_in_vpc.sql +++ /dev/null @@ -1,17 +0,0 @@ -select - -- Required Columns - c.arn as resource, - case - when i.vpc_id is null then 'alarm' - else 'ok' - end as status, - case - when i.vpc_id is null then c.title || ' not in VPC.' - else c.title || ' in VPC.' - end as reason, - -- Additional Dimensions - c.region, - c.account_id -from - aws_ecs_container_instance as c - left join aws_ec2_instance as i on c.ec2_instance_id = i.instance_id; \ No newline at end of file diff --git a/query/ecs/ecs_cluster_no_registered_container_instance.sql b/query/ecs/ecs_cluster_no_registered_container_instance.sql deleted file mode 100644 index ff17fe53..00000000 --- a/query/ecs/ecs_cluster_no_registered_container_instance.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - cluster_arn as resource, - case - when registered_container_instances_count = 0 then 'alarm' - else 'ok' - end as status, - case - when registered_container_instances_count = 0 then title || ' has no container instance registered.' - else title || ' has ' || registered_container_instances_count || ' container instance(s) registered.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ecs_cluster; \ No newline at end of file diff --git a/query/ecs/ecs_service_fargate_using_latest_platform_version.sql b/query/ecs/ecs_service_fargate_using_latest_platform_version.sql deleted file mode 100644 index be7a45cc..00000000 --- a/query/ecs/ecs_service_fargate_using_latest_platform_version.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when launch_type <> 'FARGATE' then 'skip' - when platform_version = 'LATEST' then 'ok' - else 'alarm' - end as status, - case - when launch_type <> 'FARGATE' then title || ' is ' || launch_type || ' service.' - when platform_version = 'LATEST' then title || ' running on the latest fargate platform version.' - else title || ' not running on the latest fargate platform version.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ecs_service; \ No newline at end of file diff --git a/query/ecs/ecs_service_load_balancer_attached.sql b/query/ecs/ecs_service_load_balancer_attached.sql deleted file mode 100644 index 69301ce2..00000000 --- a/query/ecs/ecs_service_load_balancer_attached.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when jsonb_array_length(load_balancers) = 0 then 'alarm' - else 'ok' - end as status, - case - when jsonb_array_length(load_balancers) = 0 then title || ' has no load balancer attached.' - else title || ' has ' || jsonb_array_length(load_balancers) || ' load balancer(s) attached.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ecs_service; \ No newline at end of file diff --git a/query/ecs/ecs_service_not_publicly_accessible.sql b/query/ecs/ecs_service_not_publicly_accessible.sql deleted file mode 100644 index 33e7109e..00000000 --- a/query/ecs/ecs_service_not_publicly_accessible.sql +++ /dev/null @@ -1,29 +0,0 @@ -with service_awsvpc_mode_task_definition as ( - select - a.service_name as service_name, - b.task_definition_arn as task_definition - from - aws_ecs_service as a - left join aws_ecs_task_definition as b on a.task_definition = b.task_definition_arn - where - b.network_mode = 'awsvpc' -) -select - -- Required Columns - a.arn as resource, - case - when b.service_name is null then 'skip' - when network_configuration -> 'AwsvpcConfiguration' ->> 'AssignPublicIp' = 'DISABLED' then 'ok' - else 'alarm' - end as status, - case - when b.service_name is null then a.title || ' task definition not host network mode.' - when network_configuration -> 'AwsvpcConfiguration' ->> 'AssignPublicIp' = 'DISABLED' then a.title || ' not publicly accessible.' - else a.title || ' publicly accessible.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ecs_service as a - left join service_awsvpc_mode_task_definition as b on a.service_name = b.service_name; \ No newline at end of file diff --git a/query/ecs/ecs_task_definition_container_environment_no_secret.sql b/query/ecs/ecs_task_definition_container_environment_no_secret.sql deleted file mode 100644 index efd95715..00000000 --- a/query/ecs/ecs_task_definition_container_environment_no_secret.sql +++ /dev/null @@ -1,33 +0,0 @@ -with definitions_with_secret_environment_variable as ( - select - distinct task_definition_arn as arn - from - aws_ecs_task_definition, - jsonb_array_elements(container_definitions) as c, - jsonb_array_elements( c -> 'Environment') as e, - jsonb_array_elements( - case jsonb_typeof(c -> 'Secrets') - when 'array' then (c -> 'Secrets') - else null end - ) as s - where - e ->> 'Name' like any (array ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY','ECS_ENGINE_AUTH_DATA']) - or s ->> 'Name' like any (array ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY','ECS_ENGINE_AUTH_DATA']) -) -select - -- Required Columns - d.task_definition_arn as resource, - case - when e.arn is null then 'ok' - else 'alarm' - end as status, - case - when e.arn is null then d.title || ' container environment variables does not have secrets.' - else d.title || ' container environment variables have secrets.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ecs_task_definition as d - left join definitions_with_secret_environment_variable as e on d.task_definition_arn = e.arn; \ No newline at end of file diff --git a/query/ecs/ecs_task_definition_container_non_privileged.sql b/query/ecs/ecs_task_definition_container_non_privileged.sql deleted file mode 100644 index 3fc41b34..00000000 --- a/query/ecs/ecs_task_definition_container_non_privileged.sql +++ /dev/null @@ -1,26 +0,0 @@ -with privileged_container_definition as ( - select - distinct task_definition_arn as arn - from - aws_ecs_task_definition, - jsonb_array_elements(container_definitions) as c - where - c ->> 'Privileged' = 'true' -) -select - -- Required Columns - d.task_definition_arn as resource, - case - when c.arn is null then 'ok' - else 'alarm' - end as status, - case - when c.arn is null then d.title || ' does not have elevated privileges.' - else d.title || ' has elevated privileges.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ecs_task_definition as d - left join privileged_container_definition as c on d.task_definition_arn = c.arn; \ No newline at end of file diff --git a/query/ecs/ecs_task_definition_container_readonly_root_filesystem.sql b/query/ecs/ecs_task_definition_container_readonly_root_filesystem.sql deleted file mode 100644 index 6487bc5b..00000000 --- a/query/ecs/ecs_task_definition_container_readonly_root_filesystem.sql +++ /dev/null @@ -1,26 +0,0 @@ -with privileged_container_definition as ( - select - distinct task_definition_arn as arn - from - aws_ecs_task_definition, - jsonb_array_elements(container_definitions) as c - where - c ->> 'ReadonlyRootFilesystem' = 'true' -) -select - -- Required Columns - d.task_definition_arn as resource, - case - when c.arn is not null then 'ok' - else 'alarm' - end as status, - case - when c.arn is not null then d.title || ' containers limited to read-only access to root filesystems.' - else d.title || ' containers not limited to read-only access to root filesystems.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ecs_task_definition as d - left join privileged_container_definition as c on d.task_definition_arn = c.arn; \ No newline at end of file diff --git a/query/ecs/ecs_task_definition_logging_enabled.sql b/query/ecs/ecs_task_definition_logging_enabled.sql deleted file mode 100644 index 54695ac6..00000000 --- a/query/ecs/ecs_task_definition_logging_enabled.sql +++ /dev/null @@ -1,26 +0,0 @@ -with task_definitions_logging_enabled as ( - select - distinct task_definition_arn as arn - from - aws_ecs_task_definition, - jsonb_array_elements(container_definitions) as c - where - c ->> 'LogConfiguration' is not null -) -select - -- Required Columns - a.task_definition_arn as resource, - case - when b.arn is not null then 'ok' - else 'alarm' - end as status, - case - when b.arn is not null then a.title || ' logging enabled.' - else a.title || ' logging disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ecs_task_definition as a - left join task_definitions_logging_enabled as b on a.task_definition_arn = b.arn; \ No newline at end of file diff --git a/query/ecs/ecs_task_definition_no_host_pid_mode.sql b/query/ecs/ecs_task_definition_no_host_pid_mode.sql deleted file mode 100644 index 283b516c..00000000 --- a/query/ecs/ecs_task_definition_no_host_pid_mode.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - task_definition_arn as resource, - case - when pid_mode = 'host' then 'alarm' - else 'ok' - end as status, - case - when pid_mode = 'host' then title || ' shares the host process namespace.' - else title || ' does not share the host process namespace.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ecs_task_definition; \ No newline at end of file diff --git a/query/ecs/ecs_task_definition_user_for_host_mode_check.sql b/query/ecs/ecs_task_definition_user_for_host_mode_check.sql deleted file mode 100644 index e896e2b2..00000000 --- a/query/ecs/ecs_task_definition_user_for_host_mode_check.sql +++ /dev/null @@ -1,36 +0,0 @@ -with host_network_task_definition as ( - select - distinct task_definition_arn as arn - from - aws_ecs_task_definition, - jsonb_array_elements(container_definitions) as c - where - network_mode = 'host' - and - (c ->> 'Privileged' is not null - and c ->> 'Privileged' <> 'false' - ) - and - ( c ->> 'User' is not null - and c ->> 'User' <> 'root' - ) -) -select - -- Required Columns - a.task_definition_arn as resource, - case - when a.network_mode is null or a.network_mode <> 'host' then 'skip' - when b.arn is not null then 'ok' - else 'alarm' - end as status, - case - when a.network_mode is null or a.network_mode <> 'host' then a.title || ' not host network mode.' - when b.arn is not null then a.title || ' have secure host network mode.' - else a.title || ' not have secure host network mode.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ecs_task_definition as a - left join host_network_task_definition as b on a.task_definition_arn = b.arn; \ No newline at end of file diff --git a/query/efs/efs_access_point_enforce_root_directory.sql b/query/efs/efs_access_point_enforce_root_directory.sql deleted file mode 100644 index 13b61720..00000000 --- a/query/efs/efs_access_point_enforce_root_directory.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - access_point_arn as resource, - case - when root_directory ->> 'Path'= '/' then 'alarm' - else 'ok' - end as status, - case - when root_directory ->> 'Path'= '/' then title || ' not configured to enforce a root directory.' - else title || ' configured to enforce a root directory.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_efs_access_point; diff --git a/query/efs/efs_access_point_enforce_user_identity.sql b/query/efs/efs_access_point_enforce_user_identity.sql deleted file mode 100644 index 11804fd7..00000000 --- a/query/efs/efs_access_point_enforce_user_identity.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - access_point_arn as resource, - case - when posix_user is null then 'alarm' - else 'ok' - end as status, - case - when posix_user is null then title || ' does not enforce a user identity.' - else title || ' enforces a user identity.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_efs_access_point; diff --git a/query/efs/efs_file_system_automatic_backups_enabled.sql b/query/efs/efs_file_system_automatic_backups_enabled.sql deleted file mode 100644 index cb5ce1ce..00000000 --- a/query/efs/efs_file_system_automatic_backups_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when automatic_backups = 'enabled' then 'ok' - else 'alarm' - end as status, - case - when automatic_backups = 'enabled' then title || ' automatic backups enabled.' - else title || ' automatic backups not enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_efs_file_system; diff --git a/query/efs/efs_file_system_encrypt_data_at_rest.sql b/query/efs/efs_file_system_encrypt_data_at_rest.sql deleted file mode 100644 index 6844655a..00000000 --- a/query/efs/efs_file_system_encrypt_data_at_rest.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when encrypted then 'ok' - else 'alarm' - end as status, - case - when encrypted then title || ' encrypted at rest.' - else title || ' not encrypted at rest.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_efs_file_system; diff --git a/query/efs/efs_file_system_encrypted_with_cmk.sql b/query/efs/efs_file_system_encrypted_with_cmk.sql deleted file mode 100644 index d3e70ddc..00000000 --- a/query/efs/efs_file_system_encrypted_with_cmk.sql +++ /dev/null @@ -1,29 +0,0 @@ -with encrypted_fs as ( - select - fs.arn as arn, - key_manager - from - aws_efs_file_system as fs - left join aws_kms_key as k on fs.kms_key_id = k.arn - where - enabled -) -select - -- Required Columns - f.arn as resource, - case - when not encrypted then 'alarm' - when encrypted and e.key_manager = 'CUSTOMER' then 'ok' - else 'alarm' - end as status, - case - when not encrypted then title || ' not encrypted.' - when encrypted and e.key_manager = 'CUSTOMER' then title || ' encrypted with CMK.' - else title || ' not encrypted with CMK.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_efs_file_system as f - left join encrypted_fs as e on f.arn = e.arn; diff --git a/query/efs/efs_file_system_enforces_ssl.sql b/query/efs/efs_file_system_enforces_ssl.sql deleted file mode 100644 index e891ffcb..00000000 --- a/query/efs/efs_file_system_enforces_ssl.sql +++ /dev/null @@ -1,35 +0,0 @@ -with ssl_ok as ( - select - distinct name, - arn, - 'ok' as status - from - aws_efs_file_system, - jsonb_array_elements(policy_std -> 'Statement') as s, - jsonb_array_elements_text(s -> 'Principal' -> 'AWS') as p, - jsonb_array_elements_text(s -> 'Action') as a, - jsonb_array_elements_text( - s -> 'Condition' -> 'Bool' -> 'aws:securetransport' - ) as ssl - where - p = '*' - and s ->> 'Effect' = 'Deny' - and ssl :: bool = false -) -select - -- Required Columns - f.arn as resource, - case - when ok.status = 'ok' then 'ok' - else 'alarm' - end status, - case - when ok.status = 'ok' then f.title || ' policy enforces HTTPS.' - else f.title || ' policy does not enforce HTTPS.' - end reason, - -- Additional Dimensions - f.region, - f.account_id -from - aws_efs_file_system as f - left join ssl_ok as ok on ok.name = f.name; \ No newline at end of file diff --git a/query/efs/efs_file_system_protected_by_backup_plan.sql b/query/efs/efs_file_system_protected_by_backup_plan.sql deleted file mode 100644 index 43ceebd7..00000000 --- a/query/efs/efs_file_system_protected_by_backup_plan.sql +++ /dev/null @@ -1,25 +0,0 @@ -with backup_protected_file_system as ( - select - resource_arn as arn - from - aws_backup_protected_resource as b - where - resource_type = 'EFS' -) -select - -- Required Columns - f.arn as resource, - case - when b.arn is not null then 'ok' - else 'alarm' - end as status, - case - when b.arn is not null then f.title || ' is protected by backup plan.' - else f.title || ' is not protected by backup plan.' - end as reason, - -- Additional Dimensions - f.region, - f.account_id -from - aws_efs_file_system as f - left join backup_protected_file_system as b on f.arn = b.arn; diff --git a/query/eks/eks_cluster_control_plane_audit_logging_enabled.sql b/query/eks/eks_cluster_control_plane_audit_logging_enabled.sql deleted file mode 100644 index 6a40bd8b..00000000 --- a/query/eks/eks_cluster_control_plane_audit_logging_enabled.sql +++ /dev/null @@ -1,31 +0,0 @@ -with control_panel_audit_logging as ( - select - distinct arn, - log -> 'Types' as log_type - from - aws_eks_cluster, - jsonb_array_elements(logging -> 'ClusterLogging') as log - where - log ->> 'Enabled' = 'true' - and (log -> 'Types') @> '["api", "audit", "authenticator", "controllerManager", "scheduler"]' -) -select - -- Required Columns - c.arn as resource, - case - when l.arn is not null then 'ok' - else 'alarm' - end as status, - case - when l.arn is not null then c.title || ' control plane audit logging enabled for all log types.' - else - case when logging -> 'ClusterLogging' @> '[{"Enabled": true}]' then c.title || ' control plane audit logging not enabled for all log types.' - else c.title || ' control plane audit logging not enabled.' - end - end as reason, - -- Additional Dimensions - c.region, - c.account_id -from - aws_eks_cluster as c - left join control_panel_audit_logging as l on l.arn = c.arn; \ No newline at end of file diff --git a/query/eks/eks_cluster_endpoint_restrict_public_access.sql b/query/eks/eks_cluster_endpoint_restrict_public_access.sql deleted file mode 100644 index d61213fa..00000000 --- a/query/eks/eks_cluster_endpoint_restrict_public_access.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when resources_vpc_config ->> 'EndpointPublicAccess' = 'true' then 'alarm' - else 'ok' - end as status, - case - when resources_vpc_config ->> 'EndpointPublicAccess' = 'true' then title || ' endpoint publicly accessible.' - else title || ' endpoint not publicly accessible.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_eks_cluster; \ No newline at end of file diff --git a/query/eks/eks_cluster_no_default_vpc.sql b/query/eks/eks_cluster_no_default_vpc.sql deleted file mode 100644 index 81209a88..00000000 --- a/query/eks/eks_cluster_no_default_vpc.sql +++ /dev/null @@ -1,26 +0,0 @@ -with default_vpc_cluster as ( - select - distinct c.arn - from - aws_eks_cluster as c - left join aws_vpc as v on v.vpc_id = c.resources_vpc_config ->> 'VpcId' - where - v.is_default -) -select - -- Required Columns - c.arn as resource, - case - when v.arn is not null then 'alarm' - else 'ok' - end as status, - case - when v.arn is not null then title || ' uses default VPC.' - else title || ' does not use default VPC.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_eks_cluster as c - left join default_vpc_cluster as v on v.arn = c.arn \ No newline at end of file diff --git a/query/eks/eks_cluster_secrets_encrypted.sql b/query/eks/eks_cluster_secrets_encrypted.sql deleted file mode 100644 index 06591f01..00000000 --- a/query/eks/eks_cluster_secrets_encrypted.sql +++ /dev/null @@ -1,28 +0,0 @@ -with eks_secrets_encrypted as ( - select - distinct arn as arn - from - aws_eks_cluster, - jsonb_array_elements(encryption_config) as e - where - e -> 'Resources' @> '["secrets"]' -) -select - -- Required Columns - a.arn as resource, - case - when encryption_config is null then 'alarm' - when b.arn is not null then 'ok' - else 'alarm' - end as status, - case - when encryption_config is null then a.title || ' encryption not enabled.' - when b.arn is not null then a.title || ' encrypted with EKS secrets.' - else a.title || ' not encrypted with EKS secrets.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_eks_cluster as a - left join eks_secrets_encrypted as b on a.arn = b.arn; \ No newline at end of file diff --git a/query/eks/eks_cluster_with_latest_kubernetes_version.sql b/query/eks/eks_cluster_with_latest_kubernetes_version.sql deleted file mode 100644 index dbc7a34d..00000000 --- a/query/eks/eks_cluster_with_latest_kubernetes_version.sql +++ /dev/null @@ -1,17 +0,0 @@ -select - -- Required Columns - arn as resource, - case - -- eks:oldestVersionSupported (Current oldest supported version is 1.19) - when (version)::decimal >= 1.19 then 'ok' - else 'alarm' - end as status, - case - when (version)::decimal >= 1.19 then title || ' runs on a supported kubernetes version.' - else title || ' does not run on a supported kubernetes version.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_eks_cluster; \ No newline at end of file diff --git a/query/elasticache/elasticache_redis_cluster_automatic_backup_retention_15_days.sql b/query/elasticache/elasticache_redis_cluster_automatic_backup_retention_15_days.sql deleted file mode 100644 index a4fd5edf..00000000 --- a/query/elasticache/elasticache_redis_cluster_automatic_backup_retention_15_days.sql +++ /dev/null @@ -1,17 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when snapshot_retention_limit < 15 then 'alarm' - else 'ok' - end as status, - case - when snapshot_retention_limit = 0 then title || ' automatic backups not enabled.' - when snapshot_retention_limit < 15 then title || ' automatic backup retention period is less than 15 days.' - else title || ' automatic backup retention period is more than 15 days.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_elasticache_replication_group; diff --git a/query/elasticbeanstalk/elastic_beanstalk_enhanced_health_reporting_enabled.sql b/query/elasticbeanstalk/elastic_beanstalk_enhanced_health_reporting_enabled.sql deleted file mode 100644 index 739812c9..00000000 --- a/query/elasticbeanstalk/elastic_beanstalk_enhanced_health_reporting_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - application_name as resource, - case - when health_status is not null and health is not null then 'ok' - else 'alarm' - end as status, - case - when health_status is not null and health is not null then application_name || ' enhanced health check enabled.' - else application_name || ' enhanced health check disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_elastic_beanstalk_environment; diff --git a/query/elb/elb_application_classic_lb_logging_enabled.sql b/query/elb/elb_application_classic_lb_logging_enabled.sql deleted file mode 100644 index b725ec74..00000000 --- a/query/elb/elb_application_classic_lb_logging_enabled.sql +++ /dev/null @@ -1,37 +0,0 @@ -( - select - -- Required Columns - arn as resource, - case - when load_balancer_attributes @> '[{"Key": "access_logs.s3.enabled", "Value": "true"}]' then 'ok' - else 'alarm' - end as status, - case - when load_balancer_attributes @> '[{"Key": "access_logs.s3.enabled", "Value": "true"}]' then title || ' logging enabled.' - else title || ' logging disabled.' - end as reason, - -- Additional Dimensions - region, - account_id - from - aws_ec2_application_load_balancer -) -union -( - select - -- Required Columns - 'arn:' || partition || ':elasticloadbalancing:' || region || ':' || account_id || ':loadbalancer/' || title as resource, - case - when access_log_enabled = 'true' then 'ok' - else 'alarm' - end as status, - case - when access_log_enabled = 'true' then title || ' logging enabled.' - else title || ' logging disabled.' - end as reason, - -- Additional Dimensions - region, - account_id - from - aws_ec2_classic_load_balancer -); \ No newline at end of file diff --git a/query/elb/elb_application_classic_network_lb_prohibit_public_access.sql b/query/elb/elb_application_classic_network_lb_prohibit_public_access.sql deleted file mode 100644 index 384c3297..00000000 --- a/query/elb/elb_application_classic_network_lb_prohibit_public_access.sql +++ /dev/null @@ -1,44 +0,0 @@ -with all_lb_details as ( - select - arn, - scheme, - title, - region, - account_id - from - aws_ec2_application_load_balancer - union - select - arn, - scheme, - title, - region, - account_id - from - aws_ec2_network_load_balancer - union - select - arn, - scheme, - title, - region, - account_id - from - aws_ec2_classic_load_balancer -) -select - -- Required Columns - arn as resource, - case - when scheme = 'internet-facing' then 'alarm' - else 'ok' - end as status, - case - when scheme = 'internet-facing' then title || ' publicly accessible.' - else title|| ' not publicly accessible.' - end as reason, - -- Additional Dimensions - region, - account_id -from - all_lb_details; \ No newline at end of file diff --git a/query/elb/elb_application_gateway_network_lb_multiple_az_configured.sql b/query/elb/elb_application_gateway_network_lb_multiple_az_configured.sql deleted file mode 100644 index 06b9a1a8..00000000 --- a/query/elb/elb_application_gateway_network_lb_multiple_az_configured.sql +++ /dev/null @@ -1,41 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when jsonb_array_length(availability_zones) < 2 then 'alarm' - else 'ok' - end as status, - title || ' has ' || jsonb_array_length(availability_zones) || ' availability zone(s).' as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_application_load_balancer -union -select - -- Required Columns - arn as resource, - case - when jsonb_array_length(availability_zones) < 2 then 'alarm' - else 'ok' - end as status, - title || ' has ' || jsonb_array_length(availability_zones) || ' availability zone(s).' as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_network_load_balancer -union -select - -- Required Columns - arn as resource, - case - when jsonb_array_length(availability_zones) < 2 then 'alarm' - else 'ok' - end as status, - title || ' has ' || jsonb_array_length(availability_zones) || ' availability zone(s).' as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_gateway_load_balancer; diff --git a/query/elb/elb_application_lb_deletion_protection_enabled.sql b/query/elb/elb_application_lb_deletion_protection_enabled.sql deleted file mode 100644 index 990636e9..00000000 --- a/query/elb/elb_application_lb_deletion_protection_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when load_balancer_attributes @> '[{"Key": "deletion_protection.enabled", "Value": "true"}]' then 'ok' - else 'alarm' - end as status, - case - when load_balancer_attributes @> '[{"Key": "deletion_protection.enabled", "Value": "true"}]' then title || ' deletion protection enabled.' - else title || ' deletion protection disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_application_load_balancer; \ No newline at end of file diff --git a/query/elb/elb_application_lb_desync_mitigation_mode.sql b/query/elb/elb_application_lb_desync_mitigation_mode.sql deleted file mode 100644 index 5a371224..00000000 --- a/query/elb/elb_application_lb_desync_mitigation_mode.sql +++ /dev/null @@ -1,25 +0,0 @@ -with app_lb_desync_mitigation_mode as ( - select - arn, - l ->> 'Key', - l ->> 'Value' as v - from - aws_ec2_application_load_balancer, - jsonb_array_elements(load_balancer_attributes) as l - where - l ->> 'Key' = 'routing.http.desync_mitigation_mode' -) -select - -- Required Columns - a.arn as resource, - case - when m.v = any(array['defensive', 'strictest']) then 'ok' - else 'alarm' - end as status, - title || ' has ' || m.v || ' desync mitigation mode.' as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_application_load_balancer as a - left join app_lb_desync_mitigation_mode as m on a.arn = m.arn; diff --git a/query/elb/elb_application_lb_drop_http_headers.sql b/query/elb/elb_application_lb_drop_http_headers.sql deleted file mode 100644 index c7535260..00000000 --- a/query/elb/elb_application_lb_drop_http_headers.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when load_balancer_attributes @> '[{"Key": "routing.http.drop_invalid_header_fields.enabled", "Value": "true"}]' then 'ok' - else 'alarm' - end as status, - case - when load_balancer_attributes @> '[{"Key": "routing.http.drop_invalid_header_fields.enabled", "Value": "true"}]' then title || ' configured to drop http headers.' - else title || ' not configured to drop http headers.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_application_load_balancer; diff --git a/query/elb/elb_application_lb_listener_certificate_expire_30_days.sql b/query/elb/elb_application_lb_listener_certificate_expire_30_days.sql deleted file mode 100644 index 18f8e01c..00000000 --- a/query/elb/elb_application_lb_listener_certificate_expire_30_days.sql +++ /dev/null @@ -1,15 +0,0 @@ -select - -- Required Columns - load_balancer_arn as resource, - case - when date(not_after) - date(current_date) >= 30 then 'ok' - else 'alarm' - end as status, - l.title || ' certificate set to expire in ' || extract(day from not_after - current_date) || ' days.' as reason, - -- Additional Dimensions - l.region, - l.account_id -from - aws_ec2_load_balancer_listener as l, - jsonb_array_elements(certificates) as c - left join aws_acm_certificate as a on c ->> 'CertificateArn' = a.certificate_arn; \ No newline at end of file diff --git a/query/elb/elb_application_lb_listener_certificate_expire_7_days.sql b/query/elb/elb_application_lb_listener_certificate_expire_7_days.sql deleted file mode 100644 index b7b467d1..00000000 --- a/query/elb/elb_application_lb_listener_certificate_expire_7_days.sql +++ /dev/null @@ -1,15 +0,0 @@ -select - -- Required Columns - load_balancer_arn as resource, - case - when date(not_after) - date(current_date) >= 7 then 'ok' - else 'alarm' - end as status, - l.title || ' certificate set to expire in ' || extract(day from not_after - current_date) || ' days.' as reason, - -- Additional Dimensions - l.region, - l.account_id -from - aws_ec2_load_balancer_listener as l, - jsonb_array_elements(certificates) as c - left join aws_acm_certificate as a on c ->> 'CertificateArn' = a.certificate_arn; \ No newline at end of file diff --git a/query/elb/elb_application_lb_redirect_http_request_to_https.sql b/query/elb/elb_application_lb_redirect_http_request_to_https.sql deleted file mode 100644 index 593176d3..00000000 --- a/query/elb/elb_application_lb_redirect_http_request_to_https.sql +++ /dev/null @@ -1,31 +0,0 @@ -with detailed_listeners as ( - select - arn, - load_balancer_arn, - protocol - from - aws_ec2_load_balancer_listener, - jsonb_array_elements(default_actions) as ac - where - split_part(arn,'/',2) = 'app' - and protocol = 'HTTP' - and ac ->> 'Type' = 'redirect' - and ac -> 'RedirectConfig' ->> 'Protocol' = 'HTTPS' -) -select - -- Required Columns - a.arn as resource, - case - when b.load_balancer_arn is null then 'alarm' - else 'ok' - end as status, - case - when b.load_balancer_arn is not null then a.title || ' associated with HTTP redirection.' - else a.title || ' not associated with HTTP redirection.' - end as reason, - -- Additional Dimensions - a.region, - a.account_id -from - aws_ec2_application_load_balancer a - left join detailed_listeners b on a.arn = b.load_balancer_arn; \ No newline at end of file diff --git a/query/elb/elb_application_lb_waf_enabled.sql b/query/elb/elb_application_lb_waf_enabled.sql deleted file mode 100644 index 7298238d..00000000 --- a/query/elb/elb_application_lb_waf_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when load_balancer_attributes @> '[{"Key":"waf.fail_open.enabled","Value":"true"}]' then 'ok' - else 'alarm' - end as status, - case - when load_balancer_attributes @> '[{"Key":"waf.fail_open.enabled","Value":"true"}]' then title || ' WAF enabled.' - else title || ' WAF disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_application_load_balancer; \ No newline at end of file diff --git a/query/elb/elb_application_lb_with_outbound_rule.sql b/query/elb/elb_application_lb_with_outbound_rule.sql deleted file mode 100644 index 370c9606..00000000 --- a/query/elb/elb_application_lb_with_outbound_rule.sql +++ /dev/null @@ -1,39 +0,0 @@ -with sg_with_outbound as ( - select - arn, - sg - from - aws_ec2_application_load_balancer, - jsonb_array_elements_text(security_groups) as sg - left join aws_vpc_security_group_rule as sgr on sg = sgr.group_id - where - sgr.type = 'egress' - group by - sg, arn -), application_lb_without_outbound as ( - select - distinct arn - from - aws_ec2_application_load_balancer, - jsonb_array_elements_text(security_groups) as s - where - s not in ( select sg from sg_with_outbound) -) -select - distinct a.arn as resource, - case - when a.security_groups is null then 'alarm' - when o.arn is not null then 'alarm' - else 'ok' - end as status, - case - when a.security_groups is null then a.title || ' does not have security group attached.' - when o.arn is not null then a.title || ' all attached security groups does not have outbound rule(s).' - else a.title || ' all attached security groups have outbound rule(s).' - end as reason, - -- Additional Dimensions - a.region, - a.account_id -from - aws_ec2_application_load_balancer as a - left join application_lb_without_outbound as o on a.arn = o.arn \ No newline at end of file diff --git a/query/elb/elb_application_network_lb_use_listeners.sql b/query/elb/elb_application_network_lb_use_listeners.sql deleted file mode 100644 index 58303ba3..00000000 --- a/query/elb/elb_application_network_lb_use_listeners.sql +++ /dev/null @@ -1,34 +0,0 @@ -with load_balancers as ( - select - n.arn, - n.title, - n.region, - n.account_id - from - aws_ec2_network_load_balancer as n - union - select - a.arn, - a.title, - a.region, - a.account_id - from - aws_ec2_application_load_balancer as a -) -select - -- Required Columns - distinct lb.arn as resource, - case - when l.load_balancer_arn is not null then 'ok' - else 'alarm' - end as status, - case - when l.load_balancer_arn is not null then lb.title || ' uses listener.' - else lb.title || ' does not uses listener.' - end as reason, - -- Additional Dimensions - lb.region, - lb.account_id -from - load_balancers as lb - left join aws_ec2_load_balancer_listener as l on lb.arn = l.load_balancer_arn; \ No newline at end of file diff --git a/query/elb/elb_application_network_lb_use_ssl_certificate.sql b/query/elb/elb_application_network_lb_use_ssl_certificate.sql deleted file mode 100644 index 7170f338..00000000 --- a/query/elb/elb_application_network_lb_use_ssl_certificate.sql +++ /dev/null @@ -1,45 +0,0 @@ - with listeners_without_certificate as ( - select - load_balancer_arn, - count(*) as count - from - aws_ec2_load_balancer_listener - where arn not in - ( select arn from aws_ec2_load_balancer_listener, jsonb_array_elements(certificates) as c - where c ->> 'CertificateArn' like 'arn:aws:acm%' ) - group by load_balancer_arn -), -all_application_network_load_balacer as ( - select - arn, - account_id, - region, - title - from - aws_ec2_application_load_balancer - union - select - arn, - account_id, - region, - title - from - aws_ec2_network_load_balancer -) -select - -- Required Columns - a.arn as resource, - case - when b.load_balancer_arn is null then 'ok' - else 'alarm' - end as status, - case - when b.load_balancer_arn is null then a.title || ' uses certificates provided by ACM.' - else a.title || ' has ' || b.count || ' listeners which do not use certificates provided by ACM.' - end as reason, - -- Additional Dimensions - a.region, - a.account_id -from - all_application_network_load_balacer as a - left join listeners_without_certificate as b on a.arn = b.load_balancer_arn; \ No newline at end of file diff --git a/query/elb/elb_classic_lb_cross_zone_load_balancing_enabled.sql b/query/elb/elb_classic_lb_cross_zone_load_balancing_enabled.sql deleted file mode 100644 index 9580226f..00000000 --- a/query/elb/elb_classic_lb_cross_zone_load_balancing_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when cross_zone_load_balancing_enabled then 'ok' - else 'alarm' - end as status, - case - when cross_zone_load_balancing_enabled then title || ' cross-zone load balancing enabled.' - else title || ' cross-zone load balancing disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_classic_load_balancer; \ No newline at end of file diff --git a/query/elb/elb_classic_lb_desync_mitigation_mode.sql b/query/elb/elb_classic_lb_desync_mitigation_mode.sql deleted file mode 100644 index 561a0341..00000000 --- a/query/elb/elb_classic_lb_desync_mitigation_mode.sql +++ /dev/null @@ -1,25 +0,0 @@ -with app_lb_desync_mitigation_mode as ( - select - arn, - a ->> 'Key', - a ->> 'Value' as v - from - aws_ec2_classic_load_balancer, - jsonb_array_elements(additional_attributes) as a - where - a ->> 'Key' = 'elb.http.desyncmitigationmode' -) -select - -- Required Columns - c.arn as resource, - case - when m.v = any(array['defensive', 'strictest']) then 'ok' - else 'alarm' - end as status, - title || ' has ' || m.v || ' desync mitigation mode.' as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_classic_load_balancer as c - left join app_lb_desync_mitigation_mode as m on c.arn = m.arn; diff --git a/query/elb/elb_classic_lb_multiple_az_configured.sql b/query/elb/elb_classic_lb_multiple_az_configured.sql deleted file mode 100644 index b064c3d3..00000000 --- a/query/elb/elb_classic_lb_multiple_az_configured.sql +++ /dev/null @@ -1,13 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when jsonb_array_length(availability_zones) < 2 then 'alarm' - else 'ok' - end as status, - title || ' has ' || jsonb_array_length(availability_zones) || ' availability zone(s).' as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_classic_load_balancer; diff --git a/query/elb/elb_classic_lb_use_ssl_certificate.sql b/query/elb/elb_classic_lb_use_ssl_certificate.sql deleted file mode 100644 index 3d15f787..00000000 --- a/query/elb/elb_classic_lb_use_ssl_certificate.sql +++ /dev/null @@ -1,29 +0,0 @@ -with detailed_classic_listeners as ( - select - name - from - aws_ec2_classic_load_balancer, - jsonb_array_elements(listener_descriptions) as listener_description - where - listener_description -> 'Listener' ->> 'Protocol' in ('HTTPS', 'SSL', 'TLS') - and listener_description -> 'Listener' ->> 'SSLCertificateId' like 'arn:aws:acm%' -) -select - -- Required Columns - 'arn:' || a.partition || ':elasticloadbalancing:' || a.region || ':' || a.account_id || ':loadbalancer/' || a.name as resource, - case - when a.listener_descriptions is null then 'skip' - when b.name is not null then 'alarm' - else 'ok' - end as status, - case - when a.listener_descriptions is null then a.title || ' has no listener.' - when b.name is not null then a.title || ' does not use certificates provided by ACM.' - else a.title || ' uses certificates provided by ACM.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_classic_load_balancer as a - left join detailed_classic_listeners as b on a.name = b.name; \ No newline at end of file diff --git a/query/elb/elb_classic_lb_use_tls_https_listeners.sql b/query/elb/elb_classic_lb_use_tls_https_listeners.sql deleted file mode 100644 index bcb948c7..00000000 --- a/query/elb/elb_classic_lb_use_tls_https_listeners.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - 'arn:' || partition || ':elasticloadbalancing:' || region || ':' || account_id || ':loadbalancer/' || title as resource, - case - when listener_description -> 'Listener' ->> 'Protocol' in ('HTTPS', 'SSL', 'TLS') then 'ok' - else 'alarm' - end as status, - case - when listener_description -> 'Listener' ->> 'Protocol' = 'HTTPS' then title || ' configured with HTTPS protocol.' - when listener_description -> 'Listener' ->> 'Protocol' = 'SSL' then title || ' configured with TLS protocol.' - else title || ' configured with ' || (listener_description -> 'Listener' ->> 'Protocol') || ' protocol.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_classic_load_balancer, - jsonb_array_elements(listener_descriptions) as listener_description; \ No newline at end of file diff --git a/query/elb/elb_classic_lb_with_outbound_rule.sql b/query/elb/elb_classic_lb_with_outbound_rule.sql deleted file mode 100644 index 57c82927..00000000 --- a/query/elb/elb_classic_lb_with_outbound_rule.sql +++ /dev/null @@ -1,39 +0,0 @@ -with sg_with_outbound as ( - select - arn, - sg - from - aws_ec2_classic_load_balancer, - jsonb_array_elements_text(security_groups) as sg - left join aws_vpc_security_group_rule as sgr on sg = sgr.group_id - where - sgr.type = 'egress' - group by - sg, arn -), classic_lb_without_outbound as ( - select - distinct arn - from - aws_ec2_classic_load_balancer, - jsonb_array_elements_text(security_groups) as s - where - s not in ( select sg from sg_with_outbound) -) -select - distinct c.arn as resource, - case - when c.security_groups is null then 'alarm' - when o.arn is not null then 'alarm' - else 'ok' - end as status, - case - when c.security_groups is null then c.title || ' does not have security group attached.' - when o.arn is not null then c.title || ' all attached security groups does not have outbound rule(s).' - else c.title || ' all attached security groups have outbound rule(s).' - end as reason, - -- Additional Dimensions - c.region, - c.account_id -from - aws_ec2_classic_load_balancer as c - left join classic_lb_without_outbound as o on c.arn = o.arn \ No newline at end of file diff --git a/query/elb/elb_listener_use_secure_ssl_cipher.sql b/query/elb/elb_listener_use_secure_ssl_cipher.sql deleted file mode 100644 index ab2cd036..00000000 --- a/query/elb/elb_listener_use_secure_ssl_cipher.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - load_balancer_arn as resource, - case - when ssl_policy like any(array['ELBSecurityPolicy-TLS-1-2-2017-01', 'ELBSecurityPolicy-TLS-1-1-2017-01']) then 'ok' - else 'alarm' - end as status, - case - when ssl_policy like any (array['ELBSecurityPolicy-TLS-1-2-2017-01', 'ELBSecurityPolicy-TLS-1-1-2017-01']) then title || ' uses secure SSL cipher.' - else title || ' uses insecure SSL cipher.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_load_balancer_listener; diff --git a/query/elb/elb_network_lb_tls_listener_security_policy_configured.sql b/query/elb/elb_network_lb_tls_listener_security_policy_configured.sql deleted file mode 100644 index fe2c40a8..00000000 --- a/query/elb/elb_network_lb_tls_listener_security_policy_configured.sql +++ /dev/null @@ -1,40 +0,0 @@ -with tls_listeners as ( - select - distinct load_balancer_arn - from - aws_ec2_load_balancer_listener - where - protocol = 'TLS' - and ssl_policy not in ('ELBSecurityPolicy-2016-08', 'ELBSecurityPolicy-FS-2018-0', 'ELBSecurityPolicy-TLS13-1-2-Ext1-2021-06', 'ELBSecurityPolicy-TLS13-1-2-2021-06') - group by - load_balancer_arn -), nwl_without_tls_listener as ( - select - load_balancer_arn, - count(*) - from - aws_ec2_load_balancer_listener - where - protocol = 'TLS' - group by - load_balancer_arn -) -select - -- Required Columns - lb.arn as resource, - case - when l.load_balancer_arn is not null and lb.arn in (select load_balancer_arn from tls_listeners) then 'alarm' - when l.load_balancer_arn is not null then 'ok' - else 'info' - end as status, - case - when l.load_balancer_arn is not null and lb.arn in (select load_balancer_arn from tls_listeners) then lb.title || ' TLS listener security policy not updated.' - when l.load_balancer_arn is not null then lb.title || ' TLS listener security policy updated.' - else lb.title || ' does not use TLS listener.' - end as reason, - -- Additional Dimensions - lb.region, - lb.account_id -from - aws_ec2_network_load_balancer as lb - left join nwl_without_tls_listener as l on l.load_balancer_arn = lb.arn; \ No newline at end of file diff --git a/query/elb/elb_tls_listener_protocol_version.sql b/query/elb/elb_tls_listener_protocol_version.sql deleted file mode 100644 index e9b72a42..00000000 --- a/query/elb/elb_tls_listener_protocol_version.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - load_balancer_arn as resource, - case - when protocol <> 'HTTPS' then 'skip' - when protocol = 'HTTPS' and ssl_policy like any(array['Protocol-SSLv3', 'Protocol-TLSv1']) then 'alarm' - else 'ok' - end as status, - case - when protocol <> 'HTTPS' then title || ' uses protocol ' || protocol || '.' - when ssl_policy like any (array['Protocol-SSLv3', 'Protocol-TLSv1']) then title || ' uses insecure SSL or TLS cipher.' - else title || ' uses secure SSL or TLS cipher.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_ec2_load_balancer_listener; \ No newline at end of file diff --git a/query/emr/emr_account_public_access_blocked.sql b/query/emr/emr_account_public_access_blocked.sql deleted file mode 100644 index 0fa660de..00000000 --- a/query/emr/emr_account_public_access_blocked.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - 'arn:' || partition || '::' || region || ':' || account_id as resource, - case - when block_public_security_group_rules then 'ok' - else 'alarm' - end as status, - case - when block_public_security_group_rules then region || ' EMR block public access enabled.' - else region || ' EMR block public access disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_emr_block_public_access_configuration; \ No newline at end of file diff --git a/query/emr/emr_cluster_kerberos_enabled.sql b/query/emr/emr_cluster_kerberos_enabled.sql deleted file mode 100644 index 3a33babe..00000000 --- a/query/emr/emr_cluster_kerberos_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - cluster_arn as resource, - case - when kerberos_attributes is null then 'alarm' - else 'ok' - end as status, - case - when kerberos_attributes is null then title || ' Kerberos not enabled.' - else title || ' Kerberos enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_emr_cluster; \ No newline at end of file diff --git a/query/emr/emr_cluster_master_nodes_no_public_ip.sql b/query/emr/emr_cluster_master_nodes_no_public_ip.sql deleted file mode 100644 index e4e7bc21..00000000 --- a/query/emr/emr_cluster_master_nodes_no_public_ip.sql +++ /dev/null @@ -1,19 +0,0 @@ -select - -- Required Columns - c.cluster_arn as resource, - case - when c.status ->> 'State' not in ('RUNNING', 'WAITING') then 'skip' - when s.map_public_ip_on_launch then 'alarm' - else 'ok' - end as status, - case - when c.status ->> 'State' not in ('RUNNING', 'WAITING') then c.title || ' is in ' || (c.status ->> 'State') || ' state.' - when s.map_public_ip_on_launch then c.title || ' master nodes assigned with public IP.' - else c.title || ' master nodes not assigned with public IP.' - end as reason, - -- Additional Dimensions - c.region, - c.account_id -from - aws_emr_cluster as c - left join aws_vpc_subnet as s on c.ec2_instance_attributes ->> 'Ec2SubnetId' = s.subnet_id; diff --git a/query/es/es_domain_audit_logging_enabled.sql b/query/es/es_domain_audit_logging_enabled.sql deleted file mode 100644 index 049aba42..00000000 --- a/query/es/es_domain_audit_logging_enabled.sql +++ /dev/null @@ -1,20 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when - log_publishing_options -> 'AUDIT_LOGS' -> 'Enabled' = 'true' - and log_publishing_options -> 'AUDIT_LOGS' -> 'CloudWatchLogsLogGroupArn' is not null then 'ok' - else 'alarm' - end as status, - case - when - log_publishing_options -> 'AUDIT_LOGS' -> 'Enabled' = 'true' - and log_publishing_options -> 'AUDIT_LOGS' -> 'CloudWatchLogsLogGroupArn' is not null then title || ' audit logging enabled.' - else title || ' audit logging disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_elasticsearch_domain; \ No newline at end of file diff --git a/query/es/es_domain_cognito_authentication_enabled.sql b/query/es/es_domain_cognito_authentication_enabled.sql deleted file mode 100644 index e444919b..00000000 --- a/query/es/es_domain_cognito_authentication_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when cognito_options ->> 'Enabled' = 'true' then 'ok' - else 'alarm' - end as status, - case - when cognito_options ->> 'Enabled' = 'true' then title || ' Amazon Cognito authentication for Kibana enabled.' - else title || ' Amazon Cognito authentication for Kibana disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_elasticsearch_domain; \ No newline at end of file diff --git a/query/es/es_domain_data_nodes_min_3.sql b/query/es/es_domain_data_nodes_min_3.sql deleted file mode 100644 index 0fb733c8..00000000 --- a/query/es/es_domain_data_nodes_min_3.sql +++ /dev/null @@ -1,19 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when elasticsearch_cluster_config ->> 'ZoneAwarenessEnabled' = 'false' then 'alarm' - when - elasticsearch_cluster_config ->> 'ZoneAwarenessEnabled' = 'true' - and (elasticsearch_cluster_config ->> 'InstanceCount')::integer >= 3 then 'ok' - else 'alarm' - end status, - case - when elasticsearch_cluster_config ->> 'ZoneAwarenessEnabled' = 'false' then title || ' zone awareness disabled.' - else title || ' has ' || (elasticsearch_cluster_config ->> 'InstanceCount') || ' data node(s).' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_elasticsearch_domain; \ No newline at end of file diff --git a/query/es/es_domain_dedicated_master_nodes_min_3.sql b/query/es/es_domain_dedicated_master_nodes_min_3.sql deleted file mode 100644 index 77f57934..00000000 --- a/query/es/es_domain_dedicated_master_nodes_min_3.sql +++ /dev/null @@ -1,19 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when elasticsearch_cluster_config ->> 'DedicatedMasterEnabled' = 'false' then 'alarm' - when - elasticsearch_cluster_config ->> 'DedicatedMasterEnabled' = 'true' - and (elasticsearch_cluster_config ->> 'DedicatedMasterCount')::integer >= 3 then 'ok' - else 'alarm' - end status, - case - when elasticsearch_cluster_config ->> 'DedicatedMasterEnabled' = 'false' then title || ' dedicated master nodes disabled.' - else title || ' has ' || (elasticsearch_cluster_config ->> 'DedicatedMasterCount') || ' dedicated master node(s).' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_elasticsearch_domain; \ No newline at end of file diff --git a/query/es/es_domain_encrypted_using_tls_1_2.sql b/query/es/es_domain_encrypted_using_tls_1_2.sql deleted file mode 100644 index ca41ecf5..00000000 --- a/query/es/es_domain_encrypted_using_tls_1_2.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when domain_endpoint_options ->> 'TLSSecurityPolicy' = 'Policy-Min-TLS-1-2-2019-07' then 'ok' - else 'alarm' - end status, - case - when domain_endpoint_options ->> 'TLSSecurityPolicy' = 'Policy-Min-TLS-1-2-2019-07' then title || ' encrypted using TLS 1.2.' - else title || ' not encrypted using TLS 1.2.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_elasticsearch_domain; \ No newline at end of file diff --git a/query/es/es_domain_encryption_at_rest_enabled.sql b/query/es/es_domain_encryption_at_rest_enabled.sql deleted file mode 100644 index d91daf6c..00000000 --- a/query/es/es_domain_encryption_at_rest_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when encryption_at_rest_options ->> 'Enabled' = 'false' then 'alarm' - else 'ok' - end status, - case - when encryption_at_rest_options ->> 'Enabled' = 'false' then title || ' encryption at rest not enabled.' - else title || ' encryption at rest enabled.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_elasticsearch_domain; diff --git a/query/es/es_domain_error_logging_enabled.sql b/query/es/es_domain_error_logging_enabled.sql deleted file mode 100644 index 306f460e..00000000 --- a/query/es/es_domain_error_logging_enabled.sql +++ /dev/null @@ -1,20 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when - log_publishing_options -> 'ES_APPLICATION_LOGS' -> 'Enabled' = 'true' - and log_publishing_options -> 'ES_APPLICATION_LOGS' -> 'CloudWatchLogsLogGroupArn' is not null then 'ok' - else 'alarm' - end as status, - case - when - log_publishing_options -> 'ES_APPLICATION_LOGS' -> 'Enabled' = 'true' - and log_publishing_options -> 'ES_APPLICATION_LOGS' -> 'CloudWatchLogsLogGroupArn' is not null then title || ' error logging enabled.' - else title || ' error logging disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_elasticsearch_domain; \ No newline at end of file diff --git a/query/es/es_domain_in_vpc.sql b/query/es/es_domain_in_vpc.sql deleted file mode 100644 index edcf3444..00000000 --- a/query/es/es_domain_in_vpc.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when vpc_options ->> 'VPCId' is null then 'alarm' - else 'ok' - end status, - case - when vpc_options ->> 'VPCId' is null then title || ' not in VPC.' - else title || ' in VPC ' || (vpc_options ->> 'VPCId') || '.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_elasticsearch_domain; \ No newline at end of file diff --git a/query/es/es_domain_internal_user_database_enabled.sql b/query/es/es_domain_internal_user_database_enabled.sql deleted file mode 100644 index e95a5445..00000000 --- a/query/es/es_domain_internal_user_database_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when advanced_security_options ->> 'InternalUserDatabaseEnabled' = 'true' then 'ok' - else 'alarm' - end as status, - case - when advanced_security_options ->> 'InternalUserDatabaseEnabled' = 'true' then title || ' internal user database enabled.' - else title || ' internal user database disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_elasticsearch_domain; \ No newline at end of file diff --git a/query/es/es_domain_logs_to_cloudwatch.sql b/query/es/es_domain_logs_to_cloudwatch.sql deleted file mode 100644 index 1b7d2e93..00000000 --- a/query/es/es_domain_logs_to_cloudwatch.sql +++ /dev/null @@ -1,39 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when - ( log_publishing_options -> 'ES_APPLICATION_LOGS' -> 'Enabled' = 'true' - and log_publishing_options -> 'ES_APPLICATION_LOGS' -> 'CloudWatchLogsLogGroupArn' is not null - ) - and - ( log_publishing_options -> 'SEARCH_SLOW_LOGS' -> 'Enabled' = 'true' - and log_publishing_options -> 'SEARCH_SLOW_LOGS' -> 'CloudWatchLogsLogGroupArn' is not null - ) - and - ( log_publishing_options -> 'INDEX_SLOW_LOGS' -> 'Enabled' = 'true' - and log_publishing_options -> 'INDEX_SLOW_LOGS' -> 'CloudWatchLogsLogGroupArn' is not null - ) - then 'ok' - else 'alarm' - end as status, - case - when - ( log_publishing_options -> 'ES_APPLICATION_LOGS' -> 'Enabled' = 'true' - and log_publishing_options -> 'ES_APPLICATION_LOGS' -> 'CloudWatchLogsLogGroupArn' is not null - ) - and - ( log_publishing_options -> 'SEARCH_SLOW_LOGS' -> 'Enabled' = 'true' - and log_publishing_options -> 'SEARCH_SLOW_LOGS' -> 'CloudWatchLogsLogGroupArn' is not null - ) - and - ( log_publishing_options -> 'INDEX_SLOW_LOGS' -> 'Enabled' = 'true' - and log_publishing_options -> 'INDEX_SLOW_LOGS' -> 'CloudWatchLogsLogGroupArn' is not null - ) then title || ' logging enabled for search , index and error.' - else title || ' logging not enabled for all search, index and error.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_elasticsearch_domain; \ No newline at end of file diff --git a/query/es/es_domain_node_to_node_encryption_enabled.sql b/query/es/es_domain_node_to_node_encryption_enabled.sql deleted file mode 100644 index b9fbe73c..00000000 --- a/query/es/es_domain_node_to_node_encryption_enabled.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when region = any(array['af-south-1', 'eu-south-1', 'cn-north-1', 'cn-northwest-1']) then 'skip' - when not enabled then 'alarm' - else 'ok' - end as status, - case - when region = any(array['af-south-1', 'eu-south-1', 'cn-north-1', 'cn-northwest-1']) then title || ' node-to-node encryption not supported in ' || region || '.' - when not enabled then title || ' node-to-node encryption disabled.' - else title || ' node-to-node encryption enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_elasticsearch_domain; diff --git a/query/fsx/fsx_file_system_protected_by_backup_plan.sql b/query/fsx/fsx_file_system_protected_by_backup_plan.sql deleted file mode 100644 index 3d276247..00000000 --- a/query/fsx/fsx_file_system_protected_by_backup_plan.sql +++ /dev/null @@ -1,25 +0,0 @@ -with backup_protected_fsx_file_system as ( - select - resource_arn as arn - from - aws_backup_protected_resource as b - where - resource_type = 'FSx' -) -select - -- Required Columns - f.arn as resource, - case - when b.arn is not null then 'ok' - else 'alarm' - end as status, - case - when b.arn is not null then f.title || ' is protected by backup plan.' - else f.title || ' is not protected by backup plan.' - end as reason, - -- Additional Dimensions - f.region, - f.account_id -from - aws_fsx_file_system as f - left join backup_protected_fsx_file_system as b on f.arn = b.arn; diff --git a/query/glue/glue_data_catalog_encryption_settings_metadata_encryption_enabled.sql b/query/glue/glue_data_catalog_encryption_settings_metadata_encryption_enabled.sql deleted file mode 100644 index 12962070..00000000 --- a/query/glue/glue_data_catalog_encryption_settings_metadata_encryption_enabled.sql +++ /dev/null @@ -1,15 +0,0 @@ -select - -- Required Columns - case - when encryption_at_rest is not null and encryption_at_rest ->> 'CatalogEncryptionMode' != 'DISABLED' then 'ok' - else 'alarm' - end as status, - case - when encryption_at_rest is not null and encryption_at_rest ->> 'CatalogEncryptionMode' != 'DISABLED' then 'Glue data catalog metadata encryption is enabled in ' || region || '.' - else 'Glue data catalog metadata encryption is disabled in ' || region || '.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_glue_data_catalog_encryption_settings; \ No newline at end of file diff --git a/query/glue/glue_data_catalog_encryption_settings_password_encryption_enabled.sql b/query/glue/glue_data_catalog_encryption_settings_password_encryption_enabled.sql deleted file mode 100644 index d442b785..00000000 --- a/query/glue/glue_data_catalog_encryption_settings_password_encryption_enabled.sql +++ /dev/null @@ -1,15 +0,0 @@ -select - -- Required Columns - case - when connection_password_encryption is not null and connection_password_encryption ->> 'ReturnConnectionPasswordEncrypted' != 'false' then 'ok' - else 'alarm' - end as status, - case - when connection_password_encryption is not null and connection_password_encryption ->> 'ReturnConnectionPasswordEncrypted' != 'false' then 'Glue data catalog connection password encryption enabled in ' || region || '.' - else 'Glue data catalog connection password encryption disabled in ' || region || '.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_glue_data_catalog_encryption_settings; \ No newline at end of file diff --git a/query/glue/glue_dev_endpoint_cloudwatch_logs_encryption_enabled.sql b/query/glue/glue_dev_endpoint_cloudwatch_logs_encryption_enabled.sql deleted file mode 100644 index 3db5e63c..00000000 --- a/query/glue/glue_dev_endpoint_cloudwatch_logs_encryption_enabled.sql +++ /dev/null @@ -1,17 +0,0 @@ -select - -- Required Columns - e.arn as resource, - case - when cloud_watch_encryption is not null and cloud_watch_encryption ->> 'CloudWatchEncryptionMode' != 'DISABLED' then 'ok' - else 'alarm' - end as status, - case - when cloud_watch_encryption is not null and cloud_watch_encryption ->> 'CloudWatchEncryptionMode' != 'DISABLED' then e.title || ' CloudWatch logs encryption enabled.' - else e.title || ' CloudWatch logs encryption disabled.' - end as reason, - -- Additional Dimensions - e.region, - e.account_id -from - aws_glue_dev_endpoint as e - left join aws_glue_security_configuration as c on e.security_configuration = c.name; \ No newline at end of file diff --git a/query/glue/glue_dev_endpoint_job_bookmark_encryption_enabled.sql b/query/glue/glue_dev_endpoint_job_bookmark_encryption_enabled.sql deleted file mode 100644 index e0a4e8a8..00000000 --- a/query/glue/glue_dev_endpoint_job_bookmark_encryption_enabled.sql +++ /dev/null @@ -1,17 +0,0 @@ -select - -- Required Columns - e.arn as resource, - case - when job_bookmarks_encryption is not null and job_bookmarks_encryption ->> 'JobBookmarksEncryptionMode' != 'DISABLED' then 'ok' - else 'alarm' - end as status, - case - when job_bookmarks_encryption is not null and job_bookmarks_encryption ->> 'JobBookmarksEncryptionMode' != 'DISABLED' then e.title || ' job bookmark encryption enabled.' - else e.title || ' job bookmark encryption disabled.' - end as reason, - -- Additional Dimensions - e.region, - e.account_id -from - aws_glue_dev_endpoint as e - left join aws_glue_security_configuration as c on e.security_configuration = c.name; \ No newline at end of file diff --git a/query/glue/glue_dev_endpoint_s3_encryption_enabled.sql b/query/glue/glue_dev_endpoint_s3_encryption_enabled.sql deleted file mode 100644 index 51f2c1d9..00000000 --- a/query/glue/glue_dev_endpoint_s3_encryption_enabled.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - d.arn as resource, - case - when e is not null and e ->> 'S3EncryptionMode' != 'DISABLED' then 'ok' - else 'alarm' - end as status, - case - when e is not null and e ->> 'S3EncryptionMode' != 'DISABLED' then d.title || ' S3 encryption enabled.' - else d.title || ' S3 encryption disabled.' - end as reason, - -- Additional Dimensions - d.region, - d.account_id -from - aws_glue_dev_endpoint as d - left join aws_glue_security_configuration s on d.security_configuration = s.name, - jsonb_array_elements(s.s3_encryption) e; \ No newline at end of file diff --git a/query/glue/glue_job_bookmarks_encryption_enabled.sql b/query/glue/glue_job_bookmarks_encryption_enabled.sql deleted file mode 100644 index 8c1750f2..00000000 --- a/query/glue/glue_job_bookmarks_encryption_enabled.sql +++ /dev/null @@ -1,17 +0,0 @@ -select - -- Required Columns - j.arn as resource, - case - when job_bookmarks_encryption is not null and job_bookmarks_encryption ->> 'JobBookmarksEncryptionMode' != 'DISABLED' then 'ok' - else 'alarm' - end as status, - case - when job_bookmarks_encryption is not null and job_bookmarks_encryption ->> 'JobBookmarksEncryptionMode' != 'DISABLED' then j.title || ' job bookmarks encryption enabled.' - else j.title || ' job bookmarks encryption disabled.' - end as reason, - -- Additional Dimensions - j.region, - j.account_id -from - aws_glue_job as j - left join aws_glue_security_configuration as c on j.security_configuration = c.name; \ No newline at end of file diff --git a/query/glue/glue_job_cloudwatch_logs_encryption_enabled.sql b/query/glue/glue_job_cloudwatch_logs_encryption_enabled.sql deleted file mode 100644 index 4e2b3f44..00000000 --- a/query/glue/glue_job_cloudwatch_logs_encryption_enabled.sql +++ /dev/null @@ -1,17 +0,0 @@ -select - -- Required Columns - j.arn as resource, - case - when cloud_watch_encryption is not null and cloud_watch_encryption ->> 'CloudWatchEncryptionMode' != 'DISABLED' then 'ok' - else 'alarm' - end as status, - case - when cloud_watch_encryption is not null and cloud_watch_encryption ->> 'CloudWatchEncryptionMode' != 'DISABLED' then j.title || ' CloudWatch logs encryption enabled.' - else j.title || ' CloudWatch logs encryption disabled.' - end as reason, - -- Additional Dimensions - j.region, - j.account_id -from - aws_glue_job as j - left join aws_glue_security_configuration as c on j.security_configuration = c.name; \ No newline at end of file diff --git a/query/glue/glue_job_s3_encryption_enabled.sql b/query/glue/glue_job_s3_encryption_enabled.sql deleted file mode 100644 index f7165bb5..00000000 --- a/query/glue/glue_job_s3_encryption_enabled.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - j.arn as resource, - case - when e is not null and e ->> 'S3EncryptionMode' != 'DISABLED' then 'ok' - else 'alarm' - end as status, - case - when e is not null and e ->> 'S3EncryptionMode' != 'DISABLED' then j.title || ' S3 encryption enabled.' - else j.title || ' S3 encryption disabled.' - end as reason, - -- Additional Dimensions - j.region, - j.account_id -from - aws_glue_job as j - left join aws_glue_security_configuration as s on j.security_configuration = s.name, - jsonb_array_elements(s.s3_encryption) e; \ No newline at end of file diff --git a/query/guardduty/guardduty_enabled.sql b/query/guardduty/guardduty_enabled.sql deleted file mode 100644 index 3ced03ff..00000000 --- a/query/guardduty/guardduty_enabled.sql +++ /dev/null @@ -1,25 +0,0 @@ -select - -- Required Columns - 'arn:' || r.partition || '::' || r.region || ':' || r.account_id as resource, - case - when r.region = any(array['af-south-1', 'ap-northeast-3', 'ap-southeast-3', 'eu-south-1', 'cn-north-1', 'cn-northwest-1', 'me-south-1', 'us-gov-east-1']) then 'skip' - -- Skip any regions that are disabled in the account. - when r.opt_in_status = 'not-opted-in' then 'skip' - when status = 'ENABLED' and master_account ->> 'AccountId' is null then 'ok' - when status = 'ENABLED' and master_account ->> 'AccountId' is not null then 'info' - else 'alarm' - end as status, - case - when r.region = any(array['af-south-1', 'ap-northeast-3', 'ap-southeast-3', 'eu-south-1', 'cn-north-1', 'cn-northwest-1', 'me-south-1', 'us-gov-east-1']) then r.region || ' region not supported.' - when r.opt_in_status = 'not-opted-in' then r.region || ' region is disabled.' - when status is null then 'No GuardDuty detector found in ' || r.region || '.' - when status = 'ENABLED' and master_account ->> 'AccountId' is null then r.region || ' detector ' || d.title || ' enabled.' - when status = 'ENABLED' and master_account ->> 'AccountId' is not null then r.region || ' detector ' || d.title || ' is managed by account ' || (master_account ->> 'AccountId') || ' via delegated admin.' - else r.region || ' detector ' || d.title || ' disabled.' - end as reason, - -- Additional Dimensions - r.region, - r.account_id -from - aws_region as r - left join aws_guardduty_detector d on r.account_id = d.account_id and r.name = d.region; diff --git a/query/guardduty/guardduty_finding_archived.sql b/query/guardduty/guardduty_finding_archived.sql deleted file mode 100644 index 9643d0b2..00000000 --- a/query/guardduty/guardduty_finding_archived.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when service ->> 'Archived' = 'false' then 'alarm' - else 'ok' - end as status, - case - when service ->> 'Archived' = 'false' then title || ' not archived.' - else title || ' archived.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_guardduty_finding; diff --git a/query/iam/account_part_of_organizations.sql b/query/iam/account_part_of_organizations.sql deleted file mode 100644 index 8ce857e5..00000000 --- a/query/iam/account_part_of_organizations.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when organization_id is not null then 'ok' - else 'alarm' - end as status, - case - when organization_id is not null then title || ' is part of organization(s).' - else title || ' is not part of organization.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_account; \ No newline at end of file diff --git a/query/iam/iam_access_analyzer_enabled.sql b/query/iam/iam_access_analyzer_enabled.sql deleted file mode 100644 index 68c10793..00000000 --- a/query/iam/iam_access_analyzer_enabled.sql +++ /dev/null @@ -1,20 +0,0 @@ -select - -- Required Columns - 'arn:' || r.partition || '::' || r.region || ':' || r.account_id as resource, - case - -- Skip any regions that are disabled in the account. - when r.opt_in_status = 'not-opted-in' then 'skip' - when aa.arn is not null then 'ok' - else 'alarm' - end as status, - case - when r.opt_in_status = 'not-opted-in' then r.region || ' region is disabled.' - when aa.arn is not null then aa.name || ' enabled in ' || r.region || '.' - else 'Access Analyzer not enabled in ' || r.region || '.' - end as reason, - -- Additional Dimensions - r.region, - r.account_id -from - aws_region as r - left join aws_accessanalyzer_analyzer as aa on r.account_id = aa.account_id and r.region = aa.region; diff --git a/query/iam/iam_account_password_policy_expire_90.sql b/query/iam/iam_account_password_policy_expire_90.sql deleted file mode 100644 index afacc2aa..00000000 --- a/query/iam/iam_account_password_policy_expire_90.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when max_password_age <= 90 then 'ok' - else 'alarm' - end as status, - case - when max_password_age is null then 'Password expiration not set.' - else 'Password expiration set to ' || max_password_age || ' days.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join aws_iam_account_password_policy as pol on a.account_id = pol.account_id; \ No newline at end of file diff --git a/query/iam/iam_account_password_policy_min_length_14.sql b/query/iam/iam_account_password_policy_min_length_14.sql deleted file mode 100644 index 33ebf72f..00000000 --- a/query/iam/iam_account_password_policy_min_length_14.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when minimum_password_length >= 14 then 'ok' - else 'alarm' - end as status, - case - when minimum_password_length is null then 'No password policy set.' - else 'Minimum password length set to ' || minimum_password_length || '.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join aws_iam_account_password_policy as pol on a.account_id = pol.account_id; diff --git a/query/iam/iam_account_password_policy_one_lowercase_letter.sql b/query/iam/iam_account_password_policy_one_lowercase_letter.sql deleted file mode 100644 index 4c4a6592..00000000 --- a/query/iam/iam_account_password_policy_one_lowercase_letter.sql +++ /dev/null @@ -1,17 +0,0 @@ -select - -- Required Columns - 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when require_lowercase_characters then 'ok' - else 'alarm' - end as status, - case - when minimum_password_length is null then 'No password policy set.' - when require_lowercase_characters then 'Lowercase character required.' - else 'Lowercase character not required.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join aws_iam_account_password_policy as pol on a.account_id = pol.account_id; \ No newline at end of file diff --git a/query/iam/iam_account_password_policy_one_number.sql b/query/iam/iam_account_password_policy_one_number.sql deleted file mode 100644 index a2bcad4d..00000000 --- a/query/iam/iam_account_password_policy_one_number.sql +++ /dev/null @@ -1,17 +0,0 @@ -select - -- Required Columns - 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when require_numbers then 'ok' - else 'alarm' - end as status, - case - when minimum_password_length is null then 'No password policy set.' - when require_numbers then 'Number required.' - else 'Number not required.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join aws_iam_account_password_policy as pol on a.account_id = pol.account_id; \ No newline at end of file diff --git a/query/iam/iam_account_password_policy_one_symbol.sql b/query/iam/iam_account_password_policy_one_symbol.sql deleted file mode 100644 index 89923dbc..00000000 --- a/query/iam/iam_account_password_policy_one_symbol.sql +++ /dev/null @@ -1,17 +0,0 @@ -select - -- Required Columns - 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when require_symbols then 'ok' - else 'alarm' - end as status, - case - when minimum_password_length is null then 'No password policy set.' - when require_symbols then 'Symbol required.' - else 'Symbol not required.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join aws_iam_account_password_policy as pol on a.account_id = pol.account_id; \ No newline at end of file diff --git a/query/iam/iam_account_password_policy_one_uppercase_letter.sql b/query/iam/iam_account_password_policy_one_uppercase_letter.sql deleted file mode 100644 index 037104e2..00000000 --- a/query/iam/iam_account_password_policy_one_uppercase_letter.sql +++ /dev/null @@ -1,17 +0,0 @@ -select - -- Required Columns - 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when require_uppercase_characters then 'ok' - else 'alarm' - end as status, - case - when minimum_password_length is null then 'No password policy set.' - when require_uppercase_characters then 'Uppercase character required.' - else 'Uppercase character not required.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join aws_iam_account_password_policy as pol on a.account_id = pol.account_id; \ No newline at end of file diff --git a/query/iam/iam_account_password_policy_reuse_24.sql b/query/iam/iam_account_password_policy_reuse_24.sql deleted file mode 100644 index 19f47acb..00000000 --- a/query/iam/iam_account_password_policy_reuse_24.sql +++ /dev/null @@ -1,17 +0,0 @@ -select - -- Required Columns - 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when password_reuse_prevention >= 24 then 'ok' - else 'alarm' - end as status, - case - when minimum_password_length is null then 'No password policy set.' - when password_reuse_prevention is null then 'Password reuse prevention not set.' - else 'Password reuse prevention set to ' || password_reuse_prevention || '.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join aws_iam_account_password_policy as pol on a.account_id = pol.account_id; diff --git a/query/iam/iam_account_password_policy_strong.sql b/query/iam/iam_account_password_policy_strong.sql deleted file mode 100644 index 0244f1b1..00000000 --- a/query/iam/iam_account_password_policy_strong.sql +++ /dev/null @@ -1,31 +0,0 @@ -select - -- Required Columns - 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when - minimum_password_length >= 14 - and password_reuse_prevention >= 5 - and require_lowercase_characters = 'true' - and require_uppercase_characters = 'true' - and require_numbers = 'true' - and max_password_age <= 90 - then 'ok' - else 'alarm' - end status, - case - when minimum_password_length is null then 'No password policy set.' - when - minimum_password_length >= 14 - and password_reuse_prevention >= 5 - and require_lowercase_characters = 'true' - and require_uppercase_characters = 'true' - and require_numbers = 'true' - and max_password_age <= 90 - then 'Strong password policies configured.' - else 'Strong password policies not configured.' - end reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join aws_iam_account_password_policy as pol on a.account_id = pol.account_id; diff --git a/query/iam/iam_account_password_policy_strong_min_length_8.sql b/query/iam/iam_account_password_policy_strong_min_length_8.sql deleted file mode 100644 index 1eb707dc..00000000 --- a/query/iam/iam_account_password_policy_strong_min_length_8.sql +++ /dev/null @@ -1,29 +0,0 @@ -select - -- Required Columns - 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when - minimum_password_length >= 8 - and require_lowercase_characters = 'true' - and require_uppercase_characters = 'true' - and require_numbers = 'true' - and require_symbols = 'true' - then 'ok' - else 'alarm' - end as status, - case - when minimum_password_length is null then 'No password policy set.' - when - minimum_password_length >= 8 - and require_lowercase_characters = 'true' - and require_uppercase_characters = 'true' - and require_numbers = 'true' - and require_symbols = 'true' - then 'Strong password policies configured.' - else 'Strong password policies not configured.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join aws_iam_account_password_policy as pol on a.account_id = pol.account_id; diff --git a/query/iam/iam_account_password_policy_strong_min_reuse_24.sql b/query/iam/iam_account_password_policy_strong_min_reuse_24.sql deleted file mode 100644 index bc771248..00000000 --- a/query/iam/iam_account_password_policy_strong_min_reuse_24.sql +++ /dev/null @@ -1,33 +0,0 @@ -select - -- Required Columns - 'arn:' || a.partition || ':::' || a.account_id as resource, - case - when - minimum_password_length >= 14 - and password_reuse_prevention >= 24 - and require_lowercase_characters = 'true' - and require_uppercase_characters = 'true' - and require_numbers = 'true' - and require_symbols = 'true' - and max_password_age <= 90 - then 'ok' - else 'alarm' - end as status, - case - when minimum_password_length is null then 'No password policy set.' - when - minimum_password_length >= 14 - and password_reuse_prevention >= 24 - and require_lowercase_characters = 'true' - and require_uppercase_characters = 'true' - and require_numbers = 'true' - and require_symbols = 'true' - and max_password_age <= 90 - then 'Strong password policies configured.' - else 'Strong password policies not configured.' - end as reason, - -- Additional Dimensions - a.account_id -from - aws_account as a - left join aws_iam_account_password_policy as pol on a.account_id = pol.account_id; diff --git a/query/iam/iam_group_not_empty.sql b/query/iam/iam_group_not_empty.sql deleted file mode 100644 index 7390007e..00000000 --- a/query/iam/iam_group_not_empty.sql +++ /dev/null @@ -1,15 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when users is null then 'alarm' - else 'ok' - end as status, - case - when users is null then title || ' not associated with any IAM user.' - else title || ' associated with IAM user.' - end as reason, - -- Additional Dimensions - account_id -from - aws_iam_group; \ No newline at end of file diff --git a/query/iam/iam_group_user_role_no_inline_policies.sql b/query/iam/iam_group_user_role_no_inline_policies.sql deleted file mode 100644 index f388aaca..00000000 --- a/query/iam/iam_group_user_role_no_inline_policies.sql +++ /dev/null @@ -1,40 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when inline_policies is null then 'ok' - else 'alarm' - end status, - 'User ' || title || ' has ' || coalesce(jsonb_array_length(inline_policies), 0) || ' inline policies.' as reason, - -- Additional Dimensions - account_id -from - aws_iam_user -union -select - -- Required Columns - arn as resource, - case - when inline_policies is null then 'ok' - else 'alarm' - end status, - 'Role ' || title || ' has ' || coalesce(jsonb_array_length(inline_policies), 0) || ' inline policies.' as reason, - -- Additional Dimensions - account_id -from - aws_iam_role -where - arn not like '%service-role/%' -union -select - -- Required Columns - arn as resource, - case - when inline_policies is null then 'ok' - else 'alarm' - end status, - 'Group ' || title || ' has ' || coalesce(jsonb_array_length(inline_policies), 0) || ' inline policies.' as reason, - -- Additional Dimensions - account_id -from - aws_iam_group; \ No newline at end of file diff --git a/query/iam/iam_managed_policy_attached_to_role.sql b/query/iam/iam_managed_policy_attached_to_role.sql deleted file mode 100644 index aea7ec93..00000000 --- a/query/iam/iam_managed_policy_attached_to_role.sql +++ /dev/null @@ -1,23 +0,0 @@ -with role_attached_policies as ( - select - jsonb_array_elements_text(attached_policy_arns) as policy_arn - from - aws_iam_role -) -select - -- Required Columns - p.arn as resource, - case - when p.arn in (select policy_arn from role_attached_policies) then 'ok' - else 'alarm' - end as status, - case - when p.arn in (select policy_arn from role_attached_policies) then title || ' attached to IAM role.' - else title || ' not attached to IAM role.' - end as reason, - -- Additional Dimensions - p.account_id -from - aws_iam_policy as p -where - is_aws_managed; \ No newline at end of file diff --git a/query/iam/iam_policy_all_attached_no_star_star.sql b/query/iam/iam_policy_all_attached_no_star_star.sql deleted file mode 100644 index 29f27662..00000000 --- a/query/iam/iam_policy_all_attached_no_star_star.sql +++ /dev/null @@ -1,36 +0,0 @@ -with star_access_policies as ( - select - arn, - count(*) as num_bad_statements - from - aws_iam_policy, - jsonb_array_elements(policy_std -> 'Statement') as s, - jsonb_array_elements_text(s -> 'Resource') as resource, - jsonb_array_elements_text(s -> 'Action') as action - where - s ->> 'Effect' = 'Allow' - and resource = '*' - and ( - (action = '*' - or action = '*:*' - ) - ) - and is_attached - group by arn - ) -select - -- Required Columns - p.arn as resource, - case - when s.arn is null then 'ok' - else 'alarm' - end status, - p.name || ' contains ' || coalesce(s.num_bad_statements,0) || - ' statements that allow action "*" on resource "*".' as reason, - -- Additional Dimensions - p.account_id -from - aws_iam_policy as p - left join star_access_policies as s on p.arn = s.arn -where - p.is_attached; diff --git a/query/iam/iam_policy_custom_attached_no_star_star.sql b/query/iam/iam_policy_custom_attached_no_star_star.sql deleted file mode 100644 index c904431e..00000000 --- a/query/iam/iam_policy_custom_attached_no_star_star.sql +++ /dev/null @@ -1,38 +0,0 @@ --- This query checks the customer managed policies having * access and attached to IAM resource(s) -with star_access_policies as ( - select - arn, - count(*) as num_bad_statements - from - aws_iam_policy, - jsonb_array_elements(policy_std -> 'Statement') as s, - jsonb_array_elements_text(s -> 'Resource') as resource, - jsonb_array_elements_text(s -> 'Action') as action - where - not is_aws_managed - and s ->> 'Effect' = 'Allow' - and resource = '*' - and ( - (action = '*' - or action = '*:*' - ) - ) - and is_attached - group by arn - ) -select - -- Required Columns - p.arn as resource, - case - when s.arn is null then 'ok' - else 'alarm' - end status, - p.name || ' contains ' || coalesce(s.num_bad_statements,0) || - ' statements that allow action "*" on resource "*".' as reason, - -- Additional Dimensions - p.account_id -from - aws_iam_policy as p - left join star_access_policies as s on p.arn = s.arn -where - not p.is_aws_managed; diff --git a/query/iam/iam_policy_custom_no_assume_role.sql b/query/iam/iam_policy_custom_no_assume_role.sql deleted file mode 100644 index 22d001e0..00000000 --- a/query/iam/iam_policy_custom_no_assume_role.sql +++ /dev/null @@ -1,30 +0,0 @@ -with filter_users as ( - select - user_id, - name, - policies - from - aws_iam_user, - jsonb_array_elements_text(inline_policies) as policies - where - policies like '%AssumeRole%' -) -select - -- Required Columns - u.arn as resource, - case - when fu.user_id is not null then 'alarm' - else 'ok' - end as status, - case - when fu.user_id is not null then u.name || ' custom policies allow STS Role assumption.' - else u.name || ' custom policies does not allow STS Role assumption.' - end as reason, - -- Additional Dimensions - u.region, - u.account_id -from - aws_iam_user as u - left join filter_users as fu on u.user_id = fu.user_id -order by - u.name; \ No newline at end of file diff --git a/query/iam/iam_policy_custom_no_blocked_kms_actions.sql b/query/iam/iam_policy_custom_no_blocked_kms_actions.sql deleted file mode 100644 index 908cea2f..00000000 --- a/query/iam/iam_policy_custom_no_blocked_kms_actions.sql +++ /dev/null @@ -1,32 +0,0 @@ -with kms_blocked_actions as ( - select - arn, - count(*) as statements_num - from - aws_iam_policy, - jsonb_array_elements(policy_std -> 'Statement') as s, - jsonb_array_elements_text(s -> 'Resource') as resource, - jsonb_array_elements_text(s -> 'Action') as action - where - not is_aws_managed - and s ->> 'Effect' = 'Allow' - and action like any(array['kms:decrypt', 'kms:reencryptfrom']) - group by - arn -) -select - -- Required Columns - p.arn as resource, - case - when w.arn is null then 'ok' - else 'alarm' - end status, - p.name || ' contains ' || coalesce(w.statements_num,0) || - ' statements that allow blocked actions on AWS KMS keys.' as reason, - -- Additional Dimensions - p.account_id -from - aws_iam_policy as p - left join kms_blocked_actions as w on p.arn = w.arn -where - not p.is_aws_managed; diff --git a/query/iam/iam_policy_custom_no_service_wildcard.sql b/query/iam/iam_policy_custom_no_service_wildcard.sql deleted file mode 100644 index 2f00e806..00000000 --- a/query/iam/iam_policy_custom_no_service_wildcard.sql +++ /dev/null @@ -1,36 +0,0 @@ -with wildcard_action_policies as ( - select - arn, - count(*) as statements_num - from - aws_iam_policy, - jsonb_array_elements(policy_std -> 'Statement') as s, - jsonb_array_elements_text(s -> 'Resource') as resource, - jsonb_array_elements_text(s -> 'Action') as action - where - not is_aws_managed - and s ->> 'Effect' = 'Allow' - and resource = '*' - and ( - action like '%:*' - or action = '*' - ) - group by - arn -) -select - -- Required Columns - p.arn as resource, - case - when w.arn is null then 'ok' - else 'alarm' - end status, - p.name || ' contains ' || coalesce(w.statements_num,0) || - ' statements that allow action "*" on at least 1 AWS service on resource "*".' as reason, - -- Additional Dimensions - p.account_id -from - aws_iam_policy as p - left join wildcard_action_policies as w on p.arn = w.arn -where - not p.is_aws_managed; diff --git a/query/iam/iam_policy_custom_no_star_star.sql b/query/iam/iam_policy_custom_no_star_star.sql deleted file mode 100644 index ffc026af..00000000 --- a/query/iam/iam_policy_custom_no_star_star.sql +++ /dev/null @@ -1,37 +0,0 @@ -with bad_policies as ( - select - arn, - count(*) as num_bad_statements - from - aws_iam_policy, - jsonb_array_elements(policy_std -> 'Statement') as s, - jsonb_array_elements_text(s -> 'Resource') as resource, - jsonb_array_elements_text(s -> 'Action') as action - where - not is_aws_managed - and s ->> 'Effect' = 'Allow' - and resource = '*' - and ( - (action = '*' - or action = '*:*' - ) - ) - group by - arn -) -select - -- Required Columns - p.arn as resource, - case - when bad.arn is null then 'ok' - else 'alarm' - end status, - p.name || ' contains ' || coalesce(bad.num_bad_statements,0) || - ' statements that allow action "*" on resource "*".' as reason, - -- Additional Dimensions - p.account_id -from - aws_iam_policy as p - left join bad_policies as bad on p.arn = bad.arn -where - not p.is_aws_managed; diff --git a/query/iam/iam_policy_inline_no_blocked_kms_actions.sql b/query/iam/iam_policy_inline_no_blocked_kms_actions.sql deleted file mode 100644 index 4fee8e5d..00000000 --- a/query/iam/iam_policy_inline_no_blocked_kms_actions.sql +++ /dev/null @@ -1,58 +0,0 @@ -with iam_resource_types as ( - select - arn, - inline_policies_std, - name, - account_id, - region - from - aws_iam_user - union - select - arn, - inline_policies_std, - name, - account_id, - region - from - aws_iam_role - union - select - arn, - inline_policies_std, - name, - account_id, - region - from - aws_iam_group -), -kms_blocked_actions as ( - select - arn, - count(*) as statements_num - from - iam_resource_types, - jsonb_array_elements(inline_policies_std) as policy_std, - jsonb_array_elements(policy_std -> 'PolicyDocument' -> 'Statement') as s, - jsonb_array_elements_text(s -> 'Resource') as resource, - jsonb_array_elements_text(s -> 'Action') as action - where - s ->> 'Effect' = 'Allow' - and action like any(array['kms:decrypt','kms:decrypt*', 'kms:reencryptfrom', 'kms:*', 'kms:reencrypt*']) - group by - arn -) -select - -- Required Columns - u.arn as resource, - case - when w.arn is null then 'ok' - else 'alarm' - end status, - u.name || ' contains ' || coalesce(w.statements_num,0) || - ' inline policy statement(s) that allow blocked actions on AWS KMS keys.' as reason, - -- Additional Dimensions - u.account_id -from - iam_resource_types as u - left join kms_blocked_actions as w on u.arn = w.arn; diff --git a/query/iam/iam_policy_unused.sql b/query/iam/iam_policy_unused.sql deleted file mode 100644 index ebcd7a7c..00000000 --- a/query/iam/iam_policy_unused.sql +++ /dev/null @@ -1,33 +0,0 @@ -with in_use_policies as ( - select - attached_policy_arns - from - aws_iam_user - union - select - attached_policy_arns - from - aws_iam_group - where - jsonb_array_length(users) > 0 - union - select - attached_policy_arns - from - aws_iam_role -) -select - -- Required Columns - arn as resource, - case - when arn in (select jsonb_array_elements_text(attached_policy_arns) from in_use_policies) then 'ok' - else 'alarm' - end as status, - case - when arn in (select jsonb_array_elements_text(attached_policy_arns) from in_use_policies) then title || ' in use.' - else title || ' not in use.' - end as reason, - -- Additional Dimensions - account_id -from - aws_iam_policy; \ No newline at end of file diff --git a/query/iam/iam_root_last_used.sql b/query/iam/iam_root_last_used.sql deleted file mode 100644 index a3d6e4ae..00000000 --- a/query/iam/iam_root_last_used.sql +++ /dev/null @@ -1,27 +0,0 @@ -select - -- Required Columns - user_arn as resource, - case - when password_last_used >= (current_date - interval '90' day) then 'alarm' - when access_key_1_last_used_date <= (current_date - interval '90' day) then 'alarm' - when access_key_2_last_used_date <= (current_date - interval '90' day) then 'alarm' - else 'ok' - end as status, - case - when password_last_used is null then 'Root never logged in with password.' - else 'Root password used ' || to_char(password_last_used , 'DD-Mon-YYYY') || ' (' || extract(day from current_timestamp - password_last_used) || ' days).' - end || - case - when access_key_1_last_used_date is null then ' Access Key 1 never used.' - else ' Access Key 1 used ' || to_char(access_key_1_last_used_date , 'DD-Mon-YYYY') || ' (' || extract(day from current_timestamp - access_key_1_last_used_date) || ' days).' - end || - case - when access_key_2_last_used_date is null then ' Access Key 2 never used.' - else ' Access Key 2 used ' || to_char(access_key_2_last_used_date , 'DD-Mon-YYYY') || ' (' || extract(day from current_timestamp - access_key_2_last_used_date) || ' days).' - end as reason, - -- Additional Dimensions - account_id -from - aws_iam_credential_report -where - user_name = ''; \ No newline at end of file diff --git a/query/iam/iam_root_user_hardware_mfa_enabled.sql b/query/iam/iam_root_user_hardware_mfa_enabled.sql deleted file mode 100644 index 7d8e6551..00000000 --- a/query/iam/iam_root_user_hardware_mfa_enabled.sql +++ /dev/null @@ -1,17 +0,0 @@ -select - -- Required Columns - 'arn:' || s.partition || ':::' || s.account_id as resource, - case - when account_mfa_enabled and serial_number is null then 'ok' - else 'alarm' - end status, - case - when account_mfa_enabled = false then 'MFA not enabled for root account.' - when serial_number is not null then 'MFA enabled for root account, but the MFA associated is a virtual device.' - else 'Hardware MFA device enabled for root account.' - end reason, - -- Additional Dimensions - s.account_id -from - aws_iam_account_summary as s - left join aws_iam_virtual_mfa_device on serial_number = 'arn:' || s.partition || ':iam::' || s.account_id || ':mfa/root-account-mfa-device' diff --git a/query/iam/iam_root_user_mfa_enabled.sql b/query/iam/iam_root_user_mfa_enabled.sql deleted file mode 100644 index 64581b5a..00000000 --- a/query/iam/iam_root_user_mfa_enabled.sql +++ /dev/null @@ -1,15 +0,0 @@ -select - -- Required Columns - 'arn:' || partition || ':::' || account_id as resource, - case - when account_mfa_enabled then 'ok' - else 'alarm' - end status, - case - when account_mfa_enabled then 'MFA enabled for root account.' - else 'MFA not enabled for root account.' - end reason, - -- Additional Dimensions - account_id -from - aws_iam_account_summary; diff --git a/query/iam/iam_root_user_no_access_keys.sql b/query/iam/iam_root_user_no_access_keys.sql deleted file mode 100644 index 0d7a9d89..00000000 --- a/query/iam/iam_root_user_no_access_keys.sql +++ /dev/null @@ -1,15 +0,0 @@ -select - -- Required Columns - 'arn:' || partition || ':::' || account_id as resource, - case - when account_access_keys_present > 0 then 'alarm' - else 'ok' - end status, - case - when account_access_keys_present > 0 then 'Root user access keys exist.' - else 'No root user access keys exist.' - end reason, - -- Additional Dimensions - account_id -from - aws_iam_account_summary; diff --git a/query/iam/iam_root_user_virtual_mfa.sql b/query/iam/iam_root_user_virtual_mfa.sql deleted file mode 100644 index bfa80fe0..00000000 --- a/query/iam/iam_root_user_virtual_mfa.sql +++ /dev/null @@ -1,17 +0,0 @@ -select - -- Required Columns - 'arn:' || s.partition || ':::' || s.account_id as resource, - case - when account_mfa_enabled and serial_number is not null then 'ok' - else 'alarm' - end status, - case - when account_mfa_enabled = false then 'MFA is not enabled for the root user.' - when serial_number is null then 'MFA is enabled for the root user, but the MFA associated with the root user is a hardware device.' - else 'Virtual MFA enabled for the root user.' - end reason, - -- Additional Dimensions - s.account_id -from - aws_iam_account_summary as s - left join aws_iam_virtual_mfa_device on serial_number = 'arn:' || s.partition || ':iam::' || s.account_id || ':mfa/root-account-mfa-device'; \ No newline at end of file diff --git a/query/iam/iam_server_certificate_not_expired.sql b/query/iam/iam_server_certificate_not_expired.sql deleted file mode 100644 index 746f5d1e..00000000 --- a/query/iam/iam_server_certificate_not_expired.sql +++ /dev/null @@ -1,15 +0,0 @@ -select - -- Required Columns - arn as resource, - case when expiration < (current_date - interval '1' second) then 'alarm' - else 'ok' - end as status, - case when expiration < (current_date - interval '1' second) then - name || ' expired ' || to_char(expiration, 'DD-Mon-YYYY') || '.' - else - name || ' valid until ' || to_char(expiration, 'DD-Mon-YYYY') || '.' - end as reason, - -- Additional Dimensions - account_id -from - aws_iam_server_certificate; \ No newline at end of file diff --git a/query/iam/iam_support_role.sql b/query/iam/iam_support_role.sql deleted file mode 100644 index b5476509..00000000 --- a/query/iam/iam_support_role.sql +++ /dev/null @@ -1,35 +0,0 @@ --- pgFormatter-ignore -with support_role_count as -( - select - -- Required Columns - 'arn:' || a.partition || ':::' || a.account_id as resource, - count(policy_arn), - a.account_id - from - aws_account as a - left join aws_iam_role as r on r.account_id = a.account_id - left join jsonb_array_elements_text(attached_policy_arns) as policy_arn on true - where - split_part(policy_arn, '/', 2) = 'AWSSupportAccess' - or policy_arn is null - group by - a.account_id, - a.partition -) -select - -- Required Columns - resource, - case - when count > 0 then 'ok' - else 'alarm' - end as status, - case - when count = 1 then 'AWSSupportAccess policy attached to 1 role.' - when count > 1 then 'AWSSupportAccess policy attached to ' || count || ' roles.' - else 'AWSSupportAccess policy not attached to any role.' - end as reason, - -- Additional Dimensions - account_id -from - support_role_count; diff --git a/query/iam/iam_user_access_key_age_90.sql b/query/iam/iam_user_access_key_age_90.sql deleted file mode 100644 index 6bb29e5f..00000000 --- a/query/iam/iam_user_access_key_age_90.sql +++ /dev/null @@ -1,14 +0,0 @@ -select - -- Required Columns - 'arn:' || partition || ':iam::' || account_id || ':user/' || user_name || '/accesskey/' || access_key_id as resource, - case - when create_date <= (current_date - interval '90' day) then 'alarm' - else 'ok' - end status, - user_name || ' ' || access_key_id || ' created ' || to_char(create_date , 'DD-Mon-YYYY') || - ' (' || extract(day from current_timestamp - create_date) || ' days).' - as reason, - -- Additional Dimensions - account_id -from - aws_iam_access_key; diff --git a/query/iam/iam_user_access_keys_and_password_at_setup.sql b/query/iam/iam_user_access_keys_and_password_at_setup.sql deleted file mode 100644 index 0f58ba92..00000000 --- a/query/iam/iam_user_access_keys_and_password_at_setup.sql +++ /dev/null @@ -1,19 +0,0 @@ -select - -- Required Columns - user_arn as resource, - case - -- alarm when password is enabled and the key was created within 10 seconds of the user - when password_enabled and (extract(epoch from (access_key_1_last_rotated - user_creation_time)) < 10) then 'alarm' - else 'ok' - end as status, - case - when not password_enabled then user_name || ' password login disabled.' - when access_key_1_last_rotated is null then user_name || ' has no access keys.' - when password_enabled and (extract(epoch from (access_key_1_last_rotated - user_creation_time)) < 10) - then user_name || ' has access key created during user creation and password login enabled.' - else user_name || ' has access key not created during user creation.' - end as reason, - -- Additional Dimensions - account_id -from - aws_iam_credential_report diff --git a/query/iam/iam_user_console_access_mfa_enabled.sql b/query/iam/iam_user_console_access_mfa_enabled.sql deleted file mode 100644 index c2769f5f..00000000 --- a/query/iam/iam_user_console_access_mfa_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - user_arn as resource, - case - when password_enabled and not mfa_active then 'alarm' - else 'ok' - end as status, - case - when not password_enabled then user_name || ' password login disabled.' - when password_enabled and not mfa_active then user_name || ' password login enabled but no MFA device configured.' - else user_name || ' password login enabled and MFA device configured.' - end as reason, - -- Additional Dimensions - account_id -from - aws_iam_credential_report; \ No newline at end of file diff --git a/query/iam/iam_user_hardware_mfa_enabled.sql b/query/iam/iam_user_hardware_mfa_enabled.sql deleted file mode 100644 index 65ca10d9..00000000 --- a/query/iam/iam_user_hardware_mfa_enabled.sql +++ /dev/null @@ -1,19 +0,0 @@ -select - -- Required Columns - u.arn as resource, - case - when serial_number is null then 'alarm' - when serial_number like any(array['%mfa%','%sms-mfa%']) then 'info' - else 'ok' - end as status, - case - when serial_number is null then u.name || ' MFA device not configured.' - when serial_number like any(array['%mfa%','%sms-mfa%']) then u.name || ' MFA enabled, but the MFA associated is a virtual device.' - else u.name || ' hardware MFA device enabled.' - end as reason, - -- Additional Dimensions - u.region, - u.account_id -from - aws_iam_virtual_mfa_device as m - right join aws_iam_user as u on m.user_id = u.user_id; diff --git a/query/iam/iam_user_in_group.sql b/query/iam/iam_user_in_group.sql deleted file mode 100644 index 33323d58..00000000 --- a/query/iam/iam_user_in_group.sql +++ /dev/null @@ -1,15 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when jsonb_array_length(groups) = 0 then 'alarm' - else 'ok' - end as status, - case - when jsonb_array_length(groups) = 0 then title || ' not associated with any IAM group.' - else title || ' associated with IAM group.' - end as reason, - -- Additional Dimensions - account_id -from - aws_iam_user; \ No newline at end of file diff --git a/query/iam/iam_user_mfa_enabled.sql b/query/iam/iam_user_mfa_enabled.sql deleted file mode 100644 index 11e7235d..00000000 --- a/query/iam/iam_user_mfa_enabled.sql +++ /dev/null @@ -1,15 +0,0 @@ -select - -- Required Columns - user_arn as resource, - case - when not mfa_active then 'alarm' - else 'ok' - end as status, - case - when not mfa_active then user_name || ' MFA device not configured.' - else user_name || ' MFA device configured.' - end as reason, - -- Additional Dimensions - account_id -from - aws_iam_credential_report; \ No newline at end of file diff --git a/query/iam/iam_user_no_inline_attached_policies.sql b/query/iam/iam_user_no_inline_attached_policies.sql deleted file mode 100644 index 61330d1d..00000000 --- a/query/iam/iam_user_no_inline_attached_policies.sql +++ /dev/null @@ -1,13 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when inline_policies is null and attached_policy_arns is null then 'ok' - else 'alarm' - end status, - name || ' has ' || coalesce(jsonb_array_length(inline_policies),0) || ' inline and ' || - coalesce(jsonb_array_length(attached_policy_arns),0) || ' directly attached policies.' as reason, - -- Additional Dimensions - account_id -from - aws_iam_user; diff --git a/query/iam/iam_user_no_policies.sql b/query/iam/iam_user_no_policies.sql deleted file mode 100644 index 39383091..00000000 --- a/query/iam/iam_user_no_policies.sql +++ /dev/null @@ -1,12 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when attached_policy_arns is null then 'ok' - else 'alarm' - end status, - name || ' has ' || coalesce(jsonb_array_length(attached_policy_arns),0) || ' attached policies.' as reason, - -- Additional Dimensions - account_id -from - aws_iam_user; diff --git a/query/iam/iam_user_one_active_key.sql b/query/iam/iam_user_one_active_key.sql deleted file mode 100644 index db759710..00000000 --- a/query/iam/iam_user_one_active_key.sql +++ /dev/null @@ -1,19 +0,0 @@ - -select - -- Required Columns - u.arn as resource, - case - when count(k.*) > 1 then 'alarm' - else 'ok' - end as status, - u.name || ' has ' || count(k.*) || ' active access keys.' as reason, - -- Additional Dimensions - u.account_id -from aws_iam_user as u -left join aws_iam_access_key as k on u.name = k.user_name and u.account_id = k.account_id -where - k.status = 'Active' or k.status is null -group by - u.arn, - u.name, - u.account_id diff --git a/query/iam/iam_user_unused_credentials_45.sql b/query/iam/iam_user_unused_credentials_45.sql deleted file mode 100644 index b7f31f15..00000000 --- a/query/iam/iam_user_unused_credentials_45.sql +++ /dev/null @@ -1,51 +0,0 @@ -select - -- Required Columns - user_arn as resource, - case - --root_account will have always password associated even though AWS credential report returns 'not_supported' for password_enabled - when user_name = '' - then 'info' - when password_enabled and password_last_used is null and password_last_changed < (current_date - interval '45' day) - then 'alarm' - when password_enabled and password_last_used < (current_date - interval '45' day) - then 'alarm' - when access_key_1_active and access_key_1_last_used_date is null and access_key_1_last_rotated < (current_date - interval '45' day) - then 'alarm' - when access_key_1_active and access_key_1_last_used_date < (current_date - interval '45' day) - then 'alarm' - when access_key_2_active and access_key_2_last_used_date is null and access_key_2_last_rotated < (current_date - interval '45' day) - then 'alarm' - when access_key_2_active and access_key_2_last_used_date < (current_date - interval '45' day) - then 'alarm' - else 'ok' - end status, - user_name || - case - when not password_enabled - then ' password not enabled,' - when password_enabled and password_last_used is null - then ' password created ' || to_char(password_last_changed, 'DD-Mon-YYYY') || ' never used,' - else - ' password used ' || to_char(password_last_used, 'DD-Mon-YYYY') || ',' - end || - case - when not access_key_1_active - then ' key 1 not enabled,' - when access_key_1_active and access_key_1_last_used_date is null - then ' key 1 created ' || to_char(access_key_1_last_rotated, 'DD-Mon-YYYY') || ' never used,' - else - ' key 1 used ' || to_char(access_key_1_last_used_date, 'DD-Mon-YYYY') || ',' - end || - case - when not access_key_2_active - then ' key 2 not enabled.' - when access_key_2_active and access_key_2_last_used_date is null - then ' key 2 created ' || to_char(access_key_2_last_rotated, 'DD-Mon-YYYY') || ' never used.' - else - ' key 2 used ' || to_char(access_key_2_last_used_date, 'DD-Mon-YYYY') || '.' - end - as reason, - -- Additional Dimensions - account_id -from - aws_iam_credential_report; \ No newline at end of file diff --git a/query/iam/iam_user_unused_credentials_90.sql b/query/iam/iam_user_unused_credentials_90.sql deleted file mode 100644 index af8c7387..00000000 --- a/query/iam/iam_user_unused_credentials_90.sql +++ /dev/null @@ -1,50 +0,0 @@ -select - -- Required Columns - user_arn as resource, - case - when user_name = '' - then 'info' - when password_enabled and password_last_used is null and password_last_changed < (current_date - interval '90' day) - then 'alarm' - when password_enabled and password_last_used < (current_date - interval '90' day) - then 'alarm' - when access_key_1_active and access_key_1_last_used_date is null and access_key_1_last_rotated < (current_date - interval '90' day) - then 'alarm' - when access_key_1_active and access_key_1_last_used_date < (current_date - interval '90' day) - then 'alarm' - when access_key_2_active and access_key_2_last_used_date is null and access_key_2_last_rotated < (current_date - interval '90' day) - then 'alarm' - when access_key_2_active and access_key_2_last_used_date < (current_date - interval '90' day) - then 'alarm' - else 'ok' - end status, - user_name || - case - when not password_enabled - then ' password not enabled,' - when password_enabled and password_last_used is null - then ' password created ' || to_char(password_last_changed, 'DD-Mon-YYYY') || ' never used,' - else - ' password used ' || to_char(password_last_used, 'DD-Mon-YYYY') || ',' - end || - case - when not access_key_1_active - then ' key 1 not enabled,' - when access_key_1_active and access_key_1_last_used_date is null - then ' key 1 created ' || to_char(access_key_1_last_rotated, 'DD-Mon-YYYY') || ' never used,' - else - ' key 1 used ' || to_char(access_key_1_last_used_date, 'DD-Mon-YYYY') || ',' - end || - case - when not access_key_2_active - then ' key 2 not enabled.' - when access_key_2_active and access_key_2_last_used_date is null - then ' key 2 created ' || to_char(access_key_2_last_rotated, 'DD-Mon-YYYY') || ' never used.' - else - ' key 2 used ' || to_char(access_key_2_last_used_date, 'DD-Mon-YYYY') || '.' - end - as reason, - -- Additional Dimensions - account_id -from - aws_iam_credential_report; diff --git a/query/iam/iam_user_with_administrator_access_mfa_enabled.sql b/query/iam/iam_user_with_administrator_access_mfa_enabled.sql deleted file mode 100644 index 4077cbc1..00000000 --- a/query/iam/iam_user_with_administrator_access_mfa_enabled.sql +++ /dev/null @@ -1,32 +0,0 @@ -with admin_users as ( - select - user_id, - name, - attachments - from - aws_iam_user, - jsonb_array_elements_text(attached_policy_arns) as attachments - where - split_part(attachments, '/', 2) = 'AdministratorAccess' -) -select - -- Required Columns - u.arn as resource, - case - when au.user_id is null then 'skip' - when au.user_id is not null and u.mfa_enabled then 'ok' - else 'alarm' - end as status, - case - when au.user_id is null then u.name || ' does not have administrator access.' - when au.user_id is not null and u.mfa_enabled then u.name || ' has MFA token enabled.' - else u.name || ' has MFA token disabled.' - end as reason, - -- Additional Dimensions - u.region, - u.account_id -from - aws_iam_user as u - left join admin_users au on u.user_id = au.user_id -order by - u.name; \ No newline at end of file diff --git a/query/kinesis/kinesis_stream_encrypted_with_kms_cmk.sql b/query/kinesis/kinesis_stream_encrypted_with_kms_cmk.sql deleted file mode 100644 index 2803c7e6..00000000 --- a/query/kinesis/kinesis_stream_encrypted_with_kms_cmk.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - stream_arn as resource, - case - when encryption_type = 'KMS' and key_id <> 'alias/aws/kinesis' then 'ok' - else 'alarm' - end as status, - case - when encryption_type = 'KMS' and key_id <> 'alias/aws/kinesis' then title || ' encrypted with CMK.' - else title || ' not encrypted with CMK.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_kinesis_stream; \ No newline at end of file diff --git a/query/kinesis/kinesis_stream_server_side_encryption_enabled.sql b/query/kinesis/kinesis_stream_server_side_encryption_enabled.sql deleted file mode 100644 index 837f68bc..00000000 --- a/query/kinesis/kinesis_stream_server_side_encryption_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - stream_arn as resource, - case - when encryption_type = 'KMS' then 'ok' - else 'alarm' - end as status, - case - when encryption_type = 'KMS' then title || ' server side encryption enabled.' - else title || ' server side encryption disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_kinesis_stream; \ No newline at end of file diff --git a/query/kms/kms_cmk_policy_prohibit_public_access.sql b/query/kms/kms_cmk_policy_prohibit_public_access.sql deleted file mode 100644 index abf10358..00000000 --- a/query/kms/kms_cmk_policy_prohibit_public_access.sql +++ /dev/null @@ -1,37 +0,0 @@ -with wildcard_action_policies as ( - select - arn, - count(*) as statements_num - from - aws_kms_key, - jsonb_array_elements(policy_std -> 'Statement') as s - where - s ->> 'Effect' = 'Allow' - and ( - ( s -> 'Principal' -> 'AWS') = '["*"]' - or s ->> 'Principal' = '*' - ) - and key_manager = 'CUSTOMER' - group by - arn -) -select - -- Required Columns - k.arn as resource, - case - when p.arn is null then 'ok' - else 'alarm' - end status, - case - when p.arn is null then title || ' does not allow public access.' - else title || ' contains ' || coalesce(p.statements_num,0) || - ' statements that allows public access.' - end as reason, - -- Additional Dimensions - k.region, - k.account_id -from - aws_kms_key as k - left join wildcard_action_policies as p on p.arn = k.arn -where - key_manager = 'CUSTOMER'; \ No newline at end of file diff --git a/query/kms/kms_cmk_rotation_enabled.sql b/query/kms/kms_cmk_rotation_enabled.sql deleted file mode 100644 index 9fb09fd1..00000000 --- a/query/kms/kms_cmk_rotation_enabled.sql +++ /dev/null @@ -1,24 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when origin = 'EXTERNAL' then 'skip' - when key_state = 'PendingDeletion' then 'skip' - when key_state = 'Disabled' then 'skip' - when not key_rotation_enabled then 'alarm' - else 'ok' - end as status, - case - when origin = 'EXTERNAL' then title || ' has imported key material.' - when key_state = 'PendingDeletion' then title || ' is pending deletion.' - when key_state = 'Disabled' then title || ' is disabled.' - when not key_rotation_enabled then title || ' key rotation disabled.' - else title || ' key rotation enabled.' - end as reason, - -- Additional columns - region, - account_id -from - aws_kms_key -where - key_manager = 'CUSTOMER'; diff --git a/query/kms/kms_key_decryption_restricted_in_iam_customer_managed_policy.sql b/query/kms/kms_key_decryption_restricted_in_iam_customer_managed_policy.sql deleted file mode 100644 index 0c03fe06..00000000 --- a/query/kms/kms_key_decryption_restricted_in_iam_customer_managed_policy.sql +++ /dev/null @@ -1,30 +0,0 @@ -with policy_with_decrypt_grant as ( - select - distinct arn - from - aws_iam_policy, - jsonb_array_elements(policy_std -> 'Statement') as statement - where - not is_aws_managed - and statement ->> 'Effect' = 'Allow' - and statement -> 'Resource' ?| array['*', 'arn:aws:kms:*:' || account_id || ':key/*', 'arn:aws:kms:*:' || account_id || ':alias/*'] - and statement -> 'Action' ?| array['*', 'kms:*', 'kms:decrypt', 'kms:reencryptfrom', 'kms:reencrypt*'] -) -select - -- Required Columns - i.arn as resource, - case - when d.arn is null then 'ok' - else 'alarm' - end as status, - case - when d.arn is null then i.title || ' doesn''t allow decryption actions on all keys.' - else i.title || ' allows decryption actions on all keys.' - end as reason, - -- Additional Dimensions - i.account_id -from - aws_iam_policy i -left join policy_with_decrypt_grant d on i.arn = d.arn -where - not is_aws_managed; \ No newline at end of file diff --git a/query/kms/kms_key_decryption_restricted_in_iam_inline_policy.sql b/query/kms/kms_key_decryption_restricted_in_iam_inline_policy.sql deleted file mode 100644 index 77999cfc..00000000 --- a/query/kms/kms_key_decryption_restricted_in_iam_inline_policy.sql +++ /dev/null @@ -1,88 +0,0 @@ -with user_with_decrypt_grant as ( - select - distinct arn - from - aws_iam_user, - jsonb_array_elements(inline_policies_std) as inline_policy, - jsonb_array_elements(inline_policy -> 'PolicyDocument' -> 'Statement') as statement - where - statement ->> 'Effect' = 'Allow' - and statement -> 'Resource' ?| array['*', 'arn:aws:kms:*:' || account_id || ':key/*', 'arn:aws:kms:*:' || account_id || ':alias/*'] - and statement -> 'Action' ?| array['*', 'kms:*', 'kms:decrypt', 'kms:deencrypt*', 'kms:reencryptfrom'] -), -role_with_decrypt_grant as ( - select - distinct arn - from - aws_iam_role, - jsonb_array_elements(inline_policies_std) as inline_policy, - jsonb_array_elements(inline_policy -> 'PolicyDocument' -> 'Statement') as statement - where - statement ->> 'Effect' = 'Allow' - and statement -> 'Resource' ?| array['*', 'arn:aws:kms:*:' || account_id || ':key/*', 'arn:aws:kms:*:' || account_id || ':alias/*'] - and statement -> 'Action' ?| array['*', 'kms:*', 'kms:decrypt', 'kms:deencrypt*', 'kms:reencryptfrom'] -), -group_with_decrypt_grant as ( - select - distinct arn - from - aws_iam_group, - jsonb_array_elements(inline_policies_std) as inline_policy, - jsonb_array_elements(inline_policy -> 'PolicyDocument' -> 'Statement') as statement - where - statement ->> 'Effect' = 'Allow' - and statement -> 'Resource' ?| array['*', 'arn:aws:kms:*:' || account_id || ':key/*', 'arn:aws:kms:*:' || account_id || ':alias/*'] - and statement -> 'Action' ?| array['*', 'kms:*', 'kms:decrypt', 'kms:deencrypt*', 'kms:reencryptfrom'] -) -select - -- Required Columns - i.arn as resource, - case - when d.arn is null then 'ok' - else 'alarm' - end as status, - case - when d.arn is null then 'User ' || i.title || ' not allowed to perform decryption actions on all keys.' - else 'User ' || i.title || ' allowed to perform decryption actions on all keys.' - end as reason, - -- Additional Dimensions - i.account_id -from - aws_iam_user i - left join user_with_decrypt_grant d on i.arn = d.arn -union -select - -- Required Columns - r.arn as resource, - case - when d.arn is null then 'ok' - else 'alarm' - end as status, - case - when d.arn is null then 'Role ' || r.title || ' not allowed to perform decryption actions on all keys.' - else 'Role ' || r.title || ' allowed to perform decryption actions on all keys.' - end as reason, - -- Additional Dimensions - r.account_id -from - aws_iam_role r - left join role_with_decrypt_grant d on r.arn = d.arn -where - r.arn not like '%service-role/%' -union -select - -- Required Columns - g.arn as resource, - case - when d.arn is null then 'ok' - else 'alarm' - end as status, - case - when d.arn is null then 'Role ' || g.title || ' not allowed to perform decryption actions on all keys.' - else 'Group ' || g.title || ' allowed to perform decryption actions on all keys.' - end as reason, - -- Additional Dimensions - g.account_id -from - aws_iam_group g - left join group_with_decrypt_grant d on g.arn = d.arn; \ No newline at end of file diff --git a/query/kms/kms_key_not_pending_deletion.sql b/query/kms/kms_key_not_pending_deletion.sql deleted file mode 100644 index 62bd87ad..00000000 --- a/query/kms/kms_key_not_pending_deletion.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when key_state = 'PendingDeletion' then 'alarm' - else 'ok' - end as status, - case - when key_state = 'PendingDeletion' then title || ' scheduled for deletion and will be deleted in ' || extract(day from deletion_date - current_timestamp) || ' day(s).' - else title || ' not scheduled for deletion.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_kms_key -where - key_manager = 'CUSTOMER'; \ No newline at end of file diff --git a/query/lambda/lambda_function_cloudtrail_logging_enabled.sql b/query/lambda/lambda_function_cloudtrail_logging_enabled.sql deleted file mode 100644 index 45a91741..00000000 --- a/query/lambda/lambda_function_cloudtrail_logging_enabled.sql +++ /dev/null @@ -1,65 +0,0 @@ -with function_logging_cloudtrails as ( - select - distinct replace(replace(v::text,'"',''),'/','') as lambda_arn, - d ->> 'Type' as type - from - aws_cloudtrail_trail, - jsonb_array_elements(event_selectors) e, - jsonb_array_elements(e -> 'DataResources') as d, - jsonb_array_elements(d -> 'Values') v - where - d ->> 'Type' = 'AWS::Lambda::Function' - and replace(replace(v::text,'"',''),'/','') <> 'arn:aws:lambda' -), function_logging_region as ( - select - region as cloudtrail_region, - replace(replace(v::text,'"',''),'/','') as lambda_arn - from - aws_cloudtrail_trail, - jsonb_array_elements(event_selectors) e, - jsonb_array_elements(e -> 'DataResources') as d, - jsonb_array_elements(d -> 'Values') v - where - d ->> 'Type' = 'AWS::Lambda::Function' - and replace(replace(v::text,'"',''),'/','') = 'arn:aws:lambda' - group by - region, - lambda_arn -), -function_logging_region_advance_es as ( - select - region as cloudtrail_region - from - aws_cloudtrail_trail, - jsonb_array_elements(advanced_event_selectors) a, - jsonb_array_elements(a -> 'FieldSelectors') as f, - jsonb_array_elements_text(f -> 'Equals') e - where - e = 'AWS::Lambda::Function' - and f ->> 'Field' != 'eventCategory' - group by - region -) -select - -- Required Columns - distinct l.arn as resource, - case - when (l.arn = c.lambda_arn) - or (r.lambda_arn = 'arn:aws:lambda' and r.cloudtrail_region = l.region ) - or a.cloudtrail_region = l.region then 'ok' - else 'alarm' - end as status, - case - when (l.arn = c.lambda_arn) - or (r.lambda_arn = 'arn:aws:s3' and r.cloudtrail_region = l.region ) - or a.cloudtrail_region = l.region then l.name || ' logging enabled.' - else l.name || ' logging not enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_lambda_function as l - left join function_logging_cloudtrails as c on l.arn = c.lambda_arn - left join function_logging_region as r on r.cloudtrail_region = l.region - left join function_logging_region_advance_es as a on a.cloudtrail_region = l.region; \ No newline at end of file diff --git a/query/lambda/lambda_function_concurrent_execution_limit_configured.sql b/query/lambda/lambda_function_concurrent_execution_limit_configured.sql deleted file mode 100644 index bf700bf2..00000000 --- a/query/lambda/lambda_function_concurrent_execution_limit_configured.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when reserved_concurrent_executions is null then 'alarm' - else 'ok' - end as status, - case - when reserved_concurrent_executions is null then title || ' function-level concurrent execution limit not configured.' - else title || ' function-level concurrent execution limit configured.' - end as reason, - -- Additional Columns - region, - account_id -from - aws_lambda_function; \ No newline at end of file diff --git a/query/lambda/lambda_function_cors_configuration.sql b/query/lambda/lambda_function_cors_configuration.sql deleted file mode 100644 index cb958d50..00000000 --- a/query/lambda/lambda_function_cors_configuration.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when url_config is null then 'info' - when url_config -> 'Cors' ->> 'AllowOrigins' = '["*"]' then 'alarm' - else 'ok' - end as status, - case - when url_config is null then title || ' does not has a URL config.' - when url_config -> 'Cors' ->> 'AllowOrigins' = '["*"]' then title || ' CORS configuration allow all origins.' - else title || ' CORS configuration does not allow all origins.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_lambda_function; \ No newline at end of file diff --git a/query/lambda/lambda_function_dead_letter_queue_configured.sql b/query/lambda/lambda_function_dead_letter_queue_configured.sql deleted file mode 100644 index 3b207441..00000000 --- a/query/lambda/lambda_function_dead_letter_queue_configured.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when dead_letter_config_target_arn is null then 'alarm' - else 'ok' - end as status, - case - when dead_letter_config_target_arn is null then title || ' configured with dead-letter queue.' - else title || ' not configured with dead-letter queue.' - end as reason, - -- Additional Columns - region, - account_id -from - aws_lambda_function; \ No newline at end of file diff --git a/query/lambda/lambda_function_in_vpc.sql b/query/lambda/lambda_function_in_vpc.sql deleted file mode 100644 index 09d759f5..00000000 --- a/query/lambda/lambda_function_in_vpc.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when vpc_id is null then 'alarm' - else 'ok' - end status, - case - when vpc_id is null then title || ' is not in VPC.' - else title || ' is in VPC ' || vpc_id || '.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_lambda_function; diff --git a/query/lambda/lambda_function_multiple_az_configured.sql b/query/lambda/lambda_function_multiple_az_configured.sql deleted file mode 100644 index 3cc1a803..00000000 --- a/query/lambda/lambda_function_multiple_az_configured.sql +++ /dev/null @@ -1,28 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when vpc_id is null then 'skip' - else case - when - ( - select - count(distinct availability_zone_id) - from - aws_vpc_subnet - where - subnet_id in (select jsonb_array_elements_text(vpc_subnet_ids) ) - ) >= 2 - then 'ok' - else 'alarm' - end - end as status, - case - when vpc_id is null then title || ' is not in VPC.' - else title || ' has ' || jsonb_array_length(vpc_subnet_ids) || ' availability zone(s).' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_lambda_function; \ No newline at end of file diff --git a/query/lambda/lambda_function_restrict_public_access.sql b/query/lambda/lambda_function_restrict_public_access.sql deleted file mode 100644 index 11d7fa33..00000000 --- a/query/lambda/lambda_function_restrict_public_access.sql +++ /dev/null @@ -1,34 +0,0 @@ -with wildcard_action_policies as ( - select - arn, - count(*) as statements_num - from - aws_lambda_function, - jsonb_array_elements(policy_std -> 'Statement') as s - where - s ->> 'Effect' = 'Allow' - and ( - ( s -> 'Principal' -> 'AWS') = '["*"]' - or s ->> 'Principal' = '*' - ) - group by - arn -) -select - -- Required Columns - f.arn as resource, - case - when p.arn is null then 'ok' - else 'alarm' - end as status, - case - when p.arn is null then title || ' does not allow public access.' - else title || ' contains ' || coalesce(p.statements_num,0) || - ' statements that allows public access.' - end as reason, - -- Additional Dimensions - f.region, - f.account_id -from - aws_lambda_function as f - left join wildcard_action_policies as p on p.arn = f.arn; \ No newline at end of file diff --git a/query/lambda/lambda_function_tracing_enabled.sql b/query/lambda/lambda_function_tracing_enabled.sql deleted file mode 100644 index a4cdaeb0..00000000 --- a/query/lambda/lambda_function_tracing_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when tracing_config ->> 'Mode' = 'PassThrough' then 'alarm' - else 'ok' - end as status, - case - when tracing_config ->> 'Mode' = 'PassThrough' then title || ' has tracing disabled.' - else title || ' has tracing enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_lambda_function; \ No newline at end of file diff --git a/query/lambda/lambda_function_use_latest_runtime.sql b/query/lambda/lambda_function_use_latest_runtime.sql deleted file mode 100644 index 1e1b6e53..00000000 --- a/query/lambda/lambda_function_use_latest_runtime.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when package_type <> 'Zip' then 'skip' - when runtime in ('nodejs16.x', 'nodejs14.x', 'nodejs12.x', 'nodejs10.x', 'python3.9', 'python3.8', 'python3.7', 'python3.6', 'ruby2.5', 'ruby2.7', 'java11', 'java8', 'java8.al2', 'go1.x', 'dotnetcore2.1', 'dotnetcore3.1', 'dotnet6') then 'ok' - else 'alarm' - end as status, - case - when package_type <> 'Zip' then title || ' package type is ' || package_type || '.' - when runtime in ('nodejs16.x', 'nodejs14.x', 'nodejs12.x', 'nodejs10.x', 'python3.9', 'python3.8', 'python3.7', 'python3.6', 'ruby2.5', 'ruby2.7', 'java11', 'java8', 'java8.al2', 'go1.x', 'dotnetcore2.1', 'dotnetcore3.1', 'dotnet6') then title || ' uses latest runtime - ' || runtime || '.' - else title || ' uses ' || runtime || ' which is not the latest version.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_lambda_function; diff --git a/query/manual_control.sql b/query/manual_control.sql deleted file mode 100644 index 23e70c2f..00000000 --- a/query/manual_control.sql +++ /dev/null @@ -1,9 +0,0 @@ -select - -- Required Columns - 'arn:' || partition || ':::' || account_id as resource, - 'info' as status, - 'Manual verification required.' as reason, - -- Additional Dimensions - account_id -from - aws_account; diff --git a/query/networkfirewall/networkfirewall_firewall_policy_default_stateless_action_check_fragmented_packets.sql b/query/networkfirewall/networkfirewall_firewall_policy_default_stateless_action_check_fragmented_packets.sql deleted file mode 100644 index 368c0b90..00000000 --- a/query/networkfirewall/networkfirewall_firewall_policy_default_stateless_action_check_fragmented_packets.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when (not (firewall_policy -> 'StatelessFragmentDefaultActions') ? 'aws:drop' - and not (firewall_policy -> 'StatelessFragmentDefaultActions') ? 'aws:forward_to_sfe') then 'alarm' - else 'ok' - end as status, - case - when (not (firewall_policy -> 'StatelessFragmentDefaultActions') ? 'aws:drop' - and not (firewall_policy -> 'StatelessFragmentDefaultActions') ? 'aws:forward_to_sfe') then title || ' stateless action is neither drop nor forward for fragmented packets.' - else title || ' stateless action is either drop or forward for fragmented packets.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_networkfirewall_firewall_policy; \ No newline at end of file diff --git a/query/networkfirewall/networkfirewall_firewall_policy_default_stateless_action_check_full_packets.sql b/query/networkfirewall/networkfirewall_firewall_policy_default_stateless_action_check_full_packets.sql deleted file mode 100644 index 9564f230..00000000 --- a/query/networkfirewall/networkfirewall_firewall_policy_default_stateless_action_check_full_packets.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when (not (firewall_policy -> 'StatelessDefaultActions') ? 'aws:drop' - and not (firewall_policy -> 'StatelessDefaultActions') ? 'aws:forward_to_sfe') then 'alarm' - else 'ok' - end as status, - case - when (not (firewall_policy -> 'StatelessDefaultActions') ? 'aws:drop' - and not (firewall_policy -> 'StatelessDefaultActions') ? 'aws:forward_to_sfe') then title || ' stateless action is neither drop nor forward for full packets.' - else title || ' stateless action is either drop or forward for full packets.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_networkfirewall_firewall_policy; \ No newline at end of file diff --git a/query/networkfirewall/networkfirewall_firewall_policy_rule_group_not_empty.sql b/query/networkfirewall/networkfirewall_firewall_policy_rule_group_not_empty.sql deleted file mode 100644 index 97fd65b6..00000000 --- a/query/networkfirewall/networkfirewall_firewall_policy_rule_group_not_empty.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when (firewall_policy ->> 'StatefulRuleGroupReferences' is null or jsonb_array_length(firewall_policy -> 'StatefulRuleGroupReferences') = 0) - and (firewall_policy ->> 'StatelessRuleGroupReferences' is null or jsonb_array_length(firewall_policy -> 'StatelessRuleGroupReferences') = 0) then 'alarm' - else 'ok' - end as status, - case - when (firewall_policy ->> 'StatefulRuleGroupReferences' is null or jsonb_array_length(firewall_policy -> 'StatefulRuleGroupReferences') = 0) - and (firewall_policy ->> 'StatelessRuleGroupReferences' is null or jsonb_array_length(firewall_policy -> 'StatelessRuleGroupReferences') = 0) then title || ' has no associated rule groups.' - else title || ' has associated rule groups.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_networkfirewall_firewall_policy; \ No newline at end of file diff --git a/query/networkfirewall/networkfirewall_stateless_rule_group_not_empty.sql b/query/networkfirewall/networkfirewall_stateless_rule_group_not_empty.sql deleted file mode 100644 index 7f1522cf..00000000 --- a/query/networkfirewall/networkfirewall_stateless_rule_group_not_empty.sql +++ /dev/null @@ -1,17 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when type = 'STATEFUL' then 'skip' - when jsonb_array_length(rules_source -> 'StatelessRulesAndCustomActions' -> 'StatelessRules') > 0 then 'ok' - else 'alarm' - end as status, - case - when type = 'STATEFUL' then title || ' is a stateful rule group.' - else title || ' has ' || jsonb_array_length(rules_source -> 'StatelessRulesAndCustomActions' -> 'StatelessRules') || ' rule(s).' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_networkfirewall_rule_group; diff --git a/query/opensearch/opensearch_domain_encryption_at_rest_enabled.sql b/query/opensearch/opensearch_domain_encryption_at_rest_enabled.sql deleted file mode 100644 index e6b53458..00000000 --- a/query/opensearch/opensearch_domain_encryption_at_rest_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when encryption_at_rest_options ->> 'Enabled' = 'false' then 'alarm' - else 'ok' - end status, - case - when encryption_at_rest_options ->> 'Enabled' = 'false' then title || ' encryption at rest not enabled.' - else title || ' encryption at rest enabled.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_opensearch_domain; diff --git a/query/opensearch/opensearch_domain_fine_grained_access_enabled.sql b/query/opensearch/opensearch_domain_fine_grained_access_enabled.sql deleted file mode 100644 index 3f6d559a..00000000 --- a/query/opensearch/opensearch_domain_fine_grained_access_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when advanced_security_options is null or not (advanced_security_options -> 'Enabled')::boolean then 'alarm' - else 'ok' - end as status, - case - when advanced_security_options is null or not (advanced_security_options -> 'Enabled')::boolean then title || ' having fine-grained access control disabled.' - else title || ' having fine-grained access control enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_opensearch_domain; \ No newline at end of file diff --git a/query/opensearch/opensearch_domain_in_vpc.sql b/query/opensearch/opensearch_domain_in_vpc.sql deleted file mode 100644 index 66c51dc3..00000000 --- a/query/opensearch/opensearch_domain_in_vpc.sql +++ /dev/null @@ -1,38 +0,0 @@ -with public_subnets as ( - select - distinct a -> 'SubnetId' as SubnetId - from - aws_vpc_route_table as t, - jsonb_array_elements(associations) as a, - jsonb_array_elements(routes) as r - where - r ->> 'DestinationCidrBlock' = '0.0.0.0/0' - and r ->> 'GatewayId' like 'igw-%' -), opensearch_domain_with_public_subnet as ( - select - arn - from - aws_opensearch_domain , - jsonb_array_elements(vpc_options -> 'SubnetIds') as s - where - s in (select SubnetId from public_subnets) -) -select - -- Required Columns - d.arn as resource, - case - when d.vpc_options ->> 'VPCId' is null then 'alarm' - when d.vpc_options ->> 'VPCId' is not null and p.arn is not null then 'alarm' - else 'ok' - end status, - case - when vpc_options ->> 'VPCId' is null then title || ' not in VPC.' - when d.vpc_options ->> 'VPCId' is not null and p.arn is not null then title || ' attached to public subnet.' - else title || ' in VPC ' || (vpc_options ->> 'VPCId') || '.' - end reason, - -- Additional Dimensions - d.region, - d.account_id -from - aws_opensearch_domain as d left join opensearch_domain_with_public_subnet as p - on d.arn = p.arn; \ No newline at end of file diff --git a/query/rds/rds_db_cluster_aurora_backtracking_enabled.sql b/query/rds/rds_db_cluster_aurora_backtracking_enabled.sql deleted file mode 100644 index 9ae79423..00000000 --- a/query/rds/rds_db_cluster_aurora_backtracking_enabled.sql +++ /dev/null @@ -1,17 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when engine not ilike '%aurora-mysql%' then 'skip' - when backtrack_window is not null then 'ok' - else 'alarm' - end as status, - case - when engine not ilike '%aurora-mysql%' then title || ' not Aurora MySQL-compatible edition.' - when backtrack_window is not null then title || ' backtracking enabled.' - else title || ' backtracking not enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from aws_rds_db_cluster; \ No newline at end of file diff --git a/query/rds/rds_db_cluster_aurora_protected_by_backup_plan.sql b/query/rds/rds_db_cluster_aurora_protected_by_backup_plan.sql deleted file mode 100644 index 62cfde23..00000000 --- a/query/rds/rds_db_cluster_aurora_protected_by_backup_plan.sql +++ /dev/null @@ -1,27 +0,0 @@ -with backup_protected_cluster as ( - select - resource_arn as arn - from - aws_backup_protected_resource as b - where - resource_type = 'Aurora' -) -select - -- Required Columns - c.arn as resource, - case - when c.engine not like '%aurora%' then 'skip' - when b.arn is not null then 'ok' - else 'alarm' - end as status, - case - when c.engine not like '%aurora%' then c.title || ' not Aurora resources.' - when b.arn is not null then c.title || ' is protected by backup plan.' - else c.title || ' is not protected by backup plan.' - end as reason, - -- Additional Dimensions - c.region, - c.account_id -from - aws_rds_db_cluster as c - left join backup_protected_cluster as b on c.arn = b.arn; \ No newline at end of file diff --git a/query/rds/rds_db_cluster_copy_tags_to_snapshot_enabled.sql b/query/rds/rds_db_cluster_copy_tags_to_snapshot_enabled.sql deleted file mode 100644 index 198be528..00000000 --- a/query/rds/rds_db_cluster_copy_tags_to_snapshot_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when copy_tags_to_snapshot then 'ok' - else 'alarm' - end as status, - case - when copy_tags_to_snapshot then title || ' copy tags to snapshot enabled.' - else title || ' copy tags to snapshot disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_cluster; diff --git a/query/rds/rds_db_cluster_deletion_protection_enabled.sql b/query/rds/rds_db_cluster_deletion_protection_enabled.sql deleted file mode 100644 index 3efbd34c..00000000 --- a/query/rds/rds_db_cluster_deletion_protection_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ - select - -- Required Columns - db_cluster_identifier as resource, - case - when deletion_protection then 'ok' - else 'alarm' - end as status, - case - when deletion_protection then title || ' deletion protection enabled.' - else title || ' deletion protection not enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_cluster; \ No newline at end of file diff --git a/query/rds/rds_db_cluster_events_subscription.sql b/query/rds/rds_db_cluster_events_subscription.sql deleted file mode 100644 index 56f10efe..00000000 --- a/query/rds/rds_db_cluster_events_subscription.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when source_type <> 'db-cluster' then 'skip' - when source_type = 'db-cluster' and enabled and event_categories_list @> '["failure", "maintenance"]' then 'ok' - else 'alarm' - end as status, - case - when source_type <> 'db-cluster' then cust_subscription_id || ' event subscription of ' || source_type || ' type.' - when source_type = 'db-cluster' and enabled and event_categories_list @> '["failure", "maintenance"]' then cust_subscription_id || ' event subscription enabled for critical db cluster events.' - else cust_subscription_id || ' event subscription missing critical db cluster events.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_event_subscription; \ No newline at end of file diff --git a/query/rds/rds_db_cluster_iam_authentication_enabled.sql b/query/rds/rds_db_cluster_iam_authentication_enabled.sql deleted file mode 100644 index 7b01d82b..00000000 --- a/query/rds/rds_db_cluster_iam_authentication_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when iam_database_authentication_enabled then 'ok' - else 'alarm' - end as status, - case - when iam_database_authentication_enabled then title || ' IAM authentication enabled.' - else title || ' IAM authentication not enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_cluster; \ No newline at end of file diff --git a/query/rds/rds_db_cluster_multiple_az_enabled.sql b/query/rds/rds_db_cluster_multiple_az_enabled.sql deleted file mode 100644 index f836622b..00000000 --- a/query/rds/rds_db_cluster_multiple_az_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when multi_az then 'ok' - else 'alarm' - end as status, - case - when multi_az then title || ' Multi-AZ enabled.' - else title || ' Multi-AZ disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_cluster; diff --git a/query/rds/rds_db_cluster_no_default_admin_name.sql b/query/rds/rds_db_cluster_no_default_admin_name.sql deleted file mode 100644 index 22a3eae3..00000000 --- a/query/rds/rds_db_cluster_no_default_admin_name.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when master_user_name in ('admin','postgres') then 'alarm' - else 'ok' - end status, - case - when master_user_name in ('admin','postgres') then title || ' using default master user name.' - else title || ' not using default master user name.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_cluster; diff --git a/query/rds/rds_db_instance_and_cluster_enhanced_monitoring_enabled.sql b/query/rds/rds_db_instance_and_cluster_enhanced_monitoring_enabled.sql deleted file mode 100644 index b86719ec..00000000 --- a/query/rds/rds_db_instance_and_cluster_enhanced_monitoring_enabled.sql +++ /dev/null @@ -1,39 +0,0 @@ -( -select - -- Required Columns - arn as resource, - case - when enabled_cloudwatch_logs_exports is not null then 'ok' - else 'alarm' - end as status, - case - when enabled_cloudwatch_logs_exports is not null then title || ' enhanced monitoring enabled.' - else title || ' enhanced monitoring not enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_cluster -) -union -( -select - -- Required Columns - arn as resource, - case - when class = 'db.m1.small' then 'skip' - when enhanced_monitoring_resource_arn is not null then 'ok' - else 'alarm' - end as status, - case - when class = 'db.m1.small' then title || ' enhanced monitoring not supported.' - when enhanced_monitoring_resource_arn is not null then title || ' enhanced monitoring enabled.' - else title || ' enhanced monitoring not enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_instance -); \ No newline at end of file diff --git a/query/rds/rds_db_instance_and_cluster_no_default_port.sql b/query/rds/rds_db_instance_and_cluster_no_default_port.sql deleted file mode 100644 index 3af25919..00000000 --- a/query/rds/rds_db_instance_and_cluster_no_default_port.sql +++ /dev/null @@ -1,49 +0,0 @@ -( -select - -- Required Columns - arn as resource, - case - when engine similar to '%(aurora|mysql|mariadb)%' and port = '3306' then 'alarm' - when engine like '%postgres%' and port = '5432' then 'alarm' - when engine like 'oracle%' and port = '1521' then 'alarm' - when engine like 'sqlserver%' and port = '1433' then 'alarm' - else 'ok' - end as status, - case - when engine similar to '%(aurora|mysql|mariadb)%' and port = '3306' then title || ' ' || engine || ' uses a default port.' - when engine like '%postgres%' and port = '5432' then title || ' ' || engine || ' uses a default port.' - when engine like 'oracle%' and port = '1521' then title || ' ' || engine || ' uses a default port.' - when engine like 'sqlserver%' and port = '1433' then title || ' ' || engine || ' uses a default port.' - else title || ' doesnt use a default port.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_cluster -) -union -( -select - -- Required Columns - arn as resource, - case - when engine similar to '%(aurora|mysql|mariadb)%' and port = '3306' then 'alarm' - when engine like '%postgres%' and port = '5432' then 'alarm' - when engine like 'oracle%' and port = '1521' then 'alarm' - when engine like 'sqlserver%' and port = '1433' then 'alarm' - else 'ok' - end as status, - case - when engine similar to '%(aurora|mysql|mariadb)%' and port = '3306' then title || ' ' || engine || ' uses a default port.' - when engine like '%postgres%' and port = '5432' then title || ' ' || engine || ' uses a default port.' - when engine like 'oracle%' and port = '1521' then title || ' ' || engine || ' uses a default port.' - when engine like 'sqlserver%' and port = '1433' then title || ' ' || engine || ' uses a default port.' - else title || ' doesnt use a default port.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_instance -); \ No newline at end of file diff --git a/query/rds/rds_db_instance_automatic_minor_version_upgrade_enabled.sql b/query/rds/rds_db_instance_automatic_minor_version_upgrade_enabled.sql deleted file mode 100644 index 9b4c898f..00000000 --- a/query/rds/rds_db_instance_automatic_minor_version_upgrade_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when auto_minor_version_upgrade then 'ok' - else 'alarm' - end as status, - case - when auto_minor_version_upgrade then title || ' automatic minor version upgrades enabled.' - else title || ' automatic minor version upgrades not enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_instance; \ No newline at end of file diff --git a/query/rds/rds_db_instance_backup_enabled.sql b/query/rds/rds_db_instance_backup_enabled.sql deleted file mode 100644 index 807115e8..00000000 --- a/query/rds/rds_db_instance_backup_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when backup_retention_period < 1 then 'alarm' - else 'ok' - end as status, - case - when backup_retention_period < 1 then title || ' backups not enabled.' - else title || ' backups enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_instance; \ No newline at end of file diff --git a/query/rds/rds_db_instance_ca_certificate_expires_7_days.sql b/query/rds/rds_db_instance_ca_certificate_expires_7_days.sql deleted file mode 100644 index c57f02c2..00000000 --- a/query/rds/rds_db_instance_ca_certificate_expires_7_days.sql +++ /dev/null @@ -1,15 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when extract(day from (to_timestamp(certificate ->> 'ValidTill','YYYY-MM-DDTHH:MI:SS')) - current_timestamp) <= '7' then 'alarm' - else 'ok' - end as status, - title || ' expires ' || to_char(to_timestamp(certificate ->> 'ValidTill','YYYY-MM-DDTHH:MI:SS'), 'DD-Mon-YYYY') || - ' (' || extract(day from (to_timestamp(certificate ->> 'ValidTill','YYYY-MM-DDTHH:MI:SS')) - current_timestamp) || ' days).' - as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_instance; diff --git a/query/rds/rds_db_instance_cloudwatch_logs_enabled.sql b/query/rds/rds_db_instance_cloudwatch_logs_enabled.sql deleted file mode 100644 index 3ddab0d5..00000000 --- a/query/rds/rds_db_instance_cloudwatch_logs_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when enabled_cloudwatch_logs_exports is not null then 'ok' - else 'alarm' - end as status, - case - when enabled_cloudwatch_logs_exports is not null then title || ' integrated with CloudWatch logs.' - else title || ' not integrated with CloudWatch logs.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_instance; \ No newline at end of file diff --git a/query/rds/rds_db_instance_copy_tags_to_snapshot_enabled.sql b/query/rds/rds_db_instance_copy_tags_to_snapshot_enabled.sql deleted file mode 100644 index b055308e..00000000 --- a/query/rds/rds_db_instance_copy_tags_to_snapshot_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when copy_tags_to_snapshot then 'ok' - else 'alarm' - end as status, - case - when copy_tags_to_snapshot then title || ' copy tags to snapshot enabled.' - else title || ' copy tags to snapshot disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_instance; diff --git a/query/rds/rds_db_instance_deletion_protection_enabled.sql b/query/rds/rds_db_instance_deletion_protection_enabled.sql deleted file mode 100644 index a2413228..00000000 --- a/query/rds/rds_db_instance_deletion_protection_enabled.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when engine like any(array['aurora%', 'docdb', 'neptune']) then 'skip' - when deletion_protection then 'ok' - else 'alarm' - end status, - case - when engine like any(array['aurora%', 'docdb', 'neptune']) then title || ' has engine ' || engine || ' cluster, deletion protection is set at cluster level.' - when deletion_protection then title || ' deletion protection enabled.' - else title || ' deletion protection not enabled.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_instance; diff --git a/query/rds/rds_db_instance_encryption_at_rest_enabled.sql b/query/rds/rds_db_instance_encryption_at_rest_enabled.sql deleted file mode 100644 index 833ec592..00000000 --- a/query/rds/rds_db_instance_encryption_at_rest_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when storage_encrypted then 'ok' - else 'alarm' - end as status, - case - when storage_encrypted then title || ' encrypted at rest.' - else title || ' not encrypted at rest.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_instance; \ No newline at end of file diff --git a/query/rds/rds_db_instance_events_subscription.sql b/query/rds/rds_db_instance_events_subscription.sql deleted file mode 100644 index df2b545f..00000000 --- a/query/rds/rds_db_instance_events_subscription.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when source_type <> 'db-instance' then 'skip' - when source_type = 'db-instance' and enabled and event_categories_list @> '["failure", "maintenance", "configuration change"]' then 'ok' - else 'alarm' - end as status, - case - when source_type <> 'db-instance' then cust_subscription_id || ' event subscription of ' || source_type || ' type.' - when source_type like 'db-instance' and enabled and event_categories_list @> '["failure", "maintenance", "configuration change"]' then cust_subscription_id || ' event subscription enabled for critical instance events.' - else cust_subscription_id || ' event subscription missing critical instance events.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_event_subscription; \ No newline at end of file diff --git a/query/rds/rds_db_instance_iam_authentication_enabled.sql b/query/rds/rds_db_instance_iam_authentication_enabled.sql deleted file mode 100644 index 51e914dd..00000000 --- a/query/rds/rds_db_instance_iam_authentication_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when iam_database_authentication_enabled then 'ok' - else 'alarm' - end as status, - case - when iam_database_authentication_enabled then title || ' IAM authentication enabled.' - else title || ' IAM authentication not enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_instance; \ No newline at end of file diff --git a/query/rds/rds_db_instance_in_backup_plan.sql b/query/rds/rds_db_instance_in_backup_plan.sql deleted file mode 100644 index 38dafc33..00000000 --- a/query/rds/rds_db_instance_in_backup_plan.sql +++ /dev/null @@ -1,46 +0,0 @@ -with mapped_with_id as ( - select - jsonb_agg(elems) as mapped_ids - from - aws_backup_selection, - jsonb_array_elements(resources) as elems - group by backup_plan_id -), -mapped_with_tags as ( - select - jsonb_agg(elems ->> 'ConditionKey') as mapped_tags - from - aws_backup_selection, - jsonb_array_elements(list_of_tags) as elems - group by backup_plan_id -), -backed_up_instance as ( - select - i.db_instance_identifier - from - aws_rds_db_instance as i - join mapped_with_id as t on t.mapped_ids ?| array[i.arn] - union - select - i.db_instance_identifier - from - aws_rds_db_instance as i - join mapped_with_tags as t on t.mapped_tags ?| array(select jsonb_object_keys(tags)) -) -select - -- Required Columns - i.arn as resource, - case - when b.db_instance_identifier is null then 'alarm' - else 'ok' - end as status, - case - when b.db_instance_identifier is null then i.title || ' not in backup plan.' - else i.title || ' in backup plan.' - end as reason, - -- Additional Dimensions - i.region, - i.account_id -from - aws_rds_db_instance as i - left join backed_up_instance as b on i.db_instance_identifier = b.db_instance_identifier; \ No newline at end of file diff --git a/query/rds/rds_db_instance_in_vpc.sql b/query/rds/rds_db_instance_in_vpc.sql deleted file mode 100644 index be308861..00000000 --- a/query/rds/rds_db_instance_in_vpc.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when vpc_id is null then 'alarm' - else 'ok' - end as status, - case - when vpc_id is null then title || ' is not in VPC.' - else title || ' is in VPC ' || vpc_id || '.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_instance; \ No newline at end of file diff --git a/query/rds/rds_db_instance_logging_enabled.sql b/query/rds/rds_db_instance_logging_enabled.sql deleted file mode 100644 index 2105b35e..00000000 --- a/query/rds/rds_db_instance_logging_enabled.sql +++ /dev/null @@ -1,30 +0,0 @@ -select - -- Required Columns - arn as resource, - engine, - case - when engine like any (array ['mariadb', '%mysql']) and enabled_cloudwatch_logs_exports ?& array ['audit','error','general','slowquery'] then 'ok' - when engine like any (array['%postgres%']) and enabled_cloudwatch_logs_exports ?& array ['postgresql','upgrade'] then 'ok' - when engine like 'oracle%' and enabled_cloudwatch_logs_exports ?& array ['alert','audit', 'trace','listener'] then 'ok' - when engine = 'sqlserver-ex' and enabled_cloudwatch_logs_exports ?& array ['error'] then 'ok' - when engine like 'sqlserver%' and enabled_cloudwatch_logs_exports ?& array ['error','agent'] then 'ok' - else 'alarm' - end as status, - case - when engine like any (array ['mariadb', '%mysql']) and enabled_cloudwatch_logs_exports ?& array ['audit','error','general','slowquery'] - then title || ' ' || engine || ' logging enabled.' - when engine like any (array['%postgres%']) and enabled_cloudwatch_logs_exports ?& array ['postgresql','upgrade'] - then title || ' ' || engine || ' logging enabled.' - when engine like 'oracle%' and enabled_cloudwatch_logs_exports ?& array ['alert','audit', 'trace','listener'] - then title || ' ' || engine || ' logging enabled.' - when engine = 'sqlserver-ex' and enabled_cloudwatch_logs_exports ?& array ['error'] - then title || ' ' || engine || ' logging enabled.' - when engine like 'sqlserver%' and enabled_cloudwatch_logs_exports ?& array ['error','agent'] - then title || ' ' || engine || ' logging enabled.' - else title || ' logging not enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_instance; diff --git a/query/rds/rds_db_instance_multiple_az_enabled.sql b/query/rds/rds_db_instance_multiple_az_enabled.sql deleted file mode 100644 index f21c708f..00000000 --- a/query/rds/rds_db_instance_multiple_az_enabled.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when engine ilike any (array ['%aurora-mysql%', '%aurora-postgres%']) then 'skip' - when multi_az then 'ok' - else 'alarm' - end as status, - case - when engine ilike any (array ['%aurora-mysql%', '%aurora-postgres%']) then title || ' cluster instance.' - when multi_az then title || ' Multi-AZ enabled.' - else title || ' Multi-AZ disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_instance; diff --git a/query/rds/rds_db_instance_no_default_admin_name.sql b/query/rds/rds_db_instance_no_default_admin_name.sql deleted file mode 100644 index 3cae1807..00000000 --- a/query/rds/rds_db_instance_no_default_admin_name.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when master_user_name in ('admin','postgres') then 'alarm' - else 'ok' - end status, - case - when master_user_name in ('admin','postgres') then title || ' using default master user name.' - else title || ' not using default master user name.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_instance; diff --git a/query/rds/rds_db_instance_prohibit_public_access.sql b/query/rds/rds_db_instance_prohibit_public_access.sql deleted file mode 100644 index df027dd4..00000000 --- a/query/rds/rds_db_instance_prohibit_public_access.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when publicly_accessible then 'alarm' - else 'ok' - end status, - case - when publicly_accessible then title || ' publicly accessible.' - else title || ' not publicly accessible.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_instance; diff --git a/query/rds/rds_db_instance_protected_by_backup_plan.sql b/query/rds/rds_db_instance_protected_by_backup_plan.sql deleted file mode 100644 index aedd0711..00000000 --- a/query/rds/rds_db_instance_protected_by_backup_plan.sql +++ /dev/null @@ -1,25 +0,0 @@ -with backup_protected_rds_isntance as ( - select - resource_arn as arn - from - aws_backup_protected_resource as b - where - resource_type = 'RDS' -) -select - -- Required Columns - r.arn as resource, - case - when b.arn is not null then 'ok' - else 'alarm' - end as status, - case - when b.arn is not null then r.title || ' is protected by backup plan.' - else r.title || ' is not protected by backup plan.' - end as reason, - -- Additional Dimensions - r.region, - r.account_id -from - aws_rds_db_instance as r - left join backup_protected_rds_isntance as b on r.arn = b.arn; diff --git a/query/rds/rds_db_parameter_group_events_subscription.sql b/query/rds/rds_db_parameter_group_events_subscription.sql deleted file mode 100644 index 0c9f0820..00000000 --- a/query/rds/rds_db_parameter_group_events_subscription.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when source_type <> 'db-parameter-group' then 'skip' - when source_type = 'db-parameter-group' and enabled and event_categories_list @> '["maintenance", "failure"]' then 'ok' - else 'alarm' - end as status, - case - when source_type <> 'db-parameter-group' then cust_subscription_id || ' event subscription of ' || source_type || ' type.' - when source_type = 'db-parameter-group' and enabled and event_categories_list @> '["configuration change"]' then cust_subscription_id || ' event subscription enabled for critical database parameter group events.' - else cust_subscription_id || ' event subscription missing critical database parameter group events.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_event_subscription; \ No newline at end of file diff --git a/query/rds/rds_db_security_group_events_subscription.sql b/query/rds/rds_db_security_group_events_subscription.sql deleted file mode 100644 index e188a288..00000000 --- a/query/rds/rds_db_security_group_events_subscription.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when source_type <> 'db-security-group' then 'skip' - when source_type = 'db-security-group' and enabled and event_categories_list @> '["failure", "configuration change"]' then 'ok' - else 'alarm' - end as status, - case - when source_type <> 'db-security-group' then cust_subscription_id || ' event subscription of ' || source_type || ' type.' - when source_type = 'db-security-group' and enabled and event_categories_list @> '["failure", "configuration change"]' then cust_subscription_id || ' event subscription enabled for critical database security group events.' - else cust_subscription_id || ' event subscription missing critical database security group events.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_event_subscription; \ No newline at end of file diff --git a/query/rds/rds_db_snapshot_encrypted_at_rest.sql b/query/rds/rds_db_snapshot_encrypted_at_rest.sql deleted file mode 100644 index c69631a5..00000000 --- a/query/rds/rds_db_snapshot_encrypted_at_rest.sql +++ /dev/null @@ -1,37 +0,0 @@ -( -select - -- Required Columns - arn as resource, - case - when storage_encrypted then 'ok' - else 'alarm' - end as status, - case - when storage_encrypted then title || ' encrypted at rest.' - else title || ' not encrypted at rest.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_cluster_snapshot -) -union -( -select - -- Required Columns - arn as resource, - case - when encrypted then 'ok' - else 'alarm' - end as status, - case - when encrypted then title || ' encrypted at rest.' - else title || ' not encrypted at rest.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_snapshot -); diff --git a/query/rds/rds_db_snapshot_prohibit_public_access.sql b/query/rds/rds_db_snapshot_prohibit_public_access.sql deleted file mode 100644 index 7cdab18b..00000000 --- a/query/rds/rds_db_snapshot_prohibit_public_access.sql +++ /dev/null @@ -1,39 +0,0 @@ -( -select - -- Required Columns - arn as resource, - case - when cluster_snapshot -> 'AttributeValues' = '["all"]' then 'alarm' - else 'ok' - end status, - case - when cluster_snapshot -> 'AttributeValues' = '["all"]' then title || ' publicly restorable.' - else title || ' not publicly restorable.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_cluster_snapshot, - jsonb_array_elements(db_cluster_snapshot_attributes) as cluster_snapshot -) -union -( -select - -- Required Columns - arn as resource, - case - when database_snapshot -> 'AttributeValues' = '["all"]' then 'alarm' - else 'ok' - end status, - case - when database_snapshot -> 'AttributeValues' = '["all"]' then title || ' publicly restorable.' - else title || ' not publicly restorable.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_rds_db_snapshot, - jsonb_array_elements(db_snapshot_attributes) as database_snapshot -); diff --git a/query/redshift/redshift_cluster_automatic_snapshots_min_7_days.sql b/query/redshift/redshift_cluster_automatic_snapshots_min_7_days.sql deleted file mode 100644 index 360fa216..00000000 --- a/query/redshift/redshift_cluster_automatic_snapshots_min_7_days.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - 'arn:aws:redshift:' || region || ':' || account_id || ':' || 'cluster' || ':' || cluster_identifier as resource, - case - when automated_snapshot_retention_period >= 7 then 'ok' - else 'alarm' - end as status, - case - when automated_snapshot_retention_period >= 7 then title || ' automatic snapshots enabled with retention period greater than equals 7 days.' - else title || ' automatic snapshots not enabled with retention period greater than equals 7 days.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_redshift_cluster; \ No newline at end of file diff --git a/query/redshift/redshift_cluster_automatic_upgrade_major_versions_enabled.sql b/query/redshift/redshift_cluster_automatic_upgrade_major_versions_enabled.sql deleted file mode 100644 index f83e54d4..00000000 --- a/query/redshift/redshift_cluster_automatic_upgrade_major_versions_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - 'arn:aws:redshift:' || region || ':' || account_id || ':' || 'cluster' || ':' || cluster_identifier as resource, - case - when allow_version_upgrade then 'ok' - else 'alarm' - end as status, - case - when allow_version_upgrade then title || ' automatic upgrades to major versions enabled.' - else title || ' automatic upgrades to major versions disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_redshift_cluster; \ No newline at end of file diff --git a/query/redshift/redshift_cluster_encryption_in_transit_enabled.sql b/query/redshift/redshift_cluster_encryption_in_transit_enabled.sql deleted file mode 100644 index 1d4361a5..00000000 --- a/query/redshift/redshift_cluster_encryption_in_transit_enabled.sql +++ /dev/null @@ -1,29 +0,0 @@ -with pg_with_ssl as ( - select - name as pg_name, - p ->> 'ParameterName' as parameter_name, - p ->> 'ParameterValue' as parameter_value -from - aws_redshift_parameter_group, - jsonb_array_elements(parameters) as p -where - p ->> 'ParameterName' = 'require_ssl' - and p ->> 'ParameterValue' = 'true' -) -select - -- Required Columns - 'arn:aws:redshift:' || region || ':' || account_id || ':' || 'cluster' || ':' || cluster_identifier as resource, - case - when cpg ->> 'ParameterGroupName' in (select pg_name from pg_with_ssl ) then 'ok' - else 'alarm' - end as status, - case - when cpg ->> 'ParameterGroupName' in (select pg_name from pg_with_ssl ) then title || ' encryption in transit enabled.' - else title || ' encryption in transit disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_redshift_cluster, - jsonb_array_elements(cluster_parameter_groups) as cpg; \ No newline at end of file diff --git a/query/redshift/redshift_cluster_encryption_logging_enabled.sql b/query/redshift/redshift_cluster_encryption_logging_enabled.sql deleted file mode 100644 index f8727f45..00000000 --- a/query/redshift/redshift_cluster_encryption_logging_enabled.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when not encrypted then 'alarm' - when not (logging_status ->> 'LoggingEnabled') :: boolean then 'alarm' - else 'ok' - end as status, - case - when not encrypted then title || ' not encrypted.' - when not (logging_status ->> 'LoggingEnabled') :: boolean then title || ' audit logging not enabled.' - else title || ' audit logging and encryption enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_redshift_cluster; \ No newline at end of file diff --git a/query/redshift/redshift_cluster_enhanced_vpc_routing_enabled.sql b/query/redshift/redshift_cluster_enhanced_vpc_routing_enabled.sql deleted file mode 100644 index 9ff28e90..00000000 --- a/query/redshift/redshift_cluster_enhanced_vpc_routing_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - 'arn:aws:redshift:' || region || ':' || account_id || ':' || 'cluster' || ':' || cluster_identifier as resource, - case - when enhanced_vpc_routing then 'ok' - else 'alarm' - end as status, - case - when enhanced_vpc_routing then title || ' enhanced VPC routing enabled.' - else title || ' enhanced VPC routing disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_redshift_cluster; \ No newline at end of file diff --git a/query/redshift/redshift_cluster_kms_enabled.sql b/query/redshift/redshift_cluster_kms_enabled.sql deleted file mode 100644 index ba38b680..00000000 --- a/query/redshift/redshift_cluster_kms_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - 'arn:aws:redshift:' || region || ':' || account_id || ':' || 'cluster' || ':' || cluster_identifier as resource, - case - when encrypted and kms_key_id is not null then 'ok' - else 'alarm' - end as status, - case - when encrypted and kms_key_id is not null then title || ' encrypted with KMS.' - else title || ' not encrypted with KMS' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_redshift_cluster; \ No newline at end of file diff --git a/query/redshift/redshift_cluster_logging_enabled.sql b/query/redshift/redshift_cluster_logging_enabled.sql deleted file mode 100644 index 4d326229..00000000 --- a/query/redshift/redshift_cluster_logging_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - 'arn:aws:redshift:' || region || ':' || account_id || ':' || 'cluster' || ':' || cluster_identifier as resource, - case - when logging_status ->> 'LoggingEnabled' = 'true' then 'ok' - else 'alarm' - end as status, - case - when logging_status ->> 'LoggingEnabled' = 'true' then title || ' logging enabled.' - else title || ' logging disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_redshift_cluster; \ No newline at end of file diff --git a/query/redshift/redshift_cluster_maintenance_settings_check.sql b/query/redshift/redshift_cluster_maintenance_settings_check.sql deleted file mode 100644 index 155006ff..00000000 --- a/query/redshift/redshift_cluster_maintenance_settings_check.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - 'arn:aws:redshift:' || region || ':' || account_id || ':' || 'cluster' || ':' || cluster_identifier as resource, - case - when allow_version_upgrade and automated_snapshot_retention_period >= 7 then 'ok' - else 'alarm' - end as status, - case - when allow_version_upgrade and automated_snapshot_retention_period >= 7 then title || ' has the required maintenance settings.' - else title || ' does not have required maintenance settings.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_redshift_cluster; \ No newline at end of file diff --git a/query/redshift/redshift_cluster_no_default_admin_name.sql b/query/redshift/redshift_cluster_no_default_admin_name.sql deleted file mode 100644 index ee81a465..00000000 --- a/query/redshift/redshift_cluster_no_default_admin_name.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when master_username = 'awsuser' then 'alarm' - else 'ok' - end status, - case - when master_username = 'awsuser' then title || ' using default master user name.' - else title || ' not using default master user name.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_redshift_cluster; diff --git a/query/redshift/redshift_cluster_no_default_database_name.sql b/query/redshift/redshift_cluster_no_default_database_name.sql deleted file mode 100644 index 14acf422..00000000 --- a/query/redshift/redshift_cluster_no_default_database_name.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when db_name = 'dev' then 'alarm' - else 'ok' - end as status, - case - when db_name = 'dev' then title || ' using default database name.' - else title || ' not using default database name.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_redshift_cluster; \ No newline at end of file diff --git a/query/redshift/redshift_cluster_prohibit_public_access.sql b/query/redshift/redshift_cluster_prohibit_public_access.sql deleted file mode 100644 index 56461151..00000000 --- a/query/redshift/redshift_cluster_prohibit_public_access.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - cluster_namespace_arn as resource, - case - when publicly_accessible then 'alarm' - else 'ok' - end status, - case - when publicly_accessible then title || ' publicly accessible.' - else title || ' not publicly accessible.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_redshift_cluster; diff --git a/query/route53/route53_domain_auto_renew_enabled.sql b/query/route53/route53_domain_auto_renew_enabled.sql deleted file mode 100644 index b9de2726..00000000 --- a/query/route53/route53_domain_auto_renew_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when auto_renew then 'ok' - else 'alarm' - end as status, - case - when auto_renew then title || ' auto renew enabled.' - else title || ' auto renew disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_route53_domain; \ No newline at end of file diff --git a/query/route53/route53_domain_expires_30_days.sql b/query/route53/route53_domain_expires_30_days.sql deleted file mode 100644 index 0966f1de..00000000 --- a/query/route53/route53_domain_expires_30_days.sql +++ /dev/null @@ -1,13 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when date(expiration_date) - date(current_date) >= 30 then 'ok' - else 'alarm' - end as status, - title || ' set to expire in ' || extract(day from expiration_date - current_date) || ' days.' as reason, - -- Additional Dimensions - region, - account_id -from - aws_route53_domain; \ No newline at end of file diff --git a/query/route53/route53_domain_expires_7_days.sql b/query/route53/route53_domain_expires_7_days.sql deleted file mode 100644 index e1a2476c..00000000 --- a/query/route53/route53_domain_expires_7_days.sql +++ /dev/null @@ -1,13 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when date(expiration_date) - date(current_date) >= 7 then 'ok' - else 'alarm' - end as status, - title || ' set to expire in ' || extract(day from expiration_date - current_date) || ' days.' as reason, - -- Additional Dimensions - region, - account_id -from - aws_route53_domain; \ No newline at end of file diff --git a/query/route53/route53_domain_not_expired.sql b/query/route53/route53_domain_not_expired.sql deleted file mode 100644 index d788f27c..00000000 --- a/query/route53/route53_domain_not_expired.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when expiration_date < (current_date - interval '1' minute) then 'alarm' - else 'ok' - end as status, - case - when expiration_date < (current_date - interval '1' minute) then title || ' expired on ' || to_char(expiration_date, 'DD-Mon-YYYY') || '.' - else title || ' set to expire in ' || extract(day from expiration_date - current_date) || ' days.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_route53_domain; \ No newline at end of file diff --git a/query/route53/route53_domain_privacy_protection_enabled.sql b/query/route53/route53_domain_privacy_protection_enabled.sql deleted file mode 100644 index 0ec61751..00000000 --- a/query/route53/route53_domain_privacy_protection_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when admin_privacy then 'ok' - else 'alarm' - end as status, - case - when admin_privacy then title || ' privacy protection enabled.' - else title || ' privacy protection disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_route53_domain; \ No newline at end of file diff --git a/query/route53/route53_domain_transfer_lock_enabled.sql b/query/route53/route53_domain_transfer_lock_enabled.sql deleted file mode 100644 index 85bc0e1a..00000000 --- a/query/route53/route53_domain_transfer_lock_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when transfer_lock then 'ok' - else 'alarm' - end as status, - case - when transfer_lock then title || ' transfer lock enabled.' - else title || ' transfer lock disabled.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_route53_domain; \ No newline at end of file diff --git a/query/route53/route53_zone_query_logging_enabled.sql b/query/route53/route53_zone_query_logging_enabled.sql deleted file mode 100644 index 80d67c5f..00000000 --- a/query/route53/route53_zone_query_logging_enabled.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - id as resource, - case - when private_zone then 'skip' - when query_logging_configs is not null or jsonb_array_length(query_logging_configs) > 0 then 'ok' - else 'alarm' - end as status, - case - when private_zone then title || ' is private hosted zone.' - when query_logging_configs is not null or jsonb_array_length(query_logging_configs) > 0 then title || ' query logging to CloudWatch enabled.' - else title || ' query logging to CloudWatch disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_route53_zone; \ No newline at end of file diff --git a/query/s3/s3_bucket_acls_should_prohibit_user_access.sql b/query/s3/s3_bucket_acls_should_prohibit_user_access.sql deleted file mode 100644 index af0f25a0..00000000 --- a/query/s3/s3_bucket_acls_should_prohibit_user_access.sql +++ /dev/null @@ -1,49 +0,0 @@ -with bucket_acl_details as ( - select - arn, - title, - array[acl -> 'Owner' ->> 'ID'] as bucket_owner, - array_agg(grantee_id) as bucket_acl_permissions, - object_ownership_controls, - region, - account_id - from - aws_s3_bucket, - jsonb_path_query(acl, '$.Grants.Grantee.ID') as grantee_id - group by - arn, - title, - acl, - region, - account_id, - object_ownership_controls -), -bucket_acl_checks as ( - select - arn, - title, - to_jsonb(bucket_acl_permissions) - bucket_owner as additional_permissions, - object_ownership_controls, - region, - account_id - from - bucket_acl_details -) -select - -- Required Columns - arn as resource, - case - when object_ownership_controls -> 'Rules' @> '[{"ObjectOwnership": "BucketOwnerEnforced"} ]' then 'ok' - when jsonb_array_length(additional_permissions) = 0 then 'ok' - else 'alarm' - end status, - case - when object_ownership_controls -> 'Rules' @> '[{"ObjectOwnership": "BucketOwnerEnforced"} ]' then title || ' ACLs are disabled.' - when jsonb_array_length(additional_permissions) = 0 then title || ' does not have ACLs for user access.' - else title || ' has ACLs for user access.' - end reason, - -- Additional Dimensions - region, - account_id -from - bucket_acl_checks; diff --git a/query/s3/s3_bucket_cross_region_replication_enabled.sql b/query/s3/s3_bucket_cross_region_replication_enabled.sql deleted file mode 100644 index a7d1e73b..00000000 --- a/query/s3/s3_bucket_cross_region_replication_enabled.sql +++ /dev/null @@ -1,25 +0,0 @@ -with bucket_with_replication as ( - select - name, - r ->> 'Status' as rep_status - from - aws_s3_bucket, - jsonb_array_elements(replication -> 'Rules' ) as r -) -select - -- Required Columns - b.arn as resource, - case - when b.name = r.name and r.rep_status = 'Enabled' then 'ok' - else 'alarm' - end as status, - case - when b.name = r.name and r.rep_status = 'Enabled' then b.title || ' enabled with cross-region replication.' - else b.title || ' not enabled with cross-region replication.' - end as reason, - -- Additional Dimensions - b.region, - b.account_id -from - aws_s3_bucket b - left join bucket_with_replication r on b.name = r.name; \ No newline at end of file diff --git a/query/s3/s3_bucket_default_encryption_enabled.sql b/query/s3/s3_bucket_default_encryption_enabled.sql deleted file mode 100644 index 2de0fe34..00000000 --- a/query/s3/s3_bucket_default_encryption_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when server_side_encryption_configuration is not null then 'ok' - else 'alarm' - end status, - case - when server_side_encryption_configuration is not null then name || ' default encryption enabled.' - else name || ' default encryption disabled.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_s3_bucket \ No newline at end of file diff --git a/query/s3/s3_bucket_default_encryption_enabled_kms.sql b/query/s3/s3_bucket_default_encryption_enabled_kms.sql deleted file mode 100644 index 5db88493..00000000 --- a/query/s3/s3_bucket_default_encryption_enabled_kms.sql +++ /dev/null @@ -1,26 +0,0 @@ -with data as ( - select - distinct name - from - aws_s3_bucket, - jsonb_array_elements(server_side_encryption_configuration -> 'Rules') as rules - where - rules -> 'ApplyServerSideEncryptionByDefault' ->> 'KMSMasterKeyID' is not null - ) -select - -- Required Columns - b.arn as resource, - case - when d.name is not null then 'ok' - else 'alarm' - end status, - case - when d.name is not null then b.name || ' default encryption with KMS enabled.' - else b.name || ' default encryption with KMS disabled.' - end reason, - -- Additional Dimensions - b.region, - b.account_id -from - aws_s3_bucket as b - left join data as d on b.name = d.name; \ No newline at end of file diff --git a/query/s3/s3_bucket_enforces_ssl.sql b/query/s3/s3_bucket_enforces_ssl.sql deleted file mode 100644 index 2196d599..00000000 --- a/query/s3/s3_bucket_enforces_ssl.sql +++ /dev/null @@ -1,36 +0,0 @@ -with ssl_ok as ( - select - distinct name, - arn, - 'ok' as status - from - aws_s3_bucket, - jsonb_array_elements(policy_std -> 'Statement') as s, - jsonb_array_elements_text(s -> 'Principal' -> 'AWS') as p, - jsonb_array_elements_text(s -> 'Action') as a, - jsonb_array_elements_text(s -> 'Resource') as r, - jsonb_array_elements_text( - s -> 'Condition' -> 'Bool' -> 'aws:securetransport' - ) as ssl - where - p = '*' - and s ->> 'Effect' = 'Deny' - and ssl :: bool = false -) -select - -- Required Columns - b.arn as resource, - case - when ok.status = 'ok' then 'ok' - else 'alarm' - end status, - case - when ok.status = 'ok' then b.name || ' bucket policy enforces HTTPS.' - else b.name || ' bucket policy does not enforce HTTPS.' - end reason, - -- Additional Dimensions - b.region, - b.account_id -from - aws_s3_bucket as b - left join ssl_ok as ok on ok.name = b.name; \ No newline at end of file diff --git a/query/s3/s3_bucket_event_notifications_enabled.sql b/query/s3/s3_bucket_event_notifications_enabled.sql deleted file mode 100644 index beddb9eb..00000000 --- a/query/s3/s3_bucket_event_notifications_enabled.sql +++ /dev/null @@ -1,24 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when - event_notification_configuration ->> 'EventBridgeConfiguration' is null - and event_notification_configuration ->> 'LambdaFunctionConfigurations' is null - and event_notification_configuration ->> 'QueueConfigurations' is null - and event_notification_configuration ->> 'TopicConfigurations' is null then 'alarm' - else 'ok' - end as status, - case - when - event_notification_configuration ->> 'EventBridgeConfiguration' is null - and event_notification_configuration ->> 'LambdaFunctionConfigurations' is null - and event_notification_configuration ->> 'QueueConfigurations' is null - and event_notification_configuration ->> 'TopicConfigurations' is null then title || ' event notifications disabled.' - else title || ' event notifications enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_s3_bucket; diff --git a/query/s3/s3_bucket_logging_enabled.sql b/query/s3/s3_bucket_logging_enabled.sql deleted file mode 100644 index 50cf5dd5..00000000 --- a/query/s3/s3_bucket_logging_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when logging -> 'TargetBucket' is null then 'alarm' - else 'ok' - end as status, - case - when logging -> 'TargetBucket' is null then title || ' logging disabled.' - else title || ' logging enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_s3_bucket; \ No newline at end of file diff --git a/query/s3/s3_bucket_mfa_delete_enabled.sql b/query/s3/s3_bucket_mfa_delete_enabled.sql deleted file mode 100644 index ba703081..00000000 --- a/query/s3/s3_bucket_mfa_delete_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when versioning_mfa_delete then 'ok' - else 'alarm' - end status, - case - when versioning_mfa_delete then name || ' MFA delete enabled.' - else name || ' MFA delete disabled.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_s3_bucket; \ No newline at end of file diff --git a/query/s3/s3_bucket_object_lock_enabled.sql b/query/s3/s3_bucket_object_lock_enabled.sql deleted file mode 100644 index b8f20f00..00000000 --- a/query/s3/s3_bucket_object_lock_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when object_lock_configuration is null then 'alarm' - else 'ok' - end as status, - case - when object_lock_configuration is null then title || ' object lock not enabled.' - else title || ' object lock enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_s3_bucket; \ No newline at end of file diff --git a/query/s3/s3_bucket_object_logging_enabled.sql b/query/s3/s3_bucket_object_logging_enabled.sql deleted file mode 100644 index 25c66492..00000000 --- a/query/s3/s3_bucket_object_logging_enabled.sql +++ /dev/null @@ -1,64 +0,0 @@ -with object_logging_cloudtrails as ( - select - d ->> 'Type' as type, - replace(replace(v::text,'"',''),'/','') as bucket_arn - from - aws_cloudtrail_trail, - jsonb_array_elements(event_selectors) e, - jsonb_array_elements(e -> 'DataResources') as d, - jsonb_array_elements(d -> 'Values') v - where - d ->> 'Type' = 'AWS::S3::Object' -), object_logging_region as ( - select - region as cloudtrail_region, - replace(replace(v::text,'"',''),'/','') as bucket_arn - from - aws_cloudtrail_trail, - jsonb_array_elements(event_selectors) e, - jsonb_array_elements(e -> 'DataResources') as d, - jsonb_array_elements(d -> 'Values') v - where - d ->> 'Type' = 'AWS::S3::Object' - and replace(replace(v::text,'"',''),'/','') = 'arn:aws:s3' - group by - region, - bucket_arn -), -object_logging_region_advance_es as ( - select - region as cloudtrail_region - from - aws_cloudtrail_trail, - jsonb_array_elements(advanced_event_selectors) a, - jsonb_array_elements(a -> 'FieldSelectors') as f, - jsonb_array_elements_text(f -> 'Equals') e - where - e = 'AWS::S3::Object' - and f ->> 'Field' != 'eventCategory' - group by - region -) -select - -- Required Columns - distinct s.arn as resource, - case - when (s.arn = c.bucket_arn) - or (r.bucket_arn = 'arn:aws:s3' and r. cloudtrail_region = s.region ) - or a. cloudtrail_region = s.region then 'ok' - else 'alarm' - end as status, - case - when (s.arn = c.bucket_arn) - or (r.bucket_arn = 'arn:aws:s3' and r. cloudtrail_region = s.region ) - or a. cloudtrail_region = s.region then s.name || ' object logging enabled.' - else s.name || ' object logging not enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_s3_bucket as s - left join object_logging_cloudtrails as c on s.arn = c.bucket_arn - left join object_logging_region as r on r. cloudtrail_region = s.region - left join object_logging_region_advance_es as a on a. cloudtrail_region = s.region; \ No newline at end of file diff --git a/query/s3/s3_bucket_policy_restricts_cross_account_permission_changes.sql b/query/s3/s3_bucket_policy_restricts_cross_account_permission_changes.sql deleted file mode 100644 index 5cda5d93..00000000 --- a/query/s3/s3_bucket_policy_restricts_cross_account_permission_changes.sql +++ /dev/null @@ -1,40 +0,0 @@ -with cross_account_buckets as ( - select - distinct arn - from - aws_s3_bucket, - jsonb_array_elements(policy_std -> 'Statement') as s, - jsonb_array_elements_text(s -> 'Principal' -> 'AWS') as p, - string_to_array(p, ':') as pa, - jsonb_array_elements_text(s -> 'Action') as a - where - s ->> 'Effect' = 'Allow' - and ( - pa [5] != account_id - or p = '*' - ) - and a in ( - 's3:deletebucketpolicy', - 's3:putbucketacl', - 's3:putbucketpolicy', - 's3:putencryptionconfiguration', - 's3:putobjectacl' - ) -) -select - -- Required Columns - a.arn as resource, - case - when b.arn is null then 'ok' - else 'alarm' - end as status, - case - when b.arn is null then title || ' restricts cross-account bucket access.' - else title || ' allows cross-account bucket access.' - end as reason, - -- Additionl Dimensions - a.region, - a.account_id -from - aws_s3_bucket a - left join cross_account_buckets b on a.arn = b.arn; \ No newline at end of file diff --git a/query/s3/s3_bucket_protected_by_macie.sql b/query/s3/s3_bucket_protected_by_macie.sql deleted file mode 100644 index e8128dd9..00000000 --- a/query/s3/s3_bucket_protected_by_macie.sql +++ /dev/null @@ -1,27 +0,0 @@ -with bucket_list as ( - select - trim(b::text, '"' ) as bucket_name - from - aws_macie2_classification_job, - jsonb_array_elements(s3_job_definition -> 'BucketDefinitions') as d, - jsonb_array_elements(d -> 'Buckets') as b -) -select - -- Required Columns - b.arn as resource, - case - when b.region = any(array['us-gov-east-1', 'us-gov-west-1']) then 'skip' - when l.bucket_name is not null then 'ok' - else 'alarm' - end status, - case - when b.region = any(array['us-gov-east-1', 'us-gov-west-1']) then b.title || ' not protected by Macie as Macie is not supported in ' || b.region || '.' - when l.bucket_name is not null then b.title || ' protected by Macie.' - else b.title || ' not protected by Macie.' - end reason, - -- Additional Dimensions - b.region, - b.account_id -from - aws_s3_bucket as b - left join bucket_list as l on b.name = l.bucket_name; diff --git a/query/s3/s3_bucket_public_access_blocked.sql b/query/s3/s3_bucket_public_access_blocked.sql deleted file mode 100644 index 00ab4378..00000000 --- a/query/s3/s3_bucket_public_access_blocked.sql +++ /dev/null @@ -1,28 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when - block_public_acls - and block_public_policy - and ignore_public_acls - and restrict_public_buckets - then - 'ok' - else - 'alarm' - end status, - case - when - block_public_acls - and block_public_policy - and ignore_public_acls - and restrict_public_buckets - then name || ' blocks public access.' - else name || ' does not block public access.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_s3_bucket \ No newline at end of file diff --git a/query/s3/s3_bucket_restrict_public_read_access.sql b/query/s3/s3_bucket_restrict_public_read_access.sql deleted file mode 100644 index 49419601..00000000 --- a/query/s3/s3_bucket_restrict_public_read_access.sql +++ /dev/null @@ -1,57 +0,0 @@ -with public_acl as ( - select - distinct name - from - aws_s3_bucket, - jsonb_array_elements(acl -> 'Grants') as grants - where - (grants -> 'Grantee' ->> 'URI' = 'http://acs.amazonaws.com/groups/global/AllUsers' - or grants -> 'Grantee' ->> 'URI' = 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers') - and ( - grants ->> 'Permission' = 'FULL_CONTROL' - or grants ->> 'Permission' = 'READ_ACP' - or grants ->> 'Permission' = 'READ' - ) - ),read_access_policy as ( - select - distinct name - from - aws_s3_bucket, - jsonb_array_elements(policy_std -> 'Statement') as s, - jsonb_array_elements_text(s -> 'Action') as action - where - s ->> 'Effect' = 'Allow' - and ( - s -> 'Principal' -> 'AWS' = '["*"]' - or s ->> 'Principal' = '*' - ) - and ( - action = '*' - or action = '*:*' - or action = 's3:*' - or action ilike 's3:get%' - or action ilike 's3:list%' - ) -) -select - -- Required Columns - b.arn as resource, - case - when (block_public_acls or a.name is null) and not bucket_policy_is_public then 'ok' - when (block_public_acls or a.name is null) and (bucket_policy_is_public and block_public_policy) then 'ok' - when (block_public_acls or a.name is null) and (bucket_policy_is_public and p.name is null) then 'ok' - else 'alarm' - end status, - case - when (block_public_acls or a.name is null) and not bucket_policy_is_public then b.title || ' not publicly readable.' - when (block_public_acls or a.name is null) and (bucket_policy_is_public and block_public_policy) then b.title || ' not publicly readable.' - when (block_public_acls or a.name is null) and (bucket_policy_is_public and p.name is null) then b.title || ' not publicly readable.' - else b.title || ' publicly readable.' - end reason, - -- Additional Dimensions - b.region, - b.account_id -from - aws_s3_bucket as b - left join public_acl as a on b.name = a.name - left join read_access_policy as p on b.name = p.name; \ No newline at end of file diff --git a/query/s3/s3_bucket_restrict_public_write_access.sql b/query/s3/s3_bucket_restrict_public_write_access.sql deleted file mode 100644 index 845ad912..00000000 --- a/query/s3/s3_bucket_restrict_public_write_access.sql +++ /dev/null @@ -1,62 +0,0 @@ -with public_acl as ( - select - distinct name - from - aws_s3_bucket, - jsonb_array_elements(acl -> 'Grants') as grants - where - (grants -> 'Grantee' ->> 'URI' = 'http://acs.amazonaws.com/groups/global/AllUsers' - or grants -> 'Grantee' ->> 'URI' = 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers') - and ( - grants ->> 'Permission' = 'FULL_CONTROL' - or grants ->> 'Permission' = 'WRITE_ACP' - or grants ->> 'Permission' = 'WRITE' - ) -), write_access_policy as ( - select - distinct name - from - aws_s3_bucket, - jsonb_array_elements(policy_std -> 'Statement') as s, - jsonb_array_elements_text(s -> 'Action') as action - where - s ->> 'Effect' = 'Allow' - and ( - s -> 'Principal' -> 'AWS' = '["*"]' - or s ->> 'Principal' = '*' - ) - and ( - action = '*' - or action = '*:*' - or action = 's3:*' - or action ilike 's3:put%' - or action ilike 's3:delete%' - or action ilike 's3:create%' - or action ilike 's3:update%' - or action ilike 's3:replicate%' - or action ilike 's3:restore%' - ) -) -select - -- Required Columns - b.arn as resource, - bucket_policy_is_public, - case - when (block_public_acls or a.name is null) and not bucket_policy_is_public then 'ok' - when (block_public_acls or a.name is null) and (bucket_policy_is_public and block_public_policy) then 'ok' - when bucket_policy_is_public and p.name is null then 'ok' - else 'alarm' - end status, - case - when (block_public_acls or a.name is null ) and not bucket_policy_is_public then b.title || ' not publicly writable.' - when (block_public_acls or a.name is null) and (bucket_policy_is_public and block_public_policy) then b.title || ' not publicly writable.' - when (block_public_acls or a.name is null) and (bucket_policy_is_public and p.name is null) then b.title || ' not publicly writable.' - else b.title || ' publicly writable.' - end reason, - -- Additional Dimensions - b.region, - b.account_id -from - aws_s3_bucket as b - left join public_acl as a on b.name = a.name - left join write_access_policy as p on b.name = p.name \ No newline at end of file diff --git a/query/s3/s3_bucket_static_website_hosting_disabled.sql b/query/s3/s3_bucket_static_website_hosting_disabled.sql deleted file mode 100644 index 44ae44af..00000000 --- a/query/s3/s3_bucket_static_website_hosting_disabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when website_configuration -> 'IndexDocument' ->> 'Suffix' is not null then 'alarm' - else 'ok' - end status, - case - when website_configuration -> 'IndexDocument' ->> 'Suffix' is not null then name || ' static website hosting enabled.' - else name || ' static website hosting disabled.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_s3_bucket; \ No newline at end of file diff --git a/query/s3/s3_bucket_versioning_and_lifecycle_policy_enabled.sql b/query/s3/s3_bucket_versioning_and_lifecycle_policy_enabled.sql deleted file mode 100644 index 350e9994..00000000 --- a/query/s3/s3_bucket_versioning_and_lifecycle_policy_enabled.sql +++ /dev/null @@ -1,28 +0,0 @@ -with lifecycle_rules_enabled as ( - select - arn - from - aws_s3_bucket, - jsonb_array_elements(lifecycle_rules) as r - where - r ->> 'Status' = 'Enabled' -) -select - -- Required Columns - b.arn as resource, - case - when not versioning_enabled then 'alarm' - when versioning_enabled and r.arn is not null then 'ok' - else 'alarm' - end status, - case - when not versioning_enabled then name || ' versioning diabled.' - when versioning_enabled and r.arn is not null then ' lifecycle policy configured.' - else name || ' lifecycle policy not configured.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_s3_bucket as b - left join lifecycle_rules_enabled as r on r.arn = b.arn; \ No newline at end of file diff --git a/query/s3/s3_bucket_versioning_enabled.sql b/query/s3/s3_bucket_versioning_enabled.sql deleted file mode 100644 index 8ebb8acb..00000000 --- a/query/s3/s3_bucket_versioning_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when versioning_enabled then 'ok' - else 'alarm' - end status, - case - when versioning_enabled then name || ' versioning enabled.' - else name || ' versioning disabled.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_s3_bucket \ No newline at end of file diff --git a/query/s3/s3_public_access_block_account.sql b/query/s3/s3_public_access_block_account.sql deleted file mode 100644 index 17e00725..00000000 --- a/query/s3/s3_public_access_block_account.sql +++ /dev/null @@ -1,29 +0,0 @@ -select - -- Required Columns - 'arn' || ':' || 'aws' || ':::' || account_id as resource, - case - when block_public_acls - and block_public_policy - and ignore_public_acls - and restrict_public_buckets - then 'ok' - else 'alarm' - end as status, - case - when block_public_acls - and block_public_policy - and ignore_public_acls - and restrict_public_buckets - then 'Account level public access blocks enabled.' - else 'Account level public access blocks not enabled for: ' || - concat_ws(', ', - case when not (block_public_acls ) then 'block_public_acls' end, - case when not (block_public_policy) then 'block_public_policy' end, - case when not (ignore_public_acls ) then 'ignore_public_acls' end, - case when not (restrict_public_buckets) then 'restrict_public_buckets' end - ) || '.' - end as reason, - -- Additional Dimensions - account_id -from - aws_s3_account_settings; \ No newline at end of file diff --git a/query/s3/s3_public_access_block_bucket.sql b/query/s3/s3_public_access_block_bucket.sql deleted file mode 100644 index 6a1438d6..00000000 --- a/query/s3/s3_public_access_block_bucket.sql +++ /dev/null @@ -1,30 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when block_public_acls - and block_public_policy - and ignore_public_acls - and restrict_public_buckets - then 'ok' - else 'alarm' - end as status, - case - when block_public_acls - and block_public_policy - and ignore_public_acls - and restrict_public_buckets - then name || ' all public access blocks enabled.' - else name || ' not enabled for: ' || - concat_ws(', ', - case when not block_public_acls then 'block_public_acls' end, - case when not block_public_policy then 'block_public_policy' end, - case when not ignore_public_acls then 'ignore_public_acls' end, - case when not restrict_public_buckets then 'restrict_public_buckets' end - ) || '.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_s3_bucket; diff --git a/query/s3/s3_public_access_block_bucket_account.sql b/query/s3/s3_public_access_block_bucket_account.sql deleted file mode 100644 index 94c17366..00000000 --- a/query/s3/s3_public_access_block_bucket_account.sql +++ /dev/null @@ -1,33 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when (bucket.block_public_acls or s3account.block_public_acls) - and (bucket.block_public_policy or s3account.block_public_policy) - and (bucket.ignore_public_acls or s3account.ignore_public_acls) - and (bucket.restrict_public_buckets or s3account.restrict_public_buckets) - then 'ok' - else 'alarm' - end as status, - case - when (bucket.block_public_acls or s3account.block_public_acls) - and (bucket.block_public_policy or s3account.block_public_policy) - and (bucket.ignore_public_acls or s3account.ignore_public_acls) - and (bucket.restrict_public_buckets or s3account.restrict_public_buckets) - then name || ' all public access blocks enabled.' - else name || ' not enabled for: ' || - concat_ws(', ', - case when not (bucket.block_public_acls or s3account.block_public_acls) then 'block_public_acls' end, - case when not (bucket.block_public_policy or s3account.block_public_policy) then 'block_public_policy' end, - case when not (bucket.ignore_public_acls or s3account.ignore_public_acls) then 'ignore_public_acls' end, - case when not (bucket.restrict_public_buckets or s3account.restrict_public_buckets) then 'restrict_public_buckets' end - ) || '.' - end as reason, - -- Additional Dimensions - bucket.region, - bucket.account_id -from - aws_s3_bucket as bucket, - aws_s3_account_settings as s3account -where - s3account.account_id = bucket.account_id; diff --git a/query/sagemaker/sagemaker_endpoint_configuration_encryption_at_rest_enabled.sql b/query/sagemaker/sagemaker_endpoint_configuration_encryption_at_rest_enabled.sql deleted file mode 100644 index 941e6b02..00000000 --- a/query/sagemaker/sagemaker_endpoint_configuration_encryption_at_rest_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when kms_key_id is null then 'alarm' - else 'ok' - end as status, - case - when kms_key_id is null then title || ' encryption at rest disabled.' - else title || ' encryption at rest enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_sagemaker_endpoint_configuration; \ No newline at end of file diff --git a/query/sagemaker/sagemaker_model_in_vpc.sql b/query/sagemaker/sagemaker_model_in_vpc.sql deleted file mode 100644 index 2d525f4e..00000000 --- a/query/sagemaker/sagemaker_model_in_vpc.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when vpc_config is not null then 'ok' - else 'alarm' - end as status, - case - when vpc_config is not null then title || ' in VPC.' - else title || ' not in VPC.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_sagemaker_model; \ No newline at end of file diff --git a/query/sagemaker/sagemaker_model_network_isolation_enabled.sql b/query/sagemaker/sagemaker_model_network_isolation_enabled.sql deleted file mode 100644 index c21bf3eb..00000000 --- a/query/sagemaker/sagemaker_model_network_isolation_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when enable_network_isolation then 'ok' - else 'alarm' - end as status, - case - when enable_network_isolation then title || ' network isolation enabled.' - else title || ' network isolation disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_sagemaker_model; \ No newline at end of file diff --git a/query/sagemaker/sagemaker_notebook_instance_direct_internet_access_disabled.sql b/query/sagemaker/sagemaker_notebook_instance_direct_internet_access_disabled.sql deleted file mode 100644 index 94a522df..00000000 --- a/query/sagemaker/sagemaker_notebook_instance_direct_internet_access_disabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when direct_internet_access = 'Enabled' then 'alarm' - else 'ok' - end status, - case - when direct_internet_access = 'Enabled' then title || ' direct internet access enabled.' - else title || ' direct internet access disabled.' - end reason, - -- Additional Dimentions - region, - account_id -from - aws_sagemaker_notebook_instance; \ No newline at end of file diff --git a/query/sagemaker/sagemaker_notebook_instance_encrypted_with_kms_cmk.sql b/query/sagemaker/sagemaker_notebook_instance_encrypted_with_kms_cmk.sql deleted file mode 100644 index cec7d0b6..00000000 --- a/query/sagemaker/sagemaker_notebook_instance_encrypted_with_kms_cmk.sql +++ /dev/null @@ -1,19 +0,0 @@ -select - -- Required Columns - i.arn as resource, - case - when kms_key_id is null then 'alarm' - when k.key_manager = 'CUSTOMER' then 'ok' - else 'alarm' - end as status, - case - when kms_key_id is null then i.title || ' encryption disabled.' - when k.key_manager = 'CUSTOMER' then i.title || ' encryption at rest with CMK enabled.' - else i.title || ' encryption at rest with CMK disabled.' - end as reason, - -- Additional Dimensions - i.region, - i.account_id -from - aws_sagemaker_notebook_instance as i - left join aws_kms_key as k on k.arn = i.kms_key_id; \ No newline at end of file diff --git a/query/sagemaker/sagemaker_notebook_instance_encryption_at_rest_enabled.sql b/query/sagemaker/sagemaker_notebook_instance_encryption_at_rest_enabled.sql deleted file mode 100644 index 9ee1907a..00000000 --- a/query/sagemaker/sagemaker_notebook_instance_encryption_at_rest_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when kms_key_id is null then 'alarm' - else 'ok' - end as status, - case - when kms_key_id is null then title || ' encryption at rest enabled' - else title || ' encryption at rest not enabled' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_sagemaker_notebook_instance; \ No newline at end of file diff --git a/query/sagemaker/sagemaker_notebook_instance_in_vpc.sql b/query/sagemaker/sagemaker_notebook_instance_in_vpc.sql deleted file mode 100644 index 07276985..00000000 --- a/query/sagemaker/sagemaker_notebook_instance_in_vpc.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when subnet_id is not null then 'ok' - else 'alarm' - end as status, - case - when subnet_id is not null then title || ' in VPC.' - else title || ' not in VPC.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_sagemaker_notebook_instance; \ No newline at end of file diff --git a/query/sagemaker/sagemaker_notebook_instance_root_access_disabled.sql b/query/sagemaker/sagemaker_notebook_instance_root_access_disabled.sql deleted file mode 100644 index 0360edd5..00000000 --- a/query/sagemaker/sagemaker_notebook_instance_root_access_disabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when root_access = 'Disabled' then 'ok' - else 'alarm' - end as status, - case - when root_access = 'Disabled' then title || ' root access disabled.' - else title || ' root access enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_sagemaker_notebook_instance; \ No newline at end of file diff --git a/query/sagemaker/sagemaker_training_job_in_vpc.sql b/query/sagemaker/sagemaker_training_job_in_vpc.sql deleted file mode 100644 index d9d3946e..00000000 --- a/query/sagemaker/sagemaker_training_job_in_vpc.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when vpc_config is not null then 'ok' - else 'alarm' - end as status, - case - when vpc_config is not null then title || ' in VPC.' - else title || ' not in VPC.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_sagemaker_training_job; \ No newline at end of file diff --git a/query/sagemaker/sagemaker_training_job_inter_container_traffic_encryption_enabled.sql b/query/sagemaker/sagemaker_training_job_inter_container_traffic_encryption_enabled.sql deleted file mode 100644 index 3a6965f5..00000000 --- a/query/sagemaker/sagemaker_training_job_inter_container_traffic_encryption_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when enable_inter_container_traffic_encryption then 'ok' - else 'alarm' - end as status, - case - when enable_inter_container_traffic_encryption then title || ' inter-container traffic encryption enabled.' - else title || ' inter-container traffic encryption disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_sagemaker_training_job; \ No newline at end of file diff --git a/query/sagemaker/sagemaker_training_job_network_isolation_enabled.sql b/query/sagemaker/sagemaker_training_job_network_isolation_enabled.sql deleted file mode 100644 index e0262f85..00000000 --- a/query/sagemaker/sagemaker_training_job_network_isolation_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when enable_network_isolation then 'ok' - else 'alarm' - end as status, - case - when enable_network_isolation then title || ' network isolation enabled.' - else title || ' network isolation disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_sagemaker_training_job; \ No newline at end of file diff --git a/query/sagemaker/sagemaker_training_job_volume_and_data_encryption_enabled.sql b/query/sagemaker/sagemaker_training_job_volume_and_data_encryption_enabled.sql deleted file mode 100644 index d8bd1403..00000000 --- a/query/sagemaker/sagemaker_training_job_volume_and_data_encryption_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when output_data_config ->> 'KmsKeyId' is null or output_data_config ->> 'KmsKeyId' = '' then 'alarm' - else 'ok' - end as status, - case - when output_data_config ->> 'KmsKeyId' is null or output_data_config ->> 'KmsKeyId' = '' then title || ' volume and output data encryption disabled.' - else title || ' volume and output data encryption enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_sagemaker_training_job; \ No newline at end of file diff --git a/query/secretsmanager/secretsmanager_secret_automatic_rotation_enabled.sql b/query/secretsmanager/secretsmanager_secret_automatic_rotation_enabled.sql deleted file mode 100644 index 048e6050..00000000 --- a/query/secretsmanager/secretsmanager_secret_automatic_rotation_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when rotation_rules is null then 'alarm' - else 'ok' - end as status, - case - when rotation_rules is null then title || ' automatic rotation not enabled.' - else title || ' automatic rotation enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_secretsmanager_secret; diff --git a/query/secretsmanager/secretsmanager_secret_automatic_rotation_lambda_enabled.sql b/query/secretsmanager/secretsmanager_secret_automatic_rotation_lambda_enabled.sql deleted file mode 100644 index f64ce0d0..00000000 --- a/query/secretsmanager/secretsmanager_secret_automatic_rotation_lambda_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when rotation_rules is not null and rotation_lambda_arn is not null then 'ok' - else 'alarm' - end as status, - case - when rotation_rules is not null and rotation_lambda_arn is not null then title || ' scheduled for rotation using Lambda function.' - else title || ' automatic rotation using Lambda function disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_secretsmanager_secret; diff --git a/query/secretsmanager/secretsmanager_secret_encrypted_with_kms_cmk.sql b/query/secretsmanager/secretsmanager_secret_encrypted_with_kms_cmk.sql deleted file mode 100644 index 34279c1c..00000000 --- a/query/secretsmanager/secretsmanager_secret_encrypted_with_kms_cmk.sql +++ /dev/null @@ -1,30 +0,0 @@ -with encryption_keys as ( - select - distinct s.arn, - k.aliases as alias - from - aws_secretsmanager_secret as s - left join aws_kms_key as k on k.arn = s.kms_key_id - where - jsonb_array_length(k.aliases) > 0 -) -select - -- Required Columns - s.arn as resource, - case - when kms_key_id is null - or kms_key_id = 'alias/aws/secretsmanager' - or k.alias @> '[{"AliasName":"alias/aws/secretsmanager"}]'then 'alarm' - else 'ok' - end as status, - case - when kms_key_id is null then title || ' not encrypted with KMS.' - when kms_key_id = 'alias/aws/secretsmanager' or k.alias @> '[{"AliasName":"alias/aws/secretsmanager"}]' then title || ' encrypted with AWS managed key.' - else title || ' encrypted with CMK.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_secretsmanager_secret as s - left join encryption_keys as k on s.arn = k.arn; diff --git a/query/secretsmanager/secretsmanager_secret_last_changed_90_day.sql b/query/secretsmanager/secretsmanager_secret_last_changed_90_day.sql deleted file mode 100644 index 12aa665d..00000000 --- a/query/secretsmanager/secretsmanager_secret_last_changed_90_day.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when last_changed_date is null then 'alarm' - when date(current_date) - date(last_changed_date) <= 90 then 'ok' - else 'alarm' - end as status, - case - when last_changed_date is null then title || ' never rotated.' - else - title || ' last rotated ' || extract(day from current_timestamp - last_changed_date) || ' day(s) ago.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_secretsmanager_secret; \ No newline at end of file diff --git a/query/secretsmanager/secretsmanager_secret_last_used_1_day.sql b/query/secretsmanager/secretsmanager_secret_last_used_1_day.sql deleted file mode 100644 index bee47d47..00000000 --- a/query/secretsmanager/secretsmanager_secret_last_used_1_day.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when date(last_accessed_date) - date(created_date) >= 1 then 'ok' - else 'alarm' - end as status, - case - when date(last_accessed_date)- date(created_date) >= 1 then title || ' recently used.' - else title || ' not used recently.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_secretsmanager_secret; diff --git a/query/secretsmanager/secretsmanager_secret_rotated_as_scheduled.sql b/query/secretsmanager/secretsmanager_secret_rotated_as_scheduled.sql deleted file mode 100644 index 0841126d..00000000 --- a/query/secretsmanager/secretsmanager_secret_rotated_as_scheduled.sql +++ /dev/null @@ -1,28 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when primary_region is not null and region != primary_region then 'skip' -- Replica secret - when rotation_rules is null then 'alarm' -- Rotation not enabled - when last_rotated_date is null - and (date(current_date) - date(created_date)) <= (rotation_rules -> 'AutomaticallyAfterDays')::integer then 'ok' -- New secret not due for rotation yet - when last_rotated_date is null - and (date(current_date) - date(created_date)) > (rotation_rules -> 'AutomaticallyAfterDays')::integer then 'alarm' -- New secret overdue for rotation - when last_rotated_date is not null - and (date(current_date) - date(last_rotated_date)) > (rotation_rules -> 'AutomaticallyAfterDays')::integer then 'alarm' -- Secret has been rotated before but is overdue for another rotation - end as status, - case - when primary_region is not null and region != primary_region then title || ' is a replica.' - when rotation_rules is null then title || ' rotation not enabled.' - when last_rotated_date is null - and (date(current_date) - date(created_date)) <= (rotation_rules -> 'AutomaticallyAfterDays')::integer then title || ' scheduled for rotation.' - when last_rotated_date is null - and (date(current_date) - date(created_date)) > (rotation_rules -> 'AutomaticallyAfterDays')::integer then title || ' not rotated as per schedule.' - when last_rotated_date is not null - and (date(current_date) - date(last_rotated_date)) > (rotation_rules -> 'AutomaticallyAfterDays')::integer then title || ' not rotated as per schedule.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_secretsmanager_secret; diff --git a/query/secretsmanager/secretsmanager_secret_unused_90_day.sql b/query/secretsmanager/secretsmanager_secret_unused_90_day.sql deleted file mode 100644 index 7023c6ef..00000000 --- a/query/secretsmanager/secretsmanager_secret_unused_90_day.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when last_accessed_date is null then 'alarm' - when date(current_date) - date(last_accessed_date) <= 90 then 'ok' - else 'alarm' - end as status, - case - when last_accessed_date is null then title || ' never accessed.' - else - title || ' last used ' || extract(day from current_timestamp - last_accessed_date) || ' day(s) ago.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_secretsmanager_secret; \ No newline at end of file diff --git a/query/securityhub/securityhub_enabled.sql b/query/securityhub/securityhub_enabled.sql deleted file mode 100644 index 26ae5c2c..00000000 --- a/query/securityhub/securityhub_enabled.sql +++ /dev/null @@ -1,22 +0,0 @@ -select - -- Required Columns - 'arn:' || r.partition || '::' || r.region || ':' || r.account_id as resource, - case - when r.region = any(array['af-south-1', 'eu-south-1', 'cn-north-1', 'cn-northwest-1', 'ap-northeast-3']) then 'skip' - -- Skip any regions that are disabled in the account. - when r.opt_in_status = 'not-opted-in' then 'skip' - when h.hub_arn is not null then 'ok' - else 'alarm' - end as status, - case - when r.region = any(array['af-south-1', 'eu-south-1', 'cn-north-1', 'cn-northwest-1', 'ap-northeast-3']) then r.region || ' region not supported.' - when r.opt_in_status = 'not-opted-in' then r.region || ' region is disabled.' - when h.hub_arn is not null then 'Security Hub enabled in ' || r.region || '.' - else 'Security Hub disabled in ' || r.region || '.' - end as reason, - -- Additional Dimensions - r.region, - r.account_id -from - aws_region as r - left join aws_securityhub_hub as h on r.account_id = h.account_id and r.name = h.region; diff --git a/query/sns/sns_topic_encrypted_at_rest.sql b/query/sns/sns_topic_encrypted_at_rest.sql deleted file mode 100644 index 18436baf..00000000 --- a/query/sns/sns_topic_encrypted_at_rest.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - topic_arn as resource, -case - when kms_master_key_id is null then 'alarm' - else 'ok' -end as status, -case - when kms_master_key_id is null then title || ' encryption at rest disabled.' - else title || ' encryption at rest enabled.' -end as reason, - -- Additional Dimensions - region, - account_id -from - aws_sns_topic; diff --git a/query/sns/sns_topic_notification_delivery_status_enabled.sql b/query/sns/sns_topic_notification_delivery_status_enabled.sql deleted file mode 100644 index 96233f1d..00000000 --- a/query/sns/sns_topic_notification_delivery_status_enabled.sql +++ /dev/null @@ -1,24 +0,0 @@ -select - -- Required Columns - topic_arn as resource, - case - when application_failure_feedback_role_arn is null - and firehose_failure_feedback_role_arn is null - and http_failure_feedback_role_arn is null - and lambda_failure_feedback_role_arn is null - and sqs_failure_feedback_role_arn is null then 'alarm' - else 'ok' - end as status, - case - when application_failure_feedback_role_arn is null - and firehose_failure_feedback_role_arn is null - and http_failure_feedback_role_arn is null - and lambda_failure_feedback_role_arn is null - and sqs_failure_feedback_role_arn is null then title || ' has delivery status logging for notification messages disabled.' - else title || ' has delivery status logging for notification messages enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_sns_topic; \ No newline at end of file diff --git a/query/sns/sns_topic_policy_prohibit_public_access.sql b/query/sns/sns_topic_policy_prohibit_public_access.sql deleted file mode 100644 index 9325564f..00000000 --- a/query/sns/sns_topic_policy_prohibit_public_access.sql +++ /dev/null @@ -1,34 +0,0 @@ -with wildcard_action_policies as ( - select - topic_arn, - count(*) as statements_num - from - aws_sns_topic, - jsonb_array_elements(policy_std -> 'Statement') as s - where - s ->> 'Effect' = 'Allow' - and ( - ( s -> 'Principal' -> 'AWS') = '["*"]' - or s ->> 'Principal' = '*' - ) - group by - topic_arn -) -select - -- Required Columns - t.topic_arn as resource, - case - when p.topic_arn is null then 'ok' - else 'alarm' - end as status, - case - when p.topic_arn is null then title || ' does not allow public access.' - else title || ' contains ' || coalesce(p.statements_num,0) || - ' statements that allows public access.' - end as reason, - -- Additional Dimensions - t.region, - t.account_id -from - aws_sns_topic as t - left join wildcard_action_policies as p on p.topic_arn = t.topic_arn; \ No newline at end of file diff --git a/query/sqs/sqs_queue_dead_letter_queue_configured.sql b/query/sqs/sqs_queue_dead_letter_queue_configured.sql deleted file mode 100644 index 35b02092..00000000 --- a/query/sqs/sqs_queue_dead_letter_queue_configured.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - queue_arn as resource, - case - when redrive_policy is not null then 'ok' - else 'alarm' - end as status, - case - when redrive_policy is not null then title || ' configured with dead-letter queue.' - else title || ' not configured with dead-letter queue.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_sqs_queue; \ No newline at end of file diff --git a/query/sqs/sqs_queue_encrypted_at_rest.sql b/query/sqs/sqs_queue_encrypted_at_rest.sql deleted file mode 100644 index b29a54d7..00000000 --- a/query/sqs/sqs_queue_encrypted_at_rest.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - queue_arn as resource, - case - when kms_master_key_id is null then 'alarm' - else 'ok' - end as status, - case - when kms_master_key_id is null then title || ' encryption at rest disabled.' - else title || ' encryption at rest enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_sqs_queue; \ No newline at end of file diff --git a/query/sqs/sqs_queue_policy_prohibit_public_access.sql b/query/sqs/sqs_queue_policy_prohibit_public_access.sql deleted file mode 100644 index c69ec96c..00000000 --- a/query/sqs/sqs_queue_policy_prohibit_public_access.sql +++ /dev/null @@ -1,34 +0,0 @@ -with wildcard_action_policies as ( - select - queue_arn, - count(*) as statements_num - from - aws_sqs_queue, - jsonb_array_elements(policy_std -> 'Statement') as s - where - s ->> 'Effect' = 'Allow' - and ( - ( s -> 'Principal' -> 'AWS') = '["*"]' - or s ->> 'Principal' = '*' - ) - group by - queue_arn -) -select - -- Required Columns - q.queue_arn as resource, - case - when p.queue_arn is null then 'ok' - else 'alarm' - end as status, - case - when p.queue_arn is null then title || ' does not allow public access.' - else title || ' contains ' || coalesce(p.statements_num,0) || - ' statements that allows public access.' - end as reason, - -- Additional Dimensions - q.region, - q.account_id -from - aws_sqs_queue as q - left join wildcard_action_policies as p on q.queue_arn = p.queue_arn; \ No newline at end of file diff --git a/query/ssm/ec2_instance_ssm_managed.sql b/query/ssm/ec2_instance_ssm_managed.sql deleted file mode 100644 index 488ca742..00000000 --- a/query/ssm/ec2_instance_ssm_managed.sql +++ /dev/null @@ -1,19 +0,0 @@ -select - -- Required Columns - i.arn as resource, - case - when i.instance_state = 'stopped' then 'info' - when m.instance_id is null then 'alarm' - else 'ok' - end as status, - case - when i.instance_state = 'stopped' then i.title || ' is in stopped state.' - when m.instance_id is null then i.title || ' not managed by AWS SSM.' - else i.title || ' managed by AWS SSM.' - end as reason, - -- Additional Dimentions - i.region, - i.account_id -from - aws_ec2_instance i - left join aws_ssm_managed_instance m on m.instance_id = i.instance_id; diff --git a/query/ssm/ssm_managed_instance_compliance_association_compliant.sql b/query/ssm/ssm_managed_instance_compliance_association_compliant.sql deleted file mode 100644 index c2771465..00000000 --- a/query/ssm/ssm_managed_instance_compliance_association_compliant.sql +++ /dev/null @@ -1,20 +0,0 @@ -select - -- Required Columns - id as resource, - case - when c.status = 'COMPLIANT' then 'ok' - else 'alarm' - end as status, - case - when c.status = 'COMPLIANT' then c.resource_id || ' association ' || c.title || ' is compliant.' - else c.resource_id || ' association ' || c.title || ' is non-compliant.' - end as reason, - -- Additional Dimensions - c.region, - c.account_id -from - aws_ssm_managed_instance as i, - aws_ssm_managed_instance_compliance as c -where - c.resource_id = i.instance_id - and c.compliance_type = 'Association'; \ No newline at end of file diff --git a/query/ssm/ssm_managed_instance_compliance_patch_compliant.sql b/query/ssm/ssm_managed_instance_compliance_patch_compliant.sql deleted file mode 100644 index b5620efe..00000000 --- a/query/ssm/ssm_managed_instance_compliance_patch_compliant.sql +++ /dev/null @@ -1,20 +0,0 @@ -select - -- Required Columns - id as resource, - case - when c.status = 'COMPLIANT' then 'ok' - else 'alarm' - end as status, - case - when c.status = 'COMPLIANT' then c.resource_id || ' patch ' || c.title || ' is compliant.' - else c.resource_id || ' patch ' || c.title || ' is non-compliant.' - end as reason, - -- Additional Dimensions - c.region, - c.account_id -from - aws_ssm_managed_instance as i, - aws_ssm_managed_instance_compliance as c -where - c.resource_id = i.instance_id - and c.compliance_type = 'Patch'; \ No newline at end of file diff --git a/query/vpc/vpc_configured_to_use_vpc_endpoints.sql b/query/vpc/vpc_configured_to_use_vpc_endpoints.sql deleted file mode 100644 index 760e2c64..00000000 --- a/query/vpc/vpc_configured_to_use_vpc_endpoints.sql +++ /dev/null @@ -1,30 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when vpc_id not in ( - select - vpc_id - from - aws_vpc_endpoint - where - service_name like 'com.amazonaws.' || region || '.ec2' - ) then 'alarm' - else 'ok' - end as status, - case - when vpc_id not in ( - select - vpc_id - from - aws_vpc_endpoint - where - service_name like 'com.amazonaws.' || region || '.ec2' - ) then title || ' not configured to use VPC endpoints.' - else title || ' configured to use VPC endpoints.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_vpc; \ No newline at end of file diff --git a/query/vpc/vpc_default_security_group_restricts_all_traffic.sql b/query/vpc/vpc_default_security_group_restricts_all_traffic.sql deleted file mode 100644 index 897981c9..00000000 --- a/query/vpc/vpc_default_security_group_restricts_all_traffic.sql +++ /dev/null @@ -1,23 +0,0 @@ -select - -- Required Columns - arn resource, - case - when jsonb_array_length(ip_permissions) = 0 and jsonb_array_length(ip_permissions_egress) = 0 then 'ok' - else 'alarm' - end status, - case - when jsonb_array_length(ip_permissions) > 0 and jsonb_array_length(ip_permissions_egress) > 0 - then 'Default security group ' || group_id || ' has inbound and outbound rules.' - when jsonb_array_length(ip_permissions) > 0 and jsonb_array_length(ip_permissions_egress) = 0 - then 'Default security group ' || group_id || ' has inbound rules.' - when jsonb_array_length(ip_permissions) = 0 and jsonb_array_length(ip_permissions_egress) > 0 - then 'Default security group ' || group_id || ' has outbound rules.' - else 'Default security group ' || group_id || ' has no inbound or outbound rules.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_vpc_security_group -where - group_name = 'default'; diff --git a/query/vpc/vpc_eip_associated.sql b/query/vpc/vpc_eip_associated.sql deleted file mode 100644 index 1c1f157e..00000000 --- a/query/vpc/vpc_eip_associated.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - 'arn:' || partition || ':ec2:' || region || ':' || account_id || ':eip/' || allocation_id as resource, - case - when association_id is null then 'alarm' - else 'ok' - end status, - case - when association_id is null then title || ' is not associated with any resource.' - else title || ' is associated with a resource.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_vpc_eip; \ No newline at end of file diff --git a/query/vpc/vpc_endpoint_service_acceptance_required_enabled.sql b/query/vpc/vpc_endpoint_service_acceptance_required_enabled.sql deleted file mode 100644 index f3306812..00000000 --- a/query/vpc/vpc_endpoint_service_acceptance_required_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - service_id as resource, - case - when acceptance_required then 'ok' - else 'alarm' - end as status, - case - when acceptance_required then title || ' acceptance_required enabled.' - else title || ' acceptance_required disabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_vpc_endpoint_service; \ No newline at end of file diff --git a/query/vpc/vpc_flow_logs_enabled.sql b/query/vpc/vpc_flow_logs_enabled.sql deleted file mode 100644 index 70433404..00000000 --- a/query/vpc/vpc_flow_logs_enabled.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - -- Required columns - distinct arn as resource, - case - when v.account_id <> v.owner_id then 'skip' - when f.resource_id is not null then 'ok' - else 'alarm' - end as status, - case - when v.account_id <> v.owner_id then vpc_id || ' is a shared VPC.' - when f.resource_id is not null then vpc_id || ' flow logging enabled.' - else vpc_id || ' flow logging disabled.' - end as reason, - -- Additional columns - v.region, - v.account_id -from - aws_vpc as v left join aws_vpc_flow_log as f on v.vpc_id = f.resource_id; diff --git a/query/vpc/vpc_igw_attached_to_authorized_vpc.sql b/query/vpc/vpc_igw_attached_to_authorized_vpc.sql deleted file mode 100644 index 41f0c69c..00000000 --- a/query/vpc/vpc_igw_attached_to_authorized_vpc.sql +++ /dev/null @@ -1,20 +0,0 @@ -select - -- Required Columns - 'arn:' || partition || ':ec2:' || region || ':' || account_id || ':internet-gateway/' || title as resource, - case - when jsonb_array_length(attachments) = 0 then 'alarm' - else 'ok' - end as status, - case - when jsonb_array_length(attachments) = 0 then title || ' not attached to VPC.' - else title || ' attached to ' || split_part( - substring(attachments :: text, 3, length(attachments :: text) -6), - '"VpcId": "', - 2 - ) || '.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_vpc_internet_gateway; \ No newline at end of file diff --git a/query/vpc/vpc_network_acl_remote_administration.sql b/query/vpc/vpc_network_acl_remote_administration.sql deleted file mode 100644 index 05935c9e..00000000 --- a/query/vpc/vpc_network_acl_remote_administration.sql +++ /dev/null @@ -1,51 +0,0 @@ -with bad_rules as ( - select - network_acl_id, - count(*) as num_bad_rules - from - aws_vpc_network_acl, - jsonb_array_elements(entries) as att - where - att ->> 'Egress' = 'false' -- as per aws egress = false indicates the ingress - and ( - att ->> 'CidrBlock' = '0.0.0.0/0' - or att ->> 'Ipv6CidrBlock' = '::/0' - ) - and att ->> 'RuleAction' = 'allow' - and ( - ( - att ->> 'Protocol' = '-1' -- all traffic - and att ->> 'PortRange' is null - ) - or ( - (att -> 'PortRange' ->> 'From') :: int <= 22 - and (att -> 'PortRange' ->> 'To') :: int >= 22 - and att ->> 'Protocol' in('6', '17') -- TCP or UDP - ) - or ( - (att -> 'PortRange' ->> 'From') :: int <= 3389 - and (att -> 'PortRange' ->> 'To') :: int >= 3389 - and att ->> 'Protocol' in('6', '17') -- TCP or UDP - ) - ) - group by - network_acl_id -) - -select - -- Required Columns - 'arn:' || acl.partition || ':ec2:' || acl.region || ':' || acl.account_id || ':network-acl/' || acl.network_acl_id as resource, - case - when bad_rules.network_acl_id is null then 'ok' - else 'alarm' - end as status, - case - when bad_rules.network_acl_id is null then acl.network_acl_id || ' does not allow ingress to port 22 or 3389 from 0.0.0.0/0 or ::/0.' - else acl.network_acl_id || ' contains ' || bad_rules.num_bad_rules || ' rule(s) allowing ingress to port 22 or 3389 from 0.0.0.0/0 or ::/0.' - end as reason, - -- Additional Dimensions - acl.region, - acl.account_id -from - aws_vpc_network_acl as acl - left join bad_rules on bad_rules.network_acl_id = acl.network_acl_id diff --git a/query/vpc/vpc_network_acl_unused.sql b/query/vpc/vpc_network_acl_unused.sql deleted file mode 100644 index 7b8a929c..00000000 --- a/query/vpc/vpc_network_acl_unused.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - network_acl_id as resource, - case - when jsonb_array_length(associations) >= 1 then 'ok' - else 'alarm' - end status, - case - when jsonb_array_length(associations) >= 1 then title || ' associated with subnet.' - else title || ' not associated with subnet.' - end reason, - -- Additional Dimensions - region, - account_id -from - aws_vpc_network_acl; \ No newline at end of file diff --git a/query/vpc/vpc_route_table_restrict_public_access_to_igw.sql b/query/vpc/vpc_route_table_restrict_public_access_to_igw.sql deleted file mode 100644 index 8b7ab9d8..00000000 --- a/query/vpc/vpc_route_table_restrict_public_access_to_igw.sql +++ /dev/null @@ -1,32 +0,0 @@ -with route_with_public_access as ( - select - route_table_id, - count(*) as num - from - aws_vpc_route_table, - jsonb_array_elements(routes) as r - where - ( r ->> 'DestinationCidrBlock' = '0.0.0.0/0' - or r ->> 'DestinationCidrBlock' = '::/0' - ) - and r ->> 'GatewayId' like 'igw%' - group by - route_table_id -) -select - -- Required Columns - a.route_table_id as resource, - case - when b.route_table_id is null then 'ok' - else 'alarm' - end as status, - case - when b.route_table_id is null then a.title || ' does not have public routes to an Internet Gateway (IGW)' - else a.title || ' contains ' || b.num || ' rule(s) which have public routes to an Internet Gateway (IGW)' - end as reason, - -- Additional Dimensions - a.region, - a.account_id -from - aws_vpc_route_table as a - left join route_with_public_access as b on b.route_table_id = a.route_table_id diff --git a/query/vpc/vpc_security_group_allows_ingress_authorized_ports.sql b/query/vpc/vpc_security_group_allows_ingress_authorized_ports.sql deleted file mode 100644 index ab1f0179..00000000 --- a/query/vpc/vpc_security_group_allows_ingress_authorized_ports.sql +++ /dev/null @@ -1,28 +0,0 @@ -with ingress_unauthorized_ports as ( - select - group_id, - count(*) - from - aws_vpc_security_group_rule - where - type = 'ingress' - and cidr_ipv4 = '0.0.0.0/0' - and (from_port is null or from_port not in (80,443)) - group by group_id -) -select - -- Required Columns - sg.arn as resource, - case - when ingress_unauthorized_ports.count > 0 then 'alarm' - else 'ok' - end as status, - case - when ingress_unauthorized_ports.count > 0 then sg.title || ' having unrestricted incoming traffic other than default ports from 0.0.0.0/0 ' - else sg.title || ' allows unrestricted incoming traffic for authorized default ports (80,443).' - end as reason, - sg.region, - sg.account_id -from - aws_vpc_security_group as sg - left join ingress_unauthorized_ports on ingress_unauthorized_ports.group_id = sg.group_id; \ No newline at end of file diff --git a/query/vpc/vpc_security_group_associated.sql b/query/vpc/vpc_security_group_associated.sql deleted file mode 100644 index 8a755c20..00000000 --- a/query/vpc/vpc_security_group_associated.sql +++ /dev/null @@ -1,27 +0,0 @@ --- This also addresses, Lambda in VPC. --- As Lambda creates an elastic network interface for each subnet in your function's VPC configuration. -with associated_sg as ( - select - sg ->> 'GroupId' as secgrp_id, - sg ->> 'GroupName' as secgrp_name - from - aws_ec2_network_interface, - jsonb_array_elements(groups) as sg -) -select - -- Required Columns - distinct s.arn as resource, - case - when a.secgrp_id = s.group_id then 'ok' - else 'alarm' - end as status, - case - when a.secgrp_id = s.group_id then s.title || ' is associated.' - else s.title || ' not associated.' - end as reason, - -- Additional Dimensions - s.region, - s.account_id -from - aws_vpc_security_group s - left join associated_sg a on s.group_id = a.secgrp_id; diff --git a/query/vpc/vpc_security_group_associated_to_eni.sql b/query/vpc/vpc_security_group_associated_to_eni.sql deleted file mode 100644 index 982a99f1..00000000 --- a/query/vpc/vpc_security_group_associated_to_eni.sql +++ /dev/null @@ -1,26 +0,0 @@ -with associated_sg as ( - select - count(sg ->> 'GroupId'), - sg ->> 'GroupId' as secgrp_id - from - aws_ec2_network_interface, - jsonb_array_elements(groups) as sg - group by sg ->> 'GroupId' -) -select - -- Required Columns - distinct s.arn as resource, - case - when a.secgrp_id = s.group_id then 'ok' - else 'alarm' - end as status, - case - when a.secgrp_id = s.group_id then s.title || ' is associated with ' || a.count || ' ENI(s).' - else s.title || ' not associated to any ENI.' - end as reason, - -- Additional Dimensions - s.region, - s.account_id -from - aws_vpc_security_group as s - left join associated_sg as a on s.group_id = a.secgrp_id; \ No newline at end of file diff --git a/query/vpc/vpc_security_group_not_uses_launch_wizard_sg.sql b/query/vpc/vpc_security_group_not_uses_launch_wizard_sg.sql deleted file mode 100644 index f16526b8..00000000 --- a/query/vpc/vpc_security_group_not_uses_launch_wizard_sg.sql +++ /dev/null @@ -1,28 +0,0 @@ -with associated_sg as ( - select - distinct (sg ->> 'GroupName') as sg_name - from - aws_ec2_network_interface, - jsonb_array_elements(groups) as sg - where - (sg ->> 'GroupName') like 'launch-wizard%' -) -select - -- Required Columns - arn as resource, - case - when a.sg_name is null then 'ok' - else 'alarm' - end as status, - case - when a.sg_name is null then title || ' not in use.' - else title || ' in use.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_vpc_security_group as s - left join associated_sg as a on a.sg_name = s.group_name -where - group_name like 'launch-wizard%' diff --git a/query/vpc/vpc_security_group_remote_administration.sql b/query/vpc/vpc_security_group_remote_administration.sql deleted file mode 100644 index cb9987ec..00000000 --- a/query/vpc/vpc_security_group_remote_administration.sql +++ /dev/null @@ -1,45 +0,0 @@ -with bad_rules as ( - select - group_id, - count(*) as num_bad_rules - from - aws_vpc_security_group_rule - where - type = 'ingress' - and ( - cidr_ipv4 = '0.0.0.0/0' - or cidr_ipv6 = '::/0' - ) - and ( - ( ip_protocol = '-1' -- all traffic - and from_port is null - ) - or ( - from_port >= 22 - and to_port <= 22 - ) - or ( - from_port >= 3389 - and to_port <= 3389 - ) - ) - group by - group_id -) -select - -- Required Columns - arn as resource, - case - when bad_rules.group_id is null then 'ok' - else 'alarm' - end as status, - case - when bad_rules.group_id is null then sg.group_id || ' does not allow ingress to port 22 or 3389 from 0.0.0.0/0 or ::/0.' - else sg.group_id || ' contains ' || bad_rules.num_bad_rules || ' rule(s) that allow ingress to port 22 or 3389 from 0.0.0.0/0 or ::/0.' - end as reason, - -- Additional Dimensions - sg.region, - sg.account_id -from - aws_vpc_security_group as sg - left join bad_rules on bad_rules.group_id = sg.group_id diff --git a/query/vpc/vpc_security_group_remote_administration_ipv4.sql b/query/vpc/vpc_security_group_remote_administration_ipv4.sql deleted file mode 100644 index 508d2336..00000000 --- a/query/vpc/vpc_security_group_remote_administration_ipv4.sql +++ /dev/null @@ -1,44 +0,0 @@ -with bad_rules as ( - select - group_id, - count(*) as num_bad_rules - from - aws_vpc_security_group_rule - where - type = 'ingress' - and ( - cidr_ipv4 = '0.0.0.0/0' - ) - and ( - ( ip_protocol = '-1' -- all traffic - and from_port is null - ) - or ( - from_port >= 22 - and to_port <= 22 - ) - or ( - from_port >= 3389 - and to_port <= 3389 - ) - ) - group by - group_id -) -select - -- Required Columns - arn as resource, - case - when bad_rules.group_id is null then 'ok' - else 'alarm' - end as status, - case - when bad_rules.group_id is null then sg.group_id || ' does not allow ingress to port 22 or 3389 from 0.0.0.0/0.' - else sg.group_id || ' contains ' || bad_rules.num_bad_rules || ' rule(s) that allow ingress to port 22 or 3389 from 0.0.0.0/0.' - end as reason, - -- Additional Dimensions - sg.region, - sg.account_id -from - aws_vpc_security_group as sg - left join bad_rules on bad_rules.group_id = sg.group_id diff --git a/query/vpc/vpc_security_group_remote_administration_ipv6.sql b/query/vpc/vpc_security_group_remote_administration_ipv6.sql deleted file mode 100644 index 5f776f81..00000000 --- a/query/vpc/vpc_security_group_remote_administration_ipv6.sql +++ /dev/null @@ -1,44 +0,0 @@ -with bad_rules as ( - select - group_id, - count(*) as num_bad_rules - from - aws_vpc_security_group_rule - where - type = 'ingress' - and ( - cidr_ipv6 = '::/0' - ) - and ( - ( ip_protocol = '-1' -- all traffic - and from_port is null - ) - or ( - from_port >= 22 - and to_port <= 22 - ) - or ( - from_port >= 3389 - and to_port <= 3389 - ) - ) - group by - group_id -) -select - -- Required Columns - arn as resource, - case - when bad_rules.group_id is null then 'ok' - else 'alarm' - end as status, - case - when bad_rules.group_id is null then sg.group_id || ' does not allow ingress to port 22 or 3389 from ::/0.' - else sg.group_id || ' contains ' || bad_rules.num_bad_rules || ' rule(s) that allow ingress to port 22 or 3389 from ::/0.' - end as reason, - -- Additional Dimensions - sg.region, - sg.account_id -from - aws_vpc_security_group as sg - left join bad_rules on bad_rules.group_id = sg.group_id diff --git a/query/vpc/vpc_security_group_restrict_ingress_common_ports_all.sql b/query/vpc/vpc_security_group_restrict_ingress_common_ports_all.sql deleted file mode 100644 index 63e0942b..00000000 --- a/query/vpc/vpc_security_group_restrict_ingress_common_ports_all.sql +++ /dev/null @@ -1,58 +0,0 @@ -with ingress_ssh_rules as ( - select - group_id, - count(*) as num_ssh_rules - from - aws_vpc_security_group_rule - where - type = 'ingress' - and cidr_ipv4 = '0.0.0.0/0' - and ( - ( ip_protocol = '-1' - and from_port is null - ) - or ( - from_port >= 22 - and to_port <= 22 - ) - or ( - from_port >= 3389 - and to_port <= 3389 - ) - or ( - from_port >= 21 - and to_port <= 21 - ) - or ( - from_port >= 20 - and to_port <= 20 - ) - or ( - from_port >= 3306 - and to_port <= 3306 - ) - or ( - from_port >= 4333 - and to_port <= 4333 - ) - ) - group by - group_id -) -select - -- Required Columns - arn as resource, - case - when ingress_ssh_rules.group_id is null then 'ok' - else 'alarm' - end as status, - case - when ingress_ssh_rules.group_id is null then sg.group_id || ' ingress restricted for ports 20, 21, 22, 3306, 3389, 4333 from 0.0.0.0/0.' - else sg.group_id || ' contains ' || ingress_ssh_rules.num_ssh_rules || ' ingress rule(s) allowing access on ports 20, 21, 22, 3306, 3389, 4333 from 0.0.0.0/0.' - end as reason, - -- Additional Dimensions - sg.region, - sg.account_id -from - aws_vpc_security_group as sg - left join ingress_ssh_rules on ingress_ssh_rules.group_id = sg.group_id; diff --git a/query/vpc/vpc_security_group_restrict_ingress_kafka_port.sql b/query/vpc/vpc_security_group_restrict_ingress_kafka_port.sql deleted file mode 100644 index d467dd42..00000000 --- a/query/vpc/vpc_security_group_restrict_ingress_kafka_port.sql +++ /dev/null @@ -1,41 +0,0 @@ -with ingress_kafka_port as ( - select - group_id, - count(*) as num_ssh_rules - from - aws_vpc_security_group_rule - where - type = 'ingress' - and ( - cidr_ipv4 = '0.0.0.0/0' - or cidr_ipv6 = '::/0' - ) - and ( - ( ip_protocol = '-1' - and from_port is null - ) - or ( - from_port >= 9092 - and to_port <= 9092 - ) - ) - group by - group_id -) -select - -- Required Columns - arn as resource, - case - when k.group_id is null then 'ok' - else 'alarm' - end as status, - case - when k.group_id is null then sg.group_id || ' ingress restricted for kafka port from 0.0.0.0/0.' - else sg.group_id || ' contains ' || k.num_ssh_rules || ' ingress rule(s) allowing kafka port from 0.0.0.0/0.' - end as reason, - -- Additional Dimensions - sg.region, - sg.account_id -from - aws_vpc_security_group as sg - left join ingress_kafka_port as k on k.group_id = sg.group_id; diff --git a/query/vpc/vpc_security_group_restrict_ingress_kibana_port.sql b/query/vpc/vpc_security_group_restrict_ingress_kibana_port.sql deleted file mode 100644 index d09d1719..00000000 --- a/query/vpc/vpc_security_group_restrict_ingress_kibana_port.sql +++ /dev/null @@ -1,45 +0,0 @@ -with ingress_kibana_port as ( - select - group_id, - count(*) as num_ssh_rules - from - aws_vpc_security_group_rule - where - type = 'ingress' - and ( - cidr_ipv4 = '0.0.0.0/0' - or cidr_ipv6 = '::/0' - ) - and ( - ( ip_protocol = '-1' - and from_port is null - ) - or ( - from_port >= 9200 - and to_port <= 9200 - ) - or ( - from_port >= 5601 - and to_port <= 5601 - ) - ) - group by - group_id -) -select - -- Required Columns - arn as resource, - case - when k.group_id is null then 'ok' - else 'alarm' - end as status, - case - when k.group_id is null then sg.group_id || ' ingress restricted for kibana port from 0.0.0.0/0.' - else sg.group_id || ' contains ' || k.num_ssh_rules || ' ingress rule(s) allowing kibana port from 0.0.0.0/0.' - end as reason, - -- Additional Dimensions - sg.region, - sg.account_id -from - aws_vpc_security_group as sg - left join ingress_kibana_port as k on k.group_id = sg.group_id; diff --git a/query/vpc/vpc_security_group_restrict_ingress_rdp_all.sql b/query/vpc/vpc_security_group_restrict_ingress_rdp_all.sql deleted file mode 100644 index c0492f54..00000000 --- a/query/vpc/vpc_security_group_restrict_ingress_rdp_all.sql +++ /dev/null @@ -1,38 +0,0 @@ -with ingress_rdp_rules as ( - select - group_id, - count(*) as num_rdp_rules - from - aws_vpc_security_group_rule - where - type = 'ingress' - and cidr_ipv4 = '0.0.0.0/0' - and ( - ( ip_protocol = '-1' - and from_port is null - ) - or ( - from_port >= 3389 - and to_port <= 3389 - ) - ) - group by - group_id -) -select - -- Required Columns - arn as resource, - case - when ingress_rdp_rules.group_id is null then 'ok' - else 'alarm' - end as status, - case - when ingress_rdp_rules.group_id is null then sg.group_id || ' ingress restricted for RDP from 0.0.0.0/0.' - else sg.group_id || ' contains ' || ingress_rdp_rules.num_rdp_rules || ' ingress rule(s) allowing RDP from 0.0.0.0/0.' - end as reason, - -- Additional Dimensions - sg.region, - sg.account_id -from - aws_vpc_security_group as sg - left join ingress_rdp_rules on ingress_rdp_rules.group_id = sg.group_id; diff --git a/query/vpc/vpc_security_group_restrict_ingress_redis_port.sql b/query/vpc/vpc_security_group_restrict_ingress_redis_port.sql deleted file mode 100644 index 3d69c173..00000000 --- a/query/vpc/vpc_security_group_restrict_ingress_redis_port.sql +++ /dev/null @@ -1,41 +0,0 @@ -with ingress_redis_port as ( - select - group_id, - count(*) as num_redis_rules - from - aws_vpc_security_group_rule - where - type = 'ingress' - and - (cidr_ipv4 = '0.0.0.0/0' - or cidr_ipv6 = '::/0') - and - ( - ( ip_protocol = '-1' - and from_port is null - ) - or ( - from_port >= 6379 - and to_port <= 6379 - ) - ) - group by - group_id -) -select - -- Required Columns - arn as resource, - case - when ingress_redis_port.group_id is null then 'ok' - else 'alarm' - end as status, - case - when ingress_redis_port.group_id is null then sg.group_id || ' restricted ingress from 0.0.0.0/0 or ::/0 to Redis port 6379.' - else sg.group_id || ' contains ' || ingress_redis_port.num_redis_rules || ' ingress rule(s) from 0.0.0.0/0 or ::/0 to Redis port 6379.' - end as reason, - -- Additional Dimensions - sg.region, - sg.account_id -from - aws_vpc_security_group as sg - left join ingress_redis_port on ingress_redis_port.group_id = sg.group_id; \ No newline at end of file diff --git a/query/vpc/vpc_security_group_restrict_ingress_ssh_all.sql b/query/vpc/vpc_security_group_restrict_ingress_ssh_all.sql deleted file mode 100644 index b637b1ff..00000000 --- a/query/vpc/vpc_security_group_restrict_ingress_ssh_all.sql +++ /dev/null @@ -1,38 +0,0 @@ -with ingress_ssh_rules as ( - select - group_id, - count(*) as num_ssh_rules - from - aws_vpc_security_group_rule - where - type = 'ingress' - and cidr_ipv4 = '0.0.0.0/0' - and ( - ( ip_protocol = '-1' - and from_port is null - ) - or ( - from_port >= 22 - and to_port <= 22 - ) - ) - group by - group_id -) -select - -- Required Columns - arn as resource, - case - when ingress_ssh_rules.group_id is null then 'ok' - else 'alarm' - end as status, - case - when ingress_ssh_rules.group_id is null then sg.group_id || ' ingress restricted for SSH from 0.0.0.0/0.' - else sg.group_id || ' contains ' || ingress_ssh_rules.num_ssh_rules || ' ingress rule(s) allowing SSH from 0.0.0.0/0.' - end as reason, - -- Additional Dimensions - sg.region, - sg.account_id -from - aws_vpc_security_group as sg - left join ingress_ssh_rules on ingress_ssh_rules.group_id = sg.group_id; diff --git a/query/vpc/vpc_security_group_restrict_ingress_tcp_udp_all.sql b/query/vpc/vpc_security_group_restrict_ingress_tcp_udp_all.sql deleted file mode 100644 index e020e973..00000000 --- a/query/vpc/vpc_security_group_restrict_ingress_tcp_udp_all.sql +++ /dev/null @@ -1,36 +0,0 @@ -with bad_rules as ( - select - group_id, - count(*) as num_bad_rules - from - aws_vpc_security_group_rule - where - type = 'ingress' - and cidr_ipv4 = '0.0.0.0/0' - and ( - ip_protocol in ('tcp', 'udp') - or ( - ip_protocol = '-1' - and from_port is null - ) - ) - group by - group_id -) -select - -- Required Columns - arn as resource, - case - when bad_rules.group_id is null then 'ok' - else 'alarm' - end as status, - case - when bad_rules.group_id is null then sg.group_id || ' does not allow ingress to TCP or UDP ports from 0.0.0.0/0.' - else sg.group_id || ' contains ' || bad_rules.num_bad_rules || ' rule(s) that allow ingress to TCP or UDP ports from 0.0.0.0/0.' - end as reason, - -- Additional Dimensions - sg.region, - sg.account_id -from - aws_vpc_security_group as sg - left join bad_rules on bad_rules.group_id = sg.group_id; diff --git a/query/vpc/vpc_security_group_restricted_common_ports.sql b/query/vpc/vpc_security_group_restricted_common_ports.sql deleted file mode 100644 index c2b34ed2..00000000 --- a/query/vpc/vpc_security_group_restricted_common_ports.sql +++ /dev/null @@ -1,110 +0,0 @@ -with ingress_ssh_rules as ( - select - group_id, - count(*) as num_ssh_rules - from - aws_vpc_security_group_rule - where - type = 'ingress' - and cidr_ipv4 = '0.0.0.0/0' - and ( - ( ip_protocol = '-1' - and from_port is null - ) - or ( - from_port >= 22 - and to_port <= 22 - ) - or ( - from_port >= 3389 - and to_port <= 3389 - ) - or ( - from_port >= 21 - and to_port <= 21 - ) - or ( - from_port >= 20 - and to_port <= 20 - ) - or ( - from_port >= 3306 - and to_port <= 3306 - ) - or ( - from_port >= 4333 - and to_port <= 4333 - ) - or ( - from_port >= 23 - and to_port <= 23 - ) - or ( - from_port >= 25 - and to_port <= 25 - ) - or ( - from_port >= 445 - and to_port <= 445 - ) - or ( - from_port >= 110 - and to_port <= 110 - ) - or ( - from_port >= 135 - and to_port <= 135 - ) - or ( - from_port >= 143 - and to_port <= 143 - ) - or ( - from_port >= 1433 - and to_port <= 3389 - ) - or ( - from_port >= 3389 - and to_port <= 1434 - ) - or ( - from_port >= 5432 - and to_port <= 5432 - ) - or ( - from_port >= 5500 - and to_port <= 5500 - ) - or ( - from_port >= 5601 - and to_port <= 5601 - ) - or ( - from_port >= 9200 - and to_port <= 9300 - ) - or ( - from_port >= 8080 - and to_port <= 8080 - ) - ) - group by - group_id -) -select - -- Required Columns - arn as resource, - case - when ingress_ssh_rules.group_id is null then 'ok' - else 'alarm' - end as status, - case - when ingress_ssh_rules.group_id is null then sg.group_id || ' ingress restricted for common ports from 0.0.0.0/0..' - else sg.group_id || ' contains ' || ingress_ssh_rules.num_ssh_rules || ' ingress rule(s) allowing access for common ports from 0.0.0.0/0.' - end as reason, - -- Additional Dimensions - sg.region, - sg.account_id -from - aws_vpc_security_group as sg - left join ingress_ssh_rules on ingress_ssh_rules.group_id = sg.group_id; diff --git a/query/vpc/vpc_security_group_unsued.sql b/query/vpc/vpc_security_group_unsued.sql deleted file mode 100644 index d237430d..00000000 --- a/query/vpc/vpc_security_group_unsued.sql +++ /dev/null @@ -1,33 +0,0 @@ -with associated_sg as ( - select - sg ->> 'GroupId' as secgrp_id - from - aws_ec2_network_interface, - jsonb_array_elements(groups) as sg - group by sg ->> 'GroupId' - union - select - sg ->> 'GroupId' as secgrp_id - from - aws_ec2_instance, - jsonb_array_elements(security_groups) as sg - group by sg ->> 'GroupId' - -) -select - -- Required Columns - distinct s.arn as resource, - case - when a.secgrp_id is not null then 'ok' - else 'alarm' - end as status, - case - when a.secgrp_id is not null then s.title || ' is in use.' - else s.title || ' not in use.' - end as reason, - -- Additional Dimensions - s.region, - s.account_id -from - aws_vpc_security_group as s - left join associated_sg as a on s.group_id = a.secgrp_id; \ No newline at end of file diff --git a/query/vpc/vpc_subnet_auto_assign_public_ip_disabled.sql b/query/vpc/vpc_subnet_auto_assign_public_ip_disabled.sql deleted file mode 100644 index b3f91690..00000000 --- a/query/vpc/vpc_subnet_auto_assign_public_ip_disabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - subnet_id as resource, - case - when map_public_ip_on_launch = 'false' then 'ok' - else 'alarm' - end as status, - case - when map_public_ip_on_launch = 'false' then title || ' auto assign public IP disabled.' - else title || ' auto assign public IP enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_vpc_subnet; \ No newline at end of file diff --git a/query/vpc/vpc_vpn_tunnel_up.sql b/query/vpc/vpc_vpn_tunnel_up.sql deleted file mode 100644 index 7abb7b0e..00000000 --- a/query/vpc/vpc_vpn_tunnel_up.sql +++ /dev/null @@ -1,28 +0,0 @@ -with filter_data as ( - select - arn, - count(t ->> 'Status') - from - aws_vpc_vpn_connection, - jsonb_array_elements(vgw_telemetry) as t - where t ->> 'Status' = 'UP' - group by arn -) -select - -- Required Columns - a.arn as resource, - case - when b.count is null or b.count < 2 then 'alarm' - else 'ok' - end as status, - case - when b.count is null then a.title || ' has both tunnels offline.' - when b.count = 1 then a.title || ' has one tunnel offline.' - else a.title || ' has both tunnels online.' - end as reason, - -- Additional Dimensions - a.region, - a.account_id -from - aws_vpc_vpn_connection as a - left join filter_data as b on a.arn = b.arn; diff --git a/query/waf/waf_rule_condition_attached.sql b/query/waf/waf_rule_condition_attached.sql deleted file mode 100644 index 634755de..00000000 --- a/query/waf/waf_rule_condition_attached.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - akas as resource, - case - when predicates is null or jsonb_array_length(predicates) = 0 then 'alarm' - else 'ok' - end as status, - case - when predicates is null or jsonb_array_length(predicates) = 0 then title || ' has no attached conditions.' - else title || ' has attached conditions.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_waf_rule; \ No newline at end of file diff --git a/query/waf/waf_rule_group_rule_attached.sql b/query/waf/waf_rule_group_rule_attached.sql deleted file mode 100644 index 5cb3d3f5..00000000 --- a/query/waf/waf_rule_group_rule_attached.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when activated_rules is null or jsonb_array_length(activated_rules) = 0 then 'alarm' - else 'ok' - end as status, - case - when activated_rules is null or jsonb_array_length(activated_rules) = 0 then title || ' has no attached rules.' - else title || ' has attached rules.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_waf_rule_group; \ No newline at end of file diff --git a/query/waf/waf_web_acl_rule_attached.sql b/query/waf/waf_web_acl_rule_attached.sql deleted file mode 100644 index 043f0307..00000000 --- a/query/waf/waf_web_acl_rule_attached.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when rules is null or jsonb_array_length(rules) = 0 then 'alarm' - else 'ok' - end as status, - case - when rules is null or jsonb_array_length(rules) = 0 then title || ' has no attached rules.' - else title || ' has attached rules.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_waf_web_acl; \ No newline at end of file diff --git a/query/wafv2/wafv2_web_acl_logging_enabled.sql b/query/wafv2/wafv2_web_acl_logging_enabled.sql deleted file mode 100644 index a945830f..00000000 --- a/query/wafv2/wafv2_web_acl_logging_enabled.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - -- Required Columns - arn as resource, - case - when logging_configuration is null then 'alarm' - else 'ok' - end as status, - case - when logging_configuration is null then title || ' logging disabled.' - else title || ' logging enabled.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_wafv2_web_acl; \ No newline at end of file diff --git a/query/wafv2/wafv2_web_acl_rule_attached.sql b/query/wafv2/wafv2_web_acl_rule_attached.sql deleted file mode 100644 index 8b1bf4c7..00000000 --- a/query/wafv2/wafv2_web_acl_rule_attached.sql +++ /dev/null @@ -1,27 +0,0 @@ -with rule_group_count as ( - select - count(*) as rule_group_count - from - aws_wafv2_web_acl, - jsonb_array_elements(rules) as r - where - r -> 'Statement' -> 'RuleGroupReferenceStatement' ->> 'ARN' is not null - group by - arn -) -select - -- Required Columns - arn as resource, - case - when rules is null or jsonb_array_length(rules) = 0 then 'alarm' - else 'ok' - end as status, - case - when rules is null or jsonb_array_length(rules) = 0 then title || ' has no attached rules.' - else title || ' has ' || (select rule_group_count from rule_group_count ) || ' rule group(s) and ' || (jsonb_array_length(rules) - (select rule_group_count from rule_group_count )) || ' rule(s) attached.' - end as reason, - -- Additional Dimensions - region, - account_id -from - aws_wafv2_web_acl; \ No newline at end of file diff --git a/steampipe.spvars.example b/steampipe.spvars.example new file mode 100644 index 00000000..a1ed782d --- /dev/null +++ b/steampipe.spvars.example @@ -0,0 +1,3 @@ +# Dimensions +common_dimensions = [ "account_id", "region" ] +tag_dimensions = []