diff --git a/specs/02-resources.md b/specs/02-resources.md index 181bbd81..fb626629 100644 --- a/specs/02-resources.md +++ b/specs/02-resources.md @@ -36,7 +36,9 @@ BaseResource # holds .api, ._content, .parse_result cla ├── HistoricalHunt # /hunt/historical ├── HistoricalHuntResult / List # /hunt/historical/results (+ /results/list) ├── LiveHuntResult / List # /hunt/live (+ /hunt/live/list) + ├── LiveHuntResultCounts # /hunt/live/results/count ├── YaraRuleset # /hunt/rule + ├── YaraRulesetFavorite # /hunt/rule/favorite ├── Tag, MalwareFamily, TagLink # /tags/tag, /tags/family, /tags/link ├── AssertionsJob, VotesJob # /consumer/assertions-job, /consumer/votes-job ├── SandboxTask, SandboxProvider # /sandbox/sandboxtask, /sandbox/provider @@ -343,7 +345,9 @@ Holds `handle`, `artifact_name`, `artifact_type`, `sha256`, `sha1`, `md5`. Also Several resources add domain-specific classmethods on top of the standard CRUD set: - `IOC` — `iocs_by_hash`, `ioc_search`, `check_known_hosts`, `create_known_good`, `create_known_bad`, `update_known_good`, `delete_known_good`. -- `LiveYaraRuleset` / `HistoricalHunt` / `YaraRuleset` — standard CRUD plus list/delete-batch variants. +- `LiveYaraRuleset` / `HistoricalHunt` / `YaraRuleset` — standard CRUD plus list/delete-batch variants. `YaraRuleset` also parses the hunt-page tracking fields (`favorite`, `favorited_at`, `rule_count` — `None` means the server had no answer, distinct from 0 — `historical_hunt_count`, `new_results_count`), and `HistoricalHunt` the source-rule provenance (`rule_id`, `rule_modified`, and the tri-state `source_rule_changed` — `None` is "unknown", never "unchanged"). All additive `.get()` parses; an older server leaves them `None`. +- `YaraRulesetFavorite` — `RESOURCE_ENDPOINT = '/hunt/rule/favorite'`, **`RESOURCE_ID_KEYS = []`**: a deliberate deviation from the default `['id']`, because the server reads the toggle exclusively from the PUT **body** — emptying the key list is the only thing routing `id` (plus `favorite`/`community`) into `json` instead of the query string. `favorite` serialises as `1`/`0` (the `_params` bool→int coercion), which the server's boolean parser accepts. Response carries the star state plus the team's `favorites_used`/`favorites_limit`. Pinned by `test/hunt_tracking_builder_test.py`. +- `LiveHuntResultCounts` — `RESOURCE_ENDPOINT = '/hunt/live/results/count'`, GET-only; `since` (seconds) rides the query and is omitted when unset (server default window). `counts` is a list of `{livescan_id, count}` with `livescan_id` as a digit string — the same join key `YaraRuleset.livescan_id` carries; a hunt absent from `counts` collected 0. - `SandboxTask` — `create_file`, `update_file`, `latest`, `my_tasks` for the various sandbox-submission shapes. **No `upload_file` instance method** in 4.0. - `Sample` — `create` with `endpoint_fmt={'sha256': sha256}` for the URL-parametrised path. - `Webhook` — `test(api, webhook_id)` for the test-payload endpoint. diff --git a/specs/03-endpoints.md b/specs/03-endpoints.md index eea14fd5..7f1e2e93 100644 --- a/specs/03-endpoints.md +++ b/specs/03-endpoints.md @@ -66,6 +66,7 @@ Internal-only CRUD for the `/known-good` binary resource (distinct from the IOC | `live_stop(rule_id)` | `LiveYaraRuleset.delete` | | `live_feed_delete(result_ids)` | `LiveHuntResultList.delete` (catches `NoResultsException`) | | `live_result(result_id)` | `LiveHuntResult.get` | +| `live_results_count(since=None)` | `LiveHuntResultCounts.get` — per-live-hunt result counts in a window, grouped by `livescan_id` (digit strings, the same join key `YaraRuleset.livescan_id` carries); a hunt absent from `counts` collected 0 | ### Historical hunts @@ -86,6 +87,7 @@ Internal-only CRUD for the `/known-good` binary resource (distinct from the IOC | `ruleset_create(name, rules, description=None)` | `YaraRuleset.create` | | `ruleset_get(ruleset_id=None)` | `YaraRuleset.get` | | `ruleset_update(ruleset_id, name=None, rules=None, description=None)` | `YaraRuleset.update` | +| `ruleset_favorite(ruleset_id, favorite=True)` | `YaraRulesetFavorite.update` — idempotent star/unstar; the response carries the team's `favorites_used`/`favorites_limit`, and an over-budget star is refused with a machine-readable `FAVORITE_LIMIT` error. The id rides the PUT **body**, not the query (see the `RESOURCE_ID_KEYS = []` note in specs/02) | | `ruleset_delete(ruleset_id)` | `YaraRuleset.delete` | | `tag_link_get(sha256)` | `TagLink.get` | | `tag_link_update(sha256, tags=None, families=None, emerging=None, remove=False)` | `TagLink.update` | @@ -186,10 +188,10 @@ refusal. | `iocs_by_hash(hash_type, hash_value, hide_known_good=False, beta=False)` | `IOC.iocs_by_hash` | | `search_by_ioc(ip=None, domain=None, ttp=None, imphash=None)` | `IOC.ioc_search` | | `check_known_hosts(ips=[], domains=[])` | `IOC.check_known_hosts` | -| `live_feed(since=None, …)` | `LiveHuntResult.list` | +| `live_feed(since=None, …, livescan_id=None)` | `LiveHuntResult.list` — `livescan_id` scopes the feed to one live hunt (the hunt-page per-ruleset feed) | | `historical_list(since=None)` | `HistoricalHunt.list` | | `historical_results(hunt=None, …)` | `HistoricalHuntResultList.get` | -| `ruleset_list()` | `YaraRuleset.list` | +| `ruleset_list(name=None, status=None, favorites_only=None, has_new_results=None, since=None, include_counts=None)` | `YaraRuleset.list` — the hunt-page filters, conjunctive and optional; unset filters are omitted from the query so the no-filter request is byte-compatible with the old contract | | `tag_list()` | `Tag.list` | | `family_list()` | `MalwareFamily.list` | | `assertions_list(engine_id)` | `AssertionsJob.list` | diff --git a/specs/04-testing.md b/specs/04-testing.md index b81e5855..bb60d552 100644 --- a/specs/04-testing.md +++ b/specs/04-testing.md @@ -26,7 +26,8 @@ How the test suite is organised. Three layers: pure unit tests (no HTTP at all - `test/async_client_test.py` — async, VCR-backed integration tests (not yet on the parametrised harness — follow-up work). - `test/jmespath_test.py` — unit tests for `BaseJsonResource.jmespath`. - `test/vcr/*.vcr` — recorded cassettes. -- `test/eicar.yara`, `test/malicious` — fixture files for upload tests. +- `test/malicious` — fixture file for upload tests (`test/eicar.yara` was retired when the rules tests moved to per-test `uid_yara` bodies). +- `test/hunt_tracking_builder_test.py` — pure-unit request-shape and parse tests for the hunt-page tracking builders/resources. ## Three test layers diff --git a/specs/05-downstream-contract.md b/specs/05-downstream-contract.md index 2b40d018..eb476820 100644 --- a/specs/05-downstream-contract.md +++ b/specs/05-downstream-contract.md @@ -147,9 +147,9 @@ ArtifactInstance, LocalArtifact, Hash Engine Metadata, MetadataMapping, MetadataFieldProperties IOC -LiveYaraRuleset, LiveHuntResult, LiveHuntResultList +LiveYaraRuleset, LiveHuntResult, LiveHuntResultList, LiveHuntResultCounts HistoricalHunt, HistoricalHuntResult, HistoricalHuntResultList, HistoricalHuntList -YaraRuleset +YaraRuleset, YaraRulesetFavorite Tag, MalwareFamily, TagLink AssertionsJob, VotesJob SandboxTask, SandboxProvider @@ -180,6 +180,8 @@ class TimeoutException(PolyswarmException): ... `KnownGoodWithheldException` is the 404 raised when a download is refused because the artifact is a known-good binary — the platform never stores or serves those bytes. It **subclasses `NotFoundException`** precisely so invariant 3 holds for existing consumers: code that already does `except NotFoundException:` keeps catching the refusal with no change, and only callers that want to distinguish "withheld by design" from a plain miss catch the subclass. It adds one attribute, `.sources` — the known-good feeds that flagged the hash (e.g. `['nsrl']`), `[]` when the server named none. The contract is only this: **always a list of strings**, whatever the envelope carried, so `for feed in exc.sources` needs no shape check. Which wire shapes are coerced, and which are dropped and logged, is `exceptions._normalise_sources`' business rather than a promise to consumers — the server sends a list of strings today. Note it is **not** normalised the same way as `ArtifactInstance.known_good_sources`, which is the same concept reached from the instance response: that one is sorted and de-duplicated, while `.sources` preserves the order the envelope carried and can repeat a feed. Don't assume parity between the two. The raw envelope stays reachable at `exc.request.errors` (`{'code': 'KNOWN_GOOD', 'known_good': True, 'sources': [...]}`). The artifact's metadata — the flagging feeds plus any scan data already collected — remains readable through the search / instance endpoints; only the bytes are withheld, and the instance's `KNOWN_GOOD` state/status is the signal for the typed refusal (there is no separate "withheld" field; a `NOT_STORED` instance has no bytes either, but 404s plainly — see below). +`FAVORITE_LIMIT` (a refused ruleset star: the team's favorite budget is spent) has **no typed exception** — deliberately, since no pre-existing consumer needs re-routing the way `NotFoundException` did. It surfaces as the generic 400 `RequestException`, and the machine-readable path is the raw envelope: `exc.request.errors == {'code': 'FAVORITE_LIMIT', 'favorites_used': N, 'favorites_limit': M}`. The counters are the same pair a successful toggle returns, so a caller can render the "budget full" state from either outcome. Pinned by the respx refusal test in `client_scan_test.py`. + **What "known-good" means on the server, as of artifact-index's two-predicate model** (its `specs/05`): the refusal fires on the server's *current understanding* — a catalogue entry exists for the sha256 **and** that entry's extension passes an executable allow-list — evaluated live on every request. Two consequences worth knowing as a consumer: the same download can start working again with no action on your part (the entry is deleted, or the policy narrows), and `ArtifactInstance.state` can report the new value **`NOT_STORED`** — a submission the server declined as known-good at the time whose hash is no longer currently known-good, so nothing was ever stored for it and a fresh submit of the same file works. `state` is a plain string here; the SDK does not enumerate it, so a new member needs no SDK release. Each `RequestException` subclass carries a `.request` attribute holding the originating `PolyswarmRequest` (set by `RequestException.__init__`). Callers can read `exc.request.status_code`, `exc.request.json` (the parsed response body after execution), `exc.request.input_json` (the body that was sent), `exc.request.request_parameters` (the request kwargs that built the call), etc. `InvalidValueException` and `TimeoutException` are client-side errors and don't carry a request descriptor. diff --git a/src/polyswarm_api/aio/api.py b/src/polyswarm_api/aio/api.py index 18ab4921..ec286cb3 100644 --- a/src/polyswarm_api/aio/api.py +++ b/src/polyswarm_api/aio/api.py @@ -502,7 +502,8 @@ async def live_stop(self, rule_id): return await self._single(resources.LiveYaraRuleset.delete(self, rule_id=rule_id)) async def live_feed(self, since=None, rule_name=None, family=None, - polyscore_lower=None, polyscore_upper=None, community=None): + polyscore_lower=None, polyscore_upper=None, community=None, + livescan_id=None): """ Get live hunts feed @@ -512,14 +513,29 @@ async def live_feed(self, since=None, rule_name=None, family=None, :param polyscore_lower: Polyscore lower bound for the hunt results. :param polyscore_upper: Polyscore upper bound for the hunt results. :param community: Community to retrieve live results from, or public/private. + :param livescan_id: Scope the feed to one live hunt's results. :return: Generator of HuntResult resources """ async for item in self._paginate(resources.LiveHuntResult.list( self, since=since, rule_name=rule_name, family=family, polyscore_lower=polyscore_lower, polyscore_upper=polyscore_upper, + livescan_id=livescan_id, community=community or self.community)): yield item + async def live_results_count(self, since=None): + """ + Per-live-hunt result counts for the current account, grouped by + livescan_id. One request answers every "new results in the window" + badge; a hunt absent from counts collected 0. + + :param since: Window in seconds (server default: 86400 — 24 hours) + :return: A LiveHuntResultCounts resource + """ + logger.info('Live results count since %s', since) + return await self._single( + resources.LiveHuntResultCounts.get(self, since=since, community=self.community)) + async def live_feed_delete(self, result_ids): """ Delete live feed results @@ -684,15 +700,47 @@ async def ruleset_delete(self, ruleset_id): logger.info('Delete ruleset %s', ruleset_id) return await self._single(resources.YaraRuleset.delete(self, id=ruleset_id, community=self.community)) - async def ruleset_list(self): + async def ruleset_list(self, name=None, status=None, favorites_only=None, + has_new_results=None, since=None, include_counts=None): """ List all YaraRulesets for the current account. + + All filters are optional and conjunctive: + :param name: Case-insensitive substring match on the ruleset name. + :param status: 'active' returns only rulesets whose live hunt is + currently running. + :param favorites_only: True returns only favorited rulesets. + :param has_new_results: True returns only rulesets whose live hunt + collected results inside the window. + :param since: Window in seconds for has_new_results/include_counts + (server default: 86400). + :param include_counts: True attaches new_results_count to each + ruleset that has a live hunt. :return: A generator of YaraRuleset resources """ logger.info('List rulesets') - async for item in self._paginate(resources.YaraRuleset.list(self, community=self.community)): + async for item in self._paginate(resources.YaraRuleset.list( + self, name=name, status=status, favorites_only=favorites_only, + has_new_results=has_new_results, since=since, + include_counts=include_counts, community=self.community)): yield item + async def ruleset_favorite(self, ruleset_id, favorite=True): + """ + Favorite or unfavorite a YaraRuleset. Idempotent; works while a live + hunt is running. Favorites are shared by the whole team and capped + (the response carries favorites_used / favorites_limit); when the + budget is exhausted the server refuses with a machine-readable + FAVORITE_LIMIT error. + + :param ruleset_id: Id of the ruleset + :param favorite: True to star, False to unstar + :return: A YaraRulesetFavorite resource + """ + logger.info('%s ruleset %s', 'Favorite' if favorite else 'Unfavorite', ruleset_id) + return await self._single(resources.YaraRulesetFavorite.update( + self, id=ruleset_id, favorite=favorite, community=self.community)) + async def tag_link_get(self, sha256): """ Fetch the Tags and Families associated with the given sha256. diff --git a/src/polyswarm_api/api.py b/src/polyswarm_api/api.py index 53400201..62b27d70 100644 --- a/src/polyswarm_api/api.py +++ b/src/polyswarm_api/api.py @@ -568,6 +568,7 @@ def live_feed( polyscore_lower=None, polyscore_upper=None, community=None, + livescan_id=None, ): """ Get live hunts feed @@ -578,6 +579,7 @@ def live_feed( :param polyscore_lower: Polyscore lower bound for the hunt results. :param polyscore_upper: Polyscore upper bound for the hunt results. :param community: Community to retrieve live results from, or public/private. + :param livescan_id: Scope the feed to one live hunt's results. :return: Generator of HuntResult resources """ for item in self._paginate( @@ -588,11 +590,28 @@ def live_feed( family=family, polyscore_lower=polyscore_lower, polyscore_upper=polyscore_upper, + livescan_id=livescan_id, community=community or self.community, ) ): yield item + def live_results_count(self, since=None): + """ + Per-live-hunt result counts for the current account, grouped by + livescan_id. One request answers every "new results in the window" + badge; a hunt absent from counts collected 0. + + :param since: Window in seconds (server default: 86400 — 24 hours) + :return: A LiveHuntResultCounts resource + """ + logger.info("Live results count since %s", since) + return self._single( + resources.LiveHuntResultCounts.get( + self, since=since, community=self.community + ) + ) + def live_feed_delete(self, result_ids): """ Delete live feed results @@ -823,17 +842,67 @@ def ruleset_delete(self, ruleset_id): resources.YaraRuleset.delete(self, id=ruleset_id, community=self.community) ) - def ruleset_list(self): + def ruleset_list( + self, + name=None, + status=None, + favorites_only=None, + has_new_results=None, + since=None, + include_counts=None, + ): """ List all YaraRulesets for the current account. + + All filters are optional and conjunctive: + :param name: Case-insensitive substring match on the ruleset name. + :param status: 'active' returns only rulesets whose live hunt is + currently running. + :param favorites_only: True returns only favorited rulesets. + :param has_new_results: True returns only rulesets whose live hunt + collected results inside the window. + :param since: Window in seconds for has_new_results/include_counts + (server default: 86400). + :param include_counts: True attaches new_results_count to each + ruleset that has a live hunt. :return: A generator of YaraRuleset resources """ logger.info("List rulesets") for item in self._paginate( - resources.YaraRuleset.list(self, community=self.community) + resources.YaraRuleset.list( + self, + name=name, + status=status, + favorites_only=favorites_only, + has_new_results=has_new_results, + since=since, + include_counts=include_counts, + community=self.community, + ) ): yield item + def ruleset_favorite(self, ruleset_id, favorite=True): + """ + Favorite or unfavorite a YaraRuleset. Idempotent; works while a live + hunt is running. Favorites are shared by the whole team and capped + (the response carries favorites_used / favorites_limit); when the + budget is exhausted the server refuses with a machine-readable + FAVORITE_LIMIT error. + + :param ruleset_id: Id of the ruleset + :param favorite: True to star, False to unstar + :return: A YaraRulesetFavorite resource + """ + logger.info( + "%s ruleset %s", "Favorite" if favorite else "Unfavorite", ruleset_id + ) + return self._single( + resources.YaraRulesetFavorite.update( + self, id=ruleset_id, favorite=favorite, community=self.community + ) + ) + def tag_link_get(self, sha256): """ Fetch the Tags and Families associated with the given sha256. diff --git a/src/polyswarm_api/resources.py b/src/polyswarm_api/resources.py index fa5f1c1d..10b909b0 100644 --- a/src/polyswarm_api/resources.py +++ b/src/polyswarm_api/resources.py @@ -746,12 +746,41 @@ def __init__(self, content, api=None): self.modified = core.parse_isoformat(content.get('modified')) self.deleted = content.get('deleted') self.yara = content.get('yara') + # Hunt-page tracking fields. All additive — an older server simply + # leaves them None. + self.favorite = content.get('favorite') + self.favorited_at = core.parse_isoformat(content.get('favorited_at')) + # Number of rules in the ruleset body. None means "no answer" (the + # server could not or has not counted), which is different from 0. + self.rule_count = content.get('rule_count') + self.historical_hunt_count = content.get('historical_hunt_count') + # Live results collected in the requested window; only present when + # the list was asked to include counts, and only for rulesets with a + # live hunt — None otherwise. + self.new_results_count = content.get('new_results_count') class LiveYaraRuleset(YaraRuleset): RESOURCE_ENDPOINT = '/hunt/rule/live' +class YaraRulesetFavorite(core.BaseJsonResource): + """The favorite-toggle response: the ruleset's new star state plus the + team's budget usage, so callers can render "N of M used" without counting + client-side.""" + RESOURCE_ENDPOINT = '/hunt/rule/favorite' + # No query-string keys: the toggle takes {id, favorite} in the JSON body. + RESOURCE_ID_KEYS = [] + + def __init__(self, content, api=None): + super().__init__(content, api=api) + self.id = content.get('id') + self.favorite = content.get('favorite') + self.favorited_at = core.parse_isoformat(content.get('favorited_at')) + self.favorites_used = content.get('favorites_used') + self.favorites_limit = content.get('favorites_limit') + + class LiveHuntResult(core.BaseJsonResource): RESOURCE_ENDPOINT = '/hunt/live' @@ -778,6 +807,22 @@ class LiveHuntResultList(LiveHuntResult): RESOURCE_ENDPOINT = '/hunt/live/list' +class LiveHuntResultCounts(core.BaseJsonResource): + """Per-live-hunt result counts inside a window: ``since`` (seconds) plus + ``counts``, a list of ``{livescan_id, count}`` — one aggregate request for + every "new results" badge. ``livescan_id`` is a digit string, the same + value ``YaraRuleset.livescan_id`` carries (the join key; ids exceed + JavaScript's safe-integer range, so they are never bare JSON ints). A hunt + with no results in the window is simply absent from ``counts``; absence + means 0.""" + RESOURCE_ENDPOINT = '/hunt/live/results/count' + + def __init__(self, content, api=None): + super().__init__(content, api=api) + self.since = content.get('since') + self.counts = content.get('counts') or [] + + class HistoricalHunt(core.BaseJsonResource): RESOURCE_ENDPOINT = '/hunt/historical' @@ -794,6 +839,16 @@ def __init__(self, content, api=None): self.progress = content['progress'] self.results_csv_uri = content['results_csv_uri'] self.communities = content.get('communities') + # Source-rule provenance. rule_id is the ruleset this hunt was + # triggered from (None for raw-yara hunts and hunts predating the + # tracking); rule_modified is a freeze-time audit timestamp. + self.rule_id = content.get('rule_id') + self.rule_modified = core.parse_isoformat(content.get('rule_modified')) + # Tri-state: has the source ruleset's body changed SINCE THE HUNT + # FROZE IT? True/False when the server could compare; None means + # unknown (no source rule, or nothing to compare against) — not + # "unchanged". + self.source_rule_changed = content.get('source_rule_changed') class HistoricalHuntList(HistoricalHunt): diff --git a/test/_e2e_helpers.py b/test/_e2e_helpers.py index 59c5d21c..d4f14ca6 100644 --- a/test/_e2e_helpers.py +++ b/test/_e2e_helpers.py @@ -23,6 +23,7 @@ import os import re import tempfile +import time from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager @@ -75,8 +76,8 @@ def uid_yara(uid): The artifact embeds ``uid`` (see ``malicious_artifact``), so a rule keying on that literal matches just this run's submission — isolating a live/historical - hunt from every other test's EICAR artifact (the generic eicar.yara substring - rule would match them all). + hunt from every other test's EICAR artifact (a generic EICAR-substring rule + would match them all). """ ident = re.sub(r'\W', '_', uid) return f'rule sdk_{ident} {{ strings: $u = "{uid}" condition: $u }}' @@ -132,3 +133,42 @@ async def run_concurrently_async(coros): if _vcr_off() and len(coros) > 1: return await asyncio.gather(*coros) return [await c for c in coros] + + +def poll_equals(read, want, tries=30, delay=1.0): + """Poll a zero-arg ``read`` until it returns ``want`` (or tries run out), + returning the last value read. For read-after-write assertions against + replica-backed GET endpoints (specs/04 in the server repo): on the e2e + stack the replica IS the primary so the first read usually wins, but a + real-replica stack lags — without the poll those assertions are + lag-flaky, and the changed-since-freeze one flakes in the silent + direction (stale source body reads as "unchanged"). Not-found during the + lag window counts as "not yet". Sleeps are free on VCR replay + (``_skip_poll_sleep_on_replay``).""" + from polyswarm_api import exceptions as _exceptions + value = None + for _ in range(tries): + try: + value = read() + except (_exceptions.NotFoundException, _exceptions.NoResultsException): + value = None + if value == want: + return value + time.sleep(delay) + return value + + +async def poll_equals_async(read, want, tries=30, delay=1.0): + """The asyncio twin of ``poll_equals`` (``read`` is a zero-arg coroutine + function).""" + from polyswarm_api import exceptions as _exceptions + value = None + for _ in range(tries): + try: + value = await read() + except (_exceptions.NotFoundException, _exceptions.NoResultsException): + value = None + if value == want: + return value + await asyncio.sleep(delay) + return value diff --git a/test/async_client_test.py b/test/async_client_test.py index b633553e..a7e82507 100644 --- a/test/async_client_test.py +++ b/test/async_client_test.py @@ -29,7 +29,7 @@ from polyswarm_api import exceptions from test._e2e_helpers import ( - EICAR_STRING, malicious_artifact, artifact_file, uid_ip, uid_host, uid_yara, + EICAR_STRING, malicious_artifact, artifact_file, uid_ip, uid_host, uid_yara, poll_equals_async, assert_scanned, run_concurrently_async, ) @@ -608,26 +608,117 @@ async def test_async_sample(self, uid): # ── YARA Rulesets ───────────────────────────────────────────────────────── @vcr.use_cassette() - async def test_async_rules(self): + async def test_async_rules(self, uid): async with self._api() as api: - with open('test/eicar.yara') as f: - contents = f.read() - rule = await api.ruleset_create('test', contents) - assert rule.name == 'test' + # A uid-namespaced single-rule body: unique name on the shared + # stack, deterministic rule_count of 1. + contents = uid_yara(uid) + rule = await api.ruleset_create(uid, contents) + assert rule.name == uid assert rule.yara == contents + # Tracking fields are live from creation. + assert rule.rule_count == 1 + assert rule.favorite is False + assert rule.favorited_at is None + assert rule.historical_hunt_count == 0 + hunt = None try: # The e2e may carry leftover rulesets from prior runs; use # a presence assertion instead of an exact count. rules = [r async for r in api.ruleset_list()] assert any(r.id == rule.id for r in rules) + by_name = [r.id async for r in api.ruleset_list(name=uid)] + assert rule.id in by_name got = await api.ruleset_get(rule.id) - assert got.name == 'test' - - updated = await api.ruleset_update(rule.id, name='test2', description='test') - assert updated.name == 'test2' + assert got.name == uid + + # favorite round-trip with the server-owned budget counts + fav = await api.ruleset_favorite(rule.id, True) + assert fav.favorite is True + assert fav.favorited_at is not None + # limit is a PIN (fixed product cap); used is a BOUND (the + # stack budget is shared across runs) + assert fav.favorites_limit == 5 + assert 1 <= fav.favorites_used <= fav.favorites_limit + favorites = [r async for r in api.ruleset_list(favorites_only=True)] + assert any(r.id == rule.id and r.favorite for r in favorites) + # unstarring here is the CONTRACT assertion; slot hygiene does + # not depend on reaching it — the finally's ruleset_delete + # soft-deletes and the budget counts only deleted=false rows + unfav = await api.ruleset_favorite(rule.id, False) + assert unfav.favorite is False + assert unfav.favorited_at is None + + # live-hunt scope: counts, include_counts and the livescan_id + # feed all need a running hunt; the fresh ruleset matches + # nothing, so every count is a computed ZERO (distinct from + # null = no live hunt to count against) + await api.live_start(int(rule.id)) + try: + livescan_id = (await api.ruleset_get(rule.id)).livescan_id + assert livescan_id is not None # a digit string + active = {r.id async for r in api.ruleset_list(status='active')} + assert rule.id in active + with_counts = None + async for r in api.ruleset_list(include_counts=True): + if r.id == rule.id: + with_counts = r + assert with_counts is not None + assert with_counts.new_results_count == 0 + counts = await api.live_results_count(since=86400) + assert counts.since == 86400 + # zero results -> our hunt is ABSENT (absence means 0) + assert livescan_id not in {entry['livescan_id'] + for entry in counts.counts} + # NOTE: zero-result hunt — pins the wire shape and the + # empty pass-through only; the scoping semantics are + # pinned by the server's own HTTP suite (same for + # has_new_results) + try: + feed = [r async for r in api.live_feed(livescan_id=livescan_id)] + assert feed == [] + except exceptions.NoResultsException: + pass + finally: + # MUST stop before the outer finally's ruleset_delete — a + # running live hunt blocks deletion server-side + await api.live_stop(int(rule.id)) + try: + still_active = {r.id async for r in api.ruleset_list(status='active')} + except exceptions.NoResultsException: + still_active = set() # nothing live anywhere: also a pass + assert rule.id not in still_active + + # a hunt triggered FROM the ruleset carries provenance and + # bumps the counter; the create response's comparison is + # unknown (None) and a read resolves it + hunt = await api.historical_create(int(rule.id)) + assert hunt.rule_id == rule.id + assert hunt.rule_modified is not None + assert hunt.source_rule_changed is None + + # replica-backed GETs: poll so a lagging replica (real + # stacks, not e2e) can't flake these — the changed-since- + # freeze flip's stale read is a silent False + async def _hunt_count(): + return (await api.ruleset_get(rule.id)).historical_hunt_count + + async def _changed(): + return (await api.historical_get(hunt.id)).source_rule_changed + + assert await poll_equals_async(_hunt_count, 1) == 1 + assert await poll_equals_async(_changed, False) is False + + # a body edit flips the hunt's source_rule_changed + updated = await api.ruleset_update( + rule.id, name=f'{uid}2', rules=f'{contents}\n// edited', description='test') + assert updated.name == f'{uid}2' assert updated.description == 'test' + assert await poll_equals_async(_changed, True) is True finally: + if hunt is not None: + await api.historical_delete(hunt.id) await api.ruleset_delete(rule.id) remaining_ids = [] try: diff --git a/test/client_scan_test.py b/test/client_scan_test.py index 443f4a56..fb9a2366 100644 --- a/test/client_scan_test.py +++ b/test/client_scan_test.py @@ -17,7 +17,7 @@ from polyswarm_api import exceptions from test._e2e_helpers import ( - EICAR_STRING, malicious_artifact, artifact_file, uid_ip, uid_host, uid_yara, + EICAR_STRING, malicious_artifact, artifact_file, uid_ip, uid_host, uid_yara, poll_equals, assert_scanned, run_concurrently, ) @@ -357,6 +357,31 @@ def test_metadata_search(self): lambda: api.search_by_metadata(f'artifact.sha256:{sha}'), tries=90) assert result and result[0].sha256 == sha + def test_favorite_limit_refusal_is_machine_readable(self): + # The FAVORITE_LIMIT refusal path (respx-mocked: producing a genuinely + # full budget on the SHARED e2e stack would require holding all five + # team slots, racing every other run). There is deliberately no typed + # exception (specs/05): the machine-readable contract is the raw + # envelope at exc.request.errors — the code plus the same counters a + # successful toggle returns. + with respx.mock(assert_all_called=True) as router: + envelope = { + 'status': 'error', + 'result': 'Favorite limit reached (5 of 5 used).', + 'errors': {'code': 'FAVORITE_LIMIT', + 'favorites_used': 5, 'favorites_limit': 5}, + } + router.put('http://localhost:3000/api/v1/hunt/rule/favorite').mock( + return_value=httpx.Response(400, json=envelope)) + api = PolyswarmAPI(self.test_api_key, uri='http://localhost:3000/api/v1', + community='gamma') + with pytest.raises(exceptions.RequestException) as excinfo: + api.ruleset_favorite(5, True) + errors = excinfo.value.request.errors + assert errors['code'] == 'FAVORITE_LIMIT' + assert errors['favorites_used'] == 5 + assert errors['favorites_limit'] == 5 + def test_resolve_engine_name(self): with respx.mock(assert_all_called=False) as router: ok_payload = {'results': [ @@ -569,26 +594,112 @@ def test_historical_results(self): @vcr.use_cassette() def test_rules(self): api = PolyswarmAPI(self.test_api_key, uri=f'http://ai:9696/{self.api_version}', community='gamma') - # creating - with open('test/eicar.yara') as rule: - contents = rule.read() - rule = api.ruleset_create('test', contents) - assert rule.name == 'test' + # creating — a uid-namespaced single-rule body, so the name is unique + # on the shared stack and rule_count is deterministically 1. + uid = self._testMethodName + contents = uid_yara(uid) + rule = api.ruleset_create(uid, contents) + assert rule.name == uid assert rule.yara == contents + # The tracking fields are live from creation: the body was counted on + # the way in, nothing is starred yet, no hunts have been triggered. + assert rule.rule_count == 1 + assert rule.favorite is False + assert rule.favorited_at is None + assert rule.historical_hunt_count == 0 + hunt = None try: # listing — the created rule must be in the list; the e2e may carry # other rulesets from earlier runs, so use a presence assertion # rather than an exact count. rules = list(api.ruleset_list()) assert any(r.id == rule.id for r in rules) + # the name filter narrows to this test's own rule + by_name = [r.id for r in api.ruleset_list(name=uid)] + assert rule.id in by_name # getting got = api.ruleset_get(rule.id) - assert got.name == 'test' - # updating - updated = api.ruleset_update(rule.id, name='test2', description='test') - assert updated.name == 'test2' + assert got.name == uid + # favorite round-trip, with the budget counts the server owns + fav = api.ruleset_favorite(rule.id, True) + assert fav.favorite is True + assert fav.favorited_at is not None + # the limit is a PIN (the fixed product cap, no plan scaling); + # used is a BOUND because the stack budget is shared across runs + assert fav.favorites_limit == 5 + assert 1 <= fav.favorites_used <= fav.favorites_limit + favorites = list(api.ruleset_list(favorites_only=True)) + assert any(r.id == rule.id and r.favorite for r in favorites) + # unstarring here is the CONTRACT assertion; slot hygiene does not + # depend on reaching it — the finally's ruleset_delete soft-deletes + # and the server's budget counts only deleted=false rows, so a + # failed run's star frees itself with the rule + unfav = api.ruleset_favorite(rule.id, False) + assert unfav.favorite is False + assert unfav.favorited_at is None + # live-hunt scope: the counts endpoint, include_counts and the + # livescan_id feed all need a running hunt. The fresh ruleset + # matches nothing, so every count is a computed ZERO — still + # distinct from null (= no live hunt to count against). + api.live_start(int(rule.id)) + try: + livescan_id = api.ruleset_get(rule.id).livescan_id + assert livescan_id is not None # a digit string + assert rule.id in {r.id for r in api.ruleset_list(status='active')} + with_counts = next(r for r in api.ruleset_list(include_counts=True) + if r.id == rule.id) + assert with_counts.new_results_count == 0 + counts = api.live_results_count(since=86400) + assert counts.since == 86400 + # zero results -> our hunt is ABSENT (absence means 0); the + # keys are the same digit strings ruleset_get renders + assert livescan_id not in {entry['livescan_id'] + for entry in counts.counts} + # NOTE: with a zero-result hunt this pins only the wire shape + # and the empty pass-through — it cannot tell a working filter + # from an ignored param (that would need a second hunt WITH + # results). The scoping semantics themselves are pinned by the + # server's own HTTP suite; same for has_new_results. + try: + assert list(api.live_feed(livescan_id=livescan_id)) == [] + except exceptions.NoResultsException: + pass + finally: + # MUST stop before the outer finally's ruleset_delete — a + # running live hunt blocks deletion server-side + api.live_stop(int(rule.id)) + try: + still_active = {r.id for r in api.ruleset_list(status='active')} + except exceptions.NoResultsException: + still_active = set() # nothing live anywhere: also a pass + assert rule.id not in still_active + # a historical hunt triggered FROM the ruleset carries the + # provenance and bumps the ruleset's counter; the create response's + # source_rule_changed is None (unknown until a read re-resolves it) + hunt = api.historical_create(int(rule.id)) + assert hunt.rule_id == rule.id + assert hunt.rule_modified is not None + assert hunt.source_rule_changed is None + # the GETs below read the replica; poll so a lagging replica + # (real stacks, not e2e) can't flake these — especially the + # changed-since-freeze flip, whose stale read is a silent False + assert poll_equals( + lambda: api.ruleset_get(rule.id).historical_hunt_count, 1) == 1 + # a read of the fresh hunt resolves the comparison: unchanged body + hunt_read = api.historical_get(hunt.id) + assert hunt_read.rule_id == rule.id + assert poll_equals( + lambda: api.historical_get(hunt.id).source_rule_changed, False) is False + # updating — a body edit flips the hunt's source_rule_changed + updated = api.ruleset_update( + rule.id, name=f'{uid}2', rules=f'{contents}\n// edited', description='test') + assert updated.name == f'{uid}2' assert updated.description == 'test' + assert poll_equals( + lambda: api.historical_get(hunt.id).source_rule_changed, True) is True finally: + if hunt is not None: + api.historical_delete(hunt.id) # deleting — the created rule disappears from the list. api.ruleset_delete(rule.id) remaining_ids = [] diff --git a/test/eicar.yara b/test/eicar.yara deleted file mode 100644 index 0c32b59f..00000000 --- a/test/eicar.yara +++ /dev/null @@ -1,34 +0,0 @@ -rule eicar_av_test { - /* - Per standard, match only if entire file is EICAR string plus optional trailing whitespace. - The raw EICAR string to be matched is: - X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H* - */ - - meta: - description = "This is a standard AV test, intended to verify that BinaryAlert is working correctly." - author = "Austin Byers | Airbnb CSIRT" - reference = "http://www.eicar.org/86-0-Intended-use.html" - - strings: - $eicar_regex = /^X5O!P%@AP\[4\\PZX54\(P\^\)7CC\)7\}\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\$H\+H\*\s*$/ - - condition: - all of them -} - -rule eicar_substring_test { - /* - More generic - match just the embedded EICAR string (e.g. in packed executables, PDFs, etc) - */ - - meta: - description = "Standard AV test, checking for an EICAR substring" - author = "Austin Byers | Airbnb CSIRT" - - strings: - $eicar_substring = "$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!" - - condition: - all of them -} \ No newline at end of file diff --git a/test/hunt_tracking_builder_test.py b/test/hunt_tracking_builder_test.py new file mode 100644 index 00000000..653a88dc --- /dev/null +++ b/test/hunt_tracking_builder_test.py @@ -0,0 +1,123 @@ +"""Pure-unit request-shape tests for the hunt-page tracking builders. + +No HTTP at all (the pure-unit tier — see specs/04-testing.md): these pin the +request *construction* for the new surfaces — and specifically the two shapes +that are entirely consequences of ``core._params`` plumbing rather than +anything visible at the call site: + +* ``YaraRulesetFavorite`` empties ``RESOURCE_ID_KEYS``, which is the ONLY + thing routing ``id`` (and ``favorite``/``community``) into the PUT's JSON + body instead of the query string — the server reads the toggle exclusively + from the body; and +* booleans serialise as ``1``/``0`` ints, not JSON ``true``/``false`` + (``core._params`` coerces before body/query routing) — the server's + boolean parser accepts exactly that, so the int-vs-bool body contract is + load-bearing. + +Endpoint *behaviour* is covered by the live-e2e VCR lifecycle tests +(``test_rules`` / ``test_async_rules``). +""" +from polyswarm_api import resources + + +class _FakeApi: + uri = 'https://api.example.test' + community = 'gamma' + + +class TestYaraRulesetFavoriteBuilder: + def test_update_routes_everything_to_the_put_body(self): + api = _FakeApi() + req = resources.YaraRulesetFavorite.update( + api, id=5, favorite=True, community=api.community) + assert req.method == 'PUT' + assert req.url == f'{api.uri}/hunt/rule/favorite' + # RESOURCE_ID_KEYS = [] is load-bearing: with the base ['id'] the id + # would ride the query string on a PUT, and the server only reads the + # body. favorite serialises as int 1, not JSON true. + assert req.params is None + assert req.input_json == {'id': '5', 'favorite': 1, 'community': 'gamma'} + assert req.result_parser is resources.YaraRulesetFavorite + + def test_unfavorite_serialises_false_as_zero(self): + req = resources.YaraRulesetFavorite.update( + _FakeApi(), id=5, favorite=False, community='gamma') + assert req.input_json['favorite'] == 0 + + +class TestLiveHuntResultCountsBuilder: + def test_get_routes_since_and_community_to_the_query(self): + api = _FakeApi() + req = resources.LiveHuntResultCounts.get( + api, since=86400, community=api.community) + assert req.method == 'GET' + assert req.url == f'{api.uri}/hunt/live/results/count' + assert req.params == {'since': 86400, 'community': 'gamma'} + assert req.input_json is None + assert req.result_parser is resources.LiveHuntResultCounts + + def test_get_omits_unset_since(self): + # None is dropped, so the server applies its own default window. + req = resources.LiveHuntResultCounts.get( + _FakeApi(), since=None, community='gamma') + assert req.params == {'community': 'gamma'} + + +class TestRulesetListFilterBuilder: + def test_list_routes_filters_to_the_query_with_int_bools(self): + api = _FakeApi() + req = resources.YaraRuleset.list( + api, name='alpha', status='active', favorites_only=True, + has_new_results=True, since=86400, include_counts=True, + community=api.community) + assert req.method == 'GET' + assert req.url == f'{api.uri}/hunt/rule/list' + assert req.params == { + 'name': 'alpha', 'status': 'active', 'favorites_only': 1, + 'has_new_results': 1, 'since': 86400, 'include_counts': 1, + 'community': 'gamma'} + + def test_list_omits_every_unset_filter(self): + # The no-filter request is byte-compatible with the pre-filter + # contract: nothing but community rides the query string. + req = resources.YaraRuleset.list( + _FakeApi(), name=None, status=None, favorites_only=None, + has_new_results=None, since=None, include_counts=None, + community='gamma') + assert req.params == {'community': 'gamma'} + + +class TestLiveFeedScopeBuilder: + def test_list_routes_livescan_id_to_the_query_as_digit_string(self): + # *_id kwargs stringify (core._params); the server casts back to int. + req = resources.LiveHuntResult.list( + _FakeApi(), since=60, livescan_id=45392847561029383, + rule_name=None, family=None, polyscore_lower=None, + polyscore_upper=None, community='gamma') + assert req.url == 'https://api.example.test/hunt/live/list' + assert req.params == {'since': 60, 'livescan_id': '45392847561029383', + 'community': 'gamma'} + + +class TestLiveHuntResultCountsParse: + """Parse-side pins for the counts resource — the recorded cassettes carry + only EMPTY counts (the live tests use a fresh zero-result hunt), so the + entry shape and the digit-string join key documented in three places are + otherwise asserted nowhere.""" + + def test_counts_entries_parse_with_digit_string_join_keys(self): + payload = {'since': 86400, + 'counts': [{'livescan_id': '45392847561029383', 'count': 3}, + {'livescan_id': '71359438369584055', 'count': 1}]} + counts = resources.LiveHuntResultCounts(payload, api=None) + assert counts.since == 86400 + assert counts.counts == payload['counts'] + # the join key is the same digit string YaraRuleset.livescan_id + # carries — a bare int would round in a JS consumer + assert all(isinstance(entry['livescan_id'], str) for entry in counts.counts) + by_id = {entry['livescan_id']: entry['count'] for entry in counts.counts} + assert by_id['45392847561029383'] == 3 + + def test_null_counts_coalesces_to_an_empty_list(self): + counts = resources.LiveHuntResultCounts({'since': 86400, 'counts': None}, api=None) + assert counts.counts == [] diff --git a/test/vcr/test_async_rules.vcr b/test/vcr/test_async_rules.vcr index 751c4b2a..1b4cb8d6 100644 --- a/test/vcr/test_async_rules.vcr +++ b/test/vcr/test_async_rules.vcr @@ -1,17 +1,7 @@ interactions: - request: - body: '{"yara":"rule eicar_av_test {\n /*\n Per standard, match only - if entire file is EICAR string plus optional trailing whitespace.\n The - raw EICAR string to be matched is:\n X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*\n */\n\n meta:\n description - = \"This is a standard AV test, intended to verify that BinaryAlert is working - correctly.\"\n author = \"Austin Byers | Airbnb CSIRT\"\n reference - = \"http://www.eicar.org/86-0-Intended-use.html\"\n\n strings:\n $eicar_regex - = /^X5O!P%@AP\\[4\\\\PZX54\\(P\\^\\)7CC\\)7\\}\\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\\$H\\+H\\*\\s*$/\n\n condition:\n all - of them\n}\n\nrule eicar_substring_test {\n /*\n More generic - match - just the embedded EICAR string (e.g. in packed executables, PDFs, etc)\n */\n\n meta:\n description - = \"Standard AV test, checking for an EICAR substring\"\n author = \"Austin - Byers | Airbnb CSIRT\"\n\n strings:\n $eicar_substring = \"$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\"\n\n condition:\n all - of them\n}","name":"test"}' + body: '{"yara":"rule sdk_test_async_rules { strings: $u = \"test_async_rules\" + condition: $u }","name":"test_async_rules"}' headers: accept: - '*/*' @@ -22,49 +12,472 @@ interactions: connection: - keep-alive content-length: - - '1126' + - '115' content-type: - application/json host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: POST uri: http://ai:9696/v3/hunt/rule response: body: - string: '{"result":{"created":"2026-06-02T21:30:27.081195+00:00","deleted":false,"description":null,"id":"40732127887168358","livescan_created":null,"livescan_id":null,"modified":"2026-06-02T21:30:27.081195+00:00","name":"test","yara":"rule - eicar_av_test {\n /*\n Per standard, match only if entire file is - EICAR string plus optional trailing whitespace.\n The raw EICAR string - to be matched is:\n X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*\n */\n\n meta:\n description - = \"This is a standard AV test, intended to verify that BinaryAlert is working - correctly.\"\n author = \"Austin Byers | Airbnb CSIRT\"\n reference - = \"http://www.eicar.org/86-0-Intended-use.html\"\n\n strings:\n $eicar_regex - = /^X5O!P%@AP\\[4\\\\PZX54\\(P\\^\\)7CC\\)7\\}\\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\\$H\\+H\\*\\s*$/\n\n condition:\n all - of them\n}\n\nrule eicar_substring_test {\n /*\n More generic - match - just the embedded EICAR string (e.g. in packed executables, PDFs, etc)\n */\n\n meta:\n description - = \"Standard AV test, checking for an EICAR substring\"\n author = - \"Austin Byers | Airbnb CSIRT\"\n\n strings:\n $eicar_substring - = \"$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\"\n\n condition:\n all - of them\n}"},"status":"OK"} + string: '{"result":{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"4466967201833338","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:52.300732+00:00","name":"test_async_rules","rule_count":1,"yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} ' headers: - Access-Control-Allow-Origin: + access-control-allow-origin: - '*' - Access-Control-Expose-Headers: + access-control-expose-headers: - Authorization - Connection: + connection: + - keep-alive + content-length: + - '412' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29: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/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"4466967201833338","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:52.300732+00:00","name":"test_async_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '391' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29: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/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?name=test_async_rules&community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"4466967201833338","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:52.300732+00:00","name":"test_async_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '391' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29: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/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule?id=4466967201833338&community=gamma + response: + body: + string: '{"result":{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"4466967201833338","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:52.300732+00:00","name":"test_async_rules","rule_count":1,"yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '412' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:52 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"id":"4466967201833338","favorite":1,"community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '58' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: PUT + uri: http://ai:9696/v3/hunt/rule/favorite + response: + body: + string: '{"result":{"favorite":true,"favorited_at":"2026-08-21T14:29:52.846750+00:00","favorites_limit":5,"favorites_used":1,"id":"4466967201833338"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '156' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29: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/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?favorites_only=1&community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":null,"favorite":true,"favorited_at":"2026-08-21T14:29:52.846750+00:00","historical_hunt_count":0,"id":"4466967201833338","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:52.300732+00:00","name":"test_async_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '420' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:52 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"id":"4466967201833338","favorite":0,"community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '58' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: PUT + uri: http://ai:9696/v3/hunt/rule/favorite + response: + body: + string: '{"result":{"favorite":false,"favorited_at":null,"favorites_limit":5,"favorites_used":0,"id":"4466967201833338"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '127' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:52 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"rule_id":"4466967201833338"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '30' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: POST + uri: http://ai:9696/v3/hunt/rule/live + response: + body: + string: '{"result":{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"4466967201833338","livescan_created":"2026-08-21T14:29:53.201880+00:00","livescan_id":"34849444833418665","modified":"2026-08-21T14:29:53.197377+00:00","name":"test_async_rules","rule_count":1,"yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '457' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:53 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule?id=4466967201833338&community=gamma + response: + body: + string: '{"result":{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"4466967201833338","livescan_created":"2026-08-21T14:29:53.201880+00:00","livescan_id":"34849444833418665","modified":"2026-08-21T14:29:53.197377+00:00","name":"test_async_rules","rule_count":1,"yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '457' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:53 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?status=active&community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"4466967201833338","livescan_created":"2026-08-21T14:29:53.201880+00:00","livescan_id":"34849444833418665","modified":"2026-08-21T14:29:53.197377+00:00","name":"test_async_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '436' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:53 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: - keep-alive - Content-Length: - - '1346' - Content-Type: + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?include_counts=1&community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"4466967201833338","livescan_created":"2026-08-21T14:29:53.201880+00:00","livescan_id":"34849444833418665","modified":"2026-08-21T14:29:53.197377+00:00","name":"test_async_rules","new_results_count":0,"rule_count":1,"yara":null}],"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '433' + content-type: - application/json - Date: - - Tue, 02 Jun 2026 21:30:27 GMT - Server: + date: + - Fri, 21 Aug 2026 14:29:53 GMT + server: - gunicorn - X-Billing-ID: + x-billing-id: - '111' status: code: 200 @@ -83,30 +496,239 @@ 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-Darwin-CPython-3.11.3) method: GET - uri: http://ai:9696/v3/hunt/rule/list?community=gamma + uri: http://ai:9696/v3/hunt/live/results/count?since=86400&community=gamma + response: + body: + string: '{"result":{"counts":[],"since":86400},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '53' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:53 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/live/list?livescan_id=34849444833418665&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: + - Fri, 21 Aug 2026 14:29:53 GMT + server: + - gunicorn + status: + code: 204 + message: NO CONTENT +- request: + body: '{"rule_id":"4466967201833338"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '30' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: DELETE + uri: http://ai:9696/v3/hunt/rule/live + response: + body: + string: '{"result":{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"4466967201833338","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:53.570180+00:00","name":"test_async_rules","rule_count":1,"yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '412' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:53 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?status=active&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: + - Fri, 21 Aug 2026 14:29:53 GMT + server: + - gunicorn + status: + code: 204 + message: NO CONTENT +- request: + body: '{"rule_id":"4466967201833338","community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '50' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: POST + uri: http://ai:9696/v3/hunt/historical + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T14:29:53.760803+00:00","failed_max_retries":0,"failed_other":0,"id":"41876045317487902","progress":null,"results_csv_uri":null,"rule_id":"4466967201833338","rule_modified":"2026-08-21T14:29:53.570180+00:00","ruleset_name":"test_async_rules","source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '577' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:53 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule?id=4466967201833338&community=gamma response: body: - string: '{"has_more":false,"limit":50,"result":[{"created":"2026-06-02T21:30:27.081195+00:00","deleted":false,"description":null,"id":"40732127887168358","livescan_created":null,"livescan_id":null,"modified":"2026-06-02T21:30:27.081195+00:00","name":"test","yara":null}],"status":"OK"} + string: '{"result":{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"4466967201833338","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:53.570180+00:00","name":"test_async_rules","rule_count":1,"yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} ' headers: - Access-Control-Allow-Origin: + access-control-allow-origin: - '*' - Access-Control-Expose-Headers: + access-control-expose-headers: - Authorization - Connection: + connection: - keep-alive - Content-Length: - - '277' - Content-Type: + content-length: + - '412' + content-type: - application/json - Date: - - Tue, 02 Jun 2026 21:30:27 GMT - Server: + date: + - Fri, 21 Aug 2026 14:29:53 GMT + server: - gunicorn - X-Billing-ID: + x-billing-id: - '111' status: code: 200 @@ -125,49 +747,38 @@ 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-Darwin-CPython-3.11.3) method: GET - uri: http://ai:9696/v3/hunt/rule?id=40732127887168358&community=gamma + uri: http://ai:9696/v3/hunt/historical?id=41876045317487902&community=gamma response: body: - string: '{"result":{"created":"2026-06-02T21:30:27.081195+00:00","deleted":false,"description":null,"id":"40732127887168358","livescan_created":null,"livescan_id":null,"modified":"2026-06-02T21:30:27.081195+00:00","name":"test","yara":"rule - eicar_av_test {\n /*\n Per standard, match only if entire file is - EICAR string plus optional trailing whitespace.\n The raw EICAR string - to be matched is:\n X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*\n */\n\n meta:\n description - = \"This is a standard AV test, intended to verify that BinaryAlert is working - correctly.\"\n author = \"Austin Byers | Airbnb CSIRT\"\n reference - = \"http://www.eicar.org/86-0-Intended-use.html\"\n\n strings:\n $eicar_regex - = /^X5O!P%@AP\\[4\\\\PZX54\\(P\\^\\)7CC\\)7\\}\\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\\$H\\+H\\*\\s*$/\n\n condition:\n all - of them\n}\n\nrule eicar_substring_test {\n /*\n More generic - match - just the embedded EICAR string (e.g. in packed executables, PDFs, etc)\n */\n\n meta:\n description - = \"Standard AV test, checking for an EICAR substring\"\n author = - \"Austin Byers | Airbnb CSIRT\"\n\n strings:\n $eicar_substring - = \"$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\"\n\n condition:\n all - of them\n}"},"status":"OK"} + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T14:29:53.760803+00:00","failed_max_retries":0,"failed_other":0,"id":"41876045317487902","progress":null,"results_csv_uri":null,"rule_id":"4466967201833338","rule_modified":"2026-08-21T14:29:53.570180+00:00","ruleset_name":"test_async_rules","source_rule_changed":false,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} ' headers: - Access-Control-Allow-Origin: + access-control-allow-origin: - '*' - Access-Control-Expose-Headers: + access-control-expose-headers: - Authorization - Connection: + connection: - keep-alive - Content-Length: - - '1346' - Content-Type: + content-length: + - '578' + content-type: - application/json - Date: - - Tue, 02 Jun 2026 21:30:27 GMT - Server: + date: + - Fri, 21 Aug 2026 14:29:53 GMT + server: - gunicorn - X-Billing-ID: + x-billing-id: - '111' status: code: 200 message: OK - request: - body: '{"name":"test2","description":"test","community":"gamma"}' + body: '{"name":"test_async_rules2","yara":"rule sdk_test_async_rules { strings: + $u = \"test_async_rules\" condition: $u }\n// edited","description":"test","community":"gamma"}' headers: accept: - '*/*' @@ -178,49 +789,128 @@ interactions: connection: - keep-alive content-length: - - '57' + - '168' content-type: - application/json host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: PUT - uri: http://ai:9696/v3/hunt/rule?id=40732127887168358 + uri: http://ai:9696/v3/hunt/rule?id=4466967201833338 + response: + body: + string: '{"result":{"created":"2026-08-21T14:29:52.300732+00:00","deleted":false,"description":"test","favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"4466967201833338","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:54.082225+00:00","name":"test_async_rules2","rule_count":1,"yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }\n// + edited"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '426' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:54 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/historical?id=41876045317487902&community=gamma + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T14:29:53.760803+00:00","failed_max_retries":0,"failed_other":0,"id":"41876045317487902","progress":null,"results_csv_uri":null,"rule_id":"4466967201833338","rule_modified":"2026-08-21T14:29:53.570180+00:00","ruleset_name":"test_async_rules","source_rule_changed":true,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '577' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:54 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '21' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: DELETE + uri: http://ai:9696/v3/hunt/historical?id=41876045317487902 response: body: - string: '{"result":{"created":"2026-06-02T21:30:27.081195+00:00","deleted":false,"description":"test","id":"40732127887168358","livescan_created":null,"livescan_id":null,"modified":"2026-06-02T21:30:27.081195+00:00","name":"test2","yara":"rule - eicar_av_test {\n /*\n Per standard, match only if entire file is - EICAR string plus optional trailing whitespace.\n The raw EICAR string - to be matched is:\n X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*\n */\n\n meta:\n description - = \"This is a standard AV test, intended to verify that BinaryAlert is working - correctly.\"\n author = \"Austin Byers | Airbnb CSIRT\"\n reference - = \"http://www.eicar.org/86-0-Intended-use.html\"\n\n strings:\n $eicar_regex - = /^X5O!P%@AP\\[4\\\\PZX54\\(P\\^\\)7CC\\)7\\}\\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\\$H\\+H\\*\\s*$/\n\n condition:\n all - of them\n}\n\nrule eicar_substring_test {\n /*\n More generic - match - just the embedded EICAR string (e.g. in packed executables, PDFs, etc)\n */\n\n meta:\n description - = \"Standard AV test, checking for an EICAR substring\"\n author = - \"Austin Byers | Airbnb CSIRT\"\n\n strings:\n $eicar_substring - = \"$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\"\n\n condition:\n all - of them\n}"},"status":"OK"} + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T14:29:53.760803+00:00","failed_max_retries":0,"failed_other":0,"id":"41876045317487902","progress":null,"results_csv_uri":null,"rule_id":"4466967201833338","rule_modified":"2026-08-21T14:29:53.570180+00:00","ruleset_name":"test_async_rules","source_rule_changed":true,"status":"DELETING","summary":null,"user_account_number":"111","yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }"},"status":"OK"} ' headers: - Access-Control-Allow-Origin: + access-control-allow-origin: - '*' - Access-Control-Expose-Headers: + access-control-expose-headers: - Authorization - Connection: + connection: - keep-alive - Content-Length: - - '1349' - Content-Type: + content-length: + - '578' + content-type: - application/json - Date: - - Tue, 02 Jun 2026 21:30:27 GMT - Server: + date: + - Fri, 21 Aug 2026 14:29:54 GMT + server: - gunicorn - X-Billing-ID: + x-billing-id: - '111' status: code: 200 @@ -243,43 +933,32 @@ 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-Darwin-CPython-3.11.3) method: DELETE - uri: http://ai:9696/v3/hunt/rule?id=40732127887168358 + uri: http://ai:9696/v3/hunt/rule?id=4466967201833338 response: body: - string: '{"result":{"created":"2026-06-02T21:30:27.081195+00:00","deleted":true,"description":"test","id":"40732127887168358","livescan_created":null,"livescan_id":null,"modified":"2026-06-02T21:30:27.152617+00:00","name":"test2","yara":"rule - eicar_av_test {\n /*\n Per standard, match only if entire file is - EICAR string plus optional trailing whitespace.\n The raw EICAR string - to be matched is:\n X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*\n */\n\n meta:\n description - = \"This is a standard AV test, intended to verify that BinaryAlert is working - correctly.\"\n author = \"Austin Byers | Airbnb CSIRT\"\n reference - = \"http://www.eicar.org/86-0-Intended-use.html\"\n\n strings:\n $eicar_regex - = /^X5O!P%@AP\\[4\\\\PZX54\\(P\\^\\)7CC\\)7\\}\\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\\$H\\+H\\*\\s*$/\n\n condition:\n all - of them\n}\n\nrule eicar_substring_test {\n /*\n More generic - match - just the embedded EICAR string (e.g. in packed executables, PDFs, etc)\n */\n\n meta:\n description - = \"Standard AV test, checking for an EICAR substring\"\n author = - \"Austin Byers | Airbnb CSIRT\"\n\n strings:\n $eicar_substring - = \"$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\"\n\n condition:\n all - of them\n}"},"status":"OK"} + string: '{"result":{"created":"2026-08-21T14:29:52.300732+00:00","deleted":true,"description":"test","favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"4466967201833338","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:54.434684+00:00","name":"test_async_rules2","rule_count":1,"yara":"rule + sdk_test_async_rules { strings: $u = \"test_async_rules\" condition: $u }\n// + edited"},"status":"OK"} ' headers: - Access-Control-Allow-Origin: + access-control-allow-origin: - '*' - Access-Control-Expose-Headers: + access-control-expose-headers: - Authorization - Connection: + connection: - keep-alive - Content-Length: - - '1348' - Content-Type: + content-length: + - '425' + content-type: - application/json - Date: - - Tue, 02 Jun 2026 21:30:27 GMT - Server: + date: + - Fri, 21 Aug 2026 14:29:54 GMT + server: - gunicorn - X-Billing-ID: + x-billing-id: - '111' status: code: 200 @@ -298,24 +977,24 @@ 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-Darwin-CPython-3.11.3) method: GET uri: http://ai:9696/v3/hunt/rule/list?community=gamma response: body: string: '' headers: - Access-Control-Allow-Origin: + access-control-allow-origin: - '*' - Access-Control-Expose-Headers: + access-control-expose-headers: - Authorization - Connection: + connection: - keep-alive - Content-Type: + content-type: - text/html; charset=utf-8 - Date: - - Tue, 02 Jun 2026 21:30:27 GMT - Server: + date: + - Fri, 21 Aug 2026 14:29:54 GMT + server: - gunicorn status: code: 204 diff --git a/test/vcr/test_rules.vcr b/test/vcr/test_rules.vcr index 7a3d8518..9c9b6dc2 100644 --- a/test/vcr/test_rules.vcr +++ b/test/vcr/test_rules.vcr @@ -1,17 +1,7 @@ interactions: - request: - body: '{"yara":"rule eicar_av_test {\n /*\n Per standard, match only - if entire file is EICAR string plus optional trailing whitespace.\n The - raw EICAR string to be matched is:\n X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*\n */\n\n meta:\n description - = \"This is a standard AV test, intended to verify that BinaryAlert is working - correctly.\"\n author = \"Austin Byers | Airbnb CSIRT\"\n reference - = \"http://www.eicar.org/86-0-Intended-use.html\"\n\n strings:\n $eicar_regex - = /^X5O!P%@AP\\[4\\\\PZX54\\(P\\^\\)7CC\\)7\\}\\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\\$H\\+H\\*\\s*$/\n\n condition:\n all - of them\n}\n\nrule eicar_substring_test {\n /*\n More generic - match - just the embedded EICAR string (e.g. in packed executables, PDFs, etc)\n */\n\n meta:\n description - = \"Standard AV test, checking for an EICAR substring\"\n author = \"Austin - Byers | Airbnb CSIRT\"\n\n strings:\n $eicar_substring = \"$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\"\n\n condition:\n all - of them\n}","name":"test"}' + body: '{"yara":"rule sdk_test_rules { strings: $u = \"test_rules\" condition: + $u }","name":"test_rules"}' headers: accept: - '*/*' @@ -22,49 +12,514 @@ interactions: connection: - keep-alive content-length: - - '1126' + - '97' content-type: - application/json host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: POST uri: http://ai:9696/v3/hunt/rule response: body: - string: '{"result":{"created":"2026-06-02T21:32:01.681881+00:00","deleted":false,"description":null,"id":"68232116824597140","livescan_created":null,"livescan_id":null,"modified":"2026-06-02T21:32:01.681881+00:00","name":"test","yara":"rule - eicar_av_test {\n /*\n Per standard, match only if entire file is - EICAR string plus optional trailing whitespace.\n The raw EICAR string - to be matched is:\n X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*\n */\n\n meta:\n description - = \"This is a standard AV test, intended to verify that BinaryAlert is working - correctly.\"\n author = \"Austin Byers | Airbnb CSIRT\"\n reference - = \"http://www.eicar.org/86-0-Intended-use.html\"\n\n strings:\n $eicar_regex - = /^X5O!P%@AP\\[4\\\\PZX54\\(P\\^\\)7CC\\)7\\}\\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\\$H\\+H\\*\\s*$/\n\n condition:\n all - of them\n}\n\nrule eicar_substring_test {\n /*\n More generic - match - just the embedded EICAR string (e.g. in packed executables, PDFs, etc)\n */\n\n meta:\n description - = \"Standard AV test, checking for an EICAR substring\"\n author = - \"Austin Byers | Airbnb CSIRT\"\n\n strings:\n $eicar_substring - = \"$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\"\n\n condition:\n all - of them\n}"},"status":"OK"} + string: '{"result":{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"71359438369584055","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:48.738607+00:00","name":"test_rules","rule_count":1,"yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} ' headers: - Access-Control-Allow-Origin: + access-control-allow-origin: - '*' - Access-Control-Expose-Headers: + access-control-expose-headers: - Authorization - Connection: + connection: + - keep-alive + content-length: + - '395' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:48 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"71359438369584055","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:48.738607+00:00","name":"test_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '386' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:48 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?name=test_rules&community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"71359438369584055","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:48.738607+00:00","name":"test_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '386' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:48 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule?id=71359438369584055&community=gamma + response: + body: + string: '{"result":{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"71359438369584055","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:48.738607+00:00","name":"test_rules","rule_count":1,"yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '395' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:48 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"id":"71359438369584055","favorite":1,"community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '59' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: PUT + uri: http://ai:9696/v3/hunt/rule/favorite + response: + body: + string: '{"result":{"favorite":true,"favorited_at":"2026-08-21T14:29:49.029459+00:00","favorites_limit":5,"favorites_used":1,"id":"71359438369584055"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '157' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:49 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?favorites_only=1&community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":null,"favorite":true,"favorited_at":"2026-08-21T14:29:49.029459+00:00","historical_hunt_count":0,"id":"71359438369584055","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:48.738607+00:00","name":"test_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '415' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:49 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"id":"71359438369584055","favorite":0,"community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '59' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: PUT + uri: http://ai:9696/v3/hunt/rule/favorite + response: + body: + string: '{"result":{"favorite":false,"favorited_at":null,"favorites_limit":5,"favorites_used":0,"id":"71359438369584055"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '128' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:49 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"rule_id":"71359438369584055"}' + 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/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: POST + uri: http://ai:9696/v3/hunt/rule/live + response: + body: + string: '{"result":{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"71359438369584055","livescan_created":"2026-08-21T14:29:49.287201+00:00","livescan_id":"11956831134597871","modified":"2026-08-21T14:29:49.282670+00:00","name":"test_rules","rule_count":1,"yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '440' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:49 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule?id=71359438369584055&community=gamma + response: + body: + string: '{"result":{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"71359438369584055","livescan_created":"2026-08-21T14:29:49.287201+00:00","livescan_id":"11956831134597871","modified":"2026-08-21T14:29:49.282670+00:00","name":"test_rules","rule_count":1,"yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '440' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:49 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?status=active&community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"71359438369584055","livescan_created":"2026-08-21T14:29:49.287201+00:00","livescan_id":"11956831134597871","modified":"2026-08-21T14:29:49.282670+00:00","name":"test_rules","new_results_count":null,"rule_count":1,"yara":null}],"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '431' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:49 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?include_counts=1&community=gamma + response: + body: + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"71359438369584055","livescan_created":"2026-08-21T14:29:49.287201+00:00","livescan_id":"11956831134597871","modified":"2026-08-21T14:29:49.282670+00:00","name":"test_rules","new_results_count":0,"rule_count":1,"yara":null}],"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '428' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:49 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 - Content-Length: - - '1346' - Content-Type: + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/live/results/count?since=86400&community=gamma + response: + body: + string: '{"result":{"counts":[],"since":86400},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '53' + content-type: - application/json - Date: - - Tue, 02 Jun 2026 21:32:01 GMT - Server: + date: + - Fri, 21 Aug 2026 14:29:49 GMT + server: - gunicorn - X-Billing-ID: + x-billing-id: - '111' status: code: 200 @@ -83,30 +538,240 @@ 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-Darwin-CPython-3.11.3) method: GET - uri: http://ai:9696/v3/hunt/rule/list?community=gamma + uri: http://ai:9696/v3/hunt/live/list?livescan_id=11956831134597871&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: + - Fri, 21 Aug 2026 14:29:49 GMT + server: + - gunicorn + status: + code: 204 + message: NO CONTENT +- request: + body: '{"rule_id":"71359438369584055"}' + 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/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: DELETE + uri: http://ai:9696/v3/hunt/rule/live + response: + body: + string: '{"result":{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"71359438369584055","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:49.650762+00:00","name":"test_rules","rule_count":1,"yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '395' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:49 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule/list?status=active&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: + - Fri, 21 Aug 2026 14:29:49 GMT + server: + - gunicorn + status: + code: 204 + message: NO CONTENT +- request: + body: '{"rule_id":"71359438369584055","community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '51' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: POST + uri: http://ai:9696/v3/hunt/historical + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T14:29:50.251832+00:00","failed_max_retries":0,"failed_other":0,"id":"90800113038824882","progress":null,"results_csv_uri":null,"rule_id":"71359438369584055","rule_modified":"2026-08-21T14:29:49.650762+00:00","ruleset_name":"test_rules","source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '560' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:50 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/rule?id=71359438369584055&community=gamma + response: + body: + string: '{"result":{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"71359438369584055","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:49.650762+00:00","name":"test_rules","rule_count":1,"yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '395' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:50 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/historical?id=90800113038824882&community=gamma response: body: - string: '{"has_more":false,"limit":50,"result":[{"created":"2026-06-02T21:32:01.681881+00:00","deleted":false,"description":null,"id":"68232116824597140","livescan_created":null,"livescan_id":null,"modified":"2026-06-02T21:32:01.681881+00:00","name":"test","yara":null}],"status":"OK"} + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T14:29:50.251832+00:00","failed_max_retries":0,"failed_other":0,"id":"90800113038824882","progress":null,"results_csv_uri":null,"rule_id":"71359438369584055","rule_modified":"2026-08-21T14:29:49.650762+00:00","ruleset_name":"test_rules","source_rule_changed":false,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} ' headers: - Access-Control-Allow-Origin: + access-control-allow-origin: - '*' - Access-Control-Expose-Headers: + access-control-expose-headers: - Authorization - Connection: + connection: - keep-alive - Content-Length: - - '277' - Content-Type: + content-length: + - '561' + content-type: - application/json - Date: - - Tue, 02 Jun 2026 21:32:01 GMT - Server: + date: + - Fri, 21 Aug 2026 14:29:50 GMT + server: - gunicorn - X-Billing-ID: + x-billing-id: - '111' status: code: 200 @@ -125,49 +790,38 @@ 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-Darwin-CPython-3.11.3) method: GET - uri: http://ai:9696/v3/hunt/rule?id=68232116824597140&community=gamma + uri: http://ai:9696/v3/hunt/historical?id=90800113038824882&community=gamma response: body: - string: '{"result":{"created":"2026-06-02T21:32:01.681881+00:00","deleted":false,"description":null,"id":"68232116824597140","livescan_created":null,"livescan_id":null,"modified":"2026-06-02T21:32:01.681881+00:00","name":"test","yara":"rule - eicar_av_test {\n /*\n Per standard, match only if entire file is - EICAR string plus optional trailing whitespace.\n The raw EICAR string - to be matched is:\n X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*\n */\n\n meta:\n description - = \"This is a standard AV test, intended to verify that BinaryAlert is working - correctly.\"\n author = \"Austin Byers | Airbnb CSIRT\"\n reference - = \"http://www.eicar.org/86-0-Intended-use.html\"\n\n strings:\n $eicar_regex - = /^X5O!P%@AP\\[4\\\\PZX54\\(P\\^\\)7CC\\)7\\}\\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\\$H\\+H\\*\\s*$/\n\n condition:\n all - of them\n}\n\nrule eicar_substring_test {\n /*\n More generic - match - just the embedded EICAR string (e.g. in packed executables, PDFs, etc)\n */\n\n meta:\n description - = \"Standard AV test, checking for an EICAR substring\"\n author = - \"Austin Byers | Airbnb CSIRT\"\n\n strings:\n $eicar_substring - = \"$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\"\n\n condition:\n all - of them\n}"},"status":"OK"} + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T14:29:50.251832+00:00","failed_max_retries":0,"failed_other":0,"id":"90800113038824882","progress":null,"results_csv_uri":null,"rule_id":"71359438369584055","rule_modified":"2026-08-21T14:29:49.650762+00:00","ruleset_name":"test_rules","source_rule_changed":false,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} ' headers: - Access-Control-Allow-Origin: + access-control-allow-origin: - '*' - Access-Control-Expose-Headers: + access-control-expose-headers: - Authorization - Connection: + connection: - keep-alive - Content-Length: - - '1346' - Content-Type: + content-length: + - '561' + content-type: - application/json - Date: - - Tue, 02 Jun 2026 21:32:01 GMT - Server: + date: + - Fri, 21 Aug 2026 14:29:50 GMT + server: - gunicorn - X-Billing-ID: + x-billing-id: - '111' status: code: 200 message: OK - request: - body: '{"name":"test2","description":"test","community":"gamma"}' + body: '{"name":"test_rules2","yara":"rule sdk_test_rules { strings: $u = \"test_rules\" + condition: $u }\n// edited","description":"test","community":"gamma"}' headers: accept: - '*/*' @@ -178,49 +832,127 @@ interactions: connection: - keep-alive content-length: - - '57' + - '150' content-type: - application/json host: - ai:9696 user-agent: - - polyswarm_api/3.21.0 (x86_64-Linux-CPython-3.14.4) + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: PUT - uri: http://ai:9696/v3/hunt/rule?id=68232116824597140 + uri: http://ai:9696/v3/hunt/rule?id=71359438369584055 + response: + body: + string: '{"result":{"created":"2026-08-21T14:29:48.738607+00:00","deleted":false,"description":"test","favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"71359438369584055","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:50.635991+00:00","name":"test_rules2","rule_count":1,"yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }\n// edited"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '409' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:50 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: GET + uri: http://ai:9696/v3/hunt/historical?id=90800113038824882&community=gamma + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T14:29:50.251832+00:00","failed_max_retries":0,"failed_other":0,"id":"90800113038824882","progress":null,"results_csv_uri":null,"rule_id":"71359438369584055","rule_modified":"2026-08-21T14:29:49.650762+00:00","ruleset_name":"test_rules","source_rule_changed":true,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '560' + content-type: + - application/json + date: + - Fri, 21 Aug 2026 14:29:50 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +- request: + body: '{"community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '21' + content-type: + - application/json + host: + - ai:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: DELETE + uri: http://ai:9696/v3/hunt/historical?id=90800113038824882 response: body: - string: '{"result":{"created":"2026-06-02T21:32:01.681881+00:00","deleted":false,"description":"test","id":"68232116824597140","livescan_created":null,"livescan_id":null,"modified":"2026-06-02T21:32:01.681881+00:00","name":"test2","yara":"rule - eicar_av_test {\n /*\n Per standard, match only if entire file is - EICAR string plus optional trailing whitespace.\n The raw EICAR string - to be matched is:\n X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*\n */\n\n meta:\n description - = \"This is a standard AV test, intended to verify that BinaryAlert is working - correctly.\"\n author = \"Austin Byers | Airbnb CSIRT\"\n reference - = \"http://www.eicar.org/86-0-Intended-use.html\"\n\n strings:\n $eicar_regex - = /^X5O!P%@AP\\[4\\\\PZX54\\(P\\^\\)7CC\\)7\\}\\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\\$H\\+H\\*\\s*$/\n\n condition:\n all - of them\n}\n\nrule eicar_substring_test {\n /*\n More generic - match - just the embedded EICAR string (e.g. in packed executables, PDFs, etc)\n */\n\n meta:\n description - = \"Standard AV test, checking for an EICAR substring\"\n author = - \"Austin Byers | Airbnb CSIRT\"\n\n strings:\n $eicar_substring - = \"$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\"\n\n condition:\n all - of them\n}"},"status":"OK"} + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-21T14:29:50.251832+00:00","failed_max_retries":0,"failed_other":0,"id":"90800113038824882","progress":null,"results_csv_uri":null,"rule_id":"71359438369584055","rule_modified":"2026-08-21T14:29:49.650762+00:00","ruleset_name":"test_rules","source_rule_changed":true,"status":"DELETING","summary":null,"user_account_number":"111","yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }"},"status":"OK"} ' headers: - Access-Control-Allow-Origin: + access-control-allow-origin: - '*' - Access-Control-Expose-Headers: + access-control-expose-headers: - Authorization - Connection: + connection: - keep-alive - Content-Length: - - '1349' - Content-Type: + content-length: + - '561' + content-type: - application/json - Date: - - Tue, 02 Jun 2026 21:32:01 GMT - Server: + date: + - Fri, 21 Aug 2026 14:29:50 GMT + server: - gunicorn - X-Billing-ID: + x-billing-id: - '111' status: code: 200 @@ -243,43 +975,31 @@ 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-Darwin-CPython-3.11.3) method: DELETE - uri: http://ai:9696/v3/hunt/rule?id=68232116824597140 + uri: http://ai:9696/v3/hunt/rule?id=71359438369584055 response: body: - string: '{"result":{"created":"2026-06-02T21:32:01.681881+00:00","deleted":true,"description":"test","id":"68232116824597140","livescan_created":null,"livescan_id":null,"modified":"2026-06-02T21:32:01.766631+00:00","name":"test2","yara":"rule - eicar_av_test {\n /*\n Per standard, match only if entire file is - EICAR string plus optional trailing whitespace.\n The raw EICAR string - to be matched is:\n X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*\n */\n\n meta:\n description - = \"This is a standard AV test, intended to verify that BinaryAlert is working - correctly.\"\n author = \"Austin Byers | Airbnb CSIRT\"\n reference - = \"http://www.eicar.org/86-0-Intended-use.html\"\n\n strings:\n $eicar_regex - = /^X5O!P%@AP\\[4\\\\PZX54\\(P\\^\\)7CC\\)7\\}\\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\\$H\\+H\\*\\s*$/\n\n condition:\n all - of them\n}\n\nrule eicar_substring_test {\n /*\n More generic - match - just the embedded EICAR string (e.g. in packed executables, PDFs, etc)\n */\n\n meta:\n description - = \"Standard AV test, checking for an EICAR substring\"\n author = - \"Austin Byers | Airbnb CSIRT\"\n\n strings:\n $eicar_substring - = \"$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\"\n\n condition:\n all - of them\n}"},"status":"OK"} + string: '{"result":{"created":"2026-08-21T14:29:48.738607+00:00","deleted":true,"description":"test","favorite":false,"favorited_at":null,"historical_hunt_count":1,"id":"71359438369584055","livescan_created":null,"livescan_id":null,"modified":"2026-08-21T14:29:50.839329+00:00","name":"test_rules2","rule_count":1,"yara":"rule + sdk_test_rules { strings: $u = \"test_rules\" condition: $u }\n// edited"},"status":"OK"} ' headers: - Access-Control-Allow-Origin: + access-control-allow-origin: - '*' - Access-Control-Expose-Headers: + access-control-expose-headers: - Authorization - Connection: + connection: - keep-alive - Content-Length: - - '1348' - Content-Type: + content-length: + - '408' + content-type: - application/json - Date: - - Tue, 02 Jun 2026 21:32:01 GMT - Server: + date: + - Fri, 21 Aug 2026 14:29:50 GMT + server: - gunicorn - X-Billing-ID: + x-billing-id: - '111' status: code: 200 @@ -298,24 +1018,24 @@ 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-Darwin-CPython-3.11.3) method: GET uri: http://ai:9696/v3/hunt/rule/list?community=gamma response: body: string: '' headers: - Access-Control-Allow-Origin: + access-control-allow-origin: - '*' - Access-Control-Expose-Headers: + access-control-expose-headers: - Authorization - Connection: + connection: - keep-alive - Content-Type: + content-type: - text/html; charset=utf-8 - Date: - - Tue, 02 Jun 2026 21:32:01 GMT - Server: + date: + - Fri, 21 Aug 2026 14:29:50 GMT + server: - gunicorn status: code: 204