Skip to content

Commit bfb618b

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

File tree

5 files changed

+634
-128
lines changed

5 files changed

+634
-128
lines changed

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

0 commit comments

Comments
 (0)