From 3367c703cd01be1c9cb7e9ee65fca03b453d1d35 Mon Sep 17 00:00:00 2001 From: Krishna Awasthi <140143710+opbot-xd@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:26:41 +0530 Subject: [PATCH 1/2] Add IOC type filter to Feeds API and page. Closes #551 (#610) * feat: add IOC type filter to Feeds API and page * fix: wrap Prioritize field in Col component to fix JSX syntax error * test: add frontend tests for IOC type filter * Fix test_valid_fields by adding missing required ioc_type field * Fix failing tests and bugs: Update assertions, fix feeds filtering, improve validation * refactor: adjust form column widths and consolidate form groups in the Feeds component. --- api/serializers.py | 5 +- api/views/feeds.py | 4 +- api/views/utils.py | 5 + frontend/src/components/feeds/Feeds.jsx | 35 +++++- .../tests/components/feeds/Feeds.test.jsx | 16 ++- tests/__init__.py | 24 ++++ tests/test_serializers.py | 1 + tests/test_views.py | 119 +++++++++++++----- 8 files changed, 168 insertions(+), 41 deletions(-) diff --git a/api/serializers.py b/api/serializers.py index 917a0f44..8be204e7 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -38,7 +38,9 @@ def validate(self, data): Check a given observable against regex expression """ observable = data["query"] - if not re.match(REGEX_IP, observable) or not re.match(REGEX_DOMAIN, observable): + if re.match(r"^[\d\.]+$", observable) and not re.match(REGEX_IP, observable): + raise serializers.ValidationError("Observable is not a valid IP") + if not re.match(REGEX_IP, observable) and not re.match(REGEX_DOMAIN, observable): raise serializers.ValidationError("Observable is not a valid IP or domain") try: required_object = IOC.objects.get(name=observable) @@ -95,6 +97,7 @@ def ordering_validation(ordering: str) -> str: class FeedsRequestSerializer(serializers.Serializer): feed_type = serializers.CharField(max_length=120) attack_type = serializers.ChoiceField(choices=["scanner", "payload_request", "all"]) + ioc_type = serializers.ChoiceField(choices=["ip", "domain", "all"]) max_age = serializers.IntegerField(min_value=1) min_days_seen = serializers.IntegerField(min_value=1) include_reputation = serializers.ListField(child=serializers.CharField(max_length=120)) diff --git a/api/views/feeds.py b/api/views/feeds.py index 5e309d11..1e953e26 100644 --- a/api/views/feeds.py +++ b/api/views/feeds.py @@ -33,7 +33,9 @@ def feeds(request, feed_type, attack_type, prioritize, format_): """ logger.info(f"request /api/feeds with params: feed type: {feed_type}, " f"attack_type: {attack_type}, prioritization: {prioritize}, format: {format_}") - feed_params = FeedRequestParams({"feed_type": feed_type, "attack_type": attack_type, "format_": format_}) + feed_params_data = request.query_params.dict() + feed_params_data.update({"feed_type": feed_type, "attack_type": attack_type, "format_": format_}) + feed_params = FeedRequestParams(feed_params_data) feed_params.apply_default_filters(request.query_params) feed_params.set_prioritization(prioritize) diff --git a/api/views/utils.py b/api/views/utils.py index 39c2ae1c..f3968740 100644 --- a/api/views/utils.py +++ b/api/views/utils.py @@ -46,6 +46,7 @@ class FeedRequestParams: Attributes: feed_type (str): Type of feed to retrieve (default: "all") attack_type (str): Type of attack to filter (default: "all") + ioc_type (str): Type of IOC to filter - 'ip', 'domain', or 'all' (default: "all") max_age (str): Maximum number of days since last occurrence (default: "3") min_days_seen (str): Minimum number of days on which an IOC must have been seen (default: "1") include_reputation (list): List of reputation values to include (default: []) @@ -65,6 +66,7 @@ def __init__(self, query_params: dict): """ self.feed_type = query_params.get("feed_type", "all").lower() self.attack_type = query_params.get("attack_type", "all").lower() + self.ioc_type = query_params.get("ioc_type", "all").lower() self.max_age = query_params.get("max_age", "3") self.min_days_seen = query_params.get("min_days_seen", "1") self.include_reputation = query_params["include_reputation"].split(";") if "include_reputation" in query_params else [] @@ -154,6 +156,9 @@ def get_queryset(request, feed_params, valid_feed_types): if feed_params.attack_type != "all": query_dict[feed_params.attack_type] = True + if feed_params.ioc_type != "all": + query_dict["type"] = feed_params.ioc_type + query_dict["last_seen__gte"] = datetime.now() - timedelta(days=int(feed_params.max_age)) if int(feed_params.min_days_seen) > 1: query_dict["number_of_days_seen__gte"] = int(feed_params.min_days_seen) diff --git a/frontend/src/components/feeds/Feeds.jsx b/frontend/src/components/feeds/Feeds.jsx index 314d3b3c..6b03bfe7 100644 --- a/frontend/src/components/feeds/Feeds.jsx +++ b/frontend/src/components/feeds/Feeds.jsx @@ -26,6 +26,12 @@ const attackTypeChoices = [ { label: "Payload request", value: "payload_request" }, ]; +const iocTypeChoices = [ + { label: "All", value: "all" }, + { label: "IP addresses", value: "ip" }, + { label: "Domains", value: "domain" }, +]; + const prioritizationChoices = [ { label: "Recent", value: "recent" }, { label: "Persistent", value: "persistent" }, @@ -36,6 +42,7 @@ const prioritizationChoices = [ const initialValues = { feeds_type: "all", attack_type: "all", + ioc_type: "all", prioritize: "recent", }; @@ -87,6 +94,7 @@ export default function Feeds() { params: { feed_type: initialValues.feeds_type, attack_type: initialValues.attack_type, + ioc_type: initialValues.ioc_type, prioritize: initialValues.prioritize, }, initialParams: { @@ -102,10 +110,11 @@ export default function Feeds() { (values) => { try { setUrl( - `${FEEDS_BASE_URI}/${values.feeds_type}/${values.attack_type}/${values.prioritize}.json` + `${FEEDS_BASE_URI}/${values.feeds_type}/${values.attack_type}/${values.prioritize}.json?ioc_type=${values.ioc_type}` ); initialValues.feeds_type = values.feeds_type; initialValues.attack_type = values.attack_type; + initialValues.ioc_type = values.ioc_type; initialValues.prioritize = values.prioritize; const resetPage = { @@ -148,7 +157,7 @@ export default function Feeds() { {(formik) => (
- +