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