Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/c7n/resources/ssm.py: 47%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

393 statements  

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'])