1"""
2Manage CloudTrail Objects
3=========================
4
5.. versionadded:: 2016.3.0
6
7Create and destroy CloudTrail objects. Be aware that this interacts with Amazon's services,
8and so may incur charges.
9
10:depends:
11    - boto
12    - boto3
13
14The dependencies listed above can be installed via package or pip.
15
16This module accepts explicit vpc credentials but can also utilize
17IAM roles assigned to the instance through Instance Profiles. Dynamic
18credentials are then automatically obtained from AWS API and no further
19configuration is necessary. More information available `here
20<http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html>`_.
21
22If IAM roles are not used you need to specify them either in a pillar file or
23in the minion's config file:
24
25.. code-block:: yaml
26
27    vpc.keyid: GKTADJGHEIQSXMKKRBJ08H
28    vpc.key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
29
30It's also possible to specify ``key``, ``keyid`` and ``region`` via a profile,
31either passed in as a dict, or as a string to pull from pillars or minion
32config:
33
34.. code-block:: yaml
35
36    myprofile:
37        keyid: GKTADJGHEIQSXMKKRBJ08H
38        key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
39            region: us-east-1
40
41.. code-block:: yaml
42
43    Ensure trail exists:
44        boto_cloudtrail.present:
45            - Name: mytrail
46            - S3BucketName: mybucket
47            - S3KeyPrefix: prefix
48            - region: us-east-1
49            - keyid: GKTADJGHEIQSXMKKRBJ08H
50            - key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
51
52"""
53
54
55import logging
56import os
57import os.path
58
59import salt.utils.data
60
61log = logging.getLogger(__name__)
62
63
64def __virtual__():
65    """
66    Only load if boto is available.
67    """
68    if "boto_cloudtrail.exists" in __salt__:
69        return "boto_cloudtrail"
70    return (False, "boto_cloudtrail module could not be loaded")
71
72
73def present(
74    name,
75    Name,
76    S3BucketName,
77    S3KeyPrefix=None,
78    SnsTopicName=None,
79    IncludeGlobalServiceEvents=True,
80    IsMultiRegionTrail=None,
81    EnableLogFileValidation=False,
82    CloudWatchLogsLogGroupArn=None,
83    CloudWatchLogsRoleArn=None,
84    KmsKeyId=None,
85    LoggingEnabled=True,
86    Tags=None,
87    region=None,
88    key=None,
89    keyid=None,
90    profile=None,
91):
92    """
93    Ensure trail exists.
94
95    name
96        The name of the state definition
97
98    Name
99        Name of the trail.
100
101    S3BucketName
102        Specifies the name of the Amazon S3 bucket designated for publishing log
103        files.
104
105    S3KeyPrefix
106        Specifies the Amazon S3 key prefix that comes after the name of the
107        bucket you have designated for log file delivery.
108
109    SnsTopicName
110        Specifies the name of the Amazon SNS topic defined for notification of
111        log file delivery. The maximum length is 256 characters.
112
113    IncludeGlobalServiceEvents
114        Specifies whether the trail is publishing events from global services
115        such as IAM to the log files.
116
117    EnableLogFileValidation
118        Specifies whether log file integrity validation is enabled. The default
119        is false.
120
121    CloudWatchLogsLogGroupArn
122        Specifies a log group name using an Amazon Resource Name (ARN), a unique
123        identifier that represents the log group to which CloudTrail logs will
124        be delivered. Not required unless you specify CloudWatchLogsRoleArn.
125
126    CloudWatchLogsRoleArn
127        Specifies the role for the CloudWatch Logs endpoint to assume to write
128        to a user's log group.
129
130    KmsKeyId
131        Specifies the KMS key ID to use to encrypt the logs delivered by
132        CloudTrail. The value can be a an alias name prefixed by "alias/", a
133        fully specified ARN to an alias, a fully specified ARN to a key, or a
134        globally unique identifier.
135
136    LoggingEnabled
137        Whether logging should be enabled for the trail
138
139    Tags
140        A dictionary of tags that should be set on the trail
141
142    region
143        Region to connect to.
144
145    key
146        Secret key to be used.
147
148    keyid
149        Access key to be used.
150
151    profile
152        A dict with region, key and keyid, or a pillar key (string) that
153        contains a dict with region, key and keyid.
154    """
155    ret = {"name": Name, "result": True, "comment": "", "changes": {}}
156
157    r = __salt__["boto_cloudtrail.exists"](
158        Name=Name, region=region, key=key, keyid=keyid, profile=profile
159    )
160
161    if "error" in r:
162        ret["result"] = False
163        ret["comment"] = "Failed to create trail: {}.".format(r["error"]["message"])
164        return ret
165
166    if not r.get("exists"):
167        if __opts__["test"]:
168            ret["comment"] = "CloudTrail {} is set to be created.".format(Name)
169            ret["result"] = None
170            return ret
171        r = __salt__["boto_cloudtrail.create"](
172            Name=Name,
173            S3BucketName=S3BucketName,
174            S3KeyPrefix=S3KeyPrefix,
175            SnsTopicName=SnsTopicName,
176            IncludeGlobalServiceEvents=IncludeGlobalServiceEvents,
177            IsMultiRegionTrail=IsMultiRegionTrail,
178            EnableLogFileValidation=EnableLogFileValidation,
179            CloudWatchLogsLogGroupArn=CloudWatchLogsLogGroupArn,
180            CloudWatchLogsRoleArn=CloudWatchLogsRoleArn,
181            KmsKeyId=KmsKeyId,
182            region=region,
183            key=key,
184            keyid=keyid,
185            profile=profile,
186        )
187        if not r.get("created"):
188            ret["result"] = False
189            ret["comment"] = "Failed to create trail: {}.".format(r["error"]["message"])
190            return ret
191        _describe = __salt__["boto_cloudtrail.describe"](
192            Name, region=region, key=key, keyid=keyid, profile=profile
193        )
194        ret["changes"]["old"] = {"trail": None}
195        ret["changes"]["new"] = _describe
196        ret["comment"] = "CloudTrail {} created.".format(Name)
197
198        if LoggingEnabled:
199            r = __salt__["boto_cloudtrail.start_logging"](
200                Name=Name, region=region, key=key, keyid=keyid, profile=profile
201            )
202            if "error" in r:
203                ret["result"] = False
204                ret["comment"] = "Failed to create trail: {}.".format(
205                    r["error"]["message"]
206                )
207                ret["changes"] = {}
208                return ret
209            ret["changes"]["new"]["trail"]["LoggingEnabled"] = True
210        else:
211            ret["changes"]["new"]["trail"]["LoggingEnabled"] = False
212
213        if bool(Tags):
214            r = __salt__["boto_cloudtrail.add_tags"](
215                Name=Name, region=region, key=key, keyid=keyid, profile=profile, **Tags
216            )
217            if not r.get("tagged"):
218                ret["result"] = False
219                ret["comment"] = "Failed to create trail: {}.".format(
220                    r["error"]["message"]
221                )
222                ret["changes"] = {}
223                return ret
224            ret["changes"]["new"]["trail"]["Tags"] = Tags
225        return ret
226
227    ret["comment"] = os.linesep.join(
228        [ret["comment"], "CloudTrail {} is present.".format(Name)]
229    )
230    ret["changes"] = {}
231    # trail exists, ensure config matches
232    _describe = __salt__["boto_cloudtrail.describe"](
233        Name=Name, region=region, key=key, keyid=keyid, profile=profile
234    )
235    if "error" in _describe:
236        ret["result"] = False
237        ret["comment"] = "Failed to update trail: {}.".format(
238            _describe["error"]["message"]
239        )
240        ret["changes"] = {}
241        return ret
242    _describe = _describe.get("trail")
243
244    r = __salt__["boto_cloudtrail.status"](
245        Name=Name, region=region, key=key, keyid=keyid, profile=profile
246    )
247    _describe["LoggingEnabled"] = r.get("trail", {}).get("IsLogging", False)
248
249    need_update = False
250    bucket_vars = {
251        "S3BucketName": "S3BucketName",
252        "S3KeyPrefix": "S3KeyPrefix",
253        "SnsTopicName": "SnsTopicName",
254        "IncludeGlobalServiceEvents": "IncludeGlobalServiceEvents",
255        "IsMultiRegionTrail": "IsMultiRegionTrail",
256        "EnableLogFileValidation": "LogFileValidationEnabled",
257        "CloudWatchLogsLogGroupArn": "CloudWatchLogsLogGroupArn",
258        "CloudWatchLogsRoleArn": "CloudWatchLogsRoleArn",
259        "KmsKeyId": "KmsKeyId",
260        "LoggingEnabled": "LoggingEnabled",
261    }
262
263    for invar, outvar in bucket_vars.items():
264        if _describe[outvar] != locals()[invar]:
265            need_update = True
266            ret["changes"].setdefault("new", {})[invar] = locals()[invar]
267            ret["changes"].setdefault("old", {})[invar] = _describe[outvar]
268
269    r = __salt__["boto_cloudtrail.list_tags"](
270        Name=Name, region=region, key=key, keyid=keyid, profile=profile
271    )
272    _describe["Tags"] = r.get("tags", {})
273    tagchange = salt.utils.data.compare_dicts(_describe["Tags"], Tags)
274    if bool(tagchange):
275        need_update = True
276        ret["changes"].setdefault("new", {})["Tags"] = Tags
277        ret["changes"].setdefault("old", {})["Tags"] = _describe["Tags"]
278
279    if need_update:
280        if __opts__["test"]:
281            msg = "CloudTrail {} set to be modified.".format(Name)
282            ret["comment"] = msg
283            ret["result"] = None
284            return ret
285
286        ret["comment"] = os.linesep.join([ret["comment"], "CloudTrail to be modified"])
287        r = __salt__["boto_cloudtrail.update"](
288            Name=Name,
289            S3BucketName=S3BucketName,
290            S3KeyPrefix=S3KeyPrefix,
291            SnsTopicName=SnsTopicName,
292            IncludeGlobalServiceEvents=IncludeGlobalServiceEvents,
293            IsMultiRegionTrail=IsMultiRegionTrail,
294            EnableLogFileValidation=EnableLogFileValidation,
295            CloudWatchLogsLogGroupArn=CloudWatchLogsLogGroupArn,
296            CloudWatchLogsRoleArn=CloudWatchLogsRoleArn,
297            KmsKeyId=KmsKeyId,
298            region=region,
299            key=key,
300            keyid=keyid,
301            profile=profile,
302        )
303        if not r.get("updated"):
304            ret["result"] = False
305            ret["comment"] = "Failed to update trail: {}.".format(r["error"]["message"])
306            ret["changes"] = {}
307            return ret
308
309        if LoggingEnabled:
310            r = __salt__["boto_cloudtrail.start_logging"](
311                Name=Name, region=region, key=key, keyid=keyid, profile=profile
312            )
313            if not r.get("started"):
314                ret["result"] = False
315                ret["comment"] = "Failed to update trail: {}.".format(
316                    r["error"]["message"]
317                )
318                ret["changes"] = {}
319                return ret
320        else:
321            r = __salt__["boto_cloudtrail.stop_logging"](
322                Name=Name, region=region, key=key, keyid=keyid, profile=profile
323            )
324            if not r.get("stopped"):
325                ret["result"] = False
326                ret["comment"] = "Failed to update trail: {}.".format(
327                    r["error"]["message"]
328                )
329                ret["changes"] = {}
330                return ret
331
332        if bool(tagchange):
333            adds = {}
334            removes = {}
335            for k, diff in tagchange.items():
336                if diff.get("new", "") != "":
337                    # there's an update for this key
338                    adds[k] = Tags[k]
339                elif diff.get("old", "") != "":
340                    removes[k] = _describe["Tags"][k]
341            if bool(adds):
342                r = __salt__["boto_cloudtrail.add_tags"](
343                    Name=Name,
344                    region=region,
345                    key=key,
346                    keyid=keyid,
347                    profile=profile,
348                    **adds
349                )
350            if bool(removes):
351                r = __salt__["boto_cloudtrail.remove_tags"](
352                    Name=Name,
353                    region=region,
354                    key=key,
355                    keyid=keyid,
356                    profile=profile,
357                    **removes
358                )
359
360    return ret
361
362
363def absent(name, Name, region=None, key=None, keyid=None, profile=None):
364    """
365    Ensure trail with passed properties is absent.
366
367    name
368        The name of the state definition.
369
370    Name
371        Name of the trail.
372
373    region
374        Region to connect to.
375
376    key
377        Secret key to be used.
378
379    keyid
380        Access key to be used.
381
382    profile
383        A dict with region, key and keyid, or a pillar key (string) that
384        contains a dict with region, key and keyid.
385    """
386
387    ret = {"name": Name, "result": True, "comment": "", "changes": {}}
388
389    r = __salt__["boto_cloudtrail.exists"](
390        Name, region=region, key=key, keyid=keyid, profile=profile
391    )
392    if "error" in r:
393        ret["result"] = False
394        ret["comment"] = "Failed to delete trail: {}.".format(r["error"]["message"])
395        return ret
396
397    if r and not r["exists"]:
398        ret["comment"] = "CloudTrail {} does not exist.".format(Name)
399        return ret
400
401    if __opts__["test"]:
402        ret["comment"] = "CloudTrail {} is set to be removed.".format(Name)
403        ret["result"] = None
404        return ret
405    r = __salt__["boto_cloudtrail.delete"](
406        Name, region=region, key=key, keyid=keyid, profile=profile
407    )
408    if not r["deleted"]:
409        ret["result"] = False
410        ret["comment"] = "Failed to delete trail: {}.".format(r["error"]["message"])
411        return ret
412    ret["changes"]["old"] = {"trail": Name}
413    ret["changes"]["new"] = {"trail": None}
414    ret["comment"] = "CloudTrail {} deleted.".format(Name)
415    return ret
416