import logging from datetime import datetime, timedelta, timezone from types import SimpleNamespace from unittest.mock import MagicMock, patch import pytest from allauth.socialaccount.models import SocialLogin from django.conf import settings from django.db import connection as django_connection from django.db import connections as django_connections from django.urls import reverse from django_celery_results.models import TaskResult from rest_framework import status from rest_framework.test import APIClient from tasks.jobs.backfill import ( backfill_resource_scan_summaries, aggregate_scan_category_summaries, aggregate_scan_resource_group_summaries, ) 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 prowler.lib.check.models import Severity from prowler.lib.outputs.finding import Status 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" 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(): if not User.objects.filter(pk=_session_test_user.pk).exists(): User.objects.create_user( id=_session_test_user.pk, name="testing", email=TEST_USER, password=TEST_PASSWORD, ) return _session_test_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 @pytest.fixture def authenticated_client_rbac_manage_account( create_test_user_rbac_manage_account, tenants_fixture, client ): client.user = create_test_user_rbac_manage_account serializer = TokenSerializer( data={ "type": "tokens", "email": "rbac_manage_account@rbac.com", "password": TEST_PASSWORD, } ) serializer.is_valid() access_token = serializer.validated_data["access"] client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}" return client @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 ): client.user = create_test_user_rbac_manage_users_only serializer = TokenSerializer( data={ "type": "tokens", "email": "rbac_manage_users_only@rbac.com", "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 authenticated_client_rbac(create_test_user_rbac, tenants_fixture, client): client.user = create_test_user_rbac tenant_id = tenants_fixture[0].id serializer = TokenSerializer( data={ "type": "tokens", "email": "rbac@rbac.com", "password": TEST_PASSWORD, "tenant_id": tenant_id, } ) serializer.is_valid(raise_exception=True) access_token = serializer.validated_data["access"] client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}" return client @pytest.fixture def authenticated_client_rbac_noroles( create_test_user_rbac_no_roles, tenants_fixture, client ): client.user = create_test_user_rbac_no_roles serializer = TokenSerializer( data={ "type": "tokens", "email": "rbac_noroles@rbac.com", "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 authenticated_client_no_permissions_rbac( create_test_user_rbac_limited, tenants_fixture, client ): client.user = create_test_user_rbac_limited serializer = TokenSerializer( data={ "type": "tokens", "email": "rbac_limited@rbac.com", "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 authenticated_client( create_test_user, tenants_fixture, set_user_admin_roles_fixture, client ): client.user = create_test_user 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 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(timezone.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 providers_fixture(tenants_fixture): tenant, *_ = tenants_fixture provider1 = Provider.objects.create( provider="aws", uid="123456789012", alias="aws_testing_1", tenant_id=tenant.id, ) provider2 = Provider.objects.create( provider="aws", uid="123456789013", alias="aws_testing_2", tenant_id=tenant.id, ) provider3 = Provider.objects.create( provider="gcp", uid="a12322-test321", alias="gcp_testing", tenant_id=tenant.id, ) provider4 = Provider.objects.create( provider="kubernetes", uid="kubernetes-test-12345", alias="k8s_testing", tenant_id=tenant.id, ) provider5 = Provider.objects.create( provider="azure", uid="37b065f8-26b0-4218-a665-0b23d07b27d9", alias="azure_testing", tenant_id=tenant.id, scanner_args={"key1": "value1", "key2": {"key21": "value21"}}, ) provider6 = Provider.objects.create( provider="m365", uid="m365.test.com", alias="m365_testing", tenant_id=tenant.id, ) provider7 = Provider.objects.create( provider="oraclecloud", uid="ocid1.tenancy.oc1..aaaaaaaa3dwoazoox4q7wrvriywpokp5grlhgnkwtyt6dmwyou7no6mdmzda", alias="oci_testing", tenant_id=tenant.id, ) provider8 = Provider.objects.create( provider="mongodbatlas", uid="64b1d3c0e4b03b1234567890", alias="mongodbatlas_testing", tenant_id=tenant.id, ) provider9 = Provider.objects.create( provider="alibabacloud", uid="1234567890123456", alias="alibabacloud_testing", tenant_id=tenant.id, ) provider10 = Provider.objects.create( provider="cloudflare", uid="a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", alias="cloudflare_testing", tenant_id=tenant.id, ) provider11 = Provider.objects.create( provider="openstack", uid="a1b2c3d4-e5f6-7890-abcd-ef1234567890", alias="openstack_testing", tenant_id=tenant.id, ) provider12 = Provider.objects.create( provider="googleworkspace", uid="C12345678", alias="googleworkspace_testing", tenant_id=tenant.id, ) provider13 = Provider.objects.create( provider="vercel", uid="team_abcdef1234567890ab", alias="vercel_testing", tenant_id=tenant.id, ) return ( provider1, provider2, provider3, provider4, provider5, provider6, provider7, provider8, provider9, provider10, provider11, provider12, provider13, ) @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(providers_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 providers_fixture ) @pytest.fixture def scans_fixture(tenants_fixture, providers_fixture): tenant, *_ = tenants_fixture provider, provider2, *_ = providers_fixture now = datetime.now(timezone.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(providers_fixture): provider, *_ = providers_fixture 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=providers_fixture[1].tenant_id, provider=providers_fixture[1], 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, providers_fixture): tenant = tenants_fixture[0] provider = providers_fixture[0] 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(providers_fixture): provider1, provider2, *_ = providers_fixture 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 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, providers_fixture, resources_fixture): provider = providers_fixture[0] tenant_id = str(providers_fixture[0].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, providers_fixture, resources_fixture ): provider = providers_fixture[0] tenant_id = str(providers_fixture[0].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, providers_fixture): provider = providers_fixture[0] tenant_id = str(providers_fixture[0].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(timezone.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 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, providers_fixture, scans_fixture ): """Create ProviderComplianceScore entries for compliance watchlist tests.""" tenant = tenants_fixture[0] provider1, provider2, *_ = providers_fixture scan1, _, scan3 = scans_fixture scan1.completed_at = datetime.now(timezone.utc) - timedelta(hours=1) scan1.save() scan3.state = StateChoices.COMPLETED scan3.completed_at = datetime.now(timezone.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, providers_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] provider1, provider2, *_ = providers_fixture 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, providers_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] provider1, provider2, *_ = providers_fixture 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 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) def pytest_configure(config): # 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()