From 593eb7570e9d31011851281459cd992054802b38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Mart=C3=ADnez?= Date: Thu, 20 Aug 2026 20:32:05 -0400 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20hunt-page=20ruleset=20tracking=20?= =?UTF-8?q?=E2=80=94=20favorites,=20rule=20counts,=20hunt=20provenance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New fields parsed on existing resources (all additive; an older server leaves them None): - YaraRuleset: favorite, favorited_at, rule_count (None means the server had no answer, distinct from 0), historical_hunt_count, and new_results_count (only when the list is asked to include counts). - HistoricalHunt: rule_id (the source ruleset), rule_modified (freeze-time audit value), and source_rule_changed — a tri-state answering "has the source ruleset's body changed since the hunt froze it?" (None = unknown, not 'unchanged'). New endpoints and filters: - ruleset_favorite(id, favorite): idempotent star/unstar; the response carries favorites_used/favorites_limit; over-budget refusals surface a machine-readable FAVORITE_LIMIT error. - ruleset_list(name=, status=, favorites_only=, has_new_results=, since=, include_counts=): the hunt-page filters, conjunctive and optional. - live_results_count(since=): per-live-hunt result counts in a window, one aggregate for every 'new results' badge. - live_feed(livescan_id=): scope the feed to one live hunt. Sync and asyncio clients both. The rules live-suite tests now create a uid-namespaced single-rule ruleset (deterministic rule_count, no name collisions on the shared stack) and exercise the favorite round-trip, provenance, counter increment, and the changed-since-freeze flip; their cassettes are removed to re-record against a stack that serves the new fields. --- src/polyswarm_api/aio/api.py | 54 +++++- src/polyswarm_api/api.py | 62 ++++++- src/polyswarm_api/resources.py | 52 ++++++ test/async_client_test.py | 55 +++++- test/client_scan_test.py | 57 +++++- test/vcr/test_async_rules.vcr | 323 --------------------------------- test/vcr/test_rules.vcr | 323 --------------------------------- 7 files changed, 257 insertions(+), 669 deletions(-) delete mode 100644 test/vcr/test_async_rules.vcr delete mode 100644 test/vcr/test_rules.vcr diff --git a/src/polyswarm_api/aio/api.py b/src/polyswarm_api/aio/api.py index 18ab4921..ec286cb3 100644 --- a/src/polyswarm_api/aio/api.py +++ b/src/polyswarm_api/aio/api.py @@ -502,7 +502,8 @@ async def live_stop(self, rule_id): return await self._single(resources.LiveYaraRuleset.delete(self, rule_id=rule_id)) async def live_feed(self, since=None, rule_name=None, family=None, - polyscore_lower=None, polyscore_upper=None, community=None): + polyscore_lower=None, polyscore_upper=None, community=None, + livescan_id=None): """ Get live hunts feed @@ -512,14 +513,29 @@ async def live_feed(self, since=None, rule_name=None, family=None, :param polyscore_lower: Polyscore lower bound for the hunt results. :param polyscore_upper: Polyscore upper bound for the hunt results. :param community: Community to retrieve live results from, or public/private. + :param livescan_id: Scope the feed to one live hunt's results. :return: Generator of HuntResult resources """ async for item in self._paginate(resources.LiveHuntResult.list( self, since=since, rule_name=rule_name, family=family, polyscore_lower=polyscore_lower, polyscore_upper=polyscore_upper, + livescan_id=livescan_id, community=community or self.community)): yield item + async def live_results_count(self, since=None): + """ + Per-live-hunt result counts for the current account, grouped by + livescan_id. One request answers every "new results in the window" + badge; a hunt absent from counts collected 0. + + :param since: Window in seconds (server default: 86400 — 24 hours) + :return: A LiveHuntResultCounts resource + """ + logger.info('Live results count since %s', since) + return await self._single( + resources.LiveHuntResultCounts.get(self, since=since, community=self.community)) + async def live_feed_delete(self, result_ids): """ Delete live feed results @@ -684,15 +700,47 @@ async def ruleset_delete(self, ruleset_id): logger.info('Delete ruleset %s', ruleset_id) return await self._single(resources.YaraRuleset.delete(self, id=ruleset_id, community=self.community)) - async def ruleset_list(self): + async def ruleset_list(self, name=None, status=None, favorites_only=None, + has_new_results=None, since=None, include_counts=None): """ List all YaraRulesets for the current account. + + All filters are optional and conjunctive: + :param name: Case-insensitive substring match on the ruleset name. + :param status: 'active' returns only rulesets whose live hunt is + currently running. + :param favorites_only: True returns only favorited rulesets. + :param has_new_results: True returns only rulesets whose live hunt + collected results inside the window. + :param since: Window in seconds for has_new_results/include_counts + (server default: 86400). + :param include_counts: True attaches new_results_count to each + ruleset that has a live hunt. :return: A generator of YaraRuleset resources """ logger.info('List rulesets') - async for item in self._paginate(resources.YaraRuleset.list(self, community=self.community)): + async for item in self._paginate(resources.YaraRuleset.list( + self, name=name, status=status, favorites_only=favorites_only, + has_new_results=has_new_results, since=since, + include_counts=include_counts, community=self.community)): yield item + async def ruleset_favorite(self, ruleset_id, favorite=True): + """ + Favorite or unfavorite a YaraRuleset. Idempotent; works while a live + hunt is running. Favorites are shared by the whole team and capped + (the response carries favorites_used / favorites_limit); when the + budget is exhausted the server refuses with a machine-readable + FAVORITE_LIMIT error. + + :param ruleset_id: Id of the ruleset + :param favorite: True to star, False to unstar + :return: A YaraRulesetFavorite resource + """ + logger.info('%s ruleset %s', 'Favorite' if favorite else 'Unfavorite', ruleset_id) + return await self._single(resources.YaraRulesetFavorite.update( + self, id=ruleset_id, favorite=favorite, community=self.community)) + async def tag_link_get(self, sha256): """ Fetch the Tags and Families associated with the given sha256. diff --git a/src/polyswarm_api/api.py b/src/polyswarm_api/api.py index 53400201..40e98315 100644 --- a/src/polyswarm_api/api.py +++ b/src/polyswarm_api/api.py @@ -568,6 +568,7 @@ def live_feed( polyscore_lower=None, polyscore_upper=None, community=None, + livescan_id=None, ): """ Get live hunts feed @@ -578,6 +579,7 @@ def live_feed( :param polyscore_lower: Polyscore lower bound for the hunt results. :param polyscore_upper: Polyscore upper bound for the hunt results. :param community: Community to retrieve live results from, or public/private. + :param livescan_id: Scope the feed to one live hunt's results. :return: Generator of HuntResult resources """ for item in self._paginate( @@ -588,6 +590,7 @@ def live_feed( family=family, polyscore_lower=polyscore_lower, polyscore_upper=polyscore_upper, + livescan_id=livescan_id, community=community or self.community, ) ): @@ -823,17 +826,72 @@ def ruleset_delete(self, ruleset_id): resources.YaraRuleset.delete(self, id=ruleset_id, community=self.community) ) - def ruleset_list(self): + def ruleset_list(self, name=None, status=None, favorites_only=None, + has_new_results=None, since=None, include_counts=None): """ List all YaraRulesets for the current account. + + All filters are optional and conjunctive: + :param name: Case-insensitive substring match on the ruleset name. + :param status: 'active' returns only rulesets whose live hunt is + currently running. + :param favorites_only: True returns only favorited rulesets. + :param has_new_results: True returns only rulesets whose live hunt + collected results inside the window. + :param since: Window in seconds for has_new_results/include_counts + (server default: 86400). + :param include_counts: True attaches new_results_count to each + ruleset that has a live hunt. :return: A generator of YaraRuleset resources """ logger.info("List rulesets") for item in self._paginate( - resources.YaraRuleset.list(self, community=self.community) + resources.YaraRuleset.list( + self, + name=name, + status=status, + favorites_only=favorites_only, + has_new_results=has_new_results, + since=since, + include_counts=include_counts, + community=self.community, + ) ): yield item + def ruleset_favorite(self, ruleset_id, favorite=True): + """ + Favorite or unfavorite a YaraRuleset. Idempotent; works while a live + hunt is running. Favorites are shared by the whole team and capped + (the response carries favorites_used / favorites_limit); when the + budget is exhausted the server refuses with a machine-readable + FAVORITE_LIMIT error. + + :param ruleset_id: Id of the ruleset + :param favorite: True to star, False to unstar + :return: A YaraRulesetFavorite resource + """ + logger.info("%s ruleset %s", "Favorite" if favorite else "Unfavorite", ruleset_id) + return self._single( + resources.YaraRulesetFavorite.update( + self, id=ruleset_id, favorite=favorite, community=self.community + ) + ) + + def live_results_count(self, since=None): + """ + Per-live-hunt result counts for the current account, grouped by + livescan_id. One request answers every "new results in the window" + badge; a hunt absent from counts collected 0. + + :param since: Window in seconds (server default: 86400 — 24 hours) + :return: A LiveHuntResultCounts resource + """ + logger.info("Live results count since %s", since) + return self._single( + resources.LiveHuntResultCounts.get(self, since=since, community=self.community) + ) + def tag_link_get(self, sha256): """ Fetch the Tags and Families associated with the given sha256. diff --git a/src/polyswarm_api/resources.py b/src/polyswarm_api/resources.py index fa5f1c1d..22de9654 100644 --- a/src/polyswarm_api/resources.py +++ b/src/polyswarm_api/resources.py @@ -746,12 +746,41 @@ def __init__(self, content, api=None): self.modified = core.parse_isoformat(content.get('modified')) self.deleted = content.get('deleted') self.yara = content.get('yara') + # Hunt-page tracking fields. All additive — an older server simply + # leaves them None. + self.favorite = content.get('favorite') + self.favorited_at = core.parse_isoformat(content.get('favorited_at')) + # Number of rules in the ruleset body. None means "no answer" (the + # server could not or has not counted), which is different from 0. + self.rule_count = content.get('rule_count') + self.historical_hunt_count = content.get('historical_hunt_count') + # Live results collected in the requested window; only present when + # the list was asked to include counts, and only for rulesets with a + # live hunt — None otherwise. + self.new_results_count = content.get('new_results_count') class LiveYaraRuleset(YaraRuleset): RESOURCE_ENDPOINT = '/hunt/rule/live' +class YaraRulesetFavorite(core.BaseJsonResource): + """The favorite-toggle response: the ruleset's new star state plus the + team's budget usage, so callers can render "N of M used" without counting + client-side.""" + RESOURCE_ENDPOINT = '/hunt/rule/favorite' + # No query-string keys: the toggle takes {id, favorite} in the JSON body. + RESOURCE_ID_KEYS = [] + + def __init__(self, content, api=None): + super().__init__(content, api=api) + self.id = content.get('id') + self.favorite = content.get('favorite') + self.favorited_at = core.parse_isoformat(content.get('favorited_at')) + self.favorites_used = content.get('favorites_used') + self.favorites_limit = content.get('favorites_limit') + + class LiveHuntResult(core.BaseJsonResource): RESOURCE_ENDPOINT = '/hunt/live' @@ -778,6 +807,19 @@ class LiveHuntResultList(LiveHuntResult): RESOURCE_ENDPOINT = '/hunt/live/list' +class LiveHuntResultCounts(core.BaseJsonResource): + """Per-live-hunt result counts inside a window: ``since`` (seconds) plus + ``counts``, a list of ``{livescan_id, count}`` — one aggregate request for + every "new results" badge. A hunt with no results in the window is simply + absent from ``counts``; absence means 0.""" + RESOURCE_ENDPOINT = '/hunt/live/results/count' + + def __init__(self, content, api=None): + super().__init__(content, api=api) + self.since = content.get('since') + self.counts = content.get('counts') or [] + + class HistoricalHunt(core.BaseJsonResource): RESOURCE_ENDPOINT = '/hunt/historical' @@ -794,6 +836,16 @@ def __init__(self, content, api=None): self.progress = content['progress'] self.results_csv_uri = content['results_csv_uri'] self.communities = content.get('communities') + # Source-rule provenance. rule_id is the ruleset this hunt was + # triggered from (None for raw-yara hunts and hunts predating the + # tracking); rule_modified is a freeze-time audit timestamp. + self.rule_id = content.get('rule_id') + self.rule_modified = core.parse_isoformat(content.get('rule_modified')) + # Tri-state: has the source ruleset's body changed SINCE THE HUNT + # FROZE IT? True/False when the server could compare; None means + # unknown (no source rule, or nothing to compare against) — not + # "unchanged". + self.source_rule_changed = content.get('source_rule_changed') class HistoricalHuntList(HistoricalHunt): diff --git a/test/async_client_test.py b/test/async_client_test.py index b633553e..b495799d 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -608,26 +608,63 @@ async def test_async_sample(self, uid): # ── YARA Rulesets ───────────────────────────────────────────────────────── @vcr.use_cassette() - async def test_async_rules(self): + async def test_async_rules(self, uid): async with self._api() as api: - with open('test/eicar.yara') as f: - contents = f.read() - rule = await api.ruleset_create('test', contents) - assert rule.name == 'test' + # A uid-namespaced single-rule body: unique name on the shared + # stack, deterministic rule_count of 1. + contents = uid_yara(uid) + rule = await api.ruleset_create(uid, contents) + assert rule.name == uid assert rule.yara == contents + # Tracking fields are live from creation. + assert rule.rule_count == 1 + assert rule.favorite is False + assert rule.favorited_at is None + assert rule.historical_hunt_count == 0 + hunt = None try: # The e2e may carry leftover rulesets from prior runs; use # a presence assertion instead of an exact count. rules = [r async for r in api.ruleset_list()] assert any(r.id == rule.id for r in rules) + by_name = [r.id async for r in api.ruleset_list(name=uid)] + assert rule.id in by_name got = await api.ruleset_get(rule.id) - assert got.name == 'test' - - updated = await api.ruleset_update(rule.id, name='test2', description='test') - assert updated.name == 'test2' + assert got.name == uid + + # favorite round-trip with the server-owned budget counts + fav = await api.ruleset_favorite(rule.id, True) + assert fav.favorite is True + assert fav.favorited_at is not None + assert fav.favorites_limit == 5 + assert 1 <= fav.favorites_used <= fav.favorites_limit + favorites = [r async for r in api.ruleset_list(favorites_only=True)] + assert any(r.id == rule.id and r.favorite for r in favorites) + unfav = await api.ruleset_favorite(rule.id, False) + assert unfav.favorite is False + assert unfav.favorited_at is None + + # a hunt triggered FROM the ruleset carries provenance and + # bumps the counter; the create response's comparison is + # unknown (None) and a read resolves it + hunt = await api.historical_create(int(rule.id)) + assert hunt.rule_id == rule.id + assert hunt.rule_modified is not None + assert hunt.source_rule_changed is None + assert (await api.ruleset_get(rule.id)).historical_hunt_count == 1 + hunt_read = await api.historical_get(hunt.id) + assert hunt_read.source_rule_changed is False + + # a body edit flips the hunt's source_rule_changed + updated = await api.ruleset_update( + rule.id, name=f'{uid}2', rules=f'{contents}\n// edited', description='test') + assert updated.name == f'{uid}2' assert updated.description == 'test' + assert (await api.historical_get(hunt.id)).source_rule_changed is True finally: + if hunt is not None: + await api.historical_delete(hunt.id) await api.ruleset_delete(rule.id) remaining_ids = [] try: diff --git a/test/client_scan_test.py b/test/client_scan_test.py index 443f4a56..65d383d1 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -569,26 +569,65 @@ def test_historical_results(self): @vcr.use_cassette() def test_rules(self): api = PolyswarmAPI(self.test_api_key, uri=f'http://ai:9696/{self.api_version}', community='gamma') - # creating - with open('test/eicar.yara') as rule: - contents = rule.read() - rule = api.ruleset_create('test', contents) - assert rule.name == 'test' + # creating — a uid-namespaced single-rule body, so the name is unique + # on the shared stack and rule_count is deterministically 1. + uid = self._testMethodName + contents = uid_yara(uid) + rule = api.ruleset_create(uid, contents) + assert rule.name == uid assert rule.yara == contents + # The tracking fields are live from creation: the body was counted on + # the way in, nothing is starred yet, no hunts have been triggered. + assert rule.rule_count == 1 + assert rule.favorite is False + assert rule.favorited_at is None + assert rule.historical_hunt_count == 0 + hunt = None try: # listing — the created rule must be in the list; the e2e may carry # other rulesets from earlier runs, so use a presence assertion # rather than an exact count. rules = list(api.ruleset_list()) assert any(r.id == rule.id for r in rules) + # the name filter narrows to this test's own rule + by_name = [r.id for r in api.ruleset_list(name=uid)] + assert rule.id in by_name # getting got = api.ruleset_get(rule.id) - assert got.name == 'test' - # updating - updated = api.ruleset_update(rule.id, name='test2', description='test') - assert updated.name == 'test2' + assert got.name == uid + # favorite round-trip, with the budget counts the server owns + fav = api.ruleset_favorite(rule.id, True) + assert fav.favorite is True + assert fav.favorited_at is not None + assert fav.favorites_limit == 5 + # other runs may hold stars on the shared stack — bound, not pin + assert 1 <= fav.favorites_used <= fav.favorites_limit + favorites = list(api.ruleset_list(favorites_only=True)) + assert any(r.id == rule.id and r.favorite for r in favorites) + unfav = api.ruleset_favorite(rule.id, False) + assert unfav.favorite is False + assert unfav.favorited_at is None + # a historical hunt triggered FROM the ruleset carries the + # provenance and bumps the ruleset's counter; the create response's + # source_rule_changed is None (unknown until a read re-resolves it) + hunt = api.historical_create(int(rule.id)) + assert hunt.rule_id == rule.id + assert hunt.rule_modified is not None + assert hunt.source_rule_changed is None + assert api.ruleset_get(rule.id).historical_hunt_count == 1 + # a read of the fresh hunt resolves the comparison: unchanged body + hunt_read = api.historical_get(hunt.id) + assert hunt_read.rule_id == rule.id + assert hunt_read.source_rule_changed is False + # updating — a body edit flips the hunt's source_rule_changed + updated = api.ruleset_update( + rule.id, name=f'{uid}2', rules=f'{contents}\n// edited', description='test') + assert updated.name == f'{uid}2' assert updated.description == 'test' + assert api.historical_get(hunt.id).source_rule_changed is True finally: + if hunt is not None: + api.historical_delete(hunt.id) # deleting — the created rule disappears from the list. api.ruleset_delete(rule.id) remaining_ids = [] diff --git a/test/vcr/test_async_rules.vcr b/test/vcr/test_async_rules.vcr deleted file mode 100644 index 751c4b2a..00000000 --- a/test/vcr/test_async_rules.vcr +++ /dev/null @@ -1,323 +0,0 @@ -interactions: -- request: - body: '{"yara":"rule eicar_av_test {\n /*\n Per standard, match only - if entire file is EICAR string plus optional trailing whitespace.\n The - raw EICAR string to be matched is:\n X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*\n */\n\n meta:\n description - = \"This is a standard AV test, intended to verify that BinaryAlert is working - correctly.\"\n author = \"Austin Byers | Airbnb CSIRT\"\n reference - = \"http://www.eicar.org/86-0-Intended-use.html\"\n\n strings:\n $eicar_regex - = /^X5O!P%@AP\\[4\\\\PZX54\\(P\\^\\)7CC\\)7\\}\\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\\$H\\+H\\*\\s*$/\n\n condition:\n all - of them\n}\n\nrule eicar_substring_test {\n /*\n More generic - match - just the embedded EICAR string (e.g. in packed executables, PDFs, etc)\n */\n\n meta:\n description - = \"Standard AV test, checking for an EICAR substring\"\n author = \"Austin - Byers | Airbnb CSIRT\"\n\n strings:\n $eicar_substring = \"$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\"\n\n condition:\n all - of them\n}","name":"test"}' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - content-length: - - '1126' - content-type: - - application/json - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: POST - uri: http://ai:9696/v3/hunt/rule - response: - body: - string: '{"result":{"created":"2026-06-02T21:30:27.081195+00:00","deleted":false,"description":null,"id":"40732127887168358","livescan_created":null,"livescan_id":null,"modified":"2026-06-02T21:30:27.081195+00:00","name":"test","yara":"rule - eicar_av_test {\n /*\n Per standard, match only if entire file is - EICAR string plus optional trailing whitespace.\n The raw EICAR string - to be matched is:\n X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*\n */\n\n meta:\n description - = \"This is a standard AV test, intended to verify that BinaryAlert is working - correctly.\"\n author = \"Austin Byers | Airbnb CSIRT\"\n reference - = \"http://www.eicar.org/86-0-Intended-use.html\"\n\n strings:\n $eicar_regex - = /^X5O!P%@AP\\[4\\\\PZX54\\(P\\^\\)7CC\\)7\\}\\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\\$H\\+H\\*\\s*$/\n\n condition:\n all - of them\n}\n\nrule eicar_substring_test {\n /*\n More generic - match - just the embedded EICAR string (e.g. in packed executables, PDFs, etc)\n */\n\n meta:\n description - = \"Standard AV test, checking for an EICAR substring\"\n author = - \"Austin Byers | Airbnb CSIRT\"\n\n strings:\n $eicar_substring - = \"$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\"\n\n condition:\n all - of them\n}"},"status":"OK"} - - ' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '1346' - Content-Type: - - application/json - Date: - - Tue, 02 Jun 2026 21:30:27 GMT - Server: - - gunicorn - X-Billing-ID: - - '111' - status: - code: 200 - message: OK -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/rule/list?community=gamma - response: - body: - string: '{"has_more":false,"limit":50,"result":[{"created":"2026-06-02T21:30:27.081195+00:00","deleted":false,"description":null,"id":"40732127887168358","livescan_created":null,"livescan_id":null,"modified":"2026-06-02T21:30:27.081195+00:00","name":"test","yara":null}],"status":"OK"} - - ' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '277' - Content-Type: - - application/json - Date: - - Tue, 02 Jun 2026 21:30:27 GMT - Server: - - gunicorn - X-Billing-ID: - - '111' - status: - code: 200 - message: OK -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/rule?id=40732127887168358&community=gamma - response: - body: - string: '{"result":{"created":"2026-06-02T21:30:27.081195+00:00","deleted":false,"description":null,"id":"40732127887168358","livescan_created":null,"livescan_id":null,"modified":"2026-06-02T21:30:27.081195+00:00","name":"test","yara":"rule - eicar_av_test {\n /*\n Per standard, match only if entire file is - EICAR string plus optional trailing whitespace.\n The raw EICAR string - to be matched is:\n X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*\n */\n\n meta:\n description - = \"This is a standard AV test, intended to verify that BinaryAlert is working - correctly.\"\n author = \"Austin Byers | Airbnb CSIRT\"\n reference - = \"http://www.eicar.org/86-0-Intended-use.html\"\n\n strings:\n $eicar_regex - = /^X5O!P%@AP\\[4\\\\PZX54\\(P\\^\\)7CC\\)7\\}\\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\\$H\\+H\\*\\s*$/\n\n condition:\n all - of them\n}\n\nrule eicar_substring_test {\n /*\n More generic - match - just the embedded EICAR string (e.g. in packed executables, PDFs, etc)\n */\n\n meta:\n description - = \"Standard AV test, checking for an EICAR substring\"\n author = - \"Austin Byers | Airbnb CSIRT\"\n\n strings:\n $eicar_substring - = \"$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\"\n\n condition:\n all - of them\n}"},"status":"OK"} - - ' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '1346' - Content-Type: - - application/json - Date: - - Tue, 02 Jun 2026 21:30:27 GMT - Server: - - gunicorn - X-Billing-ID: - - '111' - status: - code: 200 - message: OK -- request: - body: '{"name":"test2","description":"test","community":"gamma"}' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - content-length: - - '57' - content-type: - - application/json - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: PUT - uri: http://ai:9696/v3/hunt/rule?id=40732127887168358 - response: - body: - string: '{"result":{"created":"2026-06-02T21:30:27.081195+00:00","deleted":false,"description":"test","id":"40732127887168358","livescan_created":null,"livescan_id":null,"modified":"2026-06-02T21:30:27.081195+00:00","name":"test2","yara":"rule - eicar_av_test {\n /*\n Per standard, match only if entire file is - EICAR string plus optional trailing whitespace.\n The raw EICAR string - to be matched is:\n X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*\n */\n\n meta:\n description - = \"This is a standard AV test, intended to verify that BinaryAlert is working - correctly.\"\n author = \"Austin Byers | Airbnb CSIRT\"\n reference - = \"http://www.eicar.org/86-0-Intended-use.html\"\n\n strings:\n $eicar_regex - = /^X5O!P%@AP\\[4\\\\PZX54\\(P\\^\\)7CC\\)7\\}\\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\\$H\\+H\\*\\s*$/\n\n condition:\n all - of them\n}\n\nrule eicar_substring_test {\n /*\n More generic - match - just the embedded EICAR string (e.g. in packed executables, PDFs, etc)\n */\n\n meta:\n description - = \"Standard AV test, checking for an EICAR substring\"\n author = - \"Austin Byers | Airbnb CSIRT\"\n\n strings:\n $eicar_substring - = \"$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\"\n\n condition:\n all - of them\n}"},"status":"OK"} - - ' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '1349' - Content-Type: - - application/json - Date: - - Tue, 02 Jun 2026 21:30:27 GMT - Server: - - gunicorn - X-Billing-ID: - - '111' - status: - code: 200 - message: OK -- request: - body: '{"community":"gamma"}' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - content-length: - - '21' - content-type: - - application/json - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: DELETE - uri: http://ai:9696/v3/hunt/rule?id=40732127887168358 - response: - body: - string: '{"result":{"created":"2026-06-02T21:30:27.081195+00:00","deleted":true,"description":"test","id":"40732127887168358","livescan_created":null,"livescan_id":null,"modified":"2026-06-02T21:30:27.152617+00:00","name":"test2","yara":"rule - eicar_av_test {\n /*\n Per standard, match only if entire file is - EICAR string plus optional trailing whitespace.\n The raw EICAR string - to be matched is:\n X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*\n */\n\n meta:\n description - = \"This is a standard AV test, intended to verify that BinaryAlert is working - correctly.\"\n author = \"Austin Byers | Airbnb CSIRT\"\n reference - = \"http://www.eicar.org/86-0-Intended-use.html\"\n\n strings:\n $eicar_regex - = /^X5O!P%@AP\\[4\\\\PZX54\\(P\\^\\)7CC\\)7\\}\\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\\$H\\+H\\*\\s*$/\n\n condition:\n all - of them\n}\n\nrule eicar_substring_test {\n /*\n More generic - match - just the embedded EICAR string (e.g. in packed executables, PDFs, etc)\n */\n\n meta:\n description - = \"Standard AV test, checking for an EICAR substring\"\n author = - \"Austin Byers | Airbnb CSIRT\"\n\n strings:\n $eicar_substring - = \"$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\"\n\n condition:\n all - of them\n}"},"status":"OK"} - - ' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '1348' - Content-Type: - - application/json - Date: - - Tue, 02 Jun 2026 21:30:27 GMT - Server: - - gunicorn - X-Billing-ID: - - '111' - status: - code: 200 - message: OK -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/rule/list?community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Tue, 02 Jun 2026 21:30:27 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -version: 1 diff --git a/test/vcr/test_rules.vcr b/test/vcr/test_rules.vcr deleted file mode 100644 index 7a3d8518..00000000 --- a/test/vcr/test_rules.vcr +++ /dev/null @@ -1,323 +0,0 @@ -interactions: -- request: - body: '{"yara":"rule eicar_av_test {\n /*\n Per standard, match only - if entire file is EICAR string plus optional trailing whitespace.\n The - raw EICAR string to be matched is:\n X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*\n */\n\n meta:\n description - = \"This is a standard AV test, intended to verify that BinaryAlert is working - correctly.\"\n author = \"Austin Byers | Airbnb CSIRT\"\n reference - = \"http://www.eicar.org/86-0-Intended-use.html\"\n\n strings:\n $eicar_regex - = /^X5O!P%@AP\\[4\\\\PZX54\\(P\\^\\)7CC\\)7\\}\\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\\$H\\+H\\*\\s*$/\n\n condition:\n all - of them\n}\n\nrule eicar_substring_test {\n /*\n More generic - match - just the embedded EICAR string (e.g. in packed executables, PDFs, etc)\n */\n\n meta:\n description - = \"Standard AV test, checking for an EICAR substring\"\n author = \"Austin - Byers | Airbnb CSIRT\"\n\n strings:\n $eicar_substring = \"$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\"\n\n condition:\n all - of them\n}","name":"test"}' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - content-length: - - '1126' - content-type: - - application/json - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: POST - uri: http://ai:9696/v3/hunt/rule - response: - body: - string: '{"result":{"created":"2026-06-02T21:32:01.681881+00:00","deleted":false,"description":null,"id":"68232116824597140","livescan_created":null,"livescan_id":null,"modified":"2026-06-02T21:32:01.681881+00:00","name":"test","yara":"rule - eicar_av_test {\n /*\n Per standard, match only if entire file is - EICAR string plus optional trailing whitespace.\n The raw EICAR string - to be matched is:\n X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*\n */\n\n meta:\n description - = \"This is a standard AV test, intended to verify that BinaryAlert is working - correctly.\"\n author = \"Austin Byers | Airbnb CSIRT\"\n reference - = \"http://www.eicar.org/86-0-Intended-use.html\"\n\n strings:\n $eicar_regex - = /^X5O!P%@AP\\[4\\\\PZX54\\(P\\^\\)7CC\\)7\\}\\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\\$H\\+H\\*\\s*$/\n\n condition:\n all - of them\n}\n\nrule eicar_substring_test {\n /*\n More generic - match - just the embedded EICAR string (e.g. in packed executables, PDFs, etc)\n */\n\n meta:\n description - = \"Standard AV test, checking for an EICAR substring\"\n author = - \"Austin Byers | Airbnb CSIRT\"\n\n strings:\n $eicar_substring - = \"$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\"\n\n condition:\n all - of them\n}"},"status":"OK"} - - ' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '1346' - Content-Type: - - application/json - Date: - - Tue, 02 Jun 2026 21:32:01 GMT - Server: - - gunicorn - X-Billing-ID: - - '111' - status: - code: 200 - message: OK -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/rule/list?community=gamma - response: - body: - string: '{"has_more":false,"limit":50,"result":[{"created":"2026-06-02T21:32:01.681881+00:00","deleted":false,"description":null,"id":"68232116824597140","livescan_created":null,"livescan_id":null,"modified":"2026-06-02T21:32:01.681881+00:00","name":"test","yara":null}],"status":"OK"} - - ' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '277' - Content-Type: - - application/json - Date: - - Tue, 02 Jun 2026 21:32:01 GMT - Server: - - gunicorn - X-Billing-ID: - - '111' - status: - code: 200 - message: OK -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/rule?id=68232116824597140&community=gamma - response: - body: - string: '{"result":{"created":"2026-06-02T21:32:01.681881+00:00","deleted":false,"description":null,"id":"68232116824597140","livescan_created":null,"livescan_id":null,"modified":"2026-06-02T21:32:01.681881+00:00","name":"test","yara":"rule - eicar_av_test {\n /*\n Per standard, match only if entire file is - EICAR string plus optional trailing whitespace.\n The raw EICAR string - to be matched is:\n X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*\n */\n\n meta:\n description - = \"This is a standard AV test, intended to verify that BinaryAlert is working - correctly.\"\n author = \"Austin Byers | Airbnb CSIRT\"\n reference - = \"http://www.eicar.org/86-0-Intended-use.html\"\n\n strings:\n $eicar_regex - = /^X5O!P%@AP\\[4\\\\PZX54\\(P\\^\\)7CC\\)7\\}\\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\\$H\\+H\\*\\s*$/\n\n condition:\n all - of them\n}\n\nrule eicar_substring_test {\n /*\n More generic - match - just the embedded EICAR string (e.g. in packed executables, PDFs, etc)\n */\n\n meta:\n description - = \"Standard AV test, checking for an EICAR substring\"\n author = - \"Austin Byers | Airbnb CSIRT\"\n\n strings:\n $eicar_substring - = \"$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\"\n\n condition:\n all - of them\n}"},"status":"OK"} - - ' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '1346' - Content-Type: - - application/json - Date: - - Tue, 02 Jun 2026 21:32:01 GMT - Server: - - gunicorn - X-Billing-ID: - - '111' - status: - code: 200 - message: OK -- request: - body: '{"name":"test2","description":"test","community":"gamma"}' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - content-length: - - '57' - content-type: - - application/json - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: PUT - uri: http://ai:9696/v3/hunt/rule?id=68232116824597140 - response: - body: - string: '{"result":{"created":"2026-06-02T21:32:01.681881+00:00","deleted":false,"description":"test","id":"68232116824597140","livescan_created":null,"livescan_id":null,"modified":"2026-06-02T21:32:01.681881+00:00","name":"test2","yara":"rule - eicar_av_test {\n /*\n Per standard, match only if entire file is - EICAR string plus optional trailing whitespace.\n The raw EICAR string - to be matched is:\n X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*\n */\n\n meta:\n description - = \"This is a standard AV test, intended to verify that BinaryAlert is working - correctly.\"\n author = \"Austin Byers | Airbnb CSIRT\"\n reference - = \"http://www.eicar.org/86-0-Intended-use.html\"\n\n strings:\n $eicar_regex - = /^X5O!P%@AP\\[4\\\\PZX54\\(P\\^\\)7CC\\)7\\}\\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\\$H\\+H\\*\\s*$/\n\n condition:\n all - of them\n}\n\nrule eicar_substring_test {\n /*\n More generic - match - just the embedded EICAR string (e.g. in packed executables, PDFs, etc)\n */\n\n meta:\n description - = \"Standard AV test, checking for an EICAR substring\"\n author = - \"Austin Byers | Airbnb CSIRT\"\n\n strings:\n $eicar_substring - = \"$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\"\n\n condition:\n all - of them\n}"},"status":"OK"} - - ' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '1349' - Content-Type: - - application/json - Date: - - Tue, 02 Jun 2026 21:32:01 GMT - Server: - - gunicorn - X-Billing-ID: - - '111' - status: - code: 200 - message: OK -- request: - body: '{"community":"gamma"}' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - content-length: - - '21' - content-type: - - application/json - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: DELETE - uri: http://ai:9696/v3/hunt/rule?id=68232116824597140 - response: - body: - string: '{"result":{"created":"2026-06-02T21:32:01.681881+00:00","deleted":true,"description":"test","id":"68232116824597140","livescan_created":null,"livescan_id":null,"modified":"2026-06-02T21:32:01.766631+00:00","name":"test2","yara":"rule - eicar_av_test {\n /*\n Per standard, match only if entire file is - EICAR string plus optional trailing whitespace.\n The raw EICAR string - to be matched is:\n X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*\n */\n\n meta:\n description - = \"This is a standard AV test, intended to verify that BinaryAlert is working - correctly.\"\n author = \"Austin Byers | Airbnb CSIRT\"\n reference - = \"http://www.eicar.org/86-0-Intended-use.html\"\n\n strings:\n $eicar_regex - = /^X5O!P%@AP\\[4\\\\PZX54\\(P\\^\\)7CC\\)7\\}\\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\\$H\\+H\\*\\s*$/\n\n condition:\n all - of them\n}\n\nrule eicar_substring_test {\n /*\n More generic - match - just the embedded EICAR string (e.g. in packed executables, PDFs, etc)\n */\n\n meta:\n description - = \"Standard AV test, checking for an EICAR substring\"\n author = - \"Austin Byers | Airbnb CSIRT\"\n\n strings:\n $eicar_substring - = \"$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\"\n\n condition:\n all - of them\n}"},"status":"OK"} - - ' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '1348' - Content-Type: - - application/json - Date: - - Tue, 02 Jun 2026 21:32:01 GMT - Server: - - gunicorn - X-Billing-ID: - - '111' - status: - code: 200 - message: OK -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/rule/list?community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Tue, 02 Jun 2026 21:32:01 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -version: 1 From 3c1861ffdbffacb1a728e13fd08f0510cc09f516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Mart=C3=ADnez?= Date: Thu, 20 Aug 2026 21:38:50 -0400 Subject: [PATCH 2/8] fix: regenerate the sync mirror; poll replica-backed assertions The sync api.py was hand-edited; scripts/regenerate_sync.py places live_results_count in aio's order and applies ruff's formatting, which is what the unasync-mirror CI gate diffs against. The rules live-tests' three read-after-write assertions (the counter, and both sides of the changed-since-freeze flip) now poll: those GETs read the replica, and on a real-replica stack the stale read of the flip is a silent False. Sleeps are free on VCR replay. --- src/polyswarm_api/api.py | 45 ++++++++++++++++++++++++--------------- test/_e2e_helpers.py | 40 ++++++++++++++++++++++++++++++++++ test/async_client_test.py | 19 ++++++++++++----- test/client_scan_test.py | 14 ++++++++---- 4 files changed, 92 insertions(+), 26 deletions(-) diff --git a/src/polyswarm_api/api.py b/src/polyswarm_api/api.py index 40e98315..62b27d70 100644 --- a/src/polyswarm_api/api.py +++ b/src/polyswarm_api/api.py @@ -596,6 +596,22 @@ def live_feed( ): yield item + def live_results_count(self, since=None): + """ + Per-live-hunt result counts for the current account, grouped by + livescan_id. One request answers every "new results in the window" + badge; a hunt absent from counts collected 0. + + :param since: Window in seconds (server default: 86400 — 24 hours) + :return: A LiveHuntResultCounts resource + """ + logger.info("Live results count since %s", since) + return self._single( + resources.LiveHuntResultCounts.get( + self, since=since, community=self.community + ) + ) + def live_feed_delete(self, result_ids): """ Delete live feed results @@ -826,8 +842,15 @@ def ruleset_delete(self, ruleset_id): resources.YaraRuleset.delete(self, id=ruleset_id, community=self.community) ) - def ruleset_list(self, name=None, status=None, favorites_only=None, - has_new_results=None, since=None, include_counts=None): + def ruleset_list( + self, + name=None, + status=None, + favorites_only=None, + has_new_results=None, + since=None, + include_counts=None, + ): """ List all YaraRulesets for the current account. @@ -871,27 +894,15 @@ def ruleset_favorite(self, ruleset_id, favorite=True): :param favorite: True to star, False to unstar :return: A YaraRulesetFavorite resource """ - logger.info("%s ruleset %s", "Favorite" if favorite else "Unfavorite", ruleset_id) + logger.info( + "%s ruleset %s", "Favorite" if favorite else "Unfavorite", ruleset_id + ) return self._single( resources.YaraRulesetFavorite.update( self, id=ruleset_id, favorite=favorite, community=self.community ) ) - def live_results_count(self, since=None): - """ - Per-live-hunt result counts for the current account, grouped by - livescan_id. One request answers every "new results in the window" - badge; a hunt absent from counts collected 0. - - :param since: Window in seconds (server default: 86400 — 24 hours) - :return: A LiveHuntResultCounts resource - """ - logger.info("Live results count since %s", since) - return self._single( - resources.LiveHuntResultCounts.get(self, since=since, community=self.community) - ) - def tag_link_get(self, sha256): """ Fetch the Tags and Families associated with the given sha256. diff --git a/test/_e2e_helpers.py b/test/_e2e_helpers.py index 59c5d21c..d150fa75 100644 --- a/test/_e2e_helpers.py +++ b/test/_e2e_helpers.py @@ -23,6 +23,7 @@ import os import re import tempfile +import time from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager @@ -132,3 +133,42 @@ async def run_concurrently_async(coros): if _vcr_off() and len(coros) > 1: return await asyncio.gather(*coros) return [await c for c in coros] + + +def poll_equals(read, want, tries=30, delay=1.0): + """Poll a zero-arg ``read`` until it returns ``want`` (or tries run out), + returning the last value read. For read-after-write assertions against + replica-backed GET endpoints (specs/04 in the server repo): on the e2e + stack the replica IS the primary so the first read usually wins, but a + real-replica stack lags — without the poll those assertions are + lag-flaky, and the changed-since-freeze one flakes in the silent + direction (stale source body reads as "unchanged"). Not-found during the + lag window counts as "not yet". Sleeps are free on VCR replay + (``_skip_poll_sleep_on_replay``).""" + from polyswarm_api import exceptions as _exceptions + value = None + for _ in range(tries): + try: + value = read() + except (_exceptions.NotFoundException, _exceptions.NoResultsException): + value = None + if value == want: + return value + time.sleep(delay) + return value + + +async def poll_equals_async(read, want, tries=30, delay=1.0): + """The asyncio twin of ``poll_equals`` (``read`` is a zero-arg coroutine + function).""" + from polyswarm_api import exceptions as _exceptions + value = None + for _ in range(tries): + try: + value = await read() + except (_exceptions.NotFoundException, _exceptions.NoResultsException): + value = None + if value == want: + return value + await asyncio.sleep(delay) + return value diff --git a/test/async_client_test.py b/test/async_client_test.py index b495799d..b281bb5c 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -29,7 +29,7 @@ from polyswarm_api import exceptions from test._e2e_helpers import ( - EICAR_STRING, malicious_artifact, artifact_file, uid_ip, uid_host, uid_yara, + EICAR_STRING, malicious_artifact, artifact_file, uid_ip, uid_host, uid_yara, poll_equals_async, assert_scanned, run_concurrently_async, ) @@ -652,16 +652,25 @@ async def test_async_rules(self, uid): assert hunt.rule_id == rule.id assert hunt.rule_modified is not None assert hunt.source_rule_changed is None - assert (await api.ruleset_get(rule.id)).historical_hunt_count == 1 - hunt_read = await api.historical_get(hunt.id) - assert hunt_read.source_rule_changed is False + + # replica-backed GETs: poll so a lagging replica (real + # stacks, not e2e) can't flake these — the changed-since- + # freeze flip's stale read is a silent False + async def _hunt_count(): + return (await api.ruleset_get(rule.id)).historical_hunt_count + + async def _changed(): + return (await api.historical_get(hunt.id)).source_rule_changed + + assert await poll_equals_async(_hunt_count, 1) == 1 + assert await poll_equals_async(_changed, False) is False # a body edit flips the hunt's source_rule_changed updated = await api.ruleset_update( rule.id, name=f'{uid}2', rules=f'{contents}\n// edited', description='test') assert updated.name == f'{uid}2' assert updated.description == 'test' - assert (await api.historical_get(hunt.id)).source_rule_changed is True + assert await poll_equals_async(_changed, True) is True finally: if hunt is not None: await api.historical_delete(hunt.id) diff --git a/test/client_scan_test.py b/test/client_scan_test.py index 65d383d1..a48cb140 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -17,7 +17,7 @@ from polyswarm_api import exceptions from test._e2e_helpers import ( - EICAR_STRING, malicious_artifact, artifact_file, uid_ip, uid_host, uid_yara, + EICAR_STRING, malicious_artifact, artifact_file, uid_ip, uid_host, uid_yara, poll_equals, assert_scanned, run_concurrently, ) @@ -614,17 +614,23 @@ def test_rules(self): assert hunt.rule_id == rule.id assert hunt.rule_modified is not None assert hunt.source_rule_changed is None - assert api.ruleset_get(rule.id).historical_hunt_count == 1 + # the GETs below read the replica; poll so a lagging replica + # (real stacks, not e2e) can't flake these — especially the + # changed-since-freeze flip, whose stale read is a silent False + assert poll_equals( + lambda: api.ruleset_get(rule.id).historical_hunt_count, 1) == 1 # a read of the fresh hunt resolves the comparison: unchanged body hunt_read = api.historical_get(hunt.id) assert hunt_read.rule_id == rule.id - assert hunt_read.source_rule_changed is False + assert poll_equals( + lambda: api.historical_get(hunt.id).source_rule_changed, False) is False # updating — a body edit flips the hunt's source_rule_changed updated = api.ruleset_update( rule.id, name=f'{uid}2', rules=f'{contents}\n// edited', description='test') assert updated.name == f'{uid}2' assert updated.description == 'test' - assert api.historical_get(hunt.id).source_rule_changed is True + assert poll_equals( + lambda: api.historical_get(hunt.id).source_rule_changed, True) is True finally: if hunt is not None: api.historical_delete(hunt.id) From 8aeaa03d7f265fe4daedf70937838fe2434970cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Mart=C3=ADnez?= Date: Thu, 20 Aug 2026 22:55:39 -0400 Subject: [PATCH 3/8] docs: livescan_id join key is a digit string Ids exceed JavaScript's safe-integer range; the counts entries carry the same digit string YaraRuleset.livescan_id does. --- src/polyswarm_api/resources.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/polyswarm_api/resources.py b/src/polyswarm_api/resources.py index 22de9654..10b909b0 100644 --- a/src/polyswarm_api/resources.py +++ b/src/polyswarm_api/resources.py @@ -810,8 +810,11 @@ class LiveHuntResultList(LiveHuntResult): class LiveHuntResultCounts(core.BaseJsonResource): """Per-live-hunt result counts inside a window: ``since`` (seconds) plus ``counts``, a list of ``{livescan_id, count}`` — one aggregate request for - every "new results" badge. A hunt with no results in the window is simply - absent from ``counts``; absence means 0.""" + every "new results" badge. ``livescan_id`` is a digit string, the same + value ``YaraRuleset.livescan_id`` carries (the join key; ids exceed + JavaScript's safe-integer range, so they are never bare JSON ints). A hunt + with no results in the window is simply absent from ``counts``; absence + means 0.""" RESOURCE_ENDPOINT = '/hunt/live/results/count' def __init__(self, content, api=None): From b66f9d473e9b82f0f89d11f22be4eb2615db5e48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Mart=C3=ADnez?= Date: Fri, 21 Aug 2026 09:59:54 -0400 Subject: [PATCH 4/8] test: record the rules cassettes against a live stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recorded against the branch server image (both tests green live first); the offline suite replays them — 163 passed with no stack. --- test/vcr/test_async_rules.vcr | 667 ++++++++++++++++++++++++++++++++ test/vcr/test_rules.vcr | 708 ++++++++++++++++++++++++++++++++++ 2 files changed, 1375 insertions(+) create mode 100644 test/vcr/test_async_rules.vcr create mode 100644 test/vcr/test_rules.vcr diff --git a/test/vcr/test_async_rules.vcr b/test/vcr/test_async_rules.vcr new file mode 100644 index 00000000..0c27690a --- /dev/null +++ b/test/vcr/test_async_rules.vcr @@ -0,0 +1,667 @@ +interactions: +- request: + body: '{"yara":"rule sdk_test_async_rules { strings: $u = \"test_async_rules\" + condition: $u }","name":"test_async_rules"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '115' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: POST + uri: http://ai:9696/v3/hunt/rule + response: + body: + string: '{"result":{"created":"2026-08-21T13:59:21.835873+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"69323308443642315","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:21.835873+00:00","name":"test_async_rules","rule_count":1,"yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '413' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:21 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T13:59:21.835873+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"69323308443642315","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:21.835873+00:00","name":"test_async_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '392' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:21 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?name=test_async_rules&community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T13:59:21.835873+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"69323308443642315","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:21.835873+00:00","name":"test_async_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '392' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:22 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule?id=69323308443642315&community=gamma + response: + body: + string: '{"result":{"created":"2026-08-21T13:59:21.835873+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"69323308443642315","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:21.835873+00:00","name":"test_async_rules","rule_count":1,"yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '413' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:22 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"id":"69323308443642315","favorite":1,"community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '59' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: PUT + uri: http://ai:9696/v3/hunt/rule/favorite + response: + body: + string: '{"result":{"favorite":true,"favorited_at":"2026-08-21T13:59:22.129436+00:00","favorites_limit":5,"favorites_used":1,"id":"69323308443642315"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '157' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:22 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?favorites_only=1&community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T13:59:21.835873+00:00","deleted":false,"description":null,"favorite":true,"favorited_at":"2026-08-21T13:59:22.129436+00:00","historical_hunt_count":0,"id":"69323308443642315","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:21.835873+00:00","name":"test_async_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '421' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:22 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"id":"69323308443642315","favorite":0,"community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '59' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: PUT + uri: http://ai:9696/v3/hunt/rule/favorite + response: + body: + string: '{"result":{"favorite":false,"favorited_at":null,"favorites_limit":5,"favorites_used":0,"id":"69323308443642315"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '128' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:22 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"rule_id":"69323308443642315","community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '51' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: POST + uri: http://ai:9696/v3/hunt/historical + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T13:59:22.516466+00:00","failed_max_retries":0,"failed_other":0,"id":"95693942031500728","progress":null,"results_csv_uri":null,"rule_id":"69323308443642315","rule_modified":"2026-08-21T13:59:21.835873+00:00","ruleset_name":"test_async_rules","source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '578' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:22 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule?id=69323308443642315&community=gamma + response: + body: + string: '{"result":{"created":"2026-08-21T13:59:21.835873+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"69323308443642315","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:21.835873+00:00","name":"test_async_rules","rule_count":1,"yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '413' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:22 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/historical?id=95693942031500728&community=gamma + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T13:59:22.516466+00:00","failed_max_retries":0,"failed_other":0,"id":"95693942031500728","progress":null,"results_csv_uri":null,"rule_id":"69323308443642315","rule_modified":"2026-08-21T13:59:21.835873+00:00","ruleset_name":"test_async_rules","source_rule_changed":false,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '579' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:22 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"name":"test_async_rules2","yara":"rule sdk_test_async_rules { strings: + $u = \"test_async_rules\" condition: $u }\n// edited","description":"test","community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '168' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: PUT + uri: http://ai:9696/v3/hunt/rule?id=69323308443642315 + response: + body: + string: '{"result":{"created":"2026-08-21T13:59:21.835873+00:00","deleted":false,"description":"test","favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"69323308443642315","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:22.863472+00:00","name":"test_async_rules2","rule_count":1,"yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }\n// + edited"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '427' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:22 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/historical?id=95693942031500728&community=gamma + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T13:59:22.516466+00:00","failed_max_retries":0,"failed_other":0,"id":"95693942031500728","progress":null,"results_csv_uri":null,"rule_id":"69323308443642315","rule_modified":"2026-08-21T13:59:21.835873+00:00","ruleset_name":"test_async_rules","source_rule_changed":true,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '578' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:22 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '21' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: DELETE + uri: http://ai:9696/v3/hunt/historical?id=95693942031500728 + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T13:59:22.516466+00:00","failed_max_retries":0,"failed_other":0,"id":"95693942031500728","progress":null,"results_csv_uri":null,"rule_id":"69323308443642315","rule_modified":"2026-08-21T13:59:21.835873+00:00","ruleset_name":"test_async_rules","source_rule_changed":true,"status":"DELETING","summary":null,"user_account_number":"111","yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '579' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:23 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '21' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: DELETE + uri: http://ai:9696/v3/hunt/rule?id=69323308443642315 + response: + body: + string: '{"result":{"created":"2026-08-21T13:59:21.835873+00:00","deleted":true,"description":"test","favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"69323308443642315","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:23.191803+00:00","name":"test_async_rules2","rule_count":1,"yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }\n// + edited"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '426' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:23 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?community=gamma + response: + body: + string: '' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-type: + - text/html; charset=utf-8 + date: + - Fri, 21 Aug 2026 13:59:23 GMT + server: + - gunicorn + status: + code: 204 + message: NO CONTENT +version: 1 diff --git a/test/vcr/test_rules.vcr b/test/vcr/test_rules.vcr new file mode 100644 index 00000000..eb759388 --- /dev/null +++ b/test/vcr/test_rules.vcr @@ -0,0 +1,708 @@ +interactions: +- request: + body: '{"yara":"rule sdk_test_rules { strings: $u = \"test_rules\" condition: + $u }","name":"test_rules"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '97' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: POST + uri: http://ai:9696/v3/hunt/rule + response: + body: + string: '{"result":{"created":"2026-08-21T13:59:17.871893+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"14693852690318397","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:17.871893+00:00","name":"test_rules","rule_count":1,"yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '395' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:17 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T13:59:17.871893+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"14693852690318397","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:17.871893+00:00","name":"test_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '386' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:18 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?name=test_rules&community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T13:59:17.871893+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"14693852690318397","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:17.871893+00:00","name":"test_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '386' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:18 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule?id=14693852690318397&community=gamma + response: + body: + string: '{"result":{"created":"2026-08-21T13:59:17.871893+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"14693852690318397","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:17.871893+00:00","name":"test_rules","rule_count":1,"yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '395' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:18 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"id":"14693852690318397","favorite":1,"community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '59' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: PUT + uri: http://ai:9696/v3/hunt/rule/favorite + response: + body: + string: '{"result":{"favorite":true,"favorited_at":"2026-08-21T13:59:18.816554+00:00","favorites_limit":5,"favorites_used":1,"id":"14693852690318397"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '157' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:18 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?favorites_only=1&community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T13:59:17.871893+00:00","deleted":false,"description":null,"favorite":true,"favorited_at":"2026-08-21T13:59:18.816554+00:00","historical_hunt_count":0,"id":"14693852690318397","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:17.871893+00:00","name":"test_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '415' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:18 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"id":"14693852690318397","favorite":0,"community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '59' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: PUT + uri: http://ai:9696/v3/hunt/rule/favorite + response: + body: + string: '{"result":{"favorite":false,"favorited_at":null,"favorites_limit":5,"favorites_used":0,"id":"14693852690318397"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '128' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:18 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"rule_id":"14693852690318397","community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '51' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: POST + uri: http://ai:9696/v3/hunt/historical + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T13:59:19.202596+00:00","failed_max_retries":0,"failed_other":0,"id":"21276230498013503","progress":null,"results_csv_uri":null,"rule_id":"14693852690318397","rule_modified":"2026-08-21T13:59:17.871893+00:00","ruleset_name":"test_rules","source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '560' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:19 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule?id=14693852690318397&community=gamma + response: + body: + string: '{"result":{"created":"2026-08-21T13:59:17.871893+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"14693852690318397","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:17.871893+00:00","name":"test_rules","rule_count":1,"yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '395' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:19 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/historical?id=21276230498013503&community=gamma + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T13:59:19.202596+00:00","failed_max_retries":0,"failed_other":0,"id":"21276230498013503","progress":null,"results_csv_uri":null,"rule_id":"14693852690318397","rule_modified":"2026-08-21T13:59:17.871893+00:00","ruleset_name":"test_rules","source_rule_changed":false,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '561' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:19 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/historical?id=21276230498013503&community=gamma + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T13:59:19.202596+00:00","failed_max_retries":0,"failed_other":0,"id":"21276230498013503","progress":null,"results_csv_uri":null,"rule_id":"14693852690318397","rule_modified":"2026-08-21T13:59:17.871893+00:00","ruleset_name":"test_rules","source_rule_changed":false,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '561' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:19 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"name":"test_rules2","yara":"rule sdk_test_rules { strings: $u = \"test_rules\" + condition: $u }\n// edited","description":"test","community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '150' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: PUT + uri: http://ai:9696/v3/hunt/rule?id=14693852690318397 + response: + body: + string: '{"result":{"created":"2026-08-21T13:59:17.871893+00:00","deleted":false,"description":"test","favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"14693852690318397","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:19.602433+00:00","name":"test_rules2","rule_count":1,"yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }\n// edited"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '409' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:19 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/historical?id=21276230498013503&community=gamma + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T13:59:19.202596+00:00","failed_max_retries":0,"failed_other":0,"id":"21276230498013503","progress":null,"results_csv_uri":null,"rule_id":"14693852690318397","rule_modified":"2026-08-21T13:59:17.871893+00:00","ruleset_name":"test_rules","source_rule_changed":true,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '560' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:19 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '21' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: DELETE + uri: http://ai:9696/v3/hunt/historical?id=21276230498013503 + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T13:59:19.202596+00:00","failed_max_retries":0,"failed_other":0,"id":"21276230498013503","progress":null,"results_csv_uri":null,"rule_id":"14693852690318397","rule_modified":"2026-08-21T13:59:17.871893+00:00","ruleset_name":"test_rules","source_rule_changed":true,"status":"DELETING","summary":null,"user_account_number":"111","yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '561' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:19 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '21' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: DELETE + uri: http://ai:9696/v3/hunt/rule?id=14693852690318397 + response: + body: + string: '{"result":{"created":"2026-08-21T13:59:17.871893+00:00","deleted":true,"description":"test","favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"14693852690318397","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:20.153029+00:00","name":"test_rules2","rule_count":1,"yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }\n// edited"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '408' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 13:59:20 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?community=gamma + response: + body: + string: '' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-type: + - text/html; charset=utf-8 + date: + - Fri, 21 Aug 2026 13:59:20 GMT + server: + - gunicorn + status: + code: 204 + message: NO CONTENT +version: 1 From 40bc463ab5decda7924469ce63ec4e74e0686b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Mart=C3=ADnez?= Date: Fri, 21 Aug 2026 10:31:10 -0400 Subject: [PATCH 5/8] test+docs: cover the new surfaces end to end; spec the contract (review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review findings, all four: - specs updated in the same PR as required: 03-endpoints gains ruleset_favorite / live_results_count rows, the real ruleset_list signature and live_feed's livescan_id; 02-resources catalogues both new resources — including why YaraRulesetFavorite empties RESOURCE_ID_KEYS (the server reads the toggle from the PUT body; the empty key list is the only thing routing id there) and the bool→int body serialisation; 05's commonly-imported list carries both. - the rules live-tests now exercise every previously-uncovered surface against the real stack (and the cassettes record it): live_start → status=active filter → include_counts observed as a computed 0 (distinct from null) → live_results_count (our zero-result hunt ABSENT from counts, keyed by the same digit strings ruleset_get renders) → the livescan_id-scoped feed → live_stop, with the stop in a finally because a running hunt blocks ruleset deletion. - pure-unit builder tests (hunt_tracking_builder_test.py, the known_good_test pattern) pin the request shapes: the favorite PUT's body routing incl. 1/0 bools, counts query routing + None omission, the list filters' int bools and byte-compatible no-filter request, and livescan_id stringification. - the unstar stays a contract assertion with slot hygiene documented: ruleset_delete soft-deletes and the budget counts only deleted=false rows, so a failed run's star frees itself with the rule. The limit pin vs used bound is now commented as deliberate. Also: test/eicar.yara deleted (no test references it since the uid_yara move; the helper docstring no longer names the file). --- specs/02-resources.md | 6 +- specs/03-endpoints.md | 6 +- specs/05-downstream-contract.md | 4 +- test/_e2e_helpers.py | 4 +- test/async_client_test.py | 41 +++ test/client_scan_test.py | 38 ++- test/eicar.yara | 34 --- test/hunt_tracking_builder_test.py | 99 +++++++ test/vcr/test_async_rules.vcr | 447 +++++++++++++++++++++++++---- test/vcr/test_rules.vcr | 419 ++++++++++++++++++++++++--- 10 files changed, 958 insertions(+), 140 deletions(-) delete mode 100644 test/eicar.yara create mode 100644 test/hunt_tracking_builder_test.py diff --git a/specs/02-resources.md b/specs/02-resources.md index 181bbd81..fb626629 100644 --- a/specs/02-resources.md +++ b/specs/02-resources.md @@ -36,7 +36,9 @@ BaseResource # holds .api, ._content, .parse_result cla ├── HistoricalHunt # /hunt/historical ├── HistoricalHuntResult / List # /hunt/historical/results (+ /results/list) ├── LiveHuntResult / List # /hunt/live (+ /hunt/live/list) + ├── LiveHuntResultCounts # /hunt/live/results/count ├── YaraRuleset # /hunt/rule + ├── YaraRulesetFavorite # /hunt/rule/favorite ├── Tag, MalwareFamily, TagLink # /tags/tag, /tags/family, /tags/link ├── AssertionsJob, VotesJob # /consumer/assertions-job, /consumer/votes-job ├── SandboxTask, SandboxProvider # /sandbox/sandboxtask, /sandbox/provider @@ -343,7 +345,9 @@ Holds `handle`, `artifact_name`, `artifact_type`, `sha256`, `sha1`, `md5`. Also Several resources add domain-specific classmethods on top of the standard CRUD set: - `IOC` — `iocs_by_hash`, `ioc_search`, `check_known_hosts`, `create_known_good`, `create_known_bad`, `update_known_good`, `delete_known_good`. -- `LiveYaraRuleset` / `HistoricalHunt` / `YaraRuleset` — standard CRUD plus list/delete-batch variants. +- `LiveYaraRuleset` / `HistoricalHunt` / `YaraRuleset` — standard CRUD plus list/delete-batch variants. `YaraRuleset` also parses the hunt-page tracking fields (`favorite`, `favorited_at`, `rule_count` — `None` means the server had no answer, distinct from 0 — `historical_hunt_count`, `new_results_count`), and `HistoricalHunt` the source-rule provenance (`rule_id`, `rule_modified`, and the tri-state `source_rule_changed` — `None` is "unknown", never "unchanged"). All additive `.get()` parses; an older server leaves them `None`. +- `YaraRulesetFavorite` — `RESOURCE_ENDPOINT = '/hunt/rule/favorite'`, **`RESOURCE_ID_KEYS = []`**: a deliberate deviation from the default `['id']`, because the server reads the toggle exclusively from the PUT **body** — emptying the key list is the only thing routing `id` (plus `favorite`/`community`) into `json` instead of the query string. `favorite` serialises as `1`/`0` (the `_params` bool→int coercion), which the server's boolean parser accepts. Response carries the star state plus the team's `favorites_used`/`favorites_limit`. Pinned by `test/hunt_tracking_builder_test.py`. +- `LiveHuntResultCounts` — `RESOURCE_ENDPOINT = '/hunt/live/results/count'`, GET-only; `since` (seconds) rides the query and is omitted when unset (server default window). `counts` is a list of `{livescan_id, count}` with `livescan_id` as a digit string — the same join key `YaraRuleset.livescan_id` carries; a hunt absent from `counts` collected 0. - `SandboxTask` — `create_file`, `update_file`, `latest`, `my_tasks` for the various sandbox-submission shapes. **No `upload_file` instance method** in 4.0. - `Sample` — `create` with `endpoint_fmt={'sha256': sha256}` for the URL-parametrised path. - `Webhook` — `test(api, webhook_id)` for the test-payload endpoint. diff --git a/specs/03-endpoints.md b/specs/03-endpoints.md index eea14fd5..b33e2bbe 100644 --- a/specs/03-endpoints.md +++ b/specs/03-endpoints.md @@ -86,6 +86,7 @@ Internal-only CRUD for the `/known-good` binary resource (distinct from the IOC | `ruleset_create(name, rules, description=None)` | `YaraRuleset.create` | | `ruleset_get(ruleset_id=None)` | `YaraRuleset.get` | | `ruleset_update(ruleset_id, name=None, rules=None, description=None)` | `YaraRuleset.update` | +| `ruleset_favorite(ruleset_id, favorite=True)` | `YaraRulesetFavorite.update` — idempotent star/unstar; the response carries the team's `favorites_used`/`favorites_limit`, and an over-budget star is refused with a machine-readable `FAVORITE_LIMIT` error. The id rides the PUT **body**, not the query (see the `RESOURCE_ID_KEYS = []` note in specs/02) | | `ruleset_delete(ruleset_id)` | `YaraRuleset.delete` | | `tag_link_get(sha256)` | `TagLink.get` | | `tag_link_update(sha256, tags=None, families=None, emerging=None, remove=False)` | `TagLink.update` | @@ -186,10 +187,11 @@ refusal. | `iocs_by_hash(hash_type, hash_value, hide_known_good=False, beta=False)` | `IOC.iocs_by_hash` | | `search_by_ioc(ip=None, domain=None, ttp=None, imphash=None)` | `IOC.ioc_search` | | `check_known_hosts(ips=[], domains=[])` | `IOC.check_known_hosts` | -| `live_feed(since=None, …)` | `LiveHuntResult.list` | +| `live_feed(since=None, …, livescan_id=None)` | `LiveHuntResult.list` — `livescan_id` scopes the feed to one live hunt (the hunt-page per-ruleset feed) | +| `live_results_count(since=None)` | `LiveHuntResultCounts.get` — per-live-hunt result counts in a window, grouped by `livescan_id` (digit strings, the same join key `YaraRuleset.livescan_id` carries); a hunt absent from `counts` collected 0 | | `historical_list(since=None)` | `HistoricalHunt.list` | | `historical_results(hunt=None, …)` | `HistoricalHuntResultList.get` | -| `ruleset_list()` | `YaraRuleset.list` | +| `ruleset_list(name=None, status=None, favorites_only=None, has_new_results=None, since=None, include_counts=None)` | `YaraRuleset.list` — the hunt-page filters, conjunctive and optional; unset filters are omitted from the query so the no-filter request is byte-compatible with the old contract | | `tag_list()` | `Tag.list` | | `family_list()` | `MalwareFamily.list` | | `assertions_list(engine_id)` | `AssertionsJob.list` | diff --git a/specs/05-downstream-contract.md b/specs/05-downstream-contract.md index 2b40d018..555754a8 100644 --- a/specs/05-downstream-contract.md +++ b/specs/05-downstream-contract.md @@ -147,9 +147,9 @@ ArtifactInstance, LocalArtifact, Hash Engine Metadata, MetadataMapping, MetadataFieldProperties IOC -LiveYaraRuleset, LiveHuntResult, LiveHuntResultList +LiveYaraRuleset, LiveHuntResult, LiveHuntResultList, LiveHuntResultCounts HistoricalHunt, HistoricalHuntResult, HistoricalHuntResultList, HistoricalHuntList -YaraRuleset +YaraRuleset, YaraRulesetFavorite Tag, MalwareFamily, TagLink AssertionsJob, VotesJob SandboxTask, SandboxProvider diff --git a/test/_e2e_helpers.py b/test/_e2e_helpers.py index d150fa75..d4f14ca6 100644 --- a/test/_e2e_helpers.py +++ b/test/_e2e_helpers.py @@ -76,8 +76,8 @@ def uid_yara(uid): The artifact embeds ``uid`` (see ``malicious_artifact``), so a rule keying on that literal matches just this run's submission — isolating a live/historical - hunt from every other test's EICAR artifact (the generic eicar.yara substring - rule would match them all). + hunt from every other test's EICAR artifact (a generic EICAR-substring rule + would match them all). """ ident = re.sub(r'\W', '_', uid) return f'rule sdk_{ident} {{ strings: $u = "{uid}" condition: $u }}' diff --git a/test/async_client_test.py b/test/async_client_test.py index b281bb5c..22531ba3 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -637,14 +637,55 @@ async def test_async_rules(self, uid): fav = await api.ruleset_favorite(rule.id, True) assert fav.favorite is True assert fav.favorited_at is not None + # limit is a PIN (fixed product cap); used is a BOUND (the + # stack budget is shared across runs) assert fav.favorites_limit == 5 assert 1 <= fav.favorites_used <= fav.favorites_limit favorites = [r async for r in api.ruleset_list(favorites_only=True)] assert any(r.id == rule.id and r.favorite for r in favorites) + # unstarring here is the CONTRACT assertion; slot hygiene does + # not depend on reaching it — the finally's ruleset_delete + # soft-deletes and the budget counts only deleted=false rows unfav = await api.ruleset_favorite(rule.id, False) assert unfav.favorite is False assert unfav.favorited_at is None + # live-hunt scope: counts, include_counts and the livescan_id + # feed all need a running hunt; the fresh ruleset matches + # nothing, so every count is a computed ZERO (distinct from + # null = no live hunt to count against) + await api.live_start(int(rule.id)) + try: + livescan_id = (await api.ruleset_get(rule.id)).livescan_id + assert livescan_id is not None # a digit string + active = {r.id async for r in api.ruleset_list(status='active')} + assert rule.id in active + with_counts = None + async for r in api.ruleset_list(include_counts=True): + if r.id == rule.id: + with_counts = r + assert with_counts is not None + assert with_counts.new_results_count == 0 + counts = await api.live_results_count(since=86400) + assert counts.since == 86400 + # zero results -> our hunt is ABSENT (absence means 0) + assert livescan_id not in {entry['livescan_id'] + for entry in counts.counts} + try: + feed = [r async for r in api.live_feed(livescan_id=livescan_id)] + assert feed == [] + except exceptions.NoResultsException: + pass + finally: + # MUST stop before the outer finally's ruleset_delete — a + # running live hunt blocks deletion server-side + await api.live_stop(int(rule.id)) + try: + still_active = {r.id async for r in api.ruleset_list(status='active')} + except exceptions.NoResultsException: + still_active = set() # nothing live anywhere: also a pass + assert rule.id not in still_active + # a hunt triggered FROM the ruleset carries provenance and # bumps the counter; the create response's comparison is # unknown (None) and a read resolves it diff --git a/test/client_scan_test.py b/test/client_scan_test.py index a48cb140..00f4c6e2 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -599,14 +599,50 @@ def test_rules(self): fav = api.ruleset_favorite(rule.id, True) assert fav.favorite is True assert fav.favorited_at is not None + # the limit is a PIN (the fixed product cap, no plan scaling); + # used is a BOUND because the stack budget is shared across runs assert fav.favorites_limit == 5 - # other runs may hold stars on the shared stack — bound, not pin assert 1 <= fav.favorites_used <= fav.favorites_limit favorites = list(api.ruleset_list(favorites_only=True)) assert any(r.id == rule.id and r.favorite for r in favorites) + # unstarring here is the CONTRACT assertion; slot hygiene does not + # depend on reaching it — the finally's ruleset_delete soft-deletes + # and the server's budget counts only deleted=false rows, so a + # failed run's star frees itself with the rule unfav = api.ruleset_favorite(rule.id, False) assert unfav.favorite is False assert unfav.favorited_at is None + # live-hunt scope: the counts endpoint, include_counts and the + # livescan_id feed all need a running hunt. The fresh ruleset + # matches nothing, so every count is a computed ZERO — still + # distinct from null (= no live hunt to count against). + api.live_start(int(rule.id)) + try: + livescan_id = api.ruleset_get(rule.id).livescan_id + assert livescan_id is not None # a digit string + assert rule.id in {r.id for r in api.ruleset_list(status='active')} + with_counts = next(r for r in api.ruleset_list(include_counts=True) + if r.id == rule.id) + assert with_counts.new_results_count == 0 + counts = api.live_results_count(since=86400) + assert counts.since == 86400 + # zero results -> our hunt is ABSENT (absence means 0); the + # keys are the same digit strings ruleset_get renders + assert livescan_id not in {entry['livescan_id'] + for entry in counts.counts} + try: + assert list(api.live_feed(livescan_id=livescan_id)) == [] + except exceptions.NoResultsException: + pass + finally: + # MUST stop before the outer finally's ruleset_delete — a + # running live hunt blocks deletion server-side + api.live_stop(int(rule.id)) + try: + still_active = {r.id for r in api.ruleset_list(status='active')} + except exceptions.NoResultsException: + still_active = set() # nothing live anywhere: also a pass + assert rule.id not in still_active # a historical hunt triggered FROM the ruleset carries the # provenance and bumps the ruleset's counter; the create response's # source_rule_changed is None (unknown until a read re-resolves it) diff --git a/test/eicar.yara b/test/eicar.yara deleted file mode 100644 index 0c32b59f..00000000 --- a/test/eicar.yara +++ /dev/null @@ -1,34 +0,0 @@ -rule eicar_av_test { - /* - Per standard, match only if entire file is EICAR string plus optional trailing whitespace. - The raw EICAR string to be matched is: - X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H* - */ - - meta: - description = "This is a standard AV test, intended to verify that BinaryAlert is working correctly." - author = "Austin Byers | Airbnb CSIRT" - reference = "http://www.eicar.org/86-0-Intended-use.html" - - strings: - $eicar_regex = /^X5O!P%@AP\[4\\PZX54\(P\^\)7CC\)7\}\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\$H\+H\*\s*$/ - - condition: - all of them -} - -rule eicar_substring_test { - /* - More generic - match just the embedded EICAR string (e.g. in packed executables, PDFs, etc) - */ - - meta: - description = "Standard AV test, checking for an EICAR substring" - author = "Austin Byers | Airbnb CSIRT" - - strings: - $eicar_substring = "$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!" - - condition: - all of them -} \ No newline at end of file diff --git a/test/hunt_tracking_builder_test.py b/test/hunt_tracking_builder_test.py new file mode 100644 index 00000000..464d6b7a --- /dev/null +++ b/test/hunt_tracking_builder_test.py @@ -0,0 +1,99 @@ +"""Pure-unit request-shape tests for the hunt-page tracking builders. + +No HTTP at all (the pure-unit tier — see specs/04-testing.md): these pin the +request *construction* for the new surfaces — and specifically the two shapes +that are entirely consequences of ``core._params`` plumbing rather than +anything visible at the call site: + +* ``YaraRulesetFavorite`` empties ``RESOURCE_ID_KEYS``, which is the ONLY + thing routing ``id`` (and ``favorite``/``community``) into the PUT's JSON + body instead of the query string — the server reads the toggle exclusively + from the body; and +* booleans serialise as ``1``/``0`` ints, not JSON ``true``/``false`` + (``core._params`` coerces before body/query routing) — the server's + boolean parser accepts exactly that, so the int-vs-bool body contract is + load-bearing. + +Endpoint *behaviour* is covered by the live-e2e VCR lifecycle tests +(``test_rules`` / ``test_async_rules``). +""" +from polyswarm_api import resources + + +class _FakeApi: + uri = 'https://api.example.test' + community = 'gamma' + + +class TestYaraRulesetFavoriteBuilder: + def test_update_routes_everything_to_the_put_body(self): + api = _FakeApi() + req = resources.YaraRulesetFavorite.update( + api, id=5, favorite=True, community=api.community) + assert req.method == 'PUT' + assert req.url == f'{api.uri}/hunt/rule/favorite' + # RESOURCE_ID_KEYS = [] is load-bearing: with the base ['id'] the id + # would ride the query string on a PUT, and the server only reads the + # body. favorite serialises as int 1, not JSON true. + assert req.params is None + assert req.input_json == {'id': '5', 'favorite': 1, 'community': 'gamma'} + assert req.result_parser is resources.YaraRulesetFavorite + + def test_unfavorite_serialises_false_as_zero(self): + req = resources.YaraRulesetFavorite.update( + _FakeApi(), id=5, favorite=False, community='gamma') + assert req.input_json['favorite'] == 0 + + +class TestLiveHuntResultCountsBuilder: + def test_get_routes_since_and_community_to_the_query(self): + api = _FakeApi() + req = resources.LiveHuntResultCounts.get( + api, since=86400, community=api.community) + assert req.method == 'GET' + assert req.url == f'{api.uri}/hunt/live/results/count' + assert req.params == {'since': 86400, 'community': 'gamma'} + assert req.input_json is None + assert req.result_parser is resources.LiveHuntResultCounts + + def test_get_omits_unset_since(self): + # None is dropped, so the server applies its own default window. + req = resources.LiveHuntResultCounts.get( + _FakeApi(), since=None, community='gamma') + assert req.params == {'community': 'gamma'} + + +class TestRulesetListFilterBuilder: + def test_list_routes_filters_to_the_query_with_int_bools(self): + api = _FakeApi() + req = resources.YaraRuleset.list( + api, name='alpha', status='active', favorites_only=True, + has_new_results=True, since=86400, include_counts=True, + community=api.community) + assert req.method == 'GET' + assert req.url == f'{api.uri}/hunt/rule/list' + assert req.params == { + 'name': 'alpha', 'status': 'active', 'favorites_only': 1, + 'has_new_results': 1, 'since': 86400, 'include_counts': 1, + 'community': 'gamma'} + + def test_list_omits_every_unset_filter(self): + # The no-filter request is byte-compatible with the pre-filter + # contract: nothing but community rides the query string. + req = resources.YaraRuleset.list( + _FakeApi(), name=None, status=None, favorites_only=None, + has_new_results=None, since=None, include_counts=None, + community='gamma') + assert req.params == {'community': 'gamma'} + + +class TestLiveFeedScopeBuilder: + def test_list_routes_livescan_id_to_the_query_as_digit_string(self): + # *_id kwargs stringify (core._params); the server casts back to int. + req = resources.LiveHuntResult.list( + _FakeApi(), since=60, livescan_id=45392847561029383, + rule_name=None, family=None, polyscore_lower=None, + polyscore_upper=None, community='gamma') + assert req.url == 'https://api.example.test/hunt/live/list' + assert req.params == {'since': 60, 'livescan_id': '45392847561029383', + 'community': 'gamma'} diff --git a/test/vcr/test_async_rules.vcr b/test/vcr/test_async_rules.vcr index 0c27690a..1b4cb8d6 100644 --- a/test/vcr/test_async_rules.vcr +++ b/test/vcr/test_async_rules.vcr @@ -23,7 +23,7 @@ interactions: uri: http://ai:9696/v3/hunt/rule response: body: - string: '{"result":{"created":"2026-08-21T13:59:21.835873+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"69323308443642315","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:21.835873+00:00","name":"test_async_rules","rule_count":1,"yara":"rule + string: '{"result":{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"4466967201833338","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:52.300732+00:00","name":"test_async_rules","rule_count":1,"yara":"rule sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} ' @@ -35,11 +35,11 @@ interactions: connection: - keep-alive content-length: - - '413' + - '412' content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:21 GMT + - Fri, 21 Aug 2026 14:29:52 GMT server: - gunicorn x-billing-id: @@ -66,7 +66,7 @@ interactions: uri: http://ai:9696/v3/hunt/rule/list?community=gamma response: body: - string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T13:59:21.835873+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"69323308443642315","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:21.835873+00:00","name":"test_async_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"4466967201833338","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:52.300732+00:00","name":"test_async_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} ' headers: @@ -77,11 +77,11 @@ interactions: connection: - keep-alive content-length: - - '392' + - '391' content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:21 GMT + - Fri, 21 Aug 2026 14:29:52 GMT server: - gunicorn x-billing-id: @@ -108,7 +108,7 @@ interactions: uri: http://ai:9696/v3/hunt/rule/list?name=test_async_rules&community=gamma response: body: - string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T13:59:21.835873+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"69323308443642315","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:21.835873+00:00","name":"test_async_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"4466967201833338","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:52.300732+00:00","name":"test_async_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} ' headers: @@ -119,11 +119,11 @@ interactions: connection: - keep-alive content-length: - - '392' + - '391' content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:22 GMT + - Fri, 21 Aug 2026 14:29:52 GMT server: - gunicorn x-billing-id: @@ -147,10 +147,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: GET - uri: http://ai:9696/v3/hunt/rule?id=69323308443642315&community=gamma + uri: http://ai:9696/v3/hunt/rule?id=4466967201833338&community=gamma response: body: - string: '{"result":{"created":"2026-08-21T13:59:21.835873+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"69323308443642315","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:21.835873+00:00","name":"test_async_rules","rule_count":1,"yara":"rule + string: '{"result":{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"4466967201833338","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:52.300732+00:00","name":"test_async_rules","rule_count":1,"yara":"rule sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} ' @@ -162,11 +162,11 @@ interactions: connection: - keep-alive content-length: - - '413' + - '412' content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:22 GMT + - Fri, 21 Aug 2026 14:29:52 GMT server: - gunicorn x-billing-id: @@ -175,7 +175,7 @@ interactions: code: 200 message: OK - request: - body: '{"id":"69323308443642315","favorite":1,"community":"gamma"}' + body: '{"id":"4466967201833338","favorite":1,"community":"gamma"}' headers: accept: - '*/*' @@ -186,7 +186,7 @@ interactions: connection: - keep-alive content-length: - - '59' + - '58' content-type: - application/json host: @@ -197,7 +197,7 @@ interactions: uri: http://ai:9696/v3/hunt/rule/favorite response: body: - string: '{"result":{"favorite":true,"favorited_at":"2026-08-21T13:59:22.129436+00:00","favorites_limit":5,"favorites_used":1,"id":"69323308443642315"},"status":"OK"} + string: '{"result":{"favorite":true,"favorited_at":"2026-08-21T14:29:52.846750+00:00","favorites_limit":5,"favorites_used":1,"id":"4466967201833338"},"status":"OK"} ' headers: @@ -208,11 +208,11 @@ interactions: connection: - keep-alive content-length: - - '157' + - '156' content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:22 GMT + - Fri, 21 Aug 2026 14:29:52 GMT server: - gunicorn x-billing-id: @@ -239,7 +239,7 @@ interactions: uri: http://ai:9696/v3/hunt/rule/list?favorites_only=1&community=gamma response: body: - string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T13:59:21.835873+00:00","deleted":false,"description":null,"favorite":true,"favorited_at":"2026-08-21T13:59:22.129436+00:00","historical_hunt_count":0,"id":"69323308443642315","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:21.835873+00:00","name":"test_async_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":null,"favorite":true,"favorited_at":"2026-08-21T14:29:52.846750+00:00","historical_hunt_count":0,"id":"4466967201833338","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:52.300732+00:00","name":"test_async_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} ' headers: @@ -250,11 +250,11 @@ interactions: connection: - keep-alive content-length: - - '421' + - '420' content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:22 GMT + - Fri, 21 Aug 2026 14:29:52 GMT server: - gunicorn x-billing-id: @@ -263,7 +263,7 @@ interactions: code: 200 message: OK - request: - body: '{"id":"69323308443642315","favorite":0,"community":"gamma"}' + body: '{"id":"4466967201833338","favorite":0,"community":"gamma"}' headers: accept: - '*/*' @@ -274,7 +274,7 @@ interactions: connection: - keep-alive content-length: - - '59' + - '58' content-type: - application/json host: @@ -285,7 +285,7 @@ interactions: uri: http://ai:9696/v3/hunt/rule/favorite response: body: - string: '{"result":{"favorite":false,"favorited_at":null,"favorites_limit":5,"favorites_used":0,"id":"69323308443642315"},"status":"OK"} + string: '{"result":{"favorite":false,"favorited_at":null,"favorites_limit":5,"favorites_used":0,"id":"4466967201833338"},"status":"OK"} ' headers: @@ -296,11 +296,11 @@ interactions: connection: - keep-alive content-length: - - '128' + - '127' content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:22 GMT + - Fri, 21 Aug 2026 14:29:52 GMT server: - gunicorn x-billing-id: @@ -309,7 +309,7 @@ interactions: code: 200 message: OK - request: - body: '{"rule_id":"69323308443642315","community":"gamma"}' + body: '{"rule_id":"4466967201833338"}' headers: accept: - '*/*' @@ -320,7 +320,342 @@ interactions: connection: - keep-alive content-length: - - '51' + - '30' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: POST + uri: http://ai:9696/v3/hunt/rule/live + response: + body: + string: '{"result":{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"4466967201833338","livescan_created":"2026-08-21T14:29:53.201880+00:00","livescan_id":"34849444833418665","modified":"2026-08-21T14:29:53.197377+00:00","name":"test_async_rules","rule_count":1,"yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '457' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:53 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule?id=4466967201833338&community=gamma + response: + body: + string: '{"result":{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"4466967201833338","livescan_created":"2026-08-21T14:29:53.201880+00:00","livescan_id":"34849444833418665","modified":"2026-08-21T14:29:53.197377+00:00","name":"test_async_rules","rule_count":1,"yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '457' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:53 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?status=active&community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"4466967201833338","livescan_created":"2026-08-21T14:29:53.201880+00:00","livescan_id":"34849444833418665","modified":"2026-08-21T14:29:53.197377+00:00","name":"test_async_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '436' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:53 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?include_counts=1&community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"4466967201833338","livescan_created":"2026-08-21T14:29:53.201880+00:00","livescan_id":"34849444833418665","modified":"2026-08-21T14:29:53.197377+00:00","name":"test_async_rules","new_results_count":0,"rule_count":1,"yara":null}],"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '433' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:53 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/live/results/count?since=86400&community=gamma + response: + body: + string: '{"result":{"counts":[],"since":86400},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '53' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:53 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/live/list?livescan_id=34849444833418665&community=gamma + response: + body: + string: '' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-type: + - text/html; charset=utf-8 + date: + - Fri, 21 Aug 2026 14:29:53 GMT + server: + - gunicorn + status: + code: 204 + message: NO CONTENT +- request: + body: '{"rule_id":"4466967201833338"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '30' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: DELETE + uri: http://ai:9696/v3/hunt/rule/live + response: + body: + string: '{"result":{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"4466967201833338","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:53.570180+00:00","name":"test_async_rules","rule_count":1,"yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '412' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:53 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?status=active&community=gamma + response: + body: + string: '' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-type: + - text/html; charset=utf-8 + date: + - Fri, 21 Aug 2026 14:29:53 GMT + server: + - gunicorn + status: + code: 204 + message: NO CONTENT +- request: + body: '{"rule_id":"4466967201833338","community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '50' content-type: - application/json host: @@ -331,7 +666,7 @@ interactions: uri: http://ai:9696/v3/hunt/historical response: body: - string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T13:59:22.516466+00:00","failed_max_retries":0,"failed_other":0,"id":"95693942031500728","progress":null,"results_csv_uri":null,"rule_id":"69323308443642315","rule_modified":"2026-08-21T13:59:21.835873+00:00","ruleset_name":"test_async_rules","source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T14:29:53.760803+00:00","failed_max_retries":0,"failed_other":0,"id":"41876045317487902","progress":null,"results_csv_uri":null,"rule_id":"4466967201833338","rule_modified":"2026-08-21T14:29:53.570180+00:00","ruleset_name":"test_async_rules","source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} ' @@ -343,11 +678,11 @@ interactions: connection: - keep-alive content-length: - - '578' + - '577' content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:22 GMT + - Fri, 21 Aug 2026 14:29:53 GMT server: - gunicorn x-billing-id: @@ -371,10 +706,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: GET - uri: http://ai:9696/v3/hunt/rule?id=69323308443642315&community=gamma + uri: http://ai:9696/v3/hunt/rule?id=4466967201833338&community=gamma response: body: - string: '{"result":{"created":"2026-08-21T13:59:21.835873+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"69323308443642315","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:21.835873+00:00","name":"test_async_rules","rule_count":1,"yara":"rule + string: '{"result":{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"4466967201833338","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:53.570180+00:00","name":"test_async_rules","rule_count":1,"yara":"rule sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} ' @@ -386,11 +721,11 @@ interactions: connection: - keep-alive content-length: - - '413' + - '412' content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:22 GMT + - Fri, 21 Aug 2026 14:29:53 GMT server: - gunicorn x-billing-id: @@ -414,10 +749,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: GET - uri: http://ai:9696/v3/hunt/historical?id=95693942031500728&community=gamma + uri: http://ai:9696/v3/hunt/historical?id=41876045317487902&community=gamma response: body: - string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T13:59:22.516466+00:00","failed_max_retries":0,"failed_other":0,"id":"95693942031500728","progress":null,"results_csv_uri":null,"rule_id":"69323308443642315","rule_modified":"2026-08-21T13:59:21.835873+00:00","ruleset_name":"test_async_rules","source_rule_changed":false,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T14:29:53.760803+00:00","failed_max_retries":0,"failed_other":0,"id":"41876045317487902","progress":null,"results_csv_uri":null,"rule_id":"4466967201833338","rule_modified":"2026-08-21T14:29:53.570180+00:00","ruleset_name":"test_async_rules","source_rule_changed":false,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} ' @@ -429,11 +764,11 @@ interactions: connection: - keep-alive content-length: - - '579' + - '578' content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:22 GMT + - Fri, 21 Aug 2026 14:29:53 GMT server: - gunicorn x-billing-id: @@ -462,10 +797,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: PUT - uri: http://ai:9696/v3/hunt/rule?id=69323308443642315 + uri: http://ai:9696/v3/hunt/rule?id=4466967201833338 response: body: - string: '{"result":{"created":"2026-08-21T13:59:21.835873+00:00","deleted":false,"description":"test","favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"69323308443642315","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:22.863472+00:00","name":"test_async_rules2","rule_count":1,"yara":"rule + string: '{"result":{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":"test","favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"4466967201833338","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:54.082225+00:00","name":"test_async_rules2","rule_count":1,"yara":"rule sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }\n// edited"},"status":"OK"} @@ -478,11 +813,11 @@ interactions: connection: - keep-alive content-length: - - '427' + - '426' content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:22 GMT + - Fri, 21 Aug 2026 14:29:54 GMT server: - gunicorn x-billing-id: @@ -506,10 +841,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: GET - uri: http://ai:9696/v3/hunt/historical?id=95693942031500728&community=gamma + uri: http://ai:9696/v3/hunt/historical?id=41876045317487902&community=gamma response: body: - string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T13:59:22.516466+00:00","failed_max_retries":0,"failed_other":0,"id":"95693942031500728","progress":null,"results_csv_uri":null,"rule_id":"69323308443642315","rule_modified":"2026-08-21T13:59:21.835873+00:00","ruleset_name":"test_async_rules","source_rule_changed":true,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T14:29:53.760803+00:00","failed_max_retries":0,"failed_other":0,"id":"41876045317487902","progress":null,"results_csv_uri":null,"rule_id":"4466967201833338","rule_modified":"2026-08-21T14:29:53.570180+00:00","ruleset_name":"test_async_rules","source_rule_changed":true,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} ' @@ -521,11 +856,11 @@ interactions: connection: - keep-alive content-length: - - '578' + - '577' content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:22 GMT + - Fri, 21 Aug 2026 14:29:54 GMT server: - gunicorn x-billing-id: @@ -553,10 +888,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: DELETE - uri: http://ai:9696/v3/hunt/historical?id=95693942031500728 + uri: http://ai:9696/v3/hunt/historical?id=41876045317487902 response: body: - string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T13:59:22.516466+00:00","failed_max_retries":0,"failed_other":0,"id":"95693942031500728","progress":null,"results_csv_uri":null,"rule_id":"69323308443642315","rule_modified":"2026-08-21T13:59:21.835873+00:00","ruleset_name":"test_async_rules","source_rule_changed":true,"status":"DELETING","summary":null,"user_account_number":"111","yara":"rule + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T14:29:53.760803+00:00","failed_max_retries":0,"failed_other":0,"id":"41876045317487902","progress":null,"results_csv_uri":null,"rule_id":"4466967201833338","rule_modified":"2026-08-21T14:29:53.570180+00:00","ruleset_name":"test_async_rules","source_rule_changed":true,"status":"DELETING","summary":null,"user_account_number":"111","yara":"rule sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} ' @@ -568,11 +903,11 @@ interactions: connection: - keep-alive content-length: - - '579' + - '578' content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:23 GMT + - Fri, 21 Aug 2026 14:29:54 GMT server: - gunicorn x-billing-id: @@ -600,10 +935,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: DELETE - uri: http://ai:9696/v3/hunt/rule?id=69323308443642315 + uri: http://ai:9696/v3/hunt/rule?id=4466967201833338 response: body: - string: '{"result":{"created":"2026-08-21T13:59:21.835873+00:00","deleted":true,"description":"test","favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"69323308443642315","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:23.191803+00:00","name":"test_async_rules2","rule_count":1,"yara":"rule + string: '{"result":{"created":"2026-08-21T14:29:52.300732+00:00","deleted":true,"description":"test","favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"4466967201833338","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:54.434684+00:00","name":"test_async_rules2","rule_count":1,"yara":"rule sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }\n// edited"},"status":"OK"} @@ -616,11 +951,11 @@ interactions: connection: - keep-alive content-length: - - '426' + - '425' content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:23 GMT + - Fri, 21 Aug 2026 14:29:54 GMT server: - gunicorn x-billing-id: @@ -658,7 +993,7 @@ interactions: content-type: - text/html; charset=utf-8 date: - - Fri, 21 Aug 2026 13:59:23 GMT + - Fri, 21 Aug 2026 14:29:54 GMT server: - gunicorn status: diff --git a/test/vcr/test_rules.vcr b/test/vcr/test_rules.vcr index eb759388..9c9b6dc2 100644 --- a/test/vcr/test_rules.vcr +++ b/test/vcr/test_rules.vcr @@ -23,7 +23,7 @@ interactions: uri: http://ai:9696/v3/hunt/rule response: body: - string: '{"result":{"created":"2026-08-21T13:59:17.871893+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"14693852690318397","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:17.871893+00:00","name":"test_rules","rule_count":1,"yara":"rule + string: '{"result":{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"71359438369584055","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:48.738607+00:00","name":"test_rules","rule_count":1,"yara":"rule sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} ' @@ -39,7 +39,7 @@ interactions: content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:17 GMT + - Fri, 21 Aug 2026 14:29:48 GMT server: - gunicorn x-billing-id: @@ -66,7 +66,7 @@ interactions: uri: http://ai:9696/v3/hunt/rule/list?community=gamma response: body: - string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T13:59:17.871893+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"14693852690318397","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:17.871893+00:00","name":"test_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"71359438369584055","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:48.738607+00:00","name":"test_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} ' headers: @@ -81,7 +81,7 @@ interactions: content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:18 GMT + - Fri, 21 Aug 2026 14:29:48 GMT server: - gunicorn x-billing-id: @@ -108,7 +108,7 @@ interactions: uri: http://ai:9696/v3/hunt/rule/list?name=test_rules&community=gamma response: body: - string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T13:59:17.871893+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"14693852690318397","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:17.871893+00:00","name":"test_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"71359438369584055","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:48.738607+00:00","name":"test_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} ' headers: @@ -123,7 +123,7 @@ interactions: content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:18 GMT + - Fri, 21 Aug 2026 14:29:48 GMT server: - gunicorn x-billing-id: @@ -147,10 +147,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: GET - uri: http://ai:9696/v3/hunt/rule?id=14693852690318397&community=gamma + uri: http://ai:9696/v3/hunt/rule?id=71359438369584055&community=gamma response: body: - string: '{"result":{"created":"2026-08-21T13:59:17.871893+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"14693852690318397","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:17.871893+00:00","name":"test_rules","rule_count":1,"yara":"rule + string: '{"result":{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"71359438369584055","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:48.738607+00:00","name":"test_rules","rule_count":1,"yara":"rule sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} ' @@ -166,7 +166,7 @@ interactions: content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:18 GMT + - Fri, 21 Aug 2026 14:29:48 GMT server: - gunicorn x-billing-id: @@ -175,7 +175,7 @@ interactions: code: 200 message: OK - request: - body: '{"id":"14693852690318397","favorite":1,"community":"gamma"}' + body: '{"id":"71359438369584055","favorite":1,"community":"gamma"}' headers: accept: - '*/*' @@ -197,7 +197,7 @@ interactions: uri: http://ai:9696/v3/hunt/rule/favorite response: body: - string: '{"result":{"favorite":true,"favorited_at":"2026-08-21T13:59:18.816554+00:00","favorites_limit":5,"favorites_used":1,"id":"14693852690318397"},"status":"OK"} + string: '{"result":{"favorite":true,"favorited_at":"2026-08-21T14:29:49.029459+00:00","favorites_limit":5,"favorites_used":1,"id":"71359438369584055"},"status":"OK"} ' headers: @@ -212,7 +212,7 @@ interactions: content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:18 GMT + - Fri, 21 Aug 2026 14:29:49 GMT server: - gunicorn x-billing-id: @@ -239,7 +239,7 @@ interactions: uri: http://ai:9696/v3/hunt/rule/list?favorites_only=1&community=gamma response: body: - string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T13:59:17.871893+00:00","deleted":false,"description":null,"favorite":true,"favorited_at":"2026-08-21T13:59:18.816554+00:00","historical_hunt_count":0,"id":"14693852690318397","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:17.871893+00:00","name":"test_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":null,"favorite":true,"favorited_at":"2026-08-21T14:29:49.029459+00:00","historical_hunt_count":0,"id":"71359438369584055","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:48.738607+00:00","name":"test_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} ' headers: @@ -254,7 +254,7 @@ interactions: content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:18 GMT + - Fri, 21 Aug 2026 14:29:49 GMT server: - gunicorn x-billing-id: @@ -263,7 +263,7 @@ interactions: code: 200 message: OK - request: - body: '{"id":"14693852690318397","favorite":0,"community":"gamma"}' + body: '{"id":"71359438369584055","favorite":0,"community":"gamma"}' headers: accept: - '*/*' @@ -285,7 +285,7 @@ interactions: uri: http://ai:9696/v3/hunt/rule/favorite response: body: - string: '{"result":{"favorite":false,"favorited_at":null,"favorites_limit":5,"favorites_used":0,"id":"14693852690318397"},"status":"OK"} + string: '{"result":{"favorite":false,"favorited_at":null,"favorites_limit":5,"favorites_used":0,"id":"71359438369584055"},"status":"OK"} ' headers: @@ -300,7 +300,7 @@ interactions: content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:18 GMT + - Fri, 21 Aug 2026 14:29:49 GMT server: - gunicorn x-billing-id: @@ -309,7 +309,342 @@ interactions: code: 200 message: OK - request: - body: '{"rule_id":"14693852690318397","community":"gamma"}' + body: '{"rule_id":"71359438369584055"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '31' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: POST + uri: http://ai:9696/v3/hunt/rule/live + response: + body: + string: '{"result":{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"71359438369584055","livescan_created":"2026-08-21T14:29:49.287201+00:00","livescan_id":"11956831134597871","modified":"2026-08-21T14:29:49.282670+00:00","name":"test_rules","rule_count":1,"yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '440' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:49 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule?id=71359438369584055&community=gamma + response: + body: + string: '{"result":{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"71359438369584055","livescan_created":"2026-08-21T14:29:49.287201+00:00","livescan_id":"11956831134597871","modified":"2026-08-21T14:29:49.282670+00:00","name":"test_rules","rule_count":1,"yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '440' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:49 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?status=active&community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"71359438369584055","livescan_created":"2026-08-21T14:29:49.287201+00:00","livescan_id":"11956831134597871","modified":"2026-08-21T14:29:49.282670+00:00","name":"test_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '431' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:49 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?include_counts=1&community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"71359438369584055","livescan_created":"2026-08-21T14:29:49.287201+00:00","livescan_id":"11956831134597871","modified":"2026-08-21T14:29:49.282670+00:00","name":"test_rules","new_results_count":0,"rule_count":1,"yara":null}],"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '428' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:49 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/live/results/count?since=86400&community=gamma + response: + body: + string: '{"result":{"counts":[],"since":86400},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '53' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:49 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/live/list?livescan_id=11956831134597871&community=gamma + response: + body: + string: '' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-type: + - text/html; charset=utf-8 + date: + - Fri, 21 Aug 2026 14:29:49 GMT + server: + - gunicorn + status: + code: 204 + message: NO CONTENT +- request: + body: '{"rule_id":"71359438369584055"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '31' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: DELETE + uri: http://ai:9696/v3/hunt/rule/live + response: + body: + string: '{"result":{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"71359438369584055","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:49.650762+00:00","name":"test_rules","rule_count":1,"yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '395' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:49 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?status=active&community=gamma + response: + body: + string: '' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-type: + - text/html; charset=utf-8 + date: + - Fri, 21 Aug 2026 14:29:49 GMT + server: + - gunicorn + status: + code: 204 + message: NO CONTENT +- request: + body: '{"rule_id":"71359438369584055","community":"gamma"}' headers: accept: - '*/*' @@ -331,7 +666,7 @@ interactions: uri: http://ai:9696/v3/hunt/historical response: body: - string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T13:59:19.202596+00:00","failed_max_retries":0,"failed_other":0,"id":"21276230498013503","progress":null,"results_csv_uri":null,"rule_id":"14693852690318397","rule_modified":"2026-08-21T13:59:17.871893+00:00","ruleset_name":"test_rules","source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T14:29:50.251832+00:00","failed_max_retries":0,"failed_other":0,"id":"90800113038824882","progress":null,"results_csv_uri":null,"rule_id":"71359438369584055","rule_modified":"2026-08-21T14:29:49.650762+00:00","ruleset_name":"test_rules","source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} ' @@ -347,7 +682,7 @@ interactions: content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:19 GMT + - Fri, 21 Aug 2026 14:29:50 GMT server: - gunicorn x-billing-id: @@ -371,10 +706,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: GET - uri: http://ai:9696/v3/hunt/rule?id=14693852690318397&community=gamma + uri: http://ai:9696/v3/hunt/rule?id=71359438369584055&community=gamma response: body: - string: '{"result":{"created":"2026-08-21T13:59:17.871893+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"14693852690318397","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:17.871893+00:00","name":"test_rules","rule_count":1,"yara":"rule + string: '{"result":{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"71359438369584055","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:49.650762+00:00","name":"test_rules","rule_count":1,"yara":"rule sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} ' @@ -390,7 +725,7 @@ interactions: content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:19 GMT + - Fri, 21 Aug 2026 14:29:50 GMT server: - gunicorn x-billing-id: @@ -414,10 +749,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: GET - uri: http://ai:9696/v3/hunt/historical?id=21276230498013503&community=gamma + uri: http://ai:9696/v3/hunt/historical?id=90800113038824882&community=gamma response: body: - string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T13:59:19.202596+00:00","failed_max_retries":0,"failed_other":0,"id":"21276230498013503","progress":null,"results_csv_uri":null,"rule_id":"14693852690318397","rule_modified":"2026-08-21T13:59:17.871893+00:00","ruleset_name":"test_rules","source_rule_changed":false,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T14:29:50.251832+00:00","failed_max_retries":0,"failed_other":0,"id":"90800113038824882","progress":null,"results_csv_uri":null,"rule_id":"71359438369584055","rule_modified":"2026-08-21T14:29:49.650762+00:00","ruleset_name":"test_rules","source_rule_changed":false,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} ' @@ -433,7 +768,7 @@ interactions: content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:19 GMT + - Fri, 21 Aug 2026 14:29:50 GMT server: - gunicorn x-billing-id: @@ -457,10 +792,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: GET - uri: http://ai:9696/v3/hunt/historical?id=21276230498013503&community=gamma + uri: http://ai:9696/v3/hunt/historical?id=90800113038824882&community=gamma response: body: - string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T13:59:19.202596+00:00","failed_max_retries":0,"failed_other":0,"id":"21276230498013503","progress":null,"results_csv_uri":null,"rule_id":"14693852690318397","rule_modified":"2026-08-21T13:59:17.871893+00:00","ruleset_name":"test_rules","source_rule_changed":false,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T14:29:50.251832+00:00","failed_max_retries":0,"failed_other":0,"id":"90800113038824882","progress":null,"results_csv_uri":null,"rule_id":"71359438369584055","rule_modified":"2026-08-21T14:29:49.650762+00:00","ruleset_name":"test_rules","source_rule_changed":false,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} ' @@ -476,7 +811,7 @@ interactions: content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:19 GMT + - Fri, 21 Aug 2026 14:29:50 GMT server: - gunicorn x-billing-id: @@ -505,10 +840,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: PUT - uri: http://ai:9696/v3/hunt/rule?id=14693852690318397 + uri: http://ai:9696/v3/hunt/rule?id=71359438369584055 response: body: - string: '{"result":{"created":"2026-08-21T13:59:17.871893+00:00","deleted":false,"description":"test","favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"14693852690318397","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:19.602433+00:00","name":"test_rules2","rule_count":1,"yara":"rule + string: '{"result":{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":"test","favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"71359438369584055","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:50.635991+00:00","name":"test_rules2","rule_count":1,"yara":"rule sdk_test_rules { strings: $u = \"test_rules\" condition: $u }\n// edited"},"status":"OK"} ' @@ -524,7 +859,7 @@ interactions: content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:19 GMT + - Fri, 21 Aug 2026 14:29:50 GMT server: - gunicorn x-billing-id: @@ -548,10 +883,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: GET - uri: http://ai:9696/v3/hunt/historical?id=21276230498013503&community=gamma + uri: http://ai:9696/v3/hunt/historical?id=90800113038824882&community=gamma response: body: - string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T13:59:19.202596+00:00","failed_max_retries":0,"failed_other":0,"id":"21276230498013503","progress":null,"results_csv_uri":null,"rule_id":"14693852690318397","rule_modified":"2026-08-21T13:59:17.871893+00:00","ruleset_name":"test_rules","source_rule_changed":true,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T14:29:50.251832+00:00","failed_max_retries":0,"failed_other":0,"id":"90800113038824882","progress":null,"results_csv_uri":null,"rule_id":"71359438369584055","rule_modified":"2026-08-21T14:29:49.650762+00:00","ruleset_name":"test_rules","source_rule_changed":true,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} ' @@ -567,7 +902,7 @@ interactions: content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:19 GMT + - Fri, 21 Aug 2026 14:29:50 GMT server: - gunicorn x-billing-id: @@ -595,10 +930,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: DELETE - uri: http://ai:9696/v3/hunt/historical?id=21276230498013503 + uri: http://ai:9696/v3/hunt/historical?id=90800113038824882 response: body: - string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T13:59:19.202596+00:00","failed_max_retries":0,"failed_other":0,"id":"21276230498013503","progress":null,"results_csv_uri":null,"rule_id":"14693852690318397","rule_modified":"2026-08-21T13:59:17.871893+00:00","ruleset_name":"test_rules","source_rule_changed":true,"status":"DELETING","summary":null,"user_account_number":"111","yara":"rule + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T14:29:50.251832+00:00","failed_max_retries":0,"failed_other":0,"id":"90800113038824882","progress":null,"results_csv_uri":null,"rule_id":"71359438369584055","rule_modified":"2026-08-21T14:29:49.650762+00:00","ruleset_name":"test_rules","source_rule_changed":true,"status":"DELETING","summary":null,"user_account_number":"111","yara":"rule sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} ' @@ -614,7 +949,7 @@ interactions: content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:19 GMT + - Fri, 21 Aug 2026 14:29:50 GMT server: - gunicorn x-billing-id: @@ -642,10 +977,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: DELETE - uri: http://ai:9696/v3/hunt/rule?id=14693852690318397 + uri: http://ai:9696/v3/hunt/rule?id=71359438369584055 response: body: - string: '{"result":{"created":"2026-08-21T13:59:17.871893+00:00","deleted":true,"description":"test","favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"14693852690318397","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T13:59:20.153029+00:00","name":"test_rules2","rule_count":1,"yara":"rule + string: '{"result":{"created":"2026-08-21T14:29:48.738607+00:00","deleted":true,"description":"test","favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"71359438369584055","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:50.839329+00:00","name":"test_rules2","rule_count":1,"yara":"rule sdk_test_rules { strings: $u = \"test_rules\" condition: $u }\n// edited"},"status":"OK"} ' @@ -661,7 +996,7 @@ interactions: content-type: - application/json date: - - Fri, 21 Aug 2026 13:59:20 GMT + - Fri, 21 Aug 2026 14:29:50 GMT server: - gunicorn x-billing-id: @@ -699,7 +1034,7 @@ interactions: content-type: - text/html; charset=utf-8 date: - - Fri, 21 Aug 2026 13:59:20 GMT + - Fri, 21 Aug 2026 14:29:50 GMT server: - gunicorn status: From b80f8256dd418279d57f9c90592e8f037f0fc4e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Mart=C3=ADnez?= Date: Fri, 21 Aug 2026 10:42:33 -0400 Subject: [PATCH 6/8] docs+test: classification fix, parse pins, honest scoping notes (review 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All five follow-ups: - live_results_count moved to the _single Live-hunts table in specs/03 — it returns one resource, and _single-vs-_paginate is that document's organizing invariant. - specs/04's fixture inventory drops the retired test/eicar.yara and names the new pure-unit module. - Parse-side pins for the counts resource: the cassettes only carry EMPTY counts (fresh zero-result hunt), so the {livescan_id, count} entry shape, the digit-string join key and the null-counts coalesce now have canned-payload tests. - The livescan_id feed assertions no longer read as if they verify the scoping: with a zero-result hunt they pin the wire shape and the empty pass-through only, and the comments now say so (the scoping semantics are pinned by the server's own HTTP suite). - FAVORITE_LIMIT's machine-readable contract is now documented (specs/05: no typed exception by design; the path is exc.request.errors with the code plus the same counters a successful toggle returns) and pinned by a respx refusal test — mocked because a genuinely full budget on the shared stack would race every other run. --- specs/03-endpoints.md | 2 +- specs/04-testing.md | 3 ++- specs/05-downstream-contract.md | 2 ++ test/async_client_test.py | 4 ++++ test/client_scan_test.py | 30 ++++++++++++++++++++++++++++++ test/hunt_tracking_builder_test.py | 24 ++++++++++++++++++++++++ 6 files changed, 63 insertions(+), 2 deletions(-) diff --git a/specs/03-endpoints.md b/specs/03-endpoints.md index b33e2bbe..7f1e2e93 100644 --- a/specs/03-endpoints.md +++ b/specs/03-endpoints.md @@ -66,6 +66,7 @@ Internal-only CRUD for the `/known-good` binary resource (distinct from the IOC | `live_stop(rule_id)` | `LiveYaraRuleset.delete` | | `live_feed_delete(result_ids)` | `LiveHuntResultList.delete` (catches `NoResultsException`) | | `live_result(result_id)` | `LiveHuntResult.get` | +| `live_results_count(since=None)` | `LiveHuntResultCounts.get` — per-live-hunt result counts in a window, grouped by `livescan_id` (digit strings, the same join key `YaraRuleset.livescan_id` carries); a hunt absent from `counts` collected 0 | ### Historical hunts @@ -188,7 +189,6 @@ refusal. | `search_by_ioc(ip=None, domain=None, ttp=None, imphash=None)` | `IOC.ioc_search` | | `check_known_hosts(ips=[], domains=[])` | `IOC.check_known_hosts` | | `live_feed(since=None, …, livescan_id=None)` | `LiveHuntResult.list` — `livescan_id` scopes the feed to one live hunt (the hunt-page per-ruleset feed) | -| `live_results_count(since=None)` | `LiveHuntResultCounts.get` — per-live-hunt result counts in a window, grouped by `livescan_id` (digit strings, the same join key `YaraRuleset.livescan_id` carries); a hunt absent from `counts` collected 0 | | `historical_list(since=None)` | `HistoricalHunt.list` | | `historical_results(hunt=None, …)` | `HistoricalHuntResultList.get` | | `ruleset_list(name=None, status=None, favorites_only=None, has_new_results=None, since=None, include_counts=None)` | `YaraRuleset.list` — the hunt-page filters, conjunctive and optional; unset filters are omitted from the query so the no-filter request is byte-compatible with the old contract | diff --git a/specs/04-testing.md b/specs/04-testing.md index b81e5855..bb60d552 100644 --- a/specs/04-testing.md +++ b/specs/04-testing.md @@ -26,7 +26,8 @@ How the test suite is organised. Three layers: pure unit tests (no HTTP at all - `test/async_client_test.py` — async, VCR-backed integration tests (not yet on the parametrised harness — follow-up work). - `test/jmespath_test.py` — unit tests for `BaseJsonResource.jmespath`. - `test/vcr/*.vcr` — recorded cassettes. -- `test/eicar.yara`, `test/malicious` — fixture files for upload tests. +- `test/malicious` — fixture file for upload tests (`test/eicar.yara` was retired when the rules tests moved to per-test `uid_yara` bodies). +- `test/hunt_tracking_builder_test.py` — pure-unit request-shape and parse tests for the hunt-page tracking builders/resources. ## Three test layers diff --git a/specs/05-downstream-contract.md b/specs/05-downstream-contract.md index 555754a8..eb476820 100644 --- a/specs/05-downstream-contract.md +++ b/specs/05-downstream-contract.md @@ -180,6 +180,8 @@ class TimeoutException(PolyswarmException): ... `KnownGoodWithheldException` is the 404 raised when a download is refused because the artifact is a known-good binary — the platform never stores or serves those bytes. It **subclasses `NotFoundException`** precisely so invariant 3 holds for existing consumers: code that already does `except NotFoundException:` keeps catching the refusal with no change, and only callers that want to distinguish "withheld by design" from a plain miss catch the subclass. It adds one attribute, `.sources` — the known-good feeds that flagged the hash (e.g. `['nsrl']`), `[]` when the server named none. The contract is only this: **always a list of strings**, whatever the envelope carried, so `for feed in exc.sources` needs no shape check. Which wire shapes are coerced, and which are dropped and logged, is `exceptions._normalise_sources`' business rather than a promise to consumers — the server sends a list of strings today. Note it is **not** normalised the same way as `ArtifactInstance.known_good_sources`, which is the same concept reached from the instance response: that one is sorted and de-duplicated, while `.sources` preserves the order the envelope carried and can repeat a feed. Don't assume parity between the two. The raw envelope stays reachable at `exc.request.errors` (`{'code': 'KNOWN_GOOD', 'known_good': True, 'sources': [...]}`). The artifact's metadata — the flagging feeds plus any scan data already collected — remains readable through the search / instance endpoints; only the bytes are withheld, and the instance's `KNOWN_GOOD` state/status is the signal for the typed refusal (there is no separate "withheld" field; a `NOT_STORED` instance has no bytes either, but 404s plainly — see below). +`FAVORITE_LIMIT` (a refused ruleset star: the team's favorite budget is spent) has **no typed exception** — deliberately, since no pre-existing consumer needs re-routing the way `NotFoundException` did. It surfaces as the generic 400 `RequestException`, and the machine-readable path is the raw envelope: `exc.request.errors == {'code': 'FAVORITE_LIMIT', 'favorites_used': N, 'favorites_limit': M}`. The counters are the same pair a successful toggle returns, so a caller can render the "budget full" state from either outcome. Pinned by the respx refusal test in `client_scan_test.py`. + **What "known-good" means on the server, as of artifact-index's two-predicate model** (its `specs/05`): the refusal fires on the server's *current understanding* — a catalogue entry exists for the sha256 **and** that entry's extension passes an executable allow-list — evaluated live on every request. Two consequences worth knowing as a consumer: the same download can start working again with no action on your part (the entry is deleted, or the policy narrows), and `ArtifactInstance.state` can report the new value **`NOT_STORED`** — a submission the server declined as known-good at the time whose hash is no longer currently known-good, so nothing was ever stored for it and a fresh submit of the same file works. `state` is a plain string here; the SDK does not enumerate it, so a new member needs no SDK release. Each `RequestException` subclass carries a `.request` attribute holding the originating `PolyswarmRequest` (set by `RequestException.__init__`). Callers can read `exc.request.status_code`, `exc.request.json` (the parsed response body after execution), `exc.request.input_json` (the body that was sent), `exc.request.request_parameters` (the request kwargs that built the call), etc. `InvalidValueException` and `TimeoutException` are client-side errors and don't carry a request descriptor. diff --git a/test/async_client_test.py b/test/async_client_test.py index 22531ba3..a7e82507 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -671,6 +671,10 @@ async def test_async_rules(self, uid): # zero results -> our hunt is ABSENT (absence means 0) assert livescan_id not in {entry['livescan_id'] for entry in counts.counts} + # NOTE: zero-result hunt — pins the wire shape and the + # empty pass-through only; the scoping semantics are + # pinned by the server's own HTTP suite (same for + # has_new_results) try: feed = [r async for r in api.live_feed(livescan_id=livescan_id)] assert feed == [] diff --git a/test/client_scan_test.py b/test/client_scan_test.py index 00f4c6e2..fb9a2366 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -357,6 +357,31 @@ def test_metadata_search(self): lambda: api.search_by_metadata(f'artifact.sha256:{sha}'), tries=90) assert result and result[0].sha256 == sha + def test_favorite_limit_refusal_is_machine_readable(self): + # The FAVORITE_LIMIT refusal path (respx-mocked: producing a genuinely + # full budget on the SHARED e2e stack would require holding all five + # team slots, racing every other run). There is deliberately no typed + # exception (specs/05): the machine-readable contract is the raw + # envelope at exc.request.errors — the code plus the same counters a + # successful toggle returns. + with respx.mock(assert_all_called=True) as router: + envelope = { + 'status': 'error', + 'result': 'Favorite limit reached (5 of 5 used).', + 'errors': {'code': 'FAVORITE_LIMIT', + 'favorites_used': 5, 'favorites_limit': 5}, + } + router.put('http://localhost:3000/api/v1/hunt/rule/favorite').mock( + return_value=httpx.Response(400, json=envelope)) + api = PolyswarmAPI(self.test_api_key, uri='http://localhost:3000/api/v1', + community='gamma') + with pytest.raises(exceptions.RequestException) as excinfo: + api.ruleset_favorite(5, True) + errors = excinfo.value.request.errors + assert errors['code'] == 'FAVORITE_LIMIT' + assert errors['favorites_used'] == 5 + assert errors['favorites_limit'] == 5 + def test_resolve_engine_name(self): with respx.mock(assert_all_called=False) as router: ok_payload = {'results': [ @@ -630,6 +655,11 @@ def test_rules(self): # keys are the same digit strings ruleset_get renders assert livescan_id not in {entry['livescan_id'] for entry in counts.counts} + # NOTE: with a zero-result hunt this pins only the wire shape + # and the empty pass-through — it cannot tell a working filter + # from an ignored param (that would need a second hunt WITH + # results). The scoping semantics themselves are pinned by the + # server's own HTTP suite; same for has_new_results. try: assert list(api.live_feed(livescan_id=livescan_id)) == [] except exceptions.NoResultsException: diff --git a/test/hunt_tracking_builder_test.py b/test/hunt_tracking_builder_test.py index 464d6b7a..653a88dc 100644 --- a/test/hunt_tracking_builder_test.py +++ b/test/hunt_tracking_builder_test.py @@ -97,3 +97,27 @@ def test_list_routes_livescan_id_to_the_query_as_digit_string(self): assert req.url == 'https://api.example.test/hunt/live/list' assert req.params == {'since': 60, 'livescan_id': '45392847561029383', 'community': 'gamma'} + + +class TestLiveHuntResultCountsParse: + """Parse-side pins for the counts resource — the recorded cassettes carry + only EMPTY counts (the live tests use a fresh zero-result hunt), so the + entry shape and the digit-string join key documented in three places are + otherwise asserted nowhere.""" + + def test_counts_entries_parse_with_digit_string_join_keys(self): + payload = {'since': 86400, + 'counts': [{'livescan_id': '45392847561029383', 'count': 3}, + {'livescan_id': '71359438369584055', 'count': 1}]} + counts = resources.LiveHuntResultCounts(payload, api=None) + assert counts.since == 86400 + assert counts.counts == payload['counts'] + # the join key is the same digit string YaraRuleset.livescan_id + # carries — a bare int would round in a JS consumer + assert all(isinstance(entry['livescan_id'], str) for entry in counts.counts) + by_id = {entry['livescan_id']: entry['count'] for entry in counts.counts} + assert by_id['45392847561029383'] == 3 + + def test_null_counts_coalesces_to_an_empty_list(self): + counts = resources.LiveHuntResultCounts({'since': 86400, 'counts': None}, api=None) + assert counts.counts == [] From cc9ed02f763d79a3352cd8986f44295336c670b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Mart=C3=ADnez?= Date: Fri, 21 Aug 2026 10:44:22 -0400 Subject: [PATCH 7/8] =?UTF-8?q?ci:=20retry=20=E2=80=94=20runner=20failed?= =?UTF-8?q?=20at=20git=20fetch=20(transient=20403),=20commit=20never=20che?= =?UTF-8?q?cked=20out?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 4738e1e9ca109e7f6c9b431c236bce8310728c59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Mart=C3=ADnez?= Date: Fri, 21 Aug 2026 10:56:23 -0400 Subject: [PATCH 8/8] ci: retry after GitLab runner fetch 403s (infrastructure, both attempts died at get_sources)