diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 0b78f7ab1b..0000000000 --- a/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM toniblyx/prowler -MAINTAINER Bridgecrew - -USER root - -WORKDIR "./prowler" -COPY "checks" "checks" -COPY "./run.sh" "." - -RUN chown -R prowler /prowler/ - -USER prowler - -ENTRYPOINT ["./run.sh"] diff --git a/README.md b/README.md index 8ce7773e25..ac703a8137 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ - [Features](#features) - [Requirements and Installation](#requirements-and-installation) - [Usage](#usage) +- [Advanced Usage](#advanced-usage) - [Fix](#fix) - [Screenshots](#screenshots) - [Troubleshooting](#troubleshooting) @@ -63,6 +64,11 @@ This script has been written in bash using AWS-CLI and it works in Linux and OSX 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. +- Make sure jq is installed (example below with "apt" but use a valid package manager for your OS): + ```sh + sudo apt install jq + ``` + - Previous steps, from your workstation: ```sh @@ -70,7 +76,7 @@ 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 or declare AWS variables properly: +- Make sure you have properly configured your AWS-CLI with a valid Access Key and Region or declare AWS variables properly (or intance profile): ```sh aws configure @@ -88,7 +94,7 @@ This script has been written in bash using AWS-CLI and it works in Linux and OSX arn:aws:iam::aws:policy/SecurityAudit ``` - > In some cases you may need more list or get permissions in some services, look at the Troubleshooting section for a more comprehensive policy if you find issues with the default SecurityAudit policy. + > Additional permissions needed: to make sure Prowler can scan all services included in the group *Extras*, make sure you attach also the custom policy [prowler-additions-policy.json](https://github.com/toniblyx/prowler/blob/master/iam/prowler-additions-policy.json) to the role you are using. ## Usage @@ -222,9 +228,50 @@ This script has been written in bash using AWS-CLI and it works in Linux and OSX -b do not print Prowler banner -V show version number & exit -s show scoring report + -x specify external directory with custom checks (i.e. /my/own/checks, files must start by check) + -q suppress info messages and passing test output + -A account id for the account where to assume a role, requires -R and -T + (i.e.: 123456789012) + -R role name to assume in the account, requires -A and -T + (i.e.: ProwlerRole) + -T session durantion given to that role credentials in seconds, default 1h (3600) recommended 12h, requires -R and -T + (i.e.: 43200) -h this help ``` +## Advanced Usage + +### Assume Role: + +Prowler uses the AWS CLI underneath so it uses the same authentication methods. However, there are few ways to run Prowler against multiple accounts using IAM Assume Role feature depending on eachg use case. You can just set up your custom profile inside `~/.aws/config` with all needed information about the role to assume then call it with `./prowler -p your-custom-profile`. Additionally you can use `-A 123456789012` and `-R RemoteRoleToAssume` and Prowler will get those temporary credentials using `aws sts assume-role`, set them up as environment variables and run against that given account. + +``` +./prowler -A 123456789012 -R ProwlerRole +``` + +> *NOTE 1 about Session Duration*: By default it gets credentials valid for 1 hour (3600 seconds). Depending on the mount of checks you run and the size of your infrastructure, Prowler may require more than 1 hour to finish. Use option `-T ` to allow up to 12h (43200 seconds). To allow more than 1h you need to modify *"Maximum CLI/API session duration"* for that particular role, read more [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use.html#id_roles_use_view-role-max-session). + +> *NOTE 2 about Session Duration*: Bear in mind that if you are using roles assumed by role chaining there is a hard limit of 1 hour so consider not using role chaining if possible, read more about that, in foot note 1 below the table [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use.html). + +For example, if you want to get only the fails in CSV format from all checks regarding RDS without banner from the AWS Account 123456789012 assuming the role RemoteRoleToAssume and set a fixed session duration of 1h: + +``` +./prowler -A 123456789012 -R RemoteRoleToAssume -T 3600 -b -M cvs -q -g rds +``` + +### Custom folder for custom checks + +Flag `-x /my/own/checks` will include any check in that particular directory. To see how to write checks see [Add Custom Checks](#add-custom-checks) section. + +### Show or log only FAILs + +In order to remove noise and get only FAIL findings there is a `-q` flag that makes Prowler to show and log only FAILs. It can be combined with any other option. + +``` +./prowler -q -M csv -b +``` + + ## How to fix every FAIL Check your report and fix the issues following all specific guidelines per check in diff --git a/checks/check23 b/checks/check23 index 63ccd4d75e..53d1b6f6ba 100644 --- a/checks/check23 +++ b/checks/check23 @@ -17,10 +17,14 @@ CHECK_ALTERNATE_check203="check23" check23(){ # "Ensure the S3 bucket CloudTrail logs to is not publicly accessible (Scored)" CLOUDTRAILBUCKET=$($AWSCLI cloudtrail describe-trails --query 'trailList[*].S3BucketName' --output text $PROFILE_OPT --region $REGION) - if [[ $CLOUDTRAILBUCKET ]];then + if [[ $CLOUDTRAILBUCKET ]]; then for bucket in $CLOUDTRAILBUCKET;do - CLOUDTRAILBUCKET_HASALLPERMISIONS=$($AWSCLI s3api get-bucket-acl --bucket $bucket --query 'Grants[?Grantee.URI==`http://acs.amazonaws.com/groups/global/AllUsers`]' $PROFILE_OPT --region $REGION --output text) - if [[ $CLOUDTRAILBUCKET_HASALLPERMISIONS ]];then + CLOUDTRAILBUCKET_HASALLPERMISIONS=$($AWSCLI s3api get-bucket-acl --bucket $bucket --query 'Grants[?Grantee.URI==`http://acs.amazonaws.com/groups/global/AllUsers`]' $PROFILE_OPT --region $REGION --output text 2>&1) + if [[ $(echo "$CLOUDTRAILBUCKET_HASALLPERMISIONS" | grep AccessDenied) ]]; then + textFail "Access Denied Trying to Get Bucket Acl for $bucket" + continue + fi + if [[ $CLOUDTRAILBUCKET_HASALLPERMISIONS ]]; then textFail "check your $bucket CloudTrail bucket ACL and Policy!" else textPass "Bucket $bucket is set correctly" diff --git a/checks/check26 b/checks/check26 index 18d92545d6..73c1812097 100644 --- a/checks/check26 +++ b/checks/check26 @@ -20,16 +20,20 @@ check26(){ CLOUDTRAILS=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region "$REGION" --query 'trailList[*].Name' --output text| tr '\011' '\012' | awk -F: '{print $1}') ACCOUNT_ID=$($AWSCLI sts get-caller-identity --output json $PROFILE_OPT --region $REGION --query "Account" | tr -d '"') - if [[ $CLOUDTRAILS ]];then + if [[ $CLOUDTRAILS ]]; then for trail in $CLOUDTRAILS; do CLOUDTRAIL_ACCOUNT_ID=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region "$REGION" --query 'trailList[*].TrailARN' --output text | tr '\011' '\012' | grep "$trail" | awk -F: '{ print $5 }' | head -n 1) CLOUDTRAILBUCKET=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].[Name, S3BucketName]' --output text | tr '\011' ':' | grep "$trail" | awk -F: '{ print $2 }' ) - if [[ $CLOUDTRAILBUCKET ]];then + if [[ $CLOUDTRAILBUCKET ]]; then bucket=$CLOUDTRAILBUCKET - if [ "$CLOUDTRAIL_ACCOUNT_ID" == "$ACCOUNT_NUM" ];then - CLOUDTRAILBUCKET_LOGENABLED=$($AWSCLI s3api get-bucket-logging --bucket $bucket $PROFILE_OPT --region $REGION --query 'LoggingEnabled.TargetBucket' --output text|grep -v None) - if [[ $CLOUDTRAILBUCKET_LOGENABLED ]];then + if [ "$CLOUDTRAIL_ACCOUNT_ID" == "$ACCOUNT_NUM" ]; then + CLOUDTRAILBUCKET_LOGENABLED=$($AWSCLI s3api get-bucket-logging --bucket $bucket $PROFILE_OPT --region $REGION --query 'LoggingEnabled.TargetBucket' --output text 2>&1) + if [[ $(echo "$CLOUDTRAILBUCKET_LOGENABLED" | grep AccessDenied) ]]; then + textFail "Access Denied Trying to Get Bucket Logging for $bucket" + continue + fi + if [[ $CLOUDTRAILBUCKET_LOGENABLED != "null" ]]; then textPass "Bucket access logging enabled in CloudTrail S3 bucket $bucket for $trail" else textFail "Bucket access logging is not enabled in CloudTrail S3 bucket $bucket for $trail" @@ -37,7 +41,6 @@ check26(){ else textInfo "CloudTrail S3 bucket $bucket for trail $trail is not in current account" fi - else textFail "CloudTrail bucket not found!" fi diff --git a/checks/check_extra716 b/checks/check_extra716 index ea45909862..404462b1b2 100644 --- a/checks/check_extra716 +++ b/checks/check_extra716 @@ -36,10 +36,10 @@ extra716(){ else textPass "$regx: $domain is in a VPC" "$regx" fi + rm -f $TEMP_POLICY_FILE done else textInfo "$regx: No Elasticsearch Service domain found" "$regx" fi - rm -fr $TEMP_POLICY_FILE done } diff --git a/checks/check_extra723 b/checks/check_extra723 index 589df54884..96039eb993 100644 --- a/checks/check_extra723 +++ b/checks/check_extra723 @@ -11,7 +11,7 @@ # CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. CHECK_ID_extra723="7.23" -CHECK_TITLE_extra723="[extra723] Check if RDS Snapshots are public (Not Scored) (Not part of CIS benchmark)" +CHECK_TITLE_extra723="[extra723] Check if RDS Snapshots and Cluster Snapshots are public (Not Scored) (Not part of CIS benchmark)" CHECK_SCORED_extra723="NOT_SCORED" CHECK_TYPE_extra723="EXTRA" CHECK_ALTERNATE_check723="extra723" diff --git a/checks/check_extra726 b/checks/check_extra726 index 388d01a16e..b2eee1a401 100644 --- a/checks/check_extra726 +++ b/checks/check_extra726 @@ -29,12 +29,23 @@ extra726(){ for checkid in $TA_CHECKS_ID; do TA_CHECKS_NAME=$($AWSCLI support describe-trusted-advisor-checks --language en $PROFILE_OPT --region us-east-1 --query "checks[?id==\`$checkid\`].{name:name}[*]" --output text) QUERY_TA_CHECK_RESULT=$($AWSCLI support describe-trusted-advisor-check-result --check-id $checkid --language en $PROFILE_OPT --region us-east-1 --query 'result.status' --output text) - if [[ $(echo $QUERY_TA_CHECK_RESULT | grep ok) ]]; then - textPass "Trusted Advisor check $TA_CHECKS_NAME is in state $QUERY_TA_CHECK_RESULT" - elif [[ $(echo $QUERY_TA_CHECK_RESULT | grep warning) ]]; then - textInfo "Trusted Advisor check $TA_CHECKS_NAME is in state $QUERY_TA_CHECK_RESULT" - else - textFail "Trusted Advisor check $TA_CHECKS_NAME is in state $QUERY_TA_CHECK_RESULT" - fi + # Possible results - https://docs.aws.amazon.com/cli/latest/reference/support/describe-trusted-advisor-check-result.html + case "$QUERY_TA_CHECK_RESULT" in + "ok") + textPass "Trusted Advisor check $TA_CHECKS_NAME is in ok state $QUERY_TA_CHECK_RESULT" + ;; + "error") + textFail "Trusted Advisor check $TA_CHECKS_NAME is in error state $QUERY_TA_CHECK_RESULT" + ;; + "warning") + textInfo "Trusted Advisor check $TA_CHECKS_NAME is in warning state $QUERY_TA_CHECK_RESULT" + ;; + "not_available") + textInfo "Trusted Advisor check $TA_CHECKS_NAME is in not_available state $QUERY_TA_CHECK_RESULT" + ;; + "*") + textFail "Trusted Advisor check $TA_CHECKS_NAME is in unknown state $QUERY_TA_CHECK_RESULT" + ;; + esac done } diff --git a/checks/check_extra73 b/checks/check_extra73 index 8e203b7985..a587a60f6a 100644 --- a/checks/check_extra73 +++ b/checks/check_extra73 @@ -10,6 +10,7 @@ # 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_extra73="7.3,7.03" CHECK_TITLE_extra73="[extra73] Ensure there are no S3 buckets open to the Everyone or Any AWS user (Not Scored) (Not part of CIS benchmark)" CHECK_SCORED_extra73="NOT_SCORED" @@ -18,46 +19,65 @@ CHECK_ALTERNATE_extra703="extra73" CHECK_ALTERNATE_check73="extra73" CHECK_ALTERNATE_check703="extra73" -# Improved and simplified check on Nov 18th 2018 due to a new bucket attribute -# called PolicyStatus, not available in all regions yet. - -# extra73(){ -# ALL_BUCKETS_LIST=$($AWSCLI s3api list-buckets --query 'Buckets[*].{Name:Name}' $PROFILE_OPT --region $REGION --output text) -# for bucket in $ALL_BUCKETS_LIST; do -# BUCKET_LOCATION=$($AWSCLI s3api get-bucket-location --bucket $bucket $PROFILE_OPT --region $REGION --output text 2>&1) -# if [[ $(echo "$BUCKET_LOCATION" | grep AccessDenied) ]]; then -# textFail "Access Denied Trying to Get Bucket Location for $bucket" -# continue -# fi -# if [[ "None" == $BUCKET_LOCATION ]]; then -# BUCKET_LOCATION="us-east-1" -# fi -# if [[ "EU" == $BUCKET_LOCATION ]]; then -# BUCKET_LOCATION="eu-west-1" -# fi +# Verified with AWS support that if get-bucket-acl doesn't return a grant +# for All and get-bucket-policy-status returns IsPublic false or bad request +# (no policy) then the bucket can be considered not public - though +# individual objects may still be. If in addition put-public-access-block is +# used to set IgnorePublicAcls and RestrictPublicBuckets to true then that +# causes Amazon S3 to ignore all public ACLs on a bucket and any objects that +# it contains. # -# BUCKET_POLICY_STATUS=$($AWSCLI s3api get-bucket-policy-status --bucket $bucket --query PolicyStatus.IsPublic --output text | grep False) -# if [[ $BUCKET_POLICY_STATUS ]];then -# textFail "$BUCKET_LOCATION: $bucket bucket is Public!" "$BUCKET_LOCATION" -# else -# textPass "$BUCKET_LOCATION: $bucket bucket is not Public" "$BUCKET_LOCATION" -# fi -# done -# } - +# This check does not address legacy ACLs or policies that would give +# public access if not blocked at account or bucket level, instead it tries +# to reward the use of more broadly restrictive controls with quicker and less +# computational intensive checks. +# +# If we are assembling an inventory then maybe that is not what we want but +# for day to day usage that is probably desirable. 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) + + # + # If public ACLs disabled at account level then look no further + # + ACCOUNT_PUBLIC_ACCESS_BLOCK=$($AWSCLI s3control get-public-access-block $PROFILE_OPT --region $REGION --account-id $ACCOUNT_NUM --output json 2>&1) + if [[ $(echo "$ACCOUNT_PUBLIC_ACCESS_BLOCK" | grep AccessDenied) ]]; then + textFail "Access Denied Trying to Get Public Access Block for $bucket" + return + fi + if [[ $(echo "$ACCOUNT_PUBLIC_ACCESS_BLOCK" | grep NoSuchPublicAccessBlockConfiguration) ]]; then + ACCOUNTIGNOREPUBLICACLS="" + ACCOUNTRESTRICTPUBLICBUCKETS="" + else + ACCOUNTIGNOREPUBLICACLS=$(echo "$ACCOUNT_PUBLIC_ACCESS_BLOCK" | jq -r '.PublicAccessBlockConfiguration.IgnorePublicAcls') + ACCOUNTRESTRICTPUBLICBUCKETS=$(echo "$ACCOUNT_PUBLIC_ACCESS_BLOCK" | jq -r '.PublicAccessBlockConfiguration.RestrictPublicBuckets') + fi + if [[ $ACCOUNTIGNOREPUBLICACLS == "true" && $ACCOUNTRESTRICTPUBLICBUCKETS == "true" ]]; then + textPass "All S3 public access blocked at account level" + return + fi + + # + # Otherwise start to iterate bucket + # + ALL_BUCKETS_LIST=$($AWSCLI s3api list-buckets --query 'Buckets[*].{Name:Name}' $PROFILE_OPT --output text 2>&1) + if [[ $(echo "$ALL_BUCKETS_LIST" | grep AccessDenied) ]]; then + textFail "Access Denied Trying to List Buckets" + return + fi + if [[ "$ALL_BUCKETS_LIST" == "" ]]; then + textInfo "No buckets found" + return + fi 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 + # + # LOCATION - requests referencing buckets created after March 20, 2019 + # must be made to S3 endpoints in the same region as the bucket was + # created. + # BUCKET_LOCATION=$($AWSCLI s3api get-bucket-location --bucket $bucket $PROFILE_OPT --output text 2>&1) if [[ $(echo "$BUCKET_LOCATION" | grep AccessDenied) ]]; then textFail "Access Denied Trying to Get Bucket Location for $bucket" @@ -70,111 +90,65 @@ extra73(){ BUCKET_LOCATION="eu-west-1" fi - # 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 - textInfo "$BUCKET_LOCATION: $bucket have an explicit Deny. Not possible to get ACL." "$bucket" "$BUCKET_LOCATION" + # + # If public ACLs disabled at bucket level then look no further + # + BUCKET_PUBLIC_ACCESS_BLOCK=$($AWSCLI s3api get-public-access-block $PROFILE_OPT --region $BUCKET_LOCATION --bucket $bucket --output json 2>&1) + if [[ $(echo "$BUCKET_PUBLIC_ACCESS_BLOCK" | grep AccessDenied) ]]; then + textFail "Access Denied Trying to Get Public Access Block for $bucket" + continue + fi + if [[ $(echo "$BUCKET_PUBLIC_ACCESS_BLOCK" | grep NoSuchPublicAccessBlockConfiguration) ]]; then + BUCKETIGNOREPUBLICACLS="" + BUCKETRESTRICTPUBLICBUCKETS="" else - # 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 - ## 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 + BUCKETIGNOREPUBLICACLS=$(echo "$BUCKET_PUBLIC_ACCESS_BLOCK" | jq -r '.PublicAccessBlockConfiguration.IgnorePublicAcls') + BUCKETRESTRICTPUBLICBUCKETS=$(echo "$BUCKET_PUBLIC_ACCESS_BLOCK" | jq -r '.PublicAccessBlockConfiguration.RestrictPublicBuckets') 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" + if [[ $BUCKETIGNOREPUBLICACLS == "true" && $BUCKETRESTRICTPUBLICBUCKETS == "true" ]]; then + textPass "$BUCKET_LOCATION: $bucket bucket is not Public" "$BUCKET_LOCATION" + continue fi + + # + # Check for public ACL grants + # + BUCKET_ACL=$($AWSCLI s3api get-bucket-acl $PROFILE_OPT --region $BUCKET_LOCATION --bucket $bucket --output json 2>&1) + if [[ $(echo "$BUCKET_ACL" | grep AccessDenied) ]]; then + textFail "Access Denied Trying to Get Bucket Acl for $bucket" + continue + fi + + ALLUSERS_ACL=$(echo "$BUCKET_ACL" | jq '.Grants[]|select(.Grantee.URI != null)|select(.Grantee.URI | endswith("/AllUsers"))') + if [[ $ALLUSERS_ACL != "" ]]; then + textFail "$BUCKET_LOCATION: $bucket bucket is Public!" "$BUCKET_LOCATION" + continue + fi + + AUTHENTICATEDUSERS_ACL=$(echo "$BUCKET_ACL" | jq '.Grants[]|select(.Grantee.URI != null)|select(.Grantee.URI | endswith("/AuthenticatedUsers"))') + if [[ $AUTHENTICATEDUSERS_ACL != "" ]]; then + textFail "$BUCKET_LOCATION: $bucket bucket is Public!" "$BUCKET_LOCATION" + continue + fi + + # + # Check for public access in policy + # + BUCKET_POLICY_STATUS=$($AWSCLI s3api get-bucket-policy-status $PROFILE_OPT --region $BUCKET_LOCATION --bucket $bucket --query PolicyStatus.IsPublic --output text 2>&1) + if [[ $(echo "$BUCKET_POLICY_STATUS" | grep AccessDenied) ]]; then + textFail "Access Denied Trying to Get Bucket Policy Status for $bucket" + continue + fi + if [[ $(echo "$BUCKET_POLICY_STATUS" | grep NoSuchBucketPolicy) ]]; then + BUCKET_POLICY_STATUS="False" + fi + + if [[ $BUCKET_POLICY_STATUS != "" && $BUCKET_POLICY_STATUS != "False" ]]; then + textFail "$BUCKET_LOCATION: $bucket bucket is Public!" "$BUCKET_LOCATION" + continue + fi + + textPass "$BUCKET_LOCATION: $bucket bucket is not Public" "$BUCKET_LOCATION" + done } - -# Then implementation below makes pararel checks but can reach AWS API limits -# and eventually doesn't work as expected - -# extra73(){ -# textTitle "$ID73" "$TITLE73" "NOT_SCORED" "EXTRA" -# textNotice "Looking for open S3 Buckets (ACLs and Policies) in all regions... " -# ALL_BUCKETS_LIST=$($AWSCLI s3api list-buckets --query 'Buckets[*].{Name:Name}' --profile $PROFILE --region $REGION --output text) -# for bucket in $ALL_BUCKETS_LIST; do -# extra73Thread $bucket & -# done -# wait -# } -# extra73Thread(){ -# bucket=$1 -# BUCKET_LOCATION=$($AWSCLI s3api get-bucket-location --bucket $bucket --profile $PROFILE --region $REGION --output text 2>&1) -# if [[ $(echo "$BUCKET_LOCATION" | grep AccessDenied) ]]; then -# textFail "Access Denied Trying to Get Bucket Location for $bucket" -# return -# fi -# if [[ "None" == $BUCKET_LOCATION ]]; then -# BUCKET_LOCATION="us-east-1" -# fi -# if [[ "EU" == $BUCKET_LOCATION ]]; then -# BUCKET_LOCATION="eu-west-1" -# fi -# # check if AllUsers is in the ACL as Grantee -# CHECK_BUCKET_ALLUSERS_ACL=$($AWSCLI s3api get-bucket-acl --profile $PROFILE --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 $PROFILE --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 $PROFILE --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 -# textWarn "$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 -# textWarn "$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 -# textWarn "$BUCKET_LOCATION: $bucket bucket policy \"may\" allow Anonymous users to perform actions (Principal: \"*\")" "$BUCKET_LOCATION" -# fi -# else -# textOK "$BUCKET_LOCATION: $bucket bucket is not open" "$BUCKET_LOCATION" -# fi -# rm -fr $TEMP_POLICY_FILE -# } diff --git a/checks/check_extra734 b/checks/check_extra734 index 26c5fbcf2e..bebd2bfcaa 100644 --- a/checks/check_extra734 +++ b/checks/check_extra734 @@ -42,15 +42,15 @@ extra734(){ TEMP_SSE_POLICY_FILE=$(mktemp -t prowler-${ACCOUNT_NUM}-${bucket}.policy.XXXXXXXXXX) # get bucket policy - $AWSCLI s3api get-bucket-policy $PROFILE_OPT --bucket $bucket --output text --query Policy > $TEMP_SSE_POLICY_FILE 2> /dev/null + $AWSCLI s3api get-bucket-policy $PROFILE_OPT --bucket $bucket --output text --query Policy > $TEMP_SSE_POLICY_FILE 2>&1 if [[ $(grep AccessDenied $TEMP_SSE_POLICY_FILE) ]]; then textFail "Access Denied Trying to Get Bucket Policy for $bucket" - rm -fr $TEMP_SSE_POLICY_FILE + rm -f $TEMP_SSE_POLICY_FILE continue fi if [[ $(grep NoSuchBucketPolicy $TEMP_SSE_POLICY_FILE) ]]; then textFail "No bucket policy for $bucket" - rm -fr $TEMP_SSE_POLICY_FILE + rm -f $TEMP_SSE_POLICY_FILE continue fi @@ -58,14 +58,14 @@ extra734(){ CHECK_BUCKET_SSE_POLICY_PRESENT=$(cat $TEMP_SSE_POLICY_FILE | jq --arg arn "arn:aws:s3:::${bucket}/*" '.Statement[]|select(.Effect=="Deny" and ((.Principal|type == "object") and .Principal.AWS == "*") or ((.Principal|type == "string") and .Principal == "*") and .Action=="s3:PutObject" and .Resource==$arn and .Condition.StringEquals."s3:x-amz-server-side-encryption" != null)') if [[ $CHECK_BUCKET_SSE_POLICY_PRESENT == "" ]]; then textFail "Bucket $bucket does not enforce encryption!" - rm -fr $TEMP_SSE_POLICY_FILE + rm -f $TEMP_SSE_POLICY_FILE continue fi CHECK_BUCKET_SSE_POLICY_VALUE=$(echo "$CHECK_BUCKET_SSE_POLICY_PRESENT" | jq -r '.Condition.StringNotEquals."s3:x-amz-server-side-encryption"') textPass "Bucket $bucket has S3 bucket policy to enforce encryption with $CHECK_BUCKET_SSE_POLICY_VALUE" - rm -fr $TEMP_SSE_POLICY_FILE + rm -f $TEMP_SSE_POLICY_FILE done else diff --git a/checks/check_extra741 b/checks/check_extra741 index d815bd1ad2..f4e54d2cca 100644 --- a/checks/check_extra741 +++ b/checks/check_extra741 @@ -43,6 +43,8 @@ extra741(){ rm -f $EC2_USERDATA_FILE else textFail "$regx: Potential secret found in $instance" "$regx" + # delete file to not leave trace, user must look at the instance User Data + rm -f $EC2_USERDATA_FILE fi else mv $EC2_USERDATA_FILE $EC2_USERDATA_FILE.gz ; gunzip $EC2_USERDATA_FILE.gz @@ -56,12 +58,11 @@ extra741(){ fi else textPass "$regx: No secrets found in $instance User Data or it is empty" "$regx" - rm -f $EC2_USERDATA_FILE - fi + fi done else textInfo "$regx: No EC2 instances found" "$regx" fi done -# rm -rf $SECRETS_TEMP_FOLDER + rm -rf $SECRETS_TEMP_FOLDER } diff --git a/checks/check_extra742 b/checks/check_extra742 index 9fff16c7b2..277eb43568 100644 --- a/checks/check_extra742 +++ b/checks/check_extra742 @@ -45,12 +45,12 @@ extra742(){ rm -f $CFN_OUTPUTS_FILE fi else - textInfo "$regx: CloudFormation stack $stack has not Outputs" "$regx" + textInfo "$regx: CloudFormation stack $stack has no Outputs" "$regx" fi done else textInfo "$regx: No CloudFormation stacks found" "$regx" fi done -# rm -rf $SECRETS_TEMP_FOLDER + rm -rf $SECRETS_TEMP_FOLDER } diff --git a/checks/check_extra759 b/checks/check_extra759 index c9287822ab..dbd0763291 100644 --- a/checks/check_extra759 +++ b/checks/check_extra759 @@ -39,15 +39,16 @@ extra759(){ rm -f $LAMBDA_FUNCTION_VARIABLES_FILE else textFail "$regx: Potential secret found in Lambda function $lambdafunction variables" "$regx" + # delete file to not leave trace, user must look at the function + rm -f $LAMBDA_FUNCTION_VARIABLES_FILE fi else textInfo "$regx: Lambda function $stalambdafunction has not variables" "$regx" - rm -f $LAMBDA_FUNCTION_VARIABLES_FILE fi done else textInfo "$regx: No Lambda functions found" "$regx" fi done -# rm -rf $SECRETS_TEMP_FOLDER + rm -rf $SECRETS_TEMP_FOLDER } diff --git a/checks/check_extra760 b/checks/check_extra760 index d56055d385..eac466e72f 100644 --- a/checks/check_extra760 +++ b/checks/check_extra760 @@ -44,13 +44,12 @@ extra760(){ else textFail "$regx: Potential secret found in Lambda function $lambdafunction code" "$regx" # delete files to not leave trace, user must look at the function -# rm -fr $LAMBDA_FUNCTION_FOLDER + rm -fr $LAMBDA_FUNCTION_FOLDER fi done else textInfo "$regx: No Lambda functions found" "$regx" - rm -fr $LAMBDA_FUNCTION_FOLDER fi done -# rm -fr $SECRETS_TEMP_FOLDER + rm -fr $SECRETS_TEMP_FOLDER } diff --git a/checks/check_extra764 b/checks/check_extra764 index e6a4b95086..8b849208a7 100644 --- a/checks/check_extra764 +++ b/checks/check_extra764 @@ -26,10 +26,12 @@ extra764(){ $AWSCLI s3api get-bucket-policy $PROFILE_OPT --bucket $bucket --output text --query Policy > $TEMP_STP_POLICY_FILE 2>&1 if [[ $(grep AccessDenied $TEMP_STP_POLICY_FILE) ]]; then textFail "Access Denied Trying to Get Bucket Policy for $bucket" + rm -f $TEMP_STP_POLICY_FILE continue fi if [[ $(grep NoSuchBucketPolicy $TEMP_STP_POLICY_FILE) ]]; then textFail "No bucket policy for $bucket" + rm -f $TEMP_STP_POLICY_FILE continue fi diff --git a/checks/check_extra765 b/checks/check_extra765 index 3792db18ac..9cbb0930fd 100644 --- a/checks/check_extra765 +++ b/checks/check_extra765 @@ -19,7 +19,6 @@ # --region \ # --repository-name \ # --image-scanning-configuration scanOnPush=true - CHECK_ID_extra765="7.65" CHECK_TITLE_extra765="[extra765] Check if ECR image scan on push is enabled (Not Scored) (Not part of CIS benchmark)" @@ -28,19 +27,32 @@ CHECK_TYPE_extra765="EXTRA" CHECK_ALTERNATE_check765="extra765" extra765(){ - for regx in $REGIONS; do - LIST_ECR_REPOS=$($AWSCLI ecr describe-repositories $PROFILE_OPT --region $regx --query "repositories[*].[repositoryName]" --output text 2>&1) - if [[ $LIST_ECR_REPOS ]]; then + for region in $REGIONS; do + LIST_ECR_REPOS=$($AWSCLI ecr describe-repositories $PROFILE_OPT --region $region --query "repositories[*].[repositoryName]" --output text 2>&1) + if [[ $(echo "$LIST_ECR_REPOS" | grep AccessDenied) ]]; then + textFail "Access Denied Trying to describe ECR repositories" + continue + fi + if [[ ! -z "$LIST_ECR_REPOS" ]]; then for repo in $LIST_ECR_REPOS; do - SCAN_ENABLED=$($AWSCLI ecr describe-repositories $PROFILE_OPT --region $regx --query "repositories[?repositoryName==\`$repo\`].[imageScanningConfiguration.scanOnPush]" --output text|grep True) - if [[ $SCAN_ENABLED ]];then - textPass "$regx: ECR repository $repo has scan on push enabled" "$regx" - else - textFail "$regx: ECR repository $repo has scan on push disabled!" "$regx" - fi - done + SCAN_ENABLED=$($AWSCLI ecr describe-repositories $PROFILE_OPT --region $region --query "repositories[?repositoryName==\`$repo\`].[imageScanningConfiguration.scanOnPush]" --output text 2>&1) + case "$SCAN_ENABLED" in + "True") + textPass "$region: ECR repository $repo has scan on push enabled" "$region" + ;; + "False") + textFail "$region: ECR repository $repo has scan on push disabled!" "$region" + ;; + "None") + textInfo "$region: ECR repository $repo hs no scanOnPush status, newer awscli needed" "$region" + ;; + "*") + textInfo "$region: ECR repository $repo has unknown scanOnPush status \"$SCAN_ENABLED\"" "$region" + ;; + esac + done else - textInfo "$regx: No ECR repositories found" "$regx" - fi - done + textInfo "$region: No ECR repositories found" "$region" + fi + done } diff --git a/checks/check_extra766 b/checks/check_extra766 deleted file mode 100644 index 2382f4ace8..0000000000 --- a/checks/check_extra766 +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -# Prowler - the handy cloud security tool (copyright 2018) 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. - -# Remediation: -# -# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html#configuring-instance-metadata-service -# -# aws ecr put-image-scanning-configuration \ -# --region \ -# --repository-name \ -# --image-scanning-configuration scanOnPush=true - - -CHECK_ID_extra766="7.66" -CHECK_TITLE_extra766="[extra766] Check if ECR image scan on push is enabled (Not Scored) (Not part of CIS benchmark)" -CHECK_SCORED_extra766="NOT_SCORED" -CHECK_TYPE_extra766="EXTRA" -CHECK_ALTERNATE_check766="extra766" - -extra766(){ - for regx in $REGIONS; do - LIST_ECR_REPOS=$($AWSCLI ecr describe-repositories $PROFILE_OPT --region $regx --query "repositories[*].[repositoryName]" --output text 2>&1) - if [[ $LIST_ECR_REPOS ]]; then - for repo in $LIST_ECR_REPOS; do - SCAN_ENABLED=$($AWSCLI ecr describe-repositories $PROFILE_OPT --region $regx --query "repositories[?repositoryName==\`$repo\`].[imageScanningConfiguration.scanOnPush]" --output text|grep True) - if [[ $SCAN_ENABLED ]];then - textPass "$regx: ECR repository $repo has scan on push enabled" "$regx" - else - textFail "$regx: ECR repository $repo has scan on push disabled!" "$regx" - fi - done - else - textInfo "$regx: No ECR repositories found" "$regx" - fi - done -} diff --git a/checks/check_extra769 b/checks/check_extra769 index 8ec529e23a..99835ba0e4 100644 --- a/checks/check_extra769 +++ b/checks/check_extra769 @@ -10,6 +10,7 @@ # 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_extra769="7.69" CHECK_TITLE_extra769="[extra769] Check if IAM Access Analyzer is enabled and its findings (Not Scored) (Not part of CIS benchmark)" CHECK_SCORED_extra769="NOT_SCORED" @@ -18,10 +19,18 @@ CHECK_ALTERNATE_check769="extra769" extra769(){ for regx in $REGIONS; do - LIST_OF_ACCESS_ANALYZERS=$($AWSCLI accessanalyzer list-analyzers $PROFILE_OPT --region $regx --query analyzers[*].arn --output text) + LIST_OF_ACCESS_ANALYZERS=$($AWSCLI accessanalyzer list-analyzers $PROFILE_OPT --region $regx --query analyzers[*].arn --output text 2>&1) + if [[ $(echo "$LIST_OF_ACCESS_ANALYZERS" | grep -i "argument command: Invalid choice") ]]; then + textInfo "$regx: list-analyzers not supported, newer awscli needed" "$regx" + continue + fi + if [[ $(echo "$LIST_OF_ACCESS_ANALYZERS" | grep -i "AccessDeniedException") ]]; then + textFail "$regx: Access Denied trying to list-analyzers" "$regx" + continue + fi if [[ $LIST_OF_ACCESS_ANALYZERS ]]; then for accessAnalyzerArn in $LIST_OF_ACCESS_ANALYZERS;do - ANALYZER_ACTIVE_FINDINGS_COUNT=$($AWSCLI accessanalyzer list-findings $PROFILE_OPT --region $regx --analyzer-arn $accessAnalyzerArn --query 'findings[?status == `ACTIVE`].[id,status]' --output text | wc -l | tr -d ' ') + ANALYZER_ACTIVE_FINDINGS_COUNT=$($AWSCLI accessanalyzer list-findings $PROFILE_OPT --region $regx --analyzer-arn $accessAnalyzerArn --query 'findings[?status == `ACTIVE`].[id,status]' --output text | wc -l | tr -d ' ') if [[ $ANALYZER_ACTIVE_FINDINGS_COUNT -eq 0 ]];then textPass "$regx: IAM Access Analyzer $accessAnalyzerArn has no active findings" "$regx" else diff --git a/checks/check_extra77 b/checks/check_extra77 index 8e0b9b4103..cfd1078a15 100644 --- a/checks/check_extra77 +++ b/checks/check_extra77 @@ -10,6 +10,7 @@ # 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_extra77="7.7,7.07" CHECK_TITLE_extra77="[extra77] Ensure there are no ECR repositories set as Public (Not Scored) (Not part of CIS benchmark)" CHECK_SCORED_extra77="NOT_SCORED" @@ -20,20 +21,38 @@ CHECK_ALTERNATE_check707="extra77" extra77(){ # "Ensure there are no ECR repositories set as Public (Not Scored) (Not part of CIS benchmark)" - textInfo "Looking for ECR repos in all regions... " - for regx in $REGIONS; do - LIST_OF_ECR_REPOS=$($AWSCLI ecr describe-repositories $PROFILE_OPT --region $regx --query 'repositories[*].{Name:repositoryName}' --output text) - for ecr_repo in $LIST_OF_ECR_REPOS; do - TEMP_POLICY_FILE=$(mktemp -t prowler-${ACCOUNT_NUM}-ecr-repo.policy.XXXXXXXXXX) - $AWSCLI ecr get-repository-policy --repository-name $ecr_repo $PROFILE_OPT --region $regx --output text > $TEMP_POLICY_FILE 2> /dev/null - # check if the policy has Principal as * - CHECK_ECR_REPO_ALLUSERS_POLICY=$(cat $TEMP_POLICY_FILE | 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_ECR_REPO_ALLUSERS_POLICY ]];then - textFail "$regx: $ecr_repo policy \"may\" allow Anonymous users to perform actions (Principal: \"*\")" "$regx" - else - textPass "$regx: $ecr_repo is not open" "$regx" - fi - done - rm -fr $TEMP_POLICY_FILE + for region in $REGIONS; do + LIST_ECR_REPOS=$($AWSCLI ecr describe-repositories $PROFILE_OPT --region $region --query "repositories[*].[repositoryName]" --output text 2>&1) + if [[ $(echo "$LIST_ECR_REPOS" | grep AccessDenied) ]]; then + textFail "Access Denied Trying to describe ECR repositories" + continue + fi + if [[ ! -z "$LIST_ECR_REPOS" ]]; then + for repo in $LIST_ECR_REPOS; do + TEMP_POLICY_FILE=$(mktemp -t prowler-${ACCOUNT_NUM}-ecr-repo.policy.XXXXXXXXXX) + $AWSCLI ecr get-repository-policy $PROFILE_OPT --region $region --repository-name $repo --query "policyText" --output text > $TEMP_POLICY_FILE 2>&1 + if [[ $(grep AccessDenied $TEMP_POLICY_FILE) ]]; then + textFail "$region: $repo Access Denied for get-repository-policy" + rm -f $TEMP_POLICY_FILE + continue + fi + # https://docs.aws.amazon.com/AmazonECR/latest/userguide/repository-policies.html - "By default, only the repository owner has access to a repository." + if [[ $(grep RepositoryPolicyNotFoundException $TEMP_POLICY_FILE) ]]; then + textPass "$region: $repo is not open" "$region" + rm -f $TEMP_POLICY_FILE + continue + fi + # check if the policy has Principal as * + CHECK_ECR_REPO_ALLUSERS_POLICY=$(cat $TEMP_POLICY_FILE | jq '.Statement[]|select(.Effect=="Allow" and (((.Principal|type == "object") and .Principal.AWS == "*") or ((.Principal|type == "string") and .Principal == "*")))') + if [[ $CHECK_ECR_REPO_ALLUSERS_POLICY ]]; then + textFail "$region: $repo policy \"may\" allow Anonymous users to perform actions (Principal: \"*\")" "$region" + else + textPass "$region: $repo is not open" "$region" + fi + rm -f $TEMP_POLICY_FILE + done + else + textInfo "$region: No ECR repositories found" "$region" + fi done } diff --git a/checks/check_extra772 b/checks/check_extra772 new file mode 100644 index 0000000000..83f3cdd5f2 --- /dev/null +++ b/checks/check_extra772 @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# Prowler - the handy cloud security tool (copyright 2018) 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_extra772="7.72" +CHECK_TITLE_extra772="[extra772] Check if elastic IPs are unused (Not Scored) (Not part of CIS benchmark)" +CHECK_SCORED_extra772="NOT_SCORED" +CHECK_TYPE_extra772="EXTRA" +CHECK_ALTERNATE_check772="extra772" + +extra772(){ + for region in $REGIONS; do + EIP_DUMP=$($AWSCLI ec2 describe-addresses ${PROFILE_OPT} --region $region) + EIP_LIST=$(echo $EIP_DUMP | jq -r '.Addresses[].AllocationId') + if [[ $EIP_LIST ]]; then + for eip in $EIP_LIST; do + ASSOCIATION_ID=$(echo $EIP_DUMP | jq -r --arg i "$eip" '.Addresses[]|select(.AllocationId==$i)|.AssociationId') + if [[ "$ASSOCIATION_ID" == "null" ]]; then + textFail "$region: EIP $eip is unused" $region + else + textPass "$region: EIP $eip is used" $region + fi + done + else + textInfo "$region: No Elastic IPs found" $region + fi + done +} diff --git a/checks/check_extra773 b/checks/check_extra773 new file mode 100644 index 0000000000..ecd2638529 --- /dev/null +++ b/checks/check_extra773 @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +# Prowler - the handy cloud security tool (copyright 2018) 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_extra773="7.73" +CHECK_TITLE_extra773="[extra773] Check if CloudFront distributions are using WAF (Not Scored) (Not part of CIS benchmark)" +CHECK_SCORED_extra773="NOT_SCORED" +CHECK_TYPE_extra773="EXTRA" +CHECK_ALTERNATE_check773="extra773" + +extra773(){ + # "Check if CloudFront distributions have logging enabled (Not Scored) (Not part of CIS benchmark)" + LIST_OF_DISTRIBUTIONS=$($AWSCLI cloudfront list-distributions $PROFILE_OPT --query 'DistributionList.Items[].Id' --output text | grep -v "^None") + if [[ $LIST_OF_DISTRIBUTIONS ]]; then + for dist in $LIST_OF_DISTRIBUTIONS; do + WEB_ACL_ID=$($AWSCLI cloudfront get-distribution $PROFILE_OPT --id "$dist" --query 'Distribution.DistributionConfig.WebACLId' --output text) + if [[ $WEB_ACL_ID ]]; then + textPass "CloudFront distribution $dist is using AWS WAF web ACL $WEB_ACL_ID" + else + textFail "CloudFront distribution $dist is not using AWS WAF web ACL" + fi + done + else + textInfo "No CloudFront distributions found" + fi +} diff --git a/credentials.py b/credentials.py deleted file mode 100644 index e6233e8614..0000000000 --- a/credentials.py +++ /dev/null @@ -1,26 +0,0 @@ -import boto3 -import sys -import json -import os - -sts = boto3.client('sts') -aws_lambda = boto3.client('lambda', region_name='us-west-2') - -aws_account_id = sys.argv[1] - -if aws_account_id is None: - raise ValueError('Must provide an AWS account ID') - -account_details_response = aws_lambda.invoke( - FunctionName='customers-api', - Payload=json.dumps({"path": "/invoke/getTenantDetails", "body": {"awsAccountId": sys.argv[1]}, "headers": {"Content-Type": "application/json"}}).encode('utf-8') -) - -account_details = json.loads(json.loads(account_details_response['Payload'].read())['body']) - -print('Got account details, assuming role') - -temporary_creds = sts.assume_role(RoleArn=account_details['cross_account_role_arn'], ExternalId=account_details['external_id'], RoleSessionName='prowler')['Credentials'] - -os.environ['AWS_ACCESS_KEY_ID'] = temporary_creds['AccessKeyId'] -os.environ['AWS_SECRET_ACCESS_KEY'] = temporary_creds['SecretAccessKey'] diff --git a/groups/group7_extras b/groups/group7_extras index 12afb988ee..5a36dbca30 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,extra761,extra762,extra763,extra764,extra765,extra766,extra767,extra768,extra769,extra770,extra771' +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,extra763,extra764,extra765,extra767,extra768,extra769,extra770,extra771,extra772,extra773' # Extras 759 and 760 (lambda variables and code secrets finder are not included) # to run detect-secrets use `./prowler -g secrets` diff --git a/iam/prowler-additions-policy.json b/iam/prowler-additions-policy.json index c95b05f202..ccb178dd3a 100644 --- a/iam/prowler-additions-policy.json +++ b/iam/prowler-additions-policy.json @@ -30,10 +30,12 @@ "dax:describeparameters", "dax:describesubnetgroups", "dax:describetable", - "dax:listtables", + "dax:listtables", "devicefarm:list*", "discovery:list*", "dms:list*", + "ds:ListAuthorizedApplications", + "ds:DescribeRoles", "dynamodb:describebackup", "dynamodb:describeglobaltablesettings", "dynamodb:describelimits", @@ -50,6 +52,11 @@ "gamelift:list*", "glacier:list*", "importexport:listjobs", + "lambda:GetAccountSettings", + "lambda:GetFunctionConfiguration", + "lambda:GetLayerVersionPolicy", + "lambda:GetPolicy", + "lambda:List*", "lex:getbotaliases", "lex:getbotchannelassociations", "lex:getbots", diff --git a/include/assume_role b/include/assume_role new file mode 100644 index 0000000000..4fedfb3dc7 --- /dev/null +++ b/include/assume_role @@ -0,0 +1,53 @@ +#!/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. + +# both variables are mandatory to be set together +if [[ $ACCOUNT_TO_ASSUME ]]; then + if [[ -z $ROLE_TO_ASSUME ]]; then + echo "$OPTRED ERROR!$OPTNORMAL - Both Account ID (-A) and IAM Role to assume (-R) must be set" + exit 1 + fi + # if not session duration set with -T, then will be 1h. + # In some cases you will need more than 1h. + if [[ -z $SESSION_DURATION_TO_ASSUME ]]; then + SESSION_DURATION_TO_ASSUME="3600" + fi + + # temporary file where to store credentials + TEMP_STS_ASSUMED_FILE=$(mktemp -t prowler.sts_assumed-XXXXXX) + + # assume role command + $AWSCLI $PROFILE_OPT sts assume-role --role-arn arn:aws:iam::$ACCOUNT_TO_ASSUME:role/$ROLE_TO_ASSUME \ + --role-session-name ProwlerAssessmentSession \ + --duration-seconds $SESSION_DURATION_TO_ASSUME > $TEMP_STS_ASSUMED_FILE + + # if previous command fails exit with the given error from aws-cli + # this is likely to be due to session duration limit of 1h in case + # of assume role chaining: + # "The requested DurationSeconds exceeds the 1 hour session limit + # for roles assumed by role chaining." + # https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use.html + if [[ $? != 0 ]];then + exit 1 + fi + + # The profile shouldn't be used for CLI + PROFILE="" + PROFILE_OPT="" + + # set env variables with assumed role credentials + export AWS_ACCESS_KEY_ID=$(cat $TEMP_STS_ASSUMED_FILE | jq -r '.Credentials.AccessKeyId') + export AWS_SECRET_ACCESS_KEY=$(cat $TEMP_STS_ASSUMED_FILE | jq -r '.Credentials.SecretAccessKey') + export AWS_SESSION_TOKEN=$(cat $TEMP_STS_ASSUMED_FILE | jq -r '.Credentials.SessionToken') + rm -fr $TEMP_STS_ASSUMED_FILE +fi diff --git a/include/outputs b/include/outputs index 6e379d0f82..b18ac68352 100644 --- a/include/outputs +++ b/include/outputs @@ -13,6 +13,10 @@ # Output formatting functions textPass(){ + if [[ "$QUIET" == 1 ]]; then + return + fi + PASS_COUNTER=$((PASS_COUNTER+1)) if [[ "$MODE" == "csv" ]]; then if [[ $2 ]]; then @@ -55,6 +59,10 @@ textPass(){ } textInfo(){ + if [[ "$QUIET" == 1 ]]; then + return + fi + if [[ "$MODE" == "csv" ]]; then if [[ $2 ]]; then REPREGION=$2 @@ -68,7 +76,7 @@ textInfo(){ else REPREGION=$REGION fi - jq -c \ + jq -M -c \ --arg PROFILE "$PROFILE" \ --arg ACCOUNT_NUM "$ACCOUNT_NUM" \ --arg TITLE_TEXT "$TITLE_TEXT" \ @@ -111,7 +119,7 @@ textFail(){ else REPREGION=$REGION fi - jq -c \ + jq -M -c \ --arg PROFILE "$PROFILE" \ --arg ACCOUNT_NUM "$ACCOUNT_NUM" \ --arg TITLE_TEXT "$TITLE_TEXT" \ diff --git a/prowler b/prowler index 437b72941c..e4e096d000 100755 --- a/prowler +++ b/prowler @@ -40,6 +40,7 @@ FILTERREGION="" MAXITEMS=100 MONOCHROME=0 MODE="text" +QUIET=0 SEP=',' KEEPCREDREPORT=0 EXITCODE=0 @@ -75,12 +76,19 @@ USAGE: -V show version number & exit -s show scoring report -x specify external directory with custom checks (i.e. /my/own/checks, files must start by "check") + -q suppress info messages and passing test output + -A account id for the account where to assume a role, requires -R and -T + (i.e.: 123456789012) + -R role name to assume in the account, requires -A and -T + (i.e.: ProwlerRole) + -T session durantion given to that role credentials in seconds, default 1h (3600) recommended 12h, requires -R and -T + (i.e.: 43200) -h this help " exit } -while getopts ":hlLkp:r:c:g:f:m:M:E:enbVsx:" OPTION; do +while getopts ":hlLkqp:r:c:g:f:m:M:E:enbVsx:A:R:T:" OPTION; do case $OPTION in h ) usage @@ -140,6 +148,18 @@ while getopts ":hlLkp:r:c:g:f:m:M:E:enbVsx:" OPTION; do x ) EXTERNAL_CHECKS_PATH=$OPTARG ;; + q ) + QUIET=1 + ;; + A ) + ACCOUNT_TO_ASSUME=$OPTARG + ;; + R ) + ROLE_TO_ASSUME=$OPTARG + ;; + T ) + SESSION_DURATION_TO_ASSUME=$OPTARG + ;; : ) echo "" echo "$OPTRED ERROR!$OPTNORMAL -$OPTARG requires an argument" @@ -157,6 +177,9 @@ while getopts ":hlLkp:r:c:g:f:m:M:E:enbVsx:" OPTION; do esac done +# Clean up any temp files when prowler quits unexpectedly +trap "{ rm -f /tmp/prowler*.policy.*; }" EXIT + . $PROWLER_DIR/include/colors . $PROWLER_DIR/include/os_detector . $PROWLER_DIR/include/aws_profile_loader @@ -170,6 +193,7 @@ done . $PROWLER_DIR/include/python_detector . $PROWLER_DIR/include/secrets_detector . $PROWLER_DIR/include/check3x +. $PROWLER_DIR/include/assume_role # Get a list of all available AWS Regions REGIONS=$($AWSCLI ec2 describe-regions --query 'Regions[].RegionName' \ @@ -189,12 +213,12 @@ for checks in $(ls $PROWLER_DIR/checks/check*|grep -v check_sample); do . "$checks" done -# include checks if external folder is specified -if [[ $EXTERNAL_CHECKS_PATH ]]; then +# include checks if external folder is specified +if [[ $EXTERNAL_CHECKS_PATH ]]; then for checks in $(ls $EXTERNAL_CHECKS_PATH/check*); do . "$checks" done -fi +fi # Function to show the title of the check # using this way instead of arrays to keep bash3 (osx) and bash4(linux) compatibility @@ -287,7 +311,7 @@ execute_group_by_id() { done } -# Function to execute all checks in all groups except extras if -e is invoked +# Function to execute all checks in all groups except extras if -e is invoked execute_all() { for i in "${!GROUP_TITLE[@]}"; do if [[ $EXTRAS ]]; then @@ -304,7 +328,7 @@ show_all_titles() { MAIN_GROUPS=(1 2 3 4 7) for i in "${MAIN_GROUPS[@]}"; do show_group_title $i - # Display the title of the checks in groups 1,2,3,4 and 7 + # Display the title of the checks in groups 1,2,3,4 and 7 # Any other group has checks in these groups IFS=',' read -ra CHECKS <<< ${GROUP_CHECKS[$i]} for j in ${CHECKS[@]}; do @@ -415,6 +439,13 @@ if [[ $CHECK_ID ]];then exit $EXITCODE fi +if [[ $ACCOUNT_TO_ASSUME ]]; then + # unset env variables with assumed role credentials + unset AWS_ACCESS_KEY_ID + unset AWS_SECRET_ACCESS_KEY + unset AWS_SESSION_TOKEN +fi + execute_all scoring diff --git a/run.sh b/run.sh deleted file mode 100755 index f14ac9e6e4..0000000000 --- a/run.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -mkdir ~/.aws -cat << AWS_CREDS > ~/.aws/credentials -[${ACCOUNT_ID}] -credential_source = EcsContainer -role_arn = ${ROLE_ARN} -external_id = ${EXTERNAL_ID} - -AWS_CREDS - -echo "Running prowler on ${ACCOUNT_ID}" -./prowler -p "${ACCOUNT_ID}" "${CHECKS}" -M json > output.json - -echo "Results:" -cat output.json - -echo "Uploading result to s3://${BUCKET}/prowler/${ACCOUNT_ID}/output.json" -aws s3api put-object --bucket "${BUCKET}" --key prowler/"${ACCOUNT_ID}"/output.json --body output.json \ No newline at end of file