From 9b6485130afef8ab209e240621fbb05052536f44 Mon Sep 17 00:00:00 2001 From: michael Date: Fri, 24 Apr 2026 16:30:22 -0700 Subject: [PATCH 1/7] adjust instance output if artifact is not being scanned --- src/polyswarm/formatters/text.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/polyswarm/formatters/text.py b/src/polyswarm/formatters/text.py index 677dd80..5632fec 100644 --- a/src/polyswarm/formatters/text.py +++ b/src/polyswarm/formatters/text.py @@ -87,6 +87,8 @@ def artifact_instance(self, instance, write=True, timeout=False): output.append(self._red(malicious)) else: output.append(self._white(malicious)) + elif instance.json.get('bounty_state') == 6 and not instance.failed: + output.append(self._white('Detections: This artifact was stored but not submitted for scanning.')) elif not instance.window_closed and not instance.failed: output.append(self._white('Detections: This scan has not finished running yet.')) else: @@ -116,6 +118,8 @@ def artifact_instance(self, instance, write=True, timeout=False): output.append(self._white('Status: This artifact has not been scanned. You can trigger a scan now.')) elif timeout: output.append(self._yellow('Status: Lookup timed-out, please retry')) + elif instance.json.get('bounty_state') == 6: + output.append(self._white('Status: Stored')) else: output.append(self._white('Status: Running')) if instance.type == 'URL': From fb67f8753c95b86052508bc572b974080bde8467 Mon Sep 17 00:00:00 2001 From: Samuel Barbosa Date: Wed, 6 May 2026 13:38:53 -0300 Subject: [PATCH 2/7] feat(sample): expose --llm-report-id flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit artifact-index /v3/sample now accepts an llm_report_id resource id; mirror the existing --*-id pattern and plumb it through to api.sample(). The global -c/--community flag already populates api.community, which polyswarm-api now sends in the POST body — no other CLI change needed. Adds VCR + click cassettes for text and JSON outputs. --- src/polyswarm/client/sample.py | 8 ++++-- tests/cli_test.py | 15 ++++++++++- tests/vcr/test_sample_json.click | 9 +++++++ tests/vcr/test_sample_json.vcr | 46 ++++++++++++++++++++++++++++++++ tests/vcr/test_sample_text.click | 5 ++++ tests/vcr/test_sample_text.vcr | 46 ++++++++++++++++++++++++++++++++ 6 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 tests/vcr/test_sample_json.click create mode 100644 tests/vcr/test_sample_json.vcr create mode 100644 tests/vcr/test_sample_text.click create mode 100644 tests/vcr/test_sample_text.vcr diff --git a/src/polyswarm/client/sample.py b/src/polyswarm/client/sample.py index b5626eb..de98858 100644 --- a/src/polyswarm/client/sample.py +++ b/src/polyswarm/client/sample.py @@ -16,11 +16,14 @@ help='Specific Triage sandbox task ID to retrieve.') @click.option('--artifact-metadata-id', type=int, default=None, help='Specific artifact metadata ID to retrieve.') +@click.option('--llm-report-id', type=int, default=None, + help='Specific LLM report task ID to retrieve.') @click.pass_context def sample(ctx, sha256, artifact_instance_id, sandbox_task_id_cape, - sandbox_task_id_triage, artifact_metadata_id): + sandbox_task_id_triage, artifact_metadata_id, llm_report_id): """ - Get aggregated sample information including artifact instance, sandbox tasks, and metadata. + Get aggregated sample information including artifact instance, sandbox tasks, metadata, + and LLM report. SHA256 is the SHA256 hash of the artifact to retrieve information for. """ @@ -32,5 +35,6 @@ def sample(ctx, sha256, artifact_instance_id, sandbox_task_id_cape, sandbox_task_id_cape=sandbox_task_id_cape, sandbox_task_id_triage=sandbox_task_id_triage, artifact_metadata_id=artifact_metadata_id, + llm_report_id=llm_report_id, ) output.sample(result) diff --git a/tests/cli_test.py b/tests/cli_test.py index fbac29f..7df6967 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -437,4 +437,17 @@ def test_sandboxtask_list(self): def test_sandboxtask_latest(self): result = self._run_cli([ '--output-format', 'json', 'sandbox', 'lookup', 'triage', 'a709f37b3a50608f2e9830f92ea25da04bfa4f34d2efecfd061de9f29af02427']) - self._assert_text_result(result, self.click_vcr(result)) \ No newline at end of file + self._assert_text_result(result, self.click_vcr(result)) + + +class SampleTest(BaseTestCase): + @vcr.use_cassette() + def test_sample_text(self): + result = self._run_cli(['sample', self.eicar_hash]) + self._assert_text_result(result, self.click_vcr(result)) + + @vcr.use_cassette() + def test_sample_json(self): + result = self._run_cli([ + '--output-format', 'json', 'sample', self.eicar_hash]) + self._assert_json_result(result, self.click_vcr(result)) \ No newline at end of file diff --git a/tests/vcr/test_sample_json.click b/tests/vcr/test_sample_json.click new file mode 100644 index 0000000..f383bab --- /dev/null +++ b/tests/vcr/test_sample_json.click @@ -0,0 +1,9 @@ +result: '{"artifact_instance": {}, "llm_report_task": {}, "metadata": {}, "sandbox": + {"cape": {}, "triage": {}}, "tasks": {"artifact_instance": {"rendered_id": null, + "requested_id": null, "requested_status": null}, "llm_report": {"rendered_id": null, + "requested_id": null, "requested_status": null}, "metadata": {"rendered_id": null, + "requested_id": null, "requested_status": null}, "sandbox_cape": {"rendered_id": + null, "requested_id": null, "requested_status": null}, "sandbox_triage": {"rendered_id": + null, "requested_id": null, "requested_status": null}}} + + ' diff --git a/tests/vcr/test_sample_json.vcr b/tests/vcr/test_sample_json.vcr new file mode 100644 index 0000000..b339f7f --- /dev/null +++ b/tests/vcr/test_sample_json.vcr @@ -0,0 +1,46 @@ +interactions: +- request: + body: '{"community": "gamma"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, zstd + Authorization: + - '11111111111111111111111111111111' + Connection: + - keep-alive + Content-Length: + - '22' + Content-Type: + - application/json + User-Agent: + - polyswarm_api/3.19.1 (x86_64-Linux-CPython-3.14.2) + method: POST + uri: http://artifact-index-e2e:9696/v3/sample/275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f + response: + body: + string: '{"result":{"artifact_instance":{},"llm_report_task":{},"metadata":{},"sandbox":{"cape":{},"triage":{}},"tasks":{"artifact_instance":{"rendered_id":null,"requested_id":null,"requested_status":null},"llm_report":{"rendered_id":null,"requested_id":null,"requested_status":null},"metadata":{"rendered_id":null,"requested_id":null,"requested_status":null},"sandbox_cape":{"rendered_id":null,"requested_id":null,"requested_status":null},"sandbox_triage":{"rendered_id":null,"requested_id":null,"requested_status":null}}},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '530' + Content-Type: + - application/json + Date: + - Wed, 06 May 2026 16:37:06 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +version: 1 diff --git a/tests/vcr/test_sample_text.click b/tests/vcr/test_sample_text.click new file mode 100644 index 0000000..9dc2287 --- /dev/null +++ b/tests/vcr/test_sample_text.click @@ -0,0 +1,5 @@ +result: "============================= Sample =============================\n--- Artifact\ + \ Instance: Not found ---\n--- Sandbox ---\n Cape: Not found\n Triage: Not found\n\ + --- Metadata: Not found ---\n--- Tasks ---\n\tArtifact Instance: not available\n\ + \tLLM Report: not available\n\tSandbox Cape: not available\n\tSandbox Triage: not\ + \ available\n\n" diff --git a/tests/vcr/test_sample_text.vcr b/tests/vcr/test_sample_text.vcr new file mode 100644 index 0000000..b339f7f --- /dev/null +++ b/tests/vcr/test_sample_text.vcr @@ -0,0 +1,46 @@ +interactions: +- request: + body: '{"community": "gamma"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, zstd + Authorization: + - '11111111111111111111111111111111' + Connection: + - keep-alive + Content-Length: + - '22' + Content-Type: + - application/json + User-Agent: + - polyswarm_api/3.19.1 (x86_64-Linux-CPython-3.14.2) + method: POST + uri: http://artifact-index-e2e:9696/v3/sample/275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f + response: + body: + string: '{"result":{"artifact_instance":{},"llm_report_task":{},"metadata":{},"sandbox":{"cape":{},"triage":{}},"tasks":{"artifact_instance":{"rendered_id":null,"requested_id":null,"requested_status":null},"llm_report":{"rendered_id":null,"requested_id":null,"requested_status":null},"metadata":{"rendered_id":null,"requested_id":null,"requested_status":null},"sandbox_cape":{"rendered_id":null,"requested_id":null,"requested_status":null},"sandbox_triage":{"rendered_id":null,"requested_id":null,"requested_status":null}}},"status":"OK"} + + ' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Authorization + Connection: + - keep-alive + Content-Length: + - '530' + Content-Type: + - application/json + Date: + - Wed, 06 May 2026 16:37:06 GMT + Server: + - gunicorn + X-Billing-ID: + - '111' + status: + code: 200 + message: OK +version: 1 From 03accf740f5012927fbed4fd92cf9e76145cfedc Mon Sep 17 00:00:00 2001 From: Samuel Barbosa Date: Wed, 6 May 2026 13:43:02 -0300 Subject: [PATCH 3/7] fix(sample): render ip_analyzer task in text formatter The artifact-index sample response includes an `ip_analyzer` entry inside `tasks` for URL artifacts. The text formatter only iterated artifact_instance / llm_report / sandbox_cape / sandbox_triage, silently dropping it. Add it alongside the others. The existing renderer logic handles the requested/rendered/status shape, so no further changes are needed. --- src/polyswarm/formatters/text.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/polyswarm/formatters/text.py b/src/polyswarm/formatters/text.py index 5632fec..a7a6778 100644 --- a/src/polyswarm/formatters/text.py +++ b/src/polyswarm/formatters/text.py @@ -803,6 +803,7 @@ def sample(self, result, write=True): self._open_group() task_items = [ ('Artifact Instance', 'artifact_instance'), + ('IP Analyzer', 'ip_analyzer'), ('LLM Report', 'llm_report'), ('Sandbox Cape', 'sandbox_cape'), ('Sandbox Triage', 'sandbox_triage'), From 8183ba3fee8497d1e59dd8cab8dacb93a14759ca Mon Sep 17 00:00:00 2001 From: Samuel Barbosa Date: Wed, 6 May 2026 13:47:57 -0300 Subject: [PATCH 4/7] fix(sample): show task status when no id is rendered MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A task can carry a meaningful requested_status (TIMEDOUT, NOT_TRIGGERED, PENDING, etc.) even when both requested_id and rendered_id are null — e.g. ip_analyzer that timed out before producing metadata. The renderer fell through to "not available", silently dropping the status. Add an explicit branch that prints just the status in that case. --- src/polyswarm/formatters/text.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/polyswarm/formatters/text.py b/src/polyswarm/formatters/text.py index a7a6778..4fe9bac 100644 --- a/src/polyswarm/formatters/text.py +++ b/src/polyswarm/formatters/text.py @@ -818,6 +818,8 @@ def sample(self, result, write=True): output.append(self._yellow(f'{label}: ID {requested_id} ({requested_status})')) elif rendered_id: output.append(self._green(f'{label}: ID {rendered_id} (ready)')) + elif requested_status: + output.append(self._yellow(f'{label}: {requested_status}')) else: output.append(self._white(f'{label}: not available')) self._close_group() From c20eb78633feff4a388653ff8e6939b112d44f4a Mon Sep 17 00:00:00 2001 From: Samuel Barbosa Date: Wed, 6 May 2026 13:53:34 -0300 Subject: [PATCH 5/7] fix(sample): always render canonical task status, add Metadata row The artifact-index sample endpoint exposes a canonical requested_status for every entry in `tasks` (artifact_instance, ip_analyzer, llm_report, metadata, sandbox_cape, sandbox_triage). The renderer previously chose between "ID X (status)", "ID X (ready)", "status alone", and "not available" based on which fields were populated, and could drop the status when only rendered_id was present. Reorder the branches so requested_status always wins when set, and add the missing Metadata row to task_items. The new ordering also handles rendered_id-only responses by surfacing it as "(ready)". --- src/polyswarm/formatters/text.py | 27 +++++++++++++++------------ tests/vcr/test_sample_text.click | 4 ++-- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/polyswarm/formatters/text.py b/src/polyswarm/formatters/text.py index 4fe9bac..9c682f1 100644 --- a/src/polyswarm/formatters/text.py +++ b/src/polyswarm/formatters/text.py @@ -805,23 +805,26 @@ def sample(self, result, write=True): ('Artifact Instance', 'artifact_instance'), ('IP Analyzer', 'ip_analyzer'), ('LLM Report', 'llm_report'), + ('Metadata', 'metadata'), ('Sandbox Cape', 'sandbox_cape'), ('Sandbox Triage', 'sandbox_triage'), ] for label, key in task_items: task = tasks.get(key) - if task is not None: - rendered_id = task.get('rendered_id') - requested_id = task.get('requested_id') - requested_status = task.get('requested_status') - if requested_id and requested_status: - output.append(self._yellow(f'{label}: ID {requested_id} ({requested_status})')) - elif rendered_id: - output.append(self._green(f'{label}: ID {rendered_id} (ready)')) - elif requested_status: - output.append(self._yellow(f'{label}: {requested_status}')) - else: - output.append(self._white(f'{label}: not available')) + if task is None: + continue + rendered_id = task.get('rendered_id') + requested_id = task.get('requested_id') + requested_status = task.get('requested_status') + id_to_show = requested_id or rendered_id + if requested_status and id_to_show: + output.append(self._yellow(f'{label}: ID {id_to_show} ({requested_status})')) + elif requested_status: + output.append(self._yellow(f'{label}: {requested_status}')) + elif id_to_show: + output.append(self._green(f'{label}: ID {id_to_show} (ready)')) + else: + output.append(self._white(f'{label}: not available')) self._close_group() return self._output(output, write) diff --git a/tests/vcr/test_sample_text.click b/tests/vcr/test_sample_text.click index 9dc2287..b97dbdc 100644 --- a/tests/vcr/test_sample_text.click +++ b/tests/vcr/test_sample_text.click @@ -1,5 +1,5 @@ result: "============================= Sample =============================\n--- Artifact\ \ Instance: Not found ---\n--- Sandbox ---\n Cape: Not found\n Triage: Not found\n\ --- Metadata: Not found ---\n--- Tasks ---\n\tArtifact Instance: not available\n\ - \tLLM Report: not available\n\tSandbox Cape: not available\n\tSandbox Triage: not\ - \ available\n\n" + \tLLM Report: not available\n\tMetadata: not available\n\tSandbox Cape: not available\n\ + \tSandbox Triage: not available\n\n" From 41613f39757e137f5394608d42e2f1864b670684 Mon Sep 17 00:00:00 2001 From: Samuel Barbosa Date: Wed, 6 May 2026 15:16:27 -0300 Subject: [PATCH 6/7] deps: require polyswarm-api>=3.20.0 The sample command depends on the new endpoint_fmt mechanism and the POST-with-community sample call introduced in polyswarm-api 3.20.0. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8379f9a..6f6437b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ ] dependencies = [ - "polyswarm_api>=3.18.0", + "polyswarm_api>=3.20.0", "click>=7.1", "colorama>=0.4.6", "click-log>=0.4.0", From 0616c02fc9f2465d2419cf114eab41fcfab3ce7e Mon Sep 17 00:00:00 2001 From: Samuel Barbosa Date: Wed, 6 May 2026 15:16:35 -0300 Subject: [PATCH 7/7] =?UTF-8?q?Bump=20version:=203.18.0=20=E2=86=92=203.19?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 4 ++-- src/polyswarm/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6f6437b..df082b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "polyswarm" -version = "3.18.0" +version = "3.19.0" description = "CLI for using the PolySwarm Customer APIs" readme = "README.md" authors = [{ name = "PolySwarm Developers", email = "info@polyswarm.io" }] @@ -51,7 +51,7 @@ include-package-data = true where = ["src"] [tool.bumpversion] -current_version = "3.18.0" +current_version = "3.19.0" commit = true tag = false sign_tags = true diff --git a/src/polyswarm/__init__.py b/src/polyswarm/__init__.py index c754d32..786602c 100644 --- a/src/polyswarm/__init__.py +++ b/src/polyswarm/__init__.py @@ -1 +1 @@ -__version__ = '3.18.0' +__version__ = '3.19.0'