feat(api): finding group first_seen_at semantics and resource delta (#10595)

This commit is contained in:
Adrián Peña
2026-04-07 16:41:08 +02:00
committed by GitHub
parent 5e1e4bd8e4
commit abaacd7dbf
4 changed files with 47 additions and 1 deletions

View File

@@ -28,6 +28,7 @@ All notable changes to the **Prowler API** are documented in this file.
- Membership `post_delete` signal using raw FK ids to avoid `DoesNotExist` during cascade deletions [(#10497)](https://github.com/prowler-cloud/prowler/pull/10497)
- Finding group resources endpoints returning false 404 when filters match no results, and `sort` parameter being ignored [(#10510)](https://github.com/prowler-cloud/prowler/pull/10510)
- Jira integration failing with `JiraInvalidIssueTypeError` on non-English Jira instances due to hardcoded `"Task"` issue type; now dynamically fetches available issue types per project [(#10534)](https://github.com/prowler-cloud/prowler/pull/10534)
- Finding group `first_seen_at` now reflects when a new finding appeared in the scan instead of the oldest carry-forward date across all unchanged findings [(#10595)](https://github.com/prowler-cloud/prowler/pull/10595)
- Attack Paths: Remove `clear_cache` call from read-only query endpoints; cache clearing belongs to the scan/ingestion flow, not API queries [(#10586)](https://github.com/prowler-cloud/prowler/pull/10586)
### 🔐 Security

View File

@@ -4216,6 +4216,7 @@ class FindingGroupResourceSerializer(BaseSerializerV1):
provider = serializers.SerializerMethodField()
status = serializers.CharField()
severity = serializers.CharField()
delta = serializers.CharField(required=False, allow_null=True)
first_seen_at = serializers.DateTimeField(required=False, allow_null=True)
last_seen_at = serializers.DateTimeField(required=False, allow_null=True)
muted_reason = serializers.CharField(required=False, allow_null=True)

View File

@@ -7234,6 +7234,7 @@ class FindingGroupViewSet(BaseRLSViewSet):
_RESOURCE_SORT_MAP = {
"status": "status_order",
"severity": "severity_order",
"delta": "delta_order",
"first_seen_at": "first_seen_at",
"last_seen_at": "last_seen_at",
"resource.uid": "resource_uid",
@@ -7370,6 +7371,22 @@ class FindingGroupViewSet(BaseRLSViewSet):
output_field=IntegerField(),
)
),
delta_order=Max(
Case(
When(
finding__delta="new",
finding__muted=False,
then=Value(2),
),
When(
finding__delta="changed",
finding__muted=False,
then=Value(1),
),
default=Value(0),
output_field=IntegerField(),
)
),
first_seen_at=Min("finding__first_seen_at"),
last_seen_at=Max("finding__inserted_at"),
# Max() on muted_reason / check_metadata is safe because
@@ -7402,6 +7419,22 @@ class FindingGroupViewSet(BaseRLSViewSet):
output_field=IntegerField(),
)
),
"delta_order": lambda: Max(
Case(
When(
finding__delta="new",
finding__muted=False,
then=Value(2),
),
When(
finding__delta="changed",
finding__muted=False,
then=Value(1),
),
default=Value(0),
output_field=IntegerField(),
)
),
"first_seen_at": lambda: Min("finding__first_seen_at"),
"last_seen_at": lambda: Max("finding__inserted_at"),
"resource_uid": lambda: Max("resource__uid"),
@@ -7448,6 +7481,14 @@ class FindingGroupViewSet(BaseRLSViewSet):
else:
status = "MUTED"
delta_order = row.get("delta_order", 0)
if delta_order == 2:
delta = "new"
elif delta_order == 1:
delta = "changed"
else:
delta = None
results.append(
{
"resource_id": row["resource_id"],
@@ -7463,6 +7504,7 @@ class FindingGroupViewSet(BaseRLSViewSet):
"severity": SEVERITY_ORDER_REVERSE.get(
severity_order, "informational"
),
"delta": delta,
"first_seen_at": row["first_seen_at"],
"last_seen_at": row["last_seen_at"],
"muted_reason": row.get("muted_reason"),

View File

@@ -1824,7 +1824,9 @@ def aggregate_finding_group_summaries(tenant_id: str, scan_id: str):
filter=Q(status="FAIL", muted=False),
),
# Use prefixed names to avoid conflict with model field names
agg_first_seen_at=Min("first_seen_at"),
agg_first_seen_at=Min(
"first_seen_at", filter=Q(delta="new", muted=False)
),
agg_last_seen_at=Max("inserted_at"),
agg_failing_since=Min(
"first_seen_at", filter=Q(status="FAIL", muted=False)