Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions prowler/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `zone_waf_enabled` check for Cloudflare provider now appends a plan-aware hint to the FAIL `status_extended`: a possible-false-positive note on paid plans (Pro, Business, Enterprise) where the legacy `waf` zone setting can read `off` even though WAF managed rulesets are deployed via the dashboard, and a "not available on the Cloudflare Free plan" note on Free zones [(#9896)](https://github.com/prowler-cloud/prowler/pull/9896)
- Google Workspace Gmail checks sharing a single resource row, causing the service field to be overwritten by the last check executed [(#11169)](https://github.com/prowler-cloud/prowler/pull/11169)
- Google Workspace Drive and Calendar services missing server-side policy filters [(#11195)](https://github.com/prowler-cloud/prowler/pull/11195)
- `logging_sink_created` GCP check no longer false-FAILs projects covered by an organisation-level aggregated sink with `includeChildren=True`; the service now also queries `organizations/{id}/sinks` and passes any project whose org has a matching aggregated sink [(#11355)](https://github.com/prowler-cloud/prowler/pull/11355)
- `entra_users_mfa_capable` and `entra_break_glass_account_fido2_security_key_registered` report a preventive FAIL per affected user (with the missing permission named) when the M365 service principal lacks `AuditLog.Read.All`, instead of mass false positives [(#10907)](https://github.com/prowler-cloud/prowler/pull/10907)
- Duplicated GCP CIS requirements IDs [(#11180)](https://github.com/prowler-cloud/prowler/pull/11180)
- `VercelSession.token` is now excluded from serialization and representation to prevent the Vercel API token from leaking through `.dict()`, `.json()` or logs [(#11198)](https://github.com/prowler-cloud/prowler/pull/11198)
Expand Down
98 changes: 93 additions & 5 deletions prowler/providers/azure/azure_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import requests
from azure.core.exceptions import ClientAuthenticationError, HttpResponseError
from azure.identity import (
ClientAssertionCredential,
ClientSecretCredential,
CredentialUnavailableError,
DefaultAzureCredential,
Expand Down Expand Up @@ -46,6 +47,7 @@
AzureNotValidClientIdError,
AzureNotValidClientSecretError,
AzureNotValidTenantIdError,
AzureOIDCTokenMissingError,
AzureSetUpIdentityError,
AzureSetUpRegionConfigError,
AzureSetUpSessionError,
Expand Down Expand Up @@ -112,6 +114,7 @@ def __init__(
sp_env_auth: bool = False,
browser_auth: bool = False,
managed_identity_auth: bool = False,
oidc_auth: bool = False,
tenant_id: str = None,
region: str = "AzureCloud",
subscription_ids: list = [],
Expand All @@ -131,6 +134,7 @@ def __init__(
sp_env_auth (bool): Flag indicating whether to use Service Principal environment authentication.
browser_auth (bool): Flag indicating whether to use interactive browser authentication.
managed_identity_auth (bool): Flag indicating whether to use managed identity authentication.
oidc_auth (bool): Flag indicating whether to use OIDC/Workload Identity Federation authentication.
tenant_id (str): The Azure Active Directory tenant ID.
region (str): The Azure region.
subscription_ids (list): List of subscription IDs.
Expand Down Expand Up @@ -229,6 +233,7 @@ def __init__(
sp_env_auth,
browser_auth,
managed_identity_auth,
oidc_auth,
tenant_id,
client_id,
client_secret,
Expand All @@ -253,6 +258,7 @@ def __init__(
sp_env_auth,
browser_auth,
managed_identity_auth,
oidc_auth,
tenant_id,
azure_credentials,
self._region_config,
Expand All @@ -264,6 +270,7 @@ def __init__(
sp_env_auth,
browser_auth,
managed_identity_auth,
oidc_auth,
subscription_ids,
client_id,
)
Expand Down Expand Up @@ -344,6 +351,7 @@ def validate_arguments(
sp_env_auth: bool,
browser_auth: bool,
managed_identity_auth: bool,
oidc_auth: bool,
tenant_id: str,
client_id: str,
client_secret: str,
Expand All @@ -356,16 +364,18 @@ def validate_arguments(
sp_env_auth (bool): Flag indicating whether Service Principal environment authentication is enabled.
browser_auth (bool): Flag indicating whether browser authentication is enabled.
managed_identity_auth (bool): Flag indicating whether managed identity authentication is enabled.
oidc_auth (bool): Flag indicating whether OIDC/Workload Identity Federation authentication is enabled.
tenant_id (str): The Azure Tenant ID.
client_id (str): The Azure Client ID.
client_secret (str): The Azure Client Secret.

Raises:
AzureBrowserAuthNoTenantIDError: If browser authentication is enabled but the tenant ID is not found.
AzureOIDCTokenMissingError: If OIDC authentication is enabled but required env vars are missing.
"""

if not client_id and not client_secret:
if not browser_auth and tenant_id:
if not browser_auth and tenant_id and not oidc_auth:
raise AzureTenantIDNoBrowserAuthError(
file=os.path.basename(__file__),
message="Azure Tenant ID (--tenant-id) is required for browser authentication mode",
Expand All @@ -375,10 +385,11 @@ def validate_arguments(
and not sp_env_auth
and not browser_auth
and not managed_identity_auth
and not oidc_auth
):
raise AzureNoAuthenticationMethodError(
file=os.path.basename(__file__),
message="Azure provider requires at least one authentication method set: [--az-cli-auth | --sp-env-auth | --browser-auth | --managed-identity-auth]",
message="Azure provider requires at least one authentication method set: [--az-cli-auth | --sp-env-auth | --browser-auth | --managed-identity-auth | --oidc-auth]",
)
elif browser_auth and not tenant_id:
raise AzureBrowserAuthNoTenantIDError(
Expand Down Expand Up @@ -469,6 +480,7 @@ def setup_session(
sp_env_auth: bool,
browser_auth: bool,
managed_identity_auth: bool,
oidc_auth: bool,
tenant_id: str,
azure_credentials: dict,
region_config: AzureRegionConfig,
Expand All @@ -482,6 +494,7 @@ def setup_session(
sp_env_auth (bool): Flag indicating whether to use Service Principal authentication with environment variables.
browser_auth (bool): Flag indicating whether to use interactive browser authentication.
managed_identity_auth (bool): Flag indicating whether to use managed identity authentication.
oidc_auth (bool): Flag indicating whether to use OIDC/Workload Identity Federation authentication.
tenant_id (str): The Azure Active Directory tenant ID.
azure_credentials (dict): The Azure configuration object. It contains the following keys:
- tenant_id: The Azure Active Directory tenant ID.
Expand All @@ -496,6 +509,38 @@ def setup_session(
Exception: If failed to retrieve Azure credentials.

"""
# OIDC / Workload Identity Federation auth
if oidc_auth:
try:
AzureProvider.check_oidc_creds_env_vars()
oidc_tenant_id = getenv("AZURE_TENANT_ID")
oidc_client_id = getenv("AZURE_CLIENT_ID")

def get_oidc_token():
"""Return the current OIDC JWT, preferring AZURE_FEDERATED_TOKEN."""
token = getenv("AZURE_FEDERATED_TOKEN") or getenv("AZURE_OIDC_TOKEN")
return token

credentials = ClientAssertionCredential(
tenant_id=oidc_tenant_id,
client_id=oidc_client_id,
func=get_oidc_token,
)
return credentials
except AzureOIDCTokenMissingError as oidc_error:
logger.critical(
f"{oidc_error.__class__.__name__}[{oidc_error.__traceback__.tb_lineno}] -- {oidc_error}"
)
raise oidc_error
except Exception as error:
logger.critical("Failed to retrieve azure credentials using OIDC authentication")
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
raise AzureSetUpSessionError(
file=os.path.basename(__file__), original_exception=error
)

# Browser auth creds cannot be set with DefaultAzureCredentials()
if not browser_auth:
if sp_env_auth:
Expand Down Expand Up @@ -603,12 +648,14 @@ def setup_session(

return credentials


@staticmethod
def test_connection(
az_cli_auth=False,
sp_env_auth=False,
browser_auth=False,
managed_identity_auth=False,
oidc_auth=False,
tenant_id=None,
region="AzureCloud",
raise_on_exception=True,
Expand All @@ -625,6 +672,7 @@ def test_connection(
sp_env_auth (bool): Flag indicating if Service Principal environment authentication is used.
browser_auth (bool): Flag indicating if browser authentication is used.
managed_identity_auth (bool): Flag indicating if managed entity authentication is used.
oidc_auth (bool): Flag indicating if OIDC/Workload Identity Federation authentication is used.
tenant_id (str): The Azure Active Directory tenant ID.
region (str): The Azure region.
raise_on_exception (bool): Flag indicating whether to raise an exception if the connection fails.
Expand Down Expand Up @@ -659,6 +707,7 @@ def test_connection(
sp_env_auth,
browser_auth,
managed_identity_auth,
oidc_auth,
tenant_id,
client_id,
client_secret,
Expand All @@ -681,6 +730,7 @@ def test_connection(
sp_env_auth,
browser_auth,
managed_identity_auth,
oidc_auth,
tenant_id,
azure_credentials,
region_config,
Expand Down Expand Up @@ -872,12 +922,48 @@ def check_service_principal_creds_env_vars():
message=f"Missing environment variable {env_var} required to authenticate.",
)

@staticmethod
def check_oidc_creds_env_vars():
"""
Checks the presence of required environment variables for OIDC/Workload Identity Federation
authentication against Azure.

This method checks for the presence of the following environment variables:
- AZURE_CLIENT_ID: Azure client ID
- AZURE_TENANT_ID: Azure tenant ID
- AZURE_FEDERATED_TOKEN or AZURE_OIDC_TOKEN: OIDC JWT token

If any of the required environment variables is missing, it logs a critical error and raises
an AzureOIDCTokenMissingError.
"""
logger.info(
"Azure provider: checking OIDC/Workload Identity Federation environment variables ..."
)
for env_var in ["AZURE_CLIENT_ID", "AZURE_TENANT_ID"]:
if not getenv(env_var):
logger.critical(
f"Azure provider: Missing environment variable {env_var} needed for OIDC authentication"
)
raise AzureOIDCTokenMissingError(
file=os.path.basename(__file__),
message=f"Missing environment variable {env_var} required for OIDC authentication.",
)
if not getenv("AZURE_FEDERATED_TOKEN") and not getenv("AZURE_OIDC_TOKEN"):
logger.critical(
"Azure provider: Missing OIDC token. Set AZURE_FEDERATED_TOKEN or AZURE_OIDC_TOKEN."
)
raise AzureOIDCTokenMissingError(
file=os.path.basename(__file__),
message="Missing OIDC token. Set AZURE_FEDERATED_TOKEN or AZURE_OIDC_TOKEN environment variable.",
)

def setup_identity(
self,
az_cli_auth,
sp_env_auth,
browser_auth,
managed_identity_auth,
oidc_auth,
subscription_ids,
client_id,
):
Expand All @@ -889,7 +975,9 @@ def setup_identity(
sp_env_auth (bool): Flag indicating if Service Principal environment authentication is used.
browser_auth (bool): Flag indicating if browser authentication is used.
managed_identity_auth (bool): Flag indicating if managed entity authentication is used.
oidc_auth (bool): Flag indicating if OIDC/Workload Identity Federation authentication is used.
subscription_ids (list): List of subscription IDs.
client_id (str): The Azure client ID (for static credentials).

Returns:
AzureIdentityInfo: An instance of AzureIdentityInfo containing the identity information.
Expand All @@ -902,7 +990,7 @@ def setup_identity(
# 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 or client_id:
if sp_env_auth or browser_auth or az_cli_auth or client_id or oidc_auth:

async def get_azure_identity():
# Trying to recover tenant domain info
Expand Down Expand Up @@ -939,10 +1027,10 @@ async def get_azure_identity():
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 or client_id:
if sp_env_auth or client_id or oidc_auth:
# The id of the sp can be retrieved from environment variables
identity.identity_id = getenv("AZURE_CLIENT_ID", default=client_id)
identity.identity_type = "Service Principal"
identity.identity_type = "Service Principal" if not oidc_auth else "Service Principal (OIDC)"
# 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:
Expand Down
12 changes: 12 additions & 0 deletions prowler/providers/azure/exceptions/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ class AzureBaseException(ProwlerException):
"message": "The provided provider_id does not match with the available subscriptions",
"remediation": "Check the provider_id and ensure it is a valid subscription for the given credentials.",
},
(2024, "AzureOIDCTokenMissingError"): {
"message": "Azure OIDC token missing",
"remediation": "Set the AZURE_FEDERATED_TOKEN or AZURE_OIDC_TOKEN environment variable with a valid OIDC JWT token. "
"Also ensure AZURE_CLIENT_ID and AZURE_TENANT_ID are set.",
},
}

def __init__(self, code, file=None, original_exception=None, message=None):
Expand Down Expand Up @@ -291,3 +296,10 @@ def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
2023, file=file, original_exception=original_exception, message=message
)


class AzureOIDCTokenMissingError(AzureCredentialsError):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
2024, file=file, original_exception=original_exception, message=message
)
6 changes: 6 additions & 0 deletions prowler/providers/azure/lib/arguments/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ def init_parser(self):
action="store_true",
help="Use managed identity authentication to log in against Azure ",
)
azure_auth_modes_group.add_argument(
"--oidc-auth",
action="store_true",
help="Use OIDC/Workload Identity Federation authentication to log in against Azure. "
"Requires AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_FEDERATED_TOKEN (or AZURE_OIDC_TOKEN) environment variables.",
)
# Subscriptions
azure_subscriptions_subparser = azure_parser.add_argument_group("Subscriptions")
azure_subscriptions_subparser.add_argument(
Expand Down
34 changes: 34 additions & 0 deletions prowler/providers/gcp/services/logging/logging_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def __init__(self, provider: GcpProvider):
self.sinks = []
self.metrics = []
self._get_sinks()
self._get_org_sinks()
self._get_metrics()

def _get_sinks(self):
Expand Down Expand Up @@ -39,6 +40,38 @@ def _get_sinks(self):
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)

def _get_org_sinks(self):
"""Fetch org-level sinks with includeChildren so child projects are not falsely failed."""
org_ids = set()
for project in self.projects.values():
if project.organization:
org_ids.add(project.organization.id)

for org_id in org_ids:
try:
request = self.client.sinks().list(parent=f"organizations/{org_id}")
while request is not None:
response = request.execute(num_retries=DEFAULT_RETRY_ATTEMPTS)

for sink in response.get("sinks", []):
self.sinks.append(
Sink(
name=sink["name"],
destination=sink["destination"],
filter=sink.get("filter", "all"),
project_id=f"organizations/{org_id}",
include_children=sink.get("includeChildren", False),
)
)

request = self.client.sinks().list_next(
previous_request=request, previous_response=response
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)

def _get_metrics(self):
for project_id in self.project_ids:
try:
Expand Down Expand Up @@ -76,6 +109,7 @@ class Sink(BaseModel):
destination: str
filter: str
project_id: str
include_children: bool = False


class Metric(BaseModel):
Expand Down
Loading