feat(regions): Filter Audited Regions (-f) (#1202)

* feat(filter-regions): Added -f and ebs encryption check.

* feat(filter-regions): Added -f and ebs encryption check.

* feat(regional_clients): add regional_clients.

* fix(global variables): created global variables

* chore(role option): Mixed -A/-R option including error handling

* fix(arn): import errors from error.py file

* fix(review_comments): Review PR comments.

Co-authored-by: sergargar <sergio@verica.io>
Co-authored-by: n4ch04 <nachor1992@gmail.com>
This commit is contained in:
Sergio Garcia
2022-06-20 11:25:26 +02:00
committed by GitHub
parent f694a6d12a
commit b89b883741
16 changed files with 30264 additions and 96 deletions

View File

@@ -5,3 +5,9 @@ prowler_version = "3.0-alfa"
# Groups # Groups
groups_file = "groups.json" groups_file = "groups.json"
# AWS services-regions matrix json
aws_services_json_url = (
"https://api.regional-table.region-services.aws.a2z.com/index.json"
)
aws_services_json_file = "providers/aws/aws_regions_services.json"

45
lib/arn/arn.py Normal file
View File

@@ -0,0 +1,45 @@
from arnparse import arnparse
from lib.arn.error import (
RoleArnParsingEmptyResource,
RoleArnParsingFailedMissingFields,
RoleArnParsingIAMRegionNotEmpty,
RoleArnParsingInvalidAccountID,
RoleArnParsingInvalidResourceType,
RoleArnParsingPartitionEmpty,
RoleArnParsingServiceNotIAM,
)
def arn_parsing(arn):
# check for number of fields, must be six
if len(arn.split(":")) != 6:
raise RoleArnParsingFailedMissingFields
else:
arn_parsed = arnparse(arn)
# First check if region is empty (in IAM arns region is always empty)
if arn_parsed.region != None:
raise RoleArnParsingIAMRegionNotEmpty
else:
# check if needed fields are filled:
# - partition
# - service
# - account_id
# - resource_type
# - resource
if arn_parsed.partition == None:
raise RoleArnParsingPartitionEmpty
elif arn_parsed.service != "iam":
raise RoleArnParsingServiceNotIAM
elif (
arn_parsed.account_id == None
or len(arn_parsed.account_id) != 12
or not arn_parsed.account_id.isnumeric()
):
raise RoleArnParsingInvalidAccountID
elif arn_parsed.resource_type != "role":
raise RoleArnParsingInvalidResourceType
elif arn_parsed.resource == "":
raise RoleArnParsingEmptyResource
else:
return arn_parsed

43
lib/arn/error.py Normal file
View File

@@ -0,0 +1,43 @@
class RoleArnParsingFailedMissingFields(Exception):
# The arn contains a numberof fields different than six separated by :"
def __init__(self):
self.message = "The assumed role arn contains a number of fields different than six separated by :, please input a valid arn"
super().__init__(self.message)
class RoleArnParsingIAMRegionNotEmpty(Exception):
# The arn contains a non-empty value for region, since it is an IAM arn is not valid
def __init__(self):
self.message = "The assumed role arn contains a non-empty value for region, since it is an IAM arn is not valid, please input a valid arn"
super().__init__(self.message)
class RoleArnParsingPartitionEmpty(Exception):
# The arn contains an empty value for partition
def __init__(self):
self.message = "The assumed role arn does not contain a value for partition, please input a valid arn"
super().__init__(self.message)
class RoleArnParsingServiceNotIAM(Exception):
def __init__(self):
self.message = "The assumed role arn contains a value for service distinct than iam, please input a valid arn"
super().__init__(self.message)
class RoleArnParsingInvalidAccountID(Exception):
def __init__(self):
self.message = "The assumed role arn contains a value for account id empty or invalid, a valid account id must be composed of 12 numbers, please input a valid arn"
super().__init__(self.message)
class RoleArnParsingInvalidResourceType(Exception):
def __init__(self):
self.message = "The assumed role arn contains a value for resource type different than role, please input a valid arn"
super().__init__(self.message)
class RoleArnParsingEmptyResource(Exception):
def __init__(self):
self.message = "The assumed role arn does not contain a value for resource, please input a valid arn"
super().__init__(self.message)

View File

@@ -3,6 +3,7 @@ import pkgutil
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
from types import ModuleType from types import ModuleType
from colorama import Fore, Style
from config.config import groups_file from config.config import groups_file
from lib.logger import logger from lib.logger import logger
@@ -126,7 +127,7 @@ def recover_modules_from_provider(provider: str, service: str = None) -> list:
def run_check(check): def run_check(check):
print(f"\nCheck Name: {check.CheckName}") print(f"\nCheck Name: {check.CheckName} - {Fore.MAGENTA}{check.ServiceName}{Fore.YELLOW}[{check.Severity}]{Style.RESET_ALL}")
logger.debug(f"Executing check: {check.CheckName}") logger.debug(f"Executing check: {check.CheckName}")
findings = check.execute() findings = check.execute()
report(findings) report(findings)

View File

@@ -2,6 +2,7 @@ from colorama import Fore, Style
def report(check_findings): def report(check_findings):
check_findings.sort(key=lambda x: x.region)
for finding in check_findings: for finding in check_findings:
color = set_report_color(finding.status) color = set_report_color(finding.status)
print( print(

View File

@@ -6,6 +6,7 @@ from boto3 import session
from botocore.credentials import RefreshableCredentials from botocore.credentials import RefreshableCredentials
from botocore.session import get_session from botocore.session import get_session
from lib.arn.arn import arn_parsing
from lib.logger import logger from lib.logger import logger
@@ -20,20 +21,19 @@ class AWS_Credentials:
@dataclass @dataclass
class Input_Data: class Input_Data:
profile: str profile: str
role_name: str role_arn: str
account_to_assume: str
session_duration: int session_duration: int
external_id: str external_id: str
regions: list
@dataclass @dataclass
class AWS_Assume_Role: class AWS_Assume_Role:
role_name: str role_arn: str
account_to_assume: str
session_duration: int session_duration: int
external_id: str external_id: str
sts_session: session sts_session: session
caller_identity: str partition: str
@dataclass @dataclass
@@ -106,7 +106,6 @@ class AWS_Provider:
def validate_credentials(validate_session): def validate_credentials(validate_session):
try: try:
validate_credentials_client = validate_session.client("sts") validate_credentials_client = validate_session.client("sts")
caller_identity = validate_credentials_client.get_caller_identity() caller_identity = validate_credentials_client.get_caller_identity()
@@ -118,36 +117,61 @@ def validate_credentials(validate_session):
def provider_set_session(session_input): def provider_set_session(session_input):
# global variables that are going to be shared accross the project
global aws_session global aws_session
global original_session global original_session
global audited_regions
global audited_partition
global audited_account
assumed_session = None
# Initialize a session info dataclass only with info about the profile
session_info = AWS_Session_Info( session_info = AWS_Session_Info(
session_input.profile, session_input.profile,
None, None,
None, None,
) )
# Create an global original session using only profile/basic credentials info
original_session = AWS_Provider(session_info).get_session() original_session = AWS_Provider(session_info).get_session()
logger.info("Validating credentials ...") logger.info("Validating credentials ...")
# Verificate if we have valid credentials
caller_identity = validate_credentials(original_session) caller_identity = validate_credentials(original_session)
role_info = AWS_Assume_Role(
session_input.role_name,
session_input.account_to_assume,
session_input.session_duration,
session_input.external_id,
original_session,
caller_identity,
)
logger.info("Credentials validated") logger.info("Credentials validated")
logger.info(f"Original caller identity UserId : {caller_identity['UserId']}") logger.info(f"Original caller identity UserId : {caller_identity['UserId']}")
logger.info(f"Original caller identity ARN : {caller_identity['Arn']}") logger.info(f"Original caller identity ARN : {caller_identity['Arn']}")
if session_input.role_name and session_input.account_to_assume: # Set some global values for original session
logger.info( audited_regions = session_input.regions
f"Assuming role {role_info.role_name} in account {role_info.account_to_assume}" audited_account = caller_identity["Account"]
audited_partition = arnparse(caller_identity["Arn"]).partition
if session_input.role_arn:
# Check if role arn is valid
try:
# this returns the arn already parsed, calls arnparse, into a dict to be used when it is needed to access its fields
role_arn_parsed = arn_parsing(session_input.role_arn)
except Exception as error:
logger.critical(f"{error.__class__.__name__} -- {error}")
quit()
# Set info for role assumption if needed
role_info = AWS_Assume_Role(
session_input.role_arn,
session_input.session_duration,
session_input.external_id,
original_session,
audited_partition,
) )
logger.info(f"Assuming role {role_info.role_arn}")
# Assume the role
assumed_role_response = assume_role(role_info) assumed_role_response = assume_role(role_info)
logger.info("Role assumed") logger.info("Role assumed")
# Set the info needed to create a session with an assumed role
session_info = AWS_Session_Info( session_info = AWS_Session_Info(
session_input.profile, session_input.profile,
AWS_Credentials( AWS_Credentials(
@@ -160,26 +184,33 @@ def provider_set_session(session_input):
), ),
role_info, role_info,
) )
assumed_session = AWS_Provider(session_info).get_session()
aws_session = AWS_Provider(session_info).get_session() if assumed_session:
aws_session = assumed_session
audited_account = role_arn_parsed.account_id
audited_partition = role_arn_parsed.partition
else:
aws_session = original_session
def assume_role(role_info): def assume_role(role_info):
try: try:
# set the info to assume the role from the partition, account and role name
sts_client = role_info.sts_session.client("sts") sts_client = role_info.sts_session.client("sts")
arn_caller_identity = arnparse(role_info.caller_identity["Arn"]) # If external id, set it to the assume role api call
role_arn = f"arn:{arn_caller_identity.partition}:iam::{role_info.account_to_assume}:role/{role_info.role_name}"
if role_info.external_id: if role_info.external_id:
assumed_credentials = sts_client.assume_role( assumed_credentials = sts_client.assume_role(
RoleArn=role_arn, RoleArn=role_info.role_arn,
RoleSessionName="ProwlerProSession", RoleSessionName="ProwlerProSession",
DurationSeconds=role_info.session_duration, DurationSeconds=role_info.session_duration,
ExternalId=role_info.external_id, ExternalId=role_info.external_id,
) )
# else assume the role without the external id
else: else:
assumed_credentials = sts_client.assume_role( assumed_credentials = sts_client.assume_role(
RoleArn=role_arn, RoleArn=role_info.role_arn,
RoleSessionName="ProwlerProSession", RoleSessionName="ProwlerProSession",
DurationSeconds=role_info.session_duration, DurationSeconds=role_info.session_duration,
) )

File diff suppressed because it is too large Load Diff

View File

View File

@@ -0,0 +1,58 @@
{
"Categories": [
"cat1",
"cat2"
],
"CheckAlias": "extra740",
"CheckID": "ec2_ebs_snapshots_encrypted",
"CheckName": "ec2_ebs_snapshots_encrypted",
"CheckTitle": "Check if EBS snapshots are encrypted",
"CheckType": "Data Protection",
"Compliance": [
{
"Control": [
"4.4"
],
"Framework": "CIS-AWS",
"Group": [
"level1",
"level2"
],
"Version": "1.4"
}
],
"DependsOn": [
"othercheck1",
"othercheck2"
],
"Description": "If Security groups are not properly configured the attack surface is increased.",
"Notes": "additional information",
"Provider": "aws",
"RelatedTo": [
"othercheck3",
"othercheck4"
],
"RelatedUrl": "https://serviceofficialsiteorpageforthissubject",
"Remediation": {
"Code": {
"NativeIaC": "code or URL to the code location.",
"Terraform": "code or URL to the code location.",
"cli": "cli command or URL to the cli command location.",
"other": "cli command or URL to the cli command location."
},
"Recommendation": {
"Text": "Run sudo yum update and cross your fingers and toes.",
"Url": "https://myfp.com/recommendations/dangerous_things_and_how_to_fix_them.html"
}
},
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
"ResourceType": "AwsIamAccessAnalyzer",
"Risk": "Risk associated.",
"ServiceName": "ec2",
"Severity": "low",
"SubServiceName": "accessanalyzer",
"Tags": {
"Tag1Key": "value",
"Tag2Key": "value"
}
}

View File

@@ -0,0 +1,35 @@
from lib.check.check import Check, Check_Report
from providers.aws.services.ec2.ec2_service import ec2_client
class ec2_ebs_snapshots_encrypted(Check):
def execute(self):
findings = []
for regional_client in ec2_client.regional_clients:
region = regional_client.region
if hasattr(regional_client, "snapshots"):
if regional_client.snapshots:
for snapshot in regional_client.snapshots:
if snapshot["Encrypted"]:
report = Check_Report()
report.status = "PASS"
report.result_extended = (
f"EBS Snapshot {snapshot['SnapshotId']} is encrypted"
)
report.region = region
else:
report = Check_Report()
report.status = "FAIL"
report.result_extended = (
f"EBS Snapshot {snapshot['SnapshotId']} is unencrypted"
)
report.region = region
else:
report = Check_Report()
report.status = "PASS"
report.result_extended = "There are no EC2 EBS snapshots"
report.region = region
findings.append(report)
return findings

View File

@@ -0,0 +1,117 @@
import json
import threading
import urllib.request
from config.config import aws_services_json_file, aws_services_json_url
from lib.logger import logger
from lib.utils.utils import open_file, parse_json_file
from providers.aws.aws_provider import (
audited_account,
audited_partition,
audited_regions,
aws_session,
)
################## EC2
class EC2:
def __init__(self, aws_session, audited_regions):
self.service = "ec2"
self.aws_session = aws_session
self.regional_clients = self.__generate_regional_clients__(
self.service, audited_regions
)
self.__threading_call__(self.__describe_snapshots__)
def __get_clients__(self):
return self.clients
def __get_session__(self):
return self.aws_session
def __generate_regional_clients__(self, service, audited_regions):
regional_clients = []
try: # Try to get the list online
with urllib.request.urlopen(aws_services_json_url) as url:
data = json.loads(url.read().decode())
except:
# Get the list locally
f = open_file(aws_services_json_file)
data = parse_json_file(f)
for att in data["prices"]:
if audited_regions: # Check for input aws audited_regions
if (
service in att["id"].split(":")[0]
and att["attributes"]["aws:region"] in audited_regions
): # Check if service has this region
region = att["attributes"]["aws:region"]
regional_client = aws_session.client(service, region_name=region)
regional_client.region = region
regional_clients.append(regional_client)
else:
if audited_partition in "aws":
if (
service in att["id"].split(":")[0]
and "gov" not in att["attributes"]["aws:region"]
and "cn" not in att["attributes"]["aws:region"]
):
region = att["attributes"]["aws:region"]
regional_client = aws_session.client(
service, region_name=region
)
regional_client.region = region
regional_clients.append(regional_client)
elif audited_partition in "cn":
if (
service in att["id"].split(":")[0]
and "cn" in att["attributes"]["aws:region"]
):
region = att["attributes"]["aws:region"]
regional_client = aws_session.client(
service, region_name=region
)
regional_client.region = region
regional_clients.append(regional_client)
elif audited_partition in "gov":
if (
service in att["id"].split(":")[0]
and "gov" in att["attributes"]["aws:region"]
):
region = att["attributes"]["aws:region"]
regional_client = aws_session.client(
service, region_name=region
)
regional_client.region = region
regional_clients.append(regional_client)
return regional_clients
def __threading_call__(self, call):
threads = []
for regional_client in self.regional_clients:
threads.append(threading.Thread(target=call, args=(regional_client,)))
for t in threads:
t.start()
for t in threads:
t.join()
def __describe_snapshots__(self, regional_client):
logger.info("EC2 - Describing Snapshots...")
try:
describe_snapshots_paginator = regional_client.get_paginator(
"describe_snapshots"
)
snapshots = []
for page in describe_snapshots_paginator.paginate(
OwnerIds=[audited_account]
):
for snapshot in page["Snapshots"]:
snapshots.append(snapshot)
except Exception as error:
logger.error(f"{error.__class__.__name__} -- {error}")
else:
regional_client.snapshots = snapshots
ec2_client = EC2(aws_session, audited_regions)

View File

@@ -22,18 +22,18 @@ class iam_disable_30_days_credentials(Check):
) )
if time_since_insertion.days > maximum_expiration_days: if time_since_insertion.days > maximum_expiration_days:
report.status = "FAIL" report.status = "FAIL"
report.result_extended = f"User {user['UserName']} has not logged into the console in the past 90 days" report.result_extended = f"User {user['UserName']} has not logged into the console in the past 30 days"
report.region = "us-east-1" report.region = "us-east-1"
else: else:
report.status = "PASS" report.status = "PASS"
report.result_extended = f"User {user['UserName']} has logged into the console in the past 90 days" report.result_extended = f"User {user['UserName']} has logged into the console in the past 30 days"
report.region = "us-east-1" report.region = "us-east-1"
except KeyError: except KeyError:
pass pass
else: else:
report.status = "PASS" report.status = "PASS"
report.result_extended = ( report.result_extended = (
f"User {user['UserName']} has not console password" f"User {user['UserName']} has not a console password or is unused."
) )
report.region = "us-east-1" report.region = "us-east-1"
@@ -46,4 +46,4 @@ class iam_disable_30_days_credentials(Check):
report.region = "us-east-1" report.region = "us-east-1"
findings.append(report) findings.append(report)
return findings return findings

View File

@@ -7,14 +7,13 @@ maximum_expiration_days = 90
class iam_disable_90_days_credentials(Check): class iam_disable_90_days_credentials(Check):
def execute(self): def execute(self) -> Check_Report:
findings = [] findings = []
report = Check_Report
response = iam_client.users response = iam_client.users
if response: if response:
for user in response: for user in response:
report = Check_Report report = Check_Report()
if "PasswordLastUsed" in user and user["PasswordLastUsed"] != "": if "PasswordLastUsed" in user and user["PasswordLastUsed"] != "":
try: try:
time_since_insertion = ( time_since_insertion = (
@@ -34,13 +33,16 @@ class iam_disable_90_days_credentials(Check):
else: else:
report.status = "PASS" report.status = "PASS"
report.result_extended = ( report.result_extended = (
f"User {user['UserName']} has not console password" f"User {user['UserName']} has not a console password or is unused."
) )
report.region = "us-east-1" report.region = "us-east-1"
# Append report
findings.append(report) findings.append(report)
else: else:
report = Check_Report()
report.status = "PASS" report.status = "PASS"
report.result_extended = "There is no IAM users" report.result_extended = "There is no IAM users"
report.region = "us-east-1" report.region = "us-east-1"
return findings return findings

View File

@@ -23,13 +23,8 @@ class IAM:
def __get_roles__(self): def __get_roles__(self):
try: try:
get_roles_paginator = self.client.get_paginator("list_roles") get_roles_paginator = self.client.get_paginator("list_roles")
except botocore.exceptions.ClientError as error:
logger.error(
f"{error.response['Error']['Code']} -- {error.response['Error']['Message']}"
)
except Exception as error: except Exception as error:
logger.critical(f"{error.__class__.__name__} -- {error}") logger.error(f"{error.__class__.__name__} -- {error}")
quit()
else: else:
roles = [] roles = []
for page in get_roles_paginator.paginate(): for page in get_roles_paginator.paginate():
@@ -43,13 +38,8 @@ class IAM:
while not report_is_completed: while not report_is_completed:
try: try:
report_status = self.client.generate_credential_report() report_status = self.client.generate_credential_report()
except botocore.exceptions.ClientError as error:
logger.error(
f"{error.response['Error']['Code']} -- {error.response['Error']['Message']}"
)
except Exception as error: except Exception as error:
logger.critical(f"{error.__class__.__name__} -- {error}") logger.error(f"{error.__class__.__name__} -- {error}")
quit()
else: else:
if report_status["State"] == "COMPLETE": if report_status["State"] == "COMPLETE":
report_is_completed = True report_is_completed = True
@@ -59,13 +49,8 @@ class IAM:
def __get_groups__(self): def __get_groups__(self):
try: try:
get_groups_paginator = self.client.get_paginator("list_groups") get_groups_paginator = self.client.get_paginator("list_groups")
except botocore.exceptions.ClientError as error:
logger.error(
f"{error.response['Error']['Code']} -- {error.response['Error']['Message']}"
)
except Exception as error: except Exception as error:
logger.critical(f"{error.__class__.__name__} -- {error}") logger.error(f"{error.__class__.__name__} -- {error}")
quit()
else: else:
groups = [] groups = []
for page in get_groups_paginator.paginate(): for page in get_groups_paginator.paginate():
@@ -79,13 +64,8 @@ class IAM:
get_customer_managed_policies_paginator = self.client.get_paginator( get_customer_managed_policies_paginator = self.client.get_paginator(
"list_policies" "list_policies"
) )
except botocore.exceptions.ClientError as error:
logger.error(
f"{error.response['Error']['Code']} -- {error.response['Error']['Message']}"
)
except Exception as error: except Exception as error:
logger.critical(f"{error.__class__.__name__} -- {error}") logger.error(f"{error.__class__.__name__} -- {error}")
quit()
else: else:
customer_managed_policies = [] customer_managed_policies = []
for page in get_customer_managed_policies_paginator.paginate(Scope="Local"): for page in get_customer_managed_policies_paginator.paginate(Scope="Local"):
@@ -97,13 +77,8 @@ class IAM:
def __get_users__(self): def __get_users__(self):
try: try:
get_users_paginator = self.client.get_paginator("list_users") get_users_paginator = self.client.get_paginator("list_users")
except botocore.exceptions.ClientError as error:
logger.error(
f"{error.response['Error']['Code']} -- {error.response['Error']['Message']}"
)
except Exception as error: except Exception as error:
logger.critical(f"{error.__class__.__name__} -- {error}") logger.error(f"{error.__class__.__name__} -- {error}")
quit()
else: else:
users = [] users = []
for page in get_users_paginator.paginate(): for page in get_users_paginator.paginate():

View File

@@ -51,14 +51,7 @@ if __name__ == "__main__":
"--role", "--role",
nargs="?", nargs="?",
default=None, default=None,
help="Role name to be assumed in account passed with -A", help="ARN of the role to be assumed",
)
parser.add_argument(
"-A",
"--account",
nargs="?",
default=None,
help="AWS account id where the role passed by -R is assumed",
) )
parser.add_argument( parser.add_argument(
"-T", "-T",
@@ -75,6 +68,12 @@ if __name__ == "__main__":
default=None, default=None,
help="External ID to be passed when assuming role", help="External ID to be passed when assuming role",
) )
parser.add_argument(
"-f",
"--filter-region",
nargs="+",
help="AWS region names to run Prowler against",
)
# Parse Arguments # Parse Arguments
args = parser.parse_args() args = parser.parse_args()
@@ -84,38 +83,19 @@ if __name__ == "__main__":
services = args.services services = args.services
groups = args.groups groups = args.groups
checks_file = args.checks_file checks_file = args.checks_file
# Set Logger
logger.setLevel(logging_levels.get(args.log_level))
# Role assumption input options tests # Role assumption input options tests
if args.role or args.account:
if not args.account:
logger.critical(
"It is needed to input an Account Id to assume the role (-A option) when an IAM Role is provided with -R"
)
quit()
elif not args.role:
logger.critical(
"It is needed to input an IAM Role name (-R option) when an Account Id is provided with -A"
)
quit()
if args.session_duration not in range(900, 43200): if args.session_duration not in range(900, 43200):
logger.critical("Value for -T option must be between 900 and 43200") logger.critical("Value for -T option must be between 900 and 43200")
quit() quit()
if args.session_duration != 3600 or args.external_id: if args.session_duration != 3600 or args.external_id:
if not args.account or not args.role: if not args.role:
logger.critical("To use -I/-T options both -A and -R options are needed") logger.critical("To use -I/-T options -R option is needed")
quit() quit()
session_input = Input_Data(
profile=args.profile,
role_name=args.role,
account_to_assume=args.account,
session_duration=args.session_duration,
external_id=args.external_id,
)
# Set Logger
logger.setLevel(logging_levels.get(args.log_level))
if args.version: if args.version:
print_version() print_version()
quit() quit()
@@ -124,6 +104,14 @@ if __name__ == "__main__":
print_banner() print_banner()
# Setting session # Setting session
session_input = Input_Data(
profile=args.profile,
role_arn=args.role,
session_duration=args.session_duration,
external_id=args.external_id,
regions=args.filter_region,
)
provider_set_session(session_input) provider_set_session(session_input)
# Load checks to execute # Load checks to execute