feat(feeds/advanced): add country filter. Closes #525#1028
feat(feeds/advanced): add country filter. Closes #525#1028lvb05 wants to merge 7 commits intointelowlproject:developfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an optional country query parameter to /api/feeds/advanced/ to filter returned IOCs by attacker country (case-insensitive), with accompanying documentation and tests.
Changes:
- Adds
countryto request param parsing/validation and applies it to the IOC queryset filter. - Documents the new query parameter on the advanced feeds endpoint.
- Adds test coverage for the new country filter behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
api/views/utils.py |
Parses country and applies attacker_country__iexact filtering in get_queryset(). |
api/serializers.py |
Extends FeedsRequestSerializer with a new country query param. |
api/views/feeds.py |
Updates feeds_advanced docstring to mention the new country filter. |
tests/api/views/test_feeds_advanced_view.py |
Adds unit tests for filtering by country (including case-insensitivity and empty result set). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
regulartim
left a comment
There was a problem hiding this comment.
Nice! 👍 Only minor details left.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
api/views/feeds.py:117
- The docstring says “attacker country code” but the model field is populated from GeoIP
country_name(e.g., “Germany”, “United States”) and the tests also use full names. To avoid misleading API consumers, either update this parameter documentation to describe a country name (or whatever canonical format is actually stored), or switch the stored value/filtering to ISO country codes if that’s the intended contract.
feed_type (str): Type of feed to retrieve. (supported: `cowrie`, `honeytrap`, etc.; default: `all`)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Hi @regulartim @mlodic ! |
tests/test_models.py
Outdated
| credential = self.cowrie_session.credentials.first() | ||
| self.assertEqual(credential.username, "root") | ||
| self.assertEqual(credential.password, "root") | ||
| self.assertEqual(self.cowrie_session.command_execution, True) |
There was a problem hiding this comment.
This change seems to be completely unrelated. Please keep your PR focused.
api/views/utils.py
Outdated
| country = query_params.get("country") | ||
| if isinstance(country, str): | ||
| country = country.strip() | ||
| self.country = country or None |
There was a problem hiding this comment.
This seems a little overengineered, no? self.country=query_params.get("country") seems sufficient. Empty strings and None get filtered out later, right?
tests/__init__.py
Outdated
| cls.ioc_4 = IOC.objects.create( | ||
| name="200.200.200.200", | ||
| type=IocType.IP.value, | ||
| first_seen=cls.current_time, | ||
| last_seen=cls.current_time, | ||
| days_seen=[cls.current_time], | ||
| number_of_days_seen=1, | ||
| attack_count=1, | ||
| interaction_count=1, | ||
| scanner=True, | ||
| payload_request=False, | ||
| related_urls=[], | ||
| ip_reputation="", | ||
| autonomous_system=cls.as_obj, | ||
| destination_ports=[], | ||
| login_attempts=0, | ||
| recurrence_probability=0.1, | ||
| expected_interactions=11.1, | ||
| attacker_country="United States", | ||
| ) | ||
|
|
There was a problem hiding this comment.
What's the puprose of this 4th IOC? As far I can see it is not needed in your test cases, right?
7617efb to
c1e06ac
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| recurrence_probability=0.1, | ||
| expected_interactions=11.1, | ||
| attacker_country="United States", | ||
| attacker_country="China", |
There was a problem hiding this comment.
This fixture change makes "United States" no longer present in the default dataset (ioc_3 is now "China"), which will cause test_200_countries in tests/api/views/test_statistics_view.py to fail (it asserts United States count == 1). Either restore the original attacker_country values in the shared fixtures or update the downstream statistics test expectations.
| attacker_country="China", | |
| attacker_country="United States", |
| format (str): Response format type. Besides `json`, `txt` and `csv` are supported but the response will only contain IOC values (e.g. IP addresses) without further information. (default: `json`) | ||
| tag_key (str, optional): Filter IOCs by tag key, e.g. `malware` or `confidence_of_abuse`. Only IOCs with at least one matching tag are returned. | ||
| tag_value (str, optional): Filter IOCs by tag value (case-insensitive substring match), e.g. `mirai`. Can be used alone or combined with `tag_key`. | ||
| country (str, optional): Filter IOCs by attacker country code (case-insensitive, e.g. `Germany`, `United States`). |
There was a problem hiding this comment.
The new docstring says "country code" but the implementation and examples here use full country names ("Germany", "United States"), and attacker_country is populated from geoip.country_name. Either update the parameter description to say "country name" or implement/describe ISO code support to match the PR description.
| country (str, optional): Filter IOCs by attacker country code (case-insensitive, e.g. `Germany`, `United States`). | |
| country (str, optional): Filter IOCs by attacker country name (case-insensitive, e.g. `Germany`, `United States`). |
| if feed_params.end_date: | ||
| query_dict["last_seen__lte"] = feed_params.end_date | ||
| if feed_params.country: | ||
| query_dict["attacker_country__iexact"] = feed_params.country |
There was a problem hiding this comment.
Consider normalizing the incoming country filter the same way as other string params (e.g., trimming whitespace). As-is, values like " Germany " will validate but never match attacker_country__iexact, which is surprising compared to tag_key/tag_value which are stripped in feeds_advanced.
| query_dict["attacker_country__iexact"] = feed_params.country | |
| country = feed_params.country.strip() | |
| if country: | |
| query_dict["attacker_country__iexact"] = country |
| values = [i["value"] for i in iocs] | ||
| self.assertIn(self.ioc.name, values) |
There was a problem hiding this comment.
This case-insensitive test only asserts that the expected IOC is included, but it doesn’t assert that non-matching countries are excluded. Strengthen it by asserting the response contains exactly the Germany IOC (e.g., len == 1 and attacker_country == "Germany") so the test would fail if the country filter is accidentally not applied.
| values = [i["value"] for i in iocs] | |
| self.assertIn(self.ioc.name, values) | |
| self.assertEqual(len(iocs), 1) | |
| self.assertEqual(iocs[0]["value"], self.ioc.name) | |
| self.assertEqual(iocs[0]["attacker_country"], "Germany") |
| recurrence_probability=0.1, | ||
| expected_interactions=11.1, | ||
| attacker_country="China", | ||
| attacker_country="Germany", |
There was a problem hiding this comment.
Changing the shared CustomTestCase fixture attacker_country from "China" to "Germany" will break existing statistics tests that assert country counts (e.g., tests/api/views/test_statistics_view.py::test_200_countries expects China/United States distribution based on the default fixtures). Prefer keeping the original fixture values and overriding attacker_country within the specific feed tests, or update the dependent statistics assertions accordingly.
| attacker_country="Germany", | |
| attacker_country="China", |


Description
Added a country query parameter to the /api/feeds/advanced/ endpoint. When provided, the endpoint filters IOCs by the attacker's country code (case-insensitive, e.g. US, de). The filter is optional and doesn't affect existing behavior when omitted.
Related issues
Fixes #525
Type of change
Checklist
Please complete this checklist carefully. It helps guide your contribution and lets maintainers verify that all requirements are met.
Formalities
<feature name>. Closes #999develop.develop.Docs and tests
Ruff) gave 0 errors. If you have correctly installed pre-commit, it does these checks and adjustments on your behalf.