From a8e0ab184e514a4724499e915d225a6eb17e1248 Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 16 Oct 2026 15:57:59 -0300 Subject: [PATCH 01/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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).