Skip to content

Commit

Permalink
Merge a251346 into 64196ef
Browse files Browse the repository at this point in the history
  • Loading branch information
ryandeivert committed Jul 28, 2020
2 parents 64196ef + a251346 commit 3bf6e88
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 4 deletions.
26 changes: 25 additions & 1 deletion docs/source/config-clusters.rst
Expand Up @@ -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": {
Expand All @@ -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
Expand All @@ -354,8 +366,20 @@ Options
**Key** **Default** **Description**
--------------------- ----------------------------------- ---------------
``event_pattern`` ``{"account": ["<accound_id>"]}`` The `CloudWatch Events pattern <http://docs.aws.amazon.com/AmazonCloudWatch/latest/events/EventTypes.html>`_ 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:

Expand Down
@@ -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
<table>
<tr>
<th>Property</th>
<th>Description</th>
<th>Default (None=Required)</th>
</tr>
<tr>
<td>accounts</td>
<td>AWS Account IDs for which to enable cross account CloudWatch Events</td>
<td>None</td>
</tr>
<tr>
<td>organizations</td>
<td>AWS Organization IDs for which to enable cross account CloudWatch Events</td>
<td>None</td>
</tr>
<tr>
<td>region</td>
<td>AWS region in which this permission is being added</td>
<td>None</td>
</tr>
</table>
@@ -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)
}
}
@@ -0,0 +1,11 @@
variable "accounts" {
type = list(string)
}

variable "organizations" {
type = list(string)
}

variable "region" {
type = string
}
@@ -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"
Expand All @@ -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
Expand Down
57 changes: 57 additions & 0 deletions streamalert_cli/terraform/cloudwatch_events.py
Expand Up @@ -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
Expand Down Expand Up @@ -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
73 changes: 73 additions & 0 deletions tests/unit/streamalert_cli/terraform/test_generate.py
Expand Up @@ -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"""

Expand Down

0 comments on commit 3bf6e88

Please sign in to comment.