diff --git a/AGENTS.md b/AGENTS.md index eec96c6a..e588d387 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,6 +98,7 @@ Details in [`specs/04-testing.md`](./specs/04-testing.md). The shape: - Conventional commit prefixes (`feat:`, `fix:`, `refactor:`, `chore:`, `docs:`, `test:`). - Small, scoped commits — each one should be independently reviewable. - **Don't reference ticket IDs or internal project codes in commit messages, PR titles, or PR descriptions.** This repo is public; published artefacts shouldn't leak internal references. Track tickets in the internal tracker, not the git history. +- **Branch names are the exception, and the merge is where they're contained.** A change spanning this repo and the SDK must use the *identical* branch name in both, because CI resolves the companion SDK by `$CI_COMMIT_BRANCH` (`.gitlab-ci.yml`) — so a shared ticket-prefixed name is often the coordinating key and is deliberately allowed. It does reach public history, but only through the **default merge subject** (`Merge pull request #N from org/TICKET-…`). **Squash-merge with an explicit clean subject**, and it never lands. Prefer a shared descriptive name over a ticket prefix when one reads just as well. - **Don't name private companion repos in PR descriptions or commit messages on this repo.** Refer to internal services by category, not by repo name. - No AI-attribution trailers on commits (`Co-Authored-By: Claude …`, "Generated with Claude Code", etc.) — they're noise and they don't belong in project history. - PRs that depend on an unreleased `polyswarm-api` surface must link the SDK PR under a `## Requires` section (see `specs/05-sdk-contract.md`). diff --git a/pyproject.toml b/pyproject.toml index 03e289ba..941682c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "polyswarm" -version = "4.3.0" +version = "4.4.0" description = "CLI for using the PolySwarm Customer APIs" readme = "README.md" authors = [{ name = "PolySwarm Developers", email = "info@polyswarm.io" }] @@ -22,7 +22,7 @@ classifiers = [ ] dependencies = [ - "polyswarm_api>=4.3.0,<5.0.0", + "polyswarm_api>=4.4.0,<5.0.0", "click>=7.1", "colorama>=0.4.6", "click-log>=0.4.0", @@ -51,7 +51,7 @@ include-package-data = true where = ["src"] [tool.bumpversion] -current_version = "4.3.0" +current_version = "4.4.0" commit = true tag = false sign_tags = true diff --git a/specs/01-architecture.md b/specs/01-architecture.md index 5ca2673b..b8880793 100644 --- a/specs/01-architecture.md +++ b/specs/01-architecture.md @@ -38,6 +38,14 @@ The console-script entry (`__main__.py`) calls `polyswarm_cli(prog_name='polyswa The transport-error branch matches by **ancestry class name** — it intersects `{c.__name__ for c in type(e).__mro__}` with `{'HTTPError', 'RequestException', 'ConnectionError', 'SSLError'}` — because those classes come from the SDK's HTTP dependency (`httpx`; `requests` historically) and shouldn't be imported here directly. `httpx` roots every request/transport/status error at `HTTPError`, so ancestry matching covers all its leaf classes (`ConnectError`, `ReadTimeout`, `RemoteProtocolError`, `ProxyError`, …) without enumerating them. +**The order of the `except` clauses is load-bearing, not stylistic.** The SDK has its own +`api_exceptions.RequestException`, which subclasses `PolyswarmException` but shares the bare +name `requests` used — so it satisfies the ancestry-name test above and would take the +transport branch (exit `1`, "contact support") if it ever reached it. It exits `2` only +because the `PolyswarmException` clause is matched **before** the transport branch. Reordering +those clauses silently changes the exit code of every SDK request refusal, `FAVORITE_LIMIT` +included; `ExitCodeHierarchyTest` pins the subclass relation the ordering rests on. + ## The SDK wrapper — `polyswarm.py` `class Polyswarm(PolyswarmAPI)` subclasses the SDK's sync client to add **CLI-only** behaviour the SDK has no reason to ship: @@ -74,7 +82,8 @@ The catalogue of groups and the SDK methods each wraps is in [`02-commands.md`]( ## Support — `utils.py`, `exceptions.py` -- **`utils.py`** — `parallelize`/`parallel_executor` (thread-pool fan-out with per-item exception aggregation: collects results, logs per-item no-results, raises an aggregate `NoResultsException`/`NotFoundException`/`InternalFailureException` at the end), `parallel_executor_iterable_results` (the same, for SDK methods that return generators — it materialises each generator inside the worker so per-item exception handling still fires), and input parsing/validation (`parse_hashes`, hash/IP detection). +- **`utils.py`** — `parallelize`/`parallel_executor` (thread-pool fan-out with per-item exception aggregation: collects results, logs per-item no-results, raises an aggregate `NoResultsException`/`NotFoundException`/`InternalFailureException` at the end) and `parallel_executor_iterable_results` (the same, for SDK methods that return generators — it materialises each generator inside the worker so per-item exception handling still fires), plus `collect_files` and the detection helpers (`is_valid_id`, `is_ip`, `is_domain`, `is_url`). +- **`client/utils.py`** — `parse_hashes` and the click parameter validators (`validate_id`, `validate_hash(es)`, `validate_key`, …). Note the module is `client/utils.py`, not the top-level `utils.py` above — the two are distinct and easily confused. - **`exceptions.py`** — the CLI's own hierarchy, **distinct from the SDK's**: `PolyswarmException` → `NoResultsException`, `NotFoundException`, `InternalFailureException`, `PartialResultsException`. `ExceptionHandlingGroup` catches both these and the SDK's `api_exceptions.*`. ## Lifecycle of a command (end to end) diff --git a/specs/02-commands.md b/specs/02-commands.md index fdf6149f..fd1aeb7e 100644 --- a/specs/02-commands.md +++ b/specs/02-commands.md @@ -25,12 +25,12 @@ The top-level command groups, what each is for, and the primary `polyswarm-api` | `report` (`report.py`) | Create/fetch/download reports; `prompt-config` subgroup; LLM reports | `report_create`, `report_wait_for`, `report_download`, `report_get`, `llm_report_{create,get,download}`, `prompt_config_{create,get,update,list}` | | `report-template` (`report_template.py`) | Manage report templates + logos | `report_template_{create,update,get,list}`, `report_template_logo_{download,upload}` | | `engine` → `votes` / `assertions` (`engine.py`) | Consolidated votes/assertions bundles per engine | `votes_{create,get,delete,list}`, `assertions_{create,get,delete,list}` | -| `live` (`live.py`) | Live YARA hunts: start/stop, feed, results | `live_start`, `live_stop`, `live_feed`, `live_result`, `live_feed_delete` | +| `live` (`live.py`) | Live YARA hunts: start/stop, feed, results. `feed` takes `--since` in **SECONDS** (default 86400 — 24h; `0` means no time filter at all, and a negative is refused at parse time rather than forwarded). **`0` is the API's contract, not a CLI convention:** the endpoint applies the window only when `since` is truthy, so `0` and an absent parameter behave identically. The CLI's own test can only pin that it forwards `0` rather than dropping it — which is the half that can regress here, plus `--livescan-id` (the drill-down for the per-ruleset new-results badge `rules list` renders — the detail view deliberately does not carry the badge; the badge counts the hunt across **every** community it runs in, public and private, while the feed shows one at a time, so a multi-community hunt lists fewer rows than the badge reports) and `--max-results` (stop after N; unset means every page, as before). Both are guaranteed by the pin (see [05-sdk-contract.md](./05-sdk-contract.md) §Current floor) and called directly; they are forwarded only when passed purely so a pre-existing invocation's call shape is unchanged — the request is identical either way | `live_start`, `live_stop`, `live_feed`, `live_result`, `live_feed_delete` | | `historical` (`historical.py`) | Historical hunts: CRUD + results | `historical_{get,create,update,list}`, `historical_delete_multiple`, `historical_delete_list`, `historical_results_multiple`, `historical_result`, `historical_results_delete` | | `tag` (`tags.py`) | Tag CRUD | `tag_{create,delete,get,list}` | | `link` (`links.py`) | Tag/family links on artifacts | `tag_link_multiple`, `tag_link_get`, `tag_link_list` | | `family` (`families.py`) | Malware-family CRUD | `family_{create,update,delete,get,list}` | -| `rules` (`rules.py`) | YARA ruleset CRUD | `ruleset_{create,delete,update,get,list}` | +| `rules` (`rules.py`) | YARA ruleset CRUD plus `favorite [--unfavorite]` (the star toggle: renders the new state + the server-owned "N of M used" budget, and converts the machine-readable `FAVORITE_LIMIT` refusal into a clean actionable message at exit 2, never 1 — 1 is reserved for no-results/not-found; 2 is the broad bucket `ExceptionHandlingGroup` maps the PolyswarmException hierarchies to. **2 does not identify a server refusal:** click exits 2 for a `UsageError` too, so a scripted caller cannot tell “the favorite budget is full” from “you passed a bad flag” without reading the message). `list` takes the server-side filters `--name` / `--status active` / `--favorites-only` / `--has-new-results` (conjunctive; the list is keyset-paginated, so filtering locally would mean walking every page). `rules favorite` and the `rules list` filters need SDK 4.4.0, which the pin requires (see [05-sdk-contract.md](./05-sdk-contract.md) §Current floor), so they are called directly. The formatters read the hunt-page fields directly: the pin guarantees the SDK parses them, so `None` means the *server* had no answer | `ruleset_{create,delete,update,get,list,favorite}` | | `metadata` (`metadata.py`) | Rerun metadata; scan lookup; IP/URL analysis | `rerun_metadata`, `scan_lookup`, `submit_url` | | `activity` (`event.py`) | List account activity/events | `event_list` | | `account` (`account.py`) | Account whois / features | `account_whois`, `account_features` | @@ -38,6 +38,30 @@ The top-level command groups, what each is for, and the primary `polyswarm-api` | `bundle` (`bundle.py`) | Sample bundle tasks | `sample_bundle_task_create`, `sample_bundle_task_get`, `sample_bundle_download` | | `sample` (`sample.py`) | Fetch a consolidated sample view | `sample` | +> **`live feed --since` defaults to 86400 seconds (24h), not 1440.** The old +> default was written as `24 * 60` against an SDK docstring that said the +> parameter was minutes; the server has always read **seconds**, so the real +> default window was 24 minutes while the ruleset badge beside it counts 24 +> hours. The fix is here rather than on the wire: the endpoint takes ~197k +> requests per 30 days carrying `since` from clients outside our control, and +> re-basing the server to minutes would widen every one of them 60x with no +> error. `historical list --since` is seconds too — those two agree. +> +> **Migration, for a caller who relied on the old behaviour:** pass +> `--since 1440` to get the 24-minute window back. Two effects compound and +> the second is the sharper one — the default window widens ~60x, and +> `--max-results` is unset by default, so a bare `live feed` pages through all +> of it rather than stopping. This repo has no CHANGELOG, so the note lives +> here and in the command's own `--help` rather than only in a release-time +> reminder. +> +> **A third `--since` is genuinely MINUTES and must stay that way:** +> `download stream --since` (`client/download.py`, `IntRange(1, 2880)`, +> default `1440`) hits a different endpoint that really does read minutes. That +> `1440` is the same literal `live feed` is being corrected away from, and is the +> likeliest origin of the original mistake — check which endpoint you are on +> before copying a default between them. + ## Adding to the catalogue When you add or materially change a group, update its row (and add per-subcommand detail here if the behaviour is non-obvious). The `AGENTS.md` §"When adding a new command family" checklist covers the wiring + formatter + test steps. diff --git a/specs/03-formatters.md b/specs/03-formatters.md index b717ae16..e3ac6e51 100644 --- a/specs/03-formatters.md +++ b/specs/03-formatters.md @@ -14,7 +14,7 @@ How command output is rendered: the `BaseOutput` interface, the concrete formatt ## The interface — `base.py` -`BaseOutput(output, **kwargs)` holds the output stream and exposes a method per resource type, each raising `NotImplementedError`. The set includes (non-exhaustive): `artifact_instance`, `historical_result`, `hunt`, `hunt_deletion`, `local_artifact`, `ruleset`, `ioc`, `iocs`, `known_host`, `metadata`, `artifact_metadata`, `tag_link`, `family`, `tag`, `known_good`, `sandbox_list`, `sandbox_task`, `sandbox_tasks`, `bundle_task`, `sample`. Concrete formatters add further methods as command families grow (e.g. `report_task`, `webhook`, `llm_prompt_config`, `metadata_field_properties`); keep `text` and `json` in sync. +`BaseOutput(output, **kwargs)` holds the output stream and exposes a method per resource type, each raising `NotImplementedError`. The set includes (non-exhaustive): `artifact_instance`, `historical_result`, `hunt`, `hunt_deletion`, `local_artifact`, `ruleset`, `ruleset_favorite`, `ioc`, `iocs`, `known_host`, `metadata`, `artifact_metadata`, `tag_link`, `family`, `tag`, `known_good`, `sandbox_list`, `sandbox_task`, `sandbox_tasks`, `bundle_task`, `sample`. Concrete formatters add further methods as command families grow (e.g. `report_task`, `webhook`, `llm_prompt_config`, `metadata_field_properties`); keep `text` and `json` in sync. ## Concrete formatters @@ -142,7 +142,34 @@ either field) never raises `AttributeError`; an SDK without `.state` simply neve the known-good branch, which is the safe fallback — the pre-known-good rendering. That degradation is belt-and-braces, not a supported configuration: `.state` is load-bearing here with no substitute. Both attributes ship in SDK **4.1.0**, but the dependency floor is -`polyswarm_api>=4.2.0` — set by two *other* behaviours the CLI depends on, both of which +the value in `pyproject.toml` — see [05-sdk-contract.md](./05-sdk-contract.md) +§Current floor, which is authoritative, since repeating the number here is what let this +line go stale before. Its *rationale* is two behaviours that landed in 4.2.0 and still hold +transitively; those two fail silently on 4.1.0 (see [`05-sdk-contract.md`](./05-sdk-contract.md) §Version pin) — so every supported install has them. `JSONOutput` needs no change — it dumps the resource's `.json`, which already carries the raw `state` and `known_good` keys. + +## Hunt-page tracking fields (rulesets + historical hunts) + +Rendering rules that are deliberate, not incidental. Every one of these fields is +parsed by the pinned SDK, so the attribute always exists and `None` means the +**server** had no answer — never an older SDK (the floor forbids one; see +[`05-sdk-contract.md`](./05-sdk-contract.md) §Current floor). The formatters read +the attributes directly: + +- `rule_count` / `historical_hunt_count`: `0` renders as a real zero; + `None` (the server had no answer) omits the line — never shown as 0. +- `favorite` is truthy-only ("Favorite: yes"): False and None both print + nothing, deliberately indistinguishable. +- `new_results_count` is the server's STORED badge (refreshed by its + scheduled job; the window is the server's and the response does not carry + it, so the label deliberately does not name one): a number renders with its + `new_results_counted_at` staleness marker beside it; `None` (never + refreshed / no live hunt) omits both lines. +- `ruleset_favorite` renders the toggle response: `Favorite: yes/no`, the + `favorited_at` timestamp when starred, and the server-owned budget as + "Favorites used: N of M" — the client never counts. +- `source_rule_changed` is tri-state: `None` means UNKNOWN, not "unchanged", + and prints nothing; the label names its reference point — "changed since + this hunt froze it" — so it cannot read as "edited recently". diff --git a/specs/04-testing.md b/specs/04-testing.md index 7b100e64..f9ae2947 100644 --- a/specs/04-testing.md +++ b/specs/04-testing.md @@ -8,7 +8,9 @@ How the CLI is tested: the `CliRunner` harness, the two mocking styles (SDK-boun - **Anything that is command behaviour is driven through `click.testing.CliRunner`** — argument parsing, the SDK call, the wiring, the exit code: exercise the real command tree, never an internal function standing in for it. No live PolySwarm stack is required. The one sanctioned exception is pure rendering logic — see [Style 3](#style-3--formatter-unit-tests). - **Mock at the SDK boundary, or replay HTTP with VCR — never both for the same path.** A test either patches `polyswarm_api.api.PolyswarmAPI.` (unit-style) or lets VCR replay recorded HTTP (end-to-end). The CLI's own code is exercised either way. -- **VCR is an efficiency cache, not a load-bearing requirement.** The suite must pass against a live e2e stack with VCR off. Don't hardcode `record_mode='none'`; if a test only works against its recorded cassette, that's a bug in the test. + +- **VCR is an efficiency cache, not a load-bearing requirement.** The suite must pass against a live e2e stack with VCR off. Don't hardcode `record_mode='none'`; if a test only works against its recorded cassette, that's a bug in the test. Note this is about a test's *logic*, not its fixtures: a `.click` snapshot pins server-generated ids and timestamps, so re-recording needs a stack in a particular state — see [Re-recording a cassette](#re-recording-a-cassette). + - **Never `cp` a cassette from a sibling test, never hand-edit cassette bytes.** Re-record against a live stack. ## Running the suite @@ -39,6 +41,36 @@ Helpers in `cli_test.py`: `_run_cli(args)` invokes the command tree under a cass ### Re-recording a cassette +Some cassettes need a **stack state**, not just a live stack. The ruleset-favorite ones do, +because `_assert_text_result` compares the whole rendered block verbatim and the budget +counter is part of it: `test_ruleset_favorite_text` pins `Favorites used: 2 of 5` and +`test_ruleset_unfavorite_text` pins `1 of 5`. Their counters only make sense as a +sequence (star, star, unstar), so re-recording one alone produces a set that contradicts +itself. + +Re-recording them is harder than it looks, and the shipped set is **not** what a single +pass produces — `test_ruleset_favorite_text` acts on a ruleset that does not appear in +`test_ruleset_list_json`'s inventory at all, so the two were recorded against different +stack states. Two couplings to plan around before starting: + +- `unittest` runs methods in **sorted-name** order, and `test_ruleset_list_json` sorts + *between* `favorite_text` and `unfavorite_text`. Its snapshot pins `"favorite": false` on + every ruleset, so a full-suite recording captures it while a ruleset is starred and that + snapshot changes too. Re-record it with the others, or not at all. +- Renaming or reordering any of these methods changes the sequence **silently**: replay + keeps passing, and only the next re-record surfaces the contradiction. + +If keeping them consistent stops being worth it, break the coupling rather than documenting +a wider one — `_assert_text_result` already takes a `replace=` hook, and normalising the +budget counter there makes each cassette independent of what ran before it. + +**A refusal at the favorite cap is deliberately not recorded here.** Saturating the budget +takes all five team slots, which is exactly the state the tests above must *not* be in, so +the cassette and its siblings cannot both be satisfiable in one VCR-off run. That refusal is +pinned without a stack instead: the CLI's message and exit code at the SDK boundary, the +envelope spelling against a real `PolyswarmRequest`, and the wire shape by the SDK's own +stubbed-transport suite, which declined a recording for the same reason. + ```bash rm tests/vcr/.vcr # (and regenerate .click from the new run) pytest tests/cli_test.py:::: # records against whatever stack your env points at @@ -61,3 +93,42 @@ Use it **only** for that. Argument parsing, SDK calls, generator consumption, `c ## Incremental — to be expanded This spec describes the harness as it stands. Not yet documented (add as the suite grows): a per-command coverage matrix, a documented "VCR-off against live e2e" CI job, and conventions for fixture/`.click` generation. See [`99-open-questions.md`](./99-open-questions.md). + +## The SDK floor is a version pin, not a runtime probe + +**A test never asks the installed SDK whether it has a feature. The pin guarantees +it.** (For the one pre-existing render guard this does not cover, see +[`03-formatters.md`](./03-formatters.md) §Known-good artifact instances.) When this repo needs a surface the SDK does not yet publish, the SDK bumps its +version and `pyproject.toml` raises `polyswarm_api>=` to it. `pip` then refuses the +combination that would fail, at install time, before a single test runs — so a test +can simply use the surface. + +**Do not reintroduce per-test skip guards** — `hasattr` on a method, a built resource's +attribute, a parameter in the installed signature. They are the shape this rule exists to +exclude, and the reasons are worth keeping written down: + +- **The fact lived twice.** The pin said one thing; each guard re-derived the same + thing at runtime. Nothing kept them in sync, so every guard was one edit away from + disagreeing with the tests it gated. +- **Both ways of disagreeing are defects.** A guard that checks *less* than its test + uses lets the test run and **fail** where it should have skipped. One that checks + *more* **skips** a test that would have passed — silently dropping coverage while CI + stays green. The second is the dangerous one, because nothing reports it. +- **It never verified the thing it claimed to protect.** A green run against the paired + SDK said nothing about the floor install the guards existed for. + +The version contract has none of that: the claim is checked once, by a tool, against +the artifact that will actually be installed. + +**When you need a new SDK surface**, in order: add it in `polyswarm-api` → bump that +repo's version (minor, for an additive surface) **in the same PR**, because this repo's +floor cannot name a version the SDK has not declared → raise the floor here → use the +surface in code and tests with no guard. CI installs the SDK from git by branch name, +so an unreleased version is not an obstacle; see +[`05-sdk-contract.md`](./05-sdk-contract.md) §Current floor for the ordering that +forces at release time. + +**The failure mode to expect**, and it is a good one: if the paired SDK branch is +missing, CI falls back to the SDK's `develop`, whose version does not satisfy the new +floor, and `pip install .[tests]` fails loudly. Before, that fallback silently tested +against the wrong SDK. diff --git a/specs/05-sdk-contract.md b/specs/05-sdk-contract.md index 3957a916..63630a49 100644 --- a/specs/05-sdk-contract.md +++ b/specs/05-sdk-contract.md @@ -18,7 +18,7 @@ How the CLI depends on the `polyswarm-api` SDK: which parts of the SDK's public | `from polyswarm_api.api import PolyswarmAPI` | Base class of the `Polyswarm` wrapper (`src/polyswarm/polyswarm.py`). | | `from polyswarm_api import settings` | Defaults: `DEFAULT_SCAN_TIMEOUT`, `DEFAULT_REPORT_TIMEOUT`, etc. | | `from polyswarm_api import resources` | Result-parser classes for power-user calls (e.g. `resources.ArtifactInstance`); resource attributes the formatters read. | -| `from polyswarm_api import exceptions as api_exceptions` | Caught in `ExceptionHandlingGroup` and `utils.parallel_executor` (`NoResultsException`, `NotFoundException`, `FailedInstanceException`, `PolyswarmException`). | +| `from polyswarm_api import exceptions as api_exceptions` | Caught in `ExceptionHandlingGroup` and `utils.parallel_executor` (`NoResultsException`, `NotFoundException`, `FailedInstanceException`, `PolyswarmException`). Also `RequestException`, caught by `rules favorite` (`client/rules.py`) to read the machine-readable `FAVORITE_LIMIT` refusal off `exc.request.errors['code']` — and, when the envelope carries no counters, `exc.request.json['result']` as the server's own message. Note the spelling: the request object exposes the response envelope as `.json` and keeps only a private `._result`, so `exc.request.result` is not a thing — reading it yields `None` silently. The SDK does not raise a typed exception for that refusal by design: `.request.errors` is a plain dict the server's error envelope populates. It is pinned without a recording — the SDK's stubbed-transport suite fixes the wire shape, and both CLI branches are unit-pinned against a real `PolyswarmRequest` (not a hand-built mock, which fabricates whatever attribute it is asked for and so cannot detect a rename). It cannot be cassette-pinned: the server sends the counters on every `FAVORITE_LIMIT`, so the envelope the fallback exists for is one no recording can produce. The fallback is defensive against a server that omits them, and the unit test is what fixes the spelling it reads. | | `from polyswarm_api.core import parse_isoformat` | Date rendering in `formatters/text.py`. | | `import polyswarm_api` (`__version__`) | `--api-version`. | @@ -70,19 +70,81 @@ When a CLI feature needs an SDK surface that doesn't exist yet: - The CLI is **sync-only** — it imports `polyswarm_api.api.PolyswarmAPI`, never `polyswarm_api.aio`. Don't add the `polyswarm_api[async]` extra. - Bumping the pin is a normal code change; bumping the CLI's *own* version is a release step (`AGENTS.md` §Gitflow). They're unrelated. - There is **no lock file / compiled requirements** to keep in step: `pyproject.toml` is the only place the SDK version is expressed, and CI installs the SDK straight from the SDK repo's branch archive (see §Coordinated changes). A pin change is a one-file change *in this repo*, but it is not free of interactions — see below. -- **The floor must be satisfied by the SDK archive CI installs, and by PyPI.** CI installs the archive build and *then* runs `pip install .[tests]`; if the archive's declared version is below the floor, that second install silently pulls a newer SDK from PyPI **over** the archive build, and CI stops testing the SDK branch at all — the mechanism §Coordinated changes rests on, defeated with no error. Symmetrically, a floor above the newest **published** version breaks `pip install polyswarm-cli` for every consumer the moment it reaches `master`. So a floor bump has two preconditions: the version is on PyPI, and the SDK's `develop` declares at least that version. +- **The floor must be satisfied by the SDK archive CI installs, and by PyPI.** CI installs the archive build and *then* runs `pip install .[tests]`; if the archive's declared version is below the floor, that second install silently pulls a newer SDK from PyPI **over** the archive build, and CI stops testing the SDK branch at all — the mechanism §Coordinated changes rests on, defeated with no error. Symmetrically, a floor above the newest **published** version breaks `pip install polyswarm-cli` for every consumer the moment it reaches `master`. So a floor bump has two preconditions, and they fall due at **different moments** — conflating them is what makes a correct bump look wrong: - **Read the declared version off the archive's own tree, and mind pre-release suffixes.** PEP 440 orders `4.2.0.dev1 < 4.2.0`, so a `develop` head carrying a dev suffix (the SDK's `pyproject.toml` has a `[tool.bumpversion.parts.dev]`) would *not* satisfy a `>=4.2.0` floor even though it looks like 4.2.0 — and the archive build would be silently replaced from PyPI. Check the version string in the SDK branch's `pyproject.toml` / `__init__.py`, not the last release tag. For the current floor both were read from `origin/develop`: `version = "4.2.0"` and `__version__ = '4.2.0'`, no suffix. + - **To merge here:** the SDK's `develop` must declare at least the floor. Nothing about PyPI applies yet; merging to `develop` publishes nothing. + - **To release here:** the floor version must be on PyPI, which needs the SDK's own `develop → master` first. -### Current floor — `polyswarm_api>=4.2.0` + A floor naming a version that is declared on the SDK's `develop` but not yet released is therefore correct and mergeable — that is the normal state of a paired change between the two merges. -Two behaviours the CLI relies on only exist from **4.2.0**; on 4.1.0 both fail *silently*, which is why the floor is a hard requirement rather than a preference: + **Read the declared version off the archive's own tree, and mind pre-release suffixes.** PEP 440 orders `4.2.0.dev1 < 4.2.0`, so a `develop` head carrying a dev suffix (the SDK's `pyproject.toml` has a `[tool.bumpversion.parts.dev]`) would *not* satisfy a `>=4.2.0` floor even though it looks like 4.2.0 — and the archive build would be silently replaced from PyPI. Check the version string in the SDK branch's `pyproject.toml` / `__init__.py`, not the last release tag. When the floor was last verified this way both were read from `origin/develop` as `4.2.0`, no suffix; the pin has since moved on (§Current floor is the one authoritative statement of its value), and every bump should be re-checked the same way. + +### Current floor — `polyswarm_api>=4.4.0` + +The floor is whatever `pyproject.toml` pins; this header follows it. It lives in ONE authoritative place for a reason — a copy here drifted behind the pin once already. The 4.2.0 rationale below still holds transitively; on 4.1.0 both behaviours fail *silently*, which is why the floor is a hard requirement rather than a preference: 1. **`llm_report_create` sends the client's community.** 4.2.0 passes `community=self.community` when it builds the report resource; 4.1.0 omits it. `report llm-create` (`client/report.py`) supplies no community of its own — it relies entirely on the client's — so on 4.1.0 a report requested for a sample in a private community is created without one. No error, wrong resource. 2. **A streaming download answered `204 No Content` raises `NoResultsException`.** The streaming path bypasses `parse_response`, so the 204 has to be raised by the session itself; 4.2.0 does that, 4.1.0 has no such raise anywhere in its session. The CLI's `download` commands depend on it for the no-results **exit code `1`** (§No-results signalling); against 4.1.0 an empty response reads as a successful download and exits `0`. The known-good rendering attributes (`ArtifactInstance.state`, `.known_good`/`.known_good_sources`, read by `formatters/text.py` — see [`03-formatters.md`](./03-formatters.md) §Known-good artifact instances) ship in **4.1.0**, so they are *not* what sets the floor; they are simply covered by it. +**The floor is how this repo expresses every SDK dependency.** No runtime probes for the +surfaces the floor names, and no per-test skip guards: if the CLI uses an SDK surface, the +floor names a version that has it, and `pip` enforces that at install time. (One carve-out +predates this and is documented where it lives: the known-good rendering attributes in +[`03-formatters.md`](./03-formatters.md), guarded belt-and-braces against a configuration +that is not supported rather than against a version the floor permits.) The hunt-page surfaces — +`ruleset_favorite` and the `YaraRulesetFavorite` resource, the `ruleset_list` filters, +`live_feed(livescan_id=, max_results=)`, and the tracking/provenance fields the +formatters render — are what moved the floor to 4.4.0. Code and tests use them +directly. + +**Raising the floor is the whole procedure** when this repo needs something new from +the SDK: + +1. Add the surface in `polyswarm-api`. +2. Bump the SDK's version **in that same PR** — minor for an additive surface. The + floor here cannot name a version the SDK has not declared, so this is the one case + where a feature PR carries the bump rather than the release step. +3. Raise `polyswarm_api>=` here to that version. +4. Use it. No guard, no `getattr`, no signature inspection. + +**Two consequences, both worth knowing before you do it.** + +*The two `develop` branches move in lockstep, by design.* CI installs the SDK by branch +name — `$CI_COMMIT_BRANCH.zip`, falling back to `develop.zip` — so a paired feature +branch tests against its opposite number, and once merged, each repo's `develop` tests +against the other's. That is the point: both ends carry the latest features and are +exercised against each other continuously, without waiting on a release. A change that +spans the pair is pushed to both ends together and merged to both `develop`s together. + +Out of lockstep, the mechanism says so immediately: this repo's `develop` asks for the +SDK's `develop.zip`, and if that archive does not yet declare the floor, `pip install +.[tests]` fails. That is the pairing being broken, not a trap to design around — the fix +is to land the SDK side, not to loosen the floor. + +*Publication is a separate, later cutoff.* Merging to `develop` publishes nothing; PyPI +only sees a version when `develop → master` merges. So the floor a feature PR sets is a +working value that `develop` integration validates. **At cutoff:** bump the SDK version +if the repo files do not already carry it, then set this repo's dependency to the version +actually being released. This repo cannot be released before that SDK release exists. + +*Behaviour changes to existing invocations are called out at the same cutoff.* This repo +has no CHANGELOG, so a change that alters what an unchanged command line does — a default +that moves, an argument that starts being rejected — is visible to users only if the +`develop → master` PR says so. List them there, and let the release be at least a minor. +A note that lives only in a spec is not a release note. + +*A missing paired branch now fails loudly.* If the SDK branch does not exist, CI falls +back to the SDK's `develop`, whose version does not satisfy the new floor, and +`pip install .[tests]` fails. That is the intended behaviour and an improvement: the +old fallback silently tested against an SDK that lacked the surfaces. + +*Version strings must be clean.* PEP 440 orders `4.4.0.dev0` **below** `4.4.0`, so a +dev-suffixed SDK build does not satisfy `>=4.4.0` — CI then goes to PyPI for a version +that does not exist yet. The SDK's `bump-my-version` config can emit that form; its +`AGENTS.md` carries the check. + ## Worked example — the httpx SDK migration The SDK's move to an `httpx`-based, three-layer architecture (pure-dataclass `PolyswarmRequest`, session-based execution, lazy generators) removed several 3.x affordances the CLI had reached into: diff --git a/src/polyswarm/__init__.py b/src/polyswarm/__init__.py index 5ee6158c..26a6c390 100644 --- a/src/polyswarm/__init__.py +++ b/src/polyswarm/__init__.py @@ -1 +1 @@ -__version__ = '4.3.0' +__version__ = '4.4.0' diff --git a/src/polyswarm/client/live.py b/src/polyswarm/client/live.py index 1e02f27e..f5cfbb4f 100644 --- a/src/polyswarm/client/live.py +++ b/src/polyswarm/client/live.py @@ -31,20 +31,60 @@ def live_stop(ctx, ruleset_id): @live.command('feed', short_help='Get results from live hunt.') -@click.option('-s', '--since', type=click.INT, default=1440, - help='How far back in seconds to request results (default: 1440).') +# IntRange(min=0) for the same reason as --max-results below: 0 is meaningful +# (no time filter), a negative is not, and bare INT would forward it. +@click.option('-s', '--since', type=click.IntRange(min=0), default=86400, + help='How far back in SECONDS to request results ' + '(default: 86400 — 24h). ' + 'Pass 0 for no time filter at all.') +# click.INT matches every other id option in the CLI and rejects a typo before +# it reaches the server. +@click.option('-i', '--livescan-id', type=click.INT, + help="Scope the feed to one live hunt (a ruleset's Live Hunt Id). " + 'Shows one community at a time, while ' + 'the badge counts all of them, so the counts need not match.') +# IntRange(min=0) refuses a negative rather than letting it silently mean unbounded. +@click.option('-m', '--max-results', type=click.IntRange(min=0), + help='Stop after this many results. Unset or 0 means no bound — ' + 'every page, as before.') @click.option('-r', '--rule-name', help='Filter results on this rule name.') @click.option('-f', '--family', help='Filter hunt results based on the family name.') @click.option('-l', '--polyscore-lower', help='Polyscore lower bound for the hunt results.') @click.option('-u', '--polyscore-upper', help='Polyscore upper bound for the hunt results.') @click.option('-p', '--private', is_flag=True, help='Filter results to only your private community.') @click.pass_context -def live_results(ctx, since, rule_name, family, polyscore_lower, polyscore_upper, private): +def live_results(ctx, since, livescan_id, max_results, rule_name, family, + polyscore_lower, polyscore_upper, private): + """Show live-hunt results. + + `--since` is SECONDS and defaults to 86400 (24h). Earlier versions defaulted + to a 24-minute window; pass `--since 1440` for that. Because the default + window is much wider and `--max-results` is unset by default, a bare + `live feed` pages through everything in it — bound it with `--max-results` + if that matters. + + `--livescan-id` scopes the feed to one live hunt, the drill-down for the + per-ruleset new-results count that `rules list` shows. + + The two do not have to agree, and a smaller feed is not a bug: that count + covers EVERY community the hunt runs in, public and private together, while + the feed shows one at a time (`--private` selects it). A hunt spanning both + shows fewer rows here than the count reports. + """ api = ctx.obj['api'] output = ctx.obj['output'] + # Sent only when passed. The request is byte-identical either way, so this + # exists to keep a pre-existing invocation's call shape unchanged. Note + # `since` is NOT folded in here: 0 must reach the SDK (specs/02). + kwargs = {} + if livescan_id is not None: + kwargs['livescan_id'] = livescan_id + if max_results: + kwargs['max_results'] = max_results for result in api.live_feed( since, rule_name=rule_name, family=family, - polyscore_lower=polyscore_lower, polyscore_upper=polyscore_upper, community='private' if private else None): + polyscore_lower=polyscore_lower, polyscore_upper=polyscore_upper, + community='private' if private else None, **kwargs): output.live_result(result) diff --git a/src/polyswarm/client/rules.py b/src/polyswarm/client/rules.py index 5a544b90..9f8fa7e2 100644 --- a/src/polyswarm/client/rules.py +++ b/src/polyswarm/client/rules.py @@ -1,5 +1,8 @@ import click +from polyswarm_api import exceptions as api_exceptions + +from polyswarm import exceptions from polyswarm.client import utils @@ -28,15 +31,75 @@ def delete(ctx, rule_id): output.ruleset(api.ruleset_delete(rule_id)) -@rules.command('list', short_help='List all rulesets.') +@rules.command('list', short_help='List rulesets, optionally filtered.') +@click.option('-n', '--name', help='Substring match on the ruleset name (case-insensitive).') +@click.option('-s', '--status', type=click.Choice(['active']), + help='Only rulesets whose live hunt is currently running.') +@click.option('--favorites-only', is_flag=True, help='Only favorited (starred) rulesets.') +@click.option('--has-new-results', is_flag=True, + help='Only rulesets whose stored new-results counter is positive.') @click.pass_context -def list_rules(ctx): +def list_rules(ctx, name, status, favorites_only, has_new_results): + """List rulesets, optionally filtered. All filters are conjunctive. + + Filtering is applied SERVER-side: the list is keyset-paginated, so a + client filtering locally would have to walk every page to find matches. + """ api = ctx.obj['api'] output = ctx.obj['output'] - for ruleset in api.ruleset_list(): + # A False flag is not a filter: send only what the caller actually asked for. + kwargs = {k: v for k, v in (('name', name), ('status', status), + ('favorites_only', favorites_only or None), + ('has_new_results', has_new_results or None)) + if v is not None} + for ruleset in api.ruleset_list(**kwargs): output.ruleset(ruleset) +@rules.command('favorite', short_help='Favorite (star) or unfavorite a ruleset.') +@click.argument('rule_id', type=click.INT, required=True) +@click.option('--unfavorite', is_flag=True, help='Remove the star instead.') +@click.pass_context +def favorite(ctx, rule_id, unfavorite): + """Star a ruleset for the whole team (or unstar with --unfavorite). + + Stars are shared by the team and capped server-side; the response renders + the new state plus the budget ("N of M favorites used"). When the budget + is full the server refuses with a machine-readable FAVORITE_LIMIT error, + rendered here as a clean message rather than a traceback (exit 2, not 1 — + 1 is reserved for no-results/not-found). + """ + api = ctx.obj['api'] + output = ctx.obj['output'] + try: + output.ruleset_favorite(api.ruleset_favorite(rule_id, favorite=not unfavorite)) + except api_exceptions.RequestException as exc: + # `exc.request` needs no guard: __init__ always assigns it, and a None + # request flows safely through the getattr. + errors = getattr(exc.request, 'errors', None) or {} + if isinstance(errors, dict) and errors.get('code') == 'FAVORITE_LIMIT': + used = errors.get('favorites_used') + limit = errors.get('favorites_limit') + # Counters are advisory; fall back rather than render "(None of None)". + # `.json` is the response envelope and `result` a key inside it — + # the request has no `.result`, only a private `._result`. getattr + # so a bare request still reaches the message (specs/05). + envelope = getattr(exc.request, 'json', None) + server_msg = envelope.get('result') if isinstance(envelope, dict) else None + budget = (f'Favorite limit reached ({used} of {limit} used).' + if used is not None and limit is not None + else (server_msg if isinstance(server_msg, str) + else 'Favorite limit reached.')) + # PolyswarmException exits 2; ClickException would exit 1, reserved + # for no-results/not-found. + # Only starring can hit the cap, and only it has a remedy. + remedy = ('' if unfavorite else + ' Unfavorite another ruleset first: ' + '`polyswarm rules favorite --unfavorite`.') + raise exceptions.PolyswarmException(f'{budget}{remedy}') from exc + raise + + @rules.command('update', short_help='Update a ruleset.') @click.argument('rule_id', type=click.INT, required=True) @click.option('-n', '--name', type=str, help='Name of the ruleset.') diff --git a/src/polyswarm/formatters/base.py b/src/polyswarm/formatters/base.py index 8f28c3cf..dac67ee3 100644 --- a/src/polyswarm/formatters/base.py +++ b/src/polyswarm/formatters/base.py @@ -22,6 +22,9 @@ def local_artifact(self, result): def ruleset(self, result, contents=False): raise NotImplementedError + def ruleset_favorite(self, result): + raise NotImplementedError + def iocs(self, iocs, write=True): raise NotImplementedError diff --git a/src/polyswarm/formatters/json.py b/src/polyswarm/formatters/json.py index 445e0d01..4157f371 100644 --- a/src/polyswarm/formatters/json.py +++ b/src/polyswarm/formatters/json.py @@ -109,6 +109,9 @@ def local_artifact(self, artifact): def ruleset(self, result, contents=False): click.echo(self._to_json(result.json), file=self.out) + def ruleset_favorite(self, result): + click.echo(self._to_json(result.json), file=self.out) + def metadata(self, result): click.echo(self._to_json(result.json), file=self.out) diff --git a/src/polyswarm/formatters/text.py b/src/polyswarm/formatters/text.py index 1810eba6..0b32d90d 100644 --- a/src/polyswarm/formatters/text.py +++ b/src/polyswarm/formatters/text.py @@ -76,8 +76,10 @@ def artifact_instance(self, instance, write=True, timeout=False): if not instance.failed: output.append(self._white(f'Scan permalink: {instance.permalink}')) - # Defensive getattr: these attributes ship in the paired SDK release, but a - # CLI running against an older installed SDK won't have them (no AttributeError). + # Defensive getattr, kept deliberately: these attributes ship in 4.1.0, well + # below the dependency floor, so the pin already guarantees them. This is + # belt-and-braces for an unsupported configuration, NOT the version-probing + # the floor replaced -- don't add siblings for a version the floor permits. # The bounty state (KNOWN_GOOD) is the only reliable signal that this artifact is # a known-good binary whose bytes are withheld — it alone decides. known_good_sources # (the flagging feeds) is emitted for any instance whose sha256 matches a known-good @@ -201,6 +203,17 @@ def hunt(self, result, write=True): self._close_group() if result.ruleset_name is not None: output.append(self._white(f'Ruleset Name: {result.ruleset_name}')) + # Source-rule provenance. The pin guarantees the SDK parses these, + # so None means the SERVER had no answer (specs/03). + if result.rule_id is not None: + output.append(self._white(f'Source Ruleset Id: {result.rule_id}')) + if result.rule_modified is not None: + output.append(self._white(f'Source ruleset last modified at freeze: {result.rule_modified}')) + if result.source_rule_changed is not None: + # Tri-state upstream: None (unknown) prints nothing; the label + # names the reference point so it can't read as "edited recently". + changed = 'yes' if result.source_rule_changed else 'no' + output.append(self._white(f'Source ruleset changed since this hunt froze it: {changed}')) if result.yara: output.append(self._white(f'Ruleset Contents:\n{result.yara}')) return self._output(output, write) @@ -284,10 +297,48 @@ def ruleset(self, result, write=True, contents=False): output.append(self._white(f'Description: {result.description}')) output.append(self._white(f'Created at: {result.created}')) output.append(self._white(f'Modified at: {result.modified}')) + # The pin guarantees the SDK parses these, so None means the SERVER + # had no answer — never an older SDK (specs/03). + if result.favorite: + output.append(self._yellow('Favorite: yes')) + if result.favorited_at is not None: + output.append(self._white(f'Favorited at: {result.favorited_at}')) + if result.rule_count is not None: + output.append(self._white(f'Rules in ruleset: {result.rule_count}')) + if result.historical_hunt_count is not None: + output.append(self._white(f'Historical hunts triggered: {result.historical_hunt_count}')) + if result.new_results_count is not None: + # The badge is a stored counter the server's scheduled refresh + # maintains. The response carries no window and a caller cannot + # choose one, so the label must not imply a window; the marker + # below is what says how fresh the number is. + output.append(self._white(f'New live results: {result.new_results_count}')) + if result.new_results_counted_at is not None: + output.append(self._white(f'New-results count refreshed at: {result.new_results_counted_at}')) if contents: output.append(self._white(f'Ruleset Contents:\n{result.yara}')) return self._output(output, write) + def ruleset_favorite(self, result, write=True): + output = [] + output.append(self._blue(f'Ruleset Id: {result.id}')) + starred = result.favorite + # Nested under `starred`, matching `ruleset` above: the timestamp + # describes the star, so an unstar response still carrying one must not + # render "Favorite: no" with a "Favorited at" beneath it. + if starred: + output.append(self._yellow('Favorite: yes')) + if result.favorited_at is not None: + output.append(self._white(f'Favorited at: {result.favorited_at}')) + else: + output.append(self._white('Favorite: no')) + used = result.favorites_used + limit = result.favorites_limit + if used is not None and limit is not None: + # server-owned budget counters — the client never counts + output.append(self._white(f'Favorites used: {used} of {limit}')) + return self._output(output, write) + def tag_link(self, result, write=True): output = [] output.append(self._blue(f'SHA256: {result.sha256}')) diff --git a/tests/cli_test.py b/tests/cli_test.py index 6f23b7c3..2acb3bc4 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -174,23 +174,23 @@ class LiveHuntTest(BaseTestCase): @vcr.use_cassette() def test_live_hunt_start_json(self): result = self._run_cli([ - '--output-format', 'json', 'live', 'start', '17388152480558505']) + '--output-format', 'json', 'live', 'start', '44051669277897879']) self._assert_json_result(result, self.click_vcr(result)) @vcr.use_cassette() def test_live_hunt_start_text(self): result = self._run_cli([ - '--output-format', 'text', 'live', 'start', '17388152480558505']) + '--output-format', 'text', 'live', 'start', '44051669277897879']) self._assert_text_result(result, self.click_vcr(result)) @vcr.use_cassette() def test_live_hunt_stop_json(self): - result = self._run_cli(['--output-format', 'json', 'live', 'stop', '17388152480558505']) + result = self._run_cli(['--output-format', 'json', 'live', 'stop', '44051669277897879']) self._assert_json_result(result, self.click_vcr(result)) @vcr.use_cassette() def test_live_hunt_stop_text(self): - result = self._run_cli(['--output-format', 'text', 'live', 'stop', '17388152480558505']) + result = self._run_cli(['--output-format', 'text', 'live', 'stop', '44051669277897879']) self._assert_text_result(result, self.click_vcr(result)) @@ -210,13 +210,13 @@ def test_historical_hunt_create_text(self): @vcr.use_cassette() def test_historical_hunt_delete_json(self): result = self._run_cli([ - '--output-format', 'json', 'historical', 'delete', '75914219779430298']) + '--output-format', 'json', 'historical', 'delete', '32808041501095355']) self._assert_json_result(result, self.click_vcr(result)) @vcr.use_cassette() def test_historical_hunt_delete_text(self): result = self._run_cli([ - '--output-format', 'text', 'historical', 'delete', '96916002705221564']) + '--output-format', 'text', 'historical', 'delete', '3220090199138422']) self._assert_text_result(result, self.click_vcr(result)) @vcr.use_cassette() @@ -240,19 +240,19 @@ def test_ruleset_create_json(self): @vcr.use_cassette() def test_ruleset_view_json(self): result = self._run_cli([ - '--output-format', 'json', 'rules', 'view', '27214252780064715']) + '--output-format', 'json', 'rules', 'view', '78562964231669682']) self._assert_json_result(result, self.click_vcr(result)) @vcr.use_cassette() def test_ruleset_update_json(self): result = self._run_cli([ - '--output-format', 'json', 'rules', 'update', '71213140536342873', '--name', 'test2']) + '--output-format', 'json', 'rules', 'update', '4202182245812695', '--name', 'test2']) self._assert_json_result(result, self.click_vcr(result)) @vcr.use_cassette() def test_ruleset_delete_json(self): result = self._run_cli([ - '--output-format', 'json', 'rules', 'delete', '71213140536342873']) + '--output-format', 'json', 'rules', 'delete', '4202182245812695']) self._assert_json_result(result, self.click_vcr(result)) @vcr.use_cassette() @@ -261,6 +261,24 @@ def test_ruleset_list_json(self): '--output-format', 'json', 'rules', 'list']) self._assert_json_result(result, self.click_vcr(result)) + @vcr.use_cassette() + def test_ruleset_favorite_text(self): + result = self._run_cli([ + '--output-format', 'text', 'rules', 'favorite', '96652060989160147']) + self._assert_text_result(result, self.click_vcr(result)) + + @vcr.use_cassette() + def test_ruleset_unfavorite_text(self): + result = self._run_cli([ + '--output-format', 'text', 'rules', 'favorite', '96652060989160147', + '--unfavorite']) + self._assert_text_result(result, self.click_vcr(result)) + + @vcr.use_cassette() + def test_ruleset_favorite_json(self): + result = self._run_cli([ + '--output-format', 'json', 'rules', 'favorite', '14883307518120680']) + self._assert_json_result(result, self.click_vcr(result)) class SubmissionTest(BaseTestCase): @vcr.use_cassette() diff --git a/tests/formatter_hunt_fields_test.py b/tests/formatter_hunt_fields_test.py new file mode 100644 index 00000000..1d0675da --- /dev/null +++ b/tests/formatter_hunt_fields_test.py @@ -0,0 +1,459 @@ +"""The hunt-page tracking legs of the text formatter, and the flag that +reaches them. + +Pins two contracts: + +* the rendering legs against REAL SDK resources built from literal dicts, so + the tests are coupled to the SDK's actual attribute names — and they + additionally pin that ``favorited_at`` / ``rule_modified`` arrive as parsed + datetimes. The pin guarantees those attributes exist, so ``None`` here means + the SERVER had no answer; and +* the command plumbing: an UNFILTERED ``rules list`` still calls a + zero-argument ``ruleset_list()`` (a False flag is not a filter), a + FILTERED one forwards exactly the filters given, ``live feed`` forwards + ``--livescan-id`` / ``--max-results`` only when passed, and ``rules + favorite`` renders the toggle response and converts the machine-readable + FAVORITE_LIMIT refusal into a clean message. All are asserted through + autospec'd mocks, so every call is signature-checked against the SDK the + pin actually installs. +""" +from unittest import TestCase, mock + +from click.testing import CliRunner + +from polyswarm.client import polyswarm as client +from polyswarm.formatters import text +from polyswarm_api import core, exceptions, resources + + + +def _ruleset(**overrides): + content = dict(id='5', livescan_id=None, livescan_created=None, name='n', + description='d', created='2026-08-20T00:00:00+00:00', + modified='2026-08-20T00:00:00+00:00', deleted=False, yara=None) + content.update(overrides) + return resources.YaraRuleset(content, api=None) + + +def _hunt(**overrides): + content = dict(id='9', status='PENDING', progress=0.0, active=None, + created='2026-08-20T00:00:00+00:00', summary=None, + results_csv_uri=None, ruleset_name='n', yara=None) + content.update(overrides) + return resources.HistoricalHunt(content, api=None) + + + + + + +class FormatterHuntFieldsTest(TestCase): + def _render(self, method, result, **kwargs): + # write=False and join the returned lines, per specs/04 Style 3 — no + # stream, matching known_good_field_test.py. + return '\n'.join( + getattr(text.TextOutput(color=False), method)(result, write=False, **kwargs)) + def test_ruleset_tracking_fields_render_with_zero_distinct_from_absent(self): + rendered = self._render('ruleset', _ruleset( + favorite=True, favorited_at='2026-08-20T12:00:00+00:00', rule_count=0, + historical_hunt_count=0, new_results_count=3)) + assert 'Favorite: yes' in rendered + # parse_isoformat: the SDK hands the formatter a datetime, not the wire string + assert 'Favorited at: 2026-08-20 12:00:00+00:00' in rendered + assert 'Rules in ruleset: 0' in rendered + assert 'Historical hunts triggered: 0' in rendered + assert 'New live results: 3' in rendered + # The count arrived without its marker: the inner guard's False arm. + # Without this the guard could be inverted and nothing would fail. + assert 'New-results count refreshed at' not in rendered + def test_a_favorite_without_a_timestamp_renders_the_state_alone(self): + # favorite=True carries favorited_at only when the server has one; the + # nested guard's False arm, unreached by the tests above. + rendered = self._render('ruleset', _ruleset(favorite=True, favorited_at=None)) + assert 'Favorite: yes' in rendered + assert 'Favorited at' not in rendered + def test_ruleset_staleness_marker_renders_beside_the_count(self): + # The stored badge's marker: how fresh the number is. Rendered only + # with a count (the server sends them together). + rendered = self._render('ruleset', _ruleset( + new_results_count=0, + new_results_counted_at='2026-08-25T12:00:00+00:00')) + assert 'New live results: 0' in rendered + assert 'New-results count refreshed at: 2026-08-25 12:00:00+00:00' in rendered + def test_ruleset_favorite_response_renders_state_and_budget(self): + rendered = self._render('ruleset_favorite', resources.YaraRulesetFavorite( + {'id': '5', 'favorite': True, + 'favorited_at': '2026-08-25T12:00:00+00:00', + 'favorites_used': 3, 'favorites_limit': 5}, api=None)) + assert 'Ruleset Id: 5' in rendered + assert 'Favorite: yes' in rendered + assert 'Favorited at: 2026-08-25 12:00:00+00:00' in rendered + assert 'Favorites used: 3 of 5' in rendered + def test_ruleset_unfavorite_response_renders_no_state(self): + rendered = self._render('ruleset_favorite', resources.YaraRulesetFavorite( + {'id': '5', 'favorite': False, 'favorited_at': None, + 'favorites_used': 2, 'favorites_limit': 5}, api=None)) + assert 'Favorite: no' in rendered + assert 'Favorited at' not in rendered + assert 'Favorites used: 2 of 5' in rendered + def test_unfavorite_carrying_a_stale_timestamp_hides_it(self): + """An unstar response that still carries `favorited_at` must not render + it: the timestamp describes the star. No cassette produces this — the + server sends null — so it is unit-pinned.""" + rendered = self._render('ruleset_favorite', resources.YaraRulesetFavorite( + {'id': '5', 'favorite': False, + 'favorited_at': '2026-08-25T12:00:00+00:00', + 'favorites_used': 2, 'favorites_limit': 5}, api=None)) + assert 'Favorite: no' in rendered + assert 'Favorited at' not in rendered + + def test_ruleset_none_and_false_fields_are_omitted(self): + rendered = self._render('ruleset', _ruleset( + favorite=False, favorited_at=None, rule_count=None, + historical_hunt_count=None, new_results_count=None)) + assert 'Favorite' not in rendered + assert 'Rules in ruleset' not in rendered + assert 'Historical hunts triggered' not in rendered + assert 'New live results' not in rendered + + def test_hunt_provenance_fields_render_with_the_reference_point(self): + rendered = self._render('hunt', _hunt( + rule_id='5', rule_modified='2026-08-20T12:00:00+00:00', + source_rule_changed=False)) + assert 'Source Ruleset Id: 5' in rendered + assert 'Source ruleset last modified at freeze: 2026-08-20 12:00:00+00:00' in rendered + assert 'Source ruleset changed since this hunt froze it: no' in rendered + + def test_hunt_unknown_tri_state_prints_nothing(self): + rendered = self._render('hunt', _hunt( + rule_id=None, rule_modified=None, source_rule_changed=None)) + assert 'Source' not in rendered + + + +class RulesListZeroArgTest(TestCase): + """`rules list` calls a zero-argument ``ruleset_list()`` — a False flag + is not a filter, so an unfiltered list forwards no + behaviour at all. autospec makes the assertion a signature check against + the installed SDK.""" + + def test_list_passes_no_kwargs_at_all(self): + with mock.patch('polyswarm_api.api.PolyswarmAPI.ruleset_list', + autospec=True, return_value=iter(())) as ruleset_list: + result = CliRunner().invoke( + client.polyswarm_cli, + ['-a', '1' * 32, '-u', 'http://ai:9696/v3', '-c', 'gamma', + 'rules', 'list'], + catch_exceptions=False) + assert result.exit_code == 0, result.output + ruleset_list.assert_called_once_with(mock.ANY) + def test_filters_are_forwarded_only_when_given(self): + """A filtered list forwards exactly the filters passed and nothing + else — the flags default to False, and a False flag must not become + `favorites_only=False`, which would be a filter the caller never + asked for.""" + with mock.patch('polyswarm_api.api.PolyswarmAPI.ruleset_list', + autospec=True, return_value=iter(())) as ruleset_list: + result = CliRunner().invoke( + client.polyswarm_cli, + ['-a', '1' * 32, '-u', 'http://ai:9696/v3', '-c', 'gamma', + 'rules', 'list', '--name', 'alpha', '--favorites-only', + '--status', 'active', '--has-new-results'], + catch_exceptions=False) + assert result.exit_code == 0, result.output + # All four, and autospec makes this a SIGNATURE check against the + # installed SDK: a kwarg only this side renamed fails here rather than + # reaching the server as a filter it silently ignores. + ruleset_list.assert_called_once_with( + mock.ANY, name='alpha', status='active', + favorites_only=True, has_new_results=True) + + + +class LiveFeedOptionsTest(TestCase): + """`live feed` — the badge's drill-down (--livescan-id) and its bound + (--max-results). Both are forwarded only when passed, so every existing + invocation reaches the SDK exactly as it did before.""" + + def _invoke(self, *extra): + with mock.patch('polyswarm_api.api.PolyswarmAPI.live_feed', + autospec=True, return_value=iter(())) as live_feed: + result = CliRunner().invoke( + client.polyswarm_cli, + ['-a', '1' * 32, '-u', 'http://ai:9696/v3', '-c', 'gamma', + 'live', 'feed', *extra], + catch_exceptions=False) + return result, live_feed + + def test_plain_feed_forwards_neither_new_kwarg(self): + result, live_feed = self._invoke() + assert result.exit_code == 0, result.output + _, kwargs = live_feed.call_args + assert 'livescan_id' not in kwargs and 'max_results' not in kwargs + # the default window is 86400 SECONDS (24h), passed positionally — the + # wire is seconds and stays seconds, so the CLI default carries the 24h + assert live_feed.call_args[0][1] == 86400 + def test_livescan_id_and_max_results_are_forwarded(self): + result, live_feed = self._invoke( + '--livescan-id', '72927285313305230', '--max-results', '5') + assert result.exit_code == 0, result.output + _, kwargs = live_feed.call_args + # click.INT, and Python ints are arbitrary precision — a 17-digit id + # survives exactly, which is the whole reason the server renders it as + # a string for JS consumers. + assert kwargs['livescan_id'] == 72927285313305230 + assert kwargs['max_results'] == 5 + + def test_zero_max_results_is_unbounded_and_never_reaches_the_sdk(self): + """--max-results 0 is the pre-existing unbounded behaviour, so it must not + be forwarded — omitting it is what already means "no bound".""" + result, live_feed = self._invoke('--max-results', '0') + assert result.exit_code == 0, result.output + _, kwargs = live_feed.call_args + assert 'max_results' not in kwargs + + def test_zero_since_IS_forwarded_unlike_zero_max_results(self): + """Both zeros mean "no bound" to the user and take OPPOSITE paths: + --max-results 0 is dropped before the SDK (above), while --since 0 must + reach it, because 0 is how the server is told to apply no time filter. + Fold `since` into the conditional-kwargs block and this breaks.""" + result, live_feed = self._invoke('--since', '0') + assert result.exit_code == 0, result.output + assert live_feed.call_args[0][1] == 0 + + + @staticmethod + def _assert_refused_at_parse_time(result, option): + """A non-zero exit is NOT enough here. An unvalidated value is forwarded + and the request then fails on its own (no such host), which also exits + non-zero — so `exit_code != 0` passes whether or not the guard exists. + Click refuses a bad value with a UsageError before any request is made: + exit 2, and a message naming the option. Assert that instead. + """ + assert result.exit_code == 2, result.output + assert 'Invalid value' in result.output, result.output + assert option in result.output, result.output + + def test_a_negative_max_results_is_refused_at_the_interface(self): + result = CliRunner().invoke( + client.polyswarm_cli, + ['-a', '1' * 32, '-u', 'http://ai:9696/v3', '-c', 'gamma', + 'live', 'feed', '--max-results', '-1']) + self._assert_refused_at_parse_time(result, '--max-results') + + def test_a_negative_since_is_refused_at_the_interface(self): + """--since 0 is meaningful (no time filter, asserted above) but a + negative is not, and it would be forwarded verbatim to the server. + Same guard as --max-results, for the same reason.""" + result = CliRunner().invoke( + client.polyswarm_cli, + ['-a', '1' * 32, '-u', 'http://ai:9696/v3', '-c', 'gamma', + 'live', 'feed', '--since', '-1']) + self._assert_refused_at_parse_time(result, '--since') + + def test_a_non_numeric_livescan_id_is_refused_before_the_server(self): + result = CliRunner().invoke( + client.polyswarm_cli, + ['-a', '1' * 32, '-u', 'http://ai:9696/v3', '-c', 'gamma', + 'live', 'feed', '--livescan-id', 'not-an-id']) + self._assert_refused_at_parse_time(result, '--livescan-id') + + + + +class RulesFavoriteCommandTest(TestCase): + """`rules favorite` — the CLI leg of the favorite capability: renders the + toggle response (state + server-owned budget counters), passes the right + boolean for --unfavorite, and converts the machine-readable FAVORITE_LIMIT + refusal into a clean actionable message instead of a traceback.""" + + def _invoke(self, args, side_effect=None, return_value=None): + with mock.patch('polyswarm_api.api.PolyswarmAPI.ruleset_favorite', + autospec=True, side_effect=side_effect, + return_value=return_value) as toggle: + result = CliRunner().invoke( + client.polyswarm_cli, + ['-a', '1' * 32, '-u', 'http://ai:9696/v3', '-c', 'gamma', + 'rules', 'favorite'] + args, + catch_exceptions=False) + return result, toggle + + @staticmethod + def _response(favorite): + return resources.YaraRulesetFavorite( + {'id': '5', 'favorite': favorite, + 'favorited_at': '2026-08-25T12:00:00+00:00' if favorite else None, + 'favorites_used': 1, 'favorites_limit': 5}, api=None) + def test_favorite_calls_the_sdk_and_renders_the_budget(self): + result, toggle = self._invoke(['5'], return_value=self._response(True)) + assert result.exit_code == 0, result.output + toggle.assert_called_once_with(mock.ANY, 5, favorite=True) + assert 'Favorite: yes' in result.output + assert 'Favorites used: 1 of 5' in result.output + def test_unfavorite_flag_flips_the_boolean(self): + result, toggle = self._invoke(['5', '--unfavorite'], + return_value=self._response(False)) + assert result.exit_code == 0, result.output + toggle.assert_called_once_with(mock.ANY, 5, False) + assert 'Favorite: no' in result.output + def test_favorite_limit_refusal_is_a_clean_message_at_exit_2(self): + # Exit 2 is the central mapping's code for this, never 1; exit 1 is + # reserved for no-results/not-found. The friendly message rides a CLI + # PolyswarmException so ExceptionHandlingGroup logs it cleanly. + request = mock.Mock() + request.errors = {'code': 'FAVORITE_LIMIT', + 'favorites_used': 5, 'favorites_limit': 5} + refusal = exceptions.RequestException(request) + with mock.patch('polyswarm_api.api.PolyswarmAPI.ruleset_favorite', + autospec=True, side_effect=refusal): + result = CliRunner().invoke( + client.polyswarm_cli, + ['-a', '1' * 32, '-u', 'http://ai:9696/v3', '-c', 'gamma', + 'rules', 'favorite', '5']) + assert result.exit_code == 2, result.output + assert 'Favorite limit reached (5 of 5 used)' in result.output + assert '--unfavorite' in result.output # names the way out + assert 'Traceback' not in result.output + def test_favorite_limit_without_counters_uses_the_server_message(self): + # The counters are advisory; an envelope can carry the code without + # them. Interpolating them unguarded rendered "(None of None used)" at + # the user, so the server's own message is the fallback. + # A REAL request, not a Mock: a Mock fabricates whatever attribute it + # is asked for, so it passed even while the code read a spelling the + # request object does not have. Assigning `.json` below fabricates it + # just as effectively, so pin the spelling on the UNTOUCHED request + # first — that is the part a Mock could never have told us, and it is + # what fails if the SDK ever renames the envelope. + request = core.PolyswarmRequest(api=None, method='PUT', url='http://x') + assert hasattr(request, 'json'), 'SDK renamed the response envelope' + assert not hasattr(request, 'result'), 'the wrong spelling became real' + request.json = {'errors': {'code': 'FAVORITE_LIMIT'}, + 'result': 'Favorite limit reached (5 of 5 used).', + 'status': 'error'} + request.errors = request.json['errors'] + refusal = exceptions.RequestException(request) + with mock.patch('polyswarm_api.api.PolyswarmAPI.ruleset_favorite', + autospec=True, side_effect=refusal): + result = CliRunner().invoke( + client.polyswarm_cli, + ['-a', '1' * 32, '-u', 'http://ai:9696/v3', '-c', 'gamma', + 'rules', 'favorite', '5']) + assert result.exit_code == 2, result.output + assert 'None of None' not in result.output + assert 'Favorite limit reached (5 of 5 used).' in result.output + assert '--unfavorite' in result.output + def test_a_list_shaped_errors_envelope_does_not_crash_the_handler(self): + """The SDK carries a legacy LIST shape for `errors` alongside the + mapping. Only the mapping carries a machine-readable code, so a + list-shaped refusal cannot be FAVORITE_LIMIT — the `isinstance` guard + exists so such a refusal falls through to the generic path instead of + raising on `.get`. Still exit 2, still no traceback.""" + request = core.PolyswarmRequest(api=None, method='PUT', url='http://x') + request.errors = [{'code': 'FAVORITE_LIMIT', 'favorites_used': 5}] + refusal = exceptions.RequestException(request) + with mock.patch('polyswarm_api.api.PolyswarmAPI.ruleset_favorite', + autospec=True, side_effect=refusal): + result = CliRunner().invoke( + client.polyswarm_cli, + ['-a', '1' * 32, '-u', 'http://ai:9696/v3', '-c', 'gamma', + 'rules', 'favorite', '5']) + assert result.exit_code == 2, result.output + assert 'Traceback' not in result.output + + def test_favorite_limit_on_a_request_without_result_still_has_no_traceback(self): + # A Mock has every attribute, so the test above cannot fail on a missing + # `.result`. This one uses a real object that genuinely lacks it — the + # handler exists to avoid a traceback and must not raise one reaching + # for its own fallback. + class BareRequest: + errors = {'code': 'FAVORITE_LIMIT'} + + refusal = exceptions.RequestException(BareRequest()) + with mock.patch('polyswarm_api.api.PolyswarmAPI.ruleset_favorite', + autospec=True, side_effect=refusal): + result = CliRunner().invoke( + client.polyswarm_cli, + ['-a', '1' * 32, '-u', 'http://ai:9696/v3', '-c', 'gamma', + 'rules', 'favorite', '5']) + assert result.exit_code == 2, result.output + assert 'Traceback' not in result.output + assert 'None' not in result.output + assert '--unfavorite' in result.output + def test_a_list_shaped_json_envelope_does_not_crash_the_handler(self): + """The `errors` mapping is isinstance-guarded so a non-mapping falls + through; `.json` was not, so a non-dict envelope raised inside the very + handler that exists to avoid a traceback. Needs all three at once: a + dict `errors` carrying the code, no counters, and a non-dict `.json`.""" + request = core.PolyswarmRequest(api=None, method='PUT', url='http://x') + request.errors = {'code': 'FAVORITE_LIMIT'} + request.json = [{'result': 'a list, not the envelope'}] + refusal = exceptions.RequestException(request, 'refused') + with mock.patch('polyswarm_api.api.PolyswarmAPI.ruleset_favorite', + autospec=True, side_effect=refusal): + result = CliRunner().invoke( + client.polyswarm_cli, + ['-a', '1' * 32, '-u', 'http://ai:9696/v3', '-c', 'gamma', + 'rules', 'favorite', '5']) + assert result.exit_code == 2, result.output + assert 'Traceback' not in result.output + assert 'contact support' not in result.output + assert 'Favorite limit reached.' in result.output + + def test_the_unfavorite_direction_gets_no_remedy_sentence(self): + """Only the star direction can hit the cap and only it has a remedy — + telling someone unstarring to unstar something else cannot help. Every + other limit test invokes WITHOUT --unfavorite, so the empty-remedy arm + never ran and inverting the conditional would not have failed one.""" + request = mock.Mock() + request.errors = {'code': 'FAVORITE_LIMIT', + 'favorites_used': 5, 'favorites_limit': 5} + refusal = exceptions.RequestException(request) + with mock.patch('polyswarm_api.api.PolyswarmAPI.ruleset_favorite', + autospec=True, side_effect=refusal): + result = CliRunner().invoke( + client.polyswarm_cli, + ['-a', '1' * 32, '-u', 'http://ai:9696/v3', '-c', 'gamma', + 'rules', 'favorite', '5', '--unfavorite']) + assert result.exit_code == 2, result.output + assert 'Favorite limit reached (5 of 5 used).' in result.output + assert 'Unfavorite another ruleset first' not in result.output + + def test_other_refusals_still_raise(self): + request = mock.Mock() + request.errors = None + # Every production raise site passes a message alongside the request + # (`RequestException(request, err_msg)`), so build it that way: with no + # message the refusal renders blank and there is nothing positive left + # to assert. + refusal = exceptions.RequestException(request, 'Ruleset not found.') + with mock.patch('polyswarm_api.api.PolyswarmAPI.ruleset_favorite', + autospec=True, side_effect=refusal): + result = CliRunner().invoke( + client.polyswarm_cli, + ['-a', '1' * 32, '-u', 'http://ai:9696/v3', '-c', 'gamma', + 'rules', 'favorite', '5']) + assert result.exit_code == 2 # PolyswarmException family + # The claim is that a non-limit refusal FALLS THROUGH, so assert the + # server's own message reaches the user and the limit-specific message + # does not. Asserting only that the raw code stayed out of the output + # tested neither: the handler's message doesn't contain the literal + # 'FAVORITE_LIMIT' either, so the test passed with the branch forced to + # treat every refusal as the limit case. Verified by doing exactly that. + assert 'Ruleset not found.' in result.output + assert 'Favorite limit reached' not in result.output + assert 'FAVORITE_LIMIT' not in result.output # nor the raw code + assert 'Traceback' not in result.output + +class ExitCodeHierarchyTest(TestCase): + """`rules favorite`'s non-limit refusals exit 2, and that holds only because + the SDK's RequestException is a PolyswarmException — the handler catches + that base BEFORE the transport branch, which matches the bare name + 'RequestException' against the MRO and would exit 1 with "contact support". + Reparent it in the SDK and every fixable 4xx starts giving that advice, so + the dependency is pinned here rather than inferred.""" + + def test_request_exception_is_caught_as_a_polyswarm_exception(self): + assert issubclass(exceptions.RequestException, + exceptions.PolyswarmException) + + diff --git a/tests/vcr/test_historical_hunt_create_json.click b/tests/vcr/test_historical_hunt_create_json.click index 08798308..e2d1f319 100644 --- a/tests/vcr/test_historical_hunt_create_json.click +++ b/tests/vcr/test_historical_hunt_create_json.click @@ -1,15 +1,18 @@ -result: '{"created": "2022-05-26T19:08:30.323397", "id": "96916002705221564", "progress": - null, "results_csv_uri": null, "ruleset_name": "eicar.yara", "status": "PENDING", - "summary": null, "yara": "rule eicar_av_test {\n /*\n Per standard, match - only if entire file is EICAR string plus optional trailing whitespace.\n The +result: '{"account_number": "111", "archives_in_flight": 0, "archives_scanned": 0, + "archives_total": 0, "communities": ["gamma"], "created": "2026-08-25T18:27:00.017968+00:00", + "failed_max_retries": 0, "failed_other": 0, "id": "58957063950682201", "progress": + null, "results_csv_uri": null, "rule_id": null, "rule_modified": null, "ruleset_name": + "eicar.yara", "source_rule_changed": null, "status": "PENDING", "summary": null, + "user_account_number": "111", "yara": "rule eicar_av_test : eicar match {\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}"} + of them\n}\n\nrule eicar_substring_test : eicar substring {\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}"} ' diff --git a/tests/vcr/test_historical_hunt_create_json.vcr b/tests/vcr/test_historical_hunt_create_json.vcr index 983df6c0..384aee42 100644 --- a/tests/vcr/test_historical_hunt_create_json.vcr +++ b/tests/vcr/test_historical_hunt_create_json.vcr @@ -1,117 +1,72 @@ 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 + body: '{"yara":"rule eicar_av_test : eicar match {\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}", "ruleset_name": "eicar.yara"}' + of them\n}\n\nrule eicar_substring_test : eicar substring {\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}","ruleset_name":"eicar.yara","community":"gamma"}' headers: - Accept: + accept: - '*/*' - Accept-Encoding: + accept-encoding: - gzip, deflate - Authorization: + authorization: - '11111111111111111111111111111111' - Connection: + connection: - keep-alive - Content-Length: - - '1143' - Content-Type: + content-length: + - '1192' + content-type: - application/json - User-Agent: - - polyswarm-api/3.0.0 (x86_64-Linux-CPython-3.6.5) + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: POST uri: http://artifact-index-e2e:9696/v3/hunt/historical response: body: - string: ' - - Redirecting... - -

Redirecting...

- -

You should be redirected automatically to target URL: http://artifact-index-e2e:9696/v3/hunt/historical/. If - not click the link.' - headers: - Content-Length: - - '307' - Content-Type: - - text/html; charset=utf-8 - Date: - - Thu, 26 May 2022 19:08:30 GMT - Location: - - http://artifact-index-e2e:9696/v3/hunt/historical/ - Server: - - Werkzeug/1.0.1 Python/3.9.6 - status: - code: 308 - message: PERMANENT REDIRECT -- 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}", "ruleset_name": "eicar.yara"}' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Authorization: - - '11111111111111111111111111111111' - Connection: - - keep-alive - Content-Length: - - '1143' - Content-Type: - - application/json - User-Agent: - - polyswarm-api/3.0.0 (x86_64-Linux-CPython-3.6.5) - method: POST - uri: http://artifact-index-e2e:9696/v3/hunt/historical/ - response: - body: - string: '{"result":{"created":"2022-05-26T19:08:30.323397","id":"96916002705221564","progress":null,"results_csv_uri":null,"ruleset_name":"eicar.yara","status":"PENDING","summary":null,"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 + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-25T18:27:00.017968+00:00","failed_max_retries":0,"failed_other":0,"id":"58957063950682201","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":"eicar.yara","source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + eicar_av_test : eicar match {\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}\n\nrule eicar_substring_test : eicar substring {\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"} ' headers: - Content-Length: - - '1303' - Content-Type: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '1582' + content-type: - application/json - Date: - - Thu, 26 May 2022 19:08:30 GMT - Server: - - Werkzeug/1.0.1 Python/3.9.6 - X-Billing-ID: - - '1' + date: + - Tue, 25 Aug 2026 18:27:00 GMT + server: + - gunicorn + x-billing-id: + - '111' status: code: 200 message: OK diff --git a/tests/vcr/test_historical_hunt_create_text.click b/tests/vcr/test_historical_hunt_create_text.click index 362500b2..cebc53bd 100644 --- a/tests/vcr/test_historical_hunt_create_text.click +++ b/tests/vcr/test_historical_hunt_create_text.click @@ -1,15 +1,15 @@ -result: "Hunt Id: 75914219779430298\nStatus: PENDING\nCreated at: 2022-05-26 19:08:30.527759\n\ - Ruleset Name: eicar.yara\nRuleset Contents:\nrule 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\\\ +result: "Hunt Id: 67346782704448208\nStatus: PENDING\nCreated at: 2026-08-25 18:27:00.531607+00:00\n\ + Ruleset Name: eicar.yara\nRuleset Contents:\nrule eicar_av_test : eicar match {\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!\"\ + \ : eicar substring {\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}\n\n" diff --git a/tests/vcr/test_historical_hunt_create_text.vcr b/tests/vcr/test_historical_hunt_create_text.vcr index b93f9613..b5253c5b 100644 --- a/tests/vcr/test_historical_hunt_create_text.vcr +++ b/tests/vcr/test_historical_hunt_create_text.vcr @@ -1,117 +1,72 @@ 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 + body: '{"yara":"rule eicar_av_test : eicar match {\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}", "ruleset_name": "eicar.yara"}' + of them\n}\n\nrule eicar_substring_test : eicar substring {\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}","ruleset_name":"eicar.yara","community":"gamma"}' headers: - Accept: + accept: - '*/*' - Accept-Encoding: + accept-encoding: - gzip, deflate - Authorization: + authorization: - '11111111111111111111111111111111' - Connection: + connection: - keep-alive - Content-Length: - - '1143' - Content-Type: + content-length: + - '1192' + content-type: - application/json - User-Agent: - - polyswarm-api/3.0.0 (x86_64-Linux-CPython-3.6.5) + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: POST uri: http://artifact-index-e2e:9696/v3/hunt/historical response: body: - string: ' - - Redirecting... - -

Redirecting...

- -

You should be redirected automatically to target URL: http://artifact-index-e2e:9696/v3/hunt/historical/. If - not click the link.' - headers: - Content-Length: - - '307' - Content-Type: - - text/html; charset=utf-8 - Date: - - Thu, 26 May 2022 19:08:30 GMT - Location: - - http://artifact-index-e2e:9696/v3/hunt/historical/ - Server: - - Werkzeug/1.0.1 Python/3.9.6 - status: - code: 308 - message: PERMANENT REDIRECT -- 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}", "ruleset_name": "eicar.yara"}' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Authorization: - - '11111111111111111111111111111111' - Connection: - - keep-alive - Content-Length: - - '1143' - Content-Type: - - application/json - User-Agent: - - polyswarm-api/3.0.0 (x86_64-Linux-CPython-3.6.5) - method: POST - uri: http://artifact-index-e2e:9696/v3/hunt/historical/ - response: - body: - string: '{"result":{"created":"2022-05-26T19:08:30.527759","id":"75914219779430298","progress":null,"results_csv_uri":null,"ruleset_name":"eicar.yara","status":"PENDING","summary":null,"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 + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-25T18:27:00.531607+00:00","failed_max_retries":0,"failed_other":0,"id":"67346782704448208","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":"eicar.yara","source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":"rule + eicar_av_test : eicar match {\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}\n\nrule eicar_substring_test : eicar substring {\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"} ' headers: - Content-Length: - - '1303' - Content-Type: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '1582' + content-type: - application/json - Date: - - Thu, 26 May 2022 19:08:30 GMT - Server: - - Werkzeug/1.0.1 Python/3.9.6 - X-Billing-ID: - - '1' + date: + - Tue, 25 Aug 2026 18:27:00 GMT + server: + - gunicorn + x-billing-id: + - '111' status: code: 200 message: OK diff --git a/tests/vcr/test_historical_hunt_delete_json.click b/tests/vcr/test_historical_hunt_delete_json.click index 48004f53..4c7f80e8 100644 --- a/tests/vcr/test_historical_hunt_delete_json.click +++ b/tests/vcr/test_historical_hunt_delete_json.click @@ -1,15 +1,18 @@ -result: '{"created": "2022-05-26T19:08:30.527759", "id": "75914219779430298", "progress": - null, "results_csv_uri": null, "ruleset_name": "eicar.yara", "status": "DELETING", - "summary": null, "yara": "rule eicar_av_test {\n /*\n Per standard, match - only if entire file is EICAR string plus optional trailing whitespace.\n The +result: '{"account_number": "111", "archives_in_flight": 0, "archives_scanned": 0, + "archives_total": 0, "communities": ["gamma"], "created": "2026-08-25T18:25:52.397034+00:00", + "failed_max_retries": 0, "failed_other": 0, "id": "32808041501095355", "progress": + null, "results_csv_uri": null, "rule_id": null, "rule_modified": null, "ruleset_name": + null, "source_rule_changed": null, "status": "DELETING", "summary": null, "user_account_number": + "111", "yara": "rule eicar_av_test : eicar match {\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}"} + of them\n}\n\nrule eicar_substring_test : eicar substring {\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}"} ' diff --git a/tests/vcr/test_historical_hunt_delete_json.vcr b/tests/vcr/test_historical_hunt_delete_json.vcr index 89cae161..b94d26cc 100644 --- a/tests/vcr/test_historical_hunt_delete_json.vcr +++ b/tests/vcr/test_historical_hunt_delete_json.vcr @@ -1,91 +1,60 @@ interactions: - request: - body: null + body: '{"community":"gamma"}' headers: - Accept: + accept: - '*/*' - Accept-Encoding: + accept-encoding: - gzip, deflate - Authorization: + authorization: - '11111111111111111111111111111111' - Connection: + connection: - keep-alive - Content-Length: - - '0' - User-Agent: - - polyswarm-api/3.0.0 (x86_64-Linux-CPython-3.6.5) - method: DELETE - uri: http://artifact-index-e2e:9696/v3/hunt/historical?id=75914219779430298 - response: - body: - string: ' - - Redirecting... - -

Redirecting...

- -

You should be redirected automatically to target URL: http://artifact-index-e2e:9696/v3/hunt/historical/?id=75914219779430298. If - not click the link.' - headers: - Content-Length: - - '349' - Content-Type: - - text/html; charset=utf-8 - Date: - - Thu, 26 May 2022 19:15:28 GMT - Location: - - http://artifact-index-e2e:9696/v3/hunt/historical/?id=75914219779430298 - Server: - - Werkzeug/1.0.1 Python/3.9.6 - status: - code: 308 - message: PERMANENT REDIRECT -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Authorization: - - '11111111111111111111111111111111' - Connection: - - keep-alive - Content-Length: - - '0' - User-Agent: - - polyswarm-api/3.0.0 (x86_64-Linux-CPython-3.6.5) + content-length: + - '21' + content-type: + - application/json + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: DELETE - uri: http://artifact-index-e2e:9696/v3/hunt/historical/?id=75914219779430298 + uri: http://artifact-index-e2e:9696/v3/hunt/historical?id=32808041501095355 response: body: - string: '{"result":{"created":"2022-05-26T19:08:30.527759","id":"75914219779430298","progress":null,"results_csv_uri":null,"ruleset_name":"eicar.yara","status":"DELETING","summary":null,"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 + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-25T18:25:52.397034+00:00","failed_max_retries":0,"failed_other":0,"id":"32808041501095355","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"DELETING","summary":null,"user_account_number":"111","yara":"rule + eicar_av_test : eicar match {\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}\n\nrule eicar_substring_test : eicar substring {\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"} ' headers: - Content-Length: - - '1304' - Content-Type: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '1575' + content-type: - application/json - Date: - - Thu, 26 May 2022 19:15:28 GMT - Server: - - Werkzeug/1.0.1 Python/3.9.6 - X-Billing-ID: - - '1' + date: + - Tue, 25 Aug 2026 18:27:00 GMT + server: + - gunicorn + x-billing-id: + - '111' status: code: 200 message: OK diff --git a/tests/vcr/test_historical_hunt_delete_text.click b/tests/vcr/test_historical_hunt_delete_text.click index 6bcf5b27..d4913923 100644 --- a/tests/vcr/test_historical_hunt_delete_text.click +++ b/tests/vcr/test_historical_hunt_delete_text.click @@ -1,16 +1,16 @@ -result: "Successfully deleted Hunt:\nHunt Id: 96916002705221564\nStatus: DELETING\n\ - Created at: 2022-05-26 19:08:30.323397\nRuleset Name: eicar.yara\nRuleset Contents:\n\ - 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\ +result: "Successfully deleted Hunt:\nHunt Id: 3220090199138422\nStatus: DELETING\n\ + Created at: 2026-08-25 18:25:52.550248+00:00\nRuleset Contents:\nrule eicar_av_test\ + \ : eicar match {\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}\n\n" + \ all of them\n}\n\nrule eicar_substring_test : eicar substring {\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}\n\n" diff --git a/tests/vcr/test_historical_hunt_delete_text.vcr b/tests/vcr/test_historical_hunt_delete_text.vcr index 8dd487bb..9b8dddc8 100644 --- a/tests/vcr/test_historical_hunt_delete_text.vcr +++ b/tests/vcr/test_historical_hunt_delete_text.vcr @@ -1,91 +1,60 @@ interactions: - request: - body: null + body: '{"community":"gamma"}' headers: - Accept: + accept: - '*/*' - Accept-Encoding: + accept-encoding: - gzip, deflate - Authorization: + authorization: - '11111111111111111111111111111111' - Connection: + connection: - keep-alive - Content-Length: - - '0' - User-Agent: - - polyswarm-api/3.0.0 (x86_64-Linux-CPython-3.6.5) - method: DELETE - uri: http://artifact-index-e2e:9696/v3/hunt/historical?id=96916002705221564 - response: - body: - string: ' - - Redirecting... - -

Redirecting...

- -

You should be redirected automatically to target URL: http://artifact-index-e2e:9696/v3/hunt/historical/?id=96916002705221564. If - not click the link.' - headers: - Content-Length: - - '349' - Content-Type: - - text/html; charset=utf-8 - Date: - - Thu, 26 May 2022 19:15:28 GMT - Location: - - http://artifact-index-e2e:9696/v3/hunt/historical/?id=96916002705221564 - Server: - - Werkzeug/1.0.1 Python/3.9.6 - status: - code: 308 - message: PERMANENT REDIRECT -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Authorization: - - '11111111111111111111111111111111' - Connection: - - keep-alive - Content-Length: - - '0' - User-Agent: - - polyswarm-api/3.0.0 (x86_64-Linux-CPython-3.6.5) + content-length: + - '21' + content-type: + - application/json + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: DELETE - uri: http://artifact-index-e2e:9696/v3/hunt/historical/?id=96916002705221564 + uri: http://artifact-index-e2e:9696/v3/hunt/historical?id=3220090199138422 response: body: - string: '{"result":{"created":"2022-05-26T19:08:30.323397","id":"96916002705221564","progress":null,"results_csv_uri":null,"ruleset_name":"eicar.yara","status":"DELETING","summary":null,"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 + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-08-25T18:25:52.550248+00:00","failed_max_retries":0,"failed_other":0,"id":"3220090199138422","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"DELETING","summary":null,"user_account_number":"111","yara":"rule + eicar_av_test : eicar match {\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}\n\nrule eicar_substring_test : eicar substring {\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"} ' headers: - Content-Length: - - '1304' - Content-Type: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '1574' + content-type: - application/json - Date: - - Thu, 26 May 2022 19:15:28 GMT - Server: - - Werkzeug/1.0.1 Python/3.9.6 - X-Billing-ID: - - '1' + date: + - Tue, 25 Aug 2026 18:27:00 GMT + server: + - gunicorn + x-billing-id: + - '111' status: code: 200 message: OK diff --git a/tests/vcr/test_historical_hunt_list_json.click b/tests/vcr/test_historical_hunt_list_json.click index bc69e1a2..0440b812 100644 --- a/tests/vcr/test_historical_hunt_list_json.click +++ b/tests/vcr/test_historical_hunt_list_json.click @@ -1,9 +1,25 @@ -result: '{"created": "2023-08-23T15:14:52.254659", "id": "30246442833374528", "progress": - null, "results_csv_uri": null, "ruleset_name": "eicar.yara", "status": "PENDING", - "summary": null, "yara": null} +result: '{"account_number": "111", "archives_in_flight": 0, "archives_scanned": 0, + "archives_total": 0, "created": "2026-08-25T18:27:00.531607+00:00", "id": "67346782704448208", + "progress": null, "results_csv_uri": null, "rule_id": null, "rule_modified": null, + "ruleset_name": "eicar.yara", "source_rule_changed": null, "status": "PENDING", + "summary": null, "user_account_number": "111", "yara": null} - {"created": "2023-08-23T15:12:38.073323", "id": "76083665328102613", "progress": - 100.0, "results_csv_uri": null, "ruleset_name": "eicar.yara", "status": "STOPPED", - "summary": null, "yara": null} + {"account_number": "111", "archives_in_flight": 0, "archives_scanned": 0, "archives_total": + 0, "created": "2026-08-25T18:27:00.017968+00:00", "id": "58957063950682201", "progress": + null, "results_csv_uri": null, "rule_id": null, "rule_modified": null, "ruleset_name": + "eicar.yara", "source_rule_changed": null, "status": "PENDING", "summary": null, + "user_account_number": "111", "yara": null} + + {"account_number": "111", "archives_in_flight": 0, "archives_scanned": 0, "archives_total": + 0, "created": "2026-08-25T18:25:52.550248+00:00", "id": "3220090199138422", "progress": + null, "results_csv_uri": null, "rule_id": null, "rule_modified": null, "ruleset_name": + null, "source_rule_changed": null, "status": "DELETING", "summary": null, "user_account_number": + "111", "yara": null} + + {"account_number": "111", "archives_in_flight": 0, "archives_scanned": 0, "archives_total": + 0, "created": "2026-08-25T18:25:52.397034+00:00", "id": "32808041501095355", "progress": + null, "results_csv_uri": null, "rule_id": null, "rule_modified": null, "ruleset_name": + null, "source_rule_changed": null, "status": "DELETING", "summary": null, "user_account_number": + "111", "yara": null} ' diff --git a/tests/vcr/test_historical_hunt_list_json.vcr b/tests/vcr/test_historical_hunt_list_json.vcr index 7a09c347..1861c740 100644 --- a/tests/vcr/test_historical_hunt_list_json.vcr +++ b/tests/vcr/test_historical_hunt_list_json.vcr @@ -1,37 +1,43 @@ interactions: - request: - body: null + body: '' headers: - Accept: + accept: - '*/*' - Accept-Encoding: + accept-encoding: - gzip, deflate - Authorization: + authorization: - '11111111111111111111111111111111' - Connection: + connection: - keep-alive - User-Agent: - - polyswarm-api/3.4.2 (x86_64-Linux-CPython-3.10.7) + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: GET uri: http://artifact-index-e2e:9696/v3/hunt/historical/list?community=gamma response: body: - string: '{"has_more":false,"limit":2,"result":[{"created":"2023-08-23T15:14:52.254659","id":"30246442833374528","progress":null,"results_csv_uri":null,"ruleset_name":"eicar.yara","status":"PENDING","summary":null,"yara":null},{"created":"2023-08-23T15:12:38.073323","id":"76083665328102613","progress":100.0,"results_csv_uri":null,"ruleset_name":"eicar.yara","status":"STOPPED","summary":null,"yara":null}],"status":"OK"} + string: '{"has_more":false,"limit":50,"result":[{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-08-25T18:27:00.531607+00:00","id":"67346782704448208","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":"eicar.yara","source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-08-25T18:27:00.017968+00:00","id":"58957063950682201","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":"eicar.yara","source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-08-25T18:25:52.550248+00:00","id":"3220090199138422","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"DELETING","summary":null,"user_account_number":"111","yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-08-25T18:25:52.397034+00:00","id":"32808041501095355","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"DELETING","summary":null,"user_account_number":"111","yara":null}],"status":"OK"} ' headers: - Connection: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: - keep-alive - Content-Length: - - '413' - Content-Type: + content-length: + - '1488' + content-type: - application/json - Date: - - Wed, 23 Aug 2023 15:15:11 GMT - Server: + date: + - Tue, 25 Aug 2026 18:27:00 GMT + server: - gunicorn - X-Billing-ID: - - '1' + x-billing-id: + - '111' status: code: 200 message: OK diff --git a/tests/vcr/test_historical_hunt_list_text.click b/tests/vcr/test_historical_hunt_list_text.click index c7cb71db..b768fd9f 100644 --- a/tests/vcr/test_historical_hunt_list_text.click +++ b/tests/vcr/test_historical_hunt_list_text.click @@ -1,21 +1,26 @@ -result: 'Hunt Id: 30246442833374528 +result: 'Hunt Id: 67346782704448208 Status: PENDING - Created at: 2023-08-23 15:14:52.254659 + Created at: 2026-08-25 18:27:00.531607+00:00 Ruleset Name: eicar.yara - Hunt Id: 76083665328102613 + Hunt Id: 58957063950682201 - Status: STOPPED - - Progress: 100.00% + Status: PENDING - Created at: 2023-08-23 15:12:38.073323 + Created at: 2026-08-25 18:27:00.017968+00:00 Ruleset Name: eicar.yara + Hunt Id: 3220090199138422 + + Status: DELETING + + Created at: 2026-08-25 18:25:52.550248+00:00 + + ' diff --git a/tests/vcr/test_historical_hunt_list_text.vcr b/tests/vcr/test_historical_hunt_list_text.vcr index 7a09c347..c041bbdd 100644 --- a/tests/vcr/test_historical_hunt_list_text.vcr +++ b/tests/vcr/test_historical_hunt_list_text.vcr @@ -1,37 +1,43 @@ interactions: - request: - body: null + body: '' headers: - Accept: + accept: - '*/*' - Accept-Encoding: + accept-encoding: - gzip, deflate - Authorization: + authorization: - '11111111111111111111111111111111' - Connection: + connection: - keep-alive - User-Agent: - - polyswarm-api/3.4.2 (x86_64-Linux-CPython-3.10.7) + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: GET uri: http://artifact-index-e2e:9696/v3/hunt/historical/list?community=gamma response: body: - string: '{"has_more":false,"limit":2,"result":[{"created":"2023-08-23T15:14:52.254659","id":"30246442833374528","progress":null,"results_csv_uri":null,"ruleset_name":"eicar.yara","status":"PENDING","summary":null,"yara":null},{"created":"2023-08-23T15:12:38.073323","id":"76083665328102613","progress":100.0,"results_csv_uri":null,"ruleset_name":"eicar.yara","status":"STOPPED","summary":null,"yara":null}],"status":"OK"} + string: '{"has_more":false,"limit":50,"result":[{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-08-25T18:27:00.531607+00:00","id":"67346782704448208","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":"eicar.yara","source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-08-25T18:27:00.017968+00:00","id":"58957063950682201","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":"eicar.yara","source_rule_changed":null,"status":"PENDING","summary":null,"user_account_number":"111","yara":null},{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"created":"2026-08-25T18:25:52.550248+00:00","id":"3220090199138422","progress":null,"results_csv_uri":null,"rule_id":null,"rule_modified":null,"ruleset_name":null,"source_rule_changed":null,"status":"DELETING","summary":null,"user_account_number":"111","yara":null}],"status":"OK"} ' headers: - Connection: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: - keep-alive - Content-Length: - - '413' - Content-Type: + content-length: + - '1133' + content-type: - application/json - Date: - - Wed, 23 Aug 2023 15:15:11 GMT - Server: + date: + - Tue, 25 Aug 2026 18:27:01 GMT + server: - gunicorn - X-Billing-ID: - - '1' + x-billing-id: + - '111' status: code: 200 message: OK diff --git a/tests/vcr/test_live_hunt_start_json.click b/tests/vcr/test_live_hunt_start_json.click index fa12dd1c..7d2b5160 100644 --- a/tests/vcr/test_live_hunt_start_json.click +++ b/tests/vcr/test_live_hunt_start_json.click @@ -1,16 +1,17 @@ -result: '{"created": "2022-05-26T18:25:35.109366", "deleted": false, "description": - null, "id": "17388152480558505", "livescan_created": "2022-05-26T19:37:18.353094", - "livescan_id": 51856636346307547, "modified": "2022-05-26T19:37:18.284777", "name": - "eicar", "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 +result: '{"created": "2026-08-25T18:25:52.247009+00:00", "deleted": false, "description": + null, "favorite": false, "favorited_at": null, "historical_hunt_count": 0, "id": + "44051669277897879", "livescan_created": "2026-08-25T18:26:47.595698+00:00", "livescan_id": + "60545835221721456", "modified": "2026-08-25T18:26:47.582495+00:00", "name": "recording-live", + "rule_count": 2, "yara": "rule eicar_av_test : eicar match {\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}"} + of them\n}\n\nrule eicar_substring_test : eicar substring {\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}"} ' diff --git a/tests/vcr/test_live_hunt_start_json.vcr b/tests/vcr/test_live_hunt_start_json.vcr index 04cfe55b..36bcf591 100644 --- a/tests/vcr/test_live_hunt_start_json.vcr +++ b/tests/vcr/test_live_hunt_start_json.vcr @@ -1,52 +1,60 @@ interactions: - request: - body: '{"rule_id": "17388152480558505"}' + body: '{"rule_id":"44051669277897879"}' headers: - Accept: + accept: - '*/*' - Accept-Encoding: + accept-encoding: - gzip, deflate - Authorization: + authorization: - '11111111111111111111111111111111' - Connection: + connection: - keep-alive - Content-Length: - - '32' - Content-Type: + content-length: + - '31' + content-type: - application/json - User-Agent: - - polyswarm-api/3.0.0 (x86_64-Linux-CPython-3.6.5) + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: POST uri: http://artifact-index-e2e:9696/v3/hunt/rule/live response: body: - string: '{"result":{"created":"2022-05-26T18:25:35.109366","deleted":false,"description":null,"id":"17388152480558505","livescan_created":"2022-05-26T19:37:18.353094","livescan_id":51856636346307547,"modified":"2022-05-26T19:37:18.284777","name":"eicar","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 + string: '{"result":{"created":"2026-08-25T18:25:52.247009+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"44051669277897879","livescan_created":"2026-08-25T18:26:47.595698+00:00","livescan_id":"60545835221721456","modified":"2026-08-25T18:26:47.582495+00:00","name":"recording-live","rule_count":2,"yara":"rule + eicar_av_test : eicar match {\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}\n\nrule eicar_substring_test : eicar substring {\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"} ' headers: - Content-Length: - - '1372' - Content-Type: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '1511' + content-type: - application/json - Date: - - Thu, 26 May 2022 19:37:18 GMT - Server: - - Werkzeug/1.0.1 Python/3.9.6 - X-Billing-ID: - - '1' + date: + - Tue, 25 Aug 2026 18:26:47 GMT + server: + - gunicorn + x-billing-id: + - '111' status: code: 200 message: OK diff --git a/tests/vcr/test_live_hunt_start_text.click b/tests/vcr/test_live_hunt_start_text.click index 5448ac4a..0b03df7d 100644 --- a/tests/vcr/test_live_hunt_start_text.click +++ b/tests/vcr/test_live_hunt_start_text.click @@ -1,16 +1,20 @@ -result: 'Ruleset Id: 17388152480558505 +result: 'Ruleset Id: 44051669277897879 - Live Hunt Id: 51856636346307547 + Live Hunt Id: 22291404616795831 - Live Hunt Created at: 2022-05-26T19:37:18.353094 + Live Hunt Created at: 2026-08-25T18:26:50.161371+00:00 - Name: eicar + Name: recording-live Description: None - Created at: 2022-05-26 18:25:35.109366 + Created at: 2026-08-25 18:25:52.247009+00:00 - Modified at: 2022-05-26 19:37:18.284777 + Modified at: 2026-08-25 18:26:49.909685+00:00 + + Rules in ruleset: 2 + + Historical hunts triggered: 0 ' diff --git a/tests/vcr/test_live_hunt_start_text.vcr b/tests/vcr/test_live_hunt_start_text.vcr index 04cfe55b..9031e41e 100644 --- a/tests/vcr/test_live_hunt_start_text.vcr +++ b/tests/vcr/test_live_hunt_start_text.vcr @@ -1,52 +1,60 @@ interactions: - request: - body: '{"rule_id": "17388152480558505"}' + body: '{"rule_id":"44051669277897879"}' headers: - Accept: + accept: - '*/*' - Accept-Encoding: + accept-encoding: - gzip, deflate - Authorization: + authorization: - '11111111111111111111111111111111' - Connection: + connection: - keep-alive - Content-Length: - - '32' - Content-Type: + content-length: + - '31' + content-type: - application/json - User-Agent: - - polyswarm-api/3.0.0 (x86_64-Linux-CPython-3.6.5) + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: POST uri: http://artifact-index-e2e:9696/v3/hunt/rule/live response: body: - string: '{"result":{"created":"2022-05-26T18:25:35.109366","deleted":false,"description":null,"id":"17388152480558505","livescan_created":"2022-05-26T19:37:18.353094","livescan_id":51856636346307547,"modified":"2022-05-26T19:37:18.284777","name":"eicar","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 + string: '{"result":{"created":"2026-08-25T18:25:52.247009+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"44051669277897879","livescan_created":"2026-08-25T18:26:50.161371+00:00","livescan_id":"22291404616795831","modified":"2026-08-25T18:26:49.909685+00:00","name":"recording-live","rule_count":2,"yara":"rule + eicar_av_test : eicar match {\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}\n\nrule eicar_substring_test : eicar substring {\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"} ' headers: - Content-Length: - - '1372' - Content-Type: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '1511' + content-type: - application/json - Date: - - Thu, 26 May 2022 19:37:18 GMT - Server: - - Werkzeug/1.0.1 Python/3.9.6 - X-Billing-ID: - - '1' + date: + - Tue, 25 Aug 2026 18:26:50 GMT + server: + - gunicorn + x-billing-id: + - '111' status: code: 200 message: OK diff --git a/tests/vcr/test_live_hunt_stop_json.click b/tests/vcr/test_live_hunt_stop_json.click index 50e506e0..f0df8f68 100644 --- a/tests/vcr/test_live_hunt_stop_json.click +++ b/tests/vcr/test_live_hunt_stop_json.click @@ -1,16 +1,17 @@ -result: '{"created": "2022-05-26T18:25:35.109366", "deleted": false, "description": - null, "id": "17388152480558505", "livescan_created": "2022-05-26T19:37:18.353094", - "livescan_id": null, "modified": "2022-05-26T19:49:57.892219", "name": "eicar", - "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 +result: '{"created": "2026-08-25T18:25:52.247009+00:00", "deleted": false, "description": + null, "favorite": false, "favorited_at": null, "historical_hunt_count": 0, "id": + "44051669277897879", "livescan_created": null, "livescan_id": null, "modified": + "2026-08-25T18:26:48.614597+00:00", "name": "recording-live", "rule_count": 2, "yara": + "rule eicar_av_test : eicar match {\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}"} + of them\n}\n\nrule eicar_substring_test : eicar substring {\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}"} ' diff --git a/tests/vcr/test_live_hunt_stop_json.vcr b/tests/vcr/test_live_hunt_stop_json.vcr index e7447ba4..690f2e66 100644 --- a/tests/vcr/test_live_hunt_stop_json.vcr +++ b/tests/vcr/test_live_hunt_stop_json.vcr @@ -1,52 +1,60 @@ interactions: - request: - body: '{"rule_id": "17388152480558505"}' + body: '{"rule_id":"44051669277897879"}' headers: - Accept: + accept: - '*/*' - Accept-Encoding: + accept-encoding: - gzip, deflate - Authorization: + authorization: - '11111111111111111111111111111111' - Connection: + connection: - keep-alive - Content-Length: - - '32' - Content-Type: + content-length: + - '31' + content-type: - application/json - User-Agent: - - polyswarm-api/3.0.0 (x86_64-Linux-CPython-3.6.5) + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: DELETE uri: http://artifact-index-e2e:9696/v3/hunt/rule/live response: body: - string: '{"result":{"created":"2022-05-26T18:25:35.109366","deleted":false,"description":null,"id":"17388152480558505","livescan_created":"2022-05-26T19:37:18.353094","livescan_id":null,"modified":"2022-05-26T19:49:57.892219","name":"eicar","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 + string: '{"result":{"created":"2026-08-25T18:25:52.247009+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"44051669277897879","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T18:26:48.614597+00:00","name":"recording-live","rule_count":2,"yara":"rule + eicar_av_test : eicar match {\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}\n\nrule eicar_substring_test : eicar substring {\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"} ' headers: - Content-Length: - - '1359' - Content-Type: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '1466' + content-type: - application/json - Date: - - Thu, 26 May 2022 19:49:57 GMT - Server: - - Werkzeug/1.0.1 Python/3.9.6 - X-Billing-ID: - - '1' + date: + - Tue, 25 Aug 2026 18:26:48 GMT + server: + - gunicorn + x-billing-id: + - '111' status: code: 200 message: OK diff --git a/tests/vcr/test_live_hunt_stop_text.click b/tests/vcr/test_live_hunt_stop_text.click index f3cb901e..71a5af3d 100644 --- a/tests/vcr/test_live_hunt_stop_text.click +++ b/tests/vcr/test_live_hunt_stop_text.click @@ -1,12 +1,16 @@ -result: 'Ruleset Id: 17388152480558505 +result: 'Ruleset Id: 44051669277897879 - Name: eicar + Name: recording-live Description: None - Created at: 2022-05-26 18:25:35.109366 + Created at: 2026-08-25 18:25:52.247009+00:00 - Modified at: 2022-05-26 19:49:57.892219 + Modified at: 2026-08-25 18:26:51.237276+00:00 + + Rules in ruleset: 2 + + Historical hunts triggered: 0 ' diff --git a/tests/vcr/test_live_hunt_stop_text.vcr b/tests/vcr/test_live_hunt_stop_text.vcr index e7447ba4..61d9678c 100644 --- a/tests/vcr/test_live_hunt_stop_text.vcr +++ b/tests/vcr/test_live_hunt_stop_text.vcr @@ -1,52 +1,60 @@ interactions: - request: - body: '{"rule_id": "17388152480558505"}' + body: '{"rule_id":"44051669277897879"}' headers: - Accept: + accept: - '*/*' - Accept-Encoding: + accept-encoding: - gzip, deflate - Authorization: + authorization: - '11111111111111111111111111111111' - Connection: + connection: - keep-alive - Content-Length: - - '32' - Content-Type: + content-length: + - '31' + content-type: - application/json - User-Agent: - - polyswarm-api/3.0.0 (x86_64-Linux-CPython-3.6.5) + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: DELETE uri: http://artifact-index-e2e:9696/v3/hunt/rule/live response: body: - string: '{"result":{"created":"2022-05-26T18:25:35.109366","deleted":false,"description":null,"id":"17388152480558505","livescan_created":"2022-05-26T19:37:18.353094","livescan_id":null,"modified":"2022-05-26T19:49:57.892219","name":"eicar","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 + string: '{"result":{"created":"2026-08-25T18:25:52.247009+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"44051669277897879","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T18:26:51.237276+00:00","name":"recording-live","rule_count":2,"yara":"rule + eicar_av_test : eicar match {\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}\n\nrule eicar_substring_test : eicar substring {\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"} ' headers: - Content-Length: - - '1359' - Content-Type: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '1466' + content-type: - application/json - Date: - - Thu, 26 May 2022 19:49:57 GMT - Server: - - Werkzeug/1.0.1 Python/3.9.6 - X-Billing-ID: - - '1' + date: + - Tue, 25 Aug 2026 18:26:51 GMT + server: + - gunicorn + x-billing-id: + - '111' status: code: 200 message: OK diff --git a/tests/vcr/test_ruleset_create_json.click b/tests/vcr/test_ruleset_create_json.click index 3beb63d0..1aa9d238 100644 --- a/tests/vcr/test_ruleset_create_json.click +++ b/tests/vcr/test_ruleset_create_json.click @@ -1,15 +1,17 @@ -result: '{"created": "2022-05-26T19:08:31.291890", "deleted": false, "description": - null, "id": "71213140536342873", "livescan_created": null, "livescan_id": null, - "modified": "2022-05-26T19:08:31.291890", "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 +result: '{"created": "2026-08-25T18:26:22.498052+00:00", "deleted": false, "description": + null, "favorite": false, "favorited_at": null, "historical_hunt_count": 0, "id": + "77454540525125655", "livescan_created": null, "livescan_id": null, "modified": + "2026-08-25T18:26:22.498052+00:00", "name": "test", "rule_count": 2, "yara": "rule + eicar_av_test : eicar match {\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}"} + of them\n}\n\nrule eicar_substring_test : eicar substring {\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}"} ' diff --git a/tests/vcr/test_ruleset_create_json.vcr b/tests/vcr/test_ruleset_create_json.vcr index d6d5c419..532f8b20 100644 --- a/tests/vcr/test_ruleset_create_json.vcr +++ b/tests/vcr/test_ruleset_create_json.vcr @@ -1,117 +1,72 @@ 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 + body: '{"yara":"rule eicar_av_test : eicar match {\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"}' + of them\n}\n\nrule eicar_substring_test : eicar substring {\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"}' headers: - Accept: + accept: - '*/*' - Accept-Encoding: + accept-encoding: - gzip, deflate - Authorization: + authorization: - '11111111111111111111111111111111' - Connection: + connection: - keep-alive - Content-Length: - - '1129' - Content-Type: + content-length: + - '1158' + content-type: - application/json - User-Agent: - - polyswarm-api/3.0.0 (x86_64-Linux-CPython-3.6.5) + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: POST uri: http://artifact-index-e2e:9696/v3/hunt/rule response: body: - string: ' - - Redirecting... - -

Redirecting...

- -

You should be redirected automatically to target URL: http://artifact-index-e2e:9696/v3/hunt/rule/. If - not click the link.' - headers: - Content-Length: - - '295' - Content-Type: - - text/html; charset=utf-8 - Date: - - Thu, 26 May 2022 19:08:31 GMT - Location: - - http://artifact-index-e2e:9696/v3/hunt/rule/ - Server: - - Werkzeug/1.0.1 Python/3.9.6 - status: - code: 308 - message: PERMANENT REDIRECT -- 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"}' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Authorization: - - '11111111111111111111111111111111' - Connection: - - keep-alive - Content-Length: - - '1129' - Content-Type: - - application/json - User-Agent: - - polyswarm-api/3.0.0 (x86_64-Linux-CPython-3.6.5) - method: POST - uri: http://artifact-index-e2e:9696/v3/hunt/rule/ - response: - body: - string: '{"result":{"created":"2022-05-26T19:08:31.291890","deleted":false,"description":null,"id":"71213140536342873","livescan_created":null,"livescan_id":null,"modified":"2022-05-26T19:08:31.291890","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 + string: '{"result":{"created":"2026-08-25T18:26:22.498052+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"77454540525125655","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T18:26:22.498052+00:00","name":"test","rule_count":2,"yara":"rule + eicar_av_test : eicar match {\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}\n\nrule eicar_substring_test : eicar substring {\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"} ' headers: - Content-Length: - - '1334' - Content-Type: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '1456' + content-type: - application/json - Date: - - Thu, 26 May 2022 19:08:31 GMT - Server: - - Werkzeug/1.0.1 Python/3.9.6 - X-Billing-ID: - - '1' + date: + - Tue, 25 Aug 2026 18:26:22 GMT + server: + - gunicorn + x-billing-id: + - '111' status: code: 200 message: OK diff --git a/tests/vcr/test_ruleset_delete_json.click b/tests/vcr/test_ruleset_delete_json.click index e602db94..f4247118 100644 --- a/tests/vcr/test_ruleset_delete_json.click +++ b/tests/vcr/test_ruleset_delete_json.click @@ -1,15 +1,16 @@ -result: '{"created": "2022-05-26T19:08:31.291890", "deleted": true, "description": - null, "id": "71213140536342873", "livescan_created": null, "livescan_id": null, - "modified": "2022-05-26T20:00:32.664781", "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 +result: '{"created": "2026-08-25T18:25:51.926590+00:00", "deleted": true, "description": + null, "favorite": false, "favorited_at": null, "historical_hunt_count": 0, "id": + "4202182245812695", "livescan_created": null, "livescan_id": null, "modified": "2026-08-25T18:26:38.540071+00:00", + "name": "test2", "rule_count": 2, "yara": "rule eicar_av_test : eicar match {\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}"} + of them\n}\n\nrule eicar_substring_test : eicar substring {\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}"} ' diff --git a/tests/vcr/test_ruleset_delete_json.vcr b/tests/vcr/test_ruleset_delete_json.vcr index 00ff5807..a9509dc6 100644 --- a/tests/vcr/test_ruleset_delete_json.vcr +++ b/tests/vcr/test_ruleset_delete_json.vcr @@ -1,91 +1,60 @@ interactions: - request: - body: null + body: '{"community":"gamma"}' headers: - Accept: + accept: - '*/*' - Accept-Encoding: + accept-encoding: - gzip, deflate - Authorization: + authorization: - '11111111111111111111111111111111' - Connection: + connection: - keep-alive - Content-Length: - - '0' - User-Agent: - - polyswarm-api/3.0.0 (x86_64-Linux-CPython-3.6.5) - method: DELETE - uri: http://artifact-index-e2e:9696/v3/hunt/rule?id=71213140536342873 - response: - body: - string: ' - - Redirecting... - -

Redirecting...

- -

You should be redirected automatically to target URL: http://artifact-index-e2e:9696/v3/hunt/rule/?id=71213140536342873. If - not click the link.' - headers: - Content-Length: - - '337' - Content-Type: - - text/html; charset=utf-8 - Date: - - Thu, 26 May 2022 20:00:32 GMT - Location: - - http://artifact-index-e2e:9696/v3/hunt/rule/?id=71213140536342873 - Server: - - Werkzeug/1.0.1 Python/3.9.6 - status: - code: 308 - message: PERMANENT REDIRECT -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Authorization: - - '11111111111111111111111111111111' - Connection: - - keep-alive - Content-Length: - - '0' - User-Agent: - - polyswarm-api/3.0.0 (x86_64-Linux-CPython-3.6.5) + content-length: + - '21' + content-type: + - application/json + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: DELETE - uri: http://artifact-index-e2e:9696/v3/hunt/rule/?id=71213140536342873 + uri: http://artifact-index-e2e:9696/v3/hunt/rule?id=4202182245812695 response: body: - string: '{"result":{"created":"2022-05-26T19:08:31.291890","deleted":true,"description":null,"id":"71213140536342873","livescan_created":null,"livescan_id":null,"modified":"2022-05-26T20:00:32.664781","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 + string: '{"result":{"created":"2026-08-25T18:25:51.926590+00:00","deleted":true,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"4202182245812695","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T18:26:38.540071+00:00","name":"test2","rule_count":2,"yara":"rule + eicar_av_test : eicar match {\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}\n\nrule eicar_substring_test : eicar substring {\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"} ' headers: - Content-Length: - - '1334' - Content-Type: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '1455' + content-type: - application/json - Date: - - Thu, 26 May 2022 20:00:32 GMT - Server: - - Werkzeug/1.0.1 Python/3.9.6 - X-Billing-ID: - - '1' + date: + - Tue, 25 Aug 2026 18:26:38 GMT + server: + - gunicorn + x-billing-id: + - '111' status: code: 200 message: OK diff --git a/tests/vcr/test_ruleset_favorite_json.click b/tests/vcr/test_ruleset_favorite_json.click new file mode 100644 index 00000000..1aed5331 --- /dev/null +++ b/tests/vcr/test_ruleset_favorite_json.click @@ -0,0 +1,4 @@ +result: '{"favorite": true, "favorited_at": "2026-08-26T15:01:51.102476+00:00", "favorites_limit": + 5, "favorites_used": 1, "id": "14883307518120680"} + + ' diff --git a/tests/vcr/test_ruleset_favorite_json.vcr b/tests/vcr/test_ruleset_favorite_json.vcr new file mode 100644 index 00000000..f8db4998 --- /dev/null +++ b/tests/vcr/test_ruleset_favorite_json.vcr @@ -0,0 +1,48 @@ +interactions: +- request: + body: '{"id":"14883307518120680","favorite":1}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '39' + content-type: + - application/json + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: PUT + uri: http://artifact-index-e2e:9696/v3/hunt/rule/favorite?community=gamma + response: + body: + string: '{"result":{"favorite":true,"favorited_at":"2026-08-26T15:01:51.102476+00:00","favorites_limit":5,"favorites_used":1,"id":"14883307518120680"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '157' + content-type: + - application/json + date: + - Wed, 26 Aug 2026 15:01:51 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +version: 1 diff --git a/tests/vcr/test_ruleset_favorite_text.click b/tests/vcr/test_ruleset_favorite_text.click new file mode 100644 index 00000000..ee1cd6f2 --- /dev/null +++ b/tests/vcr/test_ruleset_favorite_text.click @@ -0,0 +1,10 @@ +result: 'Ruleset Id: 96652060989160147 + + Favorite: yes + + Favorited at: 2026-08-26 15:01:51.592707+00:00 + + Favorites used: 2 of 5 + + + ' diff --git a/tests/vcr/test_ruleset_favorite_text.vcr b/tests/vcr/test_ruleset_favorite_text.vcr new file mode 100644 index 00000000..4d3b3a31 --- /dev/null +++ b/tests/vcr/test_ruleset_favorite_text.vcr @@ -0,0 +1,48 @@ +interactions: +- request: + body: '{"id":"96652060989160147","favorite":1}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '39' + content-type: + - application/json + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: PUT + uri: http://artifact-index-e2e:9696/v3/hunt/rule/favorite?community=gamma + response: + body: + string: '{"result":{"favorite":true,"favorited_at":"2026-08-26T15:01:51.592707+00:00","favorites_limit":5,"favorites_used":2,"id":"96652060989160147"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '157' + content-type: + - application/json + date: + - Wed, 26 Aug 2026 15:01:51 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +version: 1 diff --git a/tests/vcr/test_ruleset_list_json.click b/tests/vcr/test_ruleset_list_json.click index e40d4a72..f91d4ce3 100644 --- a/tests/vcr/test_ruleset_list_json.click +++ b/tests/vcr/test_ruleset_list_json.click @@ -1,9 +1,19 @@ -result: '{"created": "2023-08-23T15:16:04.148857", "deleted": false, "description": - null, "id": "27214252780064715", "livescan_created": null, "livescan_id": null, - "modified": "2023-08-23T15:16:04.148857", "name": "eicar2", "yara": null} +result: '{"created": "2026-08-25T18:26:22.498052+00:00", "deleted": false, "description": + null, "favorite": false, "favorited_at": null, "historical_hunt_count": 0, "id": + "77454540525125655", "livescan_created": null, "livescan_id": null, "modified": + "2026-08-25T18:26:22.498052+00:00", "name": "test", "new_results_count": null, "new_results_counted_at": + null, "rule_count": 2, "yara": null} - {"created": "2023-08-23T15:16:01.359801", "deleted": false, "description": null, - "id": "1023947781069864", "livescan_created": null, "livescan_id": null, "modified": - "2023-08-23T15:16:01.359801", "name": "eicar1", "yara": null} + {"created": "2026-08-25T18:25:52.247009+00:00", "deleted": false, "description": + null, "favorite": false, "favorited_at": null, "historical_hunt_count": 0, "id": + "44051669277897879", "livescan_created": null, "livescan_id": null, "modified": + "2026-08-25T18:25:52.247009+00:00", "name": "recording-live", "new_results_count": + null, "new_results_counted_at": null, "rule_count": 2, "yara": null} + + {"created": "2026-08-25T18:25:51.613644+00:00", "deleted": false, "description": + null, "favorite": false, "favorited_at": null, "historical_hunt_count": 0, "id": + "78562964231669682", "livescan_created": null, "livescan_id": null, "modified": + "2026-08-25T18:25:51.613644+00:00", "name": "recording-view", "new_results_count": + null, "new_results_counted_at": null, "rule_count": 2, "yara": null} ' diff --git a/tests/vcr/test_ruleset_list_json.vcr b/tests/vcr/test_ruleset_list_json.vcr index a0e8e73a..1a501412 100644 --- a/tests/vcr/test_ruleset_list_json.vcr +++ b/tests/vcr/test_ruleset_list_json.vcr @@ -1,37 +1,43 @@ interactions: - request: - body: null + body: '' headers: - Accept: + accept: - '*/*' - Accept-Encoding: + accept-encoding: - gzip, deflate - Authorization: + authorization: - '11111111111111111111111111111111' - Connection: + connection: - keep-alive - User-Agent: - - polyswarm-api/3.4.2 (x86_64-Linux-CPython-3.10.7) + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: GET uri: http://artifact-index-e2e:9696/v3/hunt/rule/list?community=gamma response: body: - string: '{"has_more":false,"limit":2,"result":[{"created":"2023-08-23T15:16:04.148857","deleted":false,"description":null,"id":"27214252780064715","livescan_created":null,"livescan_id":null,"modified":"2023-08-23T15:16:04.148857","name":"eicar2","yara":null},{"created":"2023-08-23T15:16:01.359801","deleted":false,"description":null,"id":"1023947781069864","livescan_created":null,"livescan_id":null,"modified":"2023-08-23T15:16:01.359801","name":"eicar1","yara":null}],"status":"OK"} + string: '{"has_more":false,"limit":50,"result":[{"created":"2026-08-25T18:26:22.498052+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"77454540525125655","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T18:26:22.498052+00:00","name":"test","new_results_count":null,"new_results_counted_at":null,"rule_count":2,"yara":null},{"created":"2026-08-25T18:25:52.247009+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"44051669277897879","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T18:25:52.247009+00:00","name":"recording-live","new_results_count":null,"new_results_counted_at":null,"rule_count":2,"yara":null},{"created":"2026-08-25T18:25:51.613644+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"78562964231669682","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T18:25:51.613644+00:00","name":"recording-view","new_results_count":null,"new_results_counted_at":null,"rule_count":2,"yara":null}],"status":"OK"} ' headers: - Connection: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: - keep-alive - Content-Length: - - '477' - Content-Type: + content-length: + - '1140' + content-type: - application/json - Date: - - Wed, 23 Aug 2023 15:16:24 GMT - Server: + date: + - Tue, 25 Aug 2026 18:26:38 GMT + server: - gunicorn - X-Billing-ID: - - '1' + x-billing-id: + - '111' status: code: 200 message: OK diff --git a/tests/vcr/test_ruleset_unfavorite_text.click b/tests/vcr/test_ruleset_unfavorite_text.click new file mode 100644 index 00000000..cfa47a75 --- /dev/null +++ b/tests/vcr/test_ruleset_unfavorite_text.click @@ -0,0 +1,8 @@ +result: 'Ruleset Id: 96652060989160147 + + Favorite: no + + Favorites used: 1 of 5 + + + ' diff --git a/tests/vcr/test_ruleset_unfavorite_text.vcr b/tests/vcr/test_ruleset_unfavorite_text.vcr new file mode 100644 index 00000000..2ef66d20 --- /dev/null +++ b/tests/vcr/test_ruleset_unfavorite_text.vcr @@ -0,0 +1,48 @@ +interactions: +- request: + body: '{"id":"96652060989160147","favorite":0}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '39' + content-type: + - application/json + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) + method: PUT + uri: http://artifact-index-e2e:9696/v3/hunt/rule/favorite?community=gamma + response: + body: + string: '{"result":{"favorite":false,"favorited_at":null,"favorites_limit":5,"favorites_used":1,"id":"96652060989160147"},"status":"OK"} + + ' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '128' + content-type: + - application/json + date: + - Wed, 26 Aug 2026 15:01:51 GMT + server: + - gunicorn + x-billing-id: + - '111' + status: + code: 200 + message: OK +version: 1 diff --git a/tests/vcr/test_ruleset_update_json.click b/tests/vcr/test_ruleset_update_json.click index 38a22f8c..4c8bce44 100644 --- a/tests/vcr/test_ruleset_update_json.click +++ b/tests/vcr/test_ruleset_update_json.click @@ -1,15 +1,16 @@ -result: '{"created": "2022-05-26T19:08:31.291890", "deleted": false, "description": - null, "id": "71213140536342873", "livescan_created": null, "livescan_id": null, - "modified": "2022-05-26T19:59:46.678724", "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 +result: '{"created": "2026-08-25T18:25:51.926590+00:00", "deleted": false, "description": + null, "favorite": false, "favorited_at": null, "historical_hunt_count": 0, "id": + "4202182245812695", "livescan_created": null, "livescan_id": null, "modified": "2026-08-25T18:26:35.087131+00:00", + "name": "test2", "rule_count": 2, "yara": "rule eicar_av_test : eicar match {\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}"} + of them\n}\n\nrule eicar_substring_test : eicar substring {\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}"} ' diff --git a/tests/vcr/test_ruleset_update_json.vcr b/tests/vcr/test_ruleset_update_json.vcr index b7d74069..4f1495a1 100644 --- a/tests/vcr/test_ruleset_update_json.vcr +++ b/tests/vcr/test_ruleset_update_json.vcr @@ -1,95 +1,60 @@ interactions: - request: - body: '{"name": "test2"}' + body: '{"name":"test2","community":"gamma"}' headers: - Accept: + accept: - '*/*' - Accept-Encoding: + accept-encoding: - gzip, deflate - Authorization: + authorization: - '11111111111111111111111111111111' - Connection: + connection: - keep-alive - Content-Length: - - '17' - Content-Type: + content-length: + - '36' + content-type: - application/json - User-Agent: - - polyswarm-api/3.0.0 (x86_64-Linux-CPython-3.6.5) + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: PUT - uri: http://artifact-index-e2e:9696/v3/hunt/rule?id=71213140536342873 + uri: http://artifact-index-e2e:9696/v3/hunt/rule?id=4202182245812695 response: body: - string: ' - - Redirecting... - -

Redirecting...

- -

You should be redirected automatically to target URL: http://artifact-index-e2e:9696/v3/hunt/rule/?id=71213140536342873. If - not click the link.' - headers: - Content-Length: - - '337' - Content-Type: - - text/html; charset=utf-8 - Date: - - Thu, 26 May 2022 19:59:46 GMT - Location: - - http://artifact-index-e2e:9696/v3/hunt/rule/?id=71213140536342873 - Server: - - Werkzeug/1.0.1 Python/3.9.6 - status: - code: 308 - message: PERMANENT REDIRECT -- request: - body: '{"name": "test2"}' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Authorization: - - '11111111111111111111111111111111' - Connection: - - keep-alive - Content-Length: - - '17' - Content-Type: - - application/json - User-Agent: - - polyswarm-api/3.0.0 (x86_64-Linux-CPython-3.6.5) - method: PUT - uri: http://artifact-index-e2e:9696/v3/hunt/rule/?id=71213140536342873 - response: - body: - string: '{"result":{"created":"2022-05-26T19:08:31.291890","deleted":false,"description":null,"id":"71213140536342873","livescan_created":null,"livescan_id":null,"modified":"2022-05-26T19:59:46.678724","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 + string: '{"result":{"created":"2026-08-25T18:25:51.926590+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"4202182245812695","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T18:26:35.087131+00:00","name":"test2","rule_count":2,"yara":"rule + eicar_av_test : eicar match {\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}\n\nrule eicar_substring_test : eicar substring {\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"} ' headers: - Content-Length: - - '1335' - Content-Type: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: + - keep-alive + content-length: + - '1456' + content-type: - application/json - Date: - - Thu, 26 May 2022 19:59:46 GMT - Server: - - Werkzeug/1.0.1 Python/3.9.6 - X-Billing-ID: - - '1' + date: + - Tue, 25 Aug 2026 18:26:38 GMT + server: + - gunicorn + x-billing-id: + - '111' status: code: 200 message: OK diff --git a/tests/vcr/test_ruleset_view_json.click b/tests/vcr/test_ruleset_view_json.click index 2eb9e292..11ceb0a7 100644 --- a/tests/vcr/test_ruleset_view_json.click +++ b/tests/vcr/test_ruleset_view_json.click @@ -1,16 +1,17 @@ -result: '{"community": "_public", "created": "2023-08-23T15:16:04.148857", "deleted": - false, "description": null, "id": "27214252780064715", "livescan_created": null, - "livescan_id": null, "modified": "2023-08-23T15:16:04.148857", "name": "eicar2", - "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 +result: '{"created": "2026-08-25T18:25:51.613644+00:00", "deleted": false, "description": + null, "favorite": false, "favorited_at": null, "historical_hunt_count": 0, "id": + "78562964231669682", "livescan_created": null, "livescan_id": null, "modified": + "2026-08-25T18:25:51.613644+00:00", "name": "recording-view", "rule_count": 2, "yara": + "rule eicar_av_test : eicar match {\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}"} + of them\n}\n\nrule eicar_substring_test : eicar substring {\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}"} ' diff --git a/tests/vcr/test_ruleset_view_json.vcr b/tests/vcr/test_ruleset_view_json.vcr index df916de1..f626df67 100644 --- a/tests/vcr/test_ruleset_view_json.vcr +++ b/tests/vcr/test_ruleset_view_json.vcr @@ -1,50 +1,56 @@ interactions: - request: - body: null + body: '' headers: - Accept: + accept: - '*/*' - Accept-Encoding: + accept-encoding: - gzip, deflate - Authorization: + authorization: - '11111111111111111111111111111111' - Connection: + connection: - keep-alive - User-Agent: - - polyswarm-api/3.4.2 (x86_64-Linux-CPython-3.10.7) + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.3.0 (x86_64-Darwin-CPython-3.11.3) method: GET - uri: http://artifact-index-e2e:9696/v3/hunt/rule?id=27214252780064715&community=gamma + uri: http://artifact-index-e2e:9696/v3/hunt/rule?id=78562964231669682&community=gamma response: body: - string: '{"result":{"community":"_public","created":"2023-08-23T15:16:04.148857","deleted":false,"description":null,"id":"27214252780064715","livescan_created":null,"livescan_id":null,"modified":"2023-08-23T15:16:04.148857","name":"eicar2","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 + string: '{"result":{"created":"2026-08-25T18:25:51.613644+00:00","deleted":false,"description":null,"favorite":false,"favorited_at":null,"historical_hunt_count":0,"id":"78562964231669682","livescan_created":null,"livescan_id":null,"modified":"2026-08-25T18:25:51.613644+00:00","name":"recording-view","rule_count":2,"yara":"rule + eicar_av_test : eicar match {\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}\n\nrule eicar_substring_test : eicar substring {\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"} ' headers: - Connection: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Authorization + connection: - keep-alive - Content-Length: - - '1358' - Content-Type: + content-length: + - '1466' + content-type: - application/json - Date: - - Wed, 23 Aug 2023 15:16:57 GMT - Server: + date: + - Tue, 25 Aug 2026 18:26:33 GMT + server: - gunicorn - X-Billing-ID: - - '1' + x-billing-id: + - '111' status: code: 200 message: OK