refactor(Slack): create class (#4127)

This commit is contained in:
Pepe Fagoaga
2024-06-04 16:54:12 +02:00
committed by GitHub
parent 8645ee20c3
commit 070edc1693
5 changed files with 297 additions and 204 deletions
+12 -10
View File
@@ -40,7 +40,7 @@ from prowler.lib.outputs.compliance.compliance import display_compliance_table
from prowler.lib.outputs.html.html import add_html_footer, fill_html_overview_statistics
from prowler.lib.outputs.json.json import close_json
from prowler.lib.outputs.outputs import extract_findings_statistics
from prowler.lib.outputs.slack import send_slack_message
from prowler.lib.outputs.slack.slack import Slack
from prowler.lib.outputs.summary_table import display_summary_table
from prowler.providers.aws.lib.s3.s3 import send_to_s3_bucket
from prowler.providers.aws.lib.security_hub.security_hub import (
@@ -249,20 +249,22 @@ def prowler():
stats = extract_findings_statistics(findings)
if args.slack:
# TODO: this should be also in a config file
if "SLACK_API_TOKEN" in environ and (
"SLACK_CHANNEL_NAME" in environ or "SLACK_CHANNEL_ID" in environ
):
_ = send_slack_message(
environ["SLACK_API_TOKEN"],
(
environ["SLACK_CHANNEL_NAME"]
if "SLACK_CHANNEL_NAME" in environ
else environ["SLACK_CHANNEL_ID"]
),
stats,
global_provider,
token = environ["SLACK_API_TOKEN"]
channel = (
environ["SLACK_CHANNEL_NAME"]
if "SLACK_CHANNEL_NAME" in environ
else environ["SLACK_CHANNEL_ID"]
)
prowler_args = " ".join(sys.argv[1:])
slack = Slack(token, channel, global_provider)
_ = slack.send(stats, prowler_args)
else:
# Refactor(CLI)
logger.critical(
"Slack integration needs SLACK_API_TOKEN and SLACK_CHANNEL_NAME environment variables (see more in https://docs.prowler.cloud/en/latest/tutorials/integrations/#slack)."
)
-156
View File
@@ -1,156 +0,0 @@
import sys
from slack_sdk import WebClient
from prowler.config.config import aws_logo, azure_logo, gcp_logo, square_logo_img
from prowler.lib.logger import logger
def send_slack_message(token, channel, stats, provider):
try:
client = WebClient(token=token)
identity, logo = create_message_identity(provider)
response = client.chat_postMessage(
username="Prowler",
icon_url=square_logo_img,
channel=f"#{channel}",
blocks=create_message_blocks(identity, logo, stats),
)
return response
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
# TODO: move this to each provider
def create_message_identity(provider):
"""
Create a Slack message identity based on the provider type.
Parameters:
- provider (Provider): The Provider (e.g. "AwsProvider", "GcpProvider", "AzureProvide").
Returns:
- identity (str): The message identity based on the provider type.
- logo (str): The logo URL associated with the provider type.
"""
try:
identity = ""
logo = aws_logo
if provider.type == "aws":
identity = f"AWS Account *{provider.identity.account}*"
elif provider.type == "gcp":
identity = f"GCP Projects *{', '.join(provider.project_ids)}*"
logo = gcp_logo
elif provider.type == "azure":
printed_subscriptions = []
for key, value in provider.identity.subscriptions.items():
intermediate = f"- *{key}: {value}*\n"
printed_subscriptions.append(intermediate)
identity = f"Azure Subscriptions:\n{''.join(printed_subscriptions)}"
logo = azure_logo
return identity, logo
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def create_title(identity, stats):
try:
title = f"Hey there 👋 \n I'm *Prowler*, _the handy multi-cloud security tool_ :cloud::key:\n\n I have just finished the security assessment on your {identity} with a total of *{stats['findings_count']}* findings."
return title
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def create_message_blocks(identity, logo, stats):
try:
blocks = [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": create_title(identity, stats),
},
"accessory": {
"type": "image",
"image_url": logo,
"alt_text": "Provider Logo",
},
},
{"type": "divider"},
{
"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",
},
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"\n:x: *{stats['total_fail']} Failed findings* ({round(stats['total_fail'] / stats['findings_count'] * 100 , 2)}%)\n ",
},
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"\n:bar_chart: *{stats['resources_count']} Scanned Resources*\n",
},
},
{"type": "divider"},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": f"Used parameters: `prowler {' '.join(sys.argv[1:])} `",
}
],
},
{"type": "divider"},
{
"type": "section",
"text": {"type": "mrkdwn", "text": "Join our Slack Community!"},
"accessory": {
"type": "button",
"text": {"type": "plain_text", "text": "Prowler :slack:"},
"url": "https://join.slack.com/t/prowler-workspace/shared_invite/zt-1hix76xsl-2uq222JIXrC7Q8It~9ZNog",
},
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Feel free to contact us in our repo",
},
"accessory": {
"type": "button",
"text": {"type": "plain_text", "text": "Prowler :github:"},
"url": "https://github.com/prowler-cloud/prowler",
},
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "See all the things you can do with ProwlerPro",
},
"accessory": {
"type": "button",
"text": {"type": "plain_text", "text": "Prowler Pro"},
"url": "https://prowler.pro",
},
},
]
return blocks
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
+206
View File
@@ -0,0 +1,206 @@
from typing import Any
from slack_sdk import WebClient
from slack_sdk.web.base_client import SlackResponse
from prowler.config.config import aws_logo, azure_logo, gcp_logo, square_logo_img
from prowler.lib.logger import logger
class Slack:
_provider: Any
_token: str
_channel: str
def __init__(self, token: str, channel: str, provider: Any) -> "Slack":
self._token = token
self._channel = channel
self._provider = provider
@property
def token(self):
return self._token
@property
def channel(self):
return self._channel
def send(self, stats: dict, args: str) -> SlackResponse:
"""
Sends the findings to Slack.
Args:
stats (dict): A dictionary containing audit statistics.
args (str): Command line arguments used for the audit.
Returns:
SlackResponse: Slack response if successful, error object if an exception occurs.
"""
try:
client = WebClient(token=self.token)
identity, logo = self.__create_message_identity__(self._provider)
response = client.chat_postMessage(
username="Prowler",
icon_url=square_logo_img,
channel=f"#{self.channel}",
blocks=self.__create_message_blocks__(identity, logo, stats, args),
)
return response
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return error
def __create_message_identity__(self, provider: Any):
"""
Create a Slack message identity based on the provider type.
Parameters:
- provider (Provider): The Provider (e.g. "AwsProvider", "GcpProvider", "AzureProvide").
Returns:
- identity (str): The message identity based on the provider type.
- logo (str): The logo URL associated with the provider type.
"""
# TODO: support kubernetes
try:
identity = ""
logo = aws_logo
if provider.type == "aws":
identity = f"AWS Account *{provider.identity.account}*"
elif provider.type == "gcp":
identity = f"GCP Projects *{', '.join(provider.project_ids)}*"
logo = gcp_logo
elif provider.type == "azure":
printed_subscriptions = []
for key, value in provider.identity.subscriptions.items():
intermediate = f"- *{key}: {value}*\n"
printed_subscriptions.append(intermediate)
identity = f"Azure Subscriptions:\n{''.join(printed_subscriptions)}"
logo = azure_logo
return identity, logo
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def __create_message_blocks__(self, identity, logo, stats, args) -> list:
"""
Create the Slack message blocks.
Args:
identity: message identity.
logo: logo URL.
stats: audit statistics.
args: command line arguments used.
Returns:
list: list of Slack message blocks.
"""
try:
blocks = [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": self.__create_title__(identity, stats),
},
"accessory": {
"type": "image",
"image_url": logo,
"alt_text": "Provider Logo",
},
},
{"type": "divider"},
{
"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",
},
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"\n:x: *{stats['total_fail']} Failed findings* ({round(stats['total_fail'] / stats['findings_count'] * 100 , 2)}%)\n ",
},
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"\n:bar_chart: *{stats['resources_count']} Scanned Resources*\n",
},
},
{"type": "divider"},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": f"Used parameters: `prowler {args}`",
}
],
},
{"type": "divider"},
{
"type": "section",
"text": {"type": "mrkdwn", "text": "Join our Slack Community!"},
"accessory": {
"type": "button",
"text": {"type": "plain_text", "text": "Prowler :slack:"},
"url": "https://join.slack.com/t/prowler-workspace/shared_invite/zt-1hix76xsl-2uq222JIXrC7Q8It~9ZNog",
},
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Feel free to contact us in our repo",
},
"accessory": {
"type": "button",
"text": {"type": "plain_text", "text": "Prowler :github:"},
"url": "https://github.com/prowler-cloud/prowler",
},
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "See all the things you can do with ProwlerPro",
},
"accessory": {
"type": "button",
"text": {"type": "plain_text", "text": "Prowler Pro"},
"url": "https://prowler.pro",
},
},
]
return blocks
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def __create_title__(self, identity, stats) -> str:
"""
Create the Slack message title.
Args:
identity: message identity.
stats: audit statistics.
Returns:
str: Slack message title.
"""
try:
title = f"Hey there 👋 \n I'm *Prowler*, _the handy multi-cloud security tool_ :cloud::key:\n\n I have just finished the security assessment on your {identity} with a total of *{stats['findings_count']}* findings."
return title
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
@@ -1,13 +1,7 @@
import sys
from unittest import mock
from prowler.config.config import aws_logo, azure_logo, gcp_logo
from prowler.lib.outputs.slack import (
create_message_blocks,
create_message_identity,
create_title,
send_slack_message,
)
from prowler.lib.outputs.slack.slack import Slack
from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, set_mocked_aws_provider
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_ID,
@@ -16,28 +10,25 @@ from tests.providers.azure.azure_fixtures import (
)
from tests.providers.gcp.gcp_fixtures import set_mocked_gcp_provider
def mock_create_message_blocks(*_):
return [{}]
def mock_create_message_identity(*_):
return "", ""
SLACK_CHANNEL = "test-channel"
SLACK_TOKEN = "test-token"
class TestSlackIntegration:
def test_create_message_identity_aws(self):
aws_provider = set_mocked_aws_provider()
slack = Slack(SLACK_TOKEN, SLACK_CHANNEL, aws_provider)
assert create_message_identity(aws_provider) == (
assert slack.__create_message_identity__(aws_provider) == (
f"AWS Account *{aws_provider.identity.account}*",
aws_logo,
)
def test_create_message_identity_azure(self):
azure_provider = set_mocked_azure_provider()
slack = Slack(SLACK_TOKEN, SLACK_CHANNEL, azure_provider)
assert create_message_identity(azure_provider) == (
assert slack.__create_message_identity__(azure_provider) == (
f"Azure Subscriptions:\n- *{AZURE_SUBSCRIPTION_ID}: {AZURE_SUBSCRIPTION_NAME}*\n",
azure_logo,
)
@@ -46,27 +37,50 @@ class TestSlackIntegration:
gcp_provider = set_mocked_gcp_provider(
project_ids=["test-project1", "test-project2"],
)
slack = Slack(SLACK_TOKEN, SLACK_CHANNEL, gcp_provider)
assert create_message_identity(gcp_provider) == (
assert slack.__create_message_identity__(gcp_provider) == (
f"GCP Projects *{', '.join(gcp_provider.project_ids)}*",
gcp_logo,
)
def test_create_message_blocks(self):
aws_identity = f"AWS Account *{AWS_ACCOUNT_NUMBER}*"
azure_identity = "Azure Subscriptions:\n- *subscription 1: qwerty*\n- *subscription 2: asdfg*\n"
gcp_identity = "GCP Project *gcp-project*"
def test_create_title(self):
aws_provider = set_mocked_aws_provider()
slack = Slack(SLACK_TOKEN, SLACK_CHANNEL, aws_provider)
stats = {}
stats["total_pass"] = 12
stats["total_fail"] = 10
stats["resources_count"] = 20
stats["findings_count"] = 22
assert create_message_blocks(aws_identity, aws_logo, stats) == [
identity = slack.__create_message_identity__(aws_provider) == (
f"AWS Account *{aws_provider.identity.account}*",
aws_logo,
)
assert (
slack.__create_title__(identity, stats)
== f"Hey there 👋 \n I'm *Prowler*, _the handy multi-cloud security tool_ :cloud::key:\n\n I have just finished the security assessment on your {identity} with a total of *{stats['findings_count']}* findings."
)
def test_create_message_blocks_aws(self):
aws_provider = set_mocked_aws_provider()
slack = Slack(SLACK_TOKEN, SLACK_CHANNEL, aws_provider)
args = "--slack"
stats = {}
stats["total_pass"] = 12
stats["total_fail"] = 10
stats["resources_count"] = 20
stats["findings_count"] = 22
aws_identity = f"AWS Account *{AWS_ACCOUNT_NUMBER}*"
assert slack.__create_message_blocks__(aws_identity, aws_logo, stats, args) == [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": create_title(aws_identity, stats),
"text": slack.__create_title__(aws_identity, stats),
},
"accessory": {
"type": "image",
@@ -102,7 +116,7 @@ class TestSlackIntegration:
"elements": [
{
"type": "mrkdwn",
"text": f"Used parameters: `prowler {' '.join(sys.argv[1:])} `",
"text": f"Used parameters: `prowler {args}`",
}
],
},
@@ -141,12 +155,27 @@ class TestSlackIntegration:
},
},
]
assert create_message_blocks(azure_identity, azure_logo, stats) == [
def test_create_message_blocks_azure(self):
aws_provider = set_mocked_aws_provider()
slack = Slack(SLACK_TOKEN, SLACK_CHANNEL, aws_provider)
args = "--slack"
stats = {}
stats["total_pass"] = 12
stats["total_fail"] = 10
stats["resources_count"] = 20
stats["findings_count"] = 22
azure_identity = "Azure Subscriptions:\n- *subscription 1: qwerty*\n- *subscription 2: asdfg*\n"
assert slack.__create_message_blocks__(
azure_identity, azure_logo, stats, args
) == [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": create_title(azure_identity, stats),
"text": slack.__create_title__(azure_identity, stats),
},
"accessory": {
"type": "image",
@@ -182,7 +211,7 @@ class TestSlackIntegration:
"elements": [
{
"type": "mrkdwn",
"text": f"Used parameters: `prowler {' '.join(sys.argv[1:])} `",
"text": f"Used parameters: `prowler {args}`",
}
],
},
@@ -221,12 +250,25 @@ class TestSlackIntegration:
},
},
]
assert create_message_blocks(gcp_identity, gcp_logo, stats) == [
def test_create_message_blocks_gcp(self):
aws_provider = set_mocked_aws_provider()
slack = Slack(SLACK_TOKEN, SLACK_CHANNEL, aws_provider)
args = "--slack"
stats = {}
stats["total_pass"] = 12
stats["total_fail"] = 10
stats["resources_count"] = 20
stats["findings_count"] = 22
gcp_identity = "GCP Project *gcp-project*"
assert slack.__create_message_blocks__(gcp_identity, gcp_logo, stats, args) == [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": create_title(gcp_identity, stats),
"text": slack.__create_title__(gcp_identity, stats),
},
"accessory": {
"type": "image",
@@ -262,7 +304,7 @@ class TestSlackIntegration:
"elements": [
{
"type": "mrkdwn",
"text": f"Used parameters: `prowler {' '.join(sys.argv[1:])} `",
"text": f"Used parameters: `prowler {args}`",
}
],
},
@@ -324,14 +366,13 @@ class TestSlackIntegration:
mocked_web_client.chat_postMessage = mock.Mock(
return_value=mocked_slack_response
)
with mock.patch(
"prowler.lib.outputs.slack.create_message_blocks",
new=mock_create_message_blocks,
), mock.patch(
"prowler.lib.outputs.slack.create_message_identity",
new=mock_create_message_identity,
), mock.patch(
"prowler.lib.outputs.slack.WebClient", new=mocked_web_client
"prowler.lib.outputs.slack.slack.WebClient", new=mocked_web_client
):
response = send_slack_message("test-token", "test-channel", {}, "provider")
aws_provider = set_mocked_aws_provider()
slack = Slack(SLACK_TOKEN, SLACK_CHANNEL, aws_provider)
stats = {}
args = "--slack"
response = slack.send(stats, args)
assert response == mocked_slack_response