Skip to content

Commit 4b48087

Browse files
committed
feat(api): add findings severity timeseries endpoint with filtering options
1 parent a07e599 commit 4b48087

File tree

8 files changed

+671
-128
lines changed

8 files changed

+671
-128
lines changed

api/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ All notable changes to the **Prowler API** are documented in this file.
2222
- Support Prowler ThreatScore for the K8S provider [(#9235)](https://github.com/prowler-cloud/prowler/pull/9235)
2323
- Enhanced compliance overview endpoint with provider filtering and latest scan aggregation [(#9244)](https://github.com/prowler-cloud/prowler/pull/9244)
2424
- New endpoint `GET /api/v1/overview/regions` to retrieve aggregated findings data by region [(#9273)](https://github.com/prowler-cloud/prowler/pull/9273)
25+
- New endpoint `GET /api/v1/overview/findings_severity_timeseries` to retrieve aggregated timeseries severities in granular way and Add new Index to `ScanSummary` model [(9307)](https://github.com/prowler-cloud/prowler/compare/PROWLER-25-finding-severity-over-time-component-api)
2526

2627
### Changed
2728
- Optimized database write queries for scan related tasks [(#9190)](https://github.com/prowler-cloud/prowler/pull/9190)

api/src/backend/api/filters.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from datetime import date, datetime, timedelta, timezone
2+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
23

34
from dateutil.parser import parse
45
from django.conf import settings
5-
from django.db.models import F, Q
6+
from django.db.models import F, Q, TextChoices
7+
from django.db.models.functions import TruncDay, TruncHour, TruncWeek
68
from django_filters.rest_framework import (
79
BaseInFilter,
810
BooleanFilter,
@@ -802,6 +804,89 @@ class Meta:
802804
}
803805

804806

807+
class ScanSummaryTimeSeriesFilter(ScanSummaryFilter):
808+
"""
809+
Filter for findings_severity_timeseries endpoint.
810+
Handles range-based filtering on inserted_at.
811+
"""
812+
813+
class TimeRangeChoices(TextChoices):
814+
ONE_DAY = "1D", "Last 24 Hours"
815+
FIVE_DAYS = "5D", "Last 5 Days"
816+
ONE_WEEK = "1W", "Last 7 Days"
817+
ONE_MONTH = "1M", "Last 30 Days"
818+
ALL = "All", "All Time"
819+
820+
TIME_RANGE_CONFIG = {
821+
TimeRangeChoices.ONE_DAY: {
822+
"delta": timedelta(days=1),
823+
"trunc": TruncHour,
824+
"granularity": "hour",
825+
},
826+
TimeRangeChoices.FIVE_DAYS: {
827+
"delta": timedelta(days=5),
828+
"trunc": TruncDay,
829+
"granularity": "day",
830+
},
831+
TimeRangeChoices.ONE_WEEK: {
832+
"delta": timedelta(weeks=1),
833+
"trunc": TruncDay,
834+
"granularity": "day",
835+
},
836+
TimeRangeChoices.ONE_MONTH: {
837+
"delta": timedelta(days=30),
838+
"trunc": TruncDay,
839+
"granularity": "day",
840+
},
841+
TimeRangeChoices.ALL: {
842+
"delta": None,
843+
"trunc": TruncWeek,
844+
"granularity": "week",
845+
},
846+
}
847+
848+
range = ChoiceFilter(
849+
choices=TimeRangeChoices.choices,
850+
method="filter_range",
851+
help_text="Time range for the series (e.g., `1D`). Defaults to `5D`.",
852+
)
853+
timezone = CharFilter(
854+
method="filter_noop",
855+
help_text="Timezone for aggregation following the IANA timezone database format (e.g., `America/New_York`, `Europe/London`) defaults to `UTC`.",
856+
)
857+
858+
@property
859+
def resolved_timezone(self):
860+
"""Resolves the timezone from filter data, defaulting to UTC."""
861+
tz_name = self.data.get("timezone", "UTC")
862+
try:
863+
return ZoneInfo(tz_name)
864+
except (ZoneInfoNotFoundError, ValueError):
865+
return timezone.utc
866+
867+
def get_config(self, value):
868+
"""Helper to get config with default fallback."""
869+
return (
870+
self.TIME_RANGE_CONFIG.get(value)
871+
or self.TIME_RANGE_CONFIG[self.TimeRangeChoices.FIVE_DAYS]
872+
)
873+
874+
def filter_range(self, queryset, name, value):
875+
"""Filters the queryset by the selected time range."""
876+
config = self.get_config(value)
877+
delta = config["delta"]
878+
879+
if delta is not None:
880+
start_date = datetime.now(self.resolved_timezone) - delta
881+
return queryset.filter(inserted_at__gte=start_date)
882+
883+
return queryset
884+
885+
def filter_noop(self, queryset, name, value):
886+
"""No-op filter to allow for timezone filtering."""
887+
return queryset
888+
889+
805890
class ScanSummarySeverityFilter(ScanSummaryFilter):
806891
"""Filter for findings_severity ScanSummary endpoint - includes status filters"""
807892

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 5.1.14 on 2025-11-26 09:23
2+
3+
from django.contrib.postgres.operations import AddIndexConcurrently
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
atomic = False
9+
10+
dependencies = [
11+
("api", "0059_compliance_overview_summary"),
12+
]
13+
14+
operations = [
15+
AddIndexConcurrently(
16+
model_name="scansummary",
17+
index=models.Index(
18+
fields=["tenant_id", "inserted_at"],
19+
include=["severity", "fail", "muted"],
20+
name="ss_tenant_time_covering_idx",
21+
),
22+
),
23+
]

api/src/backend/api/models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1494,6 +1494,11 @@ class Meta(RowLevelSecurityProtectedModel.Meta):
14941494
fields=["tenant_id", "scan_id", "severity"],
14951495
name="ss_tenant_scan_severity_idx",
14961496
),
1497+
models.Index(
1498+
fields=["tenant_id", "inserted_at"],
1499+
include=["severity", "fail", "muted"],
1500+
name="ss_tenant_time_covering_idx",
1501+
),
14971502
]
14981503

14991504
class JSONAPIMeta:

0 commit comments

Comments
 (0)