From 3c9ae06086f22513e84e6be037dd45f30ce3e9ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Mon, 31 Mar 2025 12:25:58 +0200 Subject: [PATCH] feat(findings): Handle muted findings in API and UI (#7378) Co-authored-by: Pablo Lara --- api/CHANGELOG.md | 1 + api/src/backend/api/filters.py | 11 ++---- .../api/migrations/0015_finding_muted.py | 26 +++++++++++++ api/src/backend/api/models.py | 2 +- api/src/backend/api/specs/v1.yaml | 38 +++++++++++-------- api/src/backend/api/tests/test_views.py | 2 + api/src/backend/api/v1/serializers.py | 1 + api/src/backend/conftest.py | 1 + api/src/backend/tasks/jobs/scan.py | 29 +++++++------- api/src/backend/tasks/tests/test_scan.py | 1 + ui/CHANGELOG.md | 1 + ui/components/filters/data-filters.ts | 2 +- ui/components/findings/index.ts | 1 + ui/components/findings/muted.tsx | 19 ++++++++++ .../findings/table/column-findings.tsx | 22 ++++++++--- .../findings/table/finding-detail.tsx | 25 +++++++----- ui/components/icons/Icons.tsx | 28 ++++++++++++++ .../table/column-new-findings-to-date.tsx | 16 ++++++-- ui/components/ui/table/severity-badge.tsx | 2 +- .../ui/table/status-finding-badge.tsx | 6 ++- ui/types/components.ts | 3 +- 21 files changed, 174 insertions(+), 63 deletions(-) create mode 100644 api/src/backend/api/migrations/0015_finding_muted.py create mode 100644 ui/components/findings/index.ts create mode 100644 ui/components/findings/muted.tsx diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 00d6601ed6..370dbb2c94 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to the **Prowler API** are documented in this file. - Support for developing new integrations [(#7167)](https://github.com/prowler-cloud/prowler/pull/7167). - HTTP Security Headers [(#7289)](https://github.com/prowler-cloud/prowler/pull/7289). - New endpoint to get the compliance overviews metadata [(#7333)](https://github.com/prowler-cloud/prowler/pull/7333). +- Support for muted findings [(#7378)](https://github.com/prowler-cloud/prowler/pull/7378). --- diff --git a/api/src/backend/api/filters.py b/api/src/backend/api/filters.py index dbd73b41db..fc16cf394b 100644 --- a/api/src/backend/api/filters.py +++ b/api/src/backend/api/filters.py @@ -287,6 +287,9 @@ class FindingFilter(FilterSet): status = ChoiceFilter(choices=StatusChoices.choices) severity = ChoiceFilter(choices=SeverityChoices) impact = ChoiceFilter(choices=SeverityChoices) + muted = BooleanFilter( + help_text="If this filter is not provided, muted and non-muted findings will be returned." + ) resources = UUIDInFilter(field_name="resource__id", lookup_expr="in") @@ -614,12 +617,6 @@ class ScanSummaryFilter(FilterSet): field_name="scan__provider__provider", choices=Provider.ProviderChoices.choices ) region = CharFilter(field_name="region") - muted_findings = BooleanFilter(method="filter_muted_findings") - - def filter_muted_findings(self, queryset, name, value): - if not value: - return queryset.exclude(muted__gt=0) - return queryset class Meta: model = ScanSummary @@ -630,8 +627,6 @@ class ScanSummaryFilter(FilterSet): class ServiceOverviewFilter(ScanSummaryFilter): - muted_findings = None - def is_valid(self): # Check if at least one of the inserted_at filters is present inserted_at_filters = [ diff --git a/api/src/backend/api/migrations/0015_finding_muted.py b/api/src/backend/api/migrations/0015_finding_muted.py new file mode 100644 index 0000000000..3cb20f871b --- /dev/null +++ b/api/src/backend/api/migrations/0015_finding_muted.py @@ -0,0 +1,26 @@ +# Generated by Django 5.1.5 on 2025-03-25 11:29 + +from django.db import migrations, models + +import api.db_utils + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0014_integrations"), + ] + + operations = [ + migrations.AddField( + model_name="finding", + name="muted", + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name="finding", + name="status", + field=api.db_utils.StatusEnumField( + choices=[("FAIL", "Fail"), ("PASS", "Pass"), ("MANUAL", "Manual")] + ), + ), + ] diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index 0ac9f912a0..e22f37a07d 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -59,7 +59,6 @@ class StatusChoices(models.TextChoices): FAIL = "FAIL", _("Fail") PASS = "PASS", _("Pass") MANUAL = "MANUAL", _("Manual") - MUTED = "MUTED", _("Muted") class StateChoices(models.TextChoices): @@ -656,6 +655,7 @@ class Finding(PostgresPartitionedModel, RowLevelSecurityProtectedModel): tags = models.JSONField(default=dict, null=True, blank=True) check_id = models.CharField(max_length=100, blank=False, null=False) check_metadata = models.JSONField(default=dict, null=False) + muted = models.BooleanField(default=False, null=False) # Relationships scan = models.ForeignKey(to=Scan, related_name="findings", on_delete=models.CASCADE) diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index 36f67c0e75..30c9dbb675 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -294,6 +294,7 @@ paths: - inserted_at - updated_at - first_seen_at + - muted - url - scan - resources @@ -402,6 +403,12 @@ paths: type: string format: date description: Maximum date range is 7 days. + - in: query + name: filter[muted] + schema: + type: boolean + description: If this filter is not provided, muted and non-muted findings + will be returned. - in: query name: filter[provider] schema: @@ -633,13 +640,11 @@ paths: enum: - FAIL - MANUAL - - MUTED - PASS description: |- * `FAIL` - Fail * `PASS` - Pass * `MANUAL` - Manual - * `MUTED` - Muted - in: query name: filter[status__in] schema: @@ -756,6 +761,7 @@ paths: - inserted_at - updated_at - first_seen_at + - muted - url - scan - resources @@ -909,6 +915,12 @@ paths: type: string format: date description: Maximum date range is 7 days. + - in: query + name: filter[muted] + schema: + type: boolean + description: If this filter is not provided, muted and non-muted findings + will be returned. - in: query name: filter[provider] schema: @@ -1140,13 +1152,11 @@ paths: enum: - FAIL - MANUAL - - MUTED - PASS description: |- * `FAIL` - Fail * `PASS` - Pass * `MANUAL` - Manual - * `MUTED` - Muted - in: query name: filter[status__in] schema: @@ -1338,6 +1348,12 @@ paths: type: string format: date description: Maximum date range is 7 days. + - in: query + name: filter[muted] + schema: + type: boolean + description: If this filter is not provided, muted and non-muted findings + will be returned. - in: query name: filter[provider] schema: @@ -1569,13 +1585,11 @@ paths: enum: - FAIL - MANUAL - - MUTED - PASS description: |- * `FAIL` - Fail * `PASS` - Pass * `MANUAL` - Manual - * `MUTED` - Muted - in: query name: filter[status__in] schema: @@ -2019,10 +2033,6 @@ paths: schema: type: string format: date-time - - in: query - name: filter[muted_findings] - schema: - type: boolean - in: query name: filter[provider_id] schema: @@ -2180,10 +2190,6 @@ paths: schema: type: string format: date-time - - in: query - name: filter[muted_findings] - schema: - type: boolean - in: query name: filter[provider_id] schema: @@ -6238,13 +6244,11 @@ components: - FAIL - PASS - MANUAL - - MUTED type: string description: |- * `FAIL` - Fail * `PASS` - Pass * `MANUAL` - Manual - * `MUTED` - Muted status_extended: type: string nullable: true @@ -6280,6 +6284,8 @@ components: format: date-time readOnly: true nullable: true + muted: + type: boolean required: - uid - status diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index c11c0dec24..002af353da 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -2693,6 +2693,8 @@ class TestFindingViewSet: # ("resource_tags", "key:value", 2), # ("resource_tags", "not:exists", 0), # ("resource_tags", "not:exists,key:value", 2), + ("muted", True, 1), + ("muted", False, 1), ] ), ) diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index 8d45e495ea..96fe46eb85 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -1091,6 +1091,7 @@ class FindingSerializer(RLSSerializer): "inserted_at", "updated_at", "first_seen_at", + "muted", "url", # Relationships "scan", diff --git a/api/src/backend/conftest.py b/api/src/backend/conftest.py index 097998a976..acd33ea4d1 100644 --- a/api/src/backend/conftest.py +++ b/api/src/backend/conftest.py @@ -655,6 +655,7 @@ def findings_fixture(scans_fixture, resources_fixture): "Description": "test description orange juice", }, first_seen_at="2024-01-02T00:00:00Z", + muted=True, ) finding2.add_resources([resource2]) diff --git a/api/src/backend/tasks/jobs/scan.py b/api/src/backend/tasks/jobs/scan.py index 5c165b4cb4..998995731e 100644 --- a/api/src/backend/tasks/jobs/scan.py +++ b/api/src/backend/tasks/jobs/scan.py @@ -267,6 +267,7 @@ def perform_prowler_scan( check_id=finding.check_id, scan=scan_instance, first_seen_at=last_first_seen_at, + muted=finding.muted, ) finding_instance.add_resources([resource_instance]) @@ -402,21 +403,21 @@ def aggregate_findings(tenant_id: str, scan_id: str): ).annotate( fail=Sum( Case( - When(status="FAIL", then=1), + When(status="FAIL", muted=False, then=1), default=0, output_field=IntegerField(), ) ), _pass=Sum( Case( - When(status="PASS", then=1), + When(status="PASS", muted=False, then=1), default=0, output_field=IntegerField(), ) ), - muted=Sum( + muted_count=Sum( Case( - When(status="MUTED", then=1), + When(muted=True, then=1), default=0, output_field=IntegerField(), ) @@ -424,63 +425,63 @@ def aggregate_findings(tenant_id: str, scan_id: str): total=Count("id"), new=Sum( Case( - When(delta="new", then=1), + When(delta="new", muted=False, then=1), default=0, output_field=IntegerField(), ) ), changed=Sum( Case( - When(delta="changed", then=1), + When(delta="changed", muted=False, then=1), default=0, output_field=IntegerField(), ) ), unchanged=Sum( Case( - When(delta__isnull=True, then=1), + When(delta__isnull=True, muted=False, then=1), default=0, output_field=IntegerField(), ) ), fail_new=Sum( Case( - When(delta="new", status="FAIL", then=1), + When(delta="new", status="FAIL", muted=False, then=1), default=0, output_field=IntegerField(), ) ), fail_changed=Sum( Case( - When(delta="changed", status="FAIL", then=1), + When(delta="changed", status="FAIL", muted=False, then=1), default=0, output_field=IntegerField(), ) ), pass_new=Sum( Case( - When(delta="new", status="PASS", then=1), + When(delta="new", status="PASS", muted=False, then=1), default=0, output_field=IntegerField(), ) ), pass_changed=Sum( Case( - When(delta="changed", status="PASS", then=1), + When(delta="changed", status="PASS", muted=False, then=1), default=0, output_field=IntegerField(), ) ), muted_new=Sum( Case( - When(delta="new", status="MUTED", then=1), + When(delta="new", muted=True, then=1), default=0, output_field=IntegerField(), ) ), muted_changed=Sum( Case( - When(delta="changed", status="MUTED", then=1), + When(delta="changed", muted=True, then=1), default=0, output_field=IntegerField(), ) @@ -498,7 +499,7 @@ def aggregate_findings(tenant_id: str, scan_id: str): region=agg["resources__region"], fail=agg["fail"], _pass=agg["_pass"], - muted=agg["muted"], + muted=agg["muted_count"], total=agg["total"], new=agg["new"], changed=agg["changed"], diff --git a/api/src/backend/tasks/tests/test_scan.py b/api/src/backend/tasks/tests/test_scan.py index 78ba36fde8..66d1472c7b 100644 --- a/api/src/backend/tasks/tests/test_scan.py +++ b/api/src/backend/tasks/tests/test_scan.py @@ -107,6 +107,7 @@ class TestPerformScan: finding.service_name = "service_name" finding.resource_type = "resource_type" finding.resource_tags = {"tag1": "value1", "tag2": "value2"} + finding.muted = False finding.raw = {} # Mock the ProwlerScan instance diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 4500c57bd4..ea7a7565aa 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to the **Prowler UI** are documented in this file. - Added `one-time scan` feature: Adds support for single scan execution. [(#7188)](https://github.com/prowler-cloud/prowler/pull/7188) - Accepted invitations can no longer be edited. [(#7198)](https://github.com/prowler-cloud/prowler/pull/7198) - Added download column in scans table to download reports for completed scans. [(#7353)](https://github.com/prowler-cloud/prowler/pull/7353) +- Show muted icon when a finding is muted. [(#7378)](https://github.com/prowler-cloud/prowler/pull/7378) #### 🔄 Changed diff --git a/ui/components/filters/data-filters.ts b/ui/components/filters/data-filters.ts index 36594ec50b..58709ec167 100644 --- a/ui/components/filters/data-filters.ts +++ b/ui/components/filters/data-filters.ts @@ -42,7 +42,7 @@ export const filterFindings = [ { key: "status__in", labelCheckboxGroup: "Status", - values: ["PASS", "FAIL", "MANUAL", "MUTED"], + values: ["PASS", "FAIL", "MANUAL"], }, { key: "delta__in", diff --git a/ui/components/findings/index.ts b/ui/components/findings/index.ts new file mode 100644 index 0000000000..2e2bece9f7 --- /dev/null +++ b/ui/components/findings/index.ts @@ -0,0 +1 @@ +export * from "./muted"; diff --git a/ui/components/findings/muted.tsx b/ui/components/findings/muted.tsx new file mode 100644 index 0000000000..23c1422cf6 --- /dev/null +++ b/ui/components/findings/muted.tsx @@ -0,0 +1,19 @@ +import { Tooltip } from "@nextui-org/react"; + +import { MutedIcon } from "../icons"; + +interface MutedProps { + isMuted: boolean; +} + +export const Muted = ({ isMuted }: MutedProps) => { + if (isMuted === false) return null; + + return ( + +
+ +
+
+ ); +}; diff --git a/ui/components/findings/table/column-findings.tsx b/ui/components/findings/table/column-findings.tsx index f4560f3396..f341ad26ea 100644 --- a/ui/components/findings/table/column-findings.tsx +++ b/ui/components/findings/table/column-findings.tsx @@ -14,6 +14,8 @@ import { } from "@/components/ui/table"; import { FindingProps } from "@/types"; +import { Muted } from "../muted"; + const getFindingsData = (row: { original: FindingProps }) => { return row.original; }; @@ -91,10 +93,18 @@ export const ColumnFindings: ColumnDef[] = [ ), cell: ({ row }) => { const { checktitle } = getFindingsMetadata(row); + const { + attributes: { muted }, + } = getFindingsData(row); return ( -

- {checktitle} -

+
+

+ {checktitle} +

+ + + +
); }, }, @@ -124,7 +134,7 @@ export const ColumnFindings: ColumnDef[] = [ attributes: { status }, } = getFindingsData(row); - return ; + return ; }, }, { @@ -169,7 +179,7 @@ export const ColumnFindings: ColumnDef[] = [ const region = getResourceData(row, "region"); return ( -
+
{typeof region === "string" ? region : "Invalid region"}
); @@ -180,7 +190,7 @@ export const ColumnFindings: ColumnDef[] = [ header: "Service", cell: ({ row }) => { const { servicename } = getFindingsMetadata(row); - return

{servicename}

; + return

{servicename}

; }, }, { diff --git a/ui/components/findings/table/finding-detail.tsx b/ui/components/findings/table/finding-detail.tsx index 82706b2afd..1c4044fd52 100644 --- a/ui/components/findings/table/finding-detail.tsx +++ b/ui/components/findings/table/finding-detail.tsx @@ -12,6 +12,8 @@ import { import { SeverityBadge } from "@/components/ui/table/severity-badge"; import { FindingProps } from "@/types"; +import { Muted } from "../muted"; + const renderValue = (value: string | null | undefined) => { return value && value.trim() !== "" ? value : "-"; }; @@ -66,17 +68,20 @@ export const FindingDetail = ({ {renderValue(attributes.check_metadata.checktitle)}
+
+ -
- {renderValue(attributes.status)} +
+ {renderValue(attributes.status)} +
diff --git a/ui/components/icons/Icons.tsx b/ui/components/icons/Icons.tsx index 4cf36bc6c7..7d7ba0f1ae 100644 --- a/ui/components/icons/Icons.tsx +++ b/ui/components/icons/Icons.tsx @@ -1025,3 +1025,31 @@ export const GCPIcon: React.FC = ({ ); }; + +export const MutedIcon: React.FC = ({ + size, + height, + width, + ...props +}) => { + return ( + + ); +}; diff --git a/ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx b/ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx index 87f2f28784..8bdae43ef9 100644 --- a/ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx +++ b/ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx @@ -10,6 +10,8 @@ import { TriggerSheet } from "@/components/ui/sheet"; import { SeverityBadge, StatusFindingBadge } from "@/components/ui/table"; import { FindingProps } from "@/types"; +import { Muted } from "../../../findings/muted"; + const getFindingsData = (row: { original: FindingProps }) => { return row.original; }; @@ -71,10 +73,18 @@ export const ColumnNewFindingsToDate: ColumnDef[] = [ header: "Finding", cell: ({ row }) => { const { checktitle } = getFindingsMetadata(row); + const { + attributes: { muted }, + } = getFindingsData(row); return ( -

- {checktitle} -

+
+

+ {checktitle} +

+ + + +
); }, }, diff --git a/ui/components/ui/table/severity-badge.tsx b/ui/components/ui/table/severity-badge.tsx index d15c8a098a..3c03b8d99b 100644 --- a/ui/components/ui/table/severity-badge.tsx +++ b/ui/components/ui/table/severity-badge.tsx @@ -45,7 +45,7 @@ export const SeverityBadge = ({ severity }: { severity: Severity }) => { color={color} endContent={getSeverityIcon(severity)} > - {severity} + {severity} ); }; diff --git a/ui/components/ui/table/status-finding-badge.tsx b/ui/components/ui/table/status-finding-badge.tsx index 3dfc6e2613..b9532d3b5d 100644 --- a/ui/components/ui/table/status-finding-badge.tsx +++ b/ui/components/ui/table/status-finding-badge.tsx @@ -25,13 +25,15 @@ export const StatusFindingBadge = ({ return ( - {status} + + {status.charAt(0).toUpperCase() + status.slice(1).toLowerCase()} + ); }; diff --git a/ui/types/components.ts b/ui/types/components.ts index 342e4d401f..5b2aa59437 100644 --- a/ui/types/components.ts +++ b/ui/types/components.ts @@ -597,10 +597,11 @@ export interface FindingProps { attributes: { uid: string; delta: "new" | "changed" | null; - status: "PASS" | "FAIL" | "MANUAL" | "MUTED"; + status: "PASS" | "FAIL" | "MANUAL"; status_extended: string; severity: "informational" | "low" | "medium" | "high" | "critical"; check_id: string; + muted: boolean; check_metadata: { risk: string; notes: string;