AWS Security Hub Event Collector
An XSIAM event collector integration for AWS Security Hub.
Analytics & SIEM · AWS - Security Hub
Details
| ID | AWS Security Hub Event Collector |
|---|---|
| Provider | Amazon |
| Category | Analytics & SIEM |
| From Version | 6.8.0 |
| Docker Image | demisto/boto3py3:1.0.0.10221838 |
| Supported Modules | Agentix XSIAM |
README
An XSIAM event collector for AWS Security Hub.
This is the default integration for this content pack when configured by the Data Onboarder in Cortex XSIAM.
Configure AWS Security Hub Event Collector in Cortex
| Parameter | Description | Required |
|---|---|---|
| AWS Default Region | True | |
| Role Arn | False | |
| Access Key | False | |
| Secret Key | False | |
| First fetch time (<number> <time unit>, e.g., 12 hours, 7 days, 3 months, 1 year) | False | |
| Role Session Name | False | |
| Role Session Duration | False | |
| Max events per fetch | The maximum number of events to retrieve for each event type (up to 10000 events). For more information about event types, see the help section. | False |
| Timeout | The time in seconds until a timeout exception is reached. You can specify just the read timeout (for example 60) or also the connect timeout followed after a comma (for example 60,10). If a connect timeout is not specified, a default of 10 second will be used. | False |
| Retries | The maximum number of retry attempts when connection or throttling errors are encountered. Set to 0 to disable retries. The default value is 5 and the limit is 10. Note: Increasing the number of retries will increase the execution time. | False |
| Trust any certificate (not secure) | False | |
| Use system proxy settings | False |
Commands
aws-securityhub-get-events
Fetch events from AWS Security Hub.
Base Command
aws-securityhub-get-events
Input
| Argument Name | Description | Required |
|---|---|---|
| should_push_events | If true, the command will create events, otherwise it will only display them. Possible values are: true, false. Default is false. | Required |
| limit | Maximum results to return. | Optional |
| start_time | Start time for filtering events (e.g., 2023-01-01T00:00:00Z, ‘3 days ago’, ‘yesterday’). | Optional |
| end_time | End time for filtering events (e.g., 2023-01-01T23:59:59Z, ‘1 hour ago’, ‘now’). | Optional |
Configuration parameters
default_region— AWS Default Region (required)role_arn— Role Arncredentials— Access Keyfirst_fetch— First fetch time (<number> <time unit>, e.g., 12 hours, 7 days, 3 months, 1 year)role_session_name— Role Session Namerole_session_duration— Role Session Durationmax_fetch— Max events per fetchtimeout— Timeoutretries— Retriessts_regional_endpoint— AWS STS Regional Endpointsinsecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (1)
-
aws-securityhub-get-eventsFetch events from AWS Security Hub.
from pathlib import Path import datetime as dt import pytest from freezegun import freeze_time from AWSSecurityHubEventCollector import * # Import test findings data from dedicated test data file from test_data.test_constants import SAMPLE_FINDINGS, IGNORED_FINDINGS # Test configuration constants - moved to top for parametrized test FIXED_END_TIME = dt.datetime(2023, 1, 1, 13, 0, 0) FIRST_FETCH_TIME = dt.datetime(2023, 1, 1, 10, 0, 0) TEST_START_TIME = dt.datetime(2023, 1, 1, 10, 0, 0) TEST_END_TIME = dt.datetime(2023, 1, 1, 14, 0, 0) # Common ignore list patterns used in multiple tests STANDARD_IGNORE_LIST = ["ignore-me", "also-ignore"] DUPLICATE_IGNORE_LIST = ["ignore-1", "ignore-2"] def load_test_data(folder: str, file_name: str) -> dict: """ A function for loading and returning data from json files within the "test_data" folder. Args: folder (str): Name of the parent folder of the file within `test_data`. file_name (str): Name of a json file to load data from. Returns: dict: Dictionary data loaded from the json file. """ with open(Path("test_data") / folder / f"{file_name}.json") as f: return json.load(f) class MockClient: """ A client mocking Boto3.client """ findings_data = load_test_data("api_mock", "get_findings_10") calls_count = 0 last_index = 0 # Used to mock pagination def __init__(self, *args, **kwargs): pass def get_findings(self, **kwargs) -> dict: """ A function for mocking the get_findings function. Args: kwargs: Keyword arguments that would be passed to the get_findings function. """ max_index = self.last_index + kwargs["MaxResults"] return_events = self.findings_data[self.last_index : max_index] self.last_index += len(return_events) result = {"Findings": return_events} if self.last_index < len(self.findings_data): result["NextToken"] = "next_token" self.calls_count += 1 return result def reset(self): """ A function for resetting the mock client. """ self.calls_count = 0 self.last_index = 0 @pytest.fixture def client(): """ A fixture for creating a mock client. """ return MockClient() @pytest.mark.parametrize( "page_size, limit, expected_api_calls_count, expected_output_file", [ (100, 1000, 1, "fetch_events_expected_results_0"), (2, 1000, 5, "fetch_events_expected_results_0"), (3, 1000, 4, "fetch_events_expected_results_0"), (2, 5, 3, "fetch_events_expected_results_1"), (100, 5, 1, "fetch_events_expected_results_1"), ], ) def test_fetch(client, page_size: int, limit: int, expected_api_calls_count: int, expected_output_file: str): """ Given: A page size parameter for the fetch events function. When: Fetching events from the API. Then: Assert the returned events are valid, and the number of API calls is as expected. Note: This is a test for both 'fetch_events' and 'get_events_command' functions. """ expected_output = load_test_data("expected_results", expected_output_file) first_fetch_time = dt.datetime(2021, 1, 1) events, _, _ = fetch_events(client=client, last_run={}, first_fetch_time=first_fetch_time, page_size=page_size, limit=limit) assert client.calls_count == expected_api_calls_count assert len(events) == len(expected_output) assert events == expected_output client.reset() result = get_events_command( client=client, should_push_events=False, page_size=page_size, limit=limit, start_time=None, end_time=None ) assert client.calls_count == expected_api_calls_count assert result.readable_output == tableToMarkdown("AWS Security Hub Events", expected_output, sort_headers=False) class TestParseAwsTimestamp: """Test the new flexible timestamp parsing functionality.""" @pytest.mark.parametrize( "timestamp_str, expected_result", [ # Valid timestamp with milliseconds ("2023-01-01T12:30:45.123Z", dt.datetime(2023, 1, 1, 12, 30, 45, 123000)), # Valid timestamp without milliseconds ("2023-01-01T12:30:45Z", dt.datetime(2023, 1, 1, 12, 30, 45)), ], ) def test_parse_timestamp_valid_formats(self, timestamp_str, expected_result): """ Given: A valid timestamp string from AWS API (with or without milliseconds). When: Parsing the timestamp using parse_aws_timestamp. Then: Should return the correct datetime object. """ result = parse_aws_timestamp(timestamp_str) assert result == expected_result def test_parse_timestamp_invalid_format(self): """ Given: An invalid timestamp string. When: Parsing the timestamp using parse_aws_timestamp. Then: Should raise ValueError. """ invalid_timestamp = "2023/01/01 12:30:45" with pytest.raises(ValueError): parse_aws_timestamp(invalid_timestamp) class TestGenerateLastRunWithSmartAccumulation: """Test the smart ignore list accumulation functionality.""" def test_generate_last_run_same_timestamp_accumulates_ignore_list(self): """ Given: Events with same timestamp as previous run. When: Generating last run with previous_last_run parameter. Then: Should accumulate ignore list instead of replacing it. """ events = [ {"CreatedAt": "2023-01-01T12:00:00.000Z", "Id": "finding-3", "_time": "2023-01-01T12:00:00.000Z"}, {"CreatedAt": "2023-01-01T12:00:00.000Z", "Id": "finding-4", "_time": "2023-01-01T12:00:00.000Z"}, ] previous_last_run = { "last_update_date": "2023-01-01T12:00:00.000Z", "last_update_date_finding_ids": ["finding-1", "finding-2"], } result = generate_last_run(events, previous_last_run) assert result["last_update_date"] == "2023-01-01T12:00:00.000Z" assert len(result["last_update_date_finding_ids"]) == 4 assert "finding-1" in result["last_update_date_finding_ids"] assert "finding-2" in result["last_update_date_finding_ids"] assert "finding-3" in result["last_update_date_finding_ids"] assert "finding-4" in result["last_update_date_finding_ids"] def test_generate_last_run_new_timestamp_starts_fresh(self): """ Given: Events with new timestamp different from previous run. When: Generating last run with previous_last_run parameter. Then: Should start fresh ignore list instead of accumulating. """ events = [ {"CreatedAt": "2023-01-01T13:00:00.000Z", "Id": "finding-3", "_time": "2023-01-01T13:00:00.000Z"}, {"CreatedAt": "2023-01-01T13:00:00.000Z", "Id": "finding-4", "_time": "2023-01-01T13:00:00.000Z"}, ] previous_last_run = { "last_update_date": "2023-01-01T12:00:00.000Z", "last_update_date_finding_ids": ["finding-1", "finding-2"], } result = generate_last_run(events, previous_last_run) assert result["last_update_date"] == "2023-01-01T13:00:00.000Z" assert len(result["last_update_date_finding_ids"]) == 2 assert "finding-3" in result["last_update_date_finding_ids"] assert "finding-4" in result["last_update_date_finding_ids"] assert "finding-1" not in result["last_update_date_finding_ids"] assert "finding-2" not in result["last_update_date_finding_ids"] def test_generate_last_run_no_previous_run(self): """ Given: Events and no previous run data When: Generating last run Then: Should create fresh ignore list """ # Given events = [ {"CreatedAt": "2023-01-01T12:00:00.000Z", "Id": "finding-1", "_time": "2023-01-01T12:00:00.000Z"}, {"CreatedAt": "2023-01-01T12:00:00.000Z", "Id": "finding-2", "_time": "2023-01-01T12:00:00.000Z"}, ] # When result = generate_last_run(events, None) # Then assert result["last_update_date"] == "2023-01-01T12:00:00.000Z" assert len(result["last_update_date_finding_ids"]) == 2 assert "finding-1" in result["last_update_date_finding_ids"] assert "finding-2" in result["last_update_date_finding_ids"] @pytest.mark.parametrize( ( "scenario, mock_api_responses, start_time, end_time, id_ignore_list, " "page_size, limit, start_token, expected_events_count, expected_next_token, expected_api_calls, expected_debug_messages" ), [ ( "single_page_no_pagination", # Mock API returns findings in single call, no NextToken [{"Findings": SAMPLE_FINDINGS[:3]}], TEST_START_TIME, # start_time TEST_END_TIME, # end_time None, # id_ignore_list 50, # page_size 0, # limit (no limit) None, # start_token 3, # expected_events_count None, # expected_next_token 1, # expected_api_calls (single page) [ "Starting get_events pagination", "No more pages available from AWS", "Pagination completed after", ], # expected_debug_messages ), ( "multiple_pages_with_pagination", # Mock API returns findings across multiple calls with NextToken [ {"Findings": SAMPLE_FINDINGS[:2], "NextToken": "page-2-token"}, {"Findings": SAMPLE_FINDINGS[2:4], "NextToken": "page-3-token"}, {"Findings": SAMPLE_FINDINGS[4:5]}, ], TEST_START_TIME, # start_time None, # end_time (use current time) None, # id_ignore_list 2, # page_size (forces pagination) 5, # limit None, # start_token 5, # expected_events_count None, # expected_next_token (no more pages) 3, # expected_api_calls (3 pages: 2+2+1 events) [ # expected_debug_messages "Starting get_events pagination", "Continuing pagination", "Pagination completed after", ], ), ( "with_ignore_list_filters_duplicates", # Mock API returns findings, some filtered by ignore list [{"Findings": [SAMPLE_FINDINGS[0], IGNORED_FINDINGS[0], SAMPLE_FINDINGS[2], IGNORED_FINDINGS[1]]}], TEST_START_TIME, # start_time TEST_END_TIME, # end_time STANDARD_IGNORE_LIST, # id_ignore_list 50, # page_size 10, # limit None, # start_token 2, # expected_events_count (2 not ignored) None, # expected_next_token 1, # expected_api_calls (single page, filtering happens after) ["Starting get_events pagination", "2 duplicates filtered", "Pagination completed after"], # expected_debug_messages ), ( "limit_stops_pagination_early", # Mock API has more pages but limit stops collection [ {"Findings": SAMPLE_FINDINGS[:2], "NextToken": "has-more-pages"}, {"Findings": SAMPLE_FINDINGS[2:3], "NextToken": "still-more-pages"}, ], None, # start_time (no time filter) None, # end_time None, # id_ignore_list 2, # page_size 3, # limit (stops after 3 events) None, # start_token 3, # expected_events_count "still-more-pages", # expected_next_token (stopped due to limit) 2, # expected_api_calls (2 pages: 2+1 events, stops at limit) [ # expected_debug_messages "Starting get_events pagination", "Reached limit", "Pagination completed after", ], ), ( "continuation_from_start_token", # Mock API continues from provided start_token [{"Findings": SAMPLE_FINDINGS[3:5]}], TEST_START_TIME, # start_time TEST_END_TIME, # end_time None, # id_ignore_list 50, # page_size 0, # limit "continue-from-here", # start_token 2, # expected_events_count None, # expected_next_token 1, # expected_api_calls (single page from token) [ "Starting get_events pagination", "No more pages available from AWS", "Pagination completed after", ], # expected_debug_messages ), ( "empty_response_no_events", # Mock API returns no findings [{"Findings": []}], TEST_START_TIME, # start_time TEST_END_TIME, # end_time None, # id_ignore_list 50, # page_size 10, # limit None, # start_token 0, # expected_events_count None, # expected_next_token 1, # expected_api_calls (single empty response) [ # expected_debug_messages "Starting get_events pagination", "AWS returned 0 findings", "Pagination completed after", ], ), ( "all_events_filtered_by_ignore_list", # Mock API returns findings but all are in ignore list [{"Findings": IGNORED_FINDINGS[2:4]}], None, # start_time (no time filter) None, # end_time DUPLICATE_IGNORE_LIST, # id_ignore_list 50, # page_size 0, # limit None, # start_token 0, # expected_events_count (all filtered) None, # expected_next_token 1, # expected_api_calls (single page, all filtered after) ["Starting get_events pagination", "2 duplicates filtered", "Pagination completed after"], # expected_debug_messages ), ], ) @freeze_time("2023-01-01 15:00:00") def test_get_events__end_to_end( mocker, scenario, mock_api_responses, start_time, end_time, id_ignore_list, page_size, limit, start_token, expected_events_count, expected_next_token, expected_api_calls, expected_debug_messages, ): """ Test end-to-end get_events functionality covering all scenarios: Case 1 (single_page_no_pagination): Single API call returns all findings, no NextToken pagination Case 2 (multiple_pages_with_pagination): Multiple API calls with NextToken pagination until completion Case 3 (with_ignore_list_filters_duplicates): Ignore list properly filters out specified finding IDs Case 4 (limit_stops_pagination_early): Limit parameter stops pagination before all pages processed Case 5 (continuation_from_start_token): Pagination continues from provided start_token Case 6 (empty_response_no_events): API returns empty findings list gracefully Case 7 (all_events_filtered_by_ignore_list): All findings filtered out by ignore list Given: - A mocked AWS SecurityHub client that returns predefined API response sequences - Various time ranges, ignore lists, page sizes, limits, and pagination tokens - Mocked demisto.debug for logging verification - Frozen time at 2023-01-01 15:00:00 for consistent datetime.now() calls - Expected API call counts and specific debug messages for each scenario When: - Calling get_events with different parameter combinations and scenarios Then: - Ensure correct number of events are returned after deduplication and filtering - Ensure pagination works correctly with NextToken handling and limit enforcement - Ensure ignore list properly filters duplicate finding IDs - Ensure time filtering parameters are correctly passed to AWS API - Ensure continuation tokens work for resuming pagination from specific points - Ensure correct number of API calls are made (pagination iteration validation) - Ensure scenario-specific debug messages are logged correctly - Ensure events have proper structure (Id and CreatedAt fields) """ # Mock demisto.debug to capture logging mock_debug = mocker.patch("AWSSecurityHubEventCollector.demisto.debug") # Create mock client with sequential API responses mock_client = mocker.Mock() mock_client.get_findings.side_effect = mock_api_responses # Call get_events events, next_token = get_events( client=mock_client, start_time=start_time, end_time=end_time, id_ignore_list=id_ignore_list, page_size=page_size, limit=limit, start_token=start_token, ) # Verify events count assert len(events) == expected_events_count # Verify next_token assert next_token == expected_next_token # Verify expected number of API calls (pagination iterations) assert mock_client.get_findings.call_count == expected_api_calls # Verify first API call parameters first_call = mock_client.get_findings.call_args_list[0] call_kwargs = first_call.kwargs # Check sorting assert call_kwargs["SortCriteria"] == [{"Field": "CreatedAt", "SortOrder": "asc"}] # Check time filters expected_filters = {} expected_end_time = end_time or dt.datetime(2023, 1, 1, 15, 0, 0) # frozen time expected_start_time_str = start_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ") if start_time else None expected_end_time_str = expected_end_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ") if start_time: expected_filters["CreatedAt"] = [{"Start": expected_start_time_str, "End": expected_end_time_str}] assert call_kwargs["Filters"] == expected_filters else: # No Filters key when no start_time assert "Filters" not in call_kwargs # Check pagination token if start_token: assert call_kwargs["NextToken"] == start_token else: assert "NextToken" not in call_kwargs # Check page size (MaxResults) expected_max_results = page_size if limit == 0 or page_size <= limit else min(page_size, limit) assert call_kwargs["MaxResults"] == expected_max_results # Verify ignore list filtering worked correctly if id_ignore_list: returned_ids = {event["Id"] for event in events} ignored_ids = set(id_ignore_list) assert returned_ids.isdisjoint(ignored_ids), "Ignored IDs should not appear in results" # Verify events have proper structure for event in events: assert "Id" in event assert "CreatedAt" in event # Verify expected debug messages are present debug_calls = [call.args[0] for call in mock_debug.call_args_list] for expected_msg in expected_debug_messages: assert any( expected_msg in msg for msg in debug_calls ), f"Expected debug message '{expected_msg}' not found in: {debug_calls}" # Test findings data and constants imported at top of file @pytest.fixture def fetch_events_mocks(mocker): """Common mocks for fetch_events tests.""" mocks = { "debug": mocker.patch("AWSSecurityHubEventCollector.demisto.debug"), "info": mocker.patch("AWSSecurityHubEventCollector.demisto.info"), "error": mocker.patch("AWSSecurityHubEventCollector.demisto.error"), "generate_last_run": mocker.patch("AWSSecurityHubEventCollector.generate_last_run"), "parse_timestamp": mocker.patch("AWSSecurityHubEventCollector.parse_aws_timestamp"), "datetime_now": mocker.patch("AWSSecurityHubEventCollector.dt.datetime"), "get_events": mocker.patch("AWSSecurityHubEventCollector.get_events"), "client": mocker.Mock(), } # Configure common mock behaviors mocks["datetime_now"].now.return_value = FIXED_END_TIME mocks["parse_timestamp"].side_effect = lambda ts: dt.datetime.strptime(ts, "%Y-%m-%dT%H:%M:%S.%fZ") return mocks def _verify_debug_messages(debug_calls, info_calls, expected_debug=None, expected_info=None): """Helper to verify debug/info messages.""" # Always check for initial fetch message assert any("Fetching events with last_run:" in msg for msg in debug_calls) if expected_debug: for msg in expected_debug: assert any(msg in call for call in debug_calls), f"Expected debug message '{msg}' not found" if expected_info: for msg in expected_info: assert any(msg in call for call in info_calls), f"Expected info message '{msg}' not found" def test_fetch_events_first_fetch_success(fetch_events_mocks): """ Test fetch_events for first fetch scenario: empty last_run with first_fetch_time. Given: Empty last_run and valid first_fetch_time When: Calling fetch_events Then: Should fetch new events and create proper next_run structure """ mocks = fetch_events_mocks # Configure test-specific mocks mocks["get_events"].return_value = (SAMPLE_FINDINGS[:2], None) next_run = { "last_update_date": "2023-01-01T12:01:00.000Z", "last_update_date_finding_ids": ["finding-2"], } mocks["generate_last_run"].return_value = next_run # Call fetch_events events, actual_next_run, error = fetch_events( client=mocks["client"], last_run={}, first_fetch_time=FIRST_FETCH_TIME, page_size=50, limit=0 ) # Verify results assert len(events) == 2 assert error is None assert "last_update_date" in str(actual_next_run) assert "finding-2" in str(actual_next_run) # Verify get_events was called correctly assert mocks["get_events"].call_count == 1 call_kwargs = mocks["get_events"].call_args_list[0].kwargs assert call_kwargs["client"] == mocks["client"] assert call_kwargs["page_size"] == 50 assert call_kwargs["end_time"] == FIXED_END_TIME # Fixed end_time consistency assert call_kwargs["id_ignore_list"] == [] # Empty ignore list for first fetch # Verify generate_last_run was called mocks["generate_last_run"].assert_called_once_with(events, {}) # Verify debug messages debug_calls = [call.args[0] for call in mocks["debug"].call_args_list] info_calls = [call.args[0] for call in mocks["info"].call_args_list] _verify_debug_messages( debug_calls, info_calls, expected_debug=["Successfully fetched 2 new events"], expected_info=["Fetched 2 findings."] ) def test_fetch_events_infinite_loop_prevention(fetch_events_mocks): """ Test fetch_events infinite loop prevention: all duplicates but AWS has more events. Given: All events are duplicates but AWS has continuation token When: Calling fetch_events Then: Should make retry with increased limit, then exit when no more events """ mocks = fetch_events_mocks # Configure test-specific mocks for infinite loop scenario mocks["get_events"].side_effect = [ ([], "continuation-token"), # First call: all duplicates, has next token -> triggers retry ([], None), # Second call: still no events, no more pages -> exits loop ] mocks["generate_last_run"].return_value = { "last_update_date": "2023-01-01T12:00:00.000Z", "last_update_date_finding_ids": ["duplicate-1"], } # Call fetch_events events, next_run, error = fetch_events( client=mocks["client"], last_run={"last_update_date": "2023-01-01T12:00:00.000Z", "last_update_date_finding_ids": ["duplicate-1"]}, first_fetch_time=None, page_size=50, limit=10, ) # Verify results assert len(events) == 0 # No new events (all duplicates) assert error is None # Verify loop behavior: should make 2 calls assert mocks["get_events"].call_count == 2 # Verify second call has increased limit (RETRY_LIMIT_INCREMENT = 100) second_call = mocks["get_events"].call_args_list[1] expected_retry_limit = 100 # RETRY_LIMIT_INCREMENT value from code assert second_call.kwargs["limit"] == expected_retry_limit # Verify both calls use same fixed end_time for call_args in mocks["get_events"].call_args_list: assert call_args.kwargs["end_time"] == FIXED_END_TIME # Verify debug messages debug_calls = [call.args[0] for call in mocks["debug"].call_args_list] info_calls = [call.args[0] for call in mocks["info"].call_args_list] _verify_debug_messages( debug_calls, info_calls, expected_debug=["All events were duplicates and AWS has no more events"], expected_info=["No new findings were found.", "Infinite loop prevention"], ) def test_fetch_events_api_error_handling(fetch_events_mocks): """ Test fetch_events error handling: API raises exception. Given: get_events raises an exception When: Calling fetch_events Then: Should capture error and return it without raising """ mocks = fetch_events_mocks # Configure test-specific mocks for error scenario test_exception = Exception("AWS API Error: Rate limit exceeded") mocks["get_events"].side_effect = test_exception mocks["generate_last_run"].return_value = {"last_update_date": "2023-01-01T12:00:00.000Z", "last_update_date_finding_ids": []} # Call fetch_events events, next_run, error = fetch_events( client=mocks["client"], last_run={"last_update_date": "2023-01-01T12:00:00.000Z", "last_update_date_finding_ids": []}, first_fetch_time=None, page_size=50, limit=10, ) # Verify error handling assert len(events) == 0 # No events due to error assert error is not None assert isinstance(error, Exception) assert str(error) == "AWS API Error: Rate limit exceeded" # Verify error logging error_calls = [call.args[0] for call in mocks["error"].call_args_list] assert any("Error while fetching events" in msg for msg in error_calls)