diff --git a/pyproject.toml b/pyproject.toml index 6c17d747..dc119743 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "polyswarm" -version = "4.6.0" +version = "4.7.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.7.0,<5.0.0", + "polyswarm_api>=4.8.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.6.0" +current_version = "4.7.0" commit = true tag = false sign_tags = true diff --git a/specs/02-commands.md b/specs/02-commands.md index f085ecda..df8af5af 100644 --- a/specs/02-commands.md +++ b/specs/02-commands.md @@ -26,7 +26,7 @@ The top-level command groups, what each is for, and the primary `polyswarm-api` | `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. `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` | +| `historical` (`historical.py`) | Historical hunts: CRUD + results. `start` takes `--window-days {30,60,90,180}`, how many days back the hunt reaches (SDK 4.8.0, guaranteed by the pin — see [05-sdk-contract.md](./05-sdk-contract.md) §Current floor). The closed choice mirrors the server's enum, so a bad value is a usage error (exit 2) before any request; the value is forwarded as an int and only when passed, so a script that omits it makes the SDK call it always made and the hunt reaches the platform maximum, which the help does not put a day count on because the server owns it | `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}` | diff --git a/specs/03-formatters.md b/specs/03-formatters.md index 997ff603..33bb9028 100644 --- a/specs/03-formatters.md +++ b/specs/03-formatters.md @@ -173,6 +173,14 @@ the attributes directly: - `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". +- `window_days` renders as `Time Range:` right after `Created at:`, on every + historical hunt (`output.hunt` is only ever called for those): `Last N days` + for an int, `Platform maximum` for `None`. Unlike the fields above, `None` is + an answer rather than an absence — a hunt created without a window reaches + the platform maximum — so it prints a line instead of omitting one, and the + label names no day count because the server owns that maximum. The web UI + uses the same two labels. `JSONOutput` needs no change: it dumps the + resource's `.json`, which already carries the raw key (an int or `null`). ## Matched strings on hunt results diff --git a/specs/05-sdk-contract.md b/specs/05-sdk-contract.md index 8bd21704..cf3fe3bf 100644 --- a/specs/05-sdk-contract.md +++ b/specs/05-sdk-contract.md @@ -108,7 +108,7 @@ When a CLI feature needs an SDK surface that doesn't exist yet: **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.7.0` +### Current floor — `polyswarm_api>=4.8.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: @@ -138,8 +138,12 @@ the refang inside the SDK's endpoint methods, which `--refang/--no-refang` relie `refang_iocs=` and every command fails). `search ioc … --with-artifacts` forwards `search_by_ioc(with_artifacts=True)`, a keyword 4.7.0 adds, and that is what moved the floor to 4.7.0 (the autospec assertions in `tests/search_test.py` are its signature -check). Both are paired changes: the SDK branch of the same name declares the floor. -Code and tests use them directly. +check). `historical start --window-days` forwards `historical_create(window_days=)`, a +keyword 4.8.0 adds together with the `HistoricalHunt.window_days` attribute the text +formatter reads, and that is what moved the floor to 4.8.0 (the autospec assertions in +`tests/historical_window_test.py` are its signature check, and on 4.7.0 every hunt render +would raise on the missing attribute). Each is a paired change: the SDK branch of the +same name declares the floor. Code and tests use them directly. **Raising the floor is the whole procedure** when this repo needs something new from the SDK: diff --git a/src/polyswarm/__init__.py b/src/polyswarm/__init__.py index 52fde385..372a5079 100644 --- a/src/polyswarm/__init__.py +++ b/src/polyswarm/__init__.py @@ -1 +1 @@ -__version__ = '4.6.0' +__version__ = '4.7.0' diff --git a/src/polyswarm/client/historical.py b/src/polyswarm/client/historical.py index 337c6760..6701f97d 100644 --- a/src/polyswarm/client/historical.py +++ b/src/polyswarm/client/historical.py @@ -26,9 +26,11 @@ def historical_view(ctx, hunt_id): @click.argument('rule_file', type=click.File('r'), required=False) @click.option('-r', '--rule-id', type=click.INT, help='If provided, create the historical hunt from the existing ruleset.') @click.option('-n', '--name', help='Explicitly set the ruleset name for this hunt.') +@click.option('--window-days', type=click.Choice(['30', '60', '90', '180']), + help='How many days back the hunt reaches. Defaults to max') @click.pass_context @utils.any_provided('rule_file', 'rule_id') -def historical_start(ctx, rule_file, rule_id, name): +def historical_start(ctx, rule_file, rule_id, name, window_days): api = ctx.obj['api'] output = ctx.obj['output'] params = {} @@ -39,6 +41,8 @@ def historical_start(ctx, rule_file, rule_id, name): rule = rule_id if name: params['ruleset_name'] = name + if window_days is not None: + params['window_days'] = int(window_days) result = api.historical_create(rule, **params) output.hunt(result) diff --git a/src/polyswarm/formatters/text.py b/src/polyswarm/formatters/text.py index 3dfcfd69..961a7c04 100644 --- a/src/polyswarm/formatters/text.py +++ b/src/polyswarm/formatters/text.py @@ -230,6 +230,11 @@ def hunt(self, result, write=True): if result.active is not None: output.append(self._white(f'Active: {result.active}')) output.append(self._white(f'Created at: {result.created}')) + # None: no window was requested, so the hunt reaches the platform maximum. + if result.window_days is None: + output.append(self._white('Time Range: Platform maximum')) + else: + output.append(self._white(f'Time Range: Last {result.window_days} days')) if result.summary: output.append(self._white(f'Total count: {result.summary["count"]}')) self._open_group() diff --git a/tests/cli_test.py b/tests/cli_test.py index 2acb3bc4..4a3cca01 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -207,6 +207,20 @@ def test_historical_hunt_create_text(self): '--output-format', 'text', 'historical', 'start', self.resource('eicar.yara')]) self._assert_text_result(result, self.click_vcr(result)) + @vcr.use_cassette() + def test_historical_hunt_create_window_json(self): + result = self._run_cli([ + '--output-format', 'json', 'historical', 'start', self.resource('eicar.yara'), + '--window-days', '30']) + self._assert_json_result(result, self.click_vcr(result)) + + @vcr.use_cassette() + def test_historical_hunt_create_window_text(self): + result = self._run_cli([ + '--output-format', 'text', 'historical', 'start', self.resource('eicar.yara'), + '--window-days', '30']) + self._assert_text_result(result, self.click_vcr(result)) + @vcr.use_cassette() def test_historical_hunt_delete_json(self): result = self._run_cli([ diff --git a/tests/historical_window_test.py b/tests/historical_window_test.py new file mode 100644 index 00000000..b9727c72 --- /dev/null +++ b/tests/historical_window_test.py @@ -0,0 +1,136 @@ +"""``historical start --window-days`` and the ``Time Range`` line of the hunt text block. + +The command is driven through autospec'd SDK-boundary mocks (specs/04-testing.md, Style 1), +so every call is also a signature check against the installed SDK: one without +``window_days`` fails at the mock. The line itself is a Style 3 formatter test on real SDK +resources built from dicts. +""" +import json +from pathlib import Path +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 resources + +_RULE_FILE = Path(__file__).resolve().parent / 'eicar.yara' +_GLOBALS = ['-a', '1' * 32, '-u', 'http://ai:9696/v3', '-c', 'gamma'] + + +def _hunt(**overrides): + content = dict(id='9', status='PENDING', progress=0.0, active=None, + created='2026-10-01T12:00:00+00:00', summary=None, + results_csv_uri=None, ruleset_name='n', yara=None) + content.update(overrides) + return resources.HistoricalHunt(content, api=None) + + +def _echo_window(_api, _rule, **kwargs): + """Stand-in for the server: the created hunt carries the window it was asked for.""" + return _hunt(window_days=kwargs.get('window_days')) + + +class HistoricalStartWindowTest(TestCase): + def _start(self, *args): + with mock.patch('polyswarm_api.api.PolyswarmAPI.historical_create', + autospec=True, return_value=_hunt()) as historical_create: + result = CliRunner().invoke( + client.polyswarm_cli, _GLOBALS + ['historical', 'start', *args], + catch_exceptions=False) + return result, historical_create + + def test_each_window_is_forwarded_as_an_int(self): + for days in (30, 60, 90, 180): + with self.subTest(days=days): + result, historical_create = self._start(str(_RULE_FILE), '--window-days', str(days)) + assert result.exit_code == 0, result.output + historical_create.assert_called_once_with( + mock.ANY, _RULE_FILE.read_text(), ruleset_name='eicar.yara', + window_days=days) + # 30 == '30' is False, but only the type says it is not a float or a bool. + assert type(historical_create.call_args.kwargs['window_days']) is int + + def test_no_flag_forwards_no_window_at_all(self): + # A script written before the flag keeps making the SDK call it always made. + result, historical_create = self._start(str(_RULE_FILE)) + assert result.exit_code == 0, result.output + historical_create.assert_called_once_with( + mock.ANY, _RULE_FILE.read_text(), ruleset_name='eicar.yara') + assert 'window_days' not in historical_create.call_args.kwargs + + def test_window_composes_with_a_stored_rule(self): + result, historical_create = self._start('--rule-id', '5', '--window-days', '90') + assert result.exit_code == 0, result.output + historical_create.assert_called_once_with(mock.ANY, 5, window_days=90) + + def test_a_window_outside_the_choices_is_a_usage_error(self): + result, historical_create = self._start(str(_RULE_FILE), '--window-days', '45') + assert result.exit_code == 2, result.output + # Names the refused value and what is allowed; click's wording varies by version. + for text in ('45', '30', '60', '90', '180'): + assert text in result.output, result.output + historical_create.assert_not_called() + + def test_help_documents_the_choices_and_the_default(self): + result = CliRunner().invoke( + client.polyswarm_cli, _GLOBALS + ['historical', 'start', '--help'], + catch_exceptions=False) + assert result.exit_code == 0, result.output + helptext = ' '.join(result.output.split()) + assert '--window-days [30|60|90|180]' in helptext, helptext + assert 'Defaults to max' in helptext, helptext + + +class HuntTimeRangeFormatterTest(TestCase): + def _lines(self, hunt, method='hunt'): + return getattr(text.TextOutput(color=False), method)(hunt, write=False) + + def test_a_window_renders_as_the_last_n_days(self): + assert 'Time Range: Last 30 days' in self._lines(_hunt(window_days=30)) + + def test_no_window_renders_as_the_platform_maximum(self): + # An explicit null and a key the server never sent mean the same thing. + for hunt in (_hunt(window_days=None), _hunt()): + lines = self._lines(hunt) + assert 'Time Range: Platform maximum' in lines + assert not [line for line in lines if line.startswith('Time Range: Last')] + + def test_the_line_follows_created_at(self): + lines = self._lines(_hunt(window_days=90)) + created = next(i for i, line in enumerate(lines) if line.startswith('Created at:')) + assert lines[created + 1] == 'Time Range: Last 90 days' + + def test_a_deleted_hunt_shows_it_too(self): + # hunt_deletion embeds the hunt block. + assert 'Time Range: Last 60 days' in self._lines(_hunt(window_days=60), 'hunt_deletion') + + +class HistoricalStartRendersTimeRangeTest(TestCase): + """The command reaches the formatter: what the user asked for is what the hunt shows.""" + + def _start(self, *args, fmt='text', **patch): + with mock.patch('polyswarm_api.api.PolyswarmAPI.historical_create', + autospec=True, **patch): + return CliRunner().invoke( + client.polyswarm_cli, + _GLOBALS + ['--output-format', fmt, 'historical', 'start', '-r', '5', *args], + catch_exceptions=False) + + def test_the_started_hunt_shows_the_window_it_was_started_with(self): + result = self._start('--window-days', '60', side_effect=_echo_window) + assert result.exit_code == 0, result.output + assert 'Time Range: Last 60 days' in result.output + + def test_a_start_without_the_flag_shows_the_platform_maximum(self): + result = self._start(side_effect=_echo_window) + assert result.exit_code == 0, result.output + assert 'Time Range: Platform maximum' in result.output + + def test_json_carries_the_raw_key_and_no_label(self): + # The JSON formatter dumps the resource's own JSON, so it needed no change. + result = self._start(fmt='json', return_value=_hunt(window_days=30)) + assert result.exit_code == 0, result.output + assert json.loads(result.output)['window_days'] == 30 + assert 'Time Range' not in result.output diff --git a/tests/vcr/test_historical_hunt_create_text.click b/tests/vcr/test_historical_hunt_create_text.click index cebc53bd..7f71b570 100644 --- a/tests/vcr/test_historical_hunt_create_text.click +++ b/tests/vcr/test_historical_hunt_create_text.click @@ -1,15 +1,16 @@ 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\ - \ : 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!\"\ + Time Range: Platform maximum\nRuleset 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 : 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_window_json.click b/tests/vcr/test_historical_hunt_create_window_json.click new file mode 100644 index 00000000..44006a99 --- /dev/null +++ b/tests/vcr/test_historical_hunt_create_window_json.click @@ -0,0 +1,18 @@ +result: '{"account_number": "111", "archives_in_flight": 0, "archives_scanned": 0, + "archives_total": 0, "communities": ["gamma"], "created": "2026-10-21T13:56:33.796254+00:00", + "failed_max_retries": 0, "failed_other": 0, "id": "31091646183416323", "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", "window_days": 30, "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 : 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_window_json.vcr b/tests/vcr/test_historical_hunt_create_window_json.vcr new file mode 100644 index 00000000..d02fd16d --- /dev/null +++ b/tests/vcr/test_historical_hunt_create_window_json.vcr @@ -0,0 +1,73 @@ +interactions: +- request: + 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 : 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","window_days":30,"community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '1209' + content-type: + - application/json + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) + method: POST + uri: http://artifact-index-e2e:9696/v3/hunt/historical + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-10-21T13:56:33.796254+00:00","failed_max_retries":0,"failed_other":0,"id":"31091646183416323","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","window_days":30,"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 : 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: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '1599' + Content-Type: + - application/json + Date: + - Wed, 21 Oct 2026 13:56:33 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +version: 1 diff --git a/tests/vcr/test_historical_hunt_create_window_text.click b/tests/vcr/test_historical_hunt_create_window_text.click new file mode 100644 index 00000000..ee003ed9 --- /dev/null +++ b/tests/vcr/test_historical_hunt_create_window_text.click @@ -0,0 +1,16 @@ +result: "Hunt Id: 64911267626296886\nStatus: PENDING\nCreated at: 2026-10-21 13:56:33.858814+00:00\n\ + Time Range: Last 30 days\nRuleset 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 : 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_window_text.vcr b/tests/vcr/test_historical_hunt_create_window_text.vcr new file mode 100644 index 00000000..49eb0f97 --- /dev/null +++ b/tests/vcr/test_historical_hunt_create_window_text.vcr @@ -0,0 +1,73 @@ +interactions: +- request: + 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 : 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","window_days":30,"community":"gamma"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - '11111111111111111111111111111111' + connection: + - keep-alive + content-length: + - '1209' + content-type: + - application/json + host: + - artifact-index-e2e:9696 + user-agent: + - polyswarm_api/4.8.0 (x86_64-Linux-CPython-3.12.15) + method: POST + uri: http://artifact-index-e2e:9696/v3/hunt/historical + response: + body: + string: '{"result":{"account_number":"111","archives_in_flight":0,"archives_scanned":0,"archives_total":0,"communities":["gamma"],"created":"2026-10-21T13:56:33.858814+00:00","failed_max_retries":0,"failed_other":0,"id":"64911267626296886","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","window_days":30,"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 : 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: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '1599' + Content-Type: + - application/json + Date: + - Wed, 21 Oct 2026 13:56:33 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +version: 1 diff --git a/tests/vcr/test_historical_hunt_delete_text.click b/tests/vcr/test_historical_hunt_delete_text.click index d4913923..1ff9edb1 100644 --- a/tests/vcr/test_historical_hunt_delete_text.click +++ b/tests/vcr/test_historical_hunt_delete_text.click @@ -1,8 +1,8 @@ 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\ + Created at: 2026-08-25 18:25:52.550248+00:00\nTime Range: Platform maximum\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\"\ diff --git a/tests/vcr/test_historical_hunt_list_text.click b/tests/vcr/test_historical_hunt_list_text.click index b768fd9f..0b2676a9 100644 --- a/tests/vcr/test_historical_hunt_list_text.click +++ b/tests/vcr/test_historical_hunt_list_text.click @@ -4,6 +4,8 @@ result: 'Hunt Id: 67346782704448208 Created at: 2026-08-25 18:27:00.531607+00:00 + Time Range: Platform maximum + Ruleset Name: eicar.yara @@ -13,6 +15,8 @@ result: 'Hunt Id: 67346782704448208 Created at: 2026-08-25 18:27:00.017968+00:00 + Time Range: Platform maximum + Ruleset Name: eicar.yara @@ -22,5 +26,7 @@ result: 'Hunt Id: 67346782704448208 Created at: 2026-08-25 18:25:52.550248+00:00 + Time Range: Platform maximum + '