Repository: cloud-custodian/cloud-custodian

Issue:
account: enable cloudtrail action



Discussion:
Enable = start logging?
Comment from @kapilt on a dead pull request:

> yeah, my bad, it was intended to put the trail and recorder in place if it had been deleted, or to enable if it had been disabled.

I’m revisiting this. If we need to put one in place, we need to name it. We also need to create (and name) an S3 bucket for it. Any preferences for those names?
I'd defer to policy for any resource details, most folks set this up during account creation, we're effectively doing this so we can have realtime remediation if folks disable or delete an extant trail somehow.

Resolving patch:
diff --git a/c7n/resources/account.py b/c7n/resources/account.py
--- a/c7n/resources/account.py
+++ b/c7n/resources/account.py
@@ -14,6 +14,8 @@
 """AWS Account as a custodian resource.
 """
 
+import json
+from botocore.exceptions import ClientError
 from datetime import datetime, timedelta
 
 from dateutil.parser import parse as parse_date
@@ -22,6 +24,7 @@
 from botocore.exceptions import ClientError
 
 from c7n.actions import ActionRegistry
+from c7n.actions import BaseAction
 from c7n.filters import Filter, FilterRegistry, ValueFilter
 from c7n.manager import ResourceManager, resources
 from c7n.utils import local_session, type_schema
@@ -434,3 +437,145 @@ def process(self, resources, event=None):
             resources[0]['c7n:ServiceLimitsExceeded'] = exceeded
             return resources
         return []
+
+
+def cloudtrail_policy(original, bucket_name, account_id):
+    '''add CloudTrail permissions to an S3 policy, preserving existing'''
+    ct_actions = [
+        {
+            'Action': 's3:GetBucketAcl',
+            'Effect': 'Allow',
+            'Principal': {'Service': 'cloudtrail.amazonaws.com'},
+            'Resource': 'arn:aws:s3:::' + bucket_name,
+            'Sid': 'AWSCloudTrailAclCheck20150319',
+        },
+        {
+            'Action': 's3:PutObject',
+            'Condition': {
+                'StringEquals':
+                {'s3:x-amz-acl': 'bucket-owner-full-control'},
+            },
+            'Effect': 'Allow',
+            'Principal': {'Service': 'cloudtrail.amazonaws.com'},
+            'Resource': 'arn:aws:s3:::%s/AWSLogs/%s/*' % (
+                bucket_name, account_id
+            ),
+            'Sid': 'AWSCloudTrailWrite20150319',
+        },
+    ]
+    # parse original policy
+    if original is None:
+        policy = {
+            'Statement': [],
+            'Version': '2012-10-17',
+        }
+    else:
+        policy = json.loads(original['Policy'])
+    original_actions = [a.get('Action') for a in policy['Statement']]
+    for cta in ct_actions:
+        if cta['Action'] not in original_actions:
+            policy['Statement'].append(cta)
+    return json.dumps(policy)
+
+
+@actions.register('enable-cloudtrail')
+class EnableTrail(BaseAction):
+    """Enables logging on the trail(s) named in the policy
+
+    :Example:
+
+    .. code-block: yaml
+
+        policies:
+          - name: trail-test
+            description: Ensure CloudTrail logging is enabled
+            resource: account
+            actions:
+              - type: enable-cloudtrail
+                trail: mytrail
+                bucket: trails
+    """
+
+    permissions = (
+        'cloudtrail:CreateTrail',
+        'cloudtrail:DescribeTrails',
+        'cloudtrail:GetTrailStatus',
+        'cloudtrail:StartLogging',
+        'cloudtrail:UpdateTrail',
+        's3:CreateBucket',
+        's3:GetBucketPolicy',
+        's3:PutBucketPolicy',
+    )
+    schema = type_schema(
+        'enable-cloudtrail',
+        **{
+            'trail': {'type': 'string'},
+            'bucket': {'type': 'string'},
+            'multi-region': {'type': 'boolean'},
+            'global-events': {'type': 'boolean'},
+            'notify': {'type': 'string'},
+            'file-digest': {'type': 'boolean'},
+            'kms': {'type': 'boolean'},
+            'kms-key': {'type': 'string'},
+            'required': ('bucket',),
+        }
+    )
+
+    def process(self, accounts):
+        """Create or enable CloudTrail"""
+        session = local_session(self.manager.session_factory)
+        client = session.client('cloudtrail')
+        bucket_name = self.data['bucket']
+        trail_name = self.data.get('trail', 'default-trail')
+        multi_region = self.data.get('multi-region', True)
+        global_events = self.data.get('global-events', True)
+        notify = self.data.get('notify', '')
+        file_digest = self.data.get('file-digest', False)
+        kms = self.data.get('kms', False)
+        kms_key = self.data.get('kms-key', '')
+
+        s3client = session.client('s3')
+        s3client.create_bucket(Bucket=bucket_name)
+        try:
+            current_policy = s3client.get_bucket_policy(Bucket=bucket_name)
+        except ClientError:
+            current_policy = None
+
+        policy_json = cloudtrail_policy(
+            current_policy, bucket_name, self.manager.config.account_id)
+
+        s3client.put_bucket_policy(Bucket=bucket_name, Policy=policy_json)
+        trails = client.describe_trails().get('trailList', ())
+        if trail_name not in [t.get('Name') for t in trails]:
+            new_trail = client.create_trail(
+                Name=trail_name,
+                S3BucketName=bucket_name,
+            )
+            if new_trail:
+                trails.append(new_trail)
+                # the loop below will configure the new trail
+        for trail in trails:
+            if trail.get('Name') != trail_name:
+                continue
+            # enable
+            arn = trail['TrailARN']
+            status = client.get_trail_status(Name=arn)
+            if not status['IsLogging']:
+                client.start_logging(Name=arn)
+            # apply configuration changes (if any)
+            update_args = {}
+            if multi_region != trail.get('IsMultiRegionTrail'):
+                update_args['IsMultiRegionTrail'] = multi_region
+            if global_events != trail.get('IncludeGlobalServiceEvents'):
+                update_args['IncludeGlobalServiceEvents'] = global_events
+            if notify != trail.get('SNSTopicArn'):
+                update_args['SnsTopicName'] = notify
+            if file_digest != trail.get('LogFileValidationEnabled'):
+                update_args['EnableLogFileValidation'] = file_digest
+            if kms_key != trail.get('KmsKeyId'):
+                if not kms and 'KmsKeyId' in trail:
+                    kms_key = ''
+                update_args['KmsKeyId'] = kms_key
+            if update_args:
+                update_args['Name'] = trail_name
+                client.update_trail(**update_args)


Test patch:
diff --git a/tests/data/placebo/test_cloudtrail_create/cloudtrail.CreateTrail_1.json b/tests/data/placebo/test_cloudtrail_create/cloudtrail.CreateTrail_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_create/cloudtrail.CreateTrail_1.json
@@ -0,0 +1,22 @@
+{
+    "status_code": 200, 
+    "data": {
+        "IncludeGlobalServiceEvents": true, 
+        "Name": "nosetest", 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "9830fd0d-ed81-11e6-9205-e16fedb50da3", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "9830fd0d-ed81-11e6-9205-e16fedb50da3", 
+                "date": "Tue, 07 Feb 2017 22:05:53 GMT", 
+                "content-length": "216", 
+                "content-type": "application/x-amz-json-1.1"
+            }
+        }, 
+        "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/nosetest", 
+        "LogFileValidationEnabled": false, 
+        "IsMultiRegionTrail": false, 
+        "S3BucketName": "nosetest-bucket"
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_create/cloudtrail.DescribeTrails_1.json b/tests/data/placebo/test_cloudtrail_create/cloudtrail.DescribeTrails_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_create/cloudtrail.DescribeTrails_1.json
@@ -0,0 +1,29 @@
+{
+    "status_code": 200, 
+    "data": {
+        "trailList": [
+            {
+                "IncludeGlobalServiceEvents": true, 
+                "Name": "skunk-trails", 
+                "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/skunk-trails", 
+                "LogFileValidationEnabled": true, 
+                "IsMultiRegionTrail": true, 
+                "HasCustomEventSelectors": false, 
+                "S3BucketName": "custodian-skunk-trails", 
+                "KmsKeyId": "arn:aws:kms:us-east-1:644160558196:key/36812ccf-daaf-49b1-a24b-0eef254ffe41", 
+                "HomeRegion": "us-east-1"
+            }
+        ], 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "96fff6c3-ed81-11e6-b9d9-b595a348f3d8", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "96fff6c3-ed81-11e6-b9d9-b595a348f3d8", 
+                "date": "Tue, 07 Feb 2017 22:05:52 GMT", 
+                "content-length": "391", 
+                "content-type": "application/x-amz-json-1.1"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_create/cloudtrail.DescribeTrails_2.json b/tests/data/placebo/test_cloudtrail_create/cloudtrail.DescribeTrails_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_create/cloudtrail.DescribeTrails_2.json
@@ -0,0 +1,28 @@
+{
+    "status_code": 200, 
+    "data": {
+        "trailList": [
+            {
+                "IncludeGlobalServiceEvents": true, 
+                "Name": "nosetest", 
+                "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/nosetest", 
+                "LogFileValidationEnabled": false, 
+                "IsMultiRegionTrail": true, 
+                "HasCustomEventSelectors": false, 
+                "S3BucketName": "nosetest-bucket", 
+                "HomeRegion": "us-east-1"
+            }
+        ], 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "989f2739-ed81-11e6-9205-e16fedb50da3", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "989f2739-ed81-11e6-9205-e16fedb50da3", 
+                "date": "Tue, 07 Feb 2017 22:05:54 GMT", 
+                "content-length": "288", 
+                "content-type": "application/x-amz-json-1.1"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_create/cloudtrail.GetTrailStatus_1.json b/tests/data/placebo/test_cloudtrail_create/cloudtrail.GetTrailStatus_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_create/cloudtrail.GetTrailStatus_1.json
@@ -0,0 +1,23 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "98512e41-ed81-11e6-9205-e16fedb50da3", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "98512e41-ed81-11e6-9205-e16fedb50da3", 
+                "date": "Tue, 07 Feb 2017 22:05:53 GMT", 
+                "content-length": "209", 
+                "content-type": "application/x-amz-json-1.1"
+            }
+        }, 
+        "LatestDeliveryAttemptTime": "", 
+        "LatestNotificationAttemptSucceeded": "", 
+        "LatestDeliveryAttemptSucceeded": "", 
+        "IsLogging": false, 
+        "TimeLoggingStarted": "", 
+        "LatestNotificationAttemptTime": "", 
+        "TimeLoggingStopped": ""
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_create/cloudtrail.GetTrailStatus_2.json b/tests/data/placebo/test_cloudtrail_create/cloudtrail.GetTrailStatus_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_create/cloudtrail.GetTrailStatus_2.json
@@ -0,0 +1,33 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "98a9395a-ed81-11e6-9205-e16fedb50da3", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "98a9395a-ed81-11e6-9205-e16fedb50da3", 
+                "date": "Tue, 07 Feb 2017 22:05:54 GMT", 
+                "content-length": "264", 
+                "content-type": "application/x-amz-json-1.1"
+            }
+        }, 
+        "LatestDeliveryAttemptTime": "", 
+        "LatestNotificationAttemptSucceeded": "", 
+        "LatestDeliveryAttemptSucceeded": "", 
+        "IsLogging": true, 
+        "TimeLoggingStarted": "2017-02-07T22:05:54Z", 
+        "StartLoggingTime": {
+            "hour": 17, 
+            "__class__": "datetime", 
+            "month": 2, 
+            "second": 54, 
+            "microsecond": 426000, 
+            "year": 2017, 
+            "day": 7, 
+            "minute": 5
+        }, 
+        "LatestNotificationAttemptTime": "", 
+        "TimeLoggingStopped": ""
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_create/cloudtrail.StartLogging_1.json b/tests/data/placebo/test_cloudtrail_create/cloudtrail.StartLogging_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_create/cloudtrail.StartLogging_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "985c9ff4-ed81-11e6-9205-e16fedb50da3", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "985c9ff4-ed81-11e6-9205-e16fedb50da3", 
+                "date": "Tue, 07 Feb 2017 22:05:53 GMT", 
+                "content-length": "2", 
+                "content-type": "application/x-amz-json-1.1"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_create/cloudtrail.UpdateTrail_1.json b/tests/data/placebo/test_cloudtrail_create/cloudtrail.UpdateTrail_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_create/cloudtrail.UpdateTrail_1.json
@@ -0,0 +1,24 @@
+{
+    "status_code": 200, 
+    "data": {
+        "IncludeGlobalServiceEvents": true, 
+        "Name": "nosetest", 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "9869e665-ed81-11e6-9205-e16fedb50da3", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "9869e665-ed81-11e6-9205-e16fedb50da3", 
+                "date": "Tue, 07 Feb 2017 22:05:53 GMT", 
+                "content-length": "250", 
+                "content-type": "application/x-amz-json-1.1"
+            }
+        }, 
+        "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/nosetest", 
+        "LogFileValidationEnabled": false, 
+        "SnsTopicARN": "", 
+        "IsMultiRegionTrail": true, 
+        "S3BucketName": "nosetest-bucket", 
+        "SnsTopicName": ""
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_create/iam.ListAccountAliases_1.json b/tests/data/placebo/test_cloudtrail_create/iam.ListAccountAliases_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_create/iam.ListAccountAliases_1.json
@@ -0,0 +1,19 @@
+{
+    "status_code": 200, 
+    "data": {
+        "AccountAliases": [
+            "custodian-skunk-works"
+        ], 
+        "IsTruncated": false, 
+        "ResponseMetadata": {
+            "HTTPStatusCode": 200, 
+            "RequestId": "7b8e77ee-5ce1-11e6-9127-ffb34972fffe", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "7b8e77ee-5ce1-11e6-9127-ffb34972fffe", 
+                "date": "Sun, 07 Aug 2016 20:56:58 GMT", 
+                "content-length": "400", 
+                "content-type": "text/xml"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_create/iam.ListRoles_1.json b/tests/data/placebo/test_cloudtrail_create/iam.ListRoles_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_create/iam.ListRoles_1.json
@@ -0,0 +1,37 @@
+{
+    "status_code": 200, 
+    "data": {
+        "Marker": "ABLFg+mHe0Fq8JrCzN2Cwq6SgzNFiXBYOKlzxF6Ph7H/xok6nwTiabq7ERKkP4t1ZZpBbUwengoIpQzpwsWlLlXs", 
+        "IsTruncated": true, 
+        "Roles": [
+            {
+                "AssumeRolePolicyDocument": "%7B%22Version%22%3A%222012-10-17%22%2C%22Statement%22%3A%5B%7B%22Sid%22%3A%22%22%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22Service%22%3A%22apigateway.amazonaws.com%22%7D%2C%22Action%22%3A%22sts%3AAssumeRole%22%7D%5D%7D", 
+                "RoleId": "AROAJ3IYRN5TVLQIYC5NU", 
+                "CreateDate": {
+                    "hour": 23, 
+                    "__class__": "datetime", 
+                    "month": 1, 
+                    "second": 42, 
+                    "microsecond": 0, 
+                    "year": 2017, 
+                    "day": 9, 
+                    "minute": 9
+                }, 
+                "RoleName": "ApiGwLogger", 
+                "Path": "/", 
+                "Arn": "arn:aws:iam::644160558196:role/ApiGwLogger"
+            }
+        ], 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "97ddfae1-ed81-11e6-b49e-bf62fe4ce4c1", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "97ddfae1-ed81-11e6-b49e-bf62fe4ce4c1", 
+                "date": "Tue, 07 Feb 2017 22:05:52 GMT", 
+                "content-length": "961", 
+                "content-type": "text/xml"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_create/s3.CreateBucket_1.json b/tests/data/placebo/test_cloudtrail_create/s3.CreateBucket_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_create/s3.CreateBucket_1.json
@@ -0,0 +1,20 @@
+{
+    "status_code": 200, 
+    "data": {
+        "Location": "/nosetest-bucket", 
+        "ResponseMetadata": {
+            "HTTPStatusCode": 200, 
+            "RetryAttempts": 0, 
+            "HostId": "3dyCiFQ7oSBsFi4+EwV8PRo7oO9YZvULJUxfBexOGqvxd00kKlkoGvK3LMa3Mx+rglNq2niM4Cw=", 
+            "RequestId": "0B0D0A561AC88759", 
+            "HTTPHeaders": {
+                "content-length": "0", 
+                "x-amz-id-2": "3dyCiFQ7oSBsFi4+EwV8PRo7oO9YZvULJUxfBexOGqvxd00kKlkoGvK3LMa3Mx+rglNq2niM4Cw=", 
+                "server": "AmazonS3", 
+                "x-amz-request-id": "0B0D0A561AC88759", 
+                "location": "/nosetest-bucket", 
+                "date": "Tue, 07 Feb 2017 22:05:53 GMT"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_create/s3.GetBucketPolicy_1.json b/tests/data/placebo/test_cloudtrail_create/s3.GetBucketPolicy_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_create/s3.GetBucketPolicy_1.json
@@ -0,0 +1,24 @@
+{
+    "status_code": 404, 
+    "data": {
+        "ResponseMetadata": {
+            "HTTPStatusCode": 404, 
+            "RetryAttempts": 0, 
+            "HostId": "GTShdez4D8fvVAdL9vxuoZ0OIZ25w1sBE4neOiLQdImfOSDFBD1WGvzk05LSQAsxjzctaoQB0GQ=", 
+            "RequestId": "3718E6F3230EFE9B", 
+            "HTTPHeaders": {
+                "x-amz-id-2": "GTShdez4D8fvVAdL9vxuoZ0OIZ25w1sBE4neOiLQdImfOSDFBD1WGvzk05LSQAsxjzctaoQB0GQ=", 
+                "server": "AmazonS3", 
+                "transfer-encoding": "chunked", 
+                "x-amz-request-id": "3718E6F3230EFE9B", 
+                "date": "Tue, 07 Feb 2017 22:05:52 GMT", 
+                "content-type": "application/xml"
+            }
+        }, 
+        "Error": {
+            "Message": "The bucket policy does not exist", 
+            "Code": "NoSuchBucketPolicy", 
+            "BucketName": "nosetest-bucket"
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_create/s3.PutBucketPolicy_1.json b/tests/data/placebo/test_cloudtrail_create/s3.PutBucketPolicy_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_create/s3.PutBucketPolicy_1.json
@@ -0,0 +1,17 @@
+{
+    "status_code": 204, 
+    "data": {
+        "ResponseMetadata": {
+            "HTTPStatusCode": 204, 
+            "RetryAttempts": 0, 
+            "HostId": "mnoTO9leaDRyRMZG/liSII8QxXj6jUdmMytJWJnodcyrbVF8ngIKBgMrq24CuK0HTM763XFNboY=", 
+            "RequestId": "2070F861B9C749C1", 
+            "HTTPHeaders": {
+                "x-amz-id-2": "mnoTO9leaDRyRMZG/liSII8QxXj6jUdmMytJWJnodcyrbVF8ngIKBgMrq24CuK0HTM763XFNboY=", 
+                "date": "Tue, 07 Feb 2017 22:05:54 GMT", 
+                "x-amz-request-id": "2070F861B9C749C1", 
+                "server": "AmazonS3"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_enable/cloudtrail.CreateTrail_1.json b/tests/data/placebo/test_cloudtrail_enable/cloudtrail.CreateTrail_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_enable/cloudtrail.CreateTrail_1.json
@@ -0,0 +1,22 @@
+{
+    "status_code": 200, 
+    "data": {
+        "IncludeGlobalServiceEvents": true, 
+        "Name": "nosetest", 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "9830fd0d-ed81-11e6-9205-e16fedb50da3", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "9830fd0d-ed81-11e6-9205-e16fedb50da3", 
+                "date": "Tue, 07 Feb 2017 22:05:53 GMT", 
+                "content-length": "216", 
+                "content-type": "application/x-amz-json-1.1"
+            }
+        }, 
+        "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/nosetest", 
+        "LogFileValidationEnabled": false, 
+        "IsMultiRegionTrail": false, 
+        "S3BucketName": "nosetest-bucket"
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_enable/cloudtrail.DescribeTrails_1.json b/tests/data/placebo/test_cloudtrail_enable/cloudtrail.DescribeTrails_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_enable/cloudtrail.DescribeTrails_1.json
@@ -0,0 +1,39 @@
+{
+    "status_code": 200, 
+    "data": {
+        "trailList": [
+            {
+                "IncludeGlobalServiceEvents": true, 
+                "Name": "nosetest", 
+                "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/nosetest", 
+                "LogFileValidationEnabled": false, 
+                "IsMultiRegionTrail": true, 
+                "HasCustomEventSelectors": false, 
+                "S3BucketName": "nosetest-bucket", 
+                "HomeRegion": "us-east-1"
+            }, 
+            {
+                "IncludeGlobalServiceEvents": true, 
+                "Name": "skunk-trails", 
+                "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/skunk-trails", 
+                "LogFileValidationEnabled": true, 
+                "IsMultiRegionTrail": true, 
+                "HasCustomEventSelectors": false, 
+                "S3BucketName": "custodian-skunk-trails", 
+                "KmsKeyId": "arn:aws:kms:us-east-1:644160558196:key/36812ccf-daaf-49b1-a24b-0eef254ffe41", 
+                "HomeRegion": "us-east-1"
+            }
+        ], 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "72ed2980-ed84-11e6-b9d9-b595a348f3d8", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "72ed2980-ed84-11e6-b9d9-b595a348f3d8", 
+                "date": "Tue, 07 Feb 2017 22:26:19 GMT", 
+                "content-length": "664", 
+                "content-type": "application/x-amz-json-1.1"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_enable/cloudtrail.DescribeTrails_2.json b/tests/data/placebo/test_cloudtrail_enable/cloudtrail.DescribeTrails_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_enable/cloudtrail.DescribeTrails_2.json
@@ -0,0 +1,30 @@
+{
+    "status_code": 200, 
+    "data": {
+        "trailList": [
+            {
+                "IncludeGlobalServiceEvents": false, 
+                "Name": "nosetest", 
+                "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/nosetest", 
+                "LogFileValidationEnabled": true, 
+                "SnsTopicARN": "arn:aws:sns:us-east-1:644160558196:test", 
+                "IsMultiRegionTrail": false, 
+                "HasCustomEventSelectors": false, 
+                "S3BucketName": "nosetest-bucket", 
+                "SnsTopicName": "test", 
+                "HomeRegion": "us-east-1"
+            }
+        ], 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "74973015-ed84-11e6-b9d9-b595a348f3d8", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "74973015-ed84-11e6-b9d9-b595a348f3d8", 
+                "date": "Tue, 07 Feb 2017 22:26:21 GMT", 
+                "content-length": "367", 
+                "content-type": "application/x-amz-json-1.1"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_enable/cloudtrail.GetTrailStatus_1.json b/tests/data/placebo/test_cloudtrail_enable/cloudtrail.GetTrailStatus_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_enable/cloudtrail.GetTrailStatus_1.json
@@ -0,0 +1,43 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "7405022c-ed84-11e6-b9d9-b595a348f3d8", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "7405022c-ed84-11e6-b9d9-b595a348f3d8", 
+                "date": "Tue, 07 Feb 2017 22:26:21 GMT", 
+                "content-length": "320", 
+                "content-type": "application/x-amz-json-1.1"
+            }
+        }, 
+        "LatestDeliveryAttemptTime": "", 
+        "LatestNotificationAttemptSucceeded": "", 
+        "LatestDeliveryAttemptSucceeded": "", 
+        "IsLogging": false, 
+        "TimeLoggingStarted": "2017-02-07T22:25:09Z", 
+        "StartLoggingTime": {
+            "hour": 17, 
+            "__class__": "datetime", 
+            "month": 2, 
+            "second": 9, 
+            "microsecond": 786000, 
+            "year": 2017, 
+            "day": 7, 
+            "minute": 25
+        }, 
+        "StopLoggingTime": {
+            "hour": 17, 
+            "__class__": "datetime", 
+            "month": 2, 
+            "second": 49, 
+            "microsecond": 585000, 
+            "year": 2017, 
+            "day": 7, 
+            "minute": 25
+        }, 
+        "LatestNotificationAttemptTime": "", 
+        "TimeLoggingStopped": "2017-02-07T22:25:49Z"
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_enable/cloudtrail.GetTrailStatus_2.json b/tests/data/placebo/test_cloudtrail_enable/cloudtrail.GetTrailStatus_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_enable/cloudtrail.GetTrailStatus_2.json
@@ -0,0 +1,43 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "74a1b766-ed84-11e6-b9d9-b595a348f3d8", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "74a1b766-ed84-11e6-b9d9-b595a348f3d8", 
+                "date": "Tue, 07 Feb 2017 22:26:22 GMT", 
+                "content-length": "319", 
+                "content-type": "application/x-amz-json-1.1"
+            }
+        }, 
+        "LatestDeliveryAttemptTime": "", 
+        "LatestNotificationAttemptSucceeded": "", 
+        "LatestDeliveryAttemptSucceeded": "", 
+        "IsLogging": true, 
+        "TimeLoggingStarted": "2017-02-07T22:26:22Z", 
+        "StartLoggingTime": {
+            "hour": 17, 
+            "__class__": "datetime", 
+            "month": 2, 
+            "second": 22, 
+            "microsecond": 24000, 
+            "year": 2017, 
+            "day": 7, 
+            "minute": 26
+        }, 
+        "StopLoggingTime": {
+            "hour": 17, 
+            "__class__": "datetime", 
+            "month": 2, 
+            "second": 49, 
+            "microsecond": 585000, 
+            "year": 2017, 
+            "day": 7, 
+            "minute": 25
+        }, 
+        "LatestNotificationAttemptTime": "", 
+        "TimeLoggingStopped": "2017-02-07T22:25:49Z"
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_enable/cloudtrail.StartLogging_1.json b/tests/data/placebo/test_cloudtrail_enable/cloudtrail.StartLogging_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_enable/cloudtrail.StartLogging_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "7410e90d-ed84-11e6-b9d9-b595a348f3d8", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "7410e90d-ed84-11e6-b9d9-b595a348f3d8", 
+                "date": "Tue, 07 Feb 2017 22:26:21 GMT", 
+                "content-length": "2", 
+                "content-type": "application/x-amz-json-1.1"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_enable/cloudtrail.UpdateTrail_1.json b/tests/data/placebo/test_cloudtrail_enable/cloudtrail.UpdateTrail_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_enable/cloudtrail.UpdateTrail_1.json
@@ -0,0 +1,24 @@
+{
+    "status_code": 200, 
+    "data": {
+        "IncludeGlobalServiceEvents": false, 
+        "Name": "nosetest", 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "742cd57e-ed84-11e6-b9d9-b595a348f3d8", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "742cd57e-ed84-11e6-b9d9-b595a348f3d8", 
+                "date": "Tue, 07 Feb 2017 22:26:21 GMT", 
+                "content-length": "294", 
+                "content-type": "application/x-amz-json-1.1"
+            }
+        }, 
+        "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/nosetest", 
+        "LogFileValidationEnabled": true, 
+        "SnsTopicARN": "arn:aws:sns:us-east-1:644160558196:test", 
+        "IsMultiRegionTrail": false, 
+        "S3BucketName": "nosetest-bucket", 
+        "SnsTopicName": "test"
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_enable/iam.ListAccountAliases_1.json b/tests/data/placebo/test_cloudtrail_enable/iam.ListAccountAliases_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_enable/iam.ListAccountAliases_1.json
@@ -0,0 +1,19 @@
+{
+    "status_code": 200, 
+    "data": {
+        "AccountAliases": [
+            "custodian-skunk-works"
+        ], 
+        "IsTruncated": false, 
+        "ResponseMetadata": {
+            "HTTPStatusCode": 200, 
+            "RequestId": "7b8e77ee-5ce1-11e6-9127-ffb34972fffe", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "7b8e77ee-5ce1-11e6-9127-ffb34972fffe", 
+                "date": "Sun, 07 Aug 2016 20:56:58 GMT", 
+                "content-length": "400", 
+                "content-type": "text/xml"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_enable/iam.ListRoles_1.json b/tests/data/placebo/test_cloudtrail_enable/iam.ListRoles_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_enable/iam.ListRoles_1.json
@@ -0,0 +1,37 @@
+{
+    "status_code": 200, 
+    "data": {
+        "Marker": "ABIRc5mfjNiSeT/yP4s+5JHqk5if12ZbVjXrfRssUHiCvnXgRCFjTbdA9liUGDzCTRHlpvsVAMR6E93zMOEWgx27", 
+        "IsTruncated": true, 
+        "Roles": [
+            {
+                "AssumeRolePolicyDocument": "%7B%22Version%22%3A%222012-10-17%22%2C%22Statement%22%3A%5B%7B%22Sid%22%3A%22%22%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22Service%22%3A%22apigateway.amazonaws.com%22%7D%2C%22Action%22%3A%22sts%3AAssumeRole%22%7D%5D%7D", 
+                "RoleId": "AROAJ3IYRN5TVLQIYC5NU", 
+                "CreateDate": {
+                    "hour": 23, 
+                    "__class__": "datetime", 
+                    "month": 1, 
+                    "second": 42, 
+                    "microsecond": 0, 
+                    "year": 2017, 
+                    "day": 9, 
+                    "minute": 9
+                }, 
+                "RoleName": "ApiGwLogger", 
+                "Path": "/", 
+                "Arn": "arn:aws:iam::644160558196:role/ApiGwLogger"
+            }
+        ], 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "73ad6bf1-ed84-11e6-ae27-371d8cc48a82", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "73ad6bf1-ed84-11e6-ae27-371d8cc48a82", 
+                "date": "Tue, 07 Feb 2017 22:26:20 GMT", 
+                "content-length": "961", 
+                "content-type": "text/xml"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_enable/s3.CreateBucket_1.json b/tests/data/placebo/test_cloudtrail_enable/s3.CreateBucket_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_enable/s3.CreateBucket_1.json
@@ -0,0 +1,20 @@
+{
+    "status_code": 200, 
+    "data": {
+        "Location": "/nosetest-bucket", 
+        "ResponseMetadata": {
+            "HTTPStatusCode": 200, 
+            "RetryAttempts": 0, 
+            "HostId": "hgV9fiYrAwNIU7AjsvTP7tFfLxYFhrj/2EqPMfoUeTjXyzkO3/+EBdSGfS5dY3/M7u1/moGHlSA=", 
+            "RequestId": "70648BC9DF068D46", 
+            "HTTPHeaders": {
+                "content-length": "0", 
+                "x-amz-id-2": "hgV9fiYrAwNIU7AjsvTP7tFfLxYFhrj/2EqPMfoUeTjXyzkO3/+EBdSGfS5dY3/M7u1/moGHlSA=", 
+                "server": "AmazonS3", 
+                "x-amz-request-id": "70648BC9DF068D46", 
+                "location": "/nosetest-bucket", 
+                "date": "Tue, 07 Feb 2017 22:26:21 GMT"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_enable/s3.GetBucketPolicy_1.json b/tests/data/placebo/test_cloudtrail_enable/s3.GetBucketPolicy_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_enable/s3.GetBucketPolicy_1.json
@@ -0,0 +1,20 @@
+{
+    "status_code": 200, 
+    "data": {
+        "Policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"AWSCloudTrailAclCheck20150319\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"cloudtrail.amazonaws.com\"},\"Action\":\"s3:GetBucketAcl\",\"Resource\":\"arn:aws:s3:::nosetest-bucket\"},{\"Sid\":\"AWSCloudTrailWrite20150319\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"cloudtrail.amazonaws.com\"},\"Action\":\"s3:PutObject\",\"Resource\":\"arn:aws:s3:::nosetest-bucket/AWSLogs/644160558196/*\",\"Condition\":{\"StringEquals\":{\"s3:x-amz-acl\":\"bucket-owner-full-control\"}}}]}", 
+        "ResponseMetadata": {
+            "HTTPStatusCode": 200, 
+            "RetryAttempts": 0, 
+            "HostId": "dgMGV2pSF3I0MPfV+5gLjH0D707QtmzKoW/8be17Ep2iYXvT9NpQoujhh064SrRkyM2Lf3WErqw=", 
+            "RequestId": "2539CE40936C12AD", 
+            "HTTPHeaders": {
+                "content-length": "483", 
+                "x-amz-id-2": "dgMGV2pSF3I0MPfV+5gLjH0D707QtmzKoW/8be17Ep2iYXvT9NpQoujhh064SrRkyM2Lf3WErqw=", 
+                "server": "AmazonS3", 
+                "x-amz-request-id": "2539CE40936C12AD", 
+                "date": "Tue, 07 Feb 2017 22:26:22 GMT", 
+                "content-type": "application/json"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_cloudtrail_enable/s3.PutBucketPolicy_1.json b/tests/data/placebo/test_cloudtrail_enable/s3.PutBucketPolicy_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cloudtrail_enable/s3.PutBucketPolicy_1.json
@@ -0,0 +1,17 @@
+{
+    "status_code": 204, 
+    "data": {
+        "ResponseMetadata": {
+            "HTTPStatusCode": 204, 
+            "RetryAttempts": 0, 
+            "HostId": "DoxqZDTXL57OS37xMzvZHNLsvKBa8BU0QP0XJNPW1l5cQwNtLa1Lc9ogbmLOxi9K42DSe5DTME4=", 
+            "RequestId": "3A7BCEEAAD75E716", 
+            "HTTPHeaders": {
+                "x-amz-id-2": "DoxqZDTXL57OS37xMzvZHNLsvKBa8BU0QP0XJNPW1l5cQwNtLa1Lc9ogbmLOxi9K42DSe5DTME4=", 
+                "date": "Tue, 07 Feb 2017 22:26:22 GMT", 
+                "x-amz-request-id": "3A7BCEEAAD75E716", 
+                "server": "AmazonS3"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/test_account.py b/tests/test_account.py
--- a/tests/test_account.py
+++ b/tests/test_account.py
@@ -12,6 +12,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 from common import BaseTest
+from c7n.utils import local_session
+
+
+TRAIL = 'nosetest'
 
 import datetime
 from dateutil import parser
@@ -225,5 +229,62 @@ def test_missing_password_policy(self):
             session_factory=session_factory)
         resources = p.run()
         self.assertEqual(len(resources), 1)
+        
+    def test_create_trail(self):
+        factory = self.replay_flight_data('test_cloudtrail_create')
+        p = self.load_policy(
+            {
+                'name': 'trail-test',
+                'resource': 'account',
+                'actions': [
+                    {
+                        'type': 'enable-cloudtrail',
+                        'trail': TRAIL,
+                        'bucket': '%s-bucket' % TRAIL,
+                    },
+                ],
+            },
+            session_factory=factory,
+        )
+        p.run()
+        client = local_session(factory).client('cloudtrail')
+        resp = client.describe_trails(trailNameList=[TRAIL])
+        trails = resp['trailList']
+        arn = trails[0]['TrailARN']
+        status = client.get_trail_status(Name=arn)
+        self.assertTrue(status['IsLogging'])
 
-
+    def test_enable_trail(self):
+        factory = self.replay_flight_data('test_cloudtrail_enable')
+        p = self.load_policy(
+            {
+                'name': 'trail-test',
+                'resource': 'account',
+                'actions': [
+                    {
+                        'type': 'enable-cloudtrail',
+                        'trail': TRAIL,
+                        'bucket': '%s-bucket' % TRAIL,
+                        'multi-region': False,
+                        'global-events': False,
+                        'notify': 'test',
+                        'file-digest': True,
+                        'kms': True,
+                        'kms-key': 'arn:aws:kms:us-east-1:1234:key/fake',
+                    },
+                ],
+            },
+            session_factory=factory,
+        )
+        p.run()
+        client = local_session(factory).client('cloudtrail')
+        resp = client.describe_trails(trailNameList=[TRAIL])
+        trails = resp['trailList']
+        test_trail = trails[0]
+        self.assertFalse(test_trail['IsMultiRegionTrail'])
+        self.assertFalse(test_trail['IncludeGlobalServiceEvents'])
+        self.assertTrue(test_trail['LogFileValidationEnabled'])
+        self.assertEqual(test_trail['SnsTopicName'], 'test')
+        arn = test_trail['TrailARN']
+        status = client.get_trail_status(Name=arn)
+        self.assertTrue(status['IsLogging'])