diff --git a/README.md b/README.md index 70e8771655..3405705be3 100644 --- a/README.md +++ b/README.md @@ -55,13 +55,13 @@ With Prowler you can: This script has been written in bash using AWS-CLI and it works in Linux and OSX. -- Make sure your AWS-CLI is installed on your workstation, and other components needed, with Python pip already installed: +- Make sure the latest version of AWS-CLI is installed on your workstation, and other components needed, with Python pip already installed: ```sh pip install awscli ansi2html detect-secrets ``` - AWS-CLI can be also installed it using "brew", "apt", "yum" or manually from , but `ansi2html` and `detect-secrets` has to be installed using `pip` + AWS-CLI can be also installed it using "brew", "apt", "yum" or manually from , but `ansi2html` and `detect-secrets` has to be installed using `pip`. You will need to install `jq` to get more accuracy in some checks. - Previous steps, from your workstation: @@ -70,13 +70,19 @@ This script has been written in bash using AWS-CLI and it works in Linux and OSX cd prowler ``` -- Make sure you have properly configured your AWS-CLI with a valid Access Key and Region: +- Make sure you have properly configured your AWS-CLI with a valid Access Key and Region or declare AWS variables properly: ```sh aws configure ``` + or + ```sh + export AWS_ACCESS_KEY_ID="ASXXXXXXX" + export AWS_SECRET_ACCESS_KEY="XXXXXXXXX" + export AWS_SESSION_TOKEN="XXXXXXXXX" + ``` -- Make sure your Secret and Access Keys are associated to a user with proper permissions to do all checks. To make sure add SecurityAuditor default policy to your user. Policy ARN is +- Those credentials must be associated to a user or role with proper permissions to do all checks. To make sure add SecurityAuditor default policy to your user. Policy ARN is ```sh arn:aws:iam::aws:policy/SecurityAudit @@ -94,6 +100,12 @@ This script has been written in bash using AWS-CLI and it works in Linux and OSX Use `-l` to list all available checks and group of checks (sections) + If you want to avoid installing dependences run it using Docker: + + ```sh + docker run -ti --rm --name prowler --env AWS_ACCESS_KEY_ID --env AWS_SECRET_ACCESS_KEY --env AWS_SESSION_TOKEN toniblyx/prowler:latest + ``` + 1. For custom AWS-CLI profile and region, use the following: (it will use your custom profile and run checks over all regions when needed): ```sh @@ -105,6 +117,11 @@ This script has been written in bash using AWS-CLI and it works in Linux and OSX ```sh ./prowler -c check310 ``` + With Docker: + ```sh + docker run -ti --rm --name prowler --env AWS_ACCESS_KEY_ID --env AWS_SECRET_ACCESS_KEY --env AWS_SESSION_TOKEN toniblyx/prowler:latest "-c check310" + ``` + or multiple checks separated by comma: ```sh ./prowler -c check310,check722 diff --git a/checks/check_extra727 b/checks/check_extra727 index 0be47f2c25..5e14e2b5c3 100644 --- a/checks/check_extra727 +++ b/checks/check_extra727 @@ -22,18 +22,25 @@ extra727(){ LIST_SQS=$($AWSCLI sqs list-queues $PROFILE_OPT --region $regx --query QueueUrls --output text |grep -v ^None) if [[ $LIST_SQS ]]; then for queue in $LIST_SQS; do - # check if the policy has Principal as * - SQS_TO_CHECK=$($AWSCLI sqs get-queue-attributes --queue-url $queue $PROFILE_OPT --region $regx --attribute-names All --query Attributes.Policy --output text | awk -v k="text" '{n=split($0,a,","); for (i=1; i<=n; i++) print a[i]}' | awk '/Principal/ || /Condition/ && !skip { print } { skip = /Deny/} ') - PUBLIC_SQS_WCONDITION=$(echo $SQS_TO_CHECK|grep Condition) - if [[ $PUBLIC_SQS_WCONDITION ]]; then - textPass "$regx: SQS queue $queue has a Condition" "$regx" - else - PUBLIC_SQS=$(echo $SQS_TO_CHECK|grep \"Principal|grep \*) - if [[ $PUBLIC_SQS ]]; then - textFail "$regx: SQS queue $queue seems to be public (Principal: \"*\")" "$regx" + SQS_POLICY=$($AWSCLI sqs get-queue-attributes --queue-url $queue $PROFILE_OPT --region $regx --attribute-names All --query Attributes.Policy) + if [[ "$SQS_POLICY" != "null" ]]; then + SQS_POLICY_ALLOW_ALL=$(echo $SQS_POLICY \ + | jq '. | fromjson' | jq '.Statement[] | select(.Effect=="Allow") | select(.Principal=="*" or .Principal.AWS=="*" or .Principal.CanonicalUser=="*")') + if [[ $SQS_POLICY_ALLOW_ALL ]]; then + SQS_POLICY_ALLOW_ALL_WITHOUT_CONDITION=$(echo $SQS_POLICY \ + | jq '. | fromjson' | jq '.Statement[] | select(.Effect=="Allow") | select(.Principal=="*" or .Principal.AWS=="*" or .Principal.CanonicalUser=="*") | select(has("Condition") | not)') + if [[ $SQS_POLICY_ALLOW_ALL_WITHOUT_CONDITION ]]; then + SQS_POLICY_ALLOW_ALL_WITHOUT_CONDITION_DETAILS=$(echo $SQS_POLICY_ALLOW_ALL_WITHOUT_CONDITION \ + | jq '"[Principal: " + (.Principal|tostring) + " Action: " + (.Action|tostring) + "]"' ) + textFail "$regx: SQS $queue queue policy with public access: $SQS_POLICY_ALLOW_ALL_WITHOUT_CONDITION_DETAILS" "$regx" + else + textInfo "$regx: SQS $queue queue policy with public access but has a Condition" "$regx" + fi else - textPass "$regx: SQS queue $queue seems correct" "$regx" + textPass "$regx: SQS $queue queue without public access" "$regx" fi + else + textPass "$regx: SQS $queue queue without policy" "$regx" fi done else diff --git a/checks/check_extra73 b/checks/check_extra73 index 7ca96c7f77..3b0efc1abf 100644 --- a/checks/check_extra73 +++ b/checks/check_extra73 @@ -45,7 +45,15 @@ CHECK_ALTERNATE_check703="extra73" extra73(){ textInfo "Looking for open S3 Buckets (ACLs and Policies) in all regions... " ALL_BUCKETS_LIST=$($AWSCLI s3api list-buckets --query 'Buckets[*].{Name:Name}' $PROFILE_OPT --output text) + for bucket in $ALL_BUCKETS_LIST; do + + # 3 Different problems, let's show only 1 finding all together + S3_FINDING_ALLUSERS_ACL="Ok" + S3_FINDING_AUTHUSERS_ACL="Ok" + S3_FINDING_POLICY="Ok" + + # LOCATION BUCKET_LOCATION=$($AWSCLI s3api get-bucket-location --bucket $bucket $PROFILE_OPT --output text) if [[ "None" == $BUCKET_LOCATION ]]; then BUCKET_LOCATION="us-east-1" @@ -53,36 +61,60 @@ extra73(){ if [[ "EU" == $BUCKET_LOCATION ]]; then BUCKET_LOCATION="eu-west-1" fi - # Check Explicit Deny and Avoid Error + + # EXPLICIT DENY CHEK_FOR_EXPLICIT_DENY=$($AWSCLI s3api get-bucket-acl $PROFILE_OPT --region $BUCKET_LOCATION --bucket $bucket --output text 2>&1) if [[ $(echo "$CHEK_FOR_EXPLICIT_DENY" | grep AccessDenied) ]] ; then - textPass "$BUCKET_LOCATION: bucket have an explicit Deny. Not possible to get ACL." "$BUCKET_LOCATION" + textInfo "$BUCKET_LOCATION: $bucket have an explicit Deny. Not possible to get ACL." "$bucket" "$BUCKET_LOCATION" else - # check if AllUsers is in the ACL as Grantee - CHECK_BUCKET_ALLUSERS_ACL=$($AWSCLI s3api get-bucket-acl $PROFILE_OPT --region $BUCKET_LOCATION --bucket $bucket --query "Grants[?Grantee.URI == 'http://acs.amazonaws.com/groups/global/AllUsers']" --output text |grep -v GRANTEE) - CHECK_BUCKET_ALLUSERS_ACL_SINGLE_LINE=$(echo -ne $CHECK_BUCKET_ALLUSERS_ACL) - # check if AuthenticatedUsers is in the ACL as Grantee, they will have access with sigened URL only - CHECK_BUCKET_AUTHUSERS_ACL=$($AWSCLI s3api get-bucket-acl $PROFILE_OPT --region $BUCKET_LOCATION --bucket $bucket --query "Grants[?Grantee.URI == 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers']" --output text |grep -v GRANTEE) - CHECK_BUCKET_AUTHUSERS_ACL_SINGLE_LINE=$(echo -ne $CHECK_BUCKET_AUTHUSERS_ACL) - # to prevent error NoSuchBucketPolicy first clean the output controlling stderr - TEMP_POLICY_FILE=$(mktemp -t prowler-${ACCOUNT_NUM}-${bucket}.policy.XXXXXXXXXX) - $AWSCLI s3api get-bucket-policy $PROFILE_OPT --region $BUCKET_LOCATION --bucket $bucket --output text --query Policy > $TEMP_POLICY_FILE 2> /dev/null - # check if the S3 policy has Principal as * - CHECK_BUCKET_ALLUSERS_POLICY=$(cat $TEMP_POLICY_FILE | sed -e 's/[{}]/''/g' | awk -v k="text" '{n=split($0,a,","); for (i=1; i<=n; i++) print a[i]}'|awk '/Principal/ && !skip { print } { skip = /Deny/} '|grep ^\"Principal|grep \*) - if [[ $CHECK_BUCKET_ALLUSERS_ACL || $CHECK_BUCKET_AUTHUSERS_ACL || $CHECK_BUCKET_ALLUSERS_POLICY ]];then - if [[ $CHECK_BUCKET_ALLUSERS_ACL ]];then - textFail "$BUCKET_LOCATION: $bucket bucket is open to the Internet (Everyone) with permissions: $CHECK_BUCKET_ALLUSERS_ACL_SINGLE_LINE" "$BUCKET_LOCATION" - fi - if [[ $CHECK_BUCKET_AUTHUSERS_ACL ]];then - textFail "$BUCKET_LOCATION: $bucket bucket is open to Authenticated users (Any AWS user) with permissions: $CHECK_BUCKET_AUTHUSERS_ACL_SINGLE_LINE" "$BUCKET_LOCATION" - fi - if [[ $CHECK_BUCKET_ALLUSERS_POLICY ]];then - textFail "$BUCKET_LOCATION: $bucket bucket policy \"may\" allow Anonymous users to perform actions (Principal: \"*\")" "$BUCKET_LOCATION" - fi + # PUBLIC BLOCK + # https://docs.aws.amazon.com/cli/latest/reference/s3api/get-public-access-block.html + BUCKET_PUBLIC_BLOCK=$($AWSCLI s3api get-public-access-block --bucket $bucket $PROFILE_OPT --region $BUCKET_LOCATION 2>/dev/null) + BUCKET_PUBLIC_BLOCK_IGNOREPUBLICACL=$(echo $BUCKET_PUBLIC_BLOCK | jq .PublicAccessBlockConfiguration.BlockPublicAcls 2>/dev/null) + BUCKET_PUBLIC_BLOCK_BLOCKPUBLICPOLICY=$(echo $BUCKET_PUBLIC_BLOCK | jq .PublicAccessBlockConfiguration.BlockPublicPolicy 2>/dev/null) + BUCKET_PUBLIC_BLOCK_BLOCKPUBLICACLS=$(echo $BUCKET_PUBLIC_BLOCK | jq .PublicAccessBlockConfiguration.BlockPublicAcls 2>/dev/null) + BUCKET_PUBLIC_BLOCK_RESTRICPUBLICBUCKET=$(echo $BUCKET_PUBLIC_BLOCK | jq .PublicAccessBlockConfiguration.RestrictPublicBuckets 2>/dev/null) + if [[ $BUCKET_PUBLIC_BLOCK_IGNOREPUBLICACL == "true" ]] && [[ $BUCKET_PUBLIC_BLOCK_BLOCKPUBLICPOLICY == "true" ]] && [[ $BUCKET_PUBLIC_BLOCK_BLOCKPUBLICACLS == "true" ]] && [[ $BUCKET_PUBLIC_BLOCK_RESTRICPUBLICBUCKET == "true" ]]; then + textPass "$BUCKET_LOCATION: $bucket bucket is public blocked (public-access-block)" "$BUCKET_LOCATION" else - textPass "$BUCKET_LOCATION: $bucket bucket is not open" "$BUCKET_LOCATION" + ## ACL + # check if AllUsers is in the ACL as Grantee + CHECK_BUCKET_ALLUSERS_ACL=$($AWSCLI s3api get-bucket-acl $PROFILE_OPT --region $BUCKET_LOCATION --bucket $bucket --query "Grants[?Grantee.URI == 'http://acs.amazonaws.com/groups/global/AllUsers']" --output text |grep -v GRANTEE) + CHECK_BUCKET_ALLUSERS_ACL_SINGLE_LINE=$(echo -ne $CHECK_BUCKET_ALLUSERS_ACL) + # check if AuthenticatedUsers is in the ACL as Grantee, they will have access with sigened URL only + CHECK_BUCKET_AUTHUSERS_ACL=$($AWSCLI s3api get-bucket-acl $PROFILE_OPT --region $BUCKET_LOCATION --bucket $bucket --query "Grants[?Grantee.URI == 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers']" --output text |grep -v GRANTEE) + CHECK_BUCKET_AUTHUSERS_ACL_SINGLE_LINE=$(echo -ne $CHECK_BUCKET_AUTHUSERS_ACL) + ## POLICY + BUCKET_POLICY_STATUS=$($AWSCLI s3api get-bucket-policy-status $PROFILE_OPT --region $BUCKET_LOCATION --bucket $bucket --query PolicyStatus.IsPublic --output text 2>/dev/null) + if [[ $CHECK_BUCKET_ALLUSERS_ACL || $CHECK_BUCKET_AUTHUSERS_ACL || $CHECK_BUCKET_ALLUSERS_POLICY == "True" ]]; then + if [[ $CHECK_BUCKET_ALLUSERS_ACL || $CHECK_BUCKET_AUTHUSERS_ACL ]] && [[ $BUCKET_PUBLIC_BLOCK_IGNOREPUBLICACL != "true" ]];then + if [[ $CHECK_BUCKET_ALLUSERS_ACL ]];then + S3_FINDING_ALLUSERS_ACL="bucket ACL is open to the Internet (Everyone) with permissions: $CHECK_BUCKET_ALLUSERS_ACL_SINGLE_LINE" + fi + if [[ $CHECK_BUCKET_AUTHUSERS_ACL ]];then + S3_FINDING_AUTHUSERS_ACL="bucket ACL is open to Authenticated users (Any AWS user) with permissions: $CHECK_BUCKET_AUTHUSERS_ACL_SINGLE_LINE" + fi + fi + if [[ $BUCKET_PUBLIC_BLOCK_RESTRICPUBLICBUCKET != "true" ]] && [[ $BUCKET_POLICY_STATUS == "True" ]]; then + # Here comes the magic: Find Statement Allow, Principal * and No Condition + BUCKET_POLICY_ALLOW_ALL_WITHOUT_CONDITION=$($AWSCLI s3api get-bucket-policy $PROFILE_OPT --region $BUCKET_LOCATION --bucket $bucket \ + | jq '.Policy | fromjson' | jq '.Statement[] | select(.Effect=="Allow") | select(.Principal=="*" or .Principal.AWS=="*" or .Principal.CanonicalUser=="*") | select(has("Condition") | not)') + if [[ $BUCKET_POLICY_ALLOW_ALL_WITHOUT_CONDITION ]]; then + # Let's do more magic and identify who can do what + BUCKET_POLICY_ALLOW_ALL_WITHOUT_CONDITION_DETAILS=$(echo $BUCKET_POLICY_ALLOW_ALL_WITHOUT_CONDITION \ + | jq '"[Principal: " + (.Principal|tostring) + " Action: " + (.Action|tostring) + "]"' ) + S3_FINDING_POLICY="bucket policy allow perform actions: $BUCKET_POLICY_ALLOW_ALL_WITHOUT_CONDITION_DETAILS" + else + textPass "$BUCKET_LOCATION: $bucket bucket policy with conditions" "$BUCKET_LOCATION" + fi + fi + else + textPass "$BUCKET_LOCATION: $bucket bucket is not open" "$bucket" "$BUCKET_LOCATION" + fi fi - rm -fr $TEMP_POLICY_FILE + fi + if [[ $S3_FINDING_ALLUSERS_ACL != "Ok" ]] || [[ $S3_FINDING_AUTHUSERS_ACL != "Ok" ]] || [[ $S3_FINDING_POLICY != "Ok" ]] ; then + textFail "$BUCKET_LOCATION: (bucket: $bucket) ALLUSERS_ACL: $S3_FINDING_ALLUSERS_ACL | AUTHUSERS_ACL: $S3_FINDING_AUTHUSERS_ACL | BUCKET_POLICY: $S3_FINDING_POLICY" "$BUCKET_LOCATION" fi done } diff --git a/checks/check_extra731 b/checks/check_extra731 index 5fa5544404..911108ab29 100644 --- a/checks/check_extra731 +++ b/checks/check_extra731 @@ -22,23 +22,26 @@ extra731(){ LIST_SNS=$($AWSCLI sns list-topics $PROFILE_OPT --region $regx --query Topics --output text |grep -v ^None) if [[ $LIST_SNS ]]; then for topic in $LIST_SNS; do - # check if the policy has Principal as * - SNS_TO_CHECK=$($AWSCLI sns get-topic-attributes --topic-arn $topic $PROFILE_OPT --region $regx --query Attributes.Policy --output text | awk -v k="text" '{n=split($0,a,","); for (i=1; i<=n; i++) print a[i]}' | awk '/Principal/ || /Condition/ && !skip { print } { skip = /Deny/}') - PUBLIC_SNS_WCONDITION=$(echo $SNS_TO_CHECK|grep Condition) SHORT_TOPIC=$(echo $topic| cut -d: -f6) - if [[ $PUBLIC_SNS_WCONDITION ]]; then - textPass "$regx: SNS topic $SHORT_TOPIC has a Condition" "$regx" - else - PUBLIC_SNS=$(echo $SNS_TO_CHECK|grep \"Principal|grep \*) - if [[ $PUBLIC_SNS ]]; then - textFail "$regx: SNS topic $SHORT_TOPIC seems to be public (Principal: \"*\")" "$regx" + SNS_POLICY=$($AWSCLI sns get-topic-attributes --topic-arn $topic $PROFILE_OPT --region $regx --query Attributes.Policy 2>/dev/null) + SNS_POLICY_ALLOW_ALL=$(echo $SNS_POLICY \ + | jq '. | fromjson' | jq '.Statement[] | select(.Effect=="Allow") | select(.Principal=="*" or .Principal.AWS=="*" or .Principal.CanonicalUser=="*")') + if [[ $SNS_POLICY_ALLOW_ALL ]]; then + SNS_POLICY_ALLOW_ALL_WITHOUT_CONDITION=$(echo $SNS_POLICY \ + | jq '. | fromjson' | jq '.Statement[] | select(.Effect=="Allow") | select(.Principal=="*" or .Principal.AWS=="*" or .Principal.CanonicalUser=="*") | select(has("Condition") | not)') + if [[ $SNS_POLICY_ALLOW_ALL_WITHOUT_CONDITION ]]; then + SNS_POLICY_ALLOW_ALL_WITHOUT_CONDITION_DETAILS=$(echo $SNS_POLICY_ALLOW_ALL_WITHOUT_CONDITION \ + | jq '"[Principal: " + (.Principal|tostring) + " Action: " + (.Action|tostring) + "]"' ) + textFail "$regx: SNS topic policy with public access: $SNS_POLICY_ALLOW_ALL_WITHOUT_CONDITION_DETAILS" "$SHORT_TOPIC" "$regx" else - textPass "$regx: SNS topic $SHORT_TOPIC seems correct" "$regx" + textPass "$regx: SNS topic policy with public access but has a Condition" "$SHORT_TOPIC" "$regx" fi + else + textPass "$regx: SNS topic without public access" "$SHORT_TOPIC" "$regx" fi done else - textInfo "$regx: No SNS topics found" "$regx" + textInfo "$regx: No SNS topic found" "$SHORT_TOPIC" "$regx" fi done } diff --git a/checks/check_extra757 b/checks/check_extra757 index 162c7fc579..0320081a9a 100644 --- a/checks/check_extra757 +++ b/checks/check_extra757 @@ -28,13 +28,13 @@ extra757(){ do EC2_ID=$(echo "$ec2_instace" | awk '{print $1}') LAUNCH_DATE=$(echo "$ec2_instace" | awk '{print $2}') - textFail "$regx: EC2 Instance $EC2_ID running before than $OLDAGE" + textFail "$regx: EC2 Instance $EC2_ID running before than $OLDAGE" "$regx" done <<< "$INSTACES_OLD_THAN_AGE" else - textPass "All Instances newer than 6 months" + textPass "$regx: All Instances newer than 6 months" "$regx" fi else - textInfo "No EC2 Instances Found" + textInfo "$regx: No EC2 Instances Found" "$regx" fi done } diff --git a/checks/check_extra758 b/checks/check_extra758 index 93f1d16e75..1c402aa563 100644 --- a/checks/check_extra758 +++ b/checks/check_extra758 @@ -28,13 +28,13 @@ extra758(){ do EC2_ID=$(echo "$ec2_instace" | awk '{print $1}') LAUNCH_DATE=$(echo "$ec2_instace" | awk '{print $2}') - textFail "$regx: EC2 Instance $EC2_ID running before than $OLDAGE" + textFail "$regx: EC2 Instance $EC2_ID running before than $OLDAGE" "$regx" done <<< "$INSTACES_OLD_THAN_AGE" else - textPass "All Instances newer than 12 months" + textPass "$regx: All Instances newer than 12 months" "$regx" fi else - textInfo "No EC2 Instances Found" + textInfo "$regx: No EC2 Instances Found" "$regx" fi done } diff --git a/checks/check_extra761 b/checks/check_extra761 new file mode 100644 index 0000000000..a74f76de3e --- /dev/null +++ b/checks/check_extra761 @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +# Prowler - the handy cloud security tool (copyright 2019) by Toni de la Fuente +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +CHECK_ID_extra761="7.61" +CHECK_TITLE_extra761="[extra761] Check if EBS Default Encryption is activated (Not Scored) (Not part of CIS benchmark)" +CHECK_SCORED_extra761="NOT_SCORED" +CHECK_TYPE_extra761="EXTRA" +CHECK_ALTERNATE_check761="extra761" + +extra761(){ + textInfo "Looking for EBS Default Encryption activation in all regions... " + for regx in $REGIONS; do + EBS_DEFAULT_ENCRYPTION=$($AWSCLI ec2 get-ebs-encryption-by-default $PROFILE_OPT --region $regx --query 'EbsEncryptionByDefault') + if [[ $EBS_DEFAULT_ENCRYPTION == "true" ]];then + textPass "$regx: EBS Default Encryption is activated" "$regx" + else + textFail "$regx: EBS Default Encryption is not activated" "$regx" + fi + done +} diff --git a/checks/check_extra762 b/checks/check_extra762 new file mode 100644 index 0000000000..b54cd5ac52 --- /dev/null +++ b/checks/check_extra762 @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +# Prowler - the handy cloud security tool (copyright 2019) by Toni de la Fuente +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +CHECK_ID_extra762="7.62" +CHECK_TITLE_extra762="[extra762] Find obsolete Lambda runtimes (Not Scored) (Not part of CIS benchmark)" +CHECK_SCORED_extra762="NOT_SCORED" +CHECK_TYPE_extra762="EXTRA" +CHECK_ALTERNATE_check762="extra762" + +extra762(){ + + # regex to match OBSOLETE runtimes in string functionName%runtime + # https://docs.aws.amazon.com/lambda/latest/dg/runtime-support-policy.html + OBSOLETE='%(nodejs4.3|nodejs4.3-edge|nodejs6.10|nodejs8.10|dotnetcore1.0|dotnetcore2.0)' + + for regx in $REGIONS; do + LIST_OF_FUNCTIONS=$($AWSCLI lambda list-functions $PROFILE_OPT --region $regx --output text --query 'Functions[*].{R:Runtime,N:FunctionName}' | tr "\t" "%") + if [[ $LIST_OF_FUNCTIONS ]]; then + for lambdafunction in $LIST_OF_FUNCTIONS;do + fname=$(echo "$lambdafunction" | cut -d'%' -f1) + runtime=$(echo "$lambdafunction" | cut -d'%' -f2) + if echo "$lambdafunction" | grep -Eq $OBSOLETE ; then + textFail "$regx: Obsolete runtime: ${runtime} used by: ${fname}" "$regx" + else + textPass "$regx: Supported runtime: ${runtime} used by: ${fname}" "$regx" + fi + done + else + textInfo "$regx: No Lambda functions found" "$regx" + fi + done +} diff --git a/groups/group7_extras b/groups/group7_extras index 2d51b26034..b186e032b1 100644 --- a/groups/group7_extras +++ b/groups/group7_extras @@ -15,7 +15,7 @@ GROUP_ID[7]='extras' GROUP_NUMBER[7]='7.0' GROUP_TITLE[7]='Extras - [extras] **********************************************' GROUP_RUN_BY_DEFAULT[7]='Y' # run it when execute_all is called -GROUP_CHECKS[7]='extra71,extra72,extra73,extra74,extra75,extra76,extra77,extra78,extra79,extra710,extra711,extra712,extra713,extra714,extra715,extra716,extra717,extra718,extra719,extra720,extra721,extra722,extra723,extra724,extra725,extra726,extra727,extra728,extra729,extra730,extra731,extra732,extra733,extra734,extra735,extra736,extra737,extra738,extra739,extra740,extra741,extra742,extra743,extra744,extra745,extra746,extra747,extra748,extra749,extra750,extra751,extra752,extra753,extra754,extra755,extra756,extra757,extra758' +GROUP_CHECKS[7]='extra71,extra72,extra73,extra74,extra75,extra76,extra77,extra78,extra79,extra710,extra711,extra712,extra713,extra714,extra715,extra716,extra717,extra718,extra719,extra720,extra721,extra722,extra723,extra724,extra725,extra726,extra727,extra728,extra729,extra730,extra731,extra732,extra733,extra734,extra735,extra736,extra737,extra738,extra739,extra740,extra741,extra742,extra743,extra744,extra745,extra746,extra747,extra748,extra749,extra750,extra751,extra752,extra753,extra754,extra755,extra756,extra757,extra758,extra761,extra762' # Extras 759 and 760 (lambda variables and code secrets finder are not included) -# to run detect-secrets use `./prowler -g secrets` \ No newline at end of file +# to run detect-secrets use `./prowler -g secrets` diff --git a/groups/group9_gdpr b/groups/group9_gdpr index 7df4c6ecaf..858e4ab647 100644 --- a/groups/group9_gdpr +++ b/groups/group9_gdpr @@ -15,7 +15,7 @@ GROUP_ID[9]='gdpr' GROUP_NUMBER[9]='9.0' GROUP_TITLE[9]='GDPR Readiness - ONLY AS REFERENCE - [gdpr] ********************' GROUP_RUN_BY_DEFAULT[9]='N' # run it when execute_all is called -GROUP_CHECKS[9]='extra718,extra725,extra727,check12,check113,check114,extra71,extra731,extra732,extra733,check25,check39,check21,check22,check23,check24,check26,check27,check35,extra726,extra714,extra715,extra717,extra719,extra720,extra721,extra722,check43,check25,extra714,extra729,extra734,extra735,extra736,extra738,extra740' +GROUP_CHECKS[9]='extra718,extra725,extra727,check12,check113,check114,extra71,extra731,extra732,extra733,check25,check39,check21,check22,check23,check24,check26,check27,check35,extra726,extra714,extra715,extra717,extra719,extra720,extra721,extra722,check43,check25,extra714,extra729,extra734,extra735,extra736,extra738,extra740,extra761' # Resources: # https://d1.awsstatic.com/whitepapers/compliance/GDPR_Compliance_on_AWS.pdf diff --git a/prowler b/prowler index 7f92e7b1e2..fb60a32f41 100755 --- a/prowler +++ b/prowler @@ -252,12 +252,10 @@ execute_group() { NEW_CHECKS=() IFS=',' read -ra EXCLUDED_CHECKS <<< "${2}," for exc in ${EXCLUDED_CHECKS[@]} ; do - for i in ${CHECKS[@]} ; do - [[ ${i} != ${exc} ]] && NEW_CHECKS+=(${i}) + for i in ${!CHECKS[@]} ; do + [[ ${CHECKS[i]} = ${exc} ]] && unset CHECKS[i] done done - CHECKS=("${NEW_CHECKS[@]}") - unset NEW_CHECKS unset EXCLUDED_CHECKS fi for i in ${CHECKS[@]}; do diff --git a/util/Dockerfile b/util/Dockerfile index 116bc1f4f4..ae0cb0ccc0 100644 --- a/util/Dockerfile +++ b/util/Dockerfile @@ -5,11 +5,13 @@ ARG USERID=34000 RUN addgroup -g ${USERID} ${USERNAME} && \ adduser -s /bin/sh -G ${USERNAME} -D -u ${USERID} ${USERNAME} && \ - apk --update --no-cache add python3 bash curl git jq file && \ + apk --update --no-cache add python3 bash curl jq file && \ pip3 install --upgrade pip && \ - pip install awscli ansi2html boto3 detect-secrets &&\ - git clone https://github.com/toniblyx/prowler/ &&\ - chown -R prowler /prowler/ + pip install awscli ansi2html boto3 detect-secrets + +ADD . /prowler + +RUN chown -R prowler /prowler/ USER ${USERNAME} diff --git a/util/multi-account/Audit_Exec_Role.yaml b/util/multi-account/Audit_Exec_Role.yaml new file mode 100644 index 0000000000..673defaa2e --- /dev/null +++ b/util/multi-account/Audit_Exec_Role.yaml @@ -0,0 +1,73 @@ +--- +AWSTemplateFormatVersion: '2010-09-09' +Description: Prowler Auditing Role - in Control Tower pick AWSControlTowerStackSetRole for IAM role and AWSControlTowerExecution for execution + +Parameters: + + AuditorAccountId: + Default: 987600001234 + Description: AWS Account ID where the audit tooling executes + Type: Number + AuditRolePathName: + Default: '/audit/prowler/XA_AuditRole_Prowler' + Description: Path for role name in audit tooling account + Type: String + +Resources: + XAAuditRole: + Type: "AWS::IAM::Role" + Properties: # /audit/prowler/XA_AuditRole_Prowler + RoleName: XA_AuditRole_Prowler + Path: "/audit/prowler/" + ManagedPolicyArns: + - arn:aws:iam::aws:policy/SecurityAudit + - arn:aws:iam::aws:policy/AWSOrganizationsReadOnlyAccess + - arn:aws:iam::aws:policy/IAMReadOnlyAccess + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Principal: + AWS: # TODO: review permissions to see if this can be narrowed down - code build only perhaps + - !Sub "arn:aws:iam::${AuditorAccountId}:root" + Action: + - "sts:AssumeRole" + - Effect: "Allow" + Principal: + Service: + - "codebuild.amazonaws.com" + Action: + - "sts:AssumeRole" + # TODO: restrict to only AuditorAccount only + Policies: + - PolicyName: "ProwlerPolicyAdditions" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: "ProwlerPolicyAdditions" + Effect: "Allow" + Resource: "*" + Action: + - "acm:describecertificate" + - "acm:listcertificates" + - "es:describeelasticsearchdomainconfig" + - "logs:DescribeLogGroups" + - "logs:DescribeMetricFilters" + - "ses:getidentityverificationattributes" + - "sns:listsubscriptionsbytopic" + - "guardduty:ListDetectors" + - "guardduty:GetDetector" + - "S3:GetEncryptionConfiguration" + - "trustedadvisor:Describe*" + - "cloudtrail:GetEventSelectors" + - "apigateway:GET" + - "support:*" + Metadata: + cfn_nag: + rules_to_suppress: + - id: W28 + reason: "the role name is intentionally static" + - id: W11 + reason: "the policy grants read/view/audit access only, to all resources, by design" + - id: F3 + reason: "Support does not allow or deny access to individual actions" diff --git a/util/multi-account/Audit_Pipeline.yaml b/util/multi-account/Audit_Pipeline.yaml new file mode 100644 index 0000000000..acd0c21652 --- /dev/null +++ b/util/multi-account/Audit_Pipeline.yaml @@ -0,0 +1,414 @@ +--- +AWSTemplateFormatVersion: '2010-09-09' +Description: Prowler Auditing Tools Stack + +Metadata: + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: "Organizations and Accounts" + Parameters: + - pOrgMasterAccounts + - pOrgExcludedAccounts + - pStandAloneAccounts + - Label: + default: "Check Group and Execution" + Parameters: + - pProwlerCheckGroup + - pAuditEveryXHours + - Label: + default: "Advanced" + Parameters: + - pTimeoutMinutes + - pAuditRolePathName + - pCustomProwlerRepo + - pCustomProwlerCloneArgs + ParameterLabels: + pOrgMasterAccounts: + default: "Organization Master Accounts" + pOrgExcludedAccounts: + default: "Excluded Organiztion Members" + pStandAloneAccounts: + default: "Stand-alone Accounts" + pProwlerCheckGroup: + default: "Prowler Check Group" + pAuditEveryXHours: + default: "Perform Audit every X hours" + pTimeoutMinutes: + default: "Permit Audit to run for X minutes" + pAuditRolePathName: + default: "Custom audit role path" + pCustomProwlerRepo: + default: "Custom git repo location for prowler" + pCustomProwlerCloneArgs: + default: "Custom arguments to git clone --depth 1" + +Parameters: + pAuditEveryXHours: + Default: 24 + Type: Number + Description: Number of hours between prowler audit runs. + MinValue: 2 + MaxValue: 168 + pTimeoutMinutes: + Default: 30 + Type: Number + Description: Timeout for running prowler across the fleet + MinValue: 5 + MaxValue: 480 + pAuditRolePathName: + Default: '/audit/prowler/XA_AuditRole_Prowler' + Type: String + Description: Role path and name which prowler will assume in the target accounts (Audit_Exec_Role.yaml) + # TODO: Validation: begins with "/" and does NOT end with "/" + pOrgMasterAccounts: + Description: Comma Separated list of Organization Master Accounts, or 'none' + Default: 'none' + Type: String + MinLength: 4 + AllowedPattern: ^(none|([0-9]{12}(,[0-9]{12})*))$ + ConstraintDescription: comma separated list 12-digit account numbers, or 'none' + pOrgExcludedAccounts: # Comma Separated list of Org Member Accounts to EXCLUDE + Description: Comma Separated list of Skipped Organization Member Accounts, or 'none' + Default: 'none' + Type: String + MinLength: 4 + AllowedPattern: ^(none|([0-9]{12}(,[0-9]{12})*))$ + ConstraintDescription: comma separated list 12-digit account numbers, or 'none' + pStandAloneAccounts: # Comma Separated list of Stand-Alone Accounts + Description: Comma Separated list of Stand-alone Accounts, or 'none' + Default: 'none' + Type: String + MinLength: 4 + AllowedPattern: ^(none|([0-9]{12}(,[0-9]{12})*))$ + ConstraintDescription: comma separated list 12-digit account numbers, or 'none' + pProwlerCheckGroup: + Default: 'cislevel1' + Type: String + Description: Which group of checks should prowler run + AllowedValues: + - 'group1' + - 'group2' + - 'group3' + - 'group4' + - 'cislevel1' + - 'cislevel2' + - 'extras' + - 'forensics-ready' + - 'gdpr' + - 'hipaa' + - 'secrets' + - 'apigateway' + - 'rds' + pCustomProwlerRepo: + Type: String + Default: 'https://github.com/toniblyx/prowler.git' + MinLength: 10 + pCustomProwlerCloneArgs: + Type: String + Default: '--branch master' + MinLength: 0 + ##### TODO + # pResultsBucket: # if specified, use an existing bucket for the data + # pEnableAthena: + # Default: false + # Type: Boolean + # Description: Set to true to enable creation of Athena/QuickSight resources + +#### TODO +# Conditions: +# cUseAthena: False + +Resources: + + # S3 Bucket for Results, Config + ProwlerResults: + Type: "AWS::S3::Bucket" + Properties: + # BucketName: !Sub "audit-results-${AWS::AccountId}" + Tags: + - Key: "data-type" + Value: "it-audit:sensitive" + - Key: "data-public" + Value: "NO" + AccessControl: Private + BucketEncryption: + ServerSideEncryptionConfiguration: + - ServerSideEncryptionByDefault: + SSEAlgorithm: AES256 + PublicAccessBlockConfiguration: + BlockPublicAcls: True + BlockPublicPolicy: True + IgnorePublicAcls: True + RestrictPublicBuckets: True + # LoggingConfiguration: + # TODO: Enable BucketLogging - requires more parameters + DeletionPolicy: "Retain" + Metadata: + cfn_nag: + rules_to_suppress: + - id: W35 + reason: "Bucket logging requires additional configuration not yet supported by this template" + + # Policy to allow assuming the XA_AuditRole_Prowler in target accounts + ProwlerAuditManagerRole: + Type: AWS::IAM::Role + Properties: + RoleName: AuditManagerRole_Prowler + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: codebuild.amazonaws.com + Action: + - sts:AssumeRole + Path: / + Policies: + - PolicyName: AssumeRole-XA_AuditRole_Prowler + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - sts:AssumeRole + Resource: + - !Sub "arn:aws:iam::*:role${pAuditRolePathName}" + - Effect: Allow + Action: + - s3:PutObject + - s3:GetObject + - s3:GetObjectVersion + Resource: + - !Sub "${ProwlerResults.Arn}/*" + - Effect: Allow + Action: + - s3:ListBucket + - s3:HeadBucket + - s3:GetBucketLocation + - s3:GetBucketAcl + Resource: + - !Sub "${ProwlerResults.Arn}" + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: + - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:*" + - !Sub "${ProwlerResults.Arn}" + - Effect: Allow + Action: + - ssm:GetParameters + Resource: + - !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/audit/prowler/config/*" + Metadata: + cfn_nag: + rules_to_suppress: + - id: W28 + reason: "the role name is intentionally static" + - id: W11 + reason: "not sure where the violation of w11 is" + + ## Code Build Job + ProwlerBuildProject: + Type: "AWS::CodeBuild::Project" + Properties: + Name: PerformProwlerAudit + Description: "Run Prowler audit on accounts in targeted organizations" + QueuedTimeoutInMinutes: 480 + TimeoutInMinutes: !Ref pTimeoutMinutes + ServiceRole: !Ref ProwlerAuditManagerRole + EncryptionKey: !Sub "arn:aws:kms:us-east-1:${AWS::AccountId}:alias/aws/s3" + Environment: + Type: "LINUX_CONTAINER" + ComputeType: "BUILD_GENERAL1_MEDIUM" + PrivilegedMode: False + Image: "aws/codebuild/standard:2.0-1.12.0" + ImagePullCredentialsType: "CODEBUILD" + Artifacts: # s3://stack-prowlerresults-randomness/prowler/results/... + Name: "results" + Type: "S3" + Location: !Ref ProwlerResults + Path: "prowler" + NamespaceType: NONE + Packaging: NONE + OverrideArtifactName: False + EncryptionDisabled: False + LogsConfig: # S3/logs/pipeline/ + CloudWatchLogs: + Status: ENABLED + GroupName: "audit/prowler" + StreamName: "codebuild_runs" + S3Logs: + Status: DISABLED + # Location: !Sub "${ProwlerResults.Arn}/codebuild_run_logs" + EncryptionDisabled: False + BadgeEnabled: False + Tags: + - Key: "data-type" + Value: "it-audit:sensitive" + - Key: "data-public" + Value: "NO" + Cache: + Type: "NO_CACHE" + Source: + Type: NO_SOURCE + BuildSpec: | + version: 0.2 + env: + parameter-store: + PROWL_CHECK_GROUP: /audit/prowler/config/check_group + PROWL_MASTER_ACCOUNTS: /audit/prowler/config/orgmaster_accounts + PROWL_STANDALONE_ACCOUNTS: /audit/prowler/config/standalone_accounts + PROWL_SKIP_ACCOUNTS: /audit/prowler/config/skip_accounts + PROWL_AUDIT_ROLE: /audit/prowler/config/audit_role + PROWLER_REPO: /audit/prowler/config/gitrepo + PROWLER_CLONE_ARGS: /audit/prowler/config/gitcloneargs + phases: + install: + runtime-versions: + python: 3.7 + commands: + - aws --version + - git clone --depth 1 $PROWLER_REPO $PROWLER_CLONE_ARGS + pre_build: + commands: + - env | grep PROWL_ + - export OUTBASE=$(date -u +"out/diagnostics/%Y/%m/%d") + - export STAMP=$(date -u +"%Y%m%dT%H%M%SZ") + - mkdir -p $OUTBASE || true + - prowler/prowler -V + - aws sts get-caller-identity > ${OUTBASE}/${STAMP}-caller-id.json + build: + commands: + #### Run Prowler against this account, but don't fail the build + # - export PROWLER_ACCOUNT_ID=$(aws sts get-caller-identity | jq -r '.Account') + # - /bin/bash prowler/prowler -g cislevel1 -M csv -n -k > ${OUTBASE}/${STAMP}.${PROWLER_ACCOUNT_ID}.prowler.cislevel1.csv || /bin/true + # - /bin/bash prowler/prowler -g forensics-ready -M csv -n -k > ${OUTBASE}/${STAMP}.${PROWLER_ACCOUNT_ID}.prowler.forensics-ready.csv || /bin/true + #### Run Prowler targeting all accounts in the configured organizations + - test -f prowler/util/multi-account/config + - /bin/bash prowler/util/multi-account/megaprowler.sh out + finally: + - ps axuwww | grep -E 'parallel|sem|prowler' + post_build: + commands: + - echo "attempting to collect any prowler credential reports ..." + - find /tmp/ -name prowler\* | xargs -I % cp % ${OUTDIAG} || true + artifacts: + files: + - '**/*' + discard-paths: no + base-directory: out + + + + + ProwlerAuditTriggerRole: + Type: AWS::IAM::Role + Properties: + # RoleName: Let cloudformation create this + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: events.amazonaws.com + Action: + - sts:AssumeRole + Path: / + Policies: + - PolicyName: AssumeRole-XA_AuditRole_Prowler + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - codebuild:StartBuild + Resource: + - !GetAtt ProwlerBuildProject.Arn + + ProwlerAuditTrigger: + Type: AWS::Events::Rule + Properties: + Description: !Sub "Execute Prowler audit every ${pAuditEveryXHours} hours" + Name: "ScheduledProwler" + RoleArn: !GetAtt ProwlerAuditTriggerRole.Arn + ## Other ways to define scheduling + # ScheduleExpression: "cron(MM HH ? * * *)" + # ScheduleExpression: "cron(45 15 ? * * *)" + # ScheduleExpression: !Sub "rate( ${pAuditEveryXHours} hours)" + ScheduleExpression: !Sub "rate(${pAuditEveryXHours} hours)" + State: ENABLED + Targets: + - Arn: !GetAtt ProwlerBuildProject.Arn + Id: 'ScheduledProwler' + RoleArn: !GetAtt ProwlerAuditTriggerRole.Arn + + ProwlerConfigCheckGroup: + Type: AWS::SSM::Parameter + Properties: + Description: "Name of the prowler check group to use" + Name: "/audit/prowler/config/check_group" + Type: "String" + Value: !Ref pProwlerCheckGroup + + ProwlerConfigMasterAccounts: + Type: AWS::SSM::Parameter + Properties: + Description: "List of organization master accounts" + Name: "/audit/prowler/config/orgmaster_accounts" + Type: "String" + Value: !Ref pOrgMasterAccounts + + ProwlerConfigStandAloneAccounts: + Type: AWS::SSM::Parameter + Properties: + Description: "List of stand-alone accounts" + Name: "/audit/prowler/config/standalone_accounts" + Type: "String" + Value: !Ref pStandAloneAccounts + + ProwlerConfigSkipAccounts: + Type: AWS::SSM::Parameter + Properties: + Description: "List of skipped organization member accounts" + Name: "/audit/prowler/config/skip_accounts" + Type: "String" + Value: !Ref pOrgExcludedAccounts + + ProwlerConfigAuditRole: + Type: AWS::SSM::Parameter + Properties: + Description: "Role used to audit target accounts" + Name: "/audit/prowler/config/audit_role" + Type: "String" + Value: !Ref pAuditRolePathName + + ProwlerConfigGitRepo: + Type: AWS::SSM::Parameter + Properties: + Description: "Git repository where prowler is gathered" + Name: "/audit/prowler/config/gitrepo" + Type: "String" + Value: !Ref pCustomProwlerRepo + + ProwlerConfigGitCloneArgs: + Type: AWS::SSM::Parameter + Properties: + Description: "Git clone arguments" + Name: "/audit/prowler/config/gitcloneargs" + Type: "String" + Value: !Ref pCustomProwlerCloneArgs + + + # -- Conditional "cUseAthena" + # Athena + # QuickSight + # ??? + + +Outputs: + ResultsBucket: + Description: S3 Bucket with Prowler Results, Logs, Configs + Value: !Ref ProwlerResults diff --git a/util/multi-account/config b/util/multi-account/config new file mode 100644 index 0000000000..12c354927b --- /dev/null +++ b/util/multi-account/config @@ -0,0 +1,33 @@ +#!/bin/bash +########### CODEBUILD CONFIGURATION ################## +# shellcheck disable=SC2034 +## Collect environment parameters set by buildspec +CHECKGROUP=${PROWL_CHECK_GROUP} + +if [ "none" == "${PROWL_MASTER_ACCOUNTS}" ]; then + ORG_MASTERS="" +else + ORG_MASTERS=$(echo "${PROWL_MASTER_ACCOUNTS}" | tr "," " ") +fi + +if [ "none" == "${PROWL_STANDALONE_ACCOUNTS}" ]; then + STANDALONE_ACCOUNTS="" +else + STANDALONE_ACCOUNTS=$(echo "${PROWL_STANDALONE_ACCOUNTS}" | tr "," " ") +fi + +if [ "none" == "${PROWL_SKIP_ACCOUNTS}" ]; then + SKIP_ACCOUNTS_REGEX='^$' +else + skip_inside=$(echo "${PROWL_SKIP_ACCOUNTS}" | tr "," "|") + # shellcheck disable=SC2116 + SKIP_ACCOUNTS_REGEX=$(echo "(${skip_inside})" ) +fi + +AUDIT_ROLE=${PROWL_AUDIT_ROLE} + +# Adjust if you clone prowler from somewhere other than the default location +PROWLER='prowler/prowler' + +# Change this if you want to ensure it breaks in code build +CREDSOURCE='EcsContainer' diff --git a/util/multi-account/megaprowler.sh b/util/multi-account/megaprowler.sh new file mode 100644 index 0000000000..623b642e2a --- /dev/null +++ b/util/multi-account/megaprowler.sh @@ -0,0 +1,202 @@ +#!/bin/bash + +BASEDIR=$(dirname "${0}") +# source the configuration data from "config" in this directory +if [[ -f "${BASEDIR}/config" ]]; then + # shellcheck disable=SC1090 + . "${BASEDIR}/config" + +else + echo "CONFIG file missing - ${BASEDIR}/config" + exit 255 +fi + +## Check Environment variables which are set by config +if [[ "${ORG_MASTERS}X" == "X" && "${STANDALONE_ACCOUNTS}X" == "X" ]]; then + echo "No audit targets specified. Failing." + exit 15 +fi +if [[ -z $SKIP_ACCOUNTS_REGEX ]]; then + SKIP_ACCOUNTS_REGEX="" +fi + +if [[ -z $CHECKGROUP ]]; then + echo "Missing check group from config file" + exit 255 +fi +if [[ -z $AUDIT_ROLE ]]; then + echo "Missing audit role from config file" + exit 255 +fi + +## ======================================================================================== + +## Check Arguments +if [ $# -lt 1 ]; then + echo "NEED AN OUTPUT DIRECTORY" + exit 2 +else + if [[ -d $1 && -w $1 ]]; then + OUTBASE=$1 + else + echo "Output directory missing or write-protected" + exit 1 + fi +fi + + +## Check Requirements +if [[ -x $(command -v aws) ]]; then + aws --version +else + echo "AWS CLI is not in PATH ... giving up" + exit 4 +fi + +if [[ -x $(command -v jq) ]]; then + jq --version +else + echo "JQ is not in PATH ... giving up" + exit 4 +fi + +# Ensure AWS Credentials are present in environment +if [[ -z $CREDSOURCE ]]; then + echo "No source for base credentials ... giving up" + exit 5 +fi + +if [[ -f ${PROWLER} && -x ${PROWLER} ]]; then + ${PROWLER} -V +else + echo "Unable to execute prowler from ${PROWLER}" + exit 3 +fi + + +## Preflight checks complete + +DAYPATH=$(date -u +%Y/%m/%d) +STAMP=$(date -u +%Y%m%dT%H%M%SZ) +## Create output subdirs +OUTDATA="${OUTBASE}/data/${DAYPATH}" +OUTLOGS="${OUTBASE}/logs/${DAYPATH}" +mkdir -p "${OUTDATA}" "${OUTLOGS}" + + +if [[ -x $(command -v parallel) ]]; then + # Note: the "standard" codebuild container includes parallel + echo "Using GNU sem/parallel, with NCPU+4 jobs" + parallel --citation > /dev/null 2> /dev/null + PARALLEL_START="parallel --semaphore --fg --id p_${STAMP} --jobs +4 --env AWS_SHARED_CREDENTIALS_FILE" + PARALLEL_START_SUFFIX='' + PARALLEL_END="parallel --semaphore --wait --id p_${STAMP}" +else + echo "Consider installing GNU Parallel to avoid punishing your system" + PARALLEL_START='' + PARALLEL_START_SUFFIX=' &' + # shellcheck disable=SC2089 + PARALLEL_END="echo 'WAITING BLINDLY FOR PROCESSES TO COMPLETE'; wait ; sleep 30 ; wait" +fi + +echo "Execution Timestamp: ${STAMP}" + +ALL_ACCOUNTS="" + + +# Create a temporary credential file +AWS_MASTERS_CREDENTIALS_FILE=$(mktemp -t prowler.masters-XXXXXX) +echo "Preparing Credentials ${AWS_MASTERS_CREDENTIALS_FILE} ( ${CREDSOURCE} )" +echo "# Master Credentials ${STAMP}" >> "${AWS_MASTERS_CREDENTIALS_FILE}" +echo "" >> "${AWS_MASTERS_CREDENTIALS_FILE}" + +AWS_TARGETS_CREDENTIALS_FILE=$(mktemp -t prowler.targets-XXXXXX) +echo "Preparing Credentials ${AWS_TARGETS_CREDENTIALS_FILE} ( ${CREDSOURCE} )" +echo "# Target Credentials ${STAMP}" >> "${AWS_TARGETS_CREDENTIALS_FILE}" +echo "" >> "${AWS_TARGETS_CREDENTIALS_FILE}" + + +## Visit the Organization Master accounts & build a list of all member accounts +export AWS_SHARED_CREDENTIALS_FILE=$AWS_MASTERS_CREDENTIALS_FILE +for org in $ORG_MASTERS ; do + echo -n "Preparing organization $org " + # create credential profile + { + echo "[audit_${org}]" + echo "role_arn = arn:aws:iam::${org}:role${AUDIT_ROLE}" + echo "credential_source = ${CREDSOURCE}" + echo "" + } >> "${AWS_MASTERS_CREDENTIALS_FILE}" + + # Get the Organization ID to use for output paths, collecting info, etc + org_id=$(aws --output json --profile "audit_${org}" organizations describe-organization | jq -r '.Organization.Id' ) + + echo "( $org_id )" + ORG_ID_LIST="${ORG_ID_LIST} ${org_id}" + + + # Build the list of all accounts in the organizations + aws --output json --profile "audit_${org}" organizations list-accounts > "${OUTLOGS}/${STAMP}-${org_id}-account-list.json" + # shellcheck disable=SC2002 + ORG_ACCOUNTS=$( cat "${OUTLOGS}/${STAMP}-${org_id}-account-list.json" | jq -r '.Accounts[].Id' | tr "\n" " ") + ALL_ACCOUNTS="${ALL_ACCOUNTS} ${ORG_ACCOUNTS}" + + # Add the Org's Accounts (including master) to the TARGETS_CREDENTIALS file + for target in $ORG_ACCOUNTS ; do + if echo "$target" | grep -qE "${SKIP_ACCOUNTS_REGEX}"; then + echo " skipping account ${target} ( ${org_id} )" + continue + fi + # echo " ${org_id}_${target}" + { + echo "[${org_id}_${target}]" + echo "role_arn = arn:aws:iam::${target}:role${AUDIT_ROLE}" + echo "credential_source = ${CREDSOURCE}" + echo "" + } >> "${AWS_TARGETS_CREDENTIALS_FILE}" + done + +done + +# Prepare credentials for standalone accounts +if [[ "" != "${STANDALONE_ACCOUNTS}" ]] ; then + # mkdir -p ${OUTBASE}/data/standalone/${DAYPATH} ${OUTBASE}/logs/standalone/${DAYPATH} + for target in $STANDALONE_ACCOUNTS ; do + echo "Preparing account ${target} ( standalone )" + { + echo "[standalone_${target}]" + echo "role_arn = arn:aws:iam::${target}:role${AUDIT_ROLE}" + echo "credential_source = ${CREDSOURCE}" + echo "" + } >> "${AWS_TARGETS_CREDENTIALS_FILE}" + done + ALL_ACCOUNTS="${ALL_ACCOUNTS} ${STANDALONE_ACCOUNTS}" +fi + +# grep -E '^\[' $AWS_MASTERS_CREDENTIALS_FILE $AWS_TARGETS_CREDENTIALS_FILE + + +# Switch to Target Credential Set +export AWS_SHARED_CREDENTIALS_FILE=${AWS_TARGETS_CREDENTIALS_FILE} + +## visit each target account +NUM_ACCOUNTS=$(grep -cE '^\[' "${AWS_TARGETS_CREDENTIALS_FILE}") +echo "Launching ${CHECKGROUP} audit of ${NUM_ACCOUNTS} accounts" +for member in $(grep -E '^\[' "${AWS_TARGETS_CREDENTIALS_FILE}" | tr -d '][') ; do + ORG_ID=$(echo "$member" | cut -d'_' -f1) + ACCOUNT_NUM=$(echo "$member" | cut -d'_' -f2) + + # shellcheck disable=SC2086 + ${PARALLEL_START} "${PROWLER} -p ${member} -n -M csv -g ${CHECKGROUP} 2> ${OUTLOGS}/${STAMP}-${ORG_ID}-${ACCOUNT_NUM}-prowler-${CHECKGROUP}.log > ${OUTDATA}/${STAMP}-${ORG_ID}-${ACCOUNT_NUM}-prowler-${CHECKGROUP}.csv ; echo \"${ORG_ID}-${ACCOUNT_NUM}-prowler-${CHECKGROUP} finished\" " ${PARALLEL_START_SUFFIX} +done + +echo -n "waiting for parallel threads to complete - " ; date +# shellcheck disable=SC2090 +${PARALLEL_END} + +echo "Completed ${CHECKGROUP} audit with stamp ${STAMP}" + +# mkdir -p ${OUTBASE}/logs/debug/${DAYPATH} +# cp "$AWS_MASTERS_CREDENTIALS_FILE" "${OUTLOGS}/${STAMP}-master_creds.txt" +# cp "$AWS_TARGETS_CREDENTIALS_FILE" "${OUTLOGS}/${STAMP}-target_creds.txt" +rm "$AWS_MASTERS_CREDENTIALS_FILE" "$AWS_TARGETS_CREDENTIALS_FILE"