diff --git a/README.md b/README.md index 4480b09..ea87563 100644 --- a/README.md +++ b/README.md @@ -129,28 +129,30 @@ The following are the impact criteria that MetaHub evaluates by default: **Exposure** evaluates the how the the affected resource is exposed to other networks. For example, if the affected resource is public, if it is part of a VPC, if it has a public IP or if it is protected by a firewall or a security group. -| **Possible Statuses** | **Description** | -| ----------------------- | --------------- | -| 🔴 effectively-public | | -| 🟠 restricted-public | | -| 🟠 unrestricted-private | | -| 🟢 restricted | | -| 🔵 unknown | | +| **Possible Statuses** | **Description** | +| ----------------------- | -------------------------------------------------------------------------------------------------------------- | +| 🔴 effectively-public | The resource is effectively public from the Internet. | +| 🟠 restricted-public | The resource is public, but there is a restriction like a Security Group. | +| 🟠 unrestricted-private | The resource is private but unrestricted, like an open security group. | +| 🟠 launch-public | These are resources that can launch other resources as public. For example, an Auto Scaling group or a Subnet. | +| 🟢 restricted | The resource is restricted. | +| 🔵 unknown | The resource couldn't be checked | ## Access -**Access** evaluates the resource policy layer. MetaHub checks every available policy including: IAM Managed policies, IAM Inline policies, Resource Policies, and any association to other resources like IAM Roles which are then also analyzed as part of the affected resource. An unrestricted policy is not only an itsue itself of that policy, it afected any other resource which is using it. +**Access** evaluates the resource policy layer. MetaHub checks every available policy including: IAM Managed policies, IAM Inline policies, Resource Policies, Bucket ACLS, and any association to other resources like IAM Roles which its policies are also analyzed . An unrestricted policy is not only an itsue itself of that policy, it afected any other resource which is using it. -| **Possible Statuses** | **Description** | -| -------------------------- | --------------- | -| 🔴 unrestricted | | -| 🔴 untrusted-principal | | -| 🟠 unrestricted-principal | | -| 🟠 cross-account-principal | | -| 🟠 unrestricted-actions | | -| 🟠 dangerous-actions | | -| 🟢 restricted | | -| 🔵 unknown | | +| **Possible Statuses** | **Description** | +| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| 🔴 unrestricted | The principal is unrestricted, without any condition or restriction. | +| 🔴 untrusted-principal | The principal is an AWS Account, not part of your trusted accounts. | +| 🟠 unrestricted-principal | The principal is not restricted, defined with a wildcard. It could be conditions restricting it or other restrictions like s3 public blocks. | +| 🟠 cross-account-principal | The principal is from another AWS account. | +| 🟠 unrestricted-actions | The actions are defined using wildcards. | +| 🟠 dangerous-actions | Some dangerous actions are defined as part of this policy. | +| 🟠 unrestricted-service | The policy allows an AWS service as principal without restriction. | +| 🟢 restricted | The policy is restricted. | +| 🔵 unknown | The policy couldn't be checked. | ## Encryption @@ -168,10 +170,10 @@ The following are the impact criteria that MetaHub evaluates by default: | **Possible Statuses** | **Description** | | --------------------- | --------------- | -| 🔴 not-attached | | -| 🔴 not-running | | -| 🟢 attached | | -| 🟢 running | | +| 🟠 attached | | +| 🟠 running | | +| 🟢 not-attached | | +| 🟢 not-running | | | 🔵 unknown | | ## Environment @@ -180,7 +182,7 @@ The following are the impact criteria that MetaHub evaluates by default: | **Possible Statuses** | **Description** | | --------------------- | --------------- | -| 🔴 production | | +| 🟠 production | | | 🟢 staging | | | 🟢 development | | | 🔵 unknown | | diff --git a/lib/context/resources/AwsElastiCacheReplicationGroup.py b/lib/context/resources/AwsElastiCacheReplicationGroup.py new file mode 100644 index 0000000..d428f08 --- /dev/null +++ b/lib/context/resources/AwsElastiCacheReplicationGroup.py @@ -0,0 +1,111 @@ +"""ResourceType: AwsElastiCacheReplicationGroup""" + +from botocore.exceptions import ClientError + +from lib.AwsHelpers import get_boto3_client +from lib.context.resources.Base import ContextBase + + +class Metacheck(ContextBase): + def __init__( + self, + logger, + finding, + mh_filters_config, + sess, + drilled=False, + ): + self.logger = logger + self.sess = sess + self.mh_filters_config = mh_filters_config + self.parse_finding(finding, drilled) + self.client = get_boto3_client( + self.logger, "elasticache", self.region, self.sess + ) + # Describe + self.replication_group = self.describe_replication_groups() + if not self.replication_group: + return False + # Associated MetaChecks + + def parse_finding(self, finding, drilled): + self.finding = finding + self.region = finding["Region"] + self.account = finding["AwsAccountId"] + self.partition = finding["Resources"][0]["Id"].split(":")[1] + self.resource_arn = finding["Resources"][0]["Id"] + self.resource_type = finding["Resources"][0]["Type"] + if finding["Resources"][0]["Id"].split(":")[5] == "replicationgroup": + self.resource_id = finding["Resources"][0]["Id"].split(":")[-1] + else: + self.logger.error( + "Error parsing elasticache cluster resource id: %s", + self.resource_arn, + ) + self.resource_id = finding["Resources"][0]["Id"] + + # Describe functions + + def describe_replication_groups(self): + try: + response = self.client.describe_replication_groups( + ReplicationGroupId=self.resource_id, + ) + if response["ReplicationGroups"]: + return response["ReplicationGroups"][0] + except ClientError as err: + if not err.response["Error"]["Code"] == "ReplicationGroupNotFoundFault": + self.logger.error( + "Failed to describe_replication_groups: {}, {}".format( + self.resource_id, err + ) + ) + return False + + # Context Config + + def endpoint(self): + endpoints = [] + if self.replication_group: + if self.replication_group.get("ConfigurationEndpoint"): + return self.replication_group["ConfigurationEndpoint"]["Address"] + if endpoints: + return endpoints + return False + + def at_rest_encryption(self): + if self.replication_group: + if self.replication_group["AtRestEncryptionEnabled"]: + return True + return False + + def transit_encryption(self): + if self.replication_group: + if self.replication_group["TransitEncryptionEnabled"]: + return True + return False + + def resource_policy(self): + return None + + def trust_policy(self): + return None + + def public(self): + if self.endpoint(): + return True + return False + + def associations(self): + associations = {} + return associations + + def checks(self): + checks = { + "endpoint": self.endpoint(), + "rest_encrypted": self.at_rest_encryption(), + "transit_encrypted": self.transit_encryption(), + "public": self.public(), + "resource_policy": self.resource_policy(), + } + return checks diff --git a/lib/context/resources/AwsS3Bucket.py b/lib/context/resources/AwsS3Bucket.py index e0aa6a9..f692af0 100644 --- a/lib/context/resources/AwsS3Bucket.py +++ b/lib/context/resources/AwsS3Bucket.py @@ -157,36 +157,6 @@ def describe_resource_policy(self): # Context Config - def bucket_acl_cross_account(self): - acl_with_cross_account = [] - if self.bucket_acl: - for grant in self.bucket_acl: - if grant["Grantee"]["Type"] == "CanonicalUser": - if grant["Grantee"]["ID"] != self.cannonical_user_id: - # perm = grant["Permission"] - acl_with_cross_account.append(grant) - if acl_with_cross_account: - return acl_with_cross_account - return False - - def bucket_acl_public(self): - public_acls = [] - if self.bucket_acl: - for grant in self.bucket_acl: - if grant["Grantee"]["Type"] == "Group": - # use only last part of URL as a key: - # http://acs.amazonaws.com/groups/global/AuthenticatedUsers - # http://acs.amazonaws.com/groups/global/AllUsers - who = grant["Grantee"]["URI"].split("/")[-1] - if who == "AllUsers" or who == "AuthenticatedUsers": - # perm = grant["Permission"] - # group all permissions (READ(_ACP), WRITE(_ACP), FULL_CONTROL) by AWS predefined groups - # public_acls.setdefault(who, []).append(perm) - public_acls.append(grant) - if public_acls: - return public_acls - return False - def public_access_block_enabled(self): if self.bucket_public_access_block: for key, value in self.bucket_public_access_block.items(): @@ -235,8 +205,7 @@ def checks(self): "resource_policy": self.resource_policy, "website_enabled": self.website_enabled(), "bucket_acl": self.bucket_acl, - "bucket_acl_cross_account": self.bucket_acl_cross_account(), - "bucket_acl_public": self.bucket_acl_public(), + "cannonical_user_id": self.cannonical_user_id, "public_access_block_enabled": self.public_access_block_enabled(), "account_public_access_block_enabled": self.account_public_access_block_enabled(), "public": self.public(), diff --git a/lib/context/resources/__init__.py b/lib/context/resources/__init__.py index fbaa1bb..015cb19 100644 --- a/lib/context/resources/__init__.py +++ b/lib/context/resources/__init__.py @@ -17,6 +17,7 @@ AwsEfsFileSystem, AwsEksCluster, AwsElastiCacheCacheCluster, + AwsElastiCacheReplicationGroup, AwsElasticsearchDomain, AwsElbLoadBalancer, AwsElbv2LoadBalancer, diff --git a/lib/html/template.html b/lib/html/template.html index 4e3660e..6e4600f 100644 --- a/lib/html/template.html +++ b/lib/html/template.html @@ -172,7 +172,7 @@ if (value === "effectively-public") { color = "red"; emoji = "🔴"; - } else if (value === "restricted-public" || value === "unrestricted-private") { + } else if (value === "restricted-public" || value === "unrestricted-private" || value === "launch-public") { color = "orange"; emoji = "🟠"; } else if (value === "restricted") { @@ -198,7 +198,7 @@ if (value === "unrestricted" || value === "untrusted-principal") { color = "red"; emoji = "🔴"; - } else if (value === "unrestricted-principal" || value === "cross-account-principal" || value === "unrestricted-actions" || value == "dangerous-actions" ) { + } else if (value === "unrestricted-principal" || value === "cross-account-principal" || value === "unrestricted-actions" || value == "dangerous-actions" || value == "unrestricted-service") { color = "orange"; emoji = "🟠"; } else if (value === "restricted") { @@ -245,11 +245,11 @@ var color; var emoji; if (value === "not-attached" || value === "not-running") { - color = "red"; - emoji = "🔴"; - } else if (value === "attached" || value === "running") { color = "green"; emoji = "🟢"; + } else if (value === "attached" || value === "running") { + color = "orange"; + emoji = "🟠"; } else if (value === "unknown") { color = "blue"; emoji = "🔵"; @@ -268,8 +268,8 @@ var color; var emoji; if (value === "production") { - color = "red"; - emoji = "🔴"; + color = "orange"; + emoji = "🟠"; } else if (value === "staging" || value === "development") { color = "green"; emoji = "🟢"; diff --git a/lib/impact/access.py b/lib/impact/access.py index 4439be8..1cee51c 100644 --- a/lib/impact/access.py +++ b/lib/impact/access.py @@ -10,6 +10,7 @@ def get_access(self, resource_arn, resource_values): self.logger.info("Calculating access for resource: %s", resource_arn) self.resource_arn = resource_arn self.resource_account_id = resource_values.get("AwsAccountId") + self.cannonical_user_id = get_config_key(resource_values, "cannonical_user_id") access_checks = { "unrestricted": {}, @@ -18,6 +19,7 @@ def get_access(self, resource_arn, resource_values): "cross_account_principal": {}, "wildcard_actions": {}, "dangerous_actions": {}, + "unrestricted_services": {}, } # Helper function to check policies and update access_checks @@ -28,91 +30,26 @@ def check_policy_and_update(policy_json, policy_name): if check_data: access_checks[check_type].update({policy_name: check_data}) - # To Do: - # - Check S3 Public Blocks - # - Check S3 Bucket ACLs + # Helper function to check policies and update access_checks + def check_bucket_acl_and_update(policy_json, policy_name): + if policy_json: + policy_checks = self.check_bucket_acl(policy_json) + for check_type, check_data in policy_checks.items(): + if check_data: + access_checks[check_type].update({policy_name: check_data}) + + # Check S3 Public Blocks + ( + self.s3_block_public_acls, + self.s3_block_public_policy, + self.s3_ignore_public_acls, + self.s3_restrict_public_buckets, + ) = self.check_s3_public_block(resource_values) - # To add as part of policy: - # # Resource S3 Public Block - # ( - # s3_resource_block_public_acls, - # s3_resource_block_public_policy, - # s3_resource_ignore_public_acls, - # s3_resource_restrict_public_buckets, - # ) = ( - # False, - # False, - # False, - # False, - # ) - # s3_resource_public_block = get_config_key( - # resource_values, "public_access_block_enabled" - # ) - # if s3_resource_public_block: - # s3_resource_block_public_acls = s3_resource_public_block.get( - # "BlockPublicAcls" - # ) - # s3_resource_block_public_policy = s3_resource_public_block.get( - # "BlockPublicPolicy" - # ) - # s3_resource_ignore_public_acls = s3_resource_public_block.get( - # "IgnorePublicAcls" - # ) - # s3_resource_restrict_public_buckets = s3_resource_public_block.get( - # "RestrictPublicBuckets" - # ) - # # Account S3 Public Block - # ( - # s3_account_block_public_acls, - # s3_account_block_public_policy, - # s3_account_ignore_public_acls, - # s3_account_restrict_public_buckets, - # ) = ( - # False, - # False, - # False, - # False, - # ) - # s3_account_public_block = get_config_key( - # resource_values, "account_public_access_block_enabled" - # ) - # if s3_account_public_block: - # s3_account_block_public_acls = s3_account_public_block.get( - # "BlockPublicAcls" - # ) - # s3_account_block_public_policy = s3_account_public_block.get( - # "BlockPublicPolicy" - # ) - # s3_account_ignore_public_acls = s3_account_public_block.get( - # "IgnorePublicAcls" - # ) - # s3_account_restrict_public_buckets = s3_account_public_block.get( - # "RestrictPublicBuckets" - # ) - # # Let's create the final variables, based on the previous ones - # ( - # s3_block_public_acls, - # s3_block_public_policy, - # s3_ignore_public_acls, - # s3_restrict_public_buckets, - # ) = ( - # False, - # False, - # False, - # False, - # ) - # if s3_resource_public_block or s3_account_public_block: - # if s3_resource_block_public_acls or s3_account_block_public_acls: - # pass - # if s3_resource_block_public_policy or s3_account_block_public_policy: - # pass - # if s3_resource_ignore_public_acls or s3_account_ignore_public_acls: - # pass - # if ( - # s3_resource_restrict_public_buckets - # or s3_account_restrict_public_buckets - # ): - # pass + # Bucket ACL + bucket_acl = get_config_key(resource_values, "bucket_acl") + if bucket_acl: + check_bucket_acl_and_update(bucket_acl, "bucket_acl") # Resource Policy resource_policy = get_config_key(resource_values, "resource_policy") @@ -194,10 +131,14 @@ def check_policy_and_update(policy_json, policy_name): return {"cross-account-principal": access_checks} if "wildcard_principal" in access_checks: return {"unrestricted-principal": access_checks} + if "unrestricted_services" in access_checks: + return {"unrestricted-services": access_checks} return {"restricted": access_checks} def check_policy(self, policy): + principal_amazon_accounts = ["cloudfront"] + self.logger.info("Checking policy for resource: %s", self.resource_arn) failed_statements = { "wildcard_principal": [], @@ -206,6 +147,7 @@ def check_policy(self, policy): "wildcard_actions": [], "unrestricted": [], "dangerous_actions": [], + "unrestricted_services": [], } statements = [] try: @@ -216,18 +158,100 @@ def check_policy(self, policy): ) return failed_statements for statement in statements: - if self.wildcard_principal(statement): - failed_statements["wildcard_principal"].append(statement) - if self.cross_account_principal(statement): - failed_statements["cross_account_principal"].append(statement) - if self.untrusted_principal(statement): - failed_statements["untrusted_principal"].append(statement) - if self.wildcard_actions(statement): - failed_statements["wildcard_actions"].append(statement) - if self.unrestricted(statement): - failed_statements["unrestricted"].append(statement) - if self.dangerous_actions(statement): - failed_statements["dangerous_actions"].append(statement) + ( + effect, + principal, + not_principal, + condition, + action, + not_action, + resource, + ) = self.parse_statement(statement) + if effect == "Allow": + # Wildcard or Unrestricted Principal + all_principals = self.standardize_principals(principal) + for p in all_principals: + if "*" in p: + if condition and not self.is_unrestricted_conditions(condition): + failed_statements["wildcard_principal"].append(statement) + else: + if self.s3_restrict_public_buckets: + failed_statements["wildcard_principal"].append( + statement + ) + else: + failed_statements["unrestricted"].append(statement) + if ( + not_principal is not None + ): # If Allow and Not Principal, means all other principals + if condition and not self.is_unrestricted_conditions(condition): + failed_statements["wildcard_principal"].append(statement) + else: + if self.s3_restrict_public_buckets: + failed_statements["wildcard_principal"].append(statement) + else: + failed_statements["unrestricted"].append(statement) + # Cross Account or Untrusted Principal + aws_principals = self.standardize_principals(principal, "AWS") + for p in aws_principals: + if "*" not in p: + if p.startswith("arn:"): + account_id = p.split(":")[4] + else: + account_id = p + if ( + account_id != self.resource_account_id + and account_id not in principal_amazon_accounts + ): + if trusted_accounts and account_id not in trusted_accounts: + failed_statements["untrusted_principal"].append( + statement + ) + else: + failed_statements["cross_account_principal"].append( + statement + ) + # Unrestricted Service + service_principals = self.standardize_principals(principal, "Service") + for p in service_principals: + if not condition or ( + condition and self.is_unrestricted_conditions(condition) + ): + failed_statements["unrestricted_services"].append(statement) + # Wildcard or Dangerous Actions + actions = self.standardize_actions(action) + for a in actions: + if a in dangerous_iam_actions: + failed_statements["dangerous_actions"].append(statement) + if "*" in a: + failed_statements["wildcard_actions"].append(statement) + + return failed_statements + + def check_bucket_acl(self, bucket_acl): + self.logger.info("Checking bucket acl for resource: %s", self.resource_arn) + failed_statements = { + "cross_account_principal": [], + "unrestricted": [], + } + if bucket_acl: + for grant in bucket_acl: + if grant["Grantee"]["Type"] == "CanonicalUser": + if self.cannonical_user_id: + if grant["Grantee"]["ID"] != self.cannonical_user_id: + # perm = grant["Permission"] + failed_statements["cross_account_principal"].append(grant) + if grant["Grantee"]["Type"] == "Group": + # use only last part of URL as a key: + # http://acs.amazonaws.com/groups/global/AuthenticatedUsers + # http://acs.amazonaws.com/groups/global/AllUsers + who = grant["Grantee"]["URI"].split("/")[-1] + if who == "AllUsers" or who == "AuthenticatedUsers": + if self.s3_ignore_public_acls: + failed_statements["wildcard_principal"].append(grant) + else: + failed_statements["unrestricted"].append(grant) + return failed_statements def parse_statement(self, statement): @@ -246,23 +270,30 @@ def parse_statement(self, statement): return None, None, None, None, None, None, None return effect, principal, not_principal, condition, action, not_action, resource - def santandarize_principals(self, principal): - if "AWS" in principal: - principals = principal["AWS"] - elif "Service" in principal: - principals = principal["Service"] - elif "Federated" in principal: - principals = principal["Federated"] + def standardize_principals(self, principal, prequiredtype=None): + principals = [] + if principal == "*": + principals.append("*") else: - principals = principal - if type(principals) is not list: - principals = [principals] + if not prequiredtype: + for ptype in principal: + if type(principal[ptype]) is list: + principals.extend(principal[ptype]) + else: + principals.append(principal[ptype]) + if prequiredtype and prequiredtype in principal: + if type(principal[prequiredtype]) is list: + principals.extend(principal[prequiredtype]) + else: + principals.append(principal[prequiredtype]) return principals def standardize_actions(self, action): - actions = action - if type(action) is not list: - actions = [action] + actions = [] + if action: + actions = action + if type(action) is not list: + actions = [action] return actions def standardize_statements(self, statement): @@ -271,168 +302,95 @@ def standardize_statements(self, statement): statements = [statement] return statements - def wildcard_principal(self, statement): - """ - Check if resource policy (S3, SQS) is allowed for principal wildcard - """ - ( - effect, - principal, - not_principal, - condition, - action, - not_action, - resource, - ) = self.parse_statement(statement) - if effect == "Allow": - if principal == "*" or principal.get("AWS") == "*": - return statement + def is_unrestricted_conditions(self, condition): + if condition: + if "IpAddress" in condition: + if "*" in condition["IpAddress"]: + return True + if "/0" in condition["IpAddress"]: + return True return False - def cross_account_principal(self, statement): - """ - Check if policy is allowed for principal cross account - """ - amazon_accounts = ["cloudfront"] + def check_s3_public_block(self, resource_values): + # Resource S3 Public Block ( - effect, - principal, - not_principal, - condition, - action, - not_action, - resource, - ) = self.parse_statement(statement) - if effect == "Allow": - if principal and principal != "*" and principal.get("AWS") != "*": - principals = self.santandarize_principals(principal) - for p in principals: - # We are only scanninng ARN principals - if not p.startswith("arn:"): - continue - try: - account_id = p.split(":")[4] - if ( - account_id != self.resource_account_id - and account_id not in amazon_accounts - ): - return statement - except (IndexError, TypeError) as err: - self.logger.warning( - "Parsing principal %s for resource %s doesn't look like ARN, ignoring.. - %s", - p, - self.resource_arn, - err, - ) - return False - - def untrusted_principal(self, statement): - """ """ - if not trusted_accounts: - self.logger.info( - "No trusted accounts defined in configuration, skipping check for resource %s", - self.resource_arn, + s3_resource_block_public_acls, + s3_resource_block_public_policy, + s3_resource_ignore_public_acls, + s3_resource_restrict_public_buckets, + ) = ( + False, + False, + False, + False, + ) + s3_resource_public_block = get_config_key( + resource_values, "public_access_block_enabled" + ) + if s3_resource_public_block: + s3_resource_block_public_acls = s3_resource_public_block.get( + "BlockPublicAcls" ) - return False - amazon_accounts = ["cloudfront"] - ( - effect, - principal, - not_principal, - condition, - action, - not_action, - resource, - ) = self.parse_statement(statement) - if effect == "Allow": - if principal and principal != "*" and principal.get("AWS") != "*": - principals = self.santandarize_principals(principal) - for p in principals: - # We are only scanninng ARN principals - if not p.startswith("arn:"): - continue - try: - account_id = p.split(":")[4] - if ( - account_id not in trusted_accounts - and account_id not in amazon_accounts - and account_id != self.resource_account_id - ): - return statement - except IndexError: - self.logger.warning( - "Parsing principal %s for resource %s doesn't look like ARN, ignoring.. ", - p, - self.resource_arn, - ) - return False - - def wildcard_actions(self, statement): - """ """ - ( - effect, - principal, - not_principal, - condition, - action, - not_action, - resource, - ) = self.parse_statement(statement) - if effect == "Allow": - if action: - actions = self.standardize_actions(action) - for a in actions: - if "*" in a: - return statement - # Not Action (all other actions are allowed) - if not_action: - return statement - return False - - def dangerous_actions(self, statement): - """ """ + s3_resource_block_public_policy = s3_resource_public_block.get( + "BlockPublicPolicy" + ) + s3_resource_ignore_public_acls = s3_resource_public_block.get( + "IgnorePublicAcls" + ) + s3_resource_restrict_public_buckets = s3_resource_public_block.get( + "RestrictPublicBuckets" + ) + # Account S3 Public Block ( - effect, - principal, - not_principal, - condition, - action, - not_action, - resource, - ) = self.parse_statement(statement) - if effect == "Allow": - if action: - actions = self.standardize_actions(action) - for a in actions: - if a in dangerous_iam_actions: - return statement - return False - - def unrestricted(self, statement): - """ - There is no principal defined. This means that the resource is unrestricted if the policy is attached to a resource. - """ + s3_account_block_public_acls, + s3_account_block_public_policy, + s3_account_ignore_public_acls, + s3_account_restrict_public_buckets, + ) = ( + False, + False, + False, + False, + ) + s3_account_public_block = get_config_key( + resource_values, "account_public_access_block_enabled" + ) + if s3_account_public_block: + s3_account_block_public_acls = s3_account_public_block.get( + "BlockPublicAcls" + ) + s3_account_block_public_policy = s3_account_public_block.get( + "BlockPublicPolicy" + ) + s3_account_ignore_public_acls = s3_account_public_block.get( + "IgnorePublicAcls" + ) + s3_account_restrict_public_buckets = s3_account_public_block.get( + "RestrictPublicBuckets" + ) + # Let's create the final variables, based on the previous ones ( - effect, - principal, - not_principal, - condition, - action, - not_action, - resource, - ) = self.parse_statement(statement) - suffix = "/0" - if effect == "Allow": - if principal == "*" or principal.get("AWS") == "*": - if condition is not None: - # IpAddress Condition with /0 - if suffix in str(condition.get("IpAddress")): - return statement - # To Do: Add other public conditions - else: - # No Condition - return statement - # Not Principal (all other principals) - if not_principal is not None: - return statement - return False + s3_block_public_acls, + s3_block_public_policy, + s3_ignore_public_acls, + s3_restrict_public_buckets, + ) = ( + False, + False, + False, + False, + ) + if s3_resource_block_public_acls or s3_account_block_public_acls: + s3_block_public_acls = True + if s3_resource_block_public_policy or s3_account_block_public_policy: + s3_block_public_policy = True + if s3_resource_ignore_public_acls or s3_account_ignore_public_acls: + s3_ignore_public_acls = True + if s3_resource_restrict_public_buckets or s3_account_restrict_public_buckets: + s3_restrict_public_buckets = True + return ( + s3_block_public_acls, + s3_block_public_policy, + s3_ignore_public_acls, + s3_restrict_public_buckets, + ) diff --git a/lib/impact/encryption.py b/lib/impact/encryption.py index a0cac5b..74f2790 100644 --- a/lib/impact/encryption.py +++ b/lib/impact/encryption.py @@ -41,6 +41,7 @@ def get_encryption(self, resource_arn, resource_values): if resource_type in ( "AwsElasticsearchDomain", "AwsElastiCacheCacheCluster", + "AwsElastiCacheReplicationGroup", ): resource_encryption_config = False at_rest_encryption = get_config_key(resource_values, "at_rest_encryption") diff --git a/lib/impact/exposure.py b/lib/impact/exposure.py index 22fd6f3..2da9c92 100644 --- a/lib/impact/exposure.py +++ b/lib/impact/exposure.py @@ -1,3 +1,4 @@ +from lib.impact.access import Access from lib.impact.helpers import check_key, get_associated_resources, get_config_key @@ -47,6 +48,8 @@ def check_unrestricted_egress_rules(self, rule): def get_exposure(self, resource_arn, resource_values): self.logger.info("Calculating exposure for resource: %s", resource_arn) + resource_type = resource_values.get("ResourceType") + unrestricted_ingress_rules = [] unrestricted_egress_rules = [] entrypoint = None @@ -55,6 +58,15 @@ def get_exposure(self, resource_arn, resource_values): # This is a standard key we add at the resource level to check if the resoruce it's public at their config. resource_public_config = get_config_key(resource_values, "public") + # Resource Policy, for some resources, exposure is defined using a resource policy. + # We will neeed to evaluate this policy to check if the resource is public. + resource_policy = get_config_key(resource_values, "resource_policy") + unrestricted_policy_access = False + if resource_policy: + access = Access(self.logger).get_access(resource_arn, resource_values) + if "unrestricted" in access: + unrestricted_policy_access = True + # Entrypoint # We check all possible entrypoint. This is a bit messy, probably some standarizationg would be good at the resource. entrypoints = [ @@ -66,6 +78,7 @@ def get_exposure(self, resource_arn, resource_values): "endpoint", "private_ip", "private_dns", + "website_enabled", ] for ep in entrypoints: entrypoint = get_config_key(resource_values, ep) @@ -91,7 +104,9 @@ def get_exposure(self, resource_arn, resource_values): sg_details, "security_group_rules" ) if security_groups_rules: - checked_rules = self.check_security_group_rules(rules) + checked_rules = self.check_security_group_rules( + security_groups_rules + ) unrestricted_ingress_rules.extend( checked_rules["unrestricted_ingress_rules"] ) @@ -112,15 +127,36 @@ def get_exposure(self, resource_arn, resource_values): ): return {"unknown": exposure_checks} - if (resource_public_config and unrestricted_ingress_rules) or ( - resource_public_config and not security_groups + # Effectively Public If: + # 1. Public config and unrestricted SG ingress rules + # 2. Public config and no SG and no resource policy + # 3. Public config and unrestricted policy access + if ( + (resource_public_config and unrestricted_ingress_rules) + or (resource_public_config and not security_groups and not resource_policy) + or (resource_public_config and unrestricted_policy_access) ): + # These are not effectively public, but they could create a public resource. + if resource_type in ( + "AwsEc2Subnet", + "AwsEc2LaunchTemplate", + "AwsAutoScalingLaunchConfiguration", + ): + return {"launch-public": exposure_checks} return {"effectively-public": exposure_checks} - if resource_public_config and not unrestricted_ingress_rules: + # Restricted Public If: + # 1. Public config and no unrestricted SG ingress rules and no unrestricted policy access + if resource_public_config and ( + not unrestricted_ingress_rules and not unrestricted_policy_access + ): return {"restricted-public": exposure_checks} - if not resource_public_config and unrestricted_ingress_rules: + # Restricted Private If: + # 1. No public config and unrestricted SG ingress rules or unrestricted policy access + if not resource_public_config and ( + unrestricted_ingress_rules or unrestricted_policy_access + ): return {"unrestricted-private": exposure_checks} return {"restricted": exposure_checks}