Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/sentinel layer #670

Merged
merged 5 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions app/integrations/aws/lambdas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import logging
from integrations.aws.client import execute_aws_api_call, handle_aws_api_errors

logger = logging.getLogger(__name__)


@handle_aws_api_errors
def list_functions():
"""List all Lambda functions.

Returns:
list: A list of Lambda functions.
"""
return execute_aws_api_call(
"lambda", "list_functions", paginated=True, keys=["Functions"]
)


@handle_aws_api_errors
def list_layers():
"""List all Lambda layers.

Returns:
list: A list of Lambda layers.
"""
return execute_aws_api_call(
"lambda", "list_layers", paginated=True, keys=["Layers"]
)


@handle_aws_api_errors
def get_layer_version(layer_name, version_number):
"""Get a Lambda layer version.

Args:
layer_name (str): The name of the layer.
version_number (int): The version number.

Returns:
dict: The Lambda layer version.
"""
return execute_aws_api_call(
"lambda",
"get_layer_version",
LayerName=layer_name,
VersionNumber=version_number,
)
6 changes: 5 additions & 1 deletion app/modules/aws/aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from integrations.aws.organizations import get_account_id_by_name
from integrations.aws import identity_store
from integrations.slack import commands as slack_commands
from modules.aws import aws_access_requests, aws_account_health, groups, users
from modules.aws import aws_access_requests, aws_account_health, groups, users, lambdas

PREFIX = os.environ.get("PREFIX", "")
AWS_ADMIN_GROUPS = os.environ.get("AWS_ADMIN_GROUPS", "[email protected]").split(",")
Expand All @@ -33,6 +33,8 @@
\n `<operation>`: `sync`, `list`
\n `<group>`: name of the group | nom du groupe (sync only)
\n Usage: `/aws groups sync`, `/aws groups sync group-name` or/ou `/aws groups list`
\n `/aws lambdas <operation>`
\n - Manage AWS Lambda functions | Gérer les fonctions Lambda AWS
\n `/aws help | aide`
\n - Show this help text | montre le dialogue d'aide
\n `/aws health`
Expand Down Expand Up @@ -92,6 +94,8 @@ def aws_command(
users.command_handler(client, body, respond, args, logger)
case "groups":
groups.command_handler(client, body, respond, args, logger)
case "lambda" | "lambdas":
lambdas.command_handler(client, body, respond, args, logger)
case _:
respond(
f"Unknown command: `{action}`. Type `/aws help` to see a list of commands.\n"
Expand Down
74 changes: 74 additions & 0 deletions app/modules/aws/lambdas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from slack_sdk.web import WebClient
from slack_bolt import Respond

from integrations.aws import lambdas as aws_lambdas

help_text = """
\n *AWS Lambda*:
\n • `/aws lambda functions` - List all Lambda functions.
\n • `/aws lambda layers` - List all Lambda layers.
"""


def command_handler(client: WebClient, body, respond: Respond, args, logger):
"""Handle the command.

Args:
client (Slack WebClient): The Slack client.
body (dict): The request body.
respond (function): The function to respond to the request.
args (list[str]): The list of arguments.
logger (Logger): The logger.
"""

action = args.pop(0) if args else ""

match action:
case "help" | "aide":
respond(help_text)
case "functions" | "function":
request_list_functions(client, body, respond, logger)
case "layers" | "layer":
request_list_layers(client, body, respond, logger)
case _:
respond("Invalid command. Type `/aws lambda help` for more information.")


def request_list_functions(client: WebClient, body, respond: Respond, logger):
"""List all Lambda functions.

Args:
client (Slack WebClient): The Slack client.
body (dict): The request body.
respond (function): The function to respond to the request.
"""
respond("Fetching Lambda functions...")
response = aws_lambdas.list_functions()
if response:
logger.info(response)
function_string = ""
for function in response:
function_string += f"\n • {function['FunctionName']}"
respond(f"Lambda functions found:\n{function_string}")
else:
respond("Lambda functions management is currently disabled.")


def request_list_layers(client: WebClient, body, respond: Respond, logger):
"""List all Lambda layers.

Args:
client (Slack WebClient): The Slack client.
body (dict): The request body.
respond (function): The function to respond to the request.
"""
response = aws_lambdas.list_layers()
respond("Fetching Lambda layers...")
if response:
logger.info(response)
response_string = ""
for layer in response:
response_string += f"\n • {layer['LayerName']} <latest version: {layer['LatestMatchingVersion']['Version']}>"
respond(f"Lambda layers found:\n{response_string}")
else:
respond("Lambda layers management is currently disabled.")
45 changes: 45 additions & 0 deletions app/tests/integrations/aws/test_lambas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from unittest.mock import patch
from integrations.aws.lambdas import list_functions, list_layers, get_layer_version


@patch("integrations.aws.lambdas.execute_aws_api_call")
def test_list_functions(mock_execute_aws_api_call):
mock_execute_aws_api_call.return_value = [
{"FunctionName": "function1"},
{"FunctionName": "function2"},
]
result = list_functions()
mock_execute_aws_api_call.assert_called_once_with(
"lambda", "list_functions", paginated=True, keys=["Functions"]
)
assert result == [
{"FunctionName": "function1"},
{"FunctionName": "function2"},
]


@patch("integrations.aws.lambdas.execute_aws_api_call")
def test_list_layers(mock_execute_aws_api_call):
mock_execute_aws_api_call.return_value = [
{"LayerName": "layer1", "LatestMatchingVersion": {"Version": 1}},
{"LayerName": "layer2", "LatestMatchingVersion": {"Version": 23}},
]

result = list_layers()
mock_execute_aws_api_call.assert_called_once_with(
"lambda", "list_layers", paginated=True, keys=["Layers"]
)
assert result == [
{"LayerName": "layer1", "LatestMatchingVersion": {"Version": 1}},
{"LayerName": "layer2", "LatestMatchingVersion": {"Version": 23}},
]


@patch("integrations.aws.lambdas.execute_aws_api_call")
def test_get_layer_version(mock_execute_aws_api_call):
mock_execute_aws_api_call.return_value = {"LayerName": "layer1", "Version": 1}
result = get_layer_version("layer1", 1)
mock_execute_aws_api_call.assert_called_once_with(
"lambda", "get_layer_version", LayerName="layer1", VersionNumber=1
)
assert result == {"LayerName": "layer1", "Version": 1}
155 changes: 155 additions & 0 deletions app/tests/modules/aws/test_aws_lambdas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
from unittest.mock import patch, MagicMock
from modules.aws import lambdas


def test_aws_lambdas_command_handles_empty_command():
client = MagicMock()
body = MagicMock()
respond = MagicMock()
args = ""
logger = MagicMock()

lambdas.command_handler(client, body, respond, args, logger)

respond.assert_called_once_with(
"Invalid command. Type `/aws lambda help` for more information."
)
logger.info.assert_not_called()


def test_aws_lambdas_command_handles_help_command():
client = MagicMock()
body = MagicMock()
respond = MagicMock()
args = ["help"]
logger = MagicMock()

lambdas.command_handler(client, body, respond, args, logger)

respond.assert_called_once_with(lambdas.help_text)
logger.info.assert_not_called()


@patch("integrations.aws.lambdas.list_functions")
def test_aws_lambdas_command_handles_functions_command(mock_list_functions):
client = MagicMock()
body = MagicMock()
respond = MagicMock()
args = ["functions"]
logger = MagicMock()

mock_list_functions.return_value = [{"FunctionName": "test-function"}]

lambdas.command_handler(client, body, respond, args, logger)

respond.assert_any_call("Fetching Lambda functions...")
respond.assert_any_call("Lambda functions found:\n\n • test-function")
logger.info.assert_called_once_with([{"FunctionName": "test-function"}])


@patch("integrations.aws.lambdas.list_layers")
def test_aws_lambdas_command_handles_layers_command(mock_list_layers):
client = MagicMock()
body = MagicMock()
respond = MagicMock()
args = ["layers"]
logger = MagicMock()

mock_list_layers.return_value = [
{
"LayerName": "aws-sentinel-connector-layer",
"LatestMatchingVersion": {"Version": 163},
}
]

lambdas.command_handler(client, body, respond, args, logger)

respond.assert_any_call("Fetching Lambda layers...")
respond.assert_any_call(
"Lambda layers found:\n\n • aws-sentinel-connector-layer <latest version: 163>"
)
logger.info.assert_called_once_with(
[
{
"LayerName": "aws-sentinel-connector-layer",
"LatestMatchingVersion": {"Version": 163},
}
]
)


@patch("integrations.aws.lambdas.list_functions")
def test_request_list_functions_handles_empty_response(mock_list_functions):
client = MagicMock()
body = MagicMock()
respond = MagicMock()
logger = MagicMock()

mock_list_functions.return_value = []

lambdas.request_list_functions(client, body, respond, logger)

respond.assert_any_call("Fetching Lambda functions...")
logger.info.assert_not_called()


@patch("integrations.aws.lambdas.list_functions")
def test_request_list_functions_handles_non_empty_response(mock_list_functions):
client = MagicMock()
body = MagicMock()
respond = MagicMock()
logger = MagicMock()

mock_list_functions.return_value = [{"FunctionName": "test-function"}]

lambdas.request_list_functions(client, body, respond, logger)

respond.assert_any_call("Fetching Lambda functions...")
respond.assert_any_call("Lambda functions found:\n\n • test-function")
logger.info.assert_called_once_with([{"FunctionName": "test-function"}])


@patch("integrations.aws.lambdas.list_layers")
def test_request_list_layers_handles_empty_response(mock_list_layers):
client = MagicMock()
body = MagicMock()
respond = MagicMock()
logger = MagicMock()

mock_list_layers.return_value = []

lambdas.request_list_layers(client, body, respond, logger)

respond.assert_any_call("Fetching Lambda layers...")
respond.assert_any_call("Lambda layers management is currently disabled.")
logger.info.assert_not_called()


@patch("integrations.aws.lambdas.list_layers")
def test_request_list_layers_handles_non_empty_response(mock_list_layers):
client = MagicMock()
body = MagicMock()
respond = MagicMock()
logger = MagicMock()

mock_list_layers.return_value = [
{
"LayerName": "aws-sentinel-connector-layer",
"LatestMatchingVersion": {"Version": 163},
}
]

lambdas.request_list_layers(client, body, respond, logger)

respond.assert_any_call("Fetching Lambda layers...")
respond.assert_any_call(
"Lambda layers found:\n\n • aws-sentinel-connector-layer <latest version: 163>"
)
logger.info.assert_called_once_with(
[
{
"LayerName": "aws-sentinel-connector-layer",
"LatestMatchingVersion": {"Version": 163},
}
]
)
Loading