mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-26 13:59:55 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d0a659993 | ||
|
|
4db1a77d5a | ||
|
|
1f1165c2ea | ||
|
|
1dceed7129 | ||
|
|
a3b3e253eb | ||
|
|
3051929780 | ||
|
|
feae73a9d3 | ||
|
|
5c36820149 | ||
|
|
e03feafd96 | ||
|
|
3fce26fb2e | ||
|
|
f2e8cce6c3 | ||
|
|
d71f8fc701 | ||
|
|
3c3ce82eb6 | ||
|
|
1e54b6680c | ||
|
|
6f57c27a27 | ||
|
|
2ef9c2c067 | ||
|
|
677fa531cf | ||
|
|
e09f36f98b | ||
|
|
15fe1e12af |
@@ -127,6 +127,7 @@ aws:
|
||||
]
|
||||
|
||||
# AWS VPC Configuration (vpc_endpoint_connections_trust_boundaries, vpc_endpoint_services_allowed_principals_trust_boundaries)
|
||||
# AWS SSM Configuration (aws.ssm_documents_set_as_public)
|
||||
# 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.
|
||||
# trusted_account_ids : ["123456789012", "098765432109", "678901234567"]
|
||||
|
||||
@@ -36,10 +36,11 @@ If EBS default encyption is not enabled, sensitive information at rest is not pr
|
||||
|
||||
- `ec2_ebs_default_encryption`
|
||||
|
||||
If your Security groups are not properly configured the attack surface is increased, nonetheless, Prowler will detect those security groups that are being used (they are attached) to only notify those that are being used. This logic applies to the 15 checks related to open ports in security groups and the check for the default security group.
|
||||
If your Security groups are not properly configured the attack surface is increased, nonetheless, Prowler will detect those security groups that are being used (they are attached) to only notify those that are being used. This logic applies to the 15 checks related to open ports in security groups, the check for the default security group and for the security groups that allow ingress and egress traffic.
|
||||
|
||||
- `ec2_securitygroup_allow_ingress_from_internet_to_port_X` (15 checks)
|
||||
- `ec2_securitygroup_default_restrict_traffic`
|
||||
- `ec2_securitygroup_allow_wide_open_public_ipv4`
|
||||
|
||||
Prowler will also check for used Network ACLs to only alerts those with open ports that are being used.
|
||||
|
||||
|
||||
@@ -224,7 +224,8 @@ def prowler():
|
||||
# Once the provider is set and we have the eventual checks based on the resource identifier,
|
||||
# it is time to check what Prowler's checks are going to be executed
|
||||
checks_from_resources = global_provider.get_checks_to_execute_by_audit_resources()
|
||||
if checks_from_resources:
|
||||
# Intersect checks from resources with checks to execute so we only run the checks that apply to the resources with the specified ARNs or tags
|
||||
if getattr(args, "resource_arn", None) or getattr(args, "resource_tag", None):
|
||||
checks_to_execute = checks_to_execute.intersection(checks_from_resources)
|
||||
|
||||
# Sort final check list
|
||||
|
||||
@@ -19,8 +19,11 @@ Mutelist:
|
||||
- "StackSet-AWSControlTowerSecurityResources-*"
|
||||
- "StackSet-AWSControlTowerLoggingResources-*"
|
||||
- "StackSet-AWSControlTowerExecutionRole-*"
|
||||
- "AWSControlTowerBP-BASELINE-CLOUDTRAIL-MASTER"
|
||||
- "AWSControlTowerBP-BASELINE-CONFIG-MASTER"
|
||||
- "AWSControlTowerBP-BASELINE-CLOUDTRAIL-MASTER*"
|
||||
- "AWSControlTowerBP-BASELINE-CONFIG-MASTER*"
|
||||
- "StackSet-AWSControlTower*"
|
||||
- "CLOUDTRAIL-ENABLED-ON-SHARED-ACCOUNTS-*"
|
||||
- "AFT-Backend*"
|
||||
"cloudtrail_*":
|
||||
Regions:
|
||||
- "*"
|
||||
|
||||
@@ -11,7 +11,7 @@ from prowler.lib.logger import logger
|
||||
|
||||
timestamp = datetime.today()
|
||||
timestamp_utc = datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
|
||||
prowler_version = "4.3.5"
|
||||
prowler_version = "4.3.6"
|
||||
html_logo_url = "https://github.com/prowler-cloud/prowler/"
|
||||
square_logo_img = "https://prowler.com/wp-content/uploads/logo-html.png"
|
||||
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
|
||||
|
||||
@@ -43,6 +43,7 @@ aws:
|
||||
]
|
||||
|
||||
# AWS VPC Configuration (vpc_endpoint_connections_trust_boundaries, vpc_endpoint_services_allowed_principals_trust_boundaries)
|
||||
# AWS SSM Configuration (aws.ssm_documents_set_as_public)
|
||||
# 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.
|
||||
# trusted_account_ids : ["123456789012", "098765432109", "678901234567"]
|
||||
|
||||
@@ -89,7 +89,11 @@ class ASFF(Output):
|
||||
CreatedAt=timestamp,
|
||||
Severity=Severity(Label=finding.severity.value),
|
||||
Title=finding.check_title,
|
||||
Description=finding.description,
|
||||
Description=(
|
||||
(finding.status_extended[:1000] + "...")
|
||||
if len(finding.status_extended) > 1000
|
||||
else finding.status_extended
|
||||
),
|
||||
Resources=[
|
||||
Resource(
|
||||
Id=finding.resource_uid,
|
||||
|
||||
@@ -52,6 +52,14 @@ def unroll_tags(tags: list) -> dict:
|
||||
>>> unroll_tags(tags)
|
||||
{'name': 'John', 'age': '30'}
|
||||
|
||||
>>> tags = [{"key": "name"}]
|
||||
>>> unroll_tags(tags)
|
||||
{'name': ''}
|
||||
|
||||
>>> tags = [{"Key": "name"}]
|
||||
>>> unroll_tags(tags)
|
||||
{'name': ''}
|
||||
|
||||
>>> tags = [{"name": "John", "age": "30"}]
|
||||
>>> unroll_tags(tags)
|
||||
{'name': 'John', 'age': '30'}
|
||||
@@ -74,9 +82,9 @@ def unroll_tags(tags: list) -> dict:
|
||||
if isinstance(tags[0], str) and len(tags) > 0:
|
||||
return {tag: "" for tag in tags}
|
||||
if "key" in tags[0]:
|
||||
return {item["key"]: item["value"] for item in tags}
|
||||
return {item["key"]: item.get("value", "") for item in tags}
|
||||
elif "Key" in tags[0]:
|
||||
return {item["Key"]: item["Value"] for item in tags}
|
||||
return {item["Key"]: item.get("Value", "") for item in tags}
|
||||
else:
|
||||
return {key: value for d in tags for key, value in d.items()}
|
||||
return {}
|
||||
|
||||
@@ -531,7 +531,7 @@ class AwsProvider(Provider):
|
||||
token=assume_role_response.aws_session_token,
|
||||
expiry_time=assume_role_response.expiration.isoformat(),
|
||||
)
|
||||
logger.info(f"Refreshed Credentials: {refreshed_credentials}")
|
||||
logger.info("Refreshed Credentials")
|
||||
|
||||
return refreshed_credentials
|
||||
|
||||
|
||||
@@ -29,7 +29,12 @@ class cloudformation_stack_outputs_find_secrets(Check):
|
||||
|
||||
# Store the CloudFormation Stack Outputs into a file
|
||||
for output in stack.outputs:
|
||||
temp_output_file.write(f"{output}".encode())
|
||||
temp_output_file.write(
|
||||
bytes(
|
||||
f"{output}\n",
|
||||
encoding="raw_unicode_escape",
|
||||
)
|
||||
)
|
||||
temp_output_file.close()
|
||||
|
||||
# Init detect_secrets
|
||||
@@ -38,11 +43,17 @@ class cloudformation_stack_outputs_find_secrets(Check):
|
||||
with default_settings():
|
||||
secrets.scan_file(temp_output_file.name)
|
||||
|
||||
if secrets.json():
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Potential secret found in Stack {stack.name} Outputs."
|
||||
detect_secrets_output = secrets.json()
|
||||
# If secrets are found, update the report status
|
||||
if detect_secrets_output:
|
||||
secrets_string = ", ".join(
|
||||
[
|
||||
f"{secret['type']} in Output {int(secret['line_number'])}"
|
||||
for secret in detect_secrets_output[temp_output_file.name]
|
||||
]
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Potential secret found in Stack {stack.name} Outputs -> {secrets_string}."
|
||||
|
||||
os.remove(temp_output_file.name)
|
||||
else:
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsRdsDbClusters",
|
||||
"Description": "Check if Neptune Clusters has deletion protection enabled.",
|
||||
"Risk": "Enabling cluster deletion protection offers an additional layer of protection against accidental database deletion or deletion by an unauthorized user. A Neptune DB cluster can't be deleted while deletion protection is enabled. You must first disable deletion protection before a delete request can succeed.",
|
||||
"ResourceType": "AWSDocumentDBClusterSnapshot",
|
||||
"Description": "Check if DocumentDB Clusters has deletion protection enabled.",
|
||||
"Risk": "Enabling cluster deletion protection offers an additional layer of protection against accidental database deletion or deletion by an unauthorized user. A DocumentDB cluster can't be deleted while deletion protection is enabled. You must first disable deletion protection before a delete request can succeed.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/documentdb-controls.html#documentdb-5",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "ec2_securitygroup_allow_wide_open_public_ipv4",
|
||||
"CheckTitle": "Ensure no security groups allow ingress from wide-open non-RFC1918 address.",
|
||||
"CheckTitle": "Ensure no security groups allow ingress and egress from wide-open IP address with a mask between 0 and 24.",
|
||||
"CheckType": [
|
||||
"Infrastructure Security"
|
||||
],
|
||||
@@ -10,7 +10,7 @@
|
||||
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
|
||||
"Severity": "high",
|
||||
"ResourceType": "AwsEc2SecurityGroup",
|
||||
"Description": "Ensure no security groups allow ingress from wide-open non-RFC1918 address.",
|
||||
"Description": "Ensure no security groups allow ingress and egress from ide-open IP address with a mask between 0 and 24.",
|
||||
"Risk": "If Security groups are not properly configured the attack surface is increased.",
|
||||
"RelatedUrl": "",
|
||||
"Remediation": {
|
||||
|
||||
@@ -28,7 +28,7 @@ class ec2_securitygroup_allow_wide_open_public_ipv4(Check):
|
||||
for ingress_rule in security_group.ingress_rules:
|
||||
for ipv4 in ingress_rule["IpRanges"]:
|
||||
ip = ipaddress.ip_network(ipv4["CidrIp"])
|
||||
# Check if IP is public according to RFC1918 and if 0 < prefixlen < 24
|
||||
# Check if IP is public if 0 < prefixlen < 24
|
||||
if (
|
||||
ip.is_global
|
||||
and ip.prefixlen < cidr_treshold
|
||||
@@ -42,7 +42,7 @@ class ec2_securitygroup_allow_wide_open_public_ipv4(Check):
|
||||
for egress_rule in security_group.egress_rules:
|
||||
for ipv4 in egress_rule["IpRanges"]:
|
||||
ip = ipaddress.ip_network(ipv4["CidrIp"])
|
||||
# Check if IP is public according to RFC1918 and if 0 < prefixlen < 24
|
||||
# Check if IP is public if 0 < prefixlen < 24
|
||||
if (
|
||||
ip.is_global
|
||||
and ip.prefixlen < cidr_treshold
|
||||
|
||||
@@ -28,7 +28,7 @@ class Lightsail(AWSService):
|
||||
f"arn:{self.audited_partition}:lightsail:{regional_client.region}:{self.audited_account}:Instance",
|
||||
)
|
||||
|
||||
if not self.audit_resources or is_resource_filtered(
|
||||
if not self.audit_resources or (
|
||||
is_resource_filtered(arn, self.audit_resources)
|
||||
):
|
||||
ports = []
|
||||
|
||||
@@ -16,7 +16,10 @@ class rds_instance_event_subscription_security_groups(Check):
|
||||
)
|
||||
report.region = db_event.region
|
||||
if db_event.source_type == "db-security-group" and db_event.enabled:
|
||||
if db_event.event_list == []:
|
||||
if db_event.event_list == [] or set(db_event.event_list) == {
|
||||
"failure",
|
||||
"configuration change",
|
||||
}:
|
||||
report.resource_id = db_event.id
|
||||
report.resource_arn = db_event.arn
|
||||
report.status = "PASS"
|
||||
|
||||
@@ -11,12 +11,25 @@ class ssm_documents_set_as_public(Check):
|
||||
report.resource_arn = document.arn
|
||||
report.resource_id = document.name
|
||||
report.resource_tags = document.tags
|
||||
if document.account_owners:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"SSM Document {document.name} is public."
|
||||
else:
|
||||
trusted_account_ids = ssm_client.audit_config.get("trusted_account_ids", [])
|
||||
if ssm_client.audited_account not in trusted_account_ids:
|
||||
trusted_account_ids.append(ssm_client.audited_account)
|
||||
if not document.account_owners or document.account_owners == [
|
||||
ssm_client.audited_account
|
||||
]:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"SSM Document {document.name} is not public."
|
||||
elif document.account_owners == ["all"]:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"SSM Document {document.name} is public."
|
||||
elif all(owner in trusted_account_ids for owner in document.account_owners):
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"SSM Document {document.name} is shared to trusted AWS accounts: {', '.join(document.account_owners)}."
|
||||
elif not all(
|
||||
owner in trusted_account_ids for owner in document.account_owners
|
||||
):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"SSM Document {document.name} is shared to non-trusted AWS accounts: {', '.join(document.account_owners)}."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
|
||||
@@ -328,6 +328,8 @@ class VPC(AWSService):
|
||||
regional_client_for_subnet = self.regional_clients[
|
||||
regional_client.region
|
||||
]
|
||||
public = False
|
||||
nat_gateway = False
|
||||
route_tables_for_subnet = (
|
||||
regional_client_for_subnet.describe_route_tables(
|
||||
Filters=[
|
||||
@@ -350,21 +352,20 @@ class VPC(AWSService):
|
||||
]
|
||||
)
|
||||
)
|
||||
public = False
|
||||
nat_gateway = False
|
||||
for route in route_tables_for_subnet.get("RouteTables")[
|
||||
0
|
||||
].get("Routes"):
|
||||
if (
|
||||
"GatewayId" in route
|
||||
and "igw" in route["GatewayId"]
|
||||
and route.get("DestinationCidrBlock", "")
|
||||
== "0.0.0.0/0"
|
||||
):
|
||||
# If the route table has a default route to an internet gateway, the subnet is public
|
||||
public = True
|
||||
if "NatGatewayId" in route:
|
||||
nat_gateway = True
|
||||
for route_table in route_tables_for_subnet.get(
|
||||
"RouteTables"
|
||||
):
|
||||
for route in route_table.get("Routes"):
|
||||
if (
|
||||
"GatewayId" in route
|
||||
and "igw" in route["GatewayId"]
|
||||
and route.get("DestinationCidrBlock", "")
|
||||
== "0.0.0.0/0"
|
||||
):
|
||||
# If the route table has a default route to an internet gateway, the subnet is public
|
||||
public = True
|
||||
if "NatGatewayId" in route:
|
||||
nat_gateway = True
|
||||
subnet_name = ""
|
||||
for tag in subnet.get("Tags", []):
|
||||
if tag["Key"] == "Name":
|
||||
|
||||
@@ -46,7 +46,7 @@ class GcpProvider(Provider):
|
||||
self._impersonated_service_account = arguments.impersonate_service_account
|
||||
list_project_ids = arguments.list_project_id
|
||||
|
||||
self._session = self.setup_session(
|
||||
self._session, self._default_project_id = self.setup_session(
|
||||
credentials_file, self._impersonated_service_account
|
||||
)
|
||||
|
||||
@@ -128,6 +128,10 @@ class GcpProvider(Provider):
|
||||
def projects(self):
|
||||
return self._projects
|
||||
|
||||
@property
|
||||
def default_project_id(self):
|
||||
return self._default_project_id
|
||||
|
||||
@property
|
||||
def impersonated_service_account(self):
|
||||
return self._impersonated_service_account
|
||||
@@ -198,14 +202,14 @@ class GcpProvider(Provider):
|
||||
# "partition": "identity.partition",
|
||||
}
|
||||
|
||||
def setup_session(self, credentials_file: str, service_account: str) -> Credentials:
|
||||
def setup_session(self, credentials_file: str, service_account: str) -> tuple:
|
||||
"""
|
||||
Setup the GCP session with the provided credentials file or service account to impersonate
|
||||
Args:
|
||||
credentials_file: str
|
||||
service_account: str
|
||||
Returns:
|
||||
Credentials object
|
||||
Credentials object and default project ID
|
||||
"""
|
||||
try:
|
||||
scopes = ["https://www.googleapis.com/auth/cloud-platform"]
|
||||
@@ -215,7 +219,7 @@ class GcpProvider(Provider):
|
||||
self.__set_gcp_creds_env_var__(credentials_file)
|
||||
|
||||
# Get default credentials
|
||||
credentials, _ = default(scopes=scopes)
|
||||
credentials, default_project_id = default(scopes=scopes)
|
||||
|
||||
# Refresh the credentials to ensure they are valid
|
||||
credentials.refresh(Request())
|
||||
@@ -231,7 +235,7 @@ class GcpProvider(Provider):
|
||||
)
|
||||
logger.info(f"Impersonated credentials: {credentials}")
|
||||
|
||||
return credentials
|
||||
return credentials, default_project_id
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
|
||||
@@ -30,6 +30,7 @@ class GCPService:
|
||||
)
|
||||
# Only project ids that have their API enabled will be scanned
|
||||
self.project_ids = self.__is_api_active__(provider.project_ids)
|
||||
self.default_project_id = provider.default_project_id
|
||||
self.audit_config = provider.audit_config
|
||||
self.fixer_config = provider.fixer_config
|
||||
|
||||
|
||||
@@ -16,12 +16,12 @@ class apikeys_api_restrictions_configured(Check):
|
||||
if key.restrictions == {} or any(
|
||||
[
|
||||
target.get("service") == "cloudapis.googleapis.com"
|
||||
for target in key.restrictions["apiTargets"]
|
||||
for target in key.restrictions.get("apiTargets", [])
|
||||
]
|
||||
):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"API key {key.name} doens't have restrictions configured."
|
||||
f"API key {key.name} does not have restrictions configured."
|
||||
)
|
||||
findings.append(report)
|
||||
|
||||
|
||||
@@ -283,20 +283,23 @@ class Compute(GCPService):
|
||||
|
||||
def __describe_backend_service__(self):
|
||||
for balancer in self.load_balancers:
|
||||
try:
|
||||
response = (
|
||||
self.client.backendServices()
|
||||
.get(
|
||||
project=balancer.project_id,
|
||||
backendService=balancer.service.split("/")[-1],
|
||||
if balancer.service:
|
||||
try:
|
||||
response = (
|
||||
self.client.backendServices()
|
||||
.get(
|
||||
project=balancer.project_id,
|
||||
backendService=balancer.service.split("/")[-1],
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
balancer.logging = response.get("logConfig", {}).get(
|
||||
"enable", False
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
balancer.logging = response.get("logConfig", {}).get("enable", False)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
class Instance(BaseModel):
|
||||
|
||||
@@ -25,8 +25,9 @@ class DNS(GCPService):
|
||||
ManagedZone(
|
||||
name=managed_zone["name"],
|
||||
id=managed_zone["id"],
|
||||
dnssec=managed_zone["dnssecConfig"]["state"] == "on",
|
||||
key_specs=managed_zone["dnssecConfig"][
|
||||
dnssec=managed_zone.get("dnssecConfig", {})["state"]
|
||||
== "on",
|
||||
key_specs=managed_zone.get("dnssecConfig", {})[
|
||||
"defaultKeySpecs"
|
||||
],
|
||||
project_id=project_id,
|
||||
|
||||
@@ -9,7 +9,7 @@ class iam_organization_essential_contacts_configured(Check):
|
||||
findings = []
|
||||
for org in essentialcontacts_client.organizations:
|
||||
report = Check_Report_GCP(self.metadata())
|
||||
report.project_id = org.id
|
||||
report.project_id = essentialcontacts_client.default_project_id
|
||||
report.resource_id = org.id
|
||||
report.resource_name = org.name
|
||||
report.location = essentialcontacts_client.region
|
||||
|
||||
@@ -29,12 +29,12 @@ class IAM(GCPService):
|
||||
while request is not None:
|
||||
response = request.execute()
|
||||
|
||||
for account in response["accounts"]:
|
||||
for account in response.get("accounts", []):
|
||||
self.service_accounts.append(
|
||||
ServiceAccount(
|
||||
name=account["name"],
|
||||
email=account["email"],
|
||||
display_name=account.get("displayName", ""),
|
||||
display_name=account["displayName"],
|
||||
project_id=project_id,
|
||||
)
|
||||
)
|
||||
@@ -65,7 +65,7 @@ class IAM(GCPService):
|
||||
)
|
||||
response = request.execute()
|
||||
|
||||
for key in response["keys"]:
|
||||
for key in response.get("keys", []):
|
||||
sa.keys.append(
|
||||
Key(
|
||||
name=key["name"].split("/")[-1],
|
||||
@@ -149,7 +149,7 @@ class EssentialContacts(GCPService):
|
||||
.contacts()
|
||||
.list(parent="organizations/" + org.id)
|
||||
).execute()
|
||||
if len(response["contacts"]) > 0:
|
||||
if len(response.get("contacts", [])) > 0:
|
||||
contacts = True
|
||||
|
||||
self.organizations.append(
|
||||
|
||||
@@ -16,9 +16,14 @@ class kms_key_rotation_enabled(Check):
|
||||
now = datetime.datetime.now()
|
||||
condition_next_rotation_time = False
|
||||
if key.next_rotation_time:
|
||||
next_rotation_time = datetime.datetime.strptime(
|
||||
key.next_rotation_time, "%Y-%m-%dT%H:%M:%SZ"
|
||||
)
|
||||
try:
|
||||
next_rotation_time = datetime.datetime.strptime(
|
||||
key.next_rotation_time, "%Y-%m-%dT%H:%M:%S.%fZ"
|
||||
)
|
||||
except ValueError:
|
||||
next_rotation_time = datetime.datetime.strptime(
|
||||
key.next_rotation_time, "%Y-%m-%dT%H:%M:%SZ"
|
||||
)
|
||||
condition_next_rotation_time = (
|
||||
abs((next_rotation_time - now).days) <= 90
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ packages = [
|
||||
{include = "dashboard"}
|
||||
]
|
||||
readme = "README.md"
|
||||
version = "4.3.5"
|
||||
version = "4.3.6"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
alive-progress = "3.1.5"
|
||||
|
||||
@@ -43,6 +43,7 @@ aws:
|
||||
]
|
||||
|
||||
# AWS VPC Configuration (vpc_endpoint_connections_trust_boundaries, vpc_endpoint_services_allowed_principals_trust_boundaries)
|
||||
# AWS SSM Configuration (aws.ssm_documents_set_as_public)
|
||||
# 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.
|
||||
# trusted_account_ids : ["123456789012", "098765432109", "678901234567"]
|
||||
|
||||
@@ -19,6 +19,7 @@ ec2_allowed_instance_owners:
|
||||
]
|
||||
|
||||
# AWS VPC Configuration (vpc_endpoint_connections_trust_boundaries, vpc_endpoint_services_allowed_principals_trust_boundaries)
|
||||
# AWS SSM Configuration (aws.ssm_documents_set_as_public)
|
||||
# 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.
|
||||
# trusted_account_ids : ["123456789012", "098765432109", "678901234567"]
|
||||
|
||||
@@ -84,7 +84,7 @@ class TestASFF:
|
||||
Url=finding.remediation_recommendation_url,
|
||||
)
|
||||
),
|
||||
Description=finding.description,
|
||||
Description=finding.status_extended,
|
||||
)
|
||||
|
||||
asff = ASFF(findings=[finding])
|
||||
@@ -150,7 +150,7 @@ class TestASFF:
|
||||
Url="https://docs.aws.amazon.com/securityhub/latest/userguide/what-is-securityhub.html",
|
||||
)
|
||||
),
|
||||
Description=finding.description,
|
||||
Description=finding.status_extended,
|
||||
)
|
||||
|
||||
asff = ASFF(findings=[finding])
|
||||
@@ -215,7 +215,7 @@ class TestASFF:
|
||||
Url="https://docs.aws.amazon.com/securityhub/latest/userguide/what-is-securityhub.html",
|
||||
)
|
||||
),
|
||||
Description=finding.description,
|
||||
Description=finding.status_extended,
|
||||
)
|
||||
|
||||
asff = ASFF(findings=[finding])
|
||||
@@ -284,7 +284,7 @@ class TestASFF:
|
||||
Url="https://docs.aws.amazon.com/securityhub/latest/userguide/what-is-securityhub.html",
|
||||
)
|
||||
),
|
||||
Description=finding.description,
|
||||
Description=finding.status_extended,
|
||||
)
|
||||
|
||||
asff = ASFF(findings=[finding])
|
||||
@@ -491,7 +491,7 @@ class TestASFF:
|
||||
Url=finding.remediation_recommendation_url,
|
||||
)
|
||||
),
|
||||
Description=finding.description,
|
||||
Description=finding.status_extended,
|
||||
)
|
||||
|
||||
asff = ASFF(findings=[finding])
|
||||
@@ -538,7 +538,7 @@ class TestASFF:
|
||||
"CreatedAt": timestamp,
|
||||
"Severity": {"Label": "HIGH"},
|
||||
"Title": "test-check-id",
|
||||
"Description": "check description",
|
||||
"Description": "This is a test",
|
||||
"Resources": [
|
||||
{
|
||||
"Type": "test-resource",
|
||||
|
||||
@@ -159,6 +159,11 @@ class TestOutputs:
|
||||
"tag3": "",
|
||||
}
|
||||
|
||||
def test_unroll_tags_with_key_only(self):
|
||||
tags = [{"key": "name"}]
|
||||
|
||||
assert unroll_tags(tags) == {"name": ""}
|
||||
|
||||
def test_unroll_dict(self):
|
||||
test_compliance_dict = {
|
||||
"CISA": ["your-systems-3", "your-data-1", "your-data-2"],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import io
|
||||
from json import dumps
|
||||
from os import path
|
||||
|
||||
import botocore
|
||||
import yaml
|
||||
@@ -843,6 +844,134 @@ class TestAWSMutelist:
|
||||
"",
|
||||
)
|
||||
|
||||
def test_is_muted_aws_default_mutelist(
|
||||
self,
|
||||
):
|
||||
|
||||
mutelist = AWSMutelist(
|
||||
mutelist_path=f"{path.dirname(path.realpath(__file__))}/../../../../../prowler/config/aws_mutelist.yaml"
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"StackSet-AWSControlTowerBP-BASELINE-CONFIG-AAAAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"StackSet-AWSControlTowerBP-BASELINE-CLOUDWATCH-AAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"StackSet-AWSControlTowerGuardrailAWS-GR-AUDIT-BUCKET-PUBLIC-READ-PROHIBITED-AAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"StackSet-AWSControlTowerGuardrailAWS-GR-DETECT",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"CLOUDTRAIL-ENABLED-ON-SHARED-ACCOUNTS-AAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"StackSet-AWSControlTowerBP-BASELINE-SERVICE-LINKED-ROLE-AAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"StackSet-AWSControlTowerBP-BASELINE-ROLES-AAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"StackSet-AWSControlTowerBP-SECURITY-TOPICS-AAAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"StackSet-AWSControlTowerBP-BASELINE-SERVICE-ROLES-AAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"StackSet-AWSControlTowerSecurityResources-AAAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"StackSet-AWSControlTowerGuardrailAWS-GR-AUDIT-BUCKET-PUBLIC-WRITE-PROHIBITED-AAAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"AFT-Backend/AAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"AWSControlTowerBP-BASELINE-CONFIG-MASTER/AAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"AWSControlTowerBP-BASELINE-CLOUDTRAIL-MASTER/AAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"StackSet-AWSControlTowerBP-VPC-ACCOUNT-FACTORY-V1-AAA",
|
||||
"",
|
||||
)
|
||||
|
||||
def test_is_muted_single_account(self):
|
||||
# Mutelist
|
||||
mutelist_content = {
|
||||
|
||||
@@ -51,7 +51,7 @@ class Test_cloudformation_stack_outputs_find_secrets:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Potential secret found in Stack {stack_name} Outputs."
|
||||
== f"Potential secret found in Stack {stack_name} Outputs -> Secret Keyword in Output 1."
|
||||
)
|
||||
assert result[0].resource_id == "Test-Stack"
|
||||
assert (
|
||||
@@ -349,3 +349,63 @@ class Test_rds_instance__no_event_subscriptions:
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_id == AWS_ACCOUNT_NUMBER
|
||||
assert result[0].resource_arn == RDS_ACCOUNT_ARN
|
||||
|
||||
@mock_aws
|
||||
def test_rds_security_event_subscription_both_enabled(self):
|
||||
conn = client("rds", region_name=AWS_REGION_US_EAST_1)
|
||||
conn.create_db_parameter_group(
|
||||
DBParameterGroupName="test",
|
||||
DBParameterGroupFamily="default.aurora-postgresql14",
|
||||
Description="test parameter group",
|
||||
)
|
||||
conn.create_db_instance(
|
||||
DBInstanceIdentifier="db-master-1",
|
||||
AllocatedStorage=10,
|
||||
Engine="aurora-postgresql",
|
||||
DBName="aurora-postgres",
|
||||
DBInstanceClass="db.m1.small",
|
||||
DBParameterGroupName="test",
|
||||
DBClusterIdentifier="db-cluster-1",
|
||||
)
|
||||
conn.create_event_subscription(
|
||||
SubscriptionName="TestSub",
|
||||
SnsTopicArn=f"arn:aws:sns:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:test",
|
||||
SourceType="db-security-group",
|
||||
EventCategories=["configuration change", "failure"],
|
||||
Enabled=True,
|
||||
Tags=[
|
||||
{"Key": "test", "Value": "testing"},
|
||||
],
|
||||
)
|
||||
from prowler.providers.aws.services.rds.rds_service import RDS
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
):
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.rds.rds_instance_event_subscription_security_groups.rds_instance_event_subscription_security_groups.rds_client",
|
||||
new=RDS(aws_provider),
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.rds.rds_instance_event_subscription_security_groups.rds_instance_event_subscription_security_groups import (
|
||||
rds_instance_event_subscription_security_groups,
|
||||
)
|
||||
|
||||
check = rds_instance_event_subscription_security_groups()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "RDS security group events are subscribed."
|
||||
)
|
||||
assert result[0].resource_id == "TestSub"
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
== f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:es:TestSub"
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ class Test_ssm_documents_set_as_public:
|
||||
|
||||
assert len(result) == 0
|
||||
|
||||
def test_document_public(self):
|
||||
def test_document_public_account_owners(self):
|
||||
ssm_client = mock.MagicMock
|
||||
document_name = "test-document"
|
||||
document_arn = f"arn:aws:ssm:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:document/{document_name}"
|
||||
@@ -48,6 +48,42 @@ class Test_ssm_documents_set_as_public:
|
||||
check = ssm_documents_set_as_public()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_id == document_name
|
||||
assert result[0].resource_arn == document_arn
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"SSM Document {document_name} is shared to non-trusted AWS accounts: 111111111111, 111111222222."
|
||||
)
|
||||
|
||||
def test_document_public_all_account_owners(self):
|
||||
ssm_client = mock.MagicMock
|
||||
document_name = "test-document"
|
||||
document_arn = f"arn:aws:ssm:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:document/{document_name}"
|
||||
ssm_client.audited_account = AWS_ACCOUNT_NUMBER
|
||||
ssm_client.documents = {
|
||||
document_name: Document(
|
||||
arn=document_arn,
|
||||
name=document_name,
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
content="",
|
||||
account_owners=["all"],
|
||||
)
|
||||
}
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.ssm.ssm_service.SSM",
|
||||
new=ssm_client,
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.ssm.ssm_documents_set_as_public.ssm_documents_set_as_public import (
|
||||
ssm_documents_set_as_public,
|
||||
)
|
||||
|
||||
check = ssm_documents_set_as_public()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_id == document_name
|
||||
@@ -57,6 +93,81 @@ class Test_ssm_documents_set_as_public:
|
||||
result[0].status_extended == f"SSM Document {document_name} is public."
|
||||
)
|
||||
|
||||
def test_document_public_to_other_trusted_AWS_accounts(self):
|
||||
ssm_client = mock.MagicMock
|
||||
document_name = "test-document"
|
||||
document_arn = f"arn:aws:ssm:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:document/{document_name}"
|
||||
ssm_client.audited_account = AWS_ACCOUNT_NUMBER
|
||||
ssm_client.documents = {
|
||||
document_name: Document(
|
||||
arn=document_arn,
|
||||
name=document_name,
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
content="",
|
||||
account_owners=["111111111333", "111111222444"],
|
||||
)
|
||||
}
|
||||
ssm_client.audit_config = {
|
||||
"trusted_account_ids": ["111111111333", "111111222444"]
|
||||
}
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.ssm.ssm_service.SSM",
|
||||
new=ssm_client,
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.ssm.ssm_documents_set_as_public.ssm_documents_set_as_public import (
|
||||
ssm_documents_set_as_public,
|
||||
)
|
||||
|
||||
check = ssm_documents_set_as_public()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_id == document_name
|
||||
assert result[0].resource_arn == document_arn
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"SSM Document {document_name} is shared to trusted AWS accounts: 111111111333, 111111222444."
|
||||
)
|
||||
|
||||
def test_document_public_to_self_account(self):
|
||||
ssm_client = mock.MagicMock
|
||||
document_name = "test-document"
|
||||
document_arn = f"arn:aws:ssm:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:document/{document_name}"
|
||||
ssm_client.audited_account = AWS_ACCOUNT_NUMBER
|
||||
ssm_client.documents = {
|
||||
document_name: Document(
|
||||
arn=document_arn,
|
||||
name=document_name,
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
content="",
|
||||
account_owners=[AWS_ACCOUNT_NUMBER],
|
||||
)
|
||||
}
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.ssm.ssm_service.SSM",
|
||||
new=ssm_client,
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.ssm.ssm_documents_set_as_public.ssm_documents_set_as_public import (
|
||||
ssm_documents_set_as_public,
|
||||
)
|
||||
|
||||
check = ssm_documents_set_as_public()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_id == document_name
|
||||
assert result[0].resource_arn == document_arn
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"SSM Document {document_name} is not public."
|
||||
)
|
||||
|
||||
def test_document_not_public(self):
|
||||
ssm_client = mock.MagicMock
|
||||
document_name = "test-document"
|
||||
|
||||
@@ -318,13 +318,46 @@ class Test_VPC_Service:
|
||||
# Generate VPC Client
|
||||
ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1)
|
||||
# Create VPC
|
||||
vpc = ec2_client.create_vpc(
|
||||
CidrBlock="172.28.7.0/24", InstanceTenancy="default"
|
||||
vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"]
|
||||
default_sg = ec2_client.describe_security_groups(GroupNames=["default"])[
|
||||
"SecurityGroups"
|
||||
][0]
|
||||
default_sg_id = default_sg["GroupId"]
|
||||
ec2_client.authorize_security_group_ingress(
|
||||
GroupId=default_sg_id,
|
||||
IpPermissions=[
|
||||
{
|
||||
"IpProtocol": "tcp",
|
||||
"FromPort": 389,
|
||||
"ToPort": 389,
|
||||
"IpRanges": [{"CidrIp": "0.0.0.0/0"}],
|
||||
}
|
||||
],
|
||||
)
|
||||
subnet = ec2_client.create_subnet(
|
||||
VpcId=vpc["Vpc"]["VpcId"],
|
||||
CidrBlock="172.28.7.192/26",
|
||||
subnet_id = ec2_client.create_subnet(
|
||||
VpcId=vpc_id,
|
||||
CidrBlock="10.0.0.0/16",
|
||||
AvailabilityZone=f"{AWS_REGION_US_EAST_1}a",
|
||||
)["Subnet"]["SubnetId"]
|
||||
# add default route of subnet to an internet gateway to make it public
|
||||
igw_id = ec2_client.create_internet_gateway()["InternetGateway"][
|
||||
"InternetGatewayId"
|
||||
]
|
||||
# attach internet gateway to subnet
|
||||
ec2_client.attach_internet_gateway(InternetGatewayId=igw_id, VpcId=vpc_id)
|
||||
# create route table
|
||||
route_table_id = ec2_client.create_route_table(VpcId=vpc_id)["RouteTable"][
|
||||
"RouteTableId"
|
||||
]
|
||||
# associate route table with subnet
|
||||
ec2_client.associate_route_table(
|
||||
RouteTableId=route_table_id, SubnetId=subnet_id
|
||||
)
|
||||
# add route to route table
|
||||
ec2_client.create_route(
|
||||
RouteTableId=route_table_id,
|
||||
DestinationCidrBlock="0.0.0.0/0",
|
||||
GatewayId=igw_id,
|
||||
)
|
||||
# VPC client for this test class
|
||||
aws_provider = set_mocked_aws_provider(
|
||||
@@ -337,13 +370,13 @@ class Test_VPC_Service:
|
||||
len(vpc.vpcs) == 3
|
||||
) # Number of AWS regions + created VPC, one default VPC per region
|
||||
for vpc in vpc.vpcs.values():
|
||||
if vpc.cidr_block == "172.28.7.0/24":
|
||||
assert vpc.subnets[0].id == subnet["Subnet"]["SubnetId"]
|
||||
if vpc.cidr_block == "10.0.0.0/16":
|
||||
assert vpc.subnets[0].id == subnet_id
|
||||
assert vpc.subnets[0].default is False
|
||||
assert vpc.subnets[0].vpc_id == vpc.id
|
||||
assert vpc.subnets[0].cidr_block == "172.28.7.192/26"
|
||||
assert vpc.subnets[0].vpc_id == vpc_id
|
||||
assert vpc.subnets[0].cidr_block == "10.0.0.0/16"
|
||||
assert vpc.subnets[0].availability_zone == f"{AWS_REGION_US_EAST_1}a"
|
||||
assert vpc.subnets[0].public is False
|
||||
assert vpc.subnets[0].public
|
||||
assert vpc.subnets[0].nat_gateway is False
|
||||
assert vpc.subnets[0].region == AWS_REGION_US_EAST_1
|
||||
assert vpc.subnets[0].tags is None
|
||||
|
||||
@@ -12,13 +12,14 @@ GCP_US_CENTER1_LOCATION = "us-central1"
|
||||
|
||||
|
||||
def set_mocked_gcp_provider(
|
||||
project_ids: list[str] = [], profile: str = ""
|
||||
project_ids: list[str] = [GCP_PROJECT_ID], profile: str = ""
|
||||
) -> GcpProvider:
|
||||
provider = MagicMock()
|
||||
provider.type = "gcp"
|
||||
provider.session = MagicMock()
|
||||
provider.session._service_account_email = "test@test.com"
|
||||
provider.project_ids = project_ids
|
||||
provider.default_project_id = GCP_PROJECT_ID
|
||||
provider.identity = GCPIdentityInfo(
|
||||
profile=profile,
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ class TestGCPProvider:
|
||||
}
|
||||
with patch(
|
||||
"prowler.providers.gcp.gcp_provider.GcpProvider.setup_session",
|
||||
return_value=None,
|
||||
return_value=(None, "test-project"),
|
||||
), patch(
|
||||
"prowler.providers.gcp.gcp_provider.GcpProvider.get_projects",
|
||||
return_value=projects,
|
||||
@@ -47,6 +47,7 @@ class TestGCPProvider:
|
||||
assert gcp_provider.session is None
|
||||
assert gcp_provider.project_ids == ["test-project"]
|
||||
assert gcp_provider.projects == projects
|
||||
assert gcp_provider.default_project_id == "test-project"
|
||||
assert gcp_provider.identity == GCPIdentityInfo(profile="default")
|
||||
assert gcp_provider.audit_config == {"shodan_api_key": None}
|
||||
|
||||
@@ -81,7 +82,7 @@ class TestGCPProvider:
|
||||
}
|
||||
with patch(
|
||||
"prowler.providers.gcp.gcp_provider.GcpProvider.setup_session",
|
||||
return_value=None,
|
||||
return_value=(None, None),
|
||||
), patch(
|
||||
"prowler.providers.gcp.gcp_provider.GcpProvider.get_projects",
|
||||
return_value=projects,
|
||||
@@ -154,7 +155,7 @@ class TestGCPProvider:
|
||||
}
|
||||
with patch(
|
||||
"prowler.providers.gcp.gcp_provider.GcpProvider.setup_session",
|
||||
return_value=None,
|
||||
return_value=(None, None),
|
||||
), patch(
|
||||
"prowler.providers.gcp.gcp_provider.GcpProvider.get_projects",
|
||||
return_value=projects,
|
||||
|
||||
@@ -100,7 +100,7 @@ class Test_apikeys_api_restrictions_configured:
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert search(
|
||||
f"API key {key.name} doens't have restrictions configured.",
|
||||
f"API key {key.name} does not have restrictions configured.",
|
||||
result[0].status_extended,
|
||||
)
|
||||
assert result[0].resource_id == key.id
|
||||
@@ -144,7 +144,7 @@ class Test_apikeys_api_restrictions_configured:
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert search(
|
||||
f"API key {key.name} doens't have restrictions configured.",
|
||||
f"API key {key.name} does not have restrictions configured.",
|
||||
result[0].status_extended,
|
||||
)
|
||||
assert result[0].resource_id == key.id
|
||||
|
||||
@@ -40,6 +40,7 @@ class Test_iam_organization_essential_contacts_configured:
|
||||
essentialcontacts_client.organizations = [
|
||||
Organization(id="test_id", name="test", contacts=True)
|
||||
]
|
||||
essentialcontacts_client.default_project_id = "test_id"
|
||||
from prowler.providers.gcp.services.iam.iam_organization_essential_contacts_configured.iam_organization_essential_contacts_configured import (
|
||||
iam_organization_essential_contacts_configured,
|
||||
)
|
||||
@@ -73,6 +74,7 @@ class Test_iam_organization_essential_contacts_configured:
|
||||
essentialcontacts_client.organizations = [
|
||||
Organization(id="test_id", name="test", contacts=False)
|
||||
]
|
||||
essentialcontacts_client.default_project_id = "test_id"
|
||||
|
||||
from prowler.providers.gcp.services.iam.iam_organization_essential_contacts_configured.iam_organization_essential_contacts_configured import (
|
||||
iam_organization_essential_contacts_configured,
|
||||
|
||||
@@ -549,3 +549,61 @@ class Test_kms_key_rotation_enabled:
|
||||
assert result[0].resource_name == kms_client.crypto_keys[0].name
|
||||
assert result[0].location == kms_client.crypto_keys[0].location
|
||||
assert result[0].project_id == kms_client.crypto_keys[0].project_id
|
||||
|
||||
def test_kms_key_rotation_with_fractional_seconds(self):
|
||||
kms_client = mock.MagicMock
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
), mock.patch(
|
||||
"prowler.providers.gcp.services.kms.kms_key_rotation_enabled.kms_key_rotation_enabled.kms_client",
|
||||
new=kms_client,
|
||||
):
|
||||
from prowler.providers.gcp.services.kms.kms_key_rotation_enabled.kms_key_rotation_enabled import (
|
||||
kms_key_rotation_enabled,
|
||||
)
|
||||
from prowler.providers.gcp.services.kms.kms_service import (
|
||||
CriptoKey,
|
||||
KeyLocation,
|
||||
KeyRing,
|
||||
)
|
||||
|
||||
kms_client.project_ids = [GCP_PROJECT_ID]
|
||||
kms_client.region = GCP_US_CENTER1_LOCATION
|
||||
|
||||
keyring = KeyRing(
|
||||
name="projects/123/locations/us-central1/keyRings/keyring1",
|
||||
project_id=GCP_PROJECT_ID,
|
||||
)
|
||||
|
||||
keylocation = KeyLocation(
|
||||
name=GCP_US_CENTER1_LOCATION,
|
||||
project_id=GCP_PROJECT_ID,
|
||||
)
|
||||
|
||||
kms_client.crypto_keys = [
|
||||
CriptoKey(
|
||||
name="key1",
|
||||
id="projects/123/locations/us-central1/keyRings/keyring1/cryptoKeys/key1",
|
||||
project_id=GCP_PROJECT_ID,
|
||||
rotation_period="7776000s",
|
||||
next_rotation_time="2025-07-06T22:00:00.561275Z",
|
||||
key_ring=keyring.name,
|
||||
location=keylocation.name,
|
||||
members=["user:jane@example.com"],
|
||||
)
|
||||
]
|
||||
|
||||
check = kms_key_rotation_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Key {kms_client.crypto_keys[0].name} is rotated every 90 days or less but the next rotation time is in more than 90 days."
|
||||
)
|
||||
assert result[0].resource_id == kms_client.crypto_keys[0].id
|
||||
assert result[0].resource_name == kms_client.crypto_keys[0].name
|
||||
assert result[0].location == kms_client.crypto_keys[0].location
|
||||
assert result[0].project_id == kms_client.crypto_keys[0].project_id
|
||||
|
||||
Reference in New Issue
Block a user