chore(m365powershell): manage encryption from plaintext password (#7784)

Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
Hugo Pereira Brito
2025-05-22 17:36:58 +02:00
committed by GitHub
parent 99164ce93e
commit 36aaec8a55
21 changed files with 208 additions and 127 deletions
+7
View File
@@ -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
View File
@@ -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"
+9 -9
View File
@@ -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",
],
},
{
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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."
)
+5 -5
View File
@@ -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
![User Domains](../tutorials/microsoft365/img/user-domains.png)
- `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
![Prowler Cloud M365 Credentials](./img/m365-credentials.png)
Generated
+2 -26
View File
@@ -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"
+1
View File
@@ -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"],
)
+23 -23
View File
@@ -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(
+3 -2
View File
@@ -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()
+21 -21
View File
@@ -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,
)
+2 -2
View File
@@ -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}
/>
</>
);
+1 -1
View File
@@ -224,7 +224,7 @@ export type M365Credentials = {
client_secret: string;
tenant_id: string;
user: string;
encrypted_password: string;
password: string;
secretName: string;
providerId: string;
};
+1 -1
View File
@@ -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"),
}