Merge branch 'master' into PRWLR-6129-review-check-report-init-for-region-resource-id-resource-arn

This commit is contained in:
Pepe Fagoaga
2025-02-05 14:08:25 +05:45
committed by GitHub
61 changed files with 1045 additions and 550 deletions
+1 -1
View File
@@ -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 }}
+1 -1
View File
@@ -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() {
+54 -1
View File
@@ -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"
+2 -1
View File
@@ -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"
+6 -2
View File
@@ -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
+21 -26
View File
@@ -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:
@@ -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),
]
+4
View File
@@ -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):
+1 -140
View File
@@ -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:
+18 -14
View File
@@ -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):
+2 -1
View File
@@ -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"
+9 -9
View File
@@ -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,
)
+48 -37
View File
@@ -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(
+36 -19
View File
@@ -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,
},
)
+17 -6
View File
@@ -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",
+31 -18
View File
@@ -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,
+4
View File
@@ -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={
+76
View File
@@ -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"
)
+26
View File
@@ -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})
+3
View File
@@ -35,6 +35,9 @@ services:
required: false
ports:
- 3000:3000
volumes:
- "./ui:/app"
- "/app/node_modules"
postgres:
image: postgres:16.3-alpine3.20
+2
View File
@@ -133,3 +133,5 @@ While the scan is running, start exploring the findings in these sections:
<img src="../../img/issues.png" alt="Issues" width="300" style="text-align: center;"/>
- **Browse All Findings**: Detailed list of findings detected, where you can filter by severity, service, and more. <img src="../../img/findings.png" alt="Findings" width="700"/>
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`.
Generated
+58 -174
View File
@@ -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"
@@ -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"
]
},
{
+2 -1
View File
@@ -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": [
{
+2 -1
View File
@@ -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": [
{
+9 -4
View File
@@ -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": [
{
+3 -1
View File
@@ -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")
+3 -1
View File
@@ -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
@@ -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}"
@@ -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",
@@ -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
@@ -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": [
@@ -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,
@@ -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
@@ -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 <user> -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": ""
}
@@ -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
@@ -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"
@@ -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())
@@ -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]
+8 -8
View File
@@ -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]
+1 -1
View File
@@ -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",
},
+3 -3
View File
@@ -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"]
@@ -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 == []
@@ -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,
@@ -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
@@ -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"
)
@@ -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
@@ -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
+15 -18
View File
@@ -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<string, string> = {
...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<string, string> = {
...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() || "",
]),
),
};
+1 -1
View File
@@ -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",
-3
View File
@@ -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({
<Header title="Scans" icon="lucide:scan-search" />
<LaunchScanWorkflow providers={providerInfo} />
<Spacer y={4} />
<ScanWarningBar />
<Spacer y={8} />
</>
)}
+48 -7
View File
@@ -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 = ({
<Form {...form}>
<form
className="flex flex-col gap-3"
className="flex flex-col gap-4"
onSubmit={form.handleSubmit(onSubmit)}
>
{type === "sign-up" && (
@@ -167,6 +172,7 @@ export const AuthForm = ({
/>
</>
)}
<CustomInput
control={form.control}
name="email"
@@ -174,13 +180,17 @@ export const AuthForm = ({
label="Email"
placeholder="Enter your email"
isInvalid={!!form.formState.errors.email}
showFormMessage={type !== "sign-in"}
/>
<CustomInput
control={form.control}
name="password"
password
isInvalid={!!form.formState.errors.password}
isInvalid={
!!form.formState.errors.password ||
!!form.formState.errors.email
}
/>
{/* {type === "sign-in" && (
@@ -213,13 +223,44 @@ export const AuthForm = ({
isDisabled={invitationToken !== null && true}
/>
)}
{process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true" && (
<FormField
control={form.control}
name="termsAndConditions"
render={({ field }) => (
<>
<FormControl>
<Checkbox
isRequired
className="py-4"
size="sm"
checked={field.value}
onChange={(e) => field.onChange(e.target.checked)}
>
I agree with the&nbsp;
<Link
href="https://prowler.com/terms-of-service/"
size="sm"
target="_blank"
>
Terms of Service
</Link>
&nbsp;of Prowler
</Checkbox>
</FormControl>
<FormMessage className="text-system-error dark:text-system-error" />
</>
)}
/>
)}
</>
)}
{form.formState.errors?.email && (
<div className="flex flex-row items-center gap-2 text-system-error">
{type === "sign-in" && form.formState.errors?.email && (
<div className="flex flex-row items-center text-system-error">
<NotificationIcon size={16} />
<p className="text-s">No user found</p>
<p className="text-small">Invalid email or password</p>
</div>
)}
-1
View File
@@ -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";
-18
View File
@@ -1,18 +0,0 @@
"use client";
import { InfoIcon } from "../icons";
export const ScanWarningBar = () => {
return (
<div className="flex items-center rounded-lg border border-system-warning bg-system-warning-medium p-4 text-sm dark:text-default-300">
<InfoIcon className="mr-4 inline h-4 w-4 flex-shrink-0" />
<div className="flex flex-col gap-1">
<strong>Waiting for Your Scan to Show Up?</strong>
<p>
It may take a few minutes for the scan to appear on the table and be
displayed.
</p>
</div>
</div>
);
};
+3 -5
View File
@@ -117,11 +117,9 @@ export const ScanDetail = ({
<InfoField label="Completed At">
<DateWithTime inline dateTime={scan.completed_at || "-"} />
</InfoField>
{scan.next_scan_at && (
<InfoField label="Scheduled At">
<DateWithTime inline dateTime={scan.next_scan_at} />
</InfoField>
)}
<InfoField label="Scheduled At">
<DateWithTime inline dateTime={scan.scheduled_at || "-"} />
</InfoField>
</div>
</Section>
@@ -123,13 +123,13 @@ export const ColumnGetScans: ColumnDef<ScanProps>[] = [
},
},
{
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 <DateWithTime dateTime={next_scan_at} />;
return <DateWithTime dateTime={scheduled_at} />;
},
},
{
+5 -1
View File
@@ -23,6 +23,7 @@ interface CustomInputProps<T extends FieldValues> {
isRequired?: boolean;
isInvalid?: boolean;
isDisabled?: boolean;
showFormMessage?: boolean;
}
export const CustomInput = <T extends FieldValues>({
@@ -41,6 +42,7 @@ export const CustomInput = <T extends FieldValues>({
isRequired = true,
isInvalid,
isDisabled = false,
showFormMessage = true,
}: CustomInputProps<T>) => {
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const [isConfirmPasswordVisible, setIsConfirmPasswordVisible] =
@@ -112,7 +114,9 @@ export const CustomInput = <T extends FieldValues>({
{...field}
/>
</FormControl>
<FormMessage className="text-system-error dark:text-system-error" />
{showFormMessage && (
<FormMessage className="text-system-error dark:text-system-error" />
)}
</>
)}
/>
+13 -3
View File
@@ -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,