|
23 | 23 | StatusEnumField, |
24 | 24 | ) |
25 | 25 | from api.models import ( |
| 26 | + AttackPathsScan, |
26 | 27 | AttackSurfaceOverview, |
27 | 28 | ComplianceRequirementOverview, |
28 | 29 | DailySeveritySummary, |
29 | 30 | Finding, |
| 31 | + FindingGroupDailySummary, |
30 | 32 | Integration, |
31 | 33 | Invitation, |
32 | | - AttackPathsScan, |
33 | 34 | LighthouseProviderConfiguration, |
34 | 35 | LighthouseProviderModels, |
35 | 36 | Membership, |
@@ -181,7 +182,7 @@ class CommonFindingFilters(FilterSet): |
181 | 182 | help_text="If this filter is not provided, muted and non-muted findings will be returned." |
182 | 183 | ) |
183 | 184 |
|
184 | | - resources = UUIDInFilter(field_name="resource__id", lookup_expr="in") |
| 185 | + resources = UUIDInFilter(field_name="resources__id", lookup_expr="in") |
185 | 186 |
|
186 | 187 | region = CharFilter(method="filter_resource_region") |
187 | 188 | region__in = CharInFilter(field_name="resource_regions", lookup_expr="overlap") |
@@ -469,9 +470,10 @@ class ResourceFilter(ProviderRelationshipFilterSet): |
469 | 470 | class Meta: |
470 | 471 | model = Resource |
471 | 472 | fields = { |
| 473 | + "id": ["exact", "in"], |
472 | 474 | "provider": ["exact", "in"], |
473 | | - "uid": ["exact", "icontains"], |
474 | | - "name": ["exact", "icontains"], |
| 475 | + "uid": ["exact", "icontains", "in"], |
| 476 | + "name": ["exact", "icontains", "in"], |
475 | 477 | "region": ["exact", "icontains", "in"], |
476 | 478 | "service": ["exact", "icontains", "in"], |
477 | 479 | "type": ["exact", "icontains", "in"], |
@@ -554,9 +556,10 @@ class LatestResourceFilter(ProviderRelationshipFilterSet): |
554 | 556 | class Meta: |
555 | 557 | model = Resource |
556 | 558 | fields = { |
| 559 | + "id": ["exact", "in"], |
557 | 560 | "provider": ["exact", "in"], |
558 | | - "uid": ["exact", "icontains"], |
559 | | - "name": ["exact", "icontains"], |
| 561 | + "uid": ["exact", "icontains", "in"], |
| 562 | + "name": ["exact", "icontains", "in"], |
560 | 563 | "region": ["exact", "icontains", "in"], |
561 | 564 | "service": ["exact", "icontains", "in"], |
562 | 565 | "type": ["exact", "icontains", "in"], |
@@ -647,16 +650,15 @@ def filter_queryset(self, queryset): |
647 | 650 | ] |
648 | 651 | ) |
649 | 652 |
|
650 | | - gte_date = ( |
651 | | - datetime.strptime(self.data.get("inserted_at__gte"), "%Y-%m-%d").date() |
652 | | - if self.data.get("inserted_at__gte") |
653 | | - else datetime.now(timezone.utc).date() |
654 | | - ) |
655 | | - lte_date = ( |
656 | | - datetime.strptime(self.data.get("inserted_at__lte"), "%Y-%m-%d").date() |
657 | | - if self.data.get("inserted_at__lte") |
658 | | - else datetime.now(timezone.utc).date() |
659 | | - ) |
| 653 | + cleaned = self.form.cleaned_data |
| 654 | + exact_date = cleaned.get("inserted_at") or cleaned.get("inserted_at__date") |
| 655 | + gte_date = cleaned.get("inserted_at__gte") or exact_date |
| 656 | + lte_date = cleaned.get("inserted_at__lte") or exact_date |
| 657 | + |
| 658 | + if gte_date is None: |
| 659 | + gte_date = datetime.now(timezone.utc).date() |
| 660 | + if lte_date is None: |
| 661 | + lte_date = datetime.now(timezone.utc).date() |
660 | 662 |
|
661 | 663 | if abs(lte_date - gte_date) > timedelta( |
662 | 664 | days=settings.FINDINGS_MAX_DAYS_IN_RANGE |
@@ -779,6 +781,267 @@ class Meta: |
779 | 781 | } |
780 | 782 |
|
781 | 783 |
|
| 784 | +class FindingGroupFilter(CommonFindingFilters): |
| 785 | + """ |
| 786 | + Filter for FindingGroup aggregations. |
| 787 | +
|
| 788 | + Requires at least one date filter for performance (partition pruning). |
| 789 | + Inherits all provider, status, severity, region, service filters from CommonFindingFilters. |
| 790 | + """ |
| 791 | + |
| 792 | + inserted_at = DateFilter(method="filter_inserted_at", lookup_expr="date") |
| 793 | + inserted_at__date = DateFilter(method="filter_inserted_at", lookup_expr="date") |
| 794 | + inserted_at__gte = DateFilter( |
| 795 | + method="filter_inserted_at_gte", |
| 796 | + help_text=f"Maximum date range is {settings.FINDINGS_MAX_DAYS_IN_RANGE} days.", |
| 797 | + ) |
| 798 | + inserted_at__lte = DateFilter( |
| 799 | + method="filter_inserted_at_lte", |
| 800 | + help_text=f"Maximum date range is {settings.FINDINGS_MAX_DAYS_IN_RANGE} days.", |
| 801 | + ) |
| 802 | + |
| 803 | + check_id = CharFilter(field_name="check_id", lookup_expr="exact") |
| 804 | + check_id__in = CharInFilter(field_name="check_id", lookup_expr="in") |
| 805 | + check_id__icontains = CharFilter(field_name="check_id", lookup_expr="icontains") |
| 806 | + |
| 807 | + class Meta: |
| 808 | + model = Finding |
| 809 | + fields = { |
| 810 | + "check_id": ["exact", "in", "icontains"], |
| 811 | + } |
| 812 | + |
| 813 | + def filter_queryset(self, queryset): |
| 814 | + """Validate that at least one date filter is provided.""" |
| 815 | + if not ( |
| 816 | + self.data.get("inserted_at") |
| 817 | + or self.data.get("inserted_at__date") |
| 818 | + or self.data.get("inserted_at__gte") |
| 819 | + or self.data.get("inserted_at__lte") |
| 820 | + ): |
| 821 | + raise ValidationError( |
| 822 | + [ |
| 823 | + { |
| 824 | + "detail": "At least one date filter is required: filter[inserted_at], filter[inserted_at.gte], " |
| 825 | + "or filter[inserted_at.lte].", |
| 826 | + "status": 400, |
| 827 | + "source": {"pointer": "/data/attributes/inserted_at"}, |
| 828 | + "code": "required", |
| 829 | + } |
| 830 | + ] |
| 831 | + ) |
| 832 | + |
| 833 | + # Validate date range doesn't exceed maximum |
| 834 | + cleaned = self.form.cleaned_data |
| 835 | + exact_date = cleaned.get("inserted_at") or cleaned.get("inserted_at__date") |
| 836 | + gte_date = cleaned.get("inserted_at__gte") or exact_date |
| 837 | + lte_date = cleaned.get("inserted_at__lte") or exact_date |
| 838 | + |
| 839 | + if gte_date is None: |
| 840 | + gte_date = datetime.now(timezone.utc).date() |
| 841 | + if lte_date is None: |
| 842 | + lte_date = datetime.now(timezone.utc).date() |
| 843 | + |
| 844 | + if abs(lte_date - gte_date) > timedelta( |
| 845 | + days=settings.FINDINGS_MAX_DAYS_IN_RANGE |
| 846 | + ): |
| 847 | + raise ValidationError( |
| 848 | + [ |
| 849 | + { |
| 850 | + "detail": f"The date range cannot exceed {settings.FINDINGS_MAX_DAYS_IN_RANGE} days.", |
| 851 | + "status": 400, |
| 852 | + "source": {"pointer": "/data/attributes/inserted_at"}, |
| 853 | + "code": "invalid", |
| 854 | + } |
| 855 | + ] |
| 856 | + ) |
| 857 | + |
| 858 | + return super().filter_queryset(queryset) |
| 859 | + |
| 860 | + def filter_inserted_at(self, queryset, name, value): |
| 861 | + """Filter by exact date using UUIDv7 partition-aware filtering.""" |
| 862 | + datetime_value = self._maybe_date_to_datetime(value) |
| 863 | + start = uuid7_start(datetime_to_uuid7(datetime_value)) |
| 864 | + end = uuid7_start(datetime_to_uuid7(datetime_value + timedelta(days=1))) |
| 865 | + return queryset.filter(id__gte=start, id__lt=end) |
| 866 | + |
| 867 | + def filter_inserted_at_gte(self, queryset, name, value): |
| 868 | + """Filter by start date using UUIDv7 partition-aware filtering.""" |
| 869 | + datetime_value = self._maybe_date_to_datetime(value) |
| 870 | + start = uuid7_start(datetime_to_uuid7(datetime_value)) |
| 871 | + return queryset.filter(id__gte=start) |
| 872 | + |
| 873 | + def filter_inserted_at_lte(self, queryset, name, value): |
| 874 | + """Filter by end date using UUIDv7 partition-aware filtering.""" |
| 875 | + datetime_value = self._maybe_date_to_datetime(value) |
| 876 | + end = uuid7_start(datetime_to_uuid7(datetime_value + timedelta(days=1))) |
| 877 | + return queryset.filter(id__lt=end) |
| 878 | + |
| 879 | + @staticmethod |
| 880 | + def _maybe_date_to_datetime(value): |
| 881 | + """Convert date to datetime if needed.""" |
| 882 | + dt = value |
| 883 | + if isinstance(value, date): |
| 884 | + dt = datetime.combine(value, datetime.min.time(), tzinfo=timezone.utc) |
| 885 | + return dt |
| 886 | + |
| 887 | + |
| 888 | +class LatestFindingGroupFilter(CommonFindingFilters): |
| 889 | + """ |
| 890 | + Filter for FindingGroup resources in /latest endpoint. |
| 891 | +
|
| 892 | + Same as FindingGroupFilter but without date validation. |
| 893 | + """ |
| 894 | + |
| 895 | + check_id = CharFilter(field_name="check_id", lookup_expr="exact") |
| 896 | + check_id__in = CharInFilter(field_name="check_id", lookup_expr="in") |
| 897 | + check_id__icontains = CharFilter(field_name="check_id", lookup_expr="icontains") |
| 898 | + |
| 899 | + class Meta: |
| 900 | + model = Finding |
| 901 | + fields = { |
| 902 | + "check_id": ["exact", "in", "icontains"], |
| 903 | + } |
| 904 | + |
| 905 | + |
| 906 | +class FindingGroupSummaryFilter(FilterSet): |
| 907 | + """ |
| 908 | + Filter for FindingGroupDailySummary queries. |
| 909 | +
|
| 910 | + Filters the pre-aggregated summary table by date range, check_id, and provider. |
| 911 | + Requires at least one date filter for performance. |
| 912 | + """ |
| 913 | + |
| 914 | + inserted_at = DateFilter(method="filter_inserted_at", lookup_expr="date") |
| 915 | + inserted_at__date = DateFilter(method="filter_inserted_at", lookup_expr="date") |
| 916 | + inserted_at__gte = DateFilter( |
| 917 | + method="filter_inserted_at_gte", |
| 918 | + help_text=f"Maximum date range is {settings.FINDINGS_MAX_DAYS_IN_RANGE} days.", |
| 919 | + ) |
| 920 | + inserted_at__lte = DateFilter( |
| 921 | + method="filter_inserted_at_lte", |
| 922 | + help_text=f"Maximum date range is {settings.FINDINGS_MAX_DAYS_IN_RANGE} days.", |
| 923 | + ) |
| 924 | + |
| 925 | + # Check ID filters |
| 926 | + check_id = CharFilter(field_name="check_id", lookup_expr="exact") |
| 927 | + check_id__in = CharInFilter(field_name="check_id", lookup_expr="in") |
| 928 | + check_id__icontains = CharFilter(field_name="check_id", lookup_expr="icontains") |
| 929 | + |
| 930 | + # Provider filters |
| 931 | + provider_id = UUIDFilter(field_name="provider_id", lookup_expr="exact") |
| 932 | + provider_id__in = UUIDInFilter(field_name="provider_id", lookup_expr="in") |
| 933 | + provider_type = ChoiceFilter( |
| 934 | + field_name="provider__provider", choices=Provider.ProviderChoices.choices |
| 935 | + ) |
| 936 | + provider_type__in = CharInFilter(field_name="provider__provider", lookup_expr="in") |
| 937 | + |
| 938 | + class Meta: |
| 939 | + model = FindingGroupDailySummary |
| 940 | + fields = { |
| 941 | + "check_id": ["exact", "in", "icontains"], |
| 942 | + "inserted_at": ["date", "gte", "lte"], |
| 943 | + "provider_id": ["exact", "in"], |
| 944 | + } |
| 945 | + |
| 946 | + def filter_queryset(self, queryset): |
| 947 | + if not ( |
| 948 | + self.data.get("inserted_at") |
| 949 | + or self.data.get("inserted_at__date") |
| 950 | + or self.data.get("inserted_at__gte") |
| 951 | + or self.data.get("inserted_at__lte") |
| 952 | + ): |
| 953 | + raise ValidationError( |
| 954 | + [ |
| 955 | + { |
| 956 | + "detail": "At least one date filter is required: filter[inserted_at], filter[inserted_at.gte], " |
| 957 | + "or filter[inserted_at.lte].", |
| 958 | + "status": 400, |
| 959 | + "source": {"pointer": "/data/attributes/inserted_at"}, |
| 960 | + "code": "required", |
| 961 | + } |
| 962 | + ] |
| 963 | + ) |
| 964 | + |
| 965 | + cleaned = self.form.cleaned_data |
| 966 | + exact_date = cleaned.get("inserted_at") or cleaned.get("inserted_at__date") |
| 967 | + gte_date = cleaned.get("inserted_at__gte") or exact_date |
| 968 | + lte_date = cleaned.get("inserted_at__lte") or exact_date |
| 969 | + |
| 970 | + if gte_date is None: |
| 971 | + gte_date = datetime.now(timezone.utc).date() |
| 972 | + if lte_date is None: |
| 973 | + lte_date = datetime.now(timezone.utc).date() |
| 974 | + |
| 975 | + if abs(lte_date - gte_date) > timedelta( |
| 976 | + days=settings.FINDINGS_MAX_DAYS_IN_RANGE |
| 977 | + ): |
| 978 | + raise ValidationError( |
| 979 | + [ |
| 980 | + { |
| 981 | + "detail": f"The date range cannot exceed {settings.FINDINGS_MAX_DAYS_IN_RANGE} days.", |
| 982 | + "status": 400, |
| 983 | + "source": {"pointer": "/data/attributes/inserted_at"}, |
| 984 | + "code": "invalid", |
| 985 | + } |
| 986 | + ] |
| 987 | + ) |
| 988 | + |
| 989 | + return super().filter_queryset(queryset) |
| 990 | + |
| 991 | + def filter_inserted_at(self, queryset, name, value): |
| 992 | + """Filter by exact inserted_at date.""" |
| 993 | + datetime_value = self._maybe_date_to_datetime(value) |
| 994 | + start = datetime_value |
| 995 | + end = datetime_value + timedelta(days=1) |
| 996 | + return queryset.filter(inserted_at__gte=start, inserted_at__lt=end) |
| 997 | + |
| 998 | + def filter_inserted_at_gte(self, queryset, name, value): |
| 999 | + """Filter by inserted_at >= value (date boundary).""" |
| 1000 | + datetime_value = self._maybe_date_to_datetime(value) |
| 1001 | + return queryset.filter(inserted_at__gte=datetime_value) |
| 1002 | + |
| 1003 | + def filter_inserted_at_lte(self, queryset, name, value): |
| 1004 | + """Filter by inserted_at <= value (inclusive date boundary).""" |
| 1005 | + datetime_value = self._maybe_date_to_datetime(value) |
| 1006 | + return queryset.filter(inserted_at__lt=datetime_value + timedelta(days=1)) |
| 1007 | + |
| 1008 | + @staticmethod |
| 1009 | + def _maybe_date_to_datetime(value): |
| 1010 | + dt = value |
| 1011 | + if isinstance(value, date): |
| 1012 | + dt = datetime.combine(value, datetime.min.time(), tzinfo=timezone.utc) |
| 1013 | + return dt |
| 1014 | + |
| 1015 | + |
| 1016 | +class LatestFindingGroupSummaryFilter(FilterSet): |
| 1017 | + """ |
| 1018 | + Filter for FindingGroupDailySummary /latest endpoint. |
| 1019 | +
|
| 1020 | + Same as FindingGroupSummaryFilter but without date validation. |
| 1021 | + Used when the endpoint automatically determines the date. |
| 1022 | + """ |
| 1023 | + |
| 1024 | + # Check ID filters |
| 1025 | + check_id = CharFilter(field_name="check_id", lookup_expr="exact") |
| 1026 | + check_id__in = CharInFilter(field_name="check_id", lookup_expr="in") |
| 1027 | + check_id__icontains = CharFilter(field_name="check_id", lookup_expr="icontains") |
| 1028 | + |
| 1029 | + # Provider filters |
| 1030 | + provider_id = UUIDFilter(field_name="provider_id", lookup_expr="exact") |
| 1031 | + provider_id__in = UUIDInFilter(field_name="provider_id", lookup_expr="in") |
| 1032 | + provider_type = ChoiceFilter( |
| 1033 | + field_name="provider__provider", choices=Provider.ProviderChoices.choices |
| 1034 | + ) |
| 1035 | + provider_type__in = CharInFilter(field_name="provider__provider", lookup_expr="in") |
| 1036 | + |
| 1037 | + class Meta: |
| 1038 | + model = FindingGroupDailySummary |
| 1039 | + fields = { |
| 1040 | + "check_id": ["exact", "in", "icontains"], |
| 1041 | + "provider_id": ["exact", "in"], |
| 1042 | + } |
| 1043 | + |
| 1044 | + |
782 | 1045 | class ProviderSecretFilter(FilterSet): |
783 | 1046 | inserted_at = DateFilter( |
784 | 1047 | field_name="inserted_at", |
|
0 commit comments