From 9bb5348a29c170af08f5116b36a9745a4897e058 Mon Sep 17 00:00:00 2001 From: Steve Bassi Date: Tue, 5 May 2026 05:38:39 -0400 Subject: [PATCH 1/4] Add `polyswarm sandbox hash` to resandbox by sha256/sha1/md5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing `sandbox instance` subcommand requires an opaque numeric artifact-instance id, which forces a two-step `search hash` → `sandbox instance` workflow when you already have the hash in hand. This adds a `sandbox hash` subcommand that resolves the hash to its latest artifact instance under the hood and submits that instance. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/polyswarm/client/sandbox.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/polyswarm/client/sandbox.py b/src/polyswarm/client/sandbox.py index 79075ca..3c69c4e 100644 --- a/src/polyswarm/client/sandbox.py +++ b/src/polyswarm/client/sandbox.py @@ -32,6 +32,35 @@ def submit(ctx, provider_slug, artifact_id, vm_slug, internet_disabled): output.sandbox_task(tasks) +@sandbox.command('hash', short_help='Submit by hash (sha256/sha1/md5) to be sandboxed.') +@click.argument('provider_slug', type=click.STRING) +@click.argument('hash_value', nargs=-1, required=True) +@click.option('-r', '--hash-file', help='File of hashes, one per line.', type=click.File('r')) +@click.option('--hash-type', help='Hash type [default:autodetect, sha256|sha1|md5].', default=None) +@click.option('--vm_slug', 'vm_slug', type=click.STRING) +@click.option('--internet-disabled', 'internet_disabled', type=click.BOOL, is_flag=True, default=False) +@click.pass_context +def hash_(ctx, provider_slug, hash_value, hash_file, hash_type, vm_slug, internet_disabled): + """ + Submit a sample to the sandbox by hash. Resolves the hash to its latest + known artifact instance and sandboxes that instance. + """ + api = ctx.obj['api'] + output = ctx.obj['output'] + hashes = utils.parse_hashes(hash_value, hash_file=hash_file) + + instance_ids = [] + for h in hashes: + instance = next(iter(api.search_hashes([h], hash_type=hash_type)), None) + if instance is None: + raise click.ClickException(f'No artifact instances found for hash {h}') + instance_ids.append(instance.id) + + for task in api.sandbox_instances( + instance_ids, provider_slug=provider_slug, vm_slug=vm_slug, network_enabled=not internet_disabled): + output.sandbox_task(task) + + @sandbox.command('file', short_help='Submit a local file to be sandboxed.') @click.argument('provider', type=click.STRING) @click.argument('path', type=click.Path(exists=True), required=True) From 9ed0b357d51cfa118ba35784301e41672fe85b9f Mon Sep 17 00:00:00 2001 From: Samuel Barbosa Date: Thu, 14 May 2026 16:54:05 -0300 Subject: [PATCH 2/4] feat(DN-8130): show md5 and sha1 in hunt result output Text formatter: add SHA1 and MD5 lines under SHA256 for both live_result and historical_result. Lines are only rendered when the fields are present so older API responses (or null hashes on URL artifacts) don't render bare 'SHA1: None' rows. Hash formatters: extend SHA1Output and MD5Output with historical_result / live_result methods mirroring SHA256Output, so '--output-format sha1' / 'md5' work uniformly across hunt results and artifact instances. --- src/polyswarm/formatters/hashes.py | 24 ++++++++++++++++++++++++ src/polyswarm/formatters/text.py | 8 ++++++++ 2 files changed, 32 insertions(+) diff --git a/src/polyswarm/formatters/hashes.py b/src/polyswarm/formatters/hashes.py index 3cfbeef..b40a572 100644 --- a/src/polyswarm/formatters/hashes.py +++ b/src/polyswarm/formatters/hashes.py @@ -45,6 +45,18 @@ def _to_json(result): def artifact_instance(self, result, timeout=False): click.echo(result.sha1, file=self.out) + def historical_result(self, result): + if result.sha1: + click.echo(result.sha1, file=self.out) + else: + logger.warning('Could not render historical result as sha1, value is %s', result.sha1) + + def live_result(self, result): + if result.sha1: + click.echo(result.sha1, file=self.out) + else: + logger.warning('Could not render live result as sha1, value is %s', result.sha1) + def metadata(self, result): if result.sha1: click.echo(result.sha1, file=self.out) @@ -62,6 +74,18 @@ def _to_json(result): def artifact_instance(self, result, timeout=False): click.echo(result.md5, file=self.out) + def historical_result(self, result): + if result.md5: + click.echo(result.md5, file=self.out) + else: + logger.warning('Could not render historical result as md5, value is %s', result.md5) + + def live_result(self, result): + if result.md5: + click.echo(result.md5, file=self.out) + else: + logger.warning('Could not render live result as md5, value is %s', result.md5) + def metadata(self, result): if result.md5: click.echo(result.md5, file=self.out) diff --git a/src/polyswarm/formatters/text.py b/src/polyswarm/formatters/text.py index 9c682f1..bf07719 100644 --- a/src/polyswarm/formatters/text.py +++ b/src/polyswarm/formatters/text.py @@ -174,6 +174,10 @@ def historical_result(self, result, write=True): output.append(self._blue(f'Instance Id: {result.instance_id}')) output.append(self._white(f'Created at: {result.created}')) output.append(self._white(f'SHA256: {result.sha256}')) + if result.sha1: + output.append(self._white(f'SHA1: {result.sha1}')) + if result.md5: + output.append(self._white(f'MD5: {result.md5}')) output.append(self._white(f'Rule: {result.rule_name}')) if result.malware_family: output.append(self._red(f'Malware Family: {result.malware_family}')) @@ -201,6 +205,10 @@ def live_result(self, result, write=True): output.append(self._blue(f'Instance Id: {result.instance_id}')) output.append(self._white(f'Created at: {result.created}')) output.append(self._white(f'SHA256: {result.sha256}')) + if result.sha1: + output.append(self._white(f'SHA1: {result.sha1}')) + if result.md5: + output.append(self._white(f'MD5: {result.md5}')) output.append(self._white(f'Rule: {result.rule_name}')) if result.malware_family: output.append(self._red(f'Malware Family: {result.malware_family}')) From 008c1fd00c9b72edc868f862f65aa1b6eaae2619 Mon Sep 17 00:00:00 2001 From: Samuel Barbosa Date: Mon, 18 May 2026 17:41:36 -0300 Subject: [PATCH 3/4] =?UTF-8?q?Bump=20version:=203.19.0=20=E2=86=92=203.20?= =?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 df082b9..00799ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "polyswarm" -version = "3.19.0" +version = "3.20.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.19.0" +current_version = "3.20.0" commit = true tag = false sign_tags = true diff --git a/src/polyswarm/__init__.py b/src/polyswarm/__init__.py index 786602c..c170b8d 100644 --- a/src/polyswarm/__init__.py +++ b/src/polyswarm/__init__.py @@ -1 +1 @@ -__version__ = '3.19.0' +__version__ = '3.20.0' From 483fec1d7c90a73564e74ff9fa448e20bb6e71f6 Mon Sep 17 00:00:00 2001 From: Samuel Barbosa Date: Mon, 18 May 2026 17:41:54 -0300 Subject: [PATCH 4/4] deps: require polyswarm-api>=3.21.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 00799ce..dcfaa4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ ] dependencies = [ - "polyswarm_api>=3.20.0", + "polyswarm_api>=3.21.0", "click>=7.1", "colorama>=0.4.6", "click-log>=0.4.0",