diff --git a/pyproject.toml b/pyproject.toml index 4dd93bb4..e1294158 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "polyswarm_api" -version = "4.2.0" +version = "4.3.0" description = "Client library to simplify interacting with the PolySwarm consumer API" readme = "README.md" requires-python = ">=3.10,<4" @@ -55,7 +55,7 @@ package-dir = { "" = "src" } where = ["src"] [tool.bumpversion] -current_version = "4.2.0" +current_version = "4.3.0" commit = true tag = false sign_tags = true @@ -76,11 +76,17 @@ search = "__version__ = '{current_version}'" replace = "__version__ = '{new_version}'" [tool.pytest.ini_options] -log_cli = true -# Default live-log level. INFO keeps the useful app request/response lines while -# dropping the DEBUG firehose (VCR per-request match trace, parse traces) that -# otherwise bloats CI logs ~1000x. Override per-run with the TESTS_LOG_LEVEL env -# var (e.g. TESTS_LOG_LEVEL=DEBUG to log everything); see test/conftest.py. +# Live logging OFF by default. When on, pytest streams log records as the run goes +# AND forces every test onto its own nodeid line (verbose-style) — ~130 lines of +# pure noise on a green xdist run. With it off the run prints dots + an end +# summary, and a FAILING test still shows its captured logs (at log_level, below) +# plus the traceback. Opt back into live streaming with TESTS_LOG_CLI=1 (or an +# explicit --log-cli-level) when debugging a live run; see test/conftest.py. +log_cli = false +# Level used for live logging when it's opted into (TESTS_LOG_CLI=1). INFO keeps +# the useful app request/response lines while dropping the DEBUG firehose (VCR +# per-request match trace, parse traces) that otherwise bloats logs ~1000x. +# TESTS_LOG_LEVEL overrides both this and the captured-log level (conftest.py). log_cli_level = "INFO" log_format = "%(asctime)s %(levelname)-2s [%(name)s:%(filename)s:%(lineno)d:%(funcName)1s()] %(message)s" python_files = ["test_*.py", "*_test.py"] @@ -95,7 +101,6 @@ filterwarnings = [ asyncio_mode = "auto" addopts = [ "--continue-on-collection-errors", - "-v", "-s", - "-rxXs", + "-ra", ] 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..8005ba7c 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'` 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,11 +267,11 @@ 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'`: 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). -- 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-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 a9817f9f..181bbd81 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'`), 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,19 @@ 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 **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): -- `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 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`. @@ -381,9 +391,10 @@ 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, 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. +- `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/specs/03-endpoints.md b/specs/03-endpoints.md index d187837e..eea14fd5 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` 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` | | @@ -106,11 +106,27 @@ 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_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_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 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 +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. + +`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 diff --git a/specs/04-testing.md b/specs/04-testing.md index e875fc95..b81e5855 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. 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,6 +19,8 @@ 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). - `test/async_client_test.py` — async, VCR-backed integration tests (not yet on the parametrised harness — follow-up work). @@ -40,6 +42,12 @@ 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 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 - **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. @@ -47,9 +55,10 @@ The pure-unit tier exists because the 4.0 redesign made it possible: resource bu ## The parametrised `ClientTestCase` harness -Implemented in `test/metadata_field_properties_test.py`. 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 from polyswarm_api.api import PolyswarmAPI from polyswarm_api.aio import PolySwarmAsyncAPI @@ -82,7 +91,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): @@ -226,6 +236,17 @@ The speedup comes from collapsing *sequential* waits. Serial wall-clock is domin `-n` is deliberately kept out of `[tool.pytest.ini_options].addopts` in `pyproject.toml` and passed on the runner command line instead, so local and IDE runs stay serial (xdist scrambles `-s` live-log ordering and complicates breakpoints). Only the live CI image opts into parallelism. `pytest-xdist` and `pytest-timeout` are in the `[tests]` extra. +### Log output: dots by default, live logs opt-in + +The suite prints **one progress char per test** (xdist dots) plus an end-of-run summary — not a line per test. Two things must be off for that, and both were easy to get wrong: + +- **`-v` is not in `addopts`** — verbose mode prints every test's nodeid on its own line. +- **Live logging (`log_cli`) is off by default** — `log_cli = false` in `pyproject.toml`, and `test/conftest.py` does **not** set the `log_cli_level` *option*. This is the subtle one: pytest enables live logging when `log_cli` is true **or** the `log_cli_level` option is set, and live logging *also* forces every test onto its own nodeid line (verbose-style) **regardless of `-v`**. So live logging on a green 8-way run added ~130 noise lines to the harness log even with `-v` removed — the option must be left unset, not just `-v` dropped. + +A **failing** test still prints its full traceback and its **captured** logs (the "Captured log call" section, rendered at `log_level` = `TESTS_LOG_LEVEL`, default `INFO`), plus the `-ra` short-summary recap. So the run is quiet on green and detailed on failure. + +Opt back into live streaming when debugging a live-stack run with **`TESTS_LOG_CLI=1`** (or an explicit `--log-cli-level=…` on the CLI). `TESTS_LOG_LEVEL=DEBUG` raises both the captured and the live level and unpins the noisy replay/transport libraries (`vcr`, `httpx`, `httpcore`, `asyncio`). See `test/conftest.py`. + ### Why `--timeout-method=thread` and `--timeout=600` The timeout is a backstop for the live run: VCR-off keeps real poll/sleep pacing, so a non-terminating test would otherwise hang to the CI job limit. `--timeout-method=thread` is the only method that fires reliably here — the signal method can't interrupt a hang inside the asyncio event loop, whereas a watchdog thread fires regardless of loop state, dumps every thread's stack (showing where execution is stuck), then terminates. **Caveat:** the thread method ends the *whole session* on a per-test timeout rather than failing one test, so a hang fails the job with a stack dump instead of running to the job limit. The per-test budget was raised 300 → 600s to give headroom under 8× pipeline contention. @@ -280,7 +301,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)? → 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 d06ed891..2b40d018 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,10 @@ 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 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. + 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. @@ -388,6 +393,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/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/__init__.py b/src/polyswarm_api/__init__.py index 775cb442..64f6de30 100644 --- a/src/polyswarm_api/__init__.py +++ b/src/polyswarm_api/__init__.py @@ -1,5 +1,5 @@ # https://www.python.org/dev/peps/pep-0008/#module-level-dunder-names -__version__ = '4.2.0' +__version__ = '4.3.0' __release_url__ = 'https://api.github.com/repos/polyswarm/polyswarm-api/releases/latest' from . import api diff --git a/src/polyswarm_api/aio/api.py b/src/polyswarm_api/aio/api.py index 0c9d69e9..18ab4921 100644 --- a/src/polyswarm_api/aio/api.py +++ b/src/polyswarm_api/aio/api.py @@ -939,6 +939,13 @@ 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 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) @@ -1599,6 +1606,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 +1622,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 +1639,21 @@ 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``. + + 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 + ``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( @@ -1642,8 +1677,29 @@ 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. + :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. + + 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 fde4ea59..53400201 100644 --- a/src/polyswarm_api/api.py +++ b/src/polyswarm_api/api.py @@ -1139,6 +1139,13 @@ 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 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) @@ -1941,6 +1948,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 +1966,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 +1983,21 @@ 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``. + + 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 + ``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( @@ -1989,8 +2024,29 @@ 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. + :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. + + 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/core.py b/src/polyswarm_api/core.py index 7068bbda..f6e43f44 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`` 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,17 @@ 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. + # 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( + request, request._result, sources=errors.get('sources'), + ) raise exceptions.NotFoundException(request, request._result) elif request.status_code == 422: raise exceptions.FailedInstanceException(request, request._result) @@ -383,7 +396,23 @@ 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. + # 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()) + 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/src/polyswarm_api/exceptions.py b/src/polyswarm_api/exceptions.py index 171d172d..9a2ebd6b 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 @@ -32,6 +37,51 @@ 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, 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] + if isinstance(sources, list): + 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']) + else: + logger.warning('Dropping unrecognised known-good sources entry: %r', source) + return names + if sources is not None: + logger.warning('Dropping unrecognised known-good sources payload: %r', sources) + return [] + + +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']``); 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): pass diff --git a/src/polyswarm_api/resources.py b/src/polyswarm_api/resources.py index 16f4a8db..fa5f1c1d 100644 --- a/src/polyswarm_api/resources.py +++ b/src/polyswarm_api/resources.py @@ -275,9 +275,14 @@ 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'), - # additive alongside the numeric bounty_state. Optional — older servers - # omit it, so .get() yields None (no behaviour change). + # 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, and optional — older servers omit it, so .get() yields None + # (no behaviour change). self.state = content.get('state') # ArtifactInstance fields diff --git a/test/_client_harness.py b/test/_client_harness.py new file mode 100644 index 00000000..18be58fe --- /dev/null +++ b/test/_client_harness.py @@ -0,0 +1,140 @@ +"""The parametrised ``ClientTestCase`` harness for the respx-mocked tier. + +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 +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: + """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 a shared + harness any respx module can import. + """ + return str(self._router.calls[-1].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 266afa32..95602710 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -408,6 +408,30 @@ 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) + # A refused download leaves nothing behind — see the sync twin. + assert os.listdir(out_dir) == [] + assert ei.value.sources == ['nsrl'] + # 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). + # 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') assert extended.id == created.id @@ -415,10 +439,52 @@ 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): + # 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) + + @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 + + # 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): + 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 # ── Sandbox ─────────────────────────────────────────────────────────────── @@ -437,7 +503,7 @@ async def test_async_sandboxtask_submit(self, uid): instance, _ = await submit_and_scan(api, uid, wait=False) task = await _dispatch_sandbox(api, instance.id, 'cape', 'win-10-build-19041', True) assert task.json['config']['network_enabled'] is True - task = await _dispatch_sandbox(api, instance.id, 'triage', 'win10-build-15063', False) + task = await _dispatch_sandbox(api, instance.id, 'triage', 'windows11-21h2-x64', False) assert task.sandbox == 'triage' assert task.json['config']['network_enabled'] is False @@ -446,7 +512,7 @@ async def test_async_sandboxtask_latest(self, uid): async with self._api() as api: instance, sha = await submit_and_scan(api, uid, wait=False) cape = await _dispatch_sandbox(api, instance.id, 'cape', 'win-10-build-19041', True) - triage = await _dispatch_sandbox(api, instance.id, 'triage', 'win10-build-15063', False) + triage = await _dispatch_sandbox(api, instance.id, 'triage', 'windows11-21h2-x64', False) # No cape/triage VMs in e2e — drive each task to SUCCEEDED by replaying # the sandbox worker's HTTP calls. Completing a task creates its @@ -472,7 +538,7 @@ async def test_async_sandboxtask_list(self, uid): # Dispatch cape + triage concurrently (distinct sandbox slugs, independent). await run_concurrently_async([ _dispatch_sandbox(api, instance.id, 'cape', 'win-10-build-19041', True), - _dispatch_sandbox(api, instance.id, 'triage', 'win10-build-15063', False), + _dispatch_sandbox(api, instance.id, 'triage', 'windows11-21h2-x64', False), ]) # Poll until the SandboxTaskSearchHash index sees both tasks. @@ -506,7 +572,7 @@ async def test_async_sample(self, uid): async with self._api() as api: instance, sha = await submit_and_scan(api, uid, wait=False) cape = await _dispatch_sandbox(api, instance.id, 'cape', 'win-10-build-19041', True) - triage = await _dispatch_sandbox(api, instance.id, 'triage', 'win10-build-15063', False) + triage = await _dispatch_sandbox(api, instance.id, 'triage', 'windows11-21h2-x64', False) await _complete_sandbox_task(cape.id, 'cape') await _complete_sandbox_task(triage.id, 'triage') @@ -1049,25 +1115,6 @@ def write(self, b): assert all(len(w) <= 4 for w in writes), writes -@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.""" - 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: - 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/client_scan_test.py b/test/client_scan_test.py index 44d7dc0f..46b22804 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -774,6 +774,42 @@ 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 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) + # 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'] + # 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 + # 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') assert extended.id == created.id @@ -781,10 +817,68 @@ 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): + # 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_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 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): + 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 @vcr.use_cassette() def test_sandbox_providers(self): @@ -802,7 +896,7 @@ def test_sandboxtask_submit(self): task = _dispatch_sandbox(v3api, instance.id, 'cape', 'win-10-build-19041', True) assert task.json['config']['network_enabled'] is True - task = _dispatch_sandbox(v3api, instance.id, 'triage', 'win10-build-15063', False) + task = _dispatch_sandbox(v3api, instance.id, 'triage', 'windows11-21h2-x64', False) assert task.sandbox == 'triage' assert task.json['config']['network_enabled'] is False @@ -815,7 +909,7 @@ def test_sandboxtask_latest(self): # is what sandbox_task_latest reads. instance, sha256 = submit_and_scan(v3api, self._testMethodName, wait=False) cape = _dispatch_sandbox(v3api, instance.id, 'cape', 'win-10-build-19041', True) - triage = _dispatch_sandbox(v3api, instance.id, 'triage', 'win10-build-15063', False) + triage = _dispatch_sandbox(v3api, instance.id, 'triage', 'windows11-21h2-x64', False) # _complete_sandbox_task waits (event-driven) for the sandbox service to # register each just-dispatched task before driving it to SUCCEEDED. @@ -839,7 +933,7 @@ def test_sandboxtask_list(self): # Dispatch cape + triage concurrently (distinct sandbox slugs, independent). run_concurrently([ lambda: _dispatch_sandbox(v3api, instance.id, 'cape', 'win-10-build-19041', True), - lambda: _dispatch_sandbox(v3api, instance.id, 'triage', 'win10-build-15063', False), + lambda: _dispatch_sandbox(v3api, instance.id, 'triage', 'windows11-21h2-x64', False), ]) # Poll until the SandboxTaskSearchHash index sees both tasks. @@ -876,7 +970,7 @@ def test_sample(self): uid = self._testMethodName instance, sha = submit_and_scan(api, uid, wait=False) cape = _dispatch_sandbox(api, instance.id, 'cape', 'win-10-build-19041', True) - triage = _dispatch_sandbox(api, instance.id, 'triage', 'win10-build-15063', False) + triage = _dispatch_sandbox(api, instance.id, 'triage', 'windows11-21h2-x64', False) _complete_sandbox_task(cape.id, 'cape') _complete_sandbox_task(triage.id, 'triage') diff --git a/test/conftest.py b/test/conftest.py index 89fba6cf..c9eecb40 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -13,6 +13,13 @@ # to log everything (including the replay/transport libraries below). _TESTS_LOG_LEVEL = os.getenv("TESTS_LOG_LEVEL", "INFO").upper() +# Live-log streaming (log_cli). OFF by default: when on, pytest not only streams log +# records but forces every test onto its own nodeid line (verbose-style) — ~130 +# lines of noise on a green xdist run. Off, the run prints dots + an end summary, +# and a failing test still shows its captured logs at _TESTS_LOG_LEVEL plus the +# traceback. Set TESTS_LOG_CLI=1 to stream live when debugging a live-stack run. +_TESTS_LOG_CLI = os.getenv("TESTS_LOG_CLI", "").lower() in ("1", "true", "yes", "on") + # Replay/transport libraries are extremely chatty at DEBUG — vcr.matchers alone # logs a full comparison for every recorded request on every call (~24% of the # old log) and vcr.cassette/vcr.request dump entire response bodies. Pin them to @@ -45,12 +52,18 @@ def uid(request): def pytest_configure(config): - # Honor an explicit --log-cli-level / --log-level on the CLI; otherwise drive - # both the live and captured log level from TESTS_LOG_LEVEL. - if config.option.log_cli_level is None: - config.option.log_cli_level = _TESTS_LOG_LEVEL + # Captured-log level — what pytest records and then prints under a FAILING test + # ("Captured log call"). Drive it from TESTS_LOG_LEVEL so failures carry the + # app's request/response context while a green run stays quiet. Honors an + # explicit --log-level on the CLI. if config.option.log_level is None: config.option.log_level = _TESTS_LOG_LEVEL + # Live logging is enabled iff log_cli (ini, now false) is true OR this option is + # set — so setting it here is what turns on the verbose per-test streaming. + # Leave it None by default (dots + summary); opt in with TESTS_LOG_CLI=1. An + # explicit --log-cli-level on the CLI is honored (already non-None -> untouched). + if _TESTS_LOG_CLI and config.option.log_cli_level is None: + config.option.log_cli_level = _TESTS_LOG_LEVEL noisy_level = "DEBUG" if _TESTS_LOG_LEVEL == "DEBUG" else "WARNING" for name in _NOISY_LIBS: logging.getLogger(name).setLevel(noisy_level) @@ -109,6 +122,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 ) diff --git a/test/core_test.py b/test/core_test.py index 3371da90..f8fb076e 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,87 @@ 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', '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'}, + }), + req, + ) + assert ei.value.sources == [] + + def test_404_known_good_sources_normalised_to_feed_names(self): + # ``.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 + ([{'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 + ] + 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 + # 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) @@ -291,6 +372,60 @@ 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_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. diff --git a/test/exists_probe_mapping_test.py b/test/exists_probe_mapping_test.py new file mode 100644 index 00000000..931f6c22 --- /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_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 + # 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 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 ──────────────────────────────────────────────────────────── 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..65dcea9e --- /dev/null +++ b/test/vcr/test_async_hash_existence_probe_against_the_real_server.vcr @@ -0,0 +1,766 @@ +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.14.4) + 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: + - Thu, 30 Jul 2026 01:18: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.14.4) + 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: + - Thu, 30 Jul 2026 01:18: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.14.4) + method: POST + uri: http://artifact-index-e2e:9696/v3/instance + response: + body: + 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: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '1038' + Content-Type: + - application/json + Date: + - Thu, 30 Jul 2026 01:18: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_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.14.4) + method: PUT + 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: '' + headers: + Accept-Ranges: + - bytes + Content-Length: + - '0' + Date: + - Thu, 30 Jul 2026 01:18:57 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: + - 18C6EC804529061E + X-Content-Type-Options: + - nosniff + X-Ratelimit-Limit: + - '13392' + X-Ratelimit-Remaining: + - '13392' + X-Xss-Protection: + - 1; mode=block + x-amz-expiration: + - expiry-date="Sat, 01 Aug 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.14.4) + method: PUT + uri: http://artifact-index-e2e:9696/v3/instance?id=851018633677295 + response: + body: + 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: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '1038' + Content-Type: + - application/json + Date: + - Thu, 30 Jul 2026 01:18: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.14.4) + method: GET + uri: http://artifact-index-e2e:9696/v3/consumer/submission/gamma/851018633677295 + response: + body: + 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: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '1038' + Content-Type: + - application/json + Date: + - Thu, 30 Jul 2026 01:18: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.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":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/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: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2942' + Content-Type: + - application/json + Date: + - Thu, 30 Jul 2026 01:18: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.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":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/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: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2942' + Content-Type: + - application/json + Date: + - Thu, 30 Jul 2026 01:19: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.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":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/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: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3227' + Content-Type: + - application/json + Date: + - Thu, 30 Jul 2026 01:19: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.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":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/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: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3227' + Content-Type: + - application/json + Date: + - Thu, 30 Jul 2026 01:19: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.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":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/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: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3227' + Content-Type: + - application/json + Date: + - Thu, 30 Jul 2026 01:19: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.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":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/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: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3227' + Content-Type: + - application/json + Date: + - Thu, 30 Jul 2026 01:19: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.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":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: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3270' + Content-Type: + - application/json + Date: + - Thu, 30 Jul 2026 01:19: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.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":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: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '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: + - Thu, 30 Jul 2026 01:19: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.14.4) + 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: + - Thu, 30 Jul 2026 01:19: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.14.4) + 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: + - Thu, 30 Jul 2026 01:19:07 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +version: 1 diff --git a/test/vcr/test_async_known_good_lifecycle.vcr b/test/vcr/test_async_known_good_lifecycle.vcr index f9f28cf2..9fcb476d 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":"99771098961774556","created":"2026-07-29T23:04:40.642152+00:00","id":"73600979008822536","sha256":"d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b","sources":["nsrl"]},"status":"OK"} ' headers: @@ -33,11 +33,11 @@ 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 23:04:40 GMT Server: - gunicorn X-Billing-ID: @@ -45,6 +45,124 @@ 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: 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 23:04:40 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=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:04:40 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=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: @@ -63,12 +181,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":"99771098961774556","created":"2026-07-29T23:04:40.642152+00:00","id":"73600979008822536","sha256":"d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b","sources":["commercial","nsrl"]},"status":"OK"} ' headers: @@ -79,11 +197,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 23:04:40 GMT Server: - gunicorn X-Billing-ID: @@ -105,12 +223,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":"99771098961774556","created":"2026-07-29T23:04:40.642152+00:00","id":"73600979008822536","sha256":"d474e08e835f8e538f8aeb6944b4bc73a31db2be96625b0bdfe0ab8bf239226b","sources":["commercial","nsrl"]},"status":"OK"} ' headers: @@ -121,11 +239,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 23:04:40 GMT Server: - gunicorn X-Billing-ID: @@ -147,7 +265,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 23:04:40 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 +327,7 @@ interactions: Content-Type: - application/json Date: - - Mon, 29 Jun 2026 18:29:54 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn X-Billing-ID: @@ -189,7 +349,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 +369,7 @@ interactions: Content-Type: - application/json Date: - - Mon, 29 Jun 2026 18:29:54 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn status: 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..e67fba98 --- /dev/null +++ b/test/vcr/test_hash_existence_probe_against_the_real_server.vcr @@ -0,0 +1,672 @@ +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.14.4) + 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: + - Thu, 30 Jul 2026 01:18:50 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.14.4) + 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: + - Thu, 30 Jul 2026 01:18:50 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.14.4) + method: POST + uri: http://artifact-index-e2e:9696/v3/instance + response: + body: + 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: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '1044' + Content-Type: + - application/json + Date: + - Thu, 30 Jul 2026 01:18:50 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.14.4) + method: PUT + 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: '' + headers: + Accept-Ranges: + - bytes + Content-Length: + - '0' + Date: + - Thu, 30 Jul 2026 01:18:50 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: + - 18C6EC7E8F49E06B + X-Content-Type-Options: + - nosniff + X-Ratelimit-Limit: + - '13392' + X-Ratelimit-Remaining: + - '13392' + X-Xss-Protection: + - 1; mode=block + x-amz-expiration: + - expiry-date="Sat, 01 Aug 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.14.4) + method: PUT + uri: http://artifact-index-e2e:9696/v3/instance?id=88492900747453910 + response: + body: + 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: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '1044' + Content-Type: + - application/json + Date: + - Thu, 30 Jul 2026 01:18:50 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/88492900747453910 + response: + body: + 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: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '1044' + Content-Type: + - application/json + Date: + - Thu, 30 Jul 2026 01:18:50 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/88492900747453910 + response: + body: + 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/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: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2944' + Content-Type: + - application/json + Date: + - Thu, 30 Jul 2026 01:18:51 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/88492900747453910 + response: + body: + 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/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: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '2944' + Content-Type: + - application/json + Date: + - Thu, 30 Jul 2026 01:18:52 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/88492900747453910 + response: + body: + 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/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: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3229' + Content-Type: + - application/json + Date: + - Thu, 30 Jul 2026 01:18:53 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - 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/88492900747453910 + response: + body: + 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/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: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3229' + Content-Type: + - application/json + Date: + - Thu, 30 Jul 2026 01:18:54 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/88492900747453910 + response: + body: + 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/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: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3229' + Content-Type: + - application/json + Date: + - Thu, 30 Jul 2026 01:18:55 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/88492900747453910 + response: + body: + 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/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: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3229' + Content-Type: + - application/json + Date: + - Thu, 30 Jul 2026 01:18:56 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/88492900747453910 + response: + body: + 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/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: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '3531' + Content-Type: + - application/json + Date: + - Thu, 30 Jul 2026 01:18: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.14.4) + 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: + - Thu, 30 Jul 2026 01:18: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.14.4) + 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: + - Thu, 30 Jul 2026 01:18:57 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +version: 1 diff --git a/test/vcr/test_known_good_lifecycle.vcr b/test/vcr/test_known_good_lifecycle.vcr index e1e93780..dea39d68 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":"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: - - Mon, 29 Jun 2026 18:30:01 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn X-Billing-ID: @@ -45,6 +45,124 @@ 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: 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 23:04:40 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=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:04:40 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=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: @@ -63,12 +181,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":"38756881504693724","created":"2026-07-29T23:04:40.151450+00:00","id":"31218224787620566","sha256":"9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df","sources":["commercial","nsrl"]},"status":"OK"} ' headers: @@ -83,7 +201,7 @@ interactions: Content-Type: - application/json Date: - - Mon, 29 Jun 2026 18:30:01 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn X-Billing-ID: @@ -105,12 +223,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":"38756881504693724","created":"2026-07-29T23:04:40.151450+00:00","id":"31218224787620566","sha256":"9c0258ed4cc98056773bcf0c57fe4bc79618802357f0703c503b4cb172a263df","sources":["commercial","nsrl"]},"status":"OK"} ' headers: @@ -125,7 +243,7 @@ interactions: Content-Type: - application/json Date: - - Mon, 29 Jun 2026 18:30:01 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn X-Billing-ID: @@ -147,7 +265,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 23:04:40 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 +327,7 @@ interactions: Content-Type: - application/json Date: - - Mon, 29 Jun 2026 18:30:01 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn X-Billing-ID: @@ -189,7 +349,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 +369,7 @@ interactions: Content-Type: - application/json Date: - - Mon, 29 Jun 2026 18:30:01 GMT + - Wed, 29 Jul 2026 23:04:40 GMT Server: - gunicorn status: