mirror of
https://github.com/ghndrx/prowler.git
synced 2026-02-10 06:45:08 +00:00
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:
@@ -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
45
lib/arn/arn.py
Normal 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
43
lib/arn/error.py
Normal 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)
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
29866
providers/aws/aws_regions_services.json
Normal file
29866
providers/aws/aws_regions_services.json
Normal file
File diff suppressed because it is too large
Load Diff
0
providers/aws/services/ec2/__init__.py
Normal file
0
providers/aws/services/ec2/__init__.py
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
117
providers/aws/services/ec2/ec2_service.py
Normal file
117
providers/aws/services/ec2/ec2_service.py
Normal 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)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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():
|
||||||
|
|||||||
54
prowler.py
54
prowler.py
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user