1"""
2Connection module for Amazon CloudTrail
3
4.. versionadded:: 2016.3.0
5
6:depends:
7    - boto
8    - boto3
9
10The dependencies listed above can be installed via package or pip.
11
12:configuration: This module accepts explicit Lambda credentials but can also
13    utilize IAM roles assigned to the instance through Instance Profiles.
14    Dynamic credentials are then automatically obtained from AWS API and no
15    further configuration is necessary. More Information available at:
16
17    .. code-block:: text
18
19        http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
20
21    If IAM roles are not used you need to specify them either in a pillar or
22    in the minion's config file:
23
24    .. code-block:: yaml
25
26        cloudtrail.keyid: GKTADJGHEIQSXMKKRBJ08H
27        cloudtrail.key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
28
29    A region may also be specified in the configuration:
30
31    .. code-block:: yaml
32
33        cloudtrail.region: us-east-1
34
35    If a region is not specified, the default is us-east-1.
36
37    It's also possible to specify key, keyid and region via a profile, either
38    as a passed in dict, or as a string to pull from pillars or minion config:
39
40    .. code-block:: yaml
41
42        myprofile:
43            keyid: GKTADJGHEIQSXMKKRBJ08H
44            key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
45            region: us-east-1
46
47"""
48# keep lint from choking on _get_conn and _cache_id
49# pylint: disable=E0602
50
51
52import logging
53
54import salt.utils.compat
55import salt.utils.versions
56
57log = logging.getLogger(__name__)
58
59
60# pylint: disable=import-error
61try:
62    # pylint: disable=unused-import
63    import boto
64    import boto3
65
66    # pylint: enable=unused-import
67    from botocore.exceptions import ClientError
68
69    logging.getLogger("boto3").setLevel(logging.CRITICAL)
70    HAS_BOTO = True
71except ImportError:
72    HAS_BOTO = False
73# pylint: enable=import-error
74
75
76def __virtual__():
77    """
78    Only load if boto libraries exist and if boto libraries are greater than
79    a given version.
80    """
81    # the boto_lambda execution module relies on the connect_to_region() method
82    # which was added in boto 2.8.0
83    # https://github.com/boto/boto/commit/33ac26b416fbb48a60602542b4ce15dcc7029f12
84    return salt.utils.versions.check_boto_reqs(boto3_ver="1.2.5")
85
86
87def __init__(opts):
88    if HAS_BOTO:
89        __utils__["boto3.assign_funcs"](__name__, "cloudtrail")
90
91
92def exists(Name, region=None, key=None, keyid=None, profile=None):
93    """
94    Given a trail name, check to see if the given trail exists.
95
96    Returns True if the given trail exists and returns False if the given
97    trail does not exist.
98
99    CLI Example:
100
101    .. code-block:: bash
102
103        salt myminion boto_cloudtrail.exists mytrail
104
105    """
106
107    try:
108        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
109        conn.get_trail_status(Name=Name)
110        return {"exists": True}
111    except ClientError as e:
112        err = __utils__["boto3.get_error"](e)
113        if e.response.get("Error", {}).get("Code") == "TrailNotFoundException":
114            return {"exists": False}
115        return {"error": err}
116
117
118def create(
119    Name,
120    S3BucketName,
121    S3KeyPrefix=None,
122    SnsTopicName=None,
123    IncludeGlobalServiceEvents=None,
124    IsMultiRegionTrail=None,
125    EnableLogFileValidation=None,
126    CloudWatchLogsLogGroupArn=None,
127    CloudWatchLogsRoleArn=None,
128    KmsKeyId=None,
129    region=None,
130    key=None,
131    keyid=None,
132    profile=None,
133):
134    """
135    Given a valid config, create a trail.
136
137    Returns {created: true} if the trail was created and returns
138    {created: False} if the trail was not created.
139
140    CLI Example:
141
142    .. code-block:: bash
143
144        salt myminion boto_cloudtrail.create my_trail my_bucket
145
146    """
147
148    try:
149        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
150        kwargs = {}
151        for arg in (
152            "S3KeyPrefix",
153            "SnsTopicName",
154            "IncludeGlobalServiceEvents",
155            "IsMultiRegionTrail",
156            "EnableLogFileValidation",
157            "CloudWatchLogsLogGroupArn",
158            "CloudWatchLogsRoleArn",
159            "KmsKeyId",
160        ):
161            if locals()[arg] is not None:
162                kwargs[arg] = locals()[arg]
163        trail = conn.create_trail(Name=Name, S3BucketName=S3BucketName, **kwargs)
164        if trail:
165            log.info("The newly created trail name is %s", trail["Name"])
166
167            return {"created": True, "name": trail["Name"]}
168        else:
169            log.warning("Trail was not created")
170            return {"created": False}
171    except ClientError as e:
172        return {"created": False, "error": __utils__["boto3.get_error"](e)}
173
174
175def delete(Name, region=None, key=None, keyid=None, profile=None):
176    """
177    Given a trail name, delete it.
178
179    Returns {deleted: true} if the trail was deleted and returns
180    {deleted: false} if the trail was not deleted.
181
182    CLI Example:
183
184    .. code-block:: bash
185
186        salt myminion boto_cloudtrail.delete mytrail
187
188    """
189
190    try:
191        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
192        conn.delete_trail(Name=Name)
193        return {"deleted": True}
194    except ClientError as e:
195        return {"deleted": False, "error": __utils__["boto3.get_error"](e)}
196
197
198def describe(Name, region=None, key=None, keyid=None, profile=None):
199    """
200    Given a trail name describe its properties.
201
202    Returns a dictionary of interesting properties.
203
204    CLI Example:
205
206    .. code-block:: bash
207
208        salt myminion boto_cloudtrail.describe mytrail
209
210    """
211
212    try:
213        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
214        trails = conn.describe_trails(trailNameList=[Name])
215        if trails and trails.get("trailList"):
216            keys = (
217                "Name",
218                "S3BucketName",
219                "S3KeyPrefix",
220                "SnsTopicName",
221                "IncludeGlobalServiceEvents",
222                "IsMultiRegionTrail",
223                "HomeRegion",
224                "TrailARN",
225                "LogFileValidationEnabled",
226                "CloudWatchLogsLogGroupArn",
227                "CloudWatchLogsRoleArn",
228                "KmsKeyId",
229            )
230            trail = trails["trailList"].pop()
231            return {"trail": {k: trail.get(k) for k in keys}}
232        else:
233            return {"trail": None}
234    except ClientError as e:
235        err = __utils__["boto3.get_error"](e)
236        if e.response.get("Error", {}).get("Code") == "TrailNotFoundException":
237            return {"trail": None}
238        return {"error": __utils__["boto3.get_error"](e)}
239
240
241def status(Name, region=None, key=None, keyid=None, profile=None):
242    """
243    Given a trail name describe its properties.
244
245    Returns a dictionary of interesting properties.
246
247    CLI Example:
248
249    .. code-block:: bash
250
251        salt myminion boto_cloudtrail.describe mytrail
252
253    """
254
255    try:
256        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
257        trail = conn.get_trail_status(Name=Name)
258        if trail:
259            keys = (
260                "IsLogging",
261                "LatestDeliveryError",
262                "LatestNotificationError",
263                "LatestDeliveryTime",
264                "LatestNotificationTime",
265                "StartLoggingTime",
266                "StopLoggingTime",
267                "LatestCloudWatchLogsDeliveryError",
268                "LatestCloudWatchLogsDeliveryTime",
269                "LatestDigestDeliveryTime",
270                "LatestDigestDeliveryError",
271                "LatestDeliveryAttemptTime",
272                "LatestNotificationAttemptTime",
273                "LatestNotificationAttemptSucceeded",
274                "LatestDeliveryAttemptSucceeded",
275                "TimeLoggingStarted",
276                "TimeLoggingStopped",
277            )
278            return {"trail": {k: trail.get(k) for k in keys}}
279        else:
280            return {"trail": None}
281    except ClientError as e:
282        err = __utils__["boto3.get_error"](e)
283        if e.response.get("Error", {}).get("Code") == "TrailNotFoundException":
284            return {"trail": None}
285        return {"error": __utils__["boto3.get_error"](e)}
286
287
288def list(region=None, key=None, keyid=None, profile=None):
289    """
290    List all trails
291
292    Returns list of trails
293
294    CLI Example:
295
296    .. code-block:: yaml
297
298        policies:
299          - {...}
300          - {...}
301
302    """
303    try:
304        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
305        trails = conn.describe_trails()
306        if not bool(trails.get("trailList")):
307            log.warning("No trails found")
308        return {"trails": trails.get("trailList", [])}
309    except ClientError as e:
310        return {"error": __utils__["boto3.get_error"](e)}
311
312
313def update(
314    Name,
315    S3BucketName,
316    S3KeyPrefix=None,
317    SnsTopicName=None,
318    IncludeGlobalServiceEvents=None,
319    IsMultiRegionTrail=None,
320    EnableLogFileValidation=None,
321    CloudWatchLogsLogGroupArn=None,
322    CloudWatchLogsRoleArn=None,
323    KmsKeyId=None,
324    region=None,
325    key=None,
326    keyid=None,
327    profile=None,
328):
329    """
330    Given a valid config, update a trail.
331
332    Returns {created: true} if the trail was created and returns
333    {created: False} if the trail was not created.
334
335    CLI Example:
336
337    .. code-block:: bash
338
339        salt myminion boto_cloudtrail.update my_trail my_bucket
340
341    """
342
343    try:
344        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
345        kwargs = {}
346        for arg in (
347            "S3KeyPrefix",
348            "SnsTopicName",
349            "IncludeGlobalServiceEvents",
350            "IsMultiRegionTrail",
351            "EnableLogFileValidation",
352            "CloudWatchLogsLogGroupArn",
353            "CloudWatchLogsRoleArn",
354            "KmsKeyId",
355        ):
356            if locals()[arg] is not None:
357                kwargs[arg] = locals()[arg]
358        trail = conn.update_trail(Name=Name, S3BucketName=S3BucketName, **kwargs)
359        if trail:
360            log.info("The updated trail name is %s", trail["Name"])
361
362            return {"updated": True, "name": trail["Name"]}
363        else:
364            log.warning("Trail was not created")
365            return {"updated": False}
366    except ClientError as e:
367        return {"updated": False, "error": __utils__["boto3.get_error"](e)}
368
369
370def start_logging(Name, region=None, key=None, keyid=None, profile=None):
371    """
372    Start logging for a trail
373
374    Returns {started: true} if the trail was started and returns
375    {started: False} if the trail was not started.
376
377    CLI Example:
378
379    .. code-block:: bash
380
381        salt myminion boto_cloudtrail.start_logging my_trail
382
383    """
384
385    try:
386        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
387        conn.start_logging(Name=Name)
388        return {"started": True}
389    except ClientError as e:
390        return {"started": False, "error": __utils__["boto3.get_error"](e)}
391
392
393def stop_logging(Name, region=None, key=None, keyid=None, profile=None):
394    """
395    Stop logging for a trail
396
397    Returns {stopped: true} if the trail was stopped and returns
398    {stopped: False} if the trail was not stopped.
399
400    CLI Example:
401
402    .. code-block:: bash
403
404        salt myminion boto_cloudtrail.stop_logging my_trail
405
406    """
407
408    try:
409        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
410        conn.stop_logging(Name=Name)
411        return {"stopped": True}
412    except ClientError as e:
413        return {"stopped": False, "error": __utils__["boto3.get_error"](e)}
414
415
416def _get_trail_arn(name, region=None, key=None, keyid=None, profile=None):
417    if name.startswith("arn:aws:cloudtrail:"):
418        return name
419
420    account_id = __salt__["boto_iam.get_account_id"](
421        region=region, key=key, keyid=keyid, profile=profile
422    )
423    if profile and "region" in profile:
424        region = profile["region"]
425    if region is None:
426        region = "us-east-1"
427    return "arn:aws:cloudtrail:{}:{}:trail/{}".format(region, account_id, name)
428
429
430def add_tags(Name, region=None, key=None, keyid=None, profile=None, **kwargs):
431    """
432    Add tags to a trail
433
434    Returns {tagged: true} if the trail was tagged and returns
435    {tagged: False} if the trail was not tagged.
436
437    CLI Example:
438
439    .. code-block:: bash
440
441        salt myminion boto_cloudtrail.add_tags my_trail tag_a=tag_value tag_b=tag_value
442
443    """
444
445    try:
446        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
447        tagslist = []
448        for k, v in kwargs.items():
449            if str(k).startswith("__"):
450                continue
451            tagslist.append({"Key": str(k), "Value": str(v)})
452        conn.add_tags(
453            ResourceId=_get_trail_arn(
454                Name, region=region, key=key, keyid=keyid, profile=profile
455            ),
456            TagsList=tagslist,
457        )
458        return {"tagged": True}
459    except ClientError as e:
460        return {"tagged": False, "error": __utils__["boto3.get_error"](e)}
461
462
463def remove_tags(Name, region=None, key=None, keyid=None, profile=None, **kwargs):
464    """
465    Remove tags from a trail
466
467    Returns {tagged: true} if the trail was tagged and returns
468    {tagged: False} if the trail was not tagged.
469
470    CLI Example:
471
472    .. code-block:: bash
473
474        salt myminion boto_cloudtrail.remove_tags my_trail tag_a=tag_value tag_b=tag_value
475
476    """
477
478    try:
479        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
480        tagslist = []
481        for k, v in kwargs.items():
482            if str(k).startswith("__"):
483                continue
484            tagslist.append({"Key": str(k), "Value": str(v)})
485        conn.remove_tags(
486            ResourceId=_get_trail_arn(
487                Name, region=region, key=key, keyid=keyid, profile=profile
488            ),
489            TagsList=tagslist,
490        )
491        return {"tagged": True}
492    except ClientError as e:
493        return {"tagged": False, "error": __utils__["boto3.get_error"](e)}
494
495
496def list_tags(Name, region=None, key=None, keyid=None, profile=None):
497    """
498    List tags of a trail
499
500    Returns:
501        tags:
502          - {...}
503          - {...}
504
505    CLI Example:
506
507    .. code-block:: bash
508
509        salt myminion boto_cloudtrail.list_tags my_trail
510
511    """
512
513    try:
514        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
515        rid = _get_trail_arn(Name, region=region, key=key, keyid=keyid, profile=profile)
516        ret = conn.list_tags(ResourceIdList=[rid])
517        tlist = ret.get("ResourceTagList", []).pop().get("TagsList")
518        tagdict = {}
519        for tag in tlist:
520            tagdict[tag.get("Key")] = tag.get("Value")
521        return {"tags": tagdict}
522    except ClientError as e:
523        return {"error": __utils__["boto3.get_error"](e)}
524