diff --git a/.env.example b/.env.example index 31d4df03..c3586b5c 100644 --- a/.env.example +++ b/.env.example @@ -28,6 +28,26 @@ # ASANA_TEST_PROJECT_GID= # ASANA_TEST_TASK_GID= +# -- AWS -- +# AWS_ACCESS_KEY_ID= +# AWS_SECRET_ACCESS_KEY= +# AWS_REGION=us-east-1 +# Optional - STS temporary credentials +# AWS_SESSION_TOKEN= +# Optional - needed for GuardDuty tests +# AWS_DETECTOR_ID= +# Optional - needed for CloudTrail tests +# AWS_TRAIL_NAME= +# Optional - needed for CloudWatch Logs tests +# AWS_LOG_GROUP= +# AWS_LOG_STREAM= +# Optional - needed for CloudWatch alarm tests +# AWS_ALARM_NAME= +# Optional - needed for Security Hub finding tests +# AWS_FINDING_ARN= + + + # -- Bitly -- # BITLY_ACCESS_TOKEN= diff --git a/aws/config.json b/aws/config.json index 3cc5c8f3..6437f51a 100644 --- a/aws/config.json +++ b/aws/config.json @@ -1,7 +1,7 @@ { "name": "aws", "display_name": "Amazon Web Services", - "version": "1.0.0", + "version": "2.0.0", "description": "AWS security and monitoring integration for Security Hub, GuardDuty, CloudWatch, and CloudTrail", "entry_point": "aws.py", "auth": { @@ -27,6 +27,13 @@ "format": "text", "label": "AWS Region", "help_text": "AWS region (e.g. us-east-1, eu-west-1)" + }, + "aws_session_token": { + "type": "string", + "format": "password", + "label": "AWS Session Token", + "help_text": "Optional session token for temporary AWS credentials (e.g. from STS AssumeRole)", + "required": false } } } @@ -64,20 +71,11 @@ "description": "List of Security Hub findings" }, "next_token": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "Pagination token for the next page of results" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": ["string", "null"], - "description": "Error message if the operation failed" - }, - "error_code": { - "type": ["string", "null"], - "description": "AWS-specific error code if the operation failed (e.g. AccessDeniedException, ThrottlingException)" } } } @@ -93,7 +91,9 @@ "description": "The ARN of the Security Hub finding to retrieve" } }, - "required": ["finding_arn"] + "required": [ + "finding_arn" + ] }, "output_schema": { "type": "object", @@ -101,18 +101,6 @@ "finding": { "type": "object", "description": "The Security Hub finding details" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": ["string", "null"], - "description": "Error message if the operation failed" - }, - "error_code": { - "type": ["string", "null"], - "description": "AWS-specific error code if the operation failed (e.g. AccessDeniedException, ThrottlingException)" } } } @@ -133,14 +121,22 @@ "workflow_status": { "type": "string", "description": "The new workflow status to set", - "enum": ["NEW", "NOTIFIED", "RESOLVED", "SUPPRESSED"] + "enum": [ + "NEW", + "NOTIFIED", + "RESOLVED", + "SUPPRESSED" + ] }, "note": { "type": "string", "description": "Optional note to add to the findings" } }, - "required": ["finding_arns", "workflow_status"] + "required": [ + "finding_arns", + "workflow_status" + ] }, "output_schema": { "type": "object", @@ -152,18 +148,6 @@ "unprocessed_findings": { "type": "array", "description": "List of finding ARNs that could not be updated" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": ["string", "null"], - "description": "Error message if the operation failed" - }, - "error_code": { - "type": ["string", "null"], - "description": "AWS-specific error code if the operation failed (e.g. AccessDeniedException, ThrottlingException)" } } } @@ -202,20 +186,11 @@ "description": "List of Security Hub insights" }, "next_token": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "Pagination token for the next page of results" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": ["string", "null"], - "description": "Error message if the operation failed" - }, - "error_code": { - "type": ["string", "null"], - "description": "AWS-specific error code if the operation failed (e.g. AccessDeniedException, ThrottlingException)" } } } @@ -250,20 +225,11 @@ } }, "next_token": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "Pagination token for the next page of results" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": ["string", "null"], - "description": "Error message if the operation failed" - }, - "error_code": { - "type": ["string", "null"], - "description": "AWS-specific error code if the operation failed (e.g. AccessDeniedException, ThrottlingException)" } } } @@ -297,7 +263,9 @@ "description": "Pagination token from a previous request" } }, - "required": ["detector_id"] + "required": [ + "detector_id" + ] }, "output_schema": { "type": "object", @@ -310,20 +278,11 @@ } }, "next_token": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "Pagination token for the next page of results" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": ["string", "null"], - "description": "Error message if the operation failed" - }, - "error_code": { - "type": ["string", "null"], - "description": "AWS-specific error code if the operation failed (e.g. AccessDeniedException, ThrottlingException)" } } } @@ -346,7 +305,10 @@ } } }, - "required": ["detector_id", "finding_ids"] + "required": [ + "detector_id", + "finding_ids" + ] }, "output_schema": { "type": "object", @@ -354,18 +316,6 @@ "findings": { "type": "array", "description": "List of detailed GuardDuty findings" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": ["string", "null"], - "description": "Error message if the operation failed" - }, - "error_code": { - "type": ["string", "null"], - "description": "AWS-specific error code if the operation failed (e.g. AccessDeniedException, ThrottlingException)" } } } @@ -388,7 +338,10 @@ } } }, - "required": ["detector_id", "finding_ids"] + "required": [ + "detector_id", + "finding_ids" + ] }, "output_schema": { "type": "object", @@ -396,18 +349,6 @@ "success": { "type": "boolean", "description": "Whether the findings were successfully archived" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": ["string", "null"], - "description": "Error message if the operation failed" - }, - "error_code": { - "type": ["string", "null"], - "description": "AWS-specific error code if the operation failed (e.g. AccessDeniedException, ThrottlingException)" } } } @@ -448,20 +389,11 @@ "description": "List of CloudWatch metrics with namespace, name, and dimensions" }, "next_token": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "Pagination token for the next page of results" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": ["string", "null"], - "description": "Error message if the operation failed" - }, - "error_code": { - "type": ["string", "null"], - "description": "AWS-specific error code if the operation failed (e.g. AccessDeniedException, ThrottlingException)" } } } @@ -488,7 +420,11 @@ "description": "End of the time range in ISO 8601 format (e.g. 2024-01-02T00:00:00Z)" } }, - "required": ["metric_data_queries", "start_time", "end_time"] + "required": [ + "metric_data_queries", + "start_time", + "end_time" + ] }, "output_schema": { "type": "object", @@ -496,18 +432,6 @@ "metric_data_results": { "type": "array", "description": "List of metric data results with timestamps and values" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": ["string", "null"], - "description": "Error message if the operation failed" - }, - "error_code": { - "type": ["string", "null"], - "description": "AWS-specific error code if the operation failed (e.g. AccessDeniedException, ThrottlingException)" } } } @@ -532,7 +456,11 @@ "state_value": { "type": "string", "description": "Filter alarms by state", - "enum": ["OK", "ALARM", "INSUFFICIENT_DATA"] + "enum": [ + "OK", + "ALARM", + "INSUFFICIENT_DATA" + ] }, "max_records": { "type": "integer", @@ -559,20 +487,11 @@ "description": "List of composite alarms" }, "next_token": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "Pagination token for the next page of results" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": ["string", "null"], - "description": "Error message if the operation failed" - }, - "error_code": { - "type": ["string", "null"], - "description": "AWS-specific error code if the operation failed (e.g. AccessDeniedException, ThrottlingException)" } } } @@ -597,7 +516,11 @@ "history_item_type": { "type": "string", "description": "Filter by history item type", - "enum": ["ConfigurationUpdate", "StateUpdate", "Action"] + "enum": [ + "ConfigurationUpdate", + "StateUpdate", + "Action" + ] }, "start_date": { "type": "string", @@ -628,20 +551,11 @@ "description": "List of alarm history items with timestamp, type, and summary" }, "next_token": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "Pagination token for the next page of results" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": ["string", "null"], - "description": "Error message if the operation failed" - }, - "error_code": { - "type": ["string", "null"], - "description": "AWS-specific error code if the operation failed (e.g. AccessDeniedException, ThrottlingException)" } } } @@ -659,14 +573,22 @@ "state_value": { "type": "string", "description": "The state value to set", - "enum": ["OK", "ALARM", "INSUFFICIENT_DATA"] + "enum": [ + "OK", + "ALARM", + "INSUFFICIENT_DATA" + ] }, "state_reason": { "type": "string", "description": "A human-readable reason for the state change" } }, - "required": ["alarm_name", "state_value", "state_reason"] + "required": [ + "alarm_name", + "state_value", + "state_reason" + ] }, "output_schema": { "type": "object", @@ -674,18 +596,6 @@ "success": { "type": "boolean", "description": "Whether the alarm state was successfully set" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": ["string", "null"], - "description": "Error message if the operation failed" - }, - "error_code": { - "type": ["string", "null"], - "description": "AWS-specific error code if the operation failed (e.g. AccessDeniedException, ThrottlingException)" } } } @@ -722,20 +632,11 @@ "description": "List of log groups with name, ARN, creation time, and stored bytes" }, "next_token": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "Pagination token for the next page of results" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": ["string", "null"], - "description": "Error message if the operation failed" - }, - "error_code": { - "type": ["string", "null"], - "description": "AWS-specific error code if the operation failed (e.g. AccessDeniedException, ThrottlingException)" } } } @@ -781,7 +682,9 @@ "description": "Pagination token from a previous request" } }, - "required": ["log_group_name"] + "required": [ + "log_group_name" + ] }, "output_schema": { "type": "object", @@ -795,20 +698,11 @@ "description": "List of log streams that were searched" }, "next_token": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "Pagination token for the next page of results" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": ["string", "null"], - "description": "Error message if the operation failed" - }, - "error_code": { - "type": ["string", "null"], - "description": "AWS-specific error code if the operation failed (e.g. AccessDeniedException, ThrottlingException)" } } } @@ -852,7 +746,10 @@ "description": "Pagination token from a previous request" } }, - "required": ["log_group_name", "log_stream_name"] + "required": [ + "log_group_name", + "log_stream_name" + ] }, "output_schema": { "type": "object", @@ -862,24 +759,18 @@ "description": "List of log events with timestamp and message" }, "next_forward_token": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "Token for fetching the next set of events going forward in time" }, "next_backward_token": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "Token for fetching the next set of events going backward in time" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": ["string", "null"], - "description": "Error message if the operation failed" - }, - "error_code": { - "type": ["string", "null"], - "description": "AWS-specific error code if the operation failed (e.g. AccessDeniedException, ThrottlingException)" } } } @@ -927,20 +818,11 @@ "description": "List of CloudTrail events with event name, time, user, and resources" }, "next_token": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "Pagination token for the next page of results" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": ["string", "null"], - "description": "Error message if the operation failed" - }, - "error_code": { - "type": ["string", "null"], - "description": "AWS-specific error code if the operation failed (e.g. AccessDeniedException, ThrottlingException)" } } } @@ -972,18 +854,6 @@ "trails": { "type": "array", "description": "List of CloudTrail trail configurations" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": ["string", "null"], - "description": "Error message if the operation failed" - }, - "error_code": { - "type": ["string", "null"], - "description": "AWS-specific error code if the operation failed (e.g. AccessDeniedException, ThrottlingException)" } } } @@ -999,7 +869,9 @@ "description": "The trail name or ARN to get status for" } }, - "required": ["trail_name"] + "required": [ + "trail_name" + ] }, "output_schema": { "type": "object", @@ -1007,18 +879,6 @@ "trail_status": { "type": "object", "description": "Trail status including logging state, latest delivery time, and any delivery errors" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": ["string", "null"], - "description": "Error message if the operation failed" - }, - "error_code": { - "type": ["string", "null"], - "description": "AWS-specific error code if the operation failed (e.g. AccessDeniedException, ThrottlingException)" } } } @@ -1034,7 +894,9 @@ "description": "The trail name or ARN to get event selectors for" } }, - "required": ["trail_name"] + "required": [ + "trail_name" + ] }, "output_schema": { "type": "object", @@ -1050,21 +912,9 @@ "advanced_event_selectors": { "type": "array", "description": "List of advanced event selectors configured on the trail" - }, - "result": { - "type": "boolean", - "description": "Whether the operation was successful" - }, - "error": { - "type": ["string", "null"], - "description": "Error message if the operation failed" - }, - "error_code": { - "type": ["string", "null"], - "description": "AWS-specific error code if the operation failed (e.g. AccessDeniedException, ThrottlingException)" } } } } } -} +} \ No newline at end of file diff --git a/aws/helpers.py b/aws/helpers.py index 6c93c6a9..072757d0 100644 --- a/aws/helpers.py +++ b/aws/helpers.py @@ -5,20 +5,22 @@ from typing import Any, Dict import boto3 -from autohive_integrations_sdk import ActionResult, ExecutionContext +from autohive_integrations_sdk import ActionError, ActionResult, ExecutionContext def create_boto3_client(context: ExecutionContext, service_name: str): - credentials = context.auth.get("credentials", {}) - access_key = credentials.get("aws_access_key_id") - secret_key = credentials.get("aws_secret_access_key") + creds = context.auth.get("credentials") or context.auth + access_key = creds.get("aws_access_key_id") + secret_key = creds.get("aws_secret_access_key") if not access_key or not secret_key: raise ValueError("AWS credentials are missing: aws_access_key_id and aws_secret_access_key are required") + session_token = creds.get("aws_session_token") return boto3.client( service_name, aws_access_key_id=access_key, aws_secret_access_key=secret_key, - region_name=credentials.get("aws_region", "us-east-1"), + region_name=creds.get("aws_region", "us-east-1"), + aws_session_token=session_token or None, ) @@ -42,13 +44,16 @@ def serialize_response(obj: Any) -> Any: def success_result(data: Dict[str, Any]) -> ActionResult: - return ActionResult(data={"result": True, **serialize_response(data)}) + return ActionResult(data=serialize_response(data), cost_usd=0.0) -def error_result(e: Exception) -> ActionResult: +def error_result(e: Exception) -> ActionError: error_msg = str(e) - error_code = "" if hasattr(e, "response"): error_code = e.response.get("Error", {}).get("Code", "") - error_msg = e.response.get("Error", {}).get("Message", error_msg) - return ActionResult(data={"result": False, "error": error_msg, "error_code": error_code}) + api_msg = e.response.get("Error", {}).get("Message", "") + if error_code and api_msg: + error_msg = f"{error_code}: {api_msg}" + elif api_msg: + error_msg = api_msg + return ActionError(message=error_msg) diff --git a/aws/requirements.txt b/aws/requirements.txt index 96b5aede..5682c54b 100644 --- a/aws/requirements.txt +++ b/aws/requirements.txt @@ -1,2 +1,2 @@ -autohive-integrations-sdk~=1.0.2 +autohive-integrations-sdk~=2.0.0 boto3 diff --git a/aws/tests/conftest.py b/aws/tests/conftest.py new file mode 100644 index 00000000..18cfb091 --- /dev/null +++ b/aws/tests/conftest.py @@ -0,0 +1,14 @@ +import pytest +from unittest.mock import MagicMock +from autohive_integrations_sdk import ExecutionContext + + +@pytest.fixture +def mock_context(): + ctx = MagicMock(spec=ExecutionContext) + ctx.auth = { + "aws_access_key_id": "test_access_key", + "aws_secret_access_key": "test_secret_key", # nosec B105 + "aws_region": "us-east-1", + } + return ctx diff --git a/aws/tests/context.py b/aws/tests/context.py deleted file mode 100644 index 49f11d51..00000000 --- a/aws/tests/context.py +++ /dev/null @@ -1,6 +0,0 @@ -import os -import sys - -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) - -from aws import aws as integration # noqa: F401 diff --git a/aws/tests/test_aws.py b/aws/tests/test_aws.py deleted file mode 100644 index 619f2459..00000000 --- a/aws/tests/test_aws.py +++ /dev/null @@ -1,422 +0,0 @@ -""" -AWS Integration Tests - -Tests all 20 AWS actions across Security Hub, GuardDuty, -CloudWatch, CloudWatch Logs, and CloudTrail. - -To run these tests: -1. Update the credentials below with valid AWS access keys -2. Run: python tests/test_aws.py -""" - -import asyncio -from context import integration -from autohive_integrations_sdk import ExecutionContext - -TEST_AUTH = { - "credentials": { - "aws_access_key_id": "YOUR_ACCESS_KEY_ID", - "aws_secret_access_key": "YOUR_SECRET_ACCESS_KEY", # nosec B105 - "aws_region": "us-east-1", - } -} - - -# ============================================================================= -# Security Hub Actions -# ============================================================================= - - -async def test_get_findings(): - """Test retrieving Security Hub findings.""" - print("\n=== Testing get_findings ===") - inputs = {"max_results": 10} - async with ExecutionContext(auth=TEST_AUTH) as context: - try: - result = await integration.execute_action("get_findings", inputs, context) - print(f"Result: {result}") - return result - except Exception as e: - print(f"Error: {e}") - return None - - -async def test_get_finding_details(): - """Test retrieving details for a specific Security Hub finding.""" - print("\n=== Testing get_finding_details ===") - inputs = {"finding_arn": "arn:aws:securityhub:us-east-1:123456789012:finding/example"} - async with ExecutionContext(auth=TEST_AUTH) as context: - try: - result = await integration.execute_action("get_finding_details", inputs, context) - print(f"Result: {result}") - return result - except Exception as e: - print(f"Error: {e}") - return None - - -async def test_update_finding_workflow(): - """Test updating the workflow status of Security Hub findings.""" - print("\n=== Testing update_finding_workflow ===") - inputs = { - "finding_arns": ["arn:aws:securityhub:us-east-1:123456789012:finding/example"], - "workflow_status": "RESOLVED", - "note": "Resolved via Autohive", - } - async with ExecutionContext(auth=TEST_AUTH) as context: - try: - result = await integration.execute_action("update_finding_workflow", inputs, context) - print(f"Result: {result}") - return result - except Exception as e: - print(f"Error: {e}") - return None - - -async def test_get_insights(): - """Test retrieving Security Hub insights.""" - print("\n=== Testing get_insights ===") - inputs = {"max_results": 5} - async with ExecutionContext(auth=TEST_AUTH) as context: - try: - result = await integration.execute_action("get_insights", inputs, context) - print(f"Result: {result}") - return result - except Exception as e: - print(f"Error: {e}") - return None - - -# ============================================================================= -# GuardDuty Actions -# ============================================================================= - - -async def test_list_detectors(): - """Test listing GuardDuty detectors.""" - print("\n=== Testing list_detectors ===") - inputs = {"max_results": 10} - async with ExecutionContext(auth=TEST_AUTH) as context: - try: - result = await integration.execute_action("list_detectors", inputs, context) - print(f"Result: {result}") - return result - except Exception as e: - print(f"Error: {e}") - return None - - -async def test_list_guardduty_findings(): - """Test listing GuardDuty findings for a detector.""" - print("\n=== Testing list_guardduty_findings ===") - inputs = {"detector_id": "YOUR_DETECTOR_ID", "max_results": 10} - async with ExecutionContext(auth=TEST_AUTH) as context: - try: - result = await integration.execute_action("list_guardduty_findings", inputs, context) - print(f"Result: {result}") - return result - except Exception as e: - print(f"Error: {e}") - return None - - -async def test_get_guardduty_finding_details(): - """Test retrieving details for specific GuardDuty findings.""" - print("\n=== Testing get_guardduty_finding_details ===") - inputs = {"detector_id": "YOUR_DETECTOR_ID", "finding_ids": ["example-finding-id"]} - async with ExecutionContext(auth=TEST_AUTH) as context: - try: - result = await integration.execute_action("get_guardduty_finding_details", inputs, context) - print(f"Result: {result}") - return result - except Exception as e: - print(f"Error: {e}") - return None - - -async def test_archive_findings(): - """Test archiving GuardDuty findings.""" - print("\n=== Testing archive_findings ===") - inputs = {"detector_id": "YOUR_DETECTOR_ID", "finding_ids": ["example-finding-id"]} - async with ExecutionContext(auth=TEST_AUTH) as context: - try: - result = await integration.execute_action("archive_findings", inputs, context) - print(f"Result: {result}") - return result - except Exception as e: - print(f"Error: {e}") - return None - - -# ============================================================================= -# CloudWatch Actions -# ============================================================================= - - -async def test_list_metrics(): - """Test listing CloudWatch metrics.""" - print("\n=== Testing list_metrics ===") - inputs = {"namespace": "AWS/EC2"} - async with ExecutionContext(auth=TEST_AUTH) as context: - try: - result = await integration.execute_action("list_metrics", inputs, context) - print(f"Result: {result}") - return result - except Exception as e: - print(f"Error: {e}") - return None - - -async def test_get_metric_data(): - """Test retrieving CloudWatch metric data.""" - print("\n=== Testing get_metric_data ===") - inputs = { - "metric_data_queries": [ - { - "Id": "m1", - "MetricStat": { - "Metric": {"Namespace": "AWS/EC2", "MetricName": "CPUUtilization"}, - "Period": 300, - "Stat": "Average", - }, - } - ], - "start_time": "2024-01-01T00:00:00Z", - "end_time": "2024-01-02T00:00:00Z", - } - async with ExecutionContext(auth=TEST_AUTH) as context: - try: - result = await integration.execute_action("get_metric_data", inputs, context) - print(f"Result: {result}") - return result - except Exception as e: - print(f"Error: {e}") - return None - - -async def test_describe_alarms(): - """Test describing CloudWatch alarms.""" - print("\n=== Testing describe_alarms ===") - inputs = {"max_records": 10} - async with ExecutionContext(auth=TEST_AUTH) as context: - try: - result = await integration.execute_action("describe_alarms", inputs, context) - print(f"Result: {result}") - return result - except Exception as e: - print(f"Error: {e}") - return None - - -async def test_get_alarm_history(): - """Test retrieving CloudWatch alarm history.""" - print("\n=== Testing get_alarm_history ===") - inputs = {"max_records": 10} - async with ExecutionContext(auth=TEST_AUTH) as context: - try: - result = await integration.execute_action("get_alarm_history", inputs, context) - print(f"Result: {result}") - return result - except Exception as e: - print(f"Error: {e}") - return None - - -async def test_set_alarm_state(): - """Test setting the state of a CloudWatch alarm.""" - print("\n=== Testing set_alarm_state ===") - inputs = { - "alarm_name": "test-alarm", - "state_value": "OK", - "state_reason": "Testing via Autohive", - } - async with ExecutionContext(auth=TEST_AUTH) as context: - try: - result = await integration.execute_action("set_alarm_state", inputs, context) - print(f"Result: {result}") - return result - except Exception as e: - print(f"Error: {e}") - return None - - -# ============================================================================= -# CloudWatch Logs Actions -# ============================================================================= - - -async def test_describe_log_groups(): - """Test describing CloudWatch log groups.""" - print("\n=== Testing describe_log_groups ===") - inputs = {"limit": 10} - async with ExecutionContext(auth=TEST_AUTH) as context: - try: - result = await integration.execute_action("describe_log_groups", inputs, context) - print(f"Result: {result}") - return result - except Exception as e: - print(f"Error: {e}") - return None - - -async def test_filter_log_events(): - """Test filtering CloudWatch log events.""" - print("\n=== Testing filter_log_events ===") - inputs = {"log_group_name": "/aws/lambda/test-function", "limit": 10} - async with ExecutionContext(auth=TEST_AUTH) as context: - try: - result = await integration.execute_action("filter_log_events", inputs, context) - print(f"Result: {result}") - return result - except Exception as e: - print(f"Error: {e}") - return None - - -async def test_get_log_events(): - """Test retrieving CloudWatch log events from a specific stream.""" - print("\n=== Testing get_log_events ===") - inputs = { - "log_group_name": "/aws/lambda/test-function", - "log_stream_name": "test-stream", - "limit": 10, - } - async with ExecutionContext(auth=TEST_AUTH) as context: - try: - result = await integration.execute_action("get_log_events", inputs, context) - print(f"Result: {result}") - return result - except Exception as e: - print(f"Error: {e}") - return None - - -# ============================================================================= -# CloudTrail Actions -# ============================================================================= - - -async def test_lookup_events(): - """Test looking up CloudTrail events.""" - print("\n=== Testing lookup_events ===") - inputs = {"max_results": 10} - async with ExecutionContext(auth=TEST_AUTH) as context: - try: - result = await integration.execute_action("lookup_events", inputs, context) - print(f"Result: {result}") - return result - except Exception as e: - print(f"Error: {e}") - return None - - -async def test_describe_trails(): - """Test describing CloudTrail trails.""" - print("\n=== Testing describe_trails ===") - inputs = {} - async with ExecutionContext(auth=TEST_AUTH) as context: - try: - result = await integration.execute_action("describe_trails", inputs, context) - print(f"Result: {result}") - return result - except Exception as e: - print(f"Error: {e}") - return None - - -async def test_get_trail_status(): - """Test retrieving the status of a CloudTrail trail.""" - print("\n=== Testing get_trail_status ===") - inputs = {"trail_name": "management-events"} - async with ExecutionContext(auth=TEST_AUTH) as context: - try: - result = await integration.execute_action("get_trail_status", inputs, context) - print(f"Result: {result}") - return result - except Exception as e: - print(f"Error: {e}") - return None - - -async def test_get_event_selectors(): - """Test retrieving event selectors for a CloudTrail trail.""" - print("\n=== Testing get_event_selectors ===") - inputs = {"trail_name": "management-events"} - async with ExecutionContext(auth=TEST_AUTH) as context: - try: - result = await integration.execute_action("get_event_selectors", inputs, context) - print(f"Result: {result}") - return result - except Exception as e: - print(f"Error: {e}") - return None - - -# ============================================================================= -# Run All Tests -# ============================================================================= - - -async def run_all_tests(): - """Run all 20 AWS integration tests and print a summary.""" - print("=" * 60) - print("AWS Integration Tests") - print("=" * 60) - - tests = [ - # Security Hub - ("get_findings", test_get_findings), - ("get_finding_details", test_get_finding_details), - ("update_finding_workflow", test_update_finding_workflow), - ("get_insights", test_get_insights), - # GuardDuty - ("list_detectors", test_list_detectors), - ("list_guardduty_findings", test_list_guardduty_findings), - ("get_guardduty_finding_details", test_get_guardduty_finding_details), - ("archive_findings", test_archive_findings), - # CloudWatch - ("list_metrics", test_list_metrics), - ("get_metric_data", test_get_metric_data), - ("describe_alarms", test_describe_alarms), - ("get_alarm_history", test_get_alarm_history), - ("set_alarm_state", test_set_alarm_state), - # CloudWatch Logs - ("describe_log_groups", test_describe_log_groups), - ("filter_log_events", test_filter_log_events), - ("get_log_events", test_get_log_events), - # CloudTrail - ("lookup_events", test_lookup_events), - ("describe_trails", test_describe_trails), - ("get_trail_status", test_get_trail_status), - ("get_event_selectors", test_get_event_selectors), - ] - - results = [] - for name, test_func in tests: - try: - result = await test_func() - if result is not None: - results.append((name, "PASSED")) - else: - results.append((name, "FAILED: returned None")) - except Exception as e: - results.append((name, f"FAILED: {e}")) - print(f"Error in {name}: {e}") - - print("\n" + "=" * 60) - print("Test Results Summary") - print("=" * 60) - passed = 0 - failed = 0 - for name, status in results: - tag = "PASS" if status == "PASSED" else "FAIL" - print(f"[{tag}] {name}: {status}") - if status == "PASSED": - passed += 1 - else: - failed += 1 - print(f"\nTotal: {passed + failed} | Passed: {passed} | Failed: {failed}") - - -if __name__ == "__main__": - asyncio.run(run_all_tests()) diff --git a/aws/tests/test_aws_integration.py b/aws/tests/test_aws_integration.py new file mode 100644 index 00000000..8afbb215 --- /dev/null +++ b/aws/tests/test_aws_integration.py @@ -0,0 +1,325 @@ +""" +End-to-end integration tests for the AWS integration. + +Requires credentials in environment variables or a .env file at the repo root: + AWS_ACCESS_KEY_ID -- AWS access key + AWS_SECRET_ACCESS_KEY -- AWS secret access key + AWS_REGION -- AWS region (default: us-east-1) + +Optional — needed for service-specific tests: + AWS_DETECTOR_ID -- GuardDuty detector ID (for list/get/archive finding tests) + AWS_TRAIL_NAME -- CloudTrail trail name (for get_trail_status, get_event_selectors) + AWS_LOG_GROUP -- CloudWatch Logs group name (for filter_log_events) + AWS_LOG_STREAM -- CloudWatch Logs stream name (for get_log_events; requires AWS_LOG_GROUP) + AWS_ALARM_NAME -- CloudWatch alarm name (for get_alarm_history, set_alarm_state) + AWS_FINDING_ARN -- Security Hub finding ARN (for get_finding_details, update_finding_workflow) + +Run safely (read-only): + pytest aws/tests/test_aws_integration.py -m "integration and not destructive" + +Run destructive (mutates real data): + pytest aws/tests/test_aws_integration.py -m "integration and destructive" + +Never runs in CI — the default pytest marker filter (-m unit) excludes these, +and the file naming (test_*_integration.py) is not matched by python_files. +""" + +import os +import sys + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +import pytest +from autohive_integrations_sdk import ResultType +from aws import aws # noqa: E402 + +AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID", "") +AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY", "") +AWS_REGION = os.getenv("AWS_REGION", "us-east-1") +AWS_DETECTOR_ID = os.getenv("AWS_DETECTOR_ID", "") +AWS_TRAIL_NAME = os.getenv("AWS_TRAIL_NAME", "") +AWS_LOG_GROUP = os.getenv("AWS_LOG_GROUP", "") +AWS_LOG_STREAM = os.getenv("AWS_LOG_STREAM", "") +AWS_ALARM_NAME = os.getenv("AWS_ALARM_NAME", "") +AWS_FINDING_ARN = os.getenv("AWS_FINDING_ARN", "") +AWS_SESSION_TOKEN = os.getenv("AWS_SESSION_TOKEN", "") + +pytestmark = [ + pytest.mark.integration, + pytest.mark.skipif( + not AWS_ACCESS_KEY_ID or not AWS_SECRET_ACCESS_KEY, + reason="AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY required", + ), +] + + +@pytest.fixture +def live_context(): + auth = { + "aws_access_key_id": AWS_ACCESS_KEY_ID, + "aws_secret_access_key": AWS_SECRET_ACCESS_KEY, + "aws_region": AWS_REGION, + } + if AWS_SESSION_TOKEN: + auth["aws_session_token"] = AWS_SESSION_TOKEN + + class _Ctx: + pass + + ctx = _Ctx() + ctx.auth = auth + return ctx + + +# ---- Security Hub ---- + + +@pytest.mark.asyncio +async def test_get_findings(live_context): + result = await aws.execute_action("get_findings", {"max_results": 5}, live_context) + assert result.type == ResultType.ACTION, result.result.message + assert "findings" in result.result.data + + +@pytest.mark.asyncio +async def test_get_finding_details(live_context): + finding_arn = AWS_FINDING_ARN + if not finding_arn: + list_result = await aws.execute_action("get_findings", {"max_results": 1}, live_context) + findings = list_result.result.data.get("findings", []) + if not findings: + pytest.skip("No Security Hub findings available") + finding_arn = findings[0].get("Id", "") + result = await aws.execute_action("get_finding_details", {"finding_arn": finding_arn}, live_context) + assert result.type == ResultType.ACTION, result.result.message + assert "finding" in result.result.data + + +@pytest.mark.asyncio +async def test_get_insights(live_context): + result = await aws.execute_action("get_insights", {}, live_context) + assert result.type == ResultType.ACTION, result.result.message + assert "insights" in result.result.data + + +@pytest.mark.destructive +@pytest.mark.asyncio +async def test_update_finding_workflow(live_context): + finding_arn = AWS_FINDING_ARN + if not finding_arn: + pytest.skip("AWS_FINDING_ARN required for update_finding_workflow") + result = await aws.execute_action( + "update_finding_workflow", + {"finding_arns": [finding_arn], "workflow_status": "NOTIFIED"}, + live_context, + ) + assert result.type == ResultType.ACTION, result.result.message + + +# ---- GuardDuty ---- + + +@pytest.mark.asyncio +async def test_list_detectors(live_context): + result = await aws.execute_action("list_detectors", {}, live_context) + assert result.type == ResultType.ACTION, result.result.message + assert "detector_ids" in result.result.data + + +@pytest.mark.asyncio +async def test_list_guardduty_findings(live_context): + detector_id = AWS_DETECTOR_ID + if not detector_id: + list_result = await aws.execute_action("list_detectors", {}, live_context) + ids = list_result.result.data.get("detector_ids", []) + if not ids: + pytest.skip("No GuardDuty detectors found") + detector_id = ids[0] + result = await aws.execute_action("list_guardduty_findings", {"detector_id": detector_id}, live_context) + assert result.type == ResultType.ACTION, result.result.message + assert "finding_ids" in result.result.data + + +@pytest.mark.asyncio +async def test_get_guardduty_finding_details(live_context): + detector_id = AWS_DETECTOR_ID + if not detector_id: + list_result = await aws.execute_action("list_detectors", {}, live_context) + ids = list_result.result.data.get("detector_ids", []) + if not ids: + pytest.skip("No GuardDuty detectors found") + detector_id = ids[0] + finding_result = await aws.execute_action("list_guardduty_findings", {"detector_id": detector_id}, live_context) + finding_ids = finding_result.result.data.get("finding_ids", []) + if not finding_ids: + pytest.skip("No GuardDuty findings found") + result = await aws.execute_action( + "get_guardduty_finding_details", + {"detector_id": detector_id, "finding_ids": finding_ids[:1]}, + live_context, + ) + assert result.type == ResultType.ACTION, result.result.message + assert "findings" in result.result.data + + +@pytest.mark.destructive +@pytest.mark.asyncio +async def test_archive_findings(live_context): + detector_id = AWS_DETECTOR_ID + if not detector_id: + pytest.skip("AWS_DETECTOR_ID required for archive_findings") + finding_result = await aws.execute_action( + "list_guardduty_findings", + {"detector_id": detector_id}, + live_context, + ) + finding_ids = finding_result.result.data.get("finding_ids", []) + if not finding_ids: + pytest.skip("No GuardDuty findings to archive") + result = await aws.execute_action( + "archive_findings", + {"detector_id": detector_id, "finding_ids": finding_ids[:1]}, + live_context, + ) + assert result.type == ResultType.ACTION, result.result.message + + +# ---- CloudWatch ---- + + +@pytest.mark.asyncio +async def test_list_metrics(live_context): + result = await aws.execute_action("list_metrics", {"namespace": "AWS/EC2"}, live_context) + assert result.type == ResultType.ACTION, result.result.message + assert "metrics" in result.result.data + + +@pytest.mark.asyncio +async def test_get_metric_data(live_context): + result = await aws.execute_action( + "get_metric_data", + { + "start_time": "2024-01-01T00:00:00Z", + "end_time": "2024-01-02T00:00:00Z", + "metric_data_queries": [ + { + "Id": "m1", + "MetricStat": { + "Metric": {"Namespace": "AWS/EC2", "MetricName": "CPUUtilization"}, + "Period": 3600, + "Stat": "Average", + }, + } + ], + }, + live_context, + ) + assert result.type == ResultType.ACTION, result.result.message + assert "metric_data_results" in result.result.data + + +@pytest.mark.asyncio +async def test_describe_alarms(live_context): + result = await aws.execute_action("describe_alarms", {"max_records": 5}, live_context) + assert result.type == ResultType.ACTION, result.result.message + assert "metric_alarms" in result.result.data + + +@pytest.mark.asyncio +async def test_get_alarm_history(live_context): + alarm_name = AWS_ALARM_NAME + kwargs = {} + if alarm_name: + kwargs["alarm_name"] = alarm_name + result = await aws.execute_action("get_alarm_history", kwargs, live_context) + assert result.type == ResultType.ACTION, result.result.message + assert "alarm_history_items" in result.result.data + + +@pytest.mark.destructive +@pytest.mark.asyncio +async def test_set_alarm_state(live_context): + if not AWS_ALARM_NAME: + pytest.skip("AWS_ALARM_NAME required for set_alarm_state") + result = await aws.execute_action( + "set_alarm_state", + { + "alarm_name": AWS_ALARM_NAME, + "state_value": "OK", + "state_reason": "Autohive integration test - resetting to OK", + }, + live_context, + ) + assert result.type == ResultType.ACTION, result.result.message + + +# ---- CloudWatch Logs ---- + + +@pytest.mark.asyncio +async def test_describe_log_groups(live_context): + result = await aws.execute_action("describe_log_groups", {"limit": 5}, live_context) + assert result.type == ResultType.ACTION, result.result.message + assert "log_groups" in result.result.data + + +@pytest.mark.asyncio +async def test_filter_log_events(live_context): + if not AWS_LOG_GROUP: + pytest.skip("AWS_LOG_GROUP required for filter_log_events") + result = await aws.execute_action( + "filter_log_events", + {"log_group_name": AWS_LOG_GROUP}, + live_context, + ) + assert result.type == ResultType.ACTION, result.result.message + assert "events" in result.result.data + + +@pytest.mark.asyncio +async def test_get_log_events(live_context): + if not AWS_LOG_GROUP or not AWS_LOG_STREAM: + pytest.skip("AWS_LOG_GROUP and AWS_LOG_STREAM required for get_log_events") + result = await aws.execute_action( + "get_log_events", + {"log_group_name": AWS_LOG_GROUP, "log_stream_name": AWS_LOG_STREAM}, + live_context, + ) + assert result.type == ResultType.ACTION, result.result.message + assert "events" in result.result.data + + +# ---- CloudTrail ---- + + +@pytest.mark.asyncio +async def test_lookup_events(live_context): + result = await aws.execute_action("lookup_events", {"max_results": 5}, live_context) + assert result.type == ResultType.ACTION, result.result.message + assert "events" in result.result.data + + +@pytest.mark.asyncio +async def test_describe_trails(live_context): + result = await aws.execute_action("describe_trails", {}, live_context) + assert result.type == ResultType.ACTION, result.result.message + assert "trails" in result.result.data + + +@pytest.mark.asyncio +async def test_get_trail_status(live_context): + if not AWS_TRAIL_NAME: + pytest.skip("AWS_TRAIL_NAME required for get_trail_status") + result = await aws.execute_action("get_trail_status", {"trail_name": AWS_TRAIL_NAME}, live_context) + assert result.type == ResultType.ACTION, result.result.message + assert "trail_status" in result.result.data + assert "IsLogging" in result.result.data["trail_status"] + + +@pytest.mark.asyncio +async def test_get_event_selectors(live_context): + if not AWS_TRAIL_NAME: + pytest.skip("AWS_TRAIL_NAME required for get_event_selectors") + result = await aws.execute_action("get_event_selectors", {"trail_name": AWS_TRAIL_NAME}, live_context) + assert result.type == ResultType.ACTION, result.result.message + assert "event_selectors" in result.result.data diff --git a/aws/tests/test_aws_unit.py b/aws/tests/test_aws_unit.py new file mode 100644 index 00000000..c666d2a7 --- /dev/null +++ b/aws/tests/test_aws_unit.py @@ -0,0 +1,371 @@ +import os +import sys + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +import pytest +from unittest.mock import MagicMock, patch +from autohive_integrations_sdk import ResultType +from aws import aws # noqa: E402 + +pytestmark = pytest.mark.unit + + +# --------------------------------------------------------------------------- +# Security Hub +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_get_findings(mock_context): + mock_client = MagicMock() + mock_client.get_findings.return_value = {"Findings": [{"Id": "arn:aws:finding/1"}], "NextToken": None} + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action("get_findings", {"max_results": 5}, mock_context) + assert result.type != ResultType.ACTION_ERROR + assert "findings" in result.result.data + + +@pytest.mark.asyncio +async def test_get_findings_error(mock_context): + mock_client = MagicMock() + mock_client.get_findings.side_effect = Exception("Access denied") + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action("get_findings", {}, mock_context) + assert result.type == ResultType.ACTION_ERROR + assert "Access denied" in result.result.message + + +@pytest.mark.asyncio +async def test_get_finding_details(mock_context): + mock_client = MagicMock() + mock_client.get_findings.return_value = {"Findings": [{"Id": "arn:aws:finding/1", "ProductArn": "arn:aws:product"}]} + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action("get_finding_details", {"finding_arn": "arn:aws:finding/1"}, mock_context) + assert result.type != ResultType.ACTION_ERROR + assert result.result.data["finding"] is not None + + +@pytest.mark.asyncio +async def test_update_finding_workflow(mock_context): + mock_client = MagicMock() + mock_client.get_findings.return_value = {"Findings": [{"Id": "arn:aws:finding/1", "ProductArn": "arn:aws:product"}]} + mock_client.batch_update_findings.return_value = { + "ProcessedFindings": [{"Id": "arn:aws:finding/1"}], + "UnprocessedFindings": [], + } + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action( + "update_finding_workflow", + {"finding_arns": ["arn:aws:finding/1"], "workflow_status": "RESOLVED"}, + mock_context, + ) + assert result.type != ResultType.ACTION_ERROR + assert "processed_findings" in result.result.data + + +@pytest.mark.asyncio +async def test_get_insights(mock_context): + mock_client = MagicMock() + mock_client.get_insights.return_value = { + "Insights": [ + {"InsightArn": "arn:aws:insight/1", "Name": "Test Insight", "Filters": {}, "GroupByAttribute": "Type"} + ], + "NextToken": None, + } + mock_client.get_insight_results.return_value = { + "InsightResults": {"InsightArn": "arn:aws:insight/1", "ResultValues": []} + } + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action("get_insights", {"max_results": 5}, mock_context) + assert result.type != ResultType.ACTION_ERROR + assert "insights" in result.result.data + + +# --------------------------------------------------------------------------- +# GuardDuty +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_list_detectors(mock_context): + mock_client = MagicMock() + mock_client.list_detectors.return_value = {"DetectorIds": ["abc123"], "NextToken": None} + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action("list_detectors", {}, mock_context) + assert result.type != ResultType.ACTION_ERROR + assert result.result.data["detector_ids"] == ["abc123"] + + +@pytest.mark.asyncio +async def test_list_guardduty_findings(mock_context): + mock_client = MagicMock() + mock_client.list_findings.return_value = {"FindingIds": ["id1", "id2"], "NextToken": None} + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action("list_guardduty_findings", {"detector_id": "abc123"}, mock_context) + assert result.type != ResultType.ACTION_ERROR + assert result.result.data["finding_ids"] == ["id1", "id2"] + + +@pytest.mark.asyncio +async def test_list_guardduty_findings_error(mock_context): + mock_client = MagicMock() + mock_client.list_findings.side_effect = Exception("DetectorNotFoundException") + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action("list_guardduty_findings", {"detector_id": "bad"}, mock_context) + assert result.type == ResultType.ACTION_ERROR + assert "DetectorNotFoundException" in result.result.message + + +@pytest.mark.asyncio +async def test_get_guardduty_finding_details(mock_context): + mock_client = MagicMock() + mock_client.get_findings.return_value = {"Findings": [{"Id": "id1", "Type": "Recon"}]} + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action( + "get_guardduty_finding_details", + {"detector_id": "abc123", "finding_ids": ["id1"]}, + mock_context, + ) + assert result.type != ResultType.ACTION_ERROR + assert len(result.result.data["findings"]) == 1 + + +@pytest.mark.asyncio +async def test_archive_findings(mock_context): + mock_client = MagicMock() + mock_client.archive_findings.return_value = {} + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action( + "archive_findings", + {"detector_id": "abc123", "finding_ids": ["id1"]}, + mock_context, + ) + assert result.type != ResultType.ACTION_ERROR + assert result.result.data["success"] is True + + +# --------------------------------------------------------------------------- +# CloudWatch +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_list_metrics(mock_context): + mock_client = MagicMock() + mock_client.list_metrics.return_value = { + "Metrics": [{"Namespace": "AWS/EC2", "MetricName": "CPUUtilization"}], + "NextToken": None, + } + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action("list_metrics", {"namespace": "AWS/EC2"}, mock_context) + assert result.type != ResultType.ACTION_ERROR + assert len(result.result.data["metrics"]) == 1 + + +@pytest.mark.asyncio +async def test_get_metric_data(mock_context): + mock_client = MagicMock() + mock_client.get_metric_data.return_value = {"MetricDataResults": [{"Id": "m1", "Timestamps": [], "Values": []}]} + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action( + "get_metric_data", + { + "metric_data_queries": [ + { + "Id": "m1", + "MetricStat": { + "Metric": {"Namespace": "AWS/EC2", "MetricName": "CPUUtilization"}, + "Period": 300, + "Stat": "Average", + }, + } + ], + "start_time": "2024-01-01T00:00:00Z", + "end_time": "2024-01-02T00:00:00Z", + }, + mock_context, + ) + assert result.type != ResultType.ACTION_ERROR + assert "metric_data_results" in result.result.data + + +@pytest.mark.asyncio +async def test_describe_alarms(mock_context): + mock_client = MagicMock() + mock_client.describe_alarms.return_value = { + "MetricAlarms": [{"AlarmName": "cpu-alarm"}], + "CompositeAlarms": [], + "NextToken": None, + } + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action("describe_alarms", {}, mock_context) + assert result.type != ResultType.ACTION_ERROR + assert len(result.result.data["metric_alarms"]) == 1 + + +@pytest.mark.asyncio +async def test_get_alarm_history(mock_context): + mock_client = MagicMock() + mock_client.describe_alarm_history.return_value = {"AlarmHistoryItems": [], "NextToken": None} + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action("get_alarm_history", {}, mock_context) + assert result.type != ResultType.ACTION_ERROR + assert "alarm_history_items" in result.result.data + + +@pytest.mark.asyncio +async def test_set_alarm_state(mock_context): + mock_client = MagicMock() + mock_client.set_alarm_state.return_value = {} + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action( + "set_alarm_state", + {"alarm_name": "cpu-alarm", "state_value": "OK", "state_reason": "Testing"}, + mock_context, + ) + assert result.type != ResultType.ACTION_ERROR + assert result.result.data["success"] is True + + +@pytest.mark.asyncio +async def test_set_alarm_state_error(mock_context): + mock_client = MagicMock() + mock_client.set_alarm_state.side_effect = Exception("ResourceNotFoundException") + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action( + "set_alarm_state", + {"alarm_name": "bad-alarm", "state_value": "OK", "state_reason": "test"}, + mock_context, + ) + assert result.type == ResultType.ACTION_ERROR + assert "ResourceNotFoundException" in result.result.message + + +# --------------------------------------------------------------------------- +# CloudWatch Logs +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_describe_log_groups(mock_context): + mock_client = MagicMock() + mock_client.describe_log_groups.return_value = { + "logGroups": [{"logGroupName": "/aws/lambda/fn"}], + "nextToken": None, + } + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action("describe_log_groups", {}, mock_context) + assert result.type != ResultType.ACTION_ERROR + assert len(result.result.data["log_groups"]) == 1 + + +@pytest.mark.asyncio +async def test_filter_log_events(mock_context): + mock_client = MagicMock() + mock_client.filter_log_events.return_value = { + "events": [{"message": "ERROR something failed", "timestamp": 1000}], + "searchedLogStreams": [], + "nextToken": None, + } + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action( + "filter_log_events", + {"log_group_name": "/aws/lambda/fn", "filter_pattern": "ERROR"}, + mock_context, + ) + assert result.type != ResultType.ACTION_ERROR + assert len(result.result.data["events"]) == 1 + + +@pytest.mark.asyncio +async def test_get_log_events(mock_context): + mock_client = MagicMock() + mock_client.get_log_events.return_value = { + "events": [{"message": "log line", "timestamp": 1000}], + "nextForwardToken": "fwd-token", + "nextBackwardToken": "bwd-token", + } + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action( + "get_log_events", + {"log_group_name": "/aws/lambda/fn", "log_stream_name": "stream-1"}, + mock_context, + ) + assert result.type != ResultType.ACTION_ERROR + assert "events" in result.result.data + + +@pytest.mark.asyncio +async def test_get_log_events_error(mock_context): + mock_client = MagicMock() + mock_client.get_log_events.side_effect = Exception("ResourceNotFoundException") + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action( + "get_log_events", + {"log_group_name": "/aws/lambda/fn", "log_stream_name": "bad-stream"}, + mock_context, + ) + assert result.type == ResultType.ACTION_ERROR + assert "ResourceNotFoundException" in result.result.message + + +# --------------------------------------------------------------------------- +# CloudTrail +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_lookup_events(mock_context): + mock_client = MagicMock() + mock_client.lookup_events.return_value = {"Events": [{"EventName": "RunInstances"}], "NextToken": None} + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action("lookup_events", {}, mock_context) + assert result.type != ResultType.ACTION_ERROR + assert len(result.result.data["events"]) == 1 + + +@pytest.mark.asyncio +async def test_describe_trails(mock_context): + mock_client = MagicMock() + mock_client.describe_trails.return_value = { + "trailList": [{"Name": "management-events", "TrailARN": "arn:aws:cloudtrail:trail"}] + } + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action("describe_trails", {}, mock_context) + assert result.type != ResultType.ACTION_ERROR + assert len(result.result.data["trails"]) == 1 + + +@pytest.mark.asyncio +async def test_get_trail_status(mock_context): + mock_client = MagicMock() + mock_client.get_trail_status.return_value = {"IsLogging": True, "LatestDeliveryTime": "2024-01-01"} + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action("get_trail_status", {"trail_name": "management-events"}, mock_context) + assert result.type != ResultType.ACTION_ERROR + assert "trail_status" in result.result.data + + +@pytest.mark.asyncio +async def test_get_event_selectors(mock_context): + mock_client = MagicMock() + mock_client.get_event_selectors.return_value = { + "TrailARN": "arn:aws:cloudtrail:trail", + "EventSelectors": [{"ReadWriteType": "All"}], + "AdvancedEventSelectors": [], + } + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action("get_event_selectors", {"trail_name": "management-events"}, mock_context) + assert result.type != ResultType.ACTION_ERROR + assert "event_selectors" in result.result.data + + +@pytest.mark.asyncio +async def test_get_event_selectors_error(mock_context): + mock_client = MagicMock() + mock_client.get_event_selectors.side_effect = Exception("TrailNotFoundException") + with patch("helpers.boto3.client", return_value=mock_client): + result = await aws.execute_action("get_event_selectors", {"trail_name": "bad-trail"}, mock_context) + assert result.type == ResultType.ACTION_ERROR + assert "TrailNotFoundException" in result.result.message