From d191348a98100bb49efc9bc91faee4d87067d5e9 Mon Sep 17 00:00:00 2001 From: Felipe Fumero Date: Mon, 2 Nov 2026 12:02:22 -0600 Subject: [PATCH 1/3] fix: stop logging notification-rule command arguments These commands take a signing secret and custom headers as arguments; suppress argv logging for them. --- b2/_internal/console_tool.py | 3 ++ .../console_tool/test_notification_rules.py | 54 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/b2/_internal/console_tool.py b/b2/_internal/console_tool.py index 889d26ec5..331060153 100644 --- a/b2/_internal/console_tool.py +++ b/b2/_internal/console_tool.py @@ -4591,6 +4591,9 @@ def _run(self, args): class BucketNotificationRuleCreateBase( JSONOptionMixin, BucketNotificationRuleWarningMixin, Command ): + FORBID_LOGGING_ARGUMENTS = True + # b2sdk request logging is handled separately. + @classmethod def _validate_secret(cls, value: str) -> str: if not re.match(r'^[a-zA-Z0-9]{32}$', value): diff --git a/test/unit/console_tool/test_notification_rules.py b/test/unit/console_tool/test_notification_rules.py index 83d017107..a1446c746 100644 --- a/test/unit/console_tool/test_notification_rules.py +++ b/test/unit/console_tool/test_notification_rules.py @@ -279,3 +279,57 @@ def test_notification_rules__sign_secret(b2_cli, bucket, bucket_notification_rul [*command, 'list', '--json', f'b2://{bucket}'], )[1] ) == [bucket_notification_rule] + + +@pytest.mark.parametrize('subcommand', ['create', 'update']) +def test_notification_rules__sensitive_arguments_are_not_logged( + b2_cli, bucket, bucket_notification_rule, b2_cli_log_fix, caplog, subcommand +): + sign_secret = '0123456789abcdef0123456789abcdef' + custom_header = 'X-Webhook-Token=custom-header-secret' + + if subcommand == 'create': + command_args = [ + 'bucket', + 'notification-rule', + 'create', + '--verbose', + f'b2://{bucket}', + 'new-rule', + '--event-type', + 'b2:ObjectCreated:*', + '--webhook-url', + 'https://example.com/webhook', + ] + else: + command_args = [ + 'bucket', + 'notification-rule', + 'update', + '--verbose', + f'b2://{bucket}', + bucket_notification_rule['name'], + '--disable', + ] + + b2_cli.run( + [ + *command_args, + '--sign-secret', + sign_secret, + '--custom-header', + custom_header, + ] + ) + + b2_cli.run(['bucket', 'create', '--verbose', 'control-log-bucket', 'allPrivate']) + + console_tool_logs = '\n'.join( + record.getMessage() + for record in caplog.records + if record.name == 'b2._internal.console_tool' + ) + assert sign_secret not in console_tool_logs + assert custom_header not in console_tool_logs + assert 'arguments hidden' in console_tool_logs + assert 'control-log-bucket' in console_tool_logs From 0638d75a5eb350926012a1c706a2e8459e7a132e Mon Sep 17 00:00:00 2001 From: Felipe Fumero Date: Mon, 2 Nov 2026 12:48:18 -0600 Subject: [PATCH 2/3] Add changelog fragment for notification-rule argument logging --- changelog.d/+notification-rule-no-arg-logging.fixed.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/+notification-rule-no-arg-logging.fixed.md diff --git a/changelog.d/+notification-rule-no-arg-logging.fixed.md b/changelog.d/+notification-rule-no-arg-logging.fixed.md new file mode 100644 index 000000000..a7079e0ce --- /dev/null +++ b/changelog.d/+notification-rule-no-arg-logging.fixed.md @@ -0,0 +1 @@ +`bucket notification-rule create` and `bucket notification-rule update` no longer write the signing secret (`--sign-secret`) or custom header values (`--custom-header`) to the command log. From e5088a03b08e47cc4fd4b7206d09a58b005634c0 Mon Sep 17 00:00:00 2001 From: Felipe Fumero Date: Tue, 3 Nov 2026 08:50:35 -0600 Subject: [PATCH 3/3] fix: scope notification-rule debug logging Limit SDK bucket debug logging while notification-rule commands run and restore the previous logging level afterward. Cover stderr and debug files in regression tests and correct the changelog fragment. --- b2/_internal/console_tool.py | 11 +- ...+notification-rule-no-arg-logging.fixed.md | 2 +- .../console_tool/test_notification_rules.py | 126 ++++++++++++------ 3 files changed, 97 insertions(+), 42 deletions(-) diff --git a/b2/_internal/console_tool.py b/b2/_internal/console_tool.py index 331060153..5c7410e14 100644 --- a/b2/_internal/console_tool.py +++ b/b2/_internal/console_tool.py @@ -4592,7 +4592,16 @@ class BucketNotificationRuleCreateBase( JSONOptionMixin, BucketNotificationRuleWarningMixin, Command ): FORBID_LOGGING_ARGUMENTS = True - # b2sdk request logging is handled separately. + + def run(self, args): + # SDK bucket DEBUG call traces include signing secrets and custom header values. + sdk_logger = logging.getLogger('b2sdk._internal.bucket') + previous_level = sdk_logger.level + sdk_logger.setLevel(max(logging.INFO, sdk_logger.getEffectiveLevel())) + try: + return super().run(args) + finally: + sdk_logger.setLevel(previous_level) @classmethod def _validate_secret(cls, value: str) -> str: diff --git a/changelog.d/+notification-rule-no-arg-logging.fixed.md b/changelog.d/+notification-rule-no-arg-logging.fixed.md index a7079e0ce..f96a4ab3b 100644 --- a/changelog.d/+notification-rule-no-arg-logging.fixed.md +++ b/changelog.d/+notification-rule-no-arg-logging.fixed.md @@ -1 +1 @@ -`bucket notification-rule create` and `bucket notification-rule update` no longer write the signing secret (`--sign-secret`) or custom header values (`--custom-header`) to the command log. +The `bucket notification-rule create` and `bucket notification-rule update` commands no longer log signing secrets or custom header values in command arguments or SDK call traces. diff --git a/test/unit/console_tool/test_notification_rules.py b/test/unit/console_tool/test_notification_rules.py index a1446c746..4af8c44b0 100644 --- a/test/unit/console_tool/test_notification_rules.py +++ b/test/unit/console_tool/test_notification_rules.py @@ -8,9 +8,12 @@ # ###################################################################### import json +import logging import pytest +from b2._internal.console_tool import CommandError + @pytest.fixture() def bucket_notification_rule(b2_cli, bucket): @@ -281,55 +284,98 @@ def test_notification_rules__sign_secret(b2_cli, bucket, bucket_notification_rul ) == [bucket_notification_rule] +@pytest.mark.parametrize('command', [['bucket', 'notification-rule'], ['notification-rules']]) @pytest.mark.parametrize('subcommand', ['create', 'update']) +@pytest.mark.parametrize( + 'logging_flags', [['--verbose'], ['--debug-logs'], ['--verbose', '--debug-logs']] +) def test_notification_rules__sensitive_arguments_are_not_logged( - b2_cli, bucket, bucket_notification_rule, b2_cli_log_fix, caplog, subcommand + b2_cli, + bucket, + bucket_notification_rule, + b2_cli_log_fix, + cwd_path, + caplog, + capsys, + command, + subcommand, + logging_flags, ): - sign_secret = '0123456789abcdef0123456789abcdef' - custom_header = 'X-Webhook-Token=custom-header-secret' + # Deliberately synthetic values, not credentials. + sign_secret = 'a' * 32 + header_value = 'test-only-header-value' + caplog.set_level(logging.DEBUG) + caplog.set_level(logging.DEBUG, logger='b2sdk._internal.bucket') + caplog.clear() + capsys.readouterr() + original_handlers = logging.root.handlers[:] + + try: + b2_cli.run( + [ + *command, + subcommand, + *logging_flags, + f'b2://{bucket}', + 'new-rule' if subcommand == 'create' else bucket_notification_rule['name'], + '--event-type', + 'b2:ObjectCreated:*', + '--webhook-url', + 'https://example.com/webhook', + '--sign-secret', + sign_secret, + '--custom-header', + f'X-Test-Header={header_value}', + ] + ) + + outputs = [caplog.text, capsys.readouterr().err] + if '--debug-logs' in logging_flags: + outputs.append((cwd_path / 'b2_cli.log').read_text()) + for output in outputs: + assert sign_secret not in output + assert header_value not in output + assert 'arguments hidden' in caplog.text + + caplog.clear() + control_args = ['bucket', 'create', *logging_flags, 'control-log-bucket', 'allPrivate'] + b2_cli.run(control_args) + assert f"with arguments: {['b2', *control_args]}" in caplog.text + assert 'calling Bucket(' in caplog.text + finally: + for handler in logging.root.handlers[:]: + if handler not in original_handlers: + logging.root.removeHandler(handler) + handler.close() + + +@pytest.mark.parametrize('sdk_log_level', [logging.DEBUG, logging.WARNING]) +def test_notification_rules__sdk_logging_restored_after_error( + b2_cli, bucket, bucket_notification_rule, caplog, monkeypatch, sdk_log_level +): + sdk_logger = logging.getLogger('b2sdk._internal.bucket') + caplog.set_level(logging.DEBUG) + caplog.set_level(sdk_log_level, logger=sdk_logger.name) - if subcommand == 'create': - command_args = [ - 'bucket', - 'notification-rule', - 'create', - '--verbose', - f'b2://{bucket}', - 'new-rule', - '--event-type', - 'b2:ObjectCreated:*', - '--webhook-url', - 'https://example.com/webhook', - ] - else: - command_args = [ + def reject_rules(*args, **kwargs): + sdk_logger.debug('test-only SDK request trace') + sdk_logger.warning('test-only SDK warning') + raise CommandError('test-only request failure') + + monkeypatch.setattr(b2_cli.raw_api, 'set_bucket_notification_rules', reject_rules) + b2_cli.run( + [ 'bucket', 'notification-rule', 'update', - '--verbose', f'b2://{bucket}', bucket_notification_rule['name'], '--disable', - ] - - b2_cli.run( - [ - *command_args, - '--sign-secret', - sign_secret, - '--custom-header', - custom_header, - ] + ], + expected_status=1, + expected_stderr='ERROR: test-only request failure\n', ) - b2_cli.run(['bucket', 'create', '--verbose', 'control-log-bucket', 'allPrivate']) - - console_tool_logs = '\n'.join( - record.getMessage() - for record in caplog.records - if record.name == 'b2._internal.console_tool' - ) - assert sign_secret not in console_tool_logs - assert custom_header not in console_tool_logs - assert 'arguments hidden' in console_tool_logs - assert 'control-log-bucket' in console_tool_logs + assert 'test-only SDK request trace' not in caplog.text + assert 'test-only SDK warning' in caplog.text + assert sdk_logger.level == sdk_log_level