diff --git a/README.md b/README.md index 7468c8e4..fb163fd1 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ - [Forensics Ready Checks](#forensics-ready-checks) - [GDPR Checks](#gdpr-checks) - [HIPAA Checks](#hipaa-checks) +- [Trust Boundaries Checks](#trust-boundaries-checks) - [Add Custom Checks](#add-custom-checks) - [Third Party Integrations](#third-party-integrations) - [Full list of checks and groups](/LIST_OF_CHECKS_AND_GROUPS.md) @@ -41,6 +42,7 @@ It covers hardening and security best practices for all AWS regions related to t - Forensics related group of checks [forensics-ready] - GDPR [gdpr] Read more [here](#gdpr-checks) - HIPAA [hipaa] Read more [here](#hipaa-checks) +- Trust Boundaries [trustboundaries] Read more [here](#trustboundaries-checks) For a comprehensive list and resolution look at the guide on the link above. @@ -430,6 +432,47 @@ The `hipaa` group of checks uses existing and extra checks. To get a HIPAA repor ./prowler -g hipaa ``` +## Trust Boundaries Checks +### Definition and Terms +The term "trust boundary" is originating from the threat modelling process and the most popular contributor Adam Shostack and author of "Threat Modeling: Designing for Security" defines it as following ([reference](https://adam.shostack.org/uncover.html)): + +> Trust boundaries are perhaps the most subjective of all: these represent the border between trusted and untrusted elements. Trust is complex. You might trust your mechanic with your car, your dentist with your teeth, and your banker with your money, but you probably don't trust your dentist to change your spark plugs. + +AWS is made to be flexible for service links within and between different AWS accounts, we all know that. + +This group of checks helps to analyse a particular AWS account (subject) on existing links to other AWS accounts across various AWS services, in order to identify untrusted links. + +### Run +To give it a quick shot just call: +```sh +./prowler -g trustboundaries +``` +### Scenarios +Currently this check group supports two different scenarios: + 1. Single account environment: no action required, the configuration is happening automatically for you. + 2. Multi account environment: in case you environment has multiple trusted and known AWS accounts you maybe want to append them manually to [groups/group16_trustboundaries](groups/group16_trustboundaries) as a space separated list into `GROUP_TRUSTBOUNDARIES_TRUSTED_ACCOUNT_IDS` variable, then just run prowler. + +### Coverage +Current coverage of Amazon Web Service (AWS) taken from [here](https://docs.aws.amazon.com/whitepapers/latest/aws-overview/introduction.html): +| Topic | Service | Trust Boundary | +|---------------------------------|------------|---------------------------------------------------------------------------| +| Networking and Content Delivery | Amazon VPC | VPC endpoints connections ([extra786](checks/check_extra786)) | +| | | VPC endpoints whitelisted principals ([extra787](checks/check_extra787)) | + +All ideas or recommendations to extend this group are very welcome [here](https://github.com/toniblyx/prowler/issues/new/choose). + +### Detailed Explanation of the Concept +The diagrams depict two common scenarios, single account and multi account environments. +Every circle represents one AWS account. +The dashed line represents the trust boundary, that separates trust and untrusted AWS accounts. +The arrow simply describes the direction of the trust, however the data can potentially flow in both directions. + +Single Account environment assumes that only the AWS account subject to this analysis is trusted. However there is a chance that two VPCs are existing within that one AWS account which are still trusted as a self reference. +![single-account-environment](/docs/images/prowler-single-account-environment.png) + +Multi Account environments assumes a minimum of two trusted or known accounts. For this particular example all trusted and known accounts will be tested. Therefore `GROUP_TRUSTBOUNDARIES_TRUSTED_ACCOUNT_IDS` variable in [groups/group16_trustboundaries](groups/group16_trustboundaries) should include all trusted accounts Account #A, Account #B, Account #C, and Account #D in order to finally raise Account #E and Account #F for being untrusted or unknown. +![multi-account-environment](/docs/images/prowler-multi-account-environment.png) + ## Add Custom Checks In order to add any new check feel free to create a new extra check in the extras group or other group. To do so, you will need to follow these steps: diff --git a/checks/check_extra716 b/checks/check_extra716 index db3aba1d..8dc3c383 100644 --- a/checks/check_extra716 +++ b/checks/check_extra716 @@ -18,10 +18,6 @@ CHECK_ASFF_RESOURCE_TYPE_extra716="AwsElasticsearchDomain" CHECK_ALTERNATE_check716="extra716" extra716(){ - # if TEST_AUTHENTICATION has a value Prowler will try to access each ElasticSearch server to the public URI endpoint. - # That is from the host where Prowler is running and will try to read indices or get kibana status - TEST_ES_AUTHENTICATION= - # "Check if Elasticsearch Service domains allow open access (Not Scored) (Not part of CIS benchmark)" for regx in $REGIONS; do LIST_OF_DOMAINS=$($AWSCLI es list-domain-names $PROFILE_OPT --region $regx --query DomainNames --output text) @@ -41,17 +37,17 @@ extra716(){ if [[ $CHECK_ES_DOMAIN_POLICY_HAS_CONDITION ]]; then # get content of IpAddress."aws:SourceIp" and get a clean list LIST_CONDITION_IPS=$(cat $TEMP_POLICY_FILE | jq '.Statement[0] .Condition.IpAddress."aws:SourceIp"'| awk -F'"' '{print $2}' | tr -d '",^$' | sed '/^$/d') - for condition_ip in $LIST_CONDITION_IPS;do - CONDITION_HAS_PRIVATE_IP=$(echo $condition_ip | grep -E '^(192\.168|10\.|172\.1[6789]\.|172\.2[0-9]\.|172\.3[01]\.)') + for condition_ip in "${LIST_CONDITION_IPS}";do + CONDITION_HAS_PRIVATE_IP=$(echo "${condition_ip}" | grep -E '^(192\.168|10\.|172\.1[6789]\.|172\.2[0-9]\.|172\.3[01]\.)') if [[ $CONDITION_HAS_PRIVATE_IP ]];then CONDITION_HAS_PRIVATE_IP_ARRAY+=($condition_ip) fi - CONDITION_HAS_PUBLIC_IP=$(echo $condition_ip | grep -vE '^(192\.168|10\.|172\.1[6789]\.|172\.2[0-9]\.|172\.3[01]\.|0\.0\.0\.0|\*)') + CONDITION_HAS_PUBLIC_IP=$(echo "${condition_ip}" | grep -vE '^(192\.168|10\.|172\.1[6789]\.|172\.2[0-9]\.|172\.3[01]\.|0\.0\.0\.0|\*)') if [[ $CONDITION_HAS_PUBLIC_IP ]];then CONDITION_HAS_PUBLIC_IP_ARRAY+=($condition_ip) fi - CONDITION_HAS_ZERO_NET=$(echo $condition_ip | grep -E '^(0\.0\.0\.0|\*)') - CONDITION_HAS_STAR=$(echo $condition_ip | grep -E '^\*') + CONDITION_HAS_ZERO_NET=$(echo "${condition_ip}" | grep -E '^(0\.0\.0\.0)') + CONDITION_HAS_STAR=$(echo "${condition_ip}" | grep -E '^\*') done CHECK_ES_DOMAIN_POLICY_CONDITION_PRIVATE_IP=${CONDITION_HAS_PRIVATE_IP_ARRAY[@]} CHECK_ES_DOMAIN_POLICY_CONDITION_PUBLIC_IP=${CONDITION_HAS_PUBLIC_IP_ARRAY[@]} @@ -59,37 +55,17 @@ extra716(){ CHECK_ES_DOMAIN_POLICY_CONDITION_STAR=$CONDITION_HAS_STAR fi if [[ $CHECK_ES_DOMAIN_POLICY_OPEN || $CHECK_ES_DOMAIN_POLICY_CONDITION_ZERO || $CHECK_ES_DOMAIN_POLICY_CONDITION_STAR || ${CHECK_ES_DOMAIN_POLICY_CONDITION_PUBLIC_IP[@]} ]];then - #Prowler will check to read indices or kibaba status if no conditions, condition IP is *, 0.0.0.0/0, 0.0.0.0/8 or any public IP. - if [[ $TEST_ES_AUTHENTICATION ]];then - # check for REST API on port 443 - CHECH_ES_HTTPS=$(curl -m 2 -s -w "%{http_code}" -o /dev/null -X GET "https://$ES_DOMAIN_ENDPOINT/_cat/indices") - httpStatus $CHECH_ES_HTTPS - if [[ $CHECH_ES_HTTPS -eq "200" ]];then - textFail "$regx: Amazon ES domain $domain policy allows Anonymous access and ES service endpoint $ES_DOMAIN_ENDPOINT responded $SERVER_RESPONSE" "$regx" - else - textInfo "$regx: Amazon ES domain $domain policy allows Anonymous access but ES service endpoint $ES_DOMAIN_ENDPOINT responded $SERVER_RESPONSE" "$regx" - fi - # check for Kibana on port 443 - CHECH_KIBANA_HTTPS=$(curl -m 2 -s -w "%{http_code}" -o /dev/null -X GET "https://$ES_DOMAIN_ENDPOINT/_plugin/kibana/api/status") - httpStatus $CHECH_KIBANA_HTTPS - if [[ $CHECH_KIBANA_HTTPS -eq "200" ]];then - textFail "$regx: Amazon ES domain $domain policy allows Anonymous access and Kibana service endpoint $ES_DOMAIN_ENDPOINT responded $SERVER_RESPONSE" "$regx" - else - textInfo "$regx: Amazon ES domain $domain policy allows Anonymous access but Kibana service endpoint $ES_DOMAIN_ENDPOINT responded $SERVER_RESPONSE" "$regx" - fi - else - if [[ $CHECK_ES_DOMAIN_POLICY_OPEN ]];then - textFail "$regx: Amazon ES domain $domain policy allows access (Principal: \"*\") AUTH NOT TESTED" "$regx" - fi - if [[ $CHECK_ES_DOMAIN_POLICY_HAS_CONDITION && $CHECK_ES_DOMAIN_POLICY_CONDITION_ZERO ]];then - textFail "$regx: Amazon ES domain $domain policy allows access (Principal: \"*\" and network 0.0.0.0) AUTH NOT TESTED" "$regx" - fi - if [[ $CHECK_ES_DOMAIN_POLICY_HAS_CONDITION && $CHECK_ES_DOMAIN_POLICY_CONDITION_STAR ]];then - textFail "$regx: Amazon ES domain $domain policy allows access (Principal: \"*\" and network \"*\") AUTH NOT TESTED" "$regx" - fi - if [[ $CHECK_ES_DOMAIN_POLICY_HAS_CONDITION && ${CHECK_ES_DOMAIN_POLICY_CONDITION_PUBLIC_IP[@]} ]];then - textFail "$regx: Amazon ES domain $domain policy allows access (Principal: \"*\" and Public IP or Network $(echo ${CONDITION_HAS_PUBLIC_IP_ARRAY[@]})) AUTH NOT TESTED" "$regx" - fi + if [[ $CHECK_ES_DOMAIN_POLICY_OPEN ]];then + textFail "$regx: Amazon ES domain $domain policy allows access (Principal: \"*\") - use extra788 to test AUTH" "$regx" + fi + if [[ $CHECK_ES_DOMAIN_POLICY_HAS_CONDITION && $CHECK_ES_DOMAIN_POLICY_CONDITION_ZERO ]];then + textFail "$regx: Amazon ES domain $domain policy allows access (Principal: \"*\" and network 0.0.0.0) - use extra788 to test AUTH" "$regx" + fi + if [[ $CHECK_ES_DOMAIN_POLICY_HAS_CONDITION && $CHECK_ES_DOMAIN_POLICY_CONDITION_STAR ]];then + textFail "$regx: Amazon ES domain $domain policy allows access (Principal: \"*\" and network \"*\") - use extra788 to test AUTH" "$regx" + fi + if [[ $CHECK_ES_DOMAIN_POLICY_HAS_CONDITION && ${CHECK_ES_DOMAIN_POLICY_CONDITION_PUBLIC_IP[@]} ]];then + textFail "$regx: Amazon ES domain $domain policy allows access (Principal: \"*\" and Public IP or Network $(echo ${CONDITION_HAS_PUBLIC_IP_ARRAY[@]})) - use extra788 to test AUTH" "$regx" fi else if [[ $CHECK_ES_DOMAIN_POLICY_HAS_CONDITION && ${CHECK_ES_DOMAIN_POLICY_CONDITION_PRIVATE_IP[@]} ]];then diff --git a/checks/check_extra779 b/checks/check_extra779 index 02a32868..ac4d2220 100644 --- a/checks/check_extra779 +++ b/checks/check_extra779 @@ -18,14 +18,10 @@ CHECK_ASFF_RESOURCE_TYPE_extra779="AwsEc2SecurityGroup" CHECK_ALTERNATE_check779="extra779" extra779(){ - # if TEST_AUTHENTICATION has a value Prowler will try to access each ElasticSearch server to port: - # 9200 API, 9300 Communcation and 5601 Kibana to figure out if authentication is enabled. - # That is from the host where Prowler is running and will try to read indices or get kibana status - TEST_ES_AUTHENTICATION= ES_API_PORT="9200" ES_DATA_PORT="9300" ES_KIBANA_PORT="5601" - + # Test connectivity and authentication is performed by check extra787 for regx in $REGIONS; do # crate a list of SG open to the world with port $ES_API_PORT or $ES_DATA_PORT or $ES_KIBANA_PORT SG_LIST=$($AWSCLI ec2 describe-security-groups $PROFILE_OPT --region $regx --output text \ @@ -40,57 +36,14 @@ extra779(){ # in case of exposed instances it does access checks if [[ -s "$TEMP_EXTRA779_FILE" ]];then while read instance eip ; do - if [[ $TEST_ES_AUTHENTICATION ]];then - if [[ "$eip" != "None" ]];then - # check for Elasticsearch on port $ES_API_PORT, rest API HTTP. - CHECH_HTTP_ES_API=$(curl -m 2 -s -w "%{http_code}" -o /dev/null -X GET "http://$eip:$ES_API_PORT/_cat/indices") - httpStatus $CHECH_HTTP_ES_API - if [[ $CHECH_HTTP_ES_API -eq "200" ]];then - textFail "$regx: Found instance $instance with public IP $eip on Security Group: $sg with Elasticsearch port $ES_API_PORT response $SERVER_RESPONSE" "$regx" - else - textInfo "$regx: Found instance $instance with public IP $eip on Security Group: $sg with Elasticsearch port $ES_API_PORT response $SERVER_RESPONSE" "$regx" - fi - - # check for port $ES_DATA_PORT TCP, this is the communication port, not: - # test_tcp_connectivity is in include/os_detector - # syntax is 'test_tcp_connectivity $HOST $PORT $TIMEOUT' (in seconds) - CHECH_HTTP_ES_DATA=$(test_tcp_connectivity $eip $ES_DATA_PORT 2) - # Using HTTP error codes here as well to reuse httpStatus function - # codes for better handling, so 200 is open and 000 is not responding - httpStatus $CHECH_HTTP_ES_DATA - if [[ $CHECH_HTTP_ES_DATA -eq "200" ]];then - textFail "$regx: Found instance $instance with public IP $eip on Security Group: $sg with Elasticsearch port $ES_DATA_PORT response $SERVER_RESPONSE" "$regx" - else - textInfo "$regx: Found instance $instance with public IP $eip on Security Group: $sg with Elasticsearch port $ES_DATA_PORT response $SERVER_RESPONSE" "$regx" - fi - - # check for Kibana on port $ES_KIBANA_PORT - CHECH_HTTP_ES_KIBANA=$(curl -m 2 -s -w "%{http_code}" -o /dev/null -X GET "http://$eip:$ES_KIBANA_PORT/api/status") - httpStatus $CHECH_HTTP_ES_KIBANA - if [[ $CHECH_AUTH_5601 -eq "200" ]];then - textFail "$regx: Found instance $instance with public IP $eip on Security Group: $sg with Kibana on port $ES_KIBANA_PORT response $SERVER_RESPONSE" "$regx" - else - textInfo "$regx: Found instance $instance with public IP $eip on Security Group: $sg with Kibana on port $ES_KIBANA_PORT response $SERVER_RESPONSE" "$regx" - fi - fi - else - if [[ "$eip" == "None" ]];then - textInfo "$regx: Found instance $instance with private IP on Security Group: $sg" "$regx" - else - textFail "$regx: Found instance $instance with public IP $eip on Security Group: $sg open to 0.0.0.0/0 on for Elasticsearch/Kibana ports $ES_API_PORT/$ES_DATA_PORT/$ES_KIBANA_PORT" "$regx" - fi - fi if [[ "$eip" == "None" ]];then textInfo "$regx: Found instance $instance with private IP on Security Group: $sg" "$regx" + else + textFail "$regx: Found instance $instance with public IP $eip on Security Group: $sg open to 0.0.0.0/0 on for Elasticsearch/Kibana ports - use extra787 to test AUTH" "$regx" fi - # done < <(cat $TEMP_EXTRA779_FILE | grep -v None$) done < <(cat $TEMP_EXTRA779_FILE) - # while read instance eip ; do - # textInfo "$regx: Found instance $instance with private IP on Security Group: $sg" "$regx" - # done < <(cat $TEMP_EXTRA779_FILE | grep None$) fi rm -rf $TEMP_EXTRA779_FILE - #textFail "$regx: Found Security Group: $sg open to 0.0.0.0/0 on for Elasticsearch ports" "$regx" done else textPass "$regx: No Security Groups found open to 0.0.0.0/0 for Elasticsearch/Kibana ports" "$regx" diff --git a/checks/check_extra786 b/checks/check_extra786 new file mode 100644 index 00000000..dd9f378e --- /dev/null +++ b/checks/check_extra786 @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +# Prowler - the handy cloud security tool (copyright 2020) 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_extra786="7.86" +CHECK_TITLE_extra786="[extra786] Check if EC2 Instance Metadata Service Version 2 (IMDSv2) is Enabled and Required (Not Scored) (Not part of CIS benchmark)" +CHECK_SCORED_extra786="NOT_SCORED" +CHECK_TYPE_extra786="EXTRA" +CHECK_ALTERNATE_check786="extra786" + +extra786(){ + for regx in $REGIONS; do + TEMP_EXTRA786_FILE=$(mktemp -t prowler-${ACCOUNT_NUM}-es-domain.EXTRA786.XXXXXXXXXX) + $AWSCLI ec2 describe-instances $PROFILE_OPT --region $regx \ + --query 'Reservations[*].Instances[*].{HttpTokens:MetadataOptions.HttpTokens,HttpEndpoint:MetadataOptions.HttpEndpoint,InstanceId:InstanceId}' \ + --output text --max-items $MAXITEMS > $TEMP_EXTRA786_FILE + # if the file contains data, there are instances in that region + if [[ -s "$TEMP_EXTRA786_FILE" ]];then + # here we read content from the file fields instanceid httptokens_status httpendpoint + while read httpendpoint httptokens_status instanceid ; do + #echo i:$instanceid tok:$httptokens_status end:$httpendpoint + if [[ "$httpendpoint" == "enabled" && "$httptokens_status" == "required" ]];then + textPass "$regx: EC2 Instance $instanceid has IMDSv2 enabled and required" "$regx" + elif [[ "$httpendpoint" == "disabled" ]];then + textInfo "$regx: EC2 Instance $instanceid has HTTP endpoint access to metadata service disabled" "$regx" + else + textFail "$regx: EC2 Instance $instanceid has IMDSv2 disabled or not required" "$regx" + fi + done < <(cat $TEMP_EXTRA786_FILE) + else + textInfo "$regx: no EC2 Instances found" "$regx" + fi + rm -fr $TEMP_EXTRA786_FILE + done +} + +# Remediation: + +# aws ec2 modify-instance-metadata-options \ +# --instance-id i-1234567898abcdef0 \ +# --http-tokens required \ +# --http-endpoint enabled + +# More information here https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html diff --git a/checks/check_extra787 b/checks/check_extra787 new file mode 100644 index 00000000..6f867902 --- /dev/null +++ b/checks/check_extra787 @@ -0,0 +1,81 @@ +#!/usr/bin/env bash + +# Prowler - the handy cloud security tool (copyright 2020) 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_extra787="7.87" +CHECK_TITLE_extra787="[extra787] Check connection and authentication for Internet exposed Elasticsearch/Kibana ports" +CHECK_SCORED_extra787="NOT_SCORED" +CHECK_TYPE_extra787="EXTRA" +CHECK_ALTERNATE_check787="extra787" + +extra787(){ + # Prowler will try to access each ElasticSearch server to port: + # 9200 API, 9300 Communcation and 5601 Kibana to figure out if authentication is enabled. + # That is from the host where Prowler is running and will try to read indices or get kibana status + ES_API_PORT="9200" + ES_DATA_PORT="9300" + ES_KIBANA_PORT="5601" + + for regx in $REGIONS; do + # crate a list of SG open to the world with port $ES_API_PORT or $ES_DATA_PORT or $ES_KIBANA_PORT + SG_LIST=$($AWSCLI ec2 describe-security-groups $PROFILE_OPT --region $regx --output text \ + --query "SecurityGroups[?length(IpPermissions[?((FromPort==null && ToPort==null) || (FromPort<=\`$ES_API_PORT\` && ToPort>=\`$ES_API_PORT\`) || (FromPort<=\`$ES_DATA_PORT\` && ToPort>=\`$ES_DATA_PORT\`) || (FromPort<=\`$ES_KIBANA_PORT\` && ToPort>=\`$ES_KIBANA_PORT\`)) && (contains(IpRanges[].CidrIp, \`0.0.0.0/0\`) || contains(Ipv6Ranges[].CidrIpv6, \`::/0\`))]) > \`0\`].{GroupId:GroupId}") + # in case of open security groups goes through each one + if [[ $SG_LIST ]];then + for sg in $SG_LIST;do + # temp file store the list of instances IDs and public IP address if found + TEMP_EXTRA787_FILE=$(mktemp -t prowler-${ACCOUNT_NUM}-es-domain.EXTRA787.XXXXXXXXXX) + # finds instances with that open security group attached and get its public ip address (if it has one) + $AWSCLI $PROFILE_OPT --region $regx ec2 describe-instances --filters Name=instance.group-id,Values=$sg --query 'Reservations[*].Instances[*].[InstanceId,PublicIpAddress]' --output text > $TEMP_EXTRA787_FILE + # in case of exposed instances it does access checks + if [[ -s "$TEMP_EXTRA787_FILE" ]];then + while read instance eip ; do + if [[ "$eip" != "None" ]];then + # check for Elasticsearch on port $ES_API_PORT, rest API HTTP. + CHECH_HTTP_ES_API=$(curl -m 2 -s -w "%{http_code}" -o /dev/null -X GET "http://$eip:$ES_API_PORT/_cat/indices") + httpStatus $CHECH_HTTP_ES_API + if [[ $CHECH_HTTP_ES_API -eq "200" ]];then + textFail "$regx: Found instance $instance with public IP $eip on Security Group: $sg with Elasticsearch port $ES_API_PORT response $SERVER_RESPONSE" "$regx" + else + textInfo "$regx: Found instance $instance with public IP $eip on Security Group: $sg with Elasticsearch port $ES_API_PORT response $SERVER_RESPONSE" "$regx" + fi + # check for port $ES_DATA_PORT TCP, this is the communication port, not: + # test_tcp_connectivity is in include/os_detector + # syntax is 'test_tcp_connectivity $HOST $PORT $TIMEOUT' (in seconds) + CHECH_HTTP_ES_DATA=$(test_tcp_connectivity $eip $ES_DATA_PORT 2) + # Using HTTP error codes here as well to reuse httpStatus function + # codes for better handling, so 200 is open and 000 is not responding + httpStatus $CHECH_HTTP_ES_DATA + if [[ $CHECH_HTTP_ES_DATA -eq "200" ]];then + textFail "$regx: Found instance $instance with public IP $eip on Security Group: $sg with Elasticsearch port $ES_DATA_PORT response $SERVER_RESPONSE" "$regx" + else + textInfo "$regx: Found instance $instance with public IP $eip on Security Group: $sg with Elasticsearch port $ES_DATA_PORT response $SERVER_RESPONSE" "$regx" + fi + # check for Kibana on port $ES_KIBANA_PORT + CHECH_HTTP_ES_KIBANA=$(curl -m 2 -s -w "%{http_code}" -o /dev/null -X GET "http://$eip:$ES_KIBANA_PORT/api/status") + httpStatus $CHECH_HTTP_ES_KIBANA + if [[ $CHECH_AUTH_5601 -eq "200" ]];then + textFail "$regx: Found instance $instance with public IP $eip on Security Group: $sg with Kibana on port $ES_KIBANA_PORT response $SERVER_RESPONSE" "$regx" + else + textInfo "$regx: Found instance $instance with public IP $eip on Security Group: $sg with Kibana on port $ES_KIBANA_PORT response $SERVER_RESPONSE" "$regx" + fi + else + textInfo "$regx: Found instance $instance with private IP on Security Group: $sg" "$regx" + fi + done < <(cat $TEMP_EXTRA787_FILE) + fi + rm -rf $TEMP_EXTRA787_FILE + done + else + textPass "$regx: No Security Groups found open to 0.0.0.0/0 for Elasticsearch/Kibana ports" "$regx" + fi + done +} diff --git a/checks/check_extra788 b/checks/check_extra788 new file mode 100644 index 00000000..f2258843 --- /dev/null +++ b/checks/check_extra788 @@ -0,0 +1,91 @@ +#!/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_extra788="7.88" +CHECK_TITLE_extra788="[extra788] Check connection and authentication for Internet exposed Amazon Elasticsearch Service (ES) domains" +CHECK_SCORED_extra788="NOT_SCORED" +CHECK_TYPE_extra788="EXTRA" +CHECK_ALTERNATE_check788="extra788" + +extra788(){ + # Prowler will try to access each ElasticSearch server to the public URI endpoint. + # That is from the host where Prowler is running and will try to read indices or get kibana status + + # "Check if Elasticsearch Service domains allow open access (Not Scored) (Not part of CIS benchmark)" + for regx in $REGIONS; do + LIST_OF_DOMAINS=$($AWSCLI es list-domain-names $PROFILE_OPT --region $regx --query DomainNames --output text) + if [[ $LIST_OF_DOMAINS ]]; then + for domain in $LIST_OF_DOMAINS;do + TEMP_POLICY_FILE=$(mktemp -t prowler-${ACCOUNT_NUM}-es-domain.policy.XXXXXXXXXX) + # get endpoint or vpc endpoints + ES_DOMAIN_ENDPOINT=$($AWSCLI es describe-elasticsearch-domain --domain-name $domain $PROFILE_OPT --region $regx --query 'DomainStatus.[Endpoint || Endpoints]' --output text) + # If the endpoint starts with "vpc-" it is in a VPC then it is fine. + if [[ "$ES_DOMAIN_ENDPOINT" =~ ^vpc-* ]];then + ES_DOMAIN_VPC=$($AWSCLI es describe-elasticsearch-domain --domain-name $domain $PROFILE_OPT --region $regx --query 'DomainStatus.VPCOptions.VPCId' --output text) + textInfo "$regx: Amazon ES domain $domain is in VPC $ES_DOMAIN_VPC run extra779 to make sure it is not exposed using custom proxy" "$regx" + else + $AWSCLI es describe-elasticsearch-domain-config --domain-name $domain $PROFILE_OPT --region $regx --query DomainConfig.AccessPolicies.Options --output text > $TEMP_POLICY_FILE 2> /dev/null + CHECK_ES_DOMAIN_POLICY_OPEN=$(cat $TEMP_POLICY_FILE | jq -r '. | .Statement[] | select(.Effect == "Allow" and (((.Principal|type == "object") and .Principal.AWS == "*") or ((.Principal|type == "string") and .Principal == "*")) and select(has("Condition") | not))') + CHECK_ES_DOMAIN_POLICY_HAS_CONDITION=$(cat $TEMP_POLICY_FILE | jq -r '. | .Statement[] | select(.Effect == "Allow" and (((.Principal|type == "object") and .Principal.AWS == "*") or ((.Principal|type == "string") and .Principal == "*")) and select(has("Condition")))' ) + if [[ $CHECK_ES_DOMAIN_POLICY_HAS_CONDITION ]]; then + # get content of IpAddress."aws:SourceIp" and get a clean list + LIST_CONDITION_IPS=$(cat $TEMP_POLICY_FILE | jq '.Statement[0] .Condition.IpAddress."aws:SourceIp"'| awk -F'"' '{print $2}' | tr -d '",^$' | sed '/^$/d') + for condition_ip in "${LIST_CONDITION_IPS}";do + CONDITION_HAS_PRIVATE_IP=$(echo "${condition_ip}" | grep -E '^(192\.168|10\.|172\.1[6789]\.|172\.2[0-9]\.|172\.3[01]\.)') + if [[ $CONDITION_HAS_PRIVATE_IP ]];then + CONDITION_HAS_PRIVATE_IP_ARRAY+=($condition_ip) + fi + CONDITION_HAS_PUBLIC_IP=$(echo "${condition_ip}" | grep -vE '^(192\.168|10\.|172\.1[6789]\.|172\.2[0-9]\.|172\.3[01]\.|0\.0\.0\.0|\*)') + if [[ $CONDITION_HAS_PUBLIC_IP ]];then + CONDITION_HAS_PUBLIC_IP_ARRAY+=($condition_ip) + fi + CONDITION_HAS_ZERO_NET=$(echo "${condition_ip}" | grep -E '^(0\.0\.0\.0)') + CONDITION_HAS_STAR=$(echo "${condition_ip}" | grep -E '^\*') + done + CHECK_ES_DOMAIN_POLICY_CONDITION_PRIVATE_IP=${CONDITION_HAS_PRIVATE_IP_ARRAY[@]} + CHECK_ES_DOMAIN_POLICY_CONDITION_PUBLIC_IP=${CONDITION_HAS_PUBLIC_IP_ARRAY[@]} + CHECK_ES_DOMAIN_POLICY_CONDITION_ZERO=$CONDITION_HAS_ZERO_NET + CHECK_ES_DOMAIN_POLICY_CONDITION_STAR=$CONDITION_HAS_STAR + fi + if [[ $CHECK_ES_DOMAIN_POLICY_OPEN || $CHECK_ES_DOMAIN_POLICY_CONDITION_ZERO || $CHECK_ES_DOMAIN_POLICY_CONDITION_STAR || ${CHECK_ES_DOMAIN_POLICY_CONDITION_PUBLIC_IP[@]} ]];then + #Prowler will check to read indices or kibaba status if no conditions, condition IP is *, 0.0.0.0/0, 0.0.0.0/8 or any public IP. + # check for REST API on port 443 + CHECH_ES_HTTPS=$(curl -m 2 -s -w "%{http_code}" -o /dev/null -X GET "https://$ES_DOMAIN_ENDPOINT/_cat/indices") + httpStatus $CHECH_ES_HTTPS + if [[ $CHECH_ES_HTTPS -eq "200" ]];then + textFail "$regx: Amazon ES domain $domain policy allows Anonymous access and ES service endpoint $ES_DOMAIN_ENDPOINT responded $SERVER_RESPONSE" "$regx" + else + textInfo "$regx: Amazon ES domain $domain policy allows Anonymous access but ES service endpoint $ES_DOMAIN_ENDPOINT responded $SERVER_RESPONSE" "$regx" + fi + # check for Kibana on port 443 + CHECH_KIBANA_HTTPS=$(curl -m 2 -s -w "%{http_code}" -o /dev/null -X GET "https://$ES_DOMAIN_ENDPOINT/_plugin/kibana/api/status") + httpStatus $CHECH_KIBANA_HTTPS + if [[ $CHECH_KIBANA_HTTPS -eq "200" ]];then + textFail "$regx: Amazon ES domain $domain policy allows Anonymous access and Kibana service endpoint $ES_DOMAIN_ENDPOINT responded $SERVER_RESPONSE" "$regx" + else + textInfo "$regx: Amazon ES domain $domain policy allows Anonymous access but Kibana service endpoint $ES_DOMAIN_ENDPOINT responded $SERVER_RESPONSE" "$regx" + fi + else + if [[ $CHECK_ES_DOMAIN_POLICY_HAS_CONDITION && ${CHECK_ES_DOMAIN_POLICY_CONDITION_PRIVATE_IP[@]} ]];then + textInfo "$regx: Amazon ES domain $domain policy allows access from a Private IP or CIDR RFC1918 $(echo ${CONDITION_HAS_PRIVATE_IP_ARRAY[@]})" "$regx" + else + textPass "$regx: Amazon ES domain $domain does not allow Anonymous cross account access" "$regx" + fi + fi + rm -f $TEMP_POLICY_FILE + fi + done + else + textInfo "$regx: No Amazon ES domain found" "$regx" + fi + done +} diff --git a/checks/check_extra789 b/checks/check_extra789 new file mode 100644 index 00000000..964067cd --- /dev/null +++ b/checks/check_extra789 @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +# Prowler - the handy cloud security tool (copyright 2020) 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_extra789="7.89" +CHECK_TITLE_extra789="[extra789] Find trust boundaries in VPC endpoint services connections" +CHECK_SCORED_extra789="NOT_SCORED" +CHECK_TYPE_extra789="EXTRA" +CHECK_ALTERNATE_extra789="extra789" + +extra789(){ + TRUSTED_ACCOUNT_IDS=$( echo "${ACCOUNT_NUM} ${GROUP_TRUSTBOUNDARIES_TRUSTED_ACCOUNT_IDS}" | xargs ) + + for regx in ${REGIONS}; do + ENDPOINT_SERVICES_IDS=$(${AWSCLI} ec2 describe-vpc-endpoint-services \ + ${PROFILE_OPT} \ + --query "ServiceDetails[?Owner=='${ACCOUNT_NUM}'].ServiceId" \ + --region ${regx} \ + --output text | xargs + ) + + for ENDPOINT_SERVICE_ID in ${ENDPOINT_SERVICES_IDS}; do + + ENDPOINT_CONNECTION_LIST=$(${AWSCLI} ec2 describe-vpc-endpoint-connections \ + ${PROFILE_OPT} \ + --query "VpcEndpointConnections[?VpcEndpointState=='available'].VpcEndpointOwner" \ + --region ${regx} \ + --output text | xargs + ) + + for ENDPOINT_CONNECTION in ${ENDPOINT_CONNECTION_LIST}; do + for ACCOUNT_ID in ${TRUSTED_ACCOUNT_IDS}; do + if [[ "${ACCOUNT_ID}" == "${ENDPOINT_CONNECTION}" ]]; then + textPass "${regx}: Found trusted account in VPC endpoint service connection ${ENDPOINT_CONNECTION}" "${regx}" + # Algorithm: + # Remove all trusted ACCOUNT_IDs from ENDPOINT_CONNECTION_LIST. + # As a result, the ENDPOINT_CONNECTION_LIST finally contains only unknown/untrusted account ids. + ENDPOINT_CONNECTION_LIST=("${ENDPOINT_CONNECTION_LIST[@]/$ENDPOINT_CONNECTION}") # remove hit from whitelist + fi + done + done + + for UNTRUSTED_CONNECTION in ${ENDPOINT_CONNECTION_LIST}; do + textFail "${regx}: Found untrusted account in VPC endpoint service connection ${UNTRUSTED_CONNECTION}" "${regx}" + done + done + done +} diff --git a/checks/check_extra790 b/checks/check_extra790 new file mode 100644 index 00000000..9a56cf17 --- /dev/null +++ b/checks/check_extra790 @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +# Prowler - the handy cloud security tool (copyright 2020) 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_extra790="7.90" +CHECK_TITLE_extra790="[extra790] Find trust boundaries in VPC endpoint services whitelisted principles" +CHECK_SCORED_extra790="NOT_SCORED" +CHECK_TYPE_extra790="EXTRA" +CHECK_ALTERNATE_extra790="extra790" + +extra790(){ + TRUSTED_ACCOUNT_IDS=$( echo "${ACCOUNT_NUM} ${GROUP_TRUSTBOUNDARIES_TRUSTED_ACCOUNT_IDS}" | xargs ) + + for regx in ${REGIONS}; do + ENDPOINT_SERVICES_IDS=$(${AWSCLI} ec2 describe-vpc-endpoint-services \ + ${PROFILE_OPT} \ + --query "ServiceDetails[?Owner=='${ACCOUNT_NUM}'].ServiceId" \ + --region ${regx} \ + --output text | xargs + ) + + for ENDPOINT_SERVICE_ID in ${ENDPOINT_SERVICES_IDS}; do + ENDPOINT_PERMISSIONS_LIST=$(${AWSCLI} ec2 describe-vpc-endpoint-service-permissions \ + ${PROFILE_OPT} \ + --service-id ${ENDPOINT_SERVICE_ID} \ + --query "AllowedPrincipals[*].Principal" \ + --region ${regx} \ + --output text | xargs + ) + + for ENDPOINT_PERMISSION in ${ENDPOINT_PERMISSIONS_LIST}; do + # Take only account id from ENDPOINT_PERMISSION: arn:aws:iam::965406151242:root + ENDPOINT_PERMISSION_ACCOUNT_ID=$(echo ${ENDPOINT_PERMISSION} | cut -d':' -f5 | xargs) + + for ACCOUNT_ID in ${TRUSTED_ACCOUNT_IDS}; do + if [[ "${ACCOUNT_ID}" == "${ENDPOINT_PERMISSION_ACCOUNT_ID}" ]]; then + textPass "${regx}: Found trusted account in VPC endpoint service permission ${ENDPOINT_PERMISSION}" "${regx}" + # Algorithm: + # Remove all trusted ACCOUNT_IDs from ENDPOINT_PERMISSIONS_LIST. + # As a result, the ENDPOINT_PERMISSIONS_LIST finally contains only unknown/untrusted account ids. + ENDPOINT_PERMISSIONS_LIST=("${ENDPOINT_PERMISSIONS_LIST[@]/$ENDPOINT_PERMISSION}") + fi + done + done + + for UNTRUSTED_PERMISSION in ${ENDPOINT_PERMISSIONS_LIST}; do + textFail "${regx}: Found untrusted account in VPC endpoint service permission ${UNTRUSTED_PERMISSION}" "${regx}" + done + done + done +} diff --git a/docs/images/prowler-multi-account-environment.png b/docs/images/prowler-multi-account-environment.png new file mode 100644 index 00000000..89adaf5f Binary files /dev/null and b/docs/images/prowler-multi-account-environment.png differ diff --git a/docs/images/prowler-single-account-environment.png b/docs/images/prowler-single-account-environment.png new file mode 100644 index 00000000..585fe33a Binary files /dev/null and b/docs/images/prowler-single-account-environment.png differ diff --git a/groups/group14_elasticsearch b/groups/group14_elasticsearch index e046981f..22ffbb4c 100644 --- a/groups/group14_elasticsearch +++ b/groups/group14_elasticsearch @@ -13,6 +13,6 @@ GROUP_ID[14]='elasticsearch' GROUP_NUMBER[14]='14.0' -GROUP_TITLE[14]='Elasticsearch related security checks - [elasticsearch] ***************' +GROUP_TITLE[14]='Elasticsearch related security checks - [elasticsearch] *******' GROUP_RUN_BY_DEFAULT[14]='N' # run it when execute_all is called -GROUP_CHECKS[14]='extra715,extra716,extra779,extra780,extra781,extra782,extra783,extra784,extra785' +GROUP_CHECKS[14]='extra715,extra716,extra779,extra780,extra781,extra782,extra783,extra784,extra785,extra787,extra788' \ No newline at end of file diff --git a/groups/group16_trustboundaries b/groups/group16_trustboundaries new file mode 100644 index 00000000..2c6875fc --- /dev/null +++ b/groups/group16_trustboundaries @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +# Prowler - the handy cloud security tool (copyright 2020) 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. + +GROUP_ID[16]='trustboundaries' +GROUP_NUMBER[16]='16.0' +GROUP_TITLE[16]='Find cross-account trust boundaries - [trustboundaries] ****************************' +GROUP_RUN_BY_DEFAULT[16]='N' # run it when execute_all is called +GROUP_CHECKS[16]='extra789,extra790' + +# Single account environment: No action required. The AWS account number will be automatically added by the checks. +# Multi account environment: Any additional trusted account number should be added as a space separated list, e.g. +# GROUP_TRUSTBOUNDARIES_TRUSTED_ACCOUNT_IDS="1234567890 0987654321 6789012345" +GROUP_TRUSTBOUNDARIES_TRUSTED_ACCOUNT_IDS='' diff --git a/groups/group7_extras b/groups/group7_extras index bbb27d31..526df553 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 - all non CIS specific checks - [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,extra767,extra768,extra769,extra770,extra771,extra772,extra773,extra774,extra775,extra776,extra777,extra778,extra779,extra780,extra781,extra782,extra783,extra784,extra785' +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,extra774,extra775,extra776,extra777,extra778,extra779,extra780,extra781,extra782,extra783,extra784,extra785,extra786,extra787,extra788' # Extras 759 and 760 (lambda variables and code secrets finder are not included) # to run detect-secrets use `./prowler -g secrets` diff --git a/include/connection_tests b/include/connection_tests index 632be16f..a34fb46c 100644 --- a/include/connection_tests +++ b/include/connection_tests @@ -12,9 +12,10 @@ # specific language governing permissions and limitations under the License. -# Functions to connection responses initially used for Elasticsearch related checks +# Function test_tcp_connectivity is in include/os_detector -httpStatus(){ +# Functions to connection responses initially used for Elasticsearch related checks +httpStatus(){ case $1 in 000) SERVER_RESPONSE="000 Not responding" ;; 200) SERVER_RESPONSE="200 Successful" ;;