mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-04-04 06:28:14 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b0ce40f76 | ||
|
|
c66ef6b4db | ||
|
|
bf7e363415 | ||
|
|
5519462e82 | ||
|
|
572e6ffa17 | ||
|
|
d54993ec58 | ||
|
|
9290656069 | ||
|
|
721220d84f | ||
|
|
8b6d0331f6 | ||
|
|
00e5422654 | ||
|
|
8ff3972635 | ||
|
|
4f298ac46d | ||
|
|
8fc48e81f2 |
6
.github/workflows/api-pull-request.yml
vendored
6
.github/workflows/api-pull-request.yml
vendored
@@ -87,9 +87,9 @@ jobs:
|
||||
.github/workflows/api-pull-request.yml
|
||||
files_ignore: ${{ env.IGNORE_FILES }}
|
||||
|
||||
- name: Replace @master with current branch in pyproject.toml
|
||||
- name: Replace @master with current branch in pyproject.toml - Only for pull requests to `master`
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' && github.event_name == 'pull_request' && github.base_ref == 'master'
|
||||
run: |
|
||||
BRANCH_NAME="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}"
|
||||
echo "Using branch: $BRANCH_NAME"
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
|
||||
- name: Update SDK's poetry.lock resolved_reference to latest commit - Only for push events to `master`
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' && github.event_name == 'push'
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
# Get the latest commit hash from the prowler-cloud/prowler repository
|
||||
LATEST_COMMIT=$(curl -s "https://api.github.com/repos/prowler-cloud/prowler/commits/master" | jq -r '.sha')
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
## [1.13.2] (Prowler 5.12.3)
|
||||
|
||||
### Fixed
|
||||
- 500 error when deleting user [(#8731)](https://github.com/prowler-cloud/prowler/pull/8731)
|
||||
|
||||
---
|
||||
|
||||
## [1.13.1] (Prowler 5.12.2)
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -39,7 +39,7 @@ name = "prowler-api"
|
||||
package-mode = false
|
||||
# Needed for the SDK compatibility
|
||||
requires-python = ">=3.11,<3.13"
|
||||
version = "1.13.1"
|
||||
version = "1.13.2"
|
||||
|
||||
[project.scripts]
|
||||
celery = "src.backend.config.settings.celery"
|
||||
|
||||
@@ -13,6 +13,7 @@ from uuid import uuid4
|
||||
import jwt
|
||||
import pytest
|
||||
from allauth.socialaccount.models import SocialAccount, SocialApp
|
||||
from allauth.account.models import EmailAddress
|
||||
from botocore.exceptions import ClientError, NoCredentialsError
|
||||
from conftest import (
|
||||
API_JSON_CONTENT_TYPE,
|
||||
@@ -324,6 +325,78 @@ class TestUserViewSet:
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert User.objects.filter(id=another_user.id).exists()
|
||||
|
||||
def test_users_destroy_cascades_allauth_and_memberships(
|
||||
self, authenticated_client, create_test_user
|
||||
):
|
||||
# Create related admin-side objects (email + SocialAccount)
|
||||
EmailAddress.objects.create(
|
||||
user=create_test_user,
|
||||
email=create_test_user.email,
|
||||
primary=True,
|
||||
verified=True,
|
||||
)
|
||||
SocialAccount.objects.create(
|
||||
user=create_test_user, provider="fake-provider", uid="uid-fake-provider"
|
||||
)
|
||||
|
||||
# Sanity check pre-conditions
|
||||
assert EmailAddress.objects.filter(user=create_test_user).exists()
|
||||
assert SocialAccount.objects.filter(user=create_test_user).exists()
|
||||
assert Membership.objects.filter(user=create_test_user).exists()
|
||||
assert UserRoleRelationship.objects.filter(user=create_test_user).exists()
|
||||
|
||||
# Delete current user
|
||||
response = authenticated_client.delete(
|
||||
reverse("user-detail", kwargs={"pk": str(create_test_user.id)})
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
|
||||
# Assert user and related objects are gone
|
||||
assert not User.objects.filter(id=create_test_user.id).exists()
|
||||
assert not EmailAddress.objects.filter(user_id=create_test_user.id).exists()
|
||||
assert not SocialAccount.objects.filter(user_id=create_test_user.id).exists()
|
||||
assert not Membership.objects.filter(user_id=create_test_user.id).exists()
|
||||
assert not UserRoleRelationship.objects.filter(
|
||||
user_id=create_test_user.id
|
||||
).exists()
|
||||
|
||||
def test_users_destroy_with_saml_configuration_and_memberships(
|
||||
self, authenticated_client, create_test_user, saml_setup
|
||||
):
|
||||
# Ensure SAML configuration exists for tenant (from saml_setup fixture)
|
||||
domain = saml_setup["domain"]
|
||||
config = SAMLConfiguration.objects.get(email_domain=domain)
|
||||
|
||||
# Attach a SAML SocialAccount to the user
|
||||
SocialAccount.objects.create(
|
||||
user=create_test_user, provider="saml", uid="uid-saml"
|
||||
)
|
||||
|
||||
# Sanity check pre-conditions
|
||||
assert SocialAccount.objects.filter(
|
||||
user=create_test_user, provider="saml"
|
||||
).exists()
|
||||
assert Membership.objects.filter(user=create_test_user).exists()
|
||||
assert UserRoleRelationship.objects.filter(user=create_test_user).exists()
|
||||
|
||||
# Delete current user
|
||||
response = authenticated_client.delete(
|
||||
reverse("user-detail", kwargs={"pk": str(create_test_user.id)})
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
|
||||
# Assert user-related rows are removed
|
||||
assert not User.objects.filter(id=create_test_user.id).exists()
|
||||
assert not SocialAccount.objects.filter(user_id=create_test_user.id).exists()
|
||||
assert not Membership.objects.filter(user_id=create_test_user.id).exists()
|
||||
assert not UserRoleRelationship.objects.filter(
|
||||
user_id=create_test_user.id
|
||||
).exists()
|
||||
|
||||
# Tenant-level SAML configuration should remain intact
|
||||
assert SAMLConfiguration.objects.filter(id=config.id).exists()
|
||||
assert SocialApp.objects.filter(provider="saml", client_id=domain).exists()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"attribute_key, attribute_value, error_field",
|
||||
[
|
||||
|
||||
@@ -300,7 +300,7 @@ class SchemaView(SpectacularAPIView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
spectacular_settings.TITLE = "Prowler API"
|
||||
spectacular_settings.VERSION = "1.13.1"
|
||||
spectacular_settings.VERSION = "1.13.2"
|
||||
spectacular_settings.DESCRIPTION = (
|
||||
"Prowler API specification.\n\nThis file is auto-generated."
|
||||
)
|
||||
@@ -803,7 +803,9 @@ class UserViewSet(BaseUserViewset):
|
||||
if kwargs["pk"] != str(self.request.user.id):
|
||||
raise ValidationError("Only the current user can be deleted.")
|
||||
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
user = self.get_object()
|
||||
user.delete(using=MainRouter.admin_db)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
|
||||
All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
## [v5.12.4] (Prowler UNRELEASED)
|
||||
|
||||
### Fixed
|
||||
- Fix KeyError in `elb_ssl_listeners_use_acm_certificate` check and handle None cluster version in `eks_cluster_uses_a_supported_version` check [(#8791)](https://github.com/prowler-cloud/prowler/pull/8791)
|
||||
- Fix file extension parsing for compliance reports [(#8791)](https://github.com/prowler-cloud/prowler/pull/8791)
|
||||
- Added user pagination to Entra and Admincenter services [(#8858)](https://github.com/prowler-cloud/prowler/pull/8858)
|
||||
|
||||
---
|
||||
|
||||
## [v5.12.1] (Prowler v5.12.1)
|
||||
|
||||
### Fixed
|
||||
@@ -9,6 +18,8 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- `firehose_stream_encrypted_at_rest` check false positives and new api call in kafka service [(#8599)](https://github.com/prowler-cloud/prowler/pull/8599)
|
||||
- Replace defender rules policies key to use old name [(#8702)](https://github.com/prowler-cloud/prowler/pull/8702)
|
||||
|
||||
---
|
||||
|
||||
## [v5.12.0] (Prowler v5.12.0)
|
||||
|
||||
### Added
|
||||
|
||||
@@ -12,7 +12,7 @@ from prowler.lib.logger import logger
|
||||
|
||||
timestamp = datetime.today()
|
||||
timestamp_utc = datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
|
||||
prowler_version = "5.12.2"
|
||||
prowler_version = "5.12.4"
|
||||
html_logo_url = "https://github.com/prowler-cloud/prowler/"
|
||||
square_logo_img = "https://prowler.com/wp-content/uploads/logo-html.png"
|
||||
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
|
||||
|
||||
@@ -42,7 +42,10 @@ class ComplianceOutput(Output):
|
||||
self._from_cli = from_cli
|
||||
|
||||
if not file_extension and file_path:
|
||||
self._file_extension = "".join(Path(file_path).suffixes)
|
||||
# Compliance reports are always CSV, so just use the last suffix
|
||||
# e.g., "cis_5.0_aws.csv" should have extension ".csv", not ".0_aws.csv"
|
||||
path_obj = Path(file_path)
|
||||
self._file_extension = path_obj.suffix if path_obj.suffix else ""
|
||||
if file_extension:
|
||||
self._file_extension = file_extension
|
||||
self.file_path = f"{file_path}{self.file_extension}"
|
||||
|
||||
@@ -16,6 +16,10 @@ class eks_cluster_uses_a_supported_version(Check):
|
||||
for cluster in eks_client.clusters:
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=cluster)
|
||||
|
||||
# Handle case where cluster.version might be None (edge case during cluster creation/deletion)
|
||||
if not cluster.version:
|
||||
continue
|
||||
|
||||
cluster_version_major, cluster_version_minor = map(
|
||||
int, cluster.version.split(".")
|
||||
)
|
||||
|
||||
@@ -15,8 +15,14 @@ class elb_ssl_listeners_use_acm_certificate(Check):
|
||||
if (
|
||||
listener.certificate_arn
|
||||
and listener.protocol in secure_protocols
|
||||
and acm_client.certificates[listener.certificate_arn].type
|
||||
!= "AMAZON_ISSUED"
|
||||
and (
|
||||
listener.certificate_arn not in acm_client.certificates
|
||||
or (
|
||||
acm_client.certificates.get(listener.certificate_arn)
|
||||
and acm_client.certificates[listener.certificate_arn].type
|
||||
!= "AMAZON_ISSUED"
|
||||
)
|
||||
)
|
||||
):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"ELB {lb.name} has HTTPS/SSL listeners that are using certificates not managed by ACM."
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from asyncio import gather, get_event_loop
|
||||
import asyncio
|
||||
from asyncio import gather
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
@@ -15,7 +16,23 @@ class Entra(AzureService):
|
||||
def __init__(self, provider: AzureProvider):
|
||||
super().__init__(GraphServiceClient, provider)
|
||||
|
||||
loop = get_event_loop()
|
||||
created_loop = False
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
created_loop = True
|
||||
|
||||
if loop.is_closed():
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
created_loop = True
|
||||
|
||||
if loop.is_running():
|
||||
raise RuntimeError(
|
||||
"Cannot initialize Entra service while event loop is running"
|
||||
)
|
||||
|
||||
# Get users first alone because it is a dependency for other attributes
|
||||
self.users = loop.run_until_complete(self._get_users())
|
||||
@@ -38,36 +55,48 @@ class Entra(AzureService):
|
||||
self.directory_roles = attributes[4]
|
||||
self.conditional_access_policy = attributes[5]
|
||||
|
||||
if created_loop:
|
||||
asyncio.set_event_loop(None)
|
||||
loop.close()
|
||||
|
||||
async def _get_users(self):
|
||||
logger.info("Entra - Getting users...")
|
||||
users = {}
|
||||
try:
|
||||
for tenant, client in self.clients.items():
|
||||
users_list = await client.users.get()
|
||||
users.update({tenant: {}})
|
||||
users_response = await client.users.get()
|
||||
|
||||
try:
|
||||
for user in users_list.value:
|
||||
users[tenant].update(
|
||||
{
|
||||
user.id: User(
|
||||
id=user.id,
|
||||
name=user.display_name,
|
||||
authentication_methods=[
|
||||
AuthMethod(
|
||||
id=auth_method.id,
|
||||
type=getattr(
|
||||
auth_method, "odata_type", None
|
||||
),
|
||||
)
|
||||
for auth_method in (
|
||||
await client.users.by_user_id(
|
||||
user.id
|
||||
).authentication.methods.get()
|
||||
).value
|
||||
],
|
||||
)
|
||||
}
|
||||
)
|
||||
while users_response:
|
||||
for user in getattr(users_response, "value", []) or []:
|
||||
users[tenant].update(
|
||||
{
|
||||
user.id: User(
|
||||
id=user.id,
|
||||
name=user.display_name,
|
||||
authentication_methods=[
|
||||
AuthMethod(
|
||||
id=auth_method.id,
|
||||
type=getattr(
|
||||
auth_method, "odata_type", None
|
||||
),
|
||||
)
|
||||
for auth_method in (
|
||||
await client.users.by_user_id(
|
||||
user.id
|
||||
).authentication.methods.get()
|
||||
).value
|
||||
],
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
next_link = getattr(users_response, "odata_next_link", None)
|
||||
if not next_link:
|
||||
break
|
||||
users_response = await client.users.with_url(next_link).get()
|
||||
|
||||
except Exception as error:
|
||||
if (
|
||||
error.__class__.__name__ == "ODataError"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from asyncio import gather, get_event_loop
|
||||
import asyncio
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic.v1 import BaseModel
|
||||
@@ -20,13 +20,29 @@ class AdminCenter(M365Service):
|
||||
self.sharing_policy = self._get_sharing_policy()
|
||||
self.powershell.close()
|
||||
|
||||
loop = get_event_loop()
|
||||
created_loop = False
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
created_loop = True
|
||||
|
||||
if loop.is_closed():
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
created_loop = True
|
||||
|
||||
if loop.is_running():
|
||||
raise RuntimeError(
|
||||
"Cannot initialize AdminCenter service while event loop is running"
|
||||
)
|
||||
|
||||
# Get users first alone because it is a dependency for other attributes
|
||||
self.users = loop.run_until_complete(self._get_users())
|
||||
|
||||
attributes = loop.run_until_complete(
|
||||
gather(
|
||||
asyncio.gather(
|
||||
self._get_directory_roles(),
|
||||
self._get_groups(),
|
||||
self._get_domains(),
|
||||
@@ -37,6 +53,10 @@ class AdminCenter(M365Service):
|
||||
self.groups = attributes[1]
|
||||
self.domains = attributes[2]
|
||||
|
||||
if created_loop:
|
||||
asyncio.set_event_loop(None)
|
||||
loop.close()
|
||||
|
||||
def _get_organization_config(self):
|
||||
logger.info("Microsoft365 - Getting Exchange Organization configuration...")
|
||||
organization_config = None
|
||||
@@ -77,27 +97,36 @@ class AdminCenter(M365Service):
|
||||
logger.info("M365 - Getting users...")
|
||||
users = {}
|
||||
try:
|
||||
users_list = await self.client.users.get()
|
||||
users.update({})
|
||||
for user in users_list.value:
|
||||
license_details = await self.client.users.by_user_id(
|
||||
user.id
|
||||
).license_details.get()
|
||||
users.update(
|
||||
{
|
||||
user.id: User(
|
||||
id=user.id,
|
||||
name=getattr(user, "display_name", ""),
|
||||
license=(
|
||||
getattr(
|
||||
license_details.value[0], "sku_part_number", None
|
||||
)
|
||||
if license_details.value
|
||||
else None
|
||||
),
|
||||
)
|
||||
}
|
||||
)
|
||||
users_response = await self.client.users.get()
|
||||
|
||||
while users_response:
|
||||
for user in getattr(users_response, "value", []) or []:
|
||||
license_details = await self.client.users.by_user_id(
|
||||
user.id
|
||||
).license_details.get()
|
||||
users.update(
|
||||
{
|
||||
user.id: User(
|
||||
id=user.id,
|
||||
name=getattr(user, "display_name", ""),
|
||||
license=(
|
||||
getattr(
|
||||
license_details.value[0],
|
||||
"sku_part_number",
|
||||
None,
|
||||
)
|
||||
if license_details.value
|
||||
else None
|
||||
),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
next_link = getattr(users_response, "odata_next_link", None)
|
||||
if not next_link:
|
||||
break
|
||||
users_response = await self.client.users.with_url(next_link).get()
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import asyncio
|
||||
from asyncio import gather, get_event_loop
|
||||
from asyncio import gather
|
||||
from enum import Enum
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
@@ -20,7 +20,24 @@ class Entra(M365Service):
|
||||
self.user_accounts_status = self.powershell.get_user_account_status()
|
||||
self.powershell.close()
|
||||
|
||||
loop = get_event_loop()
|
||||
created_loop = False
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
created_loop = True
|
||||
|
||||
if loop.is_closed():
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
created_loop = True
|
||||
|
||||
if loop.is_running():
|
||||
raise RuntimeError(
|
||||
"Cannot initialize Entra service while event loop is running"
|
||||
)
|
||||
|
||||
self.tenant_domain = provider.identity.tenant_domain
|
||||
attributes = loop.run_until_complete(
|
||||
gather(
|
||||
@@ -41,6 +58,10 @@ class Entra(M365Service):
|
||||
self.users = attributes[5]
|
||||
self.user_accounts_status = {}
|
||||
|
||||
if created_loop:
|
||||
asyncio.set_event_loop(None)
|
||||
loop.close()
|
||||
|
||||
async def _get_authorization_policy(self):
|
||||
logger.info("Entra - Getting authorization policy...")
|
||||
authorization_policy = None
|
||||
@@ -364,7 +385,7 @@ class Entra(M365Service):
|
||||
logger.info("Entra - Getting users...")
|
||||
users = {}
|
||||
try:
|
||||
users_list = await self.client.users.get()
|
||||
users_response = await self.client.users.get()
|
||||
directory_roles = await self.client.directory_roles.get()
|
||||
|
||||
async def fetch_role_members(directory_role):
|
||||
@@ -396,23 +417,29 @@ class Entra(M365Service):
|
||||
)
|
||||
registration_details = {}
|
||||
|
||||
for user in users_list.value:
|
||||
users[user.id] = User(
|
||||
id=user.id,
|
||||
name=user.display_name,
|
||||
on_premises_sync_enabled=(
|
||||
True if (user.on_premises_sync_enabled) else False
|
||||
),
|
||||
directory_roles_ids=user_roles_map.get(user.id, []),
|
||||
is_mfa_capable=(
|
||||
registration_details.get(user.id, {}).is_mfa_capable
|
||||
if registration_details.get(user.id, None) is not None
|
||||
else False
|
||||
),
|
||||
account_enabled=not self.user_accounts_status.get(user.id, {}).get(
|
||||
"AccountDisabled", False
|
||||
),
|
||||
)
|
||||
while users_response:
|
||||
for user in getattr(users_response, "value", []) or []:
|
||||
users[user.id] = User(
|
||||
id=user.id,
|
||||
name=user.display_name,
|
||||
on_premises_sync_enabled=(
|
||||
True if (user.on_premises_sync_enabled) else False
|
||||
),
|
||||
directory_roles_ids=user_roles_map.get(user.id, []),
|
||||
is_mfa_capable=(
|
||||
registration_details.get(user.id, {}).is_mfa_capable
|
||||
if registration_details.get(user.id, None) is not None
|
||||
else False
|
||||
),
|
||||
account_enabled=not self.user_accounts_status.get(
|
||||
user.id, {}
|
||||
).get("AccountDisabled", False),
|
||||
)
|
||||
|
||||
next_link = getattr(users_response, "odata_next_link", None)
|
||||
if not next_link:
|
||||
break
|
||||
users_response = await self.client.users.with_url(next_link).get()
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import asyncio
|
||||
import uuid
|
||||
from asyncio import gather, get_event_loop
|
||||
from typing import List, Optional
|
||||
|
||||
from msgraph.generated.models.o_data_errors.o_data_error import ODataError
|
||||
@@ -16,15 +16,36 @@ class SharePoint(M365Service):
|
||||
if self.powershell:
|
||||
self.powershell.close()
|
||||
|
||||
loop = get_event_loop()
|
||||
created_loop = False
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
created_loop = True
|
||||
|
||||
if loop.is_closed():
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
created_loop = True
|
||||
|
||||
if loop.is_running():
|
||||
raise RuntimeError(
|
||||
"Cannot initialize SharePoint service while event loop is running"
|
||||
)
|
||||
|
||||
self.tenant_domain = provider.identity.tenant_domain
|
||||
attributes = loop.run_until_complete(
|
||||
gather(
|
||||
asyncio.gather(
|
||||
self._get_settings(),
|
||||
)
|
||||
)
|
||||
self.settings = attributes[0]
|
||||
|
||||
if created_loop:
|
||||
asyncio.set_event_loop(None)
|
||||
loop.close()
|
||||
|
||||
async def _get_settings(self):
|
||||
logger.info("M365 - Getting SharePoint global settings...")
|
||||
settings = None
|
||||
|
||||
@@ -74,7 +74,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"}
|
||||
name = "prowler"
|
||||
readme = "README.md"
|
||||
requires-python = ">3.9.1,<3.13"
|
||||
version = "5.12.2"
|
||||
version = "5.12.4"
|
||||
|
||||
[project.scripts]
|
||||
prowler = "prowler.__main__:prowler"
|
||||
|
||||
@@ -382,3 +382,54 @@ class TestCompliance:
|
||||
assert get_check_compliance(finding, "github", bulk_checks_metadata) == {
|
||||
"CIS-1.0": ["1.1.11"],
|
||||
}
|
||||
|
||||
|
||||
class TestComplianceOutput:
|
||||
"""Test ComplianceOutput file extension parsing fix."""
|
||||
|
||||
def test_compliance_output_file_extension_with_dots(self):
|
||||
"""Test that ComplianceOutput correctly parses file extensions when framework names contain dots."""
|
||||
from prowler.lib.outputs.compliance.generic.generic import GenericCompliance
|
||||
|
||||
compliance = Compliance(
|
||||
Framework="CIS",
|
||||
Version="5.0",
|
||||
Provider="AWS",
|
||||
Name="CIS Amazon Web Services Foundations Benchmark v5.0",
|
||||
Description="Test compliance framework",
|
||||
Requirements=[],
|
||||
)
|
||||
|
||||
# Test with problematic file path that contains dots in framework name
|
||||
# This simulates the real scenario from Prowler App S3 integration
|
||||
problematic_file_path = "output/compliance/prowler-output-123456789012-20250101120000_cis_5.0_aws.csv"
|
||||
|
||||
# Create GenericCompliance object with file_path (no explicit file_extension)
|
||||
compliance_output = GenericCompliance(
|
||||
findings=[], compliance=compliance, file_path=problematic_file_path
|
||||
)
|
||||
|
||||
assert compliance_output.file_extension == ".csv"
|
||||
assert compliance_output.file_extension != ".0_aws.csv"
|
||||
|
||||
def test_compliance_output_file_extension_explicit(self):
|
||||
"""Test that ComplianceOutput uses explicit file_extension when provided."""
|
||||
from prowler.lib.outputs.compliance.generic.generic import GenericCompliance
|
||||
|
||||
compliance = Compliance(
|
||||
Framework="CIS",
|
||||
Version="5.0",
|
||||
Provider="AWS",
|
||||
Name="CIS Amazon Web Services Foundations Benchmark v5.0",
|
||||
Description="Test compliance framework",
|
||||
Requirements=[],
|
||||
)
|
||||
|
||||
compliance_output = GenericCompliance(
|
||||
findings=[],
|
||||
compliance=compliance,
|
||||
file_path="output/compliance/test",
|
||||
file_extension=".csv",
|
||||
)
|
||||
|
||||
assert compliance_output.file_extension == ".csv"
|
||||
|
||||
@@ -13,6 +13,7 @@ class Test_eks_cluster_ensure_version_is_supported:
|
||||
def test_no_clusters(self):
|
||||
eks_client = mock.MagicMock
|
||||
eks_client.clusters = []
|
||||
eks_client.audit_config = {"eks_cluster_oldest_version_supported": "1.28"}
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.eks.eks_service.EKS",
|
||||
eks_client,
|
||||
@@ -53,7 +54,7 @@ class Test_eks_cluster_ensure_version_is_supported:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"EKS cluster {cluster_name} is in version 1.22. It should be one of the next supported versions: 1.28 or higher"
|
||||
== f"EKS cluster {cluster_name} is using version 1.22. It should be one of the supported versions: 1.28 or higher."
|
||||
)
|
||||
assert result[0].resource_id == cluster_name
|
||||
assert result[0].resource_arn == cluster_arn
|
||||
@@ -88,7 +89,7 @@ class Test_eks_cluster_ensure_version_is_supported:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"EKS cluster {cluster_name} is in version 0.22. It should be one of the next supported versions: 1.28 or higher"
|
||||
== f"EKS cluster {cluster_name} is using version 0.22. It should be one of the supported versions: 1.28 or higher."
|
||||
)
|
||||
assert result[0].resource_id == cluster_name
|
||||
assert result[0].resource_arn == cluster_arn
|
||||
@@ -199,3 +200,31 @@ class Test_eks_cluster_ensure_version_is_supported:
|
||||
assert result[0].resource_arn == cluster_arn
|
||||
assert result[0].resource_tags == []
|
||||
assert result[0].region == AWS_REGION_EU_WEST_1
|
||||
|
||||
def test_eks_cluster_with_none_version(self):
|
||||
"""Test EKS cluster with version=None - should return FAIL gracefully"""
|
||||
eks_client = mock.MagicMock
|
||||
eks_client.audit_config = {"eks_cluster_oldest_version_supported": "1.28"}
|
||||
eks_client.clusters = []
|
||||
eks_client.clusters.append(
|
||||
EKSCluster(
|
||||
name=cluster_name,
|
||||
version=None, # This should trigger the AttributeError in current implementation
|
||||
arn=cluster_arn,
|
||||
region=AWS_REGION_EU_WEST_1,
|
||||
logging=None,
|
||||
)
|
||||
)
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.eks.eks_service.EKS",
|
||||
eks_client,
|
||||
):
|
||||
from prowler.providers.aws.services.eks.eks_cluster_uses_a_supported_version.eks_cluster_uses_a_supported_version import (
|
||||
eks_cluster_uses_a_supported_version,
|
||||
)
|
||||
|
||||
check = eks_cluster_uses_a_supported_version()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 0
|
||||
|
||||
@@ -364,3 +364,144 @@ class Test_elb_ssl_listeners_use_acm_certificate:
|
||||
assert result[0].resource_arn == elb_arn
|
||||
assert result[0].resource_tags == []
|
||||
assert result[0].region == AWS_REGION
|
||||
|
||||
@mock_aws
|
||||
def test_elb_with_HTTPS_listener_IAM_certificate(self):
|
||||
"""Test ELB with HTTPS listener using IAM certificate (not ACM) - should return FAIL"""
|
||||
elb = client("elb", region_name=AWS_REGION)
|
||||
ec2 = resource("ec2", region_name=AWS_REGION)
|
||||
|
||||
# Create IAM certificate (not ACM)
|
||||
iam_certificate_arn = (
|
||||
f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:server-certificate/test-certificate"
|
||||
)
|
||||
|
||||
security_group = ec2.create_security_group(
|
||||
GroupName="sg01", Description="Test security group sg01"
|
||||
)
|
||||
|
||||
elb.create_load_balancer(
|
||||
LoadBalancerName="my-lb",
|
||||
Listeners=[
|
||||
{
|
||||
"Protocol": "https",
|
||||
"LoadBalancerPort": 80,
|
||||
"InstancePort": 8080,
|
||||
"SSLCertificateId": iam_certificate_arn,
|
||||
},
|
||||
],
|
||||
AvailabilityZones=[AWS_REGION_EU_WEST_1_AZA],
|
||||
Scheme="internal",
|
||||
SecurityGroups=[security_group.id],
|
||||
)
|
||||
|
||||
from prowler.providers.aws.services.acm.acm_service import ACM
|
||||
from prowler.providers.aws.services.elb.elb_service import ELB
|
||||
|
||||
aws_mocked_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_mocked_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.elb.elb_ssl_listeners_use_acm_certificate.elb_ssl_listeners_use_acm_certificate.elb_client",
|
||||
new=ELB(aws_mocked_provider),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.elb.elb_ssl_listeners_use_acm_certificate.elb_ssl_listeners_use_acm_certificate.acm_client",
|
||||
new=ACM(aws_mocked_provider),
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.elb.elb_ssl_listeners_use_acm_certificate.elb_ssl_listeners_use_acm_certificate import (
|
||||
elb_ssl_listeners_use_acm_certificate,
|
||||
)
|
||||
|
||||
check = elb_ssl_listeners_use_acm_certificate()
|
||||
|
||||
# This should now work correctly and return FAIL for IAM certificate
|
||||
# (unless there's still a KeyError in the current implementation)
|
||||
result = check.execute()
|
||||
|
||||
# Expected behavior: FAIL because IAM certificate is not managed by ACM
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "ELB my-lb has HTTPS/SSL listeners that are using certificates not managed by ACM."
|
||||
)
|
||||
assert result[0].resource_id == "my-lb"
|
||||
assert result[0].resource_arn == elb_arn
|
||||
assert result[0].resource_tags == []
|
||||
assert result[0].region == AWS_REGION
|
||||
|
||||
@mock_aws
|
||||
def test_elb_with_HTTPS_listener_certificate_not_in_acm(self):
|
||||
"""Test ELB with HTTPS listener using certificate that triggers not in acm_client.certificates condition"""
|
||||
elb = client("elb", region_name=AWS_REGION)
|
||||
ec2 = resource("ec2", region_name=AWS_REGION)
|
||||
|
||||
# Create a certificate ARN that will NOT be in ACM (simulating IAM certificate or any non-ACM certificate)
|
||||
# This will trigger the first condition: listener.certificate_arn not in acm_client.certificates
|
||||
non_acm_certificate_arn = (
|
||||
f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:server-certificate/non-acm-cert"
|
||||
)
|
||||
|
||||
security_group = ec2.create_security_group(
|
||||
GroupName="sg01", Description="Test security group sg01"
|
||||
)
|
||||
|
||||
elb.create_load_balancer(
|
||||
LoadBalancerName="my-lb",
|
||||
Listeners=[
|
||||
{
|
||||
"Protocol": "https",
|
||||
"LoadBalancerPort": 80,
|
||||
"InstancePort": 8080,
|
||||
"SSLCertificateId": non_acm_certificate_arn,
|
||||
},
|
||||
],
|
||||
AvailabilityZones=[AWS_REGION_EU_WEST_1_AZA],
|
||||
Scheme="internal",
|
||||
SecurityGroups=[security_group.id],
|
||||
)
|
||||
|
||||
from prowler.providers.aws.services.acm.acm_service import ACM
|
||||
from prowler.providers.aws.services.elb.elb_service import ELB
|
||||
|
||||
aws_mocked_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_mocked_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.elb.elb_ssl_listeners_use_acm_certificate.elb_ssl_listeners_use_acm_certificate.elb_client",
|
||||
new=ELB(aws_mocked_provider),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.elb.elb_ssl_listeners_use_acm_certificate.elb_ssl_listeners_use_acm_certificate.acm_client",
|
||||
new=ACM(aws_mocked_provider),
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.elb.elb_ssl_listeners_use_acm_certificate.elb_ssl_listeners_use_acm_certificate import (
|
||||
elb_ssl_listeners_use_acm_certificate,
|
||||
)
|
||||
|
||||
check = elb_ssl_listeners_use_acm_certificate()
|
||||
result = check.execute()
|
||||
|
||||
# This should trigger the first condition: listener.certificate_arn not in acm_client.certificates
|
||||
# and return FAIL without ever reaching the second part of the OR condition
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "ELB my-lb has HTTPS/SSL listeners that are using certificates not managed by ACM."
|
||||
)
|
||||
assert result[0].resource_id == "my-lb"
|
||||
assert result[0].resource_arn == elb_arn
|
||||
assert result[0].resource_tags == []
|
||||
assert result[0].region == AWS_REGION
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from unittest.mock import patch
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from uuid import uuid4
|
||||
|
||||
from prowler.providers.azure.models import AzureIdentityInfo
|
||||
@@ -223,3 +225,64 @@ class Test_Entra_Service:
|
||||
]
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
def test_azure_entra__get_users_handles_pagination():
|
||||
entra_service = Entra.__new__(Entra)
|
||||
|
||||
users_page_one = [
|
||||
SimpleNamespace(id="user-1", display_name="User 1"),
|
||||
SimpleNamespace(id="user-2", display_name="User 2"),
|
||||
]
|
||||
users_page_two = [
|
||||
SimpleNamespace(id="user-3", display_name="User 3"),
|
||||
]
|
||||
|
||||
users_response_page_one = SimpleNamespace(
|
||||
value=users_page_one,
|
||||
odata_next_link="next-link",
|
||||
)
|
||||
users_response_page_two = SimpleNamespace(
|
||||
value=users_page_two, odata_next_link=None
|
||||
)
|
||||
|
||||
users_with_url_builder = SimpleNamespace(
|
||||
get=AsyncMock(return_value=users_response_page_two)
|
||||
)
|
||||
with_url_mock = MagicMock(return_value=users_with_url_builder)
|
||||
|
||||
def by_user_id_side_effect(user_id):
|
||||
auth_methods_response = SimpleNamespace(
|
||||
value=[
|
||||
SimpleNamespace(
|
||||
id=f"{user_id}-method",
|
||||
odata_type="#microsoft.graph.passwordAuthenticationMethod",
|
||||
)
|
||||
]
|
||||
)
|
||||
return SimpleNamespace(
|
||||
authentication=SimpleNamespace(
|
||||
methods=SimpleNamespace(
|
||||
get=AsyncMock(return_value=auth_methods_response)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
users_builder = SimpleNamespace(
|
||||
get=AsyncMock(return_value=users_response_page_one),
|
||||
with_url=with_url_mock,
|
||||
by_user_id=MagicMock(side_effect=by_user_id_side_effect),
|
||||
)
|
||||
|
||||
entra_service.clients = {"tenant-1": SimpleNamespace(users=users_builder)}
|
||||
|
||||
users = asyncio.run(entra_service._get_users())
|
||||
|
||||
assert len(users["tenant-1"]) == 3
|
||||
assert users_builder.get.await_count == 1
|
||||
with_url_mock.assert_called_once_with("next-link")
|
||||
assert users["tenant-1"]["user-1"].authentication_methods[0].id == "user-1-method"
|
||||
assert (
|
||||
users["tenant-1"]["user-3"].authentication_methods[0].type
|
||||
== "#microsoft.graph.passwordAuthenticationMethod"
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from prowler.providers.m365.models import M365IdentityInfo
|
||||
from prowler.providers.m365.services.admincenter.admincenter_service import (
|
||||
@@ -161,3 +163,54 @@ class Test_AdminCenter_Service:
|
||||
assert admincenter_client.sharing_policy.name == "Test"
|
||||
assert admincenter_client.sharing_policy.enabled is False
|
||||
admincenter_client.powershell.close()
|
||||
|
||||
|
||||
def test_admincenter__get_users_handles_pagination():
|
||||
admincenter_service = AdminCenter.__new__(AdminCenter)
|
||||
|
||||
users_page_one = [
|
||||
SimpleNamespace(id="user-1", display_name="User 1"),
|
||||
SimpleNamespace(id="user-2", display_name="User 2"),
|
||||
]
|
||||
users_page_two = [
|
||||
SimpleNamespace(id="user-3", display_name="User 3"),
|
||||
]
|
||||
|
||||
users_response_page_one = SimpleNamespace(
|
||||
value=users_page_one,
|
||||
odata_next_link="next-link",
|
||||
)
|
||||
users_response_page_two = SimpleNamespace(
|
||||
value=users_page_two, odata_next_link=None
|
||||
)
|
||||
|
||||
users_with_url_builder = SimpleNamespace(
|
||||
get=AsyncMock(return_value=users_response_page_two)
|
||||
)
|
||||
with_url_mock = MagicMock(return_value=users_with_url_builder)
|
||||
|
||||
def by_user_id_side_effect(user_id):
|
||||
license_details_response = SimpleNamespace(
|
||||
value=[SimpleNamespace(sku_part_number=f"SKU-{user_id}")]
|
||||
)
|
||||
return SimpleNamespace(
|
||||
license_details=SimpleNamespace(
|
||||
get=AsyncMock(return_value=license_details_response)
|
||||
)
|
||||
)
|
||||
|
||||
users_builder = SimpleNamespace(
|
||||
get=AsyncMock(return_value=users_response_page_one),
|
||||
with_url=with_url_mock,
|
||||
by_user_id=MagicMock(side_effect=by_user_id_side_effect),
|
||||
)
|
||||
|
||||
admincenter_service.client = SimpleNamespace(users=users_builder)
|
||||
|
||||
users = asyncio.run(admincenter_service._get_users())
|
||||
|
||||
assert len(users) == 3
|
||||
assert users_builder.get.await_count == 1
|
||||
with_url_mock.assert_called_once_with("next-link")
|
||||
assert users["user-1"].license == "SKU-user-1"
|
||||
assert users["user-3"].license == "SKU-user-3"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from unittest.mock import patch
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from prowler.providers.m365.models import M365IdentityInfo
|
||||
from prowler.providers.m365.services.entra.entra_service import (
|
||||
@@ -155,17 +157,21 @@ async def mock_entra_get_organization(_):
|
||||
|
||||
class Test_Entra_Service:
|
||||
def test_get_client(self):
|
||||
admincenter_client = Entra(
|
||||
set_mocked_m365_provider(identity=M365IdentityInfo(tenant_domain=DOMAIN))
|
||||
)
|
||||
assert admincenter_client.client.__class__.__name__ == "GraphServiceClient"
|
||||
with patch("prowler.providers.m365.lib.service.service.M365PowerShell"):
|
||||
admincenter_client = Entra(
|
||||
set_mocked_m365_provider(
|
||||
identity=M365IdentityInfo(tenant_domain=DOMAIN)
|
||||
)
|
||||
)
|
||||
assert admincenter_client.client.__class__.__name__ == "GraphServiceClient"
|
||||
|
||||
@patch(
|
||||
"prowler.providers.m365.services.entra.entra_service.Entra._get_authorization_policy",
|
||||
new=mock_entra_get_authorization_policy,
|
||||
)
|
||||
def test_get_authorization_policy(self):
|
||||
entra_client = Entra(set_mocked_m365_provider())
|
||||
with patch("prowler.providers.m365.lib.service.service.M365PowerShell"):
|
||||
entra_client = Entra(set_mocked_m365_provider())
|
||||
assert entra_client.authorization_policy.id == "id-1"
|
||||
assert entra_client.authorization_policy.name == "Name 1"
|
||||
assert entra_client.authorization_policy.description == "Description 1"
|
||||
@@ -193,7 +199,8 @@ class Test_Entra_Service:
|
||||
new=mock_entra_get_conditional_access_policies,
|
||||
)
|
||||
def test_get_conditional_access_policies(self):
|
||||
entra_client = Entra(set_mocked_m365_provider())
|
||||
with patch("prowler.providers.m365.lib.service.service.M365PowerShell"):
|
||||
entra_client = Entra(set_mocked_m365_provider())
|
||||
assert entra_client.conditional_access_policies == {
|
||||
"id-1": ConditionalAccessPolicy(
|
||||
id="id-1",
|
||||
@@ -242,7 +249,8 @@ class Test_Entra_Service:
|
||||
new=mock_entra_get_groups,
|
||||
)
|
||||
def test_get_groups(self):
|
||||
entra_client = Entra(set_mocked_m365_provider())
|
||||
with patch("prowler.providers.m365.lib.service.service.M365PowerShell"):
|
||||
entra_client = Entra(set_mocked_m365_provider())
|
||||
assert len(entra_client.groups) == 2
|
||||
assert entra_client.groups[0]["id"] == "id-1"
|
||||
assert entra_client.groups[0]["name"] == "group1"
|
||||
@@ -258,7 +266,8 @@ class Test_Entra_Service:
|
||||
new=mock_entra_get_admin_consent_policy,
|
||||
)
|
||||
def test_get_admin_consent_policy(self):
|
||||
entra_client = Entra(set_mocked_m365_provider())
|
||||
with patch("prowler.providers.m365.lib.service.service.M365PowerShell"):
|
||||
entra_client = Entra(set_mocked_m365_provider())
|
||||
assert entra_client.admin_consent_policy.admin_consent_enabled
|
||||
assert entra_client.admin_consent_policy.notify_reviewers
|
||||
assert entra_client.admin_consent_policy.email_reminders_to_reviewers is False
|
||||
@@ -269,7 +278,8 @@ class Test_Entra_Service:
|
||||
new=mock_entra_get_organization,
|
||||
)
|
||||
def test_get_organization(self):
|
||||
entra_client = Entra(set_mocked_m365_provider())
|
||||
with patch("prowler.providers.m365.lib.service.service.M365PowerShell"):
|
||||
entra_client = Entra(set_mocked_m365_provider())
|
||||
assert len(entra_client.organizations) == 1
|
||||
assert entra_client.organizations[0].id == "org1"
|
||||
assert entra_client.organizations[0].name == "Organization 1"
|
||||
@@ -280,7 +290,8 @@ class Test_Entra_Service:
|
||||
new=mock_entra_get_users,
|
||||
)
|
||||
def test_get_users(self):
|
||||
entra_client = Entra(set_mocked_m365_provider())
|
||||
with patch("prowler.providers.m365.lib.service.service.M365PowerShell"):
|
||||
entra_client = Entra(set_mocked_m365_provider())
|
||||
assert len(entra_client.users) == 3
|
||||
assert entra_client.users["user-1"].id == "user-1"
|
||||
assert entra_client.users["user-1"].name == "User 1"
|
||||
@@ -303,3 +314,119 @@ class Test_Entra_Service:
|
||||
]
|
||||
assert entra_client.users["user-3"].on_premises_sync_enabled
|
||||
assert not entra_client.users["user-3"].is_mfa_capable
|
||||
|
||||
def test__get_users_paginates_through_next_links(self):
|
||||
entra_service = Entra.__new__(Entra)
|
||||
entra_service.user_accounts_status = {"user-6": {"AccountDisabled": True}}
|
||||
|
||||
users_page_one = [
|
||||
SimpleNamespace(
|
||||
id="user-1",
|
||||
display_name="User 1",
|
||||
on_premises_sync_enabled=True,
|
||||
),
|
||||
SimpleNamespace(
|
||||
id="user-2",
|
||||
display_name="User 2",
|
||||
on_premises_sync_enabled=False,
|
||||
),
|
||||
SimpleNamespace(
|
||||
id="user-3",
|
||||
display_name="User 3",
|
||||
on_premises_sync_enabled=None,
|
||||
),
|
||||
SimpleNamespace(
|
||||
id="user-4",
|
||||
display_name="User 4",
|
||||
on_premises_sync_enabled=True,
|
||||
),
|
||||
SimpleNamespace(
|
||||
id="user-5",
|
||||
display_name="User 5",
|
||||
on_premises_sync_enabled=False,
|
||||
),
|
||||
]
|
||||
users_page_two = [
|
||||
SimpleNamespace(
|
||||
id="user-6",
|
||||
display_name="User 6",
|
||||
on_premises_sync_enabled=True,
|
||||
)
|
||||
]
|
||||
|
||||
users_response_page_one = SimpleNamespace(
|
||||
value=users_page_one,
|
||||
odata_next_link="next-link",
|
||||
)
|
||||
users_response_page_two = SimpleNamespace(
|
||||
value=users_page_two,
|
||||
odata_next_link=None,
|
||||
)
|
||||
|
||||
users_with_url_builder = SimpleNamespace(
|
||||
get=AsyncMock(return_value=users_response_page_two)
|
||||
)
|
||||
with_url_mock = MagicMock(return_value=users_with_url_builder)
|
||||
|
||||
users_builder = SimpleNamespace(
|
||||
get=AsyncMock(return_value=users_response_page_one),
|
||||
with_url=with_url_mock,
|
||||
)
|
||||
|
||||
role_members_response = SimpleNamespace(
|
||||
value=[
|
||||
SimpleNamespace(id="user-1"),
|
||||
SimpleNamespace(id="user-6"),
|
||||
]
|
||||
)
|
||||
members_builder = SimpleNamespace(
|
||||
get=AsyncMock(return_value=role_members_response)
|
||||
)
|
||||
directory_roles_builder = SimpleNamespace(
|
||||
get=AsyncMock(
|
||||
return_value=SimpleNamespace(
|
||||
value=[
|
||||
SimpleNamespace(
|
||||
id="role-1",
|
||||
role_template_id="role-template-1",
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
by_directory_role_id=MagicMock(
|
||||
return_value=SimpleNamespace(members=members_builder)
|
||||
),
|
||||
)
|
||||
|
||||
registration_details_response = SimpleNamespace(
|
||||
value=[
|
||||
SimpleNamespace(id="user-1", is_mfa_capable=True),
|
||||
SimpleNamespace(id="user-6", is_mfa_capable=True),
|
||||
]
|
||||
)
|
||||
registration_details_builder = SimpleNamespace(
|
||||
get=AsyncMock(return_value=registration_details_response)
|
||||
)
|
||||
reports_builder = SimpleNamespace(
|
||||
authentication_methods=SimpleNamespace(
|
||||
user_registration_details=registration_details_builder
|
||||
)
|
||||
)
|
||||
|
||||
entra_service.client = SimpleNamespace(
|
||||
users=users_builder,
|
||||
directory_roles=directory_roles_builder,
|
||||
reports=reports_builder,
|
||||
)
|
||||
|
||||
users = asyncio.run(entra_service._get_users())
|
||||
|
||||
assert len(users) == 6
|
||||
assert users_builder.get.await_count == 1
|
||||
assert users_builder.get.await_args.kwargs == {}
|
||||
with_url_mock.assert_called_once_with("next-link")
|
||||
assert users["user-1"].directory_roles_ids == ["role-template-1"]
|
||||
assert users["user-6"].directory_roles_ids == ["role-template-1"]
|
||||
assert users["user-6"].account_enabled is False
|
||||
assert users["user-1"].is_mfa_capable is True
|
||||
assert users["user-2"].is_mfa_capable is False
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
|
||||
## [1.12.4] (Prowler v5.12.4)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- Remove maxTokens model param for GPT-5 models [(#8843)](https://github.com/prowler-cloud/prowler/pull/8843)
|
||||
|
||||
## [1.12.3] (Prowler v5.12.3)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- Disable "See Findings" button until scan completes [(#8762)](https://github.com/prowler-cloud/prowler/pull/8762)
|
||||
- Scrolling during Lighthouse AI response streaming [(#8669)](https://github.com/prowler-cloud/prowler/pull/8669)
|
||||
- Lighthouse textbox to send messages on Enter [(#8747)](https://github.com/prowler-cloud/prowler/pull/8747)
|
||||
|
||||
---
|
||||
|
||||
## [1.12.2] (Prowler v5.12.2)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
@@ -134,7 +134,7 @@ export const Chat = ({ hasConfig, isActive }: ChatProps) => {
|
||||
// Global keyboard shortcut handler
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
if (messageValue?.trim()) {
|
||||
onFormSubmit();
|
||||
@@ -146,16 +146,6 @@ export const Chat = ({ hasConfig, isActive }: ChatProps) => {
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [messageValue, onFormSubmit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (messagesContainerRef.current && latestUserMsgRef.current) {
|
||||
const container = messagesContainerRef.current;
|
||||
const userMsg = latestUserMsgRef.current;
|
||||
const containerPadding = 16; // p-4 in Tailwind = 16px
|
||||
container.scrollTop =
|
||||
userMsg.offsetTop - container.offsetTop - containerPadding;
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
const suggestedActions: SuggestedAction[] = [
|
||||
{
|
||||
title: "Are there any exposed S3",
|
||||
|
||||
@@ -108,7 +108,7 @@ export const ColumnGetScans: ColumnDef<ScanProps>[] = [
|
||||
return (
|
||||
<TableLink
|
||||
href={`/findings?filter[scan__in]=${id}&filter[status__in]=FAIL`}
|
||||
isDisabled={!["completed", "executing"].includes(scanState)}
|
||||
isDisabled={scanState !== "completed"}
|
||||
label="See Findings"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -60,7 +60,8 @@ export const getModelParams = (config: any): ModelParams => {
|
||||
|
||||
if (modelId.startsWith("gpt-5")) {
|
||||
params.temperature = undefined;
|
||||
params.reasoningEffort = "minimal";
|
||||
params.reasoningEffort = "minimal" as const;
|
||||
params.maxTokens = undefined;
|
||||
}
|
||||
|
||||
return params;
|
||||
|
||||
Reference in New Issue
Block a user