From f85b63da69f55ac57f519006e4b57a9b8a003098 Mon Sep 17 00:00:00 2001 From: Shubhank <72601061+Sagsgit@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:41:52 +0530 Subject: [PATCH 01/12] chore(aws): migrate to autohive-integrations-sdk 2.0.0 - Bump SDK to ~=2.0.0 in requirements.txt - Bump config.json version to 2.0.0 - Fix auth: flat context.auth.get() instead of nested credentials dict - Update helpers.py: error_result() now returns ActionError (not ActionResult) - Remove result/error/error_code fields from all 20 output schemas - Replace context.py + test_aws.py with conftest.py + unit + integration tests - All 20 actions covered by unit tests (25 tests, 25 passing) - Add AWS env vars to root .env.example --- .env.example | 23 ++ aws/config.json | 381 ++++++++------------------- aws/helpers.py | 22 +- aws/requirements.txt | 2 +- aws/tests/conftest.py | 14 + aws/tests/context.py | 6 - aws/tests/test_aws.py | 422 ------------------------------ aws/tests/test_aws_integration.py | 91 +++++++ aws/tests/test_aws_unit.py | 371 ++++++++++++++++++++++++++ 9 files changed, 624 insertions(+), 708 deletions(-) create mode 100644 aws/tests/conftest.py delete mode 100644 aws/tests/context.py delete mode 100644 aws/tests/test_aws.py create mode 100644 aws/tests/test_aws_integration.py create mode 100644 aws/tests/test_aws_unit.py diff --git a/.env.example b/.env.example index 18bb84f2..9e326368 100644 --- a/.env.example +++ b/.env.example @@ -168,3 +168,26 @@ # YOUTUBE_TEST_THUMBNAIL_PATH= # Video ID owned by the authenticated user (required by moderate_comment, which only works on your own channel) # YOUTUBE_TEST_OWNED_VIDEO_ID= + +# -- AWS -- +# AWS_ACCESS_KEY_ID= +# AWS_SECRET_ACCESS_KEY= +# AWS_REGION=us-east-1 +# 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= + +# -- Fergus -- +# FERGUS_API_TOKEN= + +# -- Ghost -- +# GHOST_API_URL= +# GHOST_CONTENT_API_KEY= +# GHOST_ADMIN_API_KEY= + +# -- Circle -- +# CIRCLE_API_TOKEN= diff --git a/aws/config.json b/aws/config.json index 3cc5c8f3..f4cd57c8 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": { @@ -64,20 +64,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 +84,9 @@ "description": "The ARN of the Security Hub finding to retrieve" } }, - "required": ["finding_arn"] + "required": [ + "finding_arn" + ] }, "output_schema": { "type": "object", @@ -101,18 +94,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 +114,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 +141,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 +179,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 +218,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 +256,9 @@ "description": "Pagination token from a previous request" } }, - "required": ["detector_id"] + "required": [ + "detector_id" + ] }, "output_schema": { "type": "object", @@ -310,20 +271,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 +298,10 @@ } } }, - "required": ["detector_id", "finding_ids"] + "required": [ + "detector_id", + "finding_ids" + ] }, "output_schema": { "type": "object", @@ -354,18 +309,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 +331,10 @@ } } }, - "required": ["detector_id", "finding_ids"] + "required": [ + "detector_id", + "finding_ids" + ] }, "output_schema": { "type": "object", @@ -396,18 +342,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 +382,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 +413,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 +425,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 +449,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 +480,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 +509,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 +544,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 +566,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 +589,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 +625,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 +675,9 @@ "description": "Pagination token from a previous request" } }, - "required": ["log_group_name"] + "required": [ + "log_group_name" + ] }, "output_schema": { "type": "object", @@ -795,20 +691,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 +739,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 +752,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 +811,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 +847,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 +862,9 @@ "description": "The trail name or ARN to get status for" } }, - "required": ["trail_name"] + "required": [ + "trail_name" + ] }, "output_schema": { "type": "object", @@ -1007,18 +872,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 +887,9 @@ "description": "The trail name or ARN to get event selectors for" } }, - "required": ["trail_name"] + "required": [ + "trail_name" + ] }, "output_schema": { "type": "object", @@ -1050,21 +905,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..4ac8c90c 100644 --- a/aws/helpers.py +++ b/aws/helpers.py @@ -5,20 +5,19 @@ 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") + access_key = context.auth.get("aws_access_key_id") + secret_key = context.auth.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") 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=context.auth.get("aws_region", "us-east-1"), ) @@ -42,13 +41,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..62d394d1 --- /dev/null +++ b/aws/tests/test_aws_integration.py @@ -0,0 +1,91 @@ +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", "") + +pytestmark = pytest.mark.skipif( + not AWS_ACCESS_KEY_ID or not AWS_SECRET_ACCESS_KEY, + reason="AWS credentials not set in environment", +) + + +@pytest.fixture +def live_context(): + ctx_auth = { + "aws_access_key_id": AWS_ACCESS_KEY_ID, + "aws_secret_access_key": AWS_SECRET_ACCESS_KEY, + "aws_region": AWS_REGION, + } + + class _Ctx: + auth = ctx_auth + + return _Ctx() + + +@pytest.mark.asyncio +async def test_get_findings_live(live_context): + result = await aws.execute_action("get_findings", {"max_results": 5}, live_context) + assert result.type in (ResultType.SUCCESS, ResultType.ACTION_ERROR) + if result.type == ResultType.SUCCESS: + assert "findings" in result.result.data + + +@pytest.mark.asyncio +async def test_list_detectors_live(live_context): + result = await aws.execute_action("list_detectors", {}, live_context) + assert result.type in (ResultType.SUCCESS, ResultType.ACTION_ERROR) + if result.type == ResultType.SUCCESS: + assert "detector_ids" in result.result.data + + +@pytest.mark.asyncio +async def test_list_metrics_live(live_context): + result = await aws.execute_action("list_metrics", {"namespace": "AWS/EC2"}, live_context) + assert result.type in (ResultType.SUCCESS, ResultType.ACTION_ERROR) + if result.type == ResultType.SUCCESS: + assert "metrics" in result.result.data + + +@pytest.mark.asyncio +async def test_describe_alarms_live(live_context): + result = await aws.execute_action("describe_alarms", {"max_records": 5}, live_context) + assert result.type in (ResultType.SUCCESS, ResultType.ACTION_ERROR) + if result.type == ResultType.SUCCESS: + assert "metric_alarms" in result.result.data + + +@pytest.mark.asyncio +async def test_describe_log_groups_live(live_context): + result = await aws.execute_action("describe_log_groups", {"limit": 5}, live_context) + assert result.type in (ResultType.SUCCESS, ResultType.ACTION_ERROR) + if result.type == ResultType.SUCCESS: + assert "log_groups" in result.result.data + + +@pytest.mark.asyncio +async def test_lookup_events_live(live_context): + result = await aws.execute_action("lookup_events", {"max_results": 5}, live_context) + assert result.type in (ResultType.SUCCESS, ResultType.ACTION_ERROR) + if result.type == ResultType.SUCCESS: + assert "events" in result.result.data + + +@pytest.mark.asyncio +async def test_describe_trails_live(live_context): + result = await aws.execute_action("describe_trails", {}, live_context) + assert result.type in (ResultType.SUCCESS, ResultType.ACTION_ERROR) + if result.type == ResultType.SUCCESS: + assert "trails" 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 From 701c0680033e29b77876b640769df307b4493222 Mon Sep 17 00:00:00 2001 From: Shubhank <72601061+Sagsgit@users.noreply.github.com> Date: Thu, 11 Jun 2026 12:04:17 +0530 Subject: [PATCH 02/12] fix(aws): read credentials from context.auth['credentials'] wrapper In production SDK 2 contexts, custom auth fields land under context.auth['credentials']. Fall back to the flat context.auth dict so existing unit-test mocks that pass auth keys at the top level continue to work. --- aws/helpers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/aws/helpers.py b/aws/helpers.py index 4ac8c90c..09ca1c34 100644 --- a/aws/helpers.py +++ b/aws/helpers.py @@ -9,15 +9,16 @@ def create_boto3_client(context: ExecutionContext, service_name: str): - access_key = context.auth.get("aws_access_key_id") - secret_key = context.auth.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") return boto3.client( service_name, aws_access_key_id=access_key, aws_secret_access_key=secret_key, - region_name=context.auth.get("aws_region", "us-east-1"), + region_name=creds.get("aws_region", "us-east-1"), ) From 4e0ef602d944c4a9ea98952ae1b2f2dc519913df Mon Sep 17 00:00:00 2001 From: Shubhank <72601061+Sagsgit@users.noreply.github.com> Date: Thu, 11 Jun 2026 12:05:36 +0530 Subject: [PATCH 03/12] fix(aws): rewrite integration tests with full coverage of all 20 actions - Add pytest.mark.integration to pytestmark - Replace ResultType.SUCCESS (doesn't exist in SDK 2) with ResultType.ACTION - Assert result.type == ResultType.ACTION on all live-path tests - Add tests for all 13 previously missing actions: Security Hub: get_finding_details, get_insights, update_finding_workflow GuardDuty: list_guardduty_findings, get_guardduty_finding_details, archive_findings CloudWatch: get_metric_data, get_alarm_history, set_alarm_state CloudWatch Logs: filter_log_events, get_log_events CloudTrail: get_trail_status, get_event_selectors - Gate service-specific tests on env vars or chain from list actions - Mark destructive tests (update_finding_workflow, archive_findings, set_alarm_state) - Add AWS_ALARM_NAME and AWS_FINDING_ARN to .env.example --- .env.example | 4 + aws/tests/test_aws_integration.py | 308 ++++++++++++++++++++++++++---- 2 files changed, 271 insertions(+), 41 deletions(-) diff --git a/.env.example b/.env.example index 9e326368..8feb6634 100644 --- a/.env.example +++ b/.env.example @@ -180,6 +180,10 @@ # 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= # -- Fergus -- # FERGUS_API_TOKEN= diff --git a/aws/tests/test_aws_integration.py b/aws/tests/test_aws_integration.py index 62d394d1..c487f96b 100644 --- a/aws/tests/test_aws_integration.py +++ b/aws/tests/test_aws_integration.py @@ -1,11 +1,37 @@ +""" +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__), ".."))) +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 +from aws.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", "") @@ -14,78 +40,278 @@ 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", "") -pytestmark = pytest.mark.skipif( - not AWS_ACCESS_KEY_ID or not AWS_SECRET_ACCESS_KEY, - reason="AWS credentials not set in environment", -) +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(): - ctx_auth = { - "aws_access_key_id": AWS_ACCESS_KEY_ID, - "aws_secret_access_key": AWS_SECRET_ACCESS_KEY, - "aws_region": AWS_REGION, - } - class _Ctx: - auth = ctx_auth + auth = { + "aws_access_key_id": AWS_ACCESS_KEY_ID, + "aws_secret_access_key": AWS_SECRET_ACCESS_KEY, + "aws_region": AWS_REGION, + } return _Ctx() +# ---- Security Hub ---- + + @pytest.mark.asyncio -async def test_get_findings_live(live_context): +async def test_get_findings(live_context): result = await aws.execute_action("get_findings", {"max_results": 5}, live_context) - assert result.type in (ResultType.SUCCESS, ResultType.ACTION_ERROR) - if result.type == ResultType.SUCCESS: - assert "findings" in result.result.data + 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_list_detectors_live(live_context): +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 in (ResultType.SUCCESS, ResultType.ACTION_ERROR) - if result.type == ResultType.SUCCESS: - assert "detector_ids" in result.result.data + 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(live_context): +async def test_list_metrics(live_context): result = await aws.execute_action("list_metrics", {"namespace": "AWS/EC2"}, live_context) - assert result.type in (ResultType.SUCCESS, ResultType.ACTION_ERROR) - if result.type == ResultType.SUCCESS: - assert "metrics" in result.result.data + assert result.type == ResultType.ACTION, result.result.message + assert "metrics" in result.result.data @pytest.mark.asyncio -async def test_describe_alarms_live(live_context): +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 in (ResultType.SUCCESS, ResultType.ACTION_ERROR) - if result.type == ResultType.SUCCESS: - assert "metric_alarms" in result.result.data + 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(live_context): +async def test_describe_log_groups(live_context): result = await aws.execute_action("describe_log_groups", {"limit": 5}, live_context) - assert result.type in (ResultType.SUCCESS, ResultType.ACTION_ERROR) - if result.type == ResultType.SUCCESS: - assert "log_groups" in result.result.data + assert result.type == ResultType.ACTION, result.result.message + assert "log_groups" in result.result.data @pytest.mark.asyncio -async def test_lookup_events_live(live_context): +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 in (ResultType.SUCCESS, ResultType.ACTION_ERROR) - if result.type == ResultType.SUCCESS: - assert "events" in result.result.data + assert result.type == ResultType.ACTION, result.result.message + assert "events" in result.result.data @pytest.mark.asyncio -async def test_describe_trails_live(live_context): +async def test_describe_trails(live_context): result = await aws.execute_action("describe_trails", {}, live_context) - assert result.type in (ResultType.SUCCESS, ResultType.ACTION_ERROR) - if result.type == ResultType.SUCCESS: - assert "trails" in result.result.data + 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 "is_logging" in result.result.data + + +@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 From d43f57a57768922aff6dfbadad7295e1486fc4da Mon Sep 17 00:00:00 2001 From: Shubhank <72601061+Sagsgit@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:25:59 +0530 Subject: [PATCH 04/12] fix(aws): pass session_token to boto3, fix import and live_context for STS credentials --- aws/helpers.py | 3 +++ aws/tests/test_aws_integration.py | 23 +++++++++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/aws/helpers.py b/aws/helpers.py index 09ca1c34..ea9f430b 100644 --- a/aws/helpers.py +++ b/aws/helpers.py @@ -1,5 +1,6 @@ import asyncio import functools +import os from datetime import datetime, date from decimal import Decimal from typing import Any, Dict @@ -14,11 +15,13 @@ def create_boto3_client(context: ExecutionContext, service_name: str): 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") or os.environ.get("AWS_SESSION_TOKEN") return boto3.client( service_name, aws_access_key_id=access_key, aws_secret_access_key=secret_key, region_name=creds.get("aws_region", "us-east-1"), + aws_session_token=session_token or None, ) diff --git a/aws/tests/test_aws_integration.py b/aws/tests/test_aws_integration.py index c487f96b..4ee7b606 100644 --- a/aws/tests/test_aws_integration.py +++ b/aws/tests/test_aws_integration.py @@ -27,11 +27,11 @@ import os import sys -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) import pytest from autohive_integrations_sdk import ResultType -from aws.aws import aws # noqa: E402 +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", "") @@ -42,6 +42,7 @@ 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, @@ -54,14 +55,20 @@ @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: - auth = { - "aws_access_key_id": AWS_ACCESS_KEY_ID, - "aws_secret_access_key": AWS_SECRET_ACCESS_KEY, - "aws_region": AWS_REGION, - } + pass - return _Ctx() + ctx = _Ctx() + ctx.auth = auth + return ctx # ---- Security Hub ---- From 41ea8a38e5164dd4715ba65444e41cca9d4321c2 Mon Sep 17 00:00:00 2001 From: Shubhank <72601061+Sagsgit@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:20:54 +0530 Subject: [PATCH 05/12] fix(aws): assert trail_status key in get_trail_status live test (not is_logging) --- aws/tests/test_aws_integration.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aws/tests/test_aws_integration.py b/aws/tests/test_aws_integration.py index 4ee7b606..8afbb215 100644 --- a/aws/tests/test_aws_integration.py +++ b/aws/tests/test_aws_integration.py @@ -312,7 +312,8 @@ async def test_get_trail_status(live_context): 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 "is_logging" in result.result.data + assert "trail_status" in result.result.data + assert "IsLogging" in result.result.data["trail_status"] @pytest.mark.asyncio From 38b8f4844b1e372ab1953a1b2a91f0352cf38b80 Mon Sep 17 00:00:00 2001 From: Shubhank <72601061+Sagsgit@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:21:21 +0530 Subject: [PATCH 06/12] fix(aws): read aws_session_token only from context.auth; remove os.environ fallback --- aws/helpers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aws/helpers.py b/aws/helpers.py index ea9f430b..072757d0 100644 --- a/aws/helpers.py +++ b/aws/helpers.py @@ -1,6 +1,5 @@ import asyncio import functools -import os from datetime import datetime, date from decimal import Decimal from typing import Any, Dict @@ -15,7 +14,7 @@ def create_boto3_client(context: ExecutionContext, service_name: str): 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") or os.environ.get("AWS_SESSION_TOKEN") + session_token = creds.get("aws_session_token") return boto3.client( service_name, aws_access_key_id=access_key, From 282dc39bcad5c53347d59b6b35c90b47407c9614 Mon Sep 17 00:00:00 2001 From: Shubhank <72601061+Sagsgit@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:21:42 +0530 Subject: [PATCH 07/12] fix(aws): document AWS_SESSION_TOKEN in .env.example --- .env.example | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.env.example b/.env.example index 8feb6634..35b255b0 100644 --- a/.env.example +++ b/.env.example @@ -173,6 +173,8 @@ # 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 From 01b77cef75bc107d33def656f002516bfd004dd3 Mon Sep 17 00:00:00 2001 From: Shubhank <72601061+Sagsgit@users.noreply.github.com> Date: Tue, 16 Jun 2026 08:52:37 +0530 Subject: [PATCH 08/12] feat(aws): add optional aws_session_token to custom auth schema Exposes aws_session_token as an optional password field in the auth UI so users can configure temporary STS credentials without needing to inject them via environment variables. --- aws/config.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/aws/config.json b/aws/config.json index f4cd57c8..6437f51a 100644 --- a/aws/config.json +++ b/aws/config.json @@ -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 } } } From 375239f3a52ac0dcdd560d7f5882c05c0da6f2dc Mon Sep 17 00:00:00 2001 From: Shubhank <72601061+Sagsgit@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:53:40 +0530 Subject: [PATCH 09/12] fix(.env.example): sort integration sections alphabetically to avoid merge conflicts --- .env.example | 260 +++++++++++++++++++++++++-------------------------- 1 file changed, 130 insertions(+), 130 deletions(-) diff --git a/.env.example b/.env.example index d4a89b47..e5a423ca 100644 --- a/.env.example +++ b/.env.example @@ -12,25 +12,80 @@ # Format: VARIABLE_NAME=value (no quotes needed) # ============================================================================ -# -- Box -- -# BOX_ACCESS_TOKEN= +# -- ActiveCampaign -- +# ACTIVECAMPAIGN_API_KEY= +# ACTIVECAMPAIGN_API_URL= +# Optional — targets specific resources for faster tests +# ACTIVECAMPAIGN_TEST_CAMPAIGN_ID= +# ACTIVECAMPAIGN_TEST_CONTACT_ID= + +# -- App Business Reviews -- +# APP_BUSINESS_REVIEWS_API_KEY= + +# -- 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= -# -- Dropbox -- -# DROPBOX_ACCESS_TOKEN= -# Optional: parent folder for destructive integration tests (default: /autohive_integration_test) -# DROPBOX_TEST_FOLDER= +# -- Box -- +# BOX_ACCESS_TOKEN= # -- Calendly -- # OAuth/personal access token for Calendly API v2 read-only integration tests. # The destructive webhook lifecycle test also needs a paid Calendly plan. # CALENDLY_ACCESS_TOKEN= +# -- Circle -- +# CIRCLE_API_TOKEN= # -- ClickUp -- # CLICKUP_ACCESS_TOKEN= +# -- Coda -- +# CODA_API_KEY= + +# -- Dropbox -- +# DROPBOX_ACCESS_TOKEN= +# Optional: parent folder for destructive integration tests (default: /autohive_integration_test) +# DROPBOX_TEST_FOLDER= + +# -- Fergus -- +# FERGUS_API_TOKEN= + +# -- Float -- +# FLOAT_API_KEY= + +# -- Freshdesk -- +# FRESHDESK_API_KEY= +# FRESHDESK_DOMAIN= + +# -- Front -- +# FRONT_ACCESS_TOKEN= + +# -- Ghost -- +# GHOST_API_URL= +# GHOST_CONTENT_API_KEY= +# GHOST_ADMIN_API_KEY= + +# -- GitHub -- +# OAuth2 access token for read-only integration tests. +# GITHUB_ACCESS_TOKEN= + # -- Gmail -- # Required for read-only and destructive integration tests. # Token must have the https://www.googleapis.com/auth/gmail.modify scope. @@ -40,41 +95,38 @@ # GMAIL_TEST_THREAD_ID= # GMAIL_TEST_MESSAGE_ID= -# -- GitHub -- -# OAuth2 access token for read-only integration tests. -# GITHUB_ACCESS_TOKEN= - -# -- Freshdesk -- -# FRESHDESK_API_KEY= -# FRESHDESK_DOMAIN= - -# -- Lumin PDF -- -# LUMIN_PDF_TOKEN= - -# -- NZBN -- -# NZBN_CLIENT_ID= -# NZBN_CLIENT_SECRET= -# NZBN_SUBSCRIPTION_KEY= +# -- Gong -- +# GONG_ACCESS_TOKEN= +# GONG_API_BASE_URL= -# -- Notion -- -# NOTION_ACCESS_TOKEN= +# -- Gong -- +# GONG_ACCESS_KEY= +# GONG_ACCESS_KEY_SECRET= -# -- Perplexity -- -# PERPLEXITY_API_KEY= +# -- Google Calendar -- +# OAuth2 access token with scope: https://www.googleapis.com/auth/calendar +# GOOGLE_CALENDAR_ACCESS_TOKEN= -# -- Shopify Customer -- -# SHOPIFY_CUSTOMER_ACCESS_TOKEN= -# SHOPIFY_CUSTOMER_SHOP_URL= +# -- Google Forms -- +# OAuth2 access token with scopes: forms.body, forms.responses.readonly +# GOOGLE_FORMS_ACCESS_TOKEN= +# Optional pre-existing form ID for read-only integration tests +# GOOGLE_FORMS_TEST_FORM_ID= -# -- Stripe -- -# STRIPE_TEST_API_KEY= +# -- Google Looker -- +# LOOKER_BASE_URL= +# LOOKER_CLIENT_ID= +# LOOKER_CLIENT_SECRET= +# LOOKER_TEST_DASHBOARD_ID= +# LOOKER_TEST_MODEL_NAME= -# -- Trello -- -# TRELLO_API_KEY= -# TRELLO_API_TOKEN= +# -- Google Sheets -- +# GOOGLE_SHEETS_ACCESS_TOKEN= +# GOOGLE_SHEETS_TEST_SPREADSHEET_ID= -# -- Zoom -- -# ZOOM_ACCESS_TOKEN= +# -- Harvest -- +# HARVEST_ACCESS_TOKEN= +# HARVEST_ACCOUNT_ID= # -- HubSpot -- # HUBSPOT_ACCESS_TOKEN= @@ -87,74 +139,49 @@ # HUBSPOT_TEST_NOTE_ID= # HUBSPOT_TEST_OWNER_ID= -# -- Salesforce -- -# SALESFORCE_ACCESS_TOKEN= -# SALESFORCE_INSTANCE_URL= -# SALESFORCE_TEST_RECORD_ID= -# SALESFORCE_TEST_TASK_ID= -# SALESFORCE_TEST_EVENT_ID= +# -- LinkedIn -- +# LINKEDIN_ACCESS_TOKEN= -# -- Xero -- -# XERO_ACCESS_TOKEN= -# XERO_TENANT_ID= +# -- Lumin PDF -- +# LUMIN_PDF_TOKEN= -# -- Coda -- -# CODA_API_KEY= +# -- Microsoft 365 -- +# MICROSOFT365_ACCESS_TOKEN= +# MICROSOFT365_TEST_RECIPIENT_EMAIL= # email address to use as recipient in destructive send/draft/forward tests +# MICROSOFT365_TEST_ATTENDEE_EMAIL= # email address to use as attendee in find_meeting_times live test +# MICROSOFT365_TEST_SCHEDULE_EMAIL= # email address to use in get_schedule live test (e.g. user@tenant.onmicrosoft.com) -# -- Google Sheets -- -# GOOGLE_SHEETS_ACCESS_TOKEN= -# GOOGLE_SHEETS_TEST_SPREADSHEET_ID= +# -- Notion -- +# NOTION_ACCESS_TOKEN= -# -- Google Calendar -- -# OAuth2 access token with scope: https://www.googleapis.com/auth/calendar -# GOOGLE_CALENDAR_ACCESS_TOKEN= +# -- NZBN -- +# NZBN_CLIENT_ID= +# NZBN_CLIENT_SECRET= +# NZBN_SUBSCRIPTION_KEY= -# -- Google Looker -- -# LOOKER_BASE_URL= -# LOOKER_CLIENT_ID= -# LOOKER_CLIENT_SECRET= -# LOOKER_TEST_DASHBOARD_ID= -# LOOKER_TEST_MODEL_NAME= +# -- Perplexity -- +# PERPLEXITY_API_KEY= -# -- Google Forms -- -# OAuth2 access token with scopes: forms.body, forms.responses.readonly -# GOOGLE_FORMS_ACCESS_TOKEN= -# Optional pre-existing form ID for read-only integration tests -# GOOGLE_FORMS_TEST_FORM_ID= +# -- Salesforce -- +# SALESFORCE_ACCESS_TOKEN= +# SALESFORCE_INSTANCE_URL= +# SALESFORCE_TEST_RECORD_ID= +# SALESFORCE_TEST_TASK_ID= +# SALESFORCE_TEST_EVENT_ID= -# -- Gong -- -# GONG_ACCESS_TOKEN= -# GONG_API_BASE_URL= +# -- Shopify Customer -- +# SHOPIFY_CUSTOMER_ACCESS_TOKEN= +# SHOPIFY_CUSTOMER_SHOP_URL= -# -- Float -- -# FLOAT_API_KEY= +# -- Stripe -- +# STRIPE_TEST_API_KEY= # -- Supadata -- # SUPADATA_API_KEY= -# -- LinkedIn -- -# LINKEDIN_ACCESS_TOKEN= - -# -- ActiveCampaign -- -# ACTIVECAMPAIGN_API_KEY= -# ACTIVECAMPAIGN_API_URL= -# Optional — targets specific resources for faster tests -# ACTIVECAMPAIGN_TEST_CAMPAIGN_ID= -# ACTIVECAMPAIGN_TEST_CONTACT_ID= - -# -- App Business Reviews -- -# APP_BUSINESS_REVIEWS_API_KEY= - -# -- Harvest -- -# HARVEST_ACCESS_TOKEN= -# HARVEST_ACCOUNT_ID= - -# -- Front -- -# FRONT_ACCESS_TOKEN= - -# -- Gong -- -# GONG_ACCESS_KEY= -# GONG_ACCESS_KEY_SECRET= +# -- Trello -- +# TRELLO_API_KEY= +# TRELLO_API_TOKEN= # -- WhatsApp Business -- # WHATSAPP_ACCESS_TOKEN= @@ -163,6 +190,16 @@ # WHATSAPP_TEMPLATE_NAME= # WHATSAPP_TEMPLATE_LANG= # WHATSAPP_MEDIA_URL= +# -- X (formerly Twitter) -- +# OAuth2 user access token with scopes: tweet.read, tweet.write, media.write, +# users.read, follows.read, follows.write, like.read, bookmark.read, bookmark.write, offline.access +# X_ACCESS_TOKEN= +# User id to follow/unfollow in the follow lifecycle destructive test (optional) +# X_TEST_TARGET_USER_ID= +# -- Xero -- +# XERO_ACCESS_TOKEN= +# XERO_TENANT_ID= + # -- YouTube -- # OAuth2 access token with scopes: youtube, youtube.upload, youtube.force-ssl # YOUTUBE_ACCESS_TOKEN= @@ -179,42 +216,5 @@ # Video ID owned by the authenticated user (required by moderate_comment, which only works on your own channel) # YOUTUBE_TEST_OWNED_VIDEO_ID= -# -- 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= - -# -- Fergus -- -# FERGUS_API_TOKEN= - -# -- Ghost -- -# GHOST_API_URL= -# GHOST_CONTENT_API_KEY= -# GHOST_ADMIN_API_KEY= - -# -- Circle -- -# CIRCLE_API_TOKEN= -# -- X (formerly Twitter) -- -# OAuth2 user access token with scopes: tweet.read, tweet.write, media.write, -# users.read, follows.read, follows.write, like.read, bookmark.read, bookmark.write, offline.access -# X_ACCESS_TOKEN= -# User id to follow/unfollow in the follow lifecycle destructive test (optional) -# X_TEST_TARGET_USER_ID= -# -- Microsoft 365 -- -# MICROSOFT365_ACCESS_TOKEN= -# MICROSOFT365_TEST_RECIPIENT_EMAIL= # email address to use as recipient in destructive send/draft/forward tests -# MICROSOFT365_TEST_ATTENDEE_EMAIL= # email address to use as attendee in find_meeting_times live test -# MICROSOFT365_TEST_SCHEDULE_EMAIL= # email address to use in get_schedule live test (e.g. user@tenant.onmicrosoft.com) +# -- Zoom -- +# ZOOM_ACCESS_TOKEN= From a230d062144f97098b7aa15c5253a2770b6e37f4 Mon Sep 17 00:00:00 2001 From: Shubhank <72601061+Sagsgit@users.noreply.github.com> Date: Thu, 18 Jun 2026 00:35:20 +0530 Subject: [PATCH 10/12] fix(.env.example): only add own integration section in correct alphabetical position --- .env.example | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index e5a423ca..3c067e29 100644 --- a/.env.example +++ b/.env.example @@ -40,6 +40,7 @@ # Optional — needed for Security Hub finding tests # AWS_FINDING_ARN= + # -- Bitly -- # BITLY_ACCESS_TOKEN= @@ -51,8 +52,9 @@ # The destructive webhook lifecycle test also needs a paid Calendly plan. # CALENDLY_ACCESS_TOKEN= -# -- Circle -- -# CIRCLE_API_TOKEN= +# -- Canva -- +# OAuth2 access token with the scopes declared in canva/config.json +# CANVA_ACCESS_TOKEN= # -- ClickUp -- # CLICKUP_ACCESS_TOKEN= @@ -64,9 +66,6 @@ # Optional: parent folder for destructive integration tests (default: /autohive_integration_test) # DROPBOX_TEST_FOLDER= -# -- Fergus -- -# FERGUS_API_TOKEN= - # -- Float -- # FLOAT_API_KEY= @@ -77,11 +76,6 @@ # -- Front -- # FRONT_ACCESS_TOKEN= -# -- Ghost -- -# GHOST_API_URL= -# GHOST_CONTENT_API_KEY= -# GHOST_ADMIN_API_KEY= - # -- GitHub -- # OAuth2 access token for read-only integration tests. # GITHUB_ACCESS_TOKEN= From 5728e5fc2cbdfab4f2c647f3b5fd808049aaafb5 Mon Sep 17 00:00:00 2001 From: Shubhank <72601061+Sagsgit@users.noreply.github.com> Date: Thu, 18 Jun 2026 00:39:57 +0530 Subject: [PATCH 11/12] fix(.env.example): rebuild from current master + own section only, normalize dash style --- .env.example | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index 3c067e29..c3586b5c 100644 --- a/.env.example +++ b/.env.example @@ -1,11 +1,11 @@ # ============================================================================ -# Autohive Integrations — environment variables for integration tests +# Autohive Integrations - environment variables for integration tests # ============================================================================ # # Copy this file to .env and fill in values for the integrations you need # to test against live APIs. The .env file is gitignored. # -# Unit tests (pytest -m unit) never need these — they use mocks. +# Unit tests (pytest -m unit) never need these - they use mocks. # Integration tests (pytest -m integration) will skip if the required # variable is missing. # @@ -15,32 +15,39 @@ # -- ActiveCampaign -- # ACTIVECAMPAIGN_API_KEY= # ACTIVECAMPAIGN_API_URL= -# Optional — targets specific resources for faster tests +# Optional - targets specific resources for faster tests # ACTIVECAMPAIGN_TEST_CAMPAIGN_ID= # ACTIVECAMPAIGN_TEST_CONTACT_ID= # -- App Business Reviews -- # APP_BUSINESS_REVIEWS_API_KEY= +# -- Asana -- +# ASANA_ACCESS_TOKEN= +# ASANA_TEST_WORKSPACE_GID= +# 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 +# Optional - STS temporary credentials # AWS_SESSION_TOKEN= -# Optional — needed for GuardDuty tests +# Optional - needed for GuardDuty tests # AWS_DETECTOR_ID= -# Optional — needed for CloudTrail tests +# Optional - needed for CloudTrail tests # AWS_TRAIL_NAME= -# Optional — needed for CloudWatch Logs tests +# Optional - needed for CloudWatch Logs tests # AWS_LOG_GROUP= # AWS_LOG_STREAM= -# Optional — needed for CloudWatch alarm tests +# Optional - needed for CloudWatch alarm tests # AWS_ALARM_NAME= -# Optional — needed for Security Hub finding tests +# Optional - needed for Security Hub finding tests # AWS_FINDING_ARN= + # -- Bitly -- # BITLY_ACCESS_TOKEN= @@ -55,6 +62,7 @@ # -- Canva -- # OAuth2 access token with the scopes declared in canva/config.json # CANVA_ACCESS_TOKEN= + # -- ClickUp -- # CLICKUP_ACCESS_TOKEN= @@ -84,7 +92,7 @@ # Required for read-only and destructive integration tests. # Token must have the https://www.googleapis.com/auth/gmail.modify scope. # GMAIL_ACCESS_TOKEN= -# Optional — pin to a specific thread/message for the reply test. +# Optional - pin to a specific thread/message for the reply test. # Otherwise the test picks the most-recent inbox message dynamically. # GMAIL_TEST_THREAD_ID= # GMAIL_TEST_MESSAGE_ID= @@ -184,12 +192,14 @@ # WHATSAPP_TEMPLATE_NAME= # WHATSAPP_TEMPLATE_LANG= # WHATSAPP_MEDIA_URL= + # -- X (formerly Twitter) -- # OAuth2 user access token with scopes: tweet.read, tweet.write, media.write, # users.read, follows.read, follows.write, like.read, bookmark.read, bookmark.write, offline.access # X_ACCESS_TOKEN= # User id to follow/unfollow in the follow lifecycle destructive test (optional) # X_TEST_TARGET_USER_ID= + # -- Xero -- # XERO_ACCESS_TOKEN= # XERO_TENANT_ID= From 479b6ab9bd8bd9dad6dc1c7b8fb14886986fb510 Mon Sep 17 00:00:00 2001 From: Shubhank <72601061+Sagsgit@users.noreply.github.com> Date: Thu, 18 Jun 2026 00:43:45 +0530 Subject: [PATCH 12/12] fix(.env.example): rebuild from current master + own section only --- .env.example | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) 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=