feat(azure regions): support non default azure region (#3013)

Co-authored-by: Pepe Fagoaga <pepe@verica.io>
This commit is contained in:
Nacho Rivera
2023-11-14 13:17:48 +01:00
committed by GitHub
parent 573f1eba56
commit f8e713a544
17 changed files with 284 additions and 15 deletions

View File

@@ -0,0 +1,16 @@
# Use non default Azure regions
Microsoft provides clouds for compliance with regional laws, which are available for your use.
By default, Prowler uses `AzureCloud` cloud which is the comercial one. (you can list all the available with `az cloud list --output table`).
At the time of writing this documentation the available Azure Clouds from different regions are the following:
- AzureCloud
- AzureChinaCloud
- AzureUSGovernment
- AzureGermanCloud
If you want to change the default one you must include the flag `--azure-region`, i.e.:
```console
prowler azure --az-cli-auth --azure-region AzureChinaCloud
```

View File

@@ -56,6 +56,7 @@ nav:
- Boto3 Configuration: tutorials/aws/boto3-configuration.md
- Azure:
- Authentication: tutorials/azure/authentication.md
- Non default clouds: tutorials/azure/use-non-default-cloud.md
- Subscriptions: tutorials/azure/subscriptions.md
- Google Cloud:
- Authentication: tutorials/gcp/authentication.md

35
poetry.lock generated
View File

@@ -11,6 +11,23 @@ files = [
{file = "about_time-4.2.1-py3-none-any.whl", hash = "sha256:8bbf4c75fe13cbd3d72f49a03b02c5c7dca32169b6d49117c257e7eb3eaee341"},
]
[[package]]
name = "adal"
version = "1.2.7"
description = "Note: This library is already replaced by MSAL Python, available here: https://pypi.org/project/msal/ .ADAL Python remains available here as a legacy. The ADAL for Python library makes it easy for python application to authenticate to Azure Active Directory (AAD) in order to access AAD protected web resources."
optional = false
python-versions = "*"
files = [
{file = "adal-1.2.7-py2.py3-none-any.whl", hash = "sha256:2a7451ed7441ddbc57703042204a3e30ef747478eea022c70f789fc7f084bc3d"},
{file = "adal-1.2.7.tar.gz", hash = "sha256:d74f45b81317454d96e982fd1c50e6fb5c99ac2223728aea8764433a39f566f1"},
]
[package.dependencies]
cryptography = ">=1.1.0"
PyJWT = ">=1.0.0,<3"
python-dateutil = ">=2.1.0,<3"
requests = ">=2.0.0,<3"
[[package]]
name = "alive-progress"
version = "3.1.5"
@@ -1537,6 +1554,22 @@ requests-oauthlib = ">=0.5.0"
[package.extras]
async = ["aiodns", "aiohttp (>=3.0)"]
[[package]]
name = "msrestazure"
version = "0.6.4"
description = "AutoRest swagger generator Python client runtime. Azure-specific module."
optional = false
python-versions = "*"
files = [
{file = "msrestazure-0.6.4-py2.py3-none-any.whl", hash = "sha256:3de50f56147ef529b31e099a982496690468ecef33f0544cb0fa0cfe1e1de5b9"},
{file = "msrestazure-0.6.4.tar.gz", hash = "sha256:a06f0dabc9a6f5efe3b6add4bd8fb623aeadacf816b7a35b0f89107e0544d189"},
]
[package.dependencies]
adal = ">=0.6.0,<2.0.0"
msrest = ">=0.6.0,<2.0.0"
six = "*"
[[package]]
name = "mypy-extensions"
version = "1.0.0"
@@ -2889,4 +2922,4 @@ docs = ["mkdocs", "mkdocs-material"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
content-hash = "18f9e44d555478c5d70cec7f39fc18263c54567a14ee0b100faf14f4d0cb8946"
content-hash = "594dc3dc4952b294042203c3338b6959fed04eb6eb181796a4ae8c27cde5bf32"

View File

@@ -7,6 +7,7 @@ from msgraph.core import GraphClient
from prowler.lib.logger import logger
from prowler.providers.azure.lib.audit_info.models import Azure_Identity_Info
from prowler.providers.azure.lib.regions.regions import get_regions_config
class Azure_Provider:
@@ -18,12 +19,14 @@ class Azure_Provider:
managed_entity_auth: bool,
subscription_ids: list,
tenant_id: str,
region: str,
):
logger.info("Instantiating Azure Provider ...")
self.credentials = self.__set_credentials__(
self.region_config = self.__get_region_config__(region)
self.credentials = self.__get_credentials__(
az_cli_auth, sp_env_auth, browser_auth, managed_entity_auth, tenant_id
)
self.identity = self.__set_identity_info__(
self.identity = self.__get_identity_info__(
self.credentials,
az_cli_auth,
sp_env_auth,
@@ -32,7 +35,10 @@ class Azure_Provider:
subscription_ids,
)
def __set_credentials__(
def __get_region_config__(self, region):
return get_regions_config(region)
def __get_credentials__(
self, az_cli_auth, sp_env_auth, browser_auth, managed_entity_auth, tenant_id
):
# Browser auth creds cannot be set with DefaultAzureCredentials()
@@ -52,6 +58,8 @@ class Azure_Provider:
exclude_shared_token_cache_credential=True,
# Azure Auth using PowerShell is not supported
exclude_powershell_credential=True,
# set Authority of a Microsoft Entra endpoint
authority=self.region_config["authority"],
)
except Exception as error:
logger.critical("Failed to retrieve azure credentials")
@@ -61,7 +69,6 @@ class Azure_Provider:
sys.exit(1)
else:
try:
print(tenant_id)
credentials = InteractiveBrowserCredential(tenant_id=tenant_id)
except Exception as error:
logger.critical("Failed to retrieve azure credentials")
@@ -83,7 +90,7 @@ class Azure_Provider:
)
sys.exit(1)
def __set_identity_info__(
def __get_identity_info__(
self,
credentials,
az_cli_auth,
@@ -153,7 +160,11 @@ class Azure_Provider:
logger.info(
"Trying to subscriptions and tenant ids to populate identity structure ..."
)
subscriptions_client = SubscriptionClient(credential=credentials)
subscriptions_client = SubscriptionClient(
credential=credentials,
base_url=self.region_config["base_url"],
credential_scopes=self.region_config["credential_scopes"],
)
if not subscription_ids:
logger.info("Scanning all the Azure subscriptions...")
for subscription in subscriptions_client.subscriptions.list():
@@ -195,3 +206,6 @@ class Azure_Provider:
def get_identity(self):
return self.identity
def get_region_config(self):
return self.region_config

View File

@@ -1,3 +1,6 @@
from argparse import ArgumentTypeError
def init_parser(self):
"""Init the Azure Provider CLI parser"""
azure_parser = self.subparsers.add_parser(
@@ -40,3 +43,27 @@ def init_parser(self):
default=None,
help="Azure Tenant ID to be used with --browser-auth option",
)
# Regions
azure_regions_subparser = azure_parser.add_argument_group("Regions")
azure_regions_subparser.add_argument(
"--azure-region",
nargs="?",
default="AzureCloud",
type=validate_azure_region,
help="Azure region from `az cloud list --output table`, by default AzureCloud",
)
def validate_azure_region(region):
"""validate_azure_region validates if the region passed as argument is valid"""
regions_allowed = [
"AzureChinaCloud",
"AzureUSGovernment",
"AzureGermanCloud",
"AzureCloud",
]
if region not in regions_allowed:
raise ArgumentTypeError(
f"Region {region} not allowed, allowed regions are {' '.join(regions_allowed)}"
)
return region

View File

@@ -1,6 +1,7 @@
from prowler.providers.azure.lib.audit_info.models import (
Azure_Audit_Info,
Azure_Identity_Info,
Azure_Region_Config,
)
azure_audit_info = Azure_Audit_Info(
@@ -9,4 +10,5 @@ azure_audit_info = Azure_Audit_Info(
audit_resources=None,
audit_metadata=None,
audit_config=None,
azure_region_config=Azure_Region_Config(),
)

View File

@@ -13,6 +13,13 @@ class Azure_Identity_Info(BaseModel):
subscriptions: dict = {}
class Azure_Region_Config(BaseModel):
name: str = ""
authority: str = None
base_url: str = ""
credential_scopes: list = []
@dataclass
class Azure_Audit_Info:
credentials: DefaultAzureCredential
@@ -20,12 +27,20 @@ class Azure_Audit_Info:
audit_resources: Optional[Any]
audit_metadata: Optional[Any]
audit_config: dict
azure_region_config: Azure_Region_Config
def __init__(
self, credentials, identity, audit_metadata, audit_resources, audit_config
self,
credentials,
identity,
audit_metadata,
audit_resources,
audit_config,
azure_region_config,
):
self.credentials = credentials
self.identity = identity
self.audit_metadata = audit_metadata
self.audit_resources = audit_resources
self.audit_config = audit_config
self.azure_region_config = azure_region_config

View File

@@ -0,0 +1,38 @@
from azure.identity import AzureAuthorityHosts
from msrestazure.azure_cloud import (
AZURE_CHINA_CLOUD,
AZURE_GERMAN_CLOUD,
AZURE_US_GOV_CLOUD,
)
def get_regions_config(region):
allowed_regions = {
"AzureCloud": {
"authority": None,
"base_url": "https://management.azure.com",
"credential_scopes": ["https://management.azure.com/.default"],
},
"AzureChinaCloud": {
"authority": AzureAuthorityHosts.AZURE_CHINA,
"base_url": AZURE_CHINA_CLOUD.endpoints.resource_manager,
"credential_scopes": [
AZURE_CHINA_CLOUD.endpoints.resource_manager + "/.default"
],
},
"AzureUSGovernment": {
"authority": AzureAuthorityHosts.AZURE_GOVERNMENT,
"base_url": AZURE_US_GOV_CLOUD.endpoints.resource_manager,
"credential_scopes": [
AZURE_US_GOV_CLOUD.endpoints.resource_manager + "/.default"
],
},
"AzureGermanCloud": {
"authority": AzureAuthorityHosts.AZURE_GERMANY,
"base_url": AZURE_GERMAN_CLOUD.endpoints.resource_manager,
"credential_scopes": [
AZURE_GERMAN_CLOUD.endpoints.resource_manager + "/.default"
],
},
}
return allowed_regions[region]

View File

@@ -9,17 +9,27 @@ class AzureService:
audit_info: Azure_Audit_Info,
):
self.clients = self.__set_clients__(
audit_info.identity.subscriptions, audit_info.credentials, service
audit_info.identity.subscriptions,
audit_info.credentials,
service,
audit_info.azure_region_config,
)
self.subscriptions = audit_info.identity.subscriptions
def __set_clients__(self, subscriptions, credentials, service):
def __set_clients__(self, subscriptions, credentials, service, region_config):
clients = {}
try:
for display_name, id in subscriptions.items():
clients.update(
{display_name: service(credential=credentials, subscription_id=id)}
{
display_name: service(
credential=credentials,
subscription_id=id,
base_url=region_config.base_url,
credential_scopes=region_config.credential_scopes,
)
}
)
except Exception as error:
logger.error(

View File

@@ -26,7 +26,10 @@ from prowler.providers.aws.lib.resource_api_tagging.resource_api_tagging import
)
from prowler.providers.azure.azure_provider import Azure_Provider
from prowler.providers.azure.lib.audit_info.audit_info import azure_audit_info
from prowler.providers.azure.lib.audit_info.models import Azure_Audit_Info
from prowler.providers.azure.lib.audit_info.models import (
Azure_Audit_Info,
Azure_Region_Config,
)
from prowler.providers.gcp.gcp_provider import GCP_Provider
from prowler.providers.gcp.lib.audit_info.audit_info import gcp_audit_info
from prowler.providers.gcp.lib.audit_info.models import GCP_Audit_Info
@@ -63,7 +66,7 @@ GCP Account: {Fore.YELLOW}[{profile}]{Style.RESET_ALL} GCP Project IDs: {Fore.Y
report = f"""
This report is being generated using the identity below:
Azure Tenant IDs: {Fore.YELLOW}[{" ".join(audit_info.identity.tenant_ids)}]{Style.RESET_ALL} Azure Tenant Domain: {Fore.YELLOW}[{audit_info.identity.domain}]{Style.RESET_ALL}
Azure Tenant IDs: {Fore.YELLOW}[{" ".join(audit_info.identity.tenant_ids)}]{Style.RESET_ALL} Azure Tenant Domain: {Fore.YELLOW}[{audit_info.identity.domain}]{Style.RESET_ALL} Azure Region: {Fore.YELLOW}[{audit_info.azure_region_config.name}]{Style.RESET_ALL}
Azure Subscriptions: {Fore.YELLOW}{printed_subscriptions}{Style.RESET_ALL}
Azure Identity Type: {Fore.YELLOW}[{audit_info.identity.identity_type}]{Style.RESET_ALL} Azure Identity ID: {Fore.YELLOW}[{audit_info.identity.identity_id}]{Style.RESET_ALL}
"""
@@ -282,6 +285,10 @@ Azure Identity Type: {Fore.YELLOW}[{audit_info.identity.identity_type}]{Style.RE
browser_auth = arguments.get("browser_auth")
managed_entity_auth = arguments.get("managed_entity_auth")
tenant_id = arguments.get("tenant_id")
logger.info("Checking if region is different than default one")
region = arguments.get("azure_region")
if (
not az_cli_auth
and not sp_env_auth
@@ -303,9 +310,17 @@ Azure Identity Type: {Fore.YELLOW}[{audit_info.identity.identity_type}]{Style.RE
managed_entity_auth,
subscription_ids,
tenant_id,
region,
)
azure_audit_info.credentials = azure_provider.get_credentials()
azure_audit_info.identity = azure_provider.get_identity()
region_config = azure_provider.get_region_config()
azure_audit_info.azure_region_config = Azure_Region_Config(
name=region,
authority=region_config["authority"],
base_url=region_config["base_url"],
credential_scopes=region_config["credential_scopes"],
)
if not arguments.get("only_logs"):
self.print_azure_credentials(azure_audit_info)

View File

@@ -43,6 +43,7 @@ google-auth-httplib2 = "^0.1.0"
mkdocs = {version = "1.5.3", optional = true}
mkdocs-material = {version = "9.4.8", optional = true}
msgraph-core = "0.2.2"
msrestazure = "^0.6.4"
pydantic = "1.10.13"
python = "^3.9"
schema = "0.7.5"

View File

@@ -1,9 +1,11 @@
import uuid
from argparse import ArgumentTypeError
import pytest
from mock import patch
from prowler.lib.cli.parser import ProwlerArgumentParser
from prowler.providers.azure.lib.arguments.arguments import validate_azure_region
prowler_command = "prowler"
@@ -1050,6 +1052,14 @@ class Test_Parser:
assert parsed.subscription_ids[0] == subscription_1
assert parsed.subscription_ids[1] == subscription_2
def test_parser_azure_region(self):
argument = "--azure-region"
region = "AzureChinaCloud"
command = [prowler_command, "azure", argument, region]
parsed = self.parser.parse(command)
assert parsed.provider == "azure"
assert parsed.azure_region == region
# Test AWS flags with Azure provider
def test_parser_azure_with_aws_flag(self, capsys):
command = [prowler_command, "azure", "-p"]
@@ -1092,3 +1102,33 @@ class Test_Parser:
assert len(parsed.project_ids) == 2
assert parsed.project_ids[0] == project_1
assert parsed.project_ids[1] == project_2
def test_validate_azure_region_valid_regions(self):
expected_regions = [
"AzureChinaCloud",
"AzureUSGovernment",
"AzureGermanCloud",
"AzureCloud",
]
input_regions = [
"AzureChinaCloud",
"AzureUSGovernment",
"AzureGermanCloud",
"AzureCloud",
]
for region in input_regions:
assert validate_azure_region(region) in expected_regions
def test_validate_azure_region_invalid_regions(self):
expected_regions = [
"AzureChinaCloud",
"AzureUSGovernment",
"AzureGermanCloud",
"AzureCloud",
]
invalid_region = "non-valid-region"
with pytest.raises(
ArgumentTypeError,
match=f"Region {invalid_region} not allowed, allowed regions are {' '.join(expected_regions)}",
):
validate_azure_region(invalid_region)

View File

@@ -11,6 +11,7 @@ from prowler.providers.aws.lib.audit_info.models import AWS_Audit_Info
from prowler.providers.azure.lib.audit_info.models import (
Azure_Audit_Info,
Azure_Identity_Info,
Azure_Region_Config,
)
from prowler.providers.common.models import Audit_Metadata
from prowler.providers.gcp.lib.audit_info.models import GCP_Audit_Info
@@ -76,6 +77,7 @@ class Test_Slack_Integration:
audit_resources=None,
audit_metadata=None,
audit_config=None,
azure_region_config=Azure_Region_Config(),
)
assert create_message_identity("aws", aws_audit_info) == (
f"AWS Account *{aws_audit_info.audited_account}*",

View File

@@ -0,0 +1,50 @@
from azure.identity import AzureAuthorityHosts
from msrestazure.azure_cloud import (
AZURE_CHINA_CLOUD,
AZURE_GERMAN_CLOUD,
AZURE_US_GOV_CLOUD,
)
from prowler.providers.azure.lib.regions.regions import get_regions_config
class Test_azure_regions:
def test_get_regions_config(self):
allowed_regions = [
"AzureCloud",
"AzureChinaCloud",
"AzureUSGovernment",
"AzureGermanCloud",
]
expected_output = {
"AzureCloud": {
"authority": None,
"base_url": "https://management.azure.com",
"credential_scopes": ["https://management.azure.com/.default"],
},
"AzureChinaCloud": {
"authority": AzureAuthorityHosts.AZURE_CHINA,
"base_url": AZURE_CHINA_CLOUD.endpoints.resource_manager,
"credential_scopes": [
AZURE_CHINA_CLOUD.endpoints.resource_manager + "/.default"
],
},
"AzureUSGovernment": {
"authority": AzureAuthorityHosts.AZURE_GOVERNMENT,
"base_url": AZURE_US_GOV_CLOUD.endpoints.resource_manager,
"credential_scopes": [
AZURE_US_GOV_CLOUD.endpoints.resource_manager + "/.default"
],
},
"AzureGermanCloud": {
"authority": AzureAuthorityHosts.AZURE_GERMANY,
"base_url": AZURE_GERMAN_CLOUD.endpoints.resource_manager,
"credential_scopes": [
AZURE_GERMAN_CLOUD.endpoints.resource_manager + "/.default"
],
},
}
for region in allowed_regions:
region_config = get_regions_config(region)
assert region_config == expected_output[region]

View File

@@ -11,6 +11,7 @@ from prowler.providers.azure.azure_provider import Azure_Provider
from prowler.providers.azure.lib.audit_info.models import (
Azure_Audit_Info,
Azure_Identity_Info,
Azure_Region_Config,
)
from prowler.providers.common.audit_info import (
Audit_Info,
@@ -31,6 +32,7 @@ mock_azure_audit_info = Azure_Audit_Info(
audit_metadata=None,
audit_resources=None,
audit_config=None,
azure_region_config=Azure_Region_Config(),
)
mock_set_audit_info = Audit_Info()
@@ -132,8 +134,8 @@ class Test_Set_Audit_Info:
"prowler.providers.common.audit_info.azure_audit_info",
new=mock_azure_audit_info,
)
@patch.object(Azure_Provider, "__set_credentials__", new=mock_set_azure_credentials)
@patch.object(Azure_Provider, "__set_identity_info__", new=mock_set_identity_info)
@patch.object(Azure_Provider, "__get_credentials__", new=mock_set_azure_credentials)
@patch.object(Azure_Provider, "__get_identity_info__", new=mock_set_identity_info)
def test_set_audit_info_azure(self):
provider = "azure"
arguments = {
@@ -150,6 +152,7 @@ class Test_Set_Audit_Info:
"browser_auth": None,
"managed_entity_auth": None,
"config_file": default_config_file_path,
"azure_region": "AzureCloud",
}
audit_info = set_provider_audit_info(provider, arguments)

View File

@@ -9,6 +9,7 @@ from prowler.providers.aws.lib.audit_info.audit_info import AWS_Audit_Info
from prowler.providers.azure.lib.audit_info.audit_info import (
Azure_Audit_Info,
Azure_Identity_Info,
Azure_Region_Config,
)
from prowler.providers.common.models import Audit_Metadata
from prowler.providers.common.outputs import (
@@ -33,6 +34,7 @@ class Test_Common_Output_Options:
audit_metadata=None,
audit_resources=None,
audit_config=None,
azure_region_config=Azure_Region_Config(),
)
return audit_info