diff --git a/docs/source/config-clusters.rst b/docs/source/config-clusters.rst index bab8b4a4d..1757ce93a 100644 --- a/docs/source/config-clusters.rst +++ b/docs/source/config-clusters.rst @@ -325,6 +325,18 @@ Example "EC2 Instance Terminate Successful", "EC2 Instance Terminate Unsuccessful" ] + }, + "cross_account": { + "accounts": { + "123456789012": [ + "us-east-1" + ] + }, + "organizations": { + "o-aabbccddee": [ + "us-east-1" + ] + } } }, "kinesis": { @@ -341,7 +353,7 @@ Example } This creates a CloudWatch Events Rule that will publish all events that match the provided -``event_pattern`` to the Kinesis stream for this cluster. Note in the example above that a custom +``event_pattern`` to the Kinesis Stream for this cluster. Note in the example above that a custom ``event_pattern`` is supplied, but may be omitted entirely. To override the default ``event_patten`` (shown below), a value of ``None`` or ``{}`` may also be supplied to capture all events, regardless of which account the logs came from. In this case, rules should be written against @@ -354,8 +366,20 @@ Options **Key** **Default** **Description** --------------------- ----------------------------------- --------------- ``event_pattern`` ``{"account": [""]}`` The `CloudWatch Events pattern `_ to control what is sent to Kinesis +``cross_account`` ``None`` Configuration options to enable cross account access for specific AWS Accounts and Organizations. See the `Cross Account Options`_ section below for details. ===================== =================================== =============== +Cross Account Options +--------------------- +The ``cross_account`` section of the ``cloudwatch_events`` module has two subsections, outlined here. Usage of these is also shown in the example above. + +===================== =========== =============== +**Key** **Default** **Description** +--------------------- ----------- --------------- +``accounts`` ``None`` A mapping of *account IDs* and regions for which cross account access should be enabled. Example: ``{"123456789012": ["us-east-1"], "234567890123": ["us-west-2"]}`` +``organizations`` ``None`` A mapping of *organization IDs* and regions for which cross account access should be enabled. Example: ``{"o-aabbccddee": ["us-west-2"]}`` +===================== =========== =============== + .. _cloudwatch_logs: diff --git a/streamalert_cli/_infrastructure/modules/tf_cloudwatch_events/cross_account/README.md b/streamalert_cli/_infrastructure/modules/tf_cloudwatch_events/cross_account/README.md new file mode 100644 index 000000000..677e0b670 --- /dev/null +++ b/streamalert_cli/_infrastructure/modules/tf_cloudwatch_events/cross_account/README.md @@ -0,0 +1,39 @@ +# StreamAlert CloudWatch Events Cross Account Terraform Module +Configure the necessary resources to allow for cross account CloudWatch Events via EventBridge Events Bus + +## Components +* Configures CloudWatch Event Permissions to allow external accounts or organizations to send events to the main account + +## Example +```hcl +module "cloudwatch_events_cross_account" { + source = "./modules/tf_cloudwatch_events/cross_account" + accounts = ["123456789012"] + organizations = ["o-aabbccddee"] + region = "us-east-1" +} +``` + +## Inputs + + + + + + + + + + + + + + + + + + + + + +
PropertyDescriptionDefault (None=Required)
accountsAWS Account IDs for which to enable cross account CloudWatch EventsNone
organizationsAWS Organization IDs for which to enable cross account CloudWatch EventsNone
regionAWS region in which this permission is being addedNone
diff --git a/streamalert_cli/_infrastructure/modules/tf_cloudwatch_events/cross_account/main.tf b/streamalert_cli/_infrastructure/modules/tf_cloudwatch_events/cross_account/main.tf new file mode 100644 index 000000000..78a411ea3 --- /dev/null +++ b/streamalert_cli/_infrastructure/modules/tf_cloudwatch_events/cross_account/main.tf @@ -0,0 +1,19 @@ +// CloudWatch Event Permission for Individual AWS Accounts +resource "aws_cloudwatch_event_permission" "account_access" { + count = length(var.accounts) + principal = element(var.accounts, count.index) + statement_id = "account_${element(var.accounts, count.index)}_${var.region}" +} + +// CloudWatch Event Permission for AWS Orgs +resource "aws_cloudwatch_event_permission" "organization_access" { + count = length(var.organizations) + principal = "*" + statement_id = "organization_${element(var.organizations, count.index)}_${var.region}" + + condition { + key = "aws:PrincipalOrgID" + type = "StringEquals" + value = element(var.organizations, count.index) + } +} diff --git a/streamalert_cli/_infrastructure/modules/tf_cloudwatch_events/cross_account/variables.tf b/streamalert_cli/_infrastructure/modules/tf_cloudwatch_events/cross_account/variables.tf new file mode 100644 index 000000000..147cbed7b --- /dev/null +++ b/streamalert_cli/_infrastructure/modules/tf_cloudwatch_events/cross_account/variables.tf @@ -0,0 +1,11 @@ +variable "accounts" { + type = list(string) +} + +variable "organizations" { + type = list(string) +} + +variable "region" { + type = string +} diff --git a/streamalert_cli/_infrastructure/modules/tf_cloudwatch_events/main.tf b/streamalert_cli/_infrastructure/modules/tf_cloudwatch_events/main.tf index b6062c441..6330b00af 100644 --- a/streamalert_cli/_infrastructure/modules/tf_cloudwatch_events/main.tf +++ b/streamalert_cli/_infrastructure/modules/tf_cloudwatch_events/main.tf @@ -1,4 +1,4 @@ -// Cloudwatch event to capture Cloudtrail API calls +// Cloudwatch Event Rule: Capture CloudWatch Events resource "aws_cloudwatch_event_rule" "capture_events" { name = "${var.prefix}_${var.cluster}_streamalert_all_events" description = "Capture CloudWatch events" @@ -13,8 +13,9 @@ resource "aws_cloudwatch_event_rule" "capture_events" { // The Kinesis destination for Cloudwatch events resource "aws_cloudwatch_event_target" "kinesis" { - rule = aws_cloudwatch_event_rule.capture_events.name - arn = var.kinesis_arn + target_id = "${var.prefix}_${var.cluster}_streamalert_kinesis" + rule = aws_cloudwatch_event_rule.capture_events.name + arn = var.kinesis_arn } // IAM Role: CloudWatch Events diff --git a/streamalert_cli/terraform/cloudwatch_events.py b/streamalert_cli/terraform/cloudwatch_events.py index bc1b8a063..2fd258dca 100644 --- a/streamalert_cli/terraform/cloudwatch_events.py +++ b/streamalert_cli/terraform/cloudwatch_events.py @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. """ +from collections import defaultdict import json from streamalert.shared.logger import get_logger @@ -71,4 +72,60 @@ def generate_cloudwatch_events(cluster_name, cluster_dict, config): 'event_pattern': json.dumps(event_pattern) if event_pattern is not None else event_pattern } + cross_account_settings = settings.get('cross_account') + if not cross_account_settings: + return True + + region_map = _map_regions(cross_account_settings) + for region, values in region_map.items(): + tf_module_name = 'cloudwatch_events_cross_account_{}_{}'.format(cluster_name, region) + cluster_dict['module'][tf_module_name] = { + 'source': './modules/tf_cloudwatch_events/cross_account', + 'region': region, + 'accounts': sorted(values.get('accounts', [])), + 'organizations': sorted(values.get('organizations', [])), + 'providers': { + # use the aliased provider for this region from providers.tf + 'aws': 'aws.{}'.format(region) + } + } + return True + + +def _map_regions(settings): + """Reverse the mapping of accounts/orgs <> regions to make it nicer for terraform to use + + Args: + settings (dict): Mapping or accounts/orgs to regions + Example: + { + 'accounts': { + '123456789012': ['us-east-1'], + '234567890123': ['us-east-1'] + }, + 'organizations': { + 'o-aabbccddee': ['us-west-1'] + } + } + + Returns: + dict: An inverse mapping of regions <> accounts/orgs + Example: + { + 'us-east-1': { + 'accounts': ['123456789012', '234567890123'], + }, + 'us-west-1': { + 'organizations': ['o-aabbccddee'] + } + } + """ + region_map = defaultdict(dict) + for scope in ['accounts', 'organizations']: + for aws_id, regions in settings.get(scope, {}).items(): + for region in regions: + region_map[region] = region_map.get(region, defaultdict(list)) + region_map[region][scope].append(aws_id) + + return region_map diff --git a/tests/unit/streamalert_cli/terraform/test_generate.py b/tests/unit/streamalert_cli/terraform/test_generate.py index 3e3589d2d..6a216c1f3 100644 --- a/tests/unit/streamalert_cli/terraform/test_generate.py +++ b/tests/unit/streamalert_cli/terraform/test_generate.py @@ -697,6 +697,79 @@ def test_generate_cloudwatch_events_invalid_pattern(self, log_mock): assert_true(log_mock.called) + def test_generate_cwe_cross_acct_map_regions(self): + """CLI - Terraform Generate CloudWatch Events Cross Account Region Map""" + # pylint: disable=protected-access + settings = { + 'accounts': { + '123456789012': ['us-east-1'], + '234567890123': ['us-east-1'] + }, + 'organizations': { + 'o-aabbccddee': ['us-west-1'] + } + } + + result = cloudwatch_events._map_regions(settings) + + expected = { + 'us-east-1': { + 'accounts': ['123456789012', '234567890123'], + }, + 'us-west-1': { + 'organizations': ['o-aabbccddee'] + } + } + + assert_equal(expected, result) + + def test_generate_cloudwatch_events_cross_account(self): + """CLI - Terraform Generate CloudWatch Events Cross Account""" + self.config['clusters']['advanced']['modules']['cloudwatch_events']['cross_account'] = { + 'accounts': { + '123456789012': ['us-east-1'], + '234567890123': ['us-east-1'] + }, + 'organizations': { + 'o-aabbccddee': ['us-west-1'] + } + } + cloudwatch_events.generate_cloudwatch_events( + 'advanced', + self.cluster_dict, + self.config + ) + + expected = { + 'cloudwatch_events_advanced': { + 'source': './modules/tf_cloudwatch_events', + 'prefix': 'unit-test', + 'cluster': 'advanced', + 'kinesis_arn': '${module.kinesis_advanced.arn}', + 'event_pattern': '{"account": ["12345678910"]}', + }, + 'cloudwatch_events_cross_account_advanced_us-east-1': { + 'source': './modules/tf_cloudwatch_events/cross_account', + 'region': 'us-east-1', + 'accounts': ['123456789012', '234567890123'], + 'organizations': [], + 'providers': { + 'aws': 'aws.us-east-1' + } + }, + 'cloudwatch_events_cross_account_advanced_us-west-1': { + 'source': './modules/tf_cloudwatch_events/cross_account', + 'region': 'us-west-1', + 'accounts': [], + 'organizations': ['o-aabbccddee'], + 'providers': { + 'aws': 'aws.us-west-1' + } + }, + } + + assert_equal(expected, self.cluster_dict['module']) + def test_generate_cluster_test(self): """CLI - Terraform Generate Test Cluster"""