diff --git a/.github/workflows/find-secrets.yml b/.github/workflows/find-secrets.yml index b39eb8cdc5..0eb7bbd6fe 100644 --- a/.github/workflows/find-secrets.yml +++ b/.github/workflows/find-secrets.yml @@ -11,7 +11,7 @@ jobs: with: fetch-depth: 0 - name: TruffleHog OSS - uses: trufflesecurity/trufflehog@v3.88.2 + uses: trufflesecurity/trufflehog@v3.88.4 with: path: ./ base: ${{ github.event.repository.default_branch }} diff --git a/api/docker-entrypoint.sh b/api/docker-entrypoint.sh index 62ceb69a0f..893d94af21 100755 --- a/api/docker-entrypoint.sh +++ b/api/docker-entrypoint.sh @@ -28,7 +28,7 @@ start_prod_server() { start_worker() { echo "Starting the worker..." - poetry run python -m celery -A config.celery worker -l "${DJANGO_LOGGING_LEVEL:-info}" -Q celery,scans -E + poetry run python -m celery -A config.celery worker -l "${DJANGO_LOGGING_LEVEL:-info}" -Q celery,scans -E --max-tasks-per-child 1 } start_worker_beat() { diff --git a/api/poetry.lock b/api/poetry.lock index ec24e9da8d..491b06933a 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -252,6 +252,20 @@ files = [ [package.dependencies] cryptography = "*" +[[package]] +name = "autopep8" +version = "2.3.2" +description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" +optional = false +python-versions = ">=3.9" +files = [ + {file = "autopep8-2.3.2-py2.py3-none-any.whl", hash = "sha256:ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128"}, + {file = "autopep8-2.3.2.tar.gz", hash = "sha256:89440a4f969197b69a995e4ce0661b031f455a9f776d2c5ba3dbd83466931758"}, +] + +[package.dependencies] +pycodestyle = ">=2.12.0" + [[package]] name = "awsipranges" version = "0.3.3" @@ -1488,6 +1502,23 @@ docs = ["Sphinx (==2.2.0)", "docutils (<0.18)", "sphinx-rtd-theme (==0.4.3)"] publish = ["build (==0.7.0)", "twine (==3.7.1)"] test = ["coveralls (==3.3.0)", "dj-database-url (==0.5.0)", "freezegun (==1.1.0)", "psycopg2 (>=2.8.4,<3.0.0)", "pytest (==6.2.5)", "pytest-benchmark (==3.4.1)", "pytest-cov (==3.0.0)", "pytest-django (==4.4.0)", "pytest-freezegun (==0.4.2)", "pytest-lazy-fixture (==0.6.3)", "snapshottest (==0.6.0)", "tox (==3.24.4)"] +[[package]] +name = "django-silk" +version = "5.3.2" +description = "Silky smooth profiling for the Django Framework" +optional = false +python-versions = ">=3.9" +files = [ + {file = "django_silk-5.3.2-py3-none-any.whl", hash = "sha256:49f1caebfda28b1707f0cfef524e0476beb82b8c5e40f5ccff7f73a6b4f6d3ac"}, + {file = "django_silk-5.3.2.tar.gz", hash = "sha256:b0db54eebedb8d16f572321bd6daccac0bd3f547ae2618bb45d96fe8fc02229d"}, +] + +[package.dependencies] +autopep8 = "*" +Django = ">=4.2" +gprof2dot = ">=2017.09.19" +sqlparse = "*" + [[package]] name = "django-timezone-field" version = "7.1" @@ -1984,6 +2015,17 @@ protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4 [package.extras] grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] +[[package]] +name = "gprof2dot" +version = "2024.6.6" +description = "Generate a dot graph from the output of several profilers." +optional = false +python-versions = ">=3.8" +files = [ + {file = "gprof2dot-2024.6.6-py2.py3-none-any.whl", hash = "sha256:45b14ad7ce64e299c8f526881007b9eb2c6b75505d5613e96e66ee4d5ab33696"}, + {file = "gprof2dot-2024.6.6.tar.gz", hash = "sha256:fa1420c60025a9eb7734f65225b4da02a10fc6dd741b37fa129bc6b41951e5ab"}, +] + [[package]] name = "grapheme" version = "0.6.0" @@ -3603,6 +3645,17 @@ files = [ [package.dependencies] pyasn1 = ">=0.4.6,<0.7.0" +[[package]] +name = "pycodestyle" +version = "2.12.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, + {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, +] + [[package]] name = "pycparser" version = "2.22" @@ -5070,4 +5123,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.13" -content-hash = "af973deaabe32ea4179c44ae702f6a8cc359aabe6f1641dcf2b05534b0bbb3d5" +content-hash = "6465edb36efd1fa6db06d4103fea8046951acc3f4f8b357facaaa34ae2bc74bd" diff --git a/api/pyproject.toml b/api/pyproject.toml index 74d213b0fd..2cccb9ddb3 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -8,7 +8,7 @@ description = "Prowler's API (Django/DRF)" license = "Apache-2.0" name = "prowler-api" package-mode = false -version = "1.3.1" +version = "1.4.0" [tool.poetry.dependencies] celery = {extras = ["pytest"], version = "^5.4.0"} @@ -37,6 +37,7 @@ uuid6 = "2024.7.10" [tool.poetry.group.dev.dependencies] bandit = "1.7.9" coverage = "7.5.4" +django-silk = "5.3.2" docker = "7.1.0" freezegun = "1.5.1" mypy = "1.10.1" diff --git a/api/src/backend/api/db_router.py b/api/src/backend/api/db_router.py index f3a986ac20..939672f88e 100644 --- a/api/src/backend/api/db_router.py +++ b/api/src/backend/api/db_router.py @@ -4,13 +4,17 @@ class MainRouter: def db_for_read(self, model, **hints): # noqa: F841 model_table_name = model._meta.db_table - if model_table_name.startswith("django_"): + if model_table_name.startswith("django_") or model_table_name.startswith( + "silk_" + ): return self.admin_db return None def db_for_write(self, model, **hints): # noqa: F841 model_table_name = model._meta.db_table - if model_table_name.startswith("django_"): + if model_table_name.startswith("django_") or model_table_name.startswith( + "silk_" + ): return self.admin_db return None diff --git a/api/src/backend/api/filters.py b/api/src/backend/api/filters.py index 1042c3aa86..0ace11fe9c 100644 --- a/api/src/backend/api/filters.py +++ b/api/src/backend/api/filters.py @@ -319,26 +319,27 @@ class FindingFilter(FilterSet): field_name="resources__type", lookup_expr="icontains" ) - resource_tag_key = CharFilter(field_name="resources__tags__key") - resource_tag_key__in = CharInFilter( - field_name="resources__tags__key", lookup_expr="in" - ) - resource_tag_key__icontains = CharFilter( - field_name="resources__tags__key", lookup_expr="icontains" - ) - resource_tag_value = CharFilter(field_name="resources__tags__value") - resource_tag_value__in = CharInFilter( - field_name="resources__tags__value", lookup_expr="in" - ) - resource_tag_value__icontains = CharFilter( - field_name="resources__tags__value", lookup_expr="icontains" - ) - resource_tags = CharInFilter( - method="filter_resource_tag", - lookup_expr="in", - help_text="Filter by resource tags `key:value` pairs.\nMultiple values may be " - "separated by commas.", - ) + # Temporarily disabled until we implement tag filtering in the UI + # resource_tag_key = CharFilter(field_name="resources__tags__key") + # resource_tag_key__in = CharInFilter( + # field_name="resources__tags__key", lookup_expr="in" + # ) + # resource_tag_key__icontains = CharFilter( + # field_name="resources__tags__key", lookup_expr="icontains" + # ) + # resource_tag_value = CharFilter(field_name="resources__tags__value") + # resource_tag_value__in = CharInFilter( + # field_name="resources__tags__value", lookup_expr="in" + # ) + # resource_tag_value__icontains = CharFilter( + # field_name="resources__tags__value", lookup_expr="icontains" + # ) + # resource_tags = CharInFilter( + # method="filter_resource_tag", + # lookup_expr="in", + # help_text="Filter by resource tags `key:value` pairs.\nMultiple values may be " + # "separated by commas.", + # ) scan = UUIDFilter(method="filter_scan_id") scan__in = UUIDInFilter(method="filter_scan_id_in") @@ -374,12 +375,6 @@ class FindingFilter(FilterSet): }, } - @property - def qs(self): - # Force distinct results to prevent duplicates with many-to-many relationships - parent_qs = super().qs - return parent_qs.distinct() - # Convert filter values to UUIDv7 values for use with partitioning def filter_scan_id(self, queryset, name, value): try: diff --git a/api/src/backend/api/migrations/0008_daily_scheduled_tasks_update.py b/api/src/backend/api/migrations/0008_daily_scheduled_tasks_update.py new file mode 100644 index 0000000000..7f059ea2b8 --- /dev/null +++ b/api/src/backend/api/migrations/0008_daily_scheduled_tasks_update.py @@ -0,0 +1,64 @@ +import json +from datetime import datetime, timedelta, timezone + +import django.db.models.deletion +from django.db import migrations, models +from django_celery_beat.models import PeriodicTask + +from api.db_utils import rls_transaction +from api.models import Scan, StateChoices + + +def migrate_daily_scheduled_scan_tasks(apps, schema_editor): + for daily_scheduled_scan_task in PeriodicTask.objects.filter( + task="scan-perform-scheduled" + ): + task_kwargs = json.loads(daily_scheduled_scan_task.kwargs) + tenant_id = task_kwargs["tenant_id"] + provider_id = task_kwargs["provider_id"] + + current_time = datetime.now(timezone.utc) + scheduled_time_today = datetime.combine( + current_time.date(), + daily_scheduled_scan_task.start_time.time(), + tzinfo=timezone.utc, + ) + + if current_time < scheduled_time_today: + next_scan_date = scheduled_time_today + else: + next_scan_date = scheduled_time_today + timedelta(days=1) + + with rls_transaction(tenant_id): + Scan.objects.create( + tenant_id=tenant_id, + name="Daily scheduled scan", + provider_id=provider_id, + trigger=Scan.TriggerChoices.SCHEDULED, + state=StateChoices.SCHEDULED, + scheduled_at=next_scan_date, + scheduler_task_id=daily_scheduled_scan_task.id, + ) + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("api", "0007_scan_and_scan_summaries_indexes"), + ("django_celery_beat", "0019_alter_periodictasks_options"), + ] + + operations = [ + migrations.AddField( + model_name="scan", + name="scheduler_task", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="django_celery_beat.periodictask", + ), + ), + migrations.RunPython(migrate_daily_scheduled_scan_tasks), + ] diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index 09014321cb..ef982be8c7 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -11,6 +11,7 @@ from django.core.validators import MinLengthValidator from django.db import models from django.db.models import Q from django.utils.translation import gettext_lazy as _ +from django_celery_beat.models import PeriodicTask from django_celery_results.models import TaskResult from psqlextra.manager import PostgresManager from psqlextra.models import PostgresPartitionedModel @@ -410,6 +411,9 @@ class Scan(RowLevelSecurityProtectedModel): started_at = models.DateTimeField(null=True, blank=True) completed_at = models.DateTimeField(null=True, blank=True) next_scan_at = models.DateTimeField(null=True, blank=True) + scheduler_task = models.ForeignKey( + PeriodicTask, on_delete=models.CASCADE, null=True, blank=True + ) # TODO: mutelist foreign key class Meta(RowLevelSecurityProtectedModel.Meta): diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index cb355b1aa7..ab25ef0e27 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: Prowler API - version: 1.3.1 + version: 1.4.0 description: |- Prowler API specification. @@ -478,51 +478,6 @@ paths: description: Multiple values may be separated by commas. explode: false style: form - - in: query - name: filter[resource_tag_key] - schema: - type: string - - in: query - name: filter[resource_tag_key__icontains] - schema: - type: string - - in: query - name: filter[resource_tag_key__in] - schema: - type: array - items: - type: string - description: Multiple values may be separated by commas. - explode: false - style: form - - in: query - name: filter[resource_tag_value] - schema: - type: string - - in: query - name: filter[resource_tag_value__icontains] - schema: - type: string - - in: query - name: filter[resource_tag_value__in] - schema: - type: array - items: - type: string - description: Multiple values may be separated by commas. - explode: false - style: form - - in: query - name: filter[resource_tags] - schema: - type: array - items: - type: string - description: |- - Filter by resource tags `key:value` pairs. - Multiple values may be separated by commas. - explode: false - style: form - in: query name: filter[resource_type] schema: @@ -1028,51 +983,6 @@ paths: description: Multiple values may be separated by commas. explode: false style: form - - in: query - name: filter[resource_tag_key] - schema: - type: string - - in: query - name: filter[resource_tag_key__icontains] - schema: - type: string - - in: query - name: filter[resource_tag_key__in] - schema: - type: array - items: - type: string - description: Multiple values may be separated by commas. - explode: false - style: form - - in: query - name: filter[resource_tag_value] - schema: - type: string - - in: query - name: filter[resource_tag_value__icontains] - schema: - type: string - - in: query - name: filter[resource_tag_value__in] - schema: - type: array - items: - type: string - description: Multiple values may be separated by commas. - explode: false - style: form - - in: query - name: filter[resource_tags] - schema: - type: array - items: - type: string - description: |- - Filter by resource tags `key:value` pairs. - Multiple values may be separated by commas. - explode: false - style: form - in: query name: filter[resource_type] schema: @@ -1280,7 +1190,6 @@ paths: - services - regions - resource_types - - tags description: endpoint return only specific fields in the response on a per-type basis by including a fields[TYPE] query parameter. explode: false @@ -1498,51 +1407,6 @@ paths: description: Multiple values may be separated by commas. explode: false style: form - - in: query - name: filter[resource_tag_key] - schema: - type: string - - in: query - name: filter[resource_tag_key__icontains] - schema: - type: string - - in: query - name: filter[resource_tag_key__in] - schema: - type: array - items: - type: string - description: Multiple values may be separated by commas. - explode: false - style: form - - in: query - name: filter[resource_tag_value] - schema: - type: string - - in: query - name: filter[resource_tag_value__icontains] - schema: - type: string - - in: query - name: filter[resource_tag_value__in] - schema: - type: array - items: - type: string - description: Multiple values may be separated by commas. - explode: false - style: form - - in: query - name: filter[resource_tags] - schema: - type: array - items: - type: string - description: |- - Filter by resource tags `key:value` pairs. - Multiple values may be separated by commas. - explode: false - style: form - in: query name: filter[resource_type] schema: @@ -6081,13 +5945,10 @@ components: type: array items: type: string - tags: - description: Tags are described as key-value pairs. required: - services - regions - resource_types - - tags FindingMetadataResponse: type: object properties: diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 184843af18..0173797062 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -2454,15 +2454,16 @@ class TestFindingViewSet: ("search", "ec2", 2), # full text search on finding tags ("search", "value2", 2), - ("resource_tag_key", "key", 2), - ("resource_tag_key__in", "key,key2", 2), - ("resource_tag_key__icontains", "key", 2), - ("resource_tag_value", "value", 2), - ("resource_tag_value__in", "value,value2", 2), - ("resource_tag_value__icontains", "value", 2), - ("resource_tags", "key:value", 2), - ("resource_tags", "not:exists", 0), - ("resource_tags", "not:exists,key:value", 2), + # Temporary disabled until we implement tag filtering in the UI + # ("resource_tag_key", "key", 2), + # ("resource_tag_key__in", "key,key2", 2), + # ("resource_tag_key__icontains", "key", 2), + # ("resource_tag_value", "value", 2), + # ("resource_tag_value__in", "value,value2", 2), + # ("resource_tag_value__icontains", "value", 2), + # ("resource_tags", "key:value", 2), + # ("resource_tags", "not:exists", 0), + # ("resource_tags", "not:exists,key:value", 2), ] ), ) @@ -2611,7 +2612,8 @@ class TestFindingViewSet: expected_services = {"ec2", "s3"} expected_regions = {"eu-west-1", "us-east-1"} - expected_tags = {"key": ["value"], "key2": ["value2"]} + # Temporarily disabled until we implement tag filtering in the UI + # expected_tags = {"key": ["value"], "key2": ["value2"]} expected_resource_types = {"prowler-test"} assert data["data"]["type"] == "findings-metadata" @@ -2621,7 +2623,7 @@ class TestFindingViewSet: assert ( set(data["data"]["attributes"]["resource_types"]) == expected_resource_types ) - assert data["data"]["attributes"]["tags"] == expected_tags + # assert data["data"]["attributes"]["tags"] == expected_tags def test_findings_metadata_severity_retrieve( self, authenticated_client, findings_fixture @@ -2638,7 +2640,8 @@ class TestFindingViewSet: expected_services = {"s3"} expected_regions = {"eu-west-1"} - expected_tags = {"key": ["value"], "key2": ["value2"]} + # Temporary disabled until we implement tag filtering in the UI + # expected_tags = {"key": ["value"], "key2": ["value2"]} expected_resource_types = {"prowler-test"} assert data["data"]["type"] == "findings-metadata" @@ -2648,7 +2651,7 @@ class TestFindingViewSet: assert ( set(data["data"]["attributes"]["resource_types"]) == expected_resource_types ) - assert data["data"]["attributes"]["tags"] == expected_tags + # assert data["data"]["attributes"]["tags"] == expected_tags def test_findings_metadata_future_date(self, authenticated_client): response = authenticated_client.get( @@ -2660,7 +2663,8 @@ class TestFindingViewSet: assert data["data"]["id"] is None assert data["data"]["attributes"]["services"] == [] assert data["data"]["attributes"]["regions"] == [] - assert data["data"]["attributes"]["tags"] == {} + # Temporary disabled until we implement tag filtering in the UI + # assert data["data"]["attributes"]["tags"] == {} assert data["data"]["attributes"]["resource_types"] == [] def test_findings_metadata_invalid_date(self, authenticated_client): diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index 6046799d0c..4f17d2d61c 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -933,7 +933,8 @@ class FindingMetadataSerializer(serializers.Serializer): resource_types = serializers.ListField( child=serializers.CharField(), allow_empty=True ) - tags = serializers.JSONField(help_text="Tags are described as key-value pairs.") + # Temporarily disabled until we implement tag filtering in the UI + # tags = serializers.JSONField(help_text="Tags are described as key-value pairs.") class Meta: resource_name = "findings-metadata" diff --git a/api/src/backend/api/v1/urls.py b/api/src/backend/api/v1/urls.py index fd3f9ad18a..4ff587d913 100644 --- a/api/src/backend/api/v1/urls.py +++ b/api/src/backend/api/v1/urls.py @@ -3,28 +3,28 @@ from drf_spectacular.views import SpectacularRedocView from rest_framework_nested import routers from api.v1.views import ( + ComplianceOverviewViewSet, CustomTokenObtainView, CustomTokenRefreshView, FindingViewSet, - MembershipViewSet, - ProviderGroupViewSet, - ProviderGroupProvidersRelationshipView, - ProviderSecretViewSet, - InvitationViewSet, InvitationAcceptViewSet, - RoleViewSet, - RoleProviderGroupRelationshipView, - UserRoleRelationshipView, + InvitationViewSet, + MembershipViewSet, OverviewViewSet, - ComplianceOverviewViewSet, + ProviderGroupProvidersRelationshipView, + ProviderGroupViewSet, + ProviderSecretViewSet, ProviderViewSet, ResourceViewSet, + RoleProviderGroupRelationshipView, + RoleViewSet, ScanViewSet, ScheduleViewSet, SchemaView, TaskViewSet, TenantMembersViewSet, TenantViewSet, + UserRoleRelationshipView, UserViewSet, ) diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index debfcdd57c..b11626c829 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -4,7 +4,7 @@ from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.search import SearchQuery from django.db import transaction from django.db.models import Count, F, OuterRef, Prefetch, Q, Subquery, Sum -from django.db.models.functions import Coalesce, JSONObject +from django.db.models.functions import Coalesce from django.urls import reverse from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_control @@ -193,7 +193,7 @@ class SchemaView(SpectacularAPIView): def get(self, request, *args, **kwargs): spectacular_settings.TITLE = "Prowler API" - spectacular_settings.VERSION = "1.3.1" + spectacular_settings.VERSION = "1.4.0" spectacular_settings.DESCRIPTION = ( "Prowler API specification.\n\nThis file is auto-generated." ) @@ -1392,48 +1392,59 @@ class FindingViewSet(BaseRLSViewSet): @action(detail=False, methods=["get"], url_name="metadata") def metadata(self, request): + tenant_id = self.request.tenant_id queryset = self.get_queryset() filtered_queryset = self.filter_queryset(queryset) - result = filtered_queryset.aggregate( - services=ArrayAgg("resources__service", flat=True, distinct=True), - regions=ArrayAgg("resources__region", flat=True, distinct=True), - tags=ArrayAgg( - JSONObject( - key=F("resources__tags__key"), value=F("resources__tags__value") - ), - distinct=True, - filter=Q(resources__tags__key__isnull=False), - ), - resource_types=ArrayAgg("resources__type", flat=True, distinct=True), - ) - if result["services"] is None: - result["services"] = [] - if result["regions"] is None: - result["regions"] = [] - if result["regions"] is None: - result["regions"] = [] - if result["resource_types"] is None: - result["resource_types"] = [] - if result["tags"] is None: - result["tags"] = [] + relevant_resources = Resource.objects.filter( + tenant_id=tenant_id, findings__in=filtered_queryset + ).distinct() - tags_dict = {} - for t in result["tags"]: - key, value = t["key"], t["value"] - if key not in tags_dict: - tags_dict[key] = [] - tags_dict[key].append(value) - - result["tags"] = tags_dict - - serializer = self.get_serializer( - data=result, + services = ( + relevant_resources.values_list("service", flat=True) + .distinct() + .order_by("service") ) + regions = ( + relevant_resources.exclude(region="") + .values_list("region", flat=True) + .distinct() + .order_by("region") + ) + + resource_types = ( + relevant_resources.values_list("type", flat=True) + .distinct() + .order_by("type") + ) + + # Temporarily disabled until we implement tag filtering in the UI + # tag_data = ( + # relevant_resources + # .filter(tags__key__isnull=False, tags__value__isnull=False) + # .exclude(tags__key="") + # .exclude(tags__value="") + # .values("tags__key", "tags__value") + # .distinct() + # .order_by("tags__key", "tags__value") + # ) + # + # tags_dict = {} + # for row in tag_data: + # k, v = row["tags__key"], row["tags__value"] + # tags_dict.setdefault(k, []).append(v) + + result = { + "services": list(services), + "regions": list(regions), + "resource_types": list(resource_types), + # "tags": tags_dict + } + + serializer = self.get_serializer(data=result) serializer.is_valid(raise_exception=True) - - return Response(data=serializer.data, status=status.HTTP_200_OK) + return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema_view( diff --git a/api/src/backend/tasks/beat.py b/api/src/backend/tasks/beat.py index 81dc04abba..6cd8d7a9ce 100644 --- a/api/src/backend/tasks/beat.py +++ b/api/src/backend/tasks/beat.py @@ -5,10 +5,14 @@ from django_celery_beat.models import IntervalSchedule, PeriodicTask from rest_framework_json_api.serializers import ValidationError from tasks.tasks import perform_scheduled_scan_task -from api.models import Provider +from api.db_utils import rls_transaction +from api.models import Provider, Scan, StateChoices def schedule_provider_scan(provider_instance: Provider): + tenant_id = str(provider_instance.tenant_id) + provider_id = str(provider_instance.id) + schedule, _ = IntervalSchedule.objects.get_or_create( every=24, period=IntervalSchedule.HOURS, @@ -17,23 +21,9 @@ def schedule_provider_scan(provider_instance: Provider): # Create a unique name for the periodic task task_name = f"scan-perform-scheduled-{provider_instance.id}" - # Schedule the task - _, created = PeriodicTask.objects.get_or_create( - interval=schedule, - name=task_name, - task="scan-perform-scheduled", - kwargs=json.dumps( - { - "tenant_id": str(provider_instance.tenant_id), - "provider_id": str(provider_instance.id), - } - ), - one_off=False, - defaults={ - "start_time": datetime.now(timezone.utc) + timedelta(hours=24), - }, - ) - if not created: + if PeriodicTask.objects.filter( + interval=schedule, name=task_name, task="scan-perform-scheduled" + ).exists(): raise ValidationError( [ { @@ -45,9 +35,36 @@ def schedule_provider_scan(provider_instance: Provider): ] ) + with rls_transaction(tenant_id): + scheduled_scan = Scan.objects.create( + tenant_id=tenant_id, + name="Daily scheduled scan", + provider_id=provider_id, + trigger=Scan.TriggerChoices.SCHEDULED, + state=StateChoices.AVAILABLE, + scheduled_at=datetime.now(timezone.utc), + ) + + # Schedule the task + periodic_task_instance = PeriodicTask.objects.create( + interval=schedule, + name=task_name, + task="scan-perform-scheduled", + kwargs=json.dumps( + { + "tenant_id": tenant_id, + "provider_id": provider_id, + } + ), + one_off=False, + start_time=datetime.now(timezone.utc) + timedelta(hours=24), + ) + scheduled_scan.scheduler_task_id = periodic_task_instance.id + scheduled_scan.save() + return perform_scheduled_scan_task.apply_async( kwargs={ "tenant_id": str(provider_instance.tenant_id), - "provider_id": str(provider_instance.id), + "provider_id": provider_id, }, ) diff --git a/api/src/backend/tasks/jobs/scan.py b/api/src/backend/tasks/jobs/scan.py index 25993f5bbc..ad11079ef0 100644 --- a/api/src/backend/tasks/jobs/scan.py +++ b/api/src/backend/tasks/jobs/scan.py @@ -152,6 +152,9 @@ def perform_prowler_scan( for progress, findings in prowler_scan.scan(): for finding in findings: + if finding is None: + logger.error(f"None finding detected on scan {scan_id}.") + continue for attempt in range(CELERY_DEADLOCK_ATTEMPTS): try: with rls_transaction(tenant_id): @@ -176,7 +179,10 @@ def perform_prowler_scan( # Update resource fields if necessary updated_fields = [] - if resource_instance.region != finding.region: + if ( + finding.region + and resource_instance.region != finding.region + ): resource_instance.region = finding.region updated_fields.append("region") if resource_instance.service != finding.service_name: @@ -222,8 +228,10 @@ def perform_prowler_scan( last_first_seen_at = None if finding_uid not in last_status_cache: most_recent_finding = ( - Finding.objects.filter(uid=finding_uid) - .order_by("-id") + Finding.all_objects.filter( + tenant_id=tenant_id, uid=finding_uid + ) + .order_by("-inserted_at") .values("status", "first_seen_at") .first() ) @@ -237,8 +245,11 @@ def perform_prowler_scan( status = FindingStatus[finding.status] delta = _create_finding_delta(last_status, status) - # For the findings prior to the change, when a first finding is found with delta!="new" it will be assigned a current date as first_seen_at and the successive findings with the same UID will always get the date of the previous finding. - # For new findings, when a finding (delta="new") is found for the first time, the first_seen_at attribute will be assigned the current date, the following findings will get that date. + # For the findings prior to the change, when a first finding is found with delta!="new" it will be + # assigned a current date as first_seen_at and the successive findings with the same UID will + # always get the date of the previous finding. + # For new findings, when a finding (delta="new") is found for the first time, the first_seen_at + # attribute will be assigned the current date, the following findings will get that date. if not last_first_seen_at: last_first_seen_at = datetime.now(tz=timezone.utc) @@ -372,7 +383,7 @@ def aggregate_findings(tenant_id: str, scan_id: str): - muted_changed: Muted findings with a delta of 'changed'. """ with rls_transaction(tenant_id): - findings = Finding.objects.filter(scan_id=scan_id) + findings = Finding.objects.filter(tenant_id=tenant_id, scan_id=scan_id) aggregation = findings.values( "check_id", diff --git a/api/src/backend/tasks/tasks.py b/api/src/backend/tasks/tasks.py index 1f87e0fdeb..792900c4e7 100644 --- a/api/src/backend/tasks/tasks.py +++ b/api/src/backend/tasks/tasks.py @@ -1,15 +1,14 @@ -from datetime import datetime, timedelta, timezone - from celery import shared_task from config.celery import RLSTask from django_celery_beat.models import PeriodicTask from tasks.jobs.connection import check_provider_connection from tasks.jobs.deletion import delete_provider, delete_tenant from tasks.jobs.scan import aggregate_findings, perform_prowler_scan +from tasks.utils import get_next_execution_datetime from api.db_utils import rls_transaction from api.decorators import set_tenant -from api.models import Provider, Scan +from api.models import Scan, StateChoices @shared_task(base=RLSTask, name="provider-connection-check") @@ -100,28 +99,42 @@ def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str): task_id = self.request.id with rls_transaction(tenant_id): - provider_instance = Provider.objects.get(pk=provider_id) periodic_task_instance = PeriodicTask.objects.get( name=f"scan-perform-scheduled-{provider_id}" ) - next_scan_date = datetime.combine( - datetime.now(timezone.utc), periodic_task_instance.start_time.time() - ) + timedelta(hours=24) - - scan_instance = Scan.objects.create( + next_scan_datetime = get_next_execution_datetime(task_id, provider_id) + scan_instance, _ = Scan.objects.get_or_create( tenant_id=tenant_id, - name="Daily scheduled scan", - provider=provider_instance, + provider_id=provider_id, trigger=Scan.TriggerChoices.SCHEDULED, - next_scan_at=next_scan_date, - task_id=task_id, + state__in=(StateChoices.SCHEDULED, StateChoices.AVAILABLE), + scheduler_task_id=periodic_task_instance.id, + defaults={"state": StateChoices.SCHEDULED}, ) - result = perform_prowler_scan( - tenant_id=tenant_id, - scan_id=str(scan_instance.id), - provider_id=provider_id, - ) + scan_instance.task_id = task_id + scan_instance.save() + + try: + result = perform_prowler_scan( + tenant_id=tenant_id, + scan_id=str(scan_instance.id), + provider_id=provider_id, + ) + except Exception as e: + raise e + finally: + with rls_transaction(tenant_id): + Scan.objects.get_or_create( + tenant_id=tenant_id, + name="Daily scheduled scan", + provider_id=provider_id, + trigger=Scan.TriggerChoices.SCHEDULED, + state=StateChoices.SCHEDULED, + scheduled_at=next_scan_datetime, + scheduler_task_id=periodic_task_instance.id, + ) + perform_scan_summary_task.apply_async( kwargs={ "tenant_id": tenant_id, diff --git a/api/src/backend/tasks/tests/test_beat.py b/api/src/backend/tasks/tests/test_beat.py index 78b5acb039..6e5c6fdf91 100644 --- a/api/src/backend/tasks/tests/test_beat.py +++ b/api/src/backend/tasks/tests/test_beat.py @@ -6,6 +6,8 @@ from django_celery_beat.models import IntervalSchedule, PeriodicTask from rest_framework_json_api.serializers import ValidationError from tasks.beat import schedule_provider_scan +from api.models import Scan + @pytest.mark.django_db class TestScheduleProviderScan: @@ -15,9 +17,11 @@ class TestScheduleProviderScan: with patch( "tasks.tasks.perform_scheduled_scan_task.apply_async" ) as mock_apply_async: + assert Scan.all_objects.count() == 0 result = schedule_provider_scan(provider_instance) assert result is not None + assert Scan.all_objects.count() == 1 mock_apply_async.assert_called_once_with( kwargs={ diff --git a/api/src/backend/tasks/tests/test_utils.py b/api/src/backend/tasks/tests/test_utils.py new file mode 100644 index 0000000000..619626af9e --- /dev/null +++ b/api/src/backend/tasks/tests/test_utils.py @@ -0,0 +1,76 @@ +from datetime import datetime, timedelta, timezone +from unittest.mock import patch + +import pytest +from django_celery_beat.models import IntervalSchedule, PeriodicTask +from django_celery_results.models import TaskResult +from tasks.utils import get_next_execution_datetime + + +@pytest.mark.django_db +class TestGetNextExecutionDatetime: + @pytest.fixture + def setup_periodic_task(self, db): + # Create a periodic task with an hourly interval + interval = IntervalSchedule.objects.create( + every=1, period=IntervalSchedule.HOURS + ) + periodic_task = PeriodicTask.objects.create( + name="scan-perform-scheduled-123", + task="scan-perform-scheduled", + interval=interval, + ) + return periodic_task + + @pytest.fixture + def setup_task_result(self, db): + # Create a task result record + task_result = TaskResult.objects.create( + task_id="abc123", + task_name="scan-perform-scheduled", + status="SUCCESS", + date_created=datetime.now(timezone.utc) - timedelta(hours=1), + result="Success", + ) + return task_result + + def test_get_next_execution_datetime_success( + self, setup_task_result, setup_periodic_task + ): + task_result = setup_task_result + periodic_task = setup_periodic_task + + # Mock periodic_task_name on TaskResult + with patch.object( + TaskResult, "periodic_task_name", return_value=periodic_task.name + ): + next_execution = get_next_execution_datetime( + task_id=task_result.task_id, provider_id="123" + ) + + expected_time = task_result.date_created + timedelta(hours=1) + assert next_execution == expected_time + + def test_get_next_execution_datetime_fallback_to_provider_id( + self, setup_task_result, setup_periodic_task + ): + task_result = setup_task_result + + # Simulate the case where `periodic_task_name` is missing + with patch.object(TaskResult, "periodic_task_name", return_value=None): + next_execution = get_next_execution_datetime( + task_id=task_result.task_id, provider_id="123" + ) + + expected_time = task_result.date_created + timedelta(hours=1) + assert next_execution == expected_time + + def test_get_next_execution_datetime_periodic_task_does_not_exist( + self, setup_task_result + ): + task_result = setup_task_result + + with pytest.raises(PeriodicTask.DoesNotExist): + get_next_execution_datetime( + task_id=task_result.task_id, provider_id="nonexistent" + ) diff --git a/api/src/backend/tasks/utils.py b/api/src/backend/tasks/utils.py new file mode 100644 index 0000000000..5ac78a03ab --- /dev/null +++ b/api/src/backend/tasks/utils.py @@ -0,0 +1,26 @@ +from datetime import datetime, timedelta, timezone + +from django_celery_beat.models import PeriodicTask +from django_celery_results.models import TaskResult + + +def get_next_execution_datetime(task_id: int, provider_id: str) -> datetime: + task_instance = TaskResult.objects.get(task_id=task_id) + try: + periodic_task_instance = PeriodicTask.objects.get( + name=task_instance.periodic_task_name + ) + except PeriodicTask.DoesNotExist: + periodic_task_instance = PeriodicTask.objects.get( + name=f"scan-perform-scheduled-{provider_id}" + ) + + interval = periodic_task_instance.interval + + current_scheduled_time = datetime.combine( + datetime.now(timezone.utc).date(), + task_instance.date_created.time(), + tzinfo=timezone.utc, + ) + + return current_scheduled_time + timedelta(**{interval.period: interval.every}) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index d49af9dfbb..c8ecea96d5 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -35,6 +35,9 @@ services: required: false ports: - 3000:3000 + volumes: + - "./ui:/app" + - "/app/node_modules" postgres: image: postgres:16.3-alpine3.20 diff --git a/docs/tutorials/prowler-app.md b/docs/tutorials/prowler-app.md index 40a40ed096..3184d9e958 100644 --- a/docs/tutorials/prowler-app.md +++ b/docs/tutorials/prowler-app.md @@ -133,3 +133,5 @@ While the scan is running, start exploring the findings in these sections: Issues - **Browse All Findings**: Detailed list of findings detected, where you can filter by severity, service, and more. Findings + +To view all `new` findings that have not been seen prior to this scan, click the `Delta` filter and select `new`. To view all `changed` findings that have had a status change (from `PASS` to `FAIL` for example), click the `Delta` filter and select `changed`. diff --git a/poetry.lock b/poetry.lock index b0e6e0e4ad..0f53a98c43 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. [[package]] name = "about-time" @@ -1692,13 +1692,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] [[package]] name = "google-api-python-client" -version = "2.159.0" +version = "2.160.0" description = "Google API Client Library for Python" optional = false python-versions = ">=3.7" files = [ - {file = "google_api_python_client-2.159.0-py2.py3-none-any.whl", hash = "sha256:baef0bb631a60a0bd7c0bf12a5499e3a40cd4388484de7ee55c1950bf820a0cf"}, - {file = "google_api_python_client-2.159.0.tar.gz", hash = "sha256:55197f430f25c907394b44fa078545ffef89d33fd4dca501b7db9f0d8e224bd6"}, + {file = "google_api_python_client-2.160.0-py2.py3-none-any.whl", hash = "sha256:63d61fb3e4cf3fb31a70a87f45567c22f6dfe87bbfa27252317e3e2c42900db4"}, + {file = "google_api_python_client-2.160.0.tar.gz", hash = "sha256:a8ccafaecfa42d15d5b5c3134ced8de08380019717fc9fb1ed510ca58eca3b7e"}, ] [package.dependencies] @@ -2122,13 +2122,13 @@ referencing = ">=0.31.0" [[package]] name = "kubernetes" -version = "31.0.0" +version = "32.0.0" description = "Kubernetes python client" optional = false python-versions = ">=3.6" files = [ - {file = "kubernetes-31.0.0-py2.py3-none-any.whl", hash = "sha256:bf141e2d380c8520eada8b351f4e319ffee9636328c137aa432bc486ca1200e1"}, - {file = "kubernetes-31.0.0.tar.gz", hash = "sha256:28945de906c8c259c1ebe62703b56a03b714049372196f854105afe4e6d014c0"}, + {file = "kubernetes-32.0.0-py2.py3-none-any.whl", hash = "sha256:60fd8c29e8e43d9c553ca4811895a687426717deba9c0a66fb2dcc3f5ef96692"}, + {file = "kubernetes-32.0.0.tar.gz", hash = "sha256:319fa840345a482001ac5d6062222daeb66ec4d1bcb3087402aed685adf0aecb"}, ] [package.dependencies] @@ -2359,13 +2359,13 @@ files = [ [[package]] name = "microsoft-kiota-abstractions" -version = "1.6.8" +version = "1.9.1" description = "Core abstractions for kiota generated libraries in Python" optional = false -python-versions = "<4.0,>=3.8" +python-versions = "<4.0,>=3.9" files = [ - {file = "microsoft_kiota_abstractions-1.6.8-py3-none-any.whl", hash = "sha256:12819dee24d5aaa31e99683d938f65e50cbc446de087df244cd26c3326ec4e15"}, - {file = "microsoft_kiota_abstractions-1.6.8.tar.gz", hash = "sha256:7070affabfa7182841646a0c8491cbb240af366aff2b9132f0caa45c4837dd78"}, + {file = "microsoft_kiota_abstractions-1.9.1-py3-none-any.whl", hash = "sha256:aca6147946fc56167038faddda0a07026b0ec4e76666520586de7ac5c1233295"}, + {file = "microsoft_kiota_abstractions-1.9.1.tar.gz", hash = "sha256:291f4f88bec5452b2c6bafed4927640d2c7d2f41b08c941ee786f6ecad3dcaa5"}, ] [package.dependencies] @@ -2375,19 +2375,19 @@ std-uritemplate = ">=2.0.0" [[package]] name = "microsoft-kiota-authentication-azure" -version = "1.6.8" +version = "1.9.1" description = "Core abstractions for kiota generated libraries in Python" optional = false -python-versions = "<4.0,>=3.8" +python-versions = "<4.0,>=3.9" files = [ - {file = "microsoft_kiota_authentication_azure-1.6.8-py3-none-any.whl", hash = "sha256:50455789b7133e27fbccec839d93e40d2637d18593a93921ae1338880c5b5b3b"}, - {file = "microsoft_kiota_authentication_azure-1.6.8.tar.gz", hash = "sha256:fef23f43cd4d3b9ef839c8b3d1f675ec4a1120c150f963d8c4551c5e19ac3b36"}, + {file = "microsoft_kiota_authentication_azure-1.9.1-py3-none-any.whl", hash = "sha256:3a3030b01e0cbf007736ec6548ac483742e04ad0d20979b49e293a683f839fc6"}, + {file = "microsoft_kiota_authentication_azure-1.9.1.tar.gz", hash = "sha256:8ba31b1ecf78777128daf3191c7ecd28bfc2d10d0fe80260dd5c07a9ccd5d7f5"}, ] [package.dependencies] aiohttp = ">=3.8.0" azure-core = ">=1.21.1" -microsoft-kiota-abstractions = ">=1.6.8,<1.7.0" +microsoft-kiota-abstractions = ">=1.9.1,<1.10.0" opentelemetry-api = ">=1.27.0" opentelemetry-sdk = ">=1.27.0" @@ -2408,83 +2408,61 @@ microsoft-kiota_abstractions = ">=1.0.0,<2.0.0" opentelemetry-api = ">=1.20.0" opentelemetry-sdk = ">=1.20.0" -[[package]] -name = "microsoft-kiota-http" -version = "1.6.8" -description = "Core abstractions for kiota generated libraries in Python" -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "microsoft_kiota_http-1.6.8-py3-none-any.whl", hash = "sha256:7ff76a308351d885453185d6a6538c47a64ebdc7661cce46a904e89e2ceb9a1d"}, - {file = "microsoft_kiota_http-1.6.8.tar.gz", hash = "sha256:67242690b79a30c0cadf823675249269e4bc020283e3d65b33af7d771df64df8"}, -] - -[package.dependencies] -httpx = {version = ">=0.28", extras = ["http2"]} -microsoft-kiota-abstractions = ">=1.6.8,<1.7.0" -opentelemetry-api = ">=1.27.0" -opentelemetry-sdk = ">=1.27.0" -urllib3 = ">=2.2.2,<3.0.0" - [[package]] name = "microsoft-kiota-serialization-form" -version = "1.6.8" +version = "1.9.1" description = "Core abstractions for kiota generated libraries in Python" optional = false -python-versions = "<4.0,>=3.8" +python-versions = "<4.0,>=3.9" files = [ - {file = "microsoft_kiota_serialization_form-1.6.8-py3-none-any.whl", hash = "sha256:ca7dd19e173aa87c68b38c5056cc0921570c8c86f3ba5511d1616cf97e2f0f67"}, - {file = "microsoft_kiota_serialization_form-1.6.8.tar.gz", hash = "sha256:bb9eb98b3abf596b4bfe208014dff948361ff48a757316ac58e19c31ab8d640a"}, + {file = "microsoft_kiota_serialization_form-1.9.1-py3-none-any.whl", hash = "sha256:d86c0dc08b51288f2851a265dde729b200998bca1dd1681bbebcb27cd274ba34"}, + {file = "microsoft_kiota_serialization_form-1.9.1.tar.gz", hash = "sha256:58b81eca5e0ad66bcbd6a4b65ba91e6255d3510f4c4f576fb4f5e83ca9a310c3"}, ] [package.dependencies] -microsoft-kiota-abstractions = ">=1.6.8,<1.7.0" -pendulum = ">=3.0.0b1" +microsoft-kiota-abstractions = ">=1.9.1,<1.10.0" [[package]] name = "microsoft-kiota-serialization-json" -version = "1.6.8" +version = "1.9.1" description = "Core abstractions for kiota generated libraries in Python" optional = false -python-versions = "<4.0,>=3.8" +python-versions = "<4.0,>=3.9" files = [ - {file = "microsoft_kiota_serialization_json-1.6.8-py3-none-any.whl", hash = "sha256:2734c2ad64cc089441279e4962f6fedf41af040730f6eab1533890cd5377aff5"}, - {file = "microsoft_kiota_serialization_json-1.6.8.tar.gz", hash = "sha256:89e2dd0eb4eaaa6ab74fa89ab5d84c5a53464e73b85eb7085f0aa4560a2b8183"}, + {file = "microsoft_kiota_serialization_json-1.9.1-py3-none-any.whl", hash = "sha256:ab752bf642a77266713bac3942a4c547dde60b917f674a9ab63261490fecf841"}, + {file = "microsoft_kiota_serialization_json-1.9.1.tar.gz", hash = "sha256:0039458b885875daf246f1e8c0296551695e0ec70f3e4689b00b270ce923c0cc"}, ] [package.dependencies] -microsoft-kiota-abstractions = ">=1.6.8,<1.7.0" -pendulum = ">=3.0.0b1" +microsoft-kiota-abstractions = ">=1.9.1,<1.10.0" [[package]] name = "microsoft-kiota-serialization-multipart" -version = "1.6.8" +version = "1.9.1" description = "Core abstractions for kiota generated libraries in Python" optional = false -python-versions = "<4.0,>=3.8" +python-versions = "<4.0,>=3.9" files = [ - {file = "microsoft_kiota_serialization_multipart-1.6.8-py3-none-any.whl", hash = "sha256:1ecdd15dd1f78aed031d7d1828b6fbc00c633542d863c23f96fdd0a61bfb189a"}, - {file = "microsoft_kiota_serialization_multipart-1.6.8.tar.gz", hash = "sha256:3d95c6d7186588af7a1d3aa852ce42077f80487b8b3c60e36fe109a8b4918c03"}, + {file = "microsoft_kiota_serialization_multipart-1.9.1-py3-none-any.whl", hash = "sha256:1ee69d8e3b2c0d24431b85fc1628534f97dcafed45dcb6bb3f0143824ce8a665"}, + {file = "microsoft_kiota_serialization_multipart-1.9.1.tar.gz", hash = "sha256:288790a486aad33aac0a7329f05b8851e9be82f50cc9a8f19fe6f267a4f2b183"}, ] [package.dependencies] -microsoft-kiota-abstractions = ">=1.6.8,<1.7.0" -pendulum = ">=3.0.0b1" +microsoft-kiota-abstractions = ">=1.9.1,<1.10.0" [[package]] name = "microsoft-kiota-serialization-text" -version = "1.6.8" +version = "1.9.1" description = "Core abstractions for kiota generated libraries in Python" optional = false -python-versions = "<4.0,>=3.8" +python-versions = "<4.0,>=3.9" files = [ - {file = "microsoft_kiota_serialization_text-1.6.8-py3-none-any.whl", hash = "sha256:4e5e287a614d362f864b5061dca0861c3f70b8792ec72967d1bff23944da1e80"}, - {file = "microsoft_kiota_serialization_text-1.6.8.tar.gz", hash = "sha256:687d4858337eaf4f351b12ed1c6c934d869560f54ee3855bfdde589660e07208"}, + {file = "microsoft_kiota_serialization_text-1.9.1-py3-none-any.whl", hash = "sha256:021fbfad887f7525f9e1c8bbe225d0aa1b25befe54357ae0f739f47576d946ff"}, + {file = "microsoft_kiota_serialization_text-1.9.1.tar.gz", hash = "sha256:b4b1f61c2388edd704ab33a1792f92f3507db9648d389197a625e152b52743ab"}, ] [package.dependencies] -microsoft-kiota-abstractions = ">=1.6.8,<1.7.0" -python-dateutil = "2.9.0.post0" +microsoft-kiota-abstractions = ">=1.9.1,<1.10.0" [[package]] name = "mkdocs" @@ -2558,13 +2536,13 @@ dev = ["click", "codecov", "mkdocs-gen-files", "mkdocs-git-authors-plugin", "mkd [[package]] name = "mkdocs-material" -version = "9.5.50" +version = "9.6.2" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.50-py3-none-any.whl", hash = "sha256:f24100f234741f4d423a9d672a909d859668a4f404796be3cf035f10d6050385"}, - {file = "mkdocs_material-9.5.50.tar.gz", hash = "sha256:ae5fe16f3d7c9ccd05bb6916a7da7420cf99a9ce5e33debd9d40403a090d5825"}, + {file = "mkdocs_material-9.6.2-py3-none-any.whl", hash = "sha256:71d90dbd63b393ad11a4d90151dfe3dcbfcd802c0f29ce80bebd9bbac6abc753"}, + {file = "mkdocs_material-9.6.2.tar.gz", hash = "sha256:a3de1c5d4c745f10afa78b1a02f917b9dce0808fb206adc0f5bb48b58c1ca21f"}, ] [package.dependencies] @@ -2614,13 +2592,13 @@ test = ["pytest", "pytest-cov"] [[package]] name = "moto" -version = "5.0.27" +version = "5.0.28" description = "A library that allows you to easily mock out tests based on AWS infrastructure" optional = false python-versions = ">=3.8" files = [ - {file = "moto-5.0.27-py3-none-any.whl", hash = "sha256:27042fd94c8def0166d9f2ae8d39d9488d4b3115542b5fca88566c0424549013"}, - {file = "moto-5.0.27.tar.gz", hash = "sha256:6c123de7e0e5e6508a10c399ba3ecf2d5143f263f8e804fd4a7091941c3f5207"}, + {file = "moto-5.0.28-py3-none-any.whl", hash = "sha256:2dfbea1afe3b593e13192059a1a7fc4b3cf7fdf92e432070c22346efa45aa0f0"}, + {file = "moto-5.0.28.tar.gz", hash = "sha256:4d3437693411ec943c13c77de5b0b520c4b0a9ac850fead4ba2a54709e086e8b"}, ] [package.dependencies] @@ -3241,105 +3219,6 @@ files = [ {file = "pbr-6.1.0.tar.gz", hash = "sha256:788183e382e3d1d7707db08978239965e8b9e4e5ed42669bf4758186734d5f24"}, ] -[[package]] -name = "pendulum" -version = "3.0.0" -description = "Python datetimes made easy" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pendulum-3.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2cf9e53ef11668e07f73190c805dbdf07a1939c3298b78d5a9203a86775d1bfd"}, - {file = "pendulum-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb551b9b5e6059377889d2d878d940fd0bbb80ae4810543db18e6f77b02c5ef6"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c58227ac260d5b01fc1025176d7b31858c9f62595737f350d22124a9a3ad82d"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60fb6f415fea93a11c52578eaa10594568a6716602be8430b167eb0d730f3332"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b69f6b4dbcb86f2c2fe696ba991e67347bcf87fe601362a1aba6431454b46bde"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:138afa9c373ee450ede206db5a5e9004fd3011b3c6bbe1e57015395cd076a09f"}, - {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:83d9031f39c6da9677164241fd0d37fbfc9dc8ade7043b5d6d62f56e81af8ad2"}, - {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c2308af4033fa534f089595bcd40a95a39988ce4059ccd3dc6acb9ef14ca44a"}, - {file = "pendulum-3.0.0-cp310-none-win_amd64.whl", hash = "sha256:9a59637cdb8462bdf2dbcb9d389518c0263799189d773ad5c11db6b13064fa79"}, - {file = "pendulum-3.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3725245c0352c95d6ca297193192020d1b0c0f83d5ee6bb09964edc2b5a2d508"}, - {file = "pendulum-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6c035f03a3e565ed132927e2c1b691de0dbf4eb53b02a5a3c5a97e1a64e17bec"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597e66e63cbd68dd6d58ac46cb7a92363d2088d37ccde2dae4332ef23e95cd00"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99a0f8172e19f3f0c0e4ace0ad1595134d5243cf75985dc2233e8f9e8de263ca"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77d8839e20f54706aed425bec82a83b4aec74db07f26acd039905d1237a5e1d4"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afde30e8146292b059020fbc8b6f8fd4a60ae7c5e6f0afef937bbb24880bdf01"}, - {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:660434a6fcf6303c4efd36713ca9212c753140107ee169a3fc6c49c4711c2a05"}, - {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dee9e5a48c6999dc1106eb7eea3e3a50e98a50651b72c08a87ee2154e544b33e"}, - {file = "pendulum-3.0.0-cp311-none-win_amd64.whl", hash = "sha256:d4cdecde90aec2d67cebe4042fd2a87a4441cc02152ed7ed8fb3ebb110b94ec4"}, - {file = "pendulum-3.0.0-cp311-none-win_arm64.whl", hash = "sha256:773c3bc4ddda2dda9f1b9d51fe06762f9200f3293d75c4660c19b2614b991d83"}, - {file = "pendulum-3.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:409e64e41418c49f973d43a28afe5df1df4f1dd87c41c7c90f1a63f61ae0f1f7"}, - {file = "pendulum-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a38ad2121c5ec7c4c190c7334e789c3b4624798859156b138fcc4d92295835dc"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fde4d0b2024b9785f66b7f30ed59281bd60d63d9213cda0eb0910ead777f6d37"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b2c5675769fb6d4c11238132962939b960fcb365436b6d623c5864287faa319"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8af95e03e066826f0f4c65811cbee1b3123d4a45a1c3a2b4fc23c4b0dff893b5"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2165a8f33cb15e06c67070b8afc87a62b85c5a273e3aaa6bc9d15c93a4920d6f"}, - {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ad5e65b874b5e56bd942546ea7ba9dd1d6a25121db1c517700f1c9de91b28518"}, - {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17fe4b2c844bbf5f0ece69cfd959fa02957c61317b2161763950d88fed8e13b9"}, - {file = "pendulum-3.0.0-cp312-none-win_amd64.whl", hash = "sha256:78f8f4e7efe5066aca24a7a57511b9c2119f5c2b5eb81c46ff9222ce11e0a7a5"}, - {file = "pendulum-3.0.0-cp312-none-win_arm64.whl", hash = "sha256:28f49d8d1e32aae9c284a90b6bb3873eee15ec6e1d9042edd611b22a94ac462f"}, - {file = "pendulum-3.0.0-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:d4e2512f4e1a4670284a153b214db9719eb5d14ac55ada5b76cbdb8c5c00399d"}, - {file = "pendulum-3.0.0-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:3d897eb50883cc58d9b92f6405245f84b9286cd2de6e8694cb9ea5cb15195a32"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e169cc2ca419517f397811bbe4589cf3cd13fca6dc38bb352ba15ea90739ebb"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17c3084a4524ebefd9255513692f7e7360e23c8853dc6f10c64cc184e1217ab"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:826d6e258052715f64d05ae0fc9040c0151e6a87aae7c109ba9a0ed930ce4000"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2aae97087872ef152a0c40e06100b3665d8cb86b59bc8471ca7c26132fccd0f"}, - {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ac65eeec2250d03106b5e81284ad47f0d417ca299a45e89ccc69e36130ca8bc7"}, - {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a5346d08f3f4a6e9e672187faa179c7bf9227897081d7121866358af369f44f9"}, - {file = "pendulum-3.0.0-cp37-none-win_amd64.whl", hash = "sha256:235d64e87946d8f95c796af34818c76e0f88c94d624c268693c85b723b698aa9"}, - {file = "pendulum-3.0.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:6a881d9c2a7f85bc9adafcfe671df5207f51f5715ae61f5d838b77a1356e8b7b"}, - {file = "pendulum-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d7762d2076b9b1cb718a6631ad6c16c23fc3fac76cbb8c454e81e80be98daa34"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e8e36a8130819d97a479a0e7bf379b66b3b1b520e5dc46bd7eb14634338df8c"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7dc843253ac373358ffc0711960e2dd5b94ab67530a3e204d85c6e8cb2c5fa10"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a78ad3635d609ceb1e97d6aedef6a6a6f93433ddb2312888e668365908c7120"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a137e9e0d1f751e60e67d11fc67781a572db76b2296f7b4d44554761049d6"}, - {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c95984037987f4a457bb760455d9ca80467be792236b69d0084f228a8ada0162"}, - {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d29c6e578fe0f893766c0d286adbf0b3c726a4e2341eba0917ec79c50274ec16"}, - {file = "pendulum-3.0.0-cp38-none-win_amd64.whl", hash = "sha256:deaba8e16dbfcb3d7a6b5fabdd5a38b7c982809567479987b9c89572df62e027"}, - {file = "pendulum-3.0.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b11aceea5b20b4b5382962b321dbc354af0defe35daa84e9ff3aae3c230df694"}, - {file = "pendulum-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a90d4d504e82ad236afac9adca4d6a19e4865f717034fc69bafb112c320dcc8f"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:825799c6b66e3734227756fa746cc34b3549c48693325b8b9f823cb7d21b19ac"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad769e98dc07972e24afe0cff8d365cb6f0ebc7e65620aa1976fcfbcadc4c6f3"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6fc26907eb5fb8cc6188cc620bc2075a6c534d981a2f045daa5f79dfe50d512"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c717eab1b6d898c00a3e0fa7781d615b5c5136bbd40abe82be100bb06df7a56"}, - {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3ddd1d66d1a714ce43acfe337190be055cdc221d911fc886d5a3aae28e14b76d"}, - {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:822172853d7a9cf6da95d7b66a16c7160cb99ae6df55d44373888181d7a06edc"}, - {file = "pendulum-3.0.0-cp39-none-win_amd64.whl", hash = "sha256:840de1b49cf1ec54c225a2a6f4f0784d50bd47f68e41dc005b7f67c7d5b5f3ae"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b1f74d1e6ffe5d01d6023870e2ce5c2191486928823196f8575dcc786e107b1"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:729e9f93756a2cdfa77d0fc82068346e9731c7e884097160603872686e570f07"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e586acc0b450cd21cbf0db6bae386237011b75260a3adceddc4be15334689a9a"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22e7944ffc1f0099a79ff468ee9630c73f8c7835cd76fdb57ef7320e6a409df4"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fa30af36bd8e50686846bdace37cf6707bdd044e5cb6e1109acbad3277232e04"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:440215347b11914ae707981b9a57ab9c7b6983ab0babde07063c6ee75c0dc6e7"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:314c4038dc5e6a52991570f50edb2f08c339debdf8cea68ac355b32c4174e820"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5acb1d386337415f74f4d1955c4ce8d0201978c162927d07df8eb0692b2d8533"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a789e12fbdefaffb7b8ac67f9d8f22ba17a3050ceaaa635cd1cc4645773a4b1e"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:860aa9b8a888e5913bd70d819306749e5eb488e6b99cd6c47beb701b22bdecf5"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5ebc65ea033ef0281368217fbf59f5cb05b338ac4dd23d60959c7afcd79a60a0"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d9fef18ab0386ef6a9ac7bad7e43ded42c83ff7ad412f950633854f90d59afa8"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1c134ba2f0571d0b68b83f6972e2307a55a5a849e7dac8505c715c531d2a8795"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:385680812e7e18af200bb9b4a49777418c32422d05ad5a8eb85144c4a285907b"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eec91cd87c59fb32ec49eb722f375bd58f4be790cae11c1b70fac3ee4f00da0"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4386bffeca23c4b69ad50a36211f75b35a4deb6210bdca112ac3043deb7e494a"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dfbcf1661d7146d7698da4b86e7f04814221081e9fe154183e34f4c5f5fa3bf8"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:04a1094a5aa1daa34a6b57c865b25f691848c61583fb22722a4df5699f6bf74c"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5b0ec85b9045bd49dd3a3493a5e7ddfd31c36a2a60da387c419fa04abcaecb23"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0a15b90129765b705eb2039062a6daf4d22c4e28d1a54fa260892e8c3ae6e157"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:bb8f6d7acd67a67d6fedd361ad2958ff0539445ef51cbe8cd288db4306503cd0"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd69b15374bef7e4b4440612915315cc42e8575fcda2a3d7586a0d88192d0c88"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc00f8110db6898360c53c812872662e077eaf9c75515d53ecc65d886eec209a"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:83a44e8b40655d0ba565a5c3d1365d27e3e6778ae2a05b69124db9e471255c4a"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1a3604e9fbc06b788041b2a8b78f75c243021e0f512447806a6d37ee5214905d"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:92c307ae7accebd06cbae4729f0ba9fa724df5f7d91a0964b1b972a22baa482b"}, - {file = "pendulum-3.0.0.tar.gz", hash = "sha256:5d034998dea404ec31fae27af6b22cff1708f830a1ed7353be4d1019bb9f584e"}, -] - -[package.dependencies] -python-dateutil = ">=2.6" -tzdata = ">=2020.1" - -[package.extras] -test = ["time-machine (>=2.6.0)"] - [[package]] name = "platformdirs" version = "4.3.6" @@ -3575,13 +3454,13 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] [[package]] name = "py-ocsf-models" -version = "0.2.0" +version = "0.3.0" description = "This is a Python implementation of the OCSF models. The models are used to represent the data of the OCSF Schema defined in https://schema.ocsf.io/." optional = false python-versions = "<3.13,>=3.9" files = [ - {file = "py_ocsf_models-0.2.0-py3-none-any.whl", hash = "sha256:ac75fd21077694b343ebaad3479194db113c274879b114277560ff287d5cd7b5"}, - {file = "py_ocsf_models-0.2.0.tar.gz", hash = "sha256:3e12648d05329e6776a0e6b1ffea87a3eb60aa7d8cb2c4afd69e5724f443ce03"}, + {file = "py_ocsf_models-0.3.0-py3-none-any.whl", hash = "sha256:3d31e379be5e4271f7faf62dee9c36798559a1f7f98dff142c0e4cfdb35e291c"}, + {file = "py_ocsf_models-0.3.0.tar.gz", hash = "sha256:ad46b7d9761b74010f06a894df2d9541989252b7ff738cd5c7edbf4283df2279"}, ] [package.dependencies] @@ -3756,13 +3635,13 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] name = "pylint" -version = "3.3.3" +version = "3.3.4" description = "python code static checker" optional = false python-versions = ">=3.9.0" files = [ - {file = "pylint-3.3.3-py3-none-any.whl", hash = "sha256:26e271a2bc8bce0fc23833805a9076dd9b4d5194e2a02164942cb3cdc37b4183"}, - {file = "pylint-3.3.3.tar.gz", hash = "sha256:07c607523b17e6d16e2ae0d7ef59602e332caa762af64203c24b41c27139f36a"}, + {file = "pylint-3.3.4-py3-none-any.whl", hash = "sha256:289e6a1eb27b453b08436478391a48cd53bb0efb824873f949e709350f3de018"}, + {file = "pylint-3.3.4.tar.gz", hash = "sha256:74ae7a38b177e69a9b525d0794bd8183820bfa7eb68cc1bee6e8ed22a42be4ce"}, ] [package.dependencies] @@ -3773,7 +3652,7 @@ dill = [ {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, ] -isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" +isort = ">=4.2.5,<5.13.0 || >5.13.0,<7" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} @@ -3925,13 +3804,13 @@ six = ">=1.5" [[package]] name = "pytz" -version = "2024.2" +version = "2025.1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, - {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, + {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"}, + {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"}, ] [[package]] @@ -4431,6 +4310,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, @@ -4439,6 +4319,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, @@ -4447,6 +4328,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, @@ -4455,6 +4337,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, @@ -4463,6 +4346,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, @@ -5206,4 +5090,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "f2d3920001616ff9b66353b61d20dadc358787aa154c31fcbd8f4baf45c36498" +content-hash = "7511c2a5abad01f351b1d222d73a6b2c5374f386e8c035cce79934daca7ea762" diff --git a/prowler/compliance/aws/aws_audit_manager_control_tower_guardrails_aws.json b/prowler/compliance/aws/aws_audit_manager_control_tower_guardrails_aws.json index 34a5856fd7..fa4d71c05b 100644 --- a/prowler/compliance/aws/aws_audit_manager_control_tower_guardrails_aws.json +++ b/prowler/compliance/aws/aws_audit_manager_control_tower_guardrails_aws.json @@ -28,7 +28,9 @@ "Service": "ebs" } ], - "Checks": [] + "Checks": [ + "ec2_ebs_volume_snapshots_exists" + ] }, { "Id": "1.0.3", @@ -42,7 +44,8 @@ } ], "Checks": [ - "ec2_ebs_default_encryption" + "ec2_ebs_default_encryption", + "ec2_ebs_volume_encryption" ] }, { @@ -87,7 +90,9 @@ } ], "Checks": [ - "iam_user_mfa_enabled_console_access" + "iam_user_mfa_enabled_console_access", + "iam_user_hardware_mfa_enabled", + "iam_root_mfa_enabled" ] }, { @@ -102,7 +107,9 @@ } ], "Checks": [ - "iam_user_mfa_enabled_console_access" + "iam_user_mfa_enabled_console_access", + "iam_user_hardware_mfa_enabled", + "iam_root_mfa_enabled" ] }, { @@ -117,7 +124,9 @@ } ], "Checks": [ - "iam_root_mfa_enabled" + "iam_root_mfa_enabled", + "iam_root_hardware_mfa_enabled", + "iam_user_mfa_enabled_console_access" ] }, { @@ -162,7 +171,10 @@ } ], "Checks": [ - "rds_instance_no_public_access" + "rds_instance_no_public_access", + "s3_bucket_public_access", + "s3_bucket_public_list_acl", + "s3_account_level_public_access_blocks" ] }, { @@ -192,7 +204,8 @@ } ], "Checks": [ - "rds_instance_storage_encrypted" + "rds_instance_storage_encrypted", + "rds_instance_transport_encrypted" ] }, { diff --git a/prowler/compliance/aws/cis_1.4_aws.json b/prowler/compliance/aws/cis_1.4_aws.json index da77feb991..584ca284c6 100644 --- a/prowler/compliance/aws/cis_1.4_aws.json +++ b/prowler/compliance/aws/cis_1.4_aws.json @@ -584,7 +584,8 @@ "Id": "2.3.1", "Description": "Ensure that encryption is enabled for RDS Instances", "Checks": [ - "rds_instance_storage_encrypted" + "rds_instance_storage_encrypted", + "rds_instance_transport_encrypted" ], "Attributes": [ { diff --git a/prowler/compliance/aws/cis_1.5_aws.json b/prowler/compliance/aws/cis_1.5_aws.json index 6d5283b81c..90ca3742e4 100644 --- a/prowler/compliance/aws/cis_1.5_aws.json +++ b/prowler/compliance/aws/cis_1.5_aws.json @@ -584,7 +584,8 @@ "Id": "2.3.1", "Description": "Ensure that encryption is enabled for RDS Instances", "Checks": [ - "rds_instance_storage_encrypted" + "rds_instance_storage_encrypted", + "rds_instance_transport_encrypted" ], "Attributes": [ { diff --git a/prowler/compliance/aws/cis_2.0_aws.json b/prowler/compliance/aws/cis_2.0_aws.json index 09f2ebe08f..1d12e296d5 100644 --- a/prowler/compliance/aws/cis_2.0_aws.json +++ b/prowler/compliance/aws/cis_2.0_aws.json @@ -303,7 +303,9 @@ { "Id": "1.22", "Description": "Ensure access to AWSCloudShellFullAccess is restricted", - "Checks": [], + "Checks": [ + "iam_policy_cloudshell_admin_not_attached" + ], "Attributes": [ { "Section": "1. Identity and Access Management", @@ -492,7 +494,8 @@ "Id": "2.1.2", "Description": "Ensure MFA Delete is enabled on S3 buckets", "Checks": [ - "s3_bucket_no_mfa_delete" + "s3_bucket_no_mfa_delete", + "cloudtrail_bucket_requires_mfa_delete" ], "Attributes": [ { @@ -581,7 +584,8 @@ "Id": "2.3.1", "Description": "Ensure that encryption is enabled for RDS Instances", "Checks": [ - "rds_instance_storage_encrypted" + "rds_instance_storage_encrypted", + "rds_instance_transport_encrypted" ], "Attributes": [ { @@ -1347,7 +1351,8 @@ "Id": "5.6", "Description": "Ensure that EC2 Metadata Service only allows IMDSv2", "Checks": [ - "ec2_instance_imdsv2_enabled" + "ec2_instance_imdsv2_enabled", + "ec2_instance_account_imdsv2_enabled" ], "Attributes": [ { diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index 6983644c2a..87e0fda5f4 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -567,6 +567,7 @@ class Check_Report_Microsoft365(CheckReport): resource_name: str resource_id: str + tenant_id: str location: str def __init__(self, metadata: Dict, resource: Any) -> None: @@ -580,7 +581,8 @@ class Check_Report_Microsoft365(CheckReport): self.resource_name = getattr( resource, "name", getattr(resource, "resource_name") ) - self.resource_id = getattr(resource, "id", getattr(resource, "resource_id")) + self.resource_id = getattr(resource, "id", getattr(resource, "resource_id", "")) + self.tenant_id = getattr(resource, "tenant_id", "") self.location = getattr(resource, "location", "global") diff --git a/prowler/lib/outputs/outputs.py b/prowler/lib/outputs/outputs.py index 2a61e17c36..b4699157d7 100644 --- a/prowler/lib/outputs/outputs.py +++ b/prowler/lib/outputs/outputs.py @@ -83,7 +83,9 @@ def set_report_color(status: str, muted: bool = False) -> str: elif status == "MANUAL": color = Fore.YELLOW else: - raise Exception("Invalid Report Status. Must be PASS, FAIL or MANUAL.") + raise Exception( + f"Invalid Report Status: {status}. Must be PASS, FAIL or MANUAL." + ) return color diff --git a/prowler/providers/aws/services/elasticache/elasticache_service.py b/prowler/providers/aws/services/elasticache/elasticache_service.py index 5fc295630e..6e11a85bc6 100644 --- a/prowler/providers/aws/services/elasticache/elasticache_service.py +++ b/prowler/providers/aws/services/elasticache/elasticache_service.py @@ -147,6 +147,12 @@ class ElastiCache(AWSService): logger.warning( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + except ( + regional_client.exceptions.InvalidReplicationGroupStateFault + ) as error: + logger.warning( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -163,6 +169,12 @@ class ElastiCache(AWSService): logger.warning( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + except ( + regional_client.exceptions.InvalidReplicationGroupStateFault + ) as error: + logger.warning( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" diff --git a/prowler/providers/aws/services/neptune/neptune_cluster_iam_authentication_enabled/neptune_cluster_iam_authentication_enabled.metadata.json b/prowler/providers/aws/services/neptune/neptune_cluster_iam_authentication_enabled/neptune_cluster_iam_authentication_enabled.metadata.json index 5be9fa6c4f..c6e29617f0 100644 --- a/prowler/providers/aws/services/neptune/neptune_cluster_iam_authentication_enabled/neptune_cluster_iam_authentication_enabled.metadata.json +++ b/prowler/providers/aws/services/neptune/neptune_cluster_iam_authentication_enabled/neptune_cluster_iam_authentication_enabled.metadata.json @@ -3,7 +3,7 @@ "CheckID": "neptune_cluster_iam_authentication_enabled", "CheckTitle": "Check if Neptune Clusters have IAM authentication enabled.", "CheckType": [], - "ServiceName": "rds", + "ServiceName": "neptune", "SubServiceName": "", "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster", "Severity": "medium", diff --git a/prowler/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription.py b/prowler/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription.py index e2f379232b..43751d7369 100644 --- a/prowler/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription.py +++ b/prowler/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription.py @@ -54,6 +54,9 @@ class rds_instance_critical_event_subscription(Check): }: report.status = "FAIL" report.status_extended = "RDS instance event category of maintenance is not subscribed." + else: + report.status = "FAIL" + report.status_extended = "RDS instance event categories of maintenance, configuration change, and failure are not subscribed." findings.append(report) return findings diff --git a/prowler/providers/gcp/services/compute/compute_public_address_shodan/compute_public_address_shodan.metadata.json b/prowler/providers/gcp/services/compute/compute_public_address_shodan/compute_public_address_shodan.metadata.json index 5c19d5d1f7..d897b42a1f 100644 --- a/prowler/providers/gcp/services/compute/compute_public_address_shodan/compute_public_address_shodan.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_public_address_shodan/compute_public_address_shodan.metadata.json @@ -1,5 +1,5 @@ { - "Provider": "compute", + "Provider": "gcp", "CheckID": "compute_public_address_shodan", "CheckTitle": "Check if any of the Public Addresses are in Shodan (requires Shodan API KEY).", "CheckType": [ diff --git a/prowler/providers/microsoft365/lib/regions/microsoft365_regions.py b/prowler/providers/microsoft365/lib/regions/regions.py similarity index 100% rename from prowler/providers/microsoft365/lib/regions/microsoft365_regions.py rename to prowler/providers/microsoft365/lib/regions/regions.py diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index dff8da4d97..af7a723f67 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -50,9 +50,7 @@ from prowler.providers.microsoft365.exceptions.exceptions import ( Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError, ) from prowler.providers.microsoft365.lib.mutelist.mutelist import Microsoft365Mutelist -from prowler.providers.microsoft365.lib.regions.microsoft365_regions import ( - get_regions_config, -) +from prowler.providers.microsoft365.lib.regions.regions import get_regions_config from prowler.providers.microsoft365.models import ( Microsoft365IdentityInfo, Microsoft365RegionConfig, diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_service.py b/prowler/providers/microsoft365/services/admincenter/admincenter_service.py index 2fc14d1ae6..fed450ca82 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_service.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_service.py @@ -22,11 +22,13 @@ class AdminCenter(Microsoft365Service): gather( self._get_directory_roles(), self._get_groups(), + self._get_domains(), ) ) self.directory_roles = attributes[0] self.groups = attributes[1] + self.domains = attributes[2] async def _get_users(self): logger.info("Microsoft365 - Getting users...") @@ -130,6 +132,28 @@ class AdminCenter(Microsoft365Service): ) return groups + async def _get_domains(self): + logger.info("Microsoft365 - Getting domains...") + domains = {} + try: + domains_list = await self.client.domains.get() + domains.update({}) + for domain in domains_list.value: + domains.update( + { + domain.id: Domain( + id=domain.id, + password_validity_period=domain.password_validity_period_in_days, + ) + } + ) + + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return domains + class User(BaseModel): id: str @@ -149,3 +173,8 @@ class Group(BaseModel): id: str name: str visibility: str + + +class Domain(BaseModel): + id: str + password_validity_period: int diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/__init__.py b/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.metadata.json b/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.metadata.json new file mode 100644 index 0000000000..13528b0008 --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "microsoft", + "CheckID": "admincenter_settings_password_never_expire", + "CheckTitle": "Ensure the 'Password expiration policy' is set to 'Set passwords to never expire (recommended)'", + "CheckType": [], + "ServiceName": "admincenter", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Microsoft365Domain", + "Description": "This control ensures that the password expiration policy is set to 'Set passwords to never expire (recommended)'. This aligns with modern recommendations to enhance security by avoiding arbitrary password changes and focusing on supplementary controls like MFA.", + "Risk": "Arbitrary password expiration policies can lead to weaker passwords due to frequent changes. Users may adopt insecure habits such as using simple, memorable passwords.", + "RelatedUrl": "https://www.cisecurity.org/insights/white-papers/cis-password-policy-guide", + "Remediation": { + "Code": { + "CLI": "Set-MsolUser -UserPrincipalName -PasswordNeverExpires $true", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable the 'Never Expire Passwords' option in Microsoft 365 Admin Center.", + "Url": "https://learn.microsoft.com/en-us/microsoft-365/admin/misc/password-policy-recommendations?view=o365-worldwide" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.py b/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.py new file mode 100644 index 0000000000..8de62dd8d5 --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.py @@ -0,0 +1,27 @@ +from prowler.lib.check.models import Check, Check_Report_Microsoft365 +from prowler.providers.microsoft365.services.admincenter.admincenter_client import ( + admincenter_client, +) + + +class admincenter_settings_password_never_expire(Check): + def execute(self) -> Check_Report_Microsoft365: + findings = [] + for domain in admincenter_client.domains.values(): + report = Check_Report_Microsoft365(self.metadata(), resource=domain) + report.resource_id = domain.id + report.resource_name = domain.id + report.status = "FAIL" + report.status_extended = ( + f"Domain {domain.id} does not have a Password never expires policy." + ) + + if domain.password_validity_period == 2147483647: + report.status = "PASS" + report.status_extended = ( + f"Domain {domain.id} Password policy is set to never expire." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py index aa0fc978c0..6002e67e4c 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py @@ -9,9 +9,7 @@ class admincenter_users_between_two_and_four_global_admins(Check): findings = [] directory_roles = admincenter_client.directory_roles - report = Check_Report_Microsoft365( - metadata=self.metadata(), resource=admincenter_client.directory_roles - ) + report = Check_Report_Microsoft365(metadata=self.metadata(), resource={}) report.status = "FAIL" report.resource_name = "Global Administrator" diff --git a/prowler/providers/microsoft365/services/entra/__init__.py b/prowler/providers/microsoft365/services/entra/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/microsoft365/services/entra/entra_client.py b/prowler/providers/microsoft365/services/entra/entra_client.py new file mode 100644 index 0000000000..1a3b921adf --- /dev/null +++ b/prowler/providers/microsoft365/services/entra/entra_client.py @@ -0,0 +1,4 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.microsoft365.services.entra.entra_service import Entra + +entra_client = Entra(Provider.get_global_provider()) diff --git a/prowler/providers/microsoft365/services/entra/entra_service.py b/prowler/providers/microsoft365/services/entra/entra_service.py new file mode 100644 index 0000000000..8561a31edd --- /dev/null +++ b/prowler/providers/microsoft365/services/entra/entra_service.py @@ -0,0 +1,105 @@ +from asyncio import gather, get_event_loop +from typing import List, Optional + +from pydantic import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.microsoft365.lib.service.service import Microsoft365Service +from prowler.providers.microsoft365.microsoft365_provider import Microsoft365Provider + + +class Entra(Microsoft365Service): + def __init__(self, provider: Microsoft365Provider): + super().__init__(provider) + + loop = get_event_loop() + + attributes = loop.run_until_complete( + gather( + self._get_authorization_policy(), + ) + ) + + self.authorization_policy = attributes[0] + + async def _get_authorization_policy(self): + logger.info("Entra - Getting authorization policy...") + + authorization_policy = {} + try: + auth_policy = await self.client.policies.authorization_policy.get() + + default_user_role_permissions = getattr( + auth_policy, "default_user_role_permissions", None + ) + + authorization_policy.update( + { + auth_policy.id: AuthorizationPolicy( + id=auth_policy.id, + name=auth_policy.display_name, + description=auth_policy.description, + default_user_role_permissions=DefaultUserRolePermissions( + allowed_to_create_apps=getattr( + default_user_role_permissions, + "allowed_to_create_apps", + None, + ), + allowed_to_create_security_groups=getattr( + default_user_role_permissions, + "allowed_to_create_security_groups", + None, + ), + allowed_to_create_tenants=getattr( + default_user_role_permissions, + "allowed_to_create_tenants", + None, + ), + allowed_to_read_bitlocker_keys_for_owned_device=getattr( + default_user_role_permissions, + "allowed_to_read_bitlocker_keys_for_owned_device", + None, + ), + allowed_to_read_other_users=getattr( + default_user_role_permissions, + "allowed_to_read_other_users", + None, + ), + odata_type=getattr( + default_user_role_permissions, "odata_type", None + ), + permission_grant_policies_assigned=[ + policy_assigned + for policy_assigned in getattr( + default_user_role_permissions, + "permission_grant_policies_assigned", + [], + ) + ], + ), + ) + } + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + return authorization_policy + + +class DefaultUserRolePermissions(BaseModel): + allowed_to_create_apps: Optional[bool] + allowed_to_create_security_groups: Optional[bool] + allowed_to_create_tenants: Optional[bool] + allowed_to_read_bitlocker_keys_for_owned_device: Optional[bool] + allowed_to_read_other_users: Optional[bool] + odata_type: Optional[str] + permission_grant_policies_assigned: Optional[List[str]] = None + + +class AuthorizationPolicy(BaseModel): + id: str + name: str + description: str + default_user_role_permissions: Optional[DefaultUserRolePermissions] diff --git a/pyproject.toml b/pyproject.toml index ff3156c743..b9cb7f8b1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,19 +55,19 @@ cryptography = "43.0.1" dash = "2.18.2" dash-bootstrap-components = "1.6.0" detect-secrets = "1.5.0" -google-api-python-client = "2.159.0" +google-api-python-client = "2.160.0" google-auth-httplib2 = ">=0.1,<0.3" jsonschema = "4.23.0" -kubernetes = "31.0.0" -microsoft-kiota-abstractions = "1.6.8" +kubernetes = "32.0.0" +microsoft-kiota-abstractions = "1.9.1" msgraph-sdk = "1.18.0" numpy = "2.0.2" pandas = "2.2.3" -py-ocsf-models = "0.2.0" +py-ocsf-models = "0.3.0" pydantic = "1.10.18" python = ">=3.9,<3.13" python-dateutil = "^2.9.0.post0" -pytz = "2024.2" +pytz = "2025.1" schema = "0.7.7" shodan = "1.31.0" slack-sdk = "3.34.0" @@ -82,10 +82,10 @@ docker = "7.1.0" flake8 = "7.1.1" freezegun = "1.5.1" mock = "5.1.0" -moto = {extras = ["all"], version = "5.0.27"} +moto = {extras = ["all"], version = "5.0.28"} openapi-schema-validator = "0.6.3" openapi-spec-validator = "0.7.1" -pylint = "3.3.3" +pylint = "3.3.4" pytest = "8.3.4" pytest-cov = "6.0.0" pytest-env = "1.1.5" @@ -100,7 +100,7 @@ optional = true [tool.poetry.group.docs.dependencies] mkdocs = "1.6.1" mkdocs-git-revision-date-localized-plugin = "1.3.0" -mkdocs-material = "9.5.50" +mkdocs-material = "9.6.2" mkdocs-material-extensions = "1.3.1" [tool.poetry.scripts] diff --git a/tests/lib/outputs/ocsf/ocsf_test.py b/tests/lib/outputs/ocsf/ocsf_test.py index e7bd8b2059..d272849905 100644 --- a/tests/lib/outputs/ocsf/ocsf_test.py +++ b/tests/lib/outputs/ocsf/ocsf_test.py @@ -175,7 +175,7 @@ class TestOCSF: "vendor_name": "Prowler", "version": prowler_version, }, - "version": "1.3.0", + "version": "1.4.0", "profiles": ["cloud", "datetime"], "tenant_uid": "test-organization-id", }, diff --git a/tests/lib/outputs/outputs_test.py b/tests/lib/outputs/outputs_test.py index 624366af5e..dbf7d5c8bd 100644 --- a/tests/lib/outputs/outputs_test.py +++ b/tests/lib/outputs/outputs_test.py @@ -21,7 +21,6 @@ from prowler.lib.outputs.utils import ( class TestOutputs: - def test_set_report_color(self): test_status = ["PASS", "FAIL", "MANUAL"] test_colors = [Fore.GREEN, Fore.RED, Fore.YELLOW] @@ -35,8 +34,9 @@ class TestOutputs: with pytest.raises(Exception) as exc: set_report_color(test_status) - assert "Invalid Report Status. Must be PASS, FAIL or MANUAL" in str(exc.value) - assert exc.type == Exception + assert "Invalid Report Status: INVALID. Must be PASS, FAIL or MANUAL" in str( + exc.value + ) def test_unroll_list_no_separator(self): list = ["test", "test1", "test2"] diff --git a/tests/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription_test.py b/tests/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription_test.py index d72df2303a..02729be231 100644 --- a/tests/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription_test.py +++ b/tests/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription_test.py @@ -483,3 +483,52 @@ class Test_rds_instance_critical_event_subscription: == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:es:TestSub" ) assert result[0].resource_tags == [] + + @mock_aws + def test_rds_instance_event_invalid(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_parameter_group( + DBParameterGroupName="test", + DBParameterGroupFamily="default.aurora-postgresql14", + Description="test parameter group", + ) + conn.create_event_subscription( + SubscriptionName="TestSub", + SnsTopicArn=f"arn:aws:sns:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:test", + SourceType="db-instance", + EventCategories=["invalid"], + Enabled=True, + ) + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_critical_event_subscription.rds_instance_critical_event_subscription.rds_client", + new=RDS(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.rds.rds_instance_critical_event_subscription.rds_instance_critical_event_subscription import ( + rds_instance_critical_event_subscription, + ) + + check = rds_instance_critical_event_subscription() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "RDS instance event categories of maintenance, configuration change, and failure are not subscribed." + ) + assert result[0].resource_id == "TestSub" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:es:TestSub" + ) + assert result[0].resource_tags == [] diff --git a/tests/providers/microsoft365/lib/regions/microsoft365_regions_test.py b/tests/providers/microsoft365/lib/regions/microsoft365_regions_test.py index 3e358511b8..49ec5a480d 100644 --- a/tests/providers/microsoft365/lib/regions/microsoft365_regions_test.py +++ b/tests/providers/microsoft365/lib/regions/microsoft365_regions_test.py @@ -1,6 +1,6 @@ from azure.identity import AzureAuthorityHosts -from prowler.providers.microsoft365.lib.regions.microsoft365_regions import ( +from prowler.providers.microsoft365.lib.regions.regions import ( MICROSOFT365_CHINA_CLOUD, MICROSOFT365_GENERIC_CLOUD, MICROSOFT365_US_GOV_CLOUD, diff --git a/tests/providers/microsoft365/microsoft365_fixtures.py b/tests/providers/microsoft365/microsoft365_fixtures.py index 79e8ec118f..1dbb09c612 100644 --- a/tests/providers/microsoft365/microsoft365_fixtures.py +++ b/tests/providers/microsoft365/microsoft365_fixtures.py @@ -29,7 +29,6 @@ def set_mocked_microsoft365_provider( audit_config: dict = None, azure_region_config: Microsoft365RegionConfig = Microsoft365RegionConfig(), ) -> Microsoft365Provider: - provider = MagicMock() provider.type = "microsoft365" provider.session.credentials = credentials diff --git a/tests/providers/microsoft365/microsoft365_provider_test.py b/tests/providers/microsoft365/microsoft365_provider_test.py index b475a02ca6..7658301e44 100644 --- a/tests/providers/microsoft365/microsoft365_provider_test.py +++ b/tests/providers/microsoft365/microsoft365_provider_test.py @@ -212,7 +212,6 @@ class TestMicrosoft365Provider: "prowler.providers.microsoft365.microsoft365_provider.GraphServiceClient" ) as mock_graph_client, ): - # Mock the return value of DefaultAzureCredential mock_credentials = MagicMock() mock_credentials.get_token.return_value = AccessToken( @@ -271,7 +270,6 @@ class TestMicrosoft365Provider: with patch( "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" ) as mock_setup_session: - mock_setup_session.side_effect = Microsoft365HTTPResponseError( file="test_file", original_exception="Simulated HttpResponseError" ) diff --git a/tests/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire_test.py b/tests/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire_test.py new file mode 100644 index 0000000000..b49bc98095 --- /dev/null +++ b/tests/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire_test.py @@ -0,0 +1,112 @@ +from unittest import mock +from uuid import uuid4 + +from tests.providers.microsoft365.microsoft365_fixtures import ( + DOMAIN, + set_mocked_microsoft365_provider, +) + + +class Test_admincenter_settings_password_never_expire: + def test_admincenter_no_domains(self): + admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire.admincenter_client", + new=admincenter_client, + ), + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire import ( + admincenter_settings_password_never_expire, + ) + + admincenter_client.domains = {} + + check = admincenter_settings_password_never_expire() + result = check.execute() + assert len(result) == 0 + + def test_admincenter_domain_password_expire(self): + admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire.admincenter_client", + new=admincenter_client, + ), + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + Domain, + ) + from prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire import ( + admincenter_settings_password_never_expire, + ) + + id_domain = str(uuid4()) + + admincenter_client.domains = { + id_domain: Domain(id=id_domain, password_validity_period=5), + } + + check = admincenter_settings_password_never_expire() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Domain {id_domain} does not have a Password never expires policy." + ) + assert result[0].resource_name == id_domain + assert result[0].resource_id == id_domain + + def test_admincenter_password_not_expire(self): + admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire.admincenter_client", + new=admincenter_client, + ), + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + Domain, + ) + from prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire import ( + admincenter_settings_password_never_expire, + ) + + id_domain = str(uuid4()) + + admincenter_client.domains = { + id_domain: Domain(id=id_domain, password_validity_period=2147483647), + } + + check = admincenter_settings_password_never_expire() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Domain {id_domain} Password policy is set to never expire." + ) + assert result[0].resource_name == id_domain + assert result[0].resource_id == id_domain diff --git a/tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py b/tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py new file mode 100644 index 0000000000..86109478e2 --- /dev/null +++ b/tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py @@ -0,0 +1,45 @@ +from unittest.mock import patch + +from prowler.providers.microsoft365.models import Microsoft365IdentityInfo +from prowler.providers.microsoft365.services.entra.entra_service import ( + AuthorizationPolicy, + Entra, +) +from tests.providers.microsoft365.microsoft365_fixtures import ( + DOMAIN, + set_mocked_microsoft365_provider, +) + + +async def mock_entra_get_authorization_policy(_): + return { + "id-1": AuthorizationPolicy( + id="id-1", + name="Name 1", + description="Description 1", + default_user_role_permissions=None, + ) + } + + +@patch( + "prowler.providers.microsoft365.services.entra.entra_service.Entra._get_authorization_policy", + new=mock_entra_get_authorization_policy, +) +class Test_Entra_Service: + def test_get_client(self): + admincenter_client = Entra( + set_mocked_microsoft365_provider( + identity=Microsoft365IdentityInfo(tenant_domain=DOMAIN) + ) + ) + assert admincenter_client.client.__class__.__name__ == "GraphServiceClient" + + def test_get_authorization_policy(self): + entra_client = Entra(set_mocked_microsoft365_provider()) + assert entra_client.authorization_policy["id-1"].id == "id-1" + assert entra_client.authorization_policy["id-1"].name == "Name 1" + assert entra_client.authorization_policy["id-1"].description == "Description 1" + assert not entra_client.authorization_policy[ + "id-1" + ].default_user_role_permissions diff --git a/ui/app/(prowler)/findings/page.tsx b/ui/app/(prowler)/findings/page.tsx index 0d35cac8c4..39d2ad7846 100644 --- a/ui/app/(prowler)/findings/page.tsx +++ b/ui/app/(prowler)/findings/page.tsx @@ -26,21 +26,19 @@ export default async function Findings({ searchParams: SearchParamsProps; }) { const searchParamsKey = JSON.stringify(searchParams || {}); - const defaultSort = "severity,status"; - const sort = searchParams.sort?.toString() || defaultSort; + const sort = searchParams.sort?.toString(); // Make sure the sort is correctly encoded - const encodedSort = sort.replace(/^\+/, ""); - - // Extract all filter parameters and combine with default filters - const defaultFilters = { - "filter[status__in]": "FAIL, PASS", - }; + const encodedSort = sort?.replace(/^\+/, ""); const filters: Record = { - ...defaultFilters, ...Object.fromEntries( - Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), + Object.entries(searchParams) + .filter(([key]) => key.startsWith("filter[")) + .map(([key, value]) => [ + key, + Array.isArray(value) ? value.join(",") : value?.toString() || "", + ]), ), }; @@ -137,21 +135,20 @@ const SSRDataTable = async ({ searchParams: SearchParamsProps; }) => { const page = parseInt(searchParams.page?.toString() || "1", 10); - const defaultSort = "severity,status"; + const defaultSort = "severity,status,-inserted_at"; const sort = searchParams.sort?.toString() || defaultSort; // Make sure the sort is correctly encoded const encodedSort = sort.replace(/^\+/, ""); - // Extract all filter parameters and combine with default filters - const defaultFilters = { - "filter[status__in]": "FAIL, PASS", - }; - const filters: Record = { - ...defaultFilters, ...Object.fromEntries( - Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), + Object.entries(searchParams) + .filter(([key]) => key.startsWith("filter[")) + .map(([key, value]) => [ + key, + Array.isArray(value) ? value.join(",") : value?.toString() || "", + ]), ), }; diff --git a/ui/app/(prowler)/page.tsx b/ui/app/(prowler)/page.tsx index ff2ab3d28c..c6e0748362 100644 --- a/ui/app/(prowler)/page.tsx +++ b/ui/app/(prowler)/page.tsx @@ -129,7 +129,7 @@ const SSRFindingsBySeverity = async ({ const SSRDataNewFindingsTable = async () => { const page = 1; - const sort = "severity,updated_at"; + const sort = "severity,-inserted_at"; const defaultFilters = { "filter[status__in]": "FAIL", diff --git a/ui/app/(prowler)/scans/page.tsx b/ui/app/(prowler)/scans/page.tsx index 9781627118..db38bd4e97 100644 --- a/ui/app/(prowler)/scans/page.tsx +++ b/ui/app/(prowler)/scans/page.tsx @@ -8,7 +8,6 @@ import { ButtonRefreshData, NoProvidersAdded, NoProvidersConnected, - ScanWarningBar, } from "@/components/scans"; import { LaunchScanWorkflow } from "@/components/scans/launch-workflow"; import { SkeletonTableScans } from "@/components/scans/table"; @@ -73,8 +72,6 @@ export default async function Scans({
- - )} diff --git a/ui/components/auth/oss/auth-form.tsx b/ui/components/auth/oss/auth-form.tsx index e215abad4f..967bbdd352 100644 --- a/ui/components/auth/oss/auth-form.tsx +++ b/ui/components/auth/oss/auth-form.tsx @@ -1,7 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Link } from "@nextui-org/react"; +import { Checkbox, Link } from "@nextui-org/react"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -11,7 +11,12 @@ import { NotificationIcon, ProwlerExtended } from "@/components/icons"; import { ThemeSwitch } from "@/components/ThemeSwitch"; import { useToast } from "@/components/ui"; import { CustomButton, CustomInput } from "@/components/ui/custom"; -import { Form } from "@/components/ui/form"; +import { + Form, + FormControl, + FormField, + FormMessage, +} from "@/components/ui/form"; import { ApiError, authFormSchema } from "@/types"; export const AuthForm = ({ @@ -143,7 +148,7 @@ export const AuthForm = ({
{type === "sign-up" && ( @@ -167,6 +172,7 @@ export const AuthForm = ({ /> )} + {/* {type === "sign-in" && ( @@ -213,13 +223,44 @@ export const AuthForm = ({ isDisabled={invitationToken !== null && true} /> )} + + {process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true" && ( + ( + <> + + field.onChange(e.target.checked)} + > + I agree with the  + + Terms of Service + +  of Prowler + + + + + )} + /> + )} )} - {form.formState.errors?.email && ( -
+ {type === "sign-in" && form.formState.errors?.email && ( +
-

No user found

+

Invalid email or password

)} diff --git a/ui/components/scans/index.ts b/ui/components/scans/index.ts index 98157857ab..aa42edfe3c 100644 --- a/ui/components/scans/index.ts +++ b/ui/components/scans/index.ts @@ -2,4 +2,3 @@ export * from "./button-refresh-data"; export * from "./link-to-findings-from-scan"; export * from "./no-providers-added"; export * from "./no-providers-connected"; -export * from "./scan-warning-bar"; diff --git a/ui/components/scans/scan-warning-bar.tsx b/ui/components/scans/scan-warning-bar.tsx deleted file mode 100644 index 4c9acbb978..0000000000 --- a/ui/components/scans/scan-warning-bar.tsx +++ /dev/null @@ -1,18 +0,0 @@ -"use client"; - -import { InfoIcon } from "../icons"; - -export const ScanWarningBar = () => { - return ( -
- -
- Waiting for Your Scan to Show Up? -

- It may take a few minutes for the scan to appear on the table and be - displayed. -

-
-
- ); -}; diff --git a/ui/components/scans/table/scan-detail.tsx b/ui/components/scans/table/scan-detail.tsx index 6e9c3e288c..7307ff53f0 100644 --- a/ui/components/scans/table/scan-detail.tsx +++ b/ui/components/scans/table/scan-detail.tsx @@ -117,11 +117,9 @@ export const ScanDetail = ({ - {scan.next_scan_at && ( - - - - )} + + +
diff --git a/ui/components/scans/table/scans/column-get-scans.tsx b/ui/components/scans/table/scans/column-get-scans.tsx index 55087a0e66..72a70f9311 100644 --- a/ui/components/scans/table/scans/column-get-scans.tsx +++ b/ui/components/scans/table/scans/column-get-scans.tsx @@ -123,13 +123,13 @@ export const ColumnGetScans: ColumnDef[] = [ }, }, { - accessorKey: "next_scan_at", - header: "Next scan", + accessorKey: "scheduled_at", + header: "Scheduled at", cell: ({ row }) => { const { - attributes: { next_scan_at }, + attributes: { scheduled_at }, } = getScanData(row); - return ; + return ; }, }, { diff --git a/ui/components/ui/custom/custom-input.tsx b/ui/components/ui/custom/custom-input.tsx index 1a4cdb5dfe..d01302e16f 100644 --- a/ui/components/ui/custom/custom-input.tsx +++ b/ui/components/ui/custom/custom-input.tsx @@ -23,6 +23,7 @@ interface CustomInputProps { isRequired?: boolean; isInvalid?: boolean; isDisabled?: boolean; + showFormMessage?: boolean; } export const CustomInput = ({ @@ -41,6 +42,7 @@ export const CustomInput = ({ isRequired = true, isInvalid, isDisabled = false, + showFormMessage = true, }: CustomInputProps) => { const [isPasswordVisible, setIsPasswordVisible] = useState(false); const [isConfirmPasswordVisible, setIsConfirmPasswordVisible] = @@ -112,7 +114,9 @@ export const CustomInput = ({ {...field} /> - + {showFormMessage && ( + + )} )} /> diff --git a/ui/types/authFormSchema.ts b/ui/types/authFormSchema.ts index ac0fc46dcf..58737a1f8a 100644 --- a/ui/types/authFormSchema.ts +++ b/ui/types/authFormSchema.ts @@ -24,11 +24,21 @@ export const authFormSchema = (type: string) => invitationToken: type === "sign-in" ? z.string().optional() : z.string().optional(), + termsAndConditions: + type === "sign-in" || process.env.NEXT_PUBLIC_IS_CLOUD_ENV !== "true" + ? z.boolean().optional() + : z.boolean().refine((value) => value === true, { + message: "You must accept the terms and conditions.", + }), + // Fields for Sign In and Sign Up email: z.string().email(), - password: z.string().min(12, { - message: "It must contain at least 12 characters.", - }), + password: + type === "sign-in" + ? z.string() + : z.string().min(12, { + message: "It must contain at least 12 characters.", + }), }) .refine( (data) => type === "sign-in" || data.password === data.confirmPassword,