From 01036ad9f38cc71737a68d9e64fac0323dfb1b57 Mon Sep 17 00:00:00 2001 From: Michael Bradford Date: Thu, 15 Jan 2026 10:34:49 -0800 Subject: [PATCH 1/7] webhooks --- src/polyswarm/client/polyswarm.py | 3 +- src/polyswarm/client/webhook.py | 123 ++++++++++++++++++++++++++++++ src/polyswarm/formatters/json.py | 2 + src/polyswarm/formatters/text.py | 15 ++++ 4 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 src/polyswarm/client/webhook.py diff --git a/src/polyswarm/client/polyswarm.py b/src/polyswarm/client/polyswarm.py index f1508e0..36eb58f 100644 --- a/src/polyswarm/client/polyswarm.py +++ b/src/polyswarm/client/polyswarm.py @@ -30,6 +30,7 @@ from polyswarm.client.bundle import bundle from polyswarm.client.report_template import report_template from polyswarm.client.account import account +from polyswarm.client.webhook import webhook logger = logging.getLogger(__name__) @@ -156,7 +157,7 @@ def polyswarm_cli(ctx, api_key, api_uri, output_file, output_format, color, verb cat, stream, rescan, rescan_id, rules, link, tag, family, metadata, engine, known, sandbox, sandbox_list, - activity, report, report_template, account, bundle, + activity, report, report_template, account, bundle, webhook, ] for command in commands: diff --git a/src/polyswarm/client/webhook.py b/src/polyswarm/client/webhook.py new file mode 100644 index 0000000..119ca98 --- /dev/null +++ b/src/polyswarm/client/webhook.py @@ -0,0 +1,123 @@ +import logging + +import click + +from polyswarm.client import utils + +logger = logging.getLogger(__name__) + + +@click.group(short_help='Interact with Webhooks in Polyswarm.') +def webhook(): + pass + + +@webhook.command('create', short_help='Create a new webhook.') +@click.argument('webhook-uri', type=click.STRING, required=True) +@click.argument('secret', type=click.STRING, required=True) +@click.option('--status', type=click.Choice(['enabled', 'disabled']), default='enabled', + help='Webhook status (default: enabled).') +@click.option('--events', type=click.STRING, help='JSON string specifying which events to subscribe to.') +@click.pass_context +def create(ctx, webhook_uri, secret, status, events): + """ + Create a new webhook. + + WEBHOOK_URI: The URI where webhook events should be sent + SECRET: The secret key used for HMAC signature verification + """ + api = ctx.obj['api'] + output = ctx.obj['output'] + + import json + events_dict = None + if events: + try: + events_dict = json.loads(events) + except json.JSONDecodeError as e: + raise click.BadParameter(f'Invalid JSON for events: {e}') + + result = api.webhook_create(webhook_uri=webhook_uri, secret=secret, status=status, events=events_dict) + output.webhook(result) + + +@webhook.command('get', short_help='Get a webhook by ID.') +@click.argument('webhook-id', callback=utils.validate_id) +@click.pass_context +def get(ctx, webhook_id): + """ + Get a webhook by ID. + """ + api = ctx.obj['api'] + output = ctx.obj['output'] + output.webhook(api.webhook_get(webhook_id)) + + +@webhook.command('update', short_help='Update an existing webhook.') +@click.argument('webhook-id', callback=utils.validate_id) +@click.option('--webhook-uri', type=click.STRING, help='The new webhook URI.') +@click.option('--secret', type=click.STRING, help='The new secret for HMAC signing.') +@click.option('--status', type=click.Choice(['enabled', 'disabled']), help='The new status.') +@click.option('--events', type=click.STRING, help='JSON string specifying which events to subscribe to.') +@click.pass_context +@utils.any_provided('webhook_uri', 'secret', 'status', 'events') +def update(ctx, webhook_id, webhook_uri, secret, status, events): + """ + Update an existing webhook. + """ + api = ctx.obj['api'] + output = ctx.obj['output'] + + import json + events_dict = None + if events: + try: + events_dict = json.loads(events) + except json.JSONDecodeError as e: + raise click.BadParameter(f'Invalid JSON for events: {e}') + + result = api.webhook_update( + webhook_id=webhook_id, + webhook_uri=webhook_uri, + secret=secret, + status=status, + events=events_dict + ) + output.webhook(result) + + +@webhook.command('delete', short_help='Delete a webhook.') +@click.argument('webhook-id', callback=utils.validate_id) +@click.pass_context +def delete(ctx, webhook_id): + """ + Delete a webhook. + """ + api = ctx.obj['api'] + output = ctx.obj['output'] + api.webhook_delete(webhook_id) + click.echo(f'Webhook {webhook_id} deleted successfully') + + +@webhook.command('list', short_help='List all webhooks.') +@click.pass_context +def list_webhooks(ctx): + """ + List all webhooks for the current account. + """ + api = ctx.obj['api'] + output = ctx.obj['output'] + for webhook_obj in api.webhook_list(): + output.webhook(webhook_obj) + + +@webhook.command('test', short_help='Test a webhook by sending a test payload.') +@click.argument('webhook-id', callback=utils.validate_id) +@click.pass_context +def test(ctx, webhook_id): + """ + Test a webhook by sending a test payload. + """ + api = ctx.obj['api'] + api.webhook_test(webhook_id) + click.echo(f'Test payload sent to webhook {webhook_id}') diff --git a/src/polyswarm/formatters/json.py b/src/polyswarm/formatters/json.py index 6ea2822..2473dcc 100644 --- a/src/polyswarm/formatters/json.py +++ b/src/polyswarm/formatters/json.py @@ -166,6 +166,8 @@ def account_features(self, result, write=True): def llm_prompt_config(self, result, write=True): click.echo(self._to_json(result.json), file=self.out) + def webhook(self, result): + click.echo(self._to_json(result.json), file=self.out) class PrettyJSONOutput(JSONOutput): name = 'pretty-json' diff --git a/src/polyswarm/formatters/text.py b/src/polyswarm/formatters/text.py index e5c2acd..9053d41 100644 --- a/src/polyswarm/formatters/text.py +++ b/src/polyswarm/formatters/text.py @@ -676,6 +676,21 @@ def llm_prompt_config(self, config, write=True): output.append(self._white(f'Scan-Only Prompt: {config.scan_only_prompt}')) return self._output(output, write) + def webhook(self, webhook, write=True): + output = [] + output.append(self._white('============================= Webhook =============================')) + output.append(self._blue(f'ID: {webhook.id}')) + output.append(self._white(f'Webhook URI: {webhook.webhook_uri}')) + output.append(self._white(f'Account Number: {webhook.account_number}')) + if webhook.team_account_number: + output.append(self._white(f'Team Account Number: {webhook.team_account_number}')) + output.append(self._white(f'Secret SHA256: {webhook.secret_sha256}')) + status_writer = self._green if webhook.status == 'enabled' else self._yellow + output.append(status_writer(f'Status: {webhook.status}')) + if webhook.events: + output.append(self._white(f'Events: {json.dumps(webhook.events, indent=2)}')) + return self._output(output, write) + @is_grouped def _white(self, text): return click.style(text, fg='white') From 26ed056b138fd15adf8bf07412401216df23a170 Mon Sep 17 00:00:00 2001 From: Michael Bradford Date: Thu, 15 Jan 2026 15:51:25 -0800 Subject: [PATCH 2/7] make subcommand of sandbox --- src/polyswarm/client/polyswarm.py | 3 +-- src/polyswarm/client/sandbox.py | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/polyswarm/client/polyswarm.py b/src/polyswarm/client/polyswarm.py index 36eb58f..f1508e0 100644 --- a/src/polyswarm/client/polyswarm.py +++ b/src/polyswarm/client/polyswarm.py @@ -30,7 +30,6 @@ from polyswarm.client.bundle import bundle from polyswarm.client.report_template import report_template from polyswarm.client.account import account -from polyswarm.client.webhook import webhook logger = logging.getLogger(__name__) @@ -157,7 +156,7 @@ def polyswarm_cli(ctx, api_key, api_uri, output_file, output_format, color, verb cat, stream, rescan, rescan_id, rules, link, tag, family, metadata, engine, known, sandbox, sandbox_list, - activity, report, report_template, account, bundle, webhook, + activity, report, report_template, account, bundle, ] for command in commands: diff --git a/src/polyswarm/client/sandbox.py b/src/polyswarm/client/sandbox.py index 79075ca..dab3b80 100644 --- a/src/polyswarm/client/sandbox.py +++ b/src/polyswarm/client/sandbox.py @@ -4,6 +4,7 @@ from polyswarm.client import utils +from polyswarm.client.webhook import webhook from polyswarm.utils import is_url logger = logging.getLogger(__name__) @@ -180,3 +181,7 @@ def my_task_list(ctx, sandbox_, start_date, end_date, sha256, user_account_id): sha256=sha256, user_account_id=user_account_id): output.sandbox_task(task) + + +# Add webhook as a subcommand of sandbox +sandbox.add_command(webhook) From fcb6d1854f2644e50699b0fc8582a5a2dc91ed02 Mon Sep 17 00:00:00 2001 From: Michael Bradford Date: Thu, 15 Jan 2026 16:11:15 -0800 Subject: [PATCH 3/7] notification webhook --- src/polyswarm/client/notification.py | 16 ++++++++++++++++ .../{webhook.py => notification_webhook.py} | 14 +++++++------- src/polyswarm/client/polyswarm.py | 3 ++- src/polyswarm/client/sandbox.py | 5 ----- 4 files changed, 25 insertions(+), 13 deletions(-) create mode 100644 src/polyswarm/client/notification.py rename src/polyswarm/client/{webhook.py => notification_webhook.py} (90%) diff --git a/src/polyswarm/client/notification.py b/src/polyswarm/client/notification.py new file mode 100644 index 0000000..7dab708 --- /dev/null +++ b/src/polyswarm/client/notification.py @@ -0,0 +1,16 @@ +import logging + +import click + +from polyswarm.client.notification_webhook import webhook + +logger = logging.getLogger(__name__) + + +@click.group(short_help='Interact with Polyswarm notification systems.') +def notification(): + pass + + +# Add webhook as a subcommand of notification +notification.add_command(webhook) diff --git a/src/polyswarm/client/webhook.py b/src/polyswarm/client/notification_webhook.py similarity index 90% rename from src/polyswarm/client/webhook.py rename to src/polyswarm/client/notification_webhook.py index 119ca98..43f5691 100644 --- a/src/polyswarm/client/webhook.py +++ b/src/polyswarm/client/notification_webhook.py @@ -7,7 +7,7 @@ logger = logging.getLogger(__name__) -@click.group(short_help='Interact with Webhooks in Polyswarm.') +@click.group(short_help='Interact with Notification Webhooks in Polyswarm.') def webhook(): pass @@ -23,7 +23,7 @@ def create(ctx, webhook_uri, secret, status, events): """ Create a new webhook. - WEBHOOK_URI: The URI where webhook events should be sent + WEBHOOK_URI: The URI where notification webhook events should be sent SECRET: The secret key used for HMAC signature verification """ api = ctx.obj['api'] @@ -46,7 +46,7 @@ def create(ctx, webhook_uri, secret, status, events): @click.pass_context def get(ctx, webhook_id): """ - Get a webhook by ID. + Get a notification webhook by ID. """ api = ctx.obj['api'] output = ctx.obj['output'] @@ -63,7 +63,7 @@ def get(ctx, webhook_id): @utils.any_provided('webhook_uri', 'secret', 'status', 'events') def update(ctx, webhook_id, webhook_uri, secret, status, events): """ - Update an existing webhook. + Update an existing notification webhook. """ api = ctx.obj['api'] output = ctx.obj['output'] @@ -91,7 +91,7 @@ def update(ctx, webhook_id, webhook_uri, secret, status, events): @click.pass_context def delete(ctx, webhook_id): """ - Delete a webhook. + Delete a notification webhook. """ api = ctx.obj['api'] output = ctx.obj['output'] @@ -103,7 +103,7 @@ def delete(ctx, webhook_id): @click.pass_context def list_webhooks(ctx): """ - List all webhooks for the current account. + List all notification webhooks for the current account. """ api = ctx.obj['api'] output = ctx.obj['output'] @@ -116,7 +116,7 @@ def list_webhooks(ctx): @click.pass_context def test(ctx, webhook_id): """ - Test a webhook by sending a test payload. + Test a notification webhook by sending a test payload. """ api = ctx.obj['api'] api.webhook_test(webhook_id) diff --git a/src/polyswarm/client/polyswarm.py b/src/polyswarm/client/polyswarm.py index f1508e0..1c6b4db 100644 --- a/src/polyswarm/client/polyswarm.py +++ b/src/polyswarm/client/polyswarm.py @@ -30,6 +30,7 @@ from polyswarm.client.bundle import bundle from polyswarm.client.report_template import report_template from polyswarm.client.account import account +from polyswarm.client.notification import notification logger = logging.getLogger(__name__) @@ -156,7 +157,7 @@ def polyswarm_cli(ctx, api_key, api_uri, output_file, output_format, color, verb cat, stream, rescan, rescan_id, rules, link, tag, family, metadata, engine, known, sandbox, sandbox_list, - activity, report, report_template, account, bundle, + activity, report, report_template, account, bundle, notification, ] for command in commands: diff --git a/src/polyswarm/client/sandbox.py b/src/polyswarm/client/sandbox.py index dab3b80..79075ca 100644 --- a/src/polyswarm/client/sandbox.py +++ b/src/polyswarm/client/sandbox.py @@ -4,7 +4,6 @@ from polyswarm.client import utils -from polyswarm.client.webhook import webhook from polyswarm.utils import is_url logger = logging.getLogger(__name__) @@ -181,7 +180,3 @@ def my_task_list(ctx, sandbox_, start_date, end_date, sha256, user_account_id): sha256=sha256, user_account_id=user_account_id): output.sandbox_task(task) - - -# Add webhook as a subcommand of sandbox -sandbox.add_command(webhook) From cde688f9b245aed58c5ff7d52bd73df57868b2dd Mon Sep 17 00:00:00 2001 From: Michael Bradford Date: Thu, 15 Jan 2026 16:38:21 -0800 Subject: [PATCH 4/7] simplify event input --- src/polyswarm/client/notification_webhook.py | 26 +++++--------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/src/polyswarm/client/notification_webhook.py b/src/polyswarm/client/notification_webhook.py index 43f5691..1cb1e1b 100644 --- a/src/polyswarm/client/notification_webhook.py +++ b/src/polyswarm/client/notification_webhook.py @@ -17,7 +17,8 @@ def webhook(): @click.argument('secret', type=click.STRING, required=True) @click.option('--status', type=click.Choice(['enabled', 'disabled']), default='enabled', help='Webhook status (default: enabled).') -@click.option('--events', type=click.STRING, help='JSON string specifying which events to subscribe to.') +@click.option('--events', type=click.STRING, multiple=True, + help='Event types to subscribe to (can be specified multiple times).') @click.pass_context def create(ctx, webhook_uri, secret, status, events): """ @@ -29,15 +30,7 @@ def create(ctx, webhook_uri, secret, status, events): api = ctx.obj['api'] output = ctx.obj['output'] - import json - events_dict = None - if events: - try: - events_dict = json.loads(events) - except json.JSONDecodeError as e: - raise click.BadParameter(f'Invalid JSON for events: {e}') - - result = api.webhook_create(webhook_uri=webhook_uri, secret=secret, status=status, events=events_dict) + result = api.webhook_create(webhook_uri=webhook_uri, secret=secret, status=status, events=events) output.webhook(result) @@ -58,7 +51,8 @@ def get(ctx, webhook_id): @click.option('--webhook-uri', type=click.STRING, help='The new webhook URI.') @click.option('--secret', type=click.STRING, help='The new secret for HMAC signing.') @click.option('--status', type=click.Choice(['enabled', 'disabled']), help='The new status.') -@click.option('--events', type=click.STRING, help='JSON string specifying which events to subscribe to.') +@click.option('--events', type=click.STRING, multiple=True, + help='Event types to subscribe to (can be specified multiple times).') @click.pass_context @utils.any_provided('webhook_uri', 'secret', 'status', 'events') def update(ctx, webhook_id, webhook_uri, secret, status, events): @@ -68,20 +62,12 @@ def update(ctx, webhook_id, webhook_uri, secret, status, events): api = ctx.obj['api'] output = ctx.obj['output'] - import json - events_dict = None - if events: - try: - events_dict = json.loads(events) - except json.JSONDecodeError as e: - raise click.BadParameter(f'Invalid JSON for events: {e}') - result = api.webhook_update( webhook_id=webhook_id, webhook_uri=webhook_uri, secret=secret, status=status, - events=events_dict + events=events ) output.webhook(result) From 33ac2a8759271eb14b8dc17cfade3781e425b83d Mon Sep 17 00:00:00 2001 From: Michael Bradford Date: Fri, 16 Jan 2026 11:23:41 -0800 Subject: [PATCH 5/7] not needed --- src/polyswarm/formatters/text.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/polyswarm/formatters/text.py b/src/polyswarm/formatters/text.py index 9053d41..0557266 100644 --- a/src/polyswarm/formatters/text.py +++ b/src/polyswarm/formatters/text.py @@ -684,7 +684,6 @@ def webhook(self, webhook, write=True): output.append(self._white(f'Account Number: {webhook.account_number}')) if webhook.team_account_number: output.append(self._white(f'Team Account Number: {webhook.team_account_number}')) - output.append(self._white(f'Secret SHA256: {webhook.secret_sha256}')) status_writer = self._green if webhook.status == 'enabled' else self._yellow output.append(status_writer(f'Status: {webhook.status}')) if webhook.events: From 29fe7bd533229ecf6fcb5f27ca3ba052b922765e Mon Sep 17 00:00:00 2001 From: Michael Bradford Date: Tue, 20 Jan 2026 10:08:34 -0800 Subject: [PATCH 6/7] update api method use --- src/polyswarm/client/notification_webhook.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/polyswarm/client/notification_webhook.py b/src/polyswarm/client/notification_webhook.py index 1cb1e1b..a699d7f 100644 --- a/src/polyswarm/client/notification_webhook.py +++ b/src/polyswarm/client/notification_webhook.py @@ -30,7 +30,7 @@ def create(ctx, webhook_uri, secret, status, events): api = ctx.obj['api'] output = ctx.obj['output'] - result = api.webhook_create(webhook_uri=webhook_uri, secret=secret, status=status, events=events) + result = api.notification_webhook_create(webhook_uri=webhook_uri, secret=secret, status=status, events=events) output.webhook(result) @@ -43,7 +43,7 @@ def get(ctx, webhook_id): """ api = ctx.obj['api'] output = ctx.obj['output'] - output.webhook(api.webhook_get(webhook_id)) + output.webhook(api.notification_webhook_get(webhook_id)) @webhook.command('update', short_help='Update an existing webhook.') @@ -62,7 +62,7 @@ def update(ctx, webhook_id, webhook_uri, secret, status, events): api = ctx.obj['api'] output = ctx.obj['output'] - result = api.webhook_update( + result = api.notification_webhook_update( webhook_id=webhook_id, webhook_uri=webhook_uri, secret=secret, @@ -81,7 +81,7 @@ def delete(ctx, webhook_id): """ api = ctx.obj['api'] output = ctx.obj['output'] - api.webhook_delete(webhook_id) + api.notification_webhook_delete(webhook_id) click.echo(f'Webhook {webhook_id} deleted successfully') @@ -93,7 +93,7 @@ def list_webhooks(ctx): """ api = ctx.obj['api'] output = ctx.obj['output'] - for webhook_obj in api.webhook_list(): + for webhook_obj in api.notification_webhook_list(): output.webhook(webhook_obj) @@ -105,5 +105,5 @@ def test(ctx, webhook_id): Test a notification webhook by sending a test payload. """ api = ctx.obj['api'] - api.webhook_test(webhook_id) + api.notification_webhook_test(webhook_id) click.echo(f'Test payload sent to webhook {webhook_id}') From 899825cafcbd8927bd2311d0f1c288fce97a012e Mon Sep 17 00:00:00 2001 From: Michael Bradford Date: Wed, 21 Jan 2026 08:43:24 -0800 Subject: [PATCH 7/7] bump api --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9de5201..78f3bd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ ] dependencies = [ - "polyswarm_api>=3.15", + "polyswarm_api>=3.16", "click>=7.1", "colorama>=0.4.6", "click-log>=0.4.0",