From cdb2a77e38df29241b58c9303bad7002ffd7e3ce Mon Sep 17 00:00:00 2001 From: Kyle Buchmiller Date: Fri, 21 Aug 2026 14:22:23 -0700 Subject: [PATCH 01/15] feat(hunts): expose matched strings on hunt-result resources A hunt result says which rule matched and its tags, never why. The server now returns the yara strings behind a hit; parse them onto LiveHuntResult and HistoricalHuntResult, and so onto their list subclasses. Read with .get() rather than a subscript. The key is additive, so a server predating it omits it entirely and a subscript would raise on every result. Three states reach callers and they are not interchangeable: None (not reported -- an older server, removed evidence, or a list endpoint, which omits it rather than fetch a blob per row), [] (the rule matched with no byte evidence to show), and a populated list (the evidence, as a lower bound rather than a match count). --- src/polyswarm_api/resources.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/polyswarm_api/resources.py b/src/polyswarm_api/resources.py index fa5f1c1d..25775cdf 100644 --- a/src/polyswarm_api/resources.py +++ b/src/polyswarm_api/resources.py @@ -766,6 +766,13 @@ def __init__(self, content, api=None): self.sha1 = content.get('sha1') self.rule_name = content['rule_name'] self.tags = content['tags'] + # `.get()`, not a subscript: the key is additive, so a server predating it omits + # it entirely and a subscript would raise on every result. Three distinct states + # reach here -- None (not reported: an older server, or a list endpoint, which + # omits the evidence rather than fetch a blob per row), [] (the rule matched but + # has no byte evidence to show), [...] (the evidence, as a lower bound). See + # specs/05-downstream-contract.md. + self.matched_strings = content.get('matched_strings') self.polyscore = content['polyscore'] self.malware_family = content['malware_family'] self.detections = content['detections'] @@ -814,6 +821,13 @@ def __init__(self, content, api=None): self.created = core.parse_isoformat(content['created']) self.rule_name = content['rule_name'] self.tags = content['tags'] + # `.get()`, not a subscript: the key is additive, so a server predating it omits + # it entirely and a subscript would raise on every result. Three distinct states + # reach here -- None (not reported: an older server, or a list endpoint, which + # omits the evidence rather than fetch a blob per row), [] (the rule matched but + # has no byte evidence to show), [...] (the evidence, as a lower bound). See + # specs/05-downstream-contract.md. + self.matched_strings = content.get('matched_strings') self.polyscore = content['polyscore'] self.malware_family = content['malware_family'] self.detections = content['detections'] From 08a31f4250bc3be76426ae1f6f698dc6b16b286b Mon Sep 17 00:00:00 2001 From: Kyle Buchmiller Date: Fri, 21 Aug 2026 14:22:24 -0700 Subject: [PATCH 02/15] test(hunts): pin the three matched-strings states Pure-unit: the resources parse a dict, so no HTTP and no stack is involved. Covers all four affected classes -- the list subclasses inherit __init__ and must behave identically -- and asserts the states stay distinguishable: an absent key and an explicit null both read as None, while an empty list stays an empty list rather than collapsing into it. Entries pass through verbatim, since only yara knows whether `data` is a hex dump or escaped text. --- test/hunt_matched_strings_test.py | 88 +++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 test/hunt_matched_strings_test.py diff --git a/test/hunt_matched_strings_test.py b/test/hunt_matched_strings_test.py new file mode 100644 index 00000000..c062ac52 --- /dev/null +++ b/test/hunt_matched_strings_test.py @@ -0,0 +1,88 @@ +"""Tests for the `matched_strings` attribute on hunt-result resources. + +Pure-unit: the resources parse a dict, so no HTTP and no stack are involved. The +point of these is the THREE-state contract (absent / empty / populated) documented +in specs/05-downstream-contract.md — the states are not interchangeable and a +consumer that collapses them loses the distinction between "we don't know" and +"the rule matched with no byte evidence". +""" + +import pytest + +from polyswarm_api.resources import ( + HistoricalHuntResult, + HistoricalHuntResultList, + LiveHuntResult, + LiveHuntResultList, +) + +_COMMON = { + "id": 1, + "instance_id": 2, + "created": "2022-05-26T19:41:33.797898", + "sha256": "f" * 64, + "rule_name": "dos_stub_message", + "tags": "{pe,stub}", + "polyscore": 0.5, + "malware_family": None, + "detections": {"malicious": 1, "total": 1}, +} + +_STRINGS = [ + {"offset": 78, "identifier": "$stub", "length": 14, + "data": "54 68 69 73 20 70 72 6F 67 72 61 6D 20 63", "truncated": False}, + {"offset": 0, "identifier": "$mz", "length": 512, + "data": "4D 5A 90 00 ...", "truncated": True}, +] + +# Both concrete classes plus their list-endpoint subclasses, which inherit __init__ +# and must therefore behave identically. +ALL_CLASSES = [ + LiveHuntResult, LiveHuntResultList, + HistoricalHuntResult, HistoricalHuntResultList, +] + + +def _content(cls, **extra): + content = dict(_COMMON, **extra) + if issubclass(cls, LiveHuntResult): + content["livescan_id"] = 3 + else: + content["historicalscan_id"] = 3 + return content + + +@pytest.mark.parametrize("cls", ALL_CLASSES) +def test_absent_key_parses_as_none(cls): + """A server predating the field omits the key; a subscript would raise here.""" + assert cls(_content(cls)).matched_strings is None + + +@pytest.mark.parametrize("cls", ALL_CLASSES) +def test_explicit_null_parses_as_none(cls): + """List endpoints send the key with a null value rather than omitting it.""" + assert cls(_content(cls, matched_strings=None)).matched_strings is None + + +@pytest.mark.parametrize("cls", ALL_CLASSES) +def test_empty_list_is_preserved_and_is_not_none(cls): + """`[]` means "matched, no byte evidence" — distinct from "not reported".""" + result = cls(_content(cls, matched_strings=[])) + assert result.matched_strings == [] + assert result.matched_strings is not None + + +@pytest.mark.parametrize("cls", ALL_CLASSES) +def test_populated_list_is_passed_through_verbatim(cls): + """The SDK does not reshape entries — `data` in particular stays as yara rendered it.""" + result = cls(_content(cls, matched_strings=_STRINGS)) + assert result.matched_strings == _STRINGS + assert result.matched_strings[0]["identifier"] == "$stub" + assert result.matched_strings[1]["truncated"] is True + + +@pytest.mark.parametrize("cls", ALL_CLASSES) +def test_raw_json_still_carries_the_key(cls): + """`.json` is part of the contract, so JSON-mode consumers see it without SDK work.""" + result = cls(_content(cls, matched_strings=_STRINGS)) + assert result.json["matched_strings"] == _STRINGS From 021d356d980be68398deaaee60f1253b03d91de5 Mon Sep 17 00:00:00 2001 From: Kyle Buchmiller Date: Fri, 21 Aug 2026 14:22:25 -0700 Subject: [PATCH 03/15] docs(specs): document matched_strings as a three-state attribute Records what each state means, the per-entry dict shape, and the two properties consumers get wrong: a populated list is a lower bound rather than a match count, and `truncated` means "there was more than this" rather than an exact size. Also states that the evidence rides on the detail routes only -- the paginated feed methods will always yield None -- so a caller looking for strings knows to fetch a single result. --- specs/05-downstream-contract.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/specs/05-downstream-contract.md b/specs/05-downstream-contract.md index 2b40d018..0cdbd477 100644 --- a/specs/05-downstream-contract.md +++ b/specs/05-downstream-contract.md @@ -270,6 +270,35 @@ What is **not** part of the contract: - The exact server JSON shape — that lives in the artifact-index repo's contract. - The order of fields in the JSON. +### `matched_strings` on hunt results — a three-state attribute + +`LiveHuntResult.matched_strings` / `HistoricalHuntResult.matched_strings` (and therefore +their `…List` subclasses) carry the yara strings behind a hunt hit. It is read with +`.get()` rather than a subscript, deliberately: the key is **additive**, so a server +older than it omits the key entirely and a subscript would raise on every result. + +Three values are possible and consumers **must not** collapse them: + +| Value | Meaning | +|---|---| +| `None` | Not reported. Either the server predates the field, the stored evidence was deleted, or this came from a **list** endpoint — those omit it rather than fetch a blob per row. "We don't know", *not* "there was nothing". | +| `[]` | The rule matched and there is no byte evidence to show — a rule with no strings section, one whose matching strings are all `private`, or one that matched on absence (`not $a`, `none of them`). | +| `[…]` | The evidence. A **lower bound**, not a match count: fast-scan reports only the first offset per string, `any of them` prints only the strings that hit, and `private` strings never appear. | + +Each entry is a dict: + +```python +{'offset': 78, 'identifier': '$stub', 'length': 14, 'data': '54 68 69 …', 'truncated': False} +``` + +- `data` is kept **exactly as yara rendered it** — a hex string comes back as byte pairs, a text string as ASCII with `\xNN` escapes. Only yara knows which applies, so it is not decoded back to bytes. +- `length` is the **stored** length, capped server-side. Past the cap the true length is unrecoverable. +- `truncated` means "there was more than this". It over-reports at exactly the cap, because nothing in the output distinguishes a match that ended there from one that was cut. + +**Evidence lives on the detail routes only.** `live_feed()` and `historical_results()` +page over list endpoints and will always yield `None` here; fetch a single result +(`live_result(id)` / `historical_result(id)`) to get the strings. + ## Pagination Generator endpoints return an iterable: From b29bc3d4f36d69cf64c82e025eaec8ba95ea7043 Mon Sep 17 00:00:00 2001 From: Kyle Buchmiller Date: Tue, 25 Aug 2026 12:17:27 -0700 Subject: [PATCH 04/15] docs(hunts): tighten the matched_strings comment Comment text only -- the parsed AST is identical before and after. Both copies edited identically, since the two resource classes carry the same note. Keeps what the comment exists for: why .get() and not a subscript, and that the three states are distinct. --- src/polyswarm_api/resources.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/polyswarm_api/resources.py b/src/polyswarm_api/resources.py index 25775cdf..da970a4c 100644 --- a/src/polyswarm_api/resources.py +++ b/src/polyswarm_api/resources.py @@ -767,10 +767,9 @@ def __init__(self, content, api=None): self.rule_name = content['rule_name'] self.tags = content['tags'] # `.get()`, not a subscript: the key is additive, so a server predating it omits - # it entirely and a subscript would raise on every result. Three distinct states - # reach here -- None (not reported: an older server, or a list endpoint, which - # omits the evidence rather than fetch a blob per row), [] (the rule matched but - # has no byte evidence to show), [...] (the evidence, as a lower bound). See + # it and a subscript would raise on every result. Three distinct states -- None + # (not reported: an older server, or a list endpoint), [] (matched, no byte + # evidence), [...] (the evidence, a lower bound). See # specs/05-downstream-contract.md. self.matched_strings = content.get('matched_strings') self.polyscore = content['polyscore'] @@ -822,10 +821,9 @@ def __init__(self, content, api=None): self.rule_name = content['rule_name'] self.tags = content['tags'] # `.get()`, not a subscript: the key is additive, so a server predating it omits - # it entirely and a subscript would raise on every result. Three distinct states - # reach here -- None (not reported: an older server, or a list endpoint, which - # omits the evidence rather than fetch a blob per row), [] (the rule matched but - # has no byte evidence to show), [...] (the evidence, as a lower bound). See + # it and a subscript would raise on every result. Three distinct states -- None + # (not reported: an older server, or a list endpoint), [] (matched, no byte + # evidence), [...] (the evidence, a lower bound). See # specs/05-downstream-contract.md. self.matched_strings = content.get('matched_strings') self.polyscore = content['polyscore'] From 74d0da9586510ba4ad157a7db868a199c1bd50f6 Mon Sep 17 00:00:00 2001 From: Kyle Buchmiller Date: Tue, 25 Aug 2026 15:54:19 -0700 Subject: [PATCH 05/15] test: pin matched_strings against the live server, not just dict.get The suite was pure-unit only, so nothing asserted that the detail route actually emits the key or that the list route omits it. Those tests exercise dict.get and would pass identically if the server never grew the field and the attribute were a permanent None -- which is the gap the e2e-first invariant exists to close: a fabricated response asserts what we think the server returns, a cassette asserts what it actually returned. test_live and test_async_live already poll the detail route on a rule that matched their own artifact, so the assertions cost two lines each and pin the list-vs-detail split that was previously stated only in prose. Cassettes re-recorded delete-driven against a live stack running the matching server and analyzer branches. Both now carry real evidence -- the per-test rule keys on the test's uid, so the recorded hit is `$u` at offset 69 with the uid's own length -- and null on the list rows. Verified the assertions are load-bearing: against the previous cassettes they fail with "detail route should carry the yara evidence / assert None". --- test/async_client_test.py | 7 + test/client_scan_test.py | 7 + test/vcr/test_async_live.vcr | 1030 +++++----------------------------- test/vcr/test_live.vcr | 1030 +++++----------------------------- 4 files changed, 288 insertions(+), 1786 deletions(-) diff --git a/test/async_client_test.py b/test/async_client_test.py index b633553e..14264b65 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -728,6 +728,13 @@ async def test_async_live(self, uid): result = await api.live_result(result_id) assert result.download_url + # The list/detail split, pinned against the real server rather than prose. + # The pure-unit tests exercise dict.get and would pass identically if the + # server never grew the field; only a cassette shows what it actually sent. + assert result.matched_strings, 'detail route should carry the yara evidence' + assert my_results[0].matched_strings is None, \ + 'list rows omit the evidence -- it is a per-row blob fetch' + await api.live_feed_delete([result_id]) with pytest.raises(exceptions.NotFoundException): await api.live_result(result_id) diff --git a/test/client_scan_test.py b/test/client_scan_test.py index 443f4a56..671b6786 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -501,6 +501,13 @@ def test_live(self): result = api.live_result(result_id) assert result.download_url + # The list/detail split, pinned against the real server rather than prose. + # The pure-unit tests exercise dict.get and would pass identically if the + # server never grew the field; only a cassette shows what it actually sent. + assert result.matched_strings, 'detail route should carry the yara evidence' + assert my_results[0].matched_strings is None, \ + 'list rows omit the evidence -- it is a per-row blob fetch' + api.live_feed_delete([result_id]) with pytest.raises(exceptions.NotFoundException): api.live_result(result_id) diff --git a/test/vcr/test_async_live.vcr b/test/vcr/test_async_live.vcr index 9becae4c..0f742c2b 100644 --- a/test/vcr/test_async_live.vcr +++ b/test/vcr/test_async_live.vcr @@ -18,12 +18,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: POST uri: http://ai:9696/v3/hunt/rule response: body: - string: '{"result":{"created":"2026-06-03T22:37:20.835178+00:00","deleted":false,"description":null,"id":"21319123963942093","livescan_created":null,"livescan_id":null,"modified":"2026-06-03T22:37:20.835178+00:00","name":"sdk-test_async_live","yara":"rule + string: '{"result":{"created":"2026-08-25T22:53:17.732230+00:00","deleted":false,"description":null,"id":"62754506679930173","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T22:53:17.732230+00:00","name":"sdk-test_async_live","yara":"rule sdk_test_async_live { strings: $u = \"test_async_live\" condition: $u }"},"status":"OK"} ' @@ -39,7 +39,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:37:20 GMT + - Tue, 25 Aug 2026 22:53:17 GMT Server: - gunicorn X-Billing-ID: @@ -48,7 +48,7 @@ interactions: code: 200 message: OK - request: - body: '{"rule_id":"21319123963942093"}' + body: '{"rule_id":"62754506679930173"}' headers: accept: - '*/*' @@ -65,12 +65,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: POST uri: http://ai:9696/v3/hunt/rule/live response: body: - string: '{"result":{"created":"2026-06-03T22:37:20.835178+00:00","deleted":false,"description":null,"id":"21319123963942093","livescan_created":"2026-06-03T22:37:20.890721+00:00","livescan_id":null,"modified":"2026-06-03T22:37:20.835178+00:00","name":"sdk-test_async_live","yara":"rule + string: '{"result":{"created":"2026-08-25T22:53:17.732230+00:00","deleted":false,"description":null,"id":"62754506679930173","livescan_created":"2026-08-25T22:53:17.826217+00:00","livescan_id":88995738053778536,"modified":"2026-08-25T22:53:17.824324+00:00","name":"sdk-test_async_live","yara":"rule sdk_test_async_live { strings: $u = \"test_async_live\" condition: $u }"},"status":"OK"} ' @@ -82,11 +82,11 @@ interactions: Connection: - keep-alive Content-Length: - - '366' + - '379' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:37:20 GMT + - Tue, 25 Aug 2026 22:53:17 GMT Server: - gunicorn X-Billing-ID: @@ -108,12 +108,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://ai:9696/v3/hunt/rule?id=21319123963942093&community=gamma + uri: http://ai:9696/v3/hunt/rule?id=62754506679930173&community=gamma response: body: - string: '{"result":{"created":"2026-06-03T22:37:20.835178+00:00","deleted":false,"description":null,"id":"21319123963942093","livescan_created":"2026-06-03T22:37:20.890721+00:00","livescan_id":88206418106491329,"modified":"2026-06-03T22:37:20.863779+00:00","name":"sdk-test_async_live","yara":"rule + string: '{"result":{"created":"2026-08-25T22:53:17.732230+00:00","deleted":false,"description":null,"id":"62754506679930173","livescan_created":"2026-08-25T22:53:17.826217+00:00","livescan_id":88995738053778536,"modified":"2026-08-25T22:53:17.824324+00:00","name":"sdk-test_async_live","yara":"rule sdk_test_async_live { strings: $u = \"test_async_live\" condition: $u }"},"status":"OK"} ' @@ -129,7 +129,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:37:21 GMT + - Tue, 25 Aug 2026 22:53:18 GMT Server: - gunicorn X-Billing-ID: @@ -155,12 +155,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: POST uri: http://ai:9696/v3/instance response: body: - string: '{"result":{"artifact_id":"94943400355321313","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-06-03T22:37:21.946455+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-06-03T22:37:21.946455+00:00","id":"94943400355321313","last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/94943400355321313","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/7e/34/70/7e34708c-caf6-4dd7-b5d2-9e1029e0dafc?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260603%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260603T223721Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=ef5a83090f1169483ad45b80d4b6bf9bf2a46ca939f4070b11ccc19254e68a8e","votes":[],"window_closed":false},"status":"OK"} + string: '{"result":{"artifact_id":"47587156523150020","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-08-25T22:53:18.933938+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-08-25T22:53:18.933938+00:00","id":"47587156523150020","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/47587156523150020","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/97/f8/13/97f8139b-5b64-42f8-ac4e-2cd965f3f74c?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260825%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260825T225318Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=79e8ef3ab561d8c40c9613747e83915d7b352c943fcd9a869d10d6f1cb801c65","votes":[],"window_closed":false},"status":"OK"} ' headers: @@ -171,11 +171,11 @@ interactions: Connection: - keep-alive Content-Length: - - '1008' + - '1044' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:37:21 GMT + - Tue, 25 Aug 2026 22:53:18 GMT Server: - gunicorn X-Billing-ID: @@ -199,9 +199,9 @@ interactions: host: - minio:9000 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: PUT - uri: http://minio:9000/artifact-index/instances/7e/34/70/7e34708c-caf6-4dd7-b5d2-9e1029e0dafc?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260603%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260603T223721Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=ef5a83090f1169483ad45b80d4b6bf9bf2a46ca939f4070b11ccc19254e68a8e + uri: http://minio:9000/artifact-index/instances/97/f8/13/97f8139b-5b64-42f8-ac4e-2cd965f3f74c?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260825%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260825T225318Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=79e8ef3ab561d8c40c9613747e83915d7b352c943fcd9a869d10d6f1cb801c65 response: body: string: '' @@ -211,7 +211,7 @@ interactions: Content-Length: - '0' Date: - - Wed, 03 Jun 2026 22:37:21 GMT + - Tue, 25 Aug 2026 22:53:18 GMT ETag: - '"6ac59cd96a9a3ee9d52f9ebc3ec22f70"' Server: @@ -224,17 +224,17 @@ interactions: X-Amz-Id-2: - dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8 X-Amz-Request-Id: - - 18B5B32EF4EBFFF4 + - 18CF2E38E284BA55 X-Content-Type-Options: - nosniff X-Ratelimit-Limit: - - '13624' + - '5063' X-Ratelimit-Remaining: - - '13624' + - '5063' X-Xss-Protection: - 1; mode=block x-amz-expiration: - - expiry-date="Fri, 05 Jun 2026 00:00:00 GMT", rule-id="expiration-artifact-index_0-instances" + - expiry-date="Thu, 27 Aug 2026 00:00:00 GMT", rule-id="expiration-artifact-index_0-instances" status: code: 200 message: OK @@ -256,12 +256,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: PUT - uri: http://ai:9696/v3/instance?id=94943400355321313 + uri: http://ai:9696/v3/instance?id=47587156523150020 response: body: - string: '{"result":{"artifact_id":"94943400355321313","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-06-03T22:37:21.946455+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-06-03T22:37:21.946455+00:00","id":"94943400355321313","last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/94943400355321313","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/7e/34/70/7e34708c-caf6-4dd7-b5d2-9e1029e0dafc?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260603%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260603T223721Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=ef5a83090f1169483ad45b80d4b6bf9bf2a46ca939f4070b11ccc19254e68a8e","votes":[],"window_closed":false},"status":"OK"} + string: '{"result":{"artifact_id":"47587156523150020","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-08-25T22:53:18.933938+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-08-25T22:53:18.933938+00:00","id":"47587156523150020","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/47587156523150020","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/97/f8/13/97f8139b-5b64-42f8-ac4e-2cd965f3f74c?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260825%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260825T225318Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=79e8ef3ab561d8c40c9613747e83915d7b352c943fcd9a869d10d6f1cb801c65","votes":[],"window_closed":false},"status":"OK"} ' headers: @@ -272,11 +272,11 @@ interactions: Connection: - keep-alive Content-Length: - - '1008' + - '1044' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:37:21 GMT + - Tue, 25 Aug 2026 22:53:18 GMT Server: - gunicorn X-Billing-ID: @@ -298,151 +298,7 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:37:22 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:37:24 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:37:25 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:37:26 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma response: @@ -458,7 +314,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Wed, 03 Jun 2026 22:37:27 GMT + - Tue, 25 Aug 2026 22:53:20 GMT Server: - gunicorn status: @@ -478,7 +334,7 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma response: @@ -494,7 +350,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Wed, 03 Jun 2026 22:37:28 GMT + - Tue, 25 Aug 2026 22:53:21 GMT Server: - gunicorn status: @@ -514,7 +370,7 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma response: @@ -530,7 +386,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Wed, 03 Jun 2026 22:37:29 GMT + - Tue, 25 Aug 2026 22:53:22 GMT Server: - gunicorn status: @@ -550,7 +406,7 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma response: @@ -566,7 +422,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Wed, 03 Jun 2026 22:37:30 GMT + - Tue, 25 Aug 2026 22:53:23 GMT Server: - gunicorn status: @@ -586,7 +442,7 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma response: @@ -602,7 +458,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Wed, 03 Jun 2026 22:37:31 GMT + - Tue, 25 Aug 2026 22:53:24 GMT Server: - gunicorn status: @@ -622,7 +478,7 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma response: @@ -638,7 +494,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Wed, 03 Jun 2026 22:37:32 GMT + - Tue, 25 Aug 2026 22:53:25 GMT Server: - gunicorn status: @@ -658,7 +514,7 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma response: @@ -674,7 +530,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Wed, 03 Jun 2026 22:37:33 GMT + - Tue, 25 Aug 2026 22:53:26 GMT Server: - gunicorn status: @@ -694,7 +550,7 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma response: @@ -710,7 +566,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Wed, 03 Jun 2026 22:37:34 GMT + - Tue, 25 Aug 2026 22:53:27 GMT Server: - gunicorn status: @@ -730,12 +586,14 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma response: body: - string: '' + string: '{"has_more":false,"limit":50,"result":[{"community":"gamma","created":"2026-08-25T22:53:27.621267+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":null,"first_seen":"2026-08-25T22:53:18.933938+00:00","id":"37373253354683797","instance_id":"47587156523150020","livescan_id":"88995738053778536","malware_family":"EICAR","matched_strings":null,"md5":"6ac59cd96a9a3ee9d52f9ebc3ec22f70","polyscore":null,"rule_name":"sdk_test_async_live","sha1":"fade22953ce5af251e2dbd7491124c818bf35e5e","sha256":"ff2b25f3bff7613e391b4d87e32f18c172d531bee5ae562bd13b6421d8b104a5","tags":"{}","yara":null}],"status":"OK"} + + ' headers: Access-Control-Allow-Origin: - '*' @@ -743,15 +601,19 @@ interactions: - Authorization Connection: - keep-alive + Content-Length: + - '623' Content-Type: - - text/html; charset=utf-8 + - application/json Date: - - Wed, 03 Jun 2026 22:37:35 GMT + - Tue, 25 Aug 2026 22:53:28 GMT Server: - gunicorn + X-Billing-ID: + - '111' status: - code: 204 - message: NO CONTENT + code: 200 + message: OK - request: body: '' headers: @@ -766,12 +628,15 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma + uri: http://ai:9696/v3/hunt/live?id=37373253354683797 response: body: - string: '' + string: '{"result":{"community":"gamma","created":"2026-08-25T22:53:27.621267+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":"http://minio:9000/public-cache/ff/2b/25/ff2b25f3bff7613e391b4d87e32f18c172d531bee5ae562bd13b6421d8b104a5fade22953ce5af251e2dbd7491124c818bf35e5e6ac59cd96a9a3ee9d52f9ebc3ec22f70?response-content-disposition=attachment%3Bfilename%3Dinfected&response-content-type=application%2Foctet-stream&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260825%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260825T225328Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=5d07ef302bc210af42bdc544364bfb23b47516ac4a61221a6d8ca76a06100ec5","first_seen":"2026-08-25T22:53:18.933938+00:00","id":"37373253354683797","instance_id":"47587156523150020","livescan_id":"88995738053778536","malware_family":"EICAR","matched_strings":[{"data":"test_async_live","identifier":"$u","length":15,"offset":69,"truncated":false}],"md5":"6ac59cd96a9a3ee9d52f9ebc3ec22f70","polyscore":null,"rule_name":"sdk_test_async_live","sha1":"fade22953ce5af251e2dbd7491124c818bf35e5e","sha256":"ff2b25f3bff7613e391b4d87e32f18c172d531bee5ae562bd13b6421d8b104a5","tags":"{}","yara":"rule + sdk_test_async_live { strings: $u = \"test_async_live\" condition: $u }"},"status":"OK"} + + ' headers: Access-Control-Allow-Origin: - '*' @@ -779,17 +644,21 @@ interactions: - Authorization Connection: - keep-alive + Content-Length: + - '1303' Content-Type: - - text/html; charset=utf-8 + - application/json Date: - - Wed, 03 Jun 2026 22:37:36 GMT + - Tue, 25 Aug 2026 22:53:28 GMT Server: - gunicorn + X-Billing-ID: + - '111' status: - code: 204 - message: NO CONTENT + code: 200 + message: OK - request: - body: '' + body: '{"result_ids":["37373253354683797"]}' headers: accept: - '*/*' @@ -799,15 +668,21 @@ interactions: - '11111111111111111111111111111111' connection: - keep-alive + content-length: + - '36' + content-type: + - application/json host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) + method: DELETE + uri: http://ai:9696/v3/hunt/live/list response: body: - string: '' + string: '{"has_more":true,"limit":50,"result":[{"community":"gamma","created":"2026-08-25T22:53:27.621267+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":null,"first_seen":"2026-08-25T22:53:18.933938+00:00","id":"37373253354683797","instance_id":"47587156523150020","livescan_id":"88995738053778536","malware_family":"EICAR","matched_strings":null,"md5":"6ac59cd96a9a3ee9d52f9ebc3ec22f70","polyscore":null,"rule_name":"sdk_test_async_live","sha1":"fade22953ce5af251e2dbd7491124c818bf35e5e","sha256":"ff2b25f3bff7613e391b4d87e32f18c172d531bee5ae562bd13b6421d8b104a5","tags":"{}","yara":null}],"status":"OK"} + + ' headers: Access-Control-Allow-Origin: - '*' @@ -815,15 +690,19 @@ interactions: - Authorization Connection: - keep-alive + Content-Length: + - '622' Content-Type: - - text/html; charset=utf-8 + - application/json Date: - - Wed, 03 Jun 2026 22:37:37 GMT + - Tue, 25 Aug 2026 22:53:28 GMT Server: - gunicorn + X-Billing-ID: + - '111' status: - code: 204 - message: NO CONTENT + code: 200 + message: OK - request: body: '' headers: @@ -838,12 +717,15 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma + uri: http://ai:9696/v3/hunt/live?id=37373253354683797 response: body: - string: '' + string: '{"errors":null,"result":"Could not find requested live hunt result: + 37373253354683797.","status":"error"} + + ' headers: Access-Control-Allow-Origin: - '*' @@ -851,17 +733,19 @@ interactions: - Authorization Connection: - keep-alive + Content-Length: + - '106' Content-Type: - - text/html; charset=utf-8 + - application/json Date: - - Wed, 03 Jun 2026 22:37:38 GMT + - Tue, 25 Aug 2026 22:53:28 GMT Server: - gunicorn status: - code: 204 - message: NO CONTENT + code: 404 + message: NOT FOUND - request: - body: '' + body: '{"rule_id":"62754506679930173"}' headers: accept: - '*/*' @@ -871,15 +755,22 @@ interactions: - '11111111111111111111111111111111' connection: - keep-alive + content-length: + - '31' + content-type: + - application/json host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) + method: DELETE + uri: http://ai:9696/v3/hunt/rule/live response: body: - string: '' + string: '{"result":{"created":"2026-08-25T22:53:17.732230+00:00","deleted":false,"description":null,"id":"62754506679930173","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T22:53:28.453628+00:00","name":"sdk-test_async_live","yara":"rule + sdk_test_async_live { strings: $u = \"test_async_live\" condition: $u }"},"status":"OK"} + + ' headers: Access-Control-Allow-Origin: - '*' @@ -887,15 +778,19 @@ interactions: - Authorization Connection: - keep-alive + Content-Length: + - '336' Content-Type: - - text/html; charset=utf-8 + - application/json Date: - - Wed, 03 Jun 2026 22:37:39 GMT + - Tue, 25 Aug 2026 22:53:28 GMT Server: - gunicorn + X-Billing-ID: + - '111' status: - code: 204 - message: NO CONTENT + code: 200 + message: OK - request: body: '' headers: @@ -910,666 +805,15 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma + uri: http://ai:9696/v3/hunt/rule?id=62754506679930173&community=gamma response: body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:37:40 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:37:41 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:37:42 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:37:43 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:37:44 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:37:45 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:37:46 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:37:47 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:37:48 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:37:49 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:37:50 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:37:51 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '{"has_more":false,"limit":50,"result":[{"community":"gamma","created":"2026-06-03T22:37:52.083918+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":null,"first_seen":"2026-06-03T22:37:21.946455+00:00","id":"74599466085661100","instance_id":"94943400355321313","livescan_id":"88206418106491329","malware_family":"EICAR","md5":"6ac59cd96a9a3ee9d52f9ebc3ec22f70","polyscore":null,"rule_name":"sdk_test_async_live","sha1":"fade22953ce5af251e2dbd7491124c818bf35e5e","sha256":"ff2b25f3bff7613e391b4d87e32f18c172d531bee5ae562bd13b6421d8b104a5","tags":"{}","yara":null}],"status":"OK"} - - ' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '600' - Content-Type: - - application/json - Date: - - Wed, 03 Jun 2026 22:37: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: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live?id=74599466085661100 - response: - body: - string: '{"result":{"community":"gamma","created":"2026-06-03T22:37:52.083918+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":"http://minio:9000/public-cache/ff/2b/25/ff2b25f3bff7613e391b4d87e32f18c172d531bee5ae562bd13b6421d8b104a5fade22953ce5af251e2dbd7491124c818bf35e5e6ac59cd96a9a3ee9d52f9ebc3ec22f70?response-content-disposition=attachment%3Bfilename%3Dinfected&response-content-type=application%2Foctet-stream&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260603%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260603T223752Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=d5b9d22359ba7a730490da90c750dafa5f483a11e6d0631784f1badf5ef753ac","first_seen":"2026-06-03T22:37:21.946455+00:00","id":"74599466085661100","instance_id":"94943400355321313","livescan_id":"88206418106491329","malware_family":"EICAR","md5":"6ac59cd96a9a3ee9d52f9ebc3ec22f70","polyscore":null,"rule_name":"sdk_test_async_live","sha1":"fade22953ce5af251e2dbd7491124c818bf35e5e","sha256":"ff2b25f3bff7613e391b4d87e32f18c172d531bee5ae562bd13b6421d8b104a5","tags":"{}","yara":"rule - sdk_test_async_live { strings: $u = \"test_async_live\" condition: $u }"},"status":"OK"} - - ' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '1196' - Content-Type: - - application/json - Date: - - Wed, 03 Jun 2026 22:37:52 GMT - Server: - - gunicorn - X-Billing-ID: - - '111' - status: - code: 200 - message: OK -- request: - body: '{"result_ids":["74599466085661100"]}' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - content-length: - - '36' - content-type: - - application/json - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: DELETE - uri: http://ai:9696/v3/hunt/live/list - response: - body: - string: '{"has_more":true,"limit":50,"result":[{"community":"gamma","created":"2026-06-03T22:37:52.083918+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":null,"first_seen":"2026-06-03T22:37:21.946455+00:00","id":"74599466085661100","instance_id":"94943400355321313","livescan_id":"88206418106491329","malware_family":"EICAR","md5":"6ac59cd96a9a3ee9d52f9ebc3ec22f70","polyscore":null,"rule_name":"sdk_test_async_live","sha1":"fade22953ce5af251e2dbd7491124c818bf35e5e","sha256":"ff2b25f3bff7613e391b4d87e32f18c172d531bee5ae562bd13b6421d8b104a5","tags":"{}","yara":null}],"status":"OK"} - - ' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '599' - Content-Type: - - application/json - Date: - - Wed, 03 Jun 2026 22:37: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: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live?id=74599466085661100 - response: - body: - string: '{"errors":null,"result":"Could not find requested live hunt result: - 74599466085661100.","status":"error"} - - ' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '106' - Content-Type: - - application/json - Date: - - Wed, 03 Jun 2026 22:37:52 GMT - Server: - - gunicorn - status: - code: 404 - message: NOT FOUND -- request: - body: '{"rule_id":"21319123963942093"}' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - content-length: - - '31' - content-type: - - application/json - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: DELETE - uri: http://ai:9696/v3/hunt/rule/live - response: - body: - string: '{"result":{"created":"2026-06-03T22:37:20.835178+00:00","deleted":false,"description":null,"id":"21319123963942093","livescan_created":"2026-06-03T22:37:20.890721+00:00","livescan_id":88206418106491329,"modified":"2026-06-03T22:37:20.863779+00:00","name":"sdk-test_async_live","yara":"rule - sdk_test_async_live { strings: $u = \"test_async_live\" condition: $u }"},"status":"OK"} - - ' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '379' - Content-Type: - - application/json - Date: - - Wed, 03 Jun 2026 22:37: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: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/rule?id=21319123963942093&community=gamma - response: - body: - string: '{"result":{"created":"2026-06-03T22:37:20.835178+00:00","deleted":false,"description":null,"id":"21319123963942093","livescan_created":"2026-06-03T22:37:20.890721+00:00","livescan_id":null,"modified":"2026-06-03T22:37:52.675202+00:00","name":"sdk-test_async_live","yara":"rule - sdk_test_async_live { strings: $u = \"test_async_live\" condition: $u }"},"status":"OK"} - - ' + string: '{"result":{"created":"2026-08-25T22:53:17.732230+00:00","deleted":false,"description":null,"id":"62754506679930173","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T22:53:28.453628+00:00","name":"sdk-test_async_live","yara":"rule + sdk_test_async_live { strings: $u = \"test_async_live\" condition: $u }"},"status":"OK"} + + ' headers: Access-Control-Allow-Origin: - '*' @@ -1578,11 +822,11 @@ interactions: Connection: - keep-alive Content-Length: - - '366' + - '336' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:37:52 GMT + - Tue, 25 Aug 2026 22:53:28 GMT Server: - gunicorn X-Billing-ID: @@ -1591,7 +835,7 @@ interactions: code: 200 message: OK - request: - body: '{"rule_id":"21319123963942093"}' + body: '{"rule_id":"62754506679930173"}' headers: accept: - '*/*' @@ -1608,7 +852,7 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: DELETE uri: http://ai:9696/v3/hunt/rule/live response: @@ -1628,7 +872,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:37:52 GMT + - Tue, 25 Aug 2026 22:53:28 GMT Server: - gunicorn status: @@ -1652,12 +896,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: DELETE - uri: http://ai:9696/v3/hunt/rule?id=21319123963942093 + uri: http://ai:9696/v3/hunt/rule?id=62754506679930173 response: body: - string: '{"result":{"created":"2026-06-03T22:37:20.835178+00:00","deleted":true,"description":null,"id":"21319123963942093","livescan_created":"2026-06-03T22:37:20.890721+00:00","livescan_id":null,"modified":"2026-06-03T22:37:52.675202+00:00","name":"sdk-test_async_live","yara":"rule + string: '{"result":{"created":"2026-08-25T22:53:17.732230+00:00","deleted":true,"description":null,"id":"62754506679930173","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T22:53:28.638516+00:00","name":"sdk-test_async_live","yara":"rule sdk_test_async_live { strings: $u = \"test_async_live\" condition: $u }"},"status":"OK"} ' @@ -1669,11 +913,11 @@ interactions: Connection: - keep-alive Content-Length: - - '365' + - '335' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:37:52 GMT + - Tue, 25 Aug 2026 22:53:28 GMT Server: - gunicorn X-Billing-ID: diff --git a/test/vcr/test_live.vcr b/test/vcr/test_live.vcr index 8d4df416..6e950c1a 100644 --- a/test/vcr/test_live.vcr +++ b/test/vcr/test_live.vcr @@ -18,12 +18,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: POST uri: http://ai:9696/v3/hunt/rule response: body: - string: '{"result":{"created":"2026-06-03T22:35:46.641576+00:00","deleted":false,"description":null,"id":"63962466091392300","livescan_created":null,"livescan_id":null,"modified":"2026-06-03T22:35:46.641576+00:00","name":"sdk-test_live","yara":"rule + string: '{"result":{"created":"2026-08-25T22:52:32.002247+00:00","deleted":false,"description":null,"id":"14328545122436991","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T22:52:32.002247+00:00","name":"sdk-test_live","yara":"rule sdk_test_live { strings: $u = \"test_live\" condition: $u }"},"status":"OK"} ' @@ -39,7 +39,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:35:46 GMT + - Tue, 25 Aug 2026 22:52:32 GMT Server: - gunicorn X-Billing-ID: @@ -48,7 +48,7 @@ interactions: code: 200 message: OK - request: - body: '{"rule_id":"63962466091392300"}' + body: '{"rule_id":"14328545122436991"}' headers: accept: - '*/*' @@ -65,12 +65,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: POST uri: http://ai:9696/v3/hunt/rule/live response: body: - string: '{"result":{"created":"2026-06-03T22:35:46.641576+00:00","deleted":false,"description":null,"id":"63962466091392300","livescan_created":"2026-06-03T22:35:46.690992+00:00","livescan_id":null,"modified":"2026-06-03T22:35:46.641576+00:00","name":"sdk-test_live","yara":"rule + string: '{"result":{"created":"2026-08-25T22:52:32.002247+00:00","deleted":false,"description":null,"id":"14328545122436991","livescan_created":"2026-08-25T22:52:32.132148+00:00","livescan_id":35147411870661732,"modified":"2026-08-25T22:52:32.105062+00:00","name":"sdk-test_live","yara":"rule sdk_test_live { strings: $u = \"test_live\" condition: $u }"},"status":"OK"} ' @@ -82,11 +82,11 @@ interactions: Connection: - keep-alive Content-Length: - - '348' + - '361' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:35:46 GMT + - Tue, 25 Aug 2026 22:52:32 GMT Server: - gunicorn X-Billing-ID: @@ -108,12 +108,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://ai:9696/v3/hunt/rule?id=63962466091392300&community=gamma + uri: http://ai:9696/v3/hunt/rule?id=14328545122436991&community=gamma response: body: - string: '{"result":{"created":"2026-06-03T22:35:46.641576+00:00","deleted":false,"description":null,"id":"63962466091392300","livescan_created":"2026-06-03T22:35:46.690992+00:00","livescan_id":68378491128619407,"modified":"2026-06-03T22:35:46.666460+00:00","name":"sdk-test_live","yara":"rule + string: '{"result":{"created":"2026-08-25T22:52:32.002247+00:00","deleted":false,"description":null,"id":"14328545122436991","livescan_created":"2026-08-25T22:52:32.132148+00:00","livescan_id":35147411870661732,"modified":"2026-08-25T22:52:32.105062+00:00","name":"sdk-test_live","yara":"rule sdk_test_live { strings: $u = \"test_live\" condition: $u }"},"status":"OK"} ' @@ -129,7 +129,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:35:47 GMT + - Tue, 25 Aug 2026 22:52:33 GMT Server: - gunicorn X-Billing-ID: @@ -155,12 +155,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: POST uri: http://ai:9696/v3/instance response: body: - string: '{"result":{"artifact_id":"27835191001619039","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-06-03T22:35:47.744500+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-06-03T22:35:47.744500+00:00","id":"27835191001619039","last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/27835191001619039","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/41/9e/e7/419ee76d-f760-45c0-bc48-f9c7f1cf44e6?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260603%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260603T223547Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=4a67c52be38b98e5fe6ccc916e1b8072e87fe9f0c266e81617c6969a74f14469","votes":[],"window_closed":false},"status":"OK"} + string: '{"result":{"artifact_id":"83487019275094114","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-08-25T22:52:33.280967+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-08-25T22:52:33.280967+00:00","id":"83487019275094114","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/83487019275094114","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/3b/53/3c/3b533c44-b3b3-4122-871e-2671860c7448?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260825%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260825T225233Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=379d86d9cc5ef3e61b737752b0346318bed946a3c01cc10d5746dca16bf4d085","votes":[],"window_closed":false},"status":"OK"} ' headers: @@ -171,11 +171,11 @@ interactions: Connection: - keep-alive Content-Length: - - '1008' + - '1044' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:35:47 GMT + - Tue, 25 Aug 2026 22:52:33 GMT Server: - gunicorn X-Billing-ID: @@ -199,9 +199,9 @@ interactions: host: - minio:9000 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: PUT - uri: http://minio:9000/artifact-index/instances/41/9e/e7/419ee76d-f760-45c0-bc48-f9c7f1cf44e6?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260603%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260603T223547Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=4a67c52be38b98e5fe6ccc916e1b8072e87fe9f0c266e81617c6969a74f14469 + uri: http://minio:9000/artifact-index/instances/3b/53/3c/3b533c44-b3b3-4122-871e-2671860c7448?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260825%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260825T225233Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=379d86d9cc5ef3e61b737752b0346318bed946a3c01cc10d5746dca16bf4d085 response: body: string: '' @@ -211,7 +211,7 @@ interactions: Content-Length: - '0' Date: - - Wed, 03 Jun 2026 22:35:47 GMT + - Tue, 25 Aug 2026 22:52:33 GMT ETag: - '"564296ca17d5e063856e0b92c8ad6ca6"' Server: @@ -224,17 +224,17 @@ interactions: X-Amz-Id-2: - dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8 X-Amz-Request-Id: - - 18B5B31905F65115 + - 18CF2E2E43B691C3 X-Content-Type-Options: - nosniff X-Ratelimit-Limit: - - '13624' + - '5063' X-Ratelimit-Remaining: - - '13624' + - '5063' X-Xss-Protection: - 1; mode=block x-amz-expiration: - - expiry-date="Fri, 05 Jun 2026 00:00:00 GMT", rule-id="expiration-artifact-index_0-instances" + - expiry-date="Thu, 27 Aug 2026 00:00:00 GMT", rule-id="expiration-artifact-index_0-instances" status: code: 200 message: OK @@ -256,12 +256,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: PUT - uri: http://ai:9696/v3/instance?id=27835191001619039 + uri: http://ai:9696/v3/instance?id=83487019275094114 response: body: - string: '{"result":{"artifact_id":"27835191001619039","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-06-03T22:35:47.744500+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-06-03T22:35:47.744500+00:00","id":"27835191001619039","last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/27835191001619039","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/41/9e/e7/419ee76d-f760-45c0-bc48-f9c7f1cf44e6?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260603%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260603T223547Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=4a67c52be38b98e5fe6ccc916e1b8072e87fe9f0c266e81617c6969a74f14469","votes":[],"window_closed":false},"status":"OK"} + string: '{"result":{"artifact_id":"83487019275094114","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-08-25T22:52:33.280967+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-08-25T22:52:33.280967+00:00","id":"83487019275094114","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/83487019275094114","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/3b/53/3c/3b533c44-b3b3-4122-871e-2671860c7448?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260825%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260825T225233Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=379d86d9cc5ef3e61b737752b0346318bed946a3c01cc10d5746dca16bf4d085","votes":[],"window_closed":false},"status":"OK"} ' headers: @@ -272,11 +272,11 @@ interactions: Connection: - keep-alive Content-Length: - - '1008' + - '1044' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:35:47 GMT + - Tue, 25 Aug 2026 22:52:33 GMT Server: - gunicorn X-Billing-ID: @@ -298,151 +298,7 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:35:48 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:35:49 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:35:50 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:35:51 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma response: @@ -458,7 +314,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Wed, 03 Jun 2026 22:35:52 GMT + - Tue, 25 Aug 2026 22:52:34 GMT Server: - gunicorn status: @@ -478,7 +334,7 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma response: @@ -494,7 +350,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Wed, 03 Jun 2026 22:35:53 GMT + - Tue, 25 Aug 2026 22:52:35 GMT Server: - gunicorn status: @@ -514,7 +370,7 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma response: @@ -530,7 +386,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Wed, 03 Jun 2026 22:35:54 GMT + - Tue, 25 Aug 2026 22:52:36 GMT Server: - gunicorn status: @@ -550,7 +406,7 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma response: @@ -566,7 +422,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Wed, 03 Jun 2026 22:35:55 GMT + - Tue, 25 Aug 2026 22:52:37 GMT Server: - gunicorn status: @@ -586,7 +442,7 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma response: @@ -602,7 +458,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Wed, 03 Jun 2026 22:35:56 GMT + - Tue, 25 Aug 2026 22:52:38 GMT Server: - gunicorn status: @@ -622,7 +478,7 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma response: @@ -638,7 +494,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Wed, 03 Jun 2026 22:35:58 GMT + - Tue, 25 Aug 2026 22:52:39 GMT Server: - gunicorn status: @@ -658,7 +514,7 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma response: @@ -674,7 +530,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Wed, 03 Jun 2026 22:35:59 GMT + - Tue, 25 Aug 2026 22:52:40 GMT Server: - gunicorn status: @@ -694,7 +550,7 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma response: @@ -710,7 +566,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Wed, 03 Jun 2026 22:36:00 GMT + - Tue, 25 Aug 2026 22:52:41 GMT Server: - gunicorn status: @@ -730,12 +586,14 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma response: body: - string: '' + string: '{"has_more":false,"limit":50,"result":[{"community":"gamma","created":"2026-08-25T22:52:42.306041+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":null,"first_seen":"2026-08-25T22:52:33.280967+00:00","id":"47972203596447840","instance_id":"83487019275094114","livescan_id":"35147411870661732","malware_family":"EICAR","matched_strings":null,"md5":"564296ca17d5e063856e0b92c8ad6ca6","polyscore":null,"rule_name":"sdk_test_live","sha1":"adec7bf2f04d1b710cbc3304f8dccd0f8d09d34a","sha256":"14e3a40dc4da0e6d9b331976e9d5611d51b25e64f5010bbdfc6c8a32b23eb190","tags":"{}","yara":null}],"status":"OK"} + + ' headers: Access-Control-Allow-Origin: - '*' @@ -743,15 +601,19 @@ interactions: - Authorization Connection: - keep-alive + Content-Length: + - '617' Content-Type: - - text/html; charset=utf-8 + - application/json Date: - - Wed, 03 Jun 2026 22:36:01 GMT + - Tue, 25 Aug 2026 22:52:42 GMT Server: - gunicorn + X-Billing-ID: + - '111' status: - code: 204 - message: NO CONTENT + code: 200 + message: OK - request: body: '' headers: @@ -766,12 +628,15 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma + uri: http://ai:9696/v3/hunt/live?id=47972203596447840 response: body: - string: '' + string: '{"result":{"community":"gamma","created":"2026-08-25T22:52:42.306041+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":"http://minio:9000/public-cache/14/e3/a4/14e3a40dc4da0e6d9b331976e9d5611d51b25e64f5010bbdfc6c8a32b23eb190adec7bf2f04d1b710cbc3304f8dccd0f8d09d34a564296ca17d5e063856e0b92c8ad6ca6?response-content-disposition=attachment%3Bfilename%3Dinfected&response-content-type=application%2Foctet-stream&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260825%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260825T225242Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=e3eada0af5bbebaaa07e7b5a7044915a572d960368a840f725c980dff06dc1f2","first_seen":"2026-08-25T22:52:33.280967+00:00","id":"47972203596447840","instance_id":"83487019275094114","livescan_id":"35147411870661732","malware_family":"EICAR","matched_strings":[{"data":"test_live","identifier":"$u","length":9,"offset":69,"truncated":false}],"md5":"564296ca17d5e063856e0b92c8ad6ca6","polyscore":null,"rule_name":"sdk_test_live","sha1":"adec7bf2f04d1b710cbc3304f8dccd0f8d09d34a","sha256":"14e3a40dc4da0e6d9b331976e9d5611d51b25e64f5010bbdfc6c8a32b23eb190","tags":"{}","yara":"rule + sdk_test_live { strings: $u = \"test_live\" condition: $u }"},"status":"OK"} + + ' headers: Access-Control-Allow-Origin: - '*' @@ -779,17 +644,21 @@ interactions: - Authorization Connection: - keep-alive + Content-Length: + - '1278' Content-Type: - - text/html; charset=utf-8 + - application/json Date: - - Wed, 03 Jun 2026 22:36:02 GMT + - Tue, 25 Aug 2026 22:52:42 GMT Server: - gunicorn + X-Billing-ID: + - '111' status: - code: 204 - message: NO CONTENT + code: 200 + message: OK - request: - body: '' + body: '{"result_ids":["47972203596447840"]}' headers: accept: - '*/*' @@ -799,15 +668,21 @@ interactions: - '11111111111111111111111111111111' connection: - keep-alive + content-length: + - '36' + content-type: + - application/json host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) + method: DELETE + uri: http://ai:9696/v3/hunt/live/list response: body: - string: '' + string: '{"has_more":true,"limit":50,"result":[{"community":"gamma","created":"2026-08-25T22:52:42.306041+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":null,"first_seen":"2026-08-25T22:52:33.280967+00:00","id":"47972203596447840","instance_id":"83487019275094114","livescan_id":"35147411870661732","malware_family":"EICAR","matched_strings":null,"md5":"564296ca17d5e063856e0b92c8ad6ca6","polyscore":null,"rule_name":"sdk_test_live","sha1":"adec7bf2f04d1b710cbc3304f8dccd0f8d09d34a","sha256":"14e3a40dc4da0e6d9b331976e9d5611d51b25e64f5010bbdfc6c8a32b23eb190","tags":"{}","yara":null}],"status":"OK"} + + ' headers: Access-Control-Allow-Origin: - '*' @@ -815,15 +690,19 @@ interactions: - Authorization Connection: - keep-alive + Content-Length: + - '616' Content-Type: - - text/html; charset=utf-8 + - application/json Date: - - Wed, 03 Jun 2026 22:36:03 GMT + - Tue, 25 Aug 2026 22:52:42 GMT Server: - gunicorn + X-Billing-ID: + - '111' status: - code: 204 - message: NO CONTENT + code: 200 + message: OK - request: body: '' headers: @@ -838,12 +717,15 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma + uri: http://ai:9696/v3/hunt/live?id=47972203596447840 response: body: - string: '' + string: '{"errors":null,"result":"Could not find requested live hunt result: + 47972203596447840.","status":"error"} + + ' headers: Access-Control-Allow-Origin: - '*' @@ -851,17 +733,19 @@ interactions: - Authorization Connection: - keep-alive + Content-Length: + - '106' Content-Type: - - text/html; charset=utf-8 + - application/json Date: - - Wed, 03 Jun 2026 22:36:04 GMT + - Tue, 25 Aug 2026 22:52:42 GMT Server: - gunicorn status: - code: 204 - message: NO CONTENT + code: 404 + message: NOT FOUND - request: - body: '' + body: '{"rule_id":"14328545122436991"}' headers: accept: - '*/*' @@ -871,15 +755,22 @@ interactions: - '11111111111111111111111111111111' connection: - keep-alive + content-length: + - '31' + content-type: + - application/json host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) + method: DELETE + uri: http://ai:9696/v3/hunt/rule/live response: body: - string: '' + string: '{"result":{"created":"2026-08-25T22:52:32.002247+00:00","deleted":false,"description":null,"id":"14328545122436991","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T22:52:42.860277+00:00","name":"sdk-test_live","yara":"rule + sdk_test_live { strings: $u = \"test_live\" condition: $u }"},"status":"OK"} + + ' headers: Access-Control-Allow-Origin: - '*' @@ -887,15 +778,19 @@ interactions: - Authorization Connection: - keep-alive + Content-Length: + - '318' Content-Type: - - text/html; charset=utf-8 + - application/json Date: - - Wed, 03 Jun 2026 22:36:05 GMT + - Tue, 25 Aug 2026 22:52:42 GMT Server: - gunicorn + X-Billing-ID: + - '111' status: - code: 204 - message: NO CONTENT + code: 200 + message: OK - request: body: '' headers: @@ -910,666 +805,15 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma + uri: http://ai:9696/v3/hunt/rule?id=14328545122436991&community=gamma response: body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:36:06 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:36:07 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:36:08 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:36:09 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:36:10 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:36:11 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:36:12 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:36:13 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:36:14 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:36:15 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:36:16 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Wed, 03 Jun 2026 22:36:17 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '{"has_more":false,"limit":50,"result":[{"community":"gamma","created":"2026-06-03T22:36:17.918027+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":null,"first_seen":"2026-06-03T22:33:00.053959+00:00","id":"8018942810283908","instance_id":"27835191001619039","livescan_id":"68378491128619407","malware_family":"EICAR","md5":"564296ca17d5e063856e0b92c8ad6ca6","polyscore":null,"rule_name":"sdk_test_live","sha1":"adec7bf2f04d1b710cbc3304f8dccd0f8d09d34a","sha256":"14e3a40dc4da0e6d9b331976e9d5611d51b25e64f5010bbdfc6c8a32b23eb190","tags":"{}","yara":null}],"status":"OK"} - - ' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '593' - Content-Type: - - application/json - Date: - - Wed, 03 Jun 2026 22:36:18 GMT - Server: - - gunicorn - X-Billing-ID: - - '111' - status: - code: 200 - message: OK -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live?id=8018942810283908 - response: - body: - string: '{"result":{"community":"gamma","created":"2026-06-03T22:36:17.918027+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":"http://minio:9000/public-cache/14/e3/a4/14e3a40dc4da0e6d9b331976e9d5611d51b25e64f5010bbdfc6c8a32b23eb190adec7bf2f04d1b710cbc3304f8dccd0f8d09d34a564296ca17d5e063856e0b92c8ad6ca6?response-content-disposition=attachment%3Bfilename%3Dinfected&response-content-type=application%2Foctet-stream&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260603%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260603T223618Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=dc5cf083031090e9c5266463a7f34d3180cc5cde2fccfc6204cc23d4481b22a5","first_seen":"2026-06-03T22:33:00.053959+00:00","id":"8018942810283908","instance_id":"27835191001619039","livescan_id":"68378491128619407","malware_family":"EICAR","md5":"564296ca17d5e063856e0b92c8ad6ca6","polyscore":null,"rule_name":"sdk_test_live","sha1":"adec7bf2f04d1b710cbc3304f8dccd0f8d09d34a","sha256":"14e3a40dc4da0e6d9b331976e9d5611d51b25e64f5010bbdfc6c8a32b23eb190","tags":"{}","yara":"rule - sdk_test_live { strings: $u = \"test_live\" condition: $u }"},"status":"OK"} - - ' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '1177' - Content-Type: - - application/json - Date: - - Wed, 03 Jun 2026 22:36:18 GMT - Server: - - gunicorn - X-Billing-ID: - - '111' - status: - code: 200 - message: OK -- request: - body: '{"result_ids":["8018942810283908"]}' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - content-length: - - '35' - content-type: - - application/json - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: DELETE - uri: http://ai:9696/v3/hunt/live/list - response: - body: - string: '{"has_more":true,"limit":50,"result":[{"community":"gamma","created":"2026-06-03T22:36:17.918027+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":null,"first_seen":"2026-06-03T22:33:00.053959+00:00","id":"8018942810283908","instance_id":"27835191001619039","livescan_id":"68378491128619407","malware_family":"EICAR","md5":"564296ca17d5e063856e0b92c8ad6ca6","polyscore":null,"rule_name":"sdk_test_live","sha1":"adec7bf2f04d1b710cbc3304f8dccd0f8d09d34a","sha256":"14e3a40dc4da0e6d9b331976e9d5611d51b25e64f5010bbdfc6c8a32b23eb190","tags":"{}","yara":null}],"status":"OK"} - - ' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '592' - Content-Type: - - application/json - Date: - - Wed, 03 Jun 2026 22:36:18 GMT - Server: - - gunicorn - X-Billing-ID: - - '111' - status: - code: 200 - message: OK -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live?id=8018942810283908 - response: - body: - string: '{"errors":null,"result":"Could not find requested live hunt result: - 8018942810283908.","status":"error"} - - ' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '105' - Content-Type: - - application/json - Date: - - Wed, 03 Jun 2026 22:36:18 GMT - Server: - - gunicorn - status: - code: 404 - message: NOT FOUND -- request: - body: '{"rule_id":"63962466091392300"}' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - content-length: - - '31' - content-type: - - application/json - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: DELETE - uri: http://ai:9696/v3/hunt/rule/live - response: - body: - string: '{"result":{"created":"2026-06-03T22:35:46.641576+00:00","deleted":false,"description":null,"id":"63962466091392300","livescan_created":"2026-06-03T22:35:46.690992+00:00","livescan_id":68378491128619407,"modified":"2026-06-03T22:35:46.666460+00:00","name":"sdk-test_live","yara":"rule - sdk_test_live { strings: $u = \"test_live\" condition: $u }"},"status":"OK"} - - ' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Length: - - '361' - Content-Type: - - application/json - Date: - - Wed, 03 Jun 2026 22:36:18 GMT - Server: - - gunicorn - X-Billing-ID: - - '111' - status: - code: 200 - message: OK -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/rule?id=63962466091392300&community=gamma - response: - body: - string: '{"result":{"created":"2026-06-03T22:35:46.641576+00:00","deleted":false,"description":null,"id":"63962466091392300","livescan_created":"2026-06-03T22:35:46.690992+00:00","livescan_id":null,"modified":"2026-06-03T22:36:18.435557+00:00","name":"sdk-test_live","yara":"rule - sdk_test_live { strings: $u = \"test_live\" condition: $u }"},"status":"OK"} - - ' + string: '{"result":{"created":"2026-08-25T22:52:32.002247+00:00","deleted":false,"description":null,"id":"14328545122436991","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T22:52:42.860277+00:00","name":"sdk-test_live","yara":"rule + sdk_test_live { strings: $u = \"test_live\" condition: $u }"},"status":"OK"} + + ' headers: Access-Control-Allow-Origin: - '*' @@ -1578,11 +822,11 @@ interactions: Connection: - keep-alive Content-Length: - - '348' + - '318' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:36:18 GMT + - Tue, 25 Aug 2026 22:52:43 GMT Server: - gunicorn X-Billing-ID: @@ -1591,7 +835,7 @@ interactions: code: 200 message: OK - request: - body: '{"rule_id":"63962466091392300"}' + body: '{"rule_id":"14328545122436991"}' headers: accept: - '*/*' @@ -1608,7 +852,7 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: DELETE uri: http://ai:9696/v3/hunt/rule/live response: @@ -1628,7 +872,7 @@ interactions: Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:36:18 GMT + - Tue, 25 Aug 2026 22:52:43 GMT Server: - gunicorn status: @@ -1652,12 +896,12 @@ interactions: host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: DELETE - uri: http://ai:9696/v3/hunt/rule?id=63962466091392300 + uri: http://ai:9696/v3/hunt/rule?id=14328545122436991 response: body: - string: '{"result":{"created":"2026-06-03T22:35:46.641576+00:00","deleted":true,"description":null,"id":"63962466091392300","livescan_created":"2026-06-03T22:35:46.690992+00:00","livescan_id":null,"modified":"2026-06-03T22:36:18.435557+00:00","name":"sdk-test_live","yara":"rule + string: '{"result":{"created":"2026-08-25T22:52:32.002247+00:00","deleted":true,"description":null,"id":"14328545122436991","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T22:52:43.118593+00:00","name":"sdk-test_live","yara":"rule sdk_test_live { strings: $u = \"test_live\" condition: $u }"},"status":"OK"} ' @@ -1669,11 +913,11 @@ interactions: Connection: - keep-alive Content-Length: - - '347' + - '317' Content-Type: - application/json Date: - - Wed, 03 Jun 2026 22:36:18 GMT + - Tue, 25 Aug 2026 22:52:43 GMT Server: - gunicorn X-Billing-ID: From 84c8a0b8cfb23fa1d2d7b641f140eb47ae1a2dab Mon Sep 17 00:00:00 2001 From: Kyle Buchmiller Date: Tue, 25 Aug 2026 15:54:20 -0700 Subject: [PATCH 06/15] docs(specs): note matched_strings on the hunt-result resources The resources spec is what to read before changing a resource's parsing, and it carries the two closest precedents -- known_good / known_good_sources and state are both documented there as additive, .get()-parsed fields in exactly this situation. matched_strings had no note, so a reader following the spec index would not find it. Points at the three-state table in the downstream-contract spec rather than restating it, and records the part a parser needs up front: which value means "not reported" versus "matched with no evidence", and that the list endpoints always yield the former by design. --- specs/02-resources.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/specs/02-resources.md b/specs/02-resources.md index 181bbd81..afbf8327 100644 --- a/specs/02-resources.md +++ b/specs/02-resources.md @@ -326,6 +326,25 @@ Classmethod builders (each returns a `PolyswarmRequest` descriptor): **No instance methods** that issue HTTP. Uploading to the pre-signed S3 URL is done via the session: `await api.session.upload_file(instance.upload_url, artifact)` (or `api.session.upload_file(...)` for sync). +### `LiveHuntResult` / `HistoricalHuntResult` + +**`matched_strings`.** The yara strings behind a hunt hit, so a consumer can see *why* +a rule fired rather than only which one did. Additive and optional, parsed with `.get()` +like `known_good` / `state` above — a server too old to emit it parses to `None` with no +behaviour change, and a subscript would raise on every result instead. + +It is **three-state** and the states are not interchangeable; the table, the per-entry +dict shape and the lower-bound caveat live in +[`05-downstream-contract.md`](./05-downstream-contract.md) +§"`matched_strings` on hunt results" — read it there rather than inferring from the +attribute. The short version a parser needs: `None` means *not reported* (an older +server, removed evidence, or a **list** endpoint, which omits it rather than fetch a blob +per row), `[]` means *matched with no byte evidence*, and a populated list is evidence. + +Both `…List` subclasses inherit this from their parent's `__init__`, so all four +hunt-result classes carry it — but on the list endpoints the value is always `None` by +design. + ### `LocalArtifact` A file-system or in-memory artifact prepared for upload. Constructed via: From 4d57f66036eec1d4fdc4de2d67d3b554b6f79ee7 Mon Sep 17 00:00:00 2001 From: Kyle Buchmiller Date: Fri, 28 Aug 2026 14:07:32 -0700 Subject: [PATCH 07/15] feat(hunts): expose the withheld-string count on hunt results `matched_strings_dropped` on both hunt-result resources, inherited by their list subclasses. The server bounds how many matched-string bytes one result may carry, and a truncated list is otherwise indistinguishable from a complete one: a caller reading 75 entries concludes the rule hit 75 times when it hit 400. A sibling attribute rather than a key inside `matched_strings`, which stays a plain list -- so this is additive to the shape already documented, not a change to it. Parsed with .get() like the field beside it: None means nothing was withheld, which is also what a server predating the bound reports, so callers need no special case for the older shape. It can never accompany an empty list, since a match's first string is never withheld. Also corrects the downstream contract, which told consumers fast-scan reports only the first offset per string and that this was one reason the list is a lower bound. Measured against yara 4.5.2 that is false -- fast mode collapses repeats of a single string only when the rule's condition does not need them, and never limits how many distinct strings a rule reports. The remaining reasons are sound and the byte bound is now a fourth. 195 tests pass. --- specs/02-resources.md | 12 +++++++++--- specs/05-downstream-contract.md | 17 ++++++++++++++++- src/polyswarm_api/resources.py | 8 ++++++++ test/hunt_matched_strings_test.py | 21 +++++++++++++++++++++ 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/specs/02-resources.md b/specs/02-resources.md index afbf8327..b40d6992 100644 --- a/specs/02-resources.md +++ b/specs/02-resources.md @@ -341,9 +341,15 @@ attribute. The short version a parser needs: `None` means *not reported* (an old server, removed evidence, or a **list** endpoint, which omits it rather than fetch a blob per row), `[]` means *matched with no byte evidence*, and a populated list is evidence. -Both `…List` subclasses inherit this from their parent's `__init__`, so all four -hunt-result classes carry it — but on the list endpoints the value is always `None` by -design. +**`matched_strings_dropped`.** A sibling `int`/`None`, parsed the same additive way: +how many matched strings the server's byte budget withheld from this result. `None` means +none were, which is also what a server predating the budget reports. It never accompanies +an empty `matched_strings` — a match's first string is never withheld — so a non-null +count always means "the list you have is short by this much". + +Both `…List` subclasses inherit these from their parent's `__init__`, so all four +hunt-result classes carry them — but on the list endpoints the values are always `None` +by design. ### `LocalArtifact` diff --git a/specs/05-downstream-contract.md b/specs/05-downstream-contract.md index 0cdbd477..fb5509e9 100644 --- a/specs/05-downstream-contract.md +++ b/specs/05-downstream-contract.md @@ -283,7 +283,7 @@ Three values are possible and consumers **must not** collapse them: |---|---| | `None` | Not reported. Either the server predates the field, the stored evidence was deleted, or this came from a **list** endpoint — those omit it rather than fetch a blob per row. "We don't know", *not* "there was nothing". | | `[]` | The rule matched and there is no byte evidence to show — a rule with no strings section, one whose matching strings are all `private`, or one that matched on absence (`not $a`, `none of them`). | -| `[…]` | The evidence. A **lower bound**, not a match count: fast-scan reports only the first offset per string, `any of them` prints only the strings that hit, and `private` strings never appear. | +| `[…]` | The evidence. A **lower bound**, not a match count: `any of them` prints only the strings that hit, `private` strings never appear, and the server may withhold some past a size limit (see `matched_strings_dropped`). | Each entry is a dict: @@ -295,6 +295,21 @@ Each entry is a dict: - `length` is the **stored** length, capped server-side. Past the cap the true length is unrecoverable. - `truncated` means "there was more than this". It over-reports at exactly the cap, because nothing in the output distinguishes a match that ended there from one that was cut. +### `matched_strings_dropped` — the count that keeps a short list honest + +A sibling attribute on the same four classes, `int` or `None`. It is how many matched +strings the server's per-result byte budget withheld, and it exists because a truncated +list is otherwise indistinguishable from a complete one: a consumer reading twelve +entries would conclude the rule hit twelve times when it hit thirty-one. + +`None` means nothing was withheld — which is also what every result predating the budget +reports, so no consumer needs to special-case the older shape. It is deliberately a +**sibling** rather than a key inside `matched_strings`, which stays a plain list. + +A populated `matched_strings` with a non-null count is the normal shape for a verbose +ruleset. The first string of a match is never withheld, so this can never accompany an +empty list. + **Evidence lives on the detail routes only.** `live_feed()` and `historical_results()` page over list endpoints and will always yield `None` here; fetch a single result (`live_result(id)` / `historical_result(id)`) to get the strings. diff --git a/src/polyswarm_api/resources.py b/src/polyswarm_api/resources.py index da970a4c..b3cca3c8 100644 --- a/src/polyswarm_api/resources.py +++ b/src/polyswarm_api/resources.py @@ -772,6 +772,10 @@ def __init__(self, content, api=None): # evidence), [...] (the evidence, a lower bound). See # specs/05-downstream-contract.md. self.matched_strings = content.get('matched_strings') + # How many matched strings the server's byte budget cost this result, or None + # when nothing was dropped (also what every result predating the budget reports). + # A sibling field, so `matched_strings` stays a plain list. + self.matched_strings_dropped = content.get('matched_strings_dropped') self.polyscore = content['polyscore'] self.malware_family = content['malware_family'] self.detections = content['detections'] @@ -826,6 +830,10 @@ def __init__(self, content, api=None): # evidence), [...] (the evidence, a lower bound). See # specs/05-downstream-contract.md. self.matched_strings = content.get('matched_strings') + # How many matched strings the server's byte budget cost this result, or None + # when nothing was dropped (also what every result predating the budget reports). + # A sibling field, so `matched_strings` stays a plain list. + self.matched_strings_dropped = content.get('matched_strings_dropped') self.polyscore = content['polyscore'] self.malware_family = content['malware_family'] self.detections = content['detections'] diff --git a/test/hunt_matched_strings_test.py b/test/hunt_matched_strings_test.py index c062ac52..c05bb822 100644 --- a/test/hunt_matched_strings_test.py +++ b/test/hunt_matched_strings_test.py @@ -86,3 +86,24 @@ def test_raw_json_still_carries_the_key(cls): """`.json` is part of the contract, so JSON-mode consumers see it without SDK work.""" result = cls(_content(cls, matched_strings=_STRINGS)) assert result.json["matched_strings"] == _STRINGS + + +@pytest.mark.parametrize("cls", ALL_CLASSES) +def test_dropped_count_parses(cls): + """The count that lets a consumer say "12 shown, 19 more".""" + assert cls(_content(cls, matched_strings=_STRINGS, matched_strings_dropped=19)) \ + .matched_strings_dropped == 19 + + +@pytest.mark.parametrize("cls", ALL_CLASSES) +def test_dropped_is_none_when_absent(cls): + """Nothing dropped, and every result predating the budget -- same answer.""" + assert cls(_content(cls, matched_strings=_STRINGS)).matched_strings_dropped is None + + +@pytest.mark.parametrize("cls", ALL_CLASSES) +def test_dropped_is_independent_of_the_strings_list(cls): + """A truncated list is still a list; the count is what says it is short.""" + result = cls(_content(cls, matched_strings=_STRINGS, matched_strings_dropped=19)) + assert isinstance(result.matched_strings, list) + assert len(result.matched_strings) == 2 From 49a64db13b75be62b471545a94236c39c378e01b Mon Sep 17 00:00:00 2001 From: Kyle Buchmiller Date: Sat, 29 Aug 2026 10:03:54 -0700 Subject: [PATCH 08/15] test: pin matched_strings_dropped against the live server, and fix the wire shape The cassettes were recorded before this attribute existed -- two commits before -- so neither carried the key. The pure-unit tests exercise dict.get and would have passed identically if the server never emitted it, which is the exact gap the earlier commit here argued the e2e-first invariant exists to close. Applied unevenly is worse than not applied. Re-recorded delete-driven against a stack running the matching server branch. The assertion reads `'matched_strings_dropped' in result.json` rather than checking the attribute for None, because `is None` cannot distinguish a served null from an absent key -- and what needs pinning is that the server SENDS the field. Its value is null there: the per-test rule is small and withholds nothing, so the null arm is what a passing hunt actually looks like. Also corrects both specs, which said list endpoints OMIT the key. They send an explicit null; omitted is the older-server case. That wording survived from a design that was reverted, and the recorded cassettes disagreed with it. And in the new section, "None means nothing was withheld" was stated flatly while the section above it is careful that None on matched_strings means "we don't know" -- the same conflation, now reading the same way in both places. 195 tests pass. --- specs/02-resources.md | 4 +- specs/05-downstream-contract.md | 8 +- test/async_client_test.py | 8 +- test/client_scan_test.py | 8 +- test/vcr/test_async_live.vcr | 172 +++++++++-------------------- test/vcr/test_live.vcr | 184 ++++++++++++++++++++++---------- 6 files changed, 199 insertions(+), 185 deletions(-) diff --git a/specs/02-resources.md b/specs/02-resources.md index b40d6992..f7c20d19 100644 --- a/specs/02-resources.md +++ b/specs/02-resources.md @@ -338,8 +338,8 @@ dict shape and the lower-bound caveat live in [`05-downstream-contract.md`](./05-downstream-contract.md) §"`matched_strings` on hunt results" — read it there rather than inferring from the attribute. The short version a parser needs: `None` means *not reported* (an older -server, removed evidence, or a **list** endpoint, which omits it rather than fetch a blob -per row), `[]` means *matched with no byte evidence*, and a populated list is evidence. +server, removed evidence, or a **list** endpoint, which sends an explicit `null` rather +than fetch a blob per row), `[]` means *matched with no byte evidence*, and a populated list is evidence. **`matched_strings_dropped`.** A sibling `int`/`None`, parsed the same additive way: how many matched strings the server's byte budget withheld from this result. `None` means diff --git a/specs/05-downstream-contract.md b/specs/05-downstream-contract.md index fb5509e9..3f34200f 100644 --- a/specs/05-downstream-contract.md +++ b/specs/05-downstream-contract.md @@ -281,7 +281,7 @@ Three values are possible and consumers **must not** collapse them: | Value | Meaning | |---|---| -| `None` | Not reported. Either the server predates the field, the stored evidence was deleted, or this came from a **list** endpoint — those omit it rather than fetch a blob per row. "We don't know", *not* "there was nothing". | +| `None` | Not reported. The **list** endpoints send the key as an explicit `null` rather than fetch a blob per row; a server predating the field omits it entirely; and stored evidence may have been deleted. `.get()` collapses all three. "We don't know", *not* "there was nothing". | | `[]` | The rule matched and there is no byte evidence to show — a rule with no strings section, one whose matching strings are all `private`, or one that matched on absence (`not $a`, `none of them`). | | `[…]` | The evidence. A **lower bound**, not a match count: `any of them` prints only the strings that hit, `private` strings never appear, and the server may withhold some past a size limit (see `matched_strings_dropped`). | @@ -302,8 +302,10 @@ strings the server's per-result byte budget withheld, and it exists because a tr list is otherwise indistinguishable from a complete one: a consumer reading twelve entries would conclude the rule hit twelve times when it hit thirty-one. -`None` means nothing was withheld — which is also what every result predating the budget -reports, so no consumer needs to special-case the older shape. It is deliberately a +`None` carries the same ambiguity as `matched_strings` itself and should be read the same +way: on a **detail** route it means nothing was withheld, but on a **list** route it means +the route did not look, and on an older server it means the field did not exist. It is not +a claim that the evidence is complete. It is deliberately a **sibling** rather than a key inside `matched_strings`, which stays a plain list. A populated `matched_strings` with a non-null count is the normal shape for a verbose diff --git a/test/async_client_test.py b/test/async_client_test.py index 14264b65..e780332e 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -733,7 +733,13 @@ async def test_async_live(self, uid): # server never grew the field; only a cassette shows what it actually sent. assert result.matched_strings, 'detail route should carry the yara evidence' assert my_results[0].matched_strings is None, \ - 'list rows omit the evidence -- it is a per-row blob fetch' + 'list rows do not carry the evidence -- it is a per-row blob fetch' + # On .json, not the attribute: `is None` cannot tell a served null from an + # absent key, and what needs pinning is that the server SENDS this field. + assert 'matched_strings_dropped' in result.json, \ + 'server must serve the withheld-count field' + assert result.matched_strings_dropped is None, \ + 'nothing withheld for a match this small' await api.live_feed_delete([result_id]) with pytest.raises(exceptions.NotFoundException): diff --git a/test/client_scan_test.py b/test/client_scan_test.py index 671b6786..4b9db8dc 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -506,7 +506,13 @@ def test_live(self): # server never grew the field; only a cassette shows what it actually sent. assert result.matched_strings, 'detail route should carry the yara evidence' assert my_results[0].matched_strings is None, \ - 'list rows omit the evidence -- it is a per-row blob fetch' + 'list rows do not carry the evidence -- it is a per-row blob fetch' + # On .json, not the attribute: `is None` cannot tell a served null from an + # absent key, and what needs pinning is that the server SENDS this field. + assert 'matched_strings_dropped' in result.json, \ + 'server must serve the withheld-count field' + assert result.matched_strings_dropped is None, \ + 'nothing withheld for a match this small' api.live_feed_delete([result_id]) with pytest.raises(exceptions.NotFoundException): diff --git a/test/vcr/test_async_live.vcr b/test/vcr/test_async_live.vcr index 0f742c2b..3b7d9f69 100644 --- a/test/vcr/test_async_live.vcr +++ b/test/vcr/test_async_live.vcr @@ -23,7 +23,7 @@ interactions: uri: http://ai:9696/v3/hunt/rule response: body: - string: '{"result":{"created":"2026-08-25T22:53:17.732230+00:00","deleted":false,"description":null,"id":"62754506679930173","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T22:53:17.732230+00:00","name":"sdk-test_async_live","yara":"rule + string: '{"result":{"created":"2026-08-29T00:11:42.713162+00:00","deleted":false,"description":null,"id":"82290800532833154","livescan_created":null,"livescan_id":null,"modified":"2026-08-29T00:11:42.713162+00:00","name":"sdk-test_async_live","yara":"rule sdk_test_async_live { strings: $u = \"test_async_live\" condition: $u }"},"status":"OK"} ' @@ -39,7 +39,7 @@ interactions: Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:53:17 GMT + - Sat, 29 Aug 2026 00:11:42 GMT Server: - gunicorn X-Billing-ID: @@ -48,7 +48,7 @@ interactions: code: 200 message: OK - request: - body: '{"rule_id":"62754506679930173"}' + body: '{"rule_id":"82290800532833154"}' headers: accept: - '*/*' @@ -70,7 +70,7 @@ interactions: uri: http://ai:9696/v3/hunt/rule/live response: body: - string: '{"result":{"created":"2026-08-25T22:53:17.732230+00:00","deleted":false,"description":null,"id":"62754506679930173","livescan_created":"2026-08-25T22:53:17.826217+00:00","livescan_id":88995738053778536,"modified":"2026-08-25T22:53:17.824324+00:00","name":"sdk-test_async_live","yara":"rule + string: '{"result":{"created":"2026-08-29T00:11:42.713162+00:00","deleted":false,"description":null,"id":"82290800532833154","livescan_created":"2026-08-29T00:11:42.802951+00:00","livescan_id":63056499228390147,"modified":"2026-08-29T00:11:42.800524+00:00","name":"sdk-test_async_live","yara":"rule sdk_test_async_live { strings: $u = \"test_async_live\" condition: $u }"},"status":"OK"} ' @@ -86,7 +86,7 @@ interactions: Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:53:17 GMT + - Sat, 29 Aug 2026 00:11:42 GMT Server: - gunicorn X-Billing-ID: @@ -110,10 +110,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://ai:9696/v3/hunt/rule?id=62754506679930173&community=gamma + uri: http://ai:9696/v3/hunt/rule?id=82290800532833154&community=gamma response: body: - string: '{"result":{"created":"2026-08-25T22:53:17.732230+00:00","deleted":false,"description":null,"id":"62754506679930173","livescan_created":"2026-08-25T22:53:17.826217+00:00","livescan_id":88995738053778536,"modified":"2026-08-25T22:53:17.824324+00:00","name":"sdk-test_async_live","yara":"rule + string: '{"result":{"created":"2026-08-29T00:11:42.713162+00:00","deleted":false,"description":null,"id":"82290800532833154","livescan_created":"2026-08-29T00:11:42.802951+00:00","livescan_id":63056499228390147,"modified":"2026-08-29T00:11:42.800524+00:00","name":"sdk-test_async_live","yara":"rule sdk_test_async_live { strings: $u = \"test_async_live\" condition: $u }"},"status":"OK"} ' @@ -129,7 +129,7 @@ interactions: Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:53:18 GMT + - Sat, 29 Aug 2026 00:11:43 GMT Server: - gunicorn X-Billing-ID: @@ -160,7 +160,7 @@ interactions: uri: http://ai:9696/v3/instance response: body: - string: '{"result":{"artifact_id":"47587156523150020","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-08-25T22:53:18.933938+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-08-25T22:53:18.933938+00:00","id":"47587156523150020","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/47587156523150020","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/97/f8/13/97f8139b-5b64-42f8-ac4e-2cd965f3f74c?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260825%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260825T225318Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=79e8ef3ab561d8c40c9613747e83915d7b352c943fcd9a869d10d6f1cb801c65","votes":[],"window_closed":false},"status":"OK"} + string: '{"result":{"artifact_id":"78118450880280801","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-08-29T00:11:43.904670+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-08-29T00:11:43.904670+00:00","id":"78118450880280801","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/78118450880280801","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/1a/f1/ca/1af1ca3a-7869-4653-b775-16bf39d98d73?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260829%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260829T001143Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=db5df1a1ef8dfb317ced098d31b77525ecf0f29fdcc708983a274b7b780efd12","votes":[],"window_closed":false},"status":"OK"} ' headers: @@ -175,7 +175,7 @@ interactions: Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:53:18 GMT + - Sat, 29 Aug 2026 00:11:43 GMT Server: - gunicorn X-Billing-ID: @@ -201,7 +201,7 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: PUT - uri: http://minio:9000/artifact-index/instances/97/f8/13/97f8139b-5b64-42f8-ac4e-2cd965f3f74c?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260825%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260825T225318Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=79e8ef3ab561d8c40c9613747e83915d7b352c943fcd9a869d10d6f1cb801c65 + uri: http://minio:9000/artifact-index/instances/1a/f1/ca/1af1ca3a-7869-4653-b775-16bf39d98d73?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260829%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260829T001143Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=db5df1a1ef8dfb317ced098d31b77525ecf0f29fdcc708983a274b7b780efd12 response: body: string: '' @@ -211,7 +211,7 @@ interactions: Content-Length: - '0' Date: - - Tue, 25 Aug 2026 22:53:18 GMT + - Sat, 29 Aug 2026 00:11:43 GMT ETag: - '"6ac59cd96a9a3ee9d52f9ebc3ec22f70"' Server: @@ -224,17 +224,17 @@ interactions: X-Amz-Id-2: - dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8 X-Amz-Request-Id: - - 18CF2E38E284BA55 + - 18D01E3E0CBB5837 X-Content-Type-Options: - nosniff X-Ratelimit-Limit: - - '5063' + - '5171' X-Ratelimit-Remaining: - - '5063' + - '5171' X-Xss-Protection: - 1; mode=block x-amz-expiration: - - expiry-date="Thu, 27 Aug 2026 00:00:00 GMT", rule-id="expiration-artifact-index_0-instances" + - expiry-date="Mon, 31 Aug 2026 00:00:00 GMT", rule-id="expiration-artifact-index_0-instances" status: code: 200 message: OK @@ -258,10 +258,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: PUT - uri: http://ai:9696/v3/instance?id=47587156523150020 + uri: http://ai:9696/v3/instance?id=78118450880280801 response: body: - string: '{"result":{"artifact_id":"47587156523150020","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-08-25T22:53:18.933938+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-08-25T22:53:18.933938+00:00","id":"47587156523150020","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/47587156523150020","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/97/f8/13/97f8139b-5b64-42f8-ac4e-2cd965f3f74c?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260825%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260825T225318Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=79e8ef3ab561d8c40c9613747e83915d7b352c943fcd9a869d10d6f1cb801c65","votes":[],"window_closed":false},"status":"OK"} + string: '{"result":{"artifact_id":"78118450880280801","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-08-29T00:11:43.904670+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-08-29T00:11:43.904670+00:00","id":"78118450880280801","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/78118450880280801","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/1a/f1/ca/1af1ca3a-7869-4653-b775-16bf39d98d73?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260829%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260829T001143Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=db5df1a1ef8dfb317ced098d31b77525ecf0f29fdcc708983a274b7b780efd12","votes":[],"window_closed":false},"status":"OK"} ' headers: @@ -276,7 +276,7 @@ interactions: Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:53:18 GMT + - Sat, 29 Aug 2026 00:11:43 GMT Server: - gunicorn X-Billing-ID: @@ -314,7 +314,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Tue, 25 Aug 2026 22:53:20 GMT + - Sat, 29 Aug 2026 00:11:44 GMT Server: - gunicorn status: @@ -350,7 +350,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Tue, 25 Aug 2026 22:53:21 GMT + - Sat, 29 Aug 2026 00:11:46 GMT Server: - gunicorn status: @@ -386,7 +386,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Tue, 25 Aug 2026 22:53:22 GMT + - Sat, 29 Aug 2026 00:11:47 GMT Server: - gunicorn status: @@ -422,7 +422,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Tue, 25 Aug 2026 22:53:23 GMT + - Sat, 29 Aug 2026 00:11:48 GMT Server: - gunicorn status: @@ -458,7 +458,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Tue, 25 Aug 2026 22:53:24 GMT + - Sat, 29 Aug 2026 00:11:49 GMT Server: - gunicorn status: @@ -494,7 +494,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Tue, 25 Aug 2026 22:53:25 GMT + - Sat, 29 Aug 2026 00:11:50 GMT Server: - gunicorn status: @@ -519,79 +519,7 @@ interactions: uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma response: body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Tue, 25 Aug 2026 22:53:26 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '' - headers: - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - Authorization - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Tue, 25 Aug 2026 22:53:27 GMT - Server: - - gunicorn - status: - code: 204 - message: NO CONTENT -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - authorization: - - '11111111111111111111111111111111' - connection: - - keep-alive - host: - - ai:9696 - user-agent: - - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) - method: GET - uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma - response: - body: - string: '{"has_more":false,"limit":50,"result":[{"community":"gamma","created":"2026-08-25T22:53:27.621267+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":null,"first_seen":"2026-08-25T22:53:18.933938+00:00","id":"37373253354683797","instance_id":"47587156523150020","livescan_id":"88995738053778536","malware_family":"EICAR","matched_strings":null,"md5":"6ac59cd96a9a3ee9d52f9ebc3ec22f70","polyscore":null,"rule_name":"sdk_test_async_live","sha1":"fade22953ce5af251e2dbd7491124c818bf35e5e","sha256":"ff2b25f3bff7613e391b4d87e32f18c172d531bee5ae562bd13b6421d8b104a5","tags":"{}","yara":null}],"status":"OK"} + string: '{"has_more":false,"limit":50,"result":[{"community":"gamma","created":"2026-08-29T00:11:51.184667+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":null,"first_seen":"2026-08-29T00:11:43.904670+00:00","id":"27253623675037251","instance_id":"78118450880280801","livescan_id":"63056499228390147","malware_family":"EICAR","matched_strings":null,"matched_strings_dropped":null,"md5":"6ac59cd96a9a3ee9d52f9ebc3ec22f70","polyscore":null,"rule_name":"sdk_test_async_live","sha1":"fade22953ce5af251e2dbd7491124c818bf35e5e","sha256":"ff2b25f3bff7613e391b4d87e32f18c172d531bee5ae562bd13b6421d8b104a5","tags":"{}","yara":null}],"status":"OK"} ' headers: @@ -602,11 +530,11 @@ interactions: Connection: - keep-alive Content-Length: - - '623' + - '654' Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:53:28 GMT + - Sat, 29 Aug 2026 00:11:51 GMT Server: - gunicorn X-Billing-ID: @@ -630,10 +558,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://ai:9696/v3/hunt/live?id=37373253354683797 + uri: http://ai:9696/v3/hunt/live?id=27253623675037251 response: body: - string: '{"result":{"community":"gamma","created":"2026-08-25T22:53:27.621267+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":"http://minio:9000/public-cache/ff/2b/25/ff2b25f3bff7613e391b4d87e32f18c172d531bee5ae562bd13b6421d8b104a5fade22953ce5af251e2dbd7491124c818bf35e5e6ac59cd96a9a3ee9d52f9ebc3ec22f70?response-content-disposition=attachment%3Bfilename%3Dinfected&response-content-type=application%2Foctet-stream&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260825%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260825T225328Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=5d07ef302bc210af42bdc544364bfb23b47516ac4a61221a6d8ca76a06100ec5","first_seen":"2026-08-25T22:53:18.933938+00:00","id":"37373253354683797","instance_id":"47587156523150020","livescan_id":"88995738053778536","malware_family":"EICAR","matched_strings":[{"data":"test_async_live","identifier":"$u","length":15,"offset":69,"truncated":false}],"md5":"6ac59cd96a9a3ee9d52f9ebc3ec22f70","polyscore":null,"rule_name":"sdk_test_async_live","sha1":"fade22953ce5af251e2dbd7491124c818bf35e5e","sha256":"ff2b25f3bff7613e391b4d87e32f18c172d531bee5ae562bd13b6421d8b104a5","tags":"{}","yara":"rule + string: '{"result":{"community":"gamma","created":"2026-08-29T00:11:51.184667+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":"http://minio:9000/public-cache/ff/2b/25/ff2b25f3bff7613e391b4d87e32f18c172d531bee5ae562bd13b6421d8b104a5fade22953ce5af251e2dbd7491124c818bf35e5e6ac59cd96a9a3ee9d52f9ebc3ec22f70?response-content-disposition=attachment%3Bfilename%3Dinfected&response-content-type=application%2Foctet-stream&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260829%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260829T001151Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=adc60fb87017ea3a84210c55f388665cb85763ec44687439643e81451da65e48","first_seen":"2026-08-29T00:11:43.904670+00:00","id":"27253623675037251","instance_id":"78118450880280801","livescan_id":"63056499228390147","malware_family":"EICAR","matched_strings":[{"data":"test_async_live","identifier":"$u","length":15,"offset":69,"truncated":false}],"matched_strings_dropped":null,"md5":"6ac59cd96a9a3ee9d52f9ebc3ec22f70","polyscore":null,"rule_name":"sdk_test_async_live","sha1":"fade22953ce5af251e2dbd7491124c818bf35e5e","sha256":"ff2b25f3bff7613e391b4d87e32f18c172d531bee5ae562bd13b6421d8b104a5","tags":"{}","yara":"rule sdk_test_async_live { strings: $u = \"test_async_live\" condition: $u }"},"status":"OK"} ' @@ -645,11 +573,11 @@ interactions: Connection: - keep-alive Content-Length: - - '1303' + - '1334' Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:53:28 GMT + - Sat, 29 Aug 2026 00:11:51 GMT Server: - gunicorn X-Billing-ID: @@ -658,7 +586,7 @@ interactions: code: 200 message: OK - request: - body: '{"result_ids":["37373253354683797"]}' + body: '{"result_ids":["27253623675037251"]}' headers: accept: - '*/*' @@ -680,7 +608,7 @@ interactions: uri: http://ai:9696/v3/hunt/live/list response: body: - string: '{"has_more":true,"limit":50,"result":[{"community":"gamma","created":"2026-08-25T22:53:27.621267+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":null,"first_seen":"2026-08-25T22:53:18.933938+00:00","id":"37373253354683797","instance_id":"47587156523150020","livescan_id":"88995738053778536","malware_family":"EICAR","matched_strings":null,"md5":"6ac59cd96a9a3ee9d52f9ebc3ec22f70","polyscore":null,"rule_name":"sdk_test_async_live","sha1":"fade22953ce5af251e2dbd7491124c818bf35e5e","sha256":"ff2b25f3bff7613e391b4d87e32f18c172d531bee5ae562bd13b6421d8b104a5","tags":"{}","yara":null}],"status":"OK"} + string: '{"has_more":true,"limit":50,"result":[{"community":"gamma","created":"2026-08-29T00:11:51.184667+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":null,"first_seen":"2026-08-29T00:11:43.904670+00:00","id":"27253623675037251","instance_id":"78118450880280801","livescan_id":"63056499228390147","malware_family":"EICAR","matched_strings":null,"matched_strings_dropped":null,"md5":"6ac59cd96a9a3ee9d52f9ebc3ec22f70","polyscore":null,"rule_name":"sdk_test_async_live","sha1":"fade22953ce5af251e2dbd7491124c818bf35e5e","sha256":"ff2b25f3bff7613e391b4d87e32f18c172d531bee5ae562bd13b6421d8b104a5","tags":"{}","yara":null}],"status":"OK"} ' headers: @@ -691,11 +619,11 @@ interactions: Connection: - keep-alive Content-Length: - - '622' + - '653' Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:53:28 GMT + - Sat, 29 Aug 2026 00:11:51 GMT Server: - gunicorn X-Billing-ID: @@ -719,11 +647,11 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://ai:9696/v3/hunt/live?id=37373253354683797 + uri: http://ai:9696/v3/hunt/live?id=27253623675037251 response: body: string: '{"errors":null,"result":"Could not find requested live hunt result: - 37373253354683797.","status":"error"} + 27253623675037251.","status":"error"} ' headers: @@ -738,14 +666,14 @@ interactions: Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:53:28 GMT + - Sat, 29 Aug 2026 00:11:51 GMT Server: - gunicorn status: code: 404 message: NOT FOUND - request: - body: '{"rule_id":"62754506679930173"}' + body: '{"rule_id":"82290800532833154"}' headers: accept: - '*/*' @@ -767,7 +695,7 @@ interactions: uri: http://ai:9696/v3/hunt/rule/live response: body: - string: '{"result":{"created":"2026-08-25T22:53:17.732230+00:00","deleted":false,"description":null,"id":"62754506679930173","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T22:53:28.453628+00:00","name":"sdk-test_async_live","yara":"rule + string: '{"result":{"created":"2026-08-29T00:11:42.713162+00:00","deleted":false,"description":null,"id":"82290800532833154","livescan_created":null,"livescan_id":null,"modified":"2026-08-29T00:11:51.325659+00:00","name":"sdk-test_async_live","yara":"rule sdk_test_async_live { strings: $u = \"test_async_live\" condition: $u }"},"status":"OK"} ' @@ -783,7 +711,7 @@ interactions: Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:53:28 GMT + - Sat, 29 Aug 2026 00:11:51 GMT Server: - gunicorn X-Billing-ID: @@ -807,10 +735,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://ai:9696/v3/hunt/rule?id=62754506679930173&community=gamma + uri: http://ai:9696/v3/hunt/rule?id=82290800532833154&community=gamma response: body: - string: '{"result":{"created":"2026-08-25T22:53:17.732230+00:00","deleted":false,"description":null,"id":"62754506679930173","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T22:53:28.453628+00:00","name":"sdk-test_async_live","yara":"rule + string: '{"result":{"created":"2026-08-29T00:11:42.713162+00:00","deleted":false,"description":null,"id":"82290800532833154","livescan_created":null,"livescan_id":null,"modified":"2026-08-29T00:11:51.325659+00:00","name":"sdk-test_async_live","yara":"rule sdk_test_async_live { strings: $u = \"test_async_live\" condition: $u }"},"status":"OK"} ' @@ -826,7 +754,7 @@ interactions: Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:53:28 GMT + - Sat, 29 Aug 2026 00:11:51 GMT Server: - gunicorn X-Billing-ID: @@ -835,7 +763,7 @@ interactions: code: 200 message: OK - request: - body: '{"rule_id":"62754506679930173"}' + body: '{"rule_id":"82290800532833154"}' headers: accept: - '*/*' @@ -872,7 +800,7 @@ interactions: Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:53:28 GMT + - Sat, 29 Aug 2026 00:11:51 GMT Server: - gunicorn status: @@ -898,10 +826,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: DELETE - uri: http://ai:9696/v3/hunt/rule?id=62754506679930173 + uri: http://ai:9696/v3/hunt/rule?id=82290800532833154 response: body: - string: '{"result":{"created":"2026-08-25T22:53:17.732230+00:00","deleted":true,"description":null,"id":"62754506679930173","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T22:53:28.638516+00:00","name":"sdk-test_async_live","yara":"rule + string: '{"result":{"created":"2026-08-29T00:11:42.713162+00:00","deleted":true,"description":null,"id":"82290800532833154","livescan_created":null,"livescan_id":null,"modified":"2026-08-29T00:11:51.427206+00:00","name":"sdk-test_async_live","yara":"rule sdk_test_async_live { strings: $u = \"test_async_live\" condition: $u }"},"status":"OK"} ' @@ -917,7 +845,7 @@ interactions: Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:53:28 GMT + - Sat, 29 Aug 2026 00:11:51 GMT Server: - gunicorn X-Billing-ID: diff --git a/test/vcr/test_live.vcr b/test/vcr/test_live.vcr index 6e950c1a..165b2f26 100644 --- a/test/vcr/test_live.vcr +++ b/test/vcr/test_live.vcr @@ -23,7 +23,7 @@ interactions: uri: http://ai:9696/v3/hunt/rule response: body: - string: '{"result":{"created":"2026-08-25T22:52:32.002247+00:00","deleted":false,"description":null,"id":"14328545122436991","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T22:52:32.002247+00:00","name":"sdk-test_live","yara":"rule + string: '{"result":{"created":"2026-08-29T00:11:28.650982+00:00","deleted":false,"description":null,"id":"79693411044316426","livescan_created":null,"livescan_id":null,"modified":"2026-08-29T00:11:28.650982+00:00","name":"sdk-test_live","yara":"rule sdk_test_live { strings: $u = \"test_live\" condition: $u }"},"status":"OK"} ' @@ -39,7 +39,7 @@ interactions: Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:52:32 GMT + - Sat, 29 Aug 2026 00:11:28 GMT Server: - gunicorn X-Billing-ID: @@ -48,7 +48,7 @@ interactions: code: 200 message: OK - request: - body: '{"rule_id":"14328545122436991"}' + body: '{"rule_id":"79693411044316426"}' headers: accept: - '*/*' @@ -70,7 +70,7 @@ interactions: uri: http://ai:9696/v3/hunt/rule/live response: body: - string: '{"result":{"created":"2026-08-25T22:52:32.002247+00:00","deleted":false,"description":null,"id":"14328545122436991","livescan_created":"2026-08-25T22:52:32.132148+00:00","livescan_id":35147411870661732,"modified":"2026-08-25T22:52:32.105062+00:00","name":"sdk-test_live","yara":"rule + string: '{"result":{"created":"2026-08-29T00:11:28.650982+00:00","deleted":false,"description":null,"id":"79693411044316426","livescan_created":"2026-08-29T00:11:28.814637+00:00","livescan_id":6053794961344849,"modified":"2026-08-29T00:11:28.769801+00:00","name":"sdk-test_live","yara":"rule sdk_test_live { strings: $u = \"test_live\" condition: $u }"},"status":"OK"} ' @@ -82,11 +82,11 @@ interactions: Connection: - keep-alive Content-Length: - - '361' + - '360' Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:52:32 GMT + - Sat, 29 Aug 2026 00:11:28 GMT Server: - gunicorn X-Billing-ID: @@ -110,10 +110,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://ai:9696/v3/hunt/rule?id=14328545122436991&community=gamma + uri: http://ai:9696/v3/hunt/rule?id=79693411044316426&community=gamma response: body: - string: '{"result":{"created":"2026-08-25T22:52:32.002247+00:00","deleted":false,"description":null,"id":"14328545122436991","livescan_created":"2026-08-25T22:52:32.132148+00:00","livescan_id":35147411870661732,"modified":"2026-08-25T22:52:32.105062+00:00","name":"sdk-test_live","yara":"rule + string: '{"result":{"created":"2026-08-29T00:11:28.650982+00:00","deleted":false,"description":null,"id":"79693411044316426","livescan_created":"2026-08-29T00:11:28.814637+00:00","livescan_id":6053794961344849,"modified":"2026-08-29T00:11:28.769801+00:00","name":"sdk-test_live","yara":"rule sdk_test_live { strings: $u = \"test_live\" condition: $u }"},"status":"OK"} ' @@ -125,11 +125,11 @@ interactions: Connection: - keep-alive Content-Length: - - '361' + - '360' Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:52:33 GMT + - Sat, 29 Aug 2026 00:11:29 GMT Server: - gunicorn X-Billing-ID: @@ -160,7 +160,7 @@ interactions: uri: http://ai:9696/v3/instance response: body: - string: '{"result":{"artifact_id":"83487019275094114","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-08-25T22:52:33.280967+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-08-25T22:52:33.280967+00:00","id":"83487019275094114","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/83487019275094114","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/3b/53/3c/3b533c44-b3b3-4122-871e-2671860c7448?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260825%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260825T225233Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=379d86d9cc5ef3e61b737752b0346318bed946a3c01cc10d5746dca16bf4d085","votes":[],"window_closed":false},"status":"OK"} + string: '{"result":{"artifact_id":"3936300515643678","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-08-29T00:11:30.179957+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-08-29T00:11:30.179957+00:00","id":"3936300515643678","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/3936300515643678","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/d9/23/e4/d923e4f0-44a2-4b0a-8812-165751c5e8e9?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260829%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260829T001130Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=6d98f2ae0827d37cc153870c2587b24b39989492f22166255b6d9ac784abf347","votes":[],"window_closed":false},"status":"OK"} ' headers: @@ -171,11 +171,11 @@ interactions: Connection: - keep-alive Content-Length: - - '1044' + - '1041' Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:52:33 GMT + - Sat, 29 Aug 2026 00:11:30 GMT Server: - gunicorn X-Billing-ID: @@ -201,7 +201,7 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: PUT - uri: http://minio:9000/artifact-index/instances/3b/53/3c/3b533c44-b3b3-4122-871e-2671860c7448?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260825%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260825T225233Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=379d86d9cc5ef3e61b737752b0346318bed946a3c01cc10d5746dca16bf4d085 + uri: http://minio:9000/artifact-index/instances/d9/23/e4/d923e4f0-44a2-4b0a-8812-165751c5e8e9?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260829%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260829T001130Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=6d98f2ae0827d37cc153870c2587b24b39989492f22166255b6d9ac784abf347 response: body: string: '' @@ -211,7 +211,7 @@ interactions: Content-Length: - '0' Date: - - Tue, 25 Aug 2026 22:52:33 GMT + - Sat, 29 Aug 2026 00:11:30 GMT ETag: - '"564296ca17d5e063856e0b92c8ad6ca6"' Server: @@ -224,17 +224,17 @@ interactions: X-Amz-Id-2: - dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8 X-Amz-Request-Id: - - 18CF2E2E43B691C3 + - 18D01E3ADC7F16BA X-Content-Type-Options: - nosniff X-Ratelimit-Limit: - - '5063' + - '5171' X-Ratelimit-Remaining: - - '5063' + - '5171' X-Xss-Protection: - 1; mode=block x-amz-expiration: - - expiry-date="Thu, 27 Aug 2026 00:00:00 GMT", rule-id="expiration-artifact-index_0-instances" + - expiry-date="Mon, 31 Aug 2026 00:00:00 GMT", rule-id="expiration-artifact-index_0-instances" status: code: 200 message: OK @@ -258,10 +258,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: PUT - uri: http://ai:9696/v3/instance?id=83487019275094114 + uri: http://ai:9696/v3/instance?id=3936300515643678 response: body: - string: '{"result":{"artifact_id":"83487019275094114","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-08-25T22:52:33.280967+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-08-25T22:52:33.280967+00:00","id":"83487019275094114","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/83487019275094114","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/3b/53/3c/3b533c44-b3b3-4122-871e-2671860c7448?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260825%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260825T225233Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=379d86d9cc5ef3e61b737752b0346318bed946a3c01cc10d5746dca16bf4d085","votes":[],"window_closed":false},"status":"OK"} + string: '{"result":{"artifact_id":"3936300515643678","assertions":[],"bounty_state":0,"community":"gamma","country":"","created":"2026-08-29T00:11:30.179957+00:00","detections":null,"expiration_window":null,"expire_at":null,"extended_type":null,"failed":false,"filename":"artifact","first_seen":"2026-08-29T00:11:30.179957+00:00","id":"3936300515643678","known_good":null,"last_scanned":null,"last_seen":null,"md5":null,"metadata":[],"mimetype":null,"permalink":"https://polyswarm.network/scan/results/file/None/3936300515643678","polyscore":null,"result":null,"sha1":null,"sha256":null,"size":null,"state":"CREATED","type":"FILE","upload_url":"http://minio:9000/artifact-index/instances/d9/23/e4/d923e4f0-44a2-4b0a-8812-165751c5e8e9?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260829%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260829T001130Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=6d98f2ae0827d37cc153870c2587b24b39989492f22166255b6d9ac784abf347","votes":[],"window_closed":false},"status":"OK"} ' headers: @@ -272,11 +272,11 @@ interactions: Connection: - keep-alive Content-Length: - - '1044' + - '1041' Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:52:33 GMT + - Sat, 29 Aug 2026 00:11:30 GMT Server: - gunicorn X-Billing-ID: @@ -314,7 +314,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Tue, 25 Aug 2026 22:52:34 GMT + - Sat, 29 Aug 2026 00:11:31 GMT Server: - gunicorn status: @@ -350,7 +350,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Tue, 25 Aug 2026 22:52:35 GMT + - Sat, 29 Aug 2026 00:11:32 GMT Server: - gunicorn status: @@ -386,7 +386,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Tue, 25 Aug 2026 22:52:36 GMT + - Sat, 29 Aug 2026 00:11:33 GMT Server: - gunicorn status: @@ -422,7 +422,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Tue, 25 Aug 2026 22:52:37 GMT + - Sat, 29 Aug 2026 00:11:34 GMT Server: - gunicorn status: @@ -458,7 +458,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Tue, 25 Aug 2026 22:52:38 GMT + - Sat, 29 Aug 2026 00:11:35 GMT Server: - gunicorn status: @@ -494,7 +494,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Tue, 25 Aug 2026 22:52:39 GMT + - Sat, 29 Aug 2026 00:11:36 GMT Server: - gunicorn status: @@ -530,7 +530,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Tue, 25 Aug 2026 22:52:40 GMT + - Sat, 29 Aug 2026 00:11:37 GMT Server: - gunicorn status: @@ -566,7 +566,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Tue, 25 Aug 2026 22:52:41 GMT + - Sat, 29 Aug 2026 00:11:38 GMT Server: - gunicorn status: @@ -591,7 +591,79 @@ interactions: uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma response: body: - string: '{"has_more":false,"limit":50,"result":[{"community":"gamma","created":"2026-08-25T22:52:42.306041+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":null,"first_seen":"2026-08-25T22:52:33.280967+00:00","id":"47972203596447840","instance_id":"83487019275094114","livescan_id":"35147411870661732","malware_family":"EICAR","matched_strings":null,"md5":"564296ca17d5e063856e0b92c8ad6ca6","polyscore":null,"rule_name":"sdk_test_live","sha1":"adec7bf2f04d1b710cbc3304f8dccd0f8d09d34a","sha256":"14e3a40dc4da0e6d9b331976e9d5611d51b25e64f5010bbdfc6c8a32b23eb190","tags":"{}","yara":null}],"status":"OK"} + string: '' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Type: + - text/html; charset=utf-8 + Date: + - Sat, 29 Aug 2026 00:11:39 GMT + Server: + - gunicorn + status: + code: 204 + message: NO CONTENT +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) + method: GET + uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma + response: + body: + string: '' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Type: + - text/html; charset=utf-8 + Date: + - Sat, 29 Aug 2026 00:11:40 GMT + Server: + - gunicorn + status: + code: 204 + message: NO CONTENT +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) + method: GET + uri: http://ai:9696/v3/hunt/live/list?since=600&community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"community":"gamma","created":"2026-08-29T00:11:40.978433+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":null,"first_seen":"2026-08-29T00:11:30.179957+00:00","id":"56607280072443305","instance_id":"3936300515643678","livescan_id":"6053794961344849","malware_family":"EICAR","matched_strings":null,"matched_strings_dropped":null,"md5":"564296ca17d5e063856e0b92c8ad6ca6","polyscore":null,"rule_name":"sdk_test_live","sha1":"adec7bf2f04d1b710cbc3304f8dccd0f8d09d34a","sha256":"14e3a40dc4da0e6d9b331976e9d5611d51b25e64f5010bbdfc6c8a32b23eb190","tags":"{}","yara":null}],"status":"OK"} ' headers: @@ -602,11 +674,11 @@ interactions: Connection: - keep-alive Content-Length: - - '617' + - '646' Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:52:42 GMT + - Sat, 29 Aug 2026 00:11:41 GMT Server: - gunicorn X-Billing-ID: @@ -630,10 +702,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://ai:9696/v3/hunt/live?id=47972203596447840 + uri: http://ai:9696/v3/hunt/live?id=56607280072443305 response: body: - string: '{"result":{"community":"gamma","created":"2026-08-25T22:52:42.306041+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":"http://minio:9000/public-cache/14/e3/a4/14e3a40dc4da0e6d9b331976e9d5611d51b25e64f5010bbdfc6c8a32b23eb190adec7bf2f04d1b710cbc3304f8dccd0f8d09d34a564296ca17d5e063856e0b92c8ad6ca6?response-content-disposition=attachment%3Bfilename%3Dinfected&response-content-type=application%2Foctet-stream&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260825%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260825T225242Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=e3eada0af5bbebaaa07e7b5a7044915a572d960368a840f725c980dff06dc1f2","first_seen":"2026-08-25T22:52:33.280967+00:00","id":"47972203596447840","instance_id":"83487019275094114","livescan_id":"35147411870661732","malware_family":"EICAR","matched_strings":[{"data":"test_live","identifier":"$u","length":9,"offset":69,"truncated":false}],"md5":"564296ca17d5e063856e0b92c8ad6ca6","polyscore":null,"rule_name":"sdk_test_live","sha1":"adec7bf2f04d1b710cbc3304f8dccd0f8d09d34a","sha256":"14e3a40dc4da0e6d9b331976e9d5611d51b25e64f5010bbdfc6c8a32b23eb190","tags":"{}","yara":"rule + string: '{"result":{"community":"gamma","created":"2026-08-29T00:11:40.978433+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":"http://minio:9000/public-cache/14/e3/a4/14e3a40dc4da0e6d9b331976e9d5611d51b25e64f5010bbdfc6c8a32b23eb190adec7bf2f04d1b710cbc3304f8dccd0f8d09d34a564296ca17d5e063856e0b92c8ad6ca6?response-content-disposition=attachment%3Bfilename%3Dinfected&response-content-type=application%2Foctet-stream&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20260829%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260829T001141Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=f38991abb3474ac5c5d7630b2b2eac0e361bd140cdbdede2d30d52da32efccdc","first_seen":"2026-08-29T00:11:30.179957+00:00","id":"56607280072443305","instance_id":"3936300515643678","livescan_id":"6053794961344849","malware_family":"EICAR","matched_strings":[{"data":"test_live","identifier":"$u","length":9,"offset":69,"truncated":false}],"matched_strings_dropped":null,"md5":"564296ca17d5e063856e0b92c8ad6ca6","polyscore":null,"rule_name":"sdk_test_live","sha1":"adec7bf2f04d1b710cbc3304f8dccd0f8d09d34a","sha256":"14e3a40dc4da0e6d9b331976e9d5611d51b25e64f5010bbdfc6c8a32b23eb190","tags":"{}","yara":"rule sdk_test_live { strings: $u = \"test_live\" condition: $u }"},"status":"OK"} ' @@ -645,11 +717,11 @@ interactions: Connection: - keep-alive Content-Length: - - '1278' + - '1307' Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:52:42 GMT + - Sat, 29 Aug 2026 00:11:41 GMT Server: - gunicorn X-Billing-ID: @@ -658,7 +730,7 @@ interactions: code: 200 message: OK - request: - body: '{"result_ids":["47972203596447840"]}' + body: '{"result_ids":["56607280072443305"]}' headers: accept: - '*/*' @@ -680,7 +752,7 @@ interactions: uri: http://ai:9696/v3/hunt/live/list response: body: - string: '{"has_more":true,"limit":50,"result":[{"community":"gamma","created":"2026-08-25T22:52:42.306041+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":null,"first_seen":"2026-08-25T22:52:33.280967+00:00","id":"47972203596447840","instance_id":"83487019275094114","livescan_id":"35147411870661732","malware_family":"EICAR","matched_strings":null,"md5":"564296ca17d5e063856e0b92c8ad6ca6","polyscore":null,"rule_name":"sdk_test_live","sha1":"adec7bf2f04d1b710cbc3304f8dccd0f8d09d34a","sha256":"14e3a40dc4da0e6d9b331976e9d5611d51b25e64f5010bbdfc6c8a32b23eb190","tags":"{}","yara":null}],"status":"OK"} + string: '{"has_more":true,"limit":50,"result":[{"community":"gamma","created":"2026-08-29T00:11:40.978433+00:00","detections":{"benign":0,"malicious":1,"total":1},"download_url":null,"first_seen":"2026-08-29T00:11:30.179957+00:00","id":"56607280072443305","instance_id":"3936300515643678","livescan_id":"6053794961344849","malware_family":"EICAR","matched_strings":null,"matched_strings_dropped":null,"md5":"564296ca17d5e063856e0b92c8ad6ca6","polyscore":null,"rule_name":"sdk_test_live","sha1":"adec7bf2f04d1b710cbc3304f8dccd0f8d09d34a","sha256":"14e3a40dc4da0e6d9b331976e9d5611d51b25e64f5010bbdfc6c8a32b23eb190","tags":"{}","yara":null}],"status":"OK"} ' headers: @@ -691,11 +763,11 @@ interactions: Connection: - keep-alive Content-Length: - - '616' + - '645' Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:52:42 GMT + - Sat, 29 Aug 2026 00:11:41 GMT Server: - gunicorn X-Billing-ID: @@ -719,11 +791,11 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://ai:9696/v3/hunt/live?id=47972203596447840 + uri: http://ai:9696/v3/hunt/live?id=56607280072443305 response: body: string: '{"errors":null,"result":"Could not find requested live hunt result: - 47972203596447840.","status":"error"} + 56607280072443305.","status":"error"} ' headers: @@ -738,14 +810,14 @@ interactions: Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:52:42 GMT + - Sat, 29 Aug 2026 00:11:41 GMT Server: - gunicorn status: code: 404 message: NOT FOUND - request: - body: '{"rule_id":"14328545122436991"}' + body: '{"rule_id":"79693411044316426"}' headers: accept: - '*/*' @@ -767,7 +839,7 @@ interactions: uri: http://ai:9696/v3/hunt/rule/live response: body: - string: '{"result":{"created":"2026-08-25T22:52:32.002247+00:00","deleted":false,"description":null,"id":"14328545122436991","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T22:52:42.860277+00:00","name":"sdk-test_live","yara":"rule + string: '{"result":{"created":"2026-08-29T00:11:28.650982+00:00","deleted":false,"description":null,"id":"79693411044316426","livescan_created":null,"livescan_id":null,"modified":"2026-08-29T00:11:41.856223+00:00","name":"sdk-test_live","yara":"rule sdk_test_live { strings: $u = \"test_live\" condition: $u }"},"status":"OK"} ' @@ -783,7 +855,7 @@ interactions: Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:52:42 GMT + - Sat, 29 Aug 2026 00:11:41 GMT Server: - gunicorn X-Billing-ID: @@ -807,10 +879,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: GET - uri: http://ai:9696/v3/hunt/rule?id=14328545122436991&community=gamma + uri: http://ai:9696/v3/hunt/rule?id=79693411044316426&community=gamma response: body: - string: '{"result":{"created":"2026-08-25T22:52:32.002247+00:00","deleted":false,"description":null,"id":"14328545122436991","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T22:52:42.860277+00:00","name":"sdk-test_live","yara":"rule + string: '{"result":{"created":"2026-08-29T00:11:28.650982+00:00","deleted":false,"description":null,"id":"79693411044316426","livescan_created":null,"livescan_id":null,"modified":"2026-08-29T00:11:41.856223+00:00","name":"sdk-test_live","yara":"rule sdk_test_live { strings: $u = \"test_live\" condition: $u }"},"status":"OK"} ' @@ -826,7 +898,7 @@ interactions: Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:52:43 GMT + - Sat, 29 Aug 2026 00:11:42 GMT Server: - gunicorn X-Billing-ID: @@ -835,7 +907,7 @@ interactions: code: 200 message: OK - request: - body: '{"rule_id":"14328545122436991"}' + body: '{"rule_id":"79693411044316426"}' headers: accept: - '*/*' @@ -872,7 +944,7 @@ interactions: Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:52:43 GMT + - Sat, 29 Aug 2026 00:11:42 GMT Server: - gunicorn status: @@ -898,10 +970,10 @@ interactions: user-agent: - polyswarm_api/4.3.0 (x86_64-Linux-CPython-3.14.4) method: DELETE - uri: http://ai:9696/v3/hunt/rule?id=14328545122436991 + uri: http://ai:9696/v3/hunt/rule?id=79693411044316426 response: body: - string: '{"result":{"created":"2026-08-25T22:52:32.002247+00:00","deleted":true,"description":null,"id":"14328545122436991","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T22:52:43.118593+00:00","name":"sdk-test_live","yara":"rule + string: '{"result":{"created":"2026-08-29T00:11:28.650982+00:00","deleted":true,"description":null,"id":"79693411044316426","livescan_created":null,"livescan_id":null,"modified":"2026-08-29T00:11:42.102078+00:00","name":"sdk-test_live","yara":"rule sdk_test_live { strings: $u = \"test_live\" condition: $u }"},"status":"OK"} ' @@ -917,7 +989,7 @@ interactions: Content-Type: - application/json Date: - - Tue, 25 Aug 2026 22:52:43 GMT + - Sat, 29 Aug 2026 00:11:42 GMT Server: - gunicorn X-Billing-ID: From 8299433e07b615429b7a13946ab34a91f5fac87a Mon Sep 17 00:00:00 2001 From: Kyle Buchmiller Date: Sat, 29 Aug 2026 15:31:53 -0700 Subject: [PATCH 09/15] docs: finish the None-ambiguity correction, and make two tests assert their claims The previous commit said the withheld-count conflation now "reads the same way in both places". It did not: the flat claim -- None means nothing was withheld -- was corrected in one spec and left standing in specs/02-resources.md and in both copies of the resources.py comment. specs/02 contradicted itself four lines apart, telling a reader None means nothing was withheld and then that the list endpoints always send None by design, so for a list row it asserted both "nothing withheld" and "we did not look". All three now carry the same reading. Two tests did not test what they were named for: - test_dropped_is_independent_of_the_strings_list passed a count and never read it -- removing the kwarg left it passing identically, duplicating the test above it. Renamed for the property it actually pins and now asserts the count, so the populated-list-plus-count pairing is covered. - test_raw_json_still_carries_the_key compared a dict to itself: __init__ does self.json = content, so the assertion could not fail. Replaced with the property that could -- parsing must not drop keys from .json, and the parsed attributes must agree with the raw payload. Also records what is and is not pinned against a real server. The live pair is verified end to end; the historical pair follows by SYMMETRY, because the e2e stack does not reliably populate historical results inside a test window. Both specs stated the contract for "all four classes" as established fact, which is the same "asserts what we think the server returns" problem the e2e-first invariant exists to prevent. Softened, with the gap recorded in 99-open-questions.md alongside what would close it. specs/04-testing.md's module inventory gains this module, and known_good_test.py which was already missing. 195 tests pass. --- specs/02-resources.md | 13 +++++++++---- specs/04-testing.md | 2 ++ specs/05-downstream-contract.md | 9 +++++++++ specs/99-open-questions.md | 19 +++++++++++++++++++ src/polyswarm_api/resources.py | 16 ++++++++++------ test/hunt_matched_strings_test.py | 27 +++++++++++++++++++++------ 6 files changed, 70 insertions(+), 16 deletions(-) diff --git a/specs/02-resources.md b/specs/02-resources.md index f7c20d19..1a41f8ae 100644 --- a/specs/02-resources.md +++ b/specs/02-resources.md @@ -342,10 +342,15 @@ server, removed evidence, or a **list** endpoint, which sends an explicit `null` than fetch a blob per row), `[]` means *matched with no byte evidence*, and a populated list is evidence. **`matched_strings_dropped`.** A sibling `int`/`None`, parsed the same additive way: -how many matched strings the server's byte budget withheld from this result. `None` means -none were, which is also what a server predating the budget reports. It never accompanies -an empty `matched_strings` — a match's first string is never withheld — so a non-null -count always means "the list you have is short by this much". +how many matched strings the server's byte budget withheld from this result. `None` is +**ambiguous in the same way as `matched_strings`** and must be read the same way: on a +**detail** route it means nothing was withheld, on a **list** route that the route did not +look, and on an older server that the field did not exist. It is never a claim that the +evidence is complete — which matters because the list endpoints always send `None` (below), +so on a list row the two readings are not interchangeable. + +A non-null count always means "the list you have is short by this much". It never +accompanies an empty `matched_strings` — a match's first string is never withheld. Both `…List` subclasses inherit these from their parent's `__init__`, so all four hunt-result classes carry them — but on the list endpoints the values are always `None` diff --git a/specs/04-testing.md b/specs/04-testing.md index b81e5855..69dab4b6 100644 --- a/specs/04-testing.md +++ b/specs/04-testing.md @@ -24,6 +24,8 @@ How the test suite is organised. Three layers: pure unit tests (no HTTP at all - `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). +- `test/hunt_matched_strings_test.py` — pure-unit tests for the three-state `matched_strings` contract and its `matched_strings_dropped` sibling, across all four hunt-result classes. The endpoint behaviour they cannot see (that the detail route actually emits the keys and the list route sends `null`) is pinned live in `client_scan_test.py::test_live` / `async_client_test.py::test_async_live` — the e2e-first + pure-unit pairing invariant 1 asks for. +- `test/known_good_test.py` — pure-unit tests for the known-good resource fields. - `test/jmespath_test.py` — unit tests for `BaseJsonResource.jmespath`. - `test/vcr/*.vcr` — recorded cassettes. - `test/eicar.yara`, `test/malicious` — fixture files for upload tests. diff --git a/specs/05-downstream-contract.md b/specs/05-downstream-contract.md index 3f34200f..46243918 100644 --- a/specs/05-downstream-contract.md +++ b/specs/05-downstream-contract.md @@ -312,6 +312,15 @@ A populated `matched_strings` with a non-null count is the normal shape for a ve ruleset. The first string of a match is never withheld, so this can never accompany an empty list. +**How much of this is pinned against a real server.** The live pair +(`LiveHuntResult` / `LiveHuntResultList`) is verified end to end — `test_live` / +`test_async_live` assert the detail route carries evidence, that list rows do not, and +that the server serves `matched_strings_dropped`. The **historical** pair follows by +symmetry, not by measurement: the e2e stack does not reliably populate historical results +inside a test window, so nothing pins that those routes emit either key. The server +renders both pairs through the same helpers, which is why symmetry is a reasonable +assumption — but it is an assumption. See `specs/99-open-questions.md`. + **Evidence lives on the detail routes only.** `live_feed()` and `historical_results()` page over list endpoints and will always yield `None` here; fetch a single result (`live_result(id)` / `historical_result(id)`) to get the strings. diff --git a/specs/99-open-questions.md b/specs/99-open-questions.md index 5e1fb91c..25a9b803 100644 --- a/specs/99-open-questions.md +++ b/specs/99-open-questions.md @@ -168,3 +168,22 @@ def test_rescans(self): ``` Cleanup with `try/finally` + `except NotFoundException: pass` tolerates the ioc-cache divergence (GET-by-host can serve a cached id that DELETE-by-id no longer finds). When that artifact-index bug is fixed the `except` becomes redundant. + +## Historical hunt-result fields are not pinned against a live server + +**Status:** gap, blocked on the e2e stack. + +`matched_strings` / `matched_strings_dropped` are asserted end to end for the **live** +hunt pair only. `test_historical_results` tolerates an empty result set by design — the +stack does not reliably populate historical results inside a test window — so nothing +verifies that `/hunt/historical/results` emits either key, or that +`/hunt/historical/results/list` sends the explicit `null`. + +Both specs previously stated the contract for "all four classes" as established fact; +they now say the historical half follows by symmetry. The server renders both pairs +through the same serializer helpers, so the assumption is reasonable — but a fabricated +response asserts what we *think* the server returns (invariant 1), and that is the state +the historical half is in. + +**Action:** if the stack gains a way to produce a historical result deterministically, +add the same assertions to a historical live test and delete this entry. diff --git a/src/polyswarm_api/resources.py b/src/polyswarm_api/resources.py index b3cca3c8..4fd655ab 100644 --- a/src/polyswarm_api/resources.py +++ b/src/polyswarm_api/resources.py @@ -772,9 +772,11 @@ def __init__(self, content, api=None): # evidence), [...] (the evidence, a lower bound). See # specs/05-downstream-contract.md. self.matched_strings = content.get('matched_strings') - # How many matched strings the server's byte budget cost this result, or None - # when nothing was dropped (also what every result predating the budget reports). - # A sibling field, so `matched_strings` stays a plain list. + # How many matched strings the server's byte budget withheld from this result. + # A sibling field, so `matched_strings` stays a plain list. None is AMBIGUOUS in + # the same way as matched_strings: on a detail route it means nothing was withheld, + # on a list route that the route did not look, and on an older server that the + # field did not exist. It is not a claim that the evidence is complete. self.matched_strings_dropped = content.get('matched_strings_dropped') self.polyscore = content['polyscore'] self.malware_family = content['malware_family'] @@ -830,9 +832,11 @@ def __init__(self, content, api=None): # evidence), [...] (the evidence, a lower bound). See # specs/05-downstream-contract.md. self.matched_strings = content.get('matched_strings') - # How many matched strings the server's byte budget cost this result, or None - # when nothing was dropped (also what every result predating the budget reports). - # A sibling field, so `matched_strings` stays a plain list. + # How many matched strings the server's byte budget withheld from this result. + # A sibling field, so `matched_strings` stays a plain list. None is AMBIGUOUS in + # the same way as matched_strings: on a detail route it means nothing was withheld, + # on a list route that the route did not look, and on an older server that the + # field did not exist. It is not a claim that the evidence is complete. self.matched_strings_dropped = content.get('matched_strings_dropped') self.polyscore = content['polyscore'] self.malware_family = content['malware_family'] diff --git a/test/hunt_matched_strings_test.py b/test/hunt_matched_strings_test.py index c05bb822..d4e204c5 100644 --- a/test/hunt_matched_strings_test.py +++ b/test/hunt_matched_strings_test.py @@ -82,10 +82,20 @@ def test_populated_list_is_passed_through_verbatim(cls): @pytest.mark.parametrize("cls", ALL_CLASSES) -def test_raw_json_still_carries_the_key(cls): - """`.json` is part of the contract, so JSON-mode consumers see it without SDK work.""" - result = cls(_content(cls, matched_strings=_STRINGS)) - assert result.json["matched_strings"] == _STRINGS +def test_parsing_does_not_mutate_the_raw_json(cls): + """`.json` is part of the contract: JSON-mode consumers read the server's payload + unchanged, so parsing must not reshape or strip what it read. + + (Asserting `result.json[k] == content[k]` alone would be tautological -- __init__ + assigns `self.json = content` -- so this compares the parsed attributes against the + raw dict instead, which is the property that would actually break.) + """ + content = _content(cls, matched_strings=_STRINGS, matched_strings_dropped=19) + result = cls(content) + assert result.json is content + assert result.matched_strings == content["matched_strings"] + assert result.matched_strings_dropped == content["matched_strings_dropped"] + assert set(content) <= set(result.json), "parsing must not drop keys from .json" @pytest.mark.parametrize("cls", ALL_CLASSES) @@ -102,8 +112,13 @@ def test_dropped_is_none_when_absent(cls): @pytest.mark.parametrize("cls", ALL_CLASSES) -def test_dropped_is_independent_of_the_strings_list(cls): - """A truncated list is still a list; the count is what says it is short.""" +def test_a_populated_list_and_a_count_coexist(cls): + """The normal shape for a verbose ruleset, and the pairing the contract turns on. + + A truncated list is still a list -- nothing marks it short from the inside -- so the + count is the only thing that says so. Both must survive parsing together. + """ result = cls(_content(cls, matched_strings=_STRINGS, matched_strings_dropped=19)) assert isinstance(result.matched_strings, list) assert len(result.matched_strings) == 2 + assert result.matched_strings_dropped == 19 From b482498d5cb428a3f43461af34c12004e9581303 Mon Sep 17 00:00:00 2001 From: Kyle Buchmiller Date: Sat, 29 Aug 2026 23:22:15 -0700 Subject: [PATCH 10/15] test: pin the per-entry shape against the server, and fix the class-route framing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-entry dict shape is contract -- specs/05 documents offset, identifier, length, data and truncated -- but the only assertion on it compared against a hand-written fixture, which is what we THINK the server sends rather than what it does. A server-side key rename would have passed the whole suite VCR-off. Both cassettes already carry a real entry, so the live tests now assert the key set against the recorded response, which costs nothing and closes the gap. Separately, the spec framed the `…List` subclasses as the list-route parsers. That is not true of the live pair: live_feed builds its request with LiveHuntResult.list(...), which hits /hunt/live/list but parses rows as LiveHuntResult. LiveHuntResultList is only ever a delete builder and is never instantiated from a response; only historical_results yields …List instances. The framing matters rather than being pedantry: the section exists to say that None is ambiguous, and a reader who takes the class as the route concludes a LiveHuntResult must have come from the detail route and reads its None as "nothing to show". Corrected in the spec and in the test module's comment. 195 tests pass. --- specs/02-resources.md | 9 +++++++++ test/async_client_test.py | 5 +++++ test/client_scan_test.py | 5 +++++ test/hunt_matched_strings_test.py | 5 +++-- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/specs/02-resources.md b/specs/02-resources.md index 1a41f8ae..7f365d22 100644 --- a/specs/02-resources.md +++ b/specs/02-resources.md @@ -356,6 +356,15 @@ Both `…List` subclasses inherit these from their parent's `__init__`, so all f hunt-result classes carry them — but on the list endpoints the values are always `None` by design. +**Do not read the class as telling you the route.** For the live pair it does not: +`live_feed()` builds its request with `LiveHuntResult.list(...)`, which hits +`/hunt/live/list` but parses rows as **`LiveHuntResult`** (see +[`03-endpoints.md`](./03-endpoints.md)). `LiveHuntResultList` is only ever a *delete* +builder and is never instantiated from a response; only `historical_results()` yields +`…List` instances. So a `LiveHuntResult` carrying `matched_strings is None` may well have +come from the list route — which is exactly why that `None` is ambiguous and must not be +read as "nothing to show". + ### `LocalArtifact` A file-system or in-memory artifact prepared for upload. Constructed via: diff --git a/test/async_client_test.py b/test/async_client_test.py index e780332e..1b99a4e3 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -740,6 +740,11 @@ async def test_async_live(self, uid): 'server must serve the withheld-count field' assert result.matched_strings_dropped is None, \ 'nothing withheld for a match this small' + # The per-entry shape is contract (specs/05) but was pinned only by a hand-written + # fixture -- i.e. what we THINK the server sends. This asserts it against what the + # server actually sent, so a key rename cannot pass the suite VCR-off. + assert set(result.matched_strings[0]) == { + 'offset', 'identifier', 'length', 'data', 'truncated'}, result.matched_strings[0] await api.live_feed_delete([result_id]) with pytest.raises(exceptions.NotFoundException): diff --git a/test/client_scan_test.py b/test/client_scan_test.py index 4b9db8dc..412aaeb3 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -513,6 +513,11 @@ def test_live(self): 'server must serve the withheld-count field' assert result.matched_strings_dropped is None, \ 'nothing withheld for a match this small' + # The per-entry shape is contract (specs/05) but was pinned only by a hand-written + # fixture -- i.e. what we THINK the server sends. This asserts it against what the + # server actually sent, so a key rename cannot pass the suite VCR-off. + assert set(result.matched_strings[0]) == { + 'offset', 'identifier', 'length', 'data', 'truncated'}, result.matched_strings[0] api.live_feed_delete([result_id]) with pytest.raises(exceptions.NotFoundException): diff --git a/test/hunt_matched_strings_test.py b/test/hunt_matched_strings_test.py index d4e204c5..798623ab 100644 --- a/test/hunt_matched_strings_test.py +++ b/test/hunt_matched_strings_test.py @@ -35,8 +35,9 @@ "data": "4D 5A 90 00 ...", "truncated": True}, ] -# Both concrete classes plus their list-endpoint subclasses, which inherit __init__ -# and must therefore behave identically. +# Both concrete classes plus their `…List` subclasses, which inherit __init__ and must +# behave identically. NB the subclass does not imply the route: live_feed parses list +# rows as LiveHuntResult, and LiveHuntResultList is only ever a delete builder. ALL_CLASSES = [ LiveHuntResult, LiveHuntResultList, HistoricalHuntResult, HistoricalHuntResultList, From 024449d83f02afc5281431280b027ffc8a2ae8b0 Mon Sep 17 00:00:00 2001 From: Kyle Buchmiller Date: Sun, 30 Aug 2026 12:51:53 -0700 Subject: [PATCH 11/15] docs: the ...List classes DO parse responses -- delete responses The previous commit corrected the class-to-route framing and got the replacement wrong. It said LiveHuntResultList "is never instantiated from a response; only historical_results() yields ...List instances". Both halves are false: _build_request sets result_parser=cls, so the paginated body returned by DELETE /hunt/live/list is parsed through LiveHuntResultList. The cassette re-recorded in this PR contains exactly that exchange. That makes delete responses a FOURTH source of None -- alongside an older server, a list route and removed evidence -- and none of the three places that enumerate the causes listed it. A caller iterating live_feed_delete() holds ...List objects with both fields None while the spec told them that class only comes from historical_results(), which is the worst combination: an ambiguous value plus a mental model that resolves it wrongly. Corrected in specs/02, the three-state table in specs/05, and both copies of the resources.py comment. 195 tests pass. --- specs/02-resources.md | 13 +++++++++---- specs/05-downstream-contract.md | 2 +- src/polyswarm_api/resources.py | 20 ++++++++++---------- test/hunt_matched_strings_test.py | 2 +- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/specs/02-resources.md b/specs/02-resources.md index 7f365d22..8ad3e547 100644 --- a/specs/02-resources.md +++ b/specs/02-resources.md @@ -360,10 +360,15 @@ by design. `live_feed()` builds its request with `LiveHuntResult.list(...)`, which hits `/hunt/live/list` but parses rows as **`LiveHuntResult`** (see [`03-endpoints.md`](./03-endpoints.md)). `LiveHuntResultList` is only ever a *delete* -builder and is never instantiated from a response; only `historical_results()` yields -`…List` instances. So a `LiveHuntResult` carrying `matched_strings is None` may well have -come from the list route — which is exactly why that `None` is ambiguous and must not be -read as "nothing to show". +**builder** — but the delete response is parsed **through** it (`_build_request` sets +`result_parser=cls`), so `live_feed_delete()` and `historical_results_delete()` both yield +`…List` instances, with both fields `None`. From a *read*, only `historical_results()` +yields them. + +So `None` reaches a caller from four places, not three: an older server, deleted evidence, +a list route, and a **delete response**. A `LiveHuntResult` carrying +`matched_strings is None` may well have come from the list route — which is exactly why +that `None` is ambiguous and must not be read as "nothing to show". ### `LocalArtifact` diff --git a/specs/05-downstream-contract.md b/specs/05-downstream-contract.md index 46243918..745a52cc 100644 --- a/specs/05-downstream-contract.md +++ b/specs/05-downstream-contract.md @@ -281,7 +281,7 @@ Three values are possible and consumers **must not** collapse them: | Value | Meaning | |---|---| -| `None` | Not reported. The **list** endpoints send the key as an explicit `null` rather than fetch a blob per row; a server predating the field omits it entirely; and stored evidence may have been deleted. `.get()` collapses all three. "We don't know", *not* "there was nothing". | +| `None` | Not reported. **Four** causes, which `.get()` collapses: the **list** endpoints send an explicit `null` rather than fetch a blob per row; **delete** responses (`live_feed_delete` / `historical_results_delete`) are parsed through the `…List` classes and carry `null` the same way; a server predating the field omits it entirely; and stored evidence may have been deleted. "We don't know", *not* "there was nothing". | | `[]` | The rule matched and there is no byte evidence to show — a rule with no strings section, one whose matching strings are all `private`, or one that matched on absence (`not $a`, `none of them`). | | `[…]` | The evidence. A **lower bound**, not a match count: `any of them` prints only the strings that hit, `private` strings never appear, and the server may withhold some past a size limit (see `matched_strings_dropped`). | diff --git a/src/polyswarm_api/resources.py b/src/polyswarm_api/resources.py index 4fd655ab..73ea19ba 100644 --- a/src/polyswarm_api/resources.py +++ b/src/polyswarm_api/resources.py @@ -768,15 +768,15 @@ def __init__(self, content, api=None): self.tags = content['tags'] # `.get()`, not a subscript: the key is additive, so a server predating it omits # it and a subscript would raise on every result. Three distinct states -- None - # (not reported: an older server, or a list endpoint), [] (matched, no byte - # evidence), [...] (the evidence, a lower bound). See - # specs/05-downstream-contract.md. + # (not reported: an older server, a list route, a DELETE response, or removed + # evidence), [] (matched, no byte evidence), [...] (the evidence, a lower bound). + # See specs/05-downstream-contract.md. self.matched_strings = content.get('matched_strings') # How many matched strings the server's byte budget withheld from this result. # A sibling field, so `matched_strings` stays a plain list. None is AMBIGUOUS in # the same way as matched_strings: on a detail route it means nothing was withheld, - # on a list route that the route did not look, and on an older server that the - # field did not exist. It is not a claim that the evidence is complete. + # on a list route or a DELETE response that the route did not look, and on an older + # server that the field did not exist. Not a claim the evidence is complete. self.matched_strings_dropped = content.get('matched_strings_dropped') self.polyscore = content['polyscore'] self.malware_family = content['malware_family'] @@ -828,15 +828,15 @@ def __init__(self, content, api=None): self.tags = content['tags'] # `.get()`, not a subscript: the key is additive, so a server predating it omits # it and a subscript would raise on every result. Three distinct states -- None - # (not reported: an older server, or a list endpoint), [] (matched, no byte - # evidence), [...] (the evidence, a lower bound). See - # specs/05-downstream-contract.md. + # (not reported: an older server, a list route, a DELETE response, or removed + # evidence), [] (matched, no byte evidence), [...] (the evidence, a lower bound). + # See specs/05-downstream-contract.md. self.matched_strings = content.get('matched_strings') # How many matched strings the server's byte budget withheld from this result. # A sibling field, so `matched_strings` stays a plain list. None is AMBIGUOUS in # the same way as matched_strings: on a detail route it means nothing was withheld, - # on a list route that the route did not look, and on an older server that the - # field did not exist. It is not a claim that the evidence is complete. + # on a list route or a DELETE response that the route did not look, and on an older + # server that the field did not exist. Not a claim the evidence is complete. self.matched_strings_dropped = content.get('matched_strings_dropped') self.polyscore = content['polyscore'] self.malware_family = content['malware_family'] diff --git a/test/hunt_matched_strings_test.py b/test/hunt_matched_strings_test.py index 798623ab..a54bdd6c 100644 --- a/test/hunt_matched_strings_test.py +++ b/test/hunt_matched_strings_test.py @@ -37,7 +37,7 @@ # Both concrete classes plus their `…List` subclasses, which inherit __init__ and must # behave identically. NB the subclass does not imply the route: live_feed parses list -# rows as LiveHuntResult, and LiveHuntResultList is only ever a delete builder. +# rows as LiveHuntResult, while the ...List classes parse DELETE responses. ALL_CLASSES = [ LiveHuntResult, LiveHuntResultList, HistoricalHuntResult, HistoricalHuntResultList, From 28cabee7825b94af94060770491743e1a52b3cdd Mon Sep 17 00:00:00 2001 From: Kyle Buchmiller Date: Sun, 30 Aug 2026 14:09:13 -0700 Subject: [PATCH 12/15] test: make both raw-json assertions falsifiable The previous commit replaced a tautological assertion and moved the tautology rather than removing it: `assert result.json is content` followed by `set(content) <= set(result.json)` reduces to `set(content) <= set(content)`. Nothing checked the property the test is named for either, since self.json keeps a live reference to the dict that was passed in. Now compared against an independent deepcopy, so "parsing did not mutate the payload" and "parsing did not drop keys" can both actually fail. It also survives a future change that defensively copies content, which the identity assertion would have failed for the wrong reason. Also records that the non-null matched_strings_dropped path is not pinned against a live server. The live tests assert only the None arm -- correctly, since the per-test rule withholds nothing -- so the two strongest claims the contract makes about the field rest on hand-written dicts. Producing an over-budget match on the e2e stack is disproportionate to what it would pin, so this is recorded in 99-open-questions.md rather than tested, alongside the historical-pair gap. 195 tests pass. --- specs/99-open-questions.md | 21 +++++++++++++++++++++ test/hunt_matched_strings_test.py | 16 +++++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/specs/99-open-questions.md b/specs/99-open-questions.md index 25a9b803..5e5e2515 100644 --- a/specs/99-open-questions.md +++ b/specs/99-open-questions.md @@ -187,3 +187,24 @@ the historical half is in. **Action:** if the stack gains a way to produce a historical result deterministically, add the same assertions to a historical live test and delete this entry. + +## The non-null `matched_strings_dropped` path is not pinned against a live server + +**Status:** gap, probably not worth closing with a test. + +`test_live` / `test_async_live` assert only the `is None` arm — correctly, since the +per-test rule is small and the server withholds nothing from it. So the two strongest +claims `05-downstream-contract.md` makes about this field rest entirely on hand-written +pure-unit dicts: + +- a non-null count means "the list you have is short by this much", and +- it can never accompany an empty `matched_strings`, because a match's first string is + never withheld. + +That is the same "asserts what we *think* the server returns" gap invariant 1 exists to +close, and it sits alongside the historical-pair entry above. + +Producing an over-budget match on the e2e stack means a rule whose matches exceed the +server's per-hunt byte budget across a single artifact — engineering a fixture for that is +disproportionate to what it would pin. **Recorded rather than tested, deliberately.** If a +stack fixture ever produces one cheaply, assert both claims there and delete this entry. diff --git a/test/hunt_matched_strings_test.py b/test/hunt_matched_strings_test.py index a54bdd6c..0c6f0127 100644 --- a/test/hunt_matched_strings_test.py +++ b/test/hunt_matched_strings_test.py @@ -7,6 +7,8 @@ "the rule matched with no byte evidence". """ +import copy + import pytest from polyswarm_api.resources import ( @@ -91,12 +93,16 @@ def test_parsing_does_not_mutate_the_raw_json(cls): assigns `self.json = content` -- so this compares the parsed attributes against the raw dict instead, which is the property that would actually break.) """ - content = _content(cls, matched_strings=_STRINGS, matched_strings_dropped=19) + raw = _content(cls, matched_strings=_STRINGS, matched_strings_dropped=19) + content = copy.deepcopy(raw) result = cls(content) - assert result.json is content - assert result.matched_strings == content["matched_strings"] - assert result.matched_strings_dropped == content["matched_strings_dropped"] - assert set(content) <= set(result.json), "parsing must not drop keys from .json" + # Compared against an independent copy, so both of these can actually fail. Asserting + # `result.json is content` and then `set(content) <= set(result.json)` only restated + # the identity -- the tautology moved rather than went away. + assert content == raw, "parsing must not mutate the payload it was handed" + assert set(raw) <= set(result.json), "parsing must not drop keys from .json" + assert result.matched_strings == raw["matched_strings"] + assert result.matched_strings_dropped == raw["matched_strings_dropped"] @pytest.mark.parametrize("cls", ALL_CLASSES) From a4934b2314c40cb4e3ff1d7de6528cdbf699a74d Mon Sep 17 00:00:00 2001 From: Kyle Buchmiller Date: Mon, 31 Aug 2026 09:25:01 -0700 Subject: [PATCH 13/15] test: assert the list null on .json, deep-copy the fixture, trim duplicated comments Two assertions could not fail: - The list-row check read `my_results[0].matched_strings is None`, two lines above a comment arguing that `is None` cannot distinguish a served null from an absent key -- which is why the count below it reads .json. The same argument applies here and specs/05 makes the stronger claim (an explicit null), so it now reads .json too. - test_populated_list_is_passed_through_verbatim passed the module-level _STRINGS into the content dict, which stores it BY REFERENCE, so the assertions compared the object with itself and no in-place reshape could fail them. Deep-copied, the same fix test_parsing_does_not_mutate_the_raw_json already needed. Also trims the two resources.py comment blocks, which restated the contract spec nearly verbatim at both call sites. Three copies of one argument is three places to drift, and several commits in this PR were spent reconciling exactly that. The decision and a pointer are enough; specs/05 owns the reasoning. 195 tests pass. --- src/polyswarm_api/resources.py | 26 ++++++-------------------- test/async_client_test.py | 7 +++++-- test/client_scan_test.py | 7 +++++-- test/hunt_matched_strings_test.py | 9 +++++++-- 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/src/polyswarm_api/resources.py b/src/polyswarm_api/resources.py index 73ea19ba..b736c24c 100644 --- a/src/polyswarm_api/resources.py +++ b/src/polyswarm_api/resources.py @@ -766,17 +766,10 @@ def __init__(self, content, api=None): self.sha1 = content.get('sha1') self.rule_name = content['rule_name'] self.tags = content['tags'] - # `.get()`, not a subscript: the key is additive, so a server predating it omits - # it and a subscript would raise on every result. Three distinct states -- None - # (not reported: an older server, a list route, a DELETE response, or removed - # evidence), [] (matched, no byte evidence), [...] (the evidence, a lower bound). - # See specs/05-downstream-contract.md. + # `.get()`, not a subscript -- both keys are additive, so an older server omits + # them. None is AMBIGUOUS on both (four causes) and is never a claim that the + # evidence is complete. Contract: specs/05-downstream-contract.md. self.matched_strings = content.get('matched_strings') - # How many matched strings the server's byte budget withheld from this result. - # A sibling field, so `matched_strings` stays a plain list. None is AMBIGUOUS in - # the same way as matched_strings: on a detail route it means nothing was withheld, - # on a list route or a DELETE response that the route did not look, and on an older - # server that the field did not exist. Not a claim the evidence is complete. self.matched_strings_dropped = content.get('matched_strings_dropped') self.polyscore = content['polyscore'] self.malware_family = content['malware_family'] @@ -826,17 +819,10 @@ def __init__(self, content, api=None): self.created = core.parse_isoformat(content['created']) self.rule_name = content['rule_name'] self.tags = content['tags'] - # `.get()`, not a subscript: the key is additive, so a server predating it omits - # it and a subscript would raise on every result. Three distinct states -- None - # (not reported: an older server, a list route, a DELETE response, or removed - # evidence), [] (matched, no byte evidence), [...] (the evidence, a lower bound). - # See specs/05-downstream-contract.md. + # `.get()`, not a subscript -- both keys are additive, so an older server omits + # them. None is AMBIGUOUS on both (four causes) and is never a claim that the + # evidence is complete. Contract: specs/05-downstream-contract.md. self.matched_strings = content.get('matched_strings') - # How many matched strings the server's byte budget withheld from this result. - # A sibling field, so `matched_strings` stays a plain list. None is AMBIGUOUS in - # the same way as matched_strings: on a detail route it means nothing was withheld, - # on a list route or a DELETE response that the route did not look, and on an older - # server that the field did not exist. Not a claim the evidence is complete. self.matched_strings_dropped = content.get('matched_strings_dropped') self.polyscore = content['polyscore'] self.malware_family = content['malware_family'] diff --git a/test/async_client_test.py b/test/async_client_test.py index 1b99a4e3..157f9fed 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -732,8 +732,11 @@ async def test_async_live(self, uid): # The pure-unit tests exercise dict.get and would pass identically if the # server never grew the field; only a cassette shows what it actually sent. assert result.matched_strings, 'detail route should carry the yara evidence' - assert my_results[0].matched_strings is None, \ - 'list rows do not carry the evidence -- it is a per-row blob fetch' + # On .json for the same reason as the count below: the attribute cannot + # distinguish a served null from an absent key, and specs/05 claims the + # list route sends an explicit null. + assert my_results[0].json['matched_strings'] is None, \ + 'list rows carry the key as null, not the evidence' # On .json, not the attribute: `is None` cannot tell a served null from an # absent key, and what needs pinning is that the server SENDS this field. assert 'matched_strings_dropped' in result.json, \ diff --git a/test/client_scan_test.py b/test/client_scan_test.py index 412aaeb3..0ed7bbab 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -505,8 +505,11 @@ def test_live(self): # The pure-unit tests exercise dict.get and would pass identically if the # server never grew the field; only a cassette shows what it actually sent. assert result.matched_strings, 'detail route should carry the yara evidence' - assert my_results[0].matched_strings is None, \ - 'list rows do not carry the evidence -- it is a per-row blob fetch' + # On .json for the same reason as the count below: the attribute cannot + # distinguish a served null from an absent key, and specs/05 claims the + # list route sends an explicit null. + assert my_results[0].json['matched_strings'] is None, \ + 'list rows carry the key as null, not the evidence' # On .json, not the attribute: `is None` cannot tell a served null from an # absent key, and what needs pinning is that the server SENDS this field. assert 'matched_strings_dropped' in result.json, \ diff --git a/test/hunt_matched_strings_test.py b/test/hunt_matched_strings_test.py index 0c6f0127..d2149486 100644 --- a/test/hunt_matched_strings_test.py +++ b/test/hunt_matched_strings_test.py @@ -77,8 +77,13 @@ def test_empty_list_is_preserved_and_is_not_none(cls): @pytest.mark.parametrize("cls", ALL_CLASSES) def test_populated_list_is_passed_through_verbatim(cls): - """The SDK does not reshape entries — `data` in particular stays as yara rendered it.""" - result = cls(_content(cls, matched_strings=_STRINGS)) + """The SDK does not reshape entries — `data` in particular stays as yara rendered it. + + Deep-copied: passing the module-level _STRINGS stores it BY REFERENCE, so these + assertions compared the object with itself and no in-place reshape could fail them. + Same trap as test_parsing_does_not_mutate_the_raw_json. + """ + result = cls(_content(cls, matched_strings=copy.deepcopy(_STRINGS))) assert result.matched_strings == _STRINGS assert result.matched_strings[0]["identifier"] == "$stub" assert result.matched_strings[1]["truncated"] is True From 13c191cb8817168e485fdd0eaacfa0975dea20d3 Mon Sep 17 00:00:00 2001 From: Kyle Buchmiller Date: Thu, 3 Sep 2026 16:23:01 -0700 Subject: [PATCH 14/15] docs: let the four-cause table own the None enumeration 02-resources.md states that None reaches a caller from four places, not three -- and then two summaries in that same file enumerate three, as does the matched_strings_dropped paragraph in 05. The file contradicted itself within one section, which is the defect an earlier commit was written to fix. Rather than restate the list a fourth time, the summaries now defer to the one table that owns it. A pointer cannot drift out of step with what it points at. --- specs/02-resources.md | 10 +++++----- specs/05-downstream-contract.md | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/specs/02-resources.md b/specs/02-resources.md index 142fb68e..18c28a20 100644 --- a/specs/02-resources.md +++ b/specs/02-resources.md @@ -338,15 +338,15 @@ It is **three-state** and the states are not interchangeable; the table, the per dict shape and the lower-bound caveat live in [`05-downstream-contract.md`](./05-downstream-contract.md) §"`matched_strings` on hunt results" — read it there rather than inferring from the -attribute. The short version a parser needs: `None` means *not reported* (an older -server, removed evidence, or a **list** endpoint, which sends an explicit `null` rather -than fetch a blob per row), `[]` means *matched with no byte evidence*, and a populated list is evidence. +attribute. The short version a parser needs: `None` means *not reported* — **four** +distinct causes, enumerated in that table and revisited under "four places, not three" +below — `[]` means *matched with no byte evidence*, and a populated list is evidence. **`matched_strings_dropped`.** A sibling `int`/`None`, parsed the same additive way: how many matched strings the server's byte budget withheld from this result. `None` is **ambiguous in the same way as `matched_strings`** and must be read the same way: on a -**detail** route it means nothing was withheld, on a **list** route that the route did not -look, and on an older server that the field did not exist. It is never a claim that the +**detail** route it means nothing was withheld; under any of the other three causes it +means nothing looked. It is never a claim that the evidence is complete — which matters because the list endpoints always send `None` (below), so on a list row the two readings are not interchangeable. diff --git a/specs/05-downstream-contract.md b/specs/05-downstream-contract.md index 464686fc..5bbc461a 100644 --- a/specs/05-downstream-contract.md +++ b/specs/05-downstream-contract.md @@ -306,8 +306,8 @@ list is otherwise indistinguishable from a complete one: a consumer reading twel entries would conclude the rule hit twelve times when it hit thirty-one. `None` carries the same ambiguity as `matched_strings` itself and should be read the same -way: on a **detail** route it means nothing was withheld, but on a **list** route it means -the route did not look, and on an older server it means the field did not exist. It is not +way: on a **detail** route it means nothing was withheld; under any of the other three +causes in the table above, it means nothing looked. It is not a claim that the evidence is complete. It is deliberately a **sibling** rather than a key inside `matched_strings`, which stays a plain list. From 1d2dab9bc47fdd747fa1c9e3d86a95c8162607d3 Mon Sep 17 00:00:00 2001 From: Kyle Buchmiller Date: Thu, 3 Sep 2026 16:23:02 -0700 Subject: [PATCH 15/15] test: say what a null matched_strings means on a green stack The live assertions are the only thing distinguishing served evidence from a served null -- the pure-unit tier exercises dict.get and passes identically whether or not the server ever grew the field. So the assertion stays exactly as strict. Only the message widens. The value is null unless the producer that emits the strings is deployed, so the likeliest cause of this failing is an image that predates it -- while everything else about the stack looks healthy, which makes the failure read as a server bug. The message now names the real cause. --- test/async_client_test.py | 5 ++++- test/client_scan_test.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/test/async_client_test.py b/test/async_client_test.py index 024d777c..958cc768 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -868,7 +868,10 @@ async def test_async_live(self, uid): # The list/detail split, pinned against the real server rather than prose. # The pure-unit tests exercise dict.get and would pass identically if the # server never grew the field; only a cassette shows what it actually sent. - assert result.matched_strings, 'detail route should carry the yara evidence' + assert result.matched_strings, ( + 'detail route should carry the yara evidence. A null here against an\n' + 'otherwise-green stack means the analyzer image predates the change\n' + 'that emits `strings` -- check the analyzer, not this repo.') # On .json for the same reason as the count below: the attribute cannot # distinguish a served null from an absent key, and specs/05 claims the # list route sends an explicit null. diff --git a/test/client_scan_test.py b/test/client_scan_test.py index 7e1b7578..36efc1fa 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -511,7 +511,10 @@ def test_live(self): # The list/detail split, pinned against the real server rather than prose. # The pure-unit tests exercise dict.get and would pass identically if the # server never grew the field; only a cassette shows what it actually sent. - assert result.matched_strings, 'detail route should carry the yara evidence' + assert result.matched_strings, ( + 'detail route should carry the yara evidence. A null here against an\n' + 'otherwise-green stack means the analyzer image predates the change\n' + 'that emits `strings` -- check the analyzer, not this repo.') # On .json for the same reason as the count below: the attribute cannot # distinguish a served null from an absent key, and specs/05 claims the # list route sends an explicit null.