diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 88ab6c9c88..85a7024b13 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -7,6 +7,9 @@ All notable changes to the **Prowler API** are documented in this file. ### Added - Database backend to handle already closed connections [(#7935)](https://github.com/prowler-cloud/prowler/pull/7935). +### Changed +- Renamed field encrypted_password to password for M365 provider [(#7784)](https://github.com/prowler-cloud/prowler/pull/7784) + ### Fixed - Fixed transaction persistence with RLS operations [(#7916)](https://github.com/prowler-cloud/prowler/pull/7916). - Reverted the change `get_with_retry` to use the original `get` method for retrieving tasks [(#7932)](https://github.com/prowler-cloud/prowler/pull/7932). diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index 9fb640b130..87b2536f69 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -9241,7 +9241,7 @@ components: user: type: email description: User microsoft email address. - encrypted_password: + password: type: string description: User encrypted password. required: @@ -9249,7 +9249,7 @@ components: - client_secret - tenant_id - user - - encrypted_password + - password - type: object title: GCP Static Credentials properties: @@ -10486,7 +10486,7 @@ components: user: type: email description: User microsoft email address. - encrypted_password: + password: type: string description: User encrypted password. required: @@ -10494,7 +10494,7 @@ components: - client_secret - tenant_id - user - - encrypted_password + - password - type: object title: GCP Static Credentials properties: @@ -10695,7 +10695,7 @@ components: user: type: email description: User microsoft email address. - encrypted_password: + password: type: string description: User encrypted password. required: @@ -10703,7 +10703,7 @@ components: - client_secret - tenant_id - user - - encrypted_password + - password - type: object title: GCP Static Credentials properties: @@ -10920,7 +10920,7 @@ components: user: type: email description: User microsoft email address. - encrypted_password: + password: type: string description: User encrypted password. required: @@ -10928,7 +10928,7 @@ components: - client_secret - tenant_id - user - - encrypted_password + - password - type: object title: GCP Static Credentials properties: diff --git a/api/src/backend/api/v1/serializer_utils/providers.py b/api/src/backend/api/v1/serializer_utils/providers.py index 231e5dc1bb..759f5bf764 100644 --- a/api/src/backend/api/v1/serializer_utils/providers.py +++ b/api/src/backend/api/v1/serializer_utils/providers.py @@ -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", ], }, { diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index c0440f3ef0..4ba2154327 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -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" diff --git a/docs/getting-started/requirements.md b/docs/getting-started/requirements.md index ed995ce411..ee8ca59ef9 100644 --- a/docs/getting-started/requirements.md +++ b/docs/getting-started/requirements.md @@ -156,14 +156,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. @@ -176,7 +176,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) @@ -194,7 +194,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" @@ -225,7 +225,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. diff --git a/docs/tutorials/microsoft365/getting-started-m365.md b/docs/tutorials/microsoft365/getting-started-m365.md index 067b31a1dd..bf9d6b0664 100644 --- a/docs/tutorials/microsoft365/getting-started-m365.md +++ b/docs/tutorials/microsoft365/getting-started-m365.md @@ -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) diff --git a/poetry.lock b/poetry.lock index d5c66264e5..f4c25c6315 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index b82e9e12fd..214970a1ad 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -59,8 +59,6 @@ All notable changes to the **Prowler SDK** are documented in this file. - Cover policies with conditions with SNS endpoint in `sns_topics_not_publicly_accessible`. [(#7750)](https://github.com/prowler-cloud/prowler/pull/7750) - Fix `m365_powershell test_credentials` to use sanitized credentials. [(#7761)](https://github.com/prowler-cloud/prowler/pull/7761) - Change severity logic for `ec2_securitygroup_allow_ingress_from_internet_to_all_ports` check. [(#7764)](https://github.com/prowler-cloud/prowler/pull/7764) -### Fixed -- Fix `m365_powershell test_credentials` to use sanitized credentials. [(#7761)](https://github.com/prowler-cloud/prowler/pull/7761) - Fix `admincenter_users_admins_reduced_license_footprint` check logic to pass when admin user has no license. [(#7779)](https://github.com/prowler-cloud/prowler/pull/7779) - Fix `m365_powershell` to close the PowerShell sessions in msgraph services. [(#7816)](https://github.com/prowler-cloud/prowler/pull/7816) diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py index e6a296b57c..e46e41c7e4 100644 --- a/prowler/providers/m365/lib/powershell/m365_powershell.py +++ b/prowler/providers/m365/lib/powershell/m365_powershell.py @@ -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"], ) diff --git a/prowler/providers/m365/m365_provider.py b/prowler/providers/m365/m365_provider.py index a4135b0692..c2a85f5f48 100644 --- a/prowler/providers/m365/m365_provider.py +++ b/prowler/providers/m365/m365_provider.py @@ -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( diff --git a/prowler/providers/m365/models.py b/prowler/providers/m365/models.py index d0d73263b5..f77bc8ee4c 100644 --- a/prowler/providers/m365/models.py +++ b/prowler/providers/m365/models.py @@ -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): diff --git a/tests/providers/m365/lib/powershell/m365_powershell_test.py b/tests/providers/m365/lib/powershell/m365_powershell_test.py index 7bc3236433..739dd7e8ce 100644 --- a/tests/providers/m365/lib/powershell/m365_powershell_test.py +++ b/tests/providers/m365/lib/powershell/m365_powershell_test.py @@ -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() diff --git a/tests/providers/m365/m365_provider_test.py b/tests/providers/m365/m365_provider_test.py index 2f30efabde..96a293c602 100644 --- a/tests/providers/m365/m365_provider_test.py +++ b/tests/providers/m365/m365_provider_test.py @@ -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, ) diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 1b37bc2e61..f3f3647bfd 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -9,7 +9,6 @@ All notable changes to the **Prowler UI** are documented in this file. - New profile page with details about the user and their roles. [(#7780)](https://github.com/prowler-cloud/prowler/pull/7780) - Improved `SnippetChip` component and show resource name in new findings table. [(#7813)](https://github.com/prowler-cloud/prowler/pull/7813) - Possibility to edit the organization name. [(#7829)](https://github.com/prowler-cloud/prowler/pull/7829) -<<<<<<< HEAD - Add `Provider UID` filter to scans page. [(#7820)](https://github.com/prowler-cloud/prowler/pull/7820) - Download report behaviour updated to show feedback based on API response. [(#7758)](https://github.com/prowler-cloud/prowler/pull/7758) - Missing KISA and ProwlerThreat icons added to the compliance page. [(#7860)(https://github.com/prowler-cloud/prowler/pull/7860)] @@ -25,13 +24,20 @@ All notable changes to the **Prowler UI** are documented in this file. --- +## [v1.7.3] (Prowler v5.7.3) + +### 🐞 Fixes + +- Fix encrypted password typo in `formSchemas`. [(#7828)](https://github.com/prowler-cloud/prowler/pull/7828) + +--- + ## [v1.7.2] (Prowler v5.7.2) ### 🐞 Fixes - Download report behaviour updated to show feedback based on API response. [(#7758)](https://github.com/prowler-cloud/prowler/pull/7758) - Missing KISA and ProwlerThreat icons added to the compliance page. [(#7860)(https://github.com/prowler-cloud/prowler/pull/7860)] ->>>>>>> bdda37748 (docs: update the changelog (#7901)) - Retrieve more than 10 scans in /compliance page. [(#7865)](https://github.com/prowler-cloud/prowler/pull/7865) - Improve CustomDropdownFilter component. [(#7868)(https://github.com/prowler-cloud/prowler/pull/7868)] diff --git a/ui/actions/providers/providers.ts b/ui/actions/providers/providers.ts index 2a36f7ca05..66bcf318d7 100644 --- a/ui/actions/providers/providers.ts +++ b/ui/actions/providers/providers.ts @@ -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 diff --git a/ui/components/providers/workflow/forms/update-via-credentials-form.tsx b/ui/components/providers/workflow/forms/update-via-credentials-form.tsx index 9c751eb399..640c2d5975 100644 --- a/ui/components/providers/workflow/forms/update-via-credentials-form.tsx +++ b/ui/components/providers/workflow/forms/update-via-credentials-form.tsx @@ -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, }); diff --git a/ui/components/providers/workflow/forms/via-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials-form.tsx index 38b3a98a86..5ae75e8658 100644 --- a/ui/components/providers/workflow/forms/via-credentials-form.tsx +++ b/ui/components/providers/workflow/forms/via-credentials-form.tsx @@ -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, }); diff --git a/ui/components/providers/workflow/forms/via-credentials/m365-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials/m365-credentials-form.tsx index 1ba6024ffc..d7c2d0bef1 100644 --- a/ui/components/providers/workflow/forms/via-credentials/m365-credentials-form.tsx +++ b/ui/components/providers/workflow/forms/via-credentials/m365-credentials-form.tsx @@ -64,14 +64,14 @@ export const M365CredentialsForm = ({ /> ); diff --git a/ui/types/components.ts b/ui/types/components.ts index e45913efc7..94c5a06646 100644 --- a/ui/types/components.ts +++ b/ui/types/components.ts @@ -224,7 +224,7 @@ export type M365Credentials = { client_secret: string; tenant_id: string; user: string; - encrypted_password: string; + password: string; secretName: string; providerId: string; }; diff --git a/ui/types/formSchemas.ts b/ui/types/formSchemas.ts index 217cca4e11..8fae292824 100644 --- a/ui/types/formSchemas.ts +++ b/ui/types/formSchemas.ts @@ -142,7 +142,9 @@ 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"), - password: z.string().nonempty("Password is required"), + password: z + .string() + .nonempty("Encrypted Password is required"), } : {}), });