1# Copyright The Cloud Custodian Authors.
2# SPDX-License-Identifier: Apache-2.0
3import json
4import hashlib
5import operator
6
7from concurrent.futures import as_completed
8from c7n.actions import Action
9from c7n.exceptions import PolicyValidationError
10from c7n.filters import Filter, CrossAccountAccessFilter, ValueFilter
11from c7n.filters.kms import KmsRelatedFilter
12from c7n.query import QueryResourceManager, TypeInfo
13from c7n.manager import resources
14from c7n.tags import universal_augment
15from c7n.utils import chunks, get_retry, local_session, type_schema, filter_empty
16from c7n.version import version
17
18from .aws import shape_validate
19from .ec2 import EC2
20
21
22@resources.register('ssm-parameter')
23class SSMParameter(QueryResourceManager):
24
25 class resource_type(TypeInfo):
26 service = 'ssm'
27 enum_spec = ('describe_parameters', 'Parameters', None)
28 name = "Name"
29 id = "Name"
30 universal_taggable = True
31 arn_type = "parameter"
32 cfn_type = 'AWS::SSM::Parameter'
33
34 retry = staticmethod(get_retry(('Throttled',)))
35 permissions = ('ssm:GetParameters',
36 'ssm:DescribeParameters')
37
38 augment = universal_augment
39
40
41@SSMParameter.action_registry.register('delete')
42class DeleteParameter(Action):
43
44 schema = type_schema('delete')
45 permissions = ("ssm:DeleteParameter",)
46
47 def process(self, resources):
48 client = local_session(self.manager.session_factory).client('ssm')
49 for r in resources:
50 self.manager.retry(
51 client.delete_parameter, Name=r['Name'],
52 ignore_err_codes=('ParameterNotFound',))
53
54
55@resources.register('ssm-managed-instance')
56class ManagedInstance(QueryResourceManager):
57
58 class resource_type(TypeInfo):
59 service = 'ssm'
60 enum_spec = ('describe_instance_information', 'InstanceInformationList', None)
61 id = 'InstanceId'
62 name = 'Name'
63 date = 'RegistrationDate'
64 arn_type = "managed-instance"
65
66 permissions = ('ssm:DescribeInstanceInformation',)
67
68
69@EC2.action_registry.register('send-command')
70@ManagedInstance.action_registry.register('send-command')
71class SendCommand(Action):
72 """Run an SSM Automation Document on an instance.
73
74 :Example:
75
76 Find ubuntu 18.04 instances are active with ssm.
77
78 .. code-block:: yaml
79
80 policies:
81 - name: ec2-osquery-install
82 resource: ec2
83 filters:
84 - type: ssm
85 key: PingStatus
86 value: Online
87 - type: ssm
88 key: PlatformName
89 value: Ubuntu
90 - type: ssm
91 key: PlatformVersion
92 value: 18.04
93 actions:
94 - type: send-command
95 command:
96 DocumentName: AWS-RunShellScript
97 Parameters:
98 commands:
99 - wget https://pkg.osquery.io/deb/osquery_3.3.0_1.linux.amd64.deb
100 - dpkg -i osquery_3.3.0_1.linux.amd64.deb
101 """
102
103 schema = type_schema(
104 'send-command',
105 command={'type': 'object'},
106 required=('command',))
107
108 permissions = ('ssm:SendCommand',)
109 shape = "SendCommandRequest"
110 annotation = 'c7n:SendCommand'
111
112 def validate(self):
113 shape_validate(self.data['command'], self.shape, 'ssm')
114 # If used against an ec2 resource, require an ssm status filter
115 # to ensure that we're not trying to send commands to instances
116 # that aren't in ssm.
117 if self.manager.type != 'ec2':
118 return
119
120 found = False
121 for f in self.manager.iter_filters():
122 if f.type == 'ssm':
123 found = True
124 break
125 if not found:
126 raise PolicyValidationError(
127 "send-command requires use of ssm filter on ec2 resources")
128
129 def process(self, resources):
130 client = local_session(self.manager.session_factory).client('ssm')
131 for resource_set in chunks(resources, 50):
132 self.process_resource_set(client, resource_set)
133
134 def process_resource_set(self, client, resources):
135 command = dict(self.data['command'])
136 command['InstanceIds'] = [
137 r['InstanceId'] for r in resources]
138 result = client.send_command(**command).get('Command')
139 for r in resources:
140 r.setdefault('c7n:SendCommand', []).append(result['CommandId'])
141
142
143@resources.register('ssm-activation')
144class SSMActivation(QueryResourceManager):
145
146 class resource_type(TypeInfo):
147 service = 'ssm'
148 enum_spec = ('describe_activations', 'ActivationList', None)
149 id = 'ActivationId'
150 name = 'Description'
151 date = 'CreatedDate'
152 arn = False
153
154 permissions = ('ssm:DescribeActivations',)
155
156
157@SSMActivation.action_registry.register('delete')
158class DeleteSSMActivation(Action):
159 schema = type_schema('delete')
160 permissions = ('ssm:DeleteActivation',)
161
162 def process(self, resources):
163 client = local_session(self.manager.session_factory).client('ssm')
164 for a in resources:
165 client.delete_activation(ActivationId=a["ActivationId"])
166
167
168@resources.register('ops-item')
169class OpsItem(QueryResourceManager):
170 """Resource for OpsItems in SSM OpsCenter
171 https://docs.aws.amazon.com/systems-manager/latest/userguide/OpsCenter.html
172 """
173 class resource_type(TypeInfo):
174
175 enum_spec = ('describe_ops_items', 'OpsItemSummaries', None)
176 service = 'ssm'
177 arn_type = 'opsitem'
178 id = 'OpsItemId'
179 name = 'Title'
180
181 default_report_fields = (
182 'Status', 'Title', 'LastModifiedTime',
183 'CreatedBy', 'CreatedTime')
184
185 QueryKeys = {
186 'Status',
187 'CreatedBy',
188 'Source',
189 'Priority',
190 'Title',
191 'OpsItemId',
192 'CreatedTime',
193 'LastModifiedTime',
194 'OperationalData',
195 'OperationalDataKey',
196 'OperationalDataValue',
197 'ResourceId',
198 'AutomationId'}
199 QueryOperators = {'Equal', 'LessThan', 'GreaterThan', 'Contains'}
200
201 def validate(self):
202 self.query = self.resource_query()
203 return super(OpsItem, self).validate()
204
205 def get_resources(self, ids, cache=True, augment=True):
206 if isinstance(ids, str):
207 ids = [ids]
208 return self.resources({
209 'OpsItemFilters': [{
210 'Key': 'OpsItemId',
211 'Values': [i],
212 'Operator': 'Equal'} for i in ids]})
213
214 def resources(self, query=None):
215 q = self.resource_query()
216 if q and query and 'OpsItemFilters' in query:
217 q['OpsItemFilters'].extend(query['OpsItemFilters'])
218 return super(OpsItem, self).resources(query=q)
219
220 def resource_query(self):
221 filters = []
222 for q in self.data.get('query', ()):
223 if (not isinstance(q, dict) or
224 not set(q.keys()) == {'Key', 'Values', 'Operator'} or
225 q['Key'] not in self.QueryKeys or
226 q['Operator'] not in self.QueryOperators):
227 raise PolicyValidationError(
228 "invalid ops-item query %s" % self.data['query'])
229 filters.append(q)
230 return {'OpsItemFilters': filters}
231
232
233@OpsItem.action_registry.register('update')
234class UpdateOpsItem(Action):
235 """Update an ops item.
236
237 : example :
238
239 Close out open ops items older than 30 days for a given issue.
240
241 .. code-block:: yaml
242
243 policies:
244 - name: issue-items
245 resource: aws.ops-item
246 filters:
247 - Status: Open
248 - Title: checking-lambdas
249 - type: value
250 key: CreatedTime
251 value_type: age
252 op: greater-than
253 value: 30
254 actions:
255 - type: update
256 status: Resolved
257 """
258
259 schema = type_schema(
260 'update',
261 description={'type': 'string'},
262 priority={'enum': list(range(1, 6))},
263 title={'type': 'string'},
264 topics={'type': 'array', 'items': {'type': 'string'}},
265 status={'enum': ['Open', 'In Progress', 'Resolved']},
266 )
267 permissions = ('ssm:UpdateOpsItem',)
268
269 def process(self, resources):
270 attrs = dict(self.data)
271 attrs = filter_empty({
272 'Description': attrs.get('description'),
273 'Title': attrs.get('title'),
274 'Priority': attrs.get('priority'),
275 'Status': attrs.get('status'),
276 'Notifications': [{'Arn': a} for a in attrs.get('topics', ())]})
277
278 modified = []
279 for r in resources:
280 for k, v in attrs.items():
281 if k not in r or r[k] != v:
282 modified.append(r)
283
284 self.log.debug("Updating %d of %d ops items", len(modified), len(resources))
285 client = local_session(self.manager.session_factory).client('ssm')
286 for m in modified:
287 client.update_ops_item(OpsItemId=m['OpsItemId'], **attrs)
288
289
290class OpsItemFilter(Filter):
291 """Filter resources associated to extant OpsCenter operational items.
292
293 :example:
294
295 Find ec2 instances with open ops items.
296
297 .. code-block:: yaml
298
299 policies:
300 - name: ec2-instances-ops-items
301 resource: ec2
302 filters:
303 - type: ops-item
304 # we can filter on source, title, priority
305 priority: [1, 2]
306 """
307
308 schema = type_schema(
309 'ops-item',
310 status={'type': 'array',
311 'default': ['Open'],
312 'items': {'enum': ['Open', 'In progress', 'Resolved']}},
313 priority={'type': 'array', 'items': {'enum': list(range(1, 6))}},
314 title={'type': 'string'},
315 source={'type': 'string'})
316 schema_alias = True
317 permissions = ('ssm:DescribeOpsItems',)
318
319 def process(self, resources, event=None):
320 client = local_session(self.manager.session_factory).client('ssm')
321 results = []
322
323 for resource_set in chunks(resources, 10):
324 qf = self.get_query_filter(resource_set)
325 items = client.describe_ops_items(**qf).get('OpsItemSummaries')
326
327 arn_item_map = {}
328 for i in items:
329 for arn in json.loads(
330 i['OperationalData']['/aws/resources']['Value']):
331 arn_item_map.setdefault(arn['arn'], []).append(i['OpsItemId'])
332
333 for arn, r in zip(self.manager.get_arns(resource_set), resource_set):
334 if arn in arn_item_map:
335 r['c7n:opsitems'] = arn_item_map[arn]
336 results.append(r)
337 return results
338
339 def get_query_filter(self, resources):
340 q = []
341 q.append({'Key': 'Status', 'Operator': 'Equal',
342 'Values': self.data.get('status', ('Open',))})
343 if self.data.get('priority'):
344 q.append({'Key': 'Priority', 'Operator': 'Equal',
345 'Values': list(map(str, self.data['priority']))})
346 if self.data.get('title'):
347 q.append({'Key': 'Title', 'Operator': 'Contains',
348 'Values': [self.data['title']]})
349 if self.data.get('source'):
350 q.append({'Key': 'Source', 'Operator': 'Equal',
351 'Values': [self.data['source']]})
352 q.append({'Key': 'ResourceId', 'Operator': 'Contains',
353 'Values': [r[self.manager.resource_type.id] for r in resources]})
354 return {'OpsItemFilters': q}
355
356 @classmethod
357 def register_resource(cls, registry, resource_class):
358 if 'ops-item' not in resource_class.filter_registry:
359 resource_class.filter_registry.register('ops-item', cls)
360
361
362resources.subscribe(OpsItemFilter.register_resource)
363
364
365class PostItem(Action):
366 """Post an OpsItem to AWS Systems Manager OpsCenter Dashboard.
367
368 https://docs.aws.amazon.com/systems-manager/latest/userguide/OpsCenter.html
369
370 Each ops item supports up to a 100 associated resources. This
371 action supports the builtin OpsCenter dedup logic with additional
372 support for associating new resources to existing Open ops items.
373
374 : Example :
375
376 Create an ops item for ec2 instances with Create User permissions
377
378 .. code-block:: yaml
379
380 policies:
381 - name: over-privileged-ec2
382 resource: aws.ec2
383 filters:
384 - type: check-permissions
385 match: allowed
386 actions:
387 - iam:CreateUser
388 actions:
389 - type: post-item
390 priority: 3
391
392 The builtin OpsCenter dedup logic will kick in if the same
393 resource set (ec2 instances in this case) is posted for the same
394 policy.
395
396 : Example :
397
398 Create an ops item for sqs queues with cross account access as ops items.
399
400 .. code-block:: yaml
401
402 policies:
403 - name: sqs-cross-account-access
404 resource: aws.sqs
405 filters:
406 - type: cross-account
407 actions:
408 - type: mark-for-op
409 days: 5
410 op: delete
411 - type: post-item
412 title: SQS Cross Account Access
413 description: |
414 Cross Account Access detected in SQS resource IAM Policy.
415 tags:
416 Topic: Security
417 """
418
419 schema = type_schema(
420 'post-item',
421 description={'type': 'string'},
422 tags={'type': 'object'},
423 priority={'enum': list(range(1, 6))},
424 title={'type': 'string'},
425 topics={'type': 'string'},
426 )
427 schema_alias = True
428 permissions = ('ssm:CreateOpsItem',)
429
430 def process(self, resources, event=None):
431 client = local_session(self.manager.session_factory).client('ssm')
432 item_template = self.get_item_template()
433 resources = list(sorted(resources, key=operator.itemgetter(
434 self.manager.resource_type.id)))
435 items = self.get_items(client, item_template)
436 if items:
437 # - Use a copy of the template as we'll be passing in status changes on updates.
438 # - The return resources will be those that we couldn't fit into updates
439 # to existing resources.
440 resources = self.update_items(client, items, dict(item_template), resources)
441
442 item_ids = [i['OpsItemId'] for i in items[:5]]
443
444 for resource_set in chunks(resources, 100):
445 resource_arns = json.dumps(
446 [{'arn': arn} for arn in sorted(self.manager.get_arns(resource_set))])
447 item_template['OperationalData']['/aws/resources'] = {
448 'Type': 'SearchableString', 'Value': resource_arns}
449 if items:
450 item_template['RelatedOpsItems'] = [
451 {'OpsItemId': item_ids[:5]}]
452 try:
453 oid = client.create_ops_item(**item_template).get('OpsItemId')
454 item_ids.insert(0, oid)
455 except client.exceptions.OpsItemAlreadyExistsException:
456 pass
457
458 for r in resource_set:
459 r['c7n:opsitem'] = oid
460
461 def get_items(self, client, item_template):
462 qf = [
463 {'Key': 'OperationalDataValue',
464 'Operator': 'Contains',
465 'Values': [item_template['OperationalData'][
466 '/custodian/dedup']['Value']]},
467 {'Key': 'OperationalDataKey',
468 'Operator': 'Equal',
469 'Values': ['/custodian/dedup']},
470 {'Key': 'Status',
471 'Operator': 'Equal',
472 # In progress could imply activity/executions underway, we don't want to update
473 # the resource set out from underneath that so only look at Open state.
474 'Values': ['Open']},
475 {'Key': 'Source',
476 'Operator': 'Equal',
477 'Values': ['Cloud Custodian']}]
478 items = client.describe_ops_items(OpsItemFilters=qf)['OpsItemSummaries']
479 return list(sorted(items, key=operator.itemgetter('CreatedTime'), reverse=True))
480
481 def update_items(self, client, items, item_template, resources):
482 """Update existing Open OpsItems with new resources.
483
484 Originally this tried to support attribute updates as well, but
485 the reasoning around that is a bit complex due to partial state
486 evaluation around any given execution, so its restricted atm
487 to just updating associated resources.
488
489 For management of ops items, use a policy on the
490 ops-item resource.
491
492 Rationale: Typically a custodian policy will be evaluating
493 some partial set of resources at any given execution (ie think
494 a lambda looking at newly created resources), where as a
495 collection of ops center items will represent the total
496 set. Custodian can multiplex the partial set of resource over
497 a set of ops items (100 resources per item) which minimizes
498 the item count. When updating the state of an ops item though,
499 we have to contend with the possibility that we're doing so
500 with only a partial state. Which could be confusing if we
501 tried to set the Status to Resolved even if we're only evaluating
502 a handful of resources associated to an ops item.
503 """
504 arn_item_map = {}
505 item_arn_map = {}
506 for i in items:
507 item_arn_map[i['OpsItemId']] = arns = json.loads(
508 i['OperationalData']['/aws/resources']['Value'])
509 for arn in arns:
510 arn_item_map[arn['arn']] = i['OpsItemId']
511
512 arn_resource_map = dict(zip(self.manager.get_arns(resources), resources))
513 added = set(arn_resource_map).difference(arn_item_map)
514
515 updated = set()
516 remainder = []
517
518 # Check for resource additions
519 for a in added:
520 handled = False
521 for i in items:
522 if len(item_arn_map[i['OpsItemId']]) >= 100:
523 continue
524 item_arn_map[i['OpsItemId']].append({'arn': a})
525 updated.add(i['OpsItemId'])
526 arn_resource_map[a]['c7n:opsitem'] = i['OpsItemId']
527 handled = True
528 break
529 if not handled:
530 remainder.append(a)
531
532 for i in items:
533 if i['OpsItemId'] not in updated:
534 continue
535 i = dict(i)
536 for k in ('CreatedBy', 'CreatedTime', 'Source', 'LastModifiedBy',
537 'LastModifiedTime'):
538 i.pop(k, None)
539 i['OperationalData']['/aws/resources']['Value'] = json.dumps(
540 item_arn_map[i['OpsItemId']])
541 i['OperationalData'].pop('/aws/dedup', None)
542 client.update_ops_item(**i)
543 return remainder
544
545 def get_item_template(self):
546 title = self.data.get('title', self.manager.data['name']).strip()
547 dedup = ("%s %s %s %s" % (
548 title,
549 self.manager.type,
550 self.manager.config.region,
551 self.manager.config.account_id)).encode('utf8')
552 # size restrictions on this value is 4-20, digest is 32
553 dedup = hashlib.md5(dedup).hexdigest()[:20] # nosec nosemgrep
554
555 i = dict(
556 Title=title,
557 Description=self.data.get(
558 'description',
559 self.manager.data.get(
560 'description',
561 self.manager.data.get('name'))),
562 Priority=self.data.get('priority'),
563 Source="Cloud Custodian",
564 Tags=[{'Key': k, 'Value': v} for k, v in self.data.get(
565 'tags', self.manager.data.get('tags', {})).items()],
566 Notifications=[{'Arn': a} for a in self.data.get('topics', ())],
567 OperationalData={
568 '/aws/dedup': {
569 'Type': 'SearchableString',
570 'Value': json.dumps({'dedupString': dedup})},
571 '/custodian/execution-id': {
572 'Type': 'String',
573 'Value': self.manager.ctx.execution_id},
574 # We need our own dedup string to be able to filter
575 # search on it.
576 '/custodian/dedup': {
577 'Type': 'SearchableString',
578 'Value': dedup},
579 '/custodian/policy': {
580 'Type': 'String',
581 'Value': json.dumps(self.manager.data)},
582 '/custodian/version': {
583 'Type': 'String',
584 'Value': version},
585 '/custodian/policy-name': {
586 'Type': 'SearchableString',
587 'Value': self.manager.data['name']},
588 '/custodian/resource': {
589 'Type': 'SearchableString',
590 'Value': self.manager.type},
591 }
592 )
593 return filter_empty(i)
594
595 @classmethod
596 def register_resource(cls, registry, resource_class):
597 if 'post-item' not in resource_class.action_registry:
598 resource_class.action_registry.register('post-item', cls)
599
600
601resources.subscribe(PostItem.register_resource)
602
603
604@resources.register('ssm-document')
605class SSMDocument(QueryResourceManager):
606
607 class resource_type(TypeInfo):
608 service = 'ssm'
609 enum_spec = ('list_documents', 'DocumentIdentifiers', {'Filters': [
610 {
611 'Key': 'Owner',
612 'Values': ['Self']}]})
613 name = id = 'Name'
614 date = 'RegistrationDate'
615 arn_type = 'document'
616 cfn_type = config_type = "AWS::SSM::Document"
617 universal_taggable = object()
618
619 permissions = ('ssm:ListDocuments',)
620
621
622@SSMDocument.filter_registry.register('content')
623class ContentFilter(ValueFilter):
624 """
625 Applies value type filter on the content of an SSM Document.
626 :example:
627
628 .. code-block:: yaml
629
630 policies:
631 - name: document-content
632 resource: ssm-document
633 filters:
634 - type: content
635 key: cloudWatchEncryptionEnabled
636 op: eq
637 value: false
638 """
639
640 schema = type_schema('content', rinherit=ValueFilter.schema)
641 schema_alias = False
642 permissions = ('ssm:GetDocument',)
643 policy_annotation = 'c7n:MatchedContent'
644 content_annotation = "c7n:Content"
645
646 def process(self, resources, event=None):
647 client = local_session(self.manager.session_factory).client('ssm')
648 results = []
649 for r in resources:
650 if self.content_annotation not in r:
651 doc = self.manager.retry(client.get_document, Name=r['Name'])
652 doc['Content'] = json.loads(doc['Content'])
653 doc.pop('ResponseMetadata', None)
654 r[self.content_annotation] = doc
655 if self.match(doc['Content']):
656 r[self.policy_annotation] = self.data.get('value')
657 results.append(r)
658 return results
659
660
661@SSMDocument.filter_registry.register('cross-account')
662class SSMDocumentCrossAccount(CrossAccountAccessFilter):
663 """Filter SSM documents which have cross account permissions
664
665 :example:
666
667 .. code-block:: yaml
668
669 policies:
670 - name: ssm-cross-account
671 resource: ssm-document
672 filters:
673 - type: cross-account
674 whitelist: [xxxxxxxxxxxx]
675 """
676
677 permissions = ('ssm:DescribeDocumentPermission',)
678
679 def process(self, resources, event=None):
680 self.accounts = self.get_accounts()
681 results = []
682 client = local_session(self.manager.session_factory).client('ssm')
683 with self.executor_factory(max_workers=3) as w:
684 futures = []
685 for resource_set in chunks(resources, 10):
686 futures.append(w.submit(
687 self.process_resource_set, client, resource_set))
688 for f in as_completed(futures):
689 if f.exception():
690 self.log.error(
691 "Exception checking cross account access \n %s" % (
692 f.exception()))
693 continue
694 results.extend(f.result())
695 return results
696
697 def process_resource_set(self, client, resource_set):
698 results = []
699 for r in resource_set:
700 attrs = self.manager.retry(
701 client.describe_document_permission,
702 Name=r['Name'],
703 PermissionType='Share',
704 ignore_err_codes=('InvalidDocument',))['AccountSharingInfoList']
705 shared_accounts = {
706 g.get('AccountId') for g in attrs}
707 delta_accounts = shared_accounts.difference(self.accounts)
708 if delta_accounts:
709 r['c7n:CrossAccountViolations'] = list(delta_accounts)
710 results.append(r)
711 return results
712
713
714@SSMDocument.action_registry.register('set-sharing')
715class RemoveSharingSSMDocument(Action):
716 """Edit list of accounts that share permissions on an SSM document. Pass in a list of account
717 IDs to the 'add' or 'remove' fields to edit document sharing permissions.
718 Set 'remove' to 'matched' to automatically remove any external accounts on a
719 document (use in conjunction with the cross-account filter).
720
721 :example:
722
723 .. code-block:: yaml
724
725 policies:
726 - name: ssm-set-sharing
727 resource: ssm-document
728 filters:
729 - type: cross-account
730 whitelist: [xxxxxxxxxxxx]
731 actions:
732 - type: set-sharing
733 add: [yyyyyyyyyy]
734 remove: matched
735 """
736
737 schema = type_schema('set-sharing',
738 remove={
739 'oneOf': [
740 {'enum': ['matched']},
741 {'type': 'array', 'items': {
742 'type': 'string'}},
743 ]},
744 add={
745 'type': 'array', 'items': {
746 'type': 'string'}})
747 permissions = ('ssm:ModifyDocumentPermission',)
748
749 def process(self, resources):
750 client = local_session(self.manager.session_factory).client('ssm')
751 add_accounts = self.data.get('add', [])
752 remove_accounts = self.data.get('remove', [])
753 if self.data.get('remove') == 'matched':
754 for r in resources:
755 try:
756 client.modify_document_permission(
757 Name=r['Name'],
758 PermissionType='Share',
759 AccountIdsToAdd=add_accounts,
760 AccountIdsToRemove=r['c7n:CrossAccountViolations']
761 )
762 except client.exceptions.InvalidDocumentOperation as e:
763 raise e
764 else:
765 for r in resources:
766 try:
767 client.modify_document_permission(
768 Name=r['Name'],
769 PermissionType='Share',
770 AccountIdsToAdd=add_accounts,
771 AccountIdsToRemove=remove_accounts
772 )
773 except client.exceptions.InvalidDocumentOperation as e:
774 raise e
775
776
777@SSMDocument.action_registry.register('delete')
778class DeleteSSMDocument(Action):
779 """Delete SSM documents. Set force flag to True to force delete on documents that are
780 shared across accounts. This will remove those shared accounts, and then delete the document.
781 Otherwise, delete will fail and raise InvalidDocumentOperation exception
782 if a document is shared with other accounts. Default value for force is False.
783
784 :example:
785
786 .. code-block:: yaml
787
788 policies:
789 - name: ssm-delete-documents
790 resource: ssm-document
791 filters:
792 - type: cross-account
793 whitelist: [xxxxxxxxxxxx]
794 actions:
795 - type: delete
796 force: True
797 """
798
799 schema = type_schema(
800 'delete',
801 force={'type': 'boolean'}
802 )
803
804 permissions = ('ssm:DeleteDocument', 'ssm:ModifyDocumentPermission',)
805
806 def process(self, resources):
807 client = local_session(self.manager.session_factory).client('ssm')
808 for r in resources:
809 try:
810 client.delete_document(Name=r['Name'], Force=True)
811 except client.exceptions.InvalidDocumentOperation as e:
812 if self.data.get('force', False):
813 response = client.describe_document_permission(
814 Name=r['Name'],
815 PermissionType='Share'
816 )
817 client.modify_document_permission(
818 Name=r['Name'],
819 PermissionType='Share',
820 AccountIdsToRemove=response.get('AccountIds', [])
821 )
822 client.delete_document(
823 Name=r['Name'],
824 Force=True
825 )
826 else:
827 raise e
828
829
830@resources.register('ssm-data-sync')
831class SSMDataSync(QueryResourceManager):
832 """Resource for AWS DataSync
833 https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-inventory-datasync.html
834 """
835 class resource_type(TypeInfo):
836
837 enum_spec = ('list_resource_data_sync', 'ResourceDataSyncItems', None)
838 service = 'ssm'
839 arn_type = 'resource-data-sync'
840 id = name = 'SyncName'
841
842 permissions = ('ssm:ListResourceDataSync',)
843
844
845@SSMDataSync.filter_registry.register('kms-key')
846class KmsFilter(KmsRelatedFilter):
847 RelatedIdsExpression = 'S3Destination.AWSKMSKeyARN'
848
849
850@SSMDataSync.action_registry.register('delete')
851class DeleteDataSync(Action):
852 """Delete SSM data sync resources.
853
854 :example:
855
856 .. code-block:: yaml
857
858 policies:
859 - name: delete-resource-data-sync
860 resource: ssm-data-sync
861 actions:
862 - type: delete
863 """
864 permissions = ('ssm:DeleteResourceDataSync',)
865 schema = type_schema('delete')
866
867 def process(self, resources):
868 client = local_session(self.manager.session_factory).client('ssm')
869 for r in resources:
870 try:
871 client.delete_resource_data_sync(SyncName=r['SyncName'])
872 except client.exceptions.ResourceDataSyncNotFoundException:
873 continue
874
875
876@resources.register("ssm-patch-group")
877class SsmPatchGroup(QueryResourceManager):
878 class resource_type(TypeInfo):
879 service = "ssm"
880 enum_spec = ('describe_patch_groups', 'Mappings', None)
881 arn = False
882 id = "PatchGroup"
883 name = "PatchGroup"
884
885
886@resources.register('ssm-session-manager')
887class SSMSessionManager(QueryResourceManager):
888
889 class resource_type(TypeInfo):
890 service = 'ssm'
891 enum_spec = ('describe_sessions', 'Sessions', None)
892 name = "SessionId"
893 id = "SessionId"
894 arn_type = 'session'
895
896 retry = staticmethod(get_retry(('Throttled',)))
897 permissions = ('ssm:DescribeSessions',)
898
899 augment = universal_augment
900
901 def resources(self, query=None):
902 if query is None:
903 query = {}
904 if 'State' not in query:
905 # Default to Active if not given
906 query['State'] = 'Active'
907 return super(SSMSessionManager, self).resources(query=query)
908
909
910@SSMSessionManager.action_registry.register('terminate')
911class TerminateSession(Action):
912 """ Terminate a session.
913
914 This call will permanently end a session's connection to an instance.
915
916 :Example:
917
918 .. code-block:: yaml
919
920 policies:
921 - name: ssm-session-termination
922 resource: ssm-session-manager
923 actions:
924 - terminate
925 """
926 schema = type_schema('terminate')
927 permissions = ('ssm:TerminateSession',)
928
929 def process(self, resources):
930 client = local_session(self.manager.session_factory).client('ssm')
931 for r in resources:
932 client.terminate_session(SessionId=r['SessionId'])