diff --git a/Pipfile.lock b/Pipfile.lock index 388eb73e..e94f2a03 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -112,19 +112,19 @@ }, "boto3": { "hashes": [ - "sha256:bb40a9788dd2234851cdd1110eec0e3f6b3af6b98280924fa44c25199ced5737", - "sha256:c39b7e87b27b00dcf452b2fc80252d311e275036f3d48464af34d0123077f985" + "sha256:4b4edf893b01c651007d61534c1d248cd2350d311a4e295039bd23fd60bf899a", + "sha256:59aa6c7810a815fb52671f834d10ac4cd80b9c7c01a3cbde670cb41330059464" ], "index": "pypi", - "version": "==1.26.17" + "version": "==1.26.19" }, "botocore": { "hashes": [ - "sha256:4be7ca8c581dbc6e8584876c4347dcc4f4bc6aa6e6e8131901fc11816fc8151b", - "sha256:d4bab7d42acdb18effa33fee53d137b8b1bdedc2da196428a2d1e04a123778bc" + "sha256:917807ee4ccca34a2f2848eb4fcf878d9e97a44a911a6965ff556d0830c471fd", + "sha256:a54561e591f5d8e653657ce04dcad09c10ebca9dbefba73471976e522abf038a" ], "index": "pypi", - "version": "==1.29.17" + "version": "==1.29.19" }, "certifi": { "hashes": [ @@ -589,19 +589,19 @@ }, "boto3": { "hashes": [ - "sha256:bb40a9788dd2234851cdd1110eec0e3f6b3af6b98280924fa44c25199ced5737", - "sha256:c39b7e87b27b00dcf452b2fc80252d311e275036f3d48464af34d0123077f985" + "sha256:4b4edf893b01c651007d61534c1d248cd2350d311a4e295039bd23fd60bf899a", + "sha256:59aa6c7810a815fb52671f834d10ac4cd80b9c7c01a3cbde670cb41330059464" ], "index": "pypi", - "version": "==1.26.17" + "version": "==1.26.19" }, "botocore": { "hashes": [ - "sha256:4be7ca8c581dbc6e8584876c4347dcc4f4bc6aa6e6e8131901fc11816fc8151b", - "sha256:d4bab7d42acdb18effa33fee53d137b8b1bdedc2da196428a2d1e04a123778bc" + "sha256:917807ee4ccca34a2f2848eb4fcf878d9e97a44a911a6965ff556d0830c471fd", + "sha256:a54561e591f5d8e653657ce04dcad09c10ebca9dbefba73471976e522abf038a" ], "index": "pypi", - "version": "==1.29.17" + "version": "==1.29.19" }, "certifi": { "hashes": [ @@ -905,11 +905,11 @@ }, "jsonschema": { "hashes": [ - "sha256:05b2d22c83640cde0b7e0aa329ca7754fbd98ea66ad8ae24aa61328dfe057fa3", - "sha256:410ef23dcdbca4eaedc08b850079179883c2ed09378bd1f760d4af4aacfa28d7" + "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", + "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6" ], "markers": "python_version >= '3.7'", - "version": "==4.17.1" + "version": "==4.17.3" }, "jsonschema-spec": { "hashes": [ @@ -1110,11 +1110,11 @@ }, "pylint": { "hashes": [ - "sha256:15060cc22ed6830a4049cf40bc24977744df2e554d38da1b2657591de5bcd052", - "sha256:25b13ddcf5af7d112cf96935e21806c1da60e676f952efb650130f2a4483421c" + "sha256:1d561d1d3e8be9dd880edc685162fbdaa0409c88b9b7400873c0cf345602e326", + "sha256:91e4776dbcb4b4d921a3e4b6fec669551107ba11f29d9199154a01622e460a57" ], "index": "pypi", - "version": "==2.15.6" + "version": "==2.15.7" }, "pyparsing": { "hashes": [ @@ -1360,7 +1360,7 @@ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], - "markers": "python_version < '3.11'", + "markers": "python_full_version < '3.11.0a7'", "version": "==2.0.1" }, "tomlkit": { diff --git a/lib/outputs/outputs.py b/lib/outputs/outputs.py index bf79fd98..6e7ee869 100644 --- a/lib/outputs/outputs.py +++ b/lib/outputs/outputs.py @@ -49,13 +49,14 @@ def report(check_findings, output_options, audit_info): # csv_fields = [] file_descriptors = {} if output_options.output_modes: - # We have to create the required output files - file_descriptors = fill_file_descriptors( - output_options.output_modes, - output_options.output_directory, - output_options.output_filename, - audit_info, - ) + if isinstance(audit_info, AWS_Audit_Info): + # We have to create the required output files + file_descriptors = fill_file_descriptors( + output_options.output_modes, + output_options.output_directory, + output_options.output_filename, + audit_info, + ) if check_findings: for finding in check_findings: @@ -325,6 +326,7 @@ def generate_csv_fields(format: Any) -> list[str]: def fill_json(finding_output, audit_info, finding): + finding_output.AssessmentStartTime = timestamp_iso finding_output.FindingUniqueId = "" finding_output.Profile = audit_info.profile @@ -457,7 +459,7 @@ def send_to_s3_bucket( def display_summary_table( findings: list, - audit_info: AWS_Audit_Info, + audit_info, output_options: Output_From_Options, provider: str, ): @@ -466,8 +468,14 @@ def display_summary_table( try: if provider == "aws": entity_type = "Account" + audited_entities = audit_info.audited_account elif provider == "azure": - entity_type = "Tenant Domain" + if audit_info.identity.domain: + entity_type = "Tenant Domain" + audited_entities = audit_info.identity.domain + else: + entity_type = "Tenant ID/s" + audited_entities = " ".join(audit_info.identity.tenant_ids) if findings: current = { @@ -531,12 +539,13 @@ def display_summary_table( ] ] print(tabulate(overview_table, tablefmt="rounded_grid")) + print( - f"\n{entity_type} {Fore.YELLOW}{audit_info.audited_account}{Style.RESET_ALL} Scan Results (severity columns are for fails only):" + f"\n{entity_type} {Fore.YELLOW}{audited_entities}{Style.RESET_ALL} Scan Results (severity columns are for fails only):" ) if provider == "azure": print( - f"\nSubscriptions scanned: {Fore.YELLOW}{' '.join(audit_info.subscriptions.keys())}{Style.RESET_ALL}" + f"\nSubscriptions scanned: {Fore.YELLOW}{' '.join(audit_info.identity.subscriptions.keys())}{Style.RESET_ALL}" ) print(tabulate(findings_table, headers="keys", tablefmt="rounded_grid")) print( @@ -552,7 +561,7 @@ def display_summary_table( else: print( - f"\n {Style.BRIGHT}There are no findings in {entity_type} {Fore.YELLOW}{audit_info.audited_account}{Style.RESET_ALL}\n" + f"\n {Style.BRIGHT}There are no findings in {entity_type} {Fore.YELLOW}{audited_entities}{Style.RESET_ALL}\n" ) except Exception as error: diff --git a/providers/azure/azure_provider.py b/providers/azure/azure_provider.py index 9c09b918..53c8ade1 100644 --- a/providers/azure/azure_provider.py +++ b/providers/azure/azure_provider.py @@ -1,7 +1,7 @@ import sys from os import getenv -from azure.identity import DefaultAzureCredential +from azure.identity import DefaultAzureCredential, InteractiveBrowserCredential from azure.mgmt.subscription import SubscriptionClient from msgraph.core import GraphClient @@ -11,82 +11,190 @@ from providers.azure.lib.audit_info.models import Azure_Audit_Info, Azure_Identi class Azure_Provider: - def __init__(self): + def __init__( + self, + az_cli_auth: bool, + sp_env_auth: bool, + browser_auth: bool, + managed_entity_auth: bool, + subscription_ids: list, + ): logger.info("Instantiating Azure Provider ...") - self.credentials = DefaultAzureCredential() + self.credentials = self.__set_credentials__( + az_cli_auth, sp_env_auth, browser_auth, managed_entity_auth + ) + self.identity = self.__set_identity_info__( + self.credentials, + az_cli_auth, + sp_env_auth, + browser_auth, + managed_entity_auth, + subscription_ids, + ) + + def __set_credentials__( + self, az_cli_auth, sp_env_auth, browser_auth, managed_entity_auth + ): + # Browser auth creds cannot be set with DefaultAzureCredentials() + if not browser_auth: + if sp_env_auth: + self.__check_sp_creds_env_vars__() + try: + # Since the input vars come as True when it is wanted to be used, we need to inverse it since + # DefaultAzureCredential sets the auth method excluding the others + credentials = DefaultAzureCredential( + exclude_environment_credential=not sp_env_auth, + exclude_cli_credential=not az_cli_auth, + exclude_managed_identity_credential=not managed_entity_auth, + # Azure Auth using Visual Studio is not supported + exclude_visual_studio_code_credential=True, + # Azure Auth using Shared Token Cache is not supported + exclude_shared_token_cache_credential=True, + # Azure Auth using PowerShell is not supported + exclude_powershell_credential=True, + ) + except Exception as error: + logger.critical("Failed to retrieve azure credentials") + logger.critical( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + sys.exit() + else: + credentials = InteractiveBrowserCredential() + + return credentials + + def __check_sp_creds_env_vars__(self): + logger.info( + "Azure provider: checking service principal environment variables ..." + ) + for env_var in ["AZURE_CLIENT_ID", "AZURE_TENANT_ID", "AZURE_CLIENT_SECRET"]: + if not getenv(env_var): + logger.critical( + f"Azure provider: Missing environment variable {env_var} needed to autenticate against Azure" + ) + sys.exit() + + def __set_identity_info__( + self, + credentials, + az_cli_auth, + sp_env_auth, + browser_auth, + managed_entity_auth, + subscription_ids, + ): + identity = Azure_Identity_Info() + + # If credentials comes from service principal or browser, if the required permissions are assigned + # the identity can access AAD and retrieve the tenant domain name. + # With cli also should be possible but right now it does not work, azure python package issue is coming + # At the time of writting this with az cli creds is not working, despite that is included + if sp_env_auth or browser_auth or az_cli_auth: + # Trying to recover tenant domain info + try: + logger.info( + "Trying to retrieve tenant domain from AAD to populate identity structure ..." + ) + client = GraphClient(credential=credentials) + domain_result = client.get("/domains").json() + if "value" in domain_result: + if "id" in domain_result["value"][0]: + identity.domain = domain_result["value"][0]["id"] + except Exception as error: + logger.error( + "Provided identity does not have permissions to access AAD to retrieve tenant domain" + ) + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + # since that exception is not considered as critical, we keep filling another identity fields + if sp_env_auth: + # The id of the sp can be retrieved from environment variables + identity.identity_id = getenv("AZURE_CLIENT_ID") + identity.identity_type = "Service Principal" + # Same here, if user can access AAD, some fields are retrieved if not, default value, for az cli + # should work but it doesn't, pending issue + else: + identity.identity_id = "Unknown user id (NO AAD permissions)" + identity.identity_type = "User" + try: + logger.info( + "Trying to retrieve user information from AAD to populate identity structure ..." + ) + client = GraphClient(credential=credentials) + user_name = client.get("/me").json()["userPrincipalName"] + identity.identity_id = user_name + + except Exception as error: + logger.error( + "Provided identity does not have permissions to access AAD to retrieve user's metadata" + ) + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + # Managed identities only can be assigned resource, resource group and subscription scope permissions + elif managed_entity_auth: + identity.identity_id = "Default Managed Identity ID" + identity.identity_type = "Managed Identity" + # Pending extracting info from managed identity + + # once we have populated the id, type, and domain fields, time to retrieve the subscriptions and finally the tenants + try: + logger.info( + "Trying to subscriptions and tenant ids to populate identity structure ..." + ) + subscriptions_client = SubscriptionClient(credential=credentials) + if not subscription_ids: + logger.info("Scanning all the Azure subscriptions...") + for subscription in subscriptions_client.subscriptions.list(): + + identity.subscriptions.update( + {subscription.display_name: subscription.subscription_id} + ) + else: + logger.info("Scanning the subscriptions passed as argument ...") + for id in subscription_ids: + subscription = subscriptions_client.subscriptions.get( + subscription_id=id + ) + identity.subscriptions.update({subscription.display_name: id}) + + tenants = subscriptions_client.tenants.list() + for tenant in tenants: + identity.tenant_ids.append(tenant.tenant_id) + # This error is critical, since it implies something is wrong with the credentials provided + except Exception as error: + logger.critical( + "Error with credentials provided getting subscriptions and tenants to scan" + ) + logger.critical( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + sys.exit() + + return identity def get_credentials(self): return self.credentials - -def check_credential_env_vars() -> Azure_Identity_Info: - for env_var in ["AZURE_CLIENT_ID", "AZURE_TENANT_ID", "AZURE_CLIENT_SECRET"]: - if not getenv(env_var): - logger.critical( - f"Azure provider: Missing environment variable {env_var} needed to autenticate against Azure" - ) - sys.exit() - - azure_identity = Azure_Identity_Info( - app_id=getenv("AZURE_CLIENT_ID"), tenant_id=getenv("AZURE_TENANT_ID") - ) - - return azure_identity + def get_identity(self): + return self.identity -def validate_credentials( - azure_identity: Azure_Identity_Info, client: GraphClient -) -> Azure_Identity_Info: - - try: - logger.info("Azure provider: validating service principal credentials ...") - result = client.get("/servicePrincipals/").json() - if "value" in result: - for sp in result["value"]: - if sp["appId"] == azure_identity.app_id: - azure_identity.id = sp["id"] - except Exception as error: - logger.critical( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - sys.exit() - else: - return azure_identity - - -def azure_provider_set_session(subscription_ids: list) -> Azure_Audit_Info: +def azure_provider_set_session( + subscription_ids: list, + az_cli_auth: bool, + sp_env_auth: bool, + browser_auth: bool, + managed_entity_auth: bool, +) -> Azure_Audit_Info: logger.info("Setting Azure session ...") - azure_identity = check_credential_env_vars() - azure_audit_info.credentials = Azure_Provider().get_credentials() - client = GraphClient(credential=azure_audit_info.credentials) - azure_audit_info.identity = validate_credentials(azure_identity, client) - try: - domain_result = client.get("/domains").json() - if "value" in domain_result: - if "id" in domain_result["value"][0]: - azure_audit_info.audited_account = domain_result["value"][0]["id"] - subscriptions_client = SubscriptionClient( - credential=azure_audit_info.credentials - ) - if not subscription_ids: - logger.info("Scanning all the Azure subscriptions...") - for subscription in subscriptions_client.subscriptions.list(): + azure_provider = Azure_Provider( + az_cli_auth, sp_env_auth, browser_auth, managed_entity_auth, subscription_ids + ) + azure_audit_info.credentials = azure_provider.get_credentials() + azure_audit_info.identity = azure_provider.get_identity() - azure_audit_info.subscriptions.update( - {subscription.display_name: subscription.subscription_id} - ) - else: - logger.info("Scanning the subscriptions passed as argument ...") - for id in subscription_ids: - subscription = subscriptions_client.subscriptions.get( - subscription_id=id - ) - azure_audit_info.subscriptions.update({subscription.display_name: id}) - except Exception as error: - logger.critical( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - sys.exit() - else: - return azure_audit_info + return azure_audit_info diff --git a/providers/azure/lib/audit_info/audit_info.py b/providers/azure/lib/audit_info/audit_info.py index e1dde8d1..529f7b15 100644 --- a/providers/azure/lib/audit_info/audit_info.py +++ b/providers/azure/lib/audit_info/audit_info.py @@ -1,5 +1,3 @@ from providers.azure.lib.audit_info.models import Azure_Audit_Info, Azure_Identity_Info -azure_audit_info = Azure_Audit_Info( - credentials=None, identity=Azure_Identity_Info(), subscriptions={} -) +azure_audit_info = Azure_Audit_Info(credentials=None, identity=Azure_Identity_Info()) diff --git a/providers/azure/lib/audit_info/models.py b/providers/azure/lib/audit_info/models.py index 425e819b..87a89e2d 100644 --- a/providers/azure/lib/audit_info/models.py +++ b/providers/azure/lib/audit_info/models.py @@ -5,20 +5,19 @@ from pydantic import BaseModel class Azure_Identity_Info(BaseModel): - id: str = None - app_id: str = None - tenant_id: str = None + identity_id: str = None + identity_type: str = None + tenant_ids: list[str] = [] + domain: str = None + subscriptions: dict = {} @dataclass class Azure_Audit_Info: credentials: DefaultAzureCredential identity: Azure_Identity_Info - subscriptions: dict - audited_account: str - def __init__(self, credentials, identity, subscriptions): + def __init__(self, credentials, identity): self.credentials = credentials self.identity = identity - self.subscriptions = subscriptions - self.audited_account = None + self.is_azure = True diff --git a/providers/azure/services/defender/defender_service.py b/providers/azure/services/defender/defender_service.py index ffc2f704..ec822f39 100644 --- a/providers/azure/services/defender/defender_service.py +++ b/providers/azure/services/defender/defender_service.py @@ -11,9 +11,9 @@ class Defender: def __init__(self, audit_info): self.service = "defender" self.credentials = audit_info.credentials - self.subscriptions = audit_info.subscriptions + self.subscriptions = audit_info.identity.subscriptions self.clients = self.__set_clients__( - audit_info.subscriptions, audit_info.credentials + audit_info.identity.subscriptions, audit_info.credentials ) self.pricings = self.__get_pricings__() self.region = "azure" diff --git a/providers/azure/services/iam/iam_service.py b/providers/azure/services/iam/iam_service.py index 7b252f4d..42640e45 100644 --- a/providers/azure/services/iam/iam_service.py +++ b/providers/azure/services/iam/iam_service.py @@ -11,9 +11,9 @@ class IAM: def __init__(self, audit_info): self.service = "iam" self.credentials = audit_info.credentials - self.subscriptions = audit_info.subscriptions + self.subscriptions = audit_info.identity.subscriptions self.clients = self.__set_clients__( - audit_info.subscriptions, audit_info.credentials + audit_info.identity.subscriptions, audit_info.credentials ) self.roles = self.__get_roles__() self.region = "azure" diff --git a/providers/azure/services/storage/storage_service.py b/providers/azure/services/storage/storage_service.py index 89998e15..d6ea71a6 100644 --- a/providers/azure/services/storage/storage_service.py +++ b/providers/azure/services/storage/storage_service.py @@ -11,9 +11,9 @@ class Storage: def __init__(self, audit_info): self.service = "storage" self.credentials = audit_info.credentials - self.subscriptions = audit_info.subscriptions + self.subscriptions = audit_info.identity.subscriptions self.clients = self.__set_clients__( - audit_info.subscriptions, audit_info.credentials + audit_info.identity.subscriptions, audit_info.credentials ) self.storage_accounts = self.__get_storage_accounts__() self.region = "azure" diff --git a/prowler b/prowler index a1a43df3..b4f9b4d4 100755 --- a/prowler +++ b/prowler @@ -233,6 +233,28 @@ if __name__ == "__main__": default=[], help="Azure subscription ids to be scanned by prowler", ) + az_auth = parser.add_mutually_exclusive_group() + az_auth.add_argument( + "--az-cli-auth", + action="store_true", + help="Use Azure cli credentials to log in against azure", + ) + az_auth.add_argument( + "--sp-env-auth", + action="store_true", + help="Use service principal env variables authentication to log in against azure", + ) + az_auth.add_argument( + "--browser-auth", + action="store_true", + help="Use browser authentication to log in against azure ", + ) + az_auth.add_argument( + "--managed-identity-auth", + action="store_true", + help="Use managed identity authentication to log in against azure ", + ) + # Parse Arguments args = parser.parse_args() @@ -251,6 +273,21 @@ if __name__ == "__main__": # Azure options subscriptions = args.subscription_ids + az_cli_auth = args.az_cli_auth + sp_env_auth = args.sp_env_auth + browser_auth = args.browser_auth + managed_entity_auth = args.managed_identity_auth + if provider == "azure": + if ( + not az_cli_auth + and not sp_env_auth + and not browser_auth + and not managed_entity_auth + ): + logger.critical( + "If you are using Azure provider you need to set one of the following options: --az-cli-auth, --sp-env-auth, --browser-auth, --managed-identity-auth" + ) + sys.exit() # We treat the compliance framework as another output format if compliance_framework: @@ -370,13 +407,21 @@ if __name__ == "__main__": args.organizations_role, ) elif provider == "azure": - audit_info = azure_provider_set_session(subscriptions) + audit_info = azure_provider_set_session( + subscriptions, az_cli_auth, sp_env_auth, browser_auth, managed_entity_auth + ) # Check if custom output filename was input, if not, set the default if not output_filename: - output_filename = ( - f"prowler-output-{audit_info.audited_account}-{output_file_timestamp}" - ) + if provider == "aws": + output_filename = ( + f"prowler-output-{audit_info.audited_account}-{output_file_timestamp}" + ) + elif provider == "azure": + if audit_info.identity.domain: + output_filename = f"prowler-output-{audit_info.identity.domain}-{output_file_timestamp}" + else: + output_filename = f"prowler-output-{'-'.join(audit_info.identity.tenant_ids)}-{output_file_timestamp}" # Parse content from Allowlist file and get it, if necessary, from S3 if args.allowlist_file: