From a8e0ab184e514a4724499e915d225a6eb17e1248 Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 16 Oct 2026 15:57:59 -0300 Subject: [PATCH 01/18] feat: add metadata_raw_search for the raw metadata search endpoint The server-side API gained POST /v3/search/metadata/raw, which runs a caller-authored OpenSearch query body and overlays the tenant filter itself. This is the client for it. The body is passed through untouched and the community travels in the query string, which is why the method uses the inline-dict _single form rather than the generic builder: on a POST the builder puts every parameter in the JSON body, and the server reads community/communities out of that body when it resolves scope. A community folded into the body would therefore choose the scope via a key the server's own grammar then rejects. A POST is never paginated, so _single returns the one composite result - hits and aggregations together - rather than a generator. Tests are in the respx tier rather than the e2e one: what they pin is that the body arrives verbatim and the community is not in it, and neither is visible from the far side of a live call. The live lifecycle test follows separately. --- src/polyswarm_api/aio/api.py | 28 ++++++++++ src/polyswarm_api/api.py | 30 +++++++++++ src/polyswarm_api/resources.py | 4 ++ test/metadata_raw_search_test.py | 87 ++++++++++++++++++++++++++++++++ 4 files changed, 149 insertions(+) create mode 100644 test/metadata_raw_search_test.py diff --git a/src/polyswarm_api/aio/api.py b/src/polyswarm_api/aio/api.py index 9002ecd2..9a6f6457 100644 --- a/src/polyswarm_api/aio/api.py +++ b/src/polyswarm_api/aio/api.py @@ -288,6 +288,34 @@ async def metadata_mapping(self): result_parser=resources.MetadataMapping, ) + async def metadata_raw_search(self, body, community=None): + """Run a raw OpenSearch query body against the metadata corpus. + + The server overlays the community filter itself and caps the work, so + ``body`` is a plain ``_search`` body and the community travels in the + query string rather than in it. The accepted grammar is closed: a + construct the server does not recognise is refused rather than passed + through, and so is a top-level ``community``/``communities`` key. + + Entitlement-gated and metered per query, including a query the server + refuses after running it. + + :param body: An OpenSearch ``_search`` body (``query``, ``aggs``, + ``size``, ``sort``, ``_source``, ...). + :param community: Community to search; defaults to the client's. + :return: A ``MetadataRawSearch`` whose ``json`` carries the engine's + ``hits`` and ``aggregations``, plus ``partial`` when the server + could not establish that the answer is complete. + """ + logger.info('Running a raw metadata search') + return await self._single( + {'method': 'POST', + 'url': f'{self.uri}{resources.MetadataRawSearch.RESOURCE_ENDPOINT}', + 'params': {'community': community or self.community}, + 'json': body}, + result_parser=resources.MetadataRawSearch, + ) + async def metadata_field_properties_write(self, field_path, description, example=None, category=None, aliases=None): """Upsert a metadata field properties entry (the single write path). diff --git a/src/polyswarm_api/api.py b/src/polyswarm_api/api.py index 41fb5d5e..2fb678db 100644 --- a/src/polyswarm_api/api.py +++ b/src/polyswarm_api/api.py @@ -303,6 +303,36 @@ def metadata_mapping(self): result_parser=resources.MetadataMapping, ) + def metadata_raw_search(self, body, community=None): + """Run a raw OpenSearch query body against the metadata corpus. + + The server overlays the community filter itself and caps the work, so + ``body`` is a plain ``_search`` body and the community travels in the + query string rather than in it. The accepted grammar is closed: a + construct the server does not recognise is refused rather than passed + through, and so is a top-level ``community``/``communities`` key. + + Entitlement-gated and metered per query, including a query the server + refuses after running it. + + :param body: An OpenSearch ``_search`` body (``query``, ``aggs``, + ``size``, ``sort``, ``_source``, ...). + :param community: Community to search; defaults to the client's. + :return: A ``MetadataRawSearch`` whose ``json`` carries the engine's + ``hits`` and ``aggregations``, plus ``partial`` when the server + could not establish that the answer is complete. + """ + logger.info("Running a raw metadata search") + return self._single( + { + "method": "POST", + "url": f"{self.uri}{resources.MetadataRawSearch.RESOURCE_ENDPOINT}", + "params": {"community": community or self.community}, + "json": body, + }, + result_parser=resources.MetadataRawSearch, + ) + def metadata_field_properties_write( self, field_path, description, example=None, category=None, aliases=None ): diff --git a/src/polyswarm_api/resources.py b/src/polyswarm_api/resources.py index d75d9808..96e1245c 100644 --- a/src/polyswarm_api/resources.py +++ b/src/polyswarm_api/resources.py @@ -77,6 +77,10 @@ class MetadataMapping(core.BaseJsonResource): RESOURCE_ENDPOINT = '/search/metadata/mappings' +class MetadataRawSearch(core.BaseJsonResource): + RESOURCE_ENDPOINT = '/search/metadata/raw' + + class MetadataFieldProperties(core.BaseJsonResource): RESOURCE_ENDPOINT = '/search/metadata/properties' RESOURCE_ID_KEYS = ['field_path'] diff --git a/test/metadata_raw_search_test.py b/test/metadata_raw_search_test.py new file mode 100644 index 00000000..cc833f5a --- /dev/null +++ b/test/metadata_raw_search_test.py @@ -0,0 +1,87 @@ +"""Tests for the metadata_raw_search SDK method. + +Request-shape tests, in the respx tier rather than the e2e one. What they +pin — that the query body reaches the server byte-for-byte and that the +community rides the query string instead of the body — is invisible from +the far side of a live call, which only shows that *some* scoped search +ran. The live lifecycle test lives in ``client_scan_test.py`` / +``async_client_test.py``. + +The distinction is load-bearing rather than stylistic: the server reads +``community``/``communities`` out of a POST body when deciding scope, so a +client that folded the community into the body would have its scope chosen +by a key the server's own grammar then rejects. + +Each test runs twice, once per transport — see ``test/_client_harness.py``. +""" +from test._client_harness import BASE_URL, COMMUNITY, ClientTestCase + + +_RESOURCE_URL = f'{BASE_URL}/search/metadata/raw' + + +def _envelope(hits=None, aggregations=None, **extra): + result = {'hits': hits if hits is not None else {'total': {'value': 0, 'relation': 'eq'}, + 'hits': []}} + if aggregations is not None: + result['aggregations'] = aggregations + result.update(extra) + return {'result': result, 'status': 'OK'} + + +# ── Tests ──────────────────────────────────────────────────────────── + + +class MetadataRawSearchTestCase(ClientTestCase): + def test_the_body_reaches_the_server_verbatim(self): + body = { + 'query': {'bool': {'must': [{'term': {'artifact.mimetype': 'application/zip'}}]}}, + 'aggs': {'families': {'terms': {'field': 'polyunite.malware_family', 'size': 20}}}, + 'size': 0, + 'sort': [{'artifact.created': {'order': 'desc'}}], + } + self.mock.add('POST', _RESOURCE_URL, json=_envelope(aggregations={'families': {}})) + self.api.metadata_raw_search(body) + assert self.mock.last_request_body == body + + def test_the_community_rides_the_query_string_not_the_body(self): + # The server reads community/communities out of the POST body when it + # resolves scope, and its grammar rejects those keys for that reason. + self.mock.add('POST', _RESOURCE_URL, json=_envelope()) + self.api.metadata_raw_search({'query': {'match_all': {}}}) + assert f'community={COMMUNITY}' in self.mock.last_request_url + assert 'community' not in self.mock.last_request_body + assert 'communities' not in self.mock.last_request_body + + def test_an_explicit_community_overrides_the_clients(self): + self.mock.add('POST', _RESOURCE_URL, json=_envelope()) + self.api.metadata_raw_search({'query': {'match_all': {}}}, community='delta') + assert 'community=delta' in self.mock.last_request_url + + def test_hits_and_aggregations_are_both_reachable(self): + hits = {'total': {'value': 2, 'relation': 'eq'}, + 'hits': [{'_source': {'artifact': {'sha256': 'a' * 64}}}]} + aggs = {'families': {'buckets': [{'key': 'lockbit', 'doc_count': 2}]}} + self.mock.add('POST', _RESOURCE_URL, json=_envelope(hits=hits, aggregations=aggs)) + result = self.api.metadata_raw_search({'aggs': {'families': {'terms': {'field': 'f'}}}}) + assert result.json['hits']['total']['value'] == 2 + assert result.json['aggregations']['families']['buckets'][0]['key'] == 'lockbit' + + def test_a_partial_answer_keeps_its_marker(self): + # A truncated answer is still a 200, so the only thing separating it + # from a complete one is this key. Dropping it in the parse layer would + # turn "we could not establish this is complete" into a hard number. + self.mock.add('POST', _RESOURCE_URL, + json=_envelope(partial={'terminated_early': True})) + result = self.api.metadata_raw_search({'query': {'match_all': {}}}) + assert result.json['partial'] == {'terminated_early': True} + + def test_a_complete_answer_carries_no_marker(self): + self.mock.add('POST', _RESOURCE_URL, json=_envelope()) + result = self.api.metadata_raw_search({'query': {'match_all': {}}}) + assert 'partial' not in result.json + + def test_it_posts_to_the_raw_search_endpoint(self): + self.mock.add('POST', _RESOURCE_URL, json=_envelope()) + self.api.metadata_raw_search({'query': {'match_all': {}}}) + assert self.mock.last_request_url.startswith(_RESOURCE_URL) From 214d6e14f24f52506a2fbe497b227c06b5657433 Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 16 Oct 2026 16:13:24 -0300 Subject: [PATCH 02/18] test: cover metadata_raw_search against the live stack A VCR lifecycle test per transport, each with its own cassette recorded against a freshly booted stack running the matching server branch. Both replay offline for the unit jobs and both pass with TESTS_VCR=off against the live stack, which is the invariant that keeps the cassettes an efficiency cache rather than the thing under test. The query aggregates on `extension` rather than a mimetype field: the metadata document has no artifact.mimetype at all (the mimetype it does carry is scan.mimetype), and a terms aggregation on an unmapped field is not an error - it returns an empty bucket list, so the assertion fails with nothing to say why. The respx example moved to scan.mimetype for the same reason, even though a mock would have accepted either. Recording these needed the stack's ai-es-dispatch-pending step: it is the only thing that turns a claimed es_metadata row into an Elasticsearch write, so without it the metadata index is never created and every metadata test polls an empty corpus until it gives up. --- test/async_client_test.py | 21 + test/client_scan_test.py | 21 + test/metadata_raw_search_test.py | 2 +- test/vcr/test_async_metadata_raw_search.vcr | 616 ++++++++++++++++++++ test/vcr/test_metadata_raw_search.vcr | 616 ++++++++++++++++++++ 5 files changed, 1275 insertions(+), 1 deletion(-) create mode 100644 test/vcr/test_async_metadata_raw_search.vcr create mode 100644 test/vcr/test_metadata_raw_search.vcr diff --git a/test/async_client_test.py b/test/async_client_test.py index 9dd98348..aa71fb66 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -286,6 +286,27 @@ async def test_async_metadata_search(self, uid): lambda: api.search_by_metadata(f'artifact.sha256:{sha}'), tries=90) assert result and result[0].sha256 == sha + @vcr.use_cassette() + async def test_async_metadata_raw_search(self, uid): + # Async twin of the sync test_metadata_raw_search; own cassette, same + # exchange. See that test for what the two assertions are covering. + async with self._api() as api: + _, sha = await submit_and_scan(api, uid) + body = { + 'query': {'term': {'artifact.sha256': sha}}, + 'aggs': {'extensions': {'terms': {'field': 'extension', 'size': 5}}}, + 'size': 1, + } + + async def _hit_count(): + result = await api.metadata_raw_search(body) + return result.json['hits']['total']['value'] + + assert await poll_equals_async(_hit_count, 1, tries=90) == 1 + result = await api.metadata_raw_search(body) + assert result.json['aggregations']['extensions']['buckets'] + assert 'partial' not in result.json + # ── IOCs ────────────────────────────────────────────────────────────────── @vcr.use_cassette() diff --git a/test/client_scan_test.py b/test/client_scan_test.py index 26f6e6aa..8cd16a47 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -364,6 +364,27 @@ def test_metadata_search(self): lambda: api.search_by_metadata(f'artifact.sha256:{sha}'), tries=90) assert result and result[0].sha256 == sha + @vcr.use_cassette() + def test_metadata_raw_search(self): + api = PolyswarmAPI(self.test_api_key, uri=f'http://ai:9696/{self.api_version}', community='gamma') + # The raw endpoint runs the caller's own query body, so this covers the + # two halves the SDK owns: the body survives the trip intact enough to + # match a document we just created, and an aggregation comes back beside + # the hits. The metadata write lags the scan, so poll for the hit first. + _, sha = submit_and_scan(api, self._testMethodName) + body = { + 'query': {'term': {'artifact.sha256': sha}}, + 'aggs': {'extensions': {'terms': {'field': 'extension', 'size': 5}}}, + 'size': 1, + } + found = poll_equals( + lambda: api.metadata_raw_search(body).json['hits']['total']['value'], 1, tries=90) + assert found == 1 + result = api.metadata_raw_search(body) + assert result.json['aggregations']['extensions']['buckets'] + # A complete answer carries no marker; a truncated one would say so. + assert 'partial' not in result.json + def test_resolve_engine_name(self): with respx.mock(assert_all_called=False) as router: ok_payload = {'results': [ diff --git a/test/metadata_raw_search_test.py b/test/metadata_raw_search_test.py index cc833f5a..4ff32e84 100644 --- a/test/metadata_raw_search_test.py +++ b/test/metadata_raw_search_test.py @@ -35,7 +35,7 @@ def _envelope(hits=None, aggregations=None, **extra): class MetadataRawSearchTestCase(ClientTestCase): def test_the_body_reaches_the_server_verbatim(self): body = { - 'query': {'bool': {'must': [{'term': {'artifact.mimetype': 'application/zip'}}]}}, + 'query': {'bool': {'must': [{'term': {'scan.mimetype': 'application/zip'}}]}}, 'aggs': {'families': {'terms': {'field': 'polyunite.malware_family', 'size': 20}}}, 'size': 0, 'sort': [{'artifact.created': {'order': 'desc'}}], diff --git a/test/vcr/test_async_metadata_raw_search.vcr b/test/vcr/test_async_metadata_raw_search.vcr new file mode 100644 index 00000000..f975407e --- /dev/null +++ b/test/vcr/test_async_metadata_raw_search.vcr @@ -0,0 +1,616 @@ +interactions: +- request: + body: '{"artifact_name":"artifact","artifact_type":"FILE","community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '71' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.7.0 (x86_64-Linux-CPython-3.12.13) + method: POST + uri: http://ai:9696/v3/instance + response: + body: + string: '{"result":{"artifact_id":"26342380388629077","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-10-16T19:12:08.544796+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-10-16T19:12:08.544796+00:00","id":"26342380388629077","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/26342380388629077","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/b5/a4/ea/b5a4ea70-7ad2-4e1a-ab30-99e95a055177?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261016T191208Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=540618b13a3137a78d4054e5f4b812a50c01f835059c1934fa0912934d0f4900","votes":[],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '1044' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12:08 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +- request: + body: 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H* + + test_async_metadata_raw_search' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '99' + host: + - minio:9000 + user-agent: + - polyswarm_api/4.7.0 (x86_64-Linux-CPython-3.12.13) + method: PUT + uri: http://minio:9000/artifact-index/instances/b5/a4/ea/b5a4ea70-7ad2-4e1a-ab30-99e95a055177?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261016T191208Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=540618b13a3137a78d4054e5f4b812a50c01f835059c1934fa0912934d0f4900 + response: + body: + string: '' + headers: + Accept-Ranges: + - bytes + Content-Length: + - '0' + Date: + - Fri, 16 Oct 2026 19:12:08 GMT + ETag: + - '"b7353f3b103f99087768deeac1a73302"' + Server: + - MinIO + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + Vary: + - Origin + - Accept-Encoding + X-Amz-Id-2: + - dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8 + X-Amz-Request-Id: + - 18DF1854A59952F5 + X-Content-Type-Options: + - nosniff + X-Ratelimit-Limit: + - '9189' + X-Ratelimit-Remaining: + - '9189' + X-Xss-Protection: + - 1; mode=block + x-amz-expiration: + - expiry-date="Sun, 18 Oct 2026 00:00:00 GMT", rule-id="expiration-artifact-index_0-instances" + 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.7.0 (x86_64-Linux-CPython-3.12.13) + method: PUT + uri: http://ai:9696/v3/instance?id=26342380388629077 + response: + body: + string: '{"result":{"artifact_id":"26342380388629077","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-10-16T19:12:08.544796+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-10-16T19:12:08.544796+00:00","id":"26342380388629077","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/26342380388629077","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/b5/a4/ea/b5a4ea70-7ad2-4e1a-ab30-99e95a055177?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261016T191208Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=540618b13a3137a78d4054e5f4b812a50c01f835059c1934fa0912934d0f4900","votes":[],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '1044' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12:08 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.7.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://ai:9696/v3/consumer/submission/gamma/26342380388629077 + response: + body: + string: '{"result":{"artifact_id":"26342380388629077","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-10-16T19:12:08.544796+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-10-16T19:12:08.544796+00:00","id":"26342380388629077","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/26342380388629077","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/b5/a4/ea/b5a4ea70-7ad2-4e1a-ab30-99e95a055177?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261016T191208Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=540618b13a3137a78d4054e5f4b812a50c01f835059c1934fa0912934d0f4900","votes":[],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '1044' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12:08 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.7.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://ai:9696/v3/consumer/submission/gamma/26342380388629077 + response: + body: + string: '{"result":{"artifact_id":"26342380388629077","assertions":[{"author":"917202772827143","author_name":"engine-eicar","bid":"1000000000000000000","engine":{"description":"webhook-microengine-description","name":"engine-eicar"},"mask":true,"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"verdict":true}],"bounty_state":2,"community":"gamma","country":"","created":"2026-10-16T19:12:08.544796+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-10-16T19:00:05.919385+00:00","id":"26342380388629077","known_good":null,"last_scanned":null,"last_seen":null,"md5":"b7353f3b103f99087768deeac1a73302","metadata":[{"created":"2026-10-16T19:12:08.663786+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:10:16 + 19:12:08+00:00","fileinodechangedate":"2026:10:16 19:12:08+00:00","filemodifydate":"2026:10:16 + 19:12:08+00:00","filename":"tmpzbmnd_d1","filepermissions":"-rw-r--r--","filesize":"99 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmpzbmnd_d1","wordcount":2},"updated":"2026-10-16T19:12:08.663786+00:00"},{"created":"2026-10-16T19:12:08.613583+00:00","tool":"hash","tool_metadata":{"md5":"b7353f3b103f99087768deeac1a73302","sha1":"4bd92be69056d4709d4d7cf621712e058a4cafed","sha256":"02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f","sha3_256":"a7330cdc95aa94bdcea7e10e95d52733a2fdbb4d8fa223bde33bcacc192a7746","sha3_512":"8f8fd035fea2b2fe1b07d0b6700241d2941d2e58c2659e92ed0c71edc61060d0fa74c3c161ec6372d32fb3c43e918feb57c97702cf5711436da5302010d67533","sha512":"75e71c8dffe7ab52300f15d0ad4a5af1517b2315a821c9bac09738531017874c9aa019132cfea82af754a154a85e33c8cf542a0614e60133fab309b6127b63dd","ssdeep":"3:a+JraNvsgzsVqSwHqDY9ARoERgJz:tJuOgzskTj4gJz","tlsh":"a6b012003a1feb5f552730210071f5641904511a1cd04628525420b4993208406dc554"},"updated":"2026-10-16T19:12:08.613583+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f/26342380388629077","polyscore":null,"result":null,"sha1":"4bd92be69056d4709d4d7cf621712e058a4cafed","sha256":"02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f","size":99,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/b5/a4/ea/b5a4ea70-7ad2-4e1a-ab30-99e95a055177?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261016T191208Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=540618b13a3137a78d4054e5f4b812a50c01f835059c1934fa0912934d0f4900","votes":[],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2940' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12:09 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.7.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://ai:9696/v3/consumer/submission/gamma/26342380388629077 + response: + body: + string: '{"result":{"artifact_id":"26342380388629077","assertions":[{"author":"917202772827143","author_name":"engine-eicar","bid":"1000000000000000000","engine":{"description":"webhook-microengine-description","name":"engine-eicar"},"mask":true,"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"verdict":true}],"bounty_state":2,"community":"gamma","country":"","created":"2026-10-16T19:12:08.544796+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-10-16T19:00:05.919385+00:00","id":"26342380388629077","known_good":null,"last_scanned":null,"last_seen":null,"md5":"b7353f3b103f99087768deeac1a73302","metadata":[{"created":"2026-10-16T19:12:08.663786+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:10:16 + 19:12:08+00:00","fileinodechangedate":"2026:10:16 19:12:08+00:00","filemodifydate":"2026:10:16 + 19:12:08+00:00","filename":"tmpzbmnd_d1","filepermissions":"-rw-r--r--","filesize":"99 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmpzbmnd_d1","wordcount":2},"updated":"2026-10-16T19:12:08.663786+00:00"},{"created":"2026-10-16T19:12:08.613583+00:00","tool":"hash","tool_metadata":{"md5":"b7353f3b103f99087768deeac1a73302","sha1":"4bd92be69056d4709d4d7cf621712e058a4cafed","sha256":"02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f","sha3_256":"a7330cdc95aa94bdcea7e10e95d52733a2fdbb4d8fa223bde33bcacc192a7746","sha3_512":"8f8fd035fea2b2fe1b07d0b6700241d2941d2e58c2659e92ed0c71edc61060d0fa74c3c161ec6372d32fb3c43e918feb57c97702cf5711436da5302010d67533","sha512":"75e71c8dffe7ab52300f15d0ad4a5af1517b2315a821c9bac09738531017874c9aa019132cfea82af754a154a85e33c8cf542a0614e60133fab309b6127b63dd","ssdeep":"3:a+JraNvsgzsVqSwHqDY9ARoERgJz:tJuOgzskTj4gJz","tlsh":"a6b012003a1feb5f552730210071f5641904511a1cd04628525420b4993208406dc554"},"updated":"2026-10-16T19:12:08.613583+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f/26342380388629077","polyscore":null,"result":null,"sha1":"4bd92be69056d4709d4d7cf621712e058a4cafed","sha256":"02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f","size":99,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/b5/a4/ea/b5a4ea70-7ad2-4e1a-ab30-99e95a055177?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261016T191208Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=540618b13a3137a78d4054e5f4b812a50c01f835059c1934fa0912934d0f4900","votes":[],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2940' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12:10 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.7.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://ai:9696/v3/consumer/submission/gamma/26342380388629077 + response: + body: + string: '{"result":{"artifact_id":"26342380388629077","assertions":[{"author":"917202772827143","author_name":"engine-eicar","bid":"1000000000000000000","engine":{"description":"webhook-microengine-description","name":"engine-eicar"},"mask":true,"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"verdict":true}],"bounty_state":2,"community":"gamma","country":"","created":"2026-10-16T19:12:08.544796+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-10-16T19:00:05.919385+00:00","id":"26342380388629077","known_good":null,"last_scanned":null,"last_seen":null,"md5":"b7353f3b103f99087768deeac1a73302","metadata":[{"created":"2026-10-16T19:12:08.663786+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:10:16 + 19:12:08+00:00","fileinodechangedate":"2026:10:16 19:12:08+00:00","filemodifydate":"2026:10:16 + 19:12:08+00:00","filename":"tmpzbmnd_d1","filepermissions":"-rw-r--r--","filesize":"99 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmpzbmnd_d1","wordcount":2},"updated":"2026-10-16T19:12:08.663786+00:00"},{"created":"2026-10-16T19:12:08.613583+00:00","tool":"hash","tool_metadata":{"md5":"b7353f3b103f99087768deeac1a73302","sha1":"4bd92be69056d4709d4d7cf621712e058a4cafed","sha256":"02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f","sha3_256":"a7330cdc95aa94bdcea7e10e95d52733a2fdbb4d8fa223bde33bcacc192a7746","sha3_512":"8f8fd035fea2b2fe1b07d0b6700241d2941d2e58c2659e92ed0c71edc61060d0fa74c3c161ec6372d32fb3c43e918feb57c97702cf5711436da5302010d67533","sha512":"75e71c8dffe7ab52300f15d0ad4a5af1517b2315a821c9bac09738531017874c9aa019132cfea82af754a154a85e33c8cf542a0614e60133fab309b6127b63dd","ssdeep":"3:a+JraNvsgzsVqSwHqDY9ARoERgJz:tJuOgzskTj4gJz","tlsh":"a6b012003a1feb5f552730210071f5641904511a1cd04628525420b4993208406dc554"},"updated":"2026-10-16T19:12:08.613583+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f/26342380388629077","polyscore":null,"result":null,"sha1":"4bd92be69056d4709d4d7cf621712e058a4cafed","sha256":"02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f","size":99,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/b5/a4/ea/b5a4ea70-7ad2-4e1a-ab30-99e95a055177?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261016T191208Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=540618b13a3137a78d4054e5f4b812a50c01f835059c1934fa0912934d0f4900","votes":[],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2940' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12:11 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.7.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://ai:9696/v3/consumer/submission/gamma/26342380388629077 + response: + body: + string: '{"result":{"artifact_id":"26342380388629077","assertions":[{"author":"917202772827143","author_name":"engine-eicar","bid":"1000000000000000000","engine":{"description":"webhook-microengine-description","name":"engine-eicar"},"mask":true,"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"verdict":true}],"bounty_state":2,"community":"gamma","country":"","created":"2026-10-16T19:12:08.544796+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-10-16T19:00:05.919385+00:00","id":"26342380388629077","known_good":null,"last_scanned":null,"last_seen":null,"md5":"b7353f3b103f99087768deeac1a73302","metadata":[{"created":"2026-10-16T19:12:08.663786+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:10:16 + 19:12:08+00:00","fileinodechangedate":"2026:10:16 19:12:08+00:00","filemodifydate":"2026:10:16 + 19:12:08+00:00","filename":"tmpzbmnd_d1","filepermissions":"-rw-r--r--","filesize":"99 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmpzbmnd_d1","wordcount":2},"updated":"2026-10-16T19:12:08.663786+00:00"},{"created":"2026-10-16T19:12:08.613583+00:00","tool":"hash","tool_metadata":{"md5":"b7353f3b103f99087768deeac1a73302","sha1":"4bd92be69056d4709d4d7cf621712e058a4cafed","sha256":"02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f","sha3_256":"a7330cdc95aa94bdcea7e10e95d52733a2fdbb4d8fa223bde33bcacc192a7746","sha3_512":"8f8fd035fea2b2fe1b07d0b6700241d2941d2e58c2659e92ed0c71edc61060d0fa74c3c161ec6372d32fb3c43e918feb57c97702cf5711436da5302010d67533","sha512":"75e71c8dffe7ab52300f15d0ad4a5af1517b2315a821c9bac09738531017874c9aa019132cfea82af754a154a85e33c8cf542a0614e60133fab309b6127b63dd","ssdeep":"3:a+JraNvsgzsVqSwHqDY9ARoERgJz:tJuOgzskTj4gJz","tlsh":"a6b012003a1feb5f552730210071f5641904511a1cd04628525420b4993208406dc554"},"updated":"2026-10-16T19:12:08.613583+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f/26342380388629077","polyscore":null,"result":null,"sha1":"4bd92be69056d4709d4d7cf621712e058a4cafed","sha256":"02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f","size":99,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/b5/a4/ea/b5a4ea70-7ad2-4e1a-ab30-99e95a055177?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261016T191208Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=540618b13a3137a78d4054e5f4b812a50c01f835059c1934fa0912934d0f4900","votes":[],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2940' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12:12 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.7.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://ai:9696/v3/consumer/submission/gamma/26342380388629077 + response: + body: + string: '{"result":{"artifact_id":"26342380388629077","assertions":[{"author":"917202772827143","author_name":"engine-eicar","bid":"1000000000000000000","engine":{"description":"webhook-microengine-description","name":"engine-eicar"},"mask":true,"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"verdict":true}],"bounty_state":2,"community":"gamma","country":"","created":"2026-10-16T19:12:08.544796+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-10-16T19:00:05.919385+00:00","id":"26342380388629077","known_good":null,"last_scanned":null,"last_seen":null,"md5":"b7353f3b103f99087768deeac1a73302","metadata":[{"created":"2026-10-16T19:12:08.663786+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:10:16 + 19:12:08+00:00","fileinodechangedate":"2026:10:16 19:12:08+00:00","filemodifydate":"2026:10:16 + 19:12:08+00:00","filename":"tmpzbmnd_d1","filepermissions":"-rw-r--r--","filesize":"99 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmpzbmnd_d1","wordcount":2},"updated":"2026-10-16T19:12:08.663786+00:00"},{"created":"2026-10-16T19:12:08.613583+00:00","tool":"hash","tool_metadata":{"md5":"b7353f3b103f99087768deeac1a73302","sha1":"4bd92be69056d4709d4d7cf621712e058a4cafed","sha256":"02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f","sha3_256":"a7330cdc95aa94bdcea7e10e95d52733a2fdbb4d8fa223bde33bcacc192a7746","sha3_512":"8f8fd035fea2b2fe1b07d0b6700241d2941d2e58c2659e92ed0c71edc61060d0fa74c3c161ec6372d32fb3c43e918feb57c97702cf5711436da5302010d67533","sha512":"75e71c8dffe7ab52300f15d0ad4a5af1517b2315a821c9bac09738531017874c9aa019132cfea82af754a154a85e33c8cf542a0614e60133fab309b6127b63dd","ssdeep":"3:a+JraNvsgzsVqSwHqDY9ARoERgJz:tJuOgzskTj4gJz","tlsh":"a6b012003a1feb5f552730210071f5641904511a1cd04628525420b4993208406dc554"},"updated":"2026-10-16T19:12:08.613583+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f/26342380388629077","polyscore":null,"result":null,"sha1":"4bd92be69056d4709d4d7cf621712e058a4cafed","sha256":"02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f","size":99,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/b5/a4/ea/b5a4ea70-7ad2-4e1a-ab30-99e95a055177?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261016T191208Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=540618b13a3137a78d4054e5f4b812a50c01f835059c1934fa0912934d0f4900","votes":[],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2940' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12:13 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.7.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://ai:9696/v3/consumer/submission/gamma/26342380388629077 + response: + body: + string: '{"result":{"artifact_id":"26342380388629077","assertions":[{"author":"917202772827143","author_name":"engine-eicar","bid":"1000000000000000000","engine":{"description":"webhook-microengine-description","name":"engine-eicar"},"mask":true,"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"verdict":true}],"bounty_state":2,"community":"gamma","country":"","created":"2026-10-16T19:12:08.544796+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-10-16T19:00:05.919385+00:00","id":"26342380388629077","known_good":null,"last_scanned":null,"last_seen":null,"md5":"b7353f3b103f99087768deeac1a73302","metadata":[{"created":"2026-10-16T19:12:08.663786+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:10:16 + 19:12:08+00:00","fileinodechangedate":"2026:10:16 19:12:08+00:00","filemodifydate":"2026:10:16 + 19:12:08+00:00","filename":"tmpzbmnd_d1","filepermissions":"-rw-r--r--","filesize":"99 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmpzbmnd_d1","wordcount":2},"updated":"2026-10-16T19:12:08.663786+00:00"},{"created":"2026-10-16T19:12:08.613583+00:00","tool":"hash","tool_metadata":{"md5":"b7353f3b103f99087768deeac1a73302","sha1":"4bd92be69056d4709d4d7cf621712e058a4cafed","sha256":"02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f","sha3_256":"a7330cdc95aa94bdcea7e10e95d52733a2fdbb4d8fa223bde33bcacc192a7746","sha3_512":"8f8fd035fea2b2fe1b07d0b6700241d2941d2e58c2659e92ed0c71edc61060d0fa74c3c161ec6372d32fb3c43e918feb57c97702cf5711436da5302010d67533","sha512":"75e71c8dffe7ab52300f15d0ad4a5af1517b2315a821c9bac09738531017874c9aa019132cfea82af754a154a85e33c8cf542a0614e60133fab309b6127b63dd","ssdeep":"3:a+JraNvsgzsVqSwHqDY9ARoERgJz:tJuOgzskTj4gJz","tlsh":"a6b012003a1feb5f552730210071f5641904511a1cd04628525420b4993208406dc554"},"updated":"2026-10-16T19:12:08.613583+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f/26342380388629077","polyscore":null,"result":null,"sha1":"4bd92be69056d4709d4d7cf621712e058a4cafed","sha256":"02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f","size":99,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/b5/a4/ea/b5a4ea70-7ad2-4e1a-ab30-99e95a055177?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261016T191208Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=540618b13a3137a78d4054e5f4b812a50c01f835059c1934fa0912934d0f4900","votes":[],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2940' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12:14 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.7.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://ai:9696/v3/consumer/submission/gamma/26342380388629077 + response: + body: + string: '{"result":{"artifact_id":"26342380388629077","assertions":[{"author":"917202772827143","author_name":"engine-eicar","bid":"1000000000000000000","engine":{"description":"webhook-microengine-description","name":"engine-eicar"},"mask":true,"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"verdict":true}],"bounty_state":3,"community":"gamma","country":"","created":"2026-10-16T19:12:08.544796+00:00","detections":{"benign":0,"malicious":1,"total":1},"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-10-16T19:00:05.919385+00:00","id":"26342380388629077","known_good":null,"last_scanned":"2026-10-16T19:12:08.544796+00:00","last_seen":"2026-10-16T19:12:08.544796+00:00","md5":"b7353f3b103f99087768deeac1a73302","metadata":[{"created":"2026-10-16T19:12:15.629021+00:00","tool":"polyunite","tool_metadata":{"labels":["nonmalware"],"malware_family":"EICAR","operating_system":[]},"updated":"2026-10-16T19:12:15.629021+00:00"},{"created":"2026-10-16T19:12:08.663786+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:10:16 + 19:12:08+00:00","fileinodechangedate":"2026:10:16 19:12:08+00:00","filemodifydate":"2026:10:16 + 19:12:08+00:00","filename":"tmpzbmnd_d1","filepermissions":"-rw-r--r--","filesize":"99 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmpzbmnd_d1","wordcount":2},"updated":"2026-10-16T19:12:08.663786+00:00"},{"created":"2026-10-16T19:12:08.613583+00:00","tool":"hash","tool_metadata":{"md5":"b7353f3b103f99087768deeac1a73302","sha1":"4bd92be69056d4709d4d7cf621712e058a4cafed","sha256":"02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f","sha3_256":"a7330cdc95aa94bdcea7e10e95d52733a2fdbb4d8fa223bde33bcacc192a7746","sha3_512":"8f8fd035fea2b2fe1b07d0b6700241d2941d2e58c2659e92ed0c71edc61060d0fa74c3c161ec6372d32fb3c43e918feb57c97702cf5711436da5302010d67533","sha512":"75e71c8dffe7ab52300f15d0ad4a5af1517b2315a821c9bac09738531017874c9aa019132cfea82af754a154a85e33c8cf542a0614e60133fab309b6127b63dd","ssdeep":"3:a+JraNvsgzsVqSwHqDY9ARoERgJz:tJuOgzskTj4gJz","tlsh":"a6b012003a1feb5f552730210071f5641904511a1cd04628525420b4993208406dc554"},"updated":"2026-10-16T19:12:08.613583+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f/26342380388629077","polyscore":null,"result":null,"sha1":"4bd92be69056d4709d4d7cf621712e058a4cafed","sha256":"02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f","size":99,"state":"AWAITING_ARBITRATION","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/b5/a4/ea/b5a4ea70-7ad2-4e1a-ab30-99e95a055177?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261016T191208Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=540618b13a3137a78d4054e5f4b812a50c01f835059c1934fa0912934d0f4900","votes":[],"window_closed":true},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3242' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12:15 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"query":{"term":{"artifact.sha256":"02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f"}},"aggs":{"extensions":{"terms":{"field":"extension","size":5}}},"size":1}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '177' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.7.0 (x86_64-Linux-CPython-3.12.13) + method: POST + uri: http://ai:9696/v3/search/metadata/raw?community=gamma + response: + body: + string: '{"result":{"aggregations":{"extensions":{"buckets":[{"doc_count":1,"key":"txt"}],"doc_count_error_upper_bound":0,"sum_other_doc_count":0}},"hits":{"hits":[{"_id":"20521297859705876","_index":"metadata-2963","_score":1.1451323,"_source":{"artifact":{"created":"2026-10-16T19:00:05.919385+00:00","id":"20521297859705876","md5":"b7353f3b103f99087768deeac1a73302","sha1":"4bd92be69056d4709d4d7cf621712e058a4cafed","sha256":"02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f","size":99},"exiftool":{"filesize":"99 + bytes","filetype":"TXT","filetypeextension":"txt","mimetype":"text/plain"},"extension":"txt","hash":{"md5":"b7353f3b103f99087768deeac1a73302","sha1":"4bd92be69056d4709d4d7cf621712e058a4cafed","sha256":"02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f","sha3_256":"a7330cdc95aa94bdcea7e10e95d52733a2fdbb4d8fa223bde33bcacc192a7746","sha3_512":"8f8fd035fea2b2fe1b07d0b6700241d2941d2e58c2659e92ed0c71edc61060d0fa74c3c161ec6372d32fb3c43e918feb57c97702cf5711436da5302010d67533","sha512":"75e71c8dffe7ab52300f15d0ad4a5af1517b2315a821c9bac09738531017874c9aa019132cfea82af754a154a85e33c8cf542a0614e60133fab309b6127b63dd","ssdeep":"3:a+JraNvsgzsVqSwHqDY9ARoERgJz:tJuOgzskTj4gJz","tlsh":"a6b012003a1feb5f552730210071f5641904511a1cd04628525420b4993208406dc554"},"meta_community":"_public","modified":"2026-10-16T19:10:36.705430+00:00","polyunite":{"labels":["nonmalware"],"malware_family":"EICAR"},"scan":{"detections":{"benign":0,"malicious":1,"total":1},"filename":["artifact"],"first_scan":{"artifact_instance_id":"76137403510278611","assertions":{"engine-eicar":{"assertion":"malicious","metadata":{"malware_family":"EICAR"}}},"created":"2026-10-16T19:00:05.919385+00:00","detections":{"benign":0,"malicious":1,"total":1},"filename":"artifact","votes":{}},"first_seen":"2026-10-16T19:00:05.919385+00:00","last_seen":"2026-10-16T19:10:29.621757+00:00","latest_scan":{"artifact_instance_id":"10385075083158560","assertions":{"engine-eicar":{"assertion":"malicious","metadata":{"malware_family":"EICAR"}}},"created":"2026-10-16T19:10:29.621757+00:00","detections":{"benign":0,"malicious":1,"total":1},"filename":"artifact","votes":{}},"mimetype":{"extended":"EICAR + virus test files","mime":"text/plain"}},"updated":{"exiftool":"2026-10-16T19:10:29.740999+00:00","hash":"2026-10-16T19:10:29.691749+00:00","polyunite":"2026-10-16T19:10:36.705430+00:00"}}}],"max_score":1.1451323,"total":{"relation":"eq","value":1}}},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2459' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12:15 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"query":{"term":{"artifact.sha256":"02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f"}},"aggs":{"extensions":{"terms":{"field":"extension","size":5}}},"size":1}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '177' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.7.0 (x86_64-Linux-CPython-3.12.13) + method: POST + uri: http://ai:9696/v3/search/metadata/raw?community=gamma + response: + body: + string: '{"result":{"aggregations":{"extensions":{"buckets":[{"doc_count":1,"key":"txt"}],"doc_count_error_upper_bound":0,"sum_other_doc_count":0}},"hits":{"hits":[{"_id":"20521297859705876","_index":"metadata-2963","_score":1.1451323,"_source":{"artifact":{"created":"2026-10-16T19:00:05.919385+00:00","id":"20521297859705876","md5":"b7353f3b103f99087768deeac1a73302","sha1":"4bd92be69056d4709d4d7cf621712e058a4cafed","sha256":"02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f","size":99},"exiftool":{"filesize":"99 + bytes","filetype":"TXT","filetypeextension":"txt","mimetype":"text/plain"},"extension":"txt","hash":{"md5":"b7353f3b103f99087768deeac1a73302","sha1":"4bd92be69056d4709d4d7cf621712e058a4cafed","sha256":"02d34dcceb9491e8e03c9229df4c99106c349ab76aeee790d295218fc8c9aa7f","sha3_256":"a7330cdc95aa94bdcea7e10e95d52733a2fdbb4d8fa223bde33bcacc192a7746","sha3_512":"8f8fd035fea2b2fe1b07d0b6700241d2941d2e58c2659e92ed0c71edc61060d0fa74c3c161ec6372d32fb3c43e918feb57c97702cf5711436da5302010d67533","sha512":"75e71c8dffe7ab52300f15d0ad4a5af1517b2315a821c9bac09738531017874c9aa019132cfea82af754a154a85e33c8cf542a0614e60133fab309b6127b63dd","ssdeep":"3:a+JraNvsgzsVqSwHqDY9ARoERgJz:tJuOgzskTj4gJz","tlsh":"a6b012003a1feb5f552730210071f5641904511a1cd04628525420b4993208406dc554"},"meta_community":"_public","modified":"2026-10-16T19:10:36.705430+00:00","polyunite":{"labels":["nonmalware"],"malware_family":"EICAR"},"scan":{"detections":{"benign":0,"malicious":1,"total":1},"filename":["artifact"],"first_scan":{"artifact_instance_id":"76137403510278611","assertions":{"engine-eicar":{"assertion":"malicious","metadata":{"malware_family":"EICAR"}}},"created":"2026-10-16T19:00:05.919385+00:00","detections":{"benign":0,"malicious":1,"total":1},"filename":"artifact","votes":{}},"first_seen":"2026-10-16T19:00:05.919385+00:00","last_seen":"2026-10-16T19:10:29.621757+00:00","latest_scan":{"artifact_instance_id":"10385075083158560","assertions":{"engine-eicar":{"assertion":"malicious","metadata":{"malware_family":"EICAR"}}},"created":"2026-10-16T19:10:29.621757+00:00","detections":{"benign":0,"malicious":1,"total":1},"filename":"artifact","votes":{}},"mimetype":{"extended":"EICAR + virus test files","mime":"text/plain"}},"updated":{"exiftool":"2026-10-16T19:10:29.740999+00:00","hash":"2026-10-16T19:10:29.691749+00:00","polyunite":"2026-10-16T19:10:36.705430+00:00"}}}],"max_score":1.1451323,"total":{"relation":"eq","value":1}}},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2459' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12:15 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +version: 1 diff --git a/test/vcr/test_metadata_raw_search.vcr b/test/vcr/test_metadata_raw_search.vcr new file mode 100644 index 00000000..895b0d11 --- /dev/null +++ b/test/vcr/test_metadata_raw_search.vcr @@ -0,0 +1,616 @@ +interactions: +- request: + body: '{"artifact_name":"artifact","artifact_type":"FILE","community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '71' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.7.0 (x86_64-Linux-CPython-3.12.13) + method: POST + uri: http://ai:9696/v3/instance + response: + body: + string: '{"result":{"artifact_id":"39027402148512388","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-10-16T19:12:00.979459+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-10-16T19:12:00.979459+00:00","id":"39027402148512388","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/39027402148512388","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/16/3c/7e/163c7e58-4656-4362-a3bc-5ac4283eb044?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261016T191200Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=26b7e10592c0bfbc1070cdd1b4576cd752a4c9e43de694f9c83d8f24c7f7db58","votes":[],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '1044' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12:00 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +- request: + body: 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H* + + test_metadata_raw_search' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '93' + host: + - minio:9000 + user-agent: + - polyswarm_api/4.7.0 (x86_64-Linux-CPython-3.12.13) + method: PUT + uri: http://minio:9000/artifact-index/instances/16/3c/7e/163c7e58-4656-4362-a3bc-5ac4283eb044?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261016T191200Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=26b7e10592c0bfbc1070cdd1b4576cd752a4c9e43de694f9c83d8f24c7f7db58 + response: + body: + string: '' + headers: + Accept-Ranges: + - bytes + Content-Length: + - '0' + Date: + - Fri, 16 Oct 2026 19:12:00 GMT + ETag: + - '"41e0052928de2071fbd1ba679a270cd6"' + Server: + - MinIO + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + Vary: + - Origin + - Accept-Encoding + X-Amz-Id-2: + - dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8 + X-Amz-Request-Id: + - 18DF1852E2A007B9 + X-Content-Type-Options: + - nosniff + X-Ratelimit-Limit: + - '9189' + X-Ratelimit-Remaining: + - '9189' + X-Xss-Protection: + - 1; mode=block + x-amz-expiration: + - expiry-date="Sun, 18 Oct 2026 00:00:00 GMT", rule-id="expiration-artifact-index_0-instances" + 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.7.0 (x86_64-Linux-CPython-3.12.13) + method: PUT + uri: http://ai:9696/v3/instance?id=39027402148512388 + response: + body: + string: '{"result":{"artifact_id":"39027402148512388","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-10-16T19:12:00.979459+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-10-16T19:12:00.979459+00:00","id":"39027402148512388","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/39027402148512388","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/16/3c/7e/163c7e58-4656-4362-a3bc-5ac4283eb044?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261016T191200Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=26b7e10592c0bfbc1070cdd1b4576cd752a4c9e43de694f9c83d8f24c7f7db58","votes":[],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '1044' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12: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/4.7.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://ai:9696/v3/consumer/submission/gamma/39027402148512388 + response: + body: + string: '{"result":{"artifact_id":"39027402148512388","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-10-16T19:12:00.979459+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-10-16T19:12:00.979459+00:00","id":"39027402148512388","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/39027402148512388","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/16/3c/7e/163c7e58-4656-4362-a3bc-5ac4283eb044?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261016T191200Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=26b7e10592c0bfbc1070cdd1b4576cd752a4c9e43de694f9c83d8f24c7f7db58","votes":[],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '1044' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12: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/4.7.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://ai:9696/v3/consumer/submission/gamma/39027402148512388 + response: + body: + string: '{"result":{"artifact_id":"39027402148512388","assertions":[{"author":"917202772827143","author_name":"engine-eicar","bid":"1000000000000000000","engine":{"description":"webhook-microengine-description","name":"engine-eicar"},"mask":true,"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"verdict":true}],"bounty_state":2,"community":"gamma","country":"","created":"2026-10-16T19:12:00.979459+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-10-16T19:00:49.830485+00:00","id":"39027402148512388","known_good":null,"last_scanned":null,"last_seen":null,"md5":"41e0052928de2071fbd1ba679a270cd6","metadata":[{"created":"2026-10-16T19:12:01.115418+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:10:16 + 19:12:01+00:00","fileinodechangedate":"2026:10:16 19:12:01+00:00","filemodifydate":"2026:10:16 + 19:12:01+00:00","filename":"tmp9iq2nb1e","filepermissions":"-rw-r--r--","filesize":"93 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp9iq2nb1e","wordcount":2},"updated":"2026-10-16T19:12:01.115418+00:00"},{"created":"2026-10-16T19:12:01.066743+00:00","tool":"hash","tool_metadata":{"md5":"41e0052928de2071fbd1ba679a270cd6","sha1":"13aa5ea0daab56572ca33eabb896559f5f69f865","sha256":"6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4","sha3_256":"62df3d6496ebbfc23e53ba86037a49890bf1047d9282c355ac0d213df694d5b3","sha3_512":"9ee53d77492d6e3f5a55e666fc6e0dc477f6104edbe7bd9a3968149446c128d180efb55f3f87b486726a5eb58626325ce0428b86793446d53aebccbeac4f50ee","sha512":"3471c2e3c1ad204dd512d30cbb94235c60b079151d6b333e754ad34fa916c718856cdab48ed3ec49f5bfda288db81736357567a2c656eacf050443e44e4a2ab7","ssdeep":"3:a+JraNvsgzsVqSwHqDY1RgJz:tJuOgzsk5gJz","tlsh":"4bb01200360feb6f512724111075f6641904510a1cd00628515010b4893208402dc154"},"updated":"2026-10-16T19:12:01.066743+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4/39027402148512388","polyscore":null,"result":null,"sha1":"13aa5ea0daab56572ca33eabb896559f5f69f865","sha256":"6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4","size":93,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/16/3c/7e/163c7e58-4656-4362-a3bc-5ac4283eb044?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261016T191200Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=26b7e10592c0bfbc1070cdd1b4576cd752a4c9e43de694f9c83d8f24c7f7db58","votes":[],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2934' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12:02 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.7.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://ai:9696/v3/consumer/submission/gamma/39027402148512388 + response: + body: + string: '{"result":{"artifact_id":"39027402148512388","assertions":[{"author":"917202772827143","author_name":"engine-eicar","bid":"1000000000000000000","engine":{"description":"webhook-microengine-description","name":"engine-eicar"},"mask":true,"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"verdict":true}],"bounty_state":2,"community":"gamma","country":"","created":"2026-10-16T19:12:00.979459+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-10-16T19:00:49.830485+00:00","id":"39027402148512388","known_good":null,"last_scanned":null,"last_seen":null,"md5":"41e0052928de2071fbd1ba679a270cd6","metadata":[{"created":"2026-10-16T19:12:01.115418+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:10:16 + 19:12:01+00:00","fileinodechangedate":"2026:10:16 19:12:01+00:00","filemodifydate":"2026:10:16 + 19:12:01+00:00","filename":"tmp9iq2nb1e","filepermissions":"-rw-r--r--","filesize":"93 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp9iq2nb1e","wordcount":2},"updated":"2026-10-16T19:12:01.115418+00:00"},{"created":"2026-10-16T19:12:01.066743+00:00","tool":"hash","tool_metadata":{"md5":"41e0052928de2071fbd1ba679a270cd6","sha1":"13aa5ea0daab56572ca33eabb896559f5f69f865","sha256":"6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4","sha3_256":"62df3d6496ebbfc23e53ba86037a49890bf1047d9282c355ac0d213df694d5b3","sha3_512":"9ee53d77492d6e3f5a55e666fc6e0dc477f6104edbe7bd9a3968149446c128d180efb55f3f87b486726a5eb58626325ce0428b86793446d53aebccbeac4f50ee","sha512":"3471c2e3c1ad204dd512d30cbb94235c60b079151d6b333e754ad34fa916c718856cdab48ed3ec49f5bfda288db81736357567a2c656eacf050443e44e4a2ab7","ssdeep":"3:a+JraNvsgzsVqSwHqDY1RgJz:tJuOgzsk5gJz","tlsh":"4bb01200360feb6f512724111075f6641904510a1cd00628515010b4893208402dc154"},"updated":"2026-10-16T19:12:01.066743+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4/39027402148512388","polyscore":null,"result":null,"sha1":"13aa5ea0daab56572ca33eabb896559f5f69f865","sha256":"6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4","size":93,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/16/3c/7e/163c7e58-4656-4362-a3bc-5ac4283eb044?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261016T191200Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=26b7e10592c0bfbc1070cdd1b4576cd752a4c9e43de694f9c83d8f24c7f7db58","votes":[],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2934' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12:03 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.7.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://ai:9696/v3/consumer/submission/gamma/39027402148512388 + response: + body: + string: '{"result":{"artifact_id":"39027402148512388","assertions":[{"author":"917202772827143","author_name":"engine-eicar","bid":"1000000000000000000","engine":{"description":"webhook-microengine-description","name":"engine-eicar"},"mask":true,"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"verdict":true}],"bounty_state":2,"community":"gamma","country":"","created":"2026-10-16T19:12:00.979459+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-10-16T19:00:49.830485+00:00","id":"39027402148512388","known_good":null,"last_scanned":null,"last_seen":null,"md5":"41e0052928de2071fbd1ba679a270cd6","metadata":[{"created":"2026-10-16T19:12:01.115418+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:10:16 + 19:12:01+00:00","fileinodechangedate":"2026:10:16 19:12:01+00:00","filemodifydate":"2026:10:16 + 19:12:01+00:00","filename":"tmp9iq2nb1e","filepermissions":"-rw-r--r--","filesize":"93 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp9iq2nb1e","wordcount":2},"updated":"2026-10-16T19:12:01.115418+00:00"},{"created":"2026-10-16T19:12:01.066743+00:00","tool":"hash","tool_metadata":{"md5":"41e0052928de2071fbd1ba679a270cd6","sha1":"13aa5ea0daab56572ca33eabb896559f5f69f865","sha256":"6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4","sha3_256":"62df3d6496ebbfc23e53ba86037a49890bf1047d9282c355ac0d213df694d5b3","sha3_512":"9ee53d77492d6e3f5a55e666fc6e0dc477f6104edbe7bd9a3968149446c128d180efb55f3f87b486726a5eb58626325ce0428b86793446d53aebccbeac4f50ee","sha512":"3471c2e3c1ad204dd512d30cbb94235c60b079151d6b333e754ad34fa916c718856cdab48ed3ec49f5bfda288db81736357567a2c656eacf050443e44e4a2ab7","ssdeep":"3:a+JraNvsgzsVqSwHqDY1RgJz:tJuOgzsk5gJz","tlsh":"4bb01200360feb6f512724111075f6641904510a1cd00628515010b4893208402dc154"},"updated":"2026-10-16T19:12:01.066743+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4/39027402148512388","polyscore":null,"result":null,"sha1":"13aa5ea0daab56572ca33eabb896559f5f69f865","sha256":"6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4","size":93,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/16/3c/7e/163c7e58-4656-4362-a3bc-5ac4283eb044?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261016T191200Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=26b7e10592c0bfbc1070cdd1b4576cd752a4c9e43de694f9c83d8f24c7f7db58","votes":[],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2934' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12:04 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.7.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://ai:9696/v3/consumer/submission/gamma/39027402148512388 + response: + body: + string: '{"result":{"artifact_id":"39027402148512388","assertions":[{"author":"917202772827143","author_name":"engine-eicar","bid":"1000000000000000000","engine":{"description":"webhook-microengine-description","name":"engine-eicar"},"mask":true,"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"verdict":true}],"bounty_state":2,"community":"gamma","country":"","created":"2026-10-16T19:12:00.979459+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-10-16T19:00:49.830485+00:00","id":"39027402148512388","known_good":null,"last_scanned":null,"last_seen":null,"md5":"41e0052928de2071fbd1ba679a270cd6","metadata":[{"created":"2026-10-16T19:12:01.115418+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:10:16 + 19:12:01+00:00","fileinodechangedate":"2026:10:16 19:12:01+00:00","filemodifydate":"2026:10:16 + 19:12:01+00:00","filename":"tmp9iq2nb1e","filepermissions":"-rw-r--r--","filesize":"93 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp9iq2nb1e","wordcount":2},"updated":"2026-10-16T19:12:01.115418+00:00"},{"created":"2026-10-16T19:12:01.066743+00:00","tool":"hash","tool_metadata":{"md5":"41e0052928de2071fbd1ba679a270cd6","sha1":"13aa5ea0daab56572ca33eabb896559f5f69f865","sha256":"6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4","sha3_256":"62df3d6496ebbfc23e53ba86037a49890bf1047d9282c355ac0d213df694d5b3","sha3_512":"9ee53d77492d6e3f5a55e666fc6e0dc477f6104edbe7bd9a3968149446c128d180efb55f3f87b486726a5eb58626325ce0428b86793446d53aebccbeac4f50ee","sha512":"3471c2e3c1ad204dd512d30cbb94235c60b079151d6b333e754ad34fa916c718856cdab48ed3ec49f5bfda288db81736357567a2c656eacf050443e44e4a2ab7","ssdeep":"3:a+JraNvsgzsVqSwHqDY1RgJz:tJuOgzsk5gJz","tlsh":"4bb01200360feb6f512724111075f6641904510a1cd00628515010b4893208402dc154"},"updated":"2026-10-16T19:12:01.066743+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4/39027402148512388","polyscore":null,"result":null,"sha1":"13aa5ea0daab56572ca33eabb896559f5f69f865","sha256":"6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4","size":93,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/16/3c/7e/163c7e58-4656-4362-a3bc-5ac4283eb044?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261016T191200Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=26b7e10592c0bfbc1070cdd1b4576cd752a4c9e43de694f9c83d8f24c7f7db58","votes":[],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2934' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12:05 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.7.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://ai:9696/v3/consumer/submission/gamma/39027402148512388 + response: + body: + string: '{"result":{"artifact_id":"39027402148512388","assertions":[{"author":"917202772827143","author_name":"engine-eicar","bid":"1000000000000000000","engine":{"description":"webhook-microengine-description","name":"engine-eicar"},"mask":true,"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"verdict":true}],"bounty_state":2,"community":"gamma","country":"","created":"2026-10-16T19:12:00.979459+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-10-16T19:00:49.830485+00:00","id":"39027402148512388","known_good":null,"last_scanned":null,"last_seen":null,"md5":"41e0052928de2071fbd1ba679a270cd6","metadata":[{"created":"2026-10-16T19:12:01.115418+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:10:16 + 19:12:01+00:00","fileinodechangedate":"2026:10:16 19:12:01+00:00","filemodifydate":"2026:10:16 + 19:12:01+00:00","filename":"tmp9iq2nb1e","filepermissions":"-rw-r--r--","filesize":"93 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp9iq2nb1e","wordcount":2},"updated":"2026-10-16T19:12:01.115418+00:00"},{"created":"2026-10-16T19:12:01.066743+00:00","tool":"hash","tool_metadata":{"md5":"41e0052928de2071fbd1ba679a270cd6","sha1":"13aa5ea0daab56572ca33eabb896559f5f69f865","sha256":"6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4","sha3_256":"62df3d6496ebbfc23e53ba86037a49890bf1047d9282c355ac0d213df694d5b3","sha3_512":"9ee53d77492d6e3f5a55e666fc6e0dc477f6104edbe7bd9a3968149446c128d180efb55f3f87b486726a5eb58626325ce0428b86793446d53aebccbeac4f50ee","sha512":"3471c2e3c1ad204dd512d30cbb94235c60b079151d6b333e754ad34fa916c718856cdab48ed3ec49f5bfda288db81736357567a2c656eacf050443e44e4a2ab7","ssdeep":"3:a+JraNvsgzsVqSwHqDY1RgJz:tJuOgzsk5gJz","tlsh":"4bb01200360feb6f512724111075f6641904510a1cd00628515010b4893208402dc154"},"updated":"2026-10-16T19:12:01.066743+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4/39027402148512388","polyscore":null,"result":null,"sha1":"13aa5ea0daab56572ca33eabb896559f5f69f865","sha256":"6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4","size":93,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/16/3c/7e/163c7e58-4656-4362-a3bc-5ac4283eb044?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261016T191200Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=26b7e10592c0bfbc1070cdd1b4576cd752a4c9e43de694f9c83d8f24c7f7db58","votes":[],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2934' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12:06 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.7.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://ai:9696/v3/consumer/submission/gamma/39027402148512388 + response: + body: + string: '{"result":{"artifact_id":"39027402148512388","assertions":[{"author":"917202772827143","author_name":"engine-eicar","bid":"1000000000000000000","engine":{"description":"webhook-microengine-description","name":"engine-eicar"},"mask":true,"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"verdict":true}],"bounty_state":2,"community":"gamma","country":"","created":"2026-10-16T19:12:00.979459+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-10-16T19:00:49.830485+00:00","id":"39027402148512388","known_good":null,"last_scanned":null,"last_seen":null,"md5":"41e0052928de2071fbd1ba679a270cd6","metadata":[{"created":"2026-10-16T19:12:01.115418+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:10:16 + 19:12:01+00:00","fileinodechangedate":"2026:10:16 19:12:01+00:00","filemodifydate":"2026:10:16 + 19:12:01+00:00","filename":"tmp9iq2nb1e","filepermissions":"-rw-r--r--","filesize":"93 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp9iq2nb1e","wordcount":2},"updated":"2026-10-16T19:12:01.115418+00:00"},{"created":"2026-10-16T19:12:01.066743+00:00","tool":"hash","tool_metadata":{"md5":"41e0052928de2071fbd1ba679a270cd6","sha1":"13aa5ea0daab56572ca33eabb896559f5f69f865","sha256":"6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4","sha3_256":"62df3d6496ebbfc23e53ba86037a49890bf1047d9282c355ac0d213df694d5b3","sha3_512":"9ee53d77492d6e3f5a55e666fc6e0dc477f6104edbe7bd9a3968149446c128d180efb55f3f87b486726a5eb58626325ce0428b86793446d53aebccbeac4f50ee","sha512":"3471c2e3c1ad204dd512d30cbb94235c60b079151d6b333e754ad34fa916c718856cdab48ed3ec49f5bfda288db81736357567a2c656eacf050443e44e4a2ab7","ssdeep":"3:a+JraNvsgzsVqSwHqDY1RgJz:tJuOgzsk5gJz","tlsh":"4bb01200360feb6f512724111075f6641904510a1cd00628515010b4893208402dc154"},"updated":"2026-10-16T19:12:01.066743+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4/39027402148512388","polyscore":null,"result":null,"sha1":"13aa5ea0daab56572ca33eabb896559f5f69f865","sha256":"6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4","size":93,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/16/3c/7e/163c7e58-4656-4362-a3bc-5ac4283eb044?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261016T191200Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=26b7e10592c0bfbc1070cdd1b4576cd752a4c9e43de694f9c83d8f24c7f7db58","votes":[],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2934' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12:07 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.7.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://ai:9696/v3/consumer/submission/gamma/39027402148512388 + response: + body: + string: '{"result":{"artifact_id":"39027402148512388","assertions":[{"author":"917202772827143","author_name":"engine-eicar","bid":"1000000000000000000","engine":{"description":"webhook-microengine-description","name":"engine-eicar"},"mask":true,"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"verdict":true}],"bounty_state":3,"community":"gamma","country":"","created":"2026-10-16T19:12:00.979459+00:00","detections":{"benign":0,"malicious":1,"total":1},"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-10-16T19:00:49.830485+00:00","id":"39027402148512388","known_good":null,"last_scanned":"2026-10-16T19:12:00.979459+00:00","last_seen":"2026-10-16T19:12:00.979459+00:00","md5":"41e0052928de2071fbd1ba679a270cd6","metadata":[{"created":"2026-10-16T19:12:08.073833+00:00","tool":"polyunite","tool_metadata":{"labels":["nonmalware"],"malware_family":"EICAR","operating_system":[]},"updated":"2026-10-16T19:12:08.073833+00:00"},{"created":"2026-10-16T19:12:01.115418+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:10:16 + 19:12:01+00:00","fileinodechangedate":"2026:10:16 19:12:01+00:00","filemodifydate":"2026:10:16 + 19:12:01+00:00","filename":"tmp9iq2nb1e","filepermissions":"-rw-r--r--","filesize":"93 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp9iq2nb1e","wordcount":2},"updated":"2026-10-16T19:12:01.115418+00:00"},{"created":"2026-10-16T19:12:01.066743+00:00","tool":"hash","tool_metadata":{"md5":"41e0052928de2071fbd1ba679a270cd6","sha1":"13aa5ea0daab56572ca33eabb896559f5f69f865","sha256":"6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4","sha3_256":"62df3d6496ebbfc23e53ba86037a49890bf1047d9282c355ac0d213df694d5b3","sha3_512":"9ee53d77492d6e3f5a55e666fc6e0dc477f6104edbe7bd9a3968149446c128d180efb55f3f87b486726a5eb58626325ce0428b86793446d53aebccbeac4f50ee","sha512":"3471c2e3c1ad204dd512d30cbb94235c60b079151d6b333e754ad34fa916c718856cdab48ed3ec49f5bfda288db81736357567a2c656eacf050443e44e4a2ab7","ssdeep":"3:a+JraNvsgzsVqSwHqDY1RgJz:tJuOgzsk5gJz","tlsh":"4bb01200360feb6f512724111075f6641904510a1cd00628515010b4893208402dc154"},"updated":"2026-10-16T19:12:01.066743+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4/39027402148512388","polyscore":null,"result":null,"sha1":"13aa5ea0daab56572ca33eabb896559f5f69f865","sha256":"6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4","size":93,"state":"AWAITING_ARBITRATION","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/16/3c/7e/163c7e58-4656-4362-a3bc-5ac4283eb044?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261016T191200Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=26b7e10592c0bfbc1070cdd1b4576cd752a4c9e43de694f9c83d8f24c7f7db58","votes":[],"window_closed":true},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3236' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12:08 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"query":{"term":{"artifact.sha256":"6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4"}},"aggs":{"extensions":{"terms":{"field":"extension","size":5}}},"size":1}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '177' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.7.0 (x86_64-Linux-CPython-3.12.13) + method: POST + uri: http://ai:9696/v3/search/metadata/raw?community=gamma + response: + body: + string: '{"result":{"aggregations":{"extensions":{"buckets":[{"doc_count":1,"key":"txt"}],"doc_count_error_upper_bound":0,"sum_other_doc_count":0}},"hits":{"hits":[{"_id":"48910585090325105","_index":"metadata-2963","_score":0.8938179,"_source":{"artifact":{"created":"2026-10-16T19:00:49.830485+00:00","id":"48910585090325105","md5":"41e0052928de2071fbd1ba679a270cd6","sha1":"13aa5ea0daab56572ca33eabb896559f5f69f865","sha256":"6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4","size":93},"exiftool":{"filesize":"93 + bytes","filetype":"TXT","filetypeextension":"txt","mimetype":"text/plain"},"extension":"txt","hash":{"md5":"41e0052928de2071fbd1ba679a270cd6","sha1":"13aa5ea0daab56572ca33eabb896559f5f69f865","sha256":"6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4","sha3_256":"62df3d6496ebbfc23e53ba86037a49890bf1047d9282c355ac0d213df694d5b3","sha3_512":"9ee53d77492d6e3f5a55e666fc6e0dc477f6104edbe7bd9a3968149446c128d180efb55f3f87b486726a5eb58626325ce0428b86793446d53aebccbeac4f50ee","sha512":"3471c2e3c1ad204dd512d30cbb94235c60b079151d6b333e754ad34fa916c718856cdab48ed3ec49f5bfda288db81736357567a2c656eacf050443e44e4a2ab7","ssdeep":"3:a+JraNvsgzsVqSwHqDY1RgJz:tJuOgzsk5gJz","tlsh":"4bb01200360feb6f512724111075f6641904510a1cd00628515010b4893208402dc154"},"meta_community":"_public","modified":"2026-10-16T19:11:03.128969+00:00","polyunite":{"labels":["nonmalware"],"malware_family":"EICAR"},"scan":{"detections":{"benign":0,"malicious":1,"total":1},"filename":["artifact"],"first_scan":{"artifact_instance_id":"31484117870144315","assertions":{"engine-eicar":{"assertion":"malicious","metadata":{"malware_family":"EICAR"}}},"created":"2026-10-16T19:00:49.830485+00:00","detections":{"benign":0,"malicious":1,"total":1},"filename":"artifact","votes":{}},"first_seen":"2026-10-16T19:00:49.830485+00:00","last_seen":"2026-10-16T19:10:56.036870+00:00","latest_scan":{"artifact_instance_id":"45873202982231538","assertions":{"engine-eicar":{"assertion":"malicious","metadata":{"malware_family":"EICAR"}}},"created":"2026-10-16T19:10:56.036870+00:00","detections":{"benign":0,"malicious":1,"total":1},"filename":"artifact","votes":{}},"mimetype":{"extended":"EICAR + virus test files","mime":"text/plain"}},"updated":{"exiftool":"2026-10-16T19:10:56.167945+00:00","hash":"2026-10-16T19:10:56.114851+00:00","polyunite":"2026-10-16T19:11:03.128969+00:00"}}}],"max_score":0.8938179,"total":{"relation":"eq","value":1}}},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2453' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12:08 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"query":{"term":{"artifact.sha256":"6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4"}},"aggs":{"extensions":{"terms":{"field":"extension","size":5}}},"size":1}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '177' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.7.0 (x86_64-Linux-CPython-3.12.13) + method: POST + uri: http://ai:9696/v3/search/metadata/raw?community=gamma + response: + body: + string: '{"result":{"aggregations":{"extensions":{"buckets":[{"doc_count":1,"key":"txt"}],"doc_count_error_upper_bound":0,"sum_other_doc_count":0}},"hits":{"hits":[{"_id":"48910585090325105","_index":"metadata-2963","_score":0.8938179,"_source":{"artifact":{"created":"2026-10-16T19:00:49.830485+00:00","id":"48910585090325105","md5":"41e0052928de2071fbd1ba679a270cd6","sha1":"13aa5ea0daab56572ca33eabb896559f5f69f865","sha256":"6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4","size":93},"exiftool":{"filesize":"93 + bytes","filetype":"TXT","filetypeextension":"txt","mimetype":"text/plain"},"extension":"txt","hash":{"md5":"41e0052928de2071fbd1ba679a270cd6","sha1":"13aa5ea0daab56572ca33eabb896559f5f69f865","sha256":"6955694202ef2ae2a29ba488473e5a78032024cd2213d250db7ef6f6736495e4","sha3_256":"62df3d6496ebbfc23e53ba86037a49890bf1047d9282c355ac0d213df694d5b3","sha3_512":"9ee53d77492d6e3f5a55e666fc6e0dc477f6104edbe7bd9a3968149446c128d180efb55f3f87b486726a5eb58626325ce0428b86793446d53aebccbeac4f50ee","sha512":"3471c2e3c1ad204dd512d30cbb94235c60b079151d6b333e754ad34fa916c718856cdab48ed3ec49f5bfda288db81736357567a2c656eacf050443e44e4a2ab7","ssdeep":"3:a+JraNvsgzsVqSwHqDY1RgJz:tJuOgzsk5gJz","tlsh":"4bb01200360feb6f512724111075f6641904510a1cd00628515010b4893208402dc154"},"meta_community":"_public","modified":"2026-10-16T19:11:03.128969+00:00","polyunite":{"labels":["nonmalware"],"malware_family":"EICAR"},"scan":{"detections":{"benign":0,"malicious":1,"total":1},"filename":["artifact"],"first_scan":{"artifact_instance_id":"31484117870144315","assertions":{"engine-eicar":{"assertion":"malicious","metadata":{"malware_family":"EICAR"}}},"created":"2026-10-16T19:00:49.830485+00:00","detections":{"benign":0,"malicious":1,"total":1},"filename":"artifact","votes":{}},"first_seen":"2026-10-16T19:00:49.830485+00:00","last_seen":"2026-10-16T19:10:56.036870+00:00","latest_scan":{"artifact_instance_id":"45873202982231538","assertions":{"engine-eicar":{"assertion":"malicious","metadata":{"malware_family":"EICAR"}}},"created":"2026-10-16T19:10:56.036870+00:00","detections":{"benign":0,"malicious":1,"total":1},"filename":"artifact","votes":{}},"mimetype":{"extended":"EICAR + virus test files","mime":"text/plain"}},"updated":{"exiftool":"2026-10-16T19:10:56.167945+00:00","hash":"2026-10-16T19:10:56.114851+00:00","polyunite":"2026-10-16T19:11:03.128969+00:00"}}}],"max_score":0.8938179,"total":{"relation":"eq","value":1}}},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2453' + Content-Type: + - application/json + Date: + - Fri, 16 Oct 2026 19:12:08 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +version: 1 From 5ba13570d5f275aa55f607229b20c4f3c5dd0436 Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 16 Oct 2026 17:36:01 -0300 Subject: [PATCH 03/18] test: tell a missing raw-search route from a lagging index poll_equals absorbs NotFoundException as "the index has not caught up". That is right for a document and wrong for an endpoint: a search matching nothing is a 200 with total 0, so a 404 from this route means the server side is not deployed. Absorbed as lag it burned 90 polls and then failed as `assert None == 1`, naming nothing - and because the e2e job runs with TESTS_VCR=off against a live stack, that is what develop would do if this landed before the server release, blocking the :latest promotion every other repo's pipeline pulls. The reader now skips with the reason instead. It costs no extra request - the first poll is the probe - so the cassettes still replay. Drop it once the route ships. Also document two things the method's contract depends on and its docstring left out: omitting `size` returns no documents, because the server defaults it to 0 so an aggregation caller does not pay for hits they did not ask for; and `partial` is a list of reasons, whose absence - not an empty `hits` - is what says a count is whole. --- src/polyswarm_api/aio/api.py | 11 ++++++++--- src/polyswarm_api/api.py | 11 ++++++++--- test/async_client_test.py | 9 ++++++++- test/client_scan_test.py | 23 +++++++++++++++++++++-- 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/polyswarm_api/aio/api.py b/src/polyswarm_api/aio/api.py index 9a6f6457..b3b93f33 100644 --- a/src/polyswarm_api/aio/api.py +++ b/src/polyswarm_api/aio/api.py @@ -301,11 +301,16 @@ async def metadata_raw_search(self, body, community=None): refuses after running it. :param body: An OpenSearch ``_search`` body (``query``, ``aggs``, - ``size``, ``sort``, ``_source``, ...). + ``size``, ``sort``, ``_source``, ...). Omitting ``size`` returns + **no documents**: the server defaults it to 0 so an aggregation + caller does not pay for hits they did not ask for. Ask for hits + by setting it. :param community: Community to search; defaults to the client's. :return: A ``MetadataRawSearch`` whose ``json`` carries the engine's - ``hits`` and ``aggregations``, plus ``partial`` when the server - could not establish that the answer is complete. + ``hits`` and ``aggregations``, plus ``partial`` - a list of + reasons - when the server could not establish that the answer is + complete. Treat its absence, not an empty ``hits``, as the signal + that a count is whole. """ logger.info('Running a raw metadata search') return await self._single( diff --git a/src/polyswarm_api/api.py b/src/polyswarm_api/api.py index 2fb678db..095b170d 100644 --- a/src/polyswarm_api/api.py +++ b/src/polyswarm_api/api.py @@ -316,11 +316,16 @@ def metadata_raw_search(self, body, community=None): refuses after running it. :param body: An OpenSearch ``_search`` body (``query``, ``aggs``, - ``size``, ``sort``, ``_source``, ...). + ``size``, ``sort``, ``_source``, ...). Omitting ``size`` returns + **no documents**: the server defaults it to 0 so an aggregation + caller does not pay for hits they did not ask for. Ask for hits + by setting it. :param community: Community to search; defaults to the client's. :return: A ``MetadataRawSearch`` whose ``json`` carries the engine's - ``hits`` and ``aggregations``, plus ``partial`` when the server - could not establish that the answer is complete. + ``hits`` and ``aggregations``, plus ``partial`` - a list of + reasons - when the server could not establish that the answer is + complete. Treat its absence, not an empty ``hits``, as the signal + that a count is whole. """ logger.info("Running a raw metadata search") return self._single( diff --git a/test/async_client_test.py b/test/async_client_test.py index aa71fb66..0c55ab0e 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -299,7 +299,14 @@ async def test_async_metadata_raw_search(self, uid): } async def _hit_count(): - result = await api.metadata_raw_search(body) + # A 404 is the route missing, not the index lagging - see + # _raw_or_skip in client_scan_test.py. + try: + result = await api.metadata_raw_search(body) + except exceptions.NotFoundException: + pytest.skip('POST /search/metadata/raw is not deployed ' + 'on this stack; the server release has to ' + 'land first') return result.json['hits']['total']['value'] assert await poll_equals_async(_hit_count, 1, tries=90) == 1 diff --git a/test/client_scan_test.py b/test/client_scan_test.py index 8cd16a47..64c893f3 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -69,6 +69,26 @@ def rescan_and_scan(api, sha): vcr.use_cassette = lambda *_a, **_k: (lambda _fn: _fn) + +def _raw_or_skip(api, body): + """A reader for ``poll_equals`` that tells a missing route from a missing + document. + + poll_equals treats NotFoundException as "the index has not caught up", which + is right for a document and wrong for an endpoint: a search that matches + nothing is a 200 with total 0, so a 404 here means the server route is not + deployed. Absorbed as lag it costs 90 polls and then fails as + ``assert None == 1``, which names nothing. Drop this once the route ships. + """ + def read(): + try: + return api.metadata_raw_search(body).json['hits']['total']['value'] + except exceptions.NotFoundException: + pytest.skip('POST /search/metadata/raw is not deployed on this ' + 'stack; the server release has to land first') + return read + + @contextmanager def temp_dir(files_dict): with tempfile.TemporaryDirectory() as tmp_dir: @@ -377,8 +397,7 @@ def test_metadata_raw_search(self): 'aggs': {'extensions': {'terms': {'field': 'extension', 'size': 5}}}, 'size': 1, } - found = poll_equals( - lambda: api.metadata_raw_search(body).json['hits']['total']['value'], 1, tries=90) + found = poll_equals(_raw_or_skip(api, body), 1, tries=90) assert found == 1 result = api.metadata_raw_search(body) assert result.json['aggregations']['extensions']['buckets'] From fa905ec336c8f97f4074ab1d5973ae5102e316ef Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 16 Oct 2026 18:28:13 -0300 Subject: [PATCH 04/18] docs: separate a capped hit count from a partial answer The docstring told callers that partial's absence means a count is whole. It does not: the server suppresses the hit-total signal when the body asked for no hits, because a total capped at track_total_hits is what every large query returns and reporting it would mark all of them partial. Exactness of a count is hits.total.relation - eq or gte - which is the engine's own field and passes through untouched. partial speaks for the aggregations and the search as a whole. --- src/polyswarm_api/aio/api.py | 11 +++++++++-- src/polyswarm_api/api.py | 11 +++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/polyswarm_api/aio/api.py b/src/polyswarm_api/aio/api.py index b3b93f33..0bca3ff3 100644 --- a/src/polyswarm_api/aio/api.py +++ b/src/polyswarm_api/aio/api.py @@ -309,8 +309,15 @@ async def metadata_raw_search(self, body, community=None): :return: A ``MetadataRawSearch`` whose ``json`` carries the engine's ``hits`` and ``aggregations``, plus ``partial`` - a list of reasons - when the server could not establish that the answer is - complete. Treat its absence, not an empty ``hits``, as the signal - that a count is whole. + complete. + + ``partial`` speaks for the aggregations and the search as a whole. + Whether a hit *count* is exact is a different question, answered by + ``hits.total.relation``: ``eq`` is exact, ``gte`` means the engine + stopped counting at ``track_total_hits``. That is the engine's own + field and passes through untouched. A capped total is not reported + as partial - it is what every large query returns, and the server + suppresses it entirely when the body asked for no hits. """ logger.info('Running a raw metadata search') return await self._single( diff --git a/src/polyswarm_api/api.py b/src/polyswarm_api/api.py index 095b170d..fdd0e600 100644 --- a/src/polyswarm_api/api.py +++ b/src/polyswarm_api/api.py @@ -324,8 +324,15 @@ def metadata_raw_search(self, body, community=None): :return: A ``MetadataRawSearch`` whose ``json`` carries the engine's ``hits`` and ``aggregations``, plus ``partial`` - a list of reasons - when the server could not establish that the answer is - complete. Treat its absence, not an empty ``hits``, as the signal - that a count is whole. + complete. + + ``partial`` speaks for the aggregations and the search as a whole. + Whether a hit *count* is exact is a different question, answered by + ``hits.total.relation``: ``eq`` is exact, ``gte`` means the engine + stopped counting at ``track_total_hits``. That is the engine's own + field and passes through untouched. A capped total is not reported + as partial - it is what every large query returns, and the server + suppresses it entirely when the body asked for no hits. """ logger.info("Running a raw metadata search") return self._single( From f8e3987de0f9b291564e83acaaa4b9f9eb312fb2 Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 16 Oct 2026 18:48:15 -0300 Subject: [PATCH 05/18] docs(specs): record metadata_raw_search in the endpoint and resource specs AGENTS.md step 5 asks for specs/03-endpoints.md in the same PR as a new resource, and the class tree in specs/02-resources.md lists every BaseJsonResource. Both omitted this one. --- specs/02-resources.md | 1 + specs/03-endpoints.md | 1 + 2 files changed, 2 insertions(+) diff --git a/specs/02-resources.md b/specs/02-resources.md index 1eb25abe..06e30c50 100644 --- a/specs/02-resources.md +++ b/specs/02-resources.md @@ -31,6 +31,7 @@ BaseResource # holds .api, ._content, .parse_result cla ├── Metadata # /search/metadata/query ├── MetadataFieldProperties # /search/metadata/properties ├── MetadataMapping # /search/metadata/mappings + ├── MetadataRawSearch # /search/metadata/raw ├── IOC # /ioc (known good / known bad / search) ├── LiveYaraRuleset # /hunt/rule/live ├── HistoricalHunt # /hunt/historical diff --git a/specs/03-endpoints.md b/specs/03-endpoints.md index fb20efab..5ebe7f0a 100644 --- a/specs/03-endpoints.md +++ b/specs/03-endpoints.md @@ -35,6 +35,7 @@ The full catalogue of methods on the public client surface and which transport h | Method | Resource builder | |---|---| | `metadata_mapping()` | inline (GET) — parsed into `MetadataMapping` | +| `metadata_raw_search(body, community=None)` | inline (POST) — parsed into `MetadataRawSearch`. `body` is sent verbatim: it is an OpenSearch `_search` body, and the server owns the grammar it accepts. `community` rides the **query string**, not the body, because the server overlays the community filter itself. | | `metadata_field_properties_write(field_path, description, …)` | inline (POST) | | `metadata_field_properties_get(field_path)` | inline (GET) | | `metadata_field_properties_delete(field_path)` | inline (DELETE) | From c351e769985376da54cb77e8f31c896cb9c8f5e0 Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 16 Oct 2026 19:07:55 -0300 Subject: [PATCH 06/18] fix(test): fail on a missing raw-search route instead of skipping The e2e job runs TESTS_VCR=off against a live stack, so a skip let a renamed or withdrawn route report green and take the :latest promotion with it. AGENTS.md already orders the server endpoint first, so a 404 is a real failure rather than a merge-ordering inconvenience to absorb. The early exit stays: it still names the cause immediately instead of spending 90 polls to fail as `assert None == 1`. Renamed to _raw_hit_count, since it no longer skips. --- test/async_client_test.py | 8 ++++---- test/client_scan_test.py | 18 +++++++++++------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/test/async_client_test.py b/test/async_client_test.py index 0c55ab0e..ae0d6184 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -300,13 +300,13 @@ async def test_async_metadata_raw_search(self, uid): async def _hit_count(): # A 404 is the route missing, not the index lagging - see - # _raw_or_skip in client_scan_test.py. + # _raw_hit_count in client_scan_test.py. try: result = await api.metadata_raw_search(body) except exceptions.NotFoundException: - pytest.skip('POST /search/metadata/raw is not deployed ' - 'on this stack; the server release has to ' - 'land first') + pytest.fail('POST /search/metadata/raw answered 404: the ' + 'route is not on this stack. The server change ' + 'merges first.') return result.json['hits']['total']['value'] assert await poll_equals_async(_hit_count, 1, tries=90) == 1 diff --git a/test/client_scan_test.py b/test/client_scan_test.py index 64c893f3..57e4737e 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -70,22 +70,26 @@ def rescan_and_scan(api, sha): -def _raw_or_skip(api, body): +def _raw_hit_count(api, body): """A reader for ``poll_equals`` that tells a missing route from a missing document. poll_equals treats NotFoundException as "the index has not caught up", which is right for a document and wrong for an endpoint: a search that matches - nothing is a 200 with total 0, so a 404 here means the server route is not - deployed. Absorbed as lag it costs 90 polls and then fails as - ``assert None == 1``, which names nothing. Drop this once the route ships. + nothing is a 200 with total 0, so a 404 here means the route is absent. + Absorbed as lag it costs 90 polls and then fails as ``assert None == 1``, + which names nothing. + + It fails rather than skips, and stays after the route ships: the e2e job + runs ``TESTS_VCR=off`` against a live stack, so a skip here would let a + renamed or withdrawn route report green and promote ``:latest``. """ def read(): try: return api.metadata_raw_search(body).json['hits']['total']['value'] except exceptions.NotFoundException: - pytest.skip('POST /search/metadata/raw is not deployed on this ' - 'stack; the server release has to land first') + pytest.fail('POST /search/metadata/raw answered 404: the route is ' + 'not on this stack. The server change merges first.') return read @@ -397,7 +401,7 @@ def test_metadata_raw_search(self): 'aggs': {'extensions': {'terms': {'field': 'extension', 'size': 5}}}, 'size': 1, } - found = poll_equals(_raw_or_skip(api, body), 1, tries=90) + found = poll_equals(_raw_hit_count(api, body), 1, tries=90) assert found == 1 result = api.metadata_raw_search(body) assert result.json['aggregations']['extensions']['buckets'] From 80080a25a019d59a5de5265cb0768ed96aa8bdb0 Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 16 Oct 2026 19:07:56 -0300 Subject: [PATCH 07/18] fix(test): show partial as the list of reasons it is The fixture mocked a dict, so the test demonstrated a shape the server never sends and the docstring does not promise. --- test/metadata_raw_search_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/metadata_raw_search_test.py b/test/metadata_raw_search_test.py index 4ff32e84..715d72b4 100644 --- a/test/metadata_raw_search_test.py +++ b/test/metadata_raw_search_test.py @@ -72,9 +72,9 @@ def test_a_partial_answer_keeps_its_marker(self): # from a complete one is this key. Dropping it in the parse layer would # turn "we could not establish this is complete" into a hard number. self.mock.add('POST', _RESOURCE_URL, - json=_envelope(partial={'terminated_early': True})) + json=_envelope(partial=['the search stopped at terminate_after'])) result = self.api.metadata_raw_search({'query': {'match_all': {}}}) - assert result.json['partial'] == {'terminated_early': True} + assert result.json['partial'] == ['the search stopped at terminate_after'] def test_a_complete_answer_carries_no_marker(self): self.mock.add('POST', _RESOURCE_URL, json=_envelope()) From 3d2bfbe909f39c1eafb321ed93284645a2ef4138 Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 16 Oct 2026 19:28:17 -0300 Subject: [PATCH 08/18] refactor(resources): give metadata_raw_search a builder, test it pure-unit AGENTS.md routes a new resource through a builder classmethod on the resource, and specs/04 invariant 7 puts request-shape assertions in the pure-unit tier. The inline request dict forced the opposite: body-vs-query routing could only be observed through respx, which that invariant reserves for what the e2e stack cannot produce. MetadataRawSearch.search returns the PolyswarmRequest, so the routing reads off request_parameters with no httpx and no transport parametrisation. A mutation folding the community into the body kills three of the new tests. --- specs/03-endpoints.md | 2 +- src/polyswarm_api/aio/api.py | 7 +- src/polyswarm_api/api.py | 10 +-- src/polyswarm_api/resources.py | 15 ++++ test/metadata_raw_search_test.py | 135 +++++++++++++++++-------------- 5 files changed, 94 insertions(+), 75 deletions(-) diff --git a/specs/03-endpoints.md b/specs/03-endpoints.md index 5ebe7f0a..e46353b5 100644 --- a/specs/03-endpoints.md +++ b/specs/03-endpoints.md @@ -35,7 +35,7 @@ The full catalogue of methods on the public client surface and which transport h | Method | Resource builder | |---|---| | `metadata_mapping()` | inline (GET) — parsed into `MetadataMapping` | -| `metadata_raw_search(body, community=None)` | inline (POST) — parsed into `MetadataRawSearch`. `body` is sent verbatim: it is an OpenSearch `_search` body, and the server owns the grammar it accepts. `community` rides the **query string**, not the body, because the server overlays the community filter itself. | +| `metadata_raw_search(body, community=None)` | `MetadataRawSearch.search` (POST). `body` is sent verbatim: it is an OpenSearch `_search` body, and the server owns the grammar it accepts. `community` rides the **query string**, not the body, because the server overlays the community filter itself. | | `metadata_field_properties_write(field_path, description, …)` | inline (POST) | | `metadata_field_properties_get(field_path)` | inline (GET) | | `metadata_field_properties_delete(field_path)` | inline (DELETE) | diff --git a/src/polyswarm_api/aio/api.py b/src/polyswarm_api/aio/api.py index 0bca3ff3..841be70c 100644 --- a/src/polyswarm_api/aio/api.py +++ b/src/polyswarm_api/aio/api.py @@ -321,12 +321,7 @@ async def metadata_raw_search(self, body, community=None): """ logger.info('Running a raw metadata search') return await self._single( - {'method': 'POST', - 'url': f'{self.uri}{resources.MetadataRawSearch.RESOURCE_ENDPOINT}', - 'params': {'community': community or self.community}, - 'json': body}, - result_parser=resources.MetadataRawSearch, - ) + resources.MetadataRawSearch.search(self, body, community)) async def metadata_field_properties_write(self, field_path, description, example=None, category=None, aliases=None): diff --git a/src/polyswarm_api/api.py b/src/polyswarm_api/api.py index fdd0e600..4d19257a 100644 --- a/src/polyswarm_api/api.py +++ b/src/polyswarm_api/api.py @@ -335,15 +335,7 @@ def metadata_raw_search(self, body, community=None): suppresses it entirely when the body asked for no hits. """ logger.info("Running a raw metadata search") - return self._single( - { - "method": "POST", - "url": f"{self.uri}{resources.MetadataRawSearch.RESOURCE_ENDPOINT}", - "params": {"community": community or self.community}, - "json": body, - }, - result_parser=resources.MetadataRawSearch, - ) + return self._single(resources.MetadataRawSearch.search(self, body, community)) def metadata_field_properties_write( self, field_path, description, example=None, category=None, aliases=None diff --git a/src/polyswarm_api/resources.py b/src/polyswarm_api/resources.py index 96e1245c..74cfe898 100644 --- a/src/polyswarm_api/resources.py +++ b/src/polyswarm_api/resources.py @@ -80,6 +80,21 @@ class MetadataMapping(core.BaseJsonResource): class MetadataRawSearch(core.BaseJsonResource): RESOURCE_ENDPOINT = '/search/metadata/raw' + @classmethod + def search(cls, api, body, community=None): + """``body`` is the caller's whole query and travels untouched. The + community is scope, not query, and the server reads it from the query + string - folding it into the body would pick scope with a key the + server's grammar then rejects.""" + return core.PolyswarmRequest( + api=api, + method='POST', + url=f'{api.uri}{cls.RESOURCE_ENDPOINT}', + params={'community': community or api.community}, + json=body, + result_parser=cls, + ) + class MetadataFieldProperties(core.BaseJsonResource): RESOURCE_ENDPOINT = '/search/metadata/properties' diff --git a/test/metadata_raw_search_test.py b/test/metadata_raw_search_test.py index 715d72b4..2c5a7c4c 100644 --- a/test/metadata_raw_search_test.py +++ b/test/metadata_raw_search_test.py @@ -1,38 +1,60 @@ -"""Tests for the metadata_raw_search SDK method. +"""Pure-unit tests for the ``metadata_raw_search`` builder and its parse. -Request-shape tests, in the respx tier rather than the e2e one. What they -pin — that the query body reaches the server byte-for-byte and that the -community rides the query string instead of the body — is invisible from -the far side of a live call, which only shows that *some* scoped search -ran. The live lifecycle test lives in ``client_scan_test.py`` / +Request-shape assertions belong in this tier rather than respx +(``specs/04-testing.md`` invariant 7): ``MetadataRawSearch.search`` is pure +data, so the body-vs-query routing reads straight off the ``PolyswarmRequest`` +with no httpx. The live lifecycle test is in ``client_scan_test.py`` / ``async_client_test.py``. -The distinction is load-bearing rather than stylistic: the server reads -``community``/``communities`` out of a POST body when deciding scope, so a -client that folded the community into the body would have its scope chosen -by a key the server's own grammar then rejects. +The routing is load-bearing rather than stylistic: the server reads +``community``/``communities`` out of a POST body when it resolves scope, so a +client that folded the community into the body would pick its scope with a key +the server's own grammar then rejects. -Each test runs twice, once per transport — see ``test/_client_harness.py``. +Both transports share this builder, so one assertion covers each of them. + +Assertions read ``request_parameters``, the projection of what is *sent*. +``PolyswarmRequest.json`` is reused for the parsed *response* and is None +until the call runs - see ``core_test`` for that trap. """ -from test._client_harness import BASE_URL, COMMUNITY, ClientTestCase +from polyswarm_api import resources +from polyswarm_api.core import parse_response + + +_EMPTY_HITS = {'total': {'value': 0, 'relation': 'eq'}, 'hits': []} + + +class _FakeApi: + uri = 'https://api.example.test' + community = 'gamma' + + +class _FakeResponse: + """Minimal stand-in for httpx.Response that ``parse_response`` reads.""" + + def __init__(self, status_code=200, body=None, headers=None): + self.status_code = status_code + self._body = body + self.headers = headers or {} + def json(self): + return self._body -_RESOURCE_URL = f'{BASE_URL}/search/metadata/raw' +def _build(body, community=None): + return resources.MetadataRawSearch.search(_FakeApi(), body, community) -def _envelope(hits=None, aggregations=None, **extra): - result = {'hits': hits if hits is not None else {'total': {'value': 0, 'relation': 'eq'}, - 'hits': []}} - if aggregations is not None: - result['aggregations'] = aggregations - result.update(extra) - return {'result': result, 'status': 'OK'} +def _parsed(result): + req = _build({'query': {'match_all': {}}}) + parse_response(_FakeResponse(body={'status': 'OK', 'result': result}), req) + return req._result -# ── Tests ──────────────────────────────────────────────────────────── +# ── Request shape ──────────────────────────────────────────────────── -class MetadataRawSearchTestCase(ClientTestCase): + +class TestMetadataRawSearchRequest: def test_the_body_reaches_the_server_verbatim(self): body = { 'query': {'bool': {'must': [{'term': {'scan.mimetype': 'application/zip'}}]}}, @@ -40,48 +62,43 @@ def test_the_body_reaches_the_server_verbatim(self): 'size': 0, 'sort': [{'artifact.created': {'order': 'desc'}}], } - self.mock.add('POST', _RESOURCE_URL, json=_envelope(aggregations={'families': {}})) - self.api.metadata_raw_search(body) - assert self.mock.last_request_body == body + assert _build(body).request_parameters['json'] == body def test_the_community_rides_the_query_string_not_the_body(self): - # The server reads community/communities out of the POST body when it - # resolves scope, and its grammar rejects those keys for that reason. - self.mock.add('POST', _RESOURCE_URL, json=_envelope()) - self.api.metadata_raw_search({'query': {'match_all': {}}}) - assert f'community={COMMUNITY}' in self.mock.last_request_url - assert 'community' not in self.mock.last_request_body - assert 'communities' not in self.mock.last_request_body + sent = _build({'query': {'match_all': {}}}).request_parameters + assert sent['params'] == {'community': 'gamma'} + assert 'community' not in sent['json'] + assert 'communities' not in sent['json'] def test_an_explicit_community_overrides_the_clients(self): - self.mock.add('POST', _RESOURCE_URL, json=_envelope()) - self.api.metadata_raw_search({'query': {'match_all': {}}}, community='delta') - assert 'community=delta' in self.mock.last_request_url + sent = _build({'query': {'match_all': {}}}, community='delta').request_parameters + assert sent['params']['community'] == 'delta' + + def test_it_posts_to_the_raw_search_endpoint(self): + sent = _build({'query': {'match_all': {}}}).request_parameters + assert sent['method'] == 'POST' + assert sent['url'] == 'https://api.example.test/search/metadata/raw' + +# ── Parse ──────────────────────────────────────────────────────────── + + +class TestMetadataRawSearchParse: def test_hits_and_aggregations_are_both_reachable(self): - hits = {'total': {'value': 2, 'relation': 'eq'}, - 'hits': [{'_source': {'artifact': {'sha256': 'a' * 64}}}]} - aggs = {'families': {'buckets': [{'key': 'lockbit', 'doc_count': 2}]}} - self.mock.add('POST', _RESOURCE_URL, json=_envelope(hits=hits, aggregations=aggs)) - result = self.api.metadata_raw_search({'aggs': {'families': {'terms': {'field': 'f'}}}}) - assert result.json['hits']['total']['value'] == 2 - assert result.json['aggregations']['families']['buckets'][0]['key'] == 'lockbit' - - def test_a_partial_answer_keeps_its_marker(self): - # A truncated answer is still a 200, so the only thing separating it - # from a complete one is this key. Dropping it in the parse layer would - # turn "we could not establish this is complete" into a hard number. - self.mock.add('POST', _RESOURCE_URL, - json=_envelope(partial=['the search stopped at terminate_after'])) - result = self.api.metadata_raw_search({'query': {'match_all': {}}}) - assert result.json['partial'] == ['the search stopped at terminate_after'] + parsed = _parsed({ + 'hits': {'total': {'value': 2, 'relation': 'eq'}, + 'hits': [{'_source': {'artifact': {'sha256': 'a' * 64}}}]}, + 'aggregations': {'families': {'buckets': [{'key': 'lockbit', 'doc_count': 2}]}}, + }) + assert parsed.json['hits']['total']['value'] == 2 + assert parsed.json['aggregations']['families']['buckets'][0]['key'] == 'lockbit' + + def test_a_partial_answer_keeps_its_reasons(self): + # A truncated answer is still a 200, so this list is the only thing + # separating it from a complete one. Dropping it in the parse layer + # would turn "we could not establish this is complete" into a number. + reasons = ['the search stopped at terminate_after'] + assert _parsed({'hits': _EMPTY_HITS, 'partial': reasons}).json['partial'] == reasons def test_a_complete_answer_carries_no_marker(self): - self.mock.add('POST', _RESOURCE_URL, json=_envelope()) - result = self.api.metadata_raw_search({'query': {'match_all': {}}}) - assert 'partial' not in result.json - - def test_it_posts_to_the_raw_search_endpoint(self): - self.mock.add('POST', _RESOURCE_URL, json=_envelope()) - self.api.metadata_raw_search({'query': {'match_all': {}}}) - assert self.mock.last_request_url.startswith(_RESOURCE_URL) + assert 'partial' not in _parsed({'hits': _EMPTY_HITS}).json From 90bdfe7adf231ce8b4918354082b45e4f6a5b6c5 Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 16 Oct 2026 20:30:27 -0300 Subject: [PATCH 09/18] docs(api): state when a capped hit total is reported as partial The docstring stated the exception as the rule: it said a capped total is not reported as partial, when the server reports exactly that whenever the body asked for hits, and omits it only when it asked for none. A caller following the old text would read a complete aggregation as truncated and re-run a narrowed query, paying for another metered search that can never clear the marker. --- src/polyswarm_api/aio/api.py | 13 +++++++++---- src/polyswarm_api/api.py | 13 +++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/polyswarm_api/aio/api.py b/src/polyswarm_api/aio/api.py index 841be70c..348d187c 100644 --- a/src/polyswarm_api/aio/api.py +++ b/src/polyswarm_api/aio/api.py @@ -312,12 +312,17 @@ async def metadata_raw_search(self, body, community=None): complete. ``partial`` speaks for the aggregations and the search as a whole. - Whether a hit *count* is exact is a different question, answered by + Whether a hit *count* is exact is a separate question, answered by ``hits.total.relation``: ``eq`` is exact, ``gte`` means the engine stopped counting at ``track_total_hits``. That is the engine's own - field and passes through untouched. A capped total is not reported - as partial - it is what every large query returns, and the server - suppresses it entirely when the body asked for no hits. + field and passes through untouched. + + A request that asks for hits and matches more than + ``track_total_hits`` documents does carry a ``partial`` reason for + that capped total, so a large query normally has one - it is not a + sign the aggregations are truncated. The server omits it when the + body asked for no documents, where a hit counter says nothing about + the answer. Read ``hits.total.relation`` to tell the two apart. """ logger.info('Running a raw metadata search') return await self._single( diff --git a/src/polyswarm_api/api.py b/src/polyswarm_api/api.py index 4d19257a..ba934d59 100644 --- a/src/polyswarm_api/api.py +++ b/src/polyswarm_api/api.py @@ -327,12 +327,17 @@ def metadata_raw_search(self, body, community=None): complete. ``partial`` speaks for the aggregations and the search as a whole. - Whether a hit *count* is exact is a different question, answered by + Whether a hit *count* is exact is a separate question, answered by ``hits.total.relation``: ``eq`` is exact, ``gte`` means the engine stopped counting at ``track_total_hits``. That is the engine's own - field and passes through untouched. A capped total is not reported - as partial - it is what every large query returns, and the server - suppresses it entirely when the body asked for no hits. + field and passes through untouched. + + A request that asks for hits and matches more than + ``track_total_hits`` documents does carry a ``partial`` reason for + that capped total, so a large query normally has one - it is not a + sign the aggregations are truncated. The server omits it when the + body asked for no documents, where a hit counter says nothing about + the answer. Read ``hits.total.relation`` to tell the two apart. """ logger.info("Running a raw metadata search") return self._single(resources.MetadataRawSearch.search(self, body, community)) From ba3d08d899b43de32ed752b608186b94cbb74379 Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 16 Oct 2026 21:02:17 -0300 Subject: [PATCH 10/18] docs(api): describe partial as a list of codes, not a flag The two paragraphs contradicted each other: one said partial speaks for the search as a whole, the next that a large query normally carries one. Both are true of different reasons, which is the point - so the docstring now names the closed set of codes and says to branch on them. The reasons are prose no program can discriminate, which the server fixed by giving each a code. Also drops a stray blank line before _raw_hit_count. --- src/polyswarm_api/aio/api.py | 37 ++++++++++++++++++-------------- src/polyswarm_api/api.py | 37 ++++++++++++++++++-------------- test/client_scan_test.py | 1 - test/metadata_raw_search_test.py | 7 ++++-- 4 files changed, 47 insertions(+), 35 deletions(-) diff --git a/src/polyswarm_api/aio/api.py b/src/polyswarm_api/aio/api.py index 348d187c..8e1dde5c 100644 --- a/src/polyswarm_api/aio/api.py +++ b/src/polyswarm_api/aio/api.py @@ -307,22 +307,27 @@ async def metadata_raw_search(self, body, community=None): by setting it. :param community: Community to search; defaults to the client's. :return: A ``MetadataRawSearch`` whose ``json`` carries the engine's - ``hits`` and ``aggregations``, plus ``partial`` - a list of - reasons - when the server could not establish that the answer is - complete. - - ``partial`` speaks for the aggregations and the search as a whole. - Whether a hit *count* is exact is a separate question, answered by - ``hits.total.relation``: ``eq`` is exact, ``gte`` means the engine - stopped counting at ``track_total_hits``. That is the engine's own - field and passes through untouched. - - A request that asks for hits and matches more than - ``track_total_hits`` documents does carry a ``partial`` reason for - that capped total, so a large query normally has one - it is not a - sign the aggregations are truncated. The server omits it when the - body asked for no documents, where a hit counter says nothing about - the answer. Read ``hits.total.relation`` to tell the two apart. + ``hits`` and ``aggregations``, plus ``partial`` when the server + could not establish that the answer is complete. + + ``partial`` is a **list to inspect, not a flag**. Its entries are + ``{'code': ..., 'message': ...}`` and they make different claims, + so branch on ``code`` - ``message`` is for a human and may be + reworded. The codes are a closed set: + + * ``timed_out`` / ``terminated_early`` - the search stopped early. + * ``shards_failed`` - a shard was lost; also carries ``failed``. + * ``buckets_clamped`` - a ``terms`` list dropped terms. + * ``composite_paged`` - a ``composite`` may have more pages; page + it with its ``after_key``. + * ``bucket_count_approximate`` - a ``terms`` count is approximate + rather than truncated. + * ``hit_total_capped`` - the hit counter stopped at + ``track_total_hits``. This is what *every* large query that asks + for hits returns, so on its own it says nothing about the + aggregations; the server omits it when the body asked for no + documents. ``hits.total.relation`` carries the same fact as the + engine's own field, ``eq`` exact and ``gte`` capped. """ logger.info('Running a raw metadata search') return await self._single( diff --git a/src/polyswarm_api/api.py b/src/polyswarm_api/api.py index ba934d59..69dde0ef 100644 --- a/src/polyswarm_api/api.py +++ b/src/polyswarm_api/api.py @@ -322,22 +322,27 @@ def metadata_raw_search(self, body, community=None): by setting it. :param community: Community to search; defaults to the client's. :return: A ``MetadataRawSearch`` whose ``json`` carries the engine's - ``hits`` and ``aggregations``, plus ``partial`` - a list of - reasons - when the server could not establish that the answer is - complete. - - ``partial`` speaks for the aggregations and the search as a whole. - Whether a hit *count* is exact is a separate question, answered by - ``hits.total.relation``: ``eq`` is exact, ``gte`` means the engine - stopped counting at ``track_total_hits``. That is the engine's own - field and passes through untouched. - - A request that asks for hits and matches more than - ``track_total_hits`` documents does carry a ``partial`` reason for - that capped total, so a large query normally has one - it is not a - sign the aggregations are truncated. The server omits it when the - body asked for no documents, where a hit counter says nothing about - the answer. Read ``hits.total.relation`` to tell the two apart. + ``hits`` and ``aggregations``, plus ``partial`` when the server + could not establish that the answer is complete. + + ``partial`` is a **list to inspect, not a flag**. Its entries are + ``{'code': ..., 'message': ...}`` and they make different claims, + so branch on ``code`` - ``message`` is for a human and may be + reworded. The codes are a closed set: + + * ``timed_out`` / ``terminated_early`` - the search stopped early. + * ``shards_failed`` - a shard was lost; also carries ``failed``. + * ``buckets_clamped`` - a ``terms`` list dropped terms. + * ``composite_paged`` - a ``composite`` may have more pages; page + it with its ``after_key``. + * ``bucket_count_approximate`` - a ``terms`` count is approximate + rather than truncated. + * ``hit_total_capped`` - the hit counter stopped at + ``track_total_hits``. This is what *every* large query that asks + for hits returns, so on its own it says nothing about the + aggregations; the server omits it when the body asked for no + documents. ``hits.total.relation`` carries the same fact as the + engine's own field, ``eq`` exact and ``gte`` capped. """ logger.info("Running a raw metadata search") return self._single(resources.MetadataRawSearch.search(self, body, community)) diff --git a/test/client_scan_test.py b/test/client_scan_test.py index 57e4737e..78f63790 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -69,7 +69,6 @@ def rescan_and_scan(api, sha): vcr.use_cassette = lambda *_a, **_k: (lambda _fn: _fn) - def _raw_hit_count(api, body): """A reader for ``poll_equals`` that tells a missing route from a missing document. diff --git a/test/metadata_raw_search_test.py b/test/metadata_raw_search_test.py index 2c5a7c4c..d81a06ac 100644 --- a/test/metadata_raw_search_test.py +++ b/test/metadata_raw_search_test.py @@ -97,8 +97,11 @@ def test_a_partial_answer_keeps_its_reasons(self): # A truncated answer is still a 200, so this list is the only thing # separating it from a complete one. Dropping it in the parse layer # would turn "we could not establish this is complete" into a number. - reasons = ['the search stopped at terminate_after'] - assert _parsed({'hits': _EMPTY_HITS, 'partial': reasons}).json['partial'] == reasons + reasons = [{'code': 'terminated_early', + 'message': 'the search stopped at terminate_after'}] + parsed = _parsed({'hits': _EMPTY_HITS, 'partial': reasons}) + assert parsed.json['partial'] == reasons + assert parsed.json['partial'][0]['code'] == 'terminated_early' def test_a_complete_answer_carries_no_marker(self): assert 'partial' not in _parsed({'hits': _EMPTY_HITS}).json From 33cd40f0d47094823624f12e0c3409ff063cc9ab Mon Sep 17 00:00:00 2001 From: Samuel Date: Mon, 19 Oct 2026 12:51:20 -0300 Subject: [PATCH 11/18] docs(specs): list the raw-search test module in the testing spec AGENTS.md asks for the spec in the same PR as the code, and the Files list in specs/04-testing.md names every test module. specs/02 and 03 were updated for this endpoint; 04 was missed. --- specs/04-testing.md | 1 + 1 file changed, 1 insertion(+) diff --git a/specs/04-testing.md b/specs/04-testing.md index be17cc90..5471b626 100644 --- a/specs/04-testing.md +++ b/specs/04-testing.md @@ -26,6 +26,7 @@ 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/hunt_matched_strings_test.py` — pure-unit tests for the three-state `matched_strings` contract and its `matched_strings_dropped` sibling, across all four hunt-result classes. The endpoint behaviour they cannot see (that the detail route actually emits the keys and the list route sends `null`) is pinned live in `client_scan_test.py::test_live` / `async_client_test.py::test_async_live` — the e2e-first + pure-unit pairing invariant 1 asks for. - `test/known_good_test.py` — pure-unit tests for the known-good resource fields. +- `test/metadata_raw_search_test.py` — pure-unit request-shape and parse tests for `metadata_raw_search`: that the query body is sent verbatim and `community` rides the query string (invariant 7 — the builder is pure data, so the routing reads straight off the `PolyswarmRequest`'s `request_parameters`, with no httpx), plus the `partial` reason codes the server returns. The lifecycle is live, in `client_scan_test.py` / `async_client_test.py`. - `test/jmespath_test.py` — unit tests for `BaseJsonResource.jmespath`. - `test/vcr/*.vcr` — recorded cassettes. - `test/malicious` — fixture file for upload tests (`test/eicar.yara` was retired when the rules tests moved to per-test `uid_yara` bodies). From 2967de4ea049a265e8eba6b95645f804e1df616e Mon Sep 17 00:00:00 2001 From: Claude Code Date: Tue, 20 Oct 2026 17:46:46 -0300 Subject: [PATCH 12/18] feat: historical_create takes window_days A historical hunt can now be bounded to the last 30, 60, 90 or 180 days. The new optional keyword goes into the POST body as an int; None drops the key, so a bare call sends what it always did and the server applies its own default (the platform maximum). The SDK does not validate the value: the server owns the enum and answers anything else with a 400, which surfaces as the usual typed exception. The sync mirror is regenerated from the async source. Co-Authored-By: Alan Justino --- specs/03-endpoints.md | 2 +- src/polyswarm_api/aio/api.py | 7 +++++-- src/polyswarm_api/api.py | 5 ++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/specs/03-endpoints.md b/specs/03-endpoints.md index e46353b5..225f6bd0 100644 --- a/specs/03-endpoints.md +++ b/specs/03-endpoints.md @@ -72,7 +72,7 @@ Internal-only CRUD for the `/known-good` binary resource (distinct from the IOC | Method | Resource builder | |---|---| -| `historical_create(rule=None, ruleset_name=None)` | `HistoricalHunt.create` | +| `historical_create(rule=None, ruleset_name=None, window_days=None)` | `HistoricalHunt.create` — `window_days` (4.8.0) is how many days back the hunt reaches; the server accepts 30, 60, 90 or 180 and answers anything else with a 400, which surfaces as the usual typed exception (the SDK does not validate it). It is an int and rides the JSON body. `None` leaves the key out, the server keeps its default and the hunt reaches the platform maximum, so a bare call sends exactly what it always did. | | `historical_get(hunt=None)` | `HistoricalHunt.get` | | `historical_update(hunt)` | `HistoricalHunt.update` | | `historical_delete(hunt)` | `HistoricalHunt.delete` | diff --git a/src/polyswarm_api/aio/api.py b/src/polyswarm_api/aio/api.py index 8e1dde5c..37b91990 100644 --- a/src/polyswarm_api/aio/api.py +++ b/src/polyswarm_api/aio/api.py @@ -642,18 +642,21 @@ async def live_result(self, result_id): """ return await self._single(resources.LiveHuntResult.get(self, id=result_id)) - async def historical_create(self, rule=None, ruleset_name=None): + async def historical_create(self, rule=None, ruleset_name=None, window_days=None): """ Run a new historical hunt. :param rule: YaraRuleset object or string containing YARA rules to install :param ruleset_name: Name of the ruleset. + :param window_days: Days back the hunt reaches: 30, 60, 90 or 180. Omit it for the + platform maximum. The server validates the value; the SDK does not. :return: The created Hunt resource """ logger.info('Create historical hunt %s', rule) rule, rule_id = self._parse_rule(rule) return await self._single(resources.HistoricalHunt.create(self, yara=rule.yara if rule else None, rule_id=rule_id, - ruleset_name=ruleset_name, community=self.community)) + ruleset_name=ruleset_name, window_days=window_days, + community=self.community)) async def historical_get(self, hunt=None): """ diff --git a/src/polyswarm_api/api.py b/src/polyswarm_api/api.py index 69dde0ef..4b751f8e 100644 --- a/src/polyswarm_api/api.py +++ b/src/polyswarm_api/api.py @@ -735,12 +735,14 @@ def live_result(self, result_id): """ return self._single(resources.LiveHuntResult.get(self, id=result_id)) - def historical_create(self, rule=None, ruleset_name=None): + def historical_create(self, rule=None, ruleset_name=None, window_days=None): """ Run a new historical hunt. :param rule: YaraRuleset object or string containing YARA rules to install :param ruleset_name: Name of the ruleset. + :param window_days: Days back the hunt reaches: 30, 60, 90 or 180. Omit it for the + platform maximum. The server validates the value; the SDK does not. :return: The created Hunt resource """ logger.info("Create historical hunt %s", rule) @@ -751,6 +753,7 @@ def historical_create(self, rule=None, ruleset_name=None): yara=rule.yara if rule else None, rule_id=rule_id, ruleset_name=ruleset_name, + window_days=window_days, community=self.community, ) ) From 6e7ed294e24ad04732150ca20cfdb15a7dc2e8fe Mon Sep 17 00:00:00 2001 From: Claude Code Date: Tue, 20 Oct 2026 17:46:59 -0300 Subject: [PATCH 13/18] feat: HistoricalHunt parses window_days The server now returns the hunt's time window on the single GET and on the list. It is an additive .get() like the provenance fields: an int for a hunt created with a window, None for one created without (it reaches the platform maximum) or for a server that predates the field. HistoricalHuntList inherits the parse, so both endpoints are covered. Co-Authored-By: Alan Justino --- specs/02-resources.md | 2 +- src/polyswarm_api/resources.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/specs/02-resources.md b/specs/02-resources.md index 06e30c50..97451b6a 100644 --- a/specs/02-resources.md +++ b/specs/02-resources.md @@ -389,7 +389,7 @@ 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. `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`, and the stored `new_results_count` with its staleness marker `new_results_counted_at`: the server refreshes the counter on a schedule and the marker says when, so `None` means "not yet refreshed / no live hunt", never 0), 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`. +- `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`, and the stored `new_results_count` with its staleness marker `new_results_counted_at`: the server refreshes the counter on a schedule and the marker says when, so `None` means "not yet refreshed / no live hunt", never 0), and `HistoricalHunt` the source-rule provenance (`rule_id`, `rule_modified`, and the tri-state `source_rule_changed` — `None` is "unknown", never "unchanged") and its time window `window_days` (4.8.0: the int the hunt was created with, on the single GET and the list alike; `None` means no window was requested, so the hunt reaches the platform maximum). All additive `.get()` parses; an older server leaves them `None`. - `YaraRulesetFavorite` — `RESOURCE_ENDPOINT = '/hunt/rule/favorite'`, **`RESOURCE_ID_KEYS = ['community']`**: a deliberate deviation from the default `['id']`, splitting the request the way the server reads it — the toggle reads BOTH `id` and `favorite` from the PUT **body** (the default would move `id` into the query string and the server would 400 with "A valid rule id must be provided"), while `community` rides the **query string** to match where the ruleset GET/list calls send it. (The server's community middleware accepts the value from either the query or the body — but never both at once — so the placement here is about consistency and the id-400, not about a silently-ignored body value.) `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` and both transports of `test/ruleset_favorite_respx_test.py`. - `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. diff --git a/src/polyswarm_api/resources.py b/src/polyswarm_api/resources.py index 74cfe898..4732d07b 100644 --- a/src/polyswarm_api/resources.py +++ b/src/polyswarm_api/resources.py @@ -872,6 +872,8 @@ def __init__(self, content, api=None): # unknown (no source rule, or nothing to compare against) — not # "unchanged". self.source_rule_changed = content.get('source_rule_changed') + # None: no window requested (platform maximum), or a server predating the field. + self.window_days = content.get('window_days') class HistoricalHuntList(HistoricalHunt): From 530c27e92a7a18c99d225b7a8b9c7ee0af7d61e0 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Tue, 20 Oct 2026 17:49:21 -0300 Subject: [PATCH 14/18] test: pin window_days on the builder, both clients and the parse Pure-unit tests cover the request shape (an int in the body, the key absent for None, no client-side validation) and the parse (int, explicit null, missing key). One respx body per client pair checks what historical_create sends and what the create, get and list responses carry; VCR cannot, because it does not match bodies and the recorded hunts predate the field. The live round trip needs a fresh e2e stack, so it is recorded as an open question rather than faked into the existing cassettes. Co-Authored-By: Alan Justino --- specs/04-testing.md | 1 + specs/99-open-questions.md | 18 +++++ test/historical_window_test.py | 125 +++++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+) create mode 100644 test/historical_window_test.py diff --git a/specs/04-testing.md b/specs/04-testing.md index 5471b626..5e9a884b 100644 --- a/specs/04-testing.md +++ b/specs/04-testing.md @@ -33,6 +33,7 @@ How the test suite is organised. Three layers: pure unit tests (no HTTP at all - `test/hunt_tracking_builder_test.py` — pure-unit request-shape and parse tests for the hunt-page tracking builders/resources. - `test/refang_test.py` — pure-unit tests for `polyswarm_api.refang` (driven by the shared case table `test/fixtures/refang_cases.json`, kept byte-identical with the other clients that implement the same contract) and request-shape tests for every client method that refangs its IoC inputs, captured at the `_paginate` / `_single` boundary for both transports. Pure-unit because this is client-side input normalization: the server contract is unchanged, so there is no new endpoint behaviour for a cassette to pin. - `test/ioc_search_test.py` — `search_by_ioc(with_artifacts=)`: pure-unit request shape (the int flag, the unchanged default and bare call), pass-through on both client methods, and a dual-transport (`ClientTestCase`) respx parse of an artifact row — a stand-in until the e2e stack serves the parameter (see `99-open-questions.md`). +- `test/historical_window_test.py` — `historical_create(window_days=)` and `HistoricalHunt.window_days`: pure-unit request shape (the int in the body, the key absent for `None`, no client-side validation) and parse (int, explicit null, missing key — on `HistoricalHunt` and `HistoricalHuntList`), plus dual-transport (`ClientTestCase`) respx cases for the body each client sends and for the window on the create response, the single GET and the list. Respx because VCR does not match bodies and the recorded hunts predate the field; the live round trip is pending (see `99-open-questions.md`). - `test/ruleset_favorite_respx_test.py` — dual-transport (`ClientTestCase`) respx suite for the favorite toggle: the `FAVORITE_LIMIT` refusal envelope and the query/body split. - `test/sandbox_file_qrcode_respx_test.py` — dual-transport (`ClientTestCase`) respx suite pinning that a QR-code `sandbox_file` submission uploads the image's bytes (the S3 PUT body) under its unrefanged basename. Respx rather than e2e because the stack's sandbox providers cannot reasonably process a QR image. diff --git a/specs/99-open-questions.md b/specs/99-open-questions.md index 5d4dfc1a..5fce1ccf 100644 --- a/specs/99-open-questions.md +++ b/specs/99-open-questions.md @@ -226,3 +226,21 @@ invariant 1 exists to close. the live `test_search_by_ioc` pair, record both cassettes against a fresh stack, and delete the respx `IocSearchWithArtifactsTestCase` along with this entry. The builder and pass-through tests in that module stay: they pin request shape, which a cassette cannot. + +## `window_days` on historical hunts is not pinned against a live server + +**Status:** gap, pending a recording against a fresh e2e stack. + +The window is asserted only at the SDK boundary (`test/historical_window_test.py`): the body +each client sends, and the parse of hand-built envelopes. The recorded `test_historical` / +`test_list_historical` cassettes (sync and async) predate the field, so their responses carry +no `window_days`, and VCR does not match request bodies — replaying them proves neither that +the server accepts the key nor that it echoes it back. That is the "asserts what we *think* +the server returns" gap invariant 1 exists to close. + +**Action:** against a fresh e2e stack, record a lifecycle exchange with a window — create with +`window_days`, then get and list it, asserting the value round-trips (and that a hunt created +without one reads back `None`) — in the live pair, on both transports. Re-record by deleting +the cassette, never by editing it. Then delete the respx `HistoricalWindowResponseTestCase` +along with this entry. The builder, pass-through and parse tests stay: a cassette cannot see +the request body. diff --git a/test/historical_window_test.py b/test/historical_window_test.py new file mode 100644 index 00000000..555ab554 --- /dev/null +++ b/test/historical_window_test.py @@ -0,0 +1,125 @@ +"""``window_days`` on historical hunts: request shape, pass-through on both clients, parse. + +Builder and parse cases are pure-unit. The pass-through cases are respx because VCR does +not match bodies and the recorded cassettes predate the field, so only a mocked boundary +can see what the body carried. The live round trip is pending (specs/99-open-questions.md). +""" +import pytest + +from polyswarm_api import resources +from test._client_harness import BASE_URL, COMMUNITY, ClientTestCase + +_YARA = 'rule window_probe { condition: true }' +_HUNT_URL = f'{BASE_URL}/hunt/historical' + + +class _FakeApi: + uri = 'https://api.example.test' + community = 'gamma' + + +def _hunt(**extra): + """A hunt row as the server returns it, without the optional fields.""" + return { + 'id': '7', 'created': '2026-10-01T12:00:00+00:00', 'status': 'PENDING', + 'progress': 0.0, 'results_csv_uri': None, **extra} + + +def _create(**kwargs): + return resources.HistoricalHunt.create(_FakeApi(), community='gamma', **kwargs) + + +class TestHistoricalCreateBuilder: + def test_window_rides_the_body_as_an_int(self): + req = _create(yara=_YARA, window_days=30) + assert req.method == 'POST' + assert req.url == f'{_FakeApi.uri}/hunt/historical' + assert req.input_json == {'yara': _YARA, 'window_days': 30, 'community': 'gamma'} + assert req.params is None + # A JSON number, not the string "30". + assert type(req.input_json['window_days']) is int + assert req.result_parser is resources.HistoricalHunt + + def test_no_window_leaves_the_key_out(self): + req = _create(yara=_YARA, window_days=None) + assert req.input_json == {'yara': _YARA, 'community': 'gamma'} + + def test_none_and_omitted_build_the_same_request(self): + omitted, explicit = _create(yara=_YARA), _create(yara=_YARA, window_days=None) + assert (explicit.method, explicit.url, explicit.params, explicit.input_json) == ( + omitted.method, omitted.url, omitted.params, omitted.input_json) + + def test_window_stays_an_int_beside_a_stored_rule_id(self): + # core._params stringifies *_id keys only; the window is not one. + req = _create(rule_id=5, window_days=90) + assert req.input_json == {'rule_id': '5', 'window_days': 90, 'community': 'gamma'} + + def test_the_sdk_does_not_validate_the_value(self): + # The server owns the enum and answers anything else with a 400. + assert _create(yara=_YARA, window_days=45).input_json['window_days'] == 45 + + +class HistoricalCreateWindowTestCase(ClientTestCase): + """What ``historical_create`` puts on the wire, one body over both clients.""" + + def setUp(self): + super().setUp() + self.mock.add('POST', _HUNT_URL, json={'status': 'OK', 'result': _hunt()}) + + def test_window_reaches_the_body_as_an_int(self): + self.api.historical_create(_YARA, window_days=30) + assert self.mock.last_request_body == { + 'yara': _YARA, 'window_days': 30, 'community': COMMUNITY} + + def test_window_composes_with_a_stored_rule(self): + self.api.historical_create(5, window_days=90) + assert self.mock.last_request_body == { + 'rule_id': '5', 'window_days': 90, 'community': COMMUNITY} + + def test_no_window_sends_no_key(self): + self.api.historical_create(_YARA) + assert self.mock.last_request_body == {'yara': _YARA, 'community': COMMUNITY} + + def test_explicit_none_is_byte_identical_to_a_bare_call(self): + self.api.historical_create(_YARA) + self.api.historical_create(_YARA, window_days=None) + bare, explicit = self.mock.requests + assert explicit.content == bare.content + + +class TestHistoricalHuntWindowParse: + @pytest.mark.parametrize('cls', [resources.HistoricalHunt, resources.HistoricalHuntList]) + def test_the_window_parses_as_the_int(self, cls): + assert cls(_hunt(window_days=30), api=None).window_days == 30 + + @pytest.mark.parametrize('cls', [resources.HistoricalHunt, resources.HistoricalHuntList]) + def test_an_explicit_null_is_none(self, cls): + assert cls(_hunt(window_days=None), api=None).window_days is None + + @pytest.mark.parametrize('cls', [resources.HistoricalHunt, resources.HistoricalHuntList]) + def test_a_missing_key_is_none(self, cls): + # A server predating the field omits the key altogether. + assert 'window_days' not in _hunt() + assert cls(_hunt(), api=None).window_days is None + + +class HistoricalWindowResponseTestCase(ClientTestCase): + """The window on the create response, the single GET and the list, through the full + pipeline. A fabricated envelope, standing in until a live round trip is recorded. + """ + + def test_create_returns_the_window(self): + self.mock.add('POST', _HUNT_URL, json={'status': 'OK', 'result': _hunt(window_days=30)}) + assert self.api.historical_create(_YARA, window_days=30).window_days == 30 + + def test_get_returns_the_window(self): + self.mock.add('GET', f'{_HUNT_URL}?id=7&community={COMMUNITY}', + json={'status': 'OK', 'result': _hunt(window_days=60)}) + assert self.api.historical_get(7).window_days == 60 + + def test_list_returns_the_window_on_every_hunt(self): + rows = [_hunt(id='7', window_days=30), _hunt(id='8', window_days=None)] + self.mock.add('GET', f'{_HUNT_URL}/list?community={COMMUNITY}', json={ + 'status': 'OK', 'result': rows, 'has_more': False, 'limit': 50, 'offset': None}) + hunts = list(self.api.historical_list()) + assert [(h.id, h.window_days) for h in hunts] == [('7', 30), ('8', None)] From 367d6b3efec5a7a968dda62dbfda0ccc6f1fca79 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Tue, 20 Oct 2026 17:49:45 -0300 Subject: [PATCH 15/18] feat: release 4.8.0, the floor the CLI now pins The CLI client adopts `historical_create(window_days=)` in the paired change set and expresses that as `polyswarm_api>=4.8.0`. develop already declares 4.7.0 for a surface that does not include this keyword, so the floor needs the next version up; a floor cannot name a version this repo has not declared, so the bump lands here, in the feature PR (AGENTS.md's standing exception). Minor: one new optional keyword whose default preserves today's request, plus an additive resource field, so no existing call changes behaviour. Bumped with bump-my-version; the emitted string is a clean `4.8.0`. The server already accepts and returns `window_days`, so this repo can release ahead of the CLI. Co-Authored-By: Alan Justino --- pyproject.toml | 4 ++-- src/polyswarm_api/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 142d64bc..0c910191 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "polyswarm_api" -version = "4.7.0" +version = "4.8.0" description = "Client library to simplify interacting with the PolySwarm consumer API" readme = "README.md" requires-python = ">=3.10,<4" @@ -55,7 +55,7 @@ package-dir = { "" = "src" } where = ["src"] [tool.bumpversion] -current_version = "4.7.0" +current_version = "4.8.0" commit = true tag = false sign_tags = true diff --git a/src/polyswarm_api/__init__.py b/src/polyswarm_api/__init__.py index 5cf1f295..6c1d0b88 100644 --- a/src/polyswarm_api/__init__.py +++ b/src/polyswarm_api/__init__.py @@ -1,5 +1,5 @@ # https://www.python.org/dev/peps/pep-0008/#module-level-dunder-names -__version__ = '4.7.0' +__version__ = '4.8.0' __release_url__ = 'https://api.github.com/repos/polyswarm/polyswarm-api/releases/latest' from . import api From 0de0d9bd651e3331008ac1980250a2f6cb48433e Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 21 Oct 2026 10:15:40 -0300 Subject: [PATCH 16/18] test: pin window_days on the server's own responses A live pair, sync and async, creates one hunt with a 30-day window and one without, then reads both back on create, get and list: 30 and None. The cassettes were recorded against the e2e stack, and the pair also passes with VCR off. It replaces the respx stand-in that asserted fabricated envelopes, and closes the open question that tracked the gap. The builder, pass-through and parse tests stay: a cassette cannot see request bodies. Co-Authored-By: Alan Justino --- specs/04-testing.md | 2 +- specs/99-open-questions.md | 18 - test/async_client_test.py | 18 + test/client_scan_test.py | 18 + test/historical_window_test.py | 26 +- .../vcr/test_async_historical_window_days.vcr | 326 ++++++++++++++++++ test/vcr/test_historical_window_days.vcr | 326 ++++++++++++++++++ 7 files changed, 691 insertions(+), 43 deletions(-) create mode 100644 test/vcr/test_async_historical_window_days.vcr create mode 100644 test/vcr/test_historical_window_days.vcr diff --git a/specs/04-testing.md b/specs/04-testing.md index 5e9a884b..5a84bd9d 100644 --- a/specs/04-testing.md +++ b/specs/04-testing.md @@ -33,7 +33,7 @@ How the test suite is organised. Three layers: pure unit tests (no HTTP at all - `test/hunt_tracking_builder_test.py` — pure-unit request-shape and parse tests for the hunt-page tracking builders/resources. - `test/refang_test.py` — pure-unit tests for `polyswarm_api.refang` (driven by the shared case table `test/fixtures/refang_cases.json`, kept byte-identical with the other clients that implement the same contract) and request-shape tests for every client method that refangs its IoC inputs, captured at the `_paginate` / `_single` boundary for both transports. Pure-unit because this is client-side input normalization: the server contract is unchanged, so there is no new endpoint behaviour for a cassette to pin. - `test/ioc_search_test.py` — `search_by_ioc(with_artifacts=)`: pure-unit request shape (the int flag, the unchanged default and bare call), pass-through on both client methods, and a dual-transport (`ClientTestCase`) respx parse of an artifact row — a stand-in until the e2e stack serves the parameter (see `99-open-questions.md`). -- `test/historical_window_test.py` — `historical_create(window_days=)` and `HistoricalHunt.window_days`: pure-unit request shape (the int in the body, the key absent for `None`, no client-side validation) and parse (int, explicit null, missing key — on `HistoricalHunt` and `HistoricalHuntList`), plus dual-transport (`ClientTestCase`) respx cases for the body each client sends and for the window on the create response, the single GET and the list. Respx because VCR does not match bodies and the recorded hunts predate the field; the live round trip is pending (see `99-open-questions.md`). +- `test/historical_window_test.py` — `historical_create(window_days=)` and `HistoricalHunt.window_days`: pure-unit request shape (the int in the body, the key absent for `None`, no client-side validation) and parse (int, explicit null, missing key — on `HistoricalHunt` and `HistoricalHuntList`), plus dual-transport (`ClientTestCase`) respx cases for the body each client sends — respx because VCR does not match bodies. What the server returns is pinned live by `test_historical_window_days` / `test_async_historical_window_days` (a hunt with a window and one without, read back on create, get and list). - `test/ruleset_favorite_respx_test.py` — dual-transport (`ClientTestCase`) respx suite for the favorite toggle: the `FAVORITE_LIMIT` refusal envelope and the query/body split. - `test/sandbox_file_qrcode_respx_test.py` — dual-transport (`ClientTestCase`) respx suite pinning that a QR-code `sandbox_file` submission uploads the image's bytes (the S3 PUT body) under its unrefanged basename. Respx rather than e2e because the stack's sandbox providers cannot reasonably process a QR image. diff --git a/specs/99-open-questions.md b/specs/99-open-questions.md index 5fce1ccf..5d4dfc1a 100644 --- a/specs/99-open-questions.md +++ b/specs/99-open-questions.md @@ -226,21 +226,3 @@ invariant 1 exists to close. the live `test_search_by_ioc` pair, record both cassettes against a fresh stack, and delete the respx `IocSearchWithArtifactsTestCase` along with this entry. The builder and pass-through tests in that module stay: they pin request shape, which a cassette cannot. - -## `window_days` on historical hunts is not pinned against a live server - -**Status:** gap, pending a recording against a fresh e2e stack. - -The window is asserted only at the SDK boundary (`test/historical_window_test.py`): the body -each client sends, and the parse of hand-built envelopes. The recorded `test_historical` / -`test_list_historical` cassettes (sync and async) predate the field, so their responses carry -no `window_days`, and VCR does not match request bodies — replaying them proves neither that -the server accepts the key nor that it echoes it back. That is the "asserts what we *think* -the server returns" gap invariant 1 exists to close. - -**Action:** against a fresh e2e stack, record a lifecycle exchange with a window — create with -`window_days`, then get and list it, asserting the value round-trips (and that a hunt created -without one reads back `None`) — in the live pair, on both transports. Re-record by deleting -the cassette, never by editing it. Then delete the respx `HistoricalWindowResponseTestCase` -along with this entry. The builder, pass-through and parse tests stay: a cassette cannot see -the request body. diff --git a/test/async_client_test.py b/test/async_client_test.py index ae0d6184..1be7113b 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -867,6 +867,24 @@ async def test_async_list_historical(self, uid): listed_ids = {h.id async for h in api.historical_list()} assert set(historical_ids) <= listed_ids + @vcr.use_cassette() + async def test_async_historical_window_days(self, uid): + async with self._api() as api: + yara_content = uid_yara(uid) + windowed = await api.historical_create(yara_content, window_days=30) + unbounded = await api.historical_create(yara_content) + try: + # The server echoes the window on create, get and list; None means none requested. + assert (windowed.window_days, unbounded.window_days) == (30, None) + assert (await api.historical_get(windowed.id)).window_days == 30 + assert (await api.historical_get(unbounded.id)).window_days is None + ids = {windowed.id, unbounded.id} + listed = {h.id: h.window_days async for h in api.historical_list() if h.id in ids} + assert listed == {windowed.id: 30, unbounded.id: None} + finally: + for hunt in (windowed, unbounded): + await api.historical_delete(hunt.id) + @vcr.use_cassette() async def test_async_historical_results(self, uid): async with self._api() as api: diff --git a/test/client_scan_test.py b/test/client_scan_test.py index 78f63790..567b00e2 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -628,6 +628,24 @@ def test_list_historical(self): listed_ids = {h.id for h in api.historical_list()} assert set(historical_ids) <= listed_ids + @vcr.use_cassette() + def test_historical_window_days(self): + api = PolyswarmAPI(self.test_api_key, uri=f'http://ai:9696/{self.api_version}', community='gamma') + yara_content = uid_yara(self._testMethodName) + windowed = api.historical_create(yara_content, window_days=30) + unbounded = api.historical_create(yara_content) + try: + # The server echoes the window on create, get and list; None means none requested. + assert (windowed.window_days, unbounded.window_days) == (30, None) + assert api.historical_get(windowed.id).window_days == 30 + assert api.historical_get(unbounded.id).window_days is None + ids = {windowed.id, unbounded.id} + listed = {h.id: h.window_days for h in api.historical_list() if h.id in ids} + assert listed == {windowed.id: 30, unbounded.id: None} + finally: + for hunt in (windowed, unbounded): + api.historical_delete(hunt.id) + @vcr.use_cassette() def test_historical_results(self): api = PolyswarmAPI(self.test_api_key, uri=f'http://ai:9696/{self.api_version}', community='gamma') diff --git a/test/historical_window_test.py b/test/historical_window_test.py index 555ab554..4d026fc2 100644 --- a/test/historical_window_test.py +++ b/test/historical_window_test.py @@ -1,8 +1,8 @@ """``window_days`` on historical hunts: request shape, pass-through on both clients, parse. Builder and parse cases are pure-unit. The pass-through cases are respx because VCR does -not match bodies and the recorded cassettes predate the field, so only a mocked boundary -can see what the body carried. The live round trip is pending (specs/99-open-questions.md). +not match bodies, so only a mocked boundary can see what the body carried. The server's +side is pinned live by ``test_historical_window_days`` and its async twin. """ import pytest @@ -101,25 +101,3 @@ def test_a_missing_key_is_none(self, cls): # A server predating the field omits the key altogether. assert 'window_days' not in _hunt() assert cls(_hunt(), api=None).window_days is None - - -class HistoricalWindowResponseTestCase(ClientTestCase): - """The window on the create response, the single GET and the list, through the full - pipeline. A fabricated envelope, standing in until a live round trip is recorded. - """ - - def test_create_returns_the_window(self): - self.mock.add('POST', _HUNT_URL, json={'status': 'OK', 'result': _hunt(window_days=30)}) - assert self.api.historical_create(_YARA, window_days=30).window_days == 30 - - def test_get_returns_the_window(self): - self.mock.add('GET', f'{_HUNT_URL}?id=7&community={COMMUNITY}', - json={'status': 'OK', 'result': _hunt(window_days=60)}) - assert self.api.historical_get(7).window_days == 60 - - def test_list_returns_the_window_on_every_hunt(self): - rows = [_hunt(id='7', window_days=30), _hunt(id='8', window_days=None)] - self.mock.add('GET', f'{_HUNT_URL}/list?community={COMMUNITY}', json={ - 'status': 'OK', 'result': rows, 'has_more': False, 'limit': 50, 'offset': None}) - hunts = list(self.api.historical_list()) - assert [(h.id, h.window_days) for h in hunts] == [('7', 30), ('8', None)] diff --git a/test/vcr/test_async_historical_window_days.vcr b/test/vcr/test_async_historical_window_days.vcr new file mode 100644 index 00000000..43fa12b4 --- /dev/null +++ b/test/vcr/test_async_historical_window_days.vcr @@ -0,0 +1,326 @@ +interactions: +- request: + body: '{"yara":"rule sdk_test_async_historical_window_days { strings: $u = \"test_async_historical_window_days\" + condition: $u }","window_days":30,"community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '160' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) + 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-10-21T13:14:47.679196+00:00","failed_max_retries":0,"failed_other":0,"id":"27363494633339458","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":30,"yara":"rule + sdk_test_async_historical_window_days { strings: $u = \"test_async_historical_window_days\" + condition: $u }"},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '570' + Content-Type: + - application/json + Date: + - Wed, 21 Oct 2026 13:14:47 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"yara":"rule sdk_test_async_historical_window_days { strings: $u = \"test_async_historical_window_days\" + condition: $u }","community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '143' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) + 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-10-21T13:14:47.724288+00:00","failed_max_retries":0,"failed_other":0,"id":"40075594027765663","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule + sdk_test_async_historical_window_days { strings: $u = \"test_async_historical_window_days\" + condition: $u }"},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '572' + Content-Type: + - application/json + Date: + - Wed, 21 Oct 2026 13:14:47 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.8.0 (x86_64-Linux-CPython-3.12.15) + method: GET + uri: http://ai:9696/v3/hunt/historical?id=27363494633339458&community=gamma + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-10-21T13:14:47.679196+00:00","failed_max_retries":0,"failed_other":0,"id":"27363494633339458","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":30,"yara":"rule + sdk_test_async_historical_window_days { strings: $u = \"test_async_historical_window_days\" + condition: $u }"},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '570' + Content-Type: + - application/json + Date: + - Wed, 21 Oct 2026 13:14:47 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.8.0 (x86_64-Linux-CPython-3.12.15) + method: GET + uri: http://ai:9696/v3/hunt/historical?id=40075594027765663&community=gamma + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-10-21T13:14:47.724288+00:00","failed_max_retries":0,"failed_other":0,"id":"40075594027765663","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule + sdk_test_async_historical_window_days { strings: $u = \"test_async_historical_window_days\" + condition: $u }"},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '572' + Content-Type: + - application/json + Date: + - Wed, 21 Oct 2026 13:14:47 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.8.0 (x86_64-Linux-CPython-3.12.15) + method: GET + uri: http://ai:9696/v3/hunt/historical/list?community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:47.724288+00:00","id":"40075594027765663","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:47.679196+00:00","id":"27363494633339458","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":30,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:47.606259+00:00","id":"4134553051212861","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:47.559119+00:00","id":"63790780453166614","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:47.513053+00:00","id":"83945889672772698","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:47.459810+00:00","id":"58040492053046630","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:47.411681+00:00","id":"17372717959168159","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:47.141062+00:00","id":"43783504355477803","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:46.809660+00:00","id":"6447048420148760","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:46.768119+00:00","id":"27886352331973438","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:46.723965+00:00","id":"2049602096428236","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:46.681280+00:00","id":"7126890832542907","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:46.629460+00:00","id":"59350603088096354","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null}],"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '4898' + Content-Type: + - application/json + Date: + - Wed, 21 Oct 2026 13:14:47 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.8.0 (x86_64-Linux-CPython-3.12.15) + method: DELETE + uri: http://ai:9696/v3/hunt/historical?id=27363494633339458 + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-10-21T13:14:47.679196+00:00","failed_max_retries":0,"failed_other":0,"id":"27363494633339458","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"DELETING","summary":null,"user_account_number":"111","window_days":30,"yara":"rule + sdk_test_async_historical_window_days { strings: $u = \"test_async_historical_window_days\" + condition: $u }"},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '571' + Content-Type: + - application/json + Date: + - Wed, 21 Oct 2026 13:14:47 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.8.0 (x86_64-Linux-CPython-3.12.15) + method: DELETE + uri: http://ai:9696/v3/hunt/historical?id=40075594027765663 + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-10-21T13:14:47.724288+00:00","failed_max_retries":0,"failed_other":0,"id":"40075594027765663","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"DELETING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule + sdk_test_async_historical_window_days { strings: $u = \"test_async_historical_window_days\" + condition: $u }"},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '573' + Content-Type: + - application/json + Date: + - Wed, 21 Oct 2026 13:14:47 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +version: 1 diff --git a/test/vcr/test_historical_window_days.vcr b/test/vcr/test_historical_window_days.vcr new file mode 100644 index 00000000..fe5ea610 --- /dev/null +++ b/test/vcr/test_historical_window_days.vcr @@ -0,0 +1,326 @@ +interactions: +- request: + body: '{"yara":"rule sdk_test_historical_window_days { strings: $u = \"test_historical_window_days\" + condition: $u }","window_days":30,"community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '148' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) + 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-10-21T13:14:46.882427+00:00","failed_max_retries":0,"failed_other":0,"id":"55340953576717196","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":30,"yara":"rule + sdk_test_historical_window_days { strings: $u = \"test_historical_window_days\" + condition: $u }"},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '558' + Content-Type: + - application/json + Date: + - Wed, 21 Oct 2026 13:14:46 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"yara":"rule sdk_test_historical_window_days { strings: $u = \"test_historical_window_days\" + condition: $u }","community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '131' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) + 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-10-21T13:14:46.928345+00:00","failed_max_retries":0,"failed_other":0,"id":"99256637874103471","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule + sdk_test_historical_window_days { strings: $u = \"test_historical_window_days\" + 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: + - Wed, 21 Oct 2026 13:14:46 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.8.0 (x86_64-Linux-CPython-3.12.15) + method: GET + uri: http://ai:9696/v3/hunt/historical?id=55340953576717196&community=gamma + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-10-21T13:14:46.882427+00:00","failed_max_retries":0,"failed_other":0,"id":"55340953576717196","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":30,"yara":"rule + sdk_test_historical_window_days { strings: $u = \"test_historical_window_days\" + condition: $u }"},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '558' + Content-Type: + - application/json + Date: + - Wed, 21 Oct 2026 13:14:46 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.8.0 (x86_64-Linux-CPython-3.12.15) + method: GET + uri: http://ai:9696/v3/hunt/historical?id=99256637874103471&community=gamma + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-10-21T13:14:46.928345+00:00","failed_max_retries":0,"failed_other":0,"id":"99256637874103471","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule + sdk_test_historical_window_days { strings: $u = \"test_historical_window_days\" + 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: + - Wed, 21 Oct 2026 13:14:46 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.8.0 (x86_64-Linux-CPython-3.12.15) + method: GET + uri: http://ai:9696/v3/hunt/historical/list?community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:46.928345+00:00","id":"99256637874103471","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:46.882427+00:00","id":"55340953576717196","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":30,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:46.809660+00:00","id":"6447048420148760","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:46.768119+00:00","id":"27886352331973438","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:46.723965+00:00","id":"2049602096428236","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:46.681280+00:00","id":"7126890832542907","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:46.629460+00:00","id":"59350603088096354","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null}],"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2661' + Content-Type: + - application/json + Date: + - Wed, 21 Oct 2026 13:14:47 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.8.0 (x86_64-Linux-CPython-3.12.15) + method: DELETE + uri: http://ai:9696/v3/hunt/historical?id=55340953576717196 + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-10-21T13:14:46.882427+00:00","failed_max_retries":0,"failed_other":0,"id":"55340953576717196","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"DELETING","summary":null,"user_account_number":"111","window_days":30,"yara":"rule + sdk_test_historical_window_days { strings: $u = \"test_historical_window_days\" + condition: $u }"},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '559' + Content-Type: + - application/json + Date: + - Wed, 21 Oct 2026 13:14:47 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.8.0 (x86_64-Linux-CPython-3.12.15) + method: DELETE + uri: http://ai:9696/v3/hunt/historical?id=99256637874103471 + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-10-21T13:14:46.928345+00:00","failed_max_retries":0,"failed_other":0,"id":"99256637874103471","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"DELETING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule + sdk_test_historical_window_days { strings: $u = \"test_historical_window_days\" + 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: + - Wed, 21 Oct 2026 13:14:47 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +version: 1 From 4c2554578d97aa23dd799439d3c972bb040e2ded Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 21 Oct 2026 10:15:40 -0300 Subject: [PATCH 17/18] test: re-record the historical hunt cassettes Their recorded create, get and list responses predated window_days. Re-recorded by deleting each cassette and running its test against the e2e stack, so replay serves the shape the server returns today, the key included. Co-Authored-By: Alan Justino --- test/vcr/test_async_historical.vcr | 28 ++++++------- test/vcr/test_async_historical_results.vcr | 14 +++---- test/vcr/test_async_list_historical.vcr | 48 +++++++++++----------- test/vcr/test_historical.vcr | 28 ++++++------- test/vcr/test_historical_results.vcr | 14 +++---- test/vcr/test_list_historical.vcr | 48 +++++++++++----------- 6 files changed, 90 insertions(+), 90 deletions(-) diff --git a/test/vcr/test_async_historical.vcr b/test/vcr/test_async_historical.vcr index 359fb21a..f5631529 100644 --- a/test/vcr/test_async_historical.vcr +++ b/test/vcr/test_async_historical.vcr @@ -18,12 +18,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) 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-06-03T22:37:20.403459+00:00","failed_max_retries":0,"failed_other":0,"id":"35934095384555821","progress":null,"results_csv_uri":null,"ruleset_name":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-10-21T13:14:47.216631+00:00","failed_max_retries":0,"failed_other":0,"id":"46202933697126034","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule sdk_test_async_historical { strings: $u = \"test_async_historical\" condition: $u }"},"status":"OK"} @@ -36,11 +36,11 @@ interactions: Connection: - keep-alive Content-Length: - - '466' + - '548' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:37:20 GMT + - Wed, 21 Oct 2026 13:14:47 GMT Server: - gunicorn X-Billing-ID: @@ -62,12 +62,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) method: GET - uri: http://ai:9696/v3/hunt/historical?id=35934095384555821&community=gamma + uri: http://ai:9696/v3/hunt/historical?id=46202933697126034&community=gamma response: body: - string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-06-03T22:37:20.403459+00:00","failed_max_retries":0,"failed_other":0,"id":"35934095384555821","progress":null,"results_csv_uri":null,"ruleset_name":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-10-21T13:14:47.216631+00:00","failed_max_retries":0,"failed_other":0,"id":"46202933697126034","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule sdk_test_async_historical { strings: $u = \"test_async_historical\" condition: $u }"},"status":"OK"} @@ -80,11 +80,11 @@ interactions: Connection: - keep-alive Content-Length: - - '466' + - '548' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:37:20 GMT + - Wed, 21 Oct 2026 13:14:47 GMT Server: - gunicorn X-Billing-ID: @@ -110,12 +110,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) method: DELETE - uri: http://ai:9696/v3/hunt/historical?id=35934095384555821 + uri: http://ai:9696/v3/hunt/historical?id=46202933697126034 response: body: - string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-06-03T22:37:20.403459+00:00","failed_max_retries":0,"failed_other":0,"id":"35934095384555821","progress":null,"results_csv_uri":null,"ruleset_name":null,"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-10-21T13:14:47.216631+00:00","failed_max_retries":0,"failed_other":0,"id":"46202933697126034","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"DELETING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule sdk_test_async_historical { strings: $u = \"test_async_historical\" condition: $u }"},"status":"OK"} @@ -128,11 +128,11 @@ interactions: Connection: - keep-alive Content-Length: - - '467' + - '549' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:37:20 GMT + - Wed, 21 Oct 2026 13:14:47 GMT Server: - gunicorn X-Billing-ID: diff --git a/test/vcr/test_async_historical_results.vcr b/test/vcr/test_async_historical_results.vcr index 33303359..af61265a 100644 --- a/test/vcr/test_async_historical_results.vcr +++ b/test/vcr/test_async_historical_results.vcr @@ -18,12 +18,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) 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-06-03T22:37:20.755351+00:00","failed_max_retries":0,"failed_other":0,"id":"51213972298513151","progress":null,"results_csv_uri":null,"ruleset_name":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-10-21T13:14:47.915116+00:00","failed_max_retries":0,"failed_other":0,"id":"88337888683381847","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule sdk_test_async_historical_results { strings: $u = \"test_async_historical_results\" condition: $u }"},"status":"OK"} @@ -36,11 +36,11 @@ interactions: Connection: - keep-alive Content-Length: - - '482' + - '564' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:37:20 GMT + - Wed, 21 Oct 2026 13:14:47 GMT Server: - gunicorn X-Billing-ID: @@ -62,9 +62,9 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) method: GET - uri: http://ai:9696/v3/hunt/historical/results/list?id=51213972298513151&community=gamma + uri: http://ai:9696/v3/hunt/historical/results/list?id=88337888683381847&community=gamma response: body: string: '' @@ -78,7 +78,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Wed, 03 Jun 2026 22:37:20 GMT + - Wed, 21 Oct 2026 13:14:47 GMT Server: - gunicorn status: diff --git a/test/vcr/test_async_list_historical.vcr b/test/vcr/test_async_list_historical.vcr index b05e4c0f..1a191f81 100644 --- a/test/vcr/test_async_list_historical.vcr +++ b/test/vcr/test_async_list_historical.vcr @@ -18,12 +18,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) 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-06-03T22:37:20.504860+00:00","failed_max_retries":0,"failed_other":0,"id":"8290930961482725","progress":null,"results_csv_uri":null,"ruleset_name":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-10-21T13:14:47.411681+00:00","failed_max_retries":0,"failed_other":0,"id":"17372717959168159","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule sdk_test_async_list_historical { strings: $u = \"test_async_list_historical\" condition: $u }"},"status":"OK"} @@ -36,11 +36,11 @@ interactions: Connection: - keep-alive Content-Length: - - '475' + - '558' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:37:20 GMT + - Wed, 21 Oct 2026 13:14:47 GMT Server: - gunicorn X-Billing-ID: @@ -67,12 +67,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) 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-06-03T22:37:20.550525+00:00","failed_max_retries":0,"failed_other":0,"id":"45949838125672986","progress":null,"results_csv_uri":null,"ruleset_name":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-10-21T13:14:47.459810+00:00","failed_max_retries":0,"failed_other":0,"id":"58040492053046630","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule sdk_test_async_list_historical { strings: $u = \"test_async_list_historical\" condition: $u }"},"status":"OK"} @@ -85,11 +85,11 @@ interactions: Connection: - keep-alive Content-Length: - - '476' + - '558' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:37:20 GMT + - Wed, 21 Oct 2026 13:14:47 GMT Server: - gunicorn X-Billing-ID: @@ -116,12 +116,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) 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-06-03T22:37:20.592366+00:00","failed_max_retries":0,"failed_other":0,"id":"15725209613415703","progress":null,"results_csv_uri":null,"ruleset_name":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-10-21T13:14:47.513053+00:00","failed_max_retries":0,"failed_other":0,"id":"83945889672772698","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule sdk_test_async_list_historical { strings: $u = \"test_async_list_historical\" condition: $u }"},"status":"OK"} @@ -134,11 +134,11 @@ interactions: Connection: - keep-alive Content-Length: - - '476' + - '558' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:37:20 GMT + - Wed, 21 Oct 2026 13:14:47 GMT Server: - gunicorn X-Billing-ID: @@ -165,12 +165,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) 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-06-03T22:37:20.656995+00:00","failed_max_retries":0,"failed_other":0,"id":"88826403627398312","progress":null,"results_csv_uri":null,"ruleset_name":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-10-21T13:14:47.559119+00:00","failed_max_retries":0,"failed_other":0,"id":"63790780453166614","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule sdk_test_async_list_historical { strings: $u = \"test_async_list_historical\" condition: $u }"},"status":"OK"} @@ -183,11 +183,11 @@ interactions: Connection: - keep-alive Content-Length: - - '476' + - '558' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:37:20 GMT + - Wed, 21 Oct 2026 13:14:47 GMT Server: - gunicorn X-Billing-ID: @@ -214,12 +214,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) 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-06-03T22:37:20.695183+00:00","failed_max_retries":0,"failed_other":0,"id":"13919779479626947","progress":null,"results_csv_uri":null,"ruleset_name":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-10-21T13:14:47.606259+00:00","failed_max_retries":0,"failed_other":0,"id":"4134553051212861","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule sdk_test_async_list_historical { strings: $u = \"test_async_list_historical\" condition: $u }"},"status":"OK"} @@ -232,11 +232,11 @@ interactions: Connection: - keep-alive Content-Length: - - '476' + - '557' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:37:20 GMT + - Wed, 21 Oct 2026 13:14:47 GMT Server: - gunicorn X-Billing-ID: @@ -258,12 +258,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) method: GET uri: http://ai:9696/v3/hunt/historical/list?community=gamma response: body: - string: '{"has_more":false,"limit":50,"result":[{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-06-03T22:37:20.695183+00:00","id":"13919779479626947","progress":null,"results_csv_uri":null,"ruleset_name":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-06-03T22:37:20.656995+00:00","id":"88826403627398312","progress":null,"results_csv_uri":null,"ruleset_name":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-06-03T22:37:20.592366+00:00","id":"15725209613415703","progress":null,"results_csv_uri":null,"ruleset_name":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-06-03T22:37:20.550525+00:00","id":"45949838125672986","progress":null,"results_csv_uri":null,"ruleset_name":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-06-03T22:37:20.504860+00:00","id":"8290930961482725","progress":null,"results_csv_uri":null,"ruleset_name":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-06-03T22:35:46.562853+00:00","id":"53359932363657862","progress":null,"results_csv_uri":null,"ruleset_name":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-06-03T22:35:46.521466+00:00","id":"24956296142984681","progress":null,"results_csv_uri":null,"ruleset_name":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-06-03T22:35:46.480348+00:00","id":"54272719094803031","progress":null,"results_csv_uri":null,"ruleset_name":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-06-03T22:35:46.441527+00:00","id":"58288022169534211","progress":null,"results_csv_uri":null,"ruleset_name":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-06-03T22:35:46.397319+00:00","id":"86221284475520679","progress":null,"results_csv_uri":null,"ruleset_name":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-06-03T22:35:46.255339+00:00","id":"46026864008511354","progress":null,"results_csv_uri":null,"ruleset_name":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-06-03T22:31:26.868688+00:00","id":"51603247373653891","progress":null,"results_csv_uri":null,"ruleset_name":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null}],"status":"OK"} + string: '{"has_more":false,"limit":50,"result":[{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:47.606259+00:00","id":"4134553051212861","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:47.559119+00:00","id":"63790780453166614","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:47.513053+00:00","id":"83945889672772698","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:47.459810+00:00","id":"58040492053046630","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:47.411681+00:00","id":"17372717959168159","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:47.141062+00:00","id":"43783504355477803","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:46.809660+00:00","id":"6447048420148760","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:46.768119+00:00","id":"27886352331973438","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:46.723965+00:00","id":"2049602096428236","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:46.681280+00:00","id":"7126890832542907","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:46.629460+00:00","id":"59350603088096354","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null}],"status":"OK"} ' headers: @@ -274,11 +274,11 @@ interactions: Connection: - keep-alive Content-Length: - - '3546' + - '4154' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:37:20 GMT + - Wed, 21 Oct 2026 13:14:47 GMT Server: - gunicorn X-Billing-ID: diff --git a/test/vcr/test_historical.vcr b/test/vcr/test_historical.vcr index 28a8b201..5cceb9a4 100644 --- a/test/vcr/test_historical.vcr +++ b/test/vcr/test_historical.vcr @@ -18,12 +18,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) 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-06-03T22:35:46.115323+00:00","failed_max_retries":0,"failed_other":0,"id":"29328811284340624","progress":null,"results_csv_uri":null,"ruleset_name":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-10-21T13:14:46.386019+00:00","failed_max_retries":0,"failed_other":0,"id":"93048973633363425","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule sdk_test_historical { strings: $u = \"test_historical\" condition: $u }"},"status":"OK"} ' @@ -35,11 +35,11 @@ interactions: Connection: - keep-alive Content-Length: - - '454' + - '536' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:35:46 GMT + - Wed, 21 Oct 2026 13:14:46 GMT Server: - gunicorn X-Billing-ID: @@ -61,12 +61,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) method: GET - uri: http://ai:9696/v3/hunt/historical?id=29328811284340624&community=gamma + uri: http://ai:9696/v3/hunt/historical?id=93048973633363425&community=gamma response: body: - string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-06-03T22:35:46.115323+00:00","failed_max_retries":0,"failed_other":0,"id":"29328811284340624","progress":null,"results_csv_uri":null,"ruleset_name":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-10-21T13:14:46.386019+00:00","failed_max_retries":0,"failed_other":0,"id":"93048973633363425","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule sdk_test_historical { strings: $u = \"test_historical\" condition: $u }"},"status":"OK"} ' @@ -78,11 +78,11 @@ interactions: Connection: - keep-alive Content-Length: - - '454' + - '536' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:35:46 GMT + - Wed, 21 Oct 2026 13:14:46 GMT Server: - gunicorn X-Billing-ID: @@ -108,12 +108,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) method: DELETE - uri: http://ai:9696/v3/hunt/historical?id=29328811284340624 + uri: http://ai:9696/v3/hunt/historical?id=93048973633363425 response: body: - string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-06-03T22:35:46.115323+00:00","failed_max_retries":0,"failed_other":0,"id":"29328811284340624","progress":null,"results_csv_uri":null,"ruleset_name":null,"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-10-21T13:14:46.386019+00:00","failed_max_retries":0,"failed_other":0,"id":"93048973633363425","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"DELETING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule sdk_test_historical { strings: $u = \"test_historical\" condition: $u }"},"status":"OK"} ' @@ -125,11 +125,11 @@ interactions: Connection: - keep-alive Content-Length: - - '455' + - '537' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:35:46 GMT + - Wed, 21 Oct 2026 13:14:46 GMT Server: - gunicorn X-Billing-ID: diff --git a/test/vcr/test_historical_results.vcr b/test/vcr/test_historical_results.vcr index 1756c2ff..1676b855 100644 --- a/test/vcr/test_historical_results.vcr +++ b/test/vcr/test_historical_results.vcr @@ -18,12 +18,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) 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-06-03T22:35:46.255339+00:00","failed_max_retries":0,"failed_other":0,"id":"46026864008511354","progress":null,"results_csv_uri":null,"ruleset_name":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-10-21T13:14:47.141062+00:00","failed_max_retries":0,"failed_other":0,"id":"43783504355477803","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule sdk_test_historical_results { strings: $u = \"test_historical_results\" condition: $u }"},"status":"OK"} @@ -36,11 +36,11 @@ interactions: Connection: - keep-alive Content-Length: - - '470' + - '552' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:35:46 GMT + - Wed, 21 Oct 2026 13:14:47 GMT Server: - gunicorn X-Billing-ID: @@ -62,9 +62,9 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) method: GET - uri: http://ai:9696/v3/hunt/historical/results/list?id=46026864008511354&community=gamma + uri: http://ai:9696/v3/hunt/historical/results/list?id=43783504355477803&community=gamma response: body: string: '' @@ -78,7 +78,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Wed, 03 Jun 2026 22:35:46 GMT + - Wed, 21 Oct 2026 13:14:47 GMT Server: - gunicorn status: diff --git a/test/vcr/test_list_historical.vcr b/test/vcr/test_list_historical.vcr index f735f4b5..a9ddc9f7 100644 --- a/test/vcr/test_list_historical.vcr +++ b/test/vcr/test_list_historical.vcr @@ -18,12 +18,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) 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-06-03T22:35:46.397319+00:00","failed_max_retries":0,"failed_other":0,"id":"86221284475520679","progress":null,"results_csv_uri":null,"ruleset_name":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-10-21T13:14:46.629460+00:00","failed_max_retries":0,"failed_other":0,"id":"59350603088096354","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule sdk_test_list_historical { strings: $u = \"test_list_historical\" condition: $u }"},"status":"OK"} @@ -36,11 +36,11 @@ interactions: Connection: - keep-alive Content-Length: - - '464' + - '546' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:35:46 GMT + - Wed, 21 Oct 2026 13:14:46 GMT Server: - gunicorn X-Billing-ID: @@ -67,12 +67,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) 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-06-03T22:35:46.441527+00:00","failed_max_retries":0,"failed_other":0,"id":"58288022169534211","progress":null,"results_csv_uri":null,"ruleset_name":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-10-21T13:14:46.681280+00:00","failed_max_retries":0,"failed_other":0,"id":"7126890832542907","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule sdk_test_list_historical { strings: $u = \"test_list_historical\" condition: $u }"},"status":"OK"} @@ -85,11 +85,11 @@ interactions: Connection: - keep-alive Content-Length: - - '464' + - '545' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:35:46 GMT + - Wed, 21 Oct 2026 13:14:46 GMT Server: - gunicorn X-Billing-ID: @@ -116,12 +116,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) 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-06-03T22:35:46.480348+00:00","failed_max_retries":0,"failed_other":0,"id":"54272719094803031","progress":null,"results_csv_uri":null,"ruleset_name":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-10-21T13:14:46.723965+00:00","failed_max_retries":0,"failed_other":0,"id":"2049602096428236","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule sdk_test_list_historical { strings: $u = \"test_list_historical\" condition: $u }"},"status":"OK"} @@ -134,11 +134,11 @@ interactions: Connection: - keep-alive Content-Length: - - '464' + - '545' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:35:46 GMT + - Wed, 21 Oct 2026 13:14:46 GMT Server: - gunicorn X-Billing-ID: @@ -165,12 +165,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) 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-06-03T22:35:46.521466+00:00","failed_max_retries":0,"failed_other":0,"id":"24956296142984681","progress":null,"results_csv_uri":null,"ruleset_name":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-10-21T13:14:46.768119+00:00","failed_max_retries":0,"failed_other":0,"id":"27886352331973438","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule sdk_test_list_historical { strings: $u = \"test_list_historical\" condition: $u }"},"status":"OK"} @@ -183,11 +183,11 @@ interactions: Connection: - keep-alive Content-Length: - - '464' + - '546' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:35:46 GMT + - Wed, 21 Oct 2026 13:14:46 GMT Server: - gunicorn X-Billing-ID: @@ -214,12 +214,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) 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-06-03T22:35:46.562853+00:00","failed_max_retries":0,"failed_other":0,"id":"53359932363657862","progress":null,"results_csv_uri":null,"ruleset_name":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-10-21T13:14:46.809660+00:00","failed_max_retries":0,"failed_other":0,"id":"6447048420148760","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":"rule sdk_test_list_historical { strings: $u = \"test_list_historical\" condition: $u }"},"status":"OK"} @@ -232,11 +232,11 @@ interactions: Connection: - keep-alive Content-Length: - - '464' + - '545' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:35:46 GMT + - Wed, 21 Oct 2026 13:14:46 GMT Server: - gunicorn X-Billing-ID: @@ -258,12 +258,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) method: GET uri: http://ai:9696/v3/hunt/historical/list?community=gamma response: body: - string: '{"has_more":false,"limit":50,"result":[{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-06-03T22:35:46.562853+00:00","id":"53359932363657862","progress":null,"results_csv_uri":null,"ruleset_name":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-06-03T22:35:46.521466+00:00","id":"24956296142984681","progress":null,"results_csv_uri":null,"ruleset_name":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-06-03T22:35:46.480348+00:00","id":"54272719094803031","progress":null,"results_csv_uri":null,"ruleset_name":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-06-03T22:35:46.441527+00:00","id":"58288022169534211","progress":null,"results_csv_uri":null,"ruleset_name":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-06-03T22:35:46.397319+00:00","id":"86221284475520679","progress":null,"results_csv_uri":null,"ruleset_name":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-06-03T22:35:46.255339+00:00","id":"46026864008511354","progress":null,"results_csv_uri":null,"ruleset_name":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-06-03T22:31:26.868688+00:00","id":"51603247373653891","progress":null,"results_csv_uri":null,"ruleset_name":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null}],"status":"OK"} + string: '{"has_more":false,"limit":50,"result":[{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:46.809660+00:00","id":"6447048420148760","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:46.768119+00:00","id":"27886352331973438","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:46.723965+00:00","id":"2049602096428236","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:46.681280+00:00","id":"7126890832542907","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-10-21T13:14:46.629460+00:00","id":"59350603088096354","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","window_days":null,"yara":null}],"status":"OK"} ' headers: @@ -274,11 +274,11 @@ interactions: Connection: - keep-alive Content-Length: - - '2092' + - '1917' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:35:46 GMT + - Wed, 21 Oct 2026 13:14:46 GMT Server: - gunicorn X-Billing-ID: From d5502a75c8d933544b80c5488ef6115ef30ce815 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 21 Oct 2026 11:03:57 -0300 Subject: [PATCH 18/18] ci: re-run the PR checks Co-Authored-By: Alan Justino