import logging from datetime import UTC, datetime, timedelta from types import SimpleNamespace from unittest.mock import MagicMock, patch from uuid import uuid4 import pytest from allauth.socialaccount.models import SocialLogin from api.attack_paths import ( AttackPathsQueryDefinition, AttackPathsQueryParameterDefinition, ) from api.db_utils import rls_transaction from api.models import ( AttackPathsScan, AttackSurfaceOverview, ComplianceOverview, ComplianceRequirementOverview, Finding, Integration, IntegrationProviderRelationship, Invitation, LighthouseConfiguration, Membership, MuteRule, Processor, Provider, ProviderComplianceScore, ProviderGroup, ProviderSecret, Resource, ResourceTag, ResourceTagMapping, Role, SAMLConfiguration, SAMLDomainIndex, Scan, ScanCategorySummary, ScanGroupSummary, ScanSummary, StateChoices, StatusChoices, Task, TenantAPIKey, TenantComplianceSummary, User, UserRoleRelationship, ) from api.rls import Tenant from api.v1.serializers import TokenSerializer from django.conf import settings from django.db import connection as django_connection from django.db import connections as django_connections from django.test import Client from django.urls import reverse from django_celery_results.models import TaskResult from prowler.lib.check.models import Severity from prowler.lib.outputs.finding import Status from rest_framework import status from rest_framework.test import APIClient from rest_framework_simplejwt.tokens import AccessToken from tasks.jobs.backfill import ( aggregate_scan_category_summaries, aggregate_scan_resource_group_summaries, backfill_resource_scan_summaries, ) TODAY = str(datetime.today().date()) API_JSON_CONTENT_TYPE = "application/vnd.api+json" NO_TENANT_HTTP_STATUS = status.HTTP_401_UNAUTHORIZED TEST_USER = "dev@prowler.com" TEST_PASSWORD = "testing_psswd" TEST_ADMIN_ALIAS = "admin" TEST_REPLICA_ALIAS = "test_replica" def _install_compliance_catalog_test_cache() -> None: """Memoize the heavy SDK catalog loaders for the whole test session. ``get_bulk_compliance_frameworks_universal`` re-reads and Pydantic-validates ~100 compliance JSONs (≈20 MB) and ``CheckMetadata.get_bulk`` re-reads ~1k check metadata files on *every* call. Production amortizes this through the per-process lazy caches (``PROWLER_CHECKS`` / ``PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE``) and ``warm_compliance_caches``, but the test suite parametrizes over every provider and deliberately resets the API-level caches, so the same catalogs were re-parsed dozens of times across the suite (≈3s/call locally, ≈19s under coverage in CI). The catalog files are immutable during a run and callers treat the parsed objects as read-only, so caching the result per provider is safe. This is the test-only equivalent of an ``lru_cache`` on the SDK functions, without changing SDK behavior in production. A second, lower-level cache memoizes ``load_compliance_framework_universal`` **per file path**. ``get_bulk_compliance_frameworks_universal`` parses *every* compliance JSON and only then filters by provider, so a per-provider cache still re-parses all ~100 files on the first load of each provider. The per-path cache makes the first provider parse the files once and every other provider/test reuse the already-parsed ``ComplianceFramework`` objects (only the cheap ``listdir`` + filtering re-runs). ``_load_jsons_from_dir`` calls ``load_compliance_framework_universal`` as a module global, so patching the attribute is picked up without touching the SDK. Installed at conftest import time (before test modules are collected) so that even ``from ... import get_bulk_compliance_frameworks_universal`` bindings in the test modules resolve to the cached wrapper. """ import prowler.lib.check.compliance_models as compliance_models from prowler.lib.check.models import CheckMetadata original_bulk_frameworks = ( compliance_models.get_bulk_compliance_frameworks_universal ) original_get_bulk = CheckMetadata.get_bulk original_load = compliance_models.load_compliance_framework_universal def cached_bulk_frameworks(provider): if provider not in _COMPLIANCE_FRAMEWORK_CACHE: _COMPLIANCE_FRAMEWORK_CACHE[provider] = original_bulk_frameworks(provider) return _COMPLIANCE_FRAMEWORK_CACHE[provider] def cached_get_bulk(provider): if provider not in _COMPLIANCE_CHECKS_CACHE: _COMPLIANCE_CHECKS_CACHE[provider] = original_get_bulk(provider) return _COMPLIANCE_CHECKS_CACHE[provider] def cached_load(path): if path not in _COMPLIANCE_PATH_CACHE: _COMPLIANCE_PATH_CACHE[path] = original_load(path) return _COMPLIANCE_PATH_CACHE[path] compliance_models.get_bulk_compliance_frameworks_universal = cached_bulk_frameworks compliance_models.load_compliance_framework_universal = cached_load CheckMetadata.get_bulk = staticmethod(cached_get_bulk) # ``api.compliance`` does ``from ... import get_bulk_compliance_frameworks_universal`` # so it holds its own binding; patch it too in case it was imported first. import api.compliance as api_compliance api_compliance.get_bulk_compliance_frameworks_universal = cached_bulk_frameworks # Module-scoped so the ``_compliance_cache_guard`` fixture below can reset them. # Keeping them out of ``_install_compliance_catalog_test_cache``'s local scope is # what makes the caches resettable between tests; the wrappers above close over # these names, and the original loaders stay referenced so patched behaviour is # still honoured. _COMPLIANCE_FRAMEWORK_CACHE: dict[str, dict] = {} _COMPLIANCE_CHECKS_CACHE: dict[str, dict] = {} _COMPLIANCE_PATH_CACHE: dict[str, object] = {} _install_compliance_catalog_test_cache() @pytest.fixture(autouse=True) def _compliance_cache_guard(request): """Reset the compliance catalog caches after any test that used ``monkeypatch``. The session-wide caches in ``_install_compliance_catalog_test_cache`` let the read-only, parametrized compliance tests parse the ~100 catalog JSONs once instead of dozens of times. A test that swaps a loader (or mutates a returned object) could otherwise leak that state into later tests through the shared dicts. Using ``monkeypatch`` as the opt-in signal keeps the full speed-up for catalog-reading tests while giving patching tests a clean slate afterwards; the next test simply repopulates the caches from disk. """ yield if "monkeypatch" in request.fixturenames: _COMPLIANCE_FRAMEWORK_CACHE.clear() _COMPLIANCE_CHECKS_CACHE.clear() _COMPLIANCE_PATH_CACHE.clear() import api.compliance as api_compliance api_compliance.AVAILABLE_COMPLIANCE_FRAMEWORKS.clear() def today_after_n_days(n_days: int) -> str: return datetime.strftime( datetime.today().date() + timedelta(days=n_days), "%Y-%m-%d" ) @pytest.fixture(scope="module") def enforce_test_user_db_connection(django_db_setup, django_db_blocker): """Ensure tests use the test user for database connections.""" test_user = "test" test_password = "test" with django_db_blocker.unblock(): with django_connection.cursor() as cursor: # Required for testing purposes using APIClient cursor.execute(f"GRANT ALL PRIVILEGES ON django_session TO {test_user};") original_user = settings.DATABASES["default"]["USER"] original_password = settings.DATABASES["default"]["PASSWORD"] django_connections["default"].settings_dict["USER"] = test_user django_connections["default"].settings_dict["PASSWORD"] = test_password django_connections["default"].close() django_connections["default"].connect() yield with django_db_blocker.unblock(): django_connections["default"].settings_dict["USER"] = original_user django_connections["default"].settings_dict["PASSWORD"] = original_password django_connections["default"].close() django_connections["default"].connect() @pytest.fixture(autouse=True) def disable_logging(): logging.disable(logging.CRITICAL) @pytest.fixture(scope="session") def _session_test_user(django_db_setup, django_db_blocker): """Create the test user once per session. Internal; use create_test_user instead.""" with django_db_blocker.unblock(): user = User.objects.create_user( name="testing", email=TEST_USER, password=TEST_PASSWORD, ) return user @pytest.fixture(autouse=True) def create_test_user(_session_test_user, django_db_blocker): """Re-create the session-scoped test user when a TransactionTestCase has truncated the users table.""" with django_db_blocker.unblock(): user = User.objects.filter(pk=_session_test_user.pk).first() if user is None: user = User.objects.create_user( id=_session_test_user.pk, name="testing", email=TEST_USER, password=TEST_PASSWORD, ) return user @pytest.fixture(scope="function") def create_test_user_rbac(django_db_setup, django_db_blocker, tenants_fixture): with django_db_blocker.unblock(): user = User.objects.create_user( name="testing", email="rbac@rbac.com", password=TEST_PASSWORD, ) tenant = tenants_fixture[0] Membership.objects.create( user=user, tenant=tenant, role=Membership.RoleChoices.OWNER, ) Role.objects.create( name="admin", tenant_id=tenant.id, manage_users=True, manage_account=True, manage_billing=True, manage_providers=True, manage_integrations=True, manage_scans=True, unlimited_visibility=True, ) UserRoleRelationship.objects.create( user=user, role=Role.objects.get(name="admin"), tenant_id=tenant.id, ) return user @pytest.fixture(scope="function") def create_test_user_rbac_no_roles(django_db_setup, django_db_blocker, tenants_fixture): with django_db_blocker.unblock(): user = User.objects.create_user( name="testing", email="rbac_noroles@rbac.com", password=TEST_PASSWORD, ) tenant = tenants_fixture[0] Membership.objects.create( user=user, tenant=tenant, role=Membership.RoleChoices.OWNER, ) return user @pytest.fixture(scope="function") def create_test_user_rbac_limited(django_db_setup, django_db_blocker, tenants_fixture): with django_db_blocker.unblock(): user = User.objects.create_user( name="testing_limited", email="rbac_limited@rbac.com", password=TEST_PASSWORD, ) tenant = tenants_fixture[0] Membership.objects.create( user=user, tenant=tenant, role=Membership.RoleChoices.OWNER, ) role = Role.objects.create( name="limited", tenant_id=tenant.id, manage_users=False, manage_account=False, manage_billing=False, manage_providers=False, manage_integrations=False, manage_scans=False, unlimited_visibility=False, ) UserRoleRelationship.objects.create( user=user, role=role, tenant_id=tenant.id, ) return user @pytest.fixture(scope="function") def create_test_user_rbac_manage_account(django_db_setup, django_db_blocker): """User with only manage_account permission (no manage_users).""" with django_db_blocker.unblock(): user = User.objects.create_user( name="testing_manage_account", email="rbac_manage_account@rbac.com", password=TEST_PASSWORD, ) tenant = Tenant.objects.create( name="Tenant Test Manage Account", ) Membership.objects.create( user=user, tenant=tenant, role=Membership.RoleChoices.OWNER, ) role = Role.objects.create( name="manage_account", tenant_id=tenant.id, manage_users=False, manage_account=True, manage_billing=False, manage_providers=False, manage_integrations=False, manage_scans=False, unlimited_visibility=False, ) UserRoleRelationship.objects.create( user=user, role=role, tenant_id=tenant.id, ) return user def first_membership_tenant(user): return user.memberships.order_by("date_joined").first().tenant def access_token_for_tenant(user, tenant): access_token = AccessToken.for_user(user) access_token["tenant_id"] = str(tenant.id) access_token.payload["nbf"] = access_token["iat"] return str(access_token) def authenticate_client_for_tenant(client, user, tenant): client.user = user client.defaults["HTTP_AUTHORIZATION"] = ( f"Bearer {access_token_for_tenant(user, tenant)}" ) return client @pytest.fixture def authenticated_client_for_tenant_factory(): def create_authenticated_client(user, tenant): return authenticate_client_for_tenant(Client(), user, tenant) return create_authenticated_client @pytest.fixture def authenticated_client_rbac_manage_account( create_test_user_rbac_manage_account, client ): return authenticate_client_for_tenant( client, create_test_user_rbac_manage_account, first_membership_tenant(create_test_user_rbac_manage_account), ) @pytest.fixture(scope="function") def create_test_user_rbac_manage_users_only(django_db_setup, django_db_blocker): """User with only manage_users permission (no manage_account).""" with django_db_blocker.unblock(): user = User.objects.create_user( name="testing_manage_users_only", email="rbac_manage_users_only@rbac.com", password=TEST_PASSWORD, ) tenant = Tenant.objects.create(name="Tenant Test Manage Users Only") Membership.objects.create( user=user, tenant=tenant, role=Membership.RoleChoices.OWNER, ) role = Role.objects.create( name="manage_users_only", tenant_id=tenant.id, manage_users=True, manage_account=False, manage_billing=False, manage_providers=False, manage_integrations=False, manage_scans=False, unlimited_visibility=False, ) UserRoleRelationship.objects.create(user=user, role=role, tenant_id=tenant.id) return user @pytest.fixture def authenticated_client_rbac_manage_users_only( create_test_user_rbac_manage_users_only, client ): return authenticate_client_for_tenant( client, create_test_user_rbac_manage_users_only, first_membership_tenant(create_test_user_rbac_manage_users_only), ) @pytest.fixture def authenticated_client_rbac(create_test_user_rbac, tenants_fixture, client): return authenticate_client_for_tenant( client, create_test_user_rbac, tenants_fixture[0] ) @pytest.fixture def authenticated_client_rbac_noroles( create_test_user_rbac_no_roles, tenants_fixture, client ): return authenticate_client_for_tenant( client, create_test_user_rbac_no_roles, tenants_fixture[0] ) @pytest.fixture def authenticated_client_no_permissions_rbac( create_test_user_rbac_limited, tenants_fixture, client ): return authenticate_client_for_tenant( client, create_test_user_rbac_limited, tenants_fixture[0] ) @pytest.fixture def authenticated_client( create_test_user, tenants_fixture, set_user_admin_roles_fixture, client ): return authenticate_client_for_tenant(client, create_test_user, tenants_fixture[0]) @pytest.fixture def authenticated_api_client(create_test_user, tenants_fixture): client = APIClient() serializer = TokenSerializer( data={"type": "tokens", "email": TEST_USER, "password": TEST_PASSWORD} ) serializer.is_valid() access_token = serializer.validated_data["access"] client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}" return client @pytest.fixture def tenants_fixture(create_test_user): user = create_test_user tenant1 = Tenant.objects.create( name="Tenant One", ) Membership.objects.create( user=user, tenant=tenant1, ) tenant2 = Tenant.objects.create( name="Tenant Two", ) Membership.objects.create( user=user, tenant=tenant2, role=Membership.RoleChoices.OWNER, ) tenant3 = Tenant.objects.create( name="Tenant Three", ) return tenant1, tenant2, tenant3 @pytest.fixture def set_user_admin_roles_fixture(create_test_user, tenants_fixture): user = create_test_user for tenant in tenants_fixture[:2]: with rls_transaction(str(tenant.id)): role = Role.objects.create( name="admin", tenant_id=tenant.id, manage_users=True, manage_account=True, manage_billing=True, manage_providers=True, manage_integrations=True, manage_scans=True, unlimited_visibility=True, ) UserRoleRelationship.objects.create( user=user, role=role, tenant_id=tenant.id, ) @pytest.fixture def invitations_fixture(create_test_user, tenants_fixture): user = create_test_user tenant = tenants_fixture[0] valid_invitation = Invitation.objects.create( email="testing@prowler.com", state=Invitation.State.PENDING, token="TESTING1234567", inviter=user, tenant=tenant, ) expired_invitation = Invitation.objects.create( email="testing@prowler.com", state=Invitation.State.EXPIRED, token="TESTING1234568", expires_at=datetime.now(UTC) - timedelta(days=1), inviter=user, tenant=tenant, ) return valid_invitation, expired_invitation @pytest.fixture def users_fixture(django_user_model): user1 = User.objects.create_user( name="user1", email="test_unit0@prowler.com", password="S3cret" ) user2 = User.objects.create_user( name="user2", email="test_unit1@prowler.com", password="S3cret" ) user3 = User.objects.create_user( name="user3", email="test_unit2@prowler.com", password="S3cret" ) return user1, user2, user3 @pytest.fixture def provider_factory(tenants_fixture): tenant = tenants_fixture[0] counters = {} def next_counter(provider): counters[provider] = counters.get(provider, 0) + 1 return counters[provider] def defaults_for(provider, sequence): return { Provider.ProviderChoices.AWS.value: { "uid": f"{123456789011 + sequence:012d}", "alias": f"aws_testing_{sequence}", }, Provider.ProviderChoices.AZURE.value: { "uid": str(uuid4()), "alias": f"azure_testing_{sequence}", "scanner_args": {"key1": "value1", "key2": {"key21": "value21"}}, }, Provider.ProviderChoices.GCP.value: { "uid": f"a12322-test{sequence:05d}", "alias": f"gcp_testing_{sequence}", }, Provider.ProviderChoices.KUBERNETES.value: { "uid": f"kubernetes-test-{sequence}", "alias": f"k8s_testing_{sequence}", }, Provider.ProviderChoices.M365.value: { "uid": f"m365-{sequence}.test.com", "alias": f"m365_testing_{sequence}", }, Provider.ProviderChoices.GITHUB.value: { "uid": f"github-test-{sequence}", "alias": f"github_testing_{sequence}", }, Provider.ProviderChoices.MONGODBATLAS.value: { "uid": f"64b1d3c0e4b03b{sequence:010x}", "alias": f"mongodbatlas_testing_{sequence}", }, Provider.ProviderChoices.IAC.value: { "uid": f"https://github.com/prowler-cloud/test-{sequence}.git", "alias": f"iac_testing_{sequence}", }, Provider.ProviderChoices.ORACLECLOUD.value: { "uid": f"ocid1.tenancy.oc1..aaaaaaaa{sequence:024d}", "alias": f"oci_testing_{sequence}", }, Provider.ProviderChoices.ALIBABACLOUD.value: { "uid": f"{1234567890123455 + sequence:016d}", "alias": f"alibabacloud_testing_{sequence}", }, Provider.ProviderChoices.CLOUDFLARE.value: { "uid": f"{0x1000000000000000000000000000000 + sequence:032x}", "alias": f"cloudflare_testing_{sequence}", }, Provider.ProviderChoices.OPENSTACK.value: { "uid": f"openstack-project-{sequence}", "alias": f"openstack_testing_{sequence}", }, Provider.ProviderChoices.IMAGE.value: { "uid": f"registry.example.com/prowler/test:{sequence}", "alias": f"image_testing_{sequence}", }, Provider.ProviderChoices.GOOGLEWORKSPACE.value: { "uid": f"C{12345677 + sequence}", "alias": f"googleworkspace_testing_{sequence}", }, Provider.ProviderChoices.VERCEL.value: { "uid": f"team_{sequence:016x}", "alias": f"vercel_testing_{sequence}", }, Provider.ProviderChoices.OKTA.value: { "uid": f"acme-{sequence}.okta.com", "alias": f"okta_testing_{sequence}", }, }[provider] def create_provider(provider=Provider.ProviderChoices.AWS.value, **overrides): provider_value = getattr(provider, "value", provider) selected_tenant = overrides.pop("tenant", tenant) sequence = next_counter(provider_value) attributes = { "provider": provider_value, "tenant_id": selected_tenant.id, **defaults_for(provider_value, sequence), } attributes.update(overrides) return Provider.objects.create(**attributes) return create_provider @pytest.fixture def aws_provider(provider_factory): return provider_factory(Provider.ProviderChoices.AWS.value) @pytest.fixture def aws_provider_pair(aws_provider, provider_factory): return ( aws_provider, provider_factory(Provider.ProviderChoices.AWS.value), ) @pytest.fixture def azure_provider(provider_factory): return provider_factory(Provider.ProviderChoices.AZURE.value) @pytest.fixture def gcp_provider(provider_factory): return provider_factory(Provider.ProviderChoices.GCP.value) @pytest.fixture def kubernetes_provider(provider_factory): return provider_factory(Provider.ProviderChoices.KUBERNETES.value) @pytest.fixture def m365_provider(provider_factory): return provider_factory(Provider.ProviderChoices.M365.value) @pytest.fixture def github_provider(provider_factory): return provider_factory(Provider.ProviderChoices.GITHUB.value) @pytest.fixture def mongodbatlas_provider(provider_factory): return provider_factory(Provider.ProviderChoices.MONGODBATLAS.value) @pytest.fixture def iac_provider(provider_factory): return provider_factory(Provider.ProviderChoices.IAC.value) @pytest.fixture def oraclecloud_provider(provider_factory): return provider_factory(Provider.ProviderChoices.ORACLECLOUD.value) @pytest.fixture def alibabacloud_provider(provider_factory): return provider_factory(Provider.ProviderChoices.ALIBABACLOUD.value) @pytest.fixture def cloudflare_provider(provider_factory): return provider_factory(Provider.ProviderChoices.CLOUDFLARE.value) @pytest.fixture def openstack_provider(provider_factory): return provider_factory(Provider.ProviderChoices.OPENSTACK.value) @pytest.fixture def image_provider(provider_factory): return provider_factory(Provider.ProviderChoices.IMAGE.value) @pytest.fixture def googleworkspace_provider(provider_factory): return provider_factory(Provider.ProviderChoices.GOOGLEWORKSPACE.value) @pytest.fixture def vercel_provider(provider_factory): return provider_factory(Provider.ProviderChoices.VERCEL.value) @pytest.fixture def okta_provider(provider_factory): return provider_factory(Provider.ProviderChoices.OKTA.value) @pytest.fixture def all_provider_types_fixture(provider_factory): return tuple( provider_factory(provider_choice.value) for provider_choice in Provider.ProviderChoices ) @pytest.fixture def processor_fixture(tenants_fixture): tenant, *_ = tenants_fixture processor = Processor.objects.create( tenant_id=tenant.id, processor_type="mutelist", configuration="Mutelist:\n Accounts:\n *:\n Checks:\n iam_user_hardware_mfa_enabled:\n " " Regions:\n - *\n Resources:\n - *", ) return processor @pytest.fixture def provider_groups_fixture(tenants_fixture): tenant, *_ = tenants_fixture pgroup1 = ProviderGroup.objects.create( name="Group One", tenant_id=tenant.id, ) pgroup2 = ProviderGroup.objects.create( name="Group Two", tenant_id=tenant.id, ) pgroup3 = ProviderGroup.objects.create( name="Group Three", tenant_id=tenant.id, ) return pgroup1, pgroup2, pgroup3 @pytest.fixture def admin_role_fixture(tenants_fixture): tenant, *_ = tenants_fixture return Role.objects.get_or_create( name="admin", tenant_id=tenant.id, manage_users=True, manage_account=True, manage_billing=True, manage_providers=True, manage_integrations=True, manage_scans=True, unlimited_visibility=True, )[0] @pytest.fixture def roles_fixture(tenants_fixture): tenant, *_ = tenants_fixture role1 = Role.objects.create( name="Role One", tenant_id=tenant.id, manage_users=True, manage_account=True, manage_billing=True, manage_providers=True, manage_integrations=False, manage_scans=True, unlimited_visibility=False, ) role2 = Role.objects.create( name="Role Two", tenant_id=tenant.id, manage_users=False, manage_account=False, manage_billing=False, manage_providers=True, manage_integrations=True, manage_scans=True, unlimited_visibility=True, ) role3 = Role.objects.create( name="Role Three", tenant_id=tenant.id, manage_users=True, manage_account=True, manage_billing=True, manage_providers=True, manage_integrations=True, manage_scans=True, unlimited_visibility=True, ) role4 = Role.objects.create( name="Role Four", tenant_id=tenant.id, manage_users=False, manage_account=False, manage_billing=False, manage_providers=False, manage_integrations=False, manage_scans=False, unlimited_visibility=False, ) return role1, role2, role3, role4 @pytest.fixture def provider_secret_fixture(all_provider_types_fixture): return tuple( ProviderSecret.objects.create( tenant_id=provider.tenant_id, provider=provider, secret_type=ProviderSecret.TypeChoices.STATIC, secret={"key": "value"}, name=provider.alias, ) for provider in all_provider_types_fixture ) @pytest.fixture def scans_fixture(tenants_fixture, aws_provider_pair): tenant, *_ = tenants_fixture provider, provider2 = aws_provider_pair now = datetime.now(UTC) scan1 = Scan.objects.create( name="Scan 1", provider=provider, trigger=Scan.TriggerChoices.MANUAL, state=StateChoices.COMPLETED, tenant_id=tenant.id, started_at=now, completed_at=now, ) scan2 = Scan.objects.create( name="Scan 2", provider=provider2, trigger=Scan.TriggerChoices.SCHEDULED, state=StateChoices.COMPLETED, tenant_id=tenant.id, started_at=now, completed_at=now, ) scan3 = Scan.objects.create( name="Scan 3", provider=provider2, trigger=Scan.TriggerChoices.SCHEDULED, state=StateChoices.AVAILABLE, tenant_id=tenant.id, started_at="2024-01-02T00:00:00Z", ) return scan1, scan2, scan3 @pytest.fixture def tasks_fixture(tenants_fixture): tenant, *_ = tenants_fixture task_runner_task1 = TaskResult.objects.create( task_id="81a1b34b-ff6e-498e-979c-d6a83260167f", task_name="task_runner_task1", task_kwargs='{"kwarg1": "value1"}', status="SUCCESS", ) task_runner_task2 = TaskResult.objects.create( task_id="4d0260a5-2e1f-4a34-a976-8c5acb9f5499", task_name="task_runner_task1", status="PENDING", ) task1 = Task.objects.create( id=task_runner_task1.task_id, task_runner_task=task_runner_task1, tenant_id=tenant.id, ) task2 = Task.objects.create( id=task_runner_task2.task_id, task_runner_task=task_runner_task2, tenant_id=tenant.id, ) return task1, task2 @pytest.fixture def resources_fixture(aws_provider_pair): provider, provider2 = aws_provider_pair tags = [ ResourceTag.objects.create( tenant_id=provider.tenant_id, key="key", value="value", ), ResourceTag.objects.create( tenant_id=provider.tenant_id, key="key2", value="value2", ), ] resource1 = Resource.objects.create( tenant_id=provider.tenant_id, provider=provider, uid="arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0", name="My Instance 1", region="us-east-1", service="ec2", type="prowler-test", groups=["compute"], ) resource1.upsert_or_delete_tags(tags) resource2 = Resource.objects.create( tenant_id=provider.tenant_id, provider=provider, uid="arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef1", name="My Instance 2", region="eu-west-1", service="s3", type="prowler-test", groups=["storage"], ) resource2.upsert_or_delete_tags(tags) resource3 = Resource.objects.create( tenant_id=provider2.tenant_id, provider=provider2, uid="arn:aws:ec2:us-east-1:123456789012:bucket/i-1234567890abcdef2", name="My Bucket 3", region="us-east-1", service="ec2", type="test", groups=["compute"], ) tags = [ ResourceTag.objects.create( tenant_id=provider.tenant_id, key="key3", value="multi word value3", ), ] resource3.upsert_or_delete_tags(tags) return resource1, resource2, resource3 @pytest.fixture def findings_fixture(scans_fixture, resources_fixture): scan, *_ = scans_fixture resource1, resource2, *_ = resources_fixture finding1 = Finding.objects.create( tenant_id=scan.tenant_id, uid="test_finding_uid_1", scan=scan, delta=None, status=Status.FAIL, status_extended="test status extended ", impact=Severity.critical, impact_extended="test impact extended one", severity=Severity.critical, raw_result={ "status": Status.FAIL, "impact": Severity.critical, "severity": Severity.critical, }, tags={"test": "dev-qa"}, check_id="test_check_id", check_metadata={ "CheckId": "test_check_id", "Description": "test description apple sauce", "servicename": "ec2", }, first_seen_at="2024-01-02T00:00:00Z", ) finding1.add_resources([resource1]) finding2 = Finding.objects.create( tenant_id=scan.tenant_id, uid="test_finding_uid_2", scan=scan, delta="new", status=Status.FAIL, status_extended="Load Balancer exposed to internet", impact=Severity.medium, impact_extended="test impact extended two", severity=Severity.medium, raw_result={ "status": Status.FAIL, "impact": Severity.medium, "severity": Severity.medium, }, tags={"test": "test"}, check_id="test_check_id", check_metadata={ "CheckId": "test_check_id", "Description": "test description orange juice", "servicename": "s3", }, first_seen_at="2024-01-02T00:00:00Z", muted=True, ) finding2.add_resources([resource2]) return finding1, finding2 @pytest.fixture def compliance_overviews_fixture(scans_fixture, tenants_fixture): tenant = tenants_fixture[0] scan1, scan2, scan3 = scans_fixture compliance_overview1 = ComplianceOverview.objects.create( tenant=tenant, scan=scan1, compliance_id="aws_account_security_onboarding_aws", framework="AWS-Account-Security-Onboarding", version="1.0", description="Description for AWS Account Security Onboarding", region="eu-west-1", requirements={ "requirement1": { "name": "Requirement 1", "checks": {"check1.1": "PASS", "check1.2": None}, "status": "PASS", "attributes": [], "description": "Description of requirement 1", "checks_status": { "total": 2, "failed": 0, "passed": 2, }, }, "requirement2": { "name": "Requirement 2", "checks": {"check2.1": "PASS", "check2.2": "PASS"}, "status": "PASS", "attributes": [], "description": "Description of requirement 2", "checks_status": { "total": 2, "failed": 0, "passed": 2, }, }, "requirement3": { "name": "Requirement 3 - manual", "checks": {}, "status": "PASS", "attributes": [], "description": "Description of requirement 2", "checks_status": { "total": 0, "failed": 0, "passed": 0, }, }, }, requirements_passed=2, requirements_failed=0, requirements_manual=1, total_requirements=3, ) compliance_overview2 = ComplianceOverview.objects.create( tenant=tenant, scan=scan1, compliance_id="aws_account_security_onboarding_aws", framework="AWS-Account-Security-Onboarding", version="1.0", description="Description for AWS Account Security Onboarding", region="eu-west-2", requirements={ "requirement1": { "name": "Requirement 1", "checks": {"check1.1": "PASS", "check1.2": None}, "status": "PASS", "attributes": [], "description": "Description of requirement 1", "checks_status": { "total": 2, "failed": 0, "passed": 2, }, }, "requirement2": { "name": "Requirement 2", "checks": {"check2.1": "PASS", "check2.2": "FAIL"}, "status": "FAIL", "attributes": [], "description": "Description of requirement 2", "checks_status": { "total": 2, "failed": 1, "passed": 1, }, }, "requirement3": { "name": "Requirement 3 - manual", "checks": {}, "status": "PASS", "attributes": [], "description": "Description of requirement 2", "checks_status": { "total": 0, "failed": 0, "passed": 0, }, }, }, requirements_passed=1, requirements_failed=1, requirements_manual=1, total_requirements=3, ) # Return the created compliance overviews return compliance_overview1, compliance_overview2 @pytest.fixture def compliance_requirements_overviews_fixture(scans_fixture, tenants_fixture): """Fixture for ComplianceRequirementOverview objects used by the new ComplianceOverviewViewSet.""" tenant = tenants_fixture[0] scan1, scan2, scan3 = scans_fixture # Create ComplianceRequirementOverview objects for scan1 requirement_overview1 = ComplianceRequirementOverview.objects.create( tenant=tenant, scan=scan1, compliance_id="aws_account_security_onboarding_aws", framework="AWS-Account-Security-Onboarding", version="1.0", description="Description for AWS Account Security Onboarding", region="eu-west-1", requirement_id="requirement1", requirement_status=StatusChoices.PASS, passed_checks=2, failed_checks=0, total_checks=2, ) requirement_overview2 = ComplianceRequirementOverview.objects.create( tenant=tenant, scan=scan1, compliance_id="aws_account_security_onboarding_aws", framework="AWS-Account-Security-Onboarding", version="1.0", description="Description for AWS Account Security Onboarding", region="eu-west-1", requirement_id="requirement2", requirement_status=StatusChoices.PASS, passed_checks=2, failed_checks=0, total_checks=2, ) requirement_overview3 = ComplianceRequirementOverview.objects.create( tenant=tenant, scan=scan1, compliance_id="aws_account_security_onboarding_aws", framework="AWS-Account-Security-Onboarding", version="1.0", description="Description for AWS Account Security Onboarding", region="eu-west-2", requirement_id="requirement1", requirement_status=StatusChoices.PASS, passed_checks=2, failed_checks=0, total_checks=2, ) requirement_overview4 = ComplianceRequirementOverview.objects.create( tenant=tenant, scan=scan1, compliance_id="aws_account_security_onboarding_aws", framework="AWS-Account-Security-Onboarding", version="1.0", description="Description for AWS Account Security Onboarding", region="eu-west-2", requirement_id="requirement2", requirement_status=StatusChoices.FAIL, passed_checks=1, failed_checks=1, total_checks=2, ) requirement_overview5 = ComplianceRequirementOverview.objects.create( tenant=tenant, scan=scan1, compliance_id="aws_account_security_onboarding_aws", framework="AWS-Account-Security-Onboarding", version="1.0", description="Description for AWS Account Security Onboarding (MANUAL)", region="eu-west-2", requirement_id="requirement3", requirement_status=StatusChoices.MANUAL, passed_checks=0, failed_checks=0, total_checks=0, ) # Create a different compliance framework for testing requirement_overview6 = ComplianceRequirementOverview.objects.create( tenant=tenant, scan=scan1, compliance_id="cis_1.4_aws", framework="CIS-1.4-AWS", version="1.4", description="CIS AWS Foundations Benchmark v1.4.0", region="eu-west-1", requirement_id="cis_requirement1", requirement_status=StatusChoices.FAIL, passed_checks=0, failed_checks=3, total_checks=3, ) # Create another compliance framework for testing MITRE ATT&CK requirement_overview7 = ComplianceRequirementOverview.objects.create( tenant=tenant, scan=scan1, compliance_id="mitre_attack_aws", framework="MITRE-ATTACK", version="1.0", description="MITRE ATT&CK", region="eu-west-1", requirement_id="mitre_requirement1", requirement_status=StatusChoices.FAIL, passed_checks=0, failed_checks=0, total_checks=0, ) return ( requirement_overview1, requirement_overview2, requirement_overview3, requirement_overview4, requirement_overview5, requirement_overview6, requirement_overview7, ) def get_api_tokens( api_client, user_email: str, user_password: str, tenant_id: str = None ) -> tuple[str, str]: json_body = { "data": { "type": "tokens", "attributes": { "email": user_email, "password": user_password, }, } } if tenant_id is not None: json_body["data"]["attributes"]["tenant_id"] = tenant_id response = api_client.post( reverse("token-obtain"), data=json_body, format="vnd.api+json", ) return ( response.json()["data"]["attributes"]["access"], response.json()["data"]["attributes"]["refresh"], ) @pytest.fixture def scan_summaries_fixture(tenants_fixture, aws_provider): tenant = tenants_fixture[0] provider = aws_provider scan = Scan.objects.create( name="overview scan", provider=provider, trigger=Scan.TriggerChoices.MANUAL, state=StateChoices.COMPLETED, tenant=tenant, ) ScanSummary.objects.create( tenant=tenant, check_id="check1", service="service1", severity="high", region="region1", _pass=1, fail=0, muted=2, total=3, new=1, changed=0, unchanged=0, fail_new=0, fail_changed=0, pass_new=1, pass_changed=0, muted_new=2, muted_changed=0, scan=scan, ) ScanSummary.objects.create( tenant=tenant, check_id="check1", service="service1", severity="high", region="region2", _pass=0, fail=1, muted=3, total=4, new=2, changed=0, unchanged=0, fail_new=1, fail_changed=0, pass_new=0, pass_changed=0, muted_new=3, muted_changed=0, scan=scan, ) ScanSummary.objects.create( tenant=tenant, check_id="check2", service="service2", severity="critical", region="region1", _pass=1, fail=0, muted=1, total=2, new=1, changed=0, unchanged=0, fail_new=0, fail_changed=0, pass_new=1, pass_changed=0, muted_new=1, muted_changed=0, scan=scan, ) @pytest.fixture def integrations_fixture(aws_provider_pair): provider1, provider2 = aws_provider_pair tenant_id = provider1.tenant_id integration1 = Integration.objects.create( tenant_id=tenant_id, enabled=True, connected=True, integration_type="amazon_s3", configuration={"key": "value"}, credentials={"psswd": "1234"}, ) IntegrationProviderRelationship.objects.create( tenant_id=tenant_id, integration=integration1, provider=provider1, ) integration2 = Integration.objects.create( tenant_id=tenant_id, enabled=True, connected=True, integration_type="amazon_s3", configuration={"key": "value1"}, credentials={"psswd": "1234"}, ) IntegrationProviderRelationship.objects.create( tenant_id=tenant_id, integration=integration2, provider=provider1, ) IntegrationProviderRelationship.objects.create( tenant_id=tenant_id, integration=integration2, provider=provider2, ) return integration1, integration2 @pytest.fixture def jira_integration_fixture(tenants_fixture): # Jira is a tenant-wide integration: it is not attached to any provider, and its # `domain` is read from the credentials when the integration is serialized tenant_id = tenants_fixture[0].id with rls_transaction(str(tenant_id)): return Integration.objects.create( tenant_id=tenant_id, enabled=True, connected=True, integration_type=Integration.IntegrationChoices.JIRA, configuration={"projects": {"TEST": "Test project"}}, credentials={ "domain": "test", "user_mail": "a@b.com", "api_token": "token", }, ) @pytest.fixture def backfill_scan_metadata_fixture(scans_fixture, findings_fixture): for scan_instance in scans_fixture: tenant_id = scan_instance.tenant_id scan_id = scan_instance.id backfill_resource_scan_summaries(tenant_id=tenant_id, scan_id=scan_id) @pytest.fixture def lighthouse_config_fixture(authenticated_client, tenants_fixture): return LighthouseConfiguration.objects.create( tenant_id=tenants_fixture[0].id, name="OpenAI", api_key_decoded="sk-fake-test-key-for-unit-testing-only", model="gpt-4o", temperature=0, max_tokens=4000, business_context="Test business context", is_active=True, ) @pytest.fixture(scope="function") def latest_scan_finding(authenticated_client, aws_provider, resources_fixture): provider = aws_provider tenant_id = str(aws_provider.tenant_id) resource = resources_fixture[0] scan = Scan.objects.create( name="latest completed scan", provider=provider, trigger=Scan.TriggerChoices.MANUAL, state=StateChoices.COMPLETED, tenant_id=tenant_id, ) finding = Finding.objects.create( tenant_id=tenant_id, uid="test_finding_uid_1", scan=scan, delta="new", status=Status.FAIL, status_extended="test status extended ", impact=Severity.critical, impact_extended="test impact extended one", severity=Severity.critical, raw_result={ "status": Status.FAIL, "impact": Severity.critical, "severity": Severity.critical, }, tags={"test": "dev-qa"}, check_id="test_check_id", check_metadata={ "CheckId": "test_check_id", "Description": "test description apple sauce", }, first_seen_at="2024-01-02T00:00:00Z", ) finding.add_resources([resource]) backfill_resource_scan_summaries(tenant_id, str(scan.id)) return finding @pytest.fixture(scope="function") def findings_with_categories(scans_fixture, resources_fixture): scan = scans_fixture[0] resource = resources_fixture[0] finding = Finding.objects.create( tenant_id=scan.tenant_id, uid="finding_with_categories_1", scan=scan, delta=None, status=Status.FAIL, status_extended="test status", impact=Severity.critical, impact_extended="test impact", severity=Severity.critical, raw_result={"status": Status.FAIL}, check_id="genai_check", check_metadata={"CheckId": "genai_check"}, categories=["gen-ai", "security"], first_seen_at="2024-01-02T00:00:00Z", ) finding.add_resources([resource]) backfill_resource_scan_summaries(str(scan.tenant_id), str(scan.id)) return finding @pytest.fixture(scope="function") def findings_with_multiple_categories(scans_fixture, resources_fixture): scan = scans_fixture[0] resource1, resource2 = resources_fixture[:2] finding1 = Finding.objects.create( tenant_id=scan.tenant_id, uid="finding_multi_cat_1", scan=scan, delta=None, status=Status.FAIL, status_extended="test status", impact=Severity.critical, impact_extended="test impact", severity=Severity.critical, raw_result={"status": Status.FAIL}, check_id="genai_check", check_metadata={"CheckId": "genai_check"}, categories=["gen-ai", "security"], first_seen_at="2024-01-02T00:00:00Z", ) finding1.add_resources([resource1]) finding2 = Finding.objects.create( tenant_id=scan.tenant_id, uid="finding_multi_cat_2", scan=scan, delta=None, status=Status.FAIL, status_extended="test status 2", impact=Severity.high, impact_extended="test impact 2", severity=Severity.high, raw_result={"status": Status.FAIL}, check_id="iam_check", check_metadata={"CheckId": "iam_check"}, categories=["iam", "security"], first_seen_at="2024-01-02T00:00:00Z", ) finding2.add_resources([resource2]) backfill_resource_scan_summaries(str(scan.tenant_id), str(scan.id)) return finding1, finding2 @pytest.fixture(scope="function") def latest_scan_finding_with_categories( authenticated_client, aws_provider, resources_fixture ): provider = aws_provider tenant_id = str(aws_provider.tenant_id) resource = resources_fixture[0] scan = Scan.objects.create( name="latest completed scan with categories", provider=provider, trigger=Scan.TriggerChoices.MANUAL, state=StateChoices.COMPLETED, tenant_id=tenant_id, ) finding = Finding.objects.create( tenant_id=tenant_id, uid="latest_finding_with_categories", scan=scan, delta="new", status=Status.FAIL, status_extended="test status", impact=Severity.critical, impact_extended="test impact", severity=Severity.critical, raw_result={"status": Status.FAIL}, check_id="genai_iam_check", check_metadata={"CheckId": "genai_iam_check"}, categories=["gen-ai", "iam"], resource_groups="ai_ml", first_seen_at="2024-01-02T00:00:00Z", ) finding.add_resources([resource]) backfill_resource_scan_summaries(tenant_id, str(scan.id)) aggregate_scan_category_summaries(tenant_id, str(scan.id)) aggregate_scan_resource_group_summaries(tenant_id, str(scan.id)) return finding @pytest.fixture(scope="function") def latest_scan_resource(authenticated_client, aws_provider): provider = aws_provider tenant_id = str(aws_provider.tenant_id) scan = Scan.objects.create( name="latest completed scan for resource", provider=provider, trigger=Scan.TriggerChoices.MANUAL, state=StateChoices.COMPLETED, tenant_id=tenant_id, ) resource = Resource.objects.create( tenant_id=tenant_id, provider=provider, uid="latest_resource_uid", name="Latest Resource", region="us-east-1", service="ec2", type="instance", metadata='{"test": "metadata"}', details='{"test": "details"}', ) resource_tag = ResourceTag.objects.create( tenant_id=tenant_id, key="environment", value="test", ) ResourceTagMapping.objects.create( tenant_id=tenant_id, resource=resource, tag=resource_tag, ) finding = Finding.objects.create( tenant_id=tenant_id, uid="test_finding_uid_latest", scan=scan, delta="new", status=Status.FAIL, status_extended="test status extended ", impact=Severity.critical, impact_extended="test impact extended", severity=Severity.critical, raw_result={ "status": Status.FAIL, "impact": Severity.critical, "severity": Severity.critical, }, tags={"test": "latest"}, check_id="test_check_id_latest", check_metadata={ "CheckId": "test_check_id_latest", "Description": "test description latest", }, first_seen_at="2024-01-02T00:00:00Z", ) finding.add_resources([resource]) backfill_resource_scan_summaries(tenant_id, str(scan.id)) return resource @pytest.fixture def saml_setup(tenants_fixture): tenant_id = tenants_fixture[0].id domain = "prowler.com" SAMLDomainIndex.objects.create(email_domain=domain, tenant_id=tenant_id) metadata_xml = """ TEST urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress """ SAMLConfiguration.objects.create( tenant_id=str(tenant_id), email_domain=domain, metadata_xml=metadata_xml, ) return { "email": f"user@{domain}", "domain": domain, "tenant_id": tenant_id, } @pytest.fixture def saml_sociallogin(users_fixture): user = users_fixture[0] user.email = "samlsso@acme.com" extra_data = { "firstName": ["Test"], "lastName": ["User"], "organization": ["Prowler"], "userType": ["member"], } account = MagicMock() account.provider = "saml" account.extra_data = extra_data sociallogin = MagicMock(spec=SocialLogin) sociallogin.account = account sociallogin.user = user return sociallogin @pytest.fixture def api_keys_fixture(tenants_fixture, create_test_user): """Create test API keys for testing.""" tenant = tenants_fixture[0] user = create_test_user # Create and assign role to user for API key authentication role = Role.objects.create( tenant_id=tenant.id, name="Test API Key Role", unlimited_visibility=True, manage_account=True, ) UserRoleRelationship.objects.create( user=user, role=role, tenant_id=tenant.id, ) # Create API keys with different states api_key1, raw_key1 = TenantAPIKey.objects.create_api_key( name="Test API Key 1", tenant_id=tenant.id, entity=user, ) api_key2, raw_key2 = TenantAPIKey.objects.create_api_key( name="Test API Key 2", tenant_id=tenant.id, entity=user, expiry_date=datetime.now(UTC) + timedelta(days=60), ) # Revoked API key api_key3, raw_key3 = TenantAPIKey.objects.create_api_key( name="Revoked API Key", tenant_id=tenant.id, entity=user, ) api_key3.revoked = True api_key3.save() # Store raw keys on instances for testing api_key1._raw_key = raw_key1 api_key2._raw_key = raw_key2 api_key3._raw_key = raw_key3 return [api_key1, api_key2, api_key3] @pytest.fixture def mute_rules_fixture(tenants_fixture, create_test_user, findings_fixture): """Create test mute rules for testing.""" tenant = tenants_fixture[0] user = create_test_user # Create two mute rules: one enabled, one disabled mute_rule1 = MuteRule.objects.create( tenant_id=tenant.id, name="Test Rule 1", reason="Security exception for testing", enabled=True, created_by=user, finding_uids=[findings_fixture[0].uid], ) mute_rule2 = MuteRule.objects.create( tenant_id=tenant.id, name="Test Rule 2", reason="Compliance exception approved", enabled=False, created_by=user, finding_uids=[findings_fixture[1].uid], ) return mute_rule1, mute_rule2 @pytest.fixture def create_attack_paths_scan(): """Factory fixture to create Attack Paths scans for tests.""" def _create( provider, *, scan=None, state=StateChoices.COMPLETED, progress=0, **extra_fields, ): scan_instance = scan or Scan.objects.create( name=extra_fields.pop("scan_name", "Attack Paths Supporting Scan"), provider=provider, trigger=Scan.TriggerChoices.MANUAL, state=extra_fields.pop("scan_state", StateChoices.COMPLETED), tenant_id=provider.tenant_id, ) payload = { "tenant_id": provider.tenant_id, "provider": provider, "scan": scan_instance, "state": state, "progress": progress, } payload.update(extra_fields) return AttackPathsScan.objects.create(**payload) return _create @pytest.fixture def attack_paths_query_definition_factory(): """Factory fixture for building Attack Paths query definitions.""" def _create(**overrides): cast_type = overrides.pop("cast_type", str) parameters = overrides.pop( "parameters", [ AttackPathsQueryParameterDefinition( name="limit", label="Limit", cast=cast_type, ) ], ) definition_payload = { "id": "aws-test", "name": "Attack Paths Test Query", "short_description": "Synthetic short description for tests.", "description": "Synthetic Attack Paths definition for tests.", "provider": "aws", "cypher": "RETURN 1", "parameters": parameters, } definition_payload.update(overrides) return AttackPathsQueryDefinition(**definition_payload) return _create @pytest.fixture def sink_backend_stub(): """Install a stub `SinkDatabase` into the sink factory for the test's duration. The sink factory caches a process-wide backend and lazily initializes it against `settings.DATABASES["neo4j"]` / `["neptune"]`. Tests that don't want to stand up a real Bolt driver can yield this fixture's mock and configure its return values directly: sink_backend_stub.execute_read_query.return_value = some_graph Both the active backend and the secondary-backend cache are restored on teardown so tests stay isolated. """ from api.attack_paths.sink import factory from api.attack_paths.sink.base import SinkDatabase stub = MagicMock(spec=SinkDatabase) previous_backend = factory._backend previous_secondary = dict(factory._secondary_backends) factory._backend = stub factory._secondary_backends.clear() try: yield stub finally: factory._backend = previous_backend factory._secondary_backends.clear() factory._secondary_backends.update(previous_secondary) @pytest.fixture def attack_paths_graph_stub_classes(): """Provide lightweight graph element stubs for Attack Paths serialization tests.""" class AttackPathsNativeValue: def __init__(self, value): self._value = value def to_native(self): return self._value class AttackPathsNode: def __init__(self, element_id, labels, properties): self.element_id = element_id self.labels = labels self._properties = properties class AttackPathsRelationship: def __init__(self, element_id, rel_type, start_node, end_node, properties): self.element_id = element_id self.type = rel_type self.start_node = start_node self.end_node = end_node self._properties = properties return SimpleNamespace( NativeValue=AttackPathsNativeValue, Node=AttackPathsNode, Relationship=AttackPathsRelationship, ) @pytest.fixture def create_attack_surface_overview(): def _create(tenant, scan, attack_surface_type, total=10, failed=5, muted_failed=2): return AttackSurfaceOverview.objects.create( tenant=tenant, scan=scan, attack_surface_type=attack_surface_type, total_findings=total, failed_findings=failed, muted_failed_findings=muted_failed, ) return _create @pytest.fixture def create_scan_category_summary(): def _create( tenant, scan, category, severity, total_findings=10, failed_findings=5, new_failed_findings=2, ): return ScanCategorySummary.objects.create( tenant=tenant, scan=scan, category=category, severity=severity, total_findings=total_findings, failed_findings=failed_findings, new_failed_findings=new_failed_findings, ) return _create @pytest.fixture(scope="function") def findings_with_group(scans_fixture, resources_fixture): scan = scans_fixture[0] resource = resources_fixture[0] finding = Finding.objects.create( tenant_id=scan.tenant_id, uid="finding_with_group_1", scan=scan, delta=None, status=Status.FAIL, status_extended="test status", impact=Severity.critical, impact_extended="test impact", severity=Severity.critical, raw_result={"status": Status.FAIL}, check_id="storage_check", check_metadata={"CheckId": "storage_check"}, resource_groups="storage", first_seen_at="2024-01-02T00:00:00Z", ) finding.add_resources([resource]) backfill_resource_scan_summaries(str(scan.tenant_id), str(scan.id)) return finding @pytest.fixture(scope="function") def findings_with_multiple_groups(scans_fixture, resources_fixture): scan = scans_fixture[0] resource1, resource2 = resources_fixture[:2] finding1 = Finding.objects.create( tenant_id=scan.tenant_id, uid="finding_multi_grp_1", scan=scan, delta=None, status=Status.FAIL, status_extended="test status", impact=Severity.critical, impact_extended="test impact", severity=Severity.critical, raw_result={"status": Status.FAIL}, check_id="storage_check", check_metadata={"CheckId": "storage_check"}, resource_groups="storage", first_seen_at="2024-01-02T00:00:00Z", ) finding1.add_resources([resource1]) finding2 = Finding.objects.create( tenant_id=scan.tenant_id, uid="finding_multi_grp_2", scan=scan, delta=None, status=Status.FAIL, status_extended="test status 2", impact=Severity.high, impact_extended="test impact 2", severity=Severity.high, raw_result={"status": Status.FAIL}, check_id="security_check", check_metadata={"CheckId": "security_check"}, resource_groups="security", first_seen_at="2024-01-02T00:00:00Z", ) finding2.add_resources([resource2]) backfill_resource_scan_summaries(str(scan.tenant_id), str(scan.id)) return finding1, finding2 @pytest.fixture def create_scan_resource_group_summary(): def _create( tenant, scan, resource_group, severity, total_findings=10, failed_findings=5, new_failed_findings=2, resources_count=3, ): return ScanGroupSummary.objects.create( tenant=tenant, scan=scan, resource_group=resource_group, severity=severity, total_findings=total_findings, failed_findings=failed_findings, new_failed_findings=new_failed_findings, resources_count=resources_count, ) return _create def get_authorization_header(access_token: str) -> dict: return {"Authorization": f"Bearer {access_token}"} @pytest.fixture def provider_compliance_scores_fixture( tenants_fixture, aws_provider_pair, scans_fixture ): """Create ProviderComplianceScore entries for compliance watchlist tests.""" tenant = tenants_fixture[0] provider1, provider2 = aws_provider_pair scan1, _, scan3 = scans_fixture scan1.completed_at = datetime.now(UTC) - timedelta(hours=1) scan1.save() scan3.state = StateChoices.COMPLETED scan3.completed_at = datetime.now(UTC) scan3.save() scores = [ ProviderComplianceScore.objects.create( tenant_id=tenant.id, provider=provider1, scan=scan1, compliance_id="aws_cis_2.0", requirement_id="req_1", requirement_status=StatusChoices.PASS, scan_completed_at=scan1.completed_at, ), ProviderComplianceScore.objects.create( tenant_id=tenant.id, provider=provider1, scan=scan1, compliance_id="aws_cis_2.0", requirement_id="req_2", requirement_status=StatusChoices.FAIL, scan_completed_at=scan1.completed_at, ), ProviderComplianceScore.objects.create( tenant_id=tenant.id, provider=provider1, scan=scan1, compliance_id="aws_cis_2.0", requirement_id="req_3", requirement_status=StatusChoices.MANUAL, scan_completed_at=scan1.completed_at, ), ProviderComplianceScore.objects.create( tenant_id=tenant.id, provider=provider2, scan=scan3, compliance_id="aws_cis_2.0", requirement_id="req_1", requirement_status=StatusChoices.FAIL, scan_completed_at=scan3.completed_at, ), ProviderComplianceScore.objects.create( tenant_id=tenant.id, provider=provider2, scan=scan3, compliance_id="aws_cis_2.0", requirement_id="req_2", requirement_status=StatusChoices.PASS, scan_completed_at=scan3.completed_at, ), ProviderComplianceScore.objects.create( tenant_id=tenant.id, provider=provider1, scan=scan1, compliance_id="gdpr_aws", requirement_id="gdpr_req_1", requirement_status=StatusChoices.PASS, scan_completed_at=scan1.completed_at, ), ] return scores @pytest.fixture def tenant_compliance_summary_fixture(tenants_fixture): """Create TenantComplianceSummary entries for compliance watchlist tests.""" tenant = tenants_fixture[0] summaries = [ TenantComplianceSummary.objects.create( tenant_id=tenant.id, compliance_id="aws_cis_2.0", requirements_passed=1, requirements_failed=2, requirements_manual=1, total_requirements=4, ), TenantComplianceSummary.objects.create( tenant_id=tenant.id, compliance_id="gdpr_aws", requirements_passed=5, requirements_failed=0, requirements_manual=2, total_requirements=7, ), ] return summaries @pytest.fixture def finding_groups_fixture(tenants_fixture, scans_fixture, resources_fixture): """ Create a comprehensive set of findings for testing Finding Groups aggregation. Creates findings for multiple check_ids with varying: - Statuses (PASS, FAIL) - Severities (critical, high, medium, low) - Deltas (new, changed, None) - Muted states (True, False) This fixture tests aggregation logic for: - Multiple findings per check_id - Status aggregation (FAIL > PASS > MUTED) - Severity aggregation (max severity) - Provider aggregation (distinct list) - Resource counts - Finding counts (pass, fail, muted, new, changed) """ tenant = tenants_fixture[0] scan1, scan2, *_ = scans_fixture resource1, resource2, *_ = resources_fixture findings = [] # Check 1: s3_bucket_public_access - Multiple FAIL findings (critical) # Should aggregate to: status=FAIL, severity=critical, fail_count=2, pass_count=0 finding1a = Finding.objects.create( tenant_id=tenant.id, uid="fg_s3_check_1a", scan=scan1, delta="new", status=Status.FAIL, status_extended="S3 bucket allows public access", impact=Severity.critical, impact_extended="Critical security risk", severity=Severity.critical, raw_result={"status": Status.FAIL, "severity": Severity.critical}, tags={"env": "prod"}, check_id="s3_bucket_public_access", check_metadata={ "CheckId": "s3_bucket_public_access", "checktitle": "Ensure S3 buckets do not allow public access", "Description": "S3 buckets should be configured to restrict public access.", "resourcegroup": "storage", }, first_seen_at="2024-01-02T00:00:00Z", muted=False, ) finding1a.add_resources([resource1]) findings.append(finding1a) finding1b = Finding.objects.create( tenant_id=tenant.id, uid="fg_s3_check_1b", scan=scan1, delta="changed", status=Status.FAIL, status_extended="S3 bucket allows public read", impact=Severity.high, impact_extended="High security risk", severity=Severity.high, raw_result={"status": Status.FAIL, "severity": Severity.high}, tags={"env": "staging"}, check_id="s3_bucket_public_access", check_metadata={ "CheckId": "s3_bucket_public_access", "checktitle": "Ensure S3 buckets do not allow public access", "Description": "S3 buckets should be configured to restrict public access.", "resourcegroup": "storage", }, first_seen_at="2024-01-03T00:00:00Z", muted=False, ) finding1b.add_resources([resource2]) findings.append(finding1b) # Check 2: ec2_instance_public_ip - Mixed PASS/FAIL (high severity max) # Should aggregate to: status=FAIL, severity=high, fail_count=1, pass_count=1 finding2a = Finding.objects.create( tenant_id=tenant.id, uid="fg_ec2_check_2a", scan=scan1, delta=None, status=Status.PASS, status_extended="EC2 instance has no public IP", impact=Severity.medium, impact_extended="Medium risk", severity=Severity.medium, raw_result={"status": Status.PASS, "severity": Severity.medium}, tags={"env": "dev"}, check_id="ec2_instance_public_ip", check_metadata={ "CheckId": "ec2_instance_public_ip", "checktitle": "Ensure EC2 instances do not have public IPs", "Description": "EC2 instances should use private IPs only.", }, first_seen_at="2024-01-04T00:00:00Z", muted=False, ) finding2a.add_resources([resource1]) findings.append(finding2a) finding2b = Finding.objects.create( tenant_id=tenant.id, uid="fg_ec2_check_2b", scan=scan1, delta="new", status=Status.FAIL, status_extended="EC2 instance has public IP assigned", impact=Severity.high, impact_extended="High risk", severity=Severity.high, raw_result={"status": Status.FAIL, "severity": Severity.high}, tags={"env": "prod"}, check_id="ec2_instance_public_ip", check_metadata={ "CheckId": "ec2_instance_public_ip", "checktitle": "Ensure EC2 instances do not have public IPs", "Description": "EC2 instances should use private IPs only.", }, first_seen_at="2024-01-05T00:00:00Z", muted=False, ) finding2b.add_resources([resource2]) findings.append(finding2b) # Check 3: iam_password_policy - All PASS (low severity) # Should aggregate to: status=PASS, severity=low, fail_count=0, pass_count=2 finding3a = Finding.objects.create( tenant_id=tenant.id, uid="fg_iam_check_3a", scan=scan1, delta=None, status=Status.PASS, status_extended="Password policy is compliant", impact=Severity.low, impact_extended="Low risk", severity=Severity.low, raw_result={"status": Status.PASS, "severity": Severity.low}, tags={"env": "prod"}, check_id="iam_password_policy", check_metadata={ "CheckId": "iam_password_policy", "checktitle": "Ensure IAM password policy is strong", "Description": "IAM password policy should enforce complexity.", }, first_seen_at="2024-01-06T00:00:00Z", muted=False, ) finding3a.add_resources([resource1]) findings.append(finding3a) finding3b = Finding.objects.create( tenant_id=tenant.id, uid="fg_iam_check_3b", scan=scan1, delta=None, status=Status.PASS, status_extended="Password policy meets requirements", impact=Severity.low, impact_extended="Low risk", severity=Severity.low, raw_result={"status": Status.PASS, "severity": Severity.low}, tags={"env": "staging"}, check_id="iam_password_policy", check_metadata={ "CheckId": "iam_password_policy", "checktitle": "Ensure IAM password policy is strong", "Description": "IAM password policy should enforce complexity.", }, first_seen_at="2024-01-07T00:00:00Z", muted=False, ) finding3b.add_resources([resource2]) findings.append(finding3b) # Check 4: rds_encryption - All muted (medium severity) # Should aggregate to: status=MUTED, severity=medium, fail_count=0, pass_count=0, muted_count=2 finding4a = Finding.objects.create( tenant_id=tenant.id, uid="fg_rds_check_4a", scan=scan1, delta=None, status=Status.FAIL, status_extended="RDS instance not encrypted", impact=Severity.medium, impact_extended="Medium risk", severity=Severity.medium, raw_result={"status": Status.FAIL, "severity": Severity.medium}, tags={"env": "dev"}, check_id="rds_encryption", check_metadata={ "CheckId": "rds_encryption", "checktitle": "Ensure RDS instances are encrypted", "Description": "RDS instances should use encryption at rest.", }, first_seen_at="2024-01-08T00:00:00Z", muted=True, ) finding4a.add_resources([resource1]) findings.append(finding4a) finding4b = Finding.objects.create( tenant_id=tenant.id, uid="fg_rds_check_4b", scan=scan1, delta=None, status=Status.FAIL, status_extended="RDS encryption disabled", impact=Severity.medium, impact_extended="Medium risk", severity=Severity.medium, raw_result={"status": Status.FAIL, "severity": Severity.medium}, tags={"env": "test"}, check_id="rds_encryption", check_metadata={ "CheckId": "rds_encryption", "checktitle": "Ensure RDS instances are encrypted", "Description": "RDS instances should use encryption at rest.", }, first_seen_at="2024-01-09T00:00:00Z", muted=True, ) finding4b.add_resources([resource2]) findings.append(finding4b) # Check 5: cloudtrail_enabled - Multiple providers (from scan2 which uses provider2) # Should aggregate to: impacted_providers contains both provider types finding5 = Finding.objects.create( tenant_id=tenant.id, uid="fg_cloudtrail_check_5", scan=scan2, delta="new", status=Status.FAIL, status_extended="CloudTrail not enabled", impact=Severity.critical, impact_extended="Critical risk", severity=Severity.critical, raw_result={"status": Status.FAIL, "severity": Severity.critical}, tags={"env": "prod"}, check_id="cloudtrail_enabled", check_metadata={ "CheckId": "cloudtrail_enabled", "checktitle": "Ensure CloudTrail is enabled", "Description": "CloudTrail should be enabled for audit logging.", }, first_seen_at="2024-01-10T00:00:00Z", muted=False, ) finding5.add_resources([resource1]) findings.append(finding5) # Aggregate findings into FindingGroupDailySummary for the endpoint to read from tasks.jobs.scan import aggregate_finding_group_summaries aggregate_finding_group_summaries( tenant_id=str(tenant.id), scan_id=str(scan1.id), ) aggregate_finding_group_summaries( tenant_id=str(tenant.id), scan_id=str(scan2.id), ) return findings @pytest.fixture def finding_groups_title_variants_fixture( tenants_fixture, scans_fixture, resources_fixture ): """ Two providers report the same check_id with different checktitle values. Simulates a Prowler version upgrade where the check title changed but the check_id stayed the same. Used to verify that check_title__icontains resolves to check_id first, so results include all providers regardless of which title variant matches the search term. """ tenant = tenants_fixture[0] scan1, scan2, *_ = scans_fixture resource1, resource2, *_ = resources_fixture findings = [] # Provider 1 — OLD title variant finding_old = Finding.objects.create( tenant_id=tenant.id, uid="fg_title_variant_old", scan=scan1, delta="new", status=Status.FAIL, status_extended="Secret scanning not enabled", impact=Severity.high, impact_extended="High risk", severity=Severity.high, raw_result={"status": Status.FAIL, "severity": Severity.high}, tags={}, check_id="github_secret_scanning_enabled", check_metadata={ "CheckId": "github_secret_scanning_enabled", "checktitle": "Ensure repository has secret scanning enabled", "Description": "Checks if secret scanning is enabled.", }, first_seen_at="2024-01-01T00:00:00Z", muted=False, ) finding_old.add_resources([resource1]) findings.append(finding_old) # Provider 2 — NEW title variant (same check_id, different checktitle) finding_new = Finding.objects.create( tenant_id=tenant.id, uid="fg_title_variant_new", scan=scan2, delta="new", status=Status.FAIL, status_extended="Secret scanning not enabled on repo", impact=Severity.high, impact_extended="High risk", severity=Severity.high, raw_result={"status": Status.FAIL, "severity": Severity.high}, tags={}, check_id="github_secret_scanning_enabled", check_metadata={ "CheckId": "github_secret_scanning_enabled", "checktitle": "Check if secret scanning is enabled in GitHub", "Description": "Checks if secret scanning is enabled.", }, first_seen_at="2024-01-02T00:00:00Z", muted=False, ) finding_new.add_resources([resource2]) findings.append(finding_new) from tasks.jobs.scan import aggregate_finding_group_summaries aggregate_finding_group_summaries( tenant_id=str(tenant.id), scan_id=str(scan1.id), ) aggregate_finding_group_summaries( tenant_id=str(tenant.id), scan_id=str(scan2.id), ) return findings def _ensure_mirrored_test_alias(alias: str) -> None: default_database = settings.DATABASES["default"] if alias not in settings.DATABASES: settings.DATABASES[alias] = { **default_database, "TEST": { **default_database.get("TEST", {}), "MIRROR": "default", }, } django_connections.databases[alias] = settings.DATABASES[alias] def pytest_collection_modifyitems(items): """Ensure test_rbac.py is executed first.""" items.sort(key=lambda item: 0 if "test_rbac.py" in item.nodeid else 1) if any(item.get_closest_marker("requires_test_admin_alias") for item in items): _ensure_mirrored_test_alias(TEST_ADMIN_ALIAS) if any(item.get_closest_marker("requires_test_replica_alias") for item in items): _ensure_mirrored_test_alias(TEST_REPLICA_ALIAS) def pytest_configure(config): config.addinivalue_line( "markers", "requires_test_admin_alias: creates a test-only admin alias mirrored " "to default", ) config.addinivalue_line( "markers", "requires_test_replica_alias: creates a test-only replica alias mirrored " "to default", ) # Apply the mock before the test session starts. This is necessary to avoid admin error when running the # 0004_rbac_missing_admin_roles migration patch("api.db_router.MainRouter.admin_db", new="default").start() def pytest_unconfigure(config): # Stop all patches after the test session ends. This is necessary to avoid admin error when running the # 0004_rbac_missing_admin_roles migration patch.stopall()