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