Skip to content

Commit 760ccdb

Browse files
fix(api): treat muted findings as resolved in finding-groups status (#10826)
Co-authored-by: Adrián Peña <adrianjpr@gmail.com>
1 parent e61d5f2 commit 760ccdb

3 files changed

Lines changed: 111 additions & 24 deletions

File tree

api/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
All notable changes to the **Prowler API** are documented in this file.
44

5+
## [1.25.3] (Prowler v5.24.3)
6+
7+
### 🐞 Fixed
8+
9+
- Finding groups aggregated `status` now treats muted findings as resolved: a group is `FAIL` only while at least one non-muted FAIL remains, otherwise it is `PASS` (including fully-muted groups). The `filter[status]` filter and the `sort=status` ordering share the same semantics, keeping `status` consistent with `fail_count` and the orthogonal `muted` flag [(#10825)](https://github.com/prowler-cloud/prowler/pull/10825)
10+
11+
---
12+
513
## [1.25.2] (Prowler v5.24.2)
614

715
### 🔄 Changed

api/src/backend/api/tests/test_views.py

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15446,15 +15446,15 @@ def test_finding_groups_status_pass_when_no_fail(
1544615446
# iam_password_policy has only PASS findings
1544715447
assert data[0]["attributes"]["status"] == "PASS"
1544815448

15449-
def test_finding_groups_fully_muted_group_reflects_underlying_status(
15449+
def test_finding_groups_fully_muted_group_is_pass(
1545015450
self, authenticated_client, finding_groups_fixture
1545115451
):
15452-
"""A fully-muted group still surfaces its underlying status (no MUTED).
15452+
"""A fully-muted group reports status=PASS and muted=True.
1545315453

15454-
rds_encryption has 2 muted FAIL findings, so the group must report
15455-
status=FAIL (the orthogonal `muted` boolean signals it isn't actionable).
15456-
The status×muted breakdown lets clients answer 'how many failing
15457-
findings are muted in this group'.
15454+
rds_encryption has 2 muted FAIL findings. Muted findings are treated
15455+
as resolved/accepted, so the group is no longer actionable and its
15456+
status must be PASS. The `muted` flag is True because every finding
15457+
in the group is muted.
1545815458
"""
1545915459
response = authenticated_client.get(
1546015460
reverse("finding-group-list"),
@@ -15464,7 +15464,7 @@ def test_finding_groups_fully_muted_group_reflects_underlying_status(
1546415464
data = response.json()["data"]
1546515465
assert len(data) == 1
1546615466
attrs = data[0]["attributes"]
15467-
assert attrs["status"] == "FAIL"
15467+
assert attrs["status"] == "PASS"
1546815468
assert attrs["muted"] is True
1546915469
assert attrs["fail_count"] == 0
1547015470
assert attrs["fail_muted_count"] == 2
@@ -15479,6 +15479,83 @@ def test_finding_groups_fully_muted_group_reflects_underlying_status(
1547915479
== attrs["muted_count"]
1548015480
)
1548115481

15482+
def test_finding_groups_status_ignores_muted_failures(
15483+
self,
15484+
authenticated_client,
15485+
tenants_fixture,
15486+
scans_fixture,
15487+
resources_fixture,
15488+
):
15489+
"""Muted FAIL findings must not drive the aggregated status.
15490+
15491+
When a group mixes one non-muted PASS with one muted FAIL, the
15492+
actionable outcome is PASS: there are no unmuted failures left. The
15493+
aggregated `status` must reflect that (not FAIL), while `muted`
15494+
stays False because the group still has a non-muted finding.
15495+
"""
15496+
tenant = tenants_fixture[0]
15497+
scan1, *_ = scans_fixture
15498+
resource1, *_ = resources_fixture
15499+
15500+
pass_finding = Finding.objects.create(
15501+
tenant_id=tenant.id,
15502+
uid="fg_mixed_muted_pass",
15503+
scan=scan1,
15504+
delta=None,
15505+
status=Status.PASS,
15506+
severity=Severity.low,
15507+
impact=Severity.low,
15508+
check_id="mixed_muted_check",
15509+
check_metadata={
15510+
"CheckId": "mixed_muted_check",
15511+
"checktitle": "Mixed muted check",
15512+
"Description": "Fixture for muted status aggregation.",
15513+
},
15514+
first_seen_at="2024-01-11T00:00:00Z",
15515+
muted=False,
15516+
)
15517+
pass_finding.add_resources([resource1])
15518+
15519+
fail_muted_finding = Finding.objects.create(
15520+
tenant_id=tenant.id,
15521+
uid="fg_mixed_muted_fail",
15522+
scan=scan1,
15523+
delta=None,
15524+
status=Status.FAIL,
15525+
severity=Severity.high,
15526+
impact=Severity.high,
15527+
check_id="mixed_muted_check",
15528+
check_metadata={
15529+
"CheckId": "mixed_muted_check",
15530+
"checktitle": "Mixed muted check",
15531+
"Description": "Fixture for muted status aggregation.",
15532+
},
15533+
first_seen_at="2024-01-12T00:00:00Z",
15534+
muted=True,
15535+
)
15536+
fail_muted_finding.add_resources([resource1])
15537+
15538+
# filter[region] forces finding-level aggregation so we exercise the
15539+
# raw-findings path without touching the daily summary fixture.
15540+
response = authenticated_client.get(
15541+
reverse("finding-group-list"),
15542+
{
15543+
"filter[inserted_at]": TODAY,
15544+
"filter[check_id]": "mixed_muted_check",
15545+
"filter[region]": "us-east-1",
15546+
},
15547+
)
15548+
assert response.status_code == status.HTTP_200_OK
15549+
data = response.json()["data"]
15550+
assert len(data) == 1
15551+
attrs = data[0]["attributes"]
15552+
assert attrs["status"] == "PASS"
15553+
assert attrs["muted"] is False
15554+
assert attrs["pass_count"] == 1
15555+
assert attrs["fail_count"] == 0
15556+
assert attrs["fail_muted_count"] == 1
15557+
assert attrs["muted_count"] == 1
15558+
1548215559
def test_finding_groups_status_filter(
1548315560
self, authenticated_client, finding_groups_fixture
1548415561
):

api/src/backend/api/v1/views.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7281,14 +7281,18 @@ def _post_process_aggregation(self, aggregated_data):
72817281
# finding-level aggregation path.
72827282
row.pop("nonmuted_count", None)
72837283

7284-
# Compute aggregated status from non-muted counts first, then
7285-
# fall back to muted counts so fully-muted groups still reflect
7286-
# the underlying check outcome.
7287-
total_fail = row.get("fail_count", 0) + row.get("fail_muted_count", 0)
7288-
total_pass = row.get("pass_count", 0) + row.get("pass_muted_count", 0)
7289-
if total_fail > 0:
7284+
# Muted findings are treated as resolved/accepted, so they do not
7285+
# contribute to a failing status. A group is FAIL only when there
7286+
# is at least one non-muted FAIL; otherwise any pass (muted or
7287+
# not) or any muted fail makes the group PASS. Only groups whose
7288+
# findings are exclusively MANUAL fall through to MANUAL.
7289+
if row.get("fail_count", 0) > 0:
72907290
row["status"] = "FAIL"
7291-
elif total_pass > 0:
7291+
elif (
7292+
row.get("pass_count", 0) > 0
7293+
or row.get("pass_muted_count", 0) > 0
7294+
or row.get("fail_muted_count", 0) > 0
7295+
):
72927296
row["status"] = "PASS"
72937297
else:
72947298
row["status"] = "MANUAL"
@@ -7388,12 +7392,11 @@ def _apply_aggregated_computed_filters(self, queryset, computed_params: QueryDic
73887392

73897393
if computed_params.get("status") or computed_params.getlist("status__in"):
73907394
queryset = queryset.annotate(
7391-
total_fail=F("fail_count") + F("fail_muted_count"),
7392-
total_pass=F("pass_count") + F("pass_muted_count"),
7393-
).annotate(
73947395
aggregated_status=Case(
7395-
When(total_fail__gt=0, then=Value("FAIL")),
7396-
When(total_pass__gt=0, then=Value("PASS")),
7396+
When(fail_count__gt=0, then=Value("FAIL")),
7397+
When(pass_count__gt=0, then=Value("PASS")),
7398+
When(pass_muted_count__gt=0, then=Value("PASS")),
7399+
When(fail_muted_count__gt=0, then=Value("PASS")),
73977400
default=Value("MANUAL"),
73987401
output_field=CharField(),
73997402
)
@@ -7798,12 +7801,11 @@ def _sorted_paginated_response(
77987801
if ordering:
77997802
if any(field.lstrip("-") == "status_order" for field in ordering):
78007803
aggregated_queryset = aggregated_queryset.annotate(
7801-
total_fail_for_sort=F("fail_count") + F("fail_muted_count"),
7802-
total_pass_for_sort=F("pass_count") + F("pass_muted_count"),
7803-
).annotate(
78047804
status_order=Case(
7805-
When(total_fail_for_sort__gt=0, then=Value(3)),
7806-
When(total_pass_for_sort__gt=0, then=Value(2)),
7805+
When(fail_count__gt=0, then=Value(3)),
7806+
When(pass_count__gt=0, then=Value(2)),
7807+
When(pass_muted_count__gt=0, then=Value(2)),
7808+
When(fail_muted_count__gt=0, then=Value(2)),
78077809
default=Value(1),
78087810
output_field=IntegerField(),
78097811
)

0 commit comments

Comments
 (0)