diff --git a/pyproject.toml b/pyproject.toml index 8379f9a..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" }] @@ -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", @@ -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' 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/src/polyswarm/formatters/text.py b/src/polyswarm/formatters/text.py index 677dd80..9c682f1 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': @@ -799,22 +803,28 @@ def sample(self, result, write=True): self._open_group() task_items = [ ('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)')) - 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/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..b97dbdc --- /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\tMetadata: 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