mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-04-14 16:50:04 +00:00
feat(api): finding group first_seen_at semantics and resource delta (#10595)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user