mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 20:42:02 +00:00
chore(m365powershell): manage encryption from plaintext password (#7784)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
committed by
GitHub
parent
99164ce93e
commit
36aaec8a55
@@ -2,6 +2,13 @@
|
||||
|
||||
All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
## [v1.9.0] (Prowler UNRELEASED)
|
||||
|
||||
### Changed
|
||||
- Renamed field encrypted_password to password for M365 provider [(#7784)](https://github.com/prowler-cloud/prowler/pull/7784)
|
||||
|
||||
---
|
||||
|
||||
## [v1.8.1] (Prowler v5.7.1)
|
||||
|
||||
### Fixed
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ name = "prowler-api"
|
||||
package-mode = false
|
||||
# Needed for the SDK compatibility
|
||||
requires-python = ">=3.11,<3.13"
|
||||
version = "1.8.1"
|
||||
version = "1.9.0"
|
||||
|
||||
[project.scripts]
|
||||
celery = "src.backend.config.settings.celery"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Prowler API
|
||||
version: 1.8.1
|
||||
version: 1.9.0
|
||||
description: |-
|
||||
Prowler API specification.
|
||||
|
||||
@@ -9240,7 +9240,7 @@ components:
|
||||
user:
|
||||
type: email
|
||||
description: User microsoft email address.
|
||||
encrypted_password:
|
||||
password:
|
||||
type: string
|
||||
description: User encrypted password.
|
||||
required:
|
||||
@@ -9248,7 +9248,7 @@ components:
|
||||
- client_secret
|
||||
- tenant_id
|
||||
- user
|
||||
- encrypted_password
|
||||
- password
|
||||
- type: object
|
||||
title: GCP Static Credentials
|
||||
properties:
|
||||
@@ -10485,7 +10485,7 @@ components:
|
||||
user:
|
||||
type: email
|
||||
description: User microsoft email address.
|
||||
encrypted_password:
|
||||
password:
|
||||
type: string
|
||||
description: User encrypted password.
|
||||
required:
|
||||
@@ -10493,7 +10493,7 @@ components:
|
||||
- client_secret
|
||||
- tenant_id
|
||||
- user
|
||||
- encrypted_password
|
||||
- password
|
||||
- type: object
|
||||
title: GCP Static Credentials
|
||||
properties:
|
||||
@@ -10694,7 +10694,7 @@ components:
|
||||
user:
|
||||
type: email
|
||||
description: User microsoft email address.
|
||||
encrypted_password:
|
||||
password:
|
||||
type: string
|
||||
description: User encrypted password.
|
||||
required:
|
||||
@@ -10702,7 +10702,7 @@ components:
|
||||
- client_secret
|
||||
- tenant_id
|
||||
- user
|
||||
- encrypted_password
|
||||
- password
|
||||
- type: object
|
||||
title: GCP Static Credentials
|
||||
properties:
|
||||
@@ -10919,7 +10919,7 @@ components:
|
||||
user:
|
||||
type: email
|
||||
description: User microsoft email address.
|
||||
encrypted_password:
|
||||
password:
|
||||
type: string
|
||||
description: User encrypted password.
|
||||
required:
|
||||
@@ -10927,7 +10927,7 @@ components:
|
||||
- client_secret
|
||||
- tenant_id
|
||||
- user
|
||||
- encrypted_password
|
||||
- password
|
||||
- type: object
|
||||
title: GCP Static Credentials
|
||||
properties:
|
||||
|
||||
@@ -119,7 +119,7 @@ from rest_framework_json_api import serializers
|
||||
"type": "email",
|
||||
"description": "User microsoft email address.",
|
||||
},
|
||||
"encrypted_password": {
|
||||
"password": {
|
||||
"type": "string",
|
||||
"description": "User encrypted password.",
|
||||
},
|
||||
@@ -129,7 +129,7 @@ from rest_framework_json_api import serializers
|
||||
"client_secret",
|
||||
"tenant_id",
|
||||
"user",
|
||||
"encrypted_password",
|
||||
"password",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1197,7 +1197,7 @@ class M365ProviderSecret(serializers.Serializer):
|
||||
client_secret = serializers.CharField()
|
||||
tenant_id = serializers.CharField()
|
||||
user = serializers.EmailField()
|
||||
encrypted_password = serializers.CharField()
|
||||
password = serializers.CharField()
|
||||
|
||||
class Meta:
|
||||
resource_name = "provider-secrets"
|
||||
|
||||
@@ -260,7 +260,7 @@ class SchemaView(SpectacularAPIView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
spectacular_settings.TITLE = "Prowler API"
|
||||
spectacular_settings.VERSION = "1.8.1"
|
||||
spectacular_settings.VERSION = "1.9.0"
|
||||
spectacular_settings.DESCRIPTION = (
|
||||
"Prowler API specification.\n\nThis file is auto-generated."
|
||||
)
|
||||
|
||||
@@ -153,14 +153,14 @@ With this credentials you will only be able to run the checks that work through
|
||||
|
||||
Authentication flag: `--env-auth`
|
||||
|
||||
This authentication method follows the same approach as the service principal method but introduces two additional environment variables for user credentials: `M365_USER` and `M365_ENCRYPTED_PASSWORD`.
|
||||
This authentication method follows the same approach as the service principal method but introduces two additional environment variables for user credentials: `M365_USER` and `M365_PASSWORD`.
|
||||
|
||||
```console
|
||||
export AZURE_CLIENT_ID="XXXXXXXXX"
|
||||
export AZURE_CLIENT_SECRET="XXXXXXXXX"
|
||||
export AZURE_TENANT_ID="XXXXXXXXX"
|
||||
export M365_USER="your_email@example.com"
|
||||
export M365_ENCRYPTED_PASSWORD="6500780061006d0070006c006500700061007300730077006f0072006400" # replace this to yours
|
||||
export M365_PASSWORD="6500780061006d0070006c006500700061007300730077006f0072006400" # replace this to yours
|
||||
```
|
||||
|
||||
These two new environment variables are **required** to execute the PowerShell modules needed to retrieve information from M365 services. Prowler uses Service Principal authentication to access Microsoft Graph and user credentials to authenticate to Microsoft PowerShell modules.
|
||||
@@ -173,7 +173,7 @@ These two new environment variables are **required** to execute the PowerShell m
|
||||
|
||||

|
||||
|
||||
- `M365_ENCRYPTED_PASSWORD` must be an encrypted SecureString. To convert your password into a valid encrypted string, you need to use PowerShell.
|
||||
- `M365_PASSWORD` must be an encrypted SecureString. To convert your password into a valid encrypted string, you need to use PowerShell.
|
||||
|
||||
???+ warning
|
||||
Passwords encrypted using ConvertTo-SecureString can only be decrypted on the same OS/user context. If you generate an encrypted password on macOS or Linux (both UNIX), it should fail on Windows and vice versa. As Prowler Cloud runs on UNIX if you generate your password using Windows it won't work so you'll need to generate a new password using any UNIX distro (example above)
|
||||
@@ -191,7 +191,7 @@ These two new environment variables are **required** to execute the PowerShell m
|
||||
6500780061006d0070006c006500700061007300730077006f0072006400
|
||||
```
|
||||
|
||||
If everything is done correctly, you will see the encrypted string that you need to set as the `M365_ENCRYPTED_PASSWORD` environment variable.
|
||||
If everything is done correctly, you will see the encrypted string that you need to set as the `M365_PASSWORD` environment variable.
|
||||
|
||||
=== "Windows"
|
||||
|
||||
@@ -222,7 +222,7 @@ These two new environment variables are **required** to execute the PowerShell m
|
||||
6500780061006d0070006c006500700061007300730077006f0072006400
|
||||
```
|
||||
|
||||
If everything is done correctly, you will see the encrypted string that you need to set as the `M365_ENCRYPTED_PASSWORD` environment variable.
|
||||
If everything is done correctly, you will see the encrypted string that you need to set as the `M365_PASSWORD` environment variable.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ Follow these steps to assign the role:
|
||||
|
||||
### Get your encrypted password
|
||||
|
||||
For this step you will need to use PowerShell, here you will have to create your Encrypted Password based on the password of the User that you are going to use. For more information about how to generate this Password go [here](../../getting-started/requirements.md#service-principal-and-user-credentials-authentication-recommended) and follow the steps needed to obtain `M365_ENCRYPTED_PASSWORD`.
|
||||
For this step you will need to use PowerShell, here you will have to create your Encrypted Password based on the password of the User that you are going to use. For more information about how to generate this Password go [here](../../getting-started/requirements.md#service-principal-and-user-credentials-authentication-recommended) and follow the steps needed to obtain `M365_PASSWORD`.
|
||||
|
||||
---
|
||||
|
||||
@@ -192,7 +192,7 @@ For this step you will need to use PowerShell, here you will have to create your
|
||||
- `Tenant ID`
|
||||
- `AZURE_CLIENT_SECRET` from earlier
|
||||
- `M365_USER` your user using the default domain, more info [here](../../getting-started/requirements.md#service-principal-and-user-credentials-authentication-recommended)
|
||||
- `M365_ENCRYPTED_PASSWORD` generated before
|
||||
- `M365_PASSWORD` generated before
|
||||
|
||||

|
||||
|
||||
|
||||
Generated
+2
-26
@@ -2184,6 +2184,8 @@ python-versions = "*"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c"},
|
||||
{file = "jsonpath_ng-1.7.0-py2-none-any.whl", hash = "sha256:898c93fc173f0c336784a3fa63d7434297544b7198124a68f9a3ef9597b0ae6e"},
|
||||
{file = "jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -3969,32 +3971,6 @@ cffi = ">=1.4.1"
|
||||
docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"]
|
||||
tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pynacl"
|
||||
version = "1.5.0"
|
||||
description = "Python binding to the Networking and Cryptography (NaCl) library"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"},
|
||||
{file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"},
|
||||
{file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"},
|
||||
{file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"},
|
||||
{file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"},
|
||||
{file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"},
|
||||
{file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"},
|
||||
{file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"},
|
||||
{file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"},
|
||||
{file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cffi = ">=1.4.1"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"]
|
||||
tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.2.3"
|
||||
|
||||
@@ -51,6 +51,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- Update and upgrade CIS for all the providers [(#7738)](https://github.com/prowler-cloud/prowler/pull/7738)
|
||||
- Cover policies with conditions with SNS endpoint in `sns_topics_not_publicly_accessible`. [(#7750)](https://github.com/prowler-cloud/prowler/pull/7750)
|
||||
- Change severity logic for `ec2_securitygroup_allow_ingress_from_internet_to_all_ports` check. [(#7764)](https://github.com/prowler-cloud/prowler/pull/7764)
|
||||
- Automatically encrypt password in Microsoft365 provider. [(#7784)](https://github.com/prowler-cloud/prowler/pull/7784)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import platform
|
||||
|
||||
import msal
|
||||
|
||||
@@ -64,17 +65,59 @@ class M365PowerShell(PowerShellSession):
|
||||
The credentials are sanitized to prevent command injection and
|
||||
stored securely in the PowerShell session.
|
||||
"""
|
||||
|
||||
credentials.encrypted_passwd = self.encrypt_password(credentials.passwd)
|
||||
|
||||
# Sanitize user and password
|
||||
user = self.sanitize(credentials.user)
|
||||
passwd = self.sanitize(credentials.passwd)
|
||||
sanitized_user = self.sanitize(credentials.user)
|
||||
sanitized_encrypted_passwd = self.sanitize(credentials.encrypted_passwd)
|
||||
|
||||
# Securely convert encrypted password to SecureString
|
||||
self.execute(f'$user = "{user}"')
|
||||
self.execute(f'$secureString = "{passwd}" | ConvertTo-SecureString')
|
||||
self.execute(f'$user = "{sanitized_user}"')
|
||||
self.execute(
|
||||
f'$secureString = "{sanitized_encrypted_passwd}" | ConvertTo-SecureString'
|
||||
)
|
||||
self.execute(
|
||||
"$credential = New-Object System.Management.Automation.PSCredential ($user, $secureString)"
|
||||
)
|
||||
|
||||
def encrypt_password(self, password: str) -> str:
|
||||
"""
|
||||
Encrypts a password using Windows CryptProtectData on Windows systems
|
||||
or UTF-16LE encoding on other systems.
|
||||
|
||||
Args:
|
||||
password (str): The password to encrypt
|
||||
|
||||
Returns:
|
||||
str: The encrypted password in hexadecimal format
|
||||
|
||||
Raises:
|
||||
ValueError: If password is None or empty
|
||||
"""
|
||||
try:
|
||||
if platform.system() == "Windows":
|
||||
import win32crypt
|
||||
|
||||
encrypted_blob = win32crypt.CryptProtectData(
|
||||
password.encode("utf-16le"), None, None, None, None, 0
|
||||
)
|
||||
|
||||
encrypted_bytes = encrypted_blob
|
||||
if isinstance(encrypted_blob, tuple):
|
||||
encrypted_bytes = encrypted_blob[1]
|
||||
elif hasattr(encrypted_blob, "data"):
|
||||
encrypted_bytes = encrypted_blob.data
|
||||
|
||||
return encrypted_bytes.hex()
|
||||
|
||||
else:
|
||||
return password.encode("utf-16le").hex()
|
||||
except Exception as error:
|
||||
raise Exception(
|
||||
f"[{os.path.basename(__file__)}] Error encrypting password: {str(error)}"
|
||||
)
|
||||
|
||||
def test_credentials(self, credentials: M365Credentials) -> bool:
|
||||
"""
|
||||
Test Microsoft 365 credentials by attempting to authenticate against Entra ID.
|
||||
@@ -87,14 +130,11 @@ class M365PowerShell(PowerShellSession):
|
||||
bool: True if credentials are valid and authentication succeeds, False otherwise.
|
||||
"""
|
||||
self.execute(
|
||||
f'$securePassword = "{self.sanitize(credentials.passwd)}" | ConvertTo-SecureString'
|
||||
f'$securePassword = "{credentials.encrypted_passwd}" | ConvertTo-SecureString' # encrypted password already sanitized
|
||||
)
|
||||
self.execute(
|
||||
f'$credential = New-Object System.Management.Automation.PSCredential("{self.sanitize(credentials.user)}", $securePassword)'
|
||||
)
|
||||
decrypted_password = self.execute(
|
||||
'Write-Output "$($credential.GetNetworkCredential().Password)"'
|
||||
)
|
||||
|
||||
# Validate user belongs to tenant
|
||||
user_domain = credentials.user.split("@")[1]
|
||||
@@ -116,7 +156,7 @@ class M365PowerShell(PowerShellSession):
|
||||
# Validate credentials
|
||||
result = app.acquire_token_by_username_password(
|
||||
username=credentials.user,
|
||||
password=decrypted_password, # Needs to be in plain text
|
||||
password=credentials.passwd,
|
||||
scopes=["https://graph.microsoft.com/.default"],
|
||||
)
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ class M365Provider(Provider):
|
||||
client_id: str = None,
|
||||
client_secret: str = None,
|
||||
user: str = None,
|
||||
encrypted_password: str = None,
|
||||
password: str = None,
|
||||
init_modules: bool = False,
|
||||
region: str = "M365Global",
|
||||
config_content: dict = None,
|
||||
@@ -164,7 +164,7 @@ class M365Provider(Provider):
|
||||
client_id,
|
||||
client_secret,
|
||||
user,
|
||||
encrypted_password,
|
||||
password,
|
||||
)
|
||||
|
||||
logger.info("Checking if region is different than default one")
|
||||
@@ -172,13 +172,13 @@ class M365Provider(Provider):
|
||||
|
||||
# Get the dict from the static credentials
|
||||
m365_credentials = None
|
||||
if tenant_id and client_id and client_secret and user and encrypted_password:
|
||||
if tenant_id and client_id and client_secret and user and password:
|
||||
m365_credentials = self.validate_static_credentials(
|
||||
tenant_id=tenant_id,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
user=user,
|
||||
encrypted_password=encrypted_password,
|
||||
password=password,
|
||||
)
|
||||
|
||||
# Set up the M365 session
|
||||
@@ -282,7 +282,7 @@ class M365Provider(Provider):
|
||||
client_id: str,
|
||||
client_secret: str,
|
||||
user: str,
|
||||
encrypted_password: str,
|
||||
password: str,
|
||||
):
|
||||
"""
|
||||
Validates the authentication arguments for the M365 provider.
|
||||
@@ -324,10 +324,10 @@ class M365Provider(Provider):
|
||||
message="M365 Tenant ID (--tenant-id) is required for browser authentication mode",
|
||||
)
|
||||
elif env_auth:
|
||||
if not user or not encrypted_password or not tenant_id:
|
||||
if not user or not password or not tenant_id:
|
||||
raise M365MissingEnvironmentCredentialsError(
|
||||
file=os.path.basename(__file__),
|
||||
message="M365 provider requires AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID, M365_USER and M365_ENCRYPTED_PASSWORD environment variables to be set when using --env-auth",
|
||||
message="M365 provider requires AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID, M365_USER and M365_PASSWORD environment variables to be set when using --env-auth",
|
||||
)
|
||||
else:
|
||||
if not tenant_id:
|
||||
@@ -396,7 +396,7 @@ class M365Provider(Provider):
|
||||
if m365_credentials:
|
||||
credentials = M365Credentials(
|
||||
user=m365_credentials.get("user", ""),
|
||||
passwd=m365_credentials.get("encrypted_password", ""),
|
||||
passwd=m365_credentials.get("password", ""),
|
||||
client_id=m365_credentials.get("client_id", ""),
|
||||
client_secret=m365_credentials.get("client_secret", ""),
|
||||
tenant_id=m365_credentials.get("tenant_id", ""),
|
||||
@@ -404,18 +404,18 @@ class M365Provider(Provider):
|
||||
)
|
||||
elif env_auth:
|
||||
m365_user = getenv("M365_USER")
|
||||
m365_password = getenv("M365_ENCRYPTED_PASSWORD")
|
||||
m365_password = getenv("M365_PASSWORD")
|
||||
client_id = getenv("AZURE_CLIENT_ID")
|
||||
client_secret = getenv("AZURE_CLIENT_SECRET")
|
||||
tenant_id = getenv("AZURE_TENANT_ID")
|
||||
|
||||
if not m365_user or not m365_password:
|
||||
logger.critical(
|
||||
"M365 provider: Missing M365_USER or M365_ENCRYPTED_PASSWORD environment variables needed for credentials authentication"
|
||||
"M365 provider: Missing M365_USER or M365_PASSWORD environment variables needed for credentials authentication"
|
||||
)
|
||||
raise M365MissingEnvironmentCredentialsError(
|
||||
file=os.path.basename(__file__),
|
||||
message="Missing M365_USER or M365_ENCRYPTED_PASSWORD environment variables required for credentials authentication.",
|
||||
message="Missing M365_USER or M365_PASSWORD environment variables required for credentials authentication.",
|
||||
)
|
||||
credentials = M365Credentials(
|
||||
client_id=client_id,
|
||||
@@ -494,7 +494,7 @@ class M365Provider(Provider):
|
||||
- client_id: The M365 client ID.
|
||||
- client_secret: The M365 client secret
|
||||
- user: The M365 user email
|
||||
- encrypted_password: The M365 encrypted password
|
||||
- password: The M365 encrypted password
|
||||
- provider_id: The M365 provider ID (in this case the Tenant ID).
|
||||
region_config (M365RegionConfig): The region configuration object.
|
||||
|
||||
@@ -621,7 +621,7 @@ class M365Provider(Provider):
|
||||
client_id: str = None,
|
||||
client_secret: str = None,
|
||||
user: str = None,
|
||||
encrypted_password: str = None,
|
||||
password: str = None,
|
||||
provider_id: str = None,
|
||||
) -> Connection:
|
||||
"""Test connection to M365 tenant and PowerShell modules.
|
||||
@@ -640,7 +640,7 @@ class M365Provider(Provider):
|
||||
client_id (str): The M365 client ID.
|
||||
client_secret (str): The M365 client secret.
|
||||
user (str): The M365 user email.
|
||||
encrypted_password (str): The M365 encrypted_password.
|
||||
password (str): The M365 password.
|
||||
provider_id (str): The M365 provider ID (in this case the Tenant ID).
|
||||
|
||||
|
||||
@@ -674,20 +674,20 @@ class M365Provider(Provider):
|
||||
client_id,
|
||||
client_secret,
|
||||
user,
|
||||
encrypted_password,
|
||||
password,
|
||||
)
|
||||
region_config = M365Provider.setup_region_config(region)
|
||||
|
||||
# Get the dict from the static credentials
|
||||
m365_credentials = None
|
||||
if tenant_id and client_id and client_secret:
|
||||
if not user and not encrypted_password:
|
||||
if not user and not password:
|
||||
m365_credentials = M365Provider.validate_static_credentials(
|
||||
tenant_id=tenant_id,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
user="user",
|
||||
encrypted_password="encrypted_password",
|
||||
password="password",
|
||||
)
|
||||
else:
|
||||
m365_credentials = M365Provider.validate_static_credentials(
|
||||
@@ -695,7 +695,7 @@ class M365Provider(Provider):
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
user=user,
|
||||
encrypted_password=encrypted_password,
|
||||
password=password,
|
||||
)
|
||||
|
||||
# Set up the M365 session
|
||||
@@ -733,7 +733,7 @@ class M365Provider(Provider):
|
||||
)
|
||||
|
||||
# Set up PowerShell credentials
|
||||
if user and encrypted_password:
|
||||
if user and password:
|
||||
M365Provider.setup_powershell(
|
||||
env_auth,
|
||||
m365_credentials,
|
||||
@@ -975,7 +975,7 @@ class M365Provider(Provider):
|
||||
client_id: str = None,
|
||||
client_secret: str = None,
|
||||
user: str = None,
|
||||
encrypted_password: str = None,
|
||||
password: str = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Validates the static credentials for the M365 provider.
|
||||
@@ -985,7 +985,7 @@ class M365Provider(Provider):
|
||||
client_id (str): The M365 client ID.
|
||||
client_secret (str): The M365 client secret.
|
||||
user (str): The M365 user email.
|
||||
encrypted_password (str): The M365 encrypted password.
|
||||
password (str): The M365 encrypted password.
|
||||
|
||||
Raises:
|
||||
M365NotValidTenantIdError: If the provided M365 Tenant ID is not valid.
|
||||
@@ -1031,7 +1031,7 @@ class M365Provider(Provider):
|
||||
)
|
||||
|
||||
# Validate the Encrypted Password
|
||||
if not encrypted_password:
|
||||
if not password:
|
||||
raise M365NotValidEncryptedPasswordError(
|
||||
file=os.path.basename(__file__),
|
||||
message="The provided Encrypted Password is not valid.",
|
||||
@@ -1044,7 +1044,7 @@ class M365Provider(Provider):
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"user": user,
|
||||
"encrypted_password": encrypted_password,
|
||||
"password": password,
|
||||
}
|
||||
except M365NotValidTenantIdError as tenant_id_error:
|
||||
logger.error(
|
||||
|
||||
@@ -22,12 +22,13 @@ class M365RegionConfig(BaseModel):
|
||||
|
||||
|
||||
class M365Credentials(BaseModel):
|
||||
user: str = ""
|
||||
passwd: str = ""
|
||||
encrypted_passwd: str = ""
|
||||
client_id: str = ""
|
||||
client_secret: str = ""
|
||||
tenant_id: str = ""
|
||||
tenant_domains: list[str] = []
|
||||
user: str = ""
|
||||
passwd: str = ""
|
||||
|
||||
|
||||
class M365OutputOptions(ProviderOutputOptions):
|
||||
|
||||
@@ -75,6 +75,7 @@ class Testm365PowerShell:
|
||||
credentials = M365Credentials(
|
||||
user="test@example.com",
|
||||
passwd="test_password",
|
||||
encrypted_passwd="test_password",
|
||||
client_id="test_client_id",
|
||||
client_secret="test_client_secret",
|
||||
tenant_id="test_tenant_id",
|
||||
@@ -89,13 +90,19 @@ class Testm365PowerShell:
|
||||
)
|
||||
session = M365PowerShell(credentials, identity)
|
||||
|
||||
# Mock encrypt_password to return a known value
|
||||
session.encrypt_password = MagicMock(return_value="encrypted_password")
|
||||
session.execute = MagicMock()
|
||||
|
||||
session.init_credential(credentials)
|
||||
|
||||
# Verify encrypt_password was called
|
||||
session.encrypt_password.assert_called_once_with(credentials.passwd)
|
||||
|
||||
# Verify execute was called with the correct commands
|
||||
session.execute.assert_any_call(f'$user = "{credentials.user}"')
|
||||
session.execute.assert_any_call(
|
||||
f'$secureString = "{credentials.passwd}" | ConvertTo-SecureString'
|
||||
f'$secureString = "{credentials.encrypted_passwd}" | ConvertTo-SecureString'
|
||||
)
|
||||
session.execute.assert_any_call(
|
||||
"$credential = New-Object System.Management.Automation.PSCredential ($user, $secureString)"
|
||||
@@ -116,6 +123,7 @@ class Testm365PowerShell:
|
||||
credentials = M365Credentials(
|
||||
user="test@contoso.onmicrosoft.com",
|
||||
passwd="test_password",
|
||||
encrypted_passwd="test_encrypted_password",
|
||||
client_id="test_client_id",
|
||||
client_secret="test_client_secret",
|
||||
tenant_id="test_tenant_id",
|
||||
@@ -130,11 +138,9 @@ class Testm365PowerShell:
|
||||
)
|
||||
session = M365PowerShell(credentials, identity)
|
||||
|
||||
# Mock read_output to return the decrypted password
|
||||
session.read_output = MagicMock(return_value="decrypted_password")
|
||||
|
||||
# Mock execute to return the result of read_output
|
||||
session.execute = MagicMock(side_effect=lambda _: session.read_output())
|
||||
# Mock encrypt_password to return a known value
|
||||
session.encrypt_password = MagicMock(return_value="encrypted_password")
|
||||
session.execute = MagicMock()
|
||||
|
||||
# Execute the test
|
||||
result = session.test_credentials(credentials)
|
||||
@@ -142,14 +148,11 @@ class Testm365PowerShell:
|
||||
|
||||
# Verify execute was called with the correct commands
|
||||
session.execute.assert_any_call(
|
||||
f'$securePassword = "{credentials.passwd}" | ConvertTo-SecureString'
|
||||
f'$securePassword = "{credentials.encrypted_passwd}" | ConvertTo-SecureString'
|
||||
)
|
||||
session.execute.assert_any_call(
|
||||
f'$credential = New-Object System.Management.Automation.PSCredential("{session.sanitize(credentials.user)}", $securePassword)'
|
||||
)
|
||||
session.execute.assert_any_call(
|
||||
'Write-Output "$($credential.GetNetworkCredential().Password)"'
|
||||
)
|
||||
|
||||
# Verify MSAL was called with the correct parameters
|
||||
mock_msal.assert_called_once_with(
|
||||
@@ -159,7 +162,7 @@ class Testm365PowerShell:
|
||||
)
|
||||
mock_msal_instance.acquire_token_by_username_password.assert_called_once_with(
|
||||
username="test@contoso.onmicrosoft.com",
|
||||
password="decrypted_password",
|
||||
password="test_password", # Original password, not encrypted
|
||||
scopes=["https://graph.microsoft.com/.default"],
|
||||
)
|
||||
session.close()
|
||||
@@ -229,6 +232,7 @@ class Testm365PowerShell:
|
||||
credentials = M365Credentials(
|
||||
user="test@contoso.onmicrosoft.com",
|
||||
passwd="test_password",
|
||||
encrypted_passwd="test_encrypted_password",
|
||||
client_id="test_client_id",
|
||||
client_secret="test_client_secret",
|
||||
tenant_id="test_tenant_id",
|
||||
@@ -262,7 +266,7 @@ class Testm365PowerShell:
|
||||
)
|
||||
mock_msal_instance.acquire_token_by_username_password.assert_called_once_with(
|
||||
username="test@contoso.onmicrosoft.com",
|
||||
password="decrypted_password",
|
||||
password="test_password",
|
||||
scopes=["https://graph.microsoft.com/.default"],
|
||||
)
|
||||
|
||||
@@ -561,3 +565,55 @@ class Testm365PowerShell:
|
||||
)
|
||||
# Verify no info messages were logged
|
||||
mock_info.assert_not_called()
|
||||
|
||||
@patch("subprocess.Popen")
|
||||
def test_encrypt_password(self, mock_popen):
|
||||
credentials = M365Credentials(user="test@example.com", passwd="test_password")
|
||||
identity = M365IdentityInfo(
|
||||
identity_id="test_id",
|
||||
identity_type="User",
|
||||
tenant_id="test_tenant",
|
||||
tenant_domain="example.com",
|
||||
tenant_domains=["example.com"],
|
||||
location="test_location",
|
||||
)
|
||||
session = M365PowerShell(credentials, identity)
|
||||
|
||||
# Test non-Windows system (should use utf-16le hex encoding)
|
||||
from unittest import mock
|
||||
|
||||
with mock.patch("platform.system", return_value="Linux"):
|
||||
result = session.encrypt_password("password123")
|
||||
expected = "password123".encode("utf-16le").hex()
|
||||
assert result == expected
|
||||
|
||||
# Test Windows system with tuple return
|
||||
with mock.patch("platform.system", return_value="Windows"):
|
||||
import sys
|
||||
|
||||
win32crypt_mock = mock.MagicMock()
|
||||
win32crypt_mock.CryptProtectData.return_value = (None, b"encrypted_bytes")
|
||||
sys.modules["win32crypt"] = win32crypt_mock
|
||||
|
||||
result = session.encrypt_password("password123")
|
||||
assert result == b"encrypted_bytes".hex()
|
||||
|
||||
# Clean up mock
|
||||
del sys.modules["win32crypt"]
|
||||
|
||||
# Test error handling
|
||||
with mock.patch("platform.system", return_value="Windows"):
|
||||
import sys
|
||||
|
||||
win32crypt_mock = mock.MagicMock()
|
||||
win32crypt_mock.CryptProtectData.side_effect = Exception("Test error")
|
||||
sys.modules["win32crypt"] = win32crypt_mock
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
session.encrypt_password("password123")
|
||||
assert "Error encrypting password: Test error" in str(exc_info.value)
|
||||
|
||||
# Clean up mock
|
||||
del sys.modules["win32crypt"]
|
||||
|
||||
session.close()
|
||||
|
||||
@@ -365,7 +365,7 @@ class TestM365Provider:
|
||||
assert test_connection.is_connected
|
||||
assert test_connection.error is None
|
||||
|
||||
def test_test_connection_tenant_id_client_id_client_secret_no_user_encrypted_password(
|
||||
def test_test_connection_tenant_id_client_id_client_secret_no_user_password(
|
||||
self,
|
||||
):
|
||||
with patch(
|
||||
@@ -384,13 +384,13 @@ class TestM365Provider:
|
||||
client_id=str(uuid4()),
|
||||
client_secret=str(uuid4()),
|
||||
user=None,
|
||||
encrypted_password="test_password",
|
||||
password="test_password",
|
||||
)
|
||||
|
||||
assert exception.type == M365NotValidUserError
|
||||
assert "The provided M365 User is not valid." in str(exception.value)
|
||||
|
||||
def test_test_connection_tenant_id_client_id_client_secret_user_no_encrypted_password(
|
||||
def test_test_connection_tenant_id_client_id_client_secret_user_no_password(
|
||||
self,
|
||||
):
|
||||
with patch(
|
||||
@@ -411,7 +411,7 @@ class TestM365Provider:
|
||||
client_id=str(uuid4()),
|
||||
client_secret=str(uuid4()),
|
||||
user="test@example.com",
|
||||
encrypted_password=None,
|
||||
password=None,
|
||||
)
|
||||
|
||||
assert exception.type == M365NotValidEncryptedPasswordError
|
||||
@@ -467,7 +467,7 @@ class TestM365Provider:
|
||||
def test_setup_powershell_valid_credentials(self):
|
||||
credentials_dict = {
|
||||
"user": "test@example.com",
|
||||
"encrypted_password": "test_password",
|
||||
"password": "test_password",
|
||||
"client_id": "test_client_id",
|
||||
"tenant_id": "test_tenant_id",
|
||||
"client_secret": "test_client_secret",
|
||||
@@ -492,7 +492,7 @@ class TestM365Provider:
|
||||
),
|
||||
)
|
||||
assert result.user == credentials_dict["user"]
|
||||
assert result.passwd == credentials_dict["encrypted_password"]
|
||||
assert result.passwd == credentials_dict["password"]
|
||||
|
||||
def test_setup_powershell_invalid_env_credentials(self):
|
||||
credentials = None
|
||||
@@ -510,7 +510,7 @@ class TestM365Provider:
|
||||
)
|
||||
|
||||
assert (
|
||||
"Missing M365_USER or M365_ENCRYPTED_PASSWORD environment variables required for credentials authentication"
|
||||
"Missing M365_USER or M365_PASSWORD environment variables required for credentials authentication"
|
||||
in str(exc_info.value)
|
||||
)
|
||||
mock_session.test_credentials.assert_not_called()
|
||||
@@ -534,7 +534,7 @@ class TestM365Provider:
|
||||
client_id=str(uuid4()),
|
||||
client_secret=str(uuid4()),
|
||||
user="user@otherdomain.com",
|
||||
encrypted_password="test_password",
|
||||
password="test_password",
|
||||
)
|
||||
|
||||
assert exception.type == M365UserNotBelongingToTenantError
|
||||
@@ -550,7 +550,7 @@ class TestM365Provider:
|
||||
client_id="12345678-1234-5678-1234-567812345678",
|
||||
client_secret="test_secret",
|
||||
user="test@example.com",
|
||||
encrypted_password="test_password",
|
||||
password="test_password",
|
||||
)
|
||||
assert "The provided Tenant ID is not valid." in str(exception.value)
|
||||
|
||||
@@ -561,7 +561,7 @@ class TestM365Provider:
|
||||
client_id="",
|
||||
client_secret="test_secret",
|
||||
user="test@example.com",
|
||||
encrypted_password="test_password",
|
||||
password="test_password",
|
||||
)
|
||||
assert "The provided Client ID is not valid." in str(exception.value)
|
||||
|
||||
@@ -572,7 +572,7 @@ class TestM365Provider:
|
||||
client_id="12345678-1234-5678-1234-567812345678",
|
||||
client_secret="",
|
||||
user="test@example.com",
|
||||
encrypted_password="test_password",
|
||||
password="test_password",
|
||||
)
|
||||
assert "The provided Client Secret is not valid." in str(exception.value)
|
||||
|
||||
@@ -583,18 +583,18 @@ class TestM365Provider:
|
||||
client_id="12345678-1234-5678-1234-567812345678",
|
||||
client_secret="test_secret",
|
||||
user="",
|
||||
encrypted_password="test_password",
|
||||
password="test_password",
|
||||
)
|
||||
assert "The provided User is not valid." in str(exception.value)
|
||||
|
||||
def test_validate_static_credentials_missing_encrypted_password(self):
|
||||
def test_validate_static_credentials_missing_password(self):
|
||||
with pytest.raises(M365NotValidEncryptedPasswordError) as exception:
|
||||
M365Provider.validate_static_credentials(
|
||||
tenant_id="12345678-1234-5678-1234-567812345678",
|
||||
client_id="12345678-1234-5678-1234-567812345678",
|
||||
client_secret="test_secret",
|
||||
user="test@example.com",
|
||||
encrypted_password="",
|
||||
password="",
|
||||
)
|
||||
assert "The provided Encrypted Password is not valid." in str(exception.value)
|
||||
|
||||
@@ -609,11 +609,11 @@ class TestM365Provider:
|
||||
client_id="test_client_id",
|
||||
client_secret="test_secret",
|
||||
user=None,
|
||||
encrypted_password=None,
|
||||
password=None,
|
||||
)
|
||||
|
||||
assert (
|
||||
"M365 provider requires AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID, M365_USER and M365_ENCRYPTED_PASSWORD environment variables to be set when using --env-auth"
|
||||
"M365 provider requires AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID, M365_USER and M365_PASSWORD environment variables to be set when using --env-auth"
|
||||
in str(exception.value)
|
||||
)
|
||||
|
||||
@@ -655,7 +655,7 @@ class TestM365Provider:
|
||||
client_id=str(uuid4()),
|
||||
client_secret=str(uuid4()),
|
||||
user=f"user@{user_domain}",
|
||||
encrypted_password="test_password",
|
||||
password="test_password",
|
||||
provider_id=provider_id,
|
||||
)
|
||||
|
||||
@@ -669,7 +669,7 @@ class TestM365Provider:
|
||||
"""Test that initialize_m365_powershell_modules is not called when init_modules is False"""
|
||||
credentials_dict = {
|
||||
"user": "test@example.com",
|
||||
"encrypted_password": "test_password",
|
||||
"password": "test_password",
|
||||
"client_id": "test_client_id",
|
||||
"tenant_id": "test_tenant_id",
|
||||
"client_secret": "test_client_secret",
|
||||
@@ -703,7 +703,7 @@ class TestM365Provider:
|
||||
"""Test that initialize_m365_powershell_modules is called when init_modules is True"""
|
||||
credentials_dict = {
|
||||
"user": "test@example.com",
|
||||
"encrypted_password": "test_password",
|
||||
"password": "test_password",
|
||||
"client_id": "test_client_id",
|
||||
"tenant_id": "test_tenant_id",
|
||||
"client_secret": "test_client_secret",
|
||||
@@ -737,7 +737,7 @@ class TestM365Provider:
|
||||
"""Test that setup_powershell handles initialization failures correctly"""
|
||||
credentials_dict = {
|
||||
"user": "test@example.com",
|
||||
"encrypted_password": "test_password",
|
||||
"password": "test_password",
|
||||
"client_id": "test_client_id",
|
||||
"tenant_id": "test_tenant_id",
|
||||
"client_secret": "test_client_secret",
|
||||
@@ -808,7 +808,7 @@ class TestM365Provider:
|
||||
client_id=str(uuid4()),
|
||||
client_secret=str(uuid4()),
|
||||
user="user@contoso.onmicrosoft.com",
|
||||
encrypted_password="test_password",
|
||||
password="test_password",
|
||||
provider_id=provider_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ export const addCredentialsProvider = async (formData: FormData) => {
|
||||
client_secret: formData.get("client_secret"),
|
||||
tenant_id: formData.get("tenant_id"),
|
||||
user: formData.get("user"),
|
||||
encrypted_password: formData.get("encrypted_password"),
|
||||
password: formData.get("password"),
|
||||
};
|
||||
} else if (providerType === "gcp") {
|
||||
// Static credentials configuration for GCP
|
||||
@@ -298,7 +298,7 @@ export const updateCredentialsProvider = async (
|
||||
client_secret: formData.get("client_secret"),
|
||||
tenant_id: formData.get("tenant_id"),
|
||||
user: formData.get("user"),
|
||||
encrypted_password: formData.get("encrypted_password"),
|
||||
password: formData.get("password"),
|
||||
};
|
||||
} else if (providerType === "gcp") {
|
||||
// Static credentials configuration for GCP
|
||||
|
||||
@@ -86,7 +86,7 @@ export const UpdateViaCredentialsForm = ({
|
||||
client_secret: "",
|
||||
tenant_id: "",
|
||||
user: "",
|
||||
encrypted_password: "",
|
||||
password: "",
|
||||
}
|
||||
: providerType === "gcp"
|
||||
? {
|
||||
@@ -153,8 +153,8 @@ export const UpdateViaCredentialsForm = ({
|
||||
message: errorMessage,
|
||||
});
|
||||
break;
|
||||
case "/data/attributes/secret/encrypted_password":
|
||||
form.setError("encrypted_password", {
|
||||
case "/data/attributes/secret/password":
|
||||
form.setError("password", {
|
||||
type: "server",
|
||||
message: errorMessage,
|
||||
});
|
||||
|
||||
@@ -85,7 +85,7 @@ export const ViaCredentialsForm = ({
|
||||
client_secret: "",
|
||||
tenant_id: "",
|
||||
user: "",
|
||||
encrypted_password: "",
|
||||
password: "",
|
||||
}
|
||||
: providerType === "gcp"
|
||||
? {
|
||||
@@ -152,8 +152,8 @@ export const ViaCredentialsForm = ({
|
||||
message: errorMessage,
|
||||
});
|
||||
break;
|
||||
case "/data/attributes/secret/encrypted_password":
|
||||
form.setError("encrypted_password", {
|
||||
case "/data/attributes/secret/password":
|
||||
form.setError("password", {
|
||||
type: "server",
|
||||
message: errorMessage,
|
||||
});
|
||||
|
||||
@@ -64,14 +64,14 @@ export const M365CredentialsForm = ({
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="encrypted_password"
|
||||
name="password"
|
||||
type="password"
|
||||
label="Encrypted Password"
|
||||
label="Password"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Password"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.encrypted_password}
|
||||
isInvalid={!!control._formState.errors.password}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -224,7 +224,7 @@ export type M365Credentials = {
|
||||
client_secret: string;
|
||||
tenant_id: string;
|
||||
user: string;
|
||||
encrypted_password: string;
|
||||
password: string;
|
||||
secretName: string;
|
||||
providerId: string;
|
||||
};
|
||||
|
||||
@@ -142,7 +142,7 @@ export const addCredentialsFormSchema = (providerType: string) =>
|
||||
.nonempty("Client Secret is required"),
|
||||
tenant_id: z.string().nonempty("Tenant ID is required"),
|
||||
user: z.string().nonempty("User is required"),
|
||||
encrypted_password: z
|
||||
password: z
|
||||
.string()
|
||||
.nonempty("Encrypted Password is required"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user