diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1e9d74c973..159c1f5a16 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -110,17 +110,36 @@ repos: priority: 30 ## PYTHON — API + MCP Server (ruff) - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.11 + # Run ruff through `uv run` against each project so prek uses the exact ruff + # version pinned in that project's uv.lock — the same version GitHub Actions + # runs via `uv run ruff`. This removes the drift between the local hooks and + # CI. api/ and mcp_server/ are separate uv projects, so they need separate + # hooks (each `uv run --project` resolves its own pinned ruff + config). + - repo: local hooks: - - id: ruff - name: "API + MCP - ruff check" - files: { glob: ["{api,mcp_server}/**/*.py"] } - args: ["--fix"] + - id: ruff-check-api + name: "API - ruff check" + entry: uv run --project ./api ruff check --fix + language: system + files: { glob: ["api/**/*.py"] } priority: 30 - - id: ruff-format - name: "API + MCP - ruff format" - files: { glob: ["{api,mcp_server}/**/*.py"] } + - id: ruff-format-api + name: "API - ruff format" + entry: uv run --project ./api ruff format + language: system + files: { glob: ["api/**/*.py"] } + priority: 20 + - id: ruff-check-mcp + name: "MCP - ruff check" + entry: uv run --project ./mcp_server ruff check --fix + language: system + files: { glob: ["mcp_server/**/*.py"] } + priority: 30 + - id: ruff-format-mcp + name: "MCP - ruff format" + entry: uv run --project ./mcp_server ruff format + language: system + files: { glob: ["mcp_server/**/*.py"] } priority: 20 ## PYTHON — uv (API + SDK) diff --git a/Makefile b/Makefile index e2623a4bf3..b05a7dc2db 100644 --- a/Makefile +++ b/Makefile @@ -45,18 +45,41 @@ coverage-html: ## Show Test Coverage coverage html && \ open htmlcov/index.html -##@ Linting -format: ## Format Code - @echo "Running black..." - black . +##@ Code Quality +# `make` is the single entrypoint and mirrors CI exactly (uv run + same flags): +# SDK (prowler/, util/) -> flake8 + black + pylint +# API & MCP server -> ruff (rules live in each project's pyproject.toml) +# `format` applies fixes (incl. ruff's import/upgrade autofixes); `lint` only +# verifies and is what CI gates on. +.PHONY: format format-sdk format-api format-mcp lint lint-sdk lint-api lint-mcp -lint: ## Lint Code - @echo "Running flake8..." - flake8 . --ignore=E266,W503,E203,E501,W605,E128 --exclude .venv,contrib - @echo "Running black... " - black --check . - @echo "Running pylint..." - pylint --disable=W,C,R,E -j 0 prowler util +format: format-sdk format-api format-mcp ## Format & autofix all components (SDK, API, MCP) + +lint: lint-sdk lint-api lint-mcp ## Lint all components (SDK, API, MCP) — mirrors CI + +format-sdk: ## Format SDK code (black) + uv run black --exclude "\.venv|api|ui|skills|mcp_server" . + +lint-sdk: ## Lint SDK code (flake8, black --check, pylint) + uv run flake8 . --ignore=E266,W503,E203,E501,W605,E128 --exclude .venv,contrib,ui,api,skills,mcp_server + uv run black --exclude "\.venv|api|ui|skills|mcp_server" --check . + uv run pylint --disable=W,C,R,E -j 0 -rn -sn prowler/ + +format-api: ## Format & autofix API code (ruff) + cd api && uv run ruff check . --exclude contrib --fix + cd api && uv run ruff format . --exclude contrib + +lint-api: ## Lint API code (ruff check + format --check) + cd api && uv run ruff check . --exclude contrib + cd api && uv run ruff format --check . --exclude contrib + +format-mcp: ## Format & autofix MCP server code (ruff) + cd mcp_server && uv run ruff check . --fix + cd mcp_server && uv run ruff format . + +lint-mcp: ## Lint MCP server code (ruff check + format --check) + cd mcp_server && uv run ruff check . + cd mcp_server && uv run ruff format --check . ##@ PyPI pypi-clean: ## Delete the distribution files diff --git a/api/pyproject.toml b/api/pyproject.toml index 26f70ee3b9..4036aac9a9 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -14,7 +14,7 @@ dev = [ "pytest-env==1.1.3", "pytest-randomly==3.15.0", "pytest-xdist==3.6.1", - "ruff==0.5.0", + "ruff==0.15.11", "tqdm==4.67.1", "vulture==2.14", "prek==0.3.9" @@ -73,6 +73,23 @@ package-mode = false requires-python = ">=3.11,<3.13" version = "1.33.0" +# Shared ruff baseline (kept in sync with mcp_server/pyproject.toml). +# target-version tracks this project's lowest supported Python. +[tool.ruff] +src = ["src"] +target-version = "py311" + +[tool.ruff.lint] +# Defaults (E4/E7/E9, F) plus import sorting, modern-syntax upgrades, and +# comprehension lints — all mechanically auto-fixable. flake8-bugbear (B) is a +# good next step but needs manual cleanup (e.g. B904 raise-from), so it is left +# out of the shared baseline for now. +extend-select = [ + "I", # isort — import ordering (prek's isort hook covers only the SDK) + "UP", # pyupgrade — modern syntax for the min supported Python + "C4" # flake8-comprehensions +] + [tool.uv] # Transitive pins matching master to avoid silent drift; bump deliberately. constraint-dependencies = [ @@ -393,7 +410,7 @@ constraint-dependencies = [ "rpds-py==0.30.0", "rsa==4.9.1", "ruamel-yaml==0.19.1", - "ruff==0.5.0", + "ruff==0.15.11", "s3transfer==0.14.0", "scaleway==2.10.3", "scaleway-core==2.10.3", diff --git a/api/src/backend/api/adapters.py b/api/src/backend/api/adapters.py index 1d0d2ace00..cbd6795731 100644 --- a/api/src/backend/api/adapters.py +++ b/api/src/backend/api/adapters.py @@ -1,6 +1,4 @@ from allauth.socialaccount.adapter import DefaultSocialAccountAdapter -from django.db import transaction - from api.db_router import MainRouter from api.db_utils import rls_transaction from api.models import ( @@ -11,6 +9,7 @@ from api.models import ( User, UserRoleRelationship, ) +from django.db import transaction class ProwlerSocialAccountAdapter(DefaultSocialAccountAdapter): diff --git a/api/src/backend/api/apps.py b/api/src/backend/api/apps.py index 6c3a4ec2d9..90ba96c124 100644 --- a/api/src/backend/api/apps.py +++ b/api/src/backend/api/apps.py @@ -1,14 +1,12 @@ import logging import os import sys - from pathlib import Path -from django.apps import AppConfig -from django.conf import settings - from config.custom_logging import BackendLogger from config.env import env +from django.apps import AppConfig +from django.conf import settings logger = logging.getLogger(BackendLogger.API) @@ -30,8 +28,10 @@ class ApiConfig(AppConfig): name = "api" def ready(self): - from api import schema_extensions # noqa: F401 - from api import signals # noqa: F401 + from api import ( + schema_extensions, # noqa: F401 + signals, # noqa: F401 + ) # Generate required cryptographic keys if not present, but only if: # `"manage.py" not in sys.argv[0]`: If an external server (e.g., Gunicorn) is running the app diff --git a/api/src/backend/api/attack_paths/__init__.py b/api/src/backend/api/attack_paths/__init__.py index b2917e1d86..fc41fb63c1 100644 --- a/api/src/backend/api/attack_paths/__init__.py +++ b/api/src/backend/api/attack_paths/__init__.py @@ -5,7 +5,6 @@ from api.attack_paths.queries import ( get_query_by_id, ) - __all__ = [ "AttackPathsQueryDefinition", "AttackPathsQueryParameterDefinition", diff --git a/api/src/backend/api/attack_paths/cypher_sanitizer.py b/api/src/backend/api/attack_paths/cypher_sanitizer.py index 3772b4cbef..f08172114e 100644 --- a/api/src/backend/api/attack_paths/cypher_sanitizer.py +++ b/api/src/backend/api/attack_paths/cypher_sanitizer.py @@ -22,10 +22,8 @@ Label-injection pipeline: import re from rest_framework.exceptions import ValidationError - from tasks.jobs.attack_paths.config import get_provider_label - # Step 1 - String / comment protection # Single combined regex: strings first, then line comments. # The regex engine finds the leftmost match, so a string like 'https://prowler.com' diff --git a/api/src/backend/api/attack_paths/database.py b/api/src/backend/api/attack_paths/database.py index 0e6cc083dc..4745cf79a1 100644 --- a/api/src/backend/api/attack_paths/database.py +++ b/api/src/backend/api/attack_paths/database.py @@ -1,18 +1,16 @@ import atexit import logging import threading - +from collections.abc import Iterator from contextlib import contextmanager -from typing import Any, Iterator +from typing import Any from uuid import UUID import neo4j import neo4j.exceptions - +from api.attack_paths.retryable_session import RetryableSession from config.env import env from django.conf import settings - -from api.attack_paths.retryable_session import RetryableSession from tasks.jobs.attack_paths.config import ( BATCH_SIZE, PROVIDER_RESOURCE_LABEL, diff --git a/api/src/backend/api/attack_paths/queries/__init__.py b/api/src/backend/api/attack_paths/queries/__init__.py index c5e6ab0393..aa90ba6878 100644 --- a/api/src/backend/api/attack_paths/queries/__init__.py +++ b/api/src/backend/api/attack_paths/queries/__init__.py @@ -1,12 +1,11 @@ -from api.attack_paths.queries.types import ( - AttackPathsQueryDefinition, - AttackPathsQueryParameterDefinition, -) from api.attack_paths.queries.registry import ( get_queries_for_provider, get_query_by_id, ) - +from api.attack_paths.queries.types import ( + AttackPathsQueryDefinition, + AttackPathsQueryParameterDefinition, +) __all__ = [ "AttackPathsQueryDefinition", diff --git a/api/src/backend/api/attack_paths/queries/aws.py b/api/src/backend/api/attack_paths/queries/aws.py index 81f91de24f..d9792845c0 100644 --- a/api/src/backend/api/attack_paths/queries/aws.py +++ b/api/src/backend/api/attack_paths/queries/aws.py @@ -5,7 +5,6 @@ from api.attack_paths.queries.types import ( ) from tasks.jobs.attack_paths.config import PROWLER_FINDING_LABEL - # Custom Attack Path Queries # -------------------------- diff --git a/api/src/backend/api/attack_paths/queries/registry.py b/api/src/backend/api/attack_paths/queries/registry.py index c683b2cb80..d055b842fd 100644 --- a/api/src/backend/api/attack_paths/queries/registry.py +++ b/api/src/backend/api/attack_paths/queries/registry.py @@ -1,6 +1,5 @@ -from api.attack_paths.queries.types import AttackPathsQueryDefinition from api.attack_paths.queries.aws import AWS_QUERIES - +from api.attack_paths.queries.types import AttackPathsQueryDefinition # Query definitions organized by provider _QUERY_DEFINITIONS: dict[str, list[AttackPathsQueryDefinition]] = { diff --git a/api/src/backend/api/attack_paths/retryable_session.py b/api/src/backend/api/attack_paths/retryable_session.py index 8723fe3ec9..16f0d9e31a 100644 --- a/api/src/backend/api/attack_paths/retryable_session.py +++ b/api/src/backend/api/attack_paths/retryable_session.py @@ -1,5 +1,4 @@ import logging - from collections.abc import Callable from typing import Any diff --git a/api/src/backend/api/attack_paths/views_helpers.py b/api/src/backend/api/attack_paths/views_helpers.py index 201527885e..bfb077abd0 100644 --- a/api/src/backend/api/attack_paths/views_helpers.py +++ b/api/src/backend/api/attack_paths/views_helpers.py @@ -1,12 +1,10 @@ import logging - -from typing import Any, Iterable +from collections.abc import Iterable +from typing import Any import neo4j - -from rest_framework.exceptions import APIException, PermissionDenied, ValidationError - -from api.attack_paths import database as graph_database, AttackPathsQueryDefinition +from api.attack_paths import AttackPathsQueryDefinition +from api.attack_paths import database as graph_database from api.attack_paths.cypher_sanitizer import ( inject_provider_label, validate_custom_query, @@ -17,6 +15,7 @@ from api.attack_paths.queries.schema import ( get_cartography_schema_query, ) from config.custom_logging import BackendLogger +from rest_framework.exceptions import APIException, PermissionDenied, ValidationError from tasks.jobs.attack_paths.config import ( INTERNAL_LABELS, INTERNAL_PROPERTIES, diff --git a/api/src/backend/api/authentication.py b/api/src/backend/api/authentication.py index 04740ac219..1e9eee46ff 100644 --- a/api/src/backend/api/authentication.py +++ b/api/src/backend/api/authentication.py @@ -1,6 +1,7 @@ -from typing import Optional, Tuple from uuid import UUID +from api.db_router import MainRouter +from api.models import TenantAPIKey, TenantAPIKeyManager from cryptography.fernet import InvalidToken from django.utils import timezone from drf_simple_apikey.backends import APIKeyAuthentication as BaseAPIKeyAuth @@ -10,9 +11,6 @@ from rest_framework.exceptions import AuthenticationFailed from rest_framework.request import Request from rest_framework_simplejwt.authentication import JWTAuthentication -from api.db_router import MainRouter -from api.models import TenantAPIKey, TenantAPIKeyManager - class TenantAPIKeyAuthentication(BaseAPIKeyAuth): model = TenantAPIKey @@ -81,7 +79,7 @@ class CombinedJWTOrAPIKeyAuthentication(BaseAuthentication): jwt_auth = JWTAuthentication() api_key_auth = TenantAPIKeyAuthentication() - def authenticate(self, request: Request) -> Optional[Tuple[object, dict]]: + def authenticate(self, request: Request) -> tuple[object, dict] | None: auth_header = request.headers.get("Authorization", "") # Prioritize JWT authentication if both are present diff --git a/api/src/backend/api/base_views.py b/api/src/backend/api/base_views.py index b14cc39529..e8dd728cb9 100644 --- a/api/src/backend/api/base_views.py +++ b/api/src/backend/api/base_views.py @@ -1,3 +1,9 @@ +from api.authentication import CombinedJWTOrAPIKeyAuthentication +from api.db_router import MainRouter, reset_read_db_alias, set_read_db_alias +from api.db_utils import POSTGRES_USER_VAR, rls_transaction +from api.filters import CustomDjangoFilterBackend +from api.models import Role, UserRoleRelationship +from api.rbac.permissions import HasPermissions from django.conf import settings from django.db import transaction from rest_framework import permissions @@ -8,13 +14,6 @@ from rest_framework.response import Response from rest_framework_json_api import filters from rest_framework_json_api.views import ModelViewSet -from api.authentication import CombinedJWTOrAPIKeyAuthentication -from api.db_router import MainRouter, reset_read_db_alias, set_read_db_alias -from api.db_utils import POSTGRES_USER_VAR, rls_transaction -from api.filters import CustomDjangoFilterBackend -from api.models import Role, UserRoleRelationship -from api.rbac.permissions import HasPermissions - class BaseViewSet(ModelViewSet): authentication_classes = [CombinedJWTOrAPIKeyAuthentication] diff --git a/api/src/backend/api/compliance.py b/api/src/backend/api/compliance.py index 77c45cbffd..854c7d8ba6 100644 --- a/api/src/backend/api/compliance.py +++ b/api/src/backend/api/compliance.py @@ -352,7 +352,7 @@ def generate_compliance_overview_template( total_requirements += 1 provider_check_list = list(requirement.checks.get(provider_type, [])) total_checks = len(provider_check_list) - checks_dict = {check: None for check in provider_check_list} + checks_dict = dict.fromkeys(provider_check_list) req_status_val = "MANUAL" if total_checks == 0 else "PASS" diff --git a/api/src/backend/api/db_utils.py b/api/src/backend/api/db_utils.py index e3b11d7084..2c378f2ea8 100644 --- a/api/src/backend/api/db_utils.py +++ b/api/src/backend/api/db_utils.py @@ -3,8 +3,14 @@ import secrets import time import uuid from contextlib import contextmanager -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta +from api.db_router import ( + READ_REPLICA_ALIAS, + get_read_db_alias, + reset_read_db_alias, + set_read_db_alias, +) from celery.utils.log import get_task_logger from config.env import env from django.conf import settings @@ -22,13 +28,6 @@ from psycopg2 import sql as psycopg2_sql from psycopg2.extensions import AsIs, new_type, register_adapter, register_type from rest_framework_json_api.serializers import ValidationError -from api.db_router import ( - READ_REPLICA_ALIAS, - get_read_db_alias, - reset_read_db_alias, - set_read_db_alias, -) - logger = get_task_logger(__name__) DB_USER = settings.DATABASES["default"]["USER"] if not settings.TESTING else "test" @@ -170,7 +169,7 @@ def one_week_from_now(): """ Return a datetime object with a date one week from now. """ - return datetime.now(timezone.utc) + timedelta(days=7) + return datetime.now(UTC) + timedelta(days=7) def generate_random_token(length: int = 14, symbols: str | None = None) -> str: @@ -405,10 +404,10 @@ def _should_create_index_on_partition( # Unknown month abbreviation, include it to be safe return True - partition_date = datetime(year, month, 1, tzinfo=timezone.utc) + partition_date = datetime(year, month, 1, tzinfo=UTC) # Get current month start - now = datetime.now(timezone.utc) + now = datetime.now(UTC) current_month_start = now.replace( day=1, hour=0, minute=0, second=0, microsecond=0 ) diff --git a/api/src/backend/api/decorators.py b/api/src/backend/api/decorators.py index f9b165ef20..a055b2252f 100644 --- a/api/src/backend/api/decorators.py +++ b/api/src/backend/api/decorators.py @@ -1,14 +1,13 @@ import uuid from functools import wraps -from django.core.exceptions import ObjectDoesNotExist -from django.db import DatabaseError, connection, transaction -from rest_framework_json_api.serializers import ValidationError - from api.db_router import READ_REPLICA_ALIAS from api.db_utils import POSTGRES_TENANT_VAR, SET_CONFIG_QUERY, rls_transaction from api.exceptions import ProviderDeletedException from api.models import Provider, Scan +from django.core.exceptions import ObjectDoesNotExist +from django.db import DatabaseError, connection, transaction +from rest_framework_json_api.serializers import ValidationError def set_tenant(func=None, *, keep_tenant=False): diff --git a/api/src/backend/api/filters.py b/api/src/backend/api/filters.py index 195982011d..740556329c 100644 --- a/api/src/backend/api/filters.py +++ b/api/src/backend/api/filters.py @@ -1,19 +1,4 @@ -from datetime import date, datetime, timedelta, timezone - -from dateutil.parser import parse -from django.conf import settings -from django.db.models import F, Q -from django_filters.rest_framework import ( - BaseInFilter, - BooleanFilter, - CharFilter, - ChoiceFilter, - DateFilter, - FilterSet, - UUIDFilter, -) -from rest_framework_json_api.django_filters.backends import DjangoFilterBackend -from rest_framework_json_api.serializers import ValidationError +from datetime import UTC, date, datetime, timedelta from api.constants import SEVERITY_ORDER from api.db_utils import ( @@ -68,6 +53,20 @@ from api.uuid_utils import ( uuid7_start, ) from api.v1.serializers import TaskBase +from dateutil.parser import parse +from django.conf import settings +from django.db.models import F, Q +from django_filters.rest_framework import ( + BaseInFilter, + BooleanFilter, + CharFilter, + ChoiceFilter, + DateFilter, + FilterSet, + UUIDFilter, +) +from rest_framework_json_api.django_filters.backends import DjangoFilterBackend +from rest_framework_json_api.serializers import ValidationError class CustomDjangoFilterBackend(DjangoFilterBackend): @@ -598,12 +597,12 @@ class ResourceFilter(ProviderRelationshipFilterSet): gte_date = ( parse(self.data.get("updated_at__gte")).date() if self.data.get("updated_at__gte") - else datetime.now(timezone.utc).date() + else datetime.now(UTC).date() ) lte_date = ( parse(self.data.get("updated_at__lte")).date() if self.data.get("updated_at__lte") - else datetime.now(timezone.utc).date() + else datetime.now(UTC).date() ) if abs(lte_date - gte_date) > timedelta( @@ -748,9 +747,9 @@ class FindingFilter(CommonFindingFilters): lte_date = cleaned.get("inserted_at__lte") or exact_date if gte_date is None: - gte_date = datetime.now(timezone.utc).date() + gte_date = datetime.now(UTC).date() if lte_date is None: - lte_date = datetime.now(timezone.utc).date() + lte_date = datetime.now(UTC).date() if abs(lte_date - gte_date) > timedelta( days=settings.FINDINGS_MAX_DAYS_IN_RANGE @@ -844,7 +843,7 @@ class FindingFilter(CommonFindingFilters): def maybe_date_to_datetime(value): dt = value if isinstance(value, date): - dt = datetime.combine(value, datetime.min.time(), tzinfo=timezone.utc) + dt = datetime.combine(value, datetime.min.time(), tzinfo=UTC) return dt @@ -933,9 +932,9 @@ class FindingGroupFilter(CommonFindingFilters): lte_date = cleaned.get("inserted_at__lte") or exact_date if gte_date is None: - gte_date = datetime.now(timezone.utc).date() + gte_date = datetime.now(UTC).date() if lte_date is None: - lte_date = datetime.now(timezone.utc).date() + lte_date = datetime.now(UTC).date() if abs(lte_date - gte_date) > timedelta( days=settings.FINDINGS_MAX_DAYS_IN_RANGE @@ -977,7 +976,7 @@ class FindingGroupFilter(CommonFindingFilters): """Convert date to datetime if needed.""" dt = value if isinstance(value, date): - dt = datetime.combine(value, datetime.min.time(), tzinfo=timezone.utc) + dt = datetime.combine(value, datetime.min.time(), tzinfo=UTC) return dt @@ -1091,9 +1090,9 @@ class FindingGroupSummaryFilter(_CheckTitleToCheckIdMixin, FilterSet): lte_date = cleaned.get("inserted_at__lte") or exact_date if gte_date is None: - gte_date = datetime.now(timezone.utc).date() + gte_date = datetime.now(UTC).date() if lte_date is None: - lte_date = datetime.now(timezone.utc).date() + lte_date = datetime.now(UTC).date() if abs(lte_date - gte_date) > timedelta( days=settings.FINDINGS_MAX_DAYS_IN_RANGE @@ -1132,7 +1131,7 @@ class FindingGroupSummaryFilter(_CheckTitleToCheckIdMixin, FilterSet): def _maybe_date_to_datetime(value): dt = value if isinstance(value, date): - dt = datetime.combine(value, datetime.min.time(), tzinfo=timezone.utc) + dt = datetime.combine(value, datetime.min.time(), tzinfo=UTC) return dt diff --git a/api/src/backend/api/health.py b/api/src/backend/api/health.py index 8ca3936f94..691640c0bd 100644 --- a/api/src/backend/api/health.py +++ b/api/src/backend/api/health.py @@ -12,7 +12,7 @@ import logging import threading import time from contextlib import suppress -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any import redis @@ -62,11 +62,7 @@ class HealthJSONRenderer(JSONRenderer): def _now_iso() -> str: - return ( - datetime.now(timezone.utc) - .isoformat(timespec="milliseconds") - .replace("+00:00", "Z") - ) + return datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z") def _measure(name: str, check_fn) -> tuple[dict[str, Any], float]: diff --git a/api/src/backend/api/management/commands/findings.py b/api/src/backend/api/management/commands/findings.py index e62f8f8e8e..f5348dbfc6 100644 --- a/api/src/backend/api/management/commands/findings.py +++ b/api/src/backend/api/management/commands/findings.py @@ -1,11 +1,8 @@ import random -from datetime import datetime, timezone +from datetime import UTC, datetime from math import ceil from uuid import uuid4 -from django.core.management.base import BaseCommand -from tqdm import tqdm - from api.db_utils import rls_transaction from api.models import ( Finding, @@ -16,7 +13,9 @@ from api.models import ( Scan, StatusChoices, ) +from django.core.management.base import BaseCommand from prowler.lib.check.models import CheckMetadata +from tqdm import tqdm class Command(BaseCommand): @@ -116,7 +115,7 @@ class Command(BaseCommand): trigger="manual", state="executing", progress=0, - started_at=datetime.now(timezone.utc), + started_at=datetime.now(UTC), ) scan_state = "completed" @@ -272,10 +271,8 @@ class Command(BaseCommand): self.stdout.write(self.style.ERROR(f"Failed to populate test data: {e}")) scan_state = "failed" finally: - scan.completed_at = datetime.now(timezone.utc) - scan.duration = int( - (datetime.now(timezone.utc) - scan.started_at).total_seconds() - ) + scan.completed_at = datetime.now(UTC) + scan.duration = int((datetime.now(UTC) - scan.started_at).total_seconds()) scan.progress = 100 scan.state = scan_state scan.unique_resource_count = num_resources diff --git a/api/src/backend/api/management/commands/reconcile_orphan_tasks.py b/api/src/backend/api/management/commands/reconcile_orphan_tasks.py index 8ba8f5b342..7d84e29dde 100644 --- a/api/src/backend/api/management/commands/reconcile_orphan_tasks.py +++ b/api/src/backend/api/management/commands/reconcile_orphan_tasks.py @@ -1,5 +1,4 @@ from django.core.management.base import BaseCommand - from tasks.jobs.orphan_recovery import reconcile_orphans diff --git a/api/src/backend/api/middleware.py b/api/src/backend/api/middleware.py index 2b0a2340c4..82ae8dcf67 100644 --- a/api/src/backend/api/middleware.py +++ b/api/src/backend/api/middleware.py @@ -1,11 +1,10 @@ import logging import time +from config.custom_logging import BackendLogger from django.core.handlers.asgi import ASGIRequest from django.db import connections -from config.custom_logging import BackendLogger - class CloseDBConnectionsMiddleware: """ diff --git a/api/src/backend/api/migrations/0001_initial.py b/api/src/backend/api/migrations/0001_initial.py index 6288cf4093..d4f931622f 100644 --- a/api/src/backend/api/migrations/0001_initial.py +++ b/api/src/backend/api/migrations/0001_initial.py @@ -1,26 +1,13 @@ import uuid from functools import partial +import api.rls import django.contrib.auth.models import django.contrib.postgres.indexes import django.contrib.postgres.search import django.core.validators import django.db.models.deletion import django.utils.timezone -from django.conf import settings -from django.db import migrations, models -from psqlextra.backend.migrations.operations.add_default_partition import ( - PostgresAddDefaultPartition, -) -from psqlextra.backend.migrations.operations.create_partitioned_model import ( - PostgresCreatePartitionedModel, -) -from psqlextra.manager.manager import PostgresManager -from psqlextra.models.partitioned import PostgresPartitionedModel -from psqlextra.types import PostgresPartitioningMethod -from uuid6 import uuid7 - -import api.rls from api.db_utils import ( DB_PROWLER_PASSWORD, DB_PROWLER_USER, @@ -53,6 +40,18 @@ from api.models import ( StateChoices, StatusChoices, ) +from django.conf import settings +from django.db import migrations, models +from psqlextra.backend.migrations.operations.add_default_partition import ( + PostgresAddDefaultPartition, +) +from psqlextra.backend.migrations.operations.create_partitioned_model import ( + PostgresCreatePartitionedModel, +) +from psqlextra.manager.manager import PostgresManager +from psqlextra.models.partitioned import PostgresPartitionedModel +from psqlextra.types import PostgresPartitioningMethod +from uuid6 import uuid7 DB_NAME = settings.DATABASES["default"]["NAME"] diff --git a/api/src/backend/api/migrations/0002_token_migrations.py b/api/src/backend/api/migrations/0002_token_migrations.py index 754403c62f..c7ba732fa3 100644 --- a/api/src/backend/api/migrations/0002_token_migrations.py +++ b/api/src/backend/api/migrations/0002_token_migrations.py @@ -1,8 +1,7 @@ +from api.db_utils import DB_PROWLER_USER from django.conf import settings from django.db import migrations -from api.db_utils import DB_PROWLER_USER - DB_NAME = settings.DATABASES["default"]["NAME"] diff --git a/api/src/backend/api/migrations/0004_rbac.py b/api/src/backend/api/migrations/0004_rbac.py index 4453b1b588..efac385041 100644 --- a/api/src/backend/api/migrations/0004_rbac.py +++ b/api/src/backend/api/migrations/0004_rbac.py @@ -2,12 +2,11 @@ import uuid +import api.rls import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import api.rls - class Migration(migrations.Migration): dependencies = [ diff --git a/api/src/backend/api/migrations/0005_rbac_missing_admin_roles.py b/api/src/backend/api/migrations/0005_rbac_missing_admin_roles.py index 0392145063..5ab8978b31 100644 --- a/api/src/backend/api/migrations/0005_rbac_missing_admin_roles.py +++ b/api/src/backend/api/migrations/0005_rbac_missing_admin_roles.py @@ -1,6 +1,5 @@ -from django.db import migrations - from api.db_router import MainRouter +from django.db import migrations def create_admin_role(apps, schema_editor): diff --git a/api/src/backend/api/migrations/0008_daily_scheduled_tasks_update.py b/api/src/backend/api/migrations/0008_daily_scheduled_tasks_update.py index 7f059ea2b8..d6e8bb7adb 100644 --- a/api/src/backend/api/migrations/0008_daily_scheduled_tasks_update.py +++ b/api/src/backend/api/migrations/0008_daily_scheduled_tasks_update.py @@ -1,12 +1,11 @@ import json -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta 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 +from django.db import migrations, models +from django_celery_beat.models import PeriodicTask def migrate_daily_scheduled_scan_tasks(apps, schema_editor): @@ -17,11 +16,11 @@ def migrate_daily_scheduled_scan_tasks(apps, schema_editor): tenant_id = task_kwargs["tenant_id"] provider_id = task_kwargs["provider_id"] - current_time = datetime.now(timezone.utc) + current_time = datetime.now(UTC) scheduled_time_today = datetime.combine( current_time.date(), daily_scheduled_scan_task.start_time.time(), - tzinfo=timezone.utc, + tzinfo=UTC, ) if current_time < scheduled_time_today: diff --git a/api/src/backend/api/migrations/0013_integrations_enum.py b/api/src/backend/api/migrations/0013_integrations_enum.py index 524ecbbb3d..7f2905b844 100644 --- a/api/src/backend/api/migrations/0013_integrations_enum.py +++ b/api/src/backend/api/migrations/0013_integrations_enum.py @@ -2,10 +2,9 @@ from functools import partial -from django.db import migrations - from api.db_utils import IntegrationTypeEnum, PostgresEnumMigration, register_enum from api.models import Integration +from django.db import migrations IntegrationTypeEnumMigration = PostgresEnumMigration( enum_name="integration_type", diff --git a/api/src/backend/api/migrations/0014_integrations.py b/api/src/backend/api/migrations/0014_integrations.py index 2fb3d76880..1b63c1fc8c 100644 --- a/api/src/backend/api/migrations/0014_integrations.py +++ b/api/src/backend/api/migrations/0014_integrations.py @@ -2,12 +2,11 @@ import uuid -import django.db.models.deletion -from django.db import migrations, models - import api.db_utils import api.rls +import django.db.models.deletion from api.rls import RowLevelSecurityConstraint +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0015_finding_muted.py b/api/src/backend/api/migrations/0015_finding_muted.py index 3cb20f871b..5bc408cb44 100644 --- a/api/src/backend/api/migrations/0015_finding_muted.py +++ b/api/src/backend/api/migrations/0015_finding_muted.py @@ -1,8 +1,7 @@ # Generated by Django 5.1.5 on 2025-03-25 11:29 -from django.db import migrations, models - import api.db_utils +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0017_m365_provider.py b/api/src/backend/api/migrations/0017_m365_provider.py index 62817560c5..7e9face021 100644 --- a/api/src/backend/api/migrations/0017_m365_provider.py +++ b/api/src/backend/api/migrations/0017_m365_provider.py @@ -1,8 +1,7 @@ # Generated by Django 5.1.7 on 2025-04-16 08:47 -from django.db import migrations - import api.db_utils +from django.db import migrations class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0018_resource_scan_summaries.py b/api/src/backend/api/migrations/0018_resource_scan_summaries.py index e9e2ffbe69..0e402cd7c6 100644 --- a/api/src/backend/api/migrations/0018_resource_scan_summaries.py +++ b/api/src/backend/api/migrations/0018_resource_scan_summaries.py @@ -2,12 +2,11 @@ import uuid +import api.rls import django.db.models.deletion import uuid6 from django.db import migrations, models -import api.rls - class Migration(migrations.Migration): dependencies = [ diff --git a/api/src/backend/api/migrations/0020_findings_new_performance_indexes_partitions.py b/api/src/backend/api/migrations/0020_findings_new_performance_indexes_partitions.py index eef7e10b99..544d9dee01 100644 --- a/api/src/backend/api/migrations/0020_findings_new_performance_indexes_partitions.py +++ b/api/src/backend/api/migrations/0020_findings_new_performance_indexes_partitions.py @@ -1,8 +1,7 @@ from functools import partial -from django.db import migrations - from api.db_utils import create_index_on_partitions, drop_index_on_partitions +from django.db import migrations class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0024_findings_uid_index_partitions.py b/api/src/backend/api/migrations/0024_findings_uid_index_partitions.py index d0e237453e..0bec532752 100644 --- a/api/src/backend/api/migrations/0024_findings_uid_index_partitions.py +++ b/api/src/backend/api/migrations/0024_findings_uid_index_partitions.py @@ -1,8 +1,7 @@ from functools import partial -from django.db import migrations - from api.db_utils import create_index_on_partitions, drop_index_on_partitions +from django.db import migrations class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0027_compliance_requirement_overviews.py b/api/src/backend/api/migrations/0027_compliance_requirement_overviews.py index 82bbb136a5..831e1137d4 100644 --- a/api/src/backend/api/migrations/0027_compliance_requirement_overviews.py +++ b/api/src/backend/api/migrations/0027_compliance_requirement_overviews.py @@ -2,12 +2,11 @@ import uuid -import django.db.models.deletion -from django.db import migrations, models - import api.db_utils import api.rls +import django.db.models.deletion from api.rls import RowLevelSecurityConstraint +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0028_findings_check_index_partitions.py b/api/src/backend/api/migrations/0028_findings_check_index_partitions.py index ad61f3004f..5fc1c9dc84 100644 --- a/api/src/backend/api/migrations/0028_findings_check_index_partitions.py +++ b/api/src/backend/api/migrations/0028_findings_check_index_partitions.py @@ -1,8 +1,7 @@ from functools import partial -from django.db import migrations - from api.db_utils import create_index_on_partitions, drop_index_on_partitions +from django.db import migrations class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0030_lighthouseconfiguration.py b/api/src/backend/api/migrations/0030_lighthouseconfiguration.py index b12b8cac01..f00a7a9ae6 100644 --- a/api/src/backend/api/migrations/0030_lighthouseconfiguration.py +++ b/api/src/backend/api/migrations/0030_lighthouseconfiguration.py @@ -2,12 +2,11 @@ import uuid +import api.rls import django.core.validators import django.db.models.deletion from django.db import migrations, models -import api.rls - class Migration(migrations.Migration): dependencies = [ diff --git a/api/src/backend/api/migrations/0032_saml.py b/api/src/backend/api/migrations/0032_saml.py index f1481e104b..7fe71179d5 100644 --- a/api/src/backend/api/migrations/0032_saml.py +++ b/api/src/backend/api/migrations/0032_saml.py @@ -2,13 +2,12 @@ import uuid +import api.db_utils +import api.rls import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import api.db_utils -import api.rls - class Migration(migrations.Migration): dependencies = [ diff --git a/api/src/backend/api/migrations/0033_processors_enum.py b/api/src/backend/api/migrations/0033_processors_enum.py index 7dbad72241..8a4fdef08f 100644 --- a/api/src/backend/api/migrations/0033_processors_enum.py +++ b/api/src/backend/api/migrations/0033_processors_enum.py @@ -2,10 +2,9 @@ from functools import partial -from django.db import migrations - from api.db_utils import PostgresEnumMigration, ProcessorTypeEnum, register_enum from api.models import Processor +from django.db import migrations ProcessorTypeEnumMigration = PostgresEnumMigration( enum_name="processor_type", diff --git a/api/src/backend/api/migrations/0034_processors.py b/api/src/backend/api/migrations/0034_processors.py index 3df4eaf53b..00efd0a283 100644 --- a/api/src/backend/api/migrations/0034_processors.py +++ b/api/src/backend/api/migrations/0034_processors.py @@ -2,12 +2,11 @@ import uuid -import django.db.models.deletion -from django.db import migrations, models - import api.db_utils import api.rls +import django.db.models.deletion from api.rls import RowLevelSecurityConstraint +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0036_rfm_tenant_finding_index_partitions.py b/api/src/backend/api/migrations/0036_rfm_tenant_finding_index_partitions.py index cd360eb7e4..e346a63bb7 100644 --- a/api/src/backend/api/migrations/0036_rfm_tenant_finding_index_partitions.py +++ b/api/src/backend/api/migrations/0036_rfm_tenant_finding_index_partitions.py @@ -1,8 +1,7 @@ from functools import partial -from django.db import migrations - from api.db_utils import create_index_on_partitions, drop_index_on_partitions +from django.db import migrations class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0040_rfm_tenant_resource_index_partitions.py b/api/src/backend/api/migrations/0040_rfm_tenant_resource_index_partitions.py index 431d656376..5fd165d26c 100644 --- a/api/src/backend/api/migrations/0040_rfm_tenant_resource_index_partitions.py +++ b/api/src/backend/api/migrations/0040_rfm_tenant_resource_index_partitions.py @@ -1,8 +1,7 @@ from functools import partial -from django.db import migrations - from api.db_utils import create_index_on_partitions, drop_index_on_partitions +from django.db import migrations class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0043_github_provider.py b/api/src/backend/api/migrations/0043_github_provider.py index 3607ce4af3..3a54bfbcc6 100644 --- a/api/src/backend/api/migrations/0043_github_provider.py +++ b/api/src/backend/api/migrations/0043_github_provider.py @@ -1,8 +1,7 @@ # Generated by Django 5.1.7 on 2025-07-09 14:44 -from django.db import migrations - import api.db_utils +from django.db import migrations class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0048_api_key.py b/api/src/backend/api/migrations/0048_api_key.py index c3142ecda1..a66448325b 100644 --- a/api/src/backend/api/migrations/0048_api_key.py +++ b/api/src/backend/api/migrations/0048_api_key.py @@ -2,15 +2,14 @@ import uuid +import api.db_utils +import api.rls import django.core.validators import django.db.models.deletion import drf_simple_apikey.models from django.conf import settings from django.db import migrations, models -import api.db_utils -import api.rls - class Migration(migrations.Migration): dependencies = [ diff --git a/api/src/backend/api/migrations/0050_lighthouse_multi_llm.py b/api/src/backend/api/migrations/0050_lighthouse_multi_llm.py index 99a9353327..c236f9efea 100644 --- a/api/src/backend/api/migrations/0050_lighthouse_multi_llm.py +++ b/api/src/backend/api/migrations/0050_lighthouse_multi_llm.py @@ -4,15 +4,14 @@ import json import logging import uuid +import api.rls import django.db.models.deletion +from api.db_router import MainRouter from config.custom_logging import BackendLogger from cryptography.fernet import Fernet from django.conf import settings from django.db import migrations, models -import api.rls -from api.db_router import MainRouter - logger = logging.getLogger(BackendLogger.API) diff --git a/api/src/backend/api/migrations/0051_oraclecloud_provider.py b/api/src/backend/api/migrations/0051_oraclecloud_provider.py index 022c022ea6..5688d0a764 100644 --- a/api/src/backend/api/migrations/0051_oraclecloud_provider.py +++ b/api/src/backend/api/migrations/0051_oraclecloud_provider.py @@ -1,8 +1,7 @@ # Generated by Django 5.1.7 on 2025-10-14 00:00 -from django.db import migrations - import api.db_utils +from django.db import migrations class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0052_mute_rules.py b/api/src/backend/api/migrations/0052_mute_rules.py index 56a3ff516f..358402321b 100644 --- a/api/src/backend/api/migrations/0052_mute_rules.py +++ b/api/src/backend/api/migrations/0052_mute_rules.py @@ -2,14 +2,13 @@ import uuid +import api.rls import django.contrib.postgres.fields import django.core.validators import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import api.rls - class Migration(migrations.Migration): dependencies = [ diff --git a/api/src/backend/api/migrations/0054_iac_provider.py b/api/src/backend/api/migrations/0054_iac_provider.py index 03c29e33b6..05350c251f 100644 --- a/api/src/backend/api/migrations/0054_iac_provider.py +++ b/api/src/backend/api/migrations/0054_iac_provider.py @@ -1,8 +1,7 @@ # Generated by Django 5.1.10 on 2025-09-09 09:25 -from django.db import migrations - import api.db_utils +from django.db import migrations class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0055_mongodbatlas_provider.py b/api/src/backend/api/migrations/0055_mongodbatlas_provider.py index d250a0bffd..1703decdb4 100644 --- a/api/src/backend/api/migrations/0055_mongodbatlas_provider.py +++ b/api/src/backend/api/migrations/0055_mongodbatlas_provider.py @@ -1,8 +1,7 @@ # Generated by Django 5.1.13 on 2025-11-05 08:37 -from django.db import migrations - import api.db_utils +from django.db import migrations class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0057_threatscoresnapshot.py b/api/src/backend/api/migrations/0057_threatscoresnapshot.py index ee3530a5b6..171b94c2d5 100644 --- a/api/src/backend/api/migrations/0057_threatscoresnapshot.py +++ b/api/src/backend/api/migrations/0057_threatscoresnapshot.py @@ -2,11 +2,10 @@ import uuid +import api.rls import django.db.models.deletion from django.db import migrations, models -import api.rls - class Migration(migrations.Migration): dependencies = [ diff --git a/api/src/backend/api/migrations/0059_compliance_overview_summary.py b/api/src/backend/api/migrations/0059_compliance_overview_summary.py index d2d57a34ae..06873abef1 100644 --- a/api/src/backend/api/migrations/0059_compliance_overview_summary.py +++ b/api/src/backend/api/migrations/0059_compliance_overview_summary.py @@ -2,11 +2,10 @@ import uuid +import api.rls import django.db.models.deletion from django.db import migrations, models -import api.rls - class Migration(migrations.Migration): dependencies = [ diff --git a/api/src/backend/api/migrations/0060_attack_surface_overview.py b/api/src/backend/api/migrations/0060_attack_surface_overview.py index 8007d49a70..a93cf5b38f 100644 --- a/api/src/backend/api/migrations/0060_attack_surface_overview.py +++ b/api/src/backend/api/migrations/0060_attack_surface_overview.py @@ -2,11 +2,10 @@ import uuid +import api.rls import django.db.models.deletion from django.db import migrations, models -import api.rls - class Migration(migrations.Migration): dependencies = [ diff --git a/api/src/backend/api/migrations/0061_daily_severity_summary.py b/api/src/backend/api/migrations/0061_daily_severity_summary.py index 7e4074cf7f..3aa89133bd 100644 --- a/api/src/backend/api/migrations/0061_daily_severity_summary.py +++ b/api/src/backend/api/migrations/0061_daily_severity_summary.py @@ -2,11 +2,10 @@ import uuid +import api.rls import django.db.models.deletion from django.db import migrations, models -import api.rls - class Migration(migrations.Migration): dependencies = [ diff --git a/api/src/backend/api/migrations/0062_backfill_daily_severity_summaries.py b/api/src/backend/api/migrations/0062_backfill_daily_severity_summaries.py index f893ac4305..92e53bc4ad 100644 --- a/api/src/backend/api/migrations/0062_backfill_daily_severity_summaries.py +++ b/api/src/backend/api/migrations/0062_backfill_daily_severity_summaries.py @@ -1,10 +1,9 @@ # Generated by Django 5.1.14 on 2025-12-10 -from django.db import migrations -from tasks.tasks import backfill_daily_severity_summaries_task - from api.db_router import MainRouter from api.rls import Tenant +from django.db import migrations +from tasks.tasks import backfill_daily_severity_summaries_task def trigger_backfill_task(apps, schema_editor): diff --git a/api/src/backend/api/migrations/0063_scan_category_summary.py b/api/src/backend/api/migrations/0063_scan_category_summary.py index 6ee67bf4db..25ca790c8d 100644 --- a/api/src/backend/api/migrations/0063_scan_category_summary.py +++ b/api/src/backend/api/migrations/0063_scan_category_summary.py @@ -1,10 +1,9 @@ import uuid -import django.db.models.deletion -from django.db import migrations, models - import api.db_utils import api.rls +import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0065_alibabacloud_provider.py b/api/src/backend/api/migrations/0065_alibabacloud_provider.py index 6ad542b643..d9f4250304 100644 --- a/api/src/backend/api/migrations/0065_alibabacloud_provider.py +++ b/api/src/backend/api/migrations/0065_alibabacloud_provider.py @@ -1,8 +1,7 @@ # Generated by Django migration for Alibaba Cloud provider support -from django.db import migrations - import api.db_utils +from django.db import migrations class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0066_provider_compliance_score.py b/api/src/backend/api/migrations/0066_provider_compliance_score.py index f9a6483e4f..2649d8fbdf 100644 --- a/api/src/backend/api/migrations/0066_provider_compliance_score.py +++ b/api/src/backend/api/migrations/0066_provider_compliance_score.py @@ -1,10 +1,9 @@ import uuid -import django.db.models.deletion -from django.db import migrations, models - import api.db_utils import api.rls +import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0067_tenant_compliance_summary.py b/api/src/backend/api/migrations/0067_tenant_compliance_summary.py index bd753ca575..92973320bc 100644 --- a/api/src/backend/api/migrations/0067_tenant_compliance_summary.py +++ b/api/src/backend/api/migrations/0067_tenant_compliance_summary.py @@ -1,10 +1,9 @@ import uuid +import api.rls import django.db.models.deletion from django.db import migrations, models -import api.rls - class Migration(migrations.Migration): dependencies = [ diff --git a/api/src/backend/api/migrations/0068_finding_resource_group_scangroupsummary.py b/api/src/backend/api/migrations/0068_finding_resource_group_scangroupsummary.py index 932a2a6c85..c13ada78e2 100644 --- a/api/src/backend/api/migrations/0068_finding_resource_group_scangroupsummary.py +++ b/api/src/backend/api/migrations/0068_finding_resource_group_scangroupsummary.py @@ -1,10 +1,9 @@ import uuid -import django.db.models.deletion -from django.db import migrations, models - import api.db_utils import api.rls +import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0070_attack_paths_scan.py b/api/src/backend/api/migrations/0070_attack_paths_scan.py index 3e63d3353b..557b04a9ce 100644 --- a/api/src/backend/api/migrations/0070_attack_paths_scan.py +++ b/api/src/backend/api/migrations/0070_attack_paths_scan.py @@ -1,12 +1,10 @@ # Generated by Django 5.1.13 on 2025-11-06 16:20 +import api.rls import django.db.models.deletion - from django.db import migrations, models from uuid6 import uuid7 -import api.rls - class Migration(migrations.Migration): dependencies = [ diff --git a/api/src/backend/api/migrations/0073_findings_fail_new_index_partitions.py b/api/src/backend/api/migrations/0073_findings_fail_new_index_partitions.py index 671fdf5ef6..06f04f2734 100644 --- a/api/src/backend/api/migrations/0073_findings_fail_new_index_partitions.py +++ b/api/src/backend/api/migrations/0073_findings_fail_new_index_partitions.py @@ -1,8 +1,7 @@ from functools import partial -from django.db import migrations - from api.db_utils import create_index_on_partitions, drop_index_on_partitions +from django.db import migrations class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0075_cloudflare_provider.py b/api/src/backend/api/migrations/0075_cloudflare_provider.py index 28fdbdb2a9..dcfffe83c6 100644 --- a/api/src/backend/api/migrations/0075_cloudflare_provider.py +++ b/api/src/backend/api/migrations/0075_cloudflare_provider.py @@ -1,8 +1,7 @@ # Generated by Django migration for Cloudflare provider support -from django.db import migrations - import api.db_utils +from django.db import migrations class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0076_openstack_provider.py b/api/src/backend/api/migrations/0076_openstack_provider.py index 9cc80707ea..680cc4310a 100644 --- a/api/src/backend/api/migrations/0076_openstack_provider.py +++ b/api/src/backend/api/migrations/0076_openstack_provider.py @@ -1,8 +1,7 @@ # Generated by Django migration for OpenStack provider support -from django.db import migrations - import api.db_utils +from django.db import migrations class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0080_backfill_attack_paths_graph_data_ready.py b/api/src/backend/api/migrations/0080_backfill_attack_paths_graph_data_ready.py index f780059bd2..542b117a22 100644 --- a/api/src/backend/api/migrations/0080_backfill_attack_paths_graph_data_ready.py +++ b/api/src/backend/api/migrations/0080_backfill_attack_paths_graph_data_ready.py @@ -2,9 +2,8 @@ # on different database connections, causing a deadlock when combined with RunPython # in the same migration. -from django.db import migrations - from api.db_router import MainRouter +from django.db import migrations def backfill_graph_data_ready(apps, schema_editor): diff --git a/api/src/backend/api/migrations/0081_finding_group_daily_summary.py b/api/src/backend/api/migrations/0081_finding_group_daily_summary.py index 31c09c464f..e4685cea5f 100644 --- a/api/src/backend/api/migrations/0081_finding_group_daily_summary.py +++ b/api/src/backend/api/migrations/0081_finding_group_daily_summary.py @@ -2,14 +2,13 @@ import uuid +import api.rls import django.db.models.deletion from django.contrib.postgres.indexes import GinIndex, OpClass from django.db import migrations, models from django.db.models.functions import Upper from django.utils import timezone -import api.rls - class Migration(migrations.Migration): dependencies = [ diff --git a/api/src/backend/api/migrations/0082_backfill_finding_group_summaries.py b/api/src/backend/api/migrations/0082_backfill_finding_group_summaries.py index 38cc07f43d..ef3e9c49a9 100644 --- a/api/src/backend/api/migrations/0082_backfill_finding_group_summaries.py +++ b/api/src/backend/api/migrations/0082_backfill_finding_group_summaries.py @@ -1,10 +1,9 @@ # Generated by Django 5.1.14 on 2026-02-02 -from django.db import migrations -from tasks.tasks import backfill_finding_group_summaries_task - from api.db_router import MainRouter from api.rls import Tenant +from django.db import migrations +from tasks.tasks import backfill_finding_group_summaries_task def trigger_backfill_task(apps, schema_editor): diff --git a/api/src/backend/api/migrations/0083_image_provider.py b/api/src/backend/api/migrations/0083_image_provider.py index 936fae2219..6f2b5a9d6b 100644 --- a/api/src/backend/api/migrations/0083_image_provider.py +++ b/api/src/backend/api/migrations/0083_image_provider.py @@ -1,6 +1,5 @@ -from django.db import migrations - import api.db_utils +from django.db import migrations class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0084_googleworkspace_provider.py b/api/src/backend/api/migrations/0084_googleworkspace_provider.py index eb704bb6b5..e7971e2568 100644 --- a/api/src/backend/api/migrations/0084_googleworkspace_provider.py +++ b/api/src/backend/api/migrations/0084_googleworkspace_provider.py @@ -1,6 +1,5 @@ -from django.db import migrations - import api.db_utils +from django.db import migrations class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0086_attack_paths_cleanup_periodic_task.py b/api/src/backend/api/migrations/0086_attack_paths_cleanup_periodic_task.py index 1c550f283a..2bf7bf2fb4 100644 --- a/api/src/backend/api/migrations/0086_attack_paths_cleanup_periodic_task.py +++ b/api/src/backend/api/migrations/0086_attack_paths_cleanup_periodic_task.py @@ -1,6 +1,5 @@ from django.db import migrations - TASK_NAME = "attack-paths-cleanup-stale-scans" INTERVAL_HOURS = 1 diff --git a/api/src/backend/api/migrations/0087_vercel_provider.py b/api/src/backend/api/migrations/0087_vercel_provider.py index 84a07b3194..92063fb6da 100644 --- a/api/src/backend/api/migrations/0087_vercel_provider.py +++ b/api/src/backend/api/migrations/0087_vercel_provider.py @@ -1,6 +1,5 @@ -from django.db import migrations - import api.db_utils +from django.db import migrations class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0089_backfill_finding_group_status_muted.py b/api/src/backend/api/migrations/0089_backfill_finding_group_status_muted.py index 501fcf3cb4..3df6b4b167 100644 --- a/api/src/backend/api/migrations/0089_backfill_finding_group_status_muted.py +++ b/api/src/backend/api/migrations/0089_backfill_finding_group_status_muted.py @@ -1,8 +1,7 @@ -from django.db import migrations -from tasks.tasks import backfill_finding_group_summaries_task - from api.db_router import MainRouter from api.rls import Tenant +from django.db import migrations +from tasks.tasks import backfill_finding_group_summaries_task def trigger_backfill_task(apps, schema_editor): diff --git a/api/src/backend/api/migrations/0091_findings_arrays_gin_index_partitions.py b/api/src/backend/api/migrations/0091_findings_arrays_gin_index_partitions.py index fc5716f828..6c4e978b28 100644 --- a/api/src/backend/api/migrations/0091_findings_arrays_gin_index_partitions.py +++ b/api/src/backend/api/migrations/0091_findings_arrays_gin_index_partitions.py @@ -1,8 +1,7 @@ from functools import partial -from django.db import migrations - from api.db_utils import create_index_on_partitions, drop_index_on_partitions +from django.db import migrations class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0093_okta_provider.py b/api/src/backend/api/migrations/0093_okta_provider.py index d3e4a9e397..cc28c45fdb 100644 --- a/api/src/backend/api/migrations/0093_okta_provider.py +++ b/api/src/backend/api/migrations/0093_okta_provider.py @@ -1,6 +1,5 @@ -from django.db import migrations - import api.db_utils +from django.db import migrations class Migration(migrations.Migration): diff --git a/api/src/backend/api/migrations/0095_reconcile_orphan_tasks_periodic_task.py b/api/src/backend/api/migrations/0095_reconcile_orphan_tasks_periodic_task.py index 9d67404258..6ba2fa758a 100644 --- a/api/src/backend/api/migrations/0095_reconcile_orphan_tasks_periodic_task.py +++ b/api/src/backend/api/migrations/0095_reconcile_orphan_tasks_periodic_task.py @@ -1,6 +1,5 @@ from django.db import migrations - TASK_NAME = "reconcile-orphan-tasks" INTERVAL_MINUTES = 2 diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index 3d9a26698e..47f9803b95 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -1,37 +1,11 @@ import json import logging import re -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from uuid import UUID, uuid4 import defusedxml from allauth.socialaccount.models import SocialApp -from config.custom_logging import BackendLogger -from config.settings.social_login import SOCIALACCOUNT_PROVIDERS -from cryptography.fernet import Fernet, InvalidToken -from defusedxml import ElementTree as ET -from django.conf import settings -from django.contrib.auth.models import AbstractBaseUser -from django.contrib.postgres.fields import ArrayField -from django.contrib.postgres.indexes import GinIndex, OpClass -from django.contrib.postgres.search import SearchVector, SearchVectorField -from django.contrib.sites.models import Site -from django.core.exceptions import ValidationError -from django.core.validators import MinLengthValidator -from django.db import models -from django.db.models import Q -from django.db.models.functions import Upper -from django.utils import timezone as django_timezone -from django.utils.translation import gettext_lazy as _ -from django_celery_beat.models import PeriodicTask -from django_celery_results.models import TaskResult -from drf_simple_apikey.crypto import get_crypto -from drf_simple_apikey.models import AbstractAPIKey, AbstractAPIKeyManager -from psqlextra.manager import PostgresManager -from psqlextra.models import PostgresPartitionedModel -from psqlextra.types import PostgresPartitioningMethod -from uuid6 import uuid7 - from api.db_router import MainRouter from api.db_utils import ( CustomUserManager, @@ -58,7 +32,32 @@ from api.rls import ( RowLevelSecurityProtectedModel, Tenant, ) +from config.custom_logging import BackendLogger +from config.settings.social_login import SOCIALACCOUNT_PROVIDERS +from cryptography.fernet import Fernet, InvalidToken +from defusedxml import ElementTree as ET +from django.conf import settings +from django.contrib.auth.models import AbstractBaseUser +from django.contrib.postgres.fields import ArrayField +from django.contrib.postgres.indexes import GinIndex, OpClass +from django.contrib.postgres.search import SearchVector, SearchVectorField +from django.contrib.sites.models import Site +from django.core.exceptions import ValidationError +from django.core.validators import MinLengthValidator +from django.db import models +from django.db.models import Q +from django.db.models.functions import Upper +from django.utils import timezone as django_timezone +from django.utils.translation import gettext_lazy as _ +from django_celery_beat.models import PeriodicTask +from django_celery_results.models import TaskResult +from drf_simple_apikey.crypto import get_crypto +from drf_simple_apikey.models import AbstractAPIKey, AbstractAPIKeyManager from prowler.lib.check.models import Severity +from psqlextra.manager import PostgresManager +from psqlextra.models import PostgresPartitionedModel +from psqlextra.types import PostgresPartitioningMethod +from uuid6 import uuid7 fernet = Fernet(settings.SECRETS_ENCRYPTION_KEY.encode()) @@ -1427,8 +1426,8 @@ class Role(RowLevelSecurityProtectedModel): @classmethod def filter_by_permission_state(cls, queryset, value): - q_all_true = Q(**{field: True for field in cls.PERMISSION_FIELDS}) - q_all_false = Q(**{field: False for field in cls.PERMISSION_FIELDS}) + q_all_true = Q(**dict.fromkeys(cls.PERMISSION_FIELDS, True)) + q_all_false = Q(**dict.fromkeys(cls.PERMISSION_FIELDS, False)) if value == PermissionChoices.UNLIMITED: return queryset.filter(q_all_true) @@ -2011,11 +2010,11 @@ class SAMLToken(models.Model): def save(self, *args, **kwargs): if not self.expires_at: - self.expires_at = datetime.now(timezone.utc) + timedelta(seconds=15) + self.expires_at = datetime.now(UTC) + timedelta(seconds=15) super().save(*args, **kwargs) def is_expired(self) -> bool: - return datetime.now(timezone.utc) >= self.expires_at + return datetime.now(UTC) >= self.expires_at class SAMLDomainIndex(models.Model): diff --git a/api/src/backend/api/partitions.py b/api/src/backend/api/partitions.py index 92390ffdec..8903c4504b 100644 --- a/api/src/backend/api/partitions.py +++ b/api/src/backend/api/partitions.py @@ -1,21 +1,20 @@ -from datetime import datetime, timezone -from typing import Generator, Optional - -from dateutil.relativedelta import relativedelta -from django.conf import settings -from psqlextra.partitioning import ( - PostgresPartitioningManager, - PostgresRangePartition, - PostgresRangePartitioningStrategy, - PostgresTimePartitionSize, - PostgresPartitioningError, -) -from psqlextra.partitioning.config import PostgresPartitioningConfig -from uuid6 import UUID +from collections.abc import Generator +from datetime import UTC, datetime from api.models import Finding, ResourceFindingMapping from api.rls import RowLevelSecurityConstraint from api.uuid_utils import datetime_to_uuid7 +from dateutil.relativedelta import relativedelta +from django.conf import settings +from psqlextra.partitioning import ( + PostgresPartitioningError, + PostgresPartitioningManager, + PostgresRangePartition, + PostgresRangePartitioningStrategy, + PostgresTimePartitionSize, +) +from psqlextra.partitioning.config import PostgresPartitioningConfig +from uuid6 import UUID class PostgresUUIDv7RangePartition(PostgresRangePartition): @@ -24,7 +23,7 @@ class PostgresUUIDv7RangePartition(PostgresRangePartition): from_values: UUID, to_values: UUID, size: PostgresTimePartitionSize, - name_format: Optional[str] = None, + name_format: str | None = None, **kwargs, ) -> None: self.from_values = from_values @@ -38,9 +37,7 @@ class PostgresUUIDv7RangePartition(PostgresRangePartition): start_timestamp_ms = self.from_values.time - self.start_datetime = datetime.fromtimestamp( - start_timestamp_ms / 1000, timezone.utc - ) + self.start_datetime = datetime.fromtimestamp(start_timestamp_ms / 1000, UTC) def name(self) -> str: if not self.name_format: @@ -82,8 +79,8 @@ class PostgresUUIDv7PartitioningStrategy(PostgresRangePartitioningStrategy): size: PostgresTimePartitionSize, count: int, start_date: datetime = None, - max_age: Optional[relativedelta] = None, - name_format: Optional[str] = None, + max_age: relativedelta | None = None, + name_format: str | None = None, **kwargs, ) -> None: self.start_date = start_date.replace( @@ -151,7 +148,7 @@ class PostgresUUIDv7PartitioningStrategy(PostgresRangePartitioningStrategy): Returns: datetime: A `datetime` object representing the start of the current month in UTC. """ - return datetime.now(timezone.utc).replace( + return datetime.now(UTC).replace( day=1, hour=0, minute=0, second=0, microsecond=0 ) @@ -171,7 +168,7 @@ manager = PostgresPartitioningManager( PostgresPartitioningConfig( model=Finding, strategy=PostgresUUIDv7PartitioningStrategy( - start_date=datetime.now(timezone.utc), + start_date=datetime.now(UTC), size=PostgresTimePartitionSize( months=settings.FINDINGS_TABLE_PARTITION_MONTHS ), @@ -187,7 +184,7 @@ manager = PostgresPartitioningManager( PostgresPartitioningConfig( model=ResourceFindingMapping, strategy=PostgresUUIDv7PartitioningStrategy( - start_date=datetime.now(timezone.utc), + start_date=datetime.now(UTC), size=PostgresTimePartitionSize( months=settings.FINDINGS_TABLE_PARTITION_MONTHS ), diff --git a/api/src/backend/api/rbac/permissions.py b/api/src/backend/api/rbac/permissions.py index cfbabf6c0b..ef0475fefb 100644 --- a/api/src/backend/api/rbac/permissions.py +++ b/api/src/backend/api/rbac/permissions.py @@ -1,11 +1,10 @@ from enum import Enum -from django.db.models import QuerySet -from rest_framework.exceptions import PermissionDenied -from rest_framework.permissions import BasePermission - from api.db_router import MainRouter from api.models import Provider, Role, User +from django.db.models import QuerySet +from rest_framework.exceptions import PermissionDenied +from rest_framework.permissions import BasePermission class Permissions(Enum): diff --git a/api/src/backend/api/renderers.py b/api/src/backend/api/renderers.py index 77349540ce..e0fafac3c4 100644 --- a/api/src/backend/api/renderers.py +++ b/api/src/backend/api/renderers.py @@ -1,10 +1,9 @@ from contextlib import nullcontext +from api.db_utils import rls_transaction from rest_framework.renderers import BaseRenderer from rest_framework_json_api.renderers import JSONRenderer -from api.db_utils import rls_transaction - class PlainTextRenderer(BaseRenderer): media_type = "text/plain" diff --git a/api/src/backend/api/rls.py b/api/src/backend/api/rls.py index 285b06a974..9e4754c842 100644 --- a/api/src/backend/api/rls.py +++ b/api/src/backend/api/rls.py @@ -1,12 +1,11 @@ from typing import Any from uuid import uuid4 +from api.db_utils import DB_USER, POSTGRES_TENANT_VAR from django.core.exceptions import ValidationError from django.db import DEFAULT_DB_ALIAS, models from django.db.backends.ddl_references import Statement, Table -from api.db_utils import DB_USER, POSTGRES_TENANT_VAR - class Tenant(models.Model): """ diff --git a/api/src/backend/api/signals.py b/api/src/backend/api/signals.py index 7bca0da0a6..790779f087 100644 --- a/api/src/backend/api/signals.py +++ b/api/src/backend/api/signals.py @@ -1,10 +1,3 @@ -from celery import states -from celery.signals import before_task_publish -from config.celery import celery_app -from django.db.models.signals import post_delete, pre_delete -from django.dispatch import receiver -from django_celery_results.backends.database import DatabaseBackend - from api.db_utils import delete_related_daily_task from api.models import ( LighthouseProviderConfiguration, @@ -14,6 +7,12 @@ from api.models import ( TenantAPIKey, User, ) +from celery import states +from celery.signals import before_task_publish +from config.celery import celery_app +from django.db.models.signals import post_delete, pre_delete +from django.dispatch import receiver +from django_celery_results.backends.database import DatabaseBackend def create_task_result_on_publish(sender=None, headers=None, **kwargs): # noqa: F841 diff --git a/api/src/backend/api/sse/__init__.py b/api/src/backend/api/sse/__init__.py index 244bd3c9ad..dd31d16430 100644 --- a/api/src/backend/api/sse/__init__.py +++ b/api/src/backend/api/sse/__init__.py @@ -7,7 +7,7 @@ enforces the tenant gate (:class:`api.sse.channelmanager.SSEChannelManager`), and the channel-name helpers (:func:`api.sse.utils.make_channel_name`). """ -from api.sse.utils import make_channel_name from api.sse.base_views import BaseSSEViewSet +from api.sse.utils import make_channel_name __all__ = ["BaseSSEViewSet", "make_channel_name"] diff --git a/api/src/backend/api/sse/channelmanager.py b/api/src/backend/api/sse/channelmanager.py index 72b2c18367..9190d4ab16 100644 --- a/api/src/backend/api/sse/channelmanager.py +++ b/api/src/backend/api/sse/channelmanager.py @@ -5,11 +5,10 @@ from __future__ import annotations from typing import TYPE_CHECKING from uuid import UUID +from api.sse.utils import tenant_id_from_channel from django_eventstream.channelmanager import DefaultChannelManager from rest_framework.request import Request -from api.sse.utils import tenant_id_from_channel - if TYPE_CHECKING: from api.models import User @@ -41,7 +40,7 @@ class SSEChannelManager(DefaultChannelManager): if tenant_id_from_channel(channel) == request_tenant_id } - def can_read_channel(self, user: "User | None", channel: str) -> bool: + def can_read_channel(self, user: User | None, channel: str) -> bool: """Re-verify tenant membership once the stream is established. Args: diff --git a/api/src/backend/api/tests/integration/test_authentication.py b/api/src/backend/api/tests/integration/test_authentication.py index 061c2efac0..4d1c40fe23 100644 --- a/api/src/backend/api/tests/integration/test_authentication.py +++ b/api/src/backend/api/tests/integration/test_authentication.py @@ -1,15 +1,14 @@ import time -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from uuid import uuid4 import pytest +from api.models import Membership, Role, TenantAPIKey, User, UserRoleRelationship from conftest import TEST_PASSWORD, get_api_tokens, get_authorization_header from django.urls import reverse from drf_simple_apikey.crypto import get_crypto from rest_framework.test import APIClient -from api.models import Membership, Role, TenantAPIKey, User, UserRoleRelationship - @pytest.mark.django_db def test_basic_authentication(): @@ -468,7 +467,7 @@ class TestAPIKeyErrors: name="Expired Key", tenant_id=tenants_fixture[0].id, entity=create_test_user, - expiry_date=datetime.now(timezone.utc) - timedelta(days=1), + expiry_date=datetime.now(UTC) - timedelta(days=1), ) api_key_headers = get_api_key_header(raw_key) @@ -500,7 +499,7 @@ class TestAPIKeyErrors: # Create a valid-looking key with non-existent UUID crypto = get_crypto() fake_uuid = str(uuid4()) - fake_expiry = (datetime.now(timezone.utc) + timedelta(days=30)).timestamp() + fake_expiry = (datetime.now(UTC) + timedelta(days=30)).timestamp() payload = {"_pk": fake_uuid, "_exp": fake_expiry} encrypted_payload = crypto.generate(payload) @@ -723,7 +722,7 @@ class TestAPIKeyLifecycle: assert created_data["attributes"]["revoked"] is False # Create API key with expiry - future_expiry = (datetime.now(timezone.utc) + timedelta(days=90)).isoformat() + future_expiry = (datetime.now(UTC) + timedelta(days=90)).isoformat() create_with_expiry_response = client.post( reverse("api-key-list"), data={ @@ -927,9 +926,9 @@ class TestAPIKeyLifecycle: auth_response = client.get(reverse("provider-list"), headers=api_key_headers) # Must return 401 Unauthorized, not 500 Internal Server Error - assert ( - auth_response.status_code == 401 - ), f"Expected 401 but got {auth_response.status_code}: {auth_response.json()}" + assert auth_response.status_code == 401, ( + f"Expected 401 but got {auth_response.status_code}: {auth_response.json()}" + ) # Verify error message is present response_json = auth_response.json() @@ -1267,7 +1266,7 @@ class TestAPIKeyRLSBypass: name="Expired Test Key", tenant_id=tenant.id, entity=create_test_user, - expiry_date=datetime.now(timezone.utc) - timedelta(days=1), + expiry_date=datetime.now(UTC) - timedelta(days=1), ) api_key_headers = get_api_key_header(raw_key) diff --git a/api/src/backend/api/tests/integration/test_providers.py b/api/src/backend/api/tests/integration/test_providers.py index 9c91ad2c07..e797160a15 100644 --- a/api/src/backend/api/tests/integration/test_providers.py +++ b/api/src/backend/api/tests/integration/test_providers.py @@ -1,12 +1,11 @@ from unittest.mock import Mock, patch import pytest +from api.models import Provider from conftest import get_api_tokens, get_authorization_header from django.urls import reverse from rest_framework.test import APIClient -from api.models import Provider - @patch("api.v1.views.Task.objects.get") @patch("api.v1.views.delete_provider_task.delay") diff --git a/api/src/backend/api/tests/integration/test_rls_transaction.py b/api/src/backend/api/tests/integration/test_rls_transaction.py index 6731b39d71..bd46871586 100644 --- a/api/src/backend/api/tests/integration/test_rls_transaction.py +++ b/api/src/backend/api/tests/integration/test_rls_transaction.py @@ -1,11 +1,10 @@ """Tests for rls_transaction retry and fallback logic.""" import pytest +from api.db_utils import rls_transaction from django.db import DEFAULT_DB_ALIAS from rest_framework_json_api.serializers import ValidationError -from api.db_utils import rls_transaction - @pytest.mark.django_db class TestRLSTransaction: diff --git a/api/src/backend/api/tests/integration/test_tenants.py b/api/src/backend/api/tests/integration/test_tenants.py index e14226164a..4d5dd8a523 100644 --- a/api/src/backend/api/tests/integration/test_tenants.py +++ b/api/src/backend/api/tests/integration/test_tenants.py @@ -1,10 +1,9 @@ from unittest.mock import patch import pytest +from conftest import TEST_PASSWORD, TEST_USER, get_api_tokens, get_authorization_header from django.urls import reverse -from conftest import TEST_USER, TEST_PASSWORD, get_api_tokens, get_authorization_header - @patch("api.v1.views.schedule_provider_scan") @pytest.mark.django_db diff --git a/api/src/backend/api/tests/test_adapters.py b/api/src/backend/api/tests/test_adapters.py index 182e4ddc25..91d3bb054a 100644 --- a/api/src/backend/api/tests/test_adapters.py +++ b/api/src/backend/api/tests/test_adapters.py @@ -3,11 +3,10 @@ from unittest.mock import MagicMock, patch import pytest from allauth.socialaccount.models import SocialLogin -from django.contrib.auth import get_user_model - from api.adapters import ProwlerSocialAccountAdapter from api.db_router import MainRouter from api.models import SAMLConfiguration +from django.contrib.auth import get_user_model User = get_user_model() diff --git a/api/src/backend/api/tests/test_apps.py b/api/src/backend/api/tests/test_apps.py index 2f5b55a6e2..93934df674 100644 --- a/api/src/backend/api/tests/test_apps.py +++ b/api/src/backend/api/tests/test_apps.py @@ -4,11 +4,9 @@ import types from pathlib import Path from unittest.mock import MagicMock, patch -import pytest -from django.conf import settings - import api import api.apps as api_apps_module +import pytest from api.apps import ( PRIVATE_KEY_FILE, PUBLIC_KEY_FILE, @@ -16,6 +14,7 @@ from api.apps import ( VERIFYING_KEY_ENV, ApiConfig, ) +from django.conf import settings @pytest.fixture(autouse=True) diff --git a/api/src/backend/api/tests/test_attack_paths.py b/api/src/backend/api/tests/test_attack_paths.py index 019b6aa1f2..30104a5a63 100644 --- a/api/src/backend/api/tests/test_attack_paths.py +++ b/api/src/backend/api/tests/test_attack_paths.py @@ -1,14 +1,12 @@ from types import SimpleNamespace from unittest.mock import MagicMock, patch -import pytest import neo4j import neo4j.exceptions - -from rest_framework.exceptions import APIException, PermissionDenied, ValidationError - +import pytest from api.attack_paths import database as graph_database from api.attack_paths import views_helpers +from rest_framework.exceptions import APIException, PermissionDenied, ValidationError from tasks.jobs.attack_paths.config import ( PROVIDER_ELEMENT_ID_PROPERTY, get_provider_label, diff --git a/api/src/backend/api/tests/test_attack_paths_database.py b/api/src/backend/api/tests/test_attack_paths_database.py index 7ca8a4accb..bf37daf5ee 100644 --- a/api/src/backend/api/tests/test_attack_paths_database.py +++ b/api/src/backend/api/tests/test_attack_paths_database.py @@ -6,15 +6,13 @@ never contacts Neo4j. These tests validate the database module behavior itself. """ import threading - from unittest.mock import MagicMock, patch +import api.attack_paths.database as db_module import neo4j import neo4j.exceptions import pytest -import api.attack_paths.database as db_module - class TestLazyInitialization: """Test that Neo4j driver is initialized lazily on first use.""" diff --git a/api/src/backend/api/tests/test_authentication.py b/api/src/backend/api/tests/test_authentication.py index 505d7c9320..fa72b395b3 100644 --- a/api/src/backend/api/tests/test_authentication.py +++ b/api/src/backend/api/tests/test_authentication.py @@ -1,15 +1,14 @@ import time -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from unittest.mock import MagicMock, patch from uuid import uuid4 import pytest -from django.test import RequestFactory -from rest_framework.exceptions import AuthenticationFailed - from api.authentication import SSEAuthentication, TenantAPIKeyAuthentication from api.db_router import MainRouter from api.models import TenantAPIKey +from django.test import RequestFactory +from rest_framework.exceptions import AuthenticationFailed @pytest.mark.django_db @@ -104,7 +103,7 @@ class TestTenantAPIKeyAuthentication: # Verify that last_used_at was updated api_key.refresh_from_db() assert api_key.last_used_at is not None - assert (datetime.now(timezone.utc) - api_key.last_used_at).seconds < 5 + assert (datetime.now(UTC) - api_key.last_used_at).seconds < 5 def test_authenticate_valid_api_key_uses_admin_database( self, auth_backend, api_keys_fixture, request_factory @@ -195,7 +194,7 @@ class TestTenantAPIKeyAuthentication: name="Expired API Key", tenant_id=tenant.id, entity=user, - expiry_date=datetime.now(timezone.utc) - timedelta(days=1), + expiry_date=datetime.now(UTC) - timedelta(days=1), ) request = request_factory.get("/") @@ -217,7 +216,7 @@ class TestTenantAPIKeyAuthentication: # Manually create an encrypted key with a non-existent ID payload = { "_pk": non_existent_uuid, - "_exp": (datetime.now(timezone.utc) + timedelta(days=30)).timestamp(), + "_exp": (datetime.now(UTC) + timedelta(days=30)).timestamp(), } encrypted_key = auth_backend.key_crypto.generate(payload) fake_key = f"{api_key.prefix}.{encrypted_key}" @@ -368,7 +367,7 @@ class TestTenantAPIKeyAuthentication: name="Short-lived API Key", tenant_id=tenant.id, entity=user, - expiry_date=datetime.now(timezone.utc) + timedelta(seconds=1), + expiry_date=datetime.now(UTC) + timedelta(seconds=1), ) # Wait for the key to expire diff --git a/api/src/backend/api/tests/test_compliance.py b/api/src/backend/api/tests/test_compliance.py index 99a31ea12c..d613a2538e 100644 --- a/api/src/backend/api/tests/test_compliance.py +++ b/api/src/backend/api/tests/test_compliance.py @@ -1,7 +1,6 @@ from unittest.mock import MagicMock, patch import pytest - from api import compliance as compliance_module from api.compliance import ( generate_compliance_overview_template, diff --git a/api/src/backend/api/tests/test_cypher_sanitizer.py b/api/src/backend/api/tests/test_cypher_sanitizer.py index a54afcd8bb..6caca47a56 100644 --- a/api/src/backend/api/tests/test_cypher_sanitizer.py +++ b/api/src/backend/api/tests/test_cypher_sanitizer.py @@ -3,13 +3,11 @@ from unittest.mock import patch import pytest - -from rest_framework.exceptions import ValidationError - from api.attack_paths.cypher_sanitizer import ( inject_provider_label, validate_custom_query, ) +from rest_framework.exceptions import ValidationError PROVIDER_ID = "019c41ee-7df3-7dec-a684-d839f95619f8" LABEL = "_Provider_019c41ee7df37deca684d839f95619f8" @@ -202,9 +200,7 @@ class TestClauseSplitting: def test_multiple_match_clauses(self): cypher = ( - "MATCH (a:AWSAccount)--(b:AWSRole) " - "MATCH (b)--(c:AWSPolicy) " - "RETURN a, b, c" + "MATCH (a:AWSAccount)--(b:AWSRole) MATCH (b)--(c:AWSPolicy) RETURN a, b, c" ) result = _inject(cypher) assert f"(a:AWSAccount:{LABEL})" in result @@ -265,9 +261,7 @@ class TestRealWorldQueries: def test_custom_bare_query(self): cypher = ( - "MATCH (a)-[:HAS_POLICY]->(b)\n" - "WHERE a.name CONTAINS 'admin'\n" - "RETURN a, b" + "MATCH (a)-[:HAS_POLICY]->(b)\nWHERE a.name CONTAINS 'admin'\nRETURN a, b" ) result = _inject(cypher) assert f"(a:{LABEL})" in result @@ -344,9 +338,7 @@ class TestEdgeCases: assert f"(outer:AWSAccount:{LABEL})" in result def test_multiple_protected_regions(self): - cypher = ( - "MATCH (n:X {a: 'hello'}) " 'WHERE n.b = "world" ' "// comment\n" "RETURN n" - ) + cypher = "MATCH (n:X {a: 'hello'}) WHERE n.b = \"world\" // comment\nRETURN n" result = _inject(cypher) assert "'hello'" in result assert '"world"' in result diff --git a/api/src/backend/api/tests/test_database.py b/api/src/backend/api/tests/test_database.py index 46d3203414..8d328c6a91 100644 --- a/api/src/backend/api/tests/test_database.py +++ b/api/src/backend/api/tests/test_database.py @@ -1,12 +1,12 @@ -import pytest -from django.conf import settings -from django.db.migrations.recorder import MigrationRecorder -from django.db.utils import ConnectionRouter +from unittest.mock import patch +import pytest from api.db_router import MainRouter from api.rls import Tenant from config.django.base import DATABASE_ROUTERS as PROD_DATABASE_ROUTERS -from unittest.mock import patch +from django.conf import settings +from django.db.migrations.recorder import MigrationRecorder +from django.db.utils import ConnectionRouter @patch("api.db_router.MainRouter.admin_db", new="admin") diff --git a/api/src/backend/api/tests/test_db_utils.py b/api/src/backend/api/tests/test_db_utils.py index 18935b9a3e..06b528b44a 100644 --- a/api/src/backend/api/tests/test_db_utils.py +++ b/api/src/backend/api/tests/test_db_utils.py @@ -1,14 +1,8 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from enum import Enum from unittest.mock import MagicMock, patch import pytest -from django.conf import settings -from django.db import DEFAULT_DB_ALIAS, OperationalError -from freezegun import freeze_time -from psycopg2 import sql as psycopg2_sql -from rest_framework_json_api.serializers import ValidationError - from api.db_utils import ( POSTGRES_TENANT_VAR, PostgresEnumMigration, @@ -23,6 +17,11 @@ from api.db_utils import ( update_objects_in_batches, ) from api.models import Provider +from django.conf import settings +from django.db import DEFAULT_DB_ALIAS, OperationalError +from freezegun import freeze_time +from psycopg2 import sql as psycopg2_sql +from rest_framework_json_api.serializers import ValidationError @pytest.fixture @@ -94,18 +93,16 @@ class TestEnumToChoices: class TestOneWeekFromNow: def test_one_week_from_now(self): with patch("api.db_utils.datetime") as mock_datetime: - mock_datetime.now.return_value = datetime(2023, 1, 1, tzinfo=timezone.utc) - expected_result = datetime(2023, 1, 8, tzinfo=timezone.utc) + mock_datetime.now.return_value = datetime(2023, 1, 1, tzinfo=UTC) + expected_result = datetime(2023, 1, 8, tzinfo=UTC) result = one_week_from_now() assert result == expected_result def test_one_week_from_now_with_timezone(self): with patch("api.db_utils.datetime") as mock_datetime: - mock_datetime.now.return_value = datetime( - 2023, 6, 15, 12, 0, tzinfo=timezone.utc - ) - expected_result = datetime(2023, 6, 22, 12, 0, tzinfo=timezone.utc) + mock_datetime.now.return_value = datetime(2023, 6, 15, 12, 0, tzinfo=UTC) + expected_result = datetime(2023, 6, 22, 12, 0, tzinfo=UTC) result = one_week_from_now() assert result == expected_result @@ -939,9 +936,9 @@ class TestPostgresEnumMigration: mock_cursor.execute.assert_called_once() query_arg = mock_cursor.execute.call_args[0][0] - assert isinstance( - query_arg, psycopg2_sql.Composable - ), "create_enum_type must pass a psycopg2.sql.Composable, not a raw string." + assert isinstance(query_arg, psycopg2_sql.Composable), ( + "create_enum_type must pass a psycopg2.sql.Composable, not a raw string." + ) # Verify the composed SQL structure: CREATE TYPE AS ENUM () parts = query_arg.seq assert parts[0] == psycopg2_sql.SQL("CREATE TYPE ") @@ -962,9 +959,9 @@ class TestPostgresEnumMigration: mock_cursor.execute.assert_called_once() query_arg = mock_cursor.execute.call_args[0][0] - assert isinstance( - query_arg, psycopg2_sql.Composable - ), "drop_enum_type must pass a psycopg2.sql.Composable, not a raw string." + assert isinstance(query_arg, psycopg2_sql.Composable), ( + "drop_enum_type must pass a psycopg2.sql.Composable, not a raw string." + ) # Verify the composed SQL structure: DROP TYPE parts = query_arg.seq assert parts[0] == psycopg2_sql.SQL("DROP TYPE ") diff --git a/api/src/backend/api/tests/test_decorators.py b/api/src/backend/api/tests/test_decorators.py index 2d09a40734..25053a2258 100644 --- a/api/src/backend/api/tests/test_decorators.py +++ b/api/src/backend/api/tests/test_decorators.py @@ -2,12 +2,11 @@ import uuid from unittest.mock import call, patch import pytest -from django.core.exceptions import ObjectDoesNotExist -from django.db import DatabaseError, IntegrityError - from api.db_utils import POSTGRES_TENANT_VAR, SET_CONFIG_QUERY from api.decorators import handle_provider_deletion, set_tenant from api.exceptions import ProviderDeletedException +from django.core.exceptions import ObjectDoesNotExist +from django.db import DatabaseError, IntegrityError @pytest.mark.django_db diff --git a/api/src/backend/api/tests/test_health.py b/api/src/backend/api/tests/test_health.py index 76b72c0dc7..f3a7bb34a4 100644 --- a/api/src/backend/api/tests/test_health.py +++ b/api/src/backend/api/tests/test_health.py @@ -7,15 +7,13 @@ Cover the IETF response envelope, status code mapping (200 / 503), the from unittest.mock import patch import pytest +from api import health from config import version as config_version from django.core.cache import cache from django.urls import reverse from rest_framework import status from rest_framework.test import APIClient -from api import health - - HEALTH_MEDIA_TYPE = "application/health+json" diff --git a/api/src/backend/api/tests/test_middleware.py b/api/src/backend/api/tests/test_middleware.py index 07165987de..08136dd68f 100644 --- a/api/src/backend/api/tests/test_middleware.py +++ b/api/src/backend/api/tests/test_middleware.py @@ -1,11 +1,10 @@ from unittest.mock import MagicMock, patch import pytest +from api.middleware import APILoggingMiddleware from django.http import HttpResponse from django.test import RequestFactory -from api.middleware import APILoggingMiddleware - @pytest.mark.django_db @patch("logging.getLogger") diff --git a/api/src/backend/api/tests/test_mixins.py b/api/src/backend/api/tests/test_mixins.py index 7daf9d5ff6..b90bcf4480 100644 --- a/api/src/backend/api/tests/test_mixins.py +++ b/api/src/backend/api/tests/test_mixins.py @@ -2,10 +2,6 @@ import json from uuid import uuid4 import pytest -from django_celery_results.models import TaskResult -from rest_framework import status -from rest_framework.response import Response - from api.exceptions import ( TaskFailedException, TaskInProgressException, @@ -14,6 +10,9 @@ from api.exceptions import ( from api.models import Task, User from api.rls import Tenant from api.v1.mixins import PaginateByPkMixin, TaskManagementMixin +from django_celery_results.models import TaskResult +from rest_framework import status +from rest_framework.response import Response @pytest.mark.django_db diff --git a/api/src/backend/api/tests/test_models.py b/api/src/backend/api/tests/test_models.py index b8b7f61dd1..3ec823b89a 100644 --- a/api/src/backend/api/tests/test_models.py +++ b/api/src/backend/api/tests/test_models.py @@ -1,10 +1,7 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime import pytest from allauth.socialaccount.models import SocialApp -from django.core.exceptions import ValidationError -from django.db import IntegrityError - from api.db_router import MainRouter from api.models import ( ProviderComplianceScore, @@ -16,6 +13,8 @@ from api.models import ( StatusChoices, TenantComplianceSummary, ) +from django.core.exceptions import ValidationError +from django.db import IntegrityError @pytest.mark.django_db @@ -376,7 +375,7 @@ class TestProviderComplianceScoreModel: def test_create_provider_compliance_score(self, providers_fixture, scans_fixture): provider = providers_fixture[0] scan = scans_fixture[0] - scan.completed_at = datetime.now(timezone.utc) + scan.completed_at = datetime.now(UTC) scan.save() score = ProviderComplianceScore.objects.create( @@ -398,7 +397,7 @@ class TestProviderComplianceScoreModel: ): provider = providers_fixture[0] scan = scans_fixture[0] - scan.completed_at = datetime.now(timezone.utc) + scan.completed_at = datetime.now(UTC) scan.save() ProviderComplianceScore.objects.create( @@ -427,12 +426,12 @@ class TestProviderComplianceScoreModel: ): provider1, provider2, *_ = providers_fixture scan1 = scans_fixture[0] - scan1.completed_at = datetime.now(timezone.utc) + scan1.completed_at = datetime.now(UTC) scan1.save() scan2 = scans_fixture[2] scan2.state = StateChoices.COMPLETED - scan2.completed_at = datetime.now(timezone.utc) + scan2.completed_at = datetime.now(UTC) scan2.save() score1 = ProviderComplianceScore.objects.create( diff --git a/api/src/backend/api/tests/test_rbac.py b/api/src/backend/api/tests/test_rbac.py index 684a9e44b9..4f787b1f5a 100644 --- a/api/src/backend/api/tests/test_rbac.py +++ b/api/src/backend/api/tests/test_rbac.py @@ -2,10 +2,6 @@ import json from unittest.mock import ANY, Mock, patch import pytest -from conftest import TEST_PASSWORD, TODAY -from django.urls import reverse -from rest_framework import status - from api.models import ( Membership, ProviderGroup, @@ -16,6 +12,9 @@ from api.models import ( UserRoleRelationship, ) from api.v1.serializers import TokenSerializer +from conftest import TEST_PASSWORD, TODAY +from django.urls import reverse +from rest_framework import status @pytest.mark.django_db diff --git a/api/src/backend/api/tests/test_serializers.py b/api/src/backend/api/tests/test_serializers.py index 5810a97b63..ea01075934 100644 --- a/api/src/backend/api/tests/test_serializers.py +++ b/api/src/backend/api/tests/test_serializers.py @@ -1,8 +1,7 @@ import pytest -from rest_framework.exceptions import ValidationError - from api.v1.serializer_utils.integrations import S3ConfigSerializer from api.v1.serializers import ImageProviderSecret +from rest_framework.exceptions import ValidationError class TestS3ConfigSerializer: diff --git a/api/src/backend/api/tests/test_sse.py b/api/src/backend/api/tests/test_sse.py index beba821e64..e6fdf21130 100644 --- a/api/src/backend/api/tests/test_sse.py +++ b/api/src/backend/api/tests/test_sse.py @@ -12,12 +12,11 @@ import uuid from unittest.mock import MagicMock import pytest -from django.http import StreamingHttpResponse -from rest_framework.test import APIRequestFactory, force_authenticate - from api.sse.base_views import BaseSSEViewSet from api.sse.channelmanager import SSEChannelManager from api.sse.utils import make_channel_name, tenant_id_from_channel +from django.http import StreamingHttpResponse +from rest_framework.test import APIRequestFactory, force_authenticate class TestMakeChannel: diff --git a/api/src/backend/api/tests/test_utils.py b/api/src/backend/api/tests/test_utils.py index 3fd9235dac..935a15c4f3 100644 --- a/api/src/backend/api/tests/test_utils.py +++ b/api/src/backend/api/tests/test_utils.py @@ -1,9 +1,7 @@ -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from unittest.mock import MagicMock, patch import pytest -from rest_framework.exceptions import NotFound, ValidationError - from api.db_router import MainRouter from api.exceptions import InvitationTokenExpiredException from api.models import Integration, Invitation, Provider @@ -35,6 +33,7 @@ from prowler.providers.okta.okta_provider import OktaProvider from prowler.providers.openstack.openstack_provider import OpenstackProvider from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider from prowler.providers.vercel.vercel_provider import VercelProvider +from rest_framework.exceptions import NotFound, ValidationError class TestMergeDicts: @@ -623,7 +622,7 @@ class TestValidateInvitation: invitation = MagicMock(spec=Invitation) invitation.token = "VALID_TOKEN" invitation.email = "user@example.com" - invitation.expires_at = datetime.now(timezone.utc) + timedelta(days=1) + invitation.expires_at = datetime.now(UTC) + timedelta(days=1) invitation.state = Invitation.State.PENDING invitation.tenant = MagicMock() return invitation @@ -671,7 +670,7 @@ class TestValidateInvitation: ) def test_invitation_expired(self, invitation): - expired_time = datetime.now(timezone.utc) - timedelta(days=1) + expired_time = datetime.now(UTC) - timedelta(days=1) invitation.expires_at = expired_time with ( @@ -680,7 +679,7 @@ class TestValidateInvitation: ): mock_db = mock_using.return_value mock_db.get.return_value = invitation - mock_datetime.now.return_value = datetime.now(timezone.utc) + mock_datetime.now.return_value = datetime.now(UTC) with pytest.raises(InvitationTokenExpiredException): validate_invitation("VALID_TOKEN", "user@example.com") @@ -725,7 +724,7 @@ class TestValidateInvitation: invitation = MagicMock(spec=Invitation) invitation.token = "VALID_TOKEN" invitation.email = uppercase_email - invitation.expires_at = datetime.now(timezone.utc) + timedelta(days=1) + invitation.expires_at = datetime.now(UTC) + timedelta(days=1) invitation.state = Invitation.State.PENDING invitation.tenant = MagicMock() diff --git a/api/src/backend/api/tests/test_uuid_utils.py b/api/src/backend/api/tests/test_uuid_utils.py index e202d087f3..a69d71cee9 100644 --- a/api/src/backend/api/tests/test_uuid_utils.py +++ b/api/src/backend/api/tests/test_uuid_utils.py @@ -1,23 +1,22 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from uuid import uuid4 import pytest +from api.uuid_utils import ( + datetime_from_uuid7, + datetime_to_uuid7, + transform_into_uuid7, + uuid7_end, + uuid7_range, + uuid7_start, +) from dateutil.relativedelta import relativedelta from rest_framework_json_api.serializers import ValidationError from uuid6 import UUID -from api.uuid_utils import ( - transform_into_uuid7, - datetime_to_uuid7, - datetime_from_uuid7, - uuid7_start, - uuid7_end, - uuid7_range, -) - def test_transform_into_uuid7_valid(): - uuid_v7 = datetime_to_uuid7(datetime.now(timezone.utc)) + uuid_v7 = datetime_to_uuid7(datetime.now(UTC)) transformed_uuid = transform_into_uuid7(uuid_v7) assert transformed_uuid == UUID(hex=uuid_v7.hex.upper()) assert transformed_uuid.version == 7 @@ -33,8 +32,8 @@ def test_transform_into_uuid7_invalid_version(): @pytest.mark.parametrize( "input_datetime", [ - datetime(2024, 9, 11, 7, 20, 27, tzinfo=timezone.utc), - datetime(2023, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + datetime(2024, 9, 11, 7, 20, 27, tzinfo=UTC), + datetime(2023, 1, 1, 0, 0, 0, tzinfo=UTC), ], ) def test_datetime_to_uuid7(input_datetime): @@ -48,8 +47,8 @@ def test_datetime_to_uuid7(input_datetime): @pytest.mark.parametrize( "input_datetime", [ - datetime(2024, 9, 11, 7, 20, 27, tzinfo=timezone.utc), - datetime(2023, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + datetime(2024, 9, 11, 7, 20, 27, tzinfo=UTC), + datetime(2023, 1, 1, 0, 0, 0, tzinfo=UTC), ], ) def test_datetime_from_uuid7(input_datetime): @@ -65,7 +64,7 @@ def test_datetime_from_uuid7_invalid(): def test_uuid7_start(): - dt = datetime.now(timezone.utc) + dt = datetime.now(UTC) uuid = datetime_to_uuid7(dt) start_uuid = uuid7_start(uuid) expected_dt = dt.replace(hour=0, minute=0, second=0, microsecond=0) @@ -76,7 +75,7 @@ def test_uuid7_start(): @pytest.mark.parametrize("months_offset", [0, 1, 10, 30, 60]) def test_uuid7_end(months_offset): - dt = datetime.now(timezone.utc) + dt = datetime.now(UTC) uuid = datetime_to_uuid7(dt) end_uuid = uuid7_end(uuid, months_offset) expected_dt = dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0) @@ -87,7 +86,7 @@ def test_uuid7_end(months_offset): def test_uuid7_range(): - dt_now = datetime.now(timezone.utc) + dt_now = datetime.now(UTC) uuid_list = [ datetime_to_uuid7(dt_now), datetime_to_uuid7(dt_now.replace(year=2023)), diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 613cef4755..4e8c9336b6 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -3,7 +3,7 @@ import io import json import os import tempfile -from datetime import date, datetime, timedelta, timezone +from datetime import UTC, date, datetime, timedelta from decimal import Decimal from pathlib import Path from types import SimpleNamespace @@ -15,31 +15,6 @@ import jwt import pytest from allauth.account.models import EmailAddress from allauth.socialaccount.models import SocialAccount, SocialApp -from botocore.exceptions import ClientError, NoCredentialsError -from conftest import ( - API_JSON_CONTENT_TYPE, - TEST_PASSWORD, - TEST_USER, - TODAY, - today_after_n_days, -) -from django.conf import settings -from django.db import connection -from django.db.models import Count -from django.http import JsonResponse -from django.test import RequestFactory -from django.test.utils import CaptureQueriesContext -from django.urls import reverse -from django_celery_results.models import TaskResult -from rest_framework import status -from rest_framework.exceptions import PermissionDenied -from rest_framework.response import Response -from rest_framework_simplejwt.token_blacklist.models import ( - BlacklistedToken, - OutstandingToken, -) -from rest_framework_simplejwt.tokens import RefreshToken - from api.attack_paths import ( AttackPathsQueryDefinition, AttackPathsQueryParameterDefinition, @@ -84,8 +59,32 @@ from api.models import ( from api.rls import Tenant from api.v1.serializers import TokenSerializer from api.v1.views import ComplianceOverviewViewSet, TenantFinishACSView +from botocore.exceptions import ClientError, NoCredentialsError +from conftest import ( + API_JSON_CONTENT_TYPE, + TEST_PASSWORD, + TEST_USER, + TODAY, + today_after_n_days, +) +from django.conf import settings +from django.db import connection +from django.db.models import Count +from django.http import JsonResponse +from django.test import RequestFactory +from django.test.utils import CaptureQueriesContext +from django.urls import reverse +from django_celery_results.models import TaskResult from prowler.lib.check.models import Severity from prowler.lib.outputs.finding import Status +from rest_framework import status +from rest_framework.exceptions import PermissionDenied +from rest_framework.response import Response +from rest_framework_simplejwt.token_blacklist.models import ( + BlacklistedToken, + OutstandingToken, +) +from rest_framework_simplejwt.tokens import RefreshToken class TestViewSet: @@ -1508,9 +1507,9 @@ class TestProviderViewSet: included_data = response.json()["included"] for expected_type in expected_resources: - assert any( - d.get("type") == expected_type for d in included_data - ), f"Expected type '{expected_type}' not found in included data" + assert any(d.get("type") == expected_type for d in included_data), ( + f"Expected type '{expected_type}' not found in included data" + ) def test_providers_retrieve(self, authenticated_client, providers_fixture): provider1, *_ = providers_fixture @@ -4288,11 +4287,11 @@ class TestScanViewSet: "Contents": [ { "Key": old_key, - "LastModified": datetime(2024, 1, 1, tzinfo=timezone.utc), + "LastModified": datetime(2024, 1, 1, tzinfo=UTC), }, { "Key": latest_key, - "LastModified": datetime(2024, 2, 2, tzinfo=timezone.utc), + "LastModified": datetime(2024, 2, 2, tzinfo=UTC), }, ] } @@ -4541,7 +4540,7 @@ class TestScanViewSet: ) # `inserted_at` is `auto_now_add`, and within the test transaction the DB # `now()` is constant, so force distinct timestamps to make order_by stable. - base = datetime(2024, 1, 1, tzinfo=timezone.utc) + base = datetime(2024, 1, 1, tzinfo=UTC) Task.objects.filter(pk=old_task.pk).update(inserted_at=base) Task.objects.filter(pk=new_task.pk).update( inserted_at=base + timedelta(hours=1) @@ -5785,13 +5784,13 @@ class TestAttackPathsScanViewSet: content_type=API_JSON_CONTENT_TYPE, ) if i < 10: - assert ( - response.status_code == status.HTTP_200_OK - ), f"Request {i + 1} should succeed with 200 OK, got {response.status_code}" + assert response.status_code == status.HTTP_200_OK, ( + f"Request {i + 1} should succeed with 200 OK, got {response.status_code}" + ) else: - assert ( - response.status_code == status.HTTP_429_TOO_MANY_REQUESTS - ), f"Request {i + 1} should be throttled" + assert response.status_code == status.HTTP_429_TOO_MANY_REQUESTS, ( + f"Request {i + 1} should be throttled" + ) # -- Timeout simulation ------------------------------------------------------- @@ -5994,9 +5993,9 @@ class TestResourceViewSet: included_data = response.json()["included"] for expected_type in expected_resources: - assert any( - d.get("type") == expected_type for d in included_data - ), f"Expected type '{expected_type}' not found in included data" + assert any(d.get("type") == expected_type for d in included_data), ( + f"Expected type '{expected_type}' not found in included data" + ) @pytest.mark.parametrize( "filter_name, filter_value, expected_count", @@ -6588,9 +6587,9 @@ class TestResourceViewSet: (e for e in errors if e["source"]["parameter"] == expected_invalid_param), None, ) - assert ( - error is not None - ), f"Expected error for parameter '{expected_invalid_param}'" + assert error is not None, ( + f"Expected error for parameter '{expected_invalid_param}'" + ) assert error["code"] == "invalid" assert error["status"] == "400" # Must be string per JSON:API spec assert expected_invalid_param in error["detail"] @@ -6979,9 +6978,8 @@ class TestResourceViewSet: This ensures the endpoint follows API conventions where missing authentication returns 401 Unauthorized, not 404 Not Found. """ - from rest_framework.test import APIClient - from api.models import Resource + from rest_framework.test import APIClient aws_provider = providers_fixture[0] # AWS provider from fixture @@ -7054,9 +7052,8 @@ class TestResourceViewSet: This ensures authentication errors are properly distinguished from resource not found errors. """ - from rest_framework.test import APIClient - from api.models import Resource + from rest_framework.test import APIClient aws_provider = providers_fixture[0] @@ -7074,9 +7071,8 @@ class TestResourceViewSet: tenant = tenants_fixture[0] expired_payload = { "token_type": "access", - "exp": datetime.now(timezone.utc) - - timedelta(hours=1), # Expired 1 hour ago - "iat": datetime.now(timezone.utc) - timedelta(hours=2), + "exp": datetime.now(UTC) - timedelta(hours=1), # Expired 1 hour ago + "iat": datetime.now(UTC) - timedelta(hours=2), "jti": str(uuid4()), "user_id": str(uuid4()), "tenant_id": str(tenant.id), @@ -7101,9 +7097,8 @@ class TestResourceViewSet: Malformed or invalid tokens should return 401 Unauthorized, not 404 Not Found. """ - from rest_framework.test import APIClient - from api.models import Resource + from rest_framework.test import APIClient aws_provider = providers_fixture[0] @@ -7122,16 +7117,16 @@ class TestResourceViewSet: # Test with completely malformed token client.credentials(HTTP_AUTHORIZATION="Bearer not.a.valid.jwt.token") response = client.get(reverse("resource-events", kwargs={"pk": resource.id})) - assert ( - response.status_code == status.HTTP_401_UNAUTHORIZED - ), f"Expected 401 for malformed token but got {response.status_code}" + assert response.status_code == status.HTTP_401_UNAUTHORIZED, ( + f"Expected 401 for malformed token but got {response.status_code}" + ) # Test with empty bearer token client.credentials(HTTP_AUTHORIZATION="Bearer ") response = client.get(reverse("resource-events", kwargs={"pk": resource.id})) - assert ( - response.status_code == status.HTTP_401_UNAUTHORIZED - ), f"Expected 401 for empty bearer token but got {response.status_code}" + assert response.status_code == status.HTTP_401_UNAUTHORIZED, ( + f"Expected 401 for empty bearer token but got {response.status_code}" + ) @pytest.mark.django_db @@ -7266,9 +7261,9 @@ class TestFindingViewSet: included_data = response.json()["included"] for expected_type in expected_resources: - assert any( - d.get("type") == expected_type for d in included_data - ), f"Expected type '{expected_type}' not found in included data" + assert any(d.get("type") == expected_type for d in included_data), ( + f"Expected type '{expected_type}' not found in included data" + ) @pytest.mark.parametrize( "filter_name, filter_value, expected_count", @@ -7867,9 +7862,9 @@ class TestJWTFields: reverse("token-obtain"), data, format="json" ) - assert ( - response.status_code == status.HTTP_200_OK - ), f"Unexpected status code: {response.status_code}" + assert response.status_code == status.HTTP_200_OK, ( + f"Unexpected status code: {response.status_code}" + ) access_token = response.data["attributes"]["access"] payload = jwt.decode(access_token, options={"verify_signature": False}) @@ -7883,28 +7878,28 @@ class TestJWTFields: # Verify expected fields for field in expected_fields: assert field in payload, f"The field '{field}' is not in the JWT" - assert ( - payload[field] == expected_fields[field] - ), f"The value of '{field}' does not match" + assert payload[field] == expected_fields[field], ( + f"The value of '{field}' does not match" + ) # Verify time fields are integers for time_field in ["exp", "iat", "nbf"]: assert time_field in payload, f"The field '{time_field}' is not in the JWT" - assert isinstance( - payload[time_field], int - ), f"The field '{time_field}' is not an integer" + assert isinstance(payload[time_field], int), ( + f"The field '{time_field}' is not an integer" + ) # Verify identification fields are non-empty strings for id_field in ["jti", "sub", "tenant_id"]: assert id_field in payload, f"The field '{id_field}' is not in the JWT" - assert ( - isinstance(payload[id_field], str) and payload[id_field] - ), f"The field '{id_field}' is not a valid string" + assert isinstance(payload[id_field], str) and payload[id_field], ( + f"The field '{id_field}' is not a valid string" + ) @pytest.mark.django_db class TestInvitationViewSet: - TOMORROW = datetime.now(timezone.utc) + timedelta(days=1, hours=1) + TOMORROW = datetime.now(UTC) + timedelta(days=1, hours=1) TOMORROW_ISO = TOMORROW.isoformat() def test_invitations_list(self, authenticated_client, invitations_fixture): @@ -8027,9 +8022,7 @@ class TestInvitationViewSet: "type": "invitations", "attributes": { "email": "thisisarandomemail@prowler.com", - "expires_at": ( - datetime.now(timezone.utc) + timedelta(hours=23) - ).isoformat(), + "expires_at": (datetime.now(UTC) + timedelta(hours=23)).isoformat(), }, } } @@ -8056,7 +8049,7 @@ class TestInvitationViewSet: invitation, *_ = invitations_fixture role1, role2, *_ = roles_fixture new_email = "new_email@prowler.com" - new_expires_at = datetime.now(timezone.utc) + timedelta(days=7) + new_expires_at = datetime.now(UTC) + timedelta(days=7) new_expires_at_iso = new_expires_at.isoformat() data = { "data": { @@ -8143,9 +8136,7 @@ class TestInvitationViewSet: "id": str(invitation.id), "type": "invitations", "attributes": { - "expires_at": ( - datetime.now(timezone.utc) + timedelta(hours=23) - ).isoformat(), + "expires_at": (datetime.now(UTC) + timedelta(hours=23)).isoformat(), }, } } @@ -8318,7 +8309,7 @@ class TestInvitationViewSet: self, authenticated_client, invitations_fixture ): invitation, *_ = invitations_fixture - invitation.expires_at = datetime.now(timezone.utc) - timedelta(days=1) + invitation.expires_at = datetime.now(UTC) - timedelta(days=1) invitation.email = TEST_USER invitation.save() @@ -8337,7 +8328,7 @@ class TestInvitationViewSet: ): new_email = "new_email@prowler.com" invitation, *_ = invitations_fixture - invitation.expires_at = datetime.now(timezone.utc) - timedelta(days=1) + invitation.expires_at = datetime.now(UTC) - timedelta(days=1) invitation.email = new_email invitation.save() @@ -9433,8 +9424,8 @@ class TestComplianceOverviewViewSet: trigger=Scan.TriggerChoices.MANUAL, state=StateChoices.COMPLETED, tenant_id=provider.tenant_id, - started_at=datetime.now(timezone.utc), - completed_at=datetime.now(timezone.utc), + started_at=datetime.now(UTC), + completed_at=datetime.now(UTC), ) def _create_requirement( @@ -10122,7 +10113,7 @@ class TestComplianceOverviewViewSet: assert gcp_provider.provider == Provider.ProviderChoices.GCP.value assert azure_provider.provider == Provider.ProviderChoices.AZURE.value - now = datetime.now(timezone.utc) + now = datetime.now(UTC) gcp_scan = Scan.objects.create( name="gcp scan", provider=gcp_provider, @@ -10251,7 +10242,7 @@ class TestComplianceOverviewViewSet: provider_group=provider_group, ) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) allowed_scan = Scan.objects.create( name="allowed scan", provider=allowed_provider, @@ -10603,7 +10594,7 @@ class TestOverviewViewSet: assert denied_provider.provider not in aggregated def _create_scan(self, tenant, provider, name, started_at=None): - scan_started = started_at or datetime.now(timezone.utc) - timedelta(hours=1) + scan_started = started_at or datetime.now(UTC) - timedelta(hours=1) return Scan.objects.create( tenant=tenant, provider=provider, @@ -10746,8 +10737,8 @@ class TestOverviewViewSet: failed_findings=35, ) - older_inserted = datetime(2025, 1, 1, 12, 0, tzinfo=timezone.utc) - newer_inserted = datetime(2025, 1, 2, 12, 0, tzinfo=timezone.utc) + older_inserted = datetime(2025, 1, 1, 12, 0, tzinfo=UTC) + newer_inserted = datetime(2025, 1, 2, 12, 0, tzinfo=UTC) ThreatScoreSnapshot.objects.filter(id=snapshot1.id).update( inserted_at=older_inserted ) @@ -11366,7 +11357,7 @@ class TestOverviewViewSet: trigger=Scan.TriggerChoices.MANUAL, state=StateChoices.COMPLETED, tenant=tenant, - completed_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc), + completed_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=UTC), ) # Create scan for day 3 @@ -11376,7 +11367,7 @@ class TestOverviewViewSet: trigger=Scan.TriggerChoices.MANUAL, state=StateChoices.COMPLETED, tenant=tenant, - completed_at=datetime(2024, 1, 3, 12, 0, 0, tzinfo=timezone.utc), + completed_at=datetime(2024, 1, 3, 12, 0, 0, tzinfo=UTC), ) # Create DailySeveritySummary for day 1 @@ -11449,7 +11440,7 @@ class TestOverviewViewSet: trigger=Scan.TriggerChoices.MANUAL, state=StateChoices.COMPLETED, tenant=tenant, - completed_at=datetime(2024, 2, 1, 12, 0, 0, tzinfo=timezone.utc), + completed_at=datetime(2024, 2, 1, 12, 0, 0, tzinfo=UTC), ) scan2 = Scan.objects.create( name="severity-over-time-scan-p2", @@ -11457,7 +11448,7 @@ class TestOverviewViewSet: trigger=Scan.TriggerChoices.MANUAL, state=StateChoices.COMPLETED, tenant=tenant, - completed_at=datetime(2024, 2, 1, 14, 0, 0, tzinfo=timezone.utc), + completed_at=datetime(2024, 2, 1, 14, 0, 0, tzinfo=UTC), ) # Create DailySeveritySummary for provider1 @@ -11521,7 +11512,7 @@ class TestOverviewViewSet: trigger=Scan.TriggerChoices.MANUAL, state=StateChoices.COMPLETED, tenant=tenant, - completed_at=datetime(2024, 3, 1, 12, 0, 0, tzinfo=timezone.utc), + completed_at=datetime(2024, 3, 1, 12, 0, 0, tzinfo=UTC), ) scan2 = Scan.objects.create( name="severity-over-time-filter-scan-p2", @@ -11529,7 +11520,7 @@ class TestOverviewViewSet: trigger=Scan.TriggerChoices.MANUAL, state=StateChoices.COMPLETED, tenant=tenant, - completed_at=datetime(2024, 3, 1, 14, 0, 0, tzinfo=timezone.utc), + completed_at=datetime(2024, 3, 1, 14, 0, 0, tzinfo=UTC), ) # Provider 1 - critical=100 @@ -12658,9 +12649,9 @@ class TestIntegrationViewSet: included_data = response.json()["included"] for expected_type in expected_resources: - assert any( - d.get("type") == expected_type for d in included_data - ), f"Expected type '{expected_type}' not found in included data" + assert any(d.get("type") == expected_type for d in included_data), ( + f"Expected type '{expected_type}' not found in included data" + ) @pytest.mark.parametrize( "integration_type, configuration, credentials", @@ -13325,7 +13316,7 @@ class TestSAMLTokenValidation: saml_token = SAMLToken.objects.create( token=valid_token_data, user=user, - expires_at=datetime.now(timezone.utc) + timedelta(seconds=10), + expires_at=datetime.now(UTC) + timedelta(seconds=10), ) url = reverse("token-saml") @@ -13351,7 +13342,7 @@ class TestSAMLTokenValidation: saml_token = SAMLToken.objects.create( token=expired_token_data, user=user, - expires_at=datetime.now(timezone.utc) - timedelta(seconds=1), + expires_at=datetime.now(UTC) - timedelta(seconds=1), ) url = reverse("token-saml") @@ -13370,7 +13361,7 @@ class TestSAMLTokenValidation: saml_token = SAMLToken.objects.create( token=token_data, user=user, - expires_at=datetime.now(timezone.utc) + timedelta(seconds=10), + expires_at=datetime.now(UTC) + timedelta(seconds=10), ) url = reverse("token-saml") @@ -14368,9 +14359,9 @@ class TestLighthouseConfigViewSet: ) # Check that API key is masked with asterisks only masked_api_key = data["attributes"]["api_key"] - assert all( - c == "*" for c in masked_api_key - ), "API key should contain only asterisks" + assert all(c == "*" for c in masked_api_key), ( + "API key should contain only asterisks" + ) @pytest.mark.parametrize( "field_name, invalid_value", @@ -18159,9 +18150,9 @@ class TestFindingGroupViewSet: assert len(data) == 2 for item in data: resource = item["attributes"]["resource"] - assert ( - resource["resource_group"] == "storage" - ), "resource_group must be 'storage'" + assert resource["resource_group"] == "storage", ( + "resource_group must be 'storage'" + ) def test_resources_name_icontains( self, authenticated_client, finding_groups_fixture @@ -18475,12 +18466,12 @@ class TestFindingGroupViewSet: assert response_p1.status_code == status.HTTP_200_OK p1_check_ids = {item["id"] for item in response_p1.json()["data"]} # Provider1 has scan1 with 4 checks - assert ( - len(p1_check_ids) == 4 - ), f"Provider1 should have 4 checks, got {len(p1_check_ids)}" - assert ( - "cloudtrail_enabled" not in p1_check_ids - ), "cloudtrail_enabled should NOT be in provider1" + assert len(p1_check_ids) == 4, ( + f"Provider1 should have 4 checks, got {len(p1_check_ids)}" + ) + assert "cloudtrail_enabled" not in p1_check_ids, ( + "cloudtrail_enabled should NOT be in provider1" + ) # Get finding groups for provider2 only response_p2 = authenticated_client.get( @@ -18490,12 +18481,12 @@ class TestFindingGroupViewSet: assert response_p2.status_code == status.HTTP_200_OK p2_check_ids = {item["id"] for item in response_p2.json()["data"]} # Provider2 has scan2 with 1 check - assert ( - len(p2_check_ids) == 1 - ), f"Provider2 should have 1 check, got {len(p2_check_ids)}" - assert ( - "cloudtrail_enabled" in p2_check_ids - ), "cloudtrail_enabled should be in provider2" + assert len(p2_check_ids) == 1, ( + f"Provider2 should have 1 check, got {len(p2_check_ids)}" + ) + assert "cloudtrail_enabled" in p2_check_ids, ( + "cloudtrail_enabled should be in provider2" + ) # Test provider_type filter actually filters data def test_finding_groups_provider_type_filter_actually_filters( @@ -18518,9 +18509,9 @@ class TestFindingGroupViewSet: {"filter[inserted_at]": TODAY, "filter[provider_type]": "gcp"}, ) assert response_gcp.status_code == status.HTTP_200_OK - assert ( - len(response_gcp.json()["data"]) == 0 - ), "GCP filter should return 0 results" + assert len(response_gcp.json()["data"]) == 0, ( + "GCP filter should return 0 results" + ) def test_finding_groups_pagination( self, authenticated_client, finding_groups_fixture @@ -18786,7 +18777,7 @@ class TestFindingGroupViewSet: provider=provider1, state=StateChoices.COMPLETED, trigger=Scan.TriggerChoices.MANUAL, - completed_at=datetime.now(timezone.utc), + completed_at=datetime.now(UTC), ) latest_scan_provider2 = Scan.objects.create( @@ -18794,7 +18785,7 @@ class TestFindingGroupViewSet: provider=provider2, state=StateChoices.COMPLETED, trigger=Scan.TriggerChoices.MANUAL, - completed_at=datetime.now(timezone.utc), + completed_at=datetime.now(UTC), ) older_scan_provider1 = Scan.objects.create( @@ -18802,7 +18793,7 @@ class TestFindingGroupViewSet: provider=provider1, state=StateChoices.COMPLETED, trigger=Scan.TriggerChoices.MANUAL, - completed_at=datetime.now(timezone.utc) - timedelta(days=1), + completed_at=datetime.now(UTC) - timedelta(days=1), ) # Older scan — these should be excluded from /latest @@ -18816,7 +18807,7 @@ class TestFindingGroupViewSet: impact="high", check_id=check_id, check_metadata={"CheckId": check_id, "checktitle": "Cross provider check"}, - first_seen_at=datetime.now(timezone.utc) - timedelta(days=2), + first_seen_at=datetime.now(UTC) - timedelta(days=2), muted=False, ) @@ -18831,7 +18822,7 @@ class TestFindingGroupViewSet: impact="high", check_id=check_id, check_metadata={"CheckId": check_id, "checktitle": "Cross provider check"}, - first_seen_at=datetime.now(timezone.utc) - timedelta(hours=1), + first_seen_at=datetime.now(UTC) - timedelta(hours=1), muted=False, ) latest_p1_pass.add_resources([resource1]) @@ -18846,7 +18837,7 @@ class TestFindingGroupViewSet: impact="high", check_id=check_id, check_metadata={"CheckId": check_id, "checktitle": "Cross provider check"}, - first_seen_at=datetime.now(timezone.utc) - timedelta(hours=1), + first_seen_at=datetime.now(UTC) - timedelta(hours=1), muted=False, ) latest_p1_fail.add_resources([resource2]) @@ -18862,7 +18853,7 @@ class TestFindingGroupViewSet: impact="high", check_id=check_id, check_metadata={"CheckId": check_id, "checktitle": "Cross provider check"}, - first_seen_at=datetime.now(timezone.utc) - timedelta(hours=1), + first_seen_at=datetime.now(UTC) - timedelta(hours=1), muted=False, ) latest_p2.add_resources([resource3]) @@ -19396,7 +19387,7 @@ class TestFindingGroupViewSet: resource = resources_fixture[0] check_id = "overlap_regression_check" - t0 = datetime.now(timezone.utc) - timedelta(hours=5) + t0 = datetime.now(UTC) - timedelta(hours=5) t1 = t0 + timedelta(hours=1) t1_end = t1 + timedelta(minutes=30) t2 = t0 + timedelta(hours=4) diff --git a/api/src/backend/api/utils.py b/api/src/backend/api/utils.py index 678ed24772..ce1dc0f10d 100644 --- a/api/src/backend/api/utils.py +++ b/api/src/backend/api/utils.py @@ -1,22 +1,21 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import TYPE_CHECKING from allauth.socialaccount.providers.oauth2.client import OAuth2Client -from django.contrib.postgres.aggregates import ArrayAgg -from django.db.models import Subquery -from rest_framework.exceptions import NotFound, ValidationError - from api.db_router import MainRouter from api.db_utils import rls_transaction from api.exceptions import InvitationTokenExpiredException from api.models import Integration, Invitation, Processor, Provider, Resource from api.v1.serializers import FindingMetadataSerializer +from django.contrib.postgres.aggregates import ArrayAgg +from django.db.models import Subquery from prowler.lib.outputs.jira.jira import Jira, JiraBasicAuthError from prowler.providers.aws.lib.s3.s3 import S3 from prowler.providers.aws.lib.security_hub.security_hub import SecurityHub from prowler.providers.common.models import Connection +from rest_framework.exceptions import NotFound, ValidationError if TYPE_CHECKING: from prowler.providers.alibabacloud.alibabacloud_provider import ( @@ -442,8 +441,8 @@ def prowler_integration_connection_test(integration: Integration) -> Connection: # Only save regions if connection is successful if connection.is_connected: - regions_status = {r: True for r in connection.enabled_regions} - regions_status.update({r: False for r in connection.disabled_regions}) + regions_status = dict.fromkeys(connection.enabled_regions, True) + regions_status.update(dict.fromkeys(connection.disabled_regions, False)) # Save regions information in the integration configuration integration.configuration["regions"] = regions_status @@ -525,7 +524,7 @@ def validate_invitation( raise ValidationError({"invitation_token": "Invalid invitation code."}) # Check if the invitation has expired - if invitation.expires_at < datetime.now(timezone.utc): + if invitation.expires_at < datetime.now(UTC): invitation.state = Invitation.State.EXPIRED invitation.save(using=MainRouter.admin_db) raise InvitationTokenExpiredException() @@ -596,6 +595,6 @@ def initialize_prowler_integration(integration: Integration) -> Jira: with rls_transaction(str(integration.tenant_id)): integration.configuration["projects"] = {} integration.connected = False - integration.connection_last_checked_at = datetime.now(tz=timezone.utc) + integration.connection_last_checked_at = datetime.now(tz=UTC) integration.save() raise jira_auth_error diff --git a/api/src/backend/api/uuid_utils.py b/api/src/backend/api/uuid_utils.py index b1f33432ff..4b2c01d8b0 100644 --- a/api/src/backend/api/uuid_utils.py +++ b/api/src/backend/api/uuid_utils.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from random import getrandbits from dateutil.relativedelta import relativedelta @@ -81,7 +81,7 @@ def datetime_from_uuid7(uuid7: UUID) -> datetime: A datetime object representing the timestamp encoded in the UUIDv7. """ timestamp_ms = uuid7.time - return datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc) + return datetime.fromtimestamp(timestamp_ms / 1000, tz=UTC) def uuid7_start(uuid_obj: UUID) -> UUID: diff --git a/api/src/backend/api/v1/mixins.py b/api/src/backend/api/v1/mixins.py index 849c788366..7645c92f4c 100644 --- a/api/src/backend/api/v1/mixins.py +++ b/api/src/backend/api/v1/mixins.py @@ -1,12 +1,5 @@ import uuid -from django.http import QueryDict -from django.urls import reverse -from django_celery_results.models import TaskResult -from rest_framework import status -from rest_framework.exceptions import ValidationError -from rest_framework.response import Response - from api.exceptions import ( TaskFailedException, TaskInProgressException, @@ -14,6 +7,12 @@ from api.exceptions import ( ) from api.models import Provider, StateChoices, Task from api.v1.serializers import TaskSerializer +from django.http import QueryDict +from django.urls import reverse +from django_celery_results.models import TaskResult +from rest_framework import status +from rest_framework.exceptions import ValidationError +from rest_framework.response import Response class DisablePaginationMixin: diff --git a/api/src/backend/api/v1/serializer_utils/integrations.py b/api/src/backend/api/v1/serializer_utils/integrations.py index aaa0f4aa31..a77de9c237 100644 --- a/api/src/backend/api/v1/serializer_utils/integrations.py +++ b/api/src/backend/api/v1/serializer_utils/integrations.py @@ -1,11 +1,10 @@ import os import re +from api.v1.serializer_utils.base import BaseValidateSerializer from drf_spectacular.utils import extend_schema_field from rest_framework_json_api import serializers -from api.v1.serializer_utils.base import BaseValidateSerializer - class S3ConfigSerializer(BaseValidateSerializer): bucket_name = serializers.CharField() diff --git a/api/src/backend/api/v1/serializer_utils/processors.py b/api/src/backend/api/v1/serializer_utils/processors.py index 4022f3f2bc..ee53aa8ccf 100644 --- a/api/src/backend/api/v1/serializer_utils/processors.py +++ b/api/src/backend/api/v1/serializer_utils/processors.py @@ -1,7 +1,5 @@ -from drf_spectacular.utils import extend_schema_field - from api.v1.serializer_utils.base import YamlOrJsonField - +from drf_spectacular.utils import extend_schema_field from prowler.lib.mutelist.mutelist import mutelist_schema diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index 03d2fecad2..1d160b4048 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -1,23 +1,6 @@ import base64 import json -from datetime import datetime, timedelta, timezone - -from django.conf import settings -from django.contrib.auth import authenticate -from django.contrib.auth.models import update_last_login -from django.contrib.auth.password_validation import validate_password -from django.core.exceptions import ValidationError as DjangoValidationError -from django.db import IntegrityError -from drf_spectacular.utils import extend_schema_field -from jwt.exceptions import InvalidKeyError -from rest_framework.reverse import reverse -from rest_framework.validators import UniqueTogetherValidator -from rest_framework_json_api import serializers -from rest_framework_json_api.relations import SerializerMethodResourceRelatedField -from rest_framework_json_api.serializers import ValidationError -from rest_framework_simplejwt.exceptions import TokenError -from rest_framework_simplejwt.serializers import TokenObtainPairSerializer -from rest_framework_simplejwt.tokens import RefreshToken +from datetime import UTC, datetime, timedelta from api.db_router import MainRouter from api.exceptions import ConflictException @@ -72,7 +55,23 @@ from api.v1.serializer_utils.lighthouse import ( ) from api.v1.serializer_utils.processors import ProcessorConfigField from api.v1.serializer_utils.providers import ProviderSecretField +from django.conf import settings +from django.contrib.auth import authenticate +from django.contrib.auth.models import update_last_login +from django.contrib.auth.password_validation import validate_password +from django.core.exceptions import ValidationError as DjangoValidationError +from django.db import IntegrityError +from drf_spectacular.utils import extend_schema_field +from jwt.exceptions import InvalidKeyError from prowler.lib.mutelist.mutelist import Mutelist +from rest_framework.reverse import reverse +from rest_framework.validators import UniqueTogetherValidator +from rest_framework_json_api import serializers +from rest_framework_json_api.relations import SerializerMethodResourceRelatedField +from rest_framework_json_api.serializers import ValidationError +from rest_framework_simplejwt.exceptions import TokenError +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer +from rest_framework_simplejwt.tokens import RefreshToken # Base @@ -1981,7 +1980,7 @@ class InvitationBaseWriteSerializer(BaseWriteSerializer): return value def validate_expires_at(self, value): - now = datetime.now(timezone.utc) + now = datetime.now(UTC) if value and value < now + timedelta(hours=24): raise ValidationError( "Expiry date must be at least 24 hours in the future." diff --git a/api/src/backend/api/v1/urls.py b/api/src/backend/api/v1/urls.py index 533106d0e4..b53fe1c817 100644 --- a/api/src/backend/api/v1/urls.py +++ b/api/src/backend/api/v1/urls.py @@ -1,10 +1,4 @@ from allauth.socialaccount.providers.saml.views import ACSView, MetadataView, SLSView -from django.http import JsonResponse -from django.urls import include, path -from django.views.decorators.csrf import csrf_exempt -from drf_spectacular.views import SpectacularRedocView -from rest_framework_nested import routers - from api.v1.views import ( AttackPathsScanViewSet, ComplianceOverviewViewSet, @@ -49,6 +43,11 @@ from api.v1.views import ( UserRoleRelationshipView, UserViewSet, ) +from django.http import JsonResponse +from django.urls import include, path +from django.views.decorators.csrf import csrf_exempt +from drf_spectacular.views import SpectacularRedocView +from rest_framework_nested import routers # This helper view is used to block any endpoints that should not be available diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index 6b2616b289..89c2f344a2 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -7,7 +7,7 @@ import time import uuid from collections import defaultdict from copy import deepcopy -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from decimal import ROUND_HALF_UP, Decimal, InvalidOperation from urllib.parse import urljoin @@ -16,100 +16,6 @@ from allauth.socialaccount.models import SocialAccount, SocialApp from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter from allauth.socialaccount.providers.saml.views import FinishACSView, LoginView -from botocore.exceptions import ClientError, NoCredentialsError, ParamValidationError -from celery import chain, states -from celery.result import AsyncResult -from config.custom_logging import BackendLogger -from config.env import env -from config.version import RELEASE_ID -from config.settings.social_login import ( - GITHUB_OAUTH_CALLBACK_URL, - GOOGLE_OAUTH_CALLBACK_URL, -) -from dj_rest_auth.registration.views import SocialLoginView -from django.conf import settings as django_settings -from django.contrib.postgres.aggregates import ArrayAgg, BoolAnd, StringAgg -from django.contrib.postgres.search import SearchQuery -from django.core.exceptions import ValidationError as DjangoValidationError -from django.db import transaction -from django.db.models import ( - BooleanField, - Case, - CharField, - Count, - DecimalField, - Exists, - ExpressionWrapper, - F, - IntegerField, - Max, - Min, - OuterRef, - Prefetch, - Q, - QuerySet, - Subquery, - Sum, - Value, - When, - Window, -) -from django.db.models.fields.json import KeyTextTransform -from django.db.models.functions import Cast, Coalesce, RowNumber -from django.http import HttpResponse, HttpResponseBase, HttpResponseRedirect, QueryDict -from django.shortcuts import redirect -from django.urls import reverse -from django.utils.dateparse import parse_date -from django.utils.decorators import method_decorator -from django.views.decorators.cache import cache_control -from django_celery_beat.models import PeriodicTask -from django_celery_results.models import TaskResult -from drf_spectacular.settings import spectacular_settings -from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import ( - OpenApiParameter, - OpenApiResponse, - extend_schema, - extend_schema_view, -) -from drf_spectacular.views import SpectacularAPIView -from drf_spectacular_jsonapi.schemas.openapi import JsonApiAutoSchema -from rest_framework import permissions, status -from rest_framework.decorators import action -from rest_framework.exceptions import ( - MethodNotAllowed, - NotFound, - PermissionDenied, - ValidationError, -) -from rest_framework.generics import GenericAPIView, get_object_or_404 -from rest_framework.permissions import SAFE_METHODS -from rest_framework_json_api import filters as jsonapi_filters -from rest_framework_json_api.views import RelationshipView, Response -from rest_framework_simplejwt.exceptions import InvalidToken, TokenError -from rest_framework_simplejwt.token_blacklist.models import ( - BlacklistedToken, - OutstandingToken, -) -from tasks.beat import schedule_provider_scan -from tasks.jobs.attack_paths import db_utils as attack_paths_db_utils -from tasks.jobs.export import get_s3_client -from tasks.tasks import ( - backfill_compliance_summaries_task, - backfill_scan_resource_summaries_task, - check_integration_connection_task, - check_lighthouse_connection_task, - check_lighthouse_provider_connection_task, - check_provider_connection_task, - delete_provider_task, - delete_tenant_task, - jira_integration_task, - mute_historical_findings_task, - perform_scan_task, - reaggregate_all_finding_group_summaries_task, - refresh_lighthouse_provider_models_task, -) - from api.attack_paths import database as graph_database from api.attack_paths import get_queries_for_provider, get_query_by_id from api.attack_paths import views_helpers as attack_paths_views_helpers @@ -328,6 +234,64 @@ from api.v1.serializers import ( UserSerializer, UserUpdateSerializer, ) +from botocore.exceptions import ClientError, NoCredentialsError, ParamValidationError +from celery import chain, states +from celery.result import AsyncResult +from config.custom_logging import BackendLogger +from config.env import env +from config.settings.social_login import ( + GITHUB_OAUTH_CALLBACK_URL, + GOOGLE_OAUTH_CALLBACK_URL, +) +from config.version import RELEASE_ID +from dj_rest_auth.registration.views import SocialLoginView +from django.conf import settings as django_settings +from django.contrib.postgres.aggregates import ArrayAgg, BoolAnd, StringAgg +from django.contrib.postgres.search import SearchQuery +from django.core.exceptions import ValidationError as DjangoValidationError +from django.db import transaction +from django.db.models import ( + BooleanField, + Case, + CharField, + Count, + DecimalField, + Exists, + ExpressionWrapper, + F, + IntegerField, + Max, + Min, + OuterRef, + Prefetch, + Q, + QuerySet, + Subquery, + Sum, + Value, + When, + Window, +) +from django.db.models.fields.json import KeyTextTransform +from django.db.models.functions import Cast, Coalesce, RowNumber +from django.http import HttpResponse, HttpResponseBase, HttpResponseRedirect, QueryDict +from django.shortcuts import redirect +from django.urls import reverse +from django.utils.dateparse import parse_date +from django.utils.decorators import method_decorator +from django.views.decorators.cache import cache_control +from django_celery_beat.models import PeriodicTask +from django_celery_results.models import TaskResult +from drf_spectacular.settings import spectacular_settings +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import ( + OpenApiParameter, + OpenApiResponse, + extend_schema, + extend_schema_view, +) +from drf_spectacular.views import SpectacularAPIView +from drf_spectacular_jsonapi.schemas.openapi import JsonApiAutoSchema from prowler.providers.aws.exceptions.exceptions import ( AWSAssumeRoleError, AWSCredentialsError, @@ -335,6 +299,41 @@ from prowler.providers.aws.exceptions.exceptions import ( from prowler.providers.aws.lib.cloudtrail_timeline.cloudtrail_timeline import ( CloudTrailTimeline, ) +from rest_framework import permissions, status +from rest_framework.decorators import action +from rest_framework.exceptions import ( + MethodNotAllowed, + NotFound, + PermissionDenied, + ValidationError, +) +from rest_framework.generics import GenericAPIView, get_object_or_404 +from rest_framework.permissions import SAFE_METHODS +from rest_framework_json_api import filters as jsonapi_filters +from rest_framework_json_api.views import RelationshipView, Response +from rest_framework_simplejwt.exceptions import InvalidToken, TokenError +from rest_framework_simplejwt.token_blacklist.models import ( + BlacklistedToken, + OutstandingToken, +) +from tasks.beat import schedule_provider_scan +from tasks.jobs.attack_paths import db_utils as attack_paths_db_utils +from tasks.jobs.export import get_s3_client +from tasks.tasks import ( + backfill_compliance_summaries_task, + backfill_scan_resource_summaries_task, + check_integration_connection_task, + check_lighthouse_connection_task, + check_lighthouse_provider_connection_task, + check_provider_connection_task, + delete_provider_task, + delete_tenant_task, + jira_integration_task, + mute_historical_findings_task, + perform_scan_task, + reaggregate_all_finding_group_summaries_task, + refresh_lighthouse_provider_models_task, +) logger = logging.getLogger(BackendLogger.API) @@ -3350,9 +3349,7 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet): date_filters = {} if exact: date = parse_date(exact) - datetime_start = datetime.combine( - date, datetime.min.time(), tzinfo=timezone.utc - ) + datetime_start = datetime.combine(date, datetime.min.time(), tzinfo=UTC) datetime_end = datetime_start + timedelta(days=1) date_filters["scan_id__gte"] = uuid7_start( datetime_to_uuid7(datetime_start) @@ -3364,7 +3361,7 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet): if gte: date_start = parse_date(gte) datetime_start = datetime.combine( - date_start, datetime.min.time(), tzinfo=timezone.utc + date_start, datetime.min.time(), tzinfo=UTC ) date_filters["scan_id__gte"] = uuid7_start( datetime_to_uuid7(datetime_start) @@ -3374,7 +3371,7 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet): datetime_end = datetime.combine( date_end + timedelta(days=1), datetime.min.time(), - tzinfo=timezone.utc, + tzinfo=UTC, ) date_filters["scan_id__lt"] = uuid7_start( datetime_to_uuid7(datetime_end) @@ -3418,7 +3415,7 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet): groups__isnull=False, ).values_list("groups", flat=True) groups = sorted( - set(g for groups_list in all_groups if groups_list for g in groups_list) + {g for groups_list in all_groups if groups_list for g in groups_list} ) result = { @@ -3488,7 +3485,7 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet): groups__isnull=False, ).values_list("groups", flat=True) groups = sorted( - set(g for groups_list in all_groups if groups_list for g in groups_list) + {g for groups_list in all_groups if groups_list for g in groups_list} ) result = { @@ -3915,9 +3912,7 @@ class FindingViewSet(PaginateByPkMixin, BaseRLSViewSet): date_filters = {} if exact: date = parse_date(exact) - datetime_start = datetime.combine( - date, datetime.min.time(), tzinfo=timezone.utc - ) + datetime_start = datetime.combine(date, datetime.min.time(), tzinfo=UTC) datetime_end = datetime_start + timedelta(days=1) date_filters["scan_id__gte"] = uuid7_start( datetime_to_uuid7(datetime_start) @@ -3929,7 +3924,7 @@ class FindingViewSet(PaginateByPkMixin, BaseRLSViewSet): if gte: date_start = parse_date(gte) datetime_start = datetime.combine( - date_start, datetime.min.time(), tzinfo=timezone.utc + date_start, datetime.min.time(), tzinfo=UTC ) date_filters["scan_id__gte"] = uuid7_start( datetime_to_uuid7(datetime_start) @@ -3939,7 +3934,7 @@ class FindingViewSet(PaginateByPkMixin, BaseRLSViewSet): datetime_end = datetime.combine( date_end + timedelta(days=1), datetime.min.time(), - tzinfo=timezone.utc, + tzinfo=UTC, ) date_filters["scan_id__lt"] = uuid7_start( datetime_to_uuid7(datetime_end) diff --git a/api/src/backend/config/celery.py b/api/src/backend/config/celery.py index 5d246395a5..1a35a1a753 100644 --- a/api/src/backend/config/celery.py +++ b/api/src/backend/config/celery.py @@ -1,7 +1,6 @@ import warnings from celery import Celery, Task - from config.env import env # Suppress specific warnings from django-rest-auth: https://github.com/iMerica/dj-rest-auth/issues/684 @@ -96,9 +95,8 @@ class RLSTask(Task): shadow=None, **options, ): - from django_celery_results.models import TaskResult - from api.models import Task as APITask + from django_celery_results.models import TaskResult result = super().apply_async( args=args, diff --git a/api/src/backend/config/custom_logging.py b/api/src/backend/config/custom_logging.py index fe2a090ca6..a601c8e2cd 100644 --- a/api/src/backend/config/custom_logging.py +++ b/api/src/backend/config/custom_logging.py @@ -2,7 +2,6 @@ import json import logging from enum import StrEnum - from config.env import env from django_guid.log_filters import CorrelationId diff --git a/api/src/backend/config/guniconf.py b/api/src/backend/config/guniconf.py index 6f85a9e1bc..9ede1d3164 100644 --- a/api/src/backend/config/guniconf.py +++ b/api/src/backend/config/guniconf.py @@ -3,9 +3,8 @@ import multiprocessing import os import threading -from uvicorn_worker import UvicornWorker - from config.env import env +from uvicorn_worker import UvicornWorker # Ensure the environment variable for Django settings is set os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.django.production") @@ -16,8 +15,9 @@ import django # noqa: E402 django.setup() from api.compliance import warm_compliance_caches # noqa: E402 -from config.django.production import LOGGING as DJANGO_LOGGERS, DEBUG # noqa: E402 from config.custom_logging import BackendLogger # noqa: E402 +from config.django.production import DEBUG # noqa: E402 +from config.django.production import LOGGING as DJANGO_LOGGERS # noqa: E402 BIND_ADDRESS = env("DJANGO_BIND_ADDRESS", default="127.0.0.1") PORT = env("DJANGO_PORT", default=8080) diff --git a/api/src/backend/config/settings/sentry.py b/api/src/backend/config/settings/sentry.py index 1f449f782e..5fd6e39cc9 100644 --- a/api/src/backend/config/settings/sentry.py +++ b/api/src/backend/config/settings/sentry.py @@ -1,5 +1,4 @@ import sentry_sdk - from config.env import env IGNORED_EXCEPTIONS = [ diff --git a/api/src/backend/config/urls.py b/api/src/backend/config/urls.py index 113f8bf5fc..4e7a46e31c 100644 --- a/api/src/backend/config/urls.py +++ b/api/src/backend/config/urls.py @@ -1,6 +1,5 @@ -from django.urls import include, path - from api.health import LivenessView, ReadinessView +from django.urls import include, path urlpatterns = [ path("api/v1/", include("api.v1.urls")), diff --git a/api/src/backend/conftest.py b/api/src/backend/conftest.py index eb73b0c51f..54aa99ab98 100644 --- a/api/src/backend/conftest.py +++ b/api/src/backend/conftest.py @@ -1,23 +1,10 @@ import logging -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from types import SimpleNamespace from unittest.mock import MagicMock, patch import pytest from allauth.socialaccount.models import SocialLogin -from django.conf import settings -from django.db import connection as django_connection -from django.db import connections as django_connections -from django.urls import reverse -from django_celery_results.models import TaskResult -from rest_framework import status -from rest_framework.test import APIClient -from tasks.jobs.backfill import ( - backfill_resource_scan_summaries, - aggregate_scan_category_summaries, - aggregate_scan_resource_group_summaries, -) - from api.attack_paths import ( AttackPathsQueryDefinition, AttackPathsQueryParameterDefinition, @@ -60,8 +47,20 @@ from api.models import ( ) from api.rls import Tenant from api.v1.serializers import TokenSerializer +from django.conf import settings +from django.db import connection as django_connection +from django.db import connections as django_connections +from django.urls import reverse +from django_celery_results.models import TaskResult from prowler.lib.check.models import Severity from prowler.lib.outputs.finding import Status +from rest_framework import status +from rest_framework.test import APIClient +from tasks.jobs.backfill import ( + aggregate_scan_category_summaries, + aggregate_scan_resource_group_summaries, + backfill_resource_scan_summaries, +) TODAY = str(datetime.today().date()) API_JSON_CONTENT_TYPE = "application/vnd.api+json" @@ -468,7 +467,7 @@ def invitations_fixture(create_test_user, tenants_fixture): email="testing@prowler.com", state=Invitation.State.EXPIRED, token="TESTING1234568", - expires_at=datetime.now(timezone.utc) - timedelta(days=1), + expires_at=datetime.now(UTC) - timedelta(days=1), inviter=user, tenant=tenant, ) @@ -715,7 +714,7 @@ def scans_fixture(tenants_fixture, providers_fixture): tenant, *_ = tenants_fixture provider, provider2, *_ = providers_fixture - now = datetime.now(timezone.utc) + now = datetime.now(UTC) scan1 = Scan.objects.create( name="Scan 1", @@ -1608,7 +1607,7 @@ def api_keys_fixture(tenants_fixture, create_test_user): name="Test API Key 2", tenant_id=tenant.id, entity=user, - expiry_date=datetime.now(timezone.utc) + timedelta(days=60), + expiry_date=datetime.now(UTC) + timedelta(days=60), ) # Revoked API key @@ -1902,10 +1901,10 @@ def provider_compliance_scores_fixture( provider1, provider2, *_ = providers_fixture scan1, _, scan3 = scans_fixture - scan1.completed_at = datetime.now(timezone.utc) - timedelta(hours=1) + scan1.completed_at = datetime.now(UTC) - timedelta(hours=1) scan1.save() scan3.state = StateChoices.COMPLETED - scan3.completed_at = datetime.now(timezone.utc) + scan3.completed_at = datetime.now(UTC) scan3.save() scores = [ diff --git a/api/src/backend/tasks/beat.py b/api/src/backend/tasks/beat.py index e9eb9c9309..017bec844a 100644 --- a/api/src/backend/tasks/beat.py +++ b/api/src/backend/tasks/beat.py @@ -1,13 +1,12 @@ import json -from datetime import datetime, timedelta, timezone - -from django_celery_beat.models import IntervalSchedule, PeriodicTask -from tasks.tasks import perform_scheduled_scan_task +from datetime import UTC, datetime, timedelta from api.db_utils import rls_transaction from api.exceptions import ConflictException from api.models import Provider, Scan, StateChoices +from django_celery_beat.models import IntervalSchedule, PeriodicTask from tasks.jobs.attack_paths import db_utils as attack_paths_db_utils +from tasks.tasks import perform_scheduled_scan_task def schedule_provider_scan(provider_instance: Provider): @@ -37,7 +36,7 @@ def schedule_provider_scan(provider_instance: Provider): provider_id=provider_id, trigger=Scan.TriggerChoices.SCHEDULED, state=StateChoices.AVAILABLE, - scheduled_at=datetime.now(timezone.utc), + scheduled_at=datetime.now(UTC), ) attack_paths_db_utils.create_attack_paths_scan( @@ -58,7 +57,7 @@ def schedule_provider_scan(provider_instance: Provider): } ), one_off=False, - start_time=datetime.now(timezone.utc) + timedelta(hours=24), + start_time=datetime.now(UTC) + timedelta(hours=24), ) scheduled_scan.scheduler_task_id = periodic_task_instance.id scheduled_scan.save() diff --git a/api/src/backend/tasks/jobs/attack_paths/aws.py b/api/src/backend/tasks/jobs/attack_paths/aws.py index 692eee61cd..398c261ca4 100644 --- a/api/src/backend/tasks/jobs/attack_paths/aws.py +++ b/api/src/backend/tasks/jobs/attack_paths/aws.py @@ -2,21 +2,20 @@ # (https://github.com/cartography-cncf/cartography), which is licensed under the Apache 2.0 License. import time - from typing import Any import aioboto3 import boto3 import neo4j - +from api.models import ( + AttackPathsScan as ProwlerAPIAttackPathsScan, +) +from api.models import ( + Provider as ProwlerAPIProvider, +) from cartography.config import Config as CartographyConfig from cartography.intel import aws as cartography_aws from celery.utils.log import get_task_logger - -from api.models import ( - AttackPathsScan as ProwlerAPIAttackPathsScan, - Provider as ProwlerAPIProvider, -) from prowler.providers.common.provider import Provider as ProwlerSDKProvider from tasks.jobs.attack_paths import db_utils, utils diff --git a/api/src/backend/tasks/jobs/attack_paths/cleanup.py b/api/src/backend/tasks/jobs/attack_paths/cleanup.py index fa7670afaa..9ac2733faf 100644 --- a/api/src/backend/tasks/jobs/attack_paths/cleanup.py +++ b/api/src/backend/tasks/jobs/attack_paths/cleanup.py @@ -1,5 +1,9 @@ -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta +from api.attack_paths import database as graph_database +from api.db_router import MainRouter +from api.db_utils import rls_transaction +from api.models import AttackPathsScan, StateChoices from celery import states from celery.utils.log import get_task_logger from config.django.base import ATTACK_PATHS_SCAN_STALE_THRESHOLD_MINUTES @@ -10,11 +14,6 @@ from tasks.jobs.attack_paths.db_utils import ( from tasks.jobs.orphan_recovery import is_worker_alive as _is_worker_alive from tasks.jobs.orphan_recovery import revoke_task as _revoke_task -from api.attack_paths import database as graph_database -from api.db_router import MainRouter -from api.db_utils import rls_transaction -from api.models import AttackPathsScan, StateChoices - logger = get_task_logger(__name__) @@ -30,7 +29,7 @@ def cleanup_stale_attack_paths_scans() -> dict: age plus the parent `Scan` no longer being in flight. """ threshold = timedelta(minutes=ATTACK_PATHS_SCAN_STALE_THRESHOLD_MINUTES) - now = datetime.now(tz=timezone.utc) + now = datetime.now(tz=UTC) cutoff = now - threshold cleaned_up: list[str] = [] @@ -175,7 +174,7 @@ def _cleanup_scan(scan, task_result, reason: str) -> bool: # Mark `TaskResult` as `FAILURE` (not RLS-protected, outside lock) if task_result: task_result.status = states.FAILURE - task_result.date_done = datetime.now(tz=timezone.utc) + task_result.date_done = datetime.now(tz=UTC) task_result.save(update_fields=["status", "date_done"]) recover_graph_data_ready(fresh_scan) @@ -201,7 +200,7 @@ def _cleanup_scheduled_scan(scan, task_result, reason: str) -> bool: if task_result: task_result.status = states.FAILURE - task_result.date_done = datetime.now(tz=timezone.utc) + task_result.date_done = datetime.now(tz=UTC) task_result.save(update_fields=["status", "date_done"]) logger.info(f"Cleaned up scheduled scan {scan_id_str}: {reason}") diff --git a/api/src/backend/tasks/jobs/attack_paths/config.py b/api/src/backend/tasks/jobs/attack_paths/config.py index 0816626b67..78ca7eb038 100644 --- a/api/src/backend/tasks/jobs/attack_paths/config.py +++ b/api/src/backend/tasks/jobs/attack_paths/config.py @@ -1,5 +1,5 @@ +from collections.abc import Callable from dataclasses import dataclass -from typing import Callable from uuid import UUID from config.env import env diff --git a/api/src/backend/tasks/jobs/attack_paths/db_utils.py b/api/src/backend/tasks/jobs/attack_paths/db_utils.py index a6e7da7f87..f1bb4edef4 100644 --- a/api/src/backend/tasks/jobs/attack_paths/db_utils.py +++ b/api/src/backend/tasks/jobs/attack_paths/db_utils.py @@ -1,15 +1,14 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any -from cartography.config import Config as CartographyConfig -from celery.utils.log import get_task_logger -from tasks.jobs.attack_paths.config import is_provider_available - from api.attack_paths import database as graph_database from api.db_utils import rls_transaction from api.models import AttackPathsScan as ProwlerAPIAttackPathsScan from api.models import Provider as ProwlerAPIProvider from api.models import StateChoices +from cartography.config import Config as CartographyConfig +from celery.utils.log import get_task_logger +from tasks.jobs.attack_paths.config import is_provider_available logger = get_task_logger(__name__) @@ -43,7 +42,7 @@ def create_attack_paths_scan( provider_id=provider_id, scan_id=scan_id, state=StateChoices.SCHEDULED, - started_at=datetime.now(tz=timezone.utc), + started_at=datetime.now(tz=UTC), graph_data_ready=previous_data_ready, ) attack_paths_scan.save() @@ -104,7 +103,7 @@ def starting_attack_paths_scan( return False locked.state = StateChoices.EXECUTING - locked.started_at = datetime.now(tz=timezone.utc) + locked.started_at = datetime.now(tz=UTC) locked.update_tag = cartography_config.update_tag locked.save(update_fields=["state", "started_at", "update_tag"]) @@ -121,7 +120,7 @@ def _mark_scan_finished( ingestion_exceptions: dict[str, Any], ) -> None: """Set terminal fields on a scan. Caller must be inside a transaction.""" - now = datetime.now(tz=timezone.utc) + now = datetime.now(tz=UTC) duration = ( int((now - attack_paths_scan.started_at).total_seconds()) if attack_paths_scan.started_at diff --git a/api/src/backend/tasks/jobs/attack_paths/findings.py b/api/src/backend/tasks/jobs/attack_paths/findings.py index 3581f0ca0f..a5bc4c1ad5 100644 --- a/api/src/backend/tasks/jobs/attack_paths/findings.py +++ b/api/src/backend/tasks/jobs/attack_paths/findings.py @@ -8,13 +8,18 @@ This module handles: """ from collections import defaultdict -from typing import Any, Callable, Generator +from collections.abc import Callable, Generator +from typing import Any from uuid import UUID import neo4j - +from api.db_router import READ_REPLICA_ALIAS +from api.db_utils import rls_transaction +from api.models import Finding as FindingModel +from api.models import Provider, ResourceFindingMapping from cartography.config import Config as CartographyConfig from celery.utils.log import get_task_logger +from prowler.config import config as ProwlerConfig from tasks.jobs.attack_paths.config import ( BATCH_SIZE, FINDINGS_BATCH_SIZE, @@ -29,12 +34,6 @@ from tasks.jobs.attack_paths.queries import ( render_cypher_template, ) -from api.db_router import READ_REPLICA_ALIAS -from api.db_utils import rls_transaction -from api.models import Finding as FindingModel -from api.models import Provider, ResourceFindingMapping -from prowler.config import config as ProwlerConfig - logger = get_task_logger(__name__) diff --git a/api/src/backend/tasks/jobs/attack_paths/indexes.py b/api/src/backend/tasks/jobs/attack_paths/indexes.py index 0de94a162e..c2b56197d4 100644 --- a/api/src/backend/tasks/jobs/attack_paths/indexes.py +++ b/api/src/backend/tasks/jobs/attack_paths/indexes.py @@ -1,13 +1,11 @@ import neo4j - from cartography.client.core.tx import run_write_query from celery.utils.log import get_task_logger - from tasks.jobs.attack_paths.config import ( INTERNET_NODE_LABEL, - PROWLER_FINDING_LABEL, PROVIDER_ELEMENT_ID_PROPERTY, PROVIDER_RESOURCE_LABEL, + PROWLER_FINDING_LABEL, ) logger = get_task_logger(__name__) diff --git a/api/src/backend/tasks/jobs/attack_paths/internet.py b/api/src/backend/tasks/jobs/attack_paths/internet.py index 83517bc903..4c7a61bd20 100644 --- a/api/src/backend/tasks/jobs/attack_paths/internet.py +++ b/api/src/backend/tasks/jobs/attack_paths/internet.py @@ -7,11 +7,9 @@ in the temporary scan database before sync. """ import neo4j - +from api.models import Provider from cartography.config import Config as CartographyConfig from celery.utils.log import get_task_logger - -from api.models import Provider from prowler.config import config as ProwlerConfig from tasks.jobs.attack_paths.config import get_root_node_label from tasks.jobs.attack_paths.queries import ( diff --git a/api/src/backend/tasks/jobs/attack_paths/queries.py b/api/src/backend/tasks/jobs/attack_paths/queries.py index eb1d82a96e..277305f0e0 100644 --- a/api/src/backend/tasks/jobs/attack_paths/queries.py +++ b/api/src/backend/tasks/jobs/attack_paths/queries.py @@ -1,9 +1,9 @@ # Cypher query templates for Attack Paths operations from tasks.jobs.attack_paths.config import ( INTERNET_NODE_LABEL, - PROWLER_FINDING_LABEL, PROVIDER_ELEMENT_ID_PROPERTY, PROVIDER_RESOURCE_LABEL, + PROWLER_FINDING_LABEL, ) diff --git a/api/src/backend/tasks/jobs/attack_paths/scan.py b/api/src/backend/tasks/jobs/attack_paths/scan.py index 452dae00d0..0fb8d2b885 100644 --- a/api/src/backend/tasks/jobs/attack_paths/scan.py +++ b/api/src/backend/tasks/jobs/attack_paths/scan.py @@ -55,9 +55,13 @@ exception propagates to Celery. import logging import time - from typing import Any +from api.attack_paths import database as graph_database +from api.db_utils import rls_transaction +from api.models import Provider as ProwlerAPIProvider +from api.models import StateChoices +from api.utils import initialize_prowler_provider from cartography.config import Config as CartographyConfig from cartography.intel import analysis as cartography_analysis from cartography.intel import create_indexes as cartography_create_indexes @@ -66,12 +70,6 @@ from celery.utils.log import get_task_logger from tasks.jobs.attack_paths import db_utils, findings, indexes, internet, sync, utils from tasks.jobs.attack_paths.config import get_cartography_ingestion_function -from api.attack_paths import database as graph_database -from api.db_utils import rls_transaction -from api.models import Provider as ProwlerAPIProvider -from api.models import StateChoices -from api.utils import initialize_prowler_provider - # Without this Celery goes crazy with Cartography logging logging.getLogger("cartography").setLevel(logging.ERROR) logging.getLogger("neo4j").propagate = False diff --git a/api/src/backend/tasks/jobs/attack_paths/sync.py b/api/src/backend/tasks/jobs/attack_paths/sync.py index f720a12e82..50f770deb5 100644 --- a/api/src/backend/tasks/jobs/attack_paths/sync.py +++ b/api/src/backend/tasks/jobs/attack_paths/sync.py @@ -6,11 +6,11 @@ to the tenant database, adding provider isolation labels and properties. """ import time - from collections import defaultdict from typing import Any import neo4j +from api.attack_paths import database as graph_database from celery.utils.log import get_task_logger from tasks.jobs.attack_paths.config import ( PROVIDER_ISOLATION_PROPERTIES, @@ -27,8 +27,6 @@ from tasks.jobs.attack_paths.queries import ( render_cypher_template, ) -from api.attack_paths import database as graph_database - logger = get_task_logger(__name__) diff --git a/api/src/backend/tasks/jobs/attack_paths/utils.py b/api/src/backend/tasks/jobs/attack_paths/utils.py index eef5670782..50d670bfd3 100644 --- a/api/src/backend/tasks/jobs/attack_paths/utils.py +++ b/api/src/backend/tasks/jobs/attack_paths/utils.py @@ -1,7 +1,6 @@ import asyncio import traceback - -from datetime import datetime, timezone +from datetime import UTC, datetime from celery.utils.log import get_task_logger @@ -10,7 +9,7 @@ logger = get_task_logger(__name__) def stringify_exception(exception: Exception, context: str) -> str: """Format an exception with timestamp and traceback for logging.""" - timestamp = datetime.now(tz=timezone.utc) + timestamp = datetime.now(tz=UTC) exception_traceback = traceback.TracebackException.from_exception(exception) traceback_string = "".join(exception_traceback.format()) return f"{timestamp} - {context}\n{traceback_string}" diff --git a/api/src/backend/tasks/jobs/backfill.py b/api/src/backend/tasks/jobs/backfill.py index 825dcb7ca8..56cb626786 100644 --- a/api/src/backend/tasks/jobs/backfill.py +++ b/api/src/backend/tasks/jobs/backfill.py @@ -1,19 +1,6 @@ from collections import defaultdict from datetime import timedelta -from celery.utils.log import get_task_logger -from django.db.models import OuterRef, Subquery, Sum -from django.utils import timezone -from tasks.jobs.queries import ( - COMPLIANCE_UPSERT_PROVIDER_SCORE_SQL, - COMPLIANCE_UPSERT_TENANT_SUMMARY_ALL_SQL, -) -from tasks.jobs.scan import ( - aggregate_category_counts, - aggregate_finding_group_summaries, - aggregate_resource_group_counts, -) - from api.db_router import READ_REPLICA_ALIAS, MainRouter from api.db_utils import ( POSTGRES_TENANT_VAR, @@ -36,6 +23,18 @@ from api.models import ( ScanSummary, StateChoices, ) +from celery.utils.log import get_task_logger +from django.db.models import OuterRef, Subquery, Sum +from django.utils import timezone +from tasks.jobs.queries import ( + COMPLIANCE_UPSERT_PROVIDER_SCORE_SQL, + COMPLIANCE_UPSERT_TENANT_SUMMARY_ALL_SQL, +) +from tasks.jobs.scan import ( + aggregate_category_counts, + aggregate_finding_group_summaries, + aggregate_resource_group_counts, +) logger = get_task_logger(__name__) diff --git a/api/src/backend/tasks/jobs/connection.py b/api/src/backend/tasks/jobs/connection.py index d7068ebf3b..3ae20c96a0 100644 --- a/api/src/backend/tasks/jobs/connection.py +++ b/api/src/backend/tasks/jobs/connection.py @@ -1,13 +1,12 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime import openai -from celery.utils.log import get_task_logger - from api.models import Integration, LighthouseConfiguration, Provider from api.utils import ( prowler_integration_connection_test, prowler_provider_connection_test, ) +from celery.utils.log import get_task_logger logger = get_task_logger(__name__) @@ -38,7 +37,7 @@ def check_provider_connection(provider_id: str): raise e provider_instance.connected = connection_result.is_connected - provider_instance.connection_last_checked_at = datetime.now(tz=timezone.utc) + provider_instance.connection_last_checked_at = datetime.now(tz=UTC) provider_instance.save() connection_error = f"{connection_result.error}" if connection_result.error else None @@ -111,7 +110,7 @@ def check_integration_connection(integration_id: str): # Update integration connection status integration.connected = result.is_connected - integration.connection_last_checked_at = datetime.now(tz=timezone.utc) + integration.connection_last_checked_at = datetime.now(tz=UTC) integration.save() return { diff --git a/api/src/backend/tasks/jobs/deletion.py b/api/src/backend/tasks/jobs/deletion.py index f9ead01897..fadf98d464 100644 --- a/api/src/backend/tasks/jobs/deletion.py +++ b/api/src/backend/tasks/jobs/deletion.py @@ -1,10 +1,3 @@ -from celery.utils.log import get_task_logger -from django.db import DatabaseError -from tasks.jobs.queries import ( - COMPLIANCE_DELETE_EMPTY_TENANT_SUMMARY_SQL, - COMPLIANCE_UPSERT_TENANT_SUMMARY_SQL, -) - from api.attack_paths import database as graph_database from api.db_router import MainRouter from api.db_utils import batch_delete, rls_transaction @@ -18,6 +11,12 @@ from api.models import ( ScanSummary, Tenant, ) +from celery.utils.log import get_task_logger +from django.db import DatabaseError +from tasks.jobs.queries import ( + COMPLIANCE_DELETE_EMPTY_TENANT_SUMMARY_SQL, + COMPLIANCE_UPSERT_TENANT_SUMMARY_SQL, +) logger = get_task_logger(__name__) diff --git a/api/src/backend/tasks/jobs/export.py b/api/src/backend/tasks/jobs/export.py index 96bd03ee6c..e658d6018c 100644 --- a/api/src/backend/tasks/jobs/export.py +++ b/api/src/backend/tasks/jobs/export.py @@ -4,12 +4,11 @@ import zipfile import boto3 import config.django.base as base +from api.db_utils import rls_transaction +from api.models import Scan from botocore.exceptions import ClientError, NoCredentialsError, ParamValidationError from celery.utils.log import get_task_logger from django.conf import settings - -from api.db_utils import rls_transaction -from api.models import Scan from prowler.config.config import ( csv_file_suffix, html_file_suffix, @@ -18,6 +17,9 @@ from prowler.config.config import ( set_output_timestamp, ) from prowler.lib.outputs.asff.asff import ASFF +from prowler.lib.outputs.compliance.asd_essential_eight.asd_essential_eight_aws import ( + ASDEssentialEightAWS, +) from prowler.lib.outputs.compliance.aws_well_architected.aws_well_architected import ( AWSWellArchitected, ) @@ -42,9 +44,6 @@ from prowler.lib.outputs.compliance.cisa_scuba.cisa_scuba_googleworkspace import from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS from prowler.lib.outputs.compliance.ens.ens_azure import AzureENS from prowler.lib.outputs.compliance.ens.ens_gcp import GCPENS -from prowler.lib.outputs.compliance.asd_essential_eight.asd_essential_eight_aws import ( - ASDEssentialEightAWS, -) from prowler.lib.outputs.compliance.iso27001.iso27001_aws import AWSISO27001 from prowler.lib.outputs.compliance.iso27001.iso27001_azure import AzureISO27001 from prowler.lib.outputs.compliance.iso27001.iso27001_gcp import GCPISO27001 diff --git a/api/src/backend/tasks/jobs/integrations.py b/api/src/backend/tasks/jobs/integrations.py index 5ca94057da..25722686cc 100644 --- a/api/src/backend/tasks/jobs/integrations.py +++ b/api/src/backend/tasks/jobs/integrations.py @@ -2,15 +2,13 @@ import os import time from glob import glob -from celery.utils.log import get_task_logger -from config.django.base import DJANGO_FINDINGS_BATCH_SIZE -from django.db import OperationalError -from tasks.utils import batched - from api.db_router import READ_REPLICA_ALIAS, MainRouter from api.db_utils import REPLICA_MAX_ATTEMPTS, REPLICA_RETRY_BASE_DELAY, rls_transaction from api.models import Finding, Integration, Provider from api.utils import initialize_prowler_integration, initialize_prowler_provider +from celery.utils.log import get_task_logger +from config.django.base import DJANGO_FINDINGS_BATCH_SIZE +from django.db import OperationalError from prowler.lib.outputs.asff.asff import ASFF from prowler.lib.outputs.compliance.generic.generic import GenericCompliance from prowler.lib.outputs.csv.csv import CSV @@ -24,6 +22,7 @@ from prowler.providers.aws.lib.security_hub.exceptions.exceptions import ( ) from prowler.providers.aws.lib.security_hub.security_hub import SecurityHub from prowler.providers.common.models import Connection +from tasks.utils import batched logger = get_task_logger(__name__) diff --git a/api/src/backend/tasks/jobs/lighthouse_providers.py b/api/src/backend/tasks/jobs/lighthouse_providers.py index 29e36e5e57..0f28725e01 100644 --- a/api/src/backend/tasks/jobs/lighthouse_providers.py +++ b/api/src/backend/tasks/jobs/lighthouse_providers.py @@ -1,14 +1,11 @@ -from typing import Dict - import boto3 import openai +from api.models import LighthouseProviderConfiguration, LighthouseProviderModels from botocore import UNSIGNED from botocore.config import Config from botocore.exceptions import BotoCoreError, ClientError from celery.utils.log import get_task_logger -from api.models import LighthouseProviderConfiguration, LighthouseProviderModels - logger = get_task_logger(__name__) # OpenAI model prefixes to exclude from Lighthouse model selection. @@ -104,7 +101,7 @@ def _extract_openai_api_key( def _extract_openai_compatible_params( provider_cfg: LighthouseProviderConfiguration, -) -> Dict[str, str] | None: +) -> dict[str, str] | None: """ Extract base_url and api_key for OpenAI-compatible providers. """ @@ -122,7 +119,7 @@ def _extract_openai_compatible_params( def _extract_bedrock_credentials( provider_cfg: LighthouseProviderConfiguration, -) -> Dict[str, str] | None: +) -> dict[str, str] | None: """ Safely extract AWS Bedrock credentials from a provider configuration. @@ -177,7 +174,7 @@ def _extract_bedrock_credentials( def _create_bedrock_client( - bedrock_creds: Dict[str, str], service_name: str = "bedrock" + bedrock_creds: dict[str, str], service_name: str = "bedrock" ): """ Create a boto3 Bedrock client with the appropriate authentication method. @@ -221,7 +218,7 @@ def _create_bedrock_client( ) -def check_lighthouse_provider_connection(provider_config_id: str) -> Dict: +def check_lighthouse_provider_connection(provider_config_id: str) -> dict: """ Validate a Lighthouse provider configuration by calling the provider API and toggle its active state accordingly. @@ -314,7 +311,7 @@ def check_lighthouse_provider_connection(provider_config_id: str) -> Dict: return {"connected": False, "error": error_message} -def _fetch_openai_models(api_key: str) -> Dict[str, str]: +def _fetch_openai_models(api_key: str) -> dict[str, str]: """ Fetch available models from OpenAI API. @@ -355,7 +352,7 @@ def _fetch_openai_models(api_key: str) -> Dict[str, str]: return filtered_models -def _fetch_openai_compatible_models(base_url: str, api_key: str) -> Dict[str, str]: +def _fetch_openai_compatible_models(base_url: str, api_key: str) -> dict[str, str]: """ Fetch available models from an OpenAI-compatible API using the OpenAI SDK. @@ -367,7 +364,7 @@ def _fetch_openai_compatible_models(base_url: str, api_key: str) -> Dict[str, st client = openai.OpenAI(api_key=api_key, base_url=base_url) models = client.models.list() - available_models: Dict[str, str] = {} + available_models: dict[str, str] = {} for model in models.data: model_id = model.id # Prefer provider-supplied human-friendly name when available @@ -462,7 +459,7 @@ def _extract_foundation_model_ids(profile_models: list) -> list[str]: def _build_inference_profile_map( bedrock_client, region: str -) -> Dict[str, tuple[str, str]]: +) -> dict[str, tuple[str, str]]: """ Build map of foundation_model_id -> best inference profile. @@ -472,7 +469,7 @@ def _build_inference_profile_map( Prefers region-matched profiles over others """ region_prefix = _get_region_prefix(region) - model_to_profile: Dict[str, tuple[str, str]] = {} + model_to_profile: dict[str, tuple[str, str]] = {} try: response = bedrock_client.list_inference_profiles() @@ -533,7 +530,7 @@ def _check_on_demand_availability(bedrock_client, model_id: str) -> bool: return False -def _fetch_bedrock_models(bedrock_creds: Dict[str, str]) -> Dict[str, str]: +def _fetch_bedrock_models(bedrock_creds: dict[str, str]) -> dict[str, str]: """ Fetch available models from AWS Bedrock, preferring inference profiles over ON_DEMAND. @@ -560,7 +557,7 @@ def _fetch_bedrock_models(bedrock_creds: Dict[str, str]) -> Dict[str, str]: foundation_response = bedrock_client.list_foundation_models() model_summaries = foundation_response.get("modelSummaries", []) - models_to_return: Dict[str, str] = {} + models_to_return: dict[str, str] = {} on_demand_models: set[str] = set() for model in model_summaries: @@ -585,7 +582,7 @@ def _fetch_bedrock_models(bedrock_creds: Dict[str, str]) -> Dict[str, str]: models_to_return[model_id] = model_name on_demand_models.add(model_id) - available_models: Dict[str, str] = {} + available_models: dict[str, str] = {} for model_id, model_name in models_to_return.items(): if model_id in on_demand_models: @@ -597,7 +594,7 @@ def _fetch_bedrock_models(bedrock_creds: Dict[str, str]) -> Dict[str, str]: return available_models -def refresh_lighthouse_provider_models(provider_config_id: str) -> Dict: +def refresh_lighthouse_provider_models(provider_config_id: str) -> dict: """ Refresh the catalog of models for a Lighthouse provider configuration. @@ -619,7 +616,7 @@ def refresh_lighthouse_provider_models(provider_config_id: str) -> Dict: LighthouseProviderConfiguration.DoesNotExist: If no configuration exists with the given ID. """ provider_cfg = LighthouseProviderConfiguration.objects.get(pk=provider_config_id) - fetched_models: Dict[str, str] = {} + fetched_models: dict[str, str] = {} try: if ( diff --git a/api/src/backend/tasks/jobs/muting.py b/api/src/backend/tasks/jobs/muting.py index 6ef4d127f5..12a32ac574 100644 --- a/api/src/backend/tasks/jobs/muting.py +++ b/api/src/backend/tasks/jobs/muting.py @@ -1,10 +1,9 @@ +from api.db_utils import rls_transaction +from api.models import Finding, MuteRule from celery.utils.log import get_task_logger from config.django.base import DJANGO_FINDINGS_BATCH_SIZE from tasks.utils import batched -from api.db_utils import rls_transaction -from api.models import Finding, MuteRule - logger = get_task_logger(__name__) diff --git a/api/src/backend/tasks/jobs/orphan_recovery.py b/api/src/backend/tasks/jobs/orphan_recovery.py index 1bf5c95df2..7211f1a1d5 100644 --- a/api/src/backend/tasks/jobs/orphan_recovery.py +++ b/api/src/backend/tasks/jobs/orphan_recovery.py @@ -21,7 +21,7 @@ This is the shared engine behind both the periodic Beat watchdog and the import ast import json from contextlib import contextmanager -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from uuid import uuid4 from celery import current_app, states @@ -213,7 +213,7 @@ def _reconcile_task_results( ) -> dict: from django_celery_results.models import TaskResult - cutoff = datetime.now(tz=timezone.utc) - timedelta(minutes=grace_minutes) + cutoff = datetime.now(tz=UTC) - timedelta(minutes=grace_minutes) candidates = list( TaskResult.objects.filter(status__in=IN_FLIGHT_STATES, date_created__lt=cutoff) .exclude(worker__isnull=True) @@ -278,7 +278,7 @@ def _recover_task(task_result, max_attempts: int, window_hours: int) -> str: name = task_result.task_name args_repr = task_result.task_args kwargs_repr = task_result.task_kwargs - now = datetime.now(tz=timezone.utc) + now = datetime.now(tz=UTC) # Drop any future broker redelivery of the stale message. revoke_task(task_result, terminate=False) diff --git a/api/src/backend/tasks/jobs/report.py b/api/src/backend/tasks/jobs/report.py index 36e47829c5..b40516dadf 100644 --- a/api/src/backend/tasks/jobs/report.py +++ b/api/src/backend/tasks/jobs/report.py @@ -1,3 +1,4 @@ +import fcntl import gc import os import re @@ -7,9 +8,17 @@ from pathlib import Path from shutil import rmtree from uuid import UUID -import fcntl +from api.db_router import READ_REPLICA_ALIAS, MainRouter +from api.db_utils import rls_transaction +from api.models import Provider, Scan, ScanSummary, StateChoices, ThreatScoreSnapshot +from api.utils import initialize_prowler_provider from celery.utils.log import get_task_logger from config.django.base import DJANGO_TMP_OUTPUT_DIRECTORY +from prowler.lib.check.compliance_models import ( + Compliance, + get_bulk_compliance_frameworks_universal, +) +from prowler.lib.outputs.finding import Finding as FindingOutput from tasks.jobs.export import _generate_compliance_output_directory, _upload_to_s3 from tasks.jobs.reports import ( FRAMEWORK_REGISTRY, @@ -25,16 +34,6 @@ from tasks.jobs.threatscore_utils import ( _get_compliance_check_ids, ) -from api.db_router import READ_REPLICA_ALIAS, MainRouter -from api.db_utils import rls_transaction -from api.models import Provider, Scan, ScanSummary, StateChoices, ThreatScoreSnapshot -from api.utils import initialize_prowler_provider -from prowler.lib.check.compliance_models import ( - Compliance, - get_bulk_compliance_frameworks_universal, -) -from prowler.lib.outputs.finding import Finding as FindingOutput - logger = get_task_logger(__name__) STALE_TMP_OUTPUT_MAX_AGE_HOURS = 48 STALE_TMP_OUTPUT_MAX_DELETIONS_PER_RUN = 50 diff --git a/api/src/backend/tasks/jobs/reports/base.py b/api/src/backend/tasks/jobs/reports/base.py index 27d1defff4..f51319a846 100644 --- a/api/src/backend/tasks/jobs/reports/base.py +++ b/api/src/backend/tasks/jobs/reports/base.py @@ -8,7 +8,16 @@ from dataclasses import dataclass, field from types import SimpleNamespace from typing import Any +from api.db_router import READ_REPLICA_ALIAS +from api.db_utils import rls_transaction +from api.models import Provider, StatusChoices +from api.utils import initialize_prowler_provider from celery.utils.log import get_task_logger +from prowler.lib.check.compliance_models import ( + Compliance, + get_bulk_compliance_frameworks_universal, +) +from prowler.lib.outputs.finding import Finding as FindingOutput from reportlab.lib.enums import TA_CENTER from reportlab.lib.pagesizes import letter from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet @@ -23,16 +32,6 @@ from tasks.jobs.threatscore_utils import ( _load_findings_for_requirement_checks, ) -from api.db_router import READ_REPLICA_ALIAS -from api.db_utils import rls_transaction -from api.models import Provider, StatusChoices -from api.utils import initialize_prowler_provider -from prowler.lib.check.compliance_models import ( - Compliance, - get_bulk_compliance_frameworks_universal, -) -from prowler.lib.outputs.finding import Finding as FindingOutput - from .components import ( ColumnConfig, create_data_table, diff --git a/api/src/backend/tasks/jobs/reports/charts.py b/api/src/backend/tasks/jobs/reports/charts.py index da3f5aae10..7e9ad7ef20 100644 --- a/api/src/backend/tasks/jobs/reports/charts.py +++ b/api/src/backend/tasks/jobs/reports/charts.py @@ -2,7 +2,7 @@ import gc import io import math import time -from typing import Callable +from collections.abc import Callable import matplotlib from celery.utils.log import get_task_logger diff --git a/api/src/backend/tasks/jobs/reports/cis.py b/api/src/backend/tasks/jobs/reports/cis.py index 0fbb416a17..8a1cfb6eba 100644 --- a/api/src/backend/tasks/jobs/reports/cis.py +++ b/api/src/backend/tasks/jobs/reports/cis.py @@ -3,11 +3,10 @@ import re from collections import defaultdict from typing import Any +from api.models import StatusChoices from reportlab.lib.units import inch from reportlab.platypus import Image, PageBreak, Paragraph, Spacer, Table, TableStyle -from api.models import StatusChoices - from .base import ( BaseComplianceReportGenerator, ComplianceData, diff --git a/api/src/backend/tasks/jobs/reports/components.py b/api/src/backend/tasks/jobs/reports/components.py index 31b808ec5c..0c15acb4cb 100644 --- a/api/src/backend/tasks/jobs/reports/components.py +++ b/api/src/backend/tasks/jobs/reports/components.py @@ -1,5 +1,6 @@ +from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Callable +from typing import Any from reportlab.lib import colors from reportlab.lib.styles import ParagraphStyle diff --git a/api/src/backend/tasks/jobs/reports/csa.py b/api/src/backend/tasks/jobs/reports/csa.py index c55ed198de..0a53c17cdf 100644 --- a/api/src/backend/tasks/jobs/reports/csa.py +++ b/api/src/backend/tasks/jobs/reports/csa.py @@ -1,11 +1,10 @@ from collections import defaultdict +from api.models import StatusChoices from celery.utils.log import get_task_logger from reportlab.lib.units import inch from reportlab.platypus import Image, PageBreak, Paragraph, Spacer, Table, TableStyle -from api.models import StatusChoices - from .base import ( BaseComplianceReportGenerator, ComplianceData, diff --git a/api/src/backend/tasks/jobs/reports/ens.py b/api/src/backend/tasks/jobs/reports/ens.py index 617d4ea59f..44c874bfc1 100644 --- a/api/src/backend/tasks/jobs/reports/ens.py +++ b/api/src/backend/tasks/jobs/reports/ens.py @@ -1,13 +1,12 @@ import os from collections import defaultdict +from api.models import StatusChoices from reportlab.lib import colors from reportlab.lib.styles import ParagraphStyle from reportlab.lib.units import inch from reportlab.platypus import Image, PageBreak, Paragraph, Spacer, Table, TableStyle -from api.models import StatusChoices - from .base import ( BaseComplianceReportGenerator, ComplianceData, diff --git a/api/src/backend/tasks/jobs/reports/nis2.py b/api/src/backend/tasks/jobs/reports/nis2.py index 4ac5fa3d15..ed936f9571 100644 --- a/api/src/backend/tasks/jobs/reports/nis2.py +++ b/api/src/backend/tasks/jobs/reports/nis2.py @@ -1,11 +1,10 @@ import os from collections import defaultdict +from api.models import StatusChoices from reportlab.lib.units import inch from reportlab.platypus import Image, PageBreak, Paragraph, Spacer, Table, TableStyle -from api.models import StatusChoices - from .base import ( BaseComplianceReportGenerator, ComplianceData, diff --git a/api/src/backend/tasks/jobs/reports/threatscore.py b/api/src/backend/tasks/jobs/reports/threatscore.py index e23085b1c3..a71ebde536 100644 --- a/api/src/backend/tasks/jobs/reports/threatscore.py +++ b/api/src/backend/tasks/jobs/reports/threatscore.py @@ -1,12 +1,11 @@ import gc +from api.models import StatusChoices from reportlab.lib import colors from reportlab.lib.styles import ParagraphStyle from reportlab.lib.units import inch from reportlab.platypus import Image, PageBreak, Paragraph, Spacer, Table, TableStyle -from api.models import StatusChoices - from .base import ( BaseComplianceReportGenerator, ComplianceData, diff --git a/api/src/backend/tasks/jobs/scan.py b/api/src/backend/tasks/jobs/scan.py index f6ae3e6402..dcaacf6642 100644 --- a/api/src/backend/tasks/jobs/scan.py +++ b/api/src/backend/tasks/jobs/scan.py @@ -6,34 +6,10 @@ import time import uuid from collections import defaultdict from collections.abc import Iterable -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any import sentry_sdk -from celery.utils.log import get_task_logger -from config.django.base import DJANGO_FINDINGS_BATCH_SIZE -from config.env import env -from config.settings.celery import CELERY_DEADLOCK_ATTEMPTS -from django.db import IntegrityError, OperationalError -from django.db.models import ( - Case, - Count, - Exists, - IntegerField, - Max, - Min, - OuterRef, - Q, - Sum, - When, -) -from django.utils import timezone as django_timezone -from tasks.jobs.queries import ( - COMPLIANCE_UPSERT_PROVIDER_SCORE_SQL, - COMPLIANCE_UPSERT_TENANT_SUMMARY_SQL, -) -from tasks.utils import CustomEncoder, batched - from api.compliance import PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE from api.constants import SEVERITY_ORDER from api.db_router import READ_REPLICA_ALIAS, MainRouter @@ -68,9 +44,32 @@ from api.models import ( from api.models import StatusChoices as FindingStatus from api.utils import initialize_prowler_provider, return_prowler_provider from api.v1.serializers import ScanTaskSerializer +from celery.utils.log import get_task_logger +from config.django.base import DJANGO_FINDINGS_BATCH_SIZE +from config.env import env +from config.settings.celery import CELERY_DEADLOCK_ATTEMPTS +from django.db import IntegrityError, OperationalError +from django.db.models import ( + Case, + Count, + Exists, + IntegerField, + Max, + Min, + OuterRef, + Q, + Sum, + When, +) +from django.utils import timezone as django_timezone from prowler.lib.check.models import CheckMetadata from prowler.lib.outputs.finding import Finding as ProwlerFinding from prowler.lib.scan.scan import Scan as ProwlerScan +from tasks.jobs.queries import ( + COMPLIANCE_UPSERT_PROVIDER_SCORE_SQL, + COMPLIANCE_UPSERT_TENANT_SUMMARY_SQL, +) +from tasks.utils import CustomEncoder, batched logger = get_task_logger(__name__) @@ -311,7 +310,7 @@ def _copy_compliance_requirement_rows( csv_buffer = io.StringIO() writer = csv.writer(csv_buffer) - datetime_now = datetime.now(tz=timezone.utc) + datetime_now = datetime.now(tz=UTC) for row in rows: writer.writerow( [ @@ -783,7 +782,7 @@ def _process_finding_micro_batch( delta = _create_finding_delta(last_status, status) if not last_first_seen_at: - last_first_seen_at = datetime.now(tz=timezone.utc) + last_first_seen_at = datetime.now(tz=UTC) # Determine if finding should be muted and why # Priority: mutelist processor (highest) > manual mute rules @@ -814,7 +813,7 @@ def _process_finding_micro_batch( scan=scan_instance, first_seen_at=last_first_seen_at, muted=is_muted, - muted_at=datetime.now(tz=timezone.utc) if is_muted else None, + muted_at=datetime.now(tz=UTC) if is_muted else None, muted_reason=muted_reason, compliance=finding.compliance, categories=check_metadata.get("categories", []) or [], @@ -941,7 +940,7 @@ def _process_finding_micro_batch( set(dirty_resources.keys()) | resources_with_new_tag_mappings ) if all_resource_uids_to_touch: - now_utc = datetime.now(tz=timezone.utc) + now_utc = datetime.now(tz=UTC) resources_to_bulk_update = [] for uid in all_resource_uids_to_touch: # Use the instance from dirty_resources if present (has mutated @@ -1035,7 +1034,7 @@ def perform_prowler_scan( provider_instance = Provider.objects.get(pk=provider_id) scan_instance = Scan.objects.get(pk=scan_id) scan_instance.state = StateChoices.EXECUTING - scan_instance.started_at = datetime.now(tz=timezone.utc) + scan_instance.started_at = datetime.now(tz=UTC) scan_instance.save(update_fields=["state", "started_at", "updated_at"]) # Find the mutelist processor if it exists @@ -1078,9 +1077,7 @@ def perform_prowler_scan( f"Provider {provider_instance.provider} is not connected: {e}" ) finally: - provider_instance.connection_last_checked_at = datetime.now( - tz=timezone.utc - ) + provider_instance.connection_last_checked_at = datetime.now(tz=UTC) provider_instance.save( update_fields=[ "connected", @@ -1181,7 +1178,7 @@ def perform_prowler_scan( finally: with rls_transaction(tenant_id): scan_instance.duration = time.time() - start_time - scan_instance.completed_at = datetime.now(tz=timezone.utc) + scan_instance.completed_at = datetime.now(tz=UTC) scan_instance.unique_resource_count = len(unique_resources) scan_instance.save( update_fields=[ @@ -1588,7 +1585,7 @@ def create_compliance_requirements(tenant_id: str, scan_id: str): else: requirement_stats["failed_checks"] += 1 - utc_datetime_now = datetime.now(tz=timezone.utc) + utc_datetime_now = datetime.now(tz=UTC) tenant_id_str = str(tenant_id) scan_id_str = str(scan_instance.id) @@ -2073,9 +2070,7 @@ def aggregate_finding_group_summaries(tenant_id: str, scan_id: str): summary_timestamp = scan.completed_at if django_timezone.is_naive(summary_timestamp): - summary_timestamp = django_timezone.make_aware( - summary_timestamp, timezone.utc - ) + summary_timestamp = django_timezone.make_aware(summary_timestamp, UTC) summary_timestamp = summary_timestamp.replace( hour=0, minute=0, second=0, microsecond=0 ) diff --git a/api/src/backend/tasks/jobs/threatscore.py b/api/src/backend/tasks/jobs/threatscore.py index a9a7516e55..663c179ea2 100644 --- a/api/src/backend/tasks/jobs/threatscore.py +++ b/api/src/backend/tasks/jobs/threatscore.py @@ -1,14 +1,13 @@ +from api.db_router import READ_REPLICA_ALIAS +from api.db_utils import rls_transaction +from api.models import Provider, StatusChoices from celery.utils.log import get_task_logger +from prowler.lib.check.compliance_models import Compliance from tasks.jobs.threatscore_utils import ( _aggregate_requirement_statistics_from_database, _calculate_requirements_data_from_statistics, ) -from api.db_router import READ_REPLICA_ALIAS -from api.db_utils import rls_transaction -from api.models import Provider, StatusChoices -from prowler.lib.check.compliance_models import Compliance - logger = get_task_logger(__name__) diff --git a/api/src/backend/tasks/jobs/threatscore_utils.py b/api/src/backend/tasks/jobs/threatscore_utils.py index 35fb0faeb3..2e2fb87ba5 100644 --- a/api/src/backend/tasks/jobs/threatscore_utils.py +++ b/api/src/backend/tasks/jobs/threatscore_utils.py @@ -1,13 +1,12 @@ +from api.db_router import READ_REPLICA_ALIAS +from api.db_utils import rls_transaction +from api.models import Finding, Scan, StatusChoices from celery.utils.log import get_task_logger from config.django.base import DJANGO_FINDINGS_BATCH_SIZE from django.db.models import Count, F, Q, Window from django.db.models.functions import RowNumber -from tasks.jobs.reports.config import MAX_FINDINGS_PER_CHECK - -from api.db_router import READ_REPLICA_ALIAS -from api.db_utils import rls_transaction -from api.models import Finding, Scan, StatusChoices from prowler.lib.outputs.finding import Finding as FindingOutput +from tasks.jobs.reports.config import MAX_FINDINGS_PER_CHECK logger = get_task_logger(__name__) diff --git a/api/src/backend/tasks/tasks.py b/api/src/backend/tasks/tasks.py index e1e7100ae6..e7bb0982cd 100644 --- a/api/src/backend/tasks/tasks.py +++ b/api/src/backend/tasks/tasks.py @@ -1,13 +1,29 @@ import os -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from pathlib import Path from shutil import rmtree +from api.compliance import ( + get_compliance_frameworks, + get_prowler_provider_compliance, +) +from api.db_router import READ_REPLICA_ALIAS +from api.db_utils import delete_related_daily_task, rls_transaction +from api.decorators import handle_provider_deletion, set_tenant +from api.models import Finding, Integration, Provider, Scan, ScanSummary, StateChoices +from api.utils import initialize_prowler_provider +from api.v1.serializers import ScanTaskSerializer from celery import chain, group, shared_task from celery.utils.log import get_task_logger from config.celery import RLSTask from config.django.base import DJANGO_FINDINGS_BATCH_SIZE, DJANGO_TMP_OUTPUT_DIRECTORY from django_celery_beat.models import PeriodicTask +from prowler.lib.check.compliance_models import Compliance +from prowler.lib.outputs.compliance.compliance import ( + process_universal_compliance_frameworks, +) +from prowler.lib.outputs.compliance.generic.generic import GenericCompliance +from prowler.lib.outputs.finding import Finding as FindingOutput from tasks.jobs.attack_paths import ( attack_paths_scan, can_provider_run_attack_paths_scan, @@ -15,13 +31,13 @@ from tasks.jobs.attack_paths import ( from tasks.jobs.attack_paths import db_utils as attack_paths_db_utils from tasks.jobs.attack_paths.cleanup import cleanup_stale_attack_paths_scans from tasks.jobs.backfill import ( + aggregate_scan_category_summaries, + aggregate_scan_resource_group_summaries, backfill_compliance_summaries, backfill_daily_severity_summaries, backfill_finding_group_summaries, backfill_provider_compliance_scores, backfill_resource_scan_summaries, - aggregate_scan_category_summaries, - aggregate_scan_resource_group_summaries, ) from tasks.jobs.connection import ( check_integration_connection, @@ -68,24 +84,6 @@ from tasks.utils import ( get_next_execution_datetime, ) -from api.compliance import ( - get_compliance_frameworks, - get_prowler_provider_compliance, -) -from api.db_router import READ_REPLICA_ALIAS -from api.db_utils import delete_related_daily_task, rls_transaction -from api.decorators import handle_provider_deletion, set_tenant -from api.models import Finding, Integration, Provider, Scan, ScanSummary, StateChoices -from api.utils import initialize_prowler_provider -from api.v1.serializers import ScanTaskSerializer -from prowler.lib.check.compliance_models import Compliance -from prowler.lib.outputs.compliance.compliance import ( - process_universal_compliance_frameworks, -) -from prowler.lib.outputs.compliance.generic.generic import GenericCompliance -from prowler.lib.outputs.finding import Finding as FindingOutput - - logger = get_task_logger(__name__) @@ -407,7 +405,7 @@ def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str): ) finally: with rls_transaction(tenant_id): - now = datetime.now(timezone.utc) + now = datetime.now(UTC) if next_scan_datetime <= now: interval_delta = timedelta(**{interval.period: interval.every}) while next_scan_datetime <= now: diff --git a/api/src/backend/tasks/tests/test_attack_paths_scan.py b/api/src/backend/tasks/tests/test_attack_paths_scan.py index 918e54ff6c..fef4894646 100644 --- a/api/src/backend/tasks/tests/test_attack_paths_scan.py +++ b/api/src/backend/tasks/tests/test_attack_paths_scan.py @@ -1,16 +1,9 @@ from contextlib import nullcontext -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from types import SimpleNamespace from unittest.mock import MagicMock, call, patch import pytest -from django_celery_results.models import TaskResult -from tasks.jobs.attack_paths import findings as findings_module -from tasks.jobs.attack_paths import indexes as indexes_module -from tasks.jobs.attack_paths import internet as internet_module -from tasks.jobs.attack_paths import sync as sync_module -from tasks.jobs.attack_paths.scan import run as attack_paths_run - from api.models import ( AttackPathsScan, Finding, @@ -22,7 +15,13 @@ from api.models import ( StatusChoices, Task, ) +from django_celery_results.models import TaskResult from prowler.lib.check.models import Severity +from tasks.jobs.attack_paths import findings as findings_module +from tasks.jobs.attack_paths import indexes as indexes_module +from tasks.jobs.attack_paths import internet as internet_module +from tasks.jobs.attack_paths import sync as sync_module +from tasks.jobs.attack_paths.scan import run as attack_paths_run @pytest.mark.django_db @@ -2374,7 +2373,7 @@ class TestCleanupStaleAttackPathsScans: provider=provider, scan=scan, state=StateChoices.EXECUTING, - started_at=started_at or datetime.now(tz=timezone.utc), + started_at=started_at or datetime.now(tz=UTC), ) task_result = None @@ -2467,7 +2466,7 @@ class TestCleanupStaleAttackPathsScans: provider.provider = Provider.ProviderChoices.AWS provider.save() - old_start = datetime.now(tz=timezone.utc) - timedelta(hours=49) + old_start = datetime.now(tz=UTC) - timedelta(hours=49) ap_scan, task_result = self._create_executing_scan( tenant, provider, started_at=old_start, worker="live-worker@host" ) @@ -2685,7 +2684,7 @@ class TestCleanupStaleAttackPathsScans: provider.save() # Old scan with no Task/TaskResult - old_start = datetime.now(tz=timezone.utc) - timedelta(hours=49) + old_start = datetime.now(tz=UTC) - timedelta(hours=49) ap_scan = AttackPathsScan.objects.create( tenant_id=tenant.id, provider=provider, @@ -2761,7 +2760,7 @@ class TestCleanupStaleAttackPathsScans: provider=provider, scan=parent_scan, state=StateChoices.SCHEDULED, - started_at=datetime.now(tz=timezone.utc) - timedelta(minutes=age_minutes), + started_at=datetime.now(tz=UTC) - timedelta(minutes=age_minutes), ) task_result = None diff --git a/api/src/backend/tasks/tests/test_backfill.py b/api/src/backend/tasks/tests/test_backfill.py index 3ab49a15e6..8ae39905fc 100644 --- a/api/src/backend/tasks/tests/test_backfill.py +++ b/api/src/backend/tasks/tests/test_backfill.py @@ -1,16 +1,8 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from unittest.mock import MagicMock, patch from uuid import uuid4 import pytest -from tasks.jobs.backfill import ( - backfill_compliance_summaries, - backfill_provider_compliance_scores, - backfill_resource_scan_summaries, - aggregate_scan_category_summaries, - aggregate_scan_resource_group_summaries, -) - from api.models import ( ComplianceOverviewSummary, Finding, @@ -24,6 +16,13 @@ from api.models import ( ) from prowler.lib.check.models import Severity from prowler.lib.outputs.finding import Status +from tasks.jobs.backfill import ( + aggregate_scan_category_summaries, + aggregate_scan_resource_group_summaries, + backfill_compliance_summaries, + backfill_provider_compliance_scores, + backfill_resource_scan_summaries, +) @pytest.fixture(scope="function") @@ -536,7 +535,7 @@ class TestBackfillProviderComplianceScores: scan2 = scans_fixture[1] # Set completed_at to make the scan eligible for backfill - scan.completed_at = datetime.now(timezone.utc) + scan.completed_at = datetime.now(UTC) scan.save() scan2.state = StateChoices.AVAILABLE scan2.completed_at = None diff --git a/api/src/backend/tasks/tests/test_beat.py b/api/src/backend/tasks/tests/test_beat.py index 5c25e97340..8679872164 100644 --- a/api/src/backend/tasks/tests/test_beat.py +++ b/api/src/backend/tasks/tests/test_beat.py @@ -2,11 +2,10 @@ import json from unittest.mock import patch import pytest -from django_celery_beat.models import IntervalSchedule, PeriodicTask -from tasks.beat import schedule_provider_scan - from api.exceptions import ConflictException from api.models import Scan +from django_celery_beat.models import IntervalSchedule, PeriodicTask +from tasks.beat import schedule_provider_scan @pytest.mark.django_db diff --git a/api/src/backend/tasks/tests/test_connection.py b/api/src/backend/tasks/tests/test_connection.py index e5e39d8778..e8b27e0b00 100644 --- a/api/src/backend/tasks/tests/test_connection.py +++ b/api/src/backend/tasks/tests/test_connection.py @@ -1,16 +1,15 @@ import uuid -from datetime import datetime, timezone +from datetime import UTC, datetime from unittest.mock import MagicMock, patch import pytest +from api.models import Integration, LighthouseConfiguration, Provider from tasks.jobs.connection import ( check_integration_connection, check_lighthouse_connection, check_provider_connection, ) -from api.models import Integration, LighthouseConfiguration, Provider - @pytest.mark.parametrize( "provider_data", @@ -38,7 +37,7 @@ def test_check_provider_connection( mock_provider_connection_test.assert_called_once() assert provider.connected is True assert provider.connection_last_checked_at is not None - assert provider.connection_last_checked_at <= datetime.now(tz=timezone.utc) + assert provider.connection_last_checked_at <= datetime.now(tz=UTC) @patch("tasks.jobs.connection.Provider.objects.get") diff --git a/api/src/backend/tasks/tests/test_deletion.py b/api/src/backend/tasks/tests/test_deletion.py index 0ed8c5ddb2..1124334861 100644 --- a/api/src/backend/tasks/tests/test_deletion.py +++ b/api/src/backend/tasks/tests/test_deletion.py @@ -1,11 +1,10 @@ from unittest.mock import call, patch import pytest -from django.core.exceptions import ObjectDoesNotExist -from tasks.jobs.deletion import delete_provider, delete_tenant - from api.attack_paths import database as graph_database from api.models import Provider, Tenant, TenantComplianceSummary +from django.core.exceptions import ObjectDoesNotExist +from tasks.jobs.deletion import delete_provider, delete_tenant @pytest.mark.django_db diff --git a/api/src/backend/tasks/tests/test_integrations.py b/api/src/backend/tasks/tests/test_integrations.py index e246405cdd..9cb727e8d0 100644 --- a/api/src/backend/tasks/tests/test_integrations.py +++ b/api/src/backend/tasks/tests/test_integrations.py @@ -1,7 +1,12 @@ from unittest.mock import MagicMock, patch import pytest +from api.db_router import READ_REPLICA_ALIAS, MainRouter +from api.models import Integration +from api.utils import prowler_integration_connection_test from django.db import OperationalError +from prowler.providers.aws.lib.security_hub.security_hub import SecurityHubConnection +from prowler.providers.common.models import Connection from tasks.jobs.integrations import ( get_s3_client_from_integration, get_security_hub_client_from_integration, @@ -10,12 +15,6 @@ from tasks.jobs.integrations import ( upload_security_hub_integration, ) -from api.db_router import READ_REPLICA_ALIAS, MainRouter -from api.models import Integration -from api.utils import prowler_integration_connection_test -from prowler.providers.aws.lib.security_hub.security_hub import SecurityHubConnection -from prowler.providers.common.models import Connection - @pytest.mark.django_db class TestS3IntegrationUploads: @@ -264,10 +263,9 @@ class TestS3IntegrationUploads: def test_s3_integration_rejects_invalid_output_directory_characters(self): """Test that S3 integration validation rejects invalid characters.""" - from rest_framework.exceptions import ValidationError - from api.models import Integration from api.v1.serializers import BaseWriteIntegrationSerializer + from rest_framework.exceptions import ValidationError integration_type = Integration.IntegrationChoices.AMAZON_S3 providers = [] @@ -290,10 +288,9 @@ class TestS3IntegrationUploads: def test_s3_integration_rejects_empty_output_directory(self): """Test that S3 integration validation rejects empty directories.""" - from rest_framework.exceptions import ValidationError - from api.models import Integration from api.v1.serializers import BaseWriteIntegrationSerializer + from rest_framework.exceptions import ValidationError integration_type = Integration.IntegrationChoices.AMAZON_S3 providers = [] diff --git a/api/src/backend/tasks/tests/test_muting.py b/api/src/backend/tasks/tests/test_muting.py index d8ae310f2e..2e542980bf 100644 --- a/api/src/backend/tasks/tests/test_muting.py +++ b/api/src/backend/tasks/tests/test_muting.py @@ -1,13 +1,12 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from uuid import uuid4 import pytest -from django.core.exceptions import ObjectDoesNotExist -from tasks.jobs.muting import mute_historical_findings - from api.models import Finding, MuteRule +from django.core.exceptions import ObjectDoesNotExist from prowler.lib.check.models import Severity from prowler.lib.outputs.finding import Status +from tasks.jobs.muting import mute_historical_findings @pytest.mark.django_db @@ -162,7 +161,7 @@ class TestMuteHistoricalFindings: "Description": f"Muted description {i}", }, muted=True, - muted_at=datetime.now(timezone.utc), + muted_at=datetime.now(UTC), muted_reason="Already muted", ) muted_uids.append(finding.uid) diff --git a/api/src/backend/tasks/tests/test_orphan_recovery.py b/api/src/backend/tasks/tests/test_orphan_recovery.py index abfa920b8e..b78aca4e63 100644 --- a/api/src/backend/tasks/tests/test_orphan_recovery.py +++ b/api/src/backend/tasks/tests/test_orphan_recovery.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from unittest.mock import MagicMock, patch from uuid import uuid4 @@ -6,7 +6,6 @@ import pytest from celery import states from django.test import override_settings from django_celery_results.models import TaskResult - from tasks.jobs.orphan_recovery import ( _decode_celery_field, _reconcile_task_results, @@ -29,8 +28,7 @@ def _orphan_result(*, name, kwargs, worker, created_minutes_ago, status=states.S task_args=repr([]), ) TaskResult.objects.filter(pk=tr.pk).update( - date_created=datetime.now(tz=timezone.utc) - - timedelta(minutes=created_minutes_ago) + date_created=datetime.now(tz=UTC) - timedelta(minutes=created_minutes_ago) ) tr.refresh_from_db() return tr diff --git a/api/src/backend/tasks/tests/test_reports.py b/api/src/backend/tasks/tests/test_reports.py index ad8a2ff29e..c290e6fe1d 100644 --- a/api/src/backend/tasks/tests/test_reports.py +++ b/api/src/backend/tasks/tests/test_reports.py @@ -5,10 +5,20 @@ from unittest.mock import Mock, patch import matplotlib import pytest +from api.models import ( + Finding, + Resource, + ResourceFindingMapping, + ResourceTag, + ResourceTagMapping, + StateChoices, + StatusChoices, +) +from prowler.lib.check.models import Severity from reportlab.lib import colors from tasks.jobs.report import ( - STALE_TMP_OUTPUT_MAX_AGE_HOURS, STALE_TMP_OUTPUT_LOCK_FILE_NAME, + STALE_TMP_OUTPUT_MAX_AGE_HOURS, _cleanup_stale_tmp_output_directories, _is_scan_directory_protected, _pick_latest_cis_variant, @@ -40,17 +50,6 @@ from tasks.jobs.threatscore_utils import ( _load_findings_for_requirement_checks, ) -from api.models import ( - Finding, - Resource, - ResourceFindingMapping, - ResourceTag, - ResourceTagMapping, - StateChoices, - StatusChoices, -) -from prowler.lib.check.models import Severity - matplotlib.use("Agg") # Use non-interactive backend for tests @@ -377,8 +376,8 @@ class TestLoadFindingsForChecks: finding. Without ``prefetch_related`` that's 2N additional queries; with prefetch it collapses to a small constant per iterator chunk. """ - from django.test.utils import CaptureQueriesContext from django.db import connections + from django.test.utils import CaptureQueriesContext tenant = tenants_fixture[0] scan = scans_fixture[0] @@ -539,12 +538,12 @@ class TestLoadFindingsForChecks: total_counts_out=totals, ) - assert ( - len(result[check_id]) == 5 - ), f"cap=5 should yield exactly 5 loaded findings, got {len(result[check_id])}" - assert ( - totals[check_id] == 12 - ), f"total_counts_out should report the pre-cap total (12), got {totals[check_id]}" + assert len(result[check_id]) == 5, ( + f"cap=5 should yield exactly 5 loaded findings, got {len(result[check_id])}" + ) + assert totals[check_id] == 12, ( + f"total_counts_out should report the pre-cap total (12), got {totals[check_id]}" + ) def test_only_failed_findings_pushes_down_to_sql( self, tenants_fixture, scans_fixture @@ -616,13 +615,13 @@ class TestLoadFindingsForChecks: loaded = result[check_id] assert len(loaded) == 3, f"expected 3 FAIL findings, got {len(loaded)}" statuses = {getattr(f, "status", None) for f in loaded} - assert statuses == { - StatusChoices.FAIL - }, f"expected all loaded findings to be FAIL; got statuses {statuses}" + assert statuses == {StatusChoices.FAIL}, ( + f"expected all loaded findings to be FAIL; got statuses {statuses}" + ) # total_counts must reflect the FAIL-only total, not the global total. - assert ( - totals[check_id] == 3 - ), f"total_counts should be FAIL-only (3), got {totals[check_id]}" + assert totals[check_id] == 3, ( + f"total_counts should be FAIL-only (3), got {totals[check_id]}" + ) def test_max_findings_per_check_disabled(self, tenants_fixture, scans_fixture): """``MAX_FINDINGS_PER_CHECK=0`` disables the cap; load all rows.""" @@ -1205,6 +1204,7 @@ class TestGenerateComplianceReportsOptimized: ThreatScore finishes, before ENS runs. """ from types import SimpleNamespace + from tasks.jobs import report as report_mod mock_scan_summary_filter.return_value.exists.return_value = True @@ -1259,12 +1259,12 @@ class TestGenerateComplianceReportsOptimized: # ``tsc_only`` was exclusive to ThreatScore → evicted before ENS ran. # ``shared`` is still pending for ENS → must remain. - assert ( - "tsc_only" not in observed_state["cache_keys_when_ens_runs"] - ), "tsc_only should have been evicted before ENS ran" - assert ( - "shared" in observed_state["cache_keys_when_ens_runs"] - ), "shared must remain in cache because ENS still needs it" + assert "tsc_only" not in observed_state["cache_keys_when_ens_runs"], ( + "tsc_only should have been evicted before ENS ran" + ) + assert "shared" in observed_state["cache_keys_when_ens_runs"], ( + "shared must remain in cache because ENS still needs it" + ) @patch("tasks.jobs.report.initialize_prowler_provider") @patch("tasks.jobs.report.rmtree") diff --git a/api/src/backend/tasks/tests/test_reports_cis.py b/api/src/backend/tasks/tests/test_reports_cis.py index 2d4528c82d..31e5a5495f 100644 --- a/api/src/backend/tasks/tests/test_reports_cis.py +++ b/api/src/backend/tasks/tests/test_reports_cis.py @@ -1,6 +1,7 @@ from unittest.mock import Mock, patch import pytest +from api.models import StatusChoices from reportlab.platypus import Image, LongTable, Paragraph, Table from tasks.jobs.reports import FRAMEWORK_REGISTRY, ComplianceData, RequirementData from tasks.jobs.reports.cis import ( @@ -9,8 +10,6 @@ from tasks.jobs.reports.cis import ( _profile_badge_text, ) -from api.models import StatusChoices - # ============================================================================= # Fixtures # ============================================================================= diff --git a/api/src/backend/tasks/tests/test_reports_threatscore.py b/api/src/backend/tasks/tests/test_reports_threatscore.py index c79c0b16e9..07dd654a05 100644 --- a/api/src/backend/tasks/tests/test_reports_threatscore.py +++ b/api/src/backend/tasks/tests/test_reports_threatscore.py @@ -2,6 +2,7 @@ import io from unittest.mock import Mock import pytest +from api.models import StatusChoices from reportlab.platypus import Image, PageBreak, Paragraph, Table from tasks.jobs.reports import ( FRAMEWORK_REGISTRY, @@ -10,8 +11,6 @@ from tasks.jobs.reports import ( ThreatScoreReportGenerator, ) -from api.models import StatusChoices - # ============================================================================= # Fixtures # ============================================================================= diff --git a/api/src/backend/tasks/tests/test_scan.py b/api/src/backend/tasks/tests/test_scan.py index 130d8ae118..17b3b65fc4 100644 --- a/api/src/backend/tasks/tests/test_scan.py +++ b/api/src/backend/tasks/tests/test_scan.py @@ -3,11 +3,26 @@ import json import re import uuid from contextlib import contextmanager -from datetime import datetime, timezone +from datetime import UTC, datetime from io import StringIO from unittest.mock import MagicMock, patch import pytest +from api.db_router import MainRouter +from api.exceptions import ProviderConnectionError +from api.models import ( + Finding, + MuteRule, + Provider, + Resource, + ResourceScanSummary, + Scan, + ScanSummary, + StateChoices, + StatusChoices, +) +from prowler.lib.check.models import Severity +from prowler.lib.outputs.finding import Status from tasks.jobs.scan import ( _ATTACK_SURFACE_MAPPING_CACHE, _aggregate_findings_by_region, @@ -29,22 +44,6 @@ from tasks.jobs.scan import ( ) from tasks.utils import CustomEncoder -from api.db_router import MainRouter -from api.exceptions import ProviderConnectionError -from api.models import ( - Finding, - MuteRule, - Provider, - Resource, - ResourceScanSummary, - Scan, - ScanSummary, - StateChoices, - StatusChoices, -) -from prowler.lib.check.models import Severity -from prowler.lib.outputs.finding import Status - @contextmanager def noop_rls_transaction(*args, **kwargs): @@ -1335,9 +1334,9 @@ class TestPerformScan: ) # Capture time before and after scan - before_scan = datetime.now(timezone.utc) + before_scan = datetime.now(UTC) perform_prowler_scan(tenant_id, scan_id, provider_id, []) - after_scan = datetime.now(timezone.utc) + after_scan = datetime.now(UTC) # Verify muted_at is within the scan time window finding_db = Finding.objects.get(uid=finding_uid) @@ -1473,7 +1472,7 @@ class TestProcessFindingMicroBatch: partition="aws-old", ) - previous_first_seen = datetime(2024, 1, 1, tzinfo=timezone.utc) + previous_first_seen = datetime(2024, 1, 1, tzinfo=UTC) finding = FakeFinding( uid="finding-muted", @@ -2123,7 +2122,7 @@ class TestCreateComplianceRequirements: tags={}, check_id=existing_finding.check_id, check_metadata={"CheckId": existing_finding.check_id}, - first_seen_at=datetime.now(timezone.utc), + first_seen_at=datetime.now(UTC), muted=False, ) resource = existing_finding.resources.first() @@ -2313,7 +2312,7 @@ class TestComplianceRequirementCopy: def test_persist_compliance_requirement_rows_fallback( self, mock_copy, mock_rls_transaction, mock_bulk_create ): - inserted_at = datetime.now(timezone.utc) + inserted_at = datetime.now(UTC) row = { "id": uuid.uuid4(), "tenant_id": str(uuid.uuid4()), @@ -2394,7 +2393,7 @@ class TestComplianceRequirementCopy: tenant_id = str(uuid.uuid4()) scan_id = uuid.uuid4() - inserted_at = datetime.now(timezone.utc) + inserted_at = datetime.now(UTC) rows = [ { @@ -2644,10 +2643,10 @@ class TestComplianceRequirementCopy: # Note: inserted_at is intentionally missing } - before_call = datetime.now(timezone.utc) + before_call = datetime.now(UTC) with patch.object(MainRouter, "admin_db", "admin"): _copy_compliance_requirement_rows(str(row["tenant_id"]), [row]) - after_call = datetime.now(timezone.utc) + after_call = datetime.now(UTC) csv_rows = list(csv.reader(StringIO(captured["data"]))) assert len(csv_rows) == 1 @@ -2811,7 +2810,7 @@ class TestComplianceRequirementCopy: { "id": uuid.uuid4(), "tenant_id": tenant_id, - "inserted_at": datetime.now(timezone.utc), + "inserted_at": datetime.now(UTC), "compliance_id": "test", "framework": "Test", "version": "1.0", @@ -2846,7 +2845,7 @@ class TestComplianceRequirementCopy: row = { "id": uuid.uuid4(), "tenant_id": tenant_id, - "inserted_at": datetime.now(timezone.utc), + "inserted_at": datetime.now(UTC), "compliance_id": "test", "framework": "Test", "version": "1.0", @@ -2886,7 +2885,7 @@ class TestComplianceRequirementCopy: """Test ORM fallback with multiple rows.""" tenant_id = str(uuid.uuid4()) scan_id = uuid.uuid4() - inserted_at = datetime.now(timezone.utc) + inserted_at = datetime.now(UTC) rows = [ { @@ -2968,7 +2967,7 @@ class TestComplianceRequirementCopy: tenant_id = str(uuid.uuid4()) row_id = uuid.uuid4() scan_id = uuid.uuid4() - inserted_at = datetime.now(timezone.utc) + inserted_at = datetime.now(UTC) row = { "id": row_id, @@ -4461,7 +4460,7 @@ class TestUpdateProviderComplianceScores: scan_id = str(scan.id) scan.state = StateChoices.COMPLETED - scan.completed_at = datetime.now(timezone.utc) + scan.completed_at = datetime.now(UTC) scan.save() connection = MagicMock() @@ -4538,7 +4537,7 @@ class TestUpdateProviderComplianceScores: scan_id = str(scan.id) scan.state = StateChoices.COMPLETED - scan.completed_at = datetime.now(timezone.utc) + scan.completed_at = datetime.now(UTC) scan.save() connection = MagicMock() @@ -4591,9 +4590,10 @@ class TestScanIsFullScope: # If the SDK adds a new filter, this test still passes via the # introspection-driven derivation; if it adds a non-filter kwarg # (e.g. provider-like), keep the exclusion list in sync in models.py. - from prowler.lib.scan.scan import Scan as ProwlerScan import inspect + from prowler.lib.scan.scan import Scan as ProwlerScan + expected = tuple( name for name in inspect.signature(ProwlerScan.__init__).parameters diff --git a/api/src/backend/tasks/tests/test_tasks.py b/api/src/backend/tasks/tests/test_tasks.py index 67d2c64555..95634e8a95 100644 --- a/api/src/backend/tasks/tests/test_tasks.py +++ b/api/src/backend/tasks/tests/test_tasks.py @@ -1,10 +1,18 @@ import uuid from contextlib import contextmanager -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from unittest.mock import MagicMock, patch import openai import pytest +from api.models import ( + Integration, + LighthouseProviderConfiguration, + LighthouseProviderModels, + Scan, + StateChoices, + Task, +) from botocore.exceptions import ClientError from django_celery_beat.models import IntervalSchedule, PeriodicTask from django_celery_results.models import TaskResult @@ -31,15 +39,6 @@ from tasks.tasks import ( security_hub_integration_task, ) -from api.models import ( - Integration, - LighthouseProviderConfiguration, - LighthouseProviderModels, - Scan, - StateChoices, - Task, -) - @pytest.mark.django_db class TestExtractBedrockCredentials: @@ -1478,9 +1477,9 @@ class TestCheckIntegrationsTask: ) # Verify ASFF was NOT created for non-AWS provider - assert ( - "asff" not in created_writers - ), "ASFF writer should NOT be created for non-AWS providers" + assert "asff" not in created_writers, ( + "ASFF writer should NOT be created for non-AWS providers" + ) assert "csv" in created_writers, "CSV writer should be created" assert "ocsf" in created_writers, "OCSF writer should be created" @@ -2328,7 +2327,7 @@ class TestPerformScheduledScanTask: task_id=task_id, task_name="scan-perform-scheduled", status="STARTED", - date_created=datetime.now(timezone.utc), + date_created=datetime.now(UTC), ) Task.objects.create( id=task_id, task_runner_task=task_result, tenant_id=tenant_id @@ -2416,7 +2415,7 @@ class TestPerformScheduledScanTask: state=StateChoices.SCHEDULED, ) assert scheduled_scans.count() == 1 - assert scheduled_scans.first().scheduled_at > datetime.now(timezone.utc) + assert scheduled_scans.first().scheduled_at > datetime.now(UTC) assert ( Scan.objects.filter( tenant_id=tenant.id, @@ -2452,7 +2451,7 @@ class TestPerformScheduledScanTask: name="Daily scheduled scan", trigger=Scan.TriggerChoices.SCHEDULED, state=StateChoices.SCHEDULED, - scheduled_at=datetime.now(timezone.utc), + scheduled_at=datetime.now(UTC), scheduler_task_id=periodic_task.id, ) duplicate_scan = Scan.objects.create( @@ -2582,7 +2581,7 @@ class TestReaggregateAllFindingGroupSummaries: scan_id_today_p1 = uuid.uuid4() scan_id_yesterday_p1 = uuid.uuid4() scan_id_today_p2 = uuid.uuid4() - today = datetime.now(tz=timezone.utc) + today = datetime.now(tz=UTC) yesterday = today - timedelta(days=1) mock_outer_group_result = MagicMock() @@ -2663,7 +2662,7 @@ class TestReaggregateAllFindingGroupSummaries: provider_id = uuid.uuid4() latest_scan_today = uuid.uuid4() earlier_scan_today = uuid.uuid4() - today_late = datetime.now(tz=timezone.utc) + today_late = datetime.now(tz=UTC) today_early = today_late - timedelta(hours=4) mock_outer_group_result = MagicMock() diff --git a/api/src/backend/tasks/tests/test_utils.py b/api/src/backend/tasks/tests/test_utils.py index cc7b7f188b..e6b5544a1c 100644 --- a/api/src/backend/tasks/tests/test_utils.py +++ b/api/src/backend/tasks/tests/test_utils.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from unittest.mock import patch import pytest @@ -29,7 +29,7 @@ class TestGetNextExecutionDatetime: task_id="abc123", task_name="scan-perform-scheduled", status="SUCCESS", - date_created=datetime.now(timezone.utc) - timedelta(hours=1), + date_created=datetime.now(UTC) - timedelta(hours=1), result="Success", ) return task_result diff --git a/api/src/backend/tasks/utils.py b/api/src/backend/tasks/utils.py index eded5bfb9a..26bc031d7b 100644 --- a/api/src/backend/tasks/utils.py +++ b/api/src/backend/tasks/utils.py @@ -1,12 +1,11 @@ import json -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from enum import Enum +from api.models import Scan, StateChoices from django_celery_beat.models import PeriodicTask from django_celery_results.models import TaskResult -from api.models import Scan, StateChoices - SCHEDULED_SCAN_NAME = "Daily scheduled scan" @@ -45,9 +44,9 @@ def get_next_execution_datetime(task_id: int, provider_id: str) -> datetime: interval = periodic_task_instance.interval current_scheduled_time = datetime.combine( - datetime.now(timezone.utc).date(), + datetime.now(UTC).date(), task_instance.date_created.time(), - tzinfo=timezone.utc, + tzinfo=UTC, ) return current_scheduled_time + timedelta(**{interval.period: interval.every}) diff --git a/api/tests/performance/scenarios/compliance.py b/api/tests/performance/scenarios/compliance.py index ae4e030a59..9692c3aac2 100644 --- a/api/tests/performance/scenarios/compliance.py +++ b/api/tests/performance/scenarios/compliance.py @@ -90,7 +90,7 @@ class APIUser(APIUserBase): def compliance_overviews_default(self): provider_type, scan_info = _get_random_scan() name = f"/compliance-overviews ({provider_type})" - endpoint = f"/compliance-overviews?" f"filter[scan_id]={scan_info['scan_id']}" + endpoint = f"/compliance-overviews?filter[scan_id]={scan_info['scan_id']}" self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) @task(2) @@ -122,7 +122,6 @@ class APIUser(APIUserBase): compliance_id = _get_random_compliance_id(provider_type) name = f"/compliance-overviews/attributes ({compliance_id})" endpoint = ( - f"/compliance-overviews/attributes" - f"?filter[compliance_id]={compliance_id}" + f"/compliance-overviews/attributes?filter[compliance_id]={compliance_id}" ) self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) diff --git a/api/tests/performance/scenarios/findings.py b/api/tests/performance/scenarios/findings.py index acd32f2497..7741d1df4c 100644 --- a/api/tests/performance/scenarios/findings.py +++ b/api/tests/performance/scenarios/findings.py @@ -80,7 +80,7 @@ class APIUser(APIUserBase): @task(3) def findings_metadata(self): - endpoint = f"/findings/metadata?" f"filter[inserted_at]={TARGET_INSERTED_AT}" + endpoint = f"/findings/metadata?filter[inserted_at]={TARGET_INSERTED_AT}" self.client.get( endpoint, headers=get_auth_headers(self.token), name="/findings/metadata" ) @@ -98,7 +98,7 @@ class APIUser(APIUserBase): @task def findings_metadata_scan_small(self): - endpoint = f"/findings/metadata?" f"&filter[scan]={self.s_scan_id}" + endpoint = f"/findings/metadata?&filter[scan]={self.s_scan_id}" self.client.get( endpoint, headers=get_auth_headers(self.token), @@ -118,7 +118,7 @@ class APIUser(APIUserBase): @task def findings_metadata_scan_medium(self): - endpoint = f"/findings/metadata?" f"&filter[scan]={self.m_scan_id}" + endpoint = f"/findings/metadata?&filter[scan]={self.m_scan_id}" self.client.get( endpoint, headers=get_auth_headers(self.token), @@ -150,7 +150,7 @@ class APIUser(APIUserBase): @task def findings_metadata_scan_large(self): - endpoint = f"/findings/metadata?" f"&filter[scan]={self.l_scan_id}" + endpoint = f"/findings/metadata?&filter[scan]={self.l_scan_id}" self.client.get( endpoint, headers=get_auth_headers(self.token), diff --git a/api/tests/performance/scenarios/resources.py b/api/tests/performance/scenarios/resources.py index dabf99c3f2..43e13fad92 100644 --- a/api/tests/performance/scenarios/resources.py +++ b/api/tests/performance/scenarios/resources.py @@ -97,7 +97,7 @@ class APIUser(APIUserBase): name = "/resources?filter[scan_id] - 50k" page_number = self._next_page(name) endpoint = ( - f"/resources?page[number]={page_number}" f"&filter[scan]={self.s_scan_id}" + f"/resources?page[number]={page_number}&filter[scan]={self.s_scan_id}" ) self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) @@ -116,7 +116,7 @@ class APIUser(APIUserBase): name = "/resources?filter[scan_id] - 250k" page_number = self._next_page(name) endpoint = ( - f"/resources?page[number]={page_number}" f"&filter[scan]={self.m_scan_id}" + f"/resources?page[number]={page_number}&filter[scan]={self.m_scan_id}" ) self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) @@ -135,7 +135,7 @@ class APIUser(APIUserBase): name = "/resources?filter[scan_id] - 500k" page_number = self._next_page(name) endpoint = ( - f"/resources?page[number]={page_number}" f"&filter[scan]={self.l_scan_id}" + f"/resources?page[number]={page_number}&filter[scan]={self.l_scan_id}" ) self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) diff --git a/api/tests/performance/utils/helpers.py b/api/tests/performance/utils/helpers.py index 08144a5c4b..29ce2a47fc 100644 --- a/api/tests/performance/utils/helpers.py +++ b/api/tests/performance/utils/helpers.py @@ -153,9 +153,9 @@ def get_dynamic_filters_pairs( f"{host}/{endpoint}/metadata?{metadata_filters}", headers=get_auth_headers(token), ) - assert ( - response.status_code == 200 - ), f"Failed to get resource filters values: {response.text}" + assert response.status_code == 200, ( + f"Failed to get resource filters values: {response.text}" + ) attributes = response.json()["data"]["attributes"] return { diff --git a/api/uv.lock b/api/uv.lock index 38c3b4de74..06b1968c9a 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -327,7 +327,7 @@ constraints = [ { name = "rpds-py", specifier = "==0.30.0" }, { name = "rsa", specifier = "==4.9.1" }, { name = "ruamel-yaml", specifier = "==0.19.1" }, - { name = "ruff", specifier = "==0.5.0" }, + { name = "ruff", specifier = "==0.15.11" }, { name = "s3transfer", specifier = "==0.14.0" }, { name = "scaleway", specifier = "==2.10.3" }, { name = "scaleway-core", specifier = "==2.10.3" }, @@ -4666,7 +4666,7 @@ dev = [ { name = "pytest-env", specifier = "==1.1.3" }, { name = "pytest-randomly", specifier = "==3.15.0" }, { name = "pytest-xdist", specifier = "==3.6.1" }, - { name = "ruff", specifier = "==0.5.0" }, + { name = "ruff", specifier = "==0.15.11" }, { name = "tqdm", specifier = "==4.67.1" }, { name = "vulture", specifier = "==2.14" }, ] @@ -5425,27 +5425,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.5.0" +version = "0.15.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/28/9a/dde343d95ecd0747207e4e8d143c373ef961cbd6b78c61a659f67582dbd2/ruff-0.5.0.tar.gz", hash = "sha256:eb641b5873492cf9bd45bc9c5ae5320648218e04386a5f0c264ad6ccce8226a1", size = 2587996, upload-time = "2024-06-27T15:42:16.137Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/5d/0d9510720d61df753df39bf24a96d6c141080c94fe6025568747fbea856a/ruff-0.5.0-py3-none-linux_armv6l.whl", hash = "sha256:ee770ea8ab38918f34e7560a597cc0a8c9a193aaa01bfbd879ef43cb06bd9c4c", size = 9434156, upload-time = "2024-06-27T15:40:40.824Z" }, - { url = "https://files.pythonhosted.org/packages/be/5a/7f466f5449dce168c2d956ad4a207d62dc7b76836d46f1c04249a4daaf34/ruff-0.5.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38f3b8327b3cb43474559d435f5fa65dacf723351c159ed0dc567f7ab735d1b6", size = 8536948, upload-time = "2024-06-27T15:40:47.907Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a2/afc6952d5a0199e7e6c0a2051d6f4780fb70376f5bd07f27838f8bc0cf47/ruff-0.5.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7594f8df5404a5c5c8f64b8311169879f6cf42142da644c7e0ba3c3f14130370", size = 8107163, upload-time = "2024-06-27T15:41:06.887Z" }, - { url = "https://files.pythonhosted.org/packages/34/54/ea77237405b7573298f5cc00045d1aceab609841d3cc88de3d7c3d2a6163/ruff-0.5.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adc7012d6ec85032bc4e9065110df205752d64010bed5f958d25dbee9ce35de3", size = 9877009, upload-time = "2024-06-27T15:41:11.28Z" }, - { url = "https://files.pythonhosted.org/packages/56/db/3f74873bc0ca915f79d26575e549eb5e633022d56315d314e6f9c0fa596a/ruff-0.5.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d505fb93b0fabef974b168d9b27c3960714d2ecda24b6ffa6a87ac432905ea38", size = 9219926, upload-time = "2024-06-27T15:41:15.032Z" }, - { url = "https://files.pythonhosted.org/packages/57/08/1052c80f3f44321631a8c1337e55883dd7a7b02b4efe5c9282258db42358/ruff-0.5.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dc5cfd3558f14513ed0d5b70ce531e28ea81a8a3b1b07f0f48421a3d9e7d80a", size = 10031146, upload-time = "2024-06-27T15:41:20.601Z" }, - { url = "https://files.pythonhosted.org/packages/8f/a2/f7c01c4a02b87998c9e1379ec8d7345d6a45f8b34e326e8700c13da391c3/ruff-0.5.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:db3ca35265de239a1176d56a464b51557fce41095c37d6c406e658cf80bbb362", size = 10770796, upload-time = "2024-06-27T15:41:24.665Z" }, - { url = "https://files.pythonhosted.org/packages/12/a1/5f45ab0948a202da7fe13c6e0678f907bd88caacc7e4f4909603d3774051/ruff-0.5.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1a321c4f68809fddd9b282fab6a8d8db796b270fff44722589a8b946925a2a8", size = 10364804, upload-time = "2024-06-27T15:41:29.153Z" }, - { url = "https://files.pythonhosted.org/packages/7e/40/83f88d5bda41496a90871ec82dd82545def4c4683e1c2f4a42f5a168ae3e/ruff-0.5.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c4dfcd8d34b143916994b3876b63d53f56724c03f8c1a33a253b7b1e6bf2a7d", size = 11241308, upload-time = "2024-06-27T15:41:33.569Z" }, - { url = "https://files.pythonhosted.org/packages/af/79/8a57016a761d11491b913460a3d1545cdbe96dca6acb1279102814c9147b/ruff-0.5.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81e5facfc9f4a674c6a78c64d38becfbd5e4f739c31fcd9ce44c849f1fad9e4c", size = 10064506, upload-time = "2024-06-27T15:41:38.21Z" }, - { url = "https://files.pythonhosted.org/packages/67/34/fd7cd8be0d8cd4bcce0dbef807933f6c9685d5dc2549b729da7ee7a7a5cc/ruff-0.5.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e589e27971c2a3efff3fadafb16e5aef7ff93250f0134ec4b52052b673cf988d", size = 9866155, upload-time = "2024-06-27T15:41:42.551Z" }, - { url = "https://files.pythonhosted.org/packages/7b/54/8a654417265fe91de3ff303274a9d4d64774496eaa2eadd7da8e88a48b82/ruff-0.5.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2ffbc3715a52b037bcb0f6ff524a9367f642cdc5817944f6af5479bbb2eb50e", size = 9285874, upload-time = "2024-06-27T15:41:46.991Z" }, - { url = "https://files.pythonhosted.org/packages/86/39/564161e306b12ab40d2b6be0a0bc843c692a8295cc7101fa930db89e1e7e/ruff-0.5.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cd096e23c6a4f9c819525a437fa0a99d1c67a1b6bb30948d46f33afbc53596cf", size = 9645133, upload-time = "2024-06-27T15:41:52.535Z" }, - { url = "https://files.pythonhosted.org/packages/3b/67/3203d56ee41d3dee8d94c7926b298b13a150f105a55fef38b75ccf5e0901/ruff-0.5.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:46e193b36f2255729ad34a49c9a997d506e58f08555366b2108783b3064a0e1e", size = 10143022, upload-time = "2024-06-27T15:41:56.879Z" }, - { url = "https://files.pythonhosted.org/packages/71/2e/1bab3c5a3929f348cdc086a3f3013ea0b8823ec3d273f3334ef621f4f83f/ruff-0.5.0-py3-none-win32.whl", hash = "sha256:49141d267100f5ceff541b4e06552e98527870eafa1acc9dec9139c9ec5af64c", size = 7735210, upload-time = "2024-06-27T15:42:02.173Z" }, - { url = "https://files.pythonhosted.org/packages/48/05/04bf25784ba73abf0e639065fd7a785c005c895c4bf64aa2729d26a1984f/ruff-0.5.0-py3-none-win_amd64.whl", hash = "sha256:e9118f60091047444c1b90952736ee7b1792910cab56e9b9a9ac20af94cd0440", size = 8536440, upload-time = "2024-06-27T15:42:08.275Z" }, - { url = "https://files.pythonhosted.org/packages/63/ab/a10ab4a751514d4f954079fbd2f645cc0c5982a18f510ab411048a2a5409/ruff-0.5.0-py3-none-win_arm64.whl", hash = "sha256:ed5c4df5c1fb4518abcb57725b576659542bdbe93366f4f329e8f398c4b71178", size = 7949476, upload-time = "2024-06-27T15:42:12.464Z" }, + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, ] [[package]] diff --git a/mcp_server/prowler_mcp_server/prowler_app/models/attack_paths.py b/mcp_server/prowler_mcp_server/prowler_app/models/attack_paths.py index cfd704eeca..bbe2eb7401 100644 --- a/mcp_server/prowler_mcp_server/prowler_app/models/attack_paths.py +++ b/mcp_server/prowler_mcp_server/prowler_app/models/attack_paths.py @@ -12,9 +12,10 @@ for optimal LLM token usage. from typing import Any, Literal -from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin from pydantic import BaseModel, ConfigDict, Field +from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin + class AttackPathScan(MinimalSerializerMixin, BaseModel): """Simplified attack paths scan representation for list operations. diff --git a/mcp_server/prowler_mcp_server/prowler_app/models/compliance.py b/mcp_server/prowler_mcp_server/prowler_app/models/compliance.py index 4dfe5c4839..c68bd0df85 100644 --- a/mcp_server/prowler_mcp_server/prowler_app/models/compliance.py +++ b/mcp_server/prowler_mcp_server/prowler_app/models/compliance.py @@ -2,7 +2,6 @@ from typing import Any, Literal -from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin from pydantic import ( BaseModel, ConfigDict, @@ -11,6 +10,8 @@ from pydantic import ( model_serializer, ) +from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin + class ComplianceRequirementAttribute(MinimalSerializerMixin, BaseModel): """Requirement attributes including associated check IDs. diff --git a/mcp_server/prowler_mcp_server/prowler_app/models/finding_groups.py b/mcp_server/prowler_mcp_server/prowler_app/models/finding_groups.py index 9ff9822977..ae8431ba63 100644 --- a/mcp_server/prowler_mcp_server/prowler_app/models/finding_groups.py +++ b/mcp_server/prowler_mcp_server/prowler_app/models/finding_groups.py @@ -6,7 +6,6 @@ from pydantic import Field from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin - FindingStatus = Literal["FAIL", "PASS", "MANUAL"] FindingSeverity = Literal["critical", "high", "medium", "low", "informational"] FindingDelta = Literal["new", "changed"] @@ -76,9 +75,7 @@ class SimplifiedFindingGroup(MinimalSerializerMixin): ) muted_count: int = Field(description="Total muted findings in this group", ge=0) new_count: int = Field(description="Number of new non-muted findings", ge=0) - changed_count: int = Field( - description="Number of changed non-muted findings", ge=0 - ) + changed_count: int = Field(description="Number of changed non-muted findings", ge=0) first_seen_at: str | None = Field( default=None, description="First time this group was detected" ) @@ -109,18 +106,12 @@ class DetailedFindingGroup(SimplifiedFindingGroup): new_pass_count: int = Field(description="New non-muted PASS findings", ge=0) new_pass_muted_count: int = Field(description="New muted PASS findings", ge=0) new_manual_count: int = Field(description="New non-muted MANUAL findings", ge=0) - new_manual_muted_count: int = Field( - description="New muted MANUAL findings", ge=0 - ) - changed_fail_count: int = Field( - description="Changed non-muted FAIL findings", ge=0 - ) + new_manual_muted_count: int = Field(description="New muted MANUAL findings", ge=0) + changed_fail_count: int = Field(description="Changed non-muted FAIL findings", ge=0) changed_fail_muted_count: int = Field( description="Changed muted FAIL findings", ge=0 ) - changed_pass_count: int = Field( - description="Changed non-muted PASS findings", ge=0 - ) + changed_pass_count: int = Field(description="Changed non-muted PASS findings", ge=0) changed_pass_muted_count: int = Field( description="Changed muted PASS findings", ge=0 ) diff --git a/mcp_server/prowler_mcp_server/prowler_app/models/findings.py b/mcp_server/prowler_mcp_server/prowler_app/models/findings.py index 5ef8702ce4..1373e36294 100644 --- a/mcp_server/prowler_mcp_server/prowler_app/models/findings.py +++ b/mcp_server/prowler_mcp_server/prowler_app/models/findings.py @@ -2,9 +2,10 @@ from typing import Literal -from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin from pydantic import BaseModel, ConfigDict, Field +from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin + class CheckRemediation(MinimalSerializerMixin, BaseModel): """Remediation information for a security check.""" diff --git a/mcp_server/prowler_mcp_server/prowler_app/models/muting.py b/mcp_server/prowler_mcp_server/prowler_app/models/muting.py index 779acdde5e..e50a150d20 100644 --- a/mcp_server/prowler_mcp_server/prowler_app/models/muting.py +++ b/mcp_server/prowler_mcp_server/prowler_app/models/muting.py @@ -2,9 +2,10 @@ from typing import Any -from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin from pydantic import BaseModel, ConfigDict, Field +from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin + class MutelistResponse(MinimalSerializerMixin, BaseModel): """Simplified mutelist response with Prowler configuration. diff --git a/mcp_server/prowler_mcp_server/prowler_app/models/providers.py b/mcp_server/prowler_mcp_server/prowler_app/models/providers.py index 07f3be1d37..af9509a963 100644 --- a/mcp_server/prowler_mcp_server/prowler_app/models/providers.py +++ b/mcp_server/prowler_mcp_server/prowler_app/models/providers.py @@ -2,9 +2,10 @@ from typing import Any, Literal -from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin from pydantic import BaseModel +from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin + class SimplifiedProvider(MinimalSerializerMixin, BaseModel): """Simplified provider for list/search operations.""" diff --git a/mcp_server/prowler_mcp_server/prowler_app/models/resources.py b/mcp_server/prowler_mcp_server/prowler_app/models/resources.py index 823134794f..13cb1ed4dd 100644 --- a/mcp_server/prowler_mcp_server/prowler_app/models/resources.py +++ b/mcp_server/prowler_mcp_server/prowler_app/models/resources.py @@ -1,8 +1,9 @@ """Pydantic models for simplified resources responses.""" -from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin from pydantic import BaseModel +from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin + class SimplifiedResource(MinimalSerializerMixin, BaseModel): """Simplified resource with only LLM-relevant information for list operations.""" diff --git a/mcp_server/prowler_mcp_server/prowler_app/models/scans.py b/mcp_server/prowler_mcp_server/prowler_app/models/scans.py index 696ac7837d..f8eef988ce 100644 --- a/mcp_server/prowler_mcp_server/prowler_app/models/scans.py +++ b/mcp_server/prowler_mcp_server/prowler_app/models/scans.py @@ -11,9 +11,10 @@ for optimal LLM token usage. from typing import Any, Literal -from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin from pydantic import BaseModel, ConfigDict, Field +from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin + class SimplifiedScan(MinimalSerializerMixin, BaseModel): """Simplified scan representation for list operations. diff --git a/mcp_server/prowler_mcp_server/prowler_app/server.py b/mcp_server/prowler_mcp_server/prowler_app/server.py index 39b22d095c..e8e854144f 100644 --- a/mcp_server/prowler_mcp_server/prowler_app/server.py +++ b/mcp_server/prowler_mcp_server/prowler_app/server.py @@ -1,4 +1,5 @@ from fastmcp import FastMCP + from prowler_mcp_server.prowler_app.utils.tool_loader import load_all_tools # Initialize MCP server diff --git a/mcp_server/prowler_mcp_server/prowler_app/tools/attack_paths.py b/mcp_server/prowler_mcp_server/prowler_app/tools/attack_paths.py index ff9b8045a4..b08bbfe01f 100644 --- a/mcp_server/prowler_mcp_server/prowler_app/tools/attack_paths.py +++ b/mcp_server/prowler_mcp_server/prowler_app/tools/attack_paths.py @@ -7,6 +7,8 @@ through cloud infrastructure relationships. from typing import Any, Literal +from pydantic import Field + from prowler_mcp_server.prowler_app.models.attack_paths import ( AttackPathCartographySchema, AttackPathQuery, @@ -14,7 +16,6 @@ from prowler_mcp_server.prowler_app.models.attack_paths import ( AttackPathScansListResponse, ) from prowler_mcp_server.prowler_app.tools.base import BaseTool -from pydantic import Field class AttackPathsTools(BaseTool): diff --git a/mcp_server/prowler_mcp_server/prowler_app/tools/compliance.py b/mcp_server/prowler_mcp_server/prowler_app/tools/compliance.py index 81f16a83e9..360dd5510d 100644 --- a/mcp_server/prowler_mcp_server/prowler_app/tools/compliance.py +++ b/mcp_server/prowler_mcp_server/prowler_app/tools/compliance.py @@ -6,13 +6,14 @@ across all cloud providers. from typing import Any +from pydantic import Field + from prowler_mcp_server.prowler_app.models.compliance import ( ComplianceFrameworksListResponse, ComplianceRequirementAttributesListResponse, ComplianceRequirementsListResponse, ) from prowler_mcp_server.prowler_app.tools.base import BaseTool -from pydantic import Field class ComplianceTools(BaseTool): diff --git a/mcp_server/prowler_mcp_server/prowler_app/tools/finding_groups.py b/mcp_server/prowler_mcp_server/prowler_app/tools/finding_groups.py index 363e45d970..905a352740 100644 --- a/mcp_server/prowler_mcp_server/prowler_app/tools/finding_groups.py +++ b/mcp_server/prowler_mcp_server/prowler_app/tools/finding_groups.py @@ -15,7 +15,6 @@ from prowler_mcp_server.prowler_app.models.finding_groups import ( ) from prowler_mcp_server.prowler_app.tools.base import BaseTool - StatusFilter = Literal["FAIL", "PASS", "MANUAL"] SeverityFilter = Literal["critical", "high", "medium", "low", "informational"] DeltaFilter = Literal["new", "changed"] @@ -464,9 +463,7 @@ class FindingGroupsTools(BaseTool): clean_params = self.api_client.build_filter_params(params) api_response = await self.api_client.get(endpoint, params=clean_params) - response = FindingGroupResourcesListResponse.from_api_response( - api_response - ) + response = FindingGroupResourcesListResponse.from_api_response(api_response) return response.model_dump() except Exception as e: self.logger.error(f"Error listing finding group resources: {e}") diff --git a/mcp_server/prowler_mcp_server/prowler_app/tools/resources.py b/mcp_server/prowler_mcp_server/prowler_app/tools/resources.py index 042232e6a5..011e013b91 100644 --- a/mcp_server/prowler_mcp_server/prowler_app/tools/resources.py +++ b/mcp_server/prowler_mcp_server/prowler_app/tools/resources.py @@ -6,6 +6,8 @@ across all providers. from typing import Any +from pydantic import Field + from prowler_mcp_server.prowler_app.models.resources import ( DetailedResource, ResourceEventsResponse, @@ -13,7 +15,6 @@ from prowler_mcp_server.prowler_app.models.resources import ( ResourcesMetadataResponse, ) from prowler_mcp_server.prowler_app.tools.base import BaseTool -from pydantic import Field class ResourcesTools(BaseTool): diff --git a/mcp_server/prowler_mcp_server/prowler_app/utils/api_client.py b/mcp_server/prowler_mcp_server/prowler_app/utils/api_client.py index 4b6d0e77f5..a6aacc3ce1 100644 --- a/mcp_server/prowler_mcp_server/prowler_app/utils/api_client.py +++ b/mcp_server/prowler_mcp_server/prowler_app/utils/api_client.py @@ -2,11 +2,12 @@ import asyncio from datetime import datetime, timedelta -from enum import Enum -from typing import Any, Dict +from enum import StrEnum +from typing import Any from urllib.parse import urlparse import httpx + from prowler_mcp_server import __version__ from prowler_mcp_server.lib.logger import logger from prowler_mcp_server.prowler_app.utils.auth import ProwlerAppAuth @@ -14,7 +15,7 @@ from prowler_mcp_server.prowler_app.utils.auth import ProwlerAppAuth ALLOWED_EXTERNAL_DOMAINS: frozenset[str] = frozenset({"raw.githubusercontent.com"}) -class HTTPMethod(str, Enum): +class HTTPMethod(StrEnum): """HTTP methods enum.""" GET = "GET" @@ -30,7 +31,7 @@ class SingletonMeta(type): All calls to the constructor return the same instance. """ - _instances: Dict[type, Any] = {} + _instances: dict[type, Any] = {} def __call__(cls, *args, **kwargs): """Control instance creation to ensure singleton behavior.""" diff --git a/mcp_server/prowler_mcp_server/prowler_app/utils/auth.py b/mcp_server/prowler_mcp_server/prowler_app/utils/auth.py index 48535fb10a..72c06000df 100644 --- a/mcp_server/prowler_mcp_server/prowler_app/utils/auth.py +++ b/mcp_server/prowler_mcp_server/prowler_app/utils/auth.py @@ -2,7 +2,6 @@ import base64 import json import os from datetime import datetime -from typing import Dict, Optional from fastmcp.server.dependencies import get_http_headers @@ -21,8 +20,8 @@ class ProwlerAppAuth: self.base_url = base_url.rstrip("/") logger.info(f"Using Prowler App API base URL: {self.base_url}") self.mode = mode - self.access_token: Optional[str] = None - self.api_key: Optional[str] = None + self.access_token: str | None = None + self.api_key: str | None = None if mode == "stdio": # STDIO mode self.api_key = os.getenv("PROWLER_APP_API_KEY") @@ -33,7 +32,7 @@ class ProwlerAppAuth: if not self.api_key.startswith("pk_"): raise ValueError("Prowler App API key format is incorrect") - def _parse_jwt(self, token: str) -> Optional[Dict]: + def _parse_jwt(self, token: str) -> dict | None: """Parse JWT token and return payload Args: @@ -110,7 +109,7 @@ class ProwlerAppAuth: else: return await self.authenticate() - def get_headers(self, token: str) -> Dict[str, str]: + def get_headers(self, token: str) -> dict[str, str]: """Get headers for API requests with authentication.""" if token.startswith("pk_"): authorization_header = f"Api-Key {token}" diff --git a/mcp_server/prowler_mcp_server/prowler_app/utils/tool_loader.py b/mcp_server/prowler_mcp_server/prowler_app/utils/tool_loader.py index 834bcaef7f..b85c13af35 100644 --- a/mcp_server/prowler_mcp_server/prowler_app/utils/tool_loader.py +++ b/mcp_server/prowler_mcp_server/prowler_app/utils/tool_loader.py @@ -8,6 +8,7 @@ import importlib import pkgutil from fastmcp import FastMCP + from prowler_mcp_server.lib.logger import logger from prowler_mcp_server.prowler_app.tools.base import BaseTool diff --git a/mcp_server/prowler_mcp_server/prowler_documentation/search_engine.py b/mcp_server/prowler_mcp_server/prowler_documentation/search_engine.py index a366c39c30..ba6f2a12e7 100644 --- a/mcp_server/prowler_mcp_server/prowler_documentation/search_engine.py +++ b/mcp_server/prowler_mcp_server/prowler_documentation/search_engine.py @@ -1,7 +1,8 @@ import httpx -from prowler_mcp_server import __version__ from pydantic import BaseModel, Field +from prowler_mcp_server import __version__ + class SearchResult(BaseModel): """Search result model.""" diff --git a/mcp_server/prowler_mcp_server/server.py b/mcp_server/prowler_mcp_server/server.py index 20b7966b45..7c85641dee 100644 --- a/mcp_server/prowler_mcp_server/server.py +++ b/mcp_server/prowler_mcp_server/server.py @@ -1,7 +1,8 @@ from fastmcp import FastMCP +from starlette.responses import JSONResponse + from prowler_mcp_server import __version__ from prowler_mcp_server.lib.logger import logger -from starlette.responses import JSONResponse prowler_mcp_server = FastMCP("prowler-mcp-server") diff --git a/mcp_server/pyproject.toml b/mcp_server/pyproject.toml index 154b835281..63aefdf931 100644 --- a/mcp_server/pyproject.toml +++ b/mcp_server/pyproject.toml @@ -6,6 +6,7 @@ requires = ["setuptools>=61.0", "wheel"] dev = [ "bandit==1.8.3", "pytest==9.0.3", + "ruff==0.15.11", "vulture==2.14" ] @@ -26,5 +27,21 @@ prowler-mcp = "prowler_mcp_server.main:main" [tool.pytest.ini_options] testpaths = ["tests"] +# Shared ruff baseline (kept in sync with api/pyproject.toml). +# target-version tracks this project's lowest supported Python. +[tool.ruff] +target-version = "py312" + +[tool.ruff.lint] +# Defaults (E4/E7/E9, F) plus import sorting, modern-syntax upgrades, and +# comprehension lints — all mechanically auto-fixable. flake8-bugbear (B) is a +# good next step but needs manual cleanup, so it is left out of the shared +# baseline for now. +extend-select = [ + "I", # isort — import ordering + "UP", # pyupgrade — modern syntax for the min supported Python + "C4" # flake8-comprehensions +] + [tool.uv] package = true diff --git a/mcp_server/uv.lock b/mcp_server/uv.lock index 14b8dcf4f3..3258767442 100644 --- a/mcp_server/uv.lock +++ b/mcp_server/uv.lock @@ -687,6 +687,7 @@ dependencies = [ dev = [ { name = "bandit" }, { name = "pytest" }, + { name = "ruff" }, { name = "vulture" }, ] @@ -700,6 +701,7 @@ requires-dist = [ dev = [ { name = "bandit", specifier = "==1.8.3" }, { name = "pytest", specifier = "==9.0.3" }, + { name = "ruff", specifier = "==0.15.11" }, { name = "vulture", specifier = "==2.14" }, ] @@ -1104,6 +1106,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, ] +[[package]] +name = "ruff" +version = "0.15.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, +] + [[package]] name = "secretstorage" version = "3.5.0"