From 6e7a32cb516e813b441e0fe2f7766a9ce270a488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Tue, 3 Jun 2025 16:38:00 +0200 Subject: [PATCH 01/13] revert(views): calling order to initial view method (#7921) --- api/src/backend/api/base_views.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/api/src/backend/api/base_views.py b/api/src/backend/api/base_views.py index 60940733c2..54b020597f 100644 --- a/api/src/backend/api/base_views.py +++ b/api/src/backend/api/base_views.py @@ -47,6 +47,8 @@ class BaseViewSet(ModelViewSet): class BaseRLSViewSet(BaseViewSet): def initial(self, request, *args, **kwargs): + super().initial(request, *args, **kwargs) + # Ideally, this logic would be in the `.setup()` method but DRF view sets don't call it # https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View.setup if request.auth is None: @@ -61,8 +63,6 @@ class BaseRLSViewSet(BaseViewSet): self._rls_cm = rls_transaction(tenant_id) self._rls_cm.__enter__() - super().initial(request, *args, **kwargs) - def finalize_response(self, request, response, *args, **kwargs): response = super().finalize_response(request, response, *args, **kwargs) @@ -115,6 +115,8 @@ class BaseTenantViewset(BaseViewSet): pass # Tenant might not exist, handle gracefully def initial(self, request, *args, **kwargs): + super().initial(request, *args, **kwargs) + if request.auth is None: raise NotAuthenticated @@ -127,8 +129,6 @@ class BaseTenantViewset(BaseViewSet): self._rls_cm = rls_transaction(value=user_id, parameter=POSTGRES_USER_VAR) self._rls_cm.__enter__() - super().initial(request, *args, **kwargs) - def finalize_response(self, request, response, *args, **kwargs): response = super().finalize_response(request, response, *args, **kwargs) @@ -141,9 +141,11 @@ class BaseTenantViewset(BaseViewSet): class BaseUserViewset(BaseViewSet): def initial(self, request, *args, **kwargs): + super().initial(request, *args, **kwargs) + # TODO refactor after improving RLS on users if request.stream is not None and request.stream.method == "POST": - return super().initial(request, *args, **kwargs) + return if request.auth is None: raise NotAuthenticated @@ -156,8 +158,6 @@ class BaseUserViewset(BaseViewSet): self._rls_cm = rls_transaction(tenant_id) self._rls_cm.__enter__() - super().initial(request, *args, **kwargs) - def finalize_response(self, request, response, *args, **kwargs): response = super().finalize_response(request, response, *args, **kwargs) From 4dc4d82d427db9635ff0f9e0cfc9fc50091ba351 Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Wed, 4 Jun 2025 12:26:27 +0200 Subject: [PATCH 02/13] feat: aws-well-architected compliance detailed view (#7925) --- ui/CHANGELOG.md | 1 + ui/components/compliance/compliance-card.tsx | 7 +- .../aws-well-architected-details.tsx | 91 ++++++++ .../compliance-custom-details/cis-details.tsx | 9 +- ui/lib/compliance/aws-well-architected.tsx | 221 ++++++++++++++++++ ui/lib/compliance/commons.ts | 19 ++ ui/types/compliance.ts | 18 +- 7 files changed, 360 insertions(+), 6 deletions(-) create mode 100644 ui/components/compliance/compliance-custom-details/aws-well-architected-details.tsx create mode 100644 ui/lib/compliance/aws-well-architected.tsx diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index e770712ddd..265c5535f4 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to the **Prowler UI** are documented in this file. - Add compliance detail view: ENS [(#7853)](https://github.com/prowler-cloud/prowler/pull/7853) - Add compliance detail view: ISO [(#7897)](https://github.com/prowler-cloud/prowler/pull/7897) - Add compliance detail view: CIS [(#7913)](https://github.com/prowler-cloud/prowler/pull/7913) +- Add compliance detail view: AWS Well-Architected Framework [(#7925)](https://github.com/prowler-cloud/prowler/pull/7925) ### ๐Ÿ”„ Changed diff --git a/ui/components/compliance/compliance-card.tsx b/ui/components/compliance/compliance-card.tsx index 10a473236f..63f4c0150b 100644 --- a/ui/components/compliance/compliance-card.tsx +++ b/ui/components/compliance/compliance-card.tsx @@ -73,7 +73,12 @@ export const ComplianceCard: React.FC = ({ const navigateToDetail = () => { // We will unlock this while developing the rest of complainces. - if (!id.includes("ens") && !id.includes("iso") && !id.includes("cis_")) { + if ( + !id.includes("ens") && + !id.includes("iso") && + !id.includes("cis_") && + !id.includes("pillar") + ) { return; } diff --git a/ui/components/compliance/compliance-custom-details/aws-well-architected-details.tsx b/ui/components/compliance/compliance-custom-details/aws-well-architected-details.tsx new file mode 100644 index 0000000000..35cc5c7114 --- /dev/null +++ b/ui/components/compliance/compliance-custom-details/aws-well-architected-details.tsx @@ -0,0 +1,91 @@ +import Link from "next/link"; + +import { SeverityBadge } from "@/components/ui/table"; +import { Requirement } from "@/types/compliance"; + +export const AWSWellArchitectedCustomDetails = ({ + requirement, +}: { + requirement: Requirement; +}) => { + return ( +
+ {requirement.description && ( +
+

+ Description +

+

{requirement.description}

+
+ )} + + {requirement.well_architected_name && ( +
+

+ Best Practice +

+

{requirement.well_architected_name}

+
+ )} + + {requirement.well_architected_question_id && ( +
+

+ Question ID +

+

{requirement.well_architected_question_id}

+
+ )} + + {requirement.well_architected_practice_id && ( +
+

+ Practice ID +

+

{requirement.well_architected_practice_id}

+
+ )} + + {requirement.level_of_risk && ( +
+

+ Level of Risk +

+ +
+ )} + + {requirement.assessment_method && ( +
+

+ Assessment Method +

+

{requirement.assessment_method}

+
+ )} + + {requirement.implementation_guidance_url && ( +
+

+ Implementation Guidance +

+ + {requirement.implementation_guidance_url} + +
+ )} +
+ ); +}; diff --git a/ui/components/compliance/compliance-custom-details/cis-details.tsx b/ui/components/compliance/compliance-custom-details/cis-details.tsx index e4d99db68f..ebb04e48d8 100644 --- a/ui/components/compliance/compliance-custom-details/cis-details.tsx +++ b/ui/components/compliance/compliance-custom-details/cis-details.tsx @@ -1,3 +1,4 @@ +import Link from "next/link"; import ReactMarkdown from "react-markdown"; import { Requirement } from "@/types/compliance"; @@ -83,7 +84,7 @@ export const CISCustomDetails = ({ requirement }: CISDetailsProps) => { {/* Prettier -> "plugins": ["prettier-plugin-tailwindcss"] is not ready yet to "prose": */} {/* eslint-disable-next-line */} -
+
{requirement.remediation_procedure}
@@ -96,7 +97,7 @@ export const CISCustomDetails = ({ requirement }: CISDetailsProps) => { Audit Procedure {/* eslint-disable-next-line */} -
+
{requirement.audit_procedure}
@@ -131,14 +132,14 @@ export const CISCustomDetails = ({ requirement }: CISDetailsProps) => { {processReferences(requirement.references).map( (url: string, index: number) => (
- {url} - +
), )} diff --git a/ui/lib/compliance/aws-well-architected.tsx b/ui/lib/compliance/aws-well-architected.tsx new file mode 100644 index 0000000000..d6fd809987 --- /dev/null +++ b/ui/lib/compliance/aws-well-architected.tsx @@ -0,0 +1,221 @@ +import { ClientAccordionContent } from "@/components/compliance/compliance-accordion/client-accordion-content"; +import { ComplianceAccordionRequirementTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-requeriment-title"; +import { ComplianceAccordionTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-title"; +import { AccordionItemProps } from "@/components/ui/accordion/Accordion"; +import { FindingStatus } from "@/components/ui/table/status-finding-badge"; +import { + AttributesData, + AWSWellArchitectedAttributesMetadata, + Framework, + Requirement, + RequirementItemData, + RequirementsData, + RequirementStatus, +} from "@/types/compliance"; + +export const mapComplianceData = ( + attributesData: AttributesData, + requirementsData: RequirementsData, +): Framework[] => { + const attributes = attributesData?.data || []; + const requirements = requirementsData?.data || []; + + // Create a map for quick lookup of requirements by id + const requirementsMap = new Map(); + requirements.forEach((req: RequirementItemData) => { + requirementsMap.set(req.id, req); + }); + + const frameworks: Framework[] = []; + + // Process attributes and merge with requirements data + for (const attributeItem of attributes) { + const id = attributeItem.id; + const metadataArray = attributeItem.attributes?.attributes + ?.metadata as unknown as AWSWellArchitectedAttributesMetadata[]; + const attrs = metadataArray?.[0]; + if (!attrs) continue; + + // Get corresponding requirement data + const requirementData = requirementsMap.get(id); + if (!requirementData) continue; + + const frameworkName = attributeItem.attributes.framework; + const sectionName = attrs.Section || ""; + const subSectionName = attrs.SubSection || ""; + const description = attributeItem.attributes.description; + const status = requirementData.attributes.status || ""; + const checks = attributeItem.attributes.attributes.check_ids || []; + const requirementName = id; + + if (!sectionName || !subSectionName) { + continue; + } + + // Find or create framework + let framework = frameworks.find((f) => f.name === frameworkName); + if (!framework) { + framework = { + name: frameworkName, + pass: 0, + fail: 0, + manual: 0, + categories: [], + }; + frameworks.push(framework); + } + + // Find or create category (Section) + let category = framework.categories.find((c) => c.name === sectionName); + if (!category) { + category = { + name: sectionName, + pass: 0, + fail: 0, + manual: 0, + controls: [], + }; + framework.categories.push(category); + } + + // Find or create control (SubSection) + let control = category.controls.find((c) => c.label === subSectionName); + if (!control) { + control = { + label: subSectionName, + pass: 0, + fail: 0, + manual: 0, + requirements: [], + }; + category.controls.push(control); + } + + // Create requirement + const finalStatus: RequirementStatus = status as RequirementStatus; + const requirement: Requirement = { + name: requirementName, + description: description, + status: finalStatus, + check_ids: checks, + pass: finalStatus === "PASS" ? 1 : 0, + fail: finalStatus === "FAIL" ? 1 : 0, + manual: finalStatus === "MANUAL" ? 1 : 0, + well_architected_name: attrs.Name, + well_architected_question_id: attrs.WellArchitectedQuestionId, + well_architected_practice_id: attrs.WellArchitectedPracticeId, + level_of_risk: attrs.LevelOfRisk, + assessment_method: attrs.AssessmentMethod, + implementation_guidance_url: attrs.ImplementationGuidanceUrl, + }; + + control.requirements.push(requirement); + } + + // Calculate counters + frameworks.forEach((framework) => { + framework.pass = 0; + framework.fail = 0; + framework.manual = 0; + + framework.categories.forEach((category) => { + category.pass = 0; + category.fail = 0; + category.manual = 0; + + category.controls.forEach((control) => { + control.pass = 0; + control.fail = 0; + control.manual = 0; + + control.requirements.forEach((requirement) => { + if (requirement.status === "MANUAL") { + control.manual++; + } else if (requirement.status === "PASS") { + control.pass++; + } else if (requirement.status === "FAIL") { + control.fail++; + } + }); + + category.pass += control.pass; + category.fail += control.fail; + category.manual += control.manual; + }); + + framework.pass += category.pass; + framework.fail += category.fail; + framework.manual += category.manual; + }); + }); + + return frameworks; +}; + +export const toAccordionItems = ( + data: Framework[], + scanId: string | undefined, +): AccordionItemProps[] => { + return data.flatMap((framework) => + framework.categories.map((category) => { + return { + key: `${framework.name}-${category.name}`, + title: ( + + ), + content: "", + items: category.controls.map((control, i: number) => { + return { + key: `${framework.name}-${category.name}-control-${i}`, + title: ( + + ), + content: "", + items: control.requirements.map((requirement, j: number) => { + const itemKey = `${framework.name}-${category.name}-control-${i}-req-${j}`; + + return { + key: itemKey, + title: ( + + ), + content: ( + + ), + items: [], + }; + }), + isDisabled: + control.pass === 0 && control.fail === 0 && control.manual === 0, + }; + }), + }; + }), + ); +}; diff --git a/ui/lib/compliance/commons.ts b/ui/lib/compliance/commons.ts index 6d0c3c7779..7317bb6c0f 100644 --- a/ui/lib/compliance/commons.ts +++ b/ui/lib/compliance/commons.ts @@ -1,5 +1,6 @@ import React from "react"; +import { AWSWellArchitectedCustomDetails } from "@/components/compliance/compliance-custom-details/aws-well-architected-details"; import { CISCustomDetails } from "@/components/compliance/compliance-custom-details/cis-details"; import { ENSCustomDetails } from "@/components/compliance/compliance-custom-details/ens-details"; import { ISOCustomDetails } from "@/components/compliance/compliance-custom-details/iso-details"; @@ -14,6 +15,10 @@ import { RequirementsData, } from "@/types/compliance"; +import { + mapComplianceData as mapAWSWellArchitectedComplianceData, + toAccordionItems as toAWSWellArchitectedAccordionItems, +} from "./aws-well-architected"; import { mapComplianceData as mapCISComplianceData, toAccordionItems as toCISAccordionItems, @@ -101,6 +106,20 @@ const complianceMappers: Record = { getDetailsComponent: (requirement: Requirement) => React.createElement(CISCustomDetails, { requirement }), }, + "AWS-Well-Architected-Framework-Security-Pillar": { + mapComplianceData: mapAWSWellArchitectedComplianceData, + toAccordionItems: toAWSWellArchitectedAccordionItems, + getTopFailedSections, + getDetailsComponent: (requirement: Requirement) => + React.createElement(AWSWellArchitectedCustomDetails, { requirement }), + }, + "AWS-Well-Architected-Framework-Reliability-Pillar": { + mapComplianceData: mapAWSWellArchitectedComplianceData, + toAccordionItems: toAWSWellArchitectedAccordionItems, + getTopFailedSections, + getDetailsComponent: (requirement: Requirement) => + React.createElement(AWSWellArchitectedCustomDetails, { requirement }), + }, }; // Default mapper (fallback to ENS for backward compatibility) diff --git a/ui/types/compliance.ts b/ui/types/compliance.ts index 4fa4cf7e1a..8cfaab0246 100644 --- a/ui/types/compliance.ts +++ b/ui/types/compliance.ts @@ -112,6 +112,18 @@ export interface CISAttributesMetadata { References: string; } +export interface AWSWellArchitectedAttributesMetadata { + Name: string; + WellArchitectedQuestionId: string; + WellArchitectedPracticeId: string; + Section: string; + SubSection: string; + LevelOfRisk: string; + AssessmentMethod: string; + Description: string; + ImplementationGuidanceUrl: string; +} + export interface AttributesItemData { type: "compliance-requirements-attributes"; id: string; @@ -120,7 +132,11 @@ export interface AttributesItemData { version: string; description: string; attributes: { - metadata: ENSAttributesMetadata[] | ISO27001AttributesMetadata[]; + metadata: + | ENSAttributesMetadata[] + | ISO27001AttributesMetadata[] + | CISAttributesMetadata[] + | AWSWellArchitectedAttributesMetadata[]; check_ids: string[]; }; }; From c74360ab63bc8527a1f14c66deb699019630ecf5 Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Wed, 4 Jun 2025 13:32:52 +0200 Subject: [PATCH 03/13] fix: clear filters sync (#7928) --- ui/CHANGELOG.md | 4 + .../ui/custom/custom-dropdown-filter.tsx | 95 +++++++++++-------- ui/hooks/use-url-filters.ts | 12 ++- 3 files changed, 69 insertions(+), 42 deletions(-) diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 265c5535f4..0b72a752ef 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to the **Prowler UI** are documented in this file. ## [v1.8.0] (Prowler v5.8.0) โ€“ Not released +### ๐Ÿž Fixes + +- Fix sync between filter buttons and URL when filters change. [(#7928)](https://github.com/prowler-cloud/prowler/pull/7928) + ### ๐Ÿš€ Added - New profile page with details about the user and their roles. [(#7780)](https://github.com/prowler-cloud/prowler/pull/7780) diff --git a/ui/components/ui/custom/custom-dropdown-filter.tsx b/ui/components/ui/custom/custom-dropdown-filter.tsx index b17828083a..be4282cf78 100644 --- a/ui/components/ui/custom/custom-dropdown-filter.tsx +++ b/ui/components/ui/custom/custom-dropdown-filter.tsx @@ -20,10 +20,9 @@ import React, { useState, } from "react"; +import { EntityInfoShort } from "@/components/ui/entities"; import { CustomDropdownFilterProps } from "@/types"; -import { EntityInfoShort } from "../entities"; - export const CustomDropdownFilter = ({ filter, onFilterChange, @@ -45,57 +44,75 @@ export const CustomDropdownFilter = ({ return filterParam ? filterParam.split(",") : []; }, [searchParams, filter?.key]); - // Sync URL state with component state - useEffect(() => { - if (activeFilterValue.length > 0) { - const newSelection = new Set(activeFilterValue); + // Helper function to handle URL filter values sync + const syncWithActiveFilters = useCallback(() => { + const newSelection = new Set(activeFilterValue); + if ( + newSelection.size === filterValues.length && + filter?.showSelectAll !== false + ) { + newSelection.add("all"); + } + setGroupSelected(newSelection); + }, [activeFilterValue, filterValues, filter?.showSelectAll]); + + const resetComponentState = useCallback(() => { + setGroupSelected(new Set()); + hasUserInteracted.current = false; + }, []); + + const applyDefaultValues = useCallback(() => { + if (filter?.defaultToSelectAll && filterValues.length > 0) { + const newSelection = new Set(filterValues); + if (filter?.showSelectAll !== false) { + newSelection.add("all"); + } + setGroupSelected(newSelection); + } else if (filter?.defaultValues && filter.defaultValues.length > 0) { + const validDefaultValues = filter.defaultValues.filter((value) => + filterValues.includes(value), + ); + const newSelection = new Set(validDefaultValues); + + // Add "all" if all items are selected and showSelectAll is not false if ( - newSelection.size === filterValues.length && + validDefaultValues.length === filterValues.length && filter?.showSelectAll !== false ) { newSelection.add("all"); } setGroupSelected(newSelection); - } else if (!hasUserInteracted.current) { - // Handle default behavior when no URL params exist - // Only apply defaults if user hasn't interacted yet - // Only set visual state, don't trigger URL changes automatically - if (filter?.defaultToSelectAll && filterValues.length > 0) { - const newSelection = new Set(filterValues); - if (filter?.showSelectAll !== false) { - newSelection.add("all"); - } - setGroupSelected(newSelection); - // DON'T notify parent automatically - wait for user interaction - } else if (filter?.defaultValues && filter.defaultValues.length > 0) { - // Handle specific default values - const validDefaultValues = filter.defaultValues.filter((value) => - filterValues.includes(value), - ); - const newSelection = new Set(validDefaultValues); - - // Add "all" if all items are selected and showSelectAll is not false - if ( - validDefaultValues.length === filterValues.length && - filter?.showSelectAll !== false - ) { - newSelection.add("all"); - } - - setGroupSelected(newSelection); - // DON'T notify parent automatically - wait for user interaction - } else { - setGroupSelected(new Set()); - } + } else { + setGroupSelected(new Set()); } }, [ - activeFilterValue, filterValues, filter?.defaultToSelectAll, filter?.defaultValues, filter?.showSelectAll, ]); + useEffect(() => { + const hasActiveFilters = activeFilterValue.length > 0; + const userHasInteracted = hasUserInteracted.current; + + if (hasActiveFilters) { + // URL has filter values - sync component state with URL + syncWithActiveFilters(); + } else if (userHasInteracted) { + // URL has no filters but user had interacted - reset component state + resetComponentState(); + } else { + // URL has no filters and user hasn't interacted - apply defaults + applyDefaultValues(); + } + }, [ + activeFilterValue, + syncWithActiveFilters, + resetComponentState, + applyDefaultValues, + ]); + const updateSelection = useCallback( (newValues: string[]) => { // Mark that user has interacted with the filter diff --git a/ui/hooks/use-url-filters.ts b/ui/hooks/use-url-filters.ts index c29de37327..83bd225791 100644 --- a/ui/hooks/use-url-filters.ts +++ b/ui/hooks/use-url-filters.ts @@ -16,8 +16,10 @@ export const useUrlFilters = () => { (key: string, value: string | string[] | null) => { const params = new URLSearchParams(searchParams.toString()); - // Always reset page to 1 when a filter is applied - params.set("page", "1"); + // Only reset page to 1 if page parameter already exists + if (params.has("page")) { + params.set("page", "1"); + } const filterKey = key.startsWith("filter[") ? key : `filter[${key}]`; @@ -40,7 +42,11 @@ export const useUrlFilters = () => { const filterKey = key.startsWith("filter[") ? key : `filter[${key}]`; params.delete(filterKey); - params.set("page", "1"); + + // Only reset page to 1 if page parameter already exists + if (params.has("page")) { + params.set("page", "1"); + } router.push(`${pathname}?${params.toString()}`, { scroll: false }); }, From 4ac3cfc33d48e5ae9c15e391935df44ca539f55a Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 4 Jun 2025 13:54:25 +0200 Subject: [PATCH 04/13] docs: update changelog (#7931) --- ui/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 0b72a752ef..9059414c40 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -30,7 +30,6 @@ All notable changes to the **Prowler UI** are documented in this file. ### ๐Ÿž Fixes - Download report behaviour updated to show feedback based on API response. [(#7758)](https://github.com/prowler-cloud/prowler/pull/7758) -- Compliace detail page, now available for ENS. [(#7853)](https://github.com/prowler-cloud/prowler/pull/7853) - Missing KISA and ProwlerThreat icons added to the compliance page. [(#7860)(https://github.com/prowler-cloud/prowler/pull/7860)] - Retrieve more than 10 scans in /compliance page. [(#7865)](https://github.com/prowler-cloud/prowler/pull/7865) - Improve CustomDropdownFilter component. [(#7868)(https://github.com/prowler-cloud/prowler/pull/7868)] From fb914a2c908d1dc0b4aac36efb4db8d34fc4fbb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Jes=C3=BAs=20Pe=C3=B1a=20Rodr=C3=ADguez?= Date: Wed, 4 Jun 2025 15:01:47 +0200 Subject: [PATCH 05/13] revert: remove get_with_retry (#7932) --- api/CHANGELOG.md | 1 + api/src/backend/api/models.py | 40 ------------------------ api/src/backend/api/tests/test_models.py | 37 +--------------------- api/src/backend/api/v1/views.py | 8 ++--- 4 files changed, 6 insertions(+), 80 deletions(-) diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index dd3446550a..c840379feb 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -21,6 +21,7 @@ All notable changes to the **Prowler API** are documented in this file. ### Fixed - Fixed transaction persistence with RLS operations [(#7916)](https://github.com/prowler-cloud/prowler/pull/7916). +- Reverted the change `get_with_retry` to use the original `get` method for retrieving tasks [(#7932)](https://github.com/prowler-cloud/prowler/pull/7932). --- diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index b4a5c3a894..52e32b8c38 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -1,9 +1,7 @@ import json import re -import time from uuid import UUID, uuid4 -from config.env import env from cryptography.fernet import Fernet from django.conf import settings from django.contrib.auth.models import AbstractBaseUser @@ -354,42 +352,6 @@ class ProviderGroupMembership(RowLevelSecurityProtectedModel): resource_name = "provider_groups-provider" -class TaskManager(models.Manager): - def get_with_retry( - self, - id: str, - max_retries: int = None, - delay_seconds: float = None, - ): - """ - Retry fetching a Task by ID in case it hasn't been created yet. - - Args: - id (str): The Celery task ID (expected to match Task model PK). - max_retries (int, optional): Number of retry attempts. Defaults to env TASK_RETRY_ATTEMPTS or 5. - delay_seconds (float, optional): Delay between retries in seconds. Defaults to env TASK_RETRY_DELAY_SECONDS or 0.1. - - Returns: - Task: The retrieved Task instance. - - Raises: - Task.DoesNotExist: If the task is not found after all retries. - """ - max_retries = max_retries or env.int("TASK_RETRY_ATTEMPTS", default=5) - delay_seconds = delay_seconds or env.float( - "TASK_RETRY_DELAY_SECONDS", default=0.1 - ) - - for _attempt in range(max_retries): - try: - return self.get(id=id) - except self.model.DoesNotExist: - time.sleep(delay_seconds) - raise self.model.DoesNotExist( - f"Task with ID {id} not found after {max_retries} retries." - ) - - class Task(RowLevelSecurityProtectedModel): id = models.UUIDField(primary_key=True, default=uuid4, editable=False) inserted_at = models.DateTimeField(auto_now_add=True, editable=False) @@ -402,8 +364,6 @@ class Task(RowLevelSecurityProtectedModel): blank=True, ) - objects = TaskManager() - class Meta(RowLevelSecurityProtectedModel.Meta): db_table = "tasks" diff --git a/api/src/backend/api/tests/test_models.py b/api/src/backend/api/tests/test_models.py index de2ec7c59e..c2beeb3583 100644 --- a/api/src/backend/api/tests/test_models.py +++ b/api/src/backend/api/tests/test_models.py @@ -1,9 +1,6 @@ -import uuid -from unittest import mock - import pytest -from api.models import Resource, ResourceTag, Task +from api.models import Resource, ResourceTag @pytest.mark.django_db @@ -123,35 +120,3 @@ class TestResourceModel: # compliance={}, # ) # assert Finding.objects.filter(uid=long_uid).exists() - - -@pytest.mark.django_db -class TestTaskManager: - def test_get_with_retry_success(self): - task_id = uuid.uuid4() - call_counter = {"count": 0} - - def side_effect(*args, **kwargs): - if call_counter["count"] < 2: - call_counter["count"] += 1 - raise Task.DoesNotExist() - return Task(id=task_id) - - with mock.patch.object(Task.objects, "get", side_effect=side_effect): - task = Task.objects.get_with_retry( - task_id, max_retries=5, delay_seconds=0.01 - ) - - assert task.id == task_id - assert call_counter["count"] == 2 - - def test_get_with_retry_fail(self): - non_existent_id = uuid.uuid4() - - with mock.patch.object(Task.objects, "get", side_effect=Task.DoesNotExist): - with pytest.raises(Task.DoesNotExist) as excinfo: - Task.objects.get_with_retry( - non_existent_id, max_retries=3, delay_seconds=0.01 - ) - - assert str(non_existent_id) in str(excinfo.value) diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index 394913cb6c..99b551fec5 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -1092,7 +1092,7 @@ class ProviderViewSet(BaseRLSViewSet): task = check_provider_connection_task.delay( provider_id=pk, tenant_id=self.request.tenant_id ) - prowler_task = Task.objects.get_with_retry(id=task.id) + prowler_task = Task.objects.get(id=task.id) serializer = TaskSerializer(prowler_task) return Response( data=serializer.data, @@ -1115,7 +1115,7 @@ class ProviderViewSet(BaseRLSViewSet): task = delete_provider_task.delay( provider_id=pk, tenant_id=self.request.tenant_id ) - prowler_task = Task.objects.get_with_retry(id=task.id) + prowler_task = Task.objects.get(id=task.id) serializer = TaskSerializer(prowler_task) return Response( data=serializer.data, @@ -1495,7 +1495,7 @@ class ScanViewSet(BaseRLSViewSet): }, ) - prowler_task = Task.objects.get_with_retry(id=task.id) + prowler_task = Task.objects.get(id=task.id) scan.task_id = task.id scan.save(update_fields=["task_id"]) @@ -3123,7 +3123,7 @@ class ScheduleViewSet(BaseRLSViewSet): with transaction.atomic(): task = schedule_provider_scan(provider_instance) - prowler_task = Task.objects.get_with_retry(id=task.id) + prowler_task = Task.objects.get(id=task.id) self.response_serializer_class = TaskSerializer output_serializer = self.get_serializer(prowler_task) From be420afebc9f39e1e5a3b3c69299689ecb152214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Wed, 4 Jun 2025 16:09:36 +0200 Subject: [PATCH 06/13] feat(database): handle already closed connections (#7935) --- api/CHANGELOG.md | 3 +++ api/src/backend/config/django/base.py | 1 + api/src/backend/database_backend/__init__.py | 0 api/src/backend/database_backend/base.py | 15 +++++++++++++++ 4 files changed, 19 insertions(+) create mode 100644 api/src/backend/database_backend/__init__.py create mode 100644 api/src/backend/database_backend/base.py diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index c840379feb..524e83f46b 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -19,6 +19,9 @@ All notable changes to the **Prowler API** are documented in this file. ## [v1.8.3] (Prowler v5.7.3) +### Added +- Database backend to handle already closed connections [(#7935)](https://github.com/prowler-cloud/prowler/pull/7935). + ### Fixed - Fixed transaction persistence with RLS operations [(#7916)](https://github.com/prowler-cloud/prowler/pull/7916). - Reverted the change `get_with_retry` to use the original `get` method for retrieving tasks [(#7932)](https://github.com/prowler-cloud/prowler/pull/7932). diff --git a/api/src/backend/config/django/base.py b/api/src/backend/config/django/base.py index 5de1d9ca71..db4a23e464 100644 --- a/api/src/backend/config/django/base.py +++ b/api/src/backend/config/django/base.py @@ -128,6 +128,7 @@ DJANGO_GUID = { } DATABASE_ROUTERS = ["api.db_router.MainRouter"] +POSTGRES_EXTRA_DB_BACKEND_BASE = "database_backend" # Password validation diff --git a/api/src/backend/database_backend/__init__.py b/api/src/backend/database_backend/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/src/backend/database_backend/base.py b/api/src/backend/database_backend/base.py new file mode 100644 index 0000000000..69fee7df4b --- /dev/null +++ b/api/src/backend/database_backend/base.py @@ -0,0 +1,15 @@ +import django.db +from django.db.backends.postgresql.base import ( + DatabaseWrapper as BuiltinPostgresDatabaseWrapper, +) +from psycopg2 import InterfaceError + + +class DatabaseWrapper(BuiltinPostgresDatabaseWrapper): + def create_cursor(self, name=None): + try: + return super().create_cursor(name=name) + except InterfaceError: + django.db.close_old_connections() + django.db.connection.connect() + return super().create_cursor(name=name) From d89df839046ef8e3949607df709a4a21158be4b5 Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Thu, 5 Jun 2025 08:13:47 +0200 Subject: [PATCH 07/13] fix: Improve the perfomance removing regions heatmap (#7934) --- ui/CHANGELOG.md | 1 + .../compliance/[compliancetitle]/page.tsx | 38 ++------- .../client-accordion-content.tsx | 16 ++-- ...compliance-accordion-requeriment-title.tsx | 4 +- .../compliance-accordion-title.tsx | 2 +- ui/components/compliance/compliance-card.tsx | 15 ++-- .../compliance-charts/heatmap-chart.tsx | 73 +++++++---------- .../compliance-header/compliance-header.tsx | 6 +- ui/components/ui/accordion/Accordion.tsx | 2 +- ui/lib/compliance/commons.ts | 80 ------------------- ui/types/compliance.ts | 11 --- 11 files changed, 63 insertions(+), 185 deletions(-) diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 9059414c40..467fc2dd8f 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to the **Prowler UI** are documented in this file. ### ๐Ÿž Fixes - Fix sync between filter buttons and URL when filters change. [(#7928)](https://github.com/prowler-cloud/prowler/pull/7928) +- Improve heatmap perfomance. [(#7934)](https://github.com/prowler-cloud/prowler/pull/7934) ### ๐Ÿš€ Added diff --git a/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx b/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx index 189a33531a..98a83d403b 100644 --- a/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx +++ b/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx @@ -24,7 +24,6 @@ import { getComplianceIcon } from "@/components/icons/compliance/IconCompliance" import { ContentLayout } from "@/components/ui"; import { calculateCategoryHeatmapData, - calculateRegionHeatmapData, getComplianceMapper, } from "@/lib/compliance/commons"; import { ScanProps } from "@/types"; @@ -154,6 +153,7 @@ export default async function ComplianceDetail({ uniqueRegions={uniqueRegions} showSearch={false} framework={compliancetitle} + showProviders={false} /> @@ -192,16 +187,12 @@ const SSRComplianceContent = async ({ region, filter, logoPath, - uniqueRegions, - isRegionFiltered, }: { complianceId: string; scanId: string; region?: string; filter?: string; logoPath?: string; - uniqueRegions: string[]; - isRegionFiltered: boolean; }) => { if (!scanId) { return ( @@ -209,14 +200,7 @@ const SSRComplianceContent = async ({ - + @@ -236,20 +220,15 @@ const SSRComplianceContent = async ({ // Determine framework from the first attribute item const framework = attributesData?.data?.[0]?.attributes?.framework; const mapper = getComplianceMapper(framework); + + // Use the same data for both compliance view and heatmap const data = mapper.mapComplianceData( attributesData, requirementsData, filter, ); - // Calculate region heatmap data using already obtained data - const regionHeatmapData = await calculateRegionHeatmapData( - complianceId, - scanId, - uniqueRegions, - attributesData, - mapper, - ); + // Calculate category heatmap data const categoryHeatmapData = calculateCategoryHeatmapData(data); const totalRequirements: RequirementsTotals = data.reduce( @@ -277,12 +256,7 @@ const SSRComplianceContent = async ({ manual={totalRequirements.manual} /> - + diff --git a/ui/components/compliance/compliance-accordion/client-accordion-content.tsx b/ui/components/compliance/compliance-accordion/client-accordion-content.tsx index d22c4b2fe4..ae13888bdd 100644 --- a/ui/components/compliance/compliance-accordion/client-accordion-content.tsx +++ b/ui/components/compliance/compliance-accordion/client-accordion-content.tsx @@ -12,7 +12,7 @@ import { Accordion } from "@/components/ui/accordion/Accordion"; import { DataTable } from "@/components/ui/table"; import { createDict } from "@/lib"; import { getComplianceMapper } from "@/lib/compliance/commons"; -import { ComplianceId, Requirement } from "@/types/compliance"; +import { Requirement } from "@/types/compliance"; import { FindingProps, FindingsResponse } from "@/types/components"; interface ClientAccordionContentProps { @@ -32,7 +32,7 @@ export const ClientAccordionContent = ({ const [expandedFindings, setExpandedFindings] = useState([]); const searchParams = useSearchParams(); const pageNumber = searchParams.get("page") || "1"; - const complianceId = searchParams.get("complianceId") as ComplianceId; + const complianceId = searchParams.get("complianceId"); const defaultSort = "severity,status,-inserted_at"; const sort = searchParams.get("sort") || defaultSort; const loadedPageRef = useRef(null); @@ -116,8 +116,8 @@ export const ClientAccordionContent = ({ return (
{renderDetails()} -

- This requirement has no checks; therefore, there are no findings. +

+ โš ๏ธ This requirement has no checks; therefore, there are no findings.

); @@ -164,7 +164,11 @@ export const ClientAccordionContent = ({ ); } - return
There are no findings for this regions
; + return ( +
+ โš ๏ธ There are no findings for this regions +
+ ); }; return ( @@ -172,7 +176,7 @@ export const ClientAccordionContent = ({ {renderDetails()} {checks.length > 0 && ( -
+
{ return (
-
- {name} +
+ {name}
diff --git a/ui/components/compliance/compliance-accordion/compliance-accordion-title.tsx b/ui/components/compliance/compliance-accordion/compliance-accordion-title.tsx index 0f3f681b59..c9fa69aa8d 100644 --- a/ui/components/compliance/compliance-accordion/compliance-accordion-title.tsx +++ b/ui/components/compliance/compliance-accordion/compliance-accordion-title.tsx @@ -24,7 +24,7 @@ export const ComplianceAccordionTitle = ({
{label.charAt(0).toUpperCase() + label.slice(1)} diff --git a/ui/components/compliance/compliance-card.tsx b/ui/components/compliance/compliance-card.tsx index 63f4c0150b..720c5acd77 100644 --- a/ui/components/compliance/compliance-card.tsx +++ b/ui/components/compliance/compliance-card.tsx @@ -71,14 +71,15 @@ export const ComplianceCard: React.FC = ({ return "success"; }; + const isPressable = + id.includes("ens") || + id.includes("iso") || + id.includes("cis_") || + id.includes("pillar"); + const navigateToDetail = () => { // We will unlock this while developing the rest of complainces. - if ( - !id.includes("ens") && - !id.includes("iso") && - !id.includes("cis_") && - !id.includes("pillar") - ) { + if (!isPressable) { return; } @@ -106,7 +107,7 @@ export const ComplianceCard: React.FC = ({ fullWidth isHoverable shadow="sm" - isPressable + isPressable={isPressable} onPress={navigateToDetail} > diff --git a/ui/components/compliance/compliance-charts/heatmap-chart.tsx b/ui/components/compliance/compliance-charts/heatmap-chart.tsx index d4cb3b3a0a..bf5474595f 100644 --- a/ui/components/compliance/compliance-charts/heatmap-chart.tsx +++ b/ui/components/compliance/compliance-charts/heatmap-chart.tsx @@ -1,15 +1,13 @@ "use client"; +import { cn } from "@nextui-org/react"; import { useTheme } from "next-themes"; import { useState } from "react"; -import { CategoryData, RegionData } from "@/types/compliance"; +import { CategoryData } from "@/types/compliance"; interface HeatmapChartProps { - regions: RegionData[]; categories?: CategoryData[]; - isRegionFiltered?: boolean; // Indicates if a region filter is active - filteredRegionName?: string; // Name of the filtered region } const getHeatmapColor = (percentage: number): string => { @@ -32,48 +30,32 @@ const capitalizeFirstLetter = (text: string): string => { ); }; -export const HeatmapChart = ({ - regions, - categories = [], - isRegionFiltered = false, -}: HeatmapChartProps) => { +export const HeatmapChart = ({ categories = [] }: HeatmapChartProps) => { const { theme } = useTheme(); - const [hoveredItem, setHoveredItem] = useState< - RegionData | CategoryData | null - >(null); + const [hoveredItem, setHoveredItem] = useState(null); const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); - // Determine what data to show and prepare it - const dataToShow = isRegionFiltered ? categories : regions; - const heatmapData = dataToShow + // Use categories data and prepare it + const heatmapData = categories .filter((item) => item.totalRequirements > 0) .sort((a, b) => b.failurePercentage - a.failurePercentage) .slice(0, 9); // Exactly 9 items for 3x3 grid // Check if there are no items with data - if (!dataToShow || dataToShow.length === 0 || heatmapData.length === 0) { - const noDataMessage = isRegionFiltered - ? "No category data available" - : "No regional data available"; - + if (!categories.length || heatmapData.length === 0) { return (

- {isRegionFiltered - ? "Categories Failure Rate" - : "Failure Rate by Region"} + Sections Failure Rate

-

{noDataMessage}

+

No category data available

); } - const handleMouseEnter = ( - item: RegionData | CategoryData, - event: React.MouseEvent, - ) => { + const handleMouseEnter = (item: CategoryData, event: React.MouseEvent) => { setHoveredItem(item); setMousePosition({ x: event.clientX, y: event.clientY }); }; @@ -90,19 +72,27 @@ export const HeatmapChart = ({

- {isRegionFiltered - ? "Categories Failure Rate" - : "Failure Rate by Region"} + Sections Failure Rate

-
- {/* 3x3 Grid */} -
+
+
{heatmapData.map((item) => (
-
+
- {isRegionFiltered - ? capitalizeFirstLetter(item.name) - : item.name} + {capitalizeFirstLetter(item.name)}
- {isRegionFiltered - ? capitalizeFirstLetter(hoveredItem.name) - : hoveredItem.name} + {capitalizeFirstLetter(hoveredItem.name)}
Failure Rate: {hoveredItem.failurePercentage}%
diff --git a/ui/components/compliance/compliance-header/compliance-header.tsx b/ui/components/compliance/compliance-header/compliance-header.tsx index e6bfe0dd2d..abe54adf11 100644 --- a/ui/components/compliance/compliance-header/compliance-header.tsx +++ b/ui/components/compliance/compliance-header/compliance-header.tsx @@ -14,6 +14,7 @@ interface ComplianceHeaderProps { showSearch?: boolean; showRegionFilter?: boolean; framework?: string; // Framework name to show specific filters + showProviders?: boolean; } export const ComplianceHeader = ({ @@ -22,6 +23,7 @@ export const ComplianceHeader = ({ showSearch = true, showRegionFilter = true, framework, + showProviders = true, }: ComplianceHeaderProps) => { const frameworkFilters = []; @@ -56,10 +58,10 @@ export const ComplianceHeader = ({ <> {showSearch && } - + {showProviders && } {allFilters.length > 0 && ( <> - + {showProviders && } )} diff --git a/ui/components/ui/accordion/Accordion.tsx b/ui/components/ui/accordion/Accordion.tsx index 1bbcbc9ed8..8117d5eea6 100644 --- a/ui/components/ui/accordion/Accordion.tsx +++ b/ui/components/ui/accordion/Accordion.tsx @@ -138,7 +138,7 @@ export const Accordion = ({ indicator={} classNames={{ base: index === 0 || index === 1 ? "my-1" : "my-1", - title: "text-sm font-medium max-w-full overflow-hidden truncate", + title: "text-sm", subtitle: "text-xs text-gray-500", trigger: "py-2 px-2 rounded-lg data-[hover=true]:bg-gray-50 dark:data-[hover=true]:bg-gray-800/50 w-full flex items-center", diff --git a/ui/lib/compliance/commons.ts b/ui/lib/compliance/commons.ts index 7317bb6c0f..c62933b635 100644 --- a/ui/lib/compliance/commons.ts +++ b/ui/lib/compliance/commons.ts @@ -10,7 +10,6 @@ import { CategoryData, FailedSection, Framework, - RegionData, Requirement, RequirementsData, } from "@/types/compliance"; @@ -138,85 +137,6 @@ export const getComplianceMapper = (framework?: string): ComplianceMapper => { return complianceMappers[framework] || defaultMapper; }; -export const calculateRegionHeatmapData = async ( - complianceId: string, - scanId: string, - uniqueRegions: string[], - attributesData: AttributesData, - mapper: ComplianceMapper, -): Promise => { - if (!complianceId || !scanId || !uniqueRegions?.length) { - return []; - } - - try { - const { getComplianceRequirements } = await import("@/actions/compliances"); - - // Get data for each region in parallel - const regionPromises = uniqueRegions.map(async (region) => { - try { - // Only need to fetch requirements data per region - const regionRequirementsData = await getComplianceRequirements({ - complianceId, - scanId, - region, // Filter by specific region - }); - - // Map the data using the provided mapper - const mappedData = mapper.mapComplianceData( - attributesData, - regionRequirementsData, - ); - - // Calculate totals for this region - const regionTotals = mappedData.reduce( - (acc, framework) => ({ - pass: acc.pass + framework.pass, - fail: acc.fail + framework.fail, - manual: acc.manual + framework.manual, - }), - { pass: 0, fail: 0, manual: 0 }, - ); - - const totalRequirements = - regionTotals.pass + regionTotals.fail + regionTotals.manual; - const failurePercentage = - totalRequirements > 0 - ? Math.round((regionTotals.fail / totalRequirements) * 100) - : 0; - - return { - name: region, - failurePercentage, - totalRequirements, - failedRequirements: regionTotals.fail, - }; - } catch (error) { - console.error(`Error fetching data for region ${region}:`, error); - return { - name: region, - failurePercentage: 0, - totalRequirements: 0, - failedRequirements: 0, - }; - } - }); - - const regionData = await Promise.all(regionPromises); - - // Filter, sort and limit to top 9 regions for 3x3 grid - const filteredData = regionData - .filter((region) => region.totalRequirements > 0) - .sort((a, b) => b.failurePercentage - a.failurePercentage) - .slice(0, 9); - - return filteredData; - } catch (error) { - console.error("Error calculating region heatmap data:", error); - return []; - } -}; - export const calculateCategoryHeatmapData = ( complianceData: Framework[], ): CategoryData[] => { diff --git a/ui/types/compliance.ts b/ui/types/compliance.ts index 8cfaab0246..994c90d200 100644 --- a/ui/types/compliance.ts +++ b/ui/types/compliance.ts @@ -1,16 +1,5 @@ export type RequirementStatus = "PASS" | "FAIL" | "MANUAL" | "No findings"; -export type ComplianceId = - | "ens_rd2022_aws" - | "iso27001_2013_aws" - | "iso27001_2022_aws" - | "cis_1.4_aws" - | "cis_1.5_aws" - | "cis_2.0_aws" - | "cis_3.0_aws" - | "cis_4.0_aws" - | "cis_5.0_aws"; - export interface CompliancesOverview { data: ComplianceOverviewData[]; } From 6332427e5ee6e3034c16a6609261c360f0451c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Thu, 5 Jun 2025 10:54:51 +0200 Subject: [PATCH 08/13] fix(compliance): add manual status to requirements (#7938) --- api/src/backend/api/compliance.py | 14 +++++++------- api/src/backend/api/tests/test_compliance.py | 12 ++++++------ api/src/backend/api/tests/test_views.py | 18 ++++++++++++++++++ api/src/backend/api/v1/views.py | 14 +++++++++++--- api/src/backend/conftest.py | 18 +++++++++++++++++- 5 files changed, 59 insertions(+), 17 deletions(-) diff --git a/api/src/backend/api/compliance.py b/api/src/backend/api/compliance.py index 96b5e313f3..46e7211962 100644 --- a/api/src/backend/api/compliance.py +++ b/api/src/backend/api/compliance.py @@ -190,6 +190,8 @@ def generate_compliance_overview_template(prowler_compliance: dict): total_checks = len(requirement.Checks) checks_dict = {check: None for check in requirement.Checks} + req_status_val = "MANUAL" if total_checks == 0 else "PASS" + # Build requirement dictionary requirement_dict = { "name": requirement.Name or requirement.Id, @@ -204,20 +206,18 @@ def generate_compliance_overview_template(prowler_compliance: dict): "manual": 0, "total": total_checks, }, - "status": "PASS", + "status": req_status_val, } - # Update requirements status - if total_checks == 0: + # Update requirements status counts for the framework + if req_status_val == "MANUAL": requirements_status["manual"] += 1 + elif req_status_val == "PASS": + requirements_status["passed"] += 1 # Add requirement to compliance requirements compliance_requirements[requirement.Id] = requirement_dict - # Calculate pending requirements - pending_requirements = total_requirements - requirements_status["manual"] - requirements_status["passed"] = pending_requirements - # Build compliance dictionary compliance_dict = { "framework": compliance_data.Framework, diff --git a/api/src/backend/api/tests/test_compliance.py b/api/src/backend/api/tests/test_compliance.py index 6b405536b3..bf3208a4d1 100644 --- a/api/src/backend/api/tests/test_compliance.py +++ b/api/src/backend/api/tests/test_compliance.py @@ -1,12 +1,12 @@ -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch from api.compliance import ( + generate_compliance_overview_template, + generate_scan_compliance, get_prowler_provider_checks, get_prowler_provider_compliance, - load_prowler_compliance, load_prowler_checks, - generate_scan_compliance, - generate_compliance_overview_template, + load_prowler_compliance, ) from api.models import Provider @@ -69,7 +69,7 @@ class TestCompliance: load_prowler_compliance() - from api.compliance import PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE, PROWLER_CHECKS + from api.compliance import PROWLER_CHECKS, PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE assert PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE == { "template_key": "template_value" @@ -268,7 +268,7 @@ class TestCompliance: "manual": 0, "total": 0, }, - "status": "PASS", + "status": "MANUAL", }, }, "requirements_status": { diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 512ba79a93..a588b0ca01 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -4837,6 +4837,24 @@ class TestComplianceOverviewViewSet: assert "description" in attributes assert "status" in attributes + def test_compliance_overview_requirements_manual( + self, authenticated_client, compliance_requirements_overviews_fixture + ): + scan_id = str(compliance_requirements_overviews_fixture[0].scan.id) + # Compliance with a manual requirement + compliance_id = "aws_account_security_onboarding_aws" + + response = authenticated_client.get( + reverse("complianceoverview-requirements"), + { + "filter[scan_id]": scan_id, + "filter[compliance_id]": compliance_id, + }, + ) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert data[-1]["attributes"]["status"] == "MANUAL" + def test_compliance_overview_requirements_missing_scan_id( self, authenticated_client ): diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index 99b551fec5..96f1fbb5b4 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -2733,7 +2733,10 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin): "requirement_id", "framework", "version", "description" ) .distinct() - .annotate(total_instances=Count("id")) + .annotate( + total_instances=Count("id"), + manual_count=Count("id", filter=Q(requirement_status="MANUAL")), + ) ) passed_instances = ( @@ -2751,8 +2754,13 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin): requirement_id = requirement["requirement_id"] total_instances = requirement["total_instances"] passed_count = passed_counts.get(requirement_id, 0) - - requirement_status = "PASS" if passed_count == total_instances else "FAIL" + is_manual = requirement["manual_count"] == total_instances + if is_manual: + requirement_status = "MANUAL" + elif passed_count == total_instances: + requirement_status = "PASS" + else: + requirement_status = "FAIL" requirements_summary.append( { diff --git a/api/src/backend/conftest.py b/api/src/backend/conftest.py index be215ee59c..f3c99a42ce 100644 --- a/api/src/backend/conftest.py +++ b/api/src/backend/conftest.py @@ -846,8 +846,23 @@ def compliance_requirements_overviews_fixture(scans_fixture, tenants_fixture): total_checks=2, ) - # Create a different compliance framework for testing requirement_overview5 = ComplianceRequirementOverview.objects.create( + tenant=tenant, + scan=scan1, + compliance_id="aws_account_security_onboarding_aws", + framework="AWS-Account-Security-Onboarding", + version="1.0", + description="Description for AWS Account Security Onboarding (MANUAL)", + region="eu-west-2", + requirement_id="requirement3", + requirement_status=StatusChoices.MANUAL, + passed_checks=0, + failed_checks=0, + total_checks=0, + ) + + # Create a different compliance framework for testing + requirement_overview6 = ComplianceRequirementOverview.objects.create( tenant=tenant, scan=scan1, compliance_id="cis_1.4_aws", @@ -868,6 +883,7 @@ def compliance_requirements_overviews_fixture(scans_fixture, tenants_fixture): requirement_overview3, requirement_overview4, requirement_overview5, + requirement_overview6, ) From 1281f4ec5ee49a0c7ba55ae68e492cd94de39707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Thu, 5 Jun 2025 14:07:36 +0200 Subject: [PATCH 09/13] chore(changelog): update following the correct format (#7908) --- prowler/CHANGELOG.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 2d4a622c30..a6a60bbd13 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to the **Prowler SDK** are documented in this file. -## [5.8.0] (Prowler v5.8.0) +## [v5.8.0] (Prowler UNRELEASED) ### Added - Add CIS 1.11 compliance framework for Kubernetes. [(#7790)](https://github.com/prowler-cloud/prowler/pull/7790) @@ -25,7 +25,9 @@ All notable changes to the **Prowler SDK** are documented in this file. --- -### [v5.7.2] Fixed +## [v5.7.2] (Prowler v5.7.2) + +### Fixed - Fix `m365_powershell test_credentials` to use sanitized credentials. [(#7761)](https://github.com/prowler-cloud/prowler/pull/7761) - Fix `admincenter_users_admins_reduced_license_footprint` check logic to pass when admin user has no license. [(#7779)](https://github.com/prowler-cloud/prowler/pull/7779) - Fix `m365_powershell` to close the PowerShell sessions in msgraph services. [(#7816)](https://github.com/prowler-cloud/prowler/pull/7816) @@ -67,7 +69,6 @@ All notable changes to the **Prowler SDK** are documented in this file. ## [v5.6.0] (Prowler v5.6.0) ### Added - - Add SOC2 compliance framework to Azure. [(#7489)](https://github.com/prowler-cloud/prowler/pull/7489) - Add check for unused Service Accounts in GCP. [(#7419)](https://github.com/prowler-cloud/prowler/pull/7419) - Add Powershell to Microsoft365. [(#7331)](https://github.com/prowler-cloud/prowler/pull/7331) @@ -117,7 +118,6 @@ All notable changes to the **Prowler SDK** are documented in this file. - Add Microsoft User and User Credential auth to reports [(#7681)](https://github.com/prowler-cloud/prowler/pull/7681) ### Fixed - - Fix package name location in pyproject.toml while replicating for prowler-cloud. [(#7531)](https://github.com/prowler-cloud/prowler/pull/7531) - Remove cache in PyPI release action. [(#7532)](https://github.com/prowler-cloud/prowler/pull/7532) - Add the correct values for logger.info inside iam service. [(#7526)](https://github.com/prowler-cloud/prowler/pull/7526) @@ -138,7 +138,6 @@ All notable changes to the **Prowler SDK** are documented in this file. ## [v5.5.1] (Prowler v5.5.1) ### Fixed - - Add default name to contacts in Azure Defender. [(#7483)](https://github.com/prowler-cloud/prowler/pull/7483) - Handle projects without ID in GCP. [(#7496)](https://github.com/prowler-cloud/prowler/pull/7496) - Restore packages location in PyProject. [(#7510)](https://github.com/prowler-cloud/prowler/pull/7510) From a778d005b65f939c9931f128641bc2a50f27a7de Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Thu, 5 Jun 2025 14:10:27 +0200 Subject: [PATCH 10/13] fix(docs): add mfa warning for users (#7924) Co-authored-by: Pepe Fagoaga --- docs/getting-started/requirements.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/getting-started/requirements.md b/docs/getting-started/requirements.md index e5d6087c1f..3f3a48cbb2 100644 --- a/docs/getting-started/requirements.md +++ b/docs/getting-started/requirements.md @@ -170,6 +170,9 @@ These two new environment variables are **required** to execute the PowerShell m - `M365_USER` should be your Microsoft account email using the **assigned domain in the tenant**. This means it must look like `example@YourCompany.onmicrosoft.com` or `example@YourCompany.com`, but it must be the exact domain assigned to that user in the tenant. + ???+ warning + The user must not be MFA capable. Microsoft does not allow MFA capable users to authenticate programmatically. See [Microsoft documentation](https://learn.microsoft.com/en-us/entra/identity-platform/scenario-desktop-acquire-token-username-password?tabs=dotnet) for more information. + ???+ warning Using a tenant domain other than the one assigned โ€” even if it belongs to the same tenant โ€” will cause Prowler to fail, as Microsoft authentication will not succeed. From 75a11be9e66c1af194da508a736d4a0700c54ded Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Thu, 5 Jun 2025 14:22:43 +0200 Subject: [PATCH 11/13] fix(docs): add final permission assignments example (#7943) --- .../microsoft365/getting-started-m365.md | 4 ++++ .../microsoft365/img/final-permissions-m365.png | Bin 0 -> 82615 bytes 2 files changed, 4 insertions(+) create mode 100644 docs/tutorials/microsoft365/img/final-permissions-m365.png diff --git a/docs/tutorials/microsoft365/getting-started-m365.md b/docs/tutorials/microsoft365/getting-started-m365.md index 2fde79fe84..2dd0515b4d 100644 --- a/docs/tutorials/microsoft365/getting-started-m365.md +++ b/docs/tutorials/microsoft365/getting-started-m365.md @@ -138,6 +138,10 @@ Follow these steps to assign the permissions: ![Grant Admin Consent](./img/grant-admin-consent-delegated.png) + The final result of permission assignment should be this: + + ![Final Permission Assignment](./img/final-permissions-m365.png) + --- ### Assign required roles to your user diff --git a/docs/tutorials/microsoft365/img/final-permissions-m365.png b/docs/tutorials/microsoft365/img/final-permissions-m365.png new file mode 100644 index 0000000000000000000000000000000000000000..c26d80ef66e076ca2ac69442674c076b86a0befc GIT binary patch literal 82615 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 Date: Thu, 5 Jun 2025 14:23:12 +0200 Subject: [PATCH 12/13] fix(changelog): add entries for password encryption in v5.7.3 (#7939) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Vรญctor Fernรกndez Poyatos --- api/CHANGELOG.md | 4 +++- prowler/CHANGELOG.md | 9 ++++++++- ui/CHANGELOG.md | 8 ++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 524e83f46b..6662c7922c 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -9,7 +9,6 @@ All notable changes to the **Prowler API** are documented in this file. - Added new `GET /compliance-overviews` endpoints to retrieve compliance metadata and specific requirements statuses [(#7877)](https://github.com/prowler-cloud/prowler/pull/7877). ### Changed -- Renamed field encrypted_password to password for M365 provider [(#7784)](https://github.com/prowler-cloud/prowler/pull/7784) - Reworked `GET /compliance-overviews` to return proper requirement metrics [(#7877)](https://github.com/prowler-cloud/prowler/pull/7877). ### Fixed @@ -22,6 +21,9 @@ All notable changes to the **Prowler API** are documented in this file. ### Added - Database backend to handle already closed connections [(#7935)](https://github.com/prowler-cloud/prowler/pull/7935). +### Changed +- Renamed field encrypted_password to password for M365 provider [(#7784)](https://github.com/prowler-cloud/prowler/pull/7784) + ### Fixed - Fixed transaction persistence with RLS operations [(#7916)](https://github.com/prowler-cloud/prowler/pull/7916). - Reverted the change `get_with_retry` to use the original `get` method for retrieving tasks [(#7932)](https://github.com/prowler-cloud/prowler/pull/7932). diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index a6a60bbd13..27023b6827 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -25,6 +25,14 @@ All notable changes to the **Prowler SDK** are documented in this file. --- +## [v5.7.3] (Prowler v5.7.3) + +### Fixed +- Automatically encrypt password in Microsoft365 provider. [(#7784)](https://github.com/prowler-cloud/prowler/pull/7784) +- Remove last encrypted password appearances. [(#7825)](https://github.com/prowler-cloud/prowler/pull/7825) + +--- + ## [v5.7.2] (Prowler v5.7.2) ### Fixed @@ -62,7 +70,6 @@ All notable changes to the **Prowler SDK** are documented in this file. - Update and upgrade CIS for all the providers [(#7738)](https://github.com/prowler-cloud/prowler/pull/7738) - Cover policies with conditions with SNS endpoint in `sns_topics_not_publicly_accessible`. [(#7750)](https://github.com/prowler-cloud/prowler/pull/7750) - Change severity logic for `ec2_securitygroup_allow_ingress_from_internet_to_all_ports` check. [(#7764)](https://github.com/prowler-cloud/prowler/pull/7764) -- Automatically encrypt password in Microsoft365 provider. [(#7784)](https://github.com/prowler-cloud/prowler/pull/7784) --- diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 467fc2dd8f..9c0f33d57f 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -26,6 +26,14 @@ All notable changes to the **Prowler UI** are documented in this file. --- +## [v1.7.3] (Prowler v5.7.3) + +### ๐Ÿž Fixes + +- Fix encrypted password typo in `formSchemas`. [(#7828)](https://github.com/prowler-cloud/prowler/pull/7828) + +--- + ## [v1.7.2] (Prowler v5.7.2) ### ๐Ÿž Fixes From 8c95e1efafedc1038703a19258c424c748a9e812 Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Thu, 5 Jun 2025 19:39:36 +0545 Subject: [PATCH 13/13] chore: update API changelog for v5.7.3 (#7948) --- api/CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 6662c7922c..264a47235e 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -11,9 +11,6 @@ All notable changes to the **Prowler API** are documented in this file. ### Changed - Reworked `GET /compliance-overviews` to return proper requirement metrics [(#7877)](https://github.com/prowler-cloud/prowler/pull/7877). -### Fixed -- Fixed the connection status verification before launching a scan [(#7831)](https://github.com/prowler-cloud/prowler/pull/7831) - --- ## [v1.8.3] (Prowler v5.7.3) @@ -27,6 +24,8 @@ All notable changes to the **Prowler API** are documented in this file. ### Fixed - Fixed transaction persistence with RLS operations [(#7916)](https://github.com/prowler-cloud/prowler/pull/7916). - Reverted the change `get_with_retry` to use the original `get` method for retrieving tasks [(#7932)](https://github.com/prowler-cloud/prowler/pull/7932). +- Fixed the connection status verification before launching a scan [(#7831)](https://github.com/prowler-cloud/prowler/pull/7831) + ---