feat(Azure): Include multiple authentication (#1528)

This commit is contained in:
Nacho Rivera
2022-12-02 09:20:56 +01:00
committed by GitHub
parent c2f5177afa
commit 5e40d93d63
9 changed files with 281 additions and 122 deletions

38
Pipfile.lock generated
View File

@@ -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": {

View File

@@ -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:

View File

@@ -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

View File

@@ -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())

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

53
prowler
View File

@@ -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: