diff --git a/prowler/changelog.d/slack-zero-findings.fixed.md b/prowler/changelog.d/slack-zero-findings.fixed.md new file mode 100644 index 0000000000..7b62939855 --- /dev/null +++ b/prowler/changelog.d/slack-zero-findings.fixed.md @@ -0,0 +1 @@ +CLI Slack integration (`--slack`) no longer fails when a scan produces no findings: the pass and fail percentages are guarded against a `findings_count` of 0, which previously raised `ZeroDivisionError` and sent `blocks=None` to Slack instead of the summary diff --git a/prowler/lib/outputs/slack/slack.py b/prowler/lib/outputs/slack/slack.py index d32214cc7c..ff6afe264c 100644 --- a/prowler/lib/outputs/slack/slack.py +++ b/prowler/lib/outputs/slack/slack.py @@ -110,6 +110,20 @@ class Slack: list: list of Slack message blocks. """ try: + # A scan can legitimately produce no findings, in which case + # `findings_count` is 0 and the percentages below would raise + # `ZeroDivisionError`. + findings_count = stats["findings_count"] + pass_percentage = ( + round(stats["total_pass"] / findings_count * 100, 2) + if findings_count + else 0 + ) + fail_percentage = ( + round(stats["total_fail"] / findings_count * 100, 2) + if findings_count + else 0 + ) blocks = [ { "type": "section", @@ -128,7 +142,7 @@ class Slack: "type": "section", "text": { "type": "mrkdwn", - "text": f"\n:white_check_mark: *{stats['total_pass']} Passed findings* ({round(stats['total_pass'] / stats['findings_count'] * 100, 2)}%)\n", + "text": f"\n:white_check_mark: *{stats['total_pass']} Passed findings* ({pass_percentage}%)\n", }, }, { @@ -148,7 +162,7 @@ class Slack: "type": "section", "text": { "type": "mrkdwn", - "text": f"\n:x: *{stats['total_fail']} Failed findings* ({round(stats['total_fail'] / stats['findings_count'] * 100, 2)}%)\n ", + "text": f"\n:x: *{stats['total_fail']} Failed findings* ({fail_percentage}%)\n ", }, }, { diff --git a/tests/lib/outputs/slack/slack_test.py b/tests/lib/outputs/slack/slack_test.py index 2ab3d3a5ae..84b581ace2 100644 --- a/tests/lib/outputs/slack/slack_test.py +++ b/tests/lib/outputs/slack/slack_test.py @@ -205,6 +205,47 @@ class TestSlackIntegration: }, ] + def test_create_message_blocks_no_findings(self): + """A scan that produced no findings must still build a valid message. + + The pass/fail percentages divide by ``stats["findings_count"]``, which is + initialised to 0 in ``prowler/lib/outputs/outputs.py``. With no findings + that raises ``ZeroDivisionError``, the handler logs it and returns + ``None``, and ``send()`` then passes ``blocks=None`` to + ``chat_postMessage``. + """ + aws_provider = set_mocked_aws_provider() + slack = Slack(SLACK_TOKEN, SLACK_CHANNEL, aws_provider) + args = "--slack" + stats = { + "total_pass": 0, + "total_fail": 0, + "total_critical_severity_pass": 0, + "total_critical_severity_fail": 0, + "total_high_severity_fail": 0, + "total_high_severity_pass": 0, + "total_medium_severity_fail": 0, + "total_medium_severity_pass": 0, + "total_low_severity_fail": 0, + "total_low_severity_pass": 0, + "resources_count": 0, + "findings_count": 0, + } + + blocks = slack.__create_message_blocks__( + f"AWS Account *{AWS_ACCOUNT_NUMBER}*", aws_logo, stats, args + ) + + assert blocks is not None, ( + "__create_message_blocks__ returned None, so send() passes " + "blocks=None to chat_postMessage" + ) + assert isinstance(blocks, list) and blocks + # The percentages must degrade to 0% rather than raising. + rendered = str(blocks) + assert "0 Passed findings* (0%)" in rendered + assert "0 Failed findings* (0%)" in rendered + def test_create_message_blocks_azure(self): aws_provider = set_mocked_azure_provider() slack = Slack(SLACK_TOKEN, SLACK_CHANNEL, aws_provider)