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

371 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-08 06:51 +0000

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 

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 

617 permissions = ('ssm:ListDocuments',) 

618 

619 

620@SSMDocument.filter_registry.register('cross-account') 

621class SSMDocumentCrossAccount(CrossAccountAccessFilter): 

622 """Filter SSM documents which have cross account permissions 

623 

624 :example: 

625 

626 .. code-block:: yaml 

627 

628 policies: 

629 - name: ssm-cross-account 

630 resource: ssm-document 

631 filters: 

632 - type: cross-account 

633 whitelist: [xxxxxxxxxxxx] 

634 """ 

635 

636 permissions = ('ssm:DescribeDocumentPermission',) 

637 

638 def process(self, resources, event=None): 

639 self.accounts = self.get_accounts() 

640 results = [] 

641 client = local_session(self.manager.session_factory).client('ssm') 

642 with self.executor_factory(max_workers=3) as w: 

643 futures = [] 

644 for resource_set in chunks(resources, 10): 

645 futures.append(w.submit( 

646 self.process_resource_set, client, resource_set)) 

647 for f in as_completed(futures): 

648 if f.exception(): 

649 self.log.error( 

650 "Exception checking cross account access \n %s" % ( 

651 f.exception())) 

652 continue 

653 results.extend(f.result()) 

654 return results 

655 

656 def process_resource_set(self, client, resource_set): 

657 results = [] 

658 for r in resource_set: 

659 attrs = self.manager.retry( 

660 client.describe_document_permission, 

661 Name=r['Name'], 

662 PermissionType='Share', 

663 ignore_err_codes=('InvalidDocument',))['AccountSharingInfoList'] 

664 shared_accounts = { 

665 g.get('AccountId') for g in attrs} 

666 delta_accounts = shared_accounts.difference(self.accounts) 

667 if delta_accounts: 

668 r['c7n:CrossAccountViolations'] = list(delta_accounts) 

669 results.append(r) 

670 return results 

671 

672 

673@SSMDocument.action_registry.register('set-sharing') 

674class RemoveSharingSSMDocument(Action): 

675 """Edit list of accounts that share permissions on an SSM document. Pass in a list of account 

676 IDs to the 'add' or 'remove' fields to edit document sharing permissions. 

677 Set 'remove' to 'matched' to automatically remove any external accounts on a 

678 document (use in conjunction with the cross-account filter). 

679 

680 :example: 

681 

682 .. code-block:: yaml 

683 

684 policies: 

685 - name: ssm-set-sharing 

686 resource: ssm-document 

687 filters: 

688 - type: cross-account 

689 whitelist: [xxxxxxxxxxxx] 

690 actions: 

691 - type: set-sharing 

692 add: [yyyyyyyyyy] 

693 remove: matched 

694 """ 

695 

696 schema = type_schema('set-sharing', 

697 remove={ 

698 'oneOf': [ 

699 {'enum': ['matched']}, 

700 {'type': 'array', 'items': { 

701 'type': 'string'}}, 

702 ]}, 

703 add={ 

704 'type': 'array', 'items': { 

705 'type': 'string'}}) 

706 permissions = ('ssm:ModifyDocumentPermission',) 

707 

708 def process(self, resources): 

709 client = local_session(self.manager.session_factory).client('ssm') 

710 add_accounts = self.data.get('add', []) 

711 remove_accounts = self.data.get('remove', []) 

712 if self.data.get('remove') == 'matched': 

713 for r in resources: 

714 try: 

715 client.modify_document_permission( 

716 Name=r['Name'], 

717 PermissionType='Share', 

718 AccountIdsToAdd=add_accounts, 

719 AccountIdsToRemove=r['c7n:CrossAccountViolations'] 

720 ) 

721 except client.exceptions.InvalidDocumentOperation as e: 

722 raise e 

723 else: 

724 for r in resources: 

725 try: 

726 client.modify_document_permission( 

727 Name=r['Name'], 

728 PermissionType='Share', 

729 AccountIdsToAdd=add_accounts, 

730 AccountIdsToRemove=remove_accounts 

731 ) 

732 except client.exceptions.InvalidDocumentOperation as e: 

733 raise e 

734 

735 

736@SSMDocument.action_registry.register('delete') 

737class DeleteSSMDocument(Action): 

738 """Delete SSM documents. Set force flag to True to force delete on documents that are 

739 shared across accounts. This will remove those shared accounts, and then delete the document. 

740 Otherwise, delete will fail and raise InvalidDocumentOperation exception 

741 if a document is shared with other accounts. Default value for force is False. 

742 

743 :example: 

744 

745 .. code-block:: yaml 

746 

747 policies: 

748 - name: ssm-delete-documents 

749 resource: ssm-document 

750 filters: 

751 - type: cross-account 

752 whitelist: [xxxxxxxxxxxx] 

753 actions: 

754 - type: delete 

755 force: True 

756 """ 

757 

758 schema = type_schema( 

759 'delete', 

760 force={'type': 'boolean'} 

761 ) 

762 

763 permissions = ('ssm:DeleteDocument', 'ssm:ModifyDocumentPermission',) 

764 

765 def process(self, resources): 

766 client = local_session(self.manager.session_factory).client('ssm') 

767 for r in resources: 

768 try: 

769 client.delete_document(Name=r['Name'], Force=True) 

770 except client.exceptions.InvalidDocumentOperation as e: 

771 if self.data.get('force', False): 

772 response = client.describe_document_permission( 

773 Name=r['Name'], 

774 PermissionType='Share' 

775 ) 

776 client.modify_document_permission( 

777 Name=r['Name'], 

778 PermissionType='Share', 

779 AccountIdsToRemove=response.get('AccountIds', []) 

780 ) 

781 client.delete_document( 

782 Name=r['Name'], 

783 Force=True 

784 ) 

785 else: 

786 raise e 

787 

788 

789@resources.register('ssm-data-sync') 

790class SSMDataSync(QueryResourceManager): 

791 """Resource for AWS DataSync 

792 https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-inventory-datasync.html 

793 """ 

794 class resource_type(TypeInfo): 

795 

796 enum_spec = ('list_resource_data_sync', 'ResourceDataSyncItems', None) 

797 service = 'ssm' 

798 arn_type = 'resource-data-sync' 

799 id = name = 'SyncName' 

800 

801 permissions = ('ssm:ListResourceDataSync',) 

802 

803 

804@SSMDataSync.filter_registry.register('kms-key') 

805class KmsFilter(KmsRelatedFilter): 

806 RelatedIdsExpression = 'S3Destination.AWSKMSKeyARN' 

807 

808 

809@SSMDataSync.action_registry.register('delete') 

810class DeleteDataSync(Action): 

811 """Delete SSM data sync resources. 

812 

813 :example: 

814 

815 .. code-block:: yaml 

816 

817 policies: 

818 - name: delete-resource-data-sync 

819 resource: ssm-data-sync 

820 actions: 

821 - type: delete 

822 """ 

823 permissions = ('ssm:DeleteResourceDataSync',) 

824 schema = type_schema('delete') 

825 

826 def process(self, resources): 

827 client = local_session(self.manager.session_factory).client('ssm') 

828 for r in resources: 

829 try: 

830 client.delete_resource_data_sync(SyncName=r['SyncName']) 

831 except client.exceptions.ResourceDataSyncNotFoundException: 

832 continue 

833 

834 

835 

836@resources.register("ssm-patch-group") 

837class SsmPatchGroup(QueryResourceManager): 

838 class resource_type(TypeInfo): 

839 service = "ssm" 

840 enum_spec = ('describe_patch_groups', 'Mappings', None) 

841 arn = False 

842 id = "PatchGroup" 

843 name = "PatchGroup" 

844 

845 

846@resources.register('ssm-session-manager') 

847class SSMSessionManager(QueryResourceManager): 

848 

849 class resource_type(TypeInfo): 

850 service = 'ssm' 

851 enum_spec = ('describe_sessions', 'Sessions', None) 

852 name = "SessionId" 

853 id = "SessionId" 

854 arn_type = 'session' 

855 

856 retry = staticmethod(get_retry(('Throttled',))) 

857 permissions = ('ssm:DescribeSessions', 'ssm:TerminateSession', ) 

858 

859 augment = universal_augment 

860 

861 def resources(self, query=None): 

862 if query is None: 

863 query = {} 

864 if 'State' not in query: 

865 # Default to Active if not given 

866 query['State'] = 'Active' 

867 return super(SSMSessionManager, self).resources(query=query) 

868 

869@SSMSessionManager.action_registry.register('terminate') 

870class TerminateSession(Action): 

871 """ Terminate a session. 

872 

873 This call will permanently end a session's connection to an instance. 

874 

875 :Example: 

876 

877 .. code-block:: yaml 

878 

879 policies: 

880 - name: ssm-session-termination 

881 resource: ssm-session-manager 

882 actions: 

883 - terminate 

884 """ 

885 schema = type_schema('terminate') 

886 

887 def get_permissions(self): 

888 return ('ssm:TerminateSession',) 

889 

890 def process(self, resources): 

891 client = local_session(self.manager.session_factory).client('ssm') 

892 for r in resources: 

893 client.terminate_session(SessionId=r['SessionId'])