From 61aaf082e2eb0b9763adec7153505f5c406e09c9 Mon Sep 17 00:00:00 2001 From: Samuel Date: Tue, 28 Jul 2026 17:14:05 -0300 Subject: [PATCH 01/20] feat: typed exception for a withheld known-good binary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A download refused because the sample is a known-good binary now carries a stable machine-readable code in the error envelope, so a caller can tell it apart from a deleted, expired or never-uploaded artifact instead of matching prose. `KnownGoodWithheldException` subclasses `NotFoundException` and is raised from the shared 404 arm when the payload's code says so, exposing the flagging feeds as `.sources`. Subclassing is the point: the status is unchanged and every existing `except NotFoundException` handler keeps catching it, which the downstream contract requires. Every other 404 is unaffected. Also documents the server's `require_scan` semantics: a known-good hash counts as present, because it is a decided terminal record — the platform will never scan it and never stores its binary. The 200-vs-204 contract is unchanged. --- specs/00-overview.md | 2 +- specs/01-architecture.md | 4 +-- specs/02-resources.md | 12 ++++++-- specs/03-endpoints.md | 2 +- specs/05-downstream-contract.md | 3 ++ src/polyswarm_api/aio/api.py | 2 ++ src/polyswarm_api/api.py | 2 ++ src/polyswarm_api/core.py | 13 ++++++++- src/polyswarm_api/exceptions.py | 17 +++++++++++ test/core_test.py | 51 +++++++++++++++++++++++++++++++-- 10 files changed, 99 insertions(+), 9 deletions(-) diff --git a/specs/00-overview.md b/specs/00-overview.md index c2b96e9f..e42f02a5 100644 --- a/specs/00-overview.md +++ b/specs/00-overview.md @@ -19,7 +19,7 @@ What the `polyswarm-api` Python SDK is, what it ships, where it sits in the plat | `polyswarm_api.PolyswarmSession` / `polyswarm_api.aio.AsyncPolyswarmSession` | Transport classes. Own the underlying `httpx.{,Async}Client`, expose `execute(request)` and `upload_file(url, artifact, …)`. Subclass and inject to customize transport behaviour. | | `polyswarm_api.resources` | Per-domain resource classes (`ArtifactInstance`, `LocalArtifact`, `HistoricalHunt`, `LiveYaraRuleset`, `YaraRuleset`, `MetadataFieldProperties`, `LLMPromptConfig`, …). Wrappers over the server's JSON responses. Builder classmethods (`create` / `get` / `update` / `delete` / `list` / etc.) return `PolyswarmRequest` descriptors. | | `polyswarm_api.core.PolyswarmRequest` | Pure description of an HTTP call (method, URL, params, body, parser). Constructed by resource builders; handed to a session for execution. No I/O on the descriptor itself. | -| `polyswarm_api.exceptions` | Exception hierarchy (`PolyswarmException` → `RequestException`, `NotFoundException`, `FailedInstanceException`, `NoResultsException`, `UsageLimitsExceededException`, `InvalidValueException`, `TimeoutException`). | +| `polyswarm_api.exceptions` | Exception hierarchy (`PolyswarmException` → `RequestException`, `NotFoundException` → `KnownGoodWithheldException`, `FailedInstanceException`, `NoResultsException`, `UsageLimitsExceededException`, `InvalidValueException`, `TimeoutException`). | ## Where it sits diff --git a/specs/01-architecture.md b/specs/01-architecture.md index 9fe511cf..df5f7bc3 100644 --- a/specs/01-architecture.md +++ b/specs/01-architecture.md @@ -212,7 +212,7 @@ A non-`BaseJsonResource` parser is a **download** and instead takes the streamin `parse_response(response, request)`: - HEAD: `request._result = response.status_code`; return. -- Non-2xx: extract JSON body into `request.json` / `request.status` / `request.errors`; dispatch on status code to typed exception class (`NotFoundException`, `FailedInstanceException`, `UsageLimitsExceededException`, `RequestException`); raise. +- Non-2xx: extract JSON body into `request.json` / `request.status` / `request.errors`; dispatch on status code to typed exception class (`NotFoundException`, `FailedInstanceException`, `UsageLimitsExceededException`, `RequestException`); raise. The 404 arm also reads the extracted `errors` payload: a dict carrying `code == 'KNOWN_GOOD_WITHHELD'` raises the `NotFoundException` subclass `KnownGoodWithheldException` instead. - 2xx without `result_parser`: return (fire-and-forget endpoints like `notification_webhook_test`). - 2xx with `BaseJsonResource` parser: extract JSON, populate pagination metadata (`total`, `limit`, `offset`, `has_more`, `_paginated`), dispatch on `result_parser.parse_result_list` (list) or `.parse_result` (single). - 2xx with non-`BaseJsonResource` parser: pass `(api, response)` directly to `result_parser.parse_result` (used for `LocalArtifact` file downloads). @@ -267,7 +267,7 @@ Both paths are covered by respx tests (`test_async_pagination_*` in `async_clien Every HTTP-level error maps to a subclass of `PolyswarmException`: -- 404 → `NotFoundException` +- 404 → `NotFoundException` — or `KnownGoodWithheldException` (its subclass) when the body's `errors` dict carries `code == 'KNOWN_GOOD_WITHHELD'`: the artifact is a known-good binary whose bytes are never stored or served. Subclassing keeps every existing `except NotFoundException` caller working; the exception exposes the flagging feeds as `.sources`. - 422 → `FailedInstanceException` - 429 → `UsageLimitsExceededException` - 204 on a request that expects data (JSON-parser GET **or** streaming download) → `NoResultsException` — the server did the work but matched nothing. **HEAD is exempt**: it returns the raw status code as the result (so `exists()` reads a 204 as "known-absent" rather than raising). diff --git a/specs/02-resources.md b/specs/02-resources.md index a9817f9f..682c07af 100644 --- a/specs/02-resources.md +++ b/specs/02-resources.md @@ -140,7 +140,7 @@ Behaviour: | Branch | Action | |---|---| | `request.method == 'HEAD'` | `request._result = response.status_code`; return. | -| Non-2xx | Extract JSON body into `request.json` / `.status` / `.errors`. Dispatch on status code: 404 → `NotFoundException`, 422 → `FailedInstanceException`, 429 → `UsageLimitsExceededException`, else → `RequestException`. Raise. | +| Non-2xx | Extract JSON body into `request.json` / `.status` / `.errors`. Dispatch on status code: 404 → `NotFoundException` (→ `KnownGoodWithheldException` when `errors['code'] == 'KNOWN_GOOD_WITHHELD'`), 422 → `FailedInstanceException`, 429 → `UsageLimitsExceededException`, else → `RequestException`. Raise. | | 2xx, no `result_parser` | Return (fire-and-forget endpoints). | | 2xx, `result_parser` is `BaseJsonResource` subclass, status 204 | Raise `NoResultsException`. | | 2xx, `BaseJsonResource` parser | Extract JSON. Populate pagination metadata (`_paginated` / `total` / `limit` / `offset` / `has_more`). Find `result` or `results` key in body. Dispatch on `result_parser.parse_result_list` (list) or `.parse_result` (single). | @@ -302,9 +302,16 @@ it parses to `None` with no behaviour change). It lets a consumer recognise a known-good-bypassed scan via `state == 'KNOWN_GOOD'` even when `known_good` above is `None` (the sha matched no `KnownGood`). The raw numeric `bounty_state` is unchanged. +`state == 'KNOWN_GOOD'` (equivalently the status the server reports for the instance) +is also **the** signal that the artifact's bytes are withheld — the platform never +stores or serves a known-good binary, and there is deliberately no separate +"withheld" field to read. A download attempted anyway raises +`KnownGoodWithheldException` (see §"Exceptions thrown by parsing"); the metadata — +the flagging feeds plus any scan data already collected — stays readable. + Classmethod builders (each returns a `PolyswarmRequest` descriptor): -- `exists_hash(api, hash_value, hash_type, require_scan=False)` — HEAD request, returns the status code as the result. +- `exists_hash(api, hash_value, hash_type, require_scan=False)` — HEAD request, returns the status code as the result. `require_scan=True` narrows the answer to scanned artifacts, **except** for a known-good sha256, which the server reports as present either way: a known-good hash is a decided terminal record, so it is never scanned and its binary is never stored. - `search_hash(api, hash_value, hash_type)` — GET `/search/hash/{hash_type}`. - `search_url(api, url)` — GET `/search/url`. - `list_scans(api, hash_value)` — GET `/search/instances`. @@ -381,6 +388,7 @@ This keeps the body off the heap for `folder`/file-handle destinations — parit - `NoResultsException` — HTTP 204 with a typed `result_parser`. - `NotFoundException` — HTTP 404, or a JSON-decode failure on a 404. +- `KnownGoodWithheldException` (a `NotFoundException` subclass) — HTTP 404 whose `errors` payload is a dict with `code == 'KNOWN_GOOD_WITHHELD'`: the artifact is a known-good binary and its bytes are withheld by design. Carries `.sources` (the flagging known-good feeds, `[]` when none were named). Any other 404 — a different code, a legacy list-shaped `errors`, or no `errors` at all — stays a plain `NotFoundException`. - `FailedInstanceException` — HTTP 422. - `UsageLimitsExceededException` — HTTP 429. - `RequestException` — any other non-2xx. diff --git a/specs/03-endpoints.md b/specs/03-endpoints.md index d187837e..d62c8fa4 100644 --- a/specs/03-endpoints.md +++ b/specs/03-endpoints.md @@ -25,7 +25,7 @@ The full catalogue of methods on the public client surface and which transport h | Method | Resource builder | Notes | |---|---|---| -| `exists(hash_, hash_type=None, require_scan=False)` | `ArtifactInstance.exists_hash` | HEAD; `bool` from status code — `True` **only** for `200` (present). `204` means "absent" (the request succeeded but matched no artifact) and `404` also maps to absent, so both are `False`. Do **not** treat this as a generic `2xx` check: `204` is a successful status that means the opposite of "exists". | +| `exists(hash_, hash_type=None, require_scan=False)` | `ArtifactInstance.exists_hash` | HEAD; `bool` from status code — `True` **only** for `200` (present). `204` means "absent" (the request succeeded but matched no artifact) and `404` also maps to absent, so both are `False`. Do **not** treat this as a generic `2xx` check: `204` is a successful status that means the opposite of "exists". `require_scan=True` still reports a **known-good** sha256 as present (`200`) — a known-good hash is a decided terminal record the platform will never scan or store the binary of. | | `lookup(scan)` | `ArtifactInstance.lookup_uuid` | | | `rescan(hash_, hash_type=None, scan_config=None)` | `ArtifactInstance.rescan` | | | `rescan_id(scan, scan_config=None)` | `ArtifactInstance.rescan_id` | | diff --git a/specs/05-downstream-contract.md b/specs/05-downstream-contract.md index d06ed891..afa57ba6 100644 --- a/specs/05-downstream-contract.md +++ b/specs/05-downstream-contract.md @@ -170,6 +170,7 @@ ArtifactType # enum: FILE, URL class PolyswarmException(Exception): ... class RequestException(PolyswarmException): ... class NotFoundException(RequestException): ... +class KnownGoodWithheldException(NotFoundException): ... class FailedInstanceException(RequestException): ... class NoResultsException(RequestException): ... class UsageLimitsExceededException(RequestException): ... @@ -177,6 +178,8 @@ class InvalidValueException(PolyswarmException): ... class TimeoutException(PolyswarmException): ... ``` +`KnownGoodWithheldException` is the 404 raised when a download is refused because the artifact is a known-good binary — the platform never stores or serves those bytes. It **subclasses `NotFoundException`** precisely so invariant 3 holds for existing consumers: code that already does `except NotFoundException:` keeps catching the refusal with no change, and only callers that want to distinguish "withheld by design" from a plain miss catch the subclass. It adds one attribute, `.sources` — the known-good feeds that flagged the hash (e.g. `['nsrl']`), `[]` when the server named none. The raw envelope stays reachable at `exc.request.errors` (`{'code': 'KNOWN_GOOD_WITHHELD', 'known_good': True, 'sources': [...]}`). The artifact's metadata — the flagging feeds plus any scan data already collected — remains readable through the search / instance endpoints; only the bytes are withheld, and the instance's `KNOWN_GOOD` state/status is the signal for that (there is no separate "withheld" field). + Each `RequestException` subclass carries a `.request` attribute holding the originating `PolyswarmRequest` (set by `RequestException.__init__`). Callers can read `exc.request.status_code`, `exc.request.json` (the parsed response body after execution), `exc.request.input_json` (the body that was sent), `exc.request.request_parameters` (the request kwargs that built the call), etc. `InvalidValueException` and `TimeoutException` are client-side errors and don't carry a request descriptor. Attachment happens at exception-construction time inside `parse_response` — `session.execute` does not catch and rewrap. diff --git a/src/polyswarm_api/aio/api.py b/src/polyswarm_api/aio/api.py index 0c9d69e9..e9951a60 100644 --- a/src/polyswarm_api/aio/api.py +++ b/src/polyswarm_api/aio/api.py @@ -1643,6 +1643,8 @@ async def exists(self, hash_, hash_type=None, require_scan=False): :param hash_: Hashable (Artifact, LocalArtifact, Hash) or hex-encoded SHA256/SHA1/MD5. :param hash_type: Hash type; auto-detected if not provided. :param require_scan: If True, only count artifacts that have been scanned. + A known-good hash still counts as present: it is a decided terminal + record, so the platform never scans it and never stores its binary. :return: ``True`` if the artifact exists in PolySwarm's index. """ logger.info('Exists for hash %s', hash_) diff --git a/src/polyswarm_api/api.py b/src/polyswarm_api/api.py index fde4ea59..e9353fd5 100644 --- a/src/polyswarm_api/api.py +++ b/src/polyswarm_api/api.py @@ -1990,6 +1990,8 @@ def exists(self, hash_, hash_type=None, require_scan=False): :param hash_: Hashable (Artifact, LocalArtifact, Hash) or hex-encoded SHA256/SHA1/MD5. :param hash_type: Hash type; auto-detected if not provided. :param require_scan: If True, only count artifacts that have been scanned. + A known-good hash still counts as present: it is a decided terminal + record, so the platform never scans it and never stores its binary. :return: ``True`` if the artifact exists in PolySwarm's index. """ logger.info("Exists for hash %s", hash_) diff --git a/src/polyswarm_api/core.py b/src/polyswarm_api/core.py index 7068bbda..a17d95c5 100644 --- a/src/polyswarm_api/core.py +++ b/src/polyswarm_api/core.py @@ -330,7 +330,9 @@ def _raise_for_status(response, request): Shared by ``parse_response`` (buffered/JSON path) and the session's streaming-download path, so both raise identically (429/404/422/other, - plus the non-JSON-body fallbacks). The response body must already be + plus the non-JSON-body fallbacks). A 404 whose ``errors`` payload carries the + ``KNOWN_GOOD_WITHHELD`` code raises the ``NotFoundException`` subclass + ``KnownGoodWithheldException``. The response body must already be readable — streaming callers ``read()`` / ``aread()`` it first. **Always raises; never returns.** """ @@ -354,6 +356,15 @@ def _raise_for_status(response, request): ) raise exceptions.UsageLimitsExceededException(request, message) elif request.status_code == 404: + # A download refused because the artifact is a known-good binary carries a + # machine-readable code in the error envelope's ``errors`` slot. Raise the + # ``NotFoundException`` subclass so callers can tell "withheld by design" + # apart from a plain miss; every other 404 stays a plain NotFoundException. + errors = request.errors + if isinstance(errors, dict) and errors.get('code') == 'KNOWN_GOOD_WITHHELD': + raise exceptions.KnownGoodWithheldException( + request, request._result, sources=errors.get('sources'), + ) raise exceptions.NotFoundException(request, request._result) elif request.status_code == 422: raise exceptions.FailedInstanceException(request, request._result) diff --git a/src/polyswarm_api/exceptions.py b/src/polyswarm_api/exceptions.py index 171d172d..137bddbb 100644 --- a/src/polyswarm_api/exceptions.py +++ b/src/polyswarm_api/exceptions.py @@ -32,6 +32,23 @@ class NotFoundException(RequestException): pass +class KnownGoodWithheldException(NotFoundException): + """404 for a known-good binary: the platform never stores or serves its bytes. + + Subclasses ``NotFoundException`` so every existing ``except NotFoundException`` + handler keeps working unchanged; catch this subclass only when the caller wants + to tell "withheld by design" apart from a plain "not found". The metadata the + platform holds about the artifact is still readable through the search / + instance endpoints. + """ + + def __init__(self, request, *args, sources=None): + super().__init__(request, *args) + # Known-good feeds that flagged the hash (e.g. ``['nsrl']``); empty list + # when the server named none. + self.sources = sources or [] + + class NoResultsException(RequestException): pass diff --git a/test/core_test.py b/test/core_test.py index 3371da90..b7126b81 100644 --- a/test/core_test.py +++ b/test/core_test.py @@ -4,8 +4,8 @@ - ``PolyswarmRequest`` dataclass construction and projections. - ``parse_response`` against fake response objects (HEAD, 2xx with - parser, 2xx without parser, 204, 404, 422, 429, 500, non-JSON 5xx, - non-JSON 404). + parser, 2xx without parser, 204, 404, known-good-withheld 404, 422, + 429, 500, non-JSON 5xx, non-JSON 404). - A sampling of resource builders to confirm they produce the expected ``PolyswarmRequest`` shape. @@ -261,6 +261,53 @@ def test_404_raises_not_found(self): ) assert ei.value.request is req + def test_404_known_good_withheld_raises_subclass(self): + req = PolyswarmRequest(api=_FakeApi(), method='GET', url='u', + result_parser=_SampleResource) + body = { + 'status': 'error', + 'result': 'Unable to download the provided artifact, it is a ' + 'known-good binary; its bytes are withheld by design.', + 'errors': {'code': 'KNOWN_GOOD_WITHHELD', 'known_good': True, + 'sources': ['nsrl']}, + } + with pytest.raises(exceptions.KnownGoodWithheldException) as ei: + parse_response(_FakeResponse(status_code=404, body=body), req) + # Existing ``except NotFoundException`` handlers must keep catching it. + assert isinstance(ei.value, exceptions.NotFoundException) + assert ei.value.sources == ['nsrl'] + assert ei.value.request is req + assert req.errors == body['errors'] + + def test_404_known_good_withheld_without_sources(self): + req = PolyswarmRequest(api=_FakeApi(), method='GET', url='u', + result_parser=_SampleResource) + with pytest.raises(exceptions.KnownGoodWithheldException) as ei: + parse_response( + _FakeResponse(status_code=404, body={ + 'status': 'error', 'result': 'withheld', + 'errors': {'code': 'KNOWN_GOOD_WITHHELD'}, + }), + req, + ) + assert ei.value.sources == [] + + def test_404_other_error_code_stays_plain_not_found(self): + # Only the known-good code gets the subclass; every other 404 — including + # a differently-coded or legacy list-shaped ``errors`` payload — stays a + # plain NotFoundException. + for errors in ({'code': 'DELETED'}, ['not found'], None): + req = PolyswarmRequest(api=_FakeApi(), method='GET', url='u', + result_parser=_SampleResource) + with pytest.raises(exceptions.NotFoundException) as ei: + parse_response( + _FakeResponse(status_code=404, body={ + 'status': 'error', 'result': 'missing', 'errors': errors, + }), + req, + ) + assert not isinstance(ei.value, exceptions.KnownGoodWithheldException) + def test_422_raises_failed_instance(self): req = PolyswarmRequest(api=_FakeApi(), method='POST', url='u', result_parser=_SampleResource) From 3682a8cc269c5a7f1ffe4efeaef9b7ea8b239872 Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 29 Jul 2026 13:07:13 -0300 Subject: [PATCH 02/20] fix: match the server's refusal code, now KNOWN_GOOD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server renamed the withheld-binary cause from KNOWN_GOOD_WITHHELD to KNOWN_GOOD — every member of that closed set is a cause of a refusal, so the suffix restated what the set already implies. The 404 arm has to match the value the server actually sends, or a withheld-binary refusal quietly degrades to a plain NotFoundException. The exception class keeps its name: it describes the outcome a caller catches, not the cause code it was dispatched from. --- specs/01-architecture.md | 4 ++-- specs/02-resources.md | 4 ++-- specs/05-downstream-contract.md | 2 +- src/polyswarm_api/core.py | 4 ++-- test/core_test.py | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/specs/01-architecture.md b/specs/01-architecture.md index df5f7bc3..e9629579 100644 --- a/specs/01-architecture.md +++ b/specs/01-architecture.md @@ -212,7 +212,7 @@ A non-`BaseJsonResource` parser is a **download** and instead takes the streamin `parse_response(response, request)`: - HEAD: `request._result = response.status_code`; return. -- Non-2xx: extract JSON body into `request.json` / `request.status` / `request.errors`; dispatch on status code to typed exception class (`NotFoundException`, `FailedInstanceException`, `UsageLimitsExceededException`, `RequestException`); raise. The 404 arm also reads the extracted `errors` payload: a dict carrying `code == 'KNOWN_GOOD_WITHHELD'` raises the `NotFoundException` subclass `KnownGoodWithheldException` instead. +- Non-2xx: extract JSON body into `request.json` / `request.status` / `request.errors`; dispatch on status code to typed exception class (`NotFoundException`, `FailedInstanceException`, `UsageLimitsExceededException`, `RequestException`); raise. The 404 arm also reads the extracted `errors` payload: a dict carrying `code == 'KNOWN_GOOD'` raises the `NotFoundException` subclass `KnownGoodWithheldException` instead. - 2xx without `result_parser`: return (fire-and-forget endpoints like `notification_webhook_test`). - 2xx with `BaseJsonResource` parser: extract JSON, populate pagination metadata (`total`, `limit`, `offset`, `has_more`, `_paginated`), dispatch on `result_parser.parse_result_list` (list) or `.parse_result` (single). - 2xx with non-`BaseJsonResource` parser: pass `(api, response)` directly to `result_parser.parse_result` (used for `LocalArtifact` file downloads). @@ -267,7 +267,7 @@ Both paths are covered by respx tests (`test_async_pagination_*` in `async_clien Every HTTP-level error maps to a subclass of `PolyswarmException`: -- 404 → `NotFoundException` — or `KnownGoodWithheldException` (its subclass) when the body's `errors` dict carries `code == 'KNOWN_GOOD_WITHHELD'`: the artifact is a known-good binary whose bytes are never stored or served. Subclassing keeps every existing `except NotFoundException` caller working; the exception exposes the flagging feeds as `.sources`. +- 404 → `NotFoundException` — or `KnownGoodWithheldException` (its subclass) when the body's `errors` dict carries `code == 'KNOWN_GOOD'`: the artifact is a known-good binary whose bytes are never stored or served. Subclassing keeps every existing `except NotFoundException` caller working; the exception exposes the flagging feeds as `.sources`. - 422 → `FailedInstanceException` - 429 → `UsageLimitsExceededException` - 204 on a request that expects data (JSON-parser GET **or** streaming download) → `NoResultsException` — the server did the work but matched nothing. **HEAD is exempt**: it returns the raw status code as the result (so `exists()` reads a 204 as "known-absent" rather than raising). diff --git a/specs/02-resources.md b/specs/02-resources.md index 682c07af..ab6133dc 100644 --- a/specs/02-resources.md +++ b/specs/02-resources.md @@ -140,7 +140,7 @@ Behaviour: | Branch | Action | |---|---| | `request.method == 'HEAD'` | `request._result = response.status_code`; return. | -| Non-2xx | Extract JSON body into `request.json` / `.status` / `.errors`. Dispatch on status code: 404 → `NotFoundException` (→ `KnownGoodWithheldException` when `errors['code'] == 'KNOWN_GOOD_WITHHELD'`), 422 → `FailedInstanceException`, 429 → `UsageLimitsExceededException`, else → `RequestException`. Raise. | +| Non-2xx | Extract JSON body into `request.json` / `.status` / `.errors`. Dispatch on status code: 404 → `NotFoundException` (→ `KnownGoodWithheldException` when `errors['code'] == 'KNOWN_GOOD'`), 422 → `FailedInstanceException`, 429 → `UsageLimitsExceededException`, else → `RequestException`. Raise. | | 2xx, no `result_parser` | Return (fire-and-forget endpoints). | | 2xx, `result_parser` is `BaseJsonResource` subclass, status 204 | Raise `NoResultsException`. | | 2xx, `BaseJsonResource` parser | Extract JSON. Populate pagination metadata (`_paginated` / `total` / `limit` / `offset` / `has_more`). Find `result` or `results` key in body. Dispatch on `result_parser.parse_result_list` (list) or `.parse_result` (single). | @@ -388,7 +388,7 @@ This keeps the body off the heap for `folder`/file-handle destinations — parit - `NoResultsException` — HTTP 204 with a typed `result_parser`. - `NotFoundException` — HTTP 404, or a JSON-decode failure on a 404. -- `KnownGoodWithheldException` (a `NotFoundException` subclass) — HTTP 404 whose `errors` payload is a dict with `code == 'KNOWN_GOOD_WITHHELD'`: the artifact is a known-good binary and its bytes are withheld by design. Carries `.sources` (the flagging known-good feeds, `[]` when none were named). Any other 404 — a different code, a legacy list-shaped `errors`, or no `errors` at all — stays a plain `NotFoundException`. +- `KnownGoodWithheldException` (a `NotFoundException` subclass) — HTTP 404 whose `errors` payload is a dict with `code == 'KNOWN_GOOD'`: the artifact is a known-good binary and its bytes are withheld by design. Carries `.sources` (the flagging known-good feeds, `[]` when none were named). Any other 404 — a different code, a legacy list-shaped `errors`, or no `errors` at all — stays a plain `NotFoundException`. - `FailedInstanceException` — HTTP 422. - `UsageLimitsExceededException` — HTTP 429. - `RequestException` — any other non-2xx. diff --git a/specs/05-downstream-contract.md b/specs/05-downstream-contract.md index afa57ba6..f5f3be91 100644 --- a/specs/05-downstream-contract.md +++ b/specs/05-downstream-contract.md @@ -178,7 +178,7 @@ class InvalidValueException(PolyswarmException): ... class TimeoutException(PolyswarmException): ... ``` -`KnownGoodWithheldException` is the 404 raised when a download is refused because the artifact is a known-good binary — the platform never stores or serves those bytes. It **subclasses `NotFoundException`** precisely so invariant 3 holds for existing consumers: code that already does `except NotFoundException:` keeps catching the refusal with no change, and only callers that want to distinguish "withheld by design" from a plain miss catch the subclass. It adds one attribute, `.sources` — the known-good feeds that flagged the hash (e.g. `['nsrl']`), `[]` when the server named none. The raw envelope stays reachable at `exc.request.errors` (`{'code': 'KNOWN_GOOD_WITHHELD', 'known_good': True, 'sources': [...]}`). The artifact's metadata — the flagging feeds plus any scan data already collected — remains readable through the search / instance endpoints; only the bytes are withheld, and the instance's `KNOWN_GOOD` state/status is the signal for that (there is no separate "withheld" field). +`KnownGoodWithheldException` is the 404 raised when a download is refused because the artifact is a known-good binary — the platform never stores or serves those bytes. It **subclasses `NotFoundException`** precisely so invariant 3 holds for existing consumers: code that already does `except NotFoundException:` keeps catching the refusal with no change, and only callers that want to distinguish "withheld by design" from a plain miss catch the subclass. It adds one attribute, `.sources` — the known-good feeds that flagged the hash (e.g. `['nsrl']`), `[]` when the server named none. The raw envelope stays reachable at `exc.request.errors` (`{'code': 'KNOWN_GOOD', 'known_good': True, 'sources': [...]}`). The artifact's metadata — the flagging feeds plus any scan data already collected — remains readable through the search / instance endpoints; only the bytes are withheld, and the instance's `KNOWN_GOOD` state/status is the signal for that (there is no separate "withheld" field). Each `RequestException` subclass carries a `.request` attribute holding the originating `PolyswarmRequest` (set by `RequestException.__init__`). Callers can read `exc.request.status_code`, `exc.request.json` (the parsed response body after execution), `exc.request.input_json` (the body that was sent), `exc.request.request_parameters` (the request kwargs that built the call), etc. `InvalidValueException` and `TimeoutException` are client-side errors and don't carry a request descriptor. diff --git a/src/polyswarm_api/core.py b/src/polyswarm_api/core.py index a17d95c5..54c2c7ab 100644 --- a/src/polyswarm_api/core.py +++ b/src/polyswarm_api/core.py @@ -331,7 +331,7 @@ def _raise_for_status(response, request): Shared by ``parse_response`` (buffered/JSON path) and the session's streaming-download path, so both raise identically (429/404/422/other, plus the non-JSON-body fallbacks). A 404 whose ``errors`` payload carries the - ``KNOWN_GOOD_WITHHELD`` code raises the ``NotFoundException`` subclass + ``KNOWN_GOOD`` code raises the ``NotFoundException`` subclass ``KnownGoodWithheldException``. The response body must already be readable — streaming callers ``read()`` / ``aread()`` it first. **Always raises; never returns.** @@ -361,7 +361,7 @@ def _raise_for_status(response, request): # ``NotFoundException`` subclass so callers can tell "withheld by design" # apart from a plain miss; every other 404 stays a plain NotFoundException. errors = request.errors - if isinstance(errors, dict) and errors.get('code') == 'KNOWN_GOOD_WITHHELD': + if isinstance(errors, dict) and errors.get('code') == 'KNOWN_GOOD': raise exceptions.KnownGoodWithheldException( request, request._result, sources=errors.get('sources'), ) diff --git a/test/core_test.py b/test/core_test.py index b7126b81..227caa35 100644 --- a/test/core_test.py +++ b/test/core_test.py @@ -268,7 +268,7 @@ def test_404_known_good_withheld_raises_subclass(self): 'status': 'error', 'result': 'Unable to download the provided artifact, it is a ' 'known-good binary; its bytes are withheld by design.', - 'errors': {'code': 'KNOWN_GOOD_WITHHELD', 'known_good': True, + 'errors': {'code': 'KNOWN_GOOD', 'known_good': True, 'sources': ['nsrl']}, } with pytest.raises(exceptions.KnownGoodWithheldException) as ei: @@ -286,7 +286,7 @@ def test_404_known_good_withheld_without_sources(self): parse_response( _FakeResponse(status_code=404, body={ 'status': 'error', 'result': 'withheld', - 'errors': {'code': 'KNOWN_GOOD_WITHHELD'}, + 'errors': {'code': 'KNOWN_GOOD'}, }), req, ) From e9ceb91a8d22c6c28ea3c0a77f4560cee2266506 Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 29 Jul 2026 13:49:44 -0300 Subject: [PATCH 03/20] fix: render dict-shaped errors in the failure message, and cover the streaming refusal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _bad_status_message assumed `errors` was a list, so it joined over it directly. Iterating a dict yields its keys, so any non-404 status carrying the new dict-shaped envelope rendered as "code\nknown_good\nsources" — every value silently dropped from the message the caller sees. The specs now call the list shape legacy and the dict shape the way forward, so this change is what makes the dict expected; both shapes are handled. The exception exists for a refused download, which flows through the streaming arm rather than parse_response, and no test drove that path — the coverage was pure-unit against a body this branch wrote itself. There is now a test that refuses a download at the transport and asserts the typed exception with its sources. --- specs/01-architecture.md | 2 +- specs/02-resources.md | 2 +- specs/04-testing.md | 2 ++ src/polyswarm_api/core.py | 13 ++++++++++++- test/async_client_test.py | 36 ++++++++++++++++++++++++++++++++++++ test/core_test.py | 38 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 90 insertions(+), 3 deletions(-) diff --git a/specs/01-architecture.md b/specs/01-architecture.md index e9629579..75dfb783 100644 --- a/specs/01-architecture.md +++ b/specs/01-architecture.md @@ -271,7 +271,7 @@ Every HTTP-level error maps to a subclass of `PolyswarmException`: - 422 → `FailedInstanceException` - 429 → `UsageLimitsExceededException` - 204 on a request that expects data (JSON-parser GET **or** streaming download) → `NoResultsException` — the server did the work but matched nothing. **HEAD is exempt**: it returns the raw status code as the result (so `exists()` reads a 204 as "known-absent" rather than raising). -- Other non-2xx → `RequestException` +- Other non-2xx → `RequestException`. Its message renders the request diagnostics plus the envelope's `errors` slot, in **either** shape: the legacy **list** (one entry per line) or the way-forward **mapping** (`key=value` lines). The mapping shape is not 404-only — the server forwards it on every arm (400 / 401 / 403 / 413 / 5xx) — and iterating a mapping yields only its keys, so it must not be rendered like a list or every value is silently dropped from the message. - Client-side validation failures (bad hash, missing kwarg) → `InvalidValueException` - Polling timeouts → `TimeoutException` diff --git a/specs/02-resources.md b/specs/02-resources.md index ab6133dc..724644c0 100644 --- a/specs/02-resources.md +++ b/specs/02-resources.md @@ -391,7 +391,7 @@ This keeps the body off the heap for `folder`/file-handle destinations — parit - `KnownGoodWithheldException` (a `NotFoundException` subclass) — HTTP 404 whose `errors` payload is a dict with `code == 'KNOWN_GOOD'`: the artifact is a known-good binary and its bytes are withheld by design. Carries `.sources` (the flagging known-good feeds, `[]` when none were named). Any other 404 — a different code, a legacy list-shaped `errors`, or no `errors` at all — stays a plain `NotFoundException`. - `FailedInstanceException` — HTTP 422. - `UsageLimitsExceededException` — HTTP 429. -- `RequestException` — any other non-2xx. +- `RequestException` — any other non-2xx. Its message appends the envelope's `errors` slot rendered for whichever shape arrived: a **list** renders one entry per line (the legacy shape), a **mapping** renders `key=value` lines (the way-forward shape, which the server forwards on every status — not just the 404 the `code` contract was introduced for). Each is raised by `parse_response`. `RequestException.__init__` attaches the descriptor as `.request`, so callers downstream read `exc.request.status_code`, `exc.request.json`, etc. The session does not catch and rewrap — attachment happens at construction time. diff --git a/specs/04-testing.md b/specs/04-testing.md index e875fc95..c8fb2d1d 100644 --- a/specs/04-testing.md +++ b/specs/04-testing.md @@ -40,6 +40,8 @@ The pure-unit tier exists because the 4.0 redesign made it possible: resource bu **VCR-backed live-e2e tests are the default for new endpoint tests** (invariant 1): they exercise the real server implementation and the cassette pins the actual contract, while replay keeps unit runs fast and offline. `respx` is reserved for scenarios the e2e stack can't produce (or only at disproportionate cost); pure-unit covers request-shape and parse logic without any HTTP. The canonical split for a new endpoint: a live-e2e VCR lifecycle test (behaviour, real contract) + pure-unit builder tests (body-vs-query routing, None-omission) — see `test_known_good_lifecycle` + `test/known_good_test.py`. +A third slice is legitimate when the SDK's **transport arm** is what's under test rather than the endpoint. The streaming download path (`_execute_download`) never runs `parse_response` — it reads the error body itself and calls `_raise_for_status` — so its non-2xx mapping is reachable by neither a pure-unit parse test nor a cassette-backed endpoint test that only asserts the caller-visible outcome. Those go on the respx tier: `test_async_download_known_good_refusal_raises_withheld` pins that a refusal envelope arriving on the *streaming* arm still becomes the typed exception with its payload (`.sources`) and leaves nothing written to the destination. + ### E2e sample-generation conventions - **The EICAR variant is the default sample-generation strategy.** Any test that needs an artifact — or just a sha256 — derives it from its own EICAR variant via `malicious_artifact(uid)` (`EICAR + uid` → unique content + sha, deterministic per test so cassettes replay). Don't invent parallel strategies (digest-of-test-name, random bytes, checked-in binaries) — one convention keeps every sha attributable to a test and keeps the eicar engine able to flag every sample. diff --git a/src/polyswarm_api/core.py b/src/polyswarm_api/core.py index 54c2c7ab..7fb6a27f 100644 --- a/src/polyswarm_api/core.py +++ b/src/polyswarm_api/core.py @@ -394,7 +394,18 @@ def _bad_status_message(request): f'Message: {request._result}' ) if request.errors: - errors = '\n'.join(str(error) for error in request.errors) + # Two ``errors`` shapes are in play. The legacy shape is a **list** of + # per-error entries — one rendered line each. The way-forward shape is a + # **mapping** carrying the machine-readable code plus its context + # (``{'code': 'KNOWN_GOOD', 'known_good': True, 'sources': [...]}``), and the + # server forwards it on every status, not just the 404 the code was + # introduced for. Iterating a mapping yields only its KEYS, so rendering it + # like a list would drop every value from the message the caller sees — + # render it as ``key=value`` lines instead. + if isinstance(request.errors, dict): + errors = '\n'.join(f'{k}={v}' for k, v in request.errors.items()) + else: + errors = '\n'.join(str(error) for error in request.errors) message = f'{message}\nErrors:\n{errors}' return message diff --git a/test/async_client_test.py b/test/async_client_test.py index 266afa32..efd3fbf3 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -1014,6 +1014,42 @@ async def test_async_download_204_raises_no_results(): await api.aclose() +@respx.mock +async def test_async_download_known_good_refusal_raises_withheld(): + """A refused download is what ``KnownGoodWithheldException`` exists for, and a + refusal arrives on the **streaming** arm (``_execute_download`` reads the small + error body, then hands it to the shared ``_raise_for_status``) — not through + ``parse_response``. Pure-unit coverage of the envelope can't reach that arm, so + pin it here: the typed exception must come out of ``download`` carrying the + flagging feeds, and nothing may be written to the destination folder (a refusal + must not leave a truncated or empty artifact behind, the way a 204 must not). + + respx rather than the live e2e stack because the refusal is a *transport-shape* + assertion on the streaming path; the endpoint behaviour itself belongs to the + VCR-backed lifecycle test. + """ + respx.get(f'{BASE_URL}/consumer/download/sha256/{SHA256}').mock( + return_value=httpx.Response(404, json={ + 'status': 'error', + 'result': 'Unable to download the provided artifact, it is a ' + 'known-good binary; its bytes are withheld by design.', + 'errors': {'code': 'KNOWN_GOOD', 'known_good': True, + 'sources': ['nsrl']}, + })) + + api = PolySwarmAsyncAPI(API_KEY, uri=BASE_URL, community='gamma') + try: + with tempfile.TemporaryDirectory() as tmp_dir: + with pytest.raises(exceptions.KnownGoodWithheldException) as ei: + await api.download(tmp_dir, SHA256) + assert ei.value.sources == ['nsrl'] + # Still a NotFoundException, so existing handlers keep catching it. + assert isinstance(ei.value, exceptions.NotFoundException) + assert os.listdir(tmp_dir) == [] + finally: + await api.aclose() + + @respx.mock async def test_async_download_streams_in_chunks(monkeypatch): """Regression guard for the streaming-download fix: the body is consumed via diff --git a/test/core_test.py b/test/core_test.py index 227caa35..cc651db5 100644 --- a/test/core_test.py +++ b/test/core_test.py @@ -338,6 +338,44 @@ def test_500_raises_request_exception(self): req, ) + def test_500_mapping_errors_keep_every_value_in_the_message(self): + # Regression: the diagnostic rendered ``errors`` by iterating it, which on a + # mapping-shaped envelope yields only its KEYS — the message came out as + # "code\nknown_good\nsources" with every value silently dropped. The mapping + # shape is the way-forward one and the server forwards it on every status + # (400 / 401 / 403 / 413 / 5xx all land here), so the values must survive. + req = PolyswarmRequest(api=_FakeApi(), method='POST', url='u', + result_parser=_SampleResource) + errors = {'code': 'KNOWN_GOOD', 'known_good': True, 'sources': ['nsrl']} + with pytest.raises(exceptions.RequestException) as ei: + parse_response( + _FakeResponse(status_code=500, body={ + 'status': 'error', 'result': 'boom', 'errors': errors, + }), + req, + ) + message = str(ei.value) + assert 'code=KNOWN_GOOD' in message + assert 'known_good=True' in message + assert "sources=['nsrl']" in message + # The keys alone (the old, lossy rendering) must not be the whole story. + assert 'Errors:\ncode\n' not in message + + def test_500_list_errors_still_render_one_entry_per_line(self): + # The legacy list shape must keep rendering exactly as before — one entry + # per line — now that the mapping shape is branched on separately. + req = PolyswarmRequest(api=_FakeApi(), method='POST', url='u', + result_parser=_SampleResource) + with pytest.raises(exceptions.RequestException) as ei: + parse_response( + _FakeResponse(status_code=500, body={ + 'status': 'error', 'result': 'boom', + 'errors': ['first problem', 'second problem'], + }), + req, + ) + assert 'Errors:\nfirst problem\nsecond problem' in str(ei.value) + def test_500_raises_even_without_parser(self): # Regression: fire-and-forget endpoints (no result_parser) must # still surface non-2xx as exceptions, not swallow them. From f7aaf623aee7ef2dbbf37a3fcb657bb424f93a5b Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 29 Jul 2026 14:39:50 -0300 Subject: [PATCH 04/20] fix: run the download-refusal test on both transports, and normalise .sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The streaming-refusal test added last round was an async-only respx body, which the testing spec's harness invariant forbids — and the consequence was that the sync transport's refusal arm had no coverage at all. It now runs on the parametrised harness so one body covers both, and the spec paragraph no longer sanctions a parallel async-only body. The harness moved to its own module rather than being imported from a collected test file. `.sources` was passed through from the wire unvalidated while the downstream contract promises a list of feed names, so a bare string or the feed-dict shape would have survived and made iteration surprising. It is normalised at the boundary: a list of strings stays, a bare string becomes one element, anything else becomes empty. --- specs/02-resources.md | 2 +- specs/04-testing.md | 12 ++- specs/05-downstream-contract.md | 2 +- src/polyswarm_api/core.py | 2 + src/polyswarm_api/exceptions.py | 26 ++++- test/_client_harness.py | 131 +++++++++++++++++++++++++ test/async_client_test.py | 36 ------- test/core_test.py | 34 +++++++ test/download_refusal_test.py | 58 +++++++++++ test/metadata_field_properties_test.py | 128 ++---------------------- 10 files changed, 264 insertions(+), 167 deletions(-) create mode 100644 test/_client_harness.py create mode 100644 test/download_refusal_test.py diff --git a/specs/02-resources.md b/specs/02-resources.md index 724644c0..85139ea0 100644 --- a/specs/02-resources.md +++ b/specs/02-resources.md @@ -388,7 +388,7 @@ This keeps the body off the heap for `folder`/file-handle destinations — parit - `NoResultsException` — HTTP 204 with a typed `result_parser`. - `NotFoundException` — HTTP 404, or a JSON-decode failure on a 404. -- `KnownGoodWithheldException` (a `NotFoundException` subclass) — HTTP 404 whose `errors` payload is a dict with `code == 'KNOWN_GOOD'`: the artifact is a known-good binary and its bytes are withheld by design. Carries `.sources` (the flagging known-good feeds, `[]` when none were named). Any other 404 — a different code, a legacy list-shaped `errors`, or no `errors` at all — stays a plain `NotFoundException`. +- `KnownGoodWithheldException` (a `NotFoundException` subclass) — HTTP 404 whose `errors` payload is a dict with `code == 'KNOWN_GOOD'`: the artifact is a known-good binary and its bytes are withheld by design. Carries `.sources` (the flagging known-good feeds, always a list of strings — normalised in the exception's constructor — and `[]` when none were named or the payload arrived in another shape). Any other 404 — a different code, a legacy list-shaped `errors`, or no `errors` at all — stays a plain `NotFoundException`. - `FailedInstanceException` — HTTP 422. - `UsageLimitsExceededException` — HTTP 429. - `RequestException` — any other non-2xx. Its message appends the envelope's `errors` slot rendered for whichever shape arrived: a **list** renders one entry per line (the legacy shape), a **mapping** renders `key=value` lines (the way-forward shape, which the server forwards on every status — not just the 404 the `code` contract was introduced for). diff --git a/specs/04-testing.md b/specs/04-testing.md index c8fb2d1d..a9b04713 100644 --- a/specs/04-testing.md +++ b/specs/04-testing.md @@ -19,7 +19,9 @@ How the test suite is organised. Three layers: pure unit tests (no HTTP at all - `test/conftest.py` — pytest configuration. - `test/core_test.py` — pure-unit tests for `parse_response`, `PolyswarmRequest`, and resource builders. No httpx, no fixtures. +- `test/_client_harness.py` — the parametrised `ClientTestCase` harness (`_MockBoundary` / `_AsyncToSync`) every `respx` test module imports. Not collected itself (the `_` prefix keeps it out of `python_files`), same as `_e2e_helpers.py`. - `test/metadata_field_properties_test.py` — the canonical example of the parametrised `ClientTestCase` harness with `respx`-backed mocking. +- `test/download_refusal_test.py` — the same harness applied to a transport arm: the streaming-download refusal mapping, one body over both `_execute_download` implementations. - `test/client_scan_test.py` — sync, VCR-backed integration tests (not yet on the parametrised harness — follow-up work). - `test/async_client_test.py` — async, VCR-backed integration tests (not yet on the parametrised harness — follow-up work). - `test/jmespath_test.py` — unit tests for `BaseJsonResource.jmespath`. @@ -40,7 +42,7 @@ The pure-unit tier exists because the 4.0 redesign made it possible: resource bu **VCR-backed live-e2e tests are the default for new endpoint tests** (invariant 1): they exercise the real server implementation and the cassette pins the actual contract, while replay keeps unit runs fast and offline. `respx` is reserved for scenarios the e2e stack can't produce (or only at disproportionate cost); pure-unit covers request-shape and parse logic without any HTTP. The canonical split for a new endpoint: a live-e2e VCR lifecycle test (behaviour, real contract) + pure-unit builder tests (body-vs-query routing, None-omission) — see `test_known_good_lifecycle` + `test/known_good_test.py`. -A third slice is legitimate when the SDK's **transport arm** is what's under test rather than the endpoint. The streaming download path (`_execute_download`) never runs `parse_response` — it reads the error body itself and calls `_raise_for_status` — so its non-2xx mapping is reachable by neither a pure-unit parse test nor a cassette-backed endpoint test that only asserts the caller-visible outcome. Those go on the respx tier: `test_async_download_known_good_refusal_raises_withheld` pins that a refusal envelope arriving on the *streaming* arm still becomes the typed exception with its payload (`.sources`) and leaves nothing written to the destination. +The SDK's own **transport arm** is one of the scenarios respx is reserved for. The streaming download path (`_execute_download`) never runs `parse_response` — it reads the error body itself and calls `_raise_for_status` — so its non-2xx mapping is reachable by neither a pure-unit parse test nor a cassette-backed endpoint test that only asserts the caller-visible outcome. That's still the respx tier, **not a slice of its own**: it rides the parametrised `ClientTestCase` harness like every other respx body (invariant 5). Which is the point — `_execute_download` exists twice (the canonical async source and its generated sync mirror), and a hand-written async-only body would leave the sync arm uncovered. `DownloadRefusalTestCase` (`test/download_refusal_test.py`) is the worked example: one body, auto-emitted `…Sync` / `…Async` siblings, pinning that a refusal envelope arriving on the streaming arm becomes the typed exception with its payload (`.sources`) and leaves nothing written to the destination. ### E2e sample-generation conventions @@ -49,9 +51,10 @@ A third slice is legitimate when the SDK's **transport arm** is what's under tes ## The parametrised `ClientTestCase` harness -Implemented in `test/metadata_field_properties_test.py`. The shape: +Implemented in `test/_client_harness.py`; every `respx` test module imports `ClientTestCase` from it (`metadata_field_properties_test.py` is the canonical example of using it). The shape: ```python +# test/_client_harness.py from polyswarm_api.api import PolyswarmAPI from polyswarm_api.aio import PolySwarmAsyncAPI @@ -84,7 +87,8 @@ class ClientTestCase(TestCase): self.mock.__exit__(None, None, None) -# A concrete test class — same body runs twice. +# A concrete test class in any *_test.py module — same body runs twice. +# from test._client_harness import BASE_URL, ClientTestCase class MetadataFieldPropertiesTestCase(ClientTestCase): def test_get(self): @@ -282,7 +286,7 @@ Decision tree (e2e-first — see invariant 1): 1. **Pure logic test?** (No HTTP, no resource side effects — including request-builder shape and `parse_response`.) → Plain unittest / pytest function. See `jmespath_test.py`, `core_test.py`, `known_good_test.py`. 2. **Endpoint behaviour?** → **VCR-backed live-e2e test — the default.** Write the test against the real endpoint, record once against a fresh e2e stack, commit the cassette. Sync body in `client_scan_test.py`, async in `async_client_test.py`. Need a sample or a sha? Derive it from the test's own EICAR variant (`malicious_artifact(uid)`); anything needing a verdict gets it from the `eicar` engine. -3. **Scenario the e2e stack can't produce** (transport failures, retry exhaustion, cursor pathologies, external systems)? → Parametrised `ClientTestCase` with `_MockBoundary` (`respx`). See `metadata_field_properties_test.py`. Body covers both sync and async automatically. Justify in the test docstring why the scenario can't run on e2e. +3. **Scenario the e2e stack can't produce** (transport failures, retry exhaustion, cursor pathologies, external systems, an SDK transport arm)? → Parametrised `ClientTestCase` with `_MockBoundary` (`respx`), imported from `test/_client_harness.py`. See `metadata_field_properties_test.py` (endpoint shape) and `download_refusal_test.py` (a transport arm). Body covers both sync and async automatically. Justify in the test docstring why the scenario can't run on e2e. Always: diff --git a/specs/05-downstream-contract.md b/specs/05-downstream-contract.md index f5f3be91..e3fb98e8 100644 --- a/specs/05-downstream-contract.md +++ b/specs/05-downstream-contract.md @@ -178,7 +178,7 @@ class InvalidValueException(PolyswarmException): ... class TimeoutException(PolyswarmException): ... ``` -`KnownGoodWithheldException` is the 404 raised when a download is refused because the artifact is a known-good binary — the platform never stores or serves those bytes. It **subclasses `NotFoundException`** precisely so invariant 3 holds for existing consumers: code that already does `except NotFoundException:` keeps catching the refusal with no change, and only callers that want to distinguish "withheld by design" from a plain miss catch the subclass. It adds one attribute, `.sources` — the known-good feeds that flagged the hash (e.g. `['nsrl']`), `[]` when the server named none. The raw envelope stays reachable at `exc.request.errors` (`{'code': 'KNOWN_GOOD', 'known_good': True, 'sources': [...]}`). The artifact's metadata — the flagging feeds plus any scan data already collected — remains readable through the search / instance endpoints; only the bytes are withheld, and the instance's `KNOWN_GOOD` state/status is the signal for that (there is no separate "withheld" field). +`KnownGoodWithheldException` is the 404 raised when a download is refused because the artifact is a known-good binary — the platform never stores or serves those bytes. It **subclasses `NotFoundException`** precisely so invariant 3 holds for existing consumers: code that already does `except NotFoundException:` keeps catching the refusal with no change, and only callers that want to distinguish "withheld by design" from a plain miss catch the subclass. It adds one attribute, `.sources` — the known-good feeds that flagged the hash (e.g. `['nsrl']`), `[]` when the server named none. It is **always a list of strings**: the exception normalises whatever the envelope carried (a bare `'nsrl'` becomes `['nsrl']`; a list keeps only its string entries, so the list-of-feed-dicts shape the instance-level `known_good` field uses doesn't leak through; any other shape becomes `[]`), so `for feed in exc.sources` is safe without a shape check. The raw envelope stays reachable at `exc.request.errors` (`{'code': 'KNOWN_GOOD', 'known_good': True, 'sources': [...]}`). The artifact's metadata — the flagging feeds plus any scan data already collected — remains readable through the search / instance endpoints; only the bytes are withheld, and the instance's `KNOWN_GOOD` state/status is the signal for that (there is no separate "withheld" field). Each `RequestException` subclass carries a `.request` attribute holding the originating `PolyswarmRequest` (set by `RequestException.__init__`). Callers can read `exc.request.status_code`, `exc.request.json` (the parsed response body after execution), `exc.request.input_json` (the body that was sent), `exc.request.request_parameters` (the request kwargs that built the call), etc. `InvalidValueException` and `TimeoutException` are client-side errors and don't carry a request descriptor. diff --git a/src/polyswarm_api/core.py b/src/polyswarm_api/core.py index 7fb6a27f..c9444f8a 100644 --- a/src/polyswarm_api/core.py +++ b/src/polyswarm_api/core.py @@ -360,6 +360,8 @@ def _raise_for_status(response, request): # machine-readable code in the error envelope's ``errors`` slot. Raise the # ``NotFoundException`` subclass so callers can tell "withheld by design" # apart from a plain miss; every other 404 stays a plain NotFoundException. + # The raw ``sources`` payload goes in as-is — the exception normalises it + # into the documented list-of-feed-names shape. errors = request.errors if isinstance(errors, dict) and errors.get('code') == 'KNOWN_GOOD': raise exceptions.KnownGoodWithheldException( diff --git a/src/polyswarm_api/exceptions.py b/src/polyswarm_api/exceptions.py index 137bddbb..d00dd61d 100644 --- a/src/polyswarm_api/exceptions.py +++ b/src/polyswarm_api/exceptions.py @@ -32,6 +32,25 @@ class NotFoundException(RequestException): pass +def _normalise_sources(sources): + """Coerce a server-supplied ``sources`` payload into a list of feed names. + + ``.sources`` is published as a list of strings and consumers iterate it + (``for feed in exc.sources``), so the raw payload can't be handed through + untouched: a bare ``'nsrl'`` would iterate as characters, and the + list-of-feed-dicts shape the instance-level ``known_good`` field uses would + yield dicts. A string becomes a one-element list, a list keeps only its + string entries, and any other shape (mapping, ``None``, number) becomes + ``[]``. Nothing is lost — the raw envelope stays readable at + ``exc.request.errors``. + """ + if isinstance(sources, str): + return [sources] + if isinstance(sources, list): + return [source for source in sources if isinstance(source, str)] + return [] + + class KnownGoodWithheldException(NotFoundException): """404 for a known-good binary: the platform never stores or serves its bytes. @@ -44,9 +63,10 @@ class KnownGoodWithheldException(NotFoundException): def __init__(self, request, *args, sources=None): super().__init__(request, *args) - # Known-good feeds that flagged the hash (e.g. ``['nsrl']``); empty list - # when the server named none. - self.sources = sources or [] + # Known-good feeds that flagged the hash (e.g. ``['nsrl']``); always a list + # of strings — normalised here, at the boundary, so the documented shape + # holds whatever the envelope carried. Empty when the server named none. + self.sources = _normalise_sources(sources) class NoResultsException(RequestException): diff --git a/test/_client_harness.py b/test/_client_harness.py new file mode 100644 index 00000000..80139bc0 --- /dev/null +++ b/test/_client_harness.py @@ -0,0 +1,131 @@ +"""The parametrised ``ClientTestCase`` harness for the respx-mocked tier. + +Shared by every respx test module (see specs/04-testing.md, invariant 5): one +test body runs against **both** transports, because ``__init_subclass__`` emits +``Sync`` and ``Async`` siblings for each subclass. That's what keeps +the mocked tier from growing parallel sync / async bodies — and what keeps the +generated sync mirrors (``session.py`` / ``api.py``) covered by the same +assertions as their canonical async sources. + +Not collected by pytest itself (``python_files`` only matches ``*_test.py`` / +``test_*.py``), same as ``_e2e_helpers.py``. ``metadata_field_properties_test.py`` +is the canonical example of using it. +""" +import asyncio +import json +from unittest import TestCase + +import httpx +import respx + +from polyswarm_api.aio import PolySwarmAsyncAPI +from polyswarm_api.api import PolyswarmAPI + + +BASE_URL = 'http://localhost:9696/v3' +API_KEY = '1' * 32 +COMMUNITY = 'gamma' + + +class _MockBoundary: + """Register expected HTTP exchanges via ``respx``. Both sync and + async clients now run on httpx, so a single mock library covers + both. Tests call ``add(method, url, json=..., status=...)``. + """ + + def __init__(self, client_kind: str): + self.client_kind = client_kind + self._router = respx.mock(assert_all_called=False) + + def __enter__(self): + self._router.start() + return self + + def __exit__(self, *exc): + self._router.stop() + self._router.reset() + + def add(self, method: str, url: str, json: dict, status: int = 200): + self._router.route(method=method, url=url).mock( + return_value=httpx.Response(status, json=json), + ) + + @property + def last_request_url(self) -> str: + return str(self._router.calls[0].request.url) + + @property + def last_request_body(self): + """The JSON body of the most recent request (None if it carried none).""" + content = self._router.calls[-1].request.content + return json.loads(content) if content else None + + +class _AsyncToSync: + """Run async client methods from sync test bodies via ``asyncio.run``. + + Each attribute access returns a sync callable that drives the + matching async method on a fresh event loop per call — keeps the + unittest-style test bodies unchanged. + """ + + def __init__(self, async_api: PolySwarmAsyncAPI): + self._api = async_api + + def __getattr__(self, name): + if name.startswith('_'): + raise AttributeError(name) + attr = getattr(self._api, name) + + def sync_call(*args, **kwargs): + result = attr(*args, **kwargs) + if asyncio.iscoroutine(result): + return asyncio.run(result) + # Async generator → drain it. + async def _drain(): + return [item async for item in result] + return asyncio.run(_drain()) + + return sync_call + + +class ClientTestCase(TestCase): + """Base test case. Each subclass is auto-replaced by ``Sync`` + and ``Async`` siblings so every test method runs once against + each client. The original subclass is hidden from pytest. + """ + + _client_kind = 'sync' + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + if getattr(cls, '_client_kind_variant', False): + return + import sys + module = sys.modules.get(cls.__module__) + if module is None: + return + cls.__test__ = False + for label, kind in (('Sync', 'sync'), ('Async', 'async')): + variant_name = f'{cls.__name__}{label}' + variant = type(variant_name, (cls,), { + '_client_kind': kind, + '_client_kind_variant': True, + '__test__': True, + '__module__': cls.__module__, + '__qualname__': variant_name, + }) + setattr(module, variant_name, variant) + + def setUp(self): + if self._client_kind == 'sync': + self.api = PolyswarmAPI(API_KEY, uri=BASE_URL, community=COMMUNITY) + else: + self.api = _AsyncToSync( + PolySwarmAsyncAPI(API_KEY, uri=BASE_URL, community=COMMUNITY), + ) + self.mock = _MockBoundary(self._client_kind) + self.mock.__enter__() + + def tearDown(self): + self.mock.__exit__(None, None, None) diff --git a/test/async_client_test.py b/test/async_client_test.py index efd3fbf3..266afa32 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -1014,42 +1014,6 @@ async def test_async_download_204_raises_no_results(): await api.aclose() -@respx.mock -async def test_async_download_known_good_refusal_raises_withheld(): - """A refused download is what ``KnownGoodWithheldException`` exists for, and a - refusal arrives on the **streaming** arm (``_execute_download`` reads the small - error body, then hands it to the shared ``_raise_for_status``) — not through - ``parse_response``. Pure-unit coverage of the envelope can't reach that arm, so - pin it here: the typed exception must come out of ``download`` carrying the - flagging feeds, and nothing may be written to the destination folder (a refusal - must not leave a truncated or empty artifact behind, the way a 204 must not). - - respx rather than the live e2e stack because the refusal is a *transport-shape* - assertion on the streaming path; the endpoint behaviour itself belongs to the - VCR-backed lifecycle test. - """ - respx.get(f'{BASE_URL}/consumer/download/sha256/{SHA256}').mock( - return_value=httpx.Response(404, json={ - 'status': 'error', - 'result': 'Unable to download the provided artifact, it is a ' - 'known-good binary; its bytes are withheld by design.', - 'errors': {'code': 'KNOWN_GOOD', 'known_good': True, - 'sources': ['nsrl']}, - })) - - api = PolySwarmAsyncAPI(API_KEY, uri=BASE_URL, community='gamma') - try: - with tempfile.TemporaryDirectory() as tmp_dir: - with pytest.raises(exceptions.KnownGoodWithheldException) as ei: - await api.download(tmp_dir, SHA256) - assert ei.value.sources == ['nsrl'] - # Still a NotFoundException, so existing handlers keep catching it. - assert isinstance(ei.value, exceptions.NotFoundException) - assert os.listdir(tmp_dir) == [] - finally: - await api.aclose() - - @respx.mock async def test_async_download_streams_in_chunks(monkeypatch): """Regression guard for the streaming-download fix: the body is consumed via diff --git a/test/core_test.py b/test/core_test.py index cc651db5..dcec08fc 100644 --- a/test/core_test.py +++ b/test/core_test.py @@ -292,6 +292,40 @@ def test_404_known_good_withheld_without_sources(self): ) assert ei.value.sources == [] + def test_404_known_good_sources_normalised_to_feed_names(self): + # Regression: the raise site passed the envelope's ``sources`` payload + # straight through, so any shape but a list of strings reached the caller + # — a bare 'nsrl' iterated as characters ('n', 's', 'r', 'l'), and the + # list-of-feed-dicts shape the instance-level ``known_good`` field uses + # iterated as dicts. ``.sources`` is documented as a list of feed-name + # strings, so normalise at the boundary: string → one-element list, list → + # its string entries, anything else → []. + shapes = [ + (['nsrl', 'commercial'], ['nsrl', 'commercial']), # already correct + ('nsrl', ['nsrl']), # bare string + ([{'tool': 'nsrl'}], []), # feed-dict shape + (['nsrl', {'tool': 'other'}], ['nsrl']), # mixed list + ({'tool': 'nsrl'}, []), # mapping + (None, []), # explicit null + (7, []), # nonsense + ] + for payload, expected in shapes: + errors = {'code': 'KNOWN_GOOD', 'known_good': True, 'sources': payload} + req = PolyswarmRequest(api=_FakeApi(), method='GET', url='u', + result_parser=_SampleResource) + with pytest.raises(exceptions.KnownGoodWithheldException) as ei: + parse_response( + _FakeResponse(status_code=404, body={ + 'status': 'error', 'result': 'withheld', 'errors': errors, + }), + req, + ) + assert ei.value.sources == expected, payload + # Whatever the shape, iterating yields feed-name strings... + assert all(isinstance(source, str) for source in ei.value.sources) + # ...and the raw payload is still reachable for callers that want it. + assert req.errors['sources'] == payload + def test_404_other_error_code_stays_plain_not_found(self): # Only the known-good code gets the subclass; every other 404 — including # a differently-coded or legacy list-shaped ``errors`` payload — stays a diff --git a/test/download_refusal_test.py b/test/download_refusal_test.py new file mode 100644 index 00000000..9a9d5636 --- /dev/null +++ b/test/download_refusal_test.py @@ -0,0 +1,58 @@ +"""Refusal handling on the streaming-download arm, both transports. + +respx rather than the live e2e stack (specs/04-testing.md, invariant 1): the +scenario under test is the SDK's *transport arm*, not the endpoint. A download +refusal never reaches ``parse_response`` — ``_execute_download`` reads the small +error body itself and hands it to the shared ``_raise_for_status`` — so neither a +pure-unit parse test nor a cassette-backed endpoint test that asserts only the +caller-visible outcome exercises that code path. Endpoint behaviour against the +real server stays with the live-e2e VCR tests. + +On the ``ClientTestCase`` harness (invariant 5) so the one body runs against +``PolyswarmAPI`` **and** ``PolySwarmAsyncAPI``: ``_execute_download`` exists twice +(the canonical async source and its generated sync mirror), and the sync arm is +exactly the one a hand-written async-only respx body leaves uncovered. +""" +import os +import tempfile + +from polyswarm_api import exceptions + +from test._client_harness import BASE_URL, ClientTestCase + + +SHA256 = 'a' * 64 +_DOWNLOAD_URL = f'{BASE_URL}/consumer/download/sha256/{SHA256}' + +# The server's refusal envelope: a 404 whose machine-readable ``errors`` code says +# the bytes are withheld because the artifact is a known-good binary. +_REFUSAL_BODY = { + 'status': 'error', + 'result': 'Unable to download the provided artifact, it is a known-good ' + 'binary; its bytes are withheld by design.', + 'errors': {'code': 'KNOWN_GOOD', 'known_good': True, 'sources': ['nsrl']}, +} + + +class DownloadRefusalTestCase(ClientTestCase): + def test_download_known_good_refusal_raises_withheld(self): + # Regression: the refusal arrives on the streaming arm, which bypasses + # ``parse_response`` entirely. Without the shared ``_raise_for_status`` + # mapping being reached there, a refused download surfaces as a bare + # ``NotFoundException`` (no ``.sources``, indistinguishable from a plain + # miss) — or, worse, gets written out as an artifact file holding the + # error JSON. Pin both: the typed exception with its payload, and an + # untouched destination folder. + self.mock.add('GET', _DOWNLOAD_URL, json=_REFUSAL_BODY, status=404) + with tempfile.TemporaryDirectory() as tmp_dir: + with self.assertRaises(exceptions.KnownGoodWithheldException) as caught: + self.api.download(tmp_dir, SHA256) + exc = caught.exception + assert exc.sources == ['nsrl'] + # Still a NotFoundException, so existing handlers keep catching it. + assert isinstance(exc, exceptions.NotFoundException) + # The raw envelope stays reachable for callers that want the rest. + assert exc.request.errors == _REFUSAL_BODY['errors'] + # A refusal must leave nothing behind — no empty file, no error JSON + # written out as if it were the artifact. + assert os.listdir(tmp_dir) == [] diff --git a/test/metadata_field_properties_test.py b/test/metadata_field_properties_test.py index 3de91adc..8ad28742 100644 --- a/test/metadata_field_properties_test.py +++ b/test/metadata_field_properties_test.py @@ -3,24 +3,15 @@ Each test method runs twice — once against the sync ``PolyswarmAPI`` and once against the async ``PolySwarmAsyncAPI``. Both transports are ``httpx``-backed, so the HTTP boundary is mocked by ``respx`` in either -case. The parametrisation is automatic: ``ClientTestCase`` 's -``__init_subclass__`` hook emits ``Sync`` and ``Async`` -sibling classes for every subclass declared. The base subclass is -hidden from pytest via ``__test__ = False``. +case. The parametrisation is automatic: ``ClientTestCase`` (shared harness +in ``test/_client_harness.py``) emits ``Sync`` and ``Async`` +sibling classes for every subclass declared, and hides the base subclass +from pytest via ``__test__ = False``. """ -import asyncio -import json -from unittest import TestCase +from test._client_harness import BASE_URL, ClientTestCase -import httpx -import respx -from polyswarm_api.aio import PolySwarmAsyncAPI -from polyswarm_api.api import PolyswarmAPI - - -_BASE_URL = 'http://localhost:9696/v3' -_RESOURCE_URL = f'{_BASE_URL}/search/metadata/properties' +_RESOURCE_URL = f'{BASE_URL}/search/metadata/properties' def _sample_row(field_path='polyunite.malware_family'): @@ -35,113 +26,6 @@ def _sample_row(field_path='polyunite.malware_family'): } -# ── Per-test mocking + client construction harness ─────────────────── - - -class _MockBoundary: - """Register expected HTTP exchanges via ``respx``. Both sync and - async clients now run on httpx, so a single mock library covers - both. Tests call ``add(method, url, json=..., status=...)``. - """ - - def __init__(self, client_kind: str): - self.client_kind = client_kind - self._router = respx.mock(assert_all_called=False) - - def __enter__(self): - self._router.start() - return self - - def __exit__(self, *exc): - self._router.stop() - self._router.reset() - - def add(self, method: str, url: str, json: dict, status: int = 200): - self._router.route(method=method, url=url).mock( - return_value=httpx.Response(status, json=json), - ) - - @property - def last_request_url(self) -> str: - return str(self._router.calls[0].request.url) - - @property - def last_request_body(self): - """The JSON body of the most recent request (None if it carried none).""" - content = self._router.calls[-1].request.content - return json.loads(content) if content else None - - -class _AsyncToSync: - """Run async client methods from sync test bodies via ``asyncio.run``. - - Each attribute access returns a sync callable that drives the - matching async method on a fresh event loop per call — keeps the - unittest-style test bodies unchanged. - """ - - def __init__(self, async_api: PolySwarmAsyncAPI): - self._api = async_api - - def __getattr__(self, name): - if name.startswith('_'): - raise AttributeError(name) - attr = getattr(self._api, name) - - def sync_call(*args, **kwargs): - result = attr(*args, **kwargs) - if asyncio.iscoroutine(result): - return asyncio.run(result) - # Async generator → drain it. - async def _drain(): - return [item async for item in result] - return asyncio.run(_drain()) - - return sync_call - - -class ClientTestCase(TestCase): - """Base test case. Each subclass is auto-replaced by ``Sync`` - and ``Async`` siblings so every test method runs once against - each client. The original subclass is hidden from pytest. - """ - - _client_kind = 'sync' - - def __init_subclass__(cls, **kwargs): - super().__init_subclass__(**kwargs) - if getattr(cls, '_client_kind_variant', False): - return - import sys - module = sys.modules.get(cls.__module__) - if module is None: - return - cls.__test__ = False - for label, kind in (('Sync', 'sync'), ('Async', 'async')): - variant_name = f'{cls.__name__}{label}' - variant = type(variant_name, (cls,), { - '_client_kind': kind, - '_client_kind_variant': True, - '__test__': True, - '__module__': cls.__module__, - '__qualname__': variant_name, - }) - setattr(module, variant_name, variant) - - def setUp(self): - if self._client_kind == 'sync': - self.api = PolyswarmAPI('1' * 32, uri=_BASE_URL, community='gamma') - else: - self.api = _AsyncToSync( - PolySwarmAsyncAPI('1' * 32, uri=_BASE_URL, community='gamma'), - ) - self.mock = _MockBoundary(self._client_kind) - self.mock.__enter__() - - def tearDown(self): - self.mock.__exit__(None, None, None) - - # ── Tests ──────────────────────────────────────────────────────────── From ef72cc13f80b51f1c672a30efb9171530e641b8c Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 29 Jul 2026 15:42:05 -0300 Subject: [PATCH 05/20] test: pin the refusal contract against the live server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The suite asserted the error envelope only against bodies it fabricated itself, which asserts what we think the server sends. This PR's own history is the failure mode: the fabricated body said KNOWN_GOOD_WITHHELD while the server said KNOWN_GOOD, and nothing in the suite noticed. Both known-good lifecycle tests (sync + async) now drive the real endpoint: - download() raises KnownGoodWithheldException and .sources names the flagging feeds, re-checked after a second feed extends the entry - exists(require_scan=True) is True — semantics documented in four places with no coverage until now Cassettes re-recorded against a live stack; the recordings confirm the server's shape is a list of feed-name strings. Also harden _normalise_sources: extract feed['tool'] from dict entries instead of dropping them. That is the other shape the platform uses for this concept (the instance-level known_good field), and silently emptying .sources would take the exception's whole added value with it. --- specs/05-downstream-contract.md | 2 +- specs/99-open-questions.md | 2 +- src/polyswarm_api/exceptions.py | 28 +++- test/async_client_test.py | 16 ++ test/client_scan_test.py | 21 +++ test/core_test.py | 17 +- test/vcr/test_async_known_good_lifecycle.vcr | 156 +++++++++++++++++-- test/vcr/test_known_good_lifecycle.vcr | 150 ++++++++++++++++-- 8 files changed, 350 insertions(+), 42 deletions(-) diff --git a/specs/05-downstream-contract.md b/specs/05-downstream-contract.md index e3fb98e8..9103d4dd 100644 --- a/specs/05-downstream-contract.md +++ b/specs/05-downstream-contract.md @@ -178,7 +178,7 @@ class InvalidValueException(PolyswarmException): ... class TimeoutException(PolyswarmException): ... ``` -`KnownGoodWithheldException` is the 404 raised when a download is refused because the artifact is a known-good binary — the platform never stores or serves those bytes. It **subclasses `NotFoundException`** precisely so invariant 3 holds for existing consumers: code that already does `except NotFoundException:` keeps catching the refusal with no change, and only callers that want to distinguish "withheld by design" from a plain miss catch the subclass. It adds one attribute, `.sources` — the known-good feeds that flagged the hash (e.g. `['nsrl']`), `[]` when the server named none. It is **always a list of strings**: the exception normalises whatever the envelope carried (a bare `'nsrl'` becomes `['nsrl']`; a list keeps only its string entries, so the list-of-feed-dicts shape the instance-level `known_good` field uses doesn't leak through; any other shape becomes `[]`), so `for feed in exc.sources` is safe without a shape check. The raw envelope stays reachable at `exc.request.errors` (`{'code': 'KNOWN_GOOD', 'known_good': True, 'sources': [...]}`). The artifact's metadata — the flagging feeds plus any scan data already collected — remains readable through the search / instance endpoints; only the bytes are withheld, and the instance's `KNOWN_GOOD` state/status is the signal for that (there is no separate "withheld" field). +`KnownGoodWithheldException` is the 404 raised when a download is refused because the artifact is a known-good binary — the platform never stores or serves those bytes. It **subclasses `NotFoundException`** precisely so invariant 3 holds for existing consumers: code that already does `except NotFoundException:` keeps catching the refusal with no change, and only callers that want to distinguish "withheld by design" from a plain miss catch the subclass. It adds one attribute, `.sources` — the known-good feeds that flagged the hash (e.g. `['nsrl']`), `[]` when the server named none. It is **always a list of strings**: the exception normalises whatever the envelope carried, so `for feed in exc.sources` is safe without a shape check. A bare `'nsrl'` becomes `['nsrl']`; any non-list shape (mapping, number, `null`) becomes `[]`. Within a list, **both** shapes the platform uses for this concept yield feed names — plain strings, and the feed dicts the instance-level `known_good` field carries (unpacked via `feed['tool']`, mirroring `ArtifactInstance.known_good_sources`). Discarding the dict shape would mean that if the envelope ever reused that serialiser, `.sources` would silently empty out, leaving a caller unable to tell "the server named no feeds" from "the server named feeds we dropped". The raw envelope stays reachable at `exc.request.errors` (`{'code': 'KNOWN_GOOD', 'known_good': True, 'sources': [...]}`). The artifact's metadata — the flagging feeds plus any scan data already collected — remains readable through the search / instance endpoints; only the bytes are withheld, and the instance's `KNOWN_GOOD` state/status is the signal for that (there is no separate "withheld" field). Each `RequestException` subclass carries a `.request` attribute holding the originating `PolyswarmRequest` (set by `RequestException.__init__`). Callers can read `exc.request.status_code`, `exc.request.json` (the parsed response body after execution), `exc.request.input_json` (the body that was sent), `exc.request.request_parameters` (the request kwargs that built the call), etc. `InvalidValueException` and `TimeoutException` are client-side errors and don't carry a request descriptor. diff --git a/specs/99-open-questions.md b/specs/99-open-questions.md index 22b2469c..5e1fb91c 100644 --- a/specs/99-open-questions.md +++ b/specs/99-open-questions.md @@ -8,7 +8,7 @@ Known follow-ups and unresolved questions that haven't been decided yet. Each it **Status:** partial. -The parametrised `ClientTestCase` harness in `test/metadata_field_properties_test.py` is the canonical pattern for running each test against both sync and async clients. `client_scan_test.py` (sync) and `async_client_test.py` (async) still have parallel test bodies for the bulk of the endpoint surface. +The parametrised `ClientTestCase` harness in `test/_client_harness.py` (originally written in, and still exercised by, `test/metadata_field_properties_test.py`) is the canonical pattern for running each test against both sync and async clients. `client_scan_test.py` (sync) and `async_client_test.py` (async) still have parallel test bodies for the bulk of the endpoint surface. **Action:** migrate each test pair into a `ClientTestCase` subclass, delete the original sync / async copies, re-record cassettes where the request shape differs. Mechanical work, ~one chunk per resource family. diff --git a/src/polyswarm_api/exceptions.py b/src/polyswarm_api/exceptions.py index d00dd61d..da30a9d4 100644 --- a/src/polyswarm_api/exceptions.py +++ b/src/polyswarm_api/exceptions.py @@ -37,17 +37,31 @@ def _normalise_sources(sources): ``.sources`` is published as a list of strings and consumers iterate it (``for feed in exc.sources``), so the raw payload can't be handed through - untouched: a bare ``'nsrl'`` would iterate as characters, and the - list-of-feed-dicts shape the instance-level ``known_good`` field uses would - yield dicts. A string becomes a one-element list, a list keeps only its - string entries, and any other shape (mapping, ``None``, number) becomes - ``[]``. Nothing is lost — the raw envelope stays readable at - ``exc.request.errors``. + untouched: a bare ``'nsrl'`` would iterate as characters. + + Both shapes the platform uses for this one concept yield feed names. The + error envelope sends a list of strings; the instance-level ``known_good`` + field sends a list of feed dicts, which ``resources.py`` unpacks via + ``feed['tool']`` into ``ArtifactInstance.known_good_sources``. If the + envelope ever reuses that serialiser, dropping the dicts would empty + ``.sources`` and take this exception's whole added value with it — with no + diagnostic, and no way for a caller to tell "the server named no feeds" + from "the server named feeds in a shape we discarded". + + A string becomes a one-element list; anything else (mapping, ``None``, + number) becomes ``[]``. Nothing is lost either way — the raw envelope stays + readable at ``exc.request.errors``. """ if isinstance(sources, str): return [sources] if isinstance(sources, list): - return [source for source in sources if isinstance(source, str)] + names = [] + for source in sources: + if isinstance(source, str): + names.append(source) + elif isinstance(source, dict) and isinstance(source.get('tool'), str): + names.append(source['tool']) + return names return [] diff --git a/test/async_client_test.py b/test/async_client_test.py index 266afa32..dc7d017e 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -408,6 +408,17 @@ async def test_async_known_good_lifecycle(self, uid): assert created.sha256 == sha assert created.sources == ['nsrl'] assert created.artifact_instance_id + # The refusal on the async transport, against the real server. It reaches the + # 404 arm by a different route than the sync parse path — the streaming + # download raises from `aread()` → `_raise_for_status` — so both transports + # need the live assertion, not just the fabricated respx one. + with tempfile.TemporaryDirectory() as out_dir: + with pytest.raises(exceptions.KnownGoodWithheldException) as ei: + await api.download(out_dir, sha) + assert ei.value.sources == ['nsrl'] + # The require_scan semantics this SDK documents: a known-good hash is a decided + # terminal record, so it reports present rather than "not scanned yet". + assert await api.exists(sha, hash_type='sha256', require_scan=True) is True # A second feed flagging the same sha extends the same entry (no new row). extended = await api.known_good_create(sha256=sha, source='commercial') assert extended.id == created.id @@ -415,6 +426,11 @@ async def test_async_known_good_lifecycle(self, uid): got = await api.known_good_get(sha256=sha) assert got.sha256 == sha assert sorted(got.sources) == ['commercial', 'nsrl'] + # Both feeds name themselves in the refusal once the entry is extended. + with tempfile.TemporaryDirectory() as out_dir: + with pytest.raises(exceptions.KnownGoodWithheldException) as ei: + await api.download(out_dir, sha) + assert sorted(ei.value.sources) == ['commercial', 'nsrl'] deleted = await api.known_good_delete(sha256=sha) assert deleted.sha256 == sha with pytest.raises(exceptions.NotFoundException): diff --git a/test/client_scan_test.py b/test/client_scan_test.py index 44d7dc0f..56142334 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -774,6 +774,21 @@ def test_known_good_lifecycle(self): assert created.sha256 == sha assert created.sources == ['nsrl'] assert created.artifact_instance_id + # The refusal, against the real server. Every assertion about the error envelope + # elsewhere in the suite reads a body this repo fabricated, which asserts what we + # *think* the server sends: a rename on the server side (KNOWN_GOOD_WITHHELD → + # KNOWN_GOOD, which really happened mid-review) would leave the fabricated tests + # green. Here the caller-visible outcome IS the contract — the typed exception and + # the feed names it carries. + with tempfile.TemporaryDirectory() as out_dir: + with pytest.raises(exceptions.KnownGoodWithheldException) as ei: + v3api.download(out_dir, sha) + assert ei.value.sources == ['nsrl'] + # ...and the hash still reports PRESENT under require_scan, which is the semantics + # the SDK now documents in four places: a known-good record is decided and terminal, + # so "has it been scanned" must not answer "no, go and scan it" for a sample the + # platform will never scan. + assert v3api.exists(sha, hash_type='sha256', require_scan=True) is True # A second feed flagging the same sha extends the same entry (no new row). extended = v3api.known_good_create(sha256=sha, source='commercial') assert extended.id == created.id @@ -781,6 +796,12 @@ def test_known_good_lifecycle(self): got = v3api.known_good_get(sha256=sha) assert got.sha256 == sha assert sorted(got.sources) == ['commercial', 'nsrl'] + # Both feeds now name themselves in the refusal — the exception's .sources tracks + # the catalogue rather than being a snapshot from the first flagging. + with tempfile.TemporaryDirectory() as out_dir: + with pytest.raises(exceptions.KnownGoodWithheldException) as ei: + v3api.download(out_dir, sha) + assert sorted(ei.value.sources) == ['commercial', 'nsrl'] deleted = v3api.known_good_delete(sha256=sha) assert deleted.sha256 == sha with pytest.raises(exceptions.NotFoundException): diff --git a/test/core_test.py b/test/core_test.py index dcec08fc..69ab08f0 100644 --- a/test/core_test.py +++ b/test/core_test.py @@ -299,13 +299,22 @@ def test_404_known_good_sources_normalised_to_feed_names(self): # list-of-feed-dicts shape the instance-level ``known_good`` field uses # iterated as dicts. ``.sources`` is documented as a list of feed-name # strings, so normalise at the boundary: string → one-element list, list → - # its string entries, anything else → []. + # feed names from either shape, anything else → []. + # + # The feed-dict entries yield names rather than being dropped: that is the + # OTHER shape the platform uses for this same concept (the instance-level + # ``known_good`` field, unpacked via ``feed['tool']`` into + # ``known_good_sources``). If the envelope ever reuses that serialiser, + # discarding it would silently empty ``.sources`` and take the exception's + # whole added value with it. shapes = [ (['nsrl', 'commercial'], ['nsrl', 'commercial']), # already correct ('nsrl', ['nsrl']), # bare string - ([{'tool': 'nsrl'}], []), # feed-dict shape - (['nsrl', {'tool': 'other'}], ['nsrl']), # mixed list - ({'tool': 'nsrl'}, []), # mapping + ([{'tool': 'nsrl'}], ['nsrl']), # feed-dict shape + ([{'tool': 'nsrl'}, {'tool': 'commercial'}], ['nsrl', 'commercial']), + (['nsrl', {'tool': 'other'}], ['nsrl', 'other']), # mixed list + ([{'tool': None}, {}], []), # dicts naming nothing + ({'tool': 'nsrl'}, []), # a mapping, not a list (None, []), # explicit null (7, []), # nonsense ] diff --git a/test/vcr/test_async_known_good_lifecycle.vcr b/test/vcr/test_async_known_good_lifecycle.vcr index f9f28cf2..393fce67 100644 --- a/test/vcr/test_async_known_good_lifecycle.vcr +++ b/test/vcr/test_async_known_good_lifecycle.vcr @@ -17,12 +17,12 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.1.0 (x86_64-Linux-CPython-3.12.3) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) method: POST uri: http://artifact-index-e2e:9696/v3/known-good response: body: - string: '{"result":{"artifact_instance_id":"24417037636304243","created":"2026-06-29T18:29:53.872924+00:00","id":"7418765811658834","sha256":"d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b","sources":["nsrl"]},"status":"OK"} + string: '{"result":{"artifact_instance_id":"99003580612834285","created":"2026-07-29T18:39:47.728827+00:00","id":"21743034095682783","sha256":"d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b","sources":["nsrl"]},"status":"OK"} ' headers: @@ -33,11 +33,93 @@ interactions: Connection: - keep-alive Content-Length: - - '234' + - '235' Content-Type: - application/json Date: - - Mon, 29 Jun 2026 18:29:53 GMT + - Wed, 29 Jul 2026 18:39:47 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/download/sha256/d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b?community=gamma + response: + body: + string: '{"errors":{"code":"KNOWN_GOOD","known_good":true,"sources":["nsrl"]},"result":"Unable + to download the provided artifact, it is a known-good binary; its bytes are + withheld by design.","status":"error"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '201' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 18:39:47 GMT + Server: + - gunicorn + status: + code: 404 + message: NOT FOUND +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: HEAD + uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b&community=gamma&require_scan=true + response: + body: + string: '' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '57' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 18:39:47 GMT Server: - gunicorn X-Billing-ID: @@ -63,12 +145,12 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.1.0 (x86_64-Linux-CPython-3.12.3) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) method: POST uri: http://artifact-index-e2e:9696/v3/known-good response: body: - string: '{"result":{"artifact_instance_id":"24417037636304243","created":"2026-06-29T18:29:53.872924+00:00","id":"7418765811658834","sha256":"d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b","sources":["commercial","nsrl"]},"status":"OK"} + string: '{"result":{"artifact_instance_id":"99003580612834285","created":"2026-07-29T18:39:47.728827+00:00","id":"21743034095682783","sha256":"d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b","sources":["commercial","nsrl"]},"status":"OK"} ' headers: @@ -79,11 +161,11 @@ interactions: Connection: - keep-alive Content-Length: - - '247' + - '248' Content-Type: - application/json Date: - - Mon, 29 Jun 2026 18:29:54 GMT + - Wed, 29 Jul 2026 18:39:47 GMT Server: - gunicorn X-Billing-ID: @@ -105,12 +187,12 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.1.0 (x86_64-Linux-CPython-3.12.3) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) method: GET uri: http://artifact-index-e2e:9696/v3/known-good?sha256=d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b&community=gamma response: body: - string: '{"result":{"artifact_instance_id":"24417037636304243","created":"2026-06-29T18:29:53.872924+00:00","id":"7418765811658834","sha256":"d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b","sources":["commercial","nsrl"]},"status":"OK"} + string: '{"result":{"artifact_instance_id":"99003580612834285","created":"2026-07-29T18:39:47.728827+00:00","id":"21743034095682783","sha256":"d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b","sources":["commercial","nsrl"]},"status":"OK"} ' headers: @@ -121,11 +203,11 @@ interactions: Connection: - keep-alive Content-Length: - - '247' + - '248' Content-Type: - application/json Date: - - Mon, 29 Jun 2026 18:29:54 GMT + - Wed, 29 Jul 2026 18:39:47 GMT Server: - gunicorn X-Billing-ID: @@ -147,7 +229,49 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.1.0 (x86_64-Linux-CPython-3.12.3) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/download/sha256/d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b?community=gamma + response: + body: + string: '{"errors":{"code":"KNOWN_GOOD","known_good":true,"sources":["commercial","nsrl"]},"result":"Unable + to download the provided artifact, it is a known-good binary; its bytes are + withheld by design.","status":"error"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '214' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 18:39:47 GMT + Server: + - gunicorn + status: + code: 404 + message: NOT FOUND +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) method: DELETE uri: http://artifact-index-e2e:9696/v3/known-good?sha256=d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b response: @@ -167,7 +291,7 @@ interactions: Content-Type: - application/json Date: - - Mon, 29 Jun 2026 18:29:54 GMT + - Wed, 29 Jul 2026 18:39:47 GMT Server: - gunicorn X-Billing-ID: @@ -189,7 +313,7 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.1.0 (x86_64-Linux-CPython-3.12.3) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) method: GET uri: http://artifact-index-e2e:9696/v3/known-good?sha256=d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b&community=gamma response: @@ -209,7 +333,7 @@ interactions: Content-Type: - application/json Date: - - Mon, 29 Jun 2026 18:29:54 GMT + - Wed, 29 Jul 2026 18:39:47 GMT Server: - gunicorn status: diff --git a/test/vcr/test_known_good_lifecycle.vcr b/test/vcr/test_known_good_lifecycle.vcr index e1e93780..215d866b 100644 --- a/test/vcr/test_known_good_lifecycle.vcr +++ b/test/vcr/test_known_good_lifecycle.vcr @@ -17,12 +17,12 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.1.0 (x86_64-Linux-CPython-3.12.3) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) method: POST uri: http://artifact-index-e2e:9696/v3/known-good response: body: - string: '{"result":{"artifact_instance_id":"34363762993756881","created":"2026-06-29T18:30:01.431502+00:00","id":"25565696800926378","sha256":"9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df","sources":["nsrl"]},"status":"OK"} + string: '{"result":{"artifact_instance_id":"90039023884233228","created":"2026-07-29T18:39:47.299724+00:00","id":"66682229801759442","sha256":"9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df","sources":["nsrl"]},"status":"OK"} ' headers: @@ -37,7 +37,89 @@ interactions: Content-Type: - application/json Date: - - Mon, 29 Jun 2026 18:30:01 GMT + - Wed, 29 Jul 2026 18:39:47 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/download/sha256/9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df?community=gamma + response: + body: + string: '{"errors":{"code":"KNOWN_GOOD","known_good":true,"sources":["nsrl"]},"result":"Unable + to download the provided artifact, it is a known-good binary; its bytes are + withheld by design.","status":"error"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '201' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 18:39:47 GMT + Server: + - gunicorn + status: + code: 404 + message: NOT FOUND +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: HEAD + uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df&community=gamma&require_scan=true + response: + body: + string: '' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '57' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 18:39:47 GMT Server: - gunicorn X-Billing-ID: @@ -63,12 +145,12 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.1.0 (x86_64-Linux-CPython-3.12.3) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) method: POST uri: http://artifact-index-e2e:9696/v3/known-good response: body: - string: '{"result":{"artifact_instance_id":"34363762993756881","created":"2026-06-29T18:30:01.431502+00:00","id":"25565696800926378","sha256":"9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df","sources":["commercial","nsrl"]},"status":"OK"} + string: '{"result":{"artifact_instance_id":"90039023884233228","created":"2026-07-29T18:39:47.299724+00:00","id":"66682229801759442","sha256":"9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df","sources":["commercial","nsrl"]},"status":"OK"} ' headers: @@ -83,7 +165,7 @@ interactions: Content-Type: - application/json Date: - - Mon, 29 Jun 2026 18:30:01 GMT + - Wed, 29 Jul 2026 18:39:47 GMT Server: - gunicorn X-Billing-ID: @@ -105,12 +187,12 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.1.0 (x86_64-Linux-CPython-3.12.3) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) method: GET uri: http://artifact-index-e2e:9696/v3/known-good?sha256=9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df&community=gamma response: body: - string: '{"result":{"artifact_instance_id":"34363762993756881","created":"2026-06-29T18:30:01.431502+00:00","id":"25565696800926378","sha256":"9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df","sources":["commercial","nsrl"]},"status":"OK"} + string: '{"result":{"artifact_instance_id":"90039023884233228","created":"2026-07-29T18:39:47.299724+00:00","id":"66682229801759442","sha256":"9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df","sources":["commercial","nsrl"]},"status":"OK"} ' headers: @@ -125,7 +207,7 @@ interactions: Content-Type: - application/json Date: - - Mon, 29 Jun 2026 18:30:01 GMT + - Wed, 29 Jul 2026 18:39:47 GMT Server: - gunicorn X-Billing-ID: @@ -147,7 +229,49 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.1.0 (x86_64-Linux-CPython-3.12.3) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/download/sha256/9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df?community=gamma + response: + body: + string: '{"errors":{"code":"KNOWN_GOOD","known_good":true,"sources":["commercial","nsrl"]},"result":"Unable + to download the provided artifact, it is a known-good binary; its bytes are + withheld by design.","status":"error"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '214' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 18:39:47 GMT + Server: + - gunicorn + status: + code: 404 + message: NOT FOUND +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) method: DELETE uri: http://artifact-index-e2e:9696/v3/known-good?sha256=9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df response: @@ -167,7 +291,7 @@ interactions: Content-Type: - application/json Date: - - Mon, 29 Jun 2026 18:30:01 GMT + - Wed, 29 Jul 2026 18:39:47 GMT Server: - gunicorn X-Billing-ID: @@ -189,7 +313,7 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.1.0 (x86_64-Linux-CPython-3.12.3) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) method: GET uri: http://artifact-index-e2e:9696/v3/known-good?sha256=9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df&community=gamma response: @@ -209,7 +333,7 @@ interactions: Content-Type: - application/json Date: - - Mon, 29 Jun 2026 18:30:01 GMT + - Wed, 29 Jul 2026 18:39:47 GMT Server: - gunicorn status: From ba15537e2ed1051182c44854a9c7e9f5c89e7b61 Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 29 Jul 2026 16:15:44 -0300 Subject: [PATCH 06/20] fix: render a string-shaped errors payload as one line, not one letter per line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-ups: - The dict branch added earlier reasoned that iterating a mapping yields only its keys; the same applies to a bare string, which iterates as characters. An `"errors": "some prose"` envelope rendered one letter per line. Only list/tuple payloads now get the line-per-entry treatment. - _client_harness: last_request_url read calls[0] while last_request_body read calls[-1]. Invisible in a single-request test, silently wrong in a multi-request one — and this is now the shared harness every respx module imports, not one module's private helper. - Trim the specs/05 paragraph to the contract and let _normalise_sources' docstring carry the per-shape rationale; drop a test comment that narrated review history rather than the risk. --- specs/05-downstream-contract.md | 2 +- src/polyswarm_api/core.py | 7 ++++++- test/_client_harness.py | 10 +++++++++- test/client_scan_test.py | 10 ++++------ 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/specs/05-downstream-contract.md b/specs/05-downstream-contract.md index 9103d4dd..3dbe952c 100644 --- a/specs/05-downstream-contract.md +++ b/specs/05-downstream-contract.md @@ -178,7 +178,7 @@ class InvalidValueException(PolyswarmException): ... class TimeoutException(PolyswarmException): ... ``` -`KnownGoodWithheldException` is the 404 raised when a download is refused because the artifact is a known-good binary — the platform never stores or serves those bytes. It **subclasses `NotFoundException`** precisely so invariant 3 holds for existing consumers: code that already does `except NotFoundException:` keeps catching the refusal with no change, and only callers that want to distinguish "withheld by design" from a plain miss catch the subclass. It adds one attribute, `.sources` — the known-good feeds that flagged the hash (e.g. `['nsrl']`), `[]` when the server named none. It is **always a list of strings**: the exception normalises whatever the envelope carried, so `for feed in exc.sources` is safe without a shape check. A bare `'nsrl'` becomes `['nsrl']`; any non-list shape (mapping, number, `null`) becomes `[]`. Within a list, **both** shapes the platform uses for this concept yield feed names — plain strings, and the feed dicts the instance-level `known_good` field carries (unpacked via `feed['tool']`, mirroring `ArtifactInstance.known_good_sources`). Discarding the dict shape would mean that if the envelope ever reused that serialiser, `.sources` would silently empty out, leaving a caller unable to tell "the server named no feeds" from "the server named feeds we dropped". The raw envelope stays reachable at `exc.request.errors` (`{'code': 'KNOWN_GOOD', 'known_good': True, 'sources': [...]}`). The artifact's metadata — the flagging feeds plus any scan data already collected — remains readable through the search / instance endpoints; only the bytes are withheld, and the instance's `KNOWN_GOOD` state/status is the signal for that (there is no separate "withheld" field). +`KnownGoodWithheldException` is the 404 raised when a download is refused because the artifact is a known-good binary — the platform never stores or serves those bytes. It **subclasses `NotFoundException`** precisely so invariant 3 holds for existing consumers: code that already does `except NotFoundException:` keeps catching the refusal with no change, and only callers that want to distinguish "withheld by design" from a plain miss catch the subclass. It adds one attribute, `.sources` — the known-good feeds that flagged the hash (e.g. `['nsrl']`), `[]` when the server named none. It is **always a list of strings**, whatever the envelope carried, so `for feed in exc.sources` needs no shape check — both the plain-string list the error envelope sends and the feed-dict list the instance-level `known_good` field uses yield feed names; anything else yields `[]`. `exceptions._normalise_sources` carries the per-shape rationale. The raw envelope stays reachable at `exc.request.errors` (`{'code': 'KNOWN_GOOD', 'known_good': True, 'sources': [...]}`). The artifact's metadata — the flagging feeds plus any scan data already collected — remains readable through the search / instance endpoints; only the bytes are withheld, and the instance's `KNOWN_GOOD` state/status is the signal for that (there is no separate "withheld" field). Each `RequestException` subclass carries a `.request` attribute holding the originating `PolyswarmRequest` (set by `RequestException.__init__`). Callers can read `exc.request.status_code`, `exc.request.json` (the parsed response body after execution), `exc.request.input_json` (the body that was sent), `exc.request.request_parameters` (the request kwargs that built the call), etc. `InvalidValueException` and `TimeoutException` are client-side errors and don't carry a request descriptor. diff --git a/src/polyswarm_api/core.py b/src/polyswarm_api/core.py index c9444f8a..f6e43f44 100644 --- a/src/polyswarm_api/core.py +++ b/src/polyswarm_api/core.py @@ -404,10 +404,15 @@ def _bad_status_message(request): # introduced for. Iterating a mapping yields only its KEYS, so rendering it # like a list would drop every value from the message the caller sees — # render it as ``key=value`` lines instead. + # The same reasoning applies to a bare string: iterating it yields characters, so + # `"errors": "some prose"` would render one letter per line. Only genuinely + # sequence-shaped payloads get the line-per-entry treatment. if isinstance(request.errors, dict): errors = '\n'.join(f'{k}={v}' for k, v in request.errors.items()) - else: + elif isinstance(request.errors, (list, tuple)): errors = '\n'.join(str(error) for error in request.errors) + else: + errors = str(request.errors) message = f'{message}\nErrors:\n{errors}' return message diff --git a/test/_client_harness.py b/test/_client_harness.py index 80139bc0..d3096a36 100644 --- a/test/_client_harness.py +++ b/test/_client_harness.py @@ -52,7 +52,15 @@ def add(self, method: str, url: str, json: dict, status: int = 200): @property def last_request_url(self) -> str: - return str(self._router.calls[0].request.url) + """The URL of the most recent request. + + Reads ``calls[-1]`` like ``last_request_body``: the two used to disagree (this one + read ``calls[0]``), which is invisible in a single-request test and silently wrong in + a multi-request one — a test would compare the first request's URL against the last + request's body. Harmless while this lived in one module; not once it is the shared + harness every respx test imports. + """ + return str(self._router.calls[-1].request.url) @property def last_request_body(self): diff --git a/test/client_scan_test.py b/test/client_scan_test.py index 56142334..6ac87e5b 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -774,12 +774,10 @@ def test_known_good_lifecycle(self): assert created.sha256 == sha assert created.sources == ['nsrl'] assert created.artifact_instance_id - # The refusal, against the real server. Every assertion about the error envelope - # elsewhere in the suite reads a body this repo fabricated, which asserts what we - # *think* the server sends: a rename on the server side (KNOWN_GOOD_WITHHELD → - # KNOWN_GOOD, which really happened mid-review) would leave the fabricated tests - # green. Here the caller-visible outcome IS the contract — the typed exception and - # the feed names it carries. + # The refusal, against the real server. Every other assertion about the error + # envelope reads a body this repo fabricated, which pins what we *think* the server + # sends — a rename of the code string on the server side would leave those green. + # Here the caller-visible outcome IS the contract: the typed exception and its feeds. with tempfile.TemporaryDirectory() as out_dir: with pytest.raises(exceptions.KnownGoodWithheldException) as ei: v3api.download(out_dir, sha) From 3acdc28cd440da92a9e259afa83920d99e56b4f3 Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 29 Jul 2026 16:30:37 -0300 Subject: [PATCH 07/20] fix: make a dropped sources payload visible, and pin the plain-404 half MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-ups. - _normalise_sources' docstring argues that a silent discard is the failure mode it exists to prevent, then its fall-through did exactly that for every unrecognised shape. It now logs the discard at debug level. - Both live-e2e lifecycle tests asserted the post-delete 404 with pytest.raises(NotFoundException), which the subclass also satisfies — so the one place the real server returns a plain miss never checked that it stayed the base class. No re-record needed; the cassettes already hold the interaction. - specs/01 and specs/02 said the mapping-shaped errors render applies on every status. True of what the server sends, not of what the caller sees: _bad_status_message has one call site, so the 404/422/429 arms build their messages from _result alone. Narrowed to the RequestException arm, with the payload's reachability at exc.request.errors stated separately. --- specs/01-architecture.md | 2 +- specs/02-resources.md | 2 +- src/polyswarm_api/exceptions.py | 15 +++++++++++++-- test/async_client_test.py | 6 +++++- test/client_scan_test.py | 6 +++++- 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/specs/01-architecture.md b/specs/01-architecture.md index 75dfb783..8005ba7c 100644 --- a/specs/01-architecture.md +++ b/specs/01-architecture.md @@ -271,7 +271,7 @@ Every HTTP-level error maps to a subclass of `PolyswarmException`: - 422 → `FailedInstanceException` - 429 → `UsageLimitsExceededException` - 204 on a request that expects data (JSON-parser GET **or** streaming download) → `NoResultsException` — the server did the work but matched nothing. **HEAD is exempt**: it returns the raw status code as the result (so `exists()` reads a 204 as "known-absent" rather than raising). -- Other non-2xx → `RequestException`. Its message renders the request diagnostics plus the envelope's `errors` slot, in **either** shape: the legacy **list** (one entry per line) or the way-forward **mapping** (`key=value` lines). The mapping shape is not 404-only — the server forwards it on every arm (400 / 401 / 403 / 413 / 5xx) — and iterating a mapping yields only its keys, so it must not be rendered like a list or every value is silently dropped from the message. +- Other non-2xx → `RequestException`. Its message renders the request diagnostics plus the envelope's `errors` slot, in **either** shape: the legacy **list** (one entry per line) or the way-forward **mapping** (`key=value` lines). The mapping shape is not 404-specific — the server can send it on any status — and iterating a mapping yields only its keys, so it must not be rendered like a list or every value is silently dropped from the message. (A bare string is rendered as-is for the same reason: iterating one yields characters.) Note the render itself reaches only **this** arm: `_bad_status_message` has a single call site, so the 404 / 422 / 429 arms build their messages from `request._result` alone and a mapping-shaped `errors` does not appear in them. The payload is still reachable at `exc.request.errors` on every arm. - Client-side validation failures (bad hash, missing kwarg) → `InvalidValueException` - Polling timeouts → `TimeoutException` diff --git a/specs/02-resources.md b/specs/02-resources.md index 85139ea0..9e10d904 100644 --- a/specs/02-resources.md +++ b/specs/02-resources.md @@ -391,7 +391,7 @@ This keeps the body off the heap for `folder`/file-handle destinations — parit - `KnownGoodWithheldException` (a `NotFoundException` subclass) — HTTP 404 whose `errors` payload is a dict with `code == 'KNOWN_GOOD'`: the artifact is a known-good binary and its bytes are withheld by design. Carries `.sources` (the flagging known-good feeds, always a list of strings — normalised in the exception's constructor — and `[]` when none were named or the payload arrived in another shape). Any other 404 — a different code, a legacy list-shaped `errors`, or no `errors` at all — stays a plain `NotFoundException`. - `FailedInstanceException` — HTTP 422. - `UsageLimitsExceededException` — HTTP 429. -- `RequestException` — any other non-2xx. Its message appends the envelope's `errors` slot rendered for whichever shape arrived: a **list** renders one entry per line (the legacy shape), a **mapping** renders `key=value` lines (the way-forward shape, which the server forwards on every status — not just the 404 the `code` contract was introduced for). +- `RequestException` — any other non-2xx. Its message appends the envelope's `errors` slot rendered for whichever shape arrived: a **list** renders one entry per line (the legacy shape), a **mapping** renders `key=value` lines (the way-forward shape, which the server can send on any status — not just the 404 the `code` contract was introduced for), and anything else renders as a plain string. This applies to the `RequestException` arm only — see [`01-architecture.md`](./01-architecture.md). Each is raised by `parse_response`. `RequestException.__init__` attaches the descriptor as `.request`, so callers downstream read `exc.request.status_code`, `exc.request.json`, etc. The session does not catch and rewrap — attachment happens at construction time. diff --git a/src/polyswarm_api/exceptions.py b/src/polyswarm_api/exceptions.py index da30a9d4..95585d1d 100644 --- a/src/polyswarm_api/exceptions.py +++ b/src/polyswarm_api/exceptions.py @@ -1,3 +1,8 @@ +import logging + +logger = logging.getLogger(__name__) + + class PolyswarmException(Exception): pass @@ -49,8 +54,10 @@ def _normalise_sources(sources): from "the server named feeds in a shape we discarded". A string becomes a one-element list; anything else (mapping, ``None``, - number) becomes ``[]``. Nothing is lost either way — the raw envelope stays - readable at ``exc.request.errors``. + number) becomes ``[]`` — and says so at debug level, because the argument + above cuts both ways: a discard nobody can see is the failure mode this + normalisation exists to avoid. Nothing is lost either way — the raw envelope + stays readable at ``exc.request.errors``. """ if isinstance(sources, str): return [sources] @@ -61,7 +68,11 @@ def _normalise_sources(sources): names.append(source) elif isinstance(source, dict) and isinstance(source.get('tool'), str): names.append(source['tool']) + else: + logger.debug('Dropping unrecognised known-good sources entry: %r', source) return names + if sources is not None: + logger.debug('Dropping unrecognised known-good sources payload: %r', sources) return [] diff --git a/test/async_client_test.py b/test/async_client_test.py index dc7d017e..cece3639 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -433,8 +433,12 @@ async def test_async_known_good_lifecycle(self, uid): assert sorted(ei.value.sources) == ['commercial', 'nsrl'] deleted = await api.known_good_delete(sha256=sha) assert deleted.sha256 == sha - with pytest.raises(exceptions.NotFoundException): + # A 404 with no known-good code must stay the BASE class — the subclass would + # satisfy this raises() too, so the plain-miss half of the mapping needs its own + # assertion against the real server. + with pytest.raises(exceptions.NotFoundException) as ei: await api.known_good_get(sha256=sha) + assert not isinstance(ei.value, exceptions.KnownGoodWithheldException) # ── Sandbox ─────────────────────────────────────────────────────────────── diff --git a/test/client_scan_test.py b/test/client_scan_test.py index 6ac87e5b..110415e8 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -802,8 +802,12 @@ def test_known_good_lifecycle(self): assert sorted(ei.value.sources) == ['commercial', 'nsrl'] deleted = v3api.known_good_delete(sha256=sha) assert deleted.sha256 == sha - with pytest.raises(exceptions.NotFoundException): + # A 404 with no known-good code must stay the BASE class: the subclass satisfies + # `pytest.raises(NotFoundException)` too, so without this the one place the real + # server returns a plain miss never checks that it wasn't mapped to the subclass. + with pytest.raises(exceptions.NotFoundException) as ei: v3api.known_good_get(sha256=sha) + assert not isinstance(ei.value, exceptions.KnownGoodWithheldException) @vcr.use_cassette() def test_sandbox_providers(self): From ffe290e0aa79e71b11b215dc3b749dff9e53044d Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 29 Jul 2026 16:58:38 -0300 Subject: [PATCH 08/20] test: fold the refusal's disk assertion into the live tests, drop the respx tier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-up, and a spec contradiction this branch introduced. download_refusal_test.py's docstring and the specs/04 paragraph that legitimised it both claimed the streaming refusal was "reachable by neither a pure-unit parse test nor a cassette-backed endpoint test". A later commit on this same branch made that false: both known-good lifecycle tests now drive download() against the live server on both transports, which runs _execute_download's 404 arm end to end. The only assertion unique to the respx module was that the refusal writes nothing to the destination, so that moved into the two live bodies and the module is gone. Keeping it would have left a tier whose stated justification reads as licence to reach for respx on any transport arm. specs/04 now says what actually covers that arm, and when respx is still the right tool (a connection reset, a retry ladder, a truncated body). Also, on prose: specs/05 committed the published surface to accepting the feed-dict sources shape the envelope has never sent. The defensive branch stays (it costs three lines and prevents a silent empty), but the contract is narrowed to the one promise consumers can rely on — always a list of strings — with the per-shape reasoning left in the helper's docstring. Trimmed that docstring and dropped the "Regression:" label from a comment describing a defect that only existed between commits on this branch. --- specs/04-testing.md | 9 +++-- specs/05-downstream-contract.md | 2 +- src/polyswarm_api/exceptions.py | 24 ++++---------- test/async_client_test.py | 2 ++ test/client_scan_test.py | 4 +++ test/core_test.py | 19 +++-------- test/download_refusal_test.py | 58 --------------------------------- 7 files changed, 24 insertions(+), 94 deletions(-) delete mode 100644 test/download_refusal_test.py diff --git a/specs/04-testing.md b/specs/04-testing.md index a9b04713..e27f71f7 100644 --- a/specs/04-testing.md +++ b/specs/04-testing.md @@ -21,7 +21,6 @@ How the test suite is organised. Three layers: pure unit tests (no HTTP at all - `test/core_test.py` — pure-unit tests for `parse_response`, `PolyswarmRequest`, and resource builders. No httpx, no fixtures. - `test/_client_harness.py` — the parametrised `ClientTestCase` harness (`_MockBoundary` / `_AsyncToSync`) every `respx` test module imports. Not collected itself (the `_` prefix keeps it out of `python_files`), same as `_e2e_helpers.py`. - `test/metadata_field_properties_test.py` — the canonical example of the parametrised `ClientTestCase` harness with `respx`-backed mocking. -- `test/download_refusal_test.py` — the same harness applied to a transport arm: the streaming-download refusal mapping, one body over both `_execute_download` implementations. - `test/client_scan_test.py` — sync, VCR-backed integration tests (not yet on the parametrised harness — follow-up work). - `test/async_client_test.py` — async, VCR-backed integration tests (not yet on the parametrised harness — follow-up work). - `test/jmespath_test.py` — unit tests for `BaseJsonResource.jmespath`. @@ -42,7 +41,11 @@ The pure-unit tier exists because the 4.0 redesign made it possible: resource bu **VCR-backed live-e2e tests are the default for new endpoint tests** (invariant 1): they exercise the real server implementation and the cassette pins the actual contract, while replay keeps unit runs fast and offline. `respx` is reserved for scenarios the e2e stack can't produce (or only at disproportionate cost); pure-unit covers request-shape and parse logic without any HTTP. The canonical split for a new endpoint: a live-e2e VCR lifecycle test (behaviour, real contract) + pure-unit builder tests (body-vs-query routing, None-omission) — see `test_known_good_lifecycle` + `test/known_good_test.py`. -The SDK's own **transport arm** is one of the scenarios respx is reserved for. The streaming download path (`_execute_download`) never runs `parse_response` — it reads the error body itself and calls `_raise_for_status` — so its non-2xx mapping is reachable by neither a pure-unit parse test nor a cassette-backed endpoint test that only asserts the caller-visible outcome. That's still the respx tier, **not a slice of its own**: it rides the parametrised `ClientTestCase` harness like every other respx body (invariant 5). Which is the point — `_execute_download` exists twice (the canonical async source and its generated sync mirror), and a hand-written async-only body would leave the sync arm uncovered. `DownloadRefusalTestCase` (`test/download_refusal_test.py`) is the worked example: one body, auto-emitted `…Sync` / `…Async` siblings, pinning that a refusal envelope arriving on the streaming arm becomes the typed exception with its payload (`.sources`) and leaves nothing written to the destination. +### The transport arm + +The SDK's own **transport arm** is worth naming, because the obvious reading gets it wrong. The streaming download path (`_execute_download`) never runs `parse_response` — it reads the error body itself and calls `_raise_for_status` — so its non-2xx mapping *looks* like a scenario only respx can reach. It is not: a **cassette-backed** endpoint test drives it end to end, because the caller-visible outcome (the typed exception, its payload, and an empty destination directory) **is** the mapping. The known-good download refusal is covered exactly that way, in `client_scan_test.py` and `async_client_test.py`, so both `_execute_download` implementations — the canonical async source and its generated sync mirror — run against a real recorded envelope. + +A respx body for that same arm was written first and deleted once the live coverage existed: keeping both left a second tier whose stated justification ("unreachable at the e2e tier") was no longer true, which reads as licence to reach for respx on any transport arm. Reach for it when the stack genuinely cannot produce the response — a connection reset, a retry ladder, a truncated body. ### E2e sample-generation conventions @@ -286,7 +289,7 @@ Decision tree (e2e-first — see invariant 1): 1. **Pure logic test?** (No HTTP, no resource side effects — including request-builder shape and `parse_response`.) → Plain unittest / pytest function. See `jmespath_test.py`, `core_test.py`, `known_good_test.py`. 2. **Endpoint behaviour?** → **VCR-backed live-e2e test — the default.** Write the test against the real endpoint, record once against a fresh e2e stack, commit the cassette. Sync body in `client_scan_test.py`, async in `async_client_test.py`. Need a sample or a sha? Derive it from the test's own EICAR variant (`malicious_artifact(uid)`); anything needing a verdict gets it from the `eicar` engine. -3. **Scenario the e2e stack can't produce** (transport failures, retry exhaustion, cursor pathologies, external systems, an SDK transport arm)? → Parametrised `ClientTestCase` with `_MockBoundary` (`respx`), imported from `test/_client_harness.py`. See `metadata_field_properties_test.py` (endpoint shape) and `download_refusal_test.py` (a transport arm). Body covers both sync and async automatically. Justify in the test docstring why the scenario can't run on e2e. +3. **Scenario the e2e stack can't produce** (transport failures, retry exhaustion, cursor pathologies, external systems, an SDK transport arm)? → Parametrised `ClientTestCase` with `_MockBoundary` (`respx`), imported from `test/_client_harness.py`. See `metadata_field_properties_test.py` (endpoint shape). A transport arm is **not** automatically such a scenario — see §The transport arm. Body covers both sync and async automatically. Justify in the test docstring why the scenario can't run on e2e. Always: diff --git a/specs/05-downstream-contract.md b/specs/05-downstream-contract.md index 3dbe952c..791b9017 100644 --- a/specs/05-downstream-contract.md +++ b/specs/05-downstream-contract.md @@ -178,7 +178,7 @@ class InvalidValueException(PolyswarmException): ... class TimeoutException(PolyswarmException): ... ``` -`KnownGoodWithheldException` is the 404 raised when a download is refused because the artifact is a known-good binary — the platform never stores or serves those bytes. It **subclasses `NotFoundException`** precisely so invariant 3 holds for existing consumers: code that already does `except NotFoundException:` keeps catching the refusal with no change, and only callers that want to distinguish "withheld by design" from a plain miss catch the subclass. It adds one attribute, `.sources` — the known-good feeds that flagged the hash (e.g. `['nsrl']`), `[]` when the server named none. It is **always a list of strings**, whatever the envelope carried, so `for feed in exc.sources` needs no shape check — both the plain-string list the error envelope sends and the feed-dict list the instance-level `known_good` field uses yield feed names; anything else yields `[]`. `exceptions._normalise_sources` carries the per-shape rationale. The raw envelope stays reachable at `exc.request.errors` (`{'code': 'KNOWN_GOOD', 'known_good': True, 'sources': [...]}`). The artifact's metadata — the flagging feeds plus any scan data already collected — remains readable through the search / instance endpoints; only the bytes are withheld, and the instance's `KNOWN_GOOD` state/status is the signal for that (there is no separate "withheld" field). +`KnownGoodWithheldException` is the 404 raised when a download is refused because the artifact is a known-good binary — the platform never stores or serves those bytes. It **subclasses `NotFoundException`** precisely so invariant 3 holds for existing consumers: code that already does `except NotFoundException:` keeps catching the refusal with no change, and only callers that want to distinguish "withheld by design" from a plain miss catch the subclass. It adds one attribute, `.sources` — the known-good feeds that flagged the hash (e.g. `['nsrl']`), `[]` when the server named none. The contract is only this: **always a list of strings**, whatever the envelope carried, so `for feed in exc.sources` needs no shape check. Which wire shapes are coerced, and which are dropped and logged, is `exceptions._normalise_sources`' business rather than a promise to consumers — the server sends a list of strings today. The raw envelope stays reachable at `exc.request.errors` (`{'code': 'KNOWN_GOOD', 'known_good': True, 'sources': [...]}`). The artifact's metadata — the flagging feeds plus any scan data already collected — remains readable through the search / instance endpoints; only the bytes are withheld, and the instance's `KNOWN_GOOD` state/status is the signal for that (there is no separate "withheld" field). Each `RequestException` subclass carries a `.request` attribute holding the originating `PolyswarmRequest` (set by `RequestException.__init__`). Callers can read `exc.request.status_code`, `exc.request.json` (the parsed response body after execution), `exc.request.input_json` (the body that was sent), `exc.request.request_parameters` (the request kwargs that built the call), etc. `InvalidValueException` and `TimeoutException` are client-side errors and don't carry a request descriptor. diff --git a/src/polyswarm_api/exceptions.py b/src/polyswarm_api/exceptions.py index 95585d1d..62de179e 100644 --- a/src/polyswarm_api/exceptions.py +++ b/src/polyswarm_api/exceptions.py @@ -40,24 +40,12 @@ class NotFoundException(RequestException): def _normalise_sources(sources): """Coerce a server-supplied ``sources`` payload into a list of feed names. - ``.sources`` is published as a list of strings and consumers iterate it - (``for feed in exc.sources``), so the raw payload can't be handed through - untouched: a bare ``'nsrl'`` would iterate as characters. - - Both shapes the platform uses for this one concept yield feed names. The - error envelope sends a list of strings; the instance-level ``known_good`` - field sends a list of feed dicts, which ``resources.py`` unpacks via - ``feed['tool']`` into ``ArtifactInstance.known_good_sources``. If the - envelope ever reuses that serialiser, dropping the dicts would empty - ``.sources`` and take this exception's whole added value with it — with no - diagnostic, and no way for a caller to tell "the server named no feeds" - from "the server named feeds in a shape we discarded". - - A string becomes a one-element list; anything else (mapping, ``None``, - number) becomes ``[]`` — and says so at debug level, because the argument - above cuts both ways: a discard nobody can see is the failure mode this - normalisation exists to avoid. Nothing is lost either way — the raw envelope - stays readable at ``exc.request.errors``. + ``.sources`` is published as a list of strings and consumers iterate it, so a + bare ``'nsrl'`` cannot be handed through — it would iterate as characters. + The server sends a list of strings today; a feed-dict entry (the shape the + instance-level ``known_good`` field uses) yields ``feed['tool']`` defensively + rather than being dropped. Anything unrecognised becomes ``[]`` and is logged, + so a discard is never silent. The raw payload stays at ``exc.request.errors``. """ if isinstance(sources, str): return [sources] diff --git a/test/async_client_test.py b/test/async_client_test.py index cece3639..f4ab13aa 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -415,6 +415,8 @@ async def test_async_known_good_lifecycle(self, uid): with tempfile.TemporaryDirectory() as out_dir: with pytest.raises(exceptions.KnownGoodWithheldException) as ei: await api.download(out_dir, sha) + # A refused download leaves nothing behind — see the sync twin. + assert os.listdir(out_dir) == [] assert ei.value.sources == ['nsrl'] # The require_scan semantics this SDK documents: a known-good hash is a decided # terminal record, so it reports present rather than "not scanned yet". diff --git a/test/client_scan_test.py b/test/client_scan_test.py index 110415e8..b96fcc05 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -781,6 +781,10 @@ def test_known_good_lifecycle(self): with tempfile.TemporaryDirectory() as out_dir: with pytest.raises(exceptions.KnownGoodWithheldException) as ei: v3api.download(out_dir, sha) + # A refused download leaves nothing behind: the streaming path opens its + # destination before the response is read, so a truncated or empty file here + # would look to a caller like a download that worked. + assert os.listdir(out_dir) == [] assert ei.value.sources == ['nsrl'] # ...and the hash still reports PRESENT under require_scan, which is the semantics # the SDK now documents in four places: a known-good record is decided and terminal, diff --git a/test/core_test.py b/test/core_test.py index 69ab08f0..b6f4b9a5 100644 --- a/test/core_test.py +++ b/test/core_test.py @@ -293,20 +293,11 @@ def test_404_known_good_withheld_without_sources(self): assert ei.value.sources == [] def test_404_known_good_sources_normalised_to_feed_names(self): - # Regression: the raise site passed the envelope's ``sources`` payload - # straight through, so any shape but a list of strings reached the caller - # — a bare 'nsrl' iterated as characters ('n', 's', 'r', 'l'), and the - # list-of-feed-dicts shape the instance-level ``known_good`` field uses - # iterated as dicts. ``.sources`` is documented as a list of feed-name - # strings, so normalise at the boundary: string → one-element list, list → - # feed names from either shape, anything else → []. - # - # The feed-dict entries yield names rather than being dropped: that is the - # OTHER shape the platform uses for this same concept (the instance-level - # ``known_good`` field, unpacked via ``feed['tool']`` into - # ``known_good_sources``). If the envelope ever reuses that serialiser, - # discarding it would silently empty ``.sources`` and take the exception's - # whole added value with it. + # ``.sources`` is documented as a list of feed-name strings, so the payload is + # normalised at the boundary rather than handed through: a bare 'nsrl' would + # iterate as characters. A feed-dict entry (the shape the instance-level + # ``known_good`` field uses) yields ``feed['tool']`` defensively; anything + # unrecognised yields []. shapes = [ (['nsrl', 'commercial'], ['nsrl', 'commercial']), # already correct ('nsrl', ['nsrl']), # bare string diff --git a/test/download_refusal_test.py b/test/download_refusal_test.py deleted file mode 100644 index 9a9d5636..00000000 --- a/test/download_refusal_test.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Refusal handling on the streaming-download arm, both transports. - -respx rather than the live e2e stack (specs/04-testing.md, invariant 1): the -scenario under test is the SDK's *transport arm*, not the endpoint. A download -refusal never reaches ``parse_response`` — ``_execute_download`` reads the small -error body itself and hands it to the shared ``_raise_for_status`` — so neither a -pure-unit parse test nor a cassette-backed endpoint test that asserts only the -caller-visible outcome exercises that code path. Endpoint behaviour against the -real server stays with the live-e2e VCR tests. - -On the ``ClientTestCase`` harness (invariant 5) so the one body runs against -``PolyswarmAPI`` **and** ``PolySwarmAsyncAPI``: ``_execute_download`` exists twice -(the canonical async source and its generated sync mirror), and the sync arm is -exactly the one a hand-written async-only respx body leaves uncovered. -""" -import os -import tempfile - -from polyswarm_api import exceptions - -from test._client_harness import BASE_URL, ClientTestCase - - -SHA256 = 'a' * 64 -_DOWNLOAD_URL = f'{BASE_URL}/consumer/download/sha256/{SHA256}' - -# The server's refusal envelope: a 404 whose machine-readable ``errors`` code says -# the bytes are withheld because the artifact is a known-good binary. -_REFUSAL_BODY = { - 'status': 'error', - 'result': 'Unable to download the provided artifact, it is a known-good ' - 'binary; its bytes are withheld by design.', - 'errors': {'code': 'KNOWN_GOOD', 'known_good': True, 'sources': ['nsrl']}, -} - - -class DownloadRefusalTestCase(ClientTestCase): - def test_download_known_good_refusal_raises_withheld(self): - # Regression: the refusal arrives on the streaming arm, which bypasses - # ``parse_response`` entirely. Without the shared ``_raise_for_status`` - # mapping being reached there, a refused download surfaces as a bare - # ``NotFoundException`` (no ``.sources``, indistinguishable from a plain - # miss) — or, worse, gets written out as an artifact file holding the - # error JSON. Pin both: the typed exception with its payload, and an - # untouched destination folder. - self.mock.add('GET', _DOWNLOAD_URL, json=_REFUSAL_BODY, status=404) - with tempfile.TemporaryDirectory() as tmp_dir: - with self.assertRaises(exceptions.KnownGoodWithheldException) as caught: - self.api.download(tmp_dir, SHA256) - exc = caught.exception - assert exc.sources == ['nsrl'] - # Still a NotFoundException, so existing handlers keep catching it. - assert isinstance(exc, exceptions.NotFoundException) - # The raw envelope stays reachable for callers that want the rest. - assert exc.request.errors == _REFUSAL_BODY['errors'] - # A refusal must leave nothing behind — no empty file, no error JSON - # written out as if it were the artifact. - assert os.listdir(tmp_dir) == [] From f0e09decedc1213a754f979db2e1f6c7cf4793c9 Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 29 Jul 2026 17:14:06 -0300 Subject: [PATCH 09/20] fix: correct the download-refusal comment, and the decision tree it contradicted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-ups, both about statements that were wrong rather than code. - The new os.listdir assertion was justified with "the streaming path opens its destination before the response is read". It does not: _execute_download checks the status before calling open_destination. The assertion is still worth keeping — it pins that the ordering never inverts, since an empty file is indistinguishable from a successful download — so it now says that. - specs/04's decision tree still listed "an SDK transport arm" among the scenarios respx is for, one sentence before the new section saying it is not. That parenthetical was the licence the section exists to remove. - specs/05's Versioning table had no row for a new exception class, so the release PR would have had to infer the bump. Added it, with the reasoning that makes it minor rather than major (the subclass preserves every existing `except NotFoundException`). --- specs/04-testing.md | 2 +- specs/05-downstream-contract.md | 2 ++ test/client_scan_test.py | 7 ++++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/specs/04-testing.md b/specs/04-testing.md index e27f71f7..cabf4fdc 100644 --- a/specs/04-testing.md +++ b/specs/04-testing.md @@ -289,7 +289,7 @@ Decision tree (e2e-first — see invariant 1): 1. **Pure logic test?** (No HTTP, no resource side effects — including request-builder shape and `parse_response`.) → Plain unittest / pytest function. See `jmespath_test.py`, `core_test.py`, `known_good_test.py`. 2. **Endpoint behaviour?** → **VCR-backed live-e2e test — the default.** Write the test against the real endpoint, record once against a fresh e2e stack, commit the cassette. Sync body in `client_scan_test.py`, async in `async_client_test.py`. Need a sample or a sha? Derive it from the test's own EICAR variant (`malicious_artifact(uid)`); anything needing a verdict gets it from the `eicar` engine. -3. **Scenario the e2e stack can't produce** (transport failures, retry exhaustion, cursor pathologies, external systems, an SDK transport arm)? → Parametrised `ClientTestCase` with `_MockBoundary` (`respx`), imported from `test/_client_harness.py`. See `metadata_field_properties_test.py` (endpoint shape). A transport arm is **not** automatically such a scenario — see §The transport arm. Body covers both sync and async automatically. Justify in the test docstring why the scenario can't run on e2e. +3. **Scenario the e2e stack can't produce** (transport failures, retry exhaustion, cursor pathologies, external systems)? → Parametrised `ClientTestCase` with `_MockBoundary` (`respx`), imported from `test/_client_harness.py`. See `metadata_field_properties_test.py` (endpoint shape). A **transport arm is not on that list**: the stack drives one perfectly well, because the caller-visible outcome is the mapping — see §The transport arm. Body covers both sync and async automatically. Justify in the test docstring why the scenario can't run on e2e. Always: diff --git a/specs/05-downstream-contract.md b/specs/05-downstream-contract.md index 791b9017..f4f35680 100644 --- a/specs/05-downstream-contract.md +++ b/specs/05-downstream-contract.md @@ -391,6 +391,8 @@ result = req._result # the parsed resource (or list); | New endpoint method | minor | | New resource class | minor | | New field on a resource (mirror of a new server-side field) | minor | +| **New exception class that subclasses an existing one** | **minor** — additive: every `except ` keeps catching it (invariant 3), so no consumer has to change. Raising the *base* class where a narrower one used to be raised is the major-bump direction | +| **Narrowing which exception a status maps to** (same status code, more specific class) | **minor**, on the same reasoning — but only while the new class is a subclass of the old one. A sibling class is a behaviour change on a documented contract, i.e. major | | Bug fix in request/response handling | patch | | Signature change on a public method | major | | Rename / removal of a public symbol | major | diff --git a/test/client_scan_test.py b/test/client_scan_test.py index b96fcc05..2925a600 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -781,9 +781,10 @@ def test_known_good_lifecycle(self): with tempfile.TemporaryDirectory() as out_dir: with pytest.raises(exceptions.KnownGoodWithheldException) as ei: v3api.download(out_dir, sha) - # A refused download leaves nothing behind: the streaming path opens its - # destination before the response is read, so a truncated or empty file here - # would look to a caller like a download that worked. + # A refused download leaves nothing behind. `_execute_download` checks the + # status before it calls `open_destination`, and this pins that ordering: invert + # it and a refusal would leave an empty file, which to a caller is + # indistinguishable from a download that worked. assert os.listdir(out_dir) == [] assert ei.value.sources == ['nsrl'] # ...and the hash still reports PRESENT under require_scan, which is the semantics From 0017571bf8e00e0c8652d24d28df26d1c75ed60d Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 29 Jul 2026 20:06:17 -0300 Subject: [PATCH 10/20] =?UTF-8?q?fix:=20a=20catalogued=20hash=20is=20not?= =?UTF-8?q?=20a=20scanned=20record=20=E2=80=94=20and=20pin=20that=20agains?= =?UTF-8?q?t=20the=20server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit artifact-index reverted the change that made its hash existence probe answer "found" for a known-good sha256 (PR #1919, commits 5c9268a8 / 0b90f54c). Those status codes are a frozen contract, documented there in specs/09-hash-search-head-contract.md: 200 = found, 204 = not found, anything else = the request was wrong. A KnownGood row is a fact about the sample, not a searchable record of an artifact, so widening the probe to include it moved a caller's case from 204 to 200 — which this SDK reads as a bare status code with no error channel, i.e. a wrong boolean and nothing else. This side documented and asserted the widened behaviour, which is what turned artifact-index's e2e job red: the live suite asserted exists(..., require_scan=True) is True against a server that now correctly answers 204. Corrected in all four places (both exists() docstrings, specs/02, specs/03) and in both lifecycle tests, which now assert what the server actually does for a hash catalogued through the CRUD: plain -> True (create_known_good builds a searchable reference instance, so a real record exists) require_scan -> False (nothing was ever scanned for it) Cassettes re-recorded against a live stack running the reverted server, so they now carry the first HEAD interactions in test/vcr/ — 200 for the plain probe and 204 for require_scan. That matters beyond this fix: every other assertion about this endpoint in the suite is respx-mocked, so it pins the SDK's own mapping and would stay green through a server-side flip. That is exactly how the 4.0 exists() inversion survived to ship in 4.0.0 and 4.1.0. These two tests would not. --- specs/02-resources.md | 2 +- specs/03-endpoints.md | 2 +- src/polyswarm_api/aio/api.py | 16 +++++- src/polyswarm_api/api.py | 16 +++++- test/async_client_test.py | 9 ++- test/client_scan_test.py | 19 +++++-- test/vcr/test_async_known_good_lifecycle.vcr | 60 ++++++++++++++++---- test/vcr/test_known_good_lifecycle.vcr | 60 ++++++++++++++++---- 8 files changed, 144 insertions(+), 40 deletions(-) diff --git a/specs/02-resources.md b/specs/02-resources.md index 9e10d904..fa68310a 100644 --- a/specs/02-resources.md +++ b/specs/02-resources.md @@ -311,7 +311,7 @@ the flagging feeds plus any scan data already collected — stays readable. Classmethod builders (each returns a `PolyswarmRequest` descriptor): -- `exists_hash(api, hash_value, hash_type, require_scan=False)` — HEAD request, returns the status code as the result. `require_scan=True` narrows the answer to scanned artifacts, **except** for a known-good sha256, which the server reports as present either way: a known-good hash is a decided terminal record, so it is never scanned and its binary is never stored. +- `exists_hash(api, hash_value, hash_type, require_scan=False)` — HEAD request, returns the status code as the result. `require_scan=True` narrows the answer to artifacts that were actually scanned. Being catalogued as known-good is **not** a scan — a hash whose only record is a known-good reference answers present without `require_scan` (the reference is a real searchable record) and absent with it. - `search_hash(api, hash_value, hash_type)` — GET `/search/hash/{hash_type}`. - `search_url(api, url)` — GET `/search/url`. - `list_scans(api, hash_value)` — GET `/search/instances`. diff --git a/specs/03-endpoints.md b/specs/03-endpoints.md index d62c8fa4..8533f217 100644 --- a/specs/03-endpoints.md +++ b/specs/03-endpoints.md @@ -25,7 +25,7 @@ The full catalogue of methods on the public client surface and which transport h | Method | Resource builder | Notes | |---|---|---| -| `exists(hash_, hash_type=None, require_scan=False)` | `ArtifactInstance.exists_hash` | HEAD; `bool` from status code — `True` **only** for `200` (present). `204` means "absent" (the request succeeded but matched no artifact) and `404` also maps to absent, so both are `False`. Do **not** treat this as a generic `2xx` check: `204` is a successful status that means the opposite of "exists". `require_scan=True` still reports a **known-good** sha256 as present (`200`) — a known-good hash is a decided terminal record the platform will never scan or store the binary of. | +| `exists(hash_, hash_type=None, require_scan=False)` | `ArtifactInstance.exists_hash` | HEAD; `bool` from status code — `True` **only** for `200` (present). `204` means "absent" (the request succeeded but matched no artifact) and `404` also maps to absent, so both are `False`. Do **not** treat this as a generic `2xx` check: `204` is a successful status that means the opposite of "exists". `require_scan=True` narrows *found* to artifacts that were actually **scanned**: being catalogued as known-good is not a scan, so a hash whose only record is a known-good reference reports absent under `require_scan` and present without it. **The server's codes here are a frozen contract** — see artifact-index `specs/09-hash-search-head-contract.md`; neither side may widen what counts as found, because a widening moves a caller's case from `False` to `True` with no error and no log line (that is the 4.0 `exists()` inversion, shipped in 4.0.0/4.1.0 and fixed in 4.2.0). | | `lookup(scan)` | `ArtifactInstance.lookup_uuid` | | | `rescan(hash_, hash_type=None, scan_config=None)` | `ArtifactInstance.rescan` | | | `rescan_id(scan, scan_config=None)` | `ArtifactInstance.rescan_id` | | diff --git a/src/polyswarm_api/aio/api.py b/src/polyswarm_api/aio/api.py index e9951a60..59b2befa 100644 --- a/src/polyswarm_api/aio/api.py +++ b/src/polyswarm_api/aio/api.py @@ -1642,10 +1642,20 @@ async def exists(self, hash_, hash_type=None, require_scan=False): :param hash_: Hashable (Artifact, LocalArtifact, Hash) or hex-encoded SHA256/SHA1/MD5. :param hash_type: Hash type; auto-detected if not provided. - :param require_scan: If True, only count artifacts that have been scanned. - A known-good hash still counts as present: it is a decided terminal - record, so the platform never scans it and never stores its binary. + :param require_scan: If True, only count artifacts that have been *scanned*. + Being catalogued as known-good is **not** a scan: the platform never scans + such a sample, so a hash whose only record is a known-good reference reports + absent under ``require_scan`` (and present without it, because that reference + is a real searchable record). :return: ``True`` if the artifact exists in PolySwarm's index. + + .. note:: + This is an existence probe, and its status codes are a **frozen contract**: + ``200`` means found, ``204`` means not found, and anything else means the + *request* was wrong. Because the probe carries no result parser, a non-2xx + status never raises here — it collapses to ``False``. So a server error is + indistinguishable from a genuine "does not exist", which is why neither side + may widen what counts as found. See ``specs/03-endpoints.md``. """ logger.info('Exists for hash %s', hash_) hash_ = resources.Hash.from_hashable(hash_, hash_type=hash_type) diff --git a/src/polyswarm_api/api.py b/src/polyswarm_api/api.py index e9353fd5..42068f5a 100644 --- a/src/polyswarm_api/api.py +++ b/src/polyswarm_api/api.py @@ -1989,10 +1989,20 @@ def exists(self, hash_, hash_type=None, require_scan=False): :param hash_: Hashable (Artifact, LocalArtifact, Hash) or hex-encoded SHA256/SHA1/MD5. :param hash_type: Hash type; auto-detected if not provided. - :param require_scan: If True, only count artifacts that have been scanned. - A known-good hash still counts as present: it is a decided terminal - record, so the platform never scans it and never stores its binary. + :param require_scan: If True, only count artifacts that have been *scanned*. + Being catalogued as known-good is **not** a scan: the platform never scans + such a sample, so a hash whose only record is a known-good reference reports + absent under ``require_scan`` (and present without it, because that reference + is a real searchable record). :return: ``True`` if the artifact exists in PolySwarm's index. + + .. note:: + This is an existence probe, and its status codes are a **frozen contract**: + ``200`` means found, ``204`` means not found, and anything else means the + *request* was wrong. Because the probe carries no result parser, a non-2xx + status never raises here — it collapses to ``False``. So a server error is + indistinguishable from a genuine "does not exist", which is why neither side + may widen what counts as found. See ``specs/03-endpoints.md``. """ logger.info("Exists for hash %s", hash_) hash_ = resources.Hash.from_hashable(hash_, hash_type=hash_type) diff --git a/test/async_client_test.py b/test/async_client_test.py index f4ab13aa..6228d20a 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -418,9 +418,12 @@ async def test_async_known_good_lifecycle(self, uid): # A refused download leaves nothing behind — see the sync twin. assert os.listdir(out_dir) == [] assert ei.value.sources == ['nsrl'] - # The require_scan semantics this SDK documents: a known-good hash is a decided - # terminal record, so it reports present rather than "not scanned yet". - assert await api.exists(sha, hash_type='sha256', require_scan=True) is True + # The existence probe on the async transport — see the sync twin for why these two + # lines are the fleet's only live guard on a frozen status contract. Catalogued via + # the CRUD: present plain (the reference instance is a real record), absent under + # require_scan (nothing was ever scanned for it). + assert await api.exists(sha, hash_type='sha256') is True + assert await api.exists(sha, hash_type='sha256', require_scan=True) is False # A second feed flagging the same sha extends the same entry (no new row). extended = await api.known_good_create(sha256=sha, source='commercial') assert extended.id == created.id diff --git a/test/client_scan_test.py b/test/client_scan_test.py index 2925a600..648cc58b 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -787,11 +787,20 @@ def test_known_good_lifecycle(self): # indistinguishable from a download that worked. assert os.listdir(out_dir) == [] assert ei.value.sources == ['nsrl'] - # ...and the hash still reports PRESENT under require_scan, which is the semantics - # the SDK now documents in four places: a known-good record is decided and terminal, - # so "has it been scanned" must not answer "no, go and scan it" for a sample the - # platform will never scan. - assert v3api.exists(sha, hash_type='sha256', require_scan=True) is True + # The existence probe, against the real server — and the only LIVE coverage of it in + # the fleet. Its status codes are a frozen contract (artifact-index + # specs/09-hash-search-head-contract.md): 200 = found, 204 = not found. The probe + # carries no result parser, so a non-2xx never raises and every non-200 collapses to + # False — which means a server-side widening produces a wrong boolean with no error and + # no log line. That is exactly the 4.0 inversion this SDK shipped in 4.0.0/4.1.0. + # Everything else asserting these semantics is respx-mocked, so it pins the SDK's + # mapping and would stay green through such a flip. These two lines would not. + # + # Catalogued via the CRUD, which builds a searchable reference instance: + # plain -> present, because that reference IS a real record + # require_scan -> absent, because nothing was ever scanned for it + assert v3api.exists(sha, hash_type='sha256') is True + assert v3api.exists(sha, hash_type='sha256', require_scan=True) is False # A second feed flagging the same sha extends the same entry (no new row). extended = v3api.known_good_create(sha256=sha, source='commercial') assert extended.id == created.id diff --git a/test/vcr/test_async_known_good_lifecycle.vcr b/test/vcr/test_async_known_good_lifecycle.vcr index 393fce67..9fcb476d 100644 --- a/test/vcr/test_async_known_good_lifecycle.vcr +++ b/test/vcr/test_async_known_good_lifecycle.vcr @@ -22,7 +22,7 @@ interactions: uri: http://artifact-index-e2e:9696/v3/known-good response: body: - string: '{"result":{"artifact_instance_id":"99003580612834285","created":"2026-07-29T18:39:47.728827+00:00","id":"21743034095682783","sha256":"d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b","sources":["nsrl"]},"status":"OK"} + string: '{"result":{"artifact_instance_id":"99771098961774556","created":"2026-07-29T23:04:40.642152+00:00","id":"73600979008822536","sha256":"d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b","sources":["nsrl"]},"status":"OK"} ' headers: @@ -37,7 +37,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 18:39:47 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn X-Billing-ID: @@ -81,7 +81,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 18:39:47 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn status: @@ -103,7 +103,7 @@ interactions: user-agent: - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) method: HEAD - uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b&community=gamma&require_scan=true + uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b&community=gamma&require_scan=false response: body: string: '' @@ -119,7 +119,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 18:39:47 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn X-Billing-ID: @@ -127,6 +127,42 @@ interactions: status: code: 200 message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: HEAD + uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b&community=gamma&require_scan=true + response: + body: + string: '' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Type: + - text/html; charset=utf-8 + Date: + - Wed, 29 Jul 2026 23:04:40 GMT + Server: + - gunicorn + status: + code: 204 + message: NO CONTENT - request: body: '{"sha256":"d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b","source":"commercial","community":"gamma"}' headers: @@ -150,7 +186,7 @@ interactions: uri: http://artifact-index-e2e:9696/v3/known-good response: body: - string: '{"result":{"artifact_instance_id":"99003580612834285","created":"2026-07-29T18:39:47.728827+00:00","id":"21743034095682783","sha256":"d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b","sources":["commercial","nsrl"]},"status":"OK"} + string: '{"result":{"artifact_instance_id":"99771098961774556","created":"2026-07-29T23:04:40.642152+00:00","id":"73600979008822536","sha256":"d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b","sources":["commercial","nsrl"]},"status":"OK"} ' headers: @@ -165,7 +201,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 18:39:47 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn X-Billing-ID: @@ -192,7 +228,7 @@ interactions: uri: http://artifact-index-e2e:9696/v3/known-good?sha256=d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b&community=gamma response: body: - string: '{"result":{"artifact_instance_id":"99003580612834285","created":"2026-07-29T18:39:47.728827+00:00","id":"21743034095682783","sha256":"d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b","sources":["commercial","nsrl"]},"status":"OK"} + string: '{"result":{"artifact_instance_id":"99771098961774556","created":"2026-07-29T23:04:40.642152+00:00","id":"73600979008822536","sha256":"d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b","sources":["commercial","nsrl"]},"status":"OK"} ' headers: @@ -207,7 +243,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 18:39:47 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn X-Billing-ID: @@ -251,7 +287,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 18:39:47 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn status: @@ -291,7 +327,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 18:39:47 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn X-Billing-ID: @@ -333,7 +369,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 18:39:47 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn status: diff --git a/test/vcr/test_known_good_lifecycle.vcr b/test/vcr/test_known_good_lifecycle.vcr index 215d866b..dea39d68 100644 --- a/test/vcr/test_known_good_lifecycle.vcr +++ b/test/vcr/test_known_good_lifecycle.vcr @@ -22,7 +22,7 @@ interactions: uri: http://artifact-index-e2e:9696/v3/known-good response: body: - string: '{"result":{"artifact_instance_id":"90039023884233228","created":"2026-07-29T18:39:47.299724+00:00","id":"66682229801759442","sha256":"9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df","sources":["nsrl"]},"status":"OK"} + string: '{"result":{"artifact_instance_id":"38756881504693724","created":"2026-07-29T23:04:40.151450+00:00","id":"31218224787620566","sha256":"9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df","sources":["nsrl"]},"status":"OK"} ' headers: @@ -37,7 +37,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 18:39:47 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn X-Billing-ID: @@ -81,7 +81,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 18:39:47 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn status: @@ -103,7 +103,7 @@ interactions: user-agent: - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) method: HEAD - uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df&community=gamma&require_scan=true + uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df&community=gamma&require_scan=false response: body: string: '' @@ -119,7 +119,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 18:39:47 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn X-Billing-ID: @@ -127,6 +127,42 @@ interactions: status: code: 200 message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: HEAD + uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df&community=gamma&require_scan=true + response: + body: + string: '' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Type: + - text/html; charset=utf-8 + Date: + - Wed, 29 Jul 2026 23:04:40 GMT + Server: + - gunicorn + status: + code: 204 + message: NO CONTENT - request: body: '{"sha256":"9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df","source":"commercial","community":"gamma"}' headers: @@ -150,7 +186,7 @@ interactions: uri: http://artifact-index-e2e:9696/v3/known-good response: body: - string: '{"result":{"artifact_instance_id":"90039023884233228","created":"2026-07-29T18:39:47.299724+00:00","id":"66682229801759442","sha256":"9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df","sources":["commercial","nsrl"]},"status":"OK"} + string: '{"result":{"artifact_instance_id":"38756881504693724","created":"2026-07-29T23:04:40.151450+00:00","id":"31218224787620566","sha256":"9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df","sources":["commercial","nsrl"]},"status":"OK"} ' headers: @@ -165,7 +201,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 18:39:47 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn X-Billing-ID: @@ -192,7 +228,7 @@ interactions: uri: http://artifact-index-e2e:9696/v3/known-good?sha256=9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df&community=gamma response: body: - string: '{"result":{"artifact_instance_id":"90039023884233228","created":"2026-07-29T18:39:47.299724+00:00","id":"66682229801759442","sha256":"9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df","sources":["commercial","nsrl"]},"status":"OK"} + string: '{"result":{"artifact_instance_id":"38756881504693724","created":"2026-07-29T23:04:40.151450+00:00","id":"31218224787620566","sha256":"9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df","sources":["commercial","nsrl"]},"status":"OK"} ' headers: @@ -207,7 +243,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 18:39:47 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn X-Billing-ID: @@ -251,7 +287,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 18:39:47 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn status: @@ -291,7 +327,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 18:39:47 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn X-Billing-ID: @@ -333,7 +369,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 18:39:47 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn status: From dad53b68fa60941e9bad3320a8652b2620aa6ddd Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 29 Jul 2026 20:20:23 -0300 Subject: [PATCH 11/20] fix: attribute the HEAD swallow to the method, not the missing parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-ups. - The exists() docstring said a non-2xx never raises "because the probe carries no result parser". Wrong cause, and it generalises to a falsehood: parse_response maps non-2xx to an exception whether or not a parser is set — test_500_raises_even_without_parser pins exactly that. What actually happens is the HEAD short-circuit, which sets the status as the result and returns before the mapping. exists_hash has both properties, so the conclusion held while the reason did not. - Cover _bad_status_message's third arm: a 500 whose errors is a bare string renders on one line. Both specs promise it; the dict and list arms had tests and this one did not. - specs/04 described _client_harness as what "every respx module imports" (two modules use respx directly) and credited the `_` prefix for keeping it uncollected (it is not matching *_test.py / test_*.py that does). --- specs/04-testing.md | 2 +- src/polyswarm_api/aio/api.py | 14 ++++++++++---- src/polyswarm_api/api.py | 14 ++++++++++---- test/core_test.py | 16 ++++++++++++++++ 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/specs/04-testing.md b/specs/04-testing.md index cabf4fdc..4079388c 100644 --- a/specs/04-testing.md +++ b/specs/04-testing.md @@ -19,7 +19,7 @@ How the test suite is organised. Three layers: pure unit tests (no HTTP at all - `test/conftest.py` — pytest configuration. - `test/core_test.py` — pure-unit tests for `parse_response`, `PolyswarmRequest`, and resource builders. No httpx, no fixtures. -- `test/_client_harness.py` — the parametrised `ClientTestCase` harness (`_MockBoundary` / `_AsyncToSync`) every `respx` test module imports. Not collected itself (the `_` prefix keeps it out of `python_files`), same as `_e2e_helpers.py`. +- `test/_client_harness.py` — the parametrised `ClientTestCase` harness (`_MockBoundary` / `_AsyncToSync`), importable by any `respx` module that wants one body to cover both transports (invariant 5). Not every `respx` user needs it: `client_scan_test.py` / `async_client_test.py` drive `respx` directly for a handful of cases that are already transport-specific. Not collected itself — it matches neither `*_test.py` nor `test_*.py`, so pytest never picks it up (the leading `_` is a naming convention, not the mechanism), same as `_e2e_helpers.py`. - `test/metadata_field_properties_test.py` — the canonical example of the parametrised `ClientTestCase` harness with `respx`-backed mocking. - `test/client_scan_test.py` — sync, VCR-backed integration tests (not yet on the parametrised harness — follow-up work). - `test/async_client_test.py` — async, VCR-backed integration tests (not yet on the parametrised harness — follow-up work). diff --git a/src/polyswarm_api/aio/api.py b/src/polyswarm_api/aio/api.py index 59b2befa..d3e0cb46 100644 --- a/src/polyswarm_api/aio/api.py +++ b/src/polyswarm_api/aio/api.py @@ -1652,10 +1652,16 @@ async def exists(self, hash_, hash_type=None, require_scan=False): .. note:: This is an existence probe, and its status codes are a **frozen contract**: ``200`` means found, ``204`` means not found, and anything else means the - *request* was wrong. Because the probe carries no result parser, a non-2xx - status never raises here — it collapses to ``False``. So a server error is - indistinguishable from a genuine "does not exist", which is why neither side - may widen what counts as found. See ``specs/03-endpoints.md``. + *request* was wrong. + + A non-2xx status does not raise here, and the reason is the **method**, not the + missing result parser: ``parse_response`` short-circuits ``HEAD`` — setting the + status code as the result and returning — before it reaches the + non-2xx→exception mapping, which otherwise applies whether or not a parser is + set (see ``test_500_raises_even_without_parser``). So on this one endpoint a + server error collapses to ``False``, indistinguishable from a genuine "does not + exist", which is why neither side may widen what counts as found. + See ``specs/03-endpoints.md``. """ logger.info('Exists for hash %s', hash_) hash_ = resources.Hash.from_hashable(hash_, hash_type=hash_type) diff --git a/src/polyswarm_api/api.py b/src/polyswarm_api/api.py index 42068f5a..5877a2a5 100644 --- a/src/polyswarm_api/api.py +++ b/src/polyswarm_api/api.py @@ -1999,10 +1999,16 @@ def exists(self, hash_, hash_type=None, require_scan=False): .. note:: This is an existence probe, and its status codes are a **frozen contract**: ``200`` means found, ``204`` means not found, and anything else means the - *request* was wrong. Because the probe carries no result parser, a non-2xx - status never raises here — it collapses to ``False``. So a server error is - indistinguishable from a genuine "does not exist", which is why neither side - may widen what counts as found. See ``specs/03-endpoints.md``. + *request* was wrong. + + A non-2xx status does not raise here, and the reason is the **method**, not the + missing result parser: ``parse_response`` short-circuits ``HEAD`` — setting the + status code as the result and returning — before it reaches the + non-2xx→exception mapping, which otherwise applies whether or not a parser is + set (see ``test_500_raises_even_without_parser``). So on this one endpoint a + server error collapses to ``False``, indistinguishable from a genuine "does not + exist", which is why neither side may widen what counts as found. + See ``specs/03-endpoints.md``. """ logger.info("Exists for hash %s", hash_) hash_ = resources.Hash.from_hashable(hash_, hash_type=hash_type) diff --git a/test/core_test.py b/test/core_test.py index b6f4b9a5..f8fb076e 100644 --- a/test/core_test.py +++ b/test/core_test.py @@ -410,6 +410,22 @@ def test_500_list_errors_still_render_one_entry_per_line(self): ) assert 'Errors:\nfirst problem\nsecond problem' in str(ei.value) + def test_500_string_errors_render_on_one_line(self): + # The third arm. Iterating a bare string yields characters, so without it a prose + # `errors` rendered one letter per line — the same failure the mapping arm was added + # for. Both specs now promise this shape renders as-is, so pin it beside the other two. + req = PolyswarmRequest(api=_FakeApi(), method='POST', url='u', + result_parser=_SampleResource) + with pytest.raises(exceptions.RequestException) as ei: + parse_response( + _FakeResponse(status_code=500, body={ + 'status': 'error', 'result': 'boom', 'errors': 'some prose', + }), + req, + ) + assert 'Errors:\nsome prose' in str(ei.value) + assert 'Errors:\ns\no\nm' not in str(ei.value) + def test_500_raises_even_without_parser(self): # Regression: fire-and-forget endpoints (no result_parser) must # still surface non-2xx as exceptions, not swallow them. From 3d1a4ea9f7a1e8aac424afd6027c5a1b3d6d43f7 Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 29 Jul 2026 20:58:07 -0300 Subject: [PATCH 12/20] test: assert the hash existence probe against the real server, not a mock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The probe's 200/204 arms were covered only by a respx mock, which asserts this SDK's mapping and not the server's behaviour — so a server-side flip left the suite green. That is exactly how the 4.0 exists() inversion shipped in 4.0.0 and 4.1.0, and artifact-index's specs/09 named the gap. test_hash_existence_probe_against_the_real_server (+ async twin) now provisions its own resources the way every other e2e test here does — two deterministic EICAR variants, one submitted and scanned, one nothing ever submits — and asserts: never submitted -> False (204), plain and require_scan submitted+scanned -> True (200), plain and require_scan Deriving the absent case from its own uid rather than probing the sha about to be submitted is what keeps the test re-runnable against a reused stack. Cassettes recorded live, so test/vcr/ now carries HEAD interactions for both codes and both run VCR-off against the stack in every e2e job. The mock keeps exactly one arm — 404 -> False — because a well-formed probe never produces a 404 (artifact-index reserves it for the request being wrong), and its docstring says so rather than implying e2e could not cover the rest. Also from review: - exists()' note leaked a test name into public help() output and said "anything else means the request was wrong", which reads against specs/03 (404 also maps to absent). Rewritten. - _normalise_sources logged its discards at DEBUG while the docstring and specs/05 promise a discard is never silent. A library under an app configured at INFO would show nothing. WARNING matches the promise. - specs/04 claimed every respx module imports ClientTestCase (one does) and invariant 5's only exemption was live/VCR tests, which left the direct-respx bodies in the two client modules non-compliant by its own text. Invariant 5 now names the single-transport exemption, and the harness docstring agrees. --- specs/04-testing.md | 4 +- src/polyswarm_api/aio/api.py | 19 +- src/polyswarm_api/api.py | 19 +- src/polyswarm_api/exceptions.py | 4 +- test/_client_harness.py | 7 +- test/async_client_test.py | 54 +- test/client_scan_test.py | 43 + ...xistence_probe_against_the_real_server.vcr | 712 ++++++++++++++++ ...xistence_probe_against_the_real_server.vcr | 806 ++++++++++++++++++ 9 files changed, 1636 insertions(+), 32 deletions(-) create mode 100644 test/vcr/test_async_hash_existence_probe_against_the_real_server.vcr create mode 100644 test/vcr/test_hash_existence_probe_against_the_real_server.vcr diff --git a/specs/04-testing.md b/specs/04-testing.md index 4079388c..df957be2 100644 --- a/specs/04-testing.md +++ b/specs/04-testing.md @@ -10,7 +10,7 @@ How the test suite is organised. Three layers: pure unit tests (no HTTP at all 2. **Tests must pass against the live e2e stack with VCR off.** VCR is an efficiency cache, not a load-bearing requirement. Don't hardcode `record_mode='none'`. If a test only works against the recorded cassette, that's a test bug. 3. **Cassette re-recording is delete-driven, and recording is live-only.** `rm test/vcr/.vcr && pytest …` re-records against the live e2e. `record_mode='once'` (the default) makes this work without flag-flipping. Cassettes are always produced by running the test against the live stack — never copied from a sibling test's cassette and never hand-edited (beyond scrubbing sensitive values). 4. **One cassette serves both transports.** Sync and async tests targeting the same scenario use the same on-the-wire request shape (because both clients run on `httpx`). The VCR matcher is configured so cassettes survive `httpx`'s param-ordering differences from the original `requests`-recorded format — see "VCR matcher convention" below. -5. **`respx` tests (where justified under invariant 1) use the parametrised `ClientTestCase` harness.** It auto-emits `Sync` and `Async` siblings so the same body runs against both clients. Don't write parallel sync / async respx bodies. (Live/VCR tests can't use this harness — `_AsyncToSync` is respx-only — so they follow the `client_scan_test.py` / `async_client_test.py` pattern instead.) +5. **`respx` tests (where justified under invariant 1) use the parametrised `ClientTestCase` harness** *when the scenario is transport-agnostic*. It auto-emits `Sync` and `Async` siblings so one body runs against both clients; don't hand-write parallel sync / async respx bodies for the same scenario. Two exemptions: live/VCR tests can't use it at all (`_AsyncToSync` is respx-only, so they follow the `client_scan_test.py` / `async_client_test.py` pattern), and a case that is *inherently* one-transport — an async-only streaming or cancellation behaviour, or a mapping arm asserted once because it is transport-independent — belongs beside its transport's own module. Those are the direct-`respx` bodies in `client_scan_test.py` / `async_client_test.py`; each one states in its docstring why it is not on the harness. 6. **The HTTP mocking library follows the transport.** Both clients use `httpx`, so `respx` is the mocking library. 7. **Prefer the pure-unit tier for builder + parse logic.** `PolyswarmRequest` builders are pure data; `parse_response` is a pure function. They're testable without httpx, async, or fixtures. Use this tier for any bug that can be reproduced without network involvement — including request-shape assertions (body vs query routing, None-omission, bodyless DELETE) that a cassette can't express directly. 8. **The live VCR-off run is parallel (`pytest -n 8`).** New tests must be isolation-safe: own uniquely-keyed resources (deterministic per-test `uid` — `malicious_artifact(uid)`, `uid_ip` / `uid_host` / `uid_yara`; hash-keyed resources derive their sha from the test's own EICAR variant via `malicious_artifact(uid)`), assert only on their own data, and share no mutable state, so xdist workers never collide. See "Running the suite in parallel" below. @@ -54,7 +54,7 @@ A respx body for that same arm was written first and deleted once the live cover ## The parametrised `ClientTestCase` harness -Implemented in `test/_client_harness.py`; every `respx` test module imports `ClientTestCase` from it (`metadata_field_properties_test.py` is the canonical example of using it). The shape: +Implemented in `test/_client_harness.py`, importable by any `respx` module that wants one body over both transports; `metadata_field_properties_test.py` is the canonical user (and currently the only one — the other `respx` bodies are the single-transport cases invariant 5 exempts). The shape: ```python # test/_client_harness.py diff --git a/src/polyswarm_api/aio/api.py b/src/polyswarm_api/aio/api.py index d3e0cb46..1426532c 100644 --- a/src/polyswarm_api/aio/api.py +++ b/src/polyswarm_api/aio/api.py @@ -1654,14 +1654,17 @@ async def exists(self, hash_, hash_type=None, require_scan=False): ``200`` means found, ``204`` means not found, and anything else means the *request* was wrong. - A non-2xx status does not raise here, and the reason is the **method**, not the - missing result parser: ``parse_response`` short-circuits ``HEAD`` — setting the - status code as the result and returning — before it reaches the - non-2xx→exception mapping, which otherwise applies whether or not a parser is - set (see ``test_500_raises_even_without_parser``). So on this one endpoint a - server error collapses to ``False``, indistinguishable from a genuine "does not - exist", which is why neither side may widen what counts as found. - See ``specs/03-endpoints.md``. + Only ``200`` is ``True``. ``204`` and ``404`` are both ``False`` — the server + answers ``204`` for absent, and ``404`` is tolerated as absent for historical + reasons. + + A non-2xx status does not raise here, and the reason is the **method**, not a + missing result parser: ``parse_response`` short-circuits ``HEAD``, setting the + status code as the result and returning before it reaches the non-2xx→exception + mapping (which otherwise applies whether or not a parser is set). So on this + endpoint a server error also collapses to ``False``, indistinguishable from a + genuine "does not exist" — which is why neither side may widen what counts as + found. See ``specs/03-endpoints.md``. """ logger.info('Exists for hash %s', hash_) hash_ = resources.Hash.from_hashable(hash_, hash_type=hash_type) diff --git a/src/polyswarm_api/api.py b/src/polyswarm_api/api.py index 5877a2a5..817b445f 100644 --- a/src/polyswarm_api/api.py +++ b/src/polyswarm_api/api.py @@ -2001,14 +2001,17 @@ def exists(self, hash_, hash_type=None, require_scan=False): ``200`` means found, ``204`` means not found, and anything else means the *request* was wrong. - A non-2xx status does not raise here, and the reason is the **method**, not the - missing result parser: ``parse_response`` short-circuits ``HEAD`` — setting the - status code as the result and returning — before it reaches the - non-2xx→exception mapping, which otherwise applies whether or not a parser is - set (see ``test_500_raises_even_without_parser``). So on this one endpoint a - server error collapses to ``False``, indistinguishable from a genuine "does not - exist", which is why neither side may widen what counts as found. - See ``specs/03-endpoints.md``. + Only ``200`` is ``True``. ``204`` and ``404`` are both ``False`` — the server + answers ``204`` for absent, and ``404`` is tolerated as absent for historical + reasons. + + A non-2xx status does not raise here, and the reason is the **method**, not a + missing result parser: ``parse_response`` short-circuits ``HEAD``, setting the + status code as the result and returning before it reaches the non-2xx→exception + mapping (which otherwise applies whether or not a parser is set). So on this + endpoint a server error also collapses to ``False``, indistinguishable from a + genuine "does not exist" — which is why neither side may widen what counts as + found. See ``specs/03-endpoints.md``. """ logger.info("Exists for hash %s", hash_) hash_ = resources.Hash.from_hashable(hash_, hash_type=hash_type) diff --git a/src/polyswarm_api/exceptions.py b/src/polyswarm_api/exceptions.py index 62de179e..9a2ebd6b 100644 --- a/src/polyswarm_api/exceptions.py +++ b/src/polyswarm_api/exceptions.py @@ -57,10 +57,10 @@ def _normalise_sources(sources): elif isinstance(source, dict) and isinstance(source.get('tool'), str): names.append(source['tool']) else: - logger.debug('Dropping unrecognised known-good sources entry: %r', source) + logger.warning('Dropping unrecognised known-good sources entry: %r', source) return names if sources is not None: - logger.debug('Dropping unrecognised known-good sources payload: %r', sources) + logger.warning('Dropping unrecognised known-good sources payload: %r', sources) return [] diff --git a/test/_client_harness.py b/test/_client_harness.py index d3096a36..18be58fe 100644 --- a/test/_client_harness.py +++ b/test/_client_harness.py @@ -1,6 +1,7 @@ """The parametrised ``ClientTestCase`` harness for the respx-mocked tier. -Shared by every respx test module (see specs/04-testing.md, invariant 5): one +Importable by any respx module that wants one body over both transports (see +specs/04-testing.md, invariant 5, including the single-transport exemption): one test body runs against **both** transports, because ``__init_subclass__`` emits ``Sync`` and ``Async`` siblings for each subclass. That's what keeps the mocked tier from growing parallel sync / async bodies — and what keeps the @@ -57,8 +58,8 @@ def last_request_url(self) -> str: Reads ``calls[-1]`` like ``last_request_body``: the two used to disagree (this one read ``calls[0]``), which is invisible in a single-request test and silently wrong in a multi-request one — a test would compare the first request's URL against the last - request's body. Harmless while this lived in one module; not once it is the shared - harness every respx test imports. + request's body. Harmless while this lived in one module; not once it is a shared + harness any respx module can import. """ return str(self._router.calls[-1].request.url) diff --git a/test/async_client_test.py b/test/async_client_test.py index 6228d20a..b5bef76d 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -445,6 +445,34 @@ async def test_async_known_good_lifecycle(self, uid): await api.known_good_get(sha256=sha) assert not isinstance(ei.value, exceptions.KnownGoodWithheldException) + @vcr.use_cassette() + async def test_async_hash_existence_probe_against_the_real_server(self, uid): + # Async twin of the sync probe test — see it for why this is asserted against the + # server rather than a mock: the probe is a HEAD with no result parser, so it has no + # error channel and a server-side widening of "found" is a silent wrong boolean. + async with self._api() as api: + # Two distinct variants: one submitted below, one nothing ever submits — which is + # what keeps this re-runnable against a reused stack. + _absent_content, absent_sha = malicious_artifact(f'{uid}-never-submitted') + _content, sha = malicious_artifact(uid) + assert absent_sha != sha + + # ABSENT -> 204 -> False, in both forms. + assert await api.exists(absent_sha, hash_type='sha256') is False + assert await api.exists(absent_sha, hash_type='sha256', require_scan=True) is False + + # PRESENT: submit it and let the scan settle. + instance, submitted_sha = await submit_and_scan(api, uid) + assert submitted_sha == sha + assert instance.window_closed + + for _ in range(30): + if await api.exists(sha, hash_type='sha256'): + break + await asyncio.sleep(1) + assert await api.exists(sha, hash_type='sha256') is True + assert await api.exists(sha, hash_type='sha256', require_scan=True) is True + # ── Sandbox ─────────────────────────────────────────────────────────────── @vcr.use_cassette() @@ -1075,18 +1103,26 @@ def write(self, b): @respx.mock -async def test_async_exists_maps_200_true_204_and_404_false(): - """End-to-end ``exists``: the HEAD status drives the result. The endpoint - returns 200 when the artifact is present and 204 when it is absent ("request - worked, no matching artifact"), so only a 200 is True — a 204 is a successful - 2xx that means the *opposite* of "exists" and must be False, as must a 404.""" +async def test_async_exists_maps_404_false(): + """``exists()`` maps a ``404`` to ``False`` — the one arm of this mapping the e2e stack + cannot produce, so the only one that stays mocked. + + The ``200`` (present) and ``204`` (absent) arms are asserted against the **real server** on + resources the test provisions itself, in + ``test_async_hash_existence_probe_against_the_real_server`` and its sync twin. That is the + default (invariant 1) and it is the only thing that would catch a *server-side* flip, which + a mock cannot by construction — the 4.0 ``exists()`` inversion survived precisely because + this endpoint's coverage was entirely mocked. + + ``404`` stays here because artifact-index never answers it for a well-formed probe: per its + ``specs/09-hash-search-head-contract.md`` that code is reserved for the *request* being + wrong, and a bad hash or hash type raises ``400``. The mapping is still worth pinning — + clients have tolerated ``404``-as-absent historically, so the SDK must not start raising if + a proxy or an older deployment in front of the API emits one. + """ route = respx.head(f'{BASE_URL}/search/hash/sha256') api = PolySwarmAsyncAPI(API_KEY, uri=BASE_URL, community='gamma') try: - route.mock(return_value=httpx.Response(200)) - assert await api.exists(SHA256) is True - route.mock(return_value=httpx.Response(204)) # absent: "worked, nothing found" - assert await api.exists(SHA256) is False route.mock(return_value=httpx.Response(404)) assert await api.exists(SHA256) is False finally: diff --git a/test/client_scan_test.py b/test/client_scan_test.py index 648cc58b..2117adfd 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -823,6 +823,49 @@ def test_known_good_lifecycle(self): v3api.known_good_get(sha256=sha) assert not isinstance(ei.value, exceptions.KnownGoodWithheldException) + @vcr.use_cassette() + def test_hash_existence_probe_against_the_real_server(self): + # The hash existence probe, end to end, on resources this test provisions itself. + # + # Its status codes are a frozen contract — artifact-index + # specs/09-hash-search-head-contract.md: 200 = found, 204 = not found. The SDK sends + # this with no result parser AND as a HEAD, so parse_response short-circuits before the + # non-2xx mapping and hands `exists()` a bare status code. There is no error channel: + # a server-side widening of "found" produces a wrong boolean, silently. That is the + # shape of the inversion this SDK shipped in 4.0.0/4.1.0 — `int(result) // 100 == 2` + # made every artifact the index had never seen report present, and the suite stayed + # green because its probe coverage was entirely mocked. + # + # So this asserts the server's behaviour rather than the SDK's mapping, on both sides + # of the 200/204 boundary, using the ordinary provisioning path. + v3api = PolyswarmAPI(self.test_api_key, uri=f'http://artifact-index-e2e:9696/{self.api_version}', community='gamma') + # Two distinct EICAR variants, both deterministic: one this test submits, and one + # NOTHING ever submits. Deriving the absent case from its own uid is what keeps this + # test re-runnable against a reused stack — probing the sha we are about to submit + # would pass only on a freshly booted one. + _absent_content, absent_sha = malicious_artifact(f'{self._testMethodName}-never-submitted') + _content, sha = malicious_artifact(self._testMethodName) + assert absent_sha != sha + + # ABSENT -> 204 -> False, in both forms. + assert v3api.exists(absent_sha, hash_type='sha256') is False + assert v3api.exists(absent_sha, hash_type='sha256', require_scan=True) is False + + # PRESENT: submit the same sha and let the scan settle, so `last_scanned` lands in a + # scan state and both forms answer 200 -> True. + instance, submitted_sha = submit_and_scan(v3api, self._testMethodName) + assert submitted_sha == sha, 'the probe must be asked about the sha we just submitted' + assert instance.window_closed + + # The search row is written by an async task, so allow for index lag — but assert the + # value rather than polling until it agrees. + for _ in range(30): + if v3api.exists(sha, hash_type='sha256'): + break + time.sleep(1) + assert v3api.exists(sha, hash_type='sha256') is True + assert v3api.exists(sha, hash_type='sha256', require_scan=True) is True + @vcr.use_cassette() def test_sandbox_providers(self): v3api = PolyswarmAPI(self.test_api_key, uri='http://artifact-index-e2e:9696/v3', community='gamma') diff --git a/test/vcr/test_async_hash_existence_probe_against_the_real_server.vcr b/test/vcr/test_async_hash_existence_probe_against_the_real_server.vcr new file mode 100644 index 00000000..236d9823 --- /dev/null +++ b/test/vcr/test_async_hash_existence_probe_against_the_real_server.vcr @@ -0,0 +1,712 @@ +interactions: +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: HEAD + uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=f38912693d48718b922fc4b78d0f797b5174d030284e1e2b09f4bd1c8caed82e&community=gamma&require_scan=false + response: + body: + string: '' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Type: + - text/html; charset=utf-8 + Date: + - Wed, 29 Jul 2026 23:39:07 GMT + Server: + - gunicorn + status: + code: 204 + message: NO CONTENT +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: HEAD + uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=f38912693d48718b922fc4b78d0f797b5174d030284e1e2b09f4bd1c8caed82e&community=gamma&require_scan=true + response: + body: + string: '' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Type: + - text/html; charset=utf-8 + Date: + - Wed, 29 Jul 2026 23:39:07 GMT + Server: + - gunicorn + status: + code: 204 + message: NO CONTENT +- 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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: POST + uri: http://artifact-index-e2e:9696/v3/instance + response: + body: + string: '{"result":{"artifact_id":"83673095931831170","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-07-29T23:39:07.351071+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-07-29T23:39:07.351071+00:00","id":"83673095931831170","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/83673095931831170","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/f9/dd/4a/f9dd4a91-07f9-4609-93ce-29c18cb29d9f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233907Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=8992c62f1c382e6ff360a3c033c660e00bf802d82e612ac7cf49e21f939757d4","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: + - Wed, 29 Jul 2026 23:39:07 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_hash_existence_probe_against_the_real_server' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '124' + host: + - minio:9000 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: PUT + uri: http://minio:9000/artifact-index/instances/f9/dd/4a/f9dd4a91-07f9-4609-93ce-29c18cb29d9f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233907Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=8992c62f1c382e6ff360a3c033c660e00bf802d82e612ac7cf49e21f939757d4 + response: + body: + string: '' + headers: + Accept-Ranges: + - bytes + Content-Length: + - '0' + Date: + - Wed, 29 Jul 2026 23:39:07 GMT + ETag: + - '"5a99dfcdb13dcd3832de852588a3b09c"' + Server: + - MinIO + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + Vary: + - Origin + - Accept-Encoding + X-Amz-Id-2: + - dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8 + X-Amz-Request-Id: + - 18C6E70D7B148D3C + X-Content-Type-Options: + - nosniff + X-Ratelimit-Limit: + - '13088' + X-Ratelimit-Remaining: + - '13088' + X-Xss-Protection: + - 1; mode=block + x-amz-expiration: + - expiry-date="Fri, 31 Jul 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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: PUT + uri: http://artifact-index-e2e:9696/v3/instance?id=83673095931831170 + response: + body: + string: '{"result":{"artifact_id":"83673095931831170","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-07-29T23:39:07.351071+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-07-29T23:39:07.351071+00:00","id":"83673095931831170","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/83673095931831170","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/f9/dd/4a/f9dd4a91-07f9-4609-93ce-29c18cb29d9f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233907Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=8992c62f1c382e6ff360a3c033c660e00bf802d82e612ac7cf49e21f939757d4","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: + - Wed, 29 Jul 2026 23:39: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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/83673095931831170 + response: + body: + string: '{"result":{"artifact_id":"83673095931831170","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-07-29T23:39:07.351071+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-07-29T23:39:07.351071+00:00","id":"83673095931831170","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/83673095931831170","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/f9/dd/4a/f9dd4a91-07f9-4609-93ce-29c18cb29d9f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233907Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=8992c62f1c382e6ff360a3c033c660e00bf802d82e612ac7cf49e21f939757d4","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: + - Wed, 29 Jul 2026 23:39: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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/83673095931831170 + response: + body: + string: '{"result":{"artifact_id":"83673095931831170","assertions":[{"author":"299533097263972","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-07-29T23:39:07.351071+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:39:07.351071+00:00","id":"83673095931831170","known_good":null,"last_scanned":null,"last_seen":null,"md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-29T23:39:07.567288+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 + 23:39:07+00:00","fileinodechangedate":"2026:07:29 23:39:07+00:00","filemodifydate":"2026:07:29 + 23:39:07+00:00","filename":"tmp__bt2dla","filepermissions":"-rw-r--r--","filesize":"124 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp__bt2dla","wordcount":2},"updated":"2026-07-29T23:39:07.567288+00:00"},{"created":"2026-07-29T23:39:07.502260+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-29T23:39:07.502260+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/83673095931831170","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/f9/dd/4a/f9dd4a91-07f9-4609-93ce-29c18cb29d9f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233907Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=8992c62f1c382e6ff360a3c033c660e00bf802d82e612ac7cf49e21f939757d4","votes":[],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2949' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 23:39: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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/83673095931831170 + response: + body: + string: '{"result":{"artifact_id":"83673095931831170","assertions":[{"author":"299533097263972","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-07-29T23:39:07.351071+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:39:07.351071+00:00","id":"83673095931831170","known_good":null,"last_scanned":null,"last_seen":null,"md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-29T23:39:07.567288+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 + 23:39:07+00:00","fileinodechangedate":"2026:07:29 23:39:07+00:00","filemodifydate":"2026:07:29 + 23:39:07+00:00","filename":"tmp__bt2dla","filepermissions":"-rw-r--r--","filesize":"124 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp__bt2dla","wordcount":2},"updated":"2026-07-29T23:39:07.567288+00:00"},{"created":"2026-07-29T23:39:07.502260+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-29T23:39:07.502260+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/83673095931831170","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/f9/dd/4a/f9dd4a91-07f9-4609-93ce-29c18cb29d9f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233907Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=8992c62f1c382e6ff360a3c033c660e00bf802d82e612ac7cf49e21f939757d4","votes":[],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2949' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 23:39: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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/83673095931831170 + response: + body: + string: '{"result":{"artifact_id":"83673095931831170","assertions":[{"author":"299533097263972","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-07-29T23:39:07.351071+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:39:07.351071+00:00","id":"83673095931831170","known_good":null,"last_scanned":null,"last_seen":null,"md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-29T23:39:07.567288+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 + 23:39:07+00:00","fileinodechangedate":"2026:07:29 23:39:07+00:00","filemodifydate":"2026:07:29 + 23:39:07+00:00","filename":"tmp__bt2dla","filepermissions":"-rw-r--r--","filesize":"124 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp__bt2dla","wordcount":2},"updated":"2026-07-29T23:39:07.567288+00:00"},{"created":"2026-07-29T23:39:07.502260+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-29T23:39:07.502260+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/83673095931831170","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/f9/dd/4a/f9dd4a91-07f9-4609-93ce-29c18cb29d9f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233907Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=8992c62f1c382e6ff360a3c033c660e00bf802d82e612ac7cf49e21f939757d4","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3234' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 23:39: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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/83673095931831170 + response: + body: + string: '{"result":{"artifact_id":"83673095931831170","assertions":[{"author":"299533097263972","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-07-29T23:39:07.351071+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:39:07.351071+00:00","id":"83673095931831170","known_good":null,"last_scanned":null,"last_seen":null,"md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-29T23:39:07.567288+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 + 23:39:07+00:00","fileinodechangedate":"2026:07:29 23:39:07+00:00","filemodifydate":"2026:07:29 + 23:39:07+00:00","filename":"tmp__bt2dla","filepermissions":"-rw-r--r--","filesize":"124 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp__bt2dla","wordcount":2},"updated":"2026-07-29T23:39:07.567288+00:00"},{"created":"2026-07-29T23:39:07.502260+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-29T23:39:07.502260+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/83673095931831170","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/f9/dd/4a/f9dd4a91-07f9-4609-93ce-29c18cb29d9f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233907Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=8992c62f1c382e6ff360a3c033c660e00bf802d82e612ac7cf49e21f939757d4","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3234' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 23:39: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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/83673095931831170 + response: + body: + string: '{"result":{"artifact_id":"83673095931831170","assertions":[{"author":"299533097263972","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-07-29T23:39:07.351071+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:39:07.351071+00:00","id":"83673095931831170","known_good":null,"last_scanned":null,"last_seen":null,"md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-29T23:39:07.567288+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 + 23:39:07+00:00","fileinodechangedate":"2026:07:29 23:39:07+00:00","filemodifydate":"2026:07:29 + 23:39:07+00:00","filename":"tmp__bt2dla","filepermissions":"-rw-r--r--","filesize":"124 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp__bt2dla","wordcount":2},"updated":"2026-07-29T23:39:07.567288+00:00"},{"created":"2026-07-29T23:39:07.502260+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-29T23:39:07.502260+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/83673095931831170","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/f9/dd/4a/f9dd4a91-07f9-4609-93ce-29c18cb29d9f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233907Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=8992c62f1c382e6ff360a3c033c660e00bf802d82e612ac7cf49e21f939757d4","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3234' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 23:39: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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/83673095931831170 + response: + body: + string: '{"result":{"artifact_id":"83673095931831170","assertions":[{"author":"299533097263972","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-07-29T23:39:07.351071+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:39:07.351071+00:00","id":"83673095931831170","known_good":null,"last_scanned":null,"last_seen":null,"md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-29T23:39:07.567288+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 + 23:39:07+00:00","fileinodechangedate":"2026:07:29 23:39:07+00:00","filemodifydate":"2026:07:29 + 23:39:07+00:00","filename":"tmp__bt2dla","filepermissions":"-rw-r--r--","filesize":"124 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp__bt2dla","wordcount":2},"updated":"2026-07-29T23:39:07.567288+00:00"},{"created":"2026-07-29T23:39:07.502260+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-29T23:39:07.502260+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/83673095931831170","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/f9/dd/4a/f9dd4a91-07f9-4609-93ce-29c18cb29d9f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233907Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=8992c62f1c382e6ff360a3c033c660e00bf802d82e612ac7cf49e21f939757d4","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3234' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 23:39: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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/83673095931831170 + response: + body: + string: '{"result":{"artifact_id":"83673095931831170","assertions":[{"author":"299533097263972","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-07-29T23:39:07.351071+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-07-29T23:39:07.351071+00:00","id":"83673095931831170","known_good":null,"last_scanned":"2026-07-29T23:39:07.351071+00:00","last_seen":"2026-07-29T23:39:07.351071+00:00","md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-29T23:39:14.495357+00:00","tool":"polyunite","tool_metadata":{"labels":["nonmalware"],"malware_family":"EICAR","operating_system":[]},"updated":"2026-07-29T23:39:14.495357+00:00"},{"created":"2026-07-29T23:39:07.567288+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 + 23:39:07+00:00","fileinodechangedate":"2026:07:29 23:39:07+00:00","filemodifydate":"2026:07:29 + 23:39:07+00:00","filename":"tmp__bt2dla","filepermissions":"-rw-r--r--","filesize":"124 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp__bt2dla","wordcount":2},"updated":"2026-07-29T23:39:07.567288+00:00"},{"created":"2026-07-29T23:39:07.502260+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-29T23:39:07.502260+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/83673095931831170","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"AWAITING_ARBITRATION","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/f9/dd/4a/f9dd4a91-07f9-4609-93ce-29c18cb29d9f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233907Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=8992c62f1c382e6ff360a3c033c660e00bf802d82e612ac7cf49e21f939757d4","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":true},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3536' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 23:39: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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: HEAD + uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502&community=gamma&require_scan=false + response: + body: + string: '' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '57' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 23:39: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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: HEAD + uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502&community=gamma&require_scan=false + response: + body: + string: '' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '57' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 23:39: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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: HEAD + uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502&community=gamma&require_scan=true + response: + body: + string: '' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '57' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 23:39:14 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +version: 1 diff --git a/test/vcr/test_hash_existence_probe_against_the_real_server.vcr b/test/vcr/test_hash_existence_probe_against_the_real_server.vcr new file mode 100644 index 00000000..fc3aa82b --- /dev/null +++ b/test/vcr/test_hash_existence_probe_against_the_real_server.vcr @@ -0,0 +1,806 @@ +interactions: +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: HEAD + uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=8114c0209d6e20ad5a8cbec01782f065be5cf9804d78801a90583e04551c05a8&community=gamma&require_scan=false + response: + body: + string: '' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Type: + - text/html; charset=utf-8 + Date: + - Wed, 29 Jul 2026 23:38:57 GMT + Server: + - gunicorn + status: + code: 204 + message: NO CONTENT +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: HEAD + uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=8114c0209d6e20ad5a8cbec01782f065be5cf9804d78801a90583e04551c05a8&community=gamma&require_scan=true + response: + body: + string: '' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Type: + - text/html; charset=utf-8 + Date: + - Wed, 29 Jul 2026 23:38:57 GMT + Server: + - gunicorn + status: + code: 204 + message: NO CONTENT +- 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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: POST + uri: http://artifact-index-e2e:9696/v3/instance + response: + body: + string: '{"result":{"artifact_id":"51315590942038050","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-07-29T23:38:57.583670+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/51315590942038050","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","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: + - Wed, 29 Jul 2026 23:38:57 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_hash_existence_probe_against_the_real_server' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '118' + host: + - minio:9000 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: PUT + uri: http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b + response: + body: + string: '' + headers: + Accept-Ranges: + - bytes + Content-Length: + - '0' + Date: + - Wed, 29 Jul 2026 23:38:57 GMT + ETag: + - '"f9ae88ea41d60b331ac29746fec938bd"' + Server: + - MinIO + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + Vary: + - Origin + - Accept-Encoding + X-Amz-Id-2: + - dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8 + X-Amz-Request-Id: + - 18C6E70B34E8D9D1 + X-Content-Type-Options: + - nosniff + X-Ratelimit-Limit: + - '13088' + X-Ratelimit-Remaining: + - '13088' + X-Xss-Protection: + - 1; mode=block + x-amz-expiration: + - expiry-date="Fri, 31 Jul 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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: PUT + uri: http://artifact-index-e2e:9696/v3/instance?id=51315590942038050 + response: + body: + string: '{"result":{"artifact_id":"51315590942038050","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-07-29T23:38:57.583670+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/51315590942038050","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","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: + - Wed, 29 Jul 2026 23:38:57 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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/51315590942038050 + response: + body: + string: '{"result":{"artifact_id":"51315590942038050","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-07-29T23:38:57.583670+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/51315590942038050","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","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: + - Wed, 29 Jul 2026 23:38:57 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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/51315590942038050 + response: + body: + string: '{"result":{"artifact_id":"51315590942038050","assertions":[{"author":"299533097263972","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-07-29T23:38:57.583670+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":null,"last_seen":null,"md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-29T23:38:58.231065+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 + 23:38:58+00:00","fileinodechangedate":"2026:07:29 23:38:58+00:00","filemodifydate":"2026:07:29 + 23:38:58+00:00","filename":"tmp_7t30si7","filepermissions":"-rw-r--r--","filesize":"118 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp_7t30si7","wordcount":2},"updated":"2026-07-29T23:38:58.231065+00:00"},{"created":"2026-07-29T23:38:58.163477+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-29T23:38:58.163477+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/51315590942038050","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","votes":[],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2945' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 23:38:58 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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/51315590942038050 + response: + body: + string: '{"result":{"artifact_id":"51315590942038050","assertions":[{"author":"299533097263972","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-07-29T23:38:57.583670+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":null,"last_seen":null,"md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-29T23:38:58.231065+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 + 23:38:58+00:00","fileinodechangedate":"2026:07:29 23:38:58+00:00","filemodifydate":"2026:07:29 + 23:38:58+00:00","filename":"tmp_7t30si7","filepermissions":"-rw-r--r--","filesize":"118 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp_7t30si7","wordcount":2},"updated":"2026-07-29T23:38:58.231065+00:00"},{"created":"2026-07-29T23:38:58.163477+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-29T23:38:58.163477+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/51315590942038050","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","votes":[],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2945' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 23:38:59 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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/51315590942038050 + response: + body: + string: '{"result":{"artifact_id":"51315590942038050","assertions":[{"author":"299533097263972","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-07-29T23:38:57.583670+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":null,"last_seen":null,"md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-29T23:38:58.231065+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 + 23:38:58+00:00","fileinodechangedate":"2026:07:29 23:38:58+00:00","filemodifydate":"2026:07:29 + 23:38:58+00:00","filename":"tmp_7t30si7","filepermissions":"-rw-r--r--","filesize":"118 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp_7t30si7","wordcount":2},"updated":"2026-07-29T23:38:58.231065+00:00"},{"created":"2026-07-29T23:38:58.163477+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-29T23:38:58.163477+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/51315590942038050","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3230' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 23:39:00 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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/51315590942038050 + response: + body: + string: '{"result":{"artifact_id":"51315590942038050","assertions":[{"author":"299533097263972","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-07-29T23:38:57.583670+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":null,"last_seen":null,"md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-29T23:38:58.231065+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 + 23:38:58+00:00","fileinodechangedate":"2026:07:29 23:38:58+00:00","filemodifydate":"2026:07:29 + 23:38:58+00:00","filename":"tmp_7t30si7","filepermissions":"-rw-r--r--","filesize":"118 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp_7t30si7","wordcount":2},"updated":"2026-07-29T23:38:58.231065+00:00"},{"created":"2026-07-29T23:38:58.163477+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-29T23:38:58.163477+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/51315590942038050","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3230' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 23:39: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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/51315590942038050 + response: + body: + string: '{"result":{"artifact_id":"51315590942038050","assertions":[{"author":"299533097263972","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-07-29T23:38:57.583670+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":null,"last_seen":null,"md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-29T23:38:58.231065+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 + 23:38:58+00:00","fileinodechangedate":"2026:07:29 23:38:58+00:00","filemodifydate":"2026:07:29 + 23:38:58+00:00","filename":"tmp_7t30si7","filepermissions":"-rw-r--r--","filesize":"118 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp_7t30si7","wordcount":2},"updated":"2026-07-29T23:38:58.231065+00:00"},{"created":"2026-07-29T23:38:58.163477+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-29T23:38:58.163477+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/51315590942038050","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3230' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 23:39: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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/51315590942038050 + response: + body: + string: '{"result":{"artifact_id":"51315590942038050","assertions":[{"author":"299533097263972","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-07-29T23:38:57.583670+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":null,"last_seen":null,"md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-29T23:38:58.231065+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 + 23:38:58+00:00","fileinodechangedate":"2026:07:29 23:38:58+00:00","filemodifydate":"2026:07:29 + 23:38:58+00:00","filename":"tmp_7t30si7","filepermissions":"-rw-r--r--","filesize":"118 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp_7t30si7","wordcount":2},"updated":"2026-07-29T23:38:58.231065+00:00"},{"created":"2026-07-29T23:38:58.163477+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-29T23:38:58.163477+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/51315590942038050","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3230' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 23:39: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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/51315590942038050 + response: + body: + string: '{"result":{"artifact_id":"51315590942038050","assertions":[{"author":"299533097263972","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-07-29T23:38:57.583670+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":null,"last_seen":null,"md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-29T23:38:58.231065+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 + 23:38:58+00:00","fileinodechangedate":"2026:07:29 23:38:58+00:00","filemodifydate":"2026:07:29 + 23:38:58+00:00","filename":"tmp_7t30si7","filepermissions":"-rw-r--r--","filesize":"118 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp_7t30si7","wordcount":2},"updated":"2026-07-29T23:38:58.231065+00:00"},{"created":"2026-07-29T23:38:58.163477+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-29T23:38:58.163477+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/51315590942038050","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3230' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 23:39: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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/51315590942038050 + response: + body: + string: '{"result":{"artifact_id":"51315590942038050","assertions":[{"author":"299533097263972","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-07-29T23:38:57.583670+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-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":null,"last_seen":null,"md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-29T23:38:58.231065+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 + 23:38:58+00:00","fileinodechangedate":"2026:07:29 23:38:58+00:00","filemodifydate":"2026:07:29 + 23:38:58+00:00","filename":"tmp_7t30si7","filepermissions":"-rw-r--r--","filesize":"118 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp_7t30si7","wordcount":2},"updated":"2026-07-29T23:38:58.231065+00:00"},{"created":"2026-07-29T23:38:58.163477+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-29T23:38:58.163477+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/51315590942038050","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"AWAITING_ARBITRATION","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3273' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 23:39: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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/51315590942038050 + response: + body: + string: '{"result":{"artifact_id":"51315590942038050","assertions":[{"author":"299533097263972","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-07-29T23:38:57.583670+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-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":"2026-07-29T23:38:57.583670+00:00","last_seen":"2026-07-29T23:38:57.583670+00:00","md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-29T23:39:06.204597+00:00","tool":"polyunite","tool_metadata":{"labels":["nonmalware"],"malware_family":"EICAR","operating_system":[]},"updated":"2026-07-29T23:39:06.204597+00:00"},{"created":"2026-07-29T23:38:58.231065+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 + 23:38:58+00:00","fileinodechangedate":"2026:07:29 23:38:58+00:00","filemodifydate":"2026:07:29 + 23:38:58+00:00","filename":"tmp_7t30si7","filepermissions":"-rw-r--r--","filesize":"118 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmp_7t30si7","wordcount":2},"updated":"2026-07-29T23:38:58.231065+00:00"},{"created":"2026-07-29T23:38:58.163477+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-29T23:38:58.163477+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/51315590942038050","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"AWAITING_ARBITRATION","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":true},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3532' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 23:39: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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: HEAD + uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58&community=gamma&require_scan=false + response: + body: + string: '' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '57' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 23:39: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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: HEAD + uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58&community=gamma&require_scan=false + response: + body: + string: '' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '57' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 23:39: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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + method: HEAD + uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58&community=gamma&require_scan=true + response: + body: + string: '' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '57' + Content-Type: + - application/json + Date: + - Wed, 29 Jul 2026 23:39:07 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +version: 1 From 0a704e2b30320b0d3f077edd1d6ee12f13ca25e4 Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 29 Jul 2026 21:21:53 -0300 Subject: [PATCH 13/20] test: poll the strict form, and stop the lifecycle probe racing the index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-ups on the tests added last commit. - test_known_good_lifecycle asserted exists() unpolled immediately after known_good_create, in the same PR whose sibling test explains that the search row is written by an async task. known_good_create returns an artifact_instance_id, so it goes through that same write — the assertion was a flake under TESTS_VCR=off by this PR's own reasoning. - The probe test polled the BROAD form and then asserted the NARROW one. require_scan filters on the scan state of the row the plain form only needs to exist, so it can become true no earlier — polling the loose condition and asserting the strict one is the wrong way round. Both now poll the strict form and assert the captured value, which also keeps each test's request count at or below what its cassette recorded, so no re-record was needed. - Registered `existence_probe` in _LONG_POLE_FRAGMENTS: both tests submit, wait for a settle and then tolerate index lag, but neither substring-matched, so on the live xdist run they would have backfilled the tail — the straggler pattern that list exists to prevent. - specs/04's amended invariant 5 claimed every direct-respx body states why it is not on the harness. Most predate the harness and say nothing, so the claim is now forward-looking, and the count is honest (~27, not "a handful"). - specs/05 relates KnownGoodWithheldException.sources to ArtifactInstance.known_good_sources: same concept, different normalisation (the latter sorted and deduped), so a consumer does not assume parity. --- specs/04-testing.md | 4 ++-- specs/05-downstream-contract.md | 2 +- test/async_client_test.py | 19 ++++++++++++++++--- test/client_scan_test.py | 24 +++++++++++++++++++----- test/conftest.py | 1 + 5 files changed, 39 insertions(+), 11 deletions(-) diff --git a/specs/04-testing.md b/specs/04-testing.md index df957be2..337bbec8 100644 --- a/specs/04-testing.md +++ b/specs/04-testing.md @@ -10,7 +10,7 @@ How the test suite is organised. Three layers: pure unit tests (no HTTP at all 2. **Tests must pass against the live e2e stack with VCR off.** VCR is an efficiency cache, not a load-bearing requirement. Don't hardcode `record_mode='none'`. If a test only works against the recorded cassette, that's a test bug. 3. **Cassette re-recording is delete-driven, and recording is live-only.** `rm test/vcr/.vcr && pytest …` re-records against the live e2e. `record_mode='once'` (the default) makes this work without flag-flipping. Cassettes are always produced by running the test against the live stack — never copied from a sibling test's cassette and never hand-edited (beyond scrubbing sensitive values). 4. **One cassette serves both transports.** Sync and async tests targeting the same scenario use the same on-the-wire request shape (because both clients run on `httpx`). The VCR matcher is configured so cassettes survive `httpx`'s param-ordering differences from the original `requests`-recorded format — see "VCR matcher convention" below. -5. **`respx` tests (where justified under invariant 1) use the parametrised `ClientTestCase` harness** *when the scenario is transport-agnostic*. It auto-emits `Sync` and `Async` siblings so one body runs against both clients; don't hand-write parallel sync / async respx bodies for the same scenario. Two exemptions: live/VCR tests can't use it at all (`_AsyncToSync` is respx-only, so they follow the `client_scan_test.py` / `async_client_test.py` pattern), and a case that is *inherently* one-transport — an async-only streaming or cancellation behaviour, or a mapping arm asserted once because it is transport-independent — belongs beside its transport's own module. Those are the direct-`respx` bodies in `client_scan_test.py` / `async_client_test.py`; each one states in its docstring why it is not on the harness. +5. **`respx` tests (where justified under invariant 1) use the parametrised `ClientTestCase` harness** *when the scenario is transport-agnostic*. It auto-emits `Sync` and `Async` siblings so one body runs against both clients; don't hand-write parallel sync / async respx bodies for the same scenario. Two exemptions: live/VCR tests can't use it at all (`_AsyncToSync` is respx-only, so they follow the `client_scan_test.py` / `async_client_test.py` pattern), and a case that is *inherently* one-transport — an async-only streaming or cancellation behaviour, or a mapping arm asserted once because it is transport-independent — belongs beside its transport's own module. The direct-`respx` bodies in `client_scan_test.py` / `async_client_test.py` are those cases. Most predate the harness and do not argue their exemption — treat the rule as forward-looking: a **new** respx body that is transport-agnostic goes on the harness, and one that does not should say why in its docstring. Don't read the existing set as a worked example of the rule. 6. **The HTTP mocking library follows the transport.** Both clients use `httpx`, so `respx` is the mocking library. 7. **Prefer the pure-unit tier for builder + parse logic.** `PolyswarmRequest` builders are pure data; `parse_response` is a pure function. They're testable without httpx, async, or fixtures. Use this tier for any bug that can be reproduced without network involvement — including request-shape assertions (body vs query routing, None-omission, bodyless DELETE) that a cassette can't express directly. 8. **The live VCR-off run is parallel (`pytest -n 8`).** New tests must be isolation-safe: own uniquely-keyed resources (deterministic per-test `uid` — `malicious_artifact(uid)`, `uid_ip` / `uid_host` / `uid_yara`; hash-keyed resources derive their sha from the test's own EICAR variant via `malicious_artifact(uid)`), assert only on their own data, and share no mutable state, so xdist workers never collide. See "Running the suite in parallel" below. @@ -19,7 +19,7 @@ How the test suite is organised. Three layers: pure unit tests (no HTTP at all - `test/conftest.py` — pytest configuration. - `test/core_test.py` — pure-unit tests for `parse_response`, `PolyswarmRequest`, and resource builders. No httpx, no fixtures. -- `test/_client_harness.py` — the parametrised `ClientTestCase` harness (`_MockBoundary` / `_AsyncToSync`), importable by any `respx` module that wants one body to cover both transports (invariant 5). Not every `respx` user needs it: `client_scan_test.py` / `async_client_test.py` drive `respx` directly for a handful of cases that are already transport-specific. Not collected itself — it matches neither `*_test.py` nor `test_*.py`, so pytest never picks it up (the leading `_` is a naming convention, not the mechanism), same as `_e2e_helpers.py`. +- `test/_client_harness.py` — the parametrised `ClientTestCase` harness (`_MockBoundary` / `_AsyncToSync`), importable by any `respx` module that wants one body to cover both transports (invariant 5). Not every `respx` user needs it: `client_scan_test.py` / `async_client_test.py` drive `respx` directly for a number of cases (~27 bodies between them), most of which predate the harness — see invariant 5 for which of those are legitimately exempt and which are just older than the rule. Not collected itself — it matches neither `*_test.py` nor `test_*.py`, so pytest never picks it up (the leading `_` is a naming convention, not the mechanism), same as `_e2e_helpers.py`. - `test/metadata_field_properties_test.py` — the canonical example of the parametrised `ClientTestCase` harness with `respx`-backed mocking. - `test/client_scan_test.py` — sync, VCR-backed integration tests (not yet on the parametrised harness — follow-up work). - `test/async_client_test.py` — async, VCR-backed integration tests (not yet on the parametrised harness — follow-up work). diff --git a/specs/05-downstream-contract.md b/specs/05-downstream-contract.md index f4f35680..e60472ae 100644 --- a/specs/05-downstream-contract.md +++ b/specs/05-downstream-contract.md @@ -178,7 +178,7 @@ class InvalidValueException(PolyswarmException): ... class TimeoutException(PolyswarmException): ... ``` -`KnownGoodWithheldException` is the 404 raised when a download is refused because the artifact is a known-good binary — the platform never stores or serves those bytes. It **subclasses `NotFoundException`** precisely so invariant 3 holds for existing consumers: code that already does `except NotFoundException:` keeps catching the refusal with no change, and only callers that want to distinguish "withheld by design" from a plain miss catch the subclass. It adds one attribute, `.sources` — the known-good feeds that flagged the hash (e.g. `['nsrl']`), `[]` when the server named none. The contract is only this: **always a list of strings**, whatever the envelope carried, so `for feed in exc.sources` needs no shape check. Which wire shapes are coerced, and which are dropped and logged, is `exceptions._normalise_sources`' business rather than a promise to consumers — the server sends a list of strings today. The raw envelope stays reachable at `exc.request.errors` (`{'code': 'KNOWN_GOOD', 'known_good': True, 'sources': [...]}`). The artifact's metadata — the flagging feeds plus any scan data already collected — remains readable through the search / instance endpoints; only the bytes are withheld, and the instance's `KNOWN_GOOD` state/status is the signal for that (there is no separate "withheld" field). +`KnownGoodWithheldException` is the 404 raised when a download is refused because the artifact is a known-good binary — the platform never stores or serves those bytes. It **subclasses `NotFoundException`** precisely so invariant 3 holds for existing consumers: code that already does `except NotFoundException:` keeps catching the refusal with no change, and only callers that want to distinguish "withheld by design" from a plain miss catch the subclass. It adds one attribute, `.sources` — the known-good feeds that flagged the hash (e.g. `['nsrl']`), `[]` when the server named none. The contract is only this: **always a list of strings**, whatever the envelope carried, so `for feed in exc.sources` needs no shape check. Which wire shapes are coerced, and which are dropped and logged, is `exceptions._normalise_sources`' business rather than a promise to consumers — the server sends a list of strings today. Note it is **not** normalised the same way as `ArtifactInstance.known_good_sources`, which is the same concept reached from the instance response: that one is sorted and de-duplicated, while `.sources` preserves the order the envelope carried and can repeat a feed. Don't assume parity between the two. The raw envelope stays reachable at `exc.request.errors` (`{'code': 'KNOWN_GOOD', 'known_good': True, 'sources': [...]}`). The artifact's metadata — the flagging feeds plus any scan data already collected — remains readable through the search / instance endpoints; only the bytes are withheld, and the instance's `KNOWN_GOOD` state/status is the signal for that (there is no separate "withheld" field). Each `RequestException` subclass carries a `.request` attribute holding the originating `PolyswarmRequest` (set by `RequestException.__init__`). Callers can read `exc.request.status_code`, `exc.request.json` (the parsed response body after execution), `exc.request.input_json` (the body that was sent), `exc.request.request_parameters` (the request kwargs that built the call), etc. `InvalidValueException` and `TimeoutException` are client-side errors and don't carry a request descriptor. diff --git a/test/async_client_test.py b/test/async_client_test.py index b5bef76d..05a32003 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -422,7 +422,15 @@ async def test_async_known_good_lifecycle(self, uid): # lines are the fleet's only live guard on a frozen status contract. Catalogued via # the CRUD: present plain (the reference instance is a real record), absent under # require_scan (nothing was ever scanned for it). - assert await api.exists(sha, hash_type='sha256') is True + # Polled — see the sync twin: known_good_create goes through the same async + # search-row write, so an unpolled assertion flakes under TESTS_VCR=off. + present = False + for _ in range(30): + present = await api.exists(sha, hash_type='sha256') + if present: + break + await asyncio.sleep(1) + assert present is True assert await api.exists(sha, hash_type='sha256', require_scan=True) is False # A second feed flagging the same sha extends the same entry (no new row). extended = await api.known_good_create(sha256=sha, source='commercial') @@ -466,12 +474,17 @@ async def test_async_hash_existence_probe_against_the_real_server(self, uid): assert submitted_sha == sha assert instance.window_closed + # Poll the NARROWER form — require_scan can only become true at the same time or + # later than the plain form, so polling the broad one and asserting the strict one + # is a race. See the sync twin. + scanned = False for _ in range(30): - if await api.exists(sha, hash_type='sha256'): + scanned = await api.exists(sha, hash_type='sha256', require_scan=True) + if scanned: break await asyncio.sleep(1) + assert scanned is True assert await api.exists(sha, hash_type='sha256') is True - assert await api.exists(sha, hash_type='sha256', require_scan=True) is True # ── Sandbox ─────────────────────────────────────────────────────────────── diff --git a/test/client_scan_test.py b/test/client_scan_test.py index 2117adfd..0cca5ae9 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -799,7 +799,16 @@ def test_known_good_lifecycle(self): # Catalogued via the CRUD, which builds a searchable reference instance: # plain -> present, because that reference IS a real record # require_scan -> absent, because nothing was ever scanned for it - assert v3api.exists(sha, hash_type='sha256') is True + # Polled: known_good_create returns an artifact_instance_id, so it goes through the + # same async search-row write as a submission — asserting it unpolled is a flake under + # TESTS_VCR=off, by the same reasoning as the probe test below. + present = False + for _ in range(30): + present = v3api.exists(sha, hash_type='sha256') + if present: + break + time.sleep(1) + assert present is True assert v3api.exists(sha, hash_type='sha256', require_scan=True) is False # A second feed flagging the same sha extends the same entry (no new row). extended = v3api.known_good_create(sha256=sha, source='commercial') @@ -857,14 +866,19 @@ def test_hash_existence_probe_against_the_real_server(self): assert submitted_sha == sha, 'the probe must be asked about the sha we just submitted' assert instance.window_closed - # The search row is written by an async task, so allow for index lag — but assert the - # value rather than polling until it agrees. + # The search row and its last_scanned state are written by async tasks, so allow for + # index lag — but poll on the NARROWER condition. require_scan filters on the scan + # state of the row that the plain form only needs to exist, so it can only become true + # at the same time or later; polling the broad form and then asserting the narrow one + # is a race. Assert both once the strict one holds. + scanned = False for _ in range(30): - if v3api.exists(sha, hash_type='sha256'): + scanned = v3api.exists(sha, hash_type='sha256', require_scan=True) + if scanned: break time.sleep(1) + assert scanned is True assert v3api.exists(sha, hash_type='sha256') is True - assert v3api.exists(sha, hash_type='sha256', require_scan=True) is True @vcr.use_cassette() def test_sandbox_providers(self): diff --git a/test/conftest.py b/test/conftest.py index 89fba6cf..b3fd2eb9 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -109,6 +109,7 @@ async def _noop_async_sleep(*_a, **_k): "stream", # global archiver batching "rescan", # rescan retry loop + settle "hash_search", # search-index lag + "existence_probe", # submit + settle + search-index lag (the HEAD probe tests) "sandboxtask", # sandbox completion + index lag ) From d51542c11769bd57ce89c106a2e27afc23e52f9c Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 29 Jul 2026 22:48:36 -0300 Subject: [PATCH 14/20] test: re-record the probe cassettes from the committed test body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both were recorded before the body was restructured to poll the strict require_scan form, so they carried the old order (false, false, true) — the exact 'poll the broad form, assert the narrow one' race the comment above them argues against. Replay passed only because VCR matches by request identity, not order, and left the extra interaction unplayed. Recorded fresh against a live stack: 2 plain + 2 require_scan, matching what the body now runs. --- ...xistence_probe_against_the_real_server.vcr | 254 ++++++++------ ...xistence_probe_against_the_real_server.vcr | 314 +++++------------- 2 files changed, 244 insertions(+), 324 deletions(-) diff --git a/test/vcr/test_async_hash_existence_probe_against_the_real_server.vcr b/test/vcr/test_async_hash_existence_probe_against_the_real_server.vcr index 236d9823..65dcea9e 100644 --- a/test/vcr/test_async_hash_existence_probe_against_the_real_server.vcr +++ b/test/vcr/test_async_hash_existence_probe_against_the_real_server.vcr @@ -13,7 +13,7 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: HEAD uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=f38912693d48718b922fc4b78d0f797b5174d030284e1e2b09f4bd1c8caed82e&community=gamma&require_scan=false response: @@ -29,7 +29,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Wed, 29 Jul 2026 23:39:07 GMT + - Thu, 30 Jul 2026 01:18:57 GMT Server: - gunicorn status: @@ -49,7 +49,7 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: HEAD uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=f38912693d48718b922fc4b78d0f797b5174d030284e1e2b09f4bd1c8caed82e&community=gamma&require_scan=true response: @@ -65,7 +65,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Wed, 29 Jul 2026 23:39:07 GMT + - Thu, 30 Jul 2026 01:18:57 GMT Server: - gunicorn status: @@ -89,12 +89,12 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: POST uri: http://artifact-index-e2e:9696/v3/instance response: body: - string: '{"result":{"artifact_id":"83673095931831170","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-07-29T23:39:07.351071+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-07-29T23:39:07.351071+00:00","id":"83673095931831170","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/83673095931831170","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/f9/dd/4a/f9dd4a91-07f9-4609-93ce-29c18cb29d9f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233907Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=8992c62f1c382e6ff360a3c033c660e00bf802d82e612ac7cf49e21f939757d4","votes":[],"window_closed":false},"status":"OK"} + string: '{"result":{"artifact_id":"851018633677295","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-07-30T01:18:57.953044+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-07-30T01:18:57.953044+00:00","id":"851018633677295","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/851018633677295","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/87/88/c9/8788c928-45cf-41d1-8c2c-d2ba36b408bd?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=5d83f94cf24e611cf8f87d74fc2cef53e5d70d01dcbce44871f4ad669ee640c5","votes":[],"window_closed":false},"status":"OK"} ' headers: @@ -105,11 +105,11 @@ interactions: Connection: - keep-alive Content-Length: - - '1044' + - '1038' Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:39:07 GMT + - Thu, 30 Jul 2026 01:18:57 GMT Server: - gunicorn X-Billing-ID: @@ -133,9 +133,9 @@ interactions: host: - minio:9000 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: PUT - uri: http://minio:9000/artifact-index/instances/f9/dd/4a/f9dd4a91-07f9-4609-93ce-29c18cb29d9f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233907Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=8992c62f1c382e6ff360a3c033c660e00bf802d82e612ac7cf49e21f939757d4 + uri: http://minio:9000/artifact-index/instances/87/88/c9/8788c928-45cf-41d1-8c2c-d2ba36b408bd?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=5d83f94cf24e611cf8f87d74fc2cef53e5d70d01dcbce44871f4ad669ee640c5 response: body: string: '' @@ -145,7 +145,7 @@ interactions: Content-Length: - '0' Date: - - Wed, 29 Jul 2026 23:39:07 GMT + - Thu, 30 Jul 2026 01:18:57 GMT ETag: - '"5a99dfcdb13dcd3832de852588a3b09c"' Server: @@ -158,17 +158,17 @@ interactions: X-Amz-Id-2: - dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8 X-Amz-Request-Id: - - 18C6E70D7B148D3C + - 18C6EC804529061E X-Content-Type-Options: - nosniff X-Ratelimit-Limit: - - '13088' + - '13392' X-Ratelimit-Remaining: - - '13088' + - '13392' X-Xss-Protection: - 1; mode=block x-amz-expiration: - - expiry-date="Fri, 31 Jul 2026 00:00:00 GMT", rule-id="expiration-artifact-index_0-instances" + - expiry-date="Sat, 01 Aug 2026 00:00:00 GMT", rule-id="expiration-artifact-index_0-instances" status: code: 200 message: OK @@ -190,12 +190,12 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: PUT - uri: http://artifact-index-e2e:9696/v3/instance?id=83673095931831170 + uri: http://artifact-index-e2e:9696/v3/instance?id=851018633677295 response: body: - string: '{"result":{"artifact_id":"83673095931831170","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-07-29T23:39:07.351071+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-07-29T23:39:07.351071+00:00","id":"83673095931831170","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/83673095931831170","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/f9/dd/4a/f9dd4a91-07f9-4609-93ce-29c18cb29d9f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233907Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=8992c62f1c382e6ff360a3c033c660e00bf802d82e612ac7cf49e21f939757d4","votes":[],"window_closed":false},"status":"OK"} + string: '{"result":{"artifact_id":"851018633677295","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-07-30T01:18:57.953044+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-07-30T01:18:57.953044+00:00","id":"851018633677295","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/851018633677295","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/87/88/c9/8788c928-45cf-41d1-8c2c-d2ba36b408bd?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=5d83f94cf24e611cf8f87d74fc2cef53e5d70d01dcbce44871f4ad669ee640c5","votes":[],"window_closed":false},"status":"OK"} ' headers: @@ -206,11 +206,11 @@ interactions: Connection: - keep-alive Content-Length: - - '1044' + - '1038' Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:39:07 GMT + - Thu, 30 Jul 2026 01:18:57 GMT Server: - gunicorn X-Billing-ID: @@ -232,12 +232,12 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/83673095931831170 + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/851018633677295 response: body: - string: '{"result":{"artifact_id":"83673095931831170","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-07-29T23:39:07.351071+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-07-29T23:39:07.351071+00:00","id":"83673095931831170","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/83673095931831170","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/f9/dd/4a/f9dd4a91-07f9-4609-93ce-29c18cb29d9f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233907Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=8992c62f1c382e6ff360a3c033c660e00bf802d82e612ac7cf49e21f939757d4","votes":[],"window_closed":false},"status":"OK"} + string: '{"result":{"artifact_id":"851018633677295","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-07-30T01:18:57.953044+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-07-30T01:18:57.953044+00:00","id":"851018633677295","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/851018633677295","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/87/88/c9/8788c928-45cf-41d1-8c2c-d2ba36b408bd?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=5d83f94cf24e611cf8f87d74fc2cef53e5d70d01dcbce44871f4ad669ee640c5","votes":[],"window_closed":false},"status":"OK"} ' headers: @@ -248,11 +248,11 @@ interactions: Connection: - keep-alive Content-Length: - - '1044' + - '1038' Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:39:07 GMT + - Thu, 30 Jul 2026 01:18:57 GMT Server: - gunicorn X-Billing-ID: @@ -274,17 +274,17 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/83673095931831170 + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/851018633677295 response: body: - string: '{"result":{"artifact_id":"83673095931831170","assertions":[{"author":"299533097263972","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-07-29T23:39:07.351071+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR - virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:39:07.351071+00:00","id":"83673095931831170","known_good":null,"last_scanned":null,"last_seen":null,"md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-29T23:39:07.567288+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 - 23:39:07+00:00","fileinodechangedate":"2026:07:29 23:39:07+00:00","filemodifydate":"2026:07:29 - 23:39:07+00:00","filename":"tmp__bt2dla","filepermissions":"-rw-r--r--","filesize":"124 + string: '{"result":{"artifact_id":"851018633677295","assertions":[{"author":"86494354581135","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-07-30T01:18:57.953044+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-30T01:18:57.953044+00:00","id":"851018633677295","known_good":null,"last_scanned":null,"last_seen":null,"md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-30T01:18:58.072962+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:30 + 01:18:58+00:00","fileinodechangedate":"2026:07:30 01:18:58+00:00","filemodifydate":"2026:07:30 + 01:18:58+00:00","filename":"tmpps66hhd4","filepermissions":"-rw-r--r--","filesize":"124 bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix - LF","sourcefile":"/tmp/tmp__bt2dla","wordcount":2},"updated":"2026-07-29T23:39:07.567288+00:00"},{"created":"2026-07-29T23:39:07.502260+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-29T23:39:07.502260+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/83673095931831170","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/f9/dd/4a/f9dd4a91-07f9-4609-93ce-29c18cb29d9f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233907Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=8992c62f1c382e6ff360a3c033c660e00bf802d82e612ac7cf49e21f939757d4","votes":[],"window_closed":false},"status":"OK"} + LF","sourcefile":"/tmp/tmpps66hhd4","wordcount":2},"updated":"2026-07-30T01:18:58.072962+00:00"},{"created":"2026-07-30T01:18:58.023871+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-30T01:18:58.023871+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/851018633677295","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/87/88/c9/8788c928-45cf-41d1-8c2c-d2ba36b408bd?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=5d83f94cf24e611cf8f87d74fc2cef53e5d70d01dcbce44871f4ad669ee640c5","votes":[],"window_closed":false},"status":"OK"} ' headers: @@ -295,11 +295,11 @@ interactions: Connection: - keep-alive Content-Length: - - '2949' + - '2942' Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:39:08 GMT + - Thu, 30 Jul 2026 01:18:59 GMT Server: - gunicorn X-Billing-ID: @@ -321,17 +321,17 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/83673095931831170 + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/851018633677295 response: body: - string: '{"result":{"artifact_id":"83673095931831170","assertions":[{"author":"299533097263972","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-07-29T23:39:07.351071+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR - virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:39:07.351071+00:00","id":"83673095931831170","known_good":null,"last_scanned":null,"last_seen":null,"md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-29T23:39:07.567288+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 - 23:39:07+00:00","fileinodechangedate":"2026:07:29 23:39:07+00:00","filemodifydate":"2026:07:29 - 23:39:07+00:00","filename":"tmp__bt2dla","filepermissions":"-rw-r--r--","filesize":"124 + string: '{"result":{"artifact_id":"851018633677295","assertions":[{"author":"86494354581135","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-07-30T01:18:57.953044+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-30T01:18:57.953044+00:00","id":"851018633677295","known_good":null,"last_scanned":null,"last_seen":null,"md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-30T01:18:58.072962+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:30 + 01:18:58+00:00","fileinodechangedate":"2026:07:30 01:18:58+00:00","filemodifydate":"2026:07:30 + 01:18:58+00:00","filename":"tmpps66hhd4","filepermissions":"-rw-r--r--","filesize":"124 bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix - LF","sourcefile":"/tmp/tmp__bt2dla","wordcount":2},"updated":"2026-07-29T23:39:07.567288+00:00"},{"created":"2026-07-29T23:39:07.502260+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-29T23:39:07.502260+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/83673095931831170","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/f9/dd/4a/f9dd4a91-07f9-4609-93ce-29c18cb29d9f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233907Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=8992c62f1c382e6ff360a3c033c660e00bf802d82e612ac7cf49e21f939757d4","votes":[],"window_closed":false},"status":"OK"} + LF","sourcefile":"/tmp/tmpps66hhd4","wordcount":2},"updated":"2026-07-30T01:18:58.072962+00:00"},{"created":"2026-07-30T01:18:58.023871+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-30T01:18:58.023871+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/851018633677295","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/87/88/c9/8788c928-45cf-41d1-8c2c-d2ba36b408bd?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=5d83f94cf24e611cf8f87d74fc2cef53e5d70d01dcbce44871f4ad669ee640c5","votes":[],"window_closed":false},"status":"OK"} ' headers: @@ -342,11 +342,11 @@ interactions: Connection: - keep-alive Content-Length: - - '2949' + - '2942' Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:39:09 GMT + - Thu, 30 Jul 2026 01:19:00 GMT Server: - gunicorn X-Billing-ID: @@ -368,17 +368,17 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/83673095931831170 + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/851018633677295 response: body: - string: '{"result":{"artifact_id":"83673095931831170","assertions":[{"author":"299533097263972","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-07-29T23:39:07.351071+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR - virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:39:07.351071+00:00","id":"83673095931831170","known_good":null,"last_scanned":null,"last_seen":null,"md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-29T23:39:07.567288+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 - 23:39:07+00:00","fileinodechangedate":"2026:07:29 23:39:07+00:00","filemodifydate":"2026:07:29 - 23:39:07+00:00","filename":"tmp__bt2dla","filepermissions":"-rw-r--r--","filesize":"124 + string: '{"result":{"artifact_id":"851018633677295","assertions":[{"author":"86494354581135","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-07-30T01:18:57.953044+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-30T01:18:57.953044+00:00","id":"851018633677295","known_good":null,"last_scanned":null,"last_seen":null,"md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-30T01:18:58.072962+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:30 + 01:18:58+00:00","fileinodechangedate":"2026:07:30 01:18:58+00:00","filemodifydate":"2026:07:30 + 01:18:58+00:00","filename":"tmpps66hhd4","filepermissions":"-rw-r--r--","filesize":"124 bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix - LF","sourcefile":"/tmp/tmp__bt2dla","wordcount":2},"updated":"2026-07-29T23:39:07.567288+00:00"},{"created":"2026-07-29T23:39:07.502260+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-29T23:39:07.502260+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/83673095931831170","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/f9/dd/4a/f9dd4a91-07f9-4609-93ce-29c18cb29d9f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233907Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=8992c62f1c382e6ff360a3c033c660e00bf802d82e612ac7cf49e21f939757d4","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} + LF","sourcefile":"/tmp/tmpps66hhd4","wordcount":2},"updated":"2026-07-30T01:18:58.072962+00:00"},{"created":"2026-07-30T01:18:58.023871+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-30T01:18:58.023871+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/851018633677295","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/87/88/c9/8788c928-45cf-41d1-8c2c-d2ba36b408bd?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=5d83f94cf24e611cf8f87d74fc2cef53e5d70d01dcbce44871f4ad669ee640c5","votes":[{"arbiter":"450254598420922","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} ' headers: @@ -389,11 +389,11 @@ interactions: Connection: - keep-alive Content-Length: - - '3234' + - '3227' Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:39:10 GMT + - Thu, 30 Jul 2026 01:19:01 GMT Server: - gunicorn X-Billing-ID: @@ -415,17 +415,17 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/83673095931831170 + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/851018633677295 response: body: - string: '{"result":{"artifact_id":"83673095931831170","assertions":[{"author":"299533097263972","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-07-29T23:39:07.351071+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR - virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:39:07.351071+00:00","id":"83673095931831170","known_good":null,"last_scanned":null,"last_seen":null,"md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-29T23:39:07.567288+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 - 23:39:07+00:00","fileinodechangedate":"2026:07:29 23:39:07+00:00","filemodifydate":"2026:07:29 - 23:39:07+00:00","filename":"tmp__bt2dla","filepermissions":"-rw-r--r--","filesize":"124 + string: '{"result":{"artifact_id":"851018633677295","assertions":[{"author":"86494354581135","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-07-30T01:18:57.953044+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-30T01:18:57.953044+00:00","id":"851018633677295","known_good":null,"last_scanned":null,"last_seen":null,"md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-30T01:18:58.072962+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:30 + 01:18:58+00:00","fileinodechangedate":"2026:07:30 01:18:58+00:00","filemodifydate":"2026:07:30 + 01:18:58+00:00","filename":"tmpps66hhd4","filepermissions":"-rw-r--r--","filesize":"124 bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix - LF","sourcefile":"/tmp/tmp__bt2dla","wordcount":2},"updated":"2026-07-29T23:39:07.567288+00:00"},{"created":"2026-07-29T23:39:07.502260+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-29T23:39:07.502260+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/83673095931831170","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/f9/dd/4a/f9dd4a91-07f9-4609-93ce-29c18cb29d9f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233907Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=8992c62f1c382e6ff360a3c033c660e00bf802d82e612ac7cf49e21f939757d4","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} + LF","sourcefile":"/tmp/tmpps66hhd4","wordcount":2},"updated":"2026-07-30T01:18:58.072962+00:00"},{"created":"2026-07-30T01:18:58.023871+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-30T01:18:58.023871+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/851018633677295","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/87/88/c9/8788c928-45cf-41d1-8c2c-d2ba36b408bd?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=5d83f94cf24e611cf8f87d74fc2cef53e5d70d01dcbce44871f4ad669ee640c5","votes":[{"arbiter":"450254598420922","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} ' headers: @@ -436,11 +436,11 @@ interactions: Connection: - keep-alive Content-Length: - - '3234' + - '3227' Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:39:11 GMT + - Thu, 30 Jul 2026 01:19:02 GMT Server: - gunicorn X-Billing-ID: @@ -462,17 +462,17 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/83673095931831170 + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/851018633677295 response: body: - string: '{"result":{"artifact_id":"83673095931831170","assertions":[{"author":"299533097263972","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-07-29T23:39:07.351071+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR - virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:39:07.351071+00:00","id":"83673095931831170","known_good":null,"last_scanned":null,"last_seen":null,"md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-29T23:39:07.567288+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 - 23:39:07+00:00","fileinodechangedate":"2026:07:29 23:39:07+00:00","filemodifydate":"2026:07:29 - 23:39:07+00:00","filename":"tmp__bt2dla","filepermissions":"-rw-r--r--","filesize":"124 + string: '{"result":{"artifact_id":"851018633677295","assertions":[{"author":"86494354581135","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-07-30T01:18:57.953044+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-30T01:18:57.953044+00:00","id":"851018633677295","known_good":null,"last_scanned":null,"last_seen":null,"md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-30T01:18:58.072962+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:30 + 01:18:58+00:00","fileinodechangedate":"2026:07:30 01:18:58+00:00","filemodifydate":"2026:07:30 + 01:18:58+00:00","filename":"tmpps66hhd4","filepermissions":"-rw-r--r--","filesize":"124 bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix - LF","sourcefile":"/tmp/tmp__bt2dla","wordcount":2},"updated":"2026-07-29T23:39:07.567288+00:00"},{"created":"2026-07-29T23:39:07.502260+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-29T23:39:07.502260+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/83673095931831170","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/f9/dd/4a/f9dd4a91-07f9-4609-93ce-29c18cb29d9f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233907Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=8992c62f1c382e6ff360a3c033c660e00bf802d82e612ac7cf49e21f939757d4","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} + LF","sourcefile":"/tmp/tmpps66hhd4","wordcount":2},"updated":"2026-07-30T01:18:58.072962+00:00"},{"created":"2026-07-30T01:18:58.023871+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-30T01:18:58.023871+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/851018633677295","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/87/88/c9/8788c928-45cf-41d1-8c2c-d2ba36b408bd?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=5d83f94cf24e611cf8f87d74fc2cef53e5d70d01dcbce44871f4ad669ee640c5","votes":[{"arbiter":"450254598420922","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} ' headers: @@ -483,11 +483,11 @@ interactions: Connection: - keep-alive Content-Length: - - '3234' + - '3227' Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:39:12 GMT + - Thu, 30 Jul 2026 01:19:03 GMT Server: - gunicorn X-Billing-ID: @@ -509,17 +509,17 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/83673095931831170 + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/851018633677295 response: body: - string: '{"result":{"artifact_id":"83673095931831170","assertions":[{"author":"299533097263972","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-07-29T23:39:07.351071+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR - virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:39:07.351071+00:00","id":"83673095931831170","known_good":null,"last_scanned":null,"last_seen":null,"md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-29T23:39:07.567288+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 - 23:39:07+00:00","fileinodechangedate":"2026:07:29 23:39:07+00:00","filemodifydate":"2026:07:29 - 23:39:07+00:00","filename":"tmp__bt2dla","filepermissions":"-rw-r--r--","filesize":"124 + string: '{"result":{"artifact_id":"851018633677295","assertions":[{"author":"86494354581135","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-07-30T01:18:57.953044+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-30T01:18:57.953044+00:00","id":"851018633677295","known_good":null,"last_scanned":null,"last_seen":null,"md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-30T01:18:58.072962+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:30 + 01:18:58+00:00","fileinodechangedate":"2026:07:30 01:18:58+00:00","filemodifydate":"2026:07:30 + 01:18:58+00:00","filename":"tmpps66hhd4","filepermissions":"-rw-r--r--","filesize":"124 bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix - LF","sourcefile":"/tmp/tmp__bt2dla","wordcount":2},"updated":"2026-07-29T23:39:07.567288+00:00"},{"created":"2026-07-29T23:39:07.502260+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-29T23:39:07.502260+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/83673095931831170","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/f9/dd/4a/f9dd4a91-07f9-4609-93ce-29c18cb29d9f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233907Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=8992c62f1c382e6ff360a3c033c660e00bf802d82e612ac7cf49e21f939757d4","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} + LF","sourcefile":"/tmp/tmpps66hhd4","wordcount":2},"updated":"2026-07-30T01:18:58.072962+00:00"},{"created":"2026-07-30T01:18:58.023871+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-30T01:18:58.023871+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/851018633677295","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/87/88/c9/8788c928-45cf-41d1-8c2c-d2ba36b408bd?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=5d83f94cf24e611cf8f87d74fc2cef53e5d70d01dcbce44871f4ad669ee640c5","votes":[{"arbiter":"450254598420922","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} ' headers: @@ -530,11 +530,11 @@ interactions: Connection: - keep-alive Content-Length: - - '3234' + - '3227' Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:39:13 GMT + - Thu, 30 Jul 2026 01:19:04 GMT Server: - gunicorn X-Billing-ID: @@ -556,17 +556,17 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/83673095931831170 + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/851018633677295 response: body: - string: '{"result":{"artifact_id":"83673095931831170","assertions":[{"author":"299533097263972","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-07-29T23:39:07.351071+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-07-29T23:39:07.351071+00:00","id":"83673095931831170","known_good":null,"last_scanned":"2026-07-29T23:39:07.351071+00:00","last_seen":"2026-07-29T23:39:07.351071+00:00","md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-29T23:39:14.495357+00:00","tool":"polyunite","tool_metadata":{"labels":["nonmalware"],"malware_family":"EICAR","operating_system":[]},"updated":"2026-07-29T23:39:14.495357+00:00"},{"created":"2026-07-29T23:39:07.567288+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 - 23:39:07+00:00","fileinodechangedate":"2026:07:29 23:39:07+00:00","filemodifydate":"2026:07:29 - 23:39:07+00:00","filename":"tmp__bt2dla","filepermissions":"-rw-r--r--","filesize":"124 + string: '{"result":{"artifact_id":"851018633677295","assertions":[{"author":"86494354581135","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-07-30T01:18:57.953044+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-07-30T01:18:57.953044+00:00","id":"851018633677295","known_good":null,"last_scanned":null,"last_seen":null,"md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-30T01:18:58.072962+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:30 + 01:18:58+00:00","fileinodechangedate":"2026:07:30 01:18:58+00:00","filemodifydate":"2026:07:30 + 01:18:58+00:00","filename":"tmpps66hhd4","filepermissions":"-rw-r--r--","filesize":"124 bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix - LF","sourcefile":"/tmp/tmp__bt2dla","wordcount":2},"updated":"2026-07-29T23:39:07.567288+00:00"},{"created":"2026-07-29T23:39:07.502260+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-29T23:39:07.502260+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/83673095931831170","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"AWAITING_ARBITRATION","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/f9/dd/4a/f9dd4a91-07f9-4609-93ce-29c18cb29d9f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233907Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=8992c62f1c382e6ff360a3c033c660e00bf802d82e612ac7cf49e21f939757d4","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":true},"status":"OK"} + LF","sourcefile":"/tmp/tmpps66hhd4","wordcount":2},"updated":"2026-07-30T01:18:58.072962+00:00"},{"created":"2026-07-30T01:18:58.023871+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-30T01:18:58.023871+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/851018633677295","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"AWAITING_ARBITRATION","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/87/88/c9/8788c928-45cf-41d1-8c2c-d2ba36b408bd?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=5d83f94cf24e611cf8f87d74fc2cef53e5d70d01dcbce44871f4ad669ee640c5","votes":[{"arbiter":"450254598420922","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} ' headers: @@ -577,11 +577,11 @@ interactions: Connection: - keep-alive Content-Length: - - '3536' + - '3270' Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:39:14 GMT + - Thu, 30 Jul 2026 01:19:05 GMT Server: - gunicorn X-Billing-ID: @@ -603,12 +603,19 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) - method: HEAD - uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502&community=gamma&require_scan=false + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/851018633677295 response: body: - string: '' + string: '{"result":{"artifact_id":"851018633677295","assertions":[{"author":"86494354581135","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-07-30T01:18:57.953044+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-07-30T01:18:57.953044+00:00","id":"851018633677295","known_good":null,"last_scanned":null,"last_seen":null,"md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-30T01:18:58.072962+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:30 + 01:18:58+00:00","fileinodechangedate":"2026:07:30 01:18:58+00:00","filemodifydate":"2026:07:30 + 01:18:58+00:00","filename":"tmpps66hhd4","filepermissions":"-rw-r--r--","filesize":"124 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmpps66hhd4","wordcount":2},"updated":"2026-07-30T01:18:58.072962+00:00"},{"created":"2026-07-30T01:18:58.023871+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-30T01:18:58.023871+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/851018633677295","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"AWAITING_ARBITRATION","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/87/88/c9/8788c928-45cf-41d1-8c2c-d2ba36b408bd?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=5d83f94cf24e611cf8f87d74fc2cef53e5d70d01dcbce44871f4ad669ee640c5","votes":[{"arbiter":"450254598420922","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} + + ' headers: Access-Control-Allow-Origin: - '*' @@ -617,11 +624,58 @@ interactions: Connection: - keep-alive Content-Length: - - '57' + - '3270' + Content-Type: + - application/json + Date: + - Thu, 30 Jul 2026 01:19: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: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/851018633677295 + response: + body: + string: '{"result":{"artifact_id":"851018633677295","assertions":[{"author":"86494354581135","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-07-30T01:18:57.953044+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-07-30T01:18:57.953044+00:00","id":"851018633677295","known_good":null,"last_scanned":"2026-07-30T01:18:57.953044+00:00","last_seen":"2026-07-30T01:18:57.953044+00:00","md5":"5a99dfcdb13dcd3832de852588a3b09c","metadata":[{"created":"2026-07-30T01:19:06.170605+00:00","tool":"polyunite","tool_metadata":{"labels":["nonmalware"],"malware_family":"EICAR","operating_system":[]},"updated":"2026-07-30T01:19:06.170605+00:00"},{"created":"2026-07-30T01:18:58.072962+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:30 + 01:18:58+00:00","fileinodechangedate":"2026:07:30 01:18:58+00:00","filemodifydate":"2026:07:30 + 01:18:58+00:00","filename":"tmpps66hhd4","filepermissions":"-rw-r--r--","filesize":"124 + bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix + LF","sourcefile":"/tmp/tmpps66hhd4","wordcount":2},"updated":"2026-07-30T01:18:58.072962+00:00"},{"created":"2026-07-30T01:18:58.023871+00:00","tool":"hash","tool_metadata":{"md5":"5a99dfcdb13dcd3832de852588a3b09c","sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","sha3_256":"7921e9c74476f6479123b504415bec4b7897bae1297be9b6da23920fe2e0d004","sha3_512":"3afe02e30efec2a44180511192a47e2e7d822e4d0328751da46cf487f1de9dbea48e5be623c75b5193f4f0d128fe5d9d328192c10786ae0b552dbf472cee1d9c","sha512":"0222d80f03f9acf5495910755ac63ff22b5c390e0efead00187908e509b00d184a561b24381001e3b12f2cb9110ba6129a93cc030fdd7e7dbb01c7c09901c2fb","ssdeep":"3:a+JraNvsgzsVqSwHqDY4/N6APVasq9jQ/Gn:tJuOgzskKoAosG","tlsh":"5ab09200262eef1b9656501831baba661908826a5cd8063993a661b5a8a31540a99a68"},"updated":"2026-07-30T01:18:58.023871+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502/851018633677295","polyscore":null,"result":null,"sha1":"c848aafab99053318b6ddc22d3d5eca98abce02c","sha256":"7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502","size":124,"state":"AWAITING_ARBITRATION","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/87/88/c9/8788c928-45cf-41d1-8c2c-d2ba36b408bd?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=5d83f94cf24e611cf8f87d74fc2cef53e5d70d01dcbce44871f4ad669ee640c5","votes":[{"arbiter":"450254598420922","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":true},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3529' Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:39:14 GMT + - Thu, 30 Jul 2026 01:19:07 GMT Server: - gunicorn X-Billing-ID: @@ -643,9 +697,9 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: HEAD - uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502&community=gamma&require_scan=false + uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502&community=gamma&require_scan=true response: body: string: '' @@ -661,7 +715,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:39:14 GMT + - Thu, 30 Jul 2026 01:19:07 GMT Server: - gunicorn X-Billing-ID: @@ -683,9 +737,9 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: HEAD - uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502&community=gamma&require_scan=true + uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=7adb535fe74b357800da978ff12973349110d895928e56b5b0a24049f7894502&community=gamma&require_scan=false response: body: string: '' @@ -701,7 +755,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:39:14 GMT + - Thu, 30 Jul 2026 01:19:07 GMT Server: - gunicorn X-Billing-ID: diff --git a/test/vcr/test_hash_existence_probe_against_the_real_server.vcr b/test/vcr/test_hash_existence_probe_against_the_real_server.vcr index fc3aa82b..e67fba98 100644 --- a/test/vcr/test_hash_existence_probe_against_the_real_server.vcr +++ b/test/vcr/test_hash_existence_probe_against_the_real_server.vcr @@ -13,7 +13,7 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: HEAD uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=8114c0209d6e20ad5a8cbec01782f065be5cf9804d78801a90583e04551c05a8&community=gamma&require_scan=false response: @@ -29,7 +29,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Wed, 29 Jul 2026 23:38:57 GMT + - Thu, 30 Jul 2026 01:18:50 GMT Server: - gunicorn status: @@ -49,7 +49,7 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: HEAD uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=8114c0209d6e20ad5a8cbec01782f065be5cf9804d78801a90583e04551c05a8&community=gamma&require_scan=true response: @@ -65,7 +65,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Wed, 29 Jul 2026 23:38:57 GMT + - Thu, 30 Jul 2026 01:18:50 GMT Server: - gunicorn status: @@ -89,12 +89,12 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: POST uri: http://artifact-index-e2e:9696/v3/instance response: body: - string: '{"result":{"artifact_id":"51315590942038050","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-07-29T23:38:57.583670+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/51315590942038050","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","votes":[],"window_closed":false},"status":"OK"} + string: '{"result":{"artifact_id":"88492900747453910","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-07-30T01:18:50.605483+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-07-30T01:18:50.605483+00:00","id":"88492900747453910","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/88492900747453910","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/68/f7/8e/68f78e92-72a3-47ab-a126-55a836c38bf6?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011850Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=2fb4d42512b5103856af4c1f5f8e51d2ce3ca5557f30aa8e9125a55fa310eccd","votes":[],"window_closed":false},"status":"OK"} ' headers: @@ -109,7 +109,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:38:57 GMT + - Thu, 30 Jul 2026 01:18:50 GMT Server: - gunicorn X-Billing-ID: @@ -133,9 +133,9 @@ interactions: host: - minio:9000 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: PUT - uri: http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b + uri: http://minio:9000/artifact-index/instances/68/f7/8e/68f78e92-72a3-47ab-a126-55a836c38bf6?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011850Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=2fb4d42512b5103856af4c1f5f8e51d2ce3ca5557f30aa8e9125a55fa310eccd response: body: string: '' @@ -145,7 +145,7 @@ interactions: Content-Length: - '0' Date: - - Wed, 29 Jul 2026 23:38:57 GMT + - Thu, 30 Jul 2026 01:18:50 GMT ETag: - '"f9ae88ea41d60b331ac29746fec938bd"' Server: @@ -158,17 +158,17 @@ interactions: X-Amz-Id-2: - dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8 X-Amz-Request-Id: - - 18C6E70B34E8D9D1 + - 18C6EC7E8F49E06B X-Content-Type-Options: - nosniff X-Ratelimit-Limit: - - '13088' + - '13392' X-Ratelimit-Remaining: - - '13088' + - '13392' X-Xss-Protection: - 1; mode=block x-amz-expiration: - - expiry-date="Fri, 31 Jul 2026 00:00:00 GMT", rule-id="expiration-artifact-index_0-instances" + - expiry-date="Sat, 01 Aug 2026 00:00:00 GMT", rule-id="expiration-artifact-index_0-instances" status: code: 200 message: OK @@ -190,12 +190,12 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: PUT - uri: http://artifact-index-e2e:9696/v3/instance?id=51315590942038050 + uri: http://artifact-index-e2e:9696/v3/instance?id=88492900747453910 response: body: - string: '{"result":{"artifact_id":"51315590942038050","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-07-29T23:38:57.583670+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/51315590942038050","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","votes":[],"window_closed":false},"status":"OK"} + string: '{"result":{"artifact_id":"88492900747453910","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-07-30T01:18:50.605483+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-07-30T01:18:50.605483+00:00","id":"88492900747453910","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/88492900747453910","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/68/f7/8e/68f78e92-72a3-47ab-a126-55a836c38bf6?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011850Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=2fb4d42512b5103856af4c1f5f8e51d2ce3ca5557f30aa8e9125a55fa310eccd","votes":[],"window_closed":false},"status":"OK"} ' headers: @@ -210,7 +210,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:38:57 GMT + - Thu, 30 Jul 2026 01:18:50 GMT Server: - gunicorn X-Billing-ID: @@ -232,12 +232,12 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/51315590942038050 + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/88492900747453910 response: body: - string: '{"result":{"artifact_id":"51315590942038050","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-07-29T23:38:57.583670+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/51315590942038050","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","votes":[],"window_closed":false},"status":"OK"} + string: '{"result":{"artifact_id":"88492900747453910","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-07-30T01:18:50.605483+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-07-30T01:18:50.605483+00:00","id":"88492900747453910","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/88492900747453910","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/68/f7/8e/68f78e92-72a3-47ab-a126-55a836c38bf6?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011850Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=2fb4d42512b5103856af4c1f5f8e51d2ce3ca5557f30aa8e9125a55fa310eccd","votes":[],"window_closed":false},"status":"OK"} ' headers: @@ -252,7 +252,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:38:57 GMT + - Thu, 30 Jul 2026 01:18:50 GMT Server: - gunicorn X-Billing-ID: @@ -274,17 +274,17 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/51315590942038050 + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/88492900747453910 response: body: - string: '{"result":{"artifact_id":"51315590942038050","assertions":[{"author":"299533097263972","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-07-29T23:38:57.583670+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR - virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":null,"last_seen":null,"md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-29T23:38:58.231065+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 - 23:38:58+00:00","fileinodechangedate":"2026:07:29 23:38:58+00:00","filemodifydate":"2026:07:29 - 23:38:58+00:00","filename":"tmp_7t30si7","filepermissions":"-rw-r--r--","filesize":"118 + string: '{"result":{"artifact_id":"88492900747453910","assertions":[{"author":"86494354581135","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-07-30T01:18:50.605483+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-30T01:18:50.605483+00:00","id":"88492900747453910","known_good":null,"last_scanned":null,"last_seen":null,"md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-30T01:18:50.886699+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:30 + 01:18:50+00:00","fileinodechangedate":"2026:07:30 01:18:50+00:00","filemodifydate":"2026:07:30 + 01:18:50+00:00","filename":"tmpzxu0f5he","filepermissions":"-rw-r--r--","filesize":"118 bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix - LF","sourcefile":"/tmp/tmp_7t30si7","wordcount":2},"updated":"2026-07-29T23:38:58.231065+00:00"},{"created":"2026-07-29T23:38:58.163477+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-29T23:38:58.163477+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/51315590942038050","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","votes":[],"window_closed":false},"status":"OK"} + LF","sourcefile":"/tmp/tmpzxu0f5he","wordcount":2},"updated":"2026-07-30T01:18:50.886699+00:00"},{"created":"2026-07-30T01:18:50.840842+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-30T01:18:50.840842+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/88492900747453910","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/68/f7/8e/68f78e92-72a3-47ab-a126-55a836c38bf6?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011850Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=2fb4d42512b5103856af4c1f5f8e51d2ce3ca5557f30aa8e9125a55fa310eccd","votes":[],"window_closed":false},"status":"OK"} ' headers: @@ -295,11 +295,11 @@ interactions: Connection: - keep-alive Content-Length: - - '2945' + - '2944' Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:38:58 GMT + - Thu, 30 Jul 2026 01:18:51 GMT Server: - gunicorn X-Billing-ID: @@ -321,17 +321,17 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/51315590942038050 + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/88492900747453910 response: body: - string: '{"result":{"artifact_id":"51315590942038050","assertions":[{"author":"299533097263972","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-07-29T23:38:57.583670+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR - virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":null,"last_seen":null,"md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-29T23:38:58.231065+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 - 23:38:58+00:00","fileinodechangedate":"2026:07:29 23:38:58+00:00","filemodifydate":"2026:07:29 - 23:38:58+00:00","filename":"tmp_7t30si7","filepermissions":"-rw-r--r--","filesize":"118 + string: '{"result":{"artifact_id":"88492900747453910","assertions":[{"author":"86494354581135","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-07-30T01:18:50.605483+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-30T01:18:50.605483+00:00","id":"88492900747453910","known_good":null,"last_scanned":null,"last_seen":null,"md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-30T01:18:50.886699+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:30 + 01:18:50+00:00","fileinodechangedate":"2026:07:30 01:18:50+00:00","filemodifydate":"2026:07:30 + 01:18:50+00:00","filename":"tmpzxu0f5he","filepermissions":"-rw-r--r--","filesize":"118 bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix - LF","sourcefile":"/tmp/tmp_7t30si7","wordcount":2},"updated":"2026-07-29T23:38:58.231065+00:00"},{"created":"2026-07-29T23:38:58.163477+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-29T23:38:58.163477+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/51315590942038050","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","votes":[],"window_closed":false},"status":"OK"} + LF","sourcefile":"/tmp/tmpzxu0f5he","wordcount":2},"updated":"2026-07-30T01:18:50.886699+00:00"},{"created":"2026-07-30T01:18:50.840842+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-30T01:18:50.840842+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/88492900747453910","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/68/f7/8e/68f78e92-72a3-47ab-a126-55a836c38bf6?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011850Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=2fb4d42512b5103856af4c1f5f8e51d2ce3ca5557f30aa8e9125a55fa310eccd","votes":[],"window_closed":false},"status":"OK"} ' headers: @@ -342,11 +342,11 @@ interactions: Connection: - keep-alive Content-Length: - - '2945' + - '2944' Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:38:59 GMT + - Thu, 30 Jul 2026 01:18:52 GMT Server: - gunicorn X-Billing-ID: @@ -368,17 +368,17 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/51315590942038050 + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/88492900747453910 response: body: - string: '{"result":{"artifact_id":"51315590942038050","assertions":[{"author":"299533097263972","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-07-29T23:38:57.583670+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR - virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":null,"last_seen":null,"md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-29T23:38:58.231065+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 - 23:38:58+00:00","fileinodechangedate":"2026:07:29 23:38:58+00:00","filemodifydate":"2026:07:29 - 23:38:58+00:00","filename":"tmp_7t30si7","filepermissions":"-rw-r--r--","filesize":"118 + string: '{"result":{"artifact_id":"88492900747453910","assertions":[{"author":"86494354581135","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-07-30T01:18:50.605483+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-30T01:18:50.605483+00:00","id":"88492900747453910","known_good":null,"last_scanned":null,"last_seen":null,"md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-30T01:18:50.886699+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:30 + 01:18:50+00:00","fileinodechangedate":"2026:07:30 01:18:50+00:00","filemodifydate":"2026:07:30 + 01:18:50+00:00","filename":"tmpzxu0f5he","filepermissions":"-rw-r--r--","filesize":"118 bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix - LF","sourcefile":"/tmp/tmp_7t30si7","wordcount":2},"updated":"2026-07-29T23:38:58.231065+00:00"},{"created":"2026-07-29T23:38:58.163477+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-29T23:38:58.163477+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/51315590942038050","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} + LF","sourcefile":"/tmp/tmpzxu0f5he","wordcount":2},"updated":"2026-07-30T01:18:50.886699+00:00"},{"created":"2026-07-30T01:18:50.840842+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-30T01:18:50.840842+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/88492900747453910","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/68/f7/8e/68f78e92-72a3-47ab-a126-55a836c38bf6?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011850Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=2fb4d42512b5103856af4c1f5f8e51d2ce3ca5557f30aa8e9125a55fa310eccd","votes":[{"arbiter":"450254598420922","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} ' headers: @@ -389,11 +389,11 @@ interactions: Connection: - keep-alive Content-Length: - - '3230' + - '3229' Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:39:00 GMT + - Thu, 30 Jul 2026 01:18:53 GMT Server: - gunicorn X-Billing-ID: @@ -415,17 +415,17 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/51315590942038050 + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/88492900747453910 response: body: - string: '{"result":{"artifact_id":"51315590942038050","assertions":[{"author":"299533097263972","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-07-29T23:38:57.583670+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR - virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":null,"last_seen":null,"md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-29T23:38:58.231065+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 - 23:38:58+00:00","fileinodechangedate":"2026:07:29 23:38:58+00:00","filemodifydate":"2026:07:29 - 23:38:58+00:00","filename":"tmp_7t30si7","filepermissions":"-rw-r--r--","filesize":"118 + string: '{"result":{"artifact_id":"88492900747453910","assertions":[{"author":"86494354581135","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-07-30T01:18:50.605483+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-30T01:18:50.605483+00:00","id":"88492900747453910","known_good":null,"last_scanned":null,"last_seen":null,"md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-30T01:18:50.886699+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:30 + 01:18:50+00:00","fileinodechangedate":"2026:07:30 01:18:50+00:00","filemodifydate":"2026:07:30 + 01:18:50+00:00","filename":"tmpzxu0f5he","filepermissions":"-rw-r--r--","filesize":"118 bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix - LF","sourcefile":"/tmp/tmp_7t30si7","wordcount":2},"updated":"2026-07-29T23:38:58.231065+00:00"},{"created":"2026-07-29T23:38:58.163477+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-29T23:38:58.163477+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/51315590942038050","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} + LF","sourcefile":"/tmp/tmpzxu0f5he","wordcount":2},"updated":"2026-07-30T01:18:50.886699+00:00"},{"created":"2026-07-30T01:18:50.840842+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-30T01:18:50.840842+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/88492900747453910","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/68/f7/8e/68f78e92-72a3-47ab-a126-55a836c38bf6?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011850Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=2fb4d42512b5103856af4c1f5f8e51d2ce3ca5557f30aa8e9125a55fa310eccd","votes":[{"arbiter":"450254598420922","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} ' headers: @@ -436,11 +436,11 @@ interactions: Connection: - keep-alive Content-Length: - - '3230' + - '3229' Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:39:01 GMT + - Thu, 30 Jul 2026 01:18:54 GMT Server: - gunicorn X-Billing-ID: @@ -462,17 +462,17 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/51315590942038050 + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/88492900747453910 response: body: - string: '{"result":{"artifact_id":"51315590942038050","assertions":[{"author":"299533097263972","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-07-29T23:38:57.583670+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR - virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":null,"last_seen":null,"md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-29T23:38:58.231065+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 - 23:38:58+00:00","fileinodechangedate":"2026:07:29 23:38:58+00:00","filemodifydate":"2026:07:29 - 23:38:58+00:00","filename":"tmp_7t30si7","filepermissions":"-rw-r--r--","filesize":"118 + string: '{"result":{"artifact_id":"88492900747453910","assertions":[{"author":"86494354581135","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-07-30T01:18:50.605483+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-30T01:18:50.605483+00:00","id":"88492900747453910","known_good":null,"last_scanned":null,"last_seen":null,"md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-30T01:18:50.886699+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:30 + 01:18:50+00:00","fileinodechangedate":"2026:07:30 01:18:50+00:00","filemodifydate":"2026:07:30 + 01:18:50+00:00","filename":"tmpzxu0f5he","filepermissions":"-rw-r--r--","filesize":"118 bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix - LF","sourcefile":"/tmp/tmp_7t30si7","wordcount":2},"updated":"2026-07-29T23:38:58.231065+00:00"},{"created":"2026-07-29T23:38:58.163477+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-29T23:38:58.163477+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/51315590942038050","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} + LF","sourcefile":"/tmp/tmpzxu0f5he","wordcount":2},"updated":"2026-07-30T01:18:50.886699+00:00"},{"created":"2026-07-30T01:18:50.840842+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-30T01:18:50.840842+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/88492900747453910","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/68/f7/8e/68f78e92-72a3-47ab-a126-55a836c38bf6?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011850Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=2fb4d42512b5103856af4c1f5f8e51d2ce3ca5557f30aa8e9125a55fa310eccd","votes":[{"arbiter":"450254598420922","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} ' headers: @@ -483,11 +483,11 @@ interactions: Connection: - keep-alive Content-Length: - - '3230' + - '3229' Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:39:02 GMT + - Thu, 30 Jul 2026 01:18:55 GMT Server: - gunicorn X-Billing-ID: @@ -509,17 +509,17 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/51315590942038050 + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/88492900747453910 response: body: - string: '{"result":{"artifact_id":"51315590942038050","assertions":[{"author":"299533097263972","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-07-29T23:38:57.583670+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR - virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":null,"last_seen":null,"md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-29T23:38:58.231065+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 - 23:38:58+00:00","fileinodechangedate":"2026:07:29 23:38:58+00:00","filemodifydate":"2026:07:29 - 23:38:58+00:00","filename":"tmp_7t30si7","filepermissions":"-rw-r--r--","filesize":"118 + string: '{"result":{"artifact_id":"88492900747453910","assertions":[{"author":"86494354581135","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-07-30T01:18:50.605483+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR + virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-30T01:18:50.605483+00:00","id":"88492900747453910","known_good":null,"last_scanned":null,"last_seen":null,"md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-30T01:18:50.886699+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:30 + 01:18:50+00:00","fileinodechangedate":"2026:07:30 01:18:50+00:00","filemodifydate":"2026:07:30 + 01:18:50+00:00","filename":"tmpzxu0f5he","filepermissions":"-rw-r--r--","filesize":"118 bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix - LF","sourcefile":"/tmp/tmp_7t30si7","wordcount":2},"updated":"2026-07-29T23:38:58.231065+00:00"},{"created":"2026-07-29T23:38:58.163477+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-29T23:38:58.163477+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/51315590942038050","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} + LF","sourcefile":"/tmp/tmpzxu0f5he","wordcount":2},"updated":"2026-07-30T01:18:50.886699+00:00"},{"created":"2026-07-30T01:18:50.840842+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-30T01:18:50.840842+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/88492900747453910","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/68/f7/8e/68f78e92-72a3-47ab-a126-55a836c38bf6?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011850Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=2fb4d42512b5103856af4c1f5f8e51d2ce3ca5557f30aa8e9125a55fa310eccd","votes":[{"arbiter":"450254598420922","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} ' headers: @@ -530,11 +530,11 @@ interactions: Connection: - keep-alive Content-Length: - - '3230' + - '3229' Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:39:03 GMT + - Thu, 30 Jul 2026 01:18:56 GMT Server: - gunicorn X-Billing-ID: @@ -556,17 +556,17 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/51315590942038050 + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/88492900747453910 response: body: - string: '{"result":{"artifact_id":"51315590942038050","assertions":[{"author":"299533097263972","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-07-29T23:38:57.583670+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":"EICAR - virus test files","failed":false,"filename":"artifact","first_seen":"2026-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":null,"last_seen":null,"md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-29T23:38:58.231065+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 - 23:38:58+00:00","fileinodechangedate":"2026:07:29 23:38:58+00:00","filemodifydate":"2026:07:29 - 23:38:58+00:00","filename":"tmp_7t30si7","filepermissions":"-rw-r--r--","filesize":"118 + string: '{"result":{"artifact_id":"88492900747453910","assertions":[{"author":"86494354581135","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-07-30T01:18:50.605483+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-07-30T01:18:50.605483+00:00","id":"88492900747453910","known_good":null,"last_scanned":"2026-07-30T01:18:50.605483+00:00","last_seen":"2026-07-30T01:18:50.605483+00:00","md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-30T01:18:57.730453+00:00","tool":"polyunite","tool_metadata":{"labels":["nonmalware"],"malware_family":"EICAR","operating_system":[]},"updated":"2026-07-30T01:18:57.730453+00:00"},{"created":"2026-07-30T01:18:50.886699+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:30 + 01:18:50+00:00","fileinodechangedate":"2026:07:30 01:18:50+00:00","filemodifydate":"2026:07:30 + 01:18:50+00:00","filename":"tmpzxu0f5he","filepermissions":"-rw-r--r--","filesize":"118 bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix - LF","sourcefile":"/tmp/tmp_7t30si7","wordcount":2},"updated":"2026-07-29T23:38:58.231065+00:00"},{"created":"2026-07-29T23:38:58.163477+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-29T23:38:58.163477+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/51315590942038050","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"SUBMITTED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} + LF","sourcefile":"/tmp/tmpzxu0f5he","wordcount":2},"updated":"2026-07-30T01:18:50.886699+00:00"},{"created":"2026-07-30T01:18:50.840842+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-30T01:18:50.840842+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/88492900747453910","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"AWAITING_ARBITRATION","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/68/f7/8e/68f78e92-72a3-47ab-a126-55a836c38bf6?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260730%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260730T011850Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=2fb4d42512b5103856af4c1f5f8e51d2ce3ca5557f30aa8e9125a55fa310eccd","votes":[{"arbiter":"450254598420922","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":true},"status":"OK"} ' headers: @@ -577,11 +577,11 @@ interactions: Connection: - keep-alive Content-Length: - - '3230' + - '3531' Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:39:04 GMT + - Thu, 30 Jul 2026 01:18:57 GMT Server: - gunicorn X-Billing-ID: @@ -603,103 +603,9 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) - method: GET - uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/51315590942038050 - response: - body: - string: '{"result":{"artifact_id":"51315590942038050","assertions":[{"author":"299533097263972","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-07-29T23:38:57.583670+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-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":null,"last_seen":null,"md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-29T23:38:58.231065+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 - 23:38:58+00:00","fileinodechangedate":"2026:07:29 23:38:58+00:00","filemodifydate":"2026:07:29 - 23:38:58+00:00","filename":"tmp_7t30si7","filepermissions":"-rw-r--r--","filesize":"118 - bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix - LF","sourcefile":"/tmp/tmp_7t30si7","wordcount":2},"updated":"2026-07-29T23:38:58.231065+00:00"},{"created":"2026-07-29T23:38:58.163477+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-29T23:38:58.163477+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/51315590942038050","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"AWAITING_ARBITRATION","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":false},"status":"OK"} - - ' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '3273' - Content-Type: - - application/json - Date: - - Wed, 29 Jul 2026 23:39: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: - - artifact-index-e2e:9696 - user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) - method: GET - uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/51315590942038050 - response: - body: - string: '{"result":{"artifact_id":"51315590942038050","assertions":[{"author":"299533097263972","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-07-29T23:38:57.583670+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-07-29T23:38:57.583670+00:00","id":"51315590942038050","known_good":null,"last_scanned":"2026-07-29T23:38:57.583670+00:00","last_seen":"2026-07-29T23:38:57.583670+00:00","md5":"f9ae88ea41d60b331ac29746fec938bd","metadata":[{"created":"2026-07-29T23:39:06.204597+00:00","tool":"polyunite","tool_metadata":{"labels":["nonmalware"],"malware_family":"EICAR","operating_system":[]},"updated":"2026-07-29T23:39:06.204597+00:00"},{"created":"2026-07-29T23:38:58.231065+00:00","tool":"exiftool","tool_metadata":{"directory":"/tmp","exiftoolversion":12.76,"fileaccessdate":"2026:07:29 - 23:38:58+00:00","fileinodechangedate":"2026:07:29 23:38:58+00:00","filemodifydate":"2026:07:29 - 23:38:58+00:00","filename":"tmp_7t30si7","filepermissions":"-rw-r--r--","filesize":"118 - bytes","filetype":"TXT","filetypeextension":"txt","linecount":2,"mimeencoding":"us-ascii","mimetype":"text/plain","newlines":"Unix - LF","sourcefile":"/tmp/tmp_7t30si7","wordcount":2},"updated":"2026-07-29T23:38:58.231065+00:00"},{"created":"2026-07-29T23:38:58.163477+00:00","tool":"hash","tool_metadata":{"md5":"f9ae88ea41d60b331ac29746fec938bd","sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","sha3_256":"a0c0acbd473c5566f0bb3a72dce1d5e7ef5e53bc4eee3721a5a0b593d5a14200","sha3_512":"e074bd1c3deb4ff4774a472f0e8a3a00777fe76d15f4d3625566863ea2bec63235f5e0d32272afd840e5ad217d0c784dd54c4b4c3131f35733bbdcc2e9388940","sha512":"c925e7dcb130dc072b98414cc4b33f918b2c53976ab020cf55698e239af4bc56108a53085482644cc5b6fe895e930edc217942ffa11bbbed5bf486d2ec06bd70","ssdeep":"3:a+JraNvsgzsVqSwHqDYrrPVasq9jQ/Gn:tJuOgzskBrosG","tlsh":"62b01200372fee1f9657401c31baba761908825f1cd8063cd3e2a0f9e8f315406d9a78"},"updated":"2026-07-29T23:38:58.163477+00:00"}],"mimetype":"text/plain","permalink":"https://polyswarm.network/scan/results/file/1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58/51315590942038050","polyscore":null,"result":null,"sha1":"b451951d54cfdf6d35f42f6ebdda7ec26875f470","sha256":"1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58","size":118,"state":"AWAITING_ARBITRATION","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/bc/80/e7/bc80e730-432f-42be-854a-4245ac87148b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260729T233857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=fe63b114791a04596c3bd267359c7fc8b048422a4a9fb6a7276cff423c7ab24b","votes":[{"arbiter":"770891666518328","arbiter_name":"arbiter-eicar","engine":{"description":"webhook-arbiter-description","name":"arbiter-eicar"},"metadata":{"malware_family":"EICAR","product":"eicar","scanner":{"environment":{"architecture":"x86_64","operating_system":"Linux"}}},"vote":true}],"window_closed":true},"status":"OK"} - - ' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '3532' - Content-Type: - - application/json - Date: - - Wed, 29 Jul 2026 23:39: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: - - artifact-index-e2e:9696 - user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: HEAD - uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58&community=gamma&require_scan=false + uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58&community=gamma&require_scan=true response: body: string: '' @@ -715,7 +621,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:39:06 GMT + - Thu, 30 Jul 2026 01:18:57 GMT Server: - gunicorn X-Billing-ID: @@ -737,7 +643,7 @@ interactions: host: - artifact-index-e2e:9696 user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) + - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.14.4) method: HEAD uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58&community=gamma&require_scan=false response: @@ -755,47 +661,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 29 Jul 2026 23:39: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: - - artifact-index-e2e:9696 - user-agent: - - polyswarm_api/4.2.0 (x86_64-Linux-CPython-3.12.13) - method: HEAD - uri: http://artifact-index-e2e:9696/v3/search/hash/sha256?hash=1d74a0b7915342db3f07dc10d2f4ac9160d7fb6dc70c2956e8d0dd2fd2dcbc58&community=gamma&require_scan=true - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '57' - Content-Type: - - application/json - Date: - - Wed, 29 Jul 2026 23:39:07 GMT + - Thu, 30 Jul 2026 01:18:57 GMT Server: - gunicorn X-Billing-ID: From b85af8d1ffc8244410287e587aead0cd0c3c853b Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 29 Jul 2026 22:48:37 -0300 Subject: [PATCH 15/20] docs: document the known-good refusal on the methods that raise it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All the docstring work landed on exists(), which never raises it. Every download* method can, and the endpoint catalogue said only 'closes the handle' — so a consumer reading either had no pointer to the new exception. Adds the :raises: (with the NotFoundException-subclass note, .sources, and the nothing-is-written guarantee) and the catalogue rows, including that only a dropped file can be withheld out of the sandbox-artifact route. --- specs/03-endpoints.md | 15 ++++++++++++--- src/polyswarm_api/aio/api.py | 36 ++++++++++++++++++++++++++++++++++-- src/polyswarm_api/api.py | 36 ++++++++++++++++++++++++++++++++++-- 3 files changed, 80 insertions(+), 7 deletions(-) diff --git a/specs/03-endpoints.md b/specs/03-endpoints.md index 8533f217..dd14f5ec 100644 --- a/specs/03-endpoints.md +++ b/specs/03-endpoints.md @@ -106,11 +106,20 @@ Internal-only CRUD for the `/known-good` binary resource (distinct from the IOC | Method | Resource builder | Notes | |---|---|---| -| `download(out_dir, hash_, hash_type=None)` | `LocalArtifact.download` | Closes the handle before returning. | +| `download(out_dir, hash_, hash_type=None)` | `LocalArtifact.download` | Closes the handle before returning. Raises `KnownGoodWithheldException` (see below). | | `download_id(out_dir, instance_id)` | `LocalArtifact.download_id` | Same. | -| `download_sandbox_artifact(out_dir, sandbox_task_id, instance_id)` | `LocalArtifact.download_sandbox_artifact` | Same. | +| `download_sandbox_artifact(out_dir, sandbox_task_id, instance_id)` | `LocalArtifact.download_sandbox_artifact` | Same — though only a **dropped file** can be withheld; sandbox evidence (report / raw_report / screenshot / recording / pcap / memory_dump) is exempt server-side. | | `download_archive(out_dir, s3_path)` | `LocalArtifact.download_archive` | Same. | -| `download_to_handle(hash_, fh, hash_type=None)` | `LocalArtifact.download` | Streams to an existing file handle. | +| `download_to_handle(hash_, fh, hash_type=None)` | `LocalArtifact.download` | Streams to an existing file handle. Same refusal. | + +**Every `download*` method can refuse with `KnownGoodWithheldException`** — the sha256 is +catalogued as a known-good binary, so its bytes are withheld by design rather than missing. It +subclasses `NotFoundException` (raised from the shared `_raise_for_status` 404 arm), so existing +`except NotFoundException` handling still catches it; catch it specifically to tell a deliberate +refusal apart from a gone artifact, and read `.sources` for the feeds that flagged the hash. +**Nothing is written to the destination** — `_execute_download` checks the status before it opens +the file, so a refusal never leaves a zero-byte file behind, which to a caller would be +indistinguishable from a download that worked. ### Sandbox diff --git a/src/polyswarm_api/aio/api.py b/src/polyswarm_api/aio/api.py index 1426532c..894c0c14 100644 --- a/src/polyswarm_api/aio/api.py +++ b/src/polyswarm_api/aio/api.py @@ -939,6 +939,12 @@ async def download_to_handle(self, hash_, fh, hash_type=None): :param fh: A file-like object which we are going to write the contents of the artifact to. :param hash_type: Hash type of the provided hash_. Will attempt to auto-detect if not explicitly provided. :return: A LocalHandle resource + :raises KnownGoodWithheldException: the sha256 is catalogued as a known-good binary, + so its bytes are withheld by design. A ``NotFoundException`` subclass, so existing + ``except NotFoundException`` handling still catches it; catch it specifically to + tell a deliberate refusal apart from a missing artifact, and read + ``.sources`` for the feeds that flagged it. Nothing is written to the destination — + the status is checked before the file is opened. """ logger.info('Downloading %s into handle', hash_) hash_ = resources.Hash.from_hashable(hash_, hash_type=hash_type) @@ -1599,6 +1605,12 @@ async def download(self, out_dir, hash_, hash_type=None): :param hash_: Hashable (Artifact, LocalArtifact, Hash) or hex-encoded SHA256/SHA1/MD5. :param hash_type: Hash type; auto-detected from the literal if not provided. :return: A ``LocalArtifact`` resource with its handle already closed. + :raises KnownGoodWithheldException: the sha256 is catalogued as a known-good binary, + so its bytes are withheld by design. A ``NotFoundException`` subclass, so existing + ``except NotFoundException`` handling still catches it; catch it specifically to + tell a deliberate refusal apart from a missing artifact, and read + ``.sources`` for the feeds that flagged it. Nothing is written to the destination — + the status is checked before the file is opened. """ logger.info('Downloading %s into %s', hash_, out_dir) hash_ = resources.Hash.from_hashable(hash_, hash_type=hash_type) @@ -1609,7 +1621,15 @@ async def download(self, out_dir, hash_, hash_type=None): return artifact async def download_id(self, out_dir, instance_id): - """Download an artifact by its instance id into ``out_dir``.""" + """Download an artifact by its instance id into ``out_dir``. + + :raises KnownGoodWithheldException: the sha256 is catalogued as a known-good binary, + so its bytes are withheld by design. A ``NotFoundException`` subclass, so existing + ``except NotFoundException`` handling still catches it; catch it specifically to + tell a deliberate refusal apart from a missing artifact, and read + ``.sources`` for the feeds that flagged it. Nothing is written to the destination — + the status is checked before the file is opened. + """ logger.info('Downloading %s into %s', instance_id, out_dir) artifact = await self._single( resources.LocalArtifact.download_id(self, instance_id, folder=out_dir), @@ -1618,7 +1638,19 @@ async def download_id(self, out_dir, instance_id): return artifact async def download_sandbox_artifact(self, out_dir, sandbox_task_id, instance_id): - """Download a sandbox-produced artifact (e.g. PCAP, dropped file) into ``out_dir``.""" + """Download a sandbox-produced artifact (e.g. PCAP, dropped file) into ``out_dir``. + + Sandbox **evidence** (report / raw_report / screenshot / recording / pcap / + memory_dump) is exempt from the known-good policy server-side, so in practice only a + **dropped file** raises below — its sha256 is a real file's digest. + + :raises KnownGoodWithheldException: the sha256 is catalogued as a known-good binary, + so its bytes are withheld by design. A ``NotFoundException`` subclass, so existing + ``except NotFoundException`` handling still catches it; catch it specifically to + tell a deliberate refusal apart from a missing artifact, and read + ``.sources`` for the feeds that flagged it. Nothing is written to the destination — + the status is checked before the file is opened. + """ logger.info('Downloading sandbox artifact %s %s', sandbox_task_id, instance_id) sandbox_artifact = await self._single( resources.LocalArtifact.download_sandbox_artifact( diff --git a/src/polyswarm_api/api.py b/src/polyswarm_api/api.py index 817b445f..13920927 100644 --- a/src/polyswarm_api/api.py +++ b/src/polyswarm_api/api.py @@ -1139,6 +1139,12 @@ def download_to_handle(self, hash_, fh, hash_type=None): :param fh: A file-like object which we are going to write the contents of the artifact to. :param hash_type: Hash type of the provided hash_. Will attempt to auto-detect if not explicitly provided. :return: A LocalHandle resource + :raises KnownGoodWithheldException: the sha256 is catalogued as a known-good binary, + so its bytes are withheld by design. A ``NotFoundException`` subclass, so existing + ``except NotFoundException`` handling still catches it; catch it specifically to + tell a deliberate refusal apart from a missing artifact, and read + ``.sources`` for the feeds that flagged it. Nothing is written to the destination — + the status is checked before the file is opened. """ logger.info("Downloading %s into handle", hash_) hash_ = resources.Hash.from_hashable(hash_, hash_type=hash_type) @@ -1941,6 +1947,12 @@ def download(self, out_dir, hash_, hash_type=None): :param hash_: Hashable (Artifact, LocalArtifact, Hash) or hex-encoded SHA256/SHA1/MD5. :param hash_type: Hash type; auto-detected from the literal if not provided. :return: A ``LocalArtifact`` resource with its handle already closed. + :raises KnownGoodWithheldException: the sha256 is catalogued as a known-good binary, + so its bytes are withheld by design. A ``NotFoundException`` subclass, so existing + ``except NotFoundException`` handling still catches it; catch it specifically to + tell a deliberate refusal apart from a missing artifact, and read + ``.sources`` for the feeds that flagged it. Nothing is written to the destination — + the status is checked before the file is opened. """ logger.info("Downloading %s into %s", hash_, out_dir) hash_ = resources.Hash.from_hashable(hash_, hash_type=hash_type) @@ -1953,7 +1965,15 @@ def download(self, out_dir, hash_, hash_type=None): return artifact def download_id(self, out_dir, instance_id): - """Download an artifact by its instance id into ``out_dir``.""" + """Download an artifact by its instance id into ``out_dir``. + + :raises KnownGoodWithheldException: the sha256 is catalogued as a known-good binary, + so its bytes are withheld by design. A ``NotFoundException`` subclass, so existing + ``except NotFoundException`` handling still catches it; catch it specifically to + tell a deliberate refusal apart from a missing artifact, and read + ``.sources`` for the feeds that flagged it. Nothing is written to the destination — + the status is checked before the file is opened. + """ logger.info("Downloading %s into %s", instance_id, out_dir) artifact = self._single( resources.LocalArtifact.download_id(self, instance_id, folder=out_dir), @@ -1962,7 +1982,19 @@ def download_id(self, out_dir, instance_id): return artifact def download_sandbox_artifact(self, out_dir, sandbox_task_id, instance_id): - """Download a sandbox-produced artifact (e.g. PCAP, dropped file) into ``out_dir``.""" + """Download a sandbox-produced artifact (e.g. PCAP, dropped file) into ``out_dir``. + + Sandbox **evidence** (report / raw_report / screenshot / recording / pcap / + memory_dump) is exempt from the known-good policy server-side, so in practice only a + **dropped file** raises below — its sha256 is a real file's digest. + + :raises KnownGoodWithheldException: the sha256 is catalogued as a known-good binary, + so its bytes are withheld by design. A ``NotFoundException`` subclass, so existing + ``except NotFoundException`` handling still catches it; catch it specifically to + tell a deliberate refusal apart from a missing artifact, and read + ``.sources`` for the feeds that flagged it. Nothing is written to the destination — + the status is checked before the file is opened. + """ logger.info("Downloading sandbox artifact %s %s", sandbox_task_id, instance_id) sandbox_artifact = self._single( resources.LocalArtifact.download_sandbox_artifact( From 9901922f02ff078fdc088e76bf601be9efd89210 Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 29 Jul 2026 22:48:38 -0300 Subject: [PATCH 16/20] test: cover the exists() 404 arm on both transports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mapping is transport-independent and the sync client reaches it through the same generated `int(result) == 200`, but the test sat in the async module — in the PR that extracted the harness so one body could cover both. Moved onto ClientTestCase and added the require_scan form plus the 5xx case, which is worth pinning as a recorded decision: with no error channel on a HEAD, a server error also collapses to False, i.e. a fabricated negative. --- specs/04-testing.md | 3 +- test/async_client_test.py | 27 ------------------ test/exists_probe_mapping_test.py | 47 +++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 28 deletions(-) create mode 100644 test/exists_probe_mapping_test.py diff --git a/specs/04-testing.md b/specs/04-testing.md index 337bbec8..63a7ed35 100644 --- a/specs/04-testing.md +++ b/specs/04-testing.md @@ -19,6 +19,7 @@ How the test suite is organised. Three layers: pure unit tests (no HTTP at all - `test/conftest.py` — pytest configuration. - `test/core_test.py` — pure-unit tests for `parse_response`, `PolyswarmRequest`, and resource builders. No httpx, no fixtures. +- `test/exists_probe_mapping_test.py` — the `exists()` status mapping arms the e2e stack cannot produce (`404`→`False`, plus the fabricated-negative `5xx` case recorded as a decision), on the shared harness so both transports are covered. The `200`/`204` arms are live, in `client_scan_test.py` / `async_client_test.py`. - `test/_client_harness.py` — the parametrised `ClientTestCase` harness (`_MockBoundary` / `_AsyncToSync`), importable by any `respx` module that wants one body to cover both transports (invariant 5). Not every `respx` user needs it: `client_scan_test.py` / `async_client_test.py` drive `respx` directly for a number of cases (~27 bodies between them), most of which predate the harness — see invariant 5 for which of those are legitimately exempt and which are just older than the rule. Not collected itself — it matches neither `*_test.py` nor `test_*.py`, so pytest never picks it up (the leading `_` is a naming convention, not the mechanism), same as `_e2e_helpers.py`. - `test/metadata_field_properties_test.py` — the canonical example of the parametrised `ClientTestCase` harness with `respx`-backed mocking. - `test/client_scan_test.py` — sync, VCR-backed integration tests (not yet on the parametrised harness — follow-up work). @@ -54,7 +55,7 @@ A respx body for that same arm was written first and deleted once the live cover ## The parametrised `ClientTestCase` harness -Implemented in `test/_client_harness.py`, importable by any `respx` module that wants one body over both transports; `metadata_field_properties_test.py` is the canonical user (and currently the only one — the other `respx` bodies are the single-transport cases invariant 5 exempts). The shape: +Implemented in `test/_client_harness.py`, importable by any `respx` module that wants one body over both transports; `metadata_field_properties_test.py` is the canonical user, joined by `exists_probe_mapping_test.py` (the `exists()` `404`→`False` arm, which was async-only until the harness existed — the mapping is transport-independent, so one body covers both). The remaining `respx` bodies are the single-transport cases invariant 5 exempts. The shape: ```python # test/_client_harness.py diff --git a/test/async_client_test.py b/test/async_client_test.py index 05a32003..62369995 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -1115,33 +1115,6 @@ def write(self, b): assert all(len(w) <= 4 for w in writes), writes -@respx.mock -async def test_async_exists_maps_404_false(): - """``exists()`` maps a ``404`` to ``False`` — the one arm of this mapping the e2e stack - cannot produce, so the only one that stays mocked. - - The ``200`` (present) and ``204`` (absent) arms are asserted against the **real server** on - resources the test provisions itself, in - ``test_async_hash_existence_probe_against_the_real_server`` and its sync twin. That is the - default (invariant 1) and it is the only thing that would catch a *server-side* flip, which - a mock cannot by construction — the 4.0 ``exists()`` inversion survived precisely because - this endpoint's coverage was entirely mocked. - - ``404`` stays here because artifact-index never answers it for a well-formed probe: per its - ``specs/09-hash-search-head-contract.md`` that code is reserved for the *request* being - wrong, and a bad hash or hash type raises ``400``. The mapping is still worth pinning — - clients have tolerated ``404``-as-absent historically, so the SDK must not start raising if - a proxy or an older deployment in front of the API emits one. - """ - route = respx.head(f'{BASE_URL}/search/hash/sha256') - api = PolySwarmAsyncAPI(API_KEY, uri=BASE_URL, community='gamma') - try: - route.mock(return_value=httpx.Response(404)) - assert await api.exists(SHA256) is False - finally: - await api.aclose() - - @respx.mock async def test_async_sample_bundle_download_multistep(): """``sample_bundle_download`` is a multi-step canonical async diff --git a/test/exists_probe_mapping_test.py b/test/exists_probe_mapping_test.py new file mode 100644 index 00000000..1913521c --- /dev/null +++ b/test/exists_probe_mapping_test.py @@ -0,0 +1,47 @@ +"""The one arm of ``exists()``'s status mapping the e2e stack cannot produce. + +Everything else about this endpoint is asserted against the **real server**, on resources the +tests provision themselves — ``test_hash_existence_probe_against_the_real_server`` and its async +twin cover ``200`` (present) and ``204`` (absent), in both the plain and ``require_scan`` forms. +That is the default (``specs/04-testing.md`` invariant 1), and it is the only thing that can +catch a *server-side* flip, which a mock cannot by construction: the 4.0 ``exists()`` inversion +survived release precisely because this endpoint's coverage was entirely mocked. + +``404`` stays mocked because artifact-index never answers it for a well-formed probe — per its +``specs/09-hash-search-head-contract.md`` that code is reserved for the *request* being wrong, +and a bad hash or hash type raises ``400``. The mapping is still worth pinning: clients have +tolerated ``404``-as-absent historically, so the SDK must not start raising if a proxy or an +older deployment in front of the API emits one. + +It lives on ``ClientTestCase`` rather than in one transport's module because the mapping is +transport-independent — the sync client reaches it through the generated ``int(result) == 200`` +just as the async one does, and the pure tier only covers ``parse_response``'s HEAD +short-circuit, not that comparison. +""" +from test._client_harness import BASE_URL, ClientTestCase + +_SHA256 = 'a' * 64 +_PROBE_URL = f'{BASE_URL}/search/hash/sha256' + + +class ExistsProbeMappingTestCase(ClientTestCase): + def test_a_404_maps_to_absent_rather_than_raising(self): + self.mock.add('HEAD', _PROBE_URL, json={}, status=404) + assert self.api.exists(_SHA256) is False + + def test_a_404_maps_to_absent_under_require_scan_too(self): + # Same arm, but through the query-carrying form: `require_scan` is routed as a param, + # so a regression that dropped the short-circuit for one form only would pass above. + self.mock.add('HEAD', _PROBE_URL, json={}, status=404) + assert self.api.exists(_SHA256, require_scan=True) is False + assert 'require_scan=false' not in self.mock.last_request_url + assert 'require_scan=true' in self.mock.last_request_url + + def test_a_server_error_is_not_reported_as_absent_silently(self): + # Documents the sharpest edge of having no error channel on this probe: a 5xx also + # collapses to False, i.e. a *fabricated negative* (artifact-index's contract + # invariant 6 names this as the reason never to repurpose these codes). Pinned so the + # behaviour is a recorded decision rather than an accident — if the SDK ever grows an + # error channel here, this is the test that should fail and be rewritten. + self.mock.add('HEAD', _PROBE_URL, json={}, status=500) + assert self.api.exists(_SHA256) is False From 055d6f71c66eebf413216c0253bbe994f7264b96 Mon Sep 17 00:00:00 2001 From: Samuel Date: Thu, 30 Jul 2026 20:37:55 -0300 Subject: [PATCH 17/20] docs: record what the server's two-predicate model means for consumers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit artifact-index reworked known-good to its authoritative model (its specs/05, DN-8425): the refusal now fires on the CURRENT understanding — a catalogue entry for the sha256 AND that entry's extension passing an executable allow-list — evaluated live per request. Two consumer-visible consequences, now in the downstream contract: a refused download can start working again with no action (entry deleted, or the policy narrows), and `ArtifactInstance.state` can carry the new `NOT_STORED` value for a submission the server declined as known-good whose hash is no longer currently known-good. No code change: `state` is a plain string the SDK does not enumerate, the 404 `errors.code` strings are unchanged, and KnownGoodWithheldException keeps its NotFoundException base. The lifecycle test already catalogues with an eligible filename ('kg-sample.exe'), so its assertions hold unchanged. --- specs/05-downstream-contract.md | 2 ++ src/polyswarm_api/resources.py | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/specs/05-downstream-contract.md b/specs/05-downstream-contract.md index e60472ae..cda1e4b6 100644 --- a/specs/05-downstream-contract.md +++ b/specs/05-downstream-contract.md @@ -180,6 +180,8 @@ class TimeoutException(PolyswarmException): ... `KnownGoodWithheldException` is the 404 raised when a download is refused because the artifact is a known-good binary — the platform never stores or serves those bytes. It **subclasses `NotFoundException`** precisely so invariant 3 holds for existing consumers: code that already does `except NotFoundException:` keeps catching the refusal with no change, and only callers that want to distinguish "withheld by design" from a plain miss catch the subclass. It adds one attribute, `.sources` — the known-good feeds that flagged the hash (e.g. `['nsrl']`), `[]` when the server named none. The contract is only this: **always a list of strings**, whatever the envelope carried, so `for feed in exc.sources` needs no shape check. Which wire shapes are coerced, and which are dropped and logged, is `exceptions._normalise_sources`' business rather than a promise to consumers — the server sends a list of strings today. Note it is **not** normalised the same way as `ArtifactInstance.known_good_sources`, which is the same concept reached from the instance response: that one is sorted and de-duplicated, while `.sources` preserves the order the envelope carried and can repeat a feed. Don't assume parity between the two. The raw envelope stays reachable at `exc.request.errors` (`{'code': 'KNOWN_GOOD', 'known_good': True, 'sources': [...]}`). The artifact's metadata — the flagging feeds plus any scan data already collected — remains readable through the search / instance endpoints; only the bytes are withheld, and the instance's `KNOWN_GOOD` state/status is the signal for that (there is no separate "withheld" field). +**What "known-good" means on the server, as of artifact-index's two-predicate model** (its `specs/05`, DN-8425): the refusal fires on the server's *current understanding* — a catalogue entry exists for the sha256 **and** that entry's extension passes an executable allow-list — evaluated live on every request. Two consequences worth knowing as a consumer: the same download can start working again with no action on your part (the entry is deleted, or the policy narrows), and `ArtifactInstance.state` can report the new value **`NOT_STORED`** — a submission the server declined as known-good at the time whose hash is no longer currently known-good, so nothing was ever stored for it and a fresh submit of the same file works. `state` is a plain string here; the SDK does not enumerate it, so a new member needs no SDK release. + Each `RequestException` subclass carries a `.request` attribute holding the originating `PolyswarmRequest` (set by `RequestException.__init__`). Callers can read `exc.request.status_code`, `exc.request.json` (the parsed response body after execution), `exc.request.input_json` (the body that was sent), `exc.request.request_parameters` (the request kwargs that built the call), etc. `InvalidValueException` and `TimeoutException` are client-side errors and don't carry a request descriptor. Attachment happens at exception-construction time inside `parse_response` — `session.execute` does not catch and rewrap. diff --git a/src/polyswarm_api/resources.py b/src/polyswarm_api/resources.py index 16f4a8db..94d4291c 100644 --- a/src/polyswarm_api/resources.py +++ b/src/polyswarm_api/resources.py @@ -275,7 +275,13 @@ def __init__(self, content, api=None): self.known_good_sources = sorted( {feed['tool'] for feed in self.known_good if feed.get('tool')} ) if self.known_good else [] - # Friendly bounty-state NAME (e.g. 'KNOWN_GOOD' / 'SETTLED' / 'STORED'), + # Friendly REPORTED-state NAME (e.g. 'KNOWN_GOOD' / 'SETTLED' / 'STORED' / + # 'NOT_STORED'). The server derives it from its two-predicate known-good model + # (artifact-index specs/05): 'KNOWN_GOOD' while the file is currently known-good, + # 'NOT_STORED' for a submission it declined as known-good whose hash is no longer + # currently known-good (nothing was ever stored; a fresh submit works), otherwise + # the instance's own persisted state. + # # additive alongside the numeric bounty_state. Optional — older servers # omit it, so .get() yields None (no behaviour change). self.state = content.get('state') From a4989b3d4401b688bab996d2eb6e51be467c61f3 Mon Sep 17 00:00:00 2001 From: Samuel Date: Thu, 30 Jul 2026 21:26:49 -0300 Subject: [PATCH 18/20] docs(specs): scope the typed refusal to artifact-index-served downloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review fixes, plus one drift the review missed: - Drop the internal ticket reference from the downstream-contract spec — this repo is public, and the artifact-index specs/05 pointer alone carries it. - specs/03 claimed every download* method can raise KnownGoodWithheldException. download_archive cannot: it fetches the caller-supplied pre-signed object-store URL from the stream() feed (Authorization suppressed), so an error there is the store's own XML body — no coded JSON envelope — and surfaces through the generic arms. The code had this right (it is the one download* method with no :raises docstring); the spec drifted. Scoped the claim to the four artifact-index-served downloads and gave the archive row its own honest note. - The download_sandbox_artifact row still described the retired sample-vs-evidence exemption; the server-side model gates every sandbox artifact by its own sha256. - Renamed test_a_server_error_is_not_reported_as_absent_silently — its body pins the opposite (a 5xx does collapse to a fabricated negative, as a recorded decision), so anyone grepping test names read the inverse of the guarantee. --- specs/03-endpoints.md | 13 ++++++++++--- specs/05-downstream-contract.md | 2 +- test/exists_probe_mapping_test.py | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/specs/03-endpoints.md b/specs/03-endpoints.md index dd14f5ec..eea14fd5 100644 --- a/specs/03-endpoints.md +++ b/specs/03-endpoints.md @@ -108,11 +108,12 @@ Internal-only CRUD for the `/known-good` binary resource (distinct from the IOC |---|---|---| | `download(out_dir, hash_, hash_type=None)` | `LocalArtifact.download` | Closes the handle before returning. Raises `KnownGoodWithheldException` (see below). | | `download_id(out_dir, instance_id)` | `LocalArtifact.download_id` | Same. | -| `download_sandbox_artifact(out_dir, sandbox_task_id, instance_id)` | `LocalArtifact.download_sandbox_artifact` | Same — though only a **dropped file** can be withheld; sandbox evidence (report / raw_report / screenshot / recording / pcap / memory_dump) is exempt server-side. | -| `download_archive(out_dir, s3_path)` | `LocalArtifact.download_archive` | Same. | +| `download_sandbox_artifact(out_dir, sandbox_task_id, instance_id)` | `LocalArtifact.download_sandbox_artifact` | Same — and the gate applies to **every** sandbox artifact by its own sha256 (dropped file, screenshot, report, …); the server-side model has no sample-vs-evidence carve-out. | +| `download_archive(out_dir, s3_path)` | `LocalArtifact.download_archive` | Closes the handle before returning. **Not** an artifact-index call (see below), so it never raises `KnownGoodWithheldException`. | | `download_to_handle(hash_, fh, hash_type=None)` | `LocalArtifact.download` | Streams to an existing file handle. Same refusal. | -**Every `download*` method can refuse with `KnownGoodWithheldException`** — the sha256 is +**Every artifact-index-served download** (`download`, `download_id`, `download_to_handle`, +`download_sandbox_artifact`) **can refuse with `KnownGoodWithheldException`** — the sha256 is catalogued as a known-good binary, so its bytes are withheld by design rather than missing. It subclasses `NotFoundException` (raised from the shared `_raise_for_status` 404 arm), so existing `except NotFoundException` handling still catches it; catch it specifically to tell a deliberate @@ -121,6 +122,12 @@ refusal apart from a gone artifact, and read `.sources` for the feeds that flagg the file, so a refusal never leaves a zero-byte file behind, which to a caller would be indistinguishable from a download that worked. +`download_archive` is the exception because it is not an artifact-index request at all: it fetches +the caller-supplied pre-signed object-store URL from the `stream()` feed, with the `Authorization` +header suppressed. An error there is the store's own (XML body, no coded JSON envelope), so it +surfaces as a plain `NotFoundException` / server error from the generic arms, never as the typed +refusal. + ### Sandbox | Method | Resource builder | diff --git a/specs/05-downstream-contract.md b/specs/05-downstream-contract.md index cda1e4b6..d91ea219 100644 --- a/specs/05-downstream-contract.md +++ b/specs/05-downstream-contract.md @@ -180,7 +180,7 @@ class TimeoutException(PolyswarmException): ... `KnownGoodWithheldException` is the 404 raised when a download is refused because the artifact is a known-good binary — the platform never stores or serves those bytes. It **subclasses `NotFoundException`** precisely so invariant 3 holds for existing consumers: code that already does `except NotFoundException:` keeps catching the refusal with no change, and only callers that want to distinguish "withheld by design" from a plain miss catch the subclass. It adds one attribute, `.sources` — the known-good feeds that flagged the hash (e.g. `['nsrl']`), `[]` when the server named none. The contract is only this: **always a list of strings**, whatever the envelope carried, so `for feed in exc.sources` needs no shape check. Which wire shapes are coerced, and which are dropped and logged, is `exceptions._normalise_sources`' business rather than a promise to consumers — the server sends a list of strings today. Note it is **not** normalised the same way as `ArtifactInstance.known_good_sources`, which is the same concept reached from the instance response: that one is sorted and de-duplicated, while `.sources` preserves the order the envelope carried and can repeat a feed. Don't assume parity between the two. The raw envelope stays reachable at `exc.request.errors` (`{'code': 'KNOWN_GOOD', 'known_good': True, 'sources': [...]}`). The artifact's metadata — the flagging feeds plus any scan data already collected — remains readable through the search / instance endpoints; only the bytes are withheld, and the instance's `KNOWN_GOOD` state/status is the signal for that (there is no separate "withheld" field). -**What "known-good" means on the server, as of artifact-index's two-predicate model** (its `specs/05`, DN-8425): the refusal fires on the server's *current understanding* — a catalogue entry exists for the sha256 **and** that entry's extension passes an executable allow-list — evaluated live on every request. Two consequences worth knowing as a consumer: the same download can start working again with no action on your part (the entry is deleted, or the policy narrows), and `ArtifactInstance.state` can report the new value **`NOT_STORED`** — a submission the server declined as known-good at the time whose hash is no longer currently known-good, so nothing was ever stored for it and a fresh submit of the same file works. `state` is a plain string here; the SDK does not enumerate it, so a new member needs no SDK release. +**What "known-good" means on the server, as of artifact-index's two-predicate model** (its `specs/05`): the refusal fires on the server's *current understanding* — a catalogue entry exists for the sha256 **and** that entry's extension passes an executable allow-list — evaluated live on every request. Two consequences worth knowing as a consumer: the same download can start working again with no action on your part (the entry is deleted, or the policy narrows), and `ArtifactInstance.state` can report the new value **`NOT_STORED`** — a submission the server declined as known-good at the time whose hash is no longer currently known-good, so nothing was ever stored for it and a fresh submit of the same file works. `state` is a plain string here; the SDK does not enumerate it, so a new member needs no SDK release. Each `RequestException` subclass carries a `.request` attribute holding the originating `PolyswarmRequest` (set by `RequestException.__init__`). Callers can read `exc.request.status_code`, `exc.request.json` (the parsed response body after execution), `exc.request.input_json` (the body that was sent), `exc.request.request_parameters` (the request kwargs that built the call), etc. `InvalidValueException` and `TimeoutException` are client-side errors and don't carry a request descriptor. diff --git a/test/exists_probe_mapping_test.py b/test/exists_probe_mapping_test.py index 1913521c..931f6c22 100644 --- a/test/exists_probe_mapping_test.py +++ b/test/exists_probe_mapping_test.py @@ -37,7 +37,7 @@ def test_a_404_maps_to_absent_under_require_scan_too(self): assert 'require_scan=false' not in self.mock.last_request_url assert 'require_scan=true' in self.mock.last_request_url - def test_a_server_error_is_not_reported_as_absent_silently(self): + def test_a_server_error_also_collapses_to_absent(self): # Documents the sharpest edge of having no error channel on this probe: a 5xx also # collapses to False, i.e. a *fabricated negative* (artifact-index's contract # invariant 6 names this as the reason never to repurpose these codes). Pinned so the From fad03514aea5c33da45eb7c20e1fb289ea417b4a Mon Sep 17 00:00:00 2001 From: Samuel Date: Thu, 30 Jul 2026 21:41:35 -0300 Subject: [PATCH 19/20] docs: align the evidence-exemption prose with the server; qualify the withheld signal - download_sandbox_artifact's docstring (both mirrors) still described the retired server-side sample-vs-evidence exemption, telling callers not to handle a refusal they will get; specs/03 already had it right. The gate applies to every sandbox artifact by its own sha256. - specs/02 and specs/05 called state == KNOWN_GOOD "the signal that bytes are withheld" in general. Qualified: it signals the *typed refusal*; a NOT_STORED instance has no bytes either but 404s plainly, because nothing is being withheld by policy any more. A consumer branching on state to predict availability would have gotten NOT_STORED wrong. - resources.py: rejoined a comment fragment the rewrite orphaned. --- specs/02-resources.md | 13 ++++++++----- specs/05-downstream-contract.md | 2 +- src/polyswarm_api/aio/api.py | 8 +++++--- src/polyswarm_api/api.py | 8 +++++--- src/polyswarm_api/resources.py | 7 +++---- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/specs/02-resources.md b/specs/02-resources.md index fa68310a..181bbd81 100644 --- a/specs/02-resources.md +++ b/specs/02-resources.md @@ -303,11 +303,14 @@ known-good-bypassed scan via `state == 'KNOWN_GOOD'` even when `known_good` abov `None` (the sha matched no `KnownGood`). The raw numeric `bounty_state` is unchanged. `state == 'KNOWN_GOOD'` (equivalently the status the server reports for the instance) -is also **the** signal that the artifact's bytes are withheld — the platform never -stores or serves a known-good binary, and there is deliberately no separate -"withheld" field to read. A download attempted anyway raises -`KnownGoodWithheldException` (see §"Exceptions thrown by parsing"); the metadata — -the flagging feeds plus any scan data already collected — stays readable. +is **the** signal for the *typed refusal* — the platform never stores or serves a +known-good binary, there is deliberately no separate "withheld" field to read, and a +download attempted anyway raises `KnownGoodWithheldException` (see §"Exceptions thrown +by parsing"); the metadata — the flagging feeds plus any scan data already collected — +stays readable. It is not the signal for "bytes are unavailable" in general: +`state == 'NOT_STORED'` (below) also has no bytes — nothing was ever stored for that +instance — but its download 404s **plainly**, without the `KNOWN_GOOD` code, because +nothing is being withheld by policy any more. Classmethod builders (each returns a `PolyswarmRequest` descriptor): diff --git a/specs/05-downstream-contract.md b/specs/05-downstream-contract.md index d91ea219..2b40d018 100644 --- a/specs/05-downstream-contract.md +++ b/specs/05-downstream-contract.md @@ -178,7 +178,7 @@ class InvalidValueException(PolyswarmException): ... class TimeoutException(PolyswarmException): ... ``` -`KnownGoodWithheldException` is the 404 raised when a download is refused because the artifact is a known-good binary — the platform never stores or serves those bytes. It **subclasses `NotFoundException`** precisely so invariant 3 holds for existing consumers: code that already does `except NotFoundException:` keeps catching the refusal with no change, and only callers that want to distinguish "withheld by design" from a plain miss catch the subclass. It adds one attribute, `.sources` — the known-good feeds that flagged the hash (e.g. `['nsrl']`), `[]` when the server named none. The contract is only this: **always a list of strings**, whatever the envelope carried, so `for feed in exc.sources` needs no shape check. Which wire shapes are coerced, and which are dropped and logged, is `exceptions._normalise_sources`' business rather than a promise to consumers — the server sends a list of strings today. Note it is **not** normalised the same way as `ArtifactInstance.known_good_sources`, which is the same concept reached from the instance response: that one is sorted and de-duplicated, while `.sources` preserves the order the envelope carried and can repeat a feed. Don't assume parity between the two. The raw envelope stays reachable at `exc.request.errors` (`{'code': 'KNOWN_GOOD', 'known_good': True, 'sources': [...]}`). The artifact's metadata — the flagging feeds plus any scan data already collected — remains readable through the search / instance endpoints; only the bytes are withheld, and the instance's `KNOWN_GOOD` state/status is the signal for that (there is no separate "withheld" field). +`KnownGoodWithheldException` is the 404 raised when a download is refused because the artifact is a known-good binary — the platform never stores or serves those bytes. It **subclasses `NotFoundException`** precisely so invariant 3 holds for existing consumers: code that already does `except NotFoundException:` keeps catching the refusal with no change, and only callers that want to distinguish "withheld by design" from a plain miss catch the subclass. It adds one attribute, `.sources` — the known-good feeds that flagged the hash (e.g. `['nsrl']`), `[]` when the server named none. The contract is only this: **always a list of strings**, whatever the envelope carried, so `for feed in exc.sources` needs no shape check. Which wire shapes are coerced, and which are dropped and logged, is `exceptions._normalise_sources`' business rather than a promise to consumers — the server sends a list of strings today. Note it is **not** normalised the same way as `ArtifactInstance.known_good_sources`, which is the same concept reached from the instance response: that one is sorted and de-duplicated, while `.sources` preserves the order the envelope carried and can repeat a feed. Don't assume parity between the two. The raw envelope stays reachable at `exc.request.errors` (`{'code': 'KNOWN_GOOD', 'known_good': True, 'sources': [...]}`). The artifact's metadata — the flagging feeds plus any scan data already collected — remains readable through the search / instance endpoints; only the bytes are withheld, and the instance's `KNOWN_GOOD` state/status is the signal for the typed refusal (there is no separate "withheld" field; a `NOT_STORED` instance has no bytes either, but 404s plainly — see below). **What "known-good" means on the server, as of artifact-index's two-predicate model** (its `specs/05`): the refusal fires on the server's *current understanding* — a catalogue entry exists for the sha256 **and** that entry's extension passes an executable allow-list — evaluated live on every request. Two consequences worth knowing as a consumer: the same download can start working again with no action on your part (the entry is deleted, or the policy narrows), and `ArtifactInstance.state` can report the new value **`NOT_STORED`** — a submission the server declined as known-good at the time whose hash is no longer currently known-good, so nothing was ever stored for it and a fresh submit of the same file works. `state` is a plain string here; the SDK does not enumerate it, so a new member needs no SDK release. diff --git a/src/polyswarm_api/aio/api.py b/src/polyswarm_api/aio/api.py index 894c0c14..465cc1ce 100644 --- a/src/polyswarm_api/aio/api.py +++ b/src/polyswarm_api/aio/api.py @@ -1640,9 +1640,11 @@ async def download_id(self, out_dir, instance_id): async def download_sandbox_artifact(self, out_dir, sandbox_task_id, instance_id): """Download a sandbox-produced artifact (e.g. PCAP, dropped file) into ``out_dir``. - Sandbox **evidence** (report / raw_report / screenshot / recording / pcap / - memory_dump) is exempt from the known-good policy server-side, so in practice only a - **dropped file** raises below — its sha256 is a real file's digest. + The known-good gate applies to **every** sandbox artifact by its own sha256 — + dropped file, screenshot, report, … — with no sample-vs-evidence carve-out + server-side, so any member whose digest is catalogued (and eligible) raises below. + In practice that is almost always a dropped file; evidence collides only on byte + identity, and the empty-file digest is refused at the catalogue boundary instead. :raises KnownGoodWithheldException: the sha256 is catalogued as a known-good binary, so its bytes are withheld by design. A ``NotFoundException`` subclass, so existing diff --git a/src/polyswarm_api/api.py b/src/polyswarm_api/api.py index 13920927..b7ca0ba0 100644 --- a/src/polyswarm_api/api.py +++ b/src/polyswarm_api/api.py @@ -1984,9 +1984,11 @@ def download_id(self, out_dir, instance_id): def download_sandbox_artifact(self, out_dir, sandbox_task_id, instance_id): """Download a sandbox-produced artifact (e.g. PCAP, dropped file) into ``out_dir``. - Sandbox **evidence** (report / raw_report / screenshot / recording / pcap / - memory_dump) is exempt from the known-good policy server-side, so in practice only a - **dropped file** raises below — its sha256 is a real file's digest. + The known-good gate applies to **every** sandbox artifact by its own sha256 — + dropped file, screenshot, report, … — with no sample-vs-evidence carve-out + server-side, so any member whose digest is catalogued (and eligible) raises below. + In practice that is almost always a dropped file; evidence collides only on byte + identity, and the empty-file digest is refused at the catalogue boundary instead. :raises KnownGoodWithheldException: the sha256 is catalogued as a known-good binary, so its bytes are withheld by design. A ``NotFoundException`` subclass, so existing diff --git a/src/polyswarm_api/resources.py b/src/polyswarm_api/resources.py index 94d4291c..fa5f1c1d 100644 --- a/src/polyswarm_api/resources.py +++ b/src/polyswarm_api/resources.py @@ -280,10 +280,9 @@ def __init__(self, content, api=None): # (artifact-index specs/05): 'KNOWN_GOOD' while the file is currently known-good, # 'NOT_STORED' for a submission it declined as known-good whose hash is no longer # currently known-good (nothing was ever stored; a fresh submit works), otherwise - # the instance's own persisted state. - # - # additive alongside the numeric bounty_state. Optional — older servers - # omit it, so .get() yields None (no behaviour change). + # the instance's own persisted state. Additive alongside the numeric + # bounty_state, and optional — older servers omit it, so .get() yields None + # (no behaviour change). self.state = content.get('state') # ArtifactInstance fields From 67520c854481ce1fa704b675946e9910a20715e7 Mon Sep 17 00:00:00 2001 From: Samuel Date: Thu, 30 Jul 2026 21:49:10 -0300 Subject: [PATCH 20/20] docs: state the right reason nothing is written on download_to_handle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shared :raises block said 'the status is checked before the file is opened' — copy-pasted from the folder-destination methods. download_to_handle never opens anything: the handle is the caller's and arrives open. The guarantee holds (the status check precedes any write); only the justification was wrong, on the one method where a reader most wants it restated. --- src/polyswarm_api/aio/api.py | 3 ++- src/polyswarm_api/api.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/polyswarm_api/aio/api.py b/src/polyswarm_api/aio/api.py index 465cc1ce..18ab4921 100644 --- a/src/polyswarm_api/aio/api.py +++ b/src/polyswarm_api/aio/api.py @@ -944,7 +944,8 @@ async def download_to_handle(self, hash_, fh, hash_type=None): ``except NotFoundException`` handling still catches it; catch it specifically to tell a deliberate refusal apart from a missing artifact, and read ``.sources`` for the feeds that flagged it. Nothing is written to the destination — - the status is checked before the file is opened. + the status is checked before any write (the handle is the caller's and arrives + already open, so "before the file is opened" would be the wrong reason here). """ logger.info('Downloading %s into handle', hash_) hash_ = resources.Hash.from_hashable(hash_, hash_type=hash_type) diff --git a/src/polyswarm_api/api.py b/src/polyswarm_api/api.py index b7ca0ba0..53400201 100644 --- a/src/polyswarm_api/api.py +++ b/src/polyswarm_api/api.py @@ -1144,7 +1144,8 @@ def download_to_handle(self, hash_, fh, hash_type=None): ``except NotFoundException`` handling still catches it; catch it specifically to tell a deliberate refusal apart from a missing artifact, and read ``.sources`` for the feeds that flagged it. Nothing is written to the destination — - the status is checked before the file is opened. + the status is checked before any write (the handle is the caller's and arrives + already open, so "before the file is opened" would be the wrong reason here). """ logger.info("Downloading %s into handle", hash_) hash_ = resources.Hash.from_hashable(hash_, hash_type=hash_type)