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

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

1084 statements  

1# Copyright The Cloud Custodian Authors. 

2# SPDX-License-Identifier: Apache-2.0 

3"""AWS Account as a custodian resource. 

4""" 

5import json 

6import time 

7import datetime 

8from contextlib import suppress 

9from botocore.exceptions import ClientError 

10from fnmatch import fnmatch 

11from dateutil.parser import parse as parse_date 

12from dateutil.tz import tzutc 

13 

14from c7n.actions import ActionRegistry, BaseAction 

15from c7n.exceptions import PolicyValidationError 

16from c7n.filters import Filter, FilterRegistry, ValueFilter 

17from c7n.filters.kms import KmsRelatedFilter 

18from c7n.filters.multiattr import MultiAttrFilter 

19from c7n.filters.missing import Missing 

20from c7n.manager import resources 

21from c7n.utils import local_session, type_schema, generate_arn, get_support_region, jmespath_search 

22from c7n.query import QueryResourceManager, TypeInfo, DescribeSource 

23from c7n.filters import ListItemFilter 

24 

25from c7n.resources.iam import CredentialReport 

26from c7n.resources.securityhub import OtherResourcePostFinding 

27 

28from .aws import shape_validate, Arn 

29 

30filters = FilterRegistry('aws.account.filters') 

31actions = ActionRegistry('aws.account.actions') 

32 

33retry = staticmethod(QueryResourceManager.retry) 

34filters.register('missing', Missing) 

35 

36 

37class DescribeAccount(DescribeSource): 

38 

39 def get_account(self): 

40 client = local_session(self.manager.session_factory).client("iam") 

41 aliases = client.list_account_aliases().get( 

42 'AccountAliases', ('',)) 

43 name = aliases and aliases[0] or "" 

44 return {'account_id': self.manager.config.account_id, 

45 'account_name': name} 

46 

47 def resources(self, query=None): 

48 return [self.get_account()] 

49 

50 def get_resources(self, resource_ids): 

51 return [self.get_account()] 

52 

53 

54@resources.register('account') 

55class Account(QueryResourceManager): 

56 

57 filter_registry = filters 

58 action_registry = actions 

59 retry = staticmethod(QueryResourceManager.retry) 

60 source_type = 'describe' 

61 

62 class resource_type(TypeInfo): 

63 id = 'account_id' 

64 name = 'account_name' 

65 filter_name = None 

66 global_resource = True 

67 # fake this for doc gen 

68 service = "account" 

69 # for posting config rule evaluations 

70 cfn_type = 'AWS::::Account' 

71 

72 source_mapping = { 

73 'describe': DescribeAccount 

74 } 

75 

76 @classmethod 

77 def get_permissions(cls): 

78 return ('iam:ListAccountAliases',) 

79 

80 @classmethod 

81 def has_arn(cls): 

82 return True 

83 

84 def get_arns(self, resources): 

85 return ["arn:::{account_id}".format(**r) for r in resources] 

86 

87 

88@filters.register('credential') 

89class AccountCredentialReport(CredentialReport): 

90 

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

92 super(AccountCredentialReport, self).process(resources, event) 

93 report = self.get_credential_report() 

94 if report is None: 

95 return [] 

96 results = [] 

97 info = report.get('<root_account>') 

98 for r in resources: 

99 if self.match(r, info): 

100 r['c7n:credential-report'] = info 

101 results.append(r) 

102 return results 

103 

104 

105@filters.register('organization') 

106class AccountOrganization(ValueFilter): 

107 """Check organization enrollment and configuration 

108 

109 :example: 

110 

111 determine if an account is not in an organization 

112 

113 .. code-block:: yaml 

114 

115 policies: 

116 - name: no-org 

117 resource: account 

118 filters: 

119 - type: organization 

120 key: Id 

121 value: absent 

122 

123 

124 :example: 

125 

126 determine if an account is setup for organization policies 

127 

128 .. code-block:: yaml 

129 

130 policies: 

131 - name: org-policies-not-enabled 

132 resource: account 

133 filters: 

134 - type: organization 

135 key: FeatureSet 

136 value: ALL 

137 op: not-equal 

138 """ 

139 schema = type_schema('organization', rinherit=ValueFilter.schema) 

140 schema_alias = False 

141 

142 annotation_key = 'c7n:org' 

143 annotate = False 

144 

145 permissions = ('organizations:DescribeOrganization',) 

146 

147 def get_org_info(self, account): 

148 client = local_session( 

149 self.manager.session_factory).client('organizations') 

150 try: 

151 org_info = client.describe_organization().get('Organization') 

152 except client.exceptions.AWSOrganizationsNotInUseException: 

153 org_info = {} 

154 except ClientError as e: 

155 self.log.warning('organization filter error accessing org info %s', e) 

156 org_info = None 

157 account[self.annotation_key] = org_info 

158 

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

160 if self.annotation_key not in resources[0]: 

161 self.get_org_info(resources[0]) 

162 # if we can't access org info, we've already logged, and return 

163 if resources[0][self.annotation_key] is None: 

164 return [] 

165 if super().process([resources[0][self.annotation_key]]): 

166 return resources 

167 return [] 

168 

169 

170@filters.register('check-macie') 

171class MacieEnabled(ValueFilter): 

172 """Check status of macie v2 in the account. 

173 

174 Gets the macie session info for the account, and 

175 the macie adminstrator account for the current account if 

176 configured. 

177 """ 

178 

179 schema = type_schema('check-macie', rinherit=ValueFilter.schema) 

180 schema_alias = False 

181 annotation_key = 'c7n:macie' 

182 annotate = False 

183 permissions = ('macie2:GetMacieSession', 'macie2:GetAdministratorAccount',) 

184 

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

186 

187 if self.annotation_key not in resources[0]: 

188 self.get_macie_info(resources[0]) 

189 

190 if super().process([resources[0][self.annotation_key]]): 

191 return resources 

192 

193 return [] 

194 

195 def get_macie_info(self, account): 

196 client = local_session( 

197 self.manager.session_factory).client('macie2') 

198 

199 try: 

200 info = client.get_macie_session() 

201 info.pop('ResponseMetadata') 

202 except client.exceptions.AccessDeniedException: 

203 info = {} 

204 

205 try: 

206 minfo = client.get_administrator_account().get('administrator') 

207 except (client.exceptions.AccessDeniedException, 

208 client.exceptions.ResourceNotFoundException): 

209 info['master'] = {} 

210 else: 

211 info['master'] = minfo 

212 info['administrator'] = info['master'] 

213 account[self.annotation_key] = info 

214 

215 

216@filters.register('check-cloudtrail') 

217class CloudTrailEnabled(Filter): 

218 """Verify cloud trail enabled for this account per specifications. 

219 

220 Returns an annotated account resource if trail is not enabled. 

221 

222 Of particular note, the current-region option will evaluate whether cloudtrail is available 

223 in the current region, either as a multi region trail or as a trail with it as the home region. 

224 

225 The log-metric-filter-pattern option checks for the existence of a cloudwatch alarm and a 

226 corresponding SNS subscription for a specific filter pattern 

227 

228 :example: 

229 

230 .. code-block:: yaml 

231 

232 policies: 

233 - name: account-cloudtrail-enabled 

234 resource: account 

235 region: us-east-1 

236 filters: 

237 - type: check-cloudtrail 

238 global-events: true 

239 multi-region: true 

240 running: true 

241 include-management-events: true 

242 log-metric-filter-pattern: "{ ($.eventName = \\"ConsoleLogin\\") }" 

243 

244 Check for CloudWatch log group with a metric filter that has a filter pattern 

245 matching a regex pattern: 

246 

247 .. code-block:: yaml 

248 

249 policies: 

250 - name: account-cloudtrail-with-matching-log-metric-filter 

251 resource: account 

252 region: us-east-1 

253 filters: 

254 - type: check-cloudtrail 

255 log-metric-filter-pattern: 

256 type: value 

257 op: regex 

258 value: '\\{ ?(\\()? ?\\$\\.eventName ?= ?(")?ConsoleLogin(")? ?(\\))? ?\\}' 

259 """ 

260 schema = type_schema( 

261 'check-cloudtrail', 

262 **{'multi-region': {'type': 'boolean'}, 

263 'global-events': {'type': 'boolean'}, 

264 'current-region': {'type': 'boolean'}, 

265 'running': {'type': 'boolean'}, 

266 'notifies': {'type': 'boolean'}, 

267 'file-digest': {'type': 'boolean'}, 

268 'kms': {'type': 'boolean'}, 

269 'kms-key': {'type': 'string'}, 

270 'include-management-events': {'type': 'boolean'}, 

271 'log-metric-filter-pattern': {'oneOf': [ 

272 {'$ref': '#/definitions/filters/value'}, 

273 {'type': 'string'}]}}) 

274 

275 permissions = ('cloudtrail:DescribeTrails', 'cloudtrail:GetTrailStatus', 

276 'cloudtrail:GetEventSelectors', 'cloudwatch:DescribeAlarmsForMetric', 

277 'logs:DescribeMetricFilters', 'sns:GetTopicAttributes') 

278 

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

280 session = local_session(self.manager.session_factory) 

281 client = session.client('cloudtrail') 

282 trails = client.describe_trails()['trailList'] 

283 resources[0]['c7n:cloudtrails'] = trails 

284 

285 if self.data.get('global-events'): 

286 trails = [t for t in trails if t.get('IncludeGlobalServiceEvents')] 

287 if self.data.get('current-region'): 

288 current_region = session.region_name 

289 trails = [t for t in trails if t.get( 

290 'HomeRegion') == current_region or t.get('IsMultiRegionTrail')] 

291 if self.data.get('kms'): 

292 trails = [t for t in trails if t.get('KmsKeyId')] 

293 if self.data.get('kms-key'): 

294 trails = [t for t in trails 

295 if t.get('KmsKeyId', '') == self.data['kms-key']] 

296 if self.data.get('file-digest'): 

297 trails = [t for t in trails 

298 if t.get('LogFileValidationEnabled')] 

299 if self.data.get('multi-region'): 

300 trails = [t for t in trails if t.get('IsMultiRegionTrail')] 

301 if self.data.get('notifies'): 

302 trails = [t for t in trails if t.get('SnsTopicARN')] 

303 if self.data.get('running', True): 

304 running = [] 

305 for t in list(trails): 

306 t['Status'] = status = client.get_trail_status( 

307 Name=t['TrailARN']) 

308 if status['IsLogging'] and not status.get( 

309 'LatestDeliveryError'): 

310 running.append(t) 

311 trails = running 

312 if self.data.get('include-management-events'): 

313 matched = [] 

314 for t in list(trails): 

315 selectors = client.get_event_selectors(TrailName=t['TrailARN']) 

316 if 'EventSelectors' in selectors.keys(): 

317 for s in selectors['EventSelectors']: 

318 if s['IncludeManagementEvents'] and s['ReadWriteType'] == 'All': 

319 matched.append(t) 

320 elif 'AdvancedEventSelectors' in selectors.keys(): 

321 for s in selectors['AdvancedEventSelectors']: 

322 management = False 

323 readonly = False 

324 for field_selector in s['FieldSelectors']: 

325 if field_selector['Field'] == 'eventCategory' and \ 

326 'Management' in field_selector['Equals']: 

327 management = True 

328 elif field_selector['Field'] == 'readOnly': 

329 readonly = True 

330 if management and not readonly: 

331 matched.append(t) 

332 

333 trails = matched 

334 if self.data.get('log-metric-filter-pattern'): 

335 client_logs = session.client('logs') 

336 client_cw = session.client('cloudwatch') 

337 client_sns = session.client('sns') 

338 matched = [] 

339 pattern = self.data.get('log-metric-filter-pattern') 

340 if isinstance(pattern, str): 

341 vf = ValueFilter({'key': 'filterPattern', 'value': pattern}) 

342 else: 

343 pattern.setdefault('key', 'filterPattern') 

344 vf = ValueFilter(pattern) 

345 

346 for t in list(trails): 

347 if 'CloudWatchLogsLogGroupArn' not in t.keys(): 

348 continue 

349 log_group_name = t['CloudWatchLogsLogGroupArn'].split(':')[6] 

350 try: 

351 metric_filters_log_group = \ 

352 client_logs.describe_metric_filters( 

353 logGroupName=log_group_name)['metricFilters'] 

354 except ClientError as e: 

355 if e.response['Error']['Code'] == 'ResourceNotFoundException': 

356 continue 

357 filter_matched = None 

358 if metric_filters_log_group: 

359 for f in metric_filters_log_group: 

360 if vf(f): 

361 filter_matched = f 

362 break 

363 if not filter_matched: 

364 continue 

365 alarms = client_cw.describe_alarms_for_metric( 

366 MetricName=filter_matched["metricTransformations"][0]["metricName"], 

367 Namespace=filter_matched["metricTransformations"][0]["metricNamespace"] 

368 )['MetricAlarms'] 

369 alarm_actions = [] 

370 for a in alarms: 

371 alarm_actions.extend(a['AlarmActions']) 

372 if not alarm_actions: 

373 continue 

374 alarm_actions = set(alarm_actions) 

375 for a in alarm_actions: 

376 try: 

377 sns_topic_attributes = client_sns.get_topic_attributes(TopicArn=a) 

378 sns_topic_attributes = sns_topic_attributes.get('Attributes') 

379 if sns_topic_attributes.get('SubscriptionsConfirmed', '0') != '0': 

380 matched.append(t) 

381 except client_sns.exceptions.InvalidParameterValueException: 

382 # we can ignore any exception here, the alarm action might 

383 # not be an sns topic for instance 

384 continue 

385 trails = matched 

386 if trails: 

387 return [] 

388 return resources 

389 

390 

391@filters.register('guard-duty') 

392class GuardDutyEnabled(MultiAttrFilter): 

393 """Check if the guard duty service is enabled. 

394 

395 This allows looking at account's detector and its associated 

396 master if any. 

397 

398 :example: 

399 

400 Check to ensure guard duty is active on account and associated to a master. 

401 

402 .. code-block:: yaml 

403 

404 policies: 

405 - name: guardduty-enabled 

406 resource: account 

407 filters: 

408 - type: guard-duty 

409 Detector.Status: ENABLED 

410 Administrator.AccountId: "00011001" 

411 Administrator.RelationshipStatus: "Enabled" 

412 """ 

413 

414 schema = { 

415 'type': 'object', 

416 'additionalProperties': False, 

417 'properties': { 

418 'type': {'enum': ['guard-duty']}, 

419 'match-operator': {'enum': ['or', 'and']}}, 

420 'patternProperties': { 

421 '^Detector': {'oneOf': [{'type': 'object'}, {'type': 'string'}]}, 

422 '^Administrator': {'oneOf': [{'type': 'object'}, {'type': 'string'}]}}, 

423 } 

424 

425 annotation = "c7n:guard-duty" 

426 permissions = ( 

427 'guardduty:GetAdministratorAccount', 

428 'guardduty:ListDetectors', 

429 'guardduty:GetDetector') 

430 

431 def validate(self): 

432 attrs = set() 

433 for k in self.data: 

434 if k.startswith('Detector') or k.startswith('Master'): 

435 attrs.add(k) 

436 self.multi_attrs = attrs 

437 return super(GuardDutyEnabled, self).validate() 

438 

439 def get_target(self, resource): 

440 if self.annotation in resource: 

441 return resource[self.annotation] 

442 

443 client = local_session(self.manager.session_factory).client('guardduty') 

444 # detectors are singletons too. 

445 detector_ids = client.list_detectors().get('DetectorIds') 

446 

447 if not detector_ids: 

448 return None 

449 else: 

450 detector_id = detector_ids.pop() 

451 

452 detector = client.get_detector(DetectorId=detector_id) 

453 detector.pop('ResponseMetadata', None) 

454 admin = client.get_administrator_account(DetectorId=detector_id).get('Administrator') 

455 resource[self.annotation] = r = {'Detector': detector, 'Administrator': admin} 

456 return r 

457 

458 

459@filters.register('check-config') 

460class ConfigEnabled(Filter): 

461 """Is config service enabled for this account 

462 

463 :example: 

464 

465 .. code-block:: yaml 

466 

467 policies: 

468 - name: account-check-config-services 

469 resource: account 

470 region: us-east-1 

471 filters: 

472 - type: check-config 

473 all-resources: true 

474 global-resources: true 

475 running: true 

476 """ 

477 

478 schema = type_schema( 

479 'check-config', **{ 

480 'all-resources': {'type': 'boolean'}, 

481 'running': {'type': 'boolean'}, 

482 'global-resources': {'type': 'boolean'}}) 

483 

484 permissions = ('config:DescribeDeliveryChannels', 

485 'config:DescribeConfigurationRecorders', 

486 'config:DescribeConfigurationRecorderStatus') 

487 

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

489 client = local_session( 

490 self.manager.session_factory).client('config') 

491 channels = client.describe_delivery_channels()[ 

492 'DeliveryChannels'] 

493 recorders = client.describe_configuration_recorders()[ 

494 'ConfigurationRecorders'] 

495 resources[0]['c7n:config_recorders'] = recorders 

496 resources[0]['c7n:config_channels'] = channels 

497 if self.data.get('global-resources'): 

498 recorders = [ 

499 r for r in recorders 

500 if r['recordingGroup'].get('includeGlobalResourceTypes')] 

501 if self.data.get('all-resources'): 

502 recorders = [r for r in recorders 

503 if r['recordingGroup'].get('allSupported')] 

504 if self.data.get('running', True) and recorders: 

505 status = {s['name']: s for 

506 s in client.describe_configuration_recorder_status( 

507 )['ConfigurationRecordersStatus']} 

508 resources[0]['c7n:config_status'] = status 

509 recorders = [r for r in recorders if status[r['name']]['recording'] and 

510 status[r['name']]['lastStatus'].lower() in ('pending', 'success')] 

511 if channels and recorders: 

512 return [] 

513 return resources 

514 

515 

516@filters.register('iam-summary') 

517class IAMSummary(ValueFilter): 

518 """Return annotated account resource if iam summary filter matches. 

519 

520 Some use cases include, detecting root api keys or mfa usage. 

521 

522 Example iam summary wrt to matchable fields:: 

523 

524 { 

525 "AccessKeysPerUserQuota": 2, 

526 "AccountAccessKeysPresent": 0, 

527 "AccountMFAEnabled": 1, 

528 "AccountSigningCertificatesPresent": 0, 

529 "AssumeRolePolicySizeQuota": 2048, 

530 "AttachedPoliciesPerGroupQuota": 10, 

531 "AttachedPoliciesPerRoleQuota": 10, 

532 "AttachedPoliciesPerUserQuota": 10, 

533 "GroupPolicySizeQuota": 5120, 

534 "Groups": 1, 

535 "GroupsPerUserQuota": 10, 

536 "GroupsQuota": 100, 

537 "InstanceProfiles": 0, 

538 "InstanceProfilesQuota": 100, 

539 "MFADevices": 3, 

540 "MFADevicesInUse": 2, 

541 "Policies": 3, 

542 "PoliciesQuota": 1000, 

543 "PolicySizeQuota": 5120, 

544 "PolicyVersionsInUse": 5, 

545 "PolicyVersionsInUseQuota": 10000, 

546 "Providers": 0, 

547 "RolePolicySizeQuota": 10240, 

548 "Roles": 4, 

549 "RolesQuota": 250, 

550 "ServerCertificates": 0, 

551 "ServerCertificatesQuota": 20, 

552 "SigningCertificatesPerUserQuota": 2, 

553 "UserPolicySizeQuota": 2048, 

554 "Users": 5, 

555 "UsersQuota": 5000, 

556 "VersionsPerPolicyQuota": 5, 

557 } 

558 

559 For example to determine if an account has either not been 

560 enabled with root mfa or has root api keys. 

561 

562 .. code-block:: yaml 

563 

564 policies: 

565 - name: root-keys-or-no-mfa 

566 resource: account 

567 filters: 

568 - type: iam-summary 

569 key: AccountMFAEnabled 

570 value: true 

571 op: eq 

572 value_type: swap 

573 """ 

574 schema = type_schema('iam-summary', rinherit=ValueFilter.schema) 

575 schema_alias = False 

576 permissions = ('iam:GetAccountSummary',) 

577 

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

579 if not resources[0].get('c7n:iam_summary'): 

580 client = local_session( 

581 self.manager.session_factory).client('iam') 

582 resources[0]['c7n:iam_summary'] = client.get_account_summary( 

583 )['SummaryMap'] 

584 if self.match(resources[0]['c7n:iam_summary']): 

585 return resources 

586 return [] 

587 

588 

589@filters.register('access-analyzer') 

590class AccessAnalyzer(ValueFilter): 

591 """Check for access analyzers in an account 

592 

593 :example: 

594 

595 .. code-block:: yaml 

596 

597 policies: 

598 - name: account-access-analyzer 

599 resource: account 

600 filters: 

601 - type: access-analyzer 

602 key: 'status' 

603 value: ACTIVE 

604 op: eq 

605 """ 

606 

607 schema = type_schema('access-analyzer', rinherit=ValueFilter.schema) 

608 schema_alias = False 

609 permissions = ('access-analyzer:ListAnalyzers',) 

610 annotation_key = 'c7n:matched-analyzers' 

611 

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

613 account = resources[0] 

614 if not account.get(self.annotation_key): 

615 client = local_session(self.manager.session_factory).client('accessanalyzer') 

616 analyzers = self.manager.retry(client.list_analyzers)['analyzers'] 

617 else: 

618 analyzers = account.get(self.annotation_key) 

619 

620 matched_analyzers = [] 

621 for analyzer in analyzers: 

622 if self.match(analyzer): 

623 matched_analyzers.append(analyzer) 

624 account[self.annotation_key] = matched_analyzers 

625 return matched_analyzers and resources or [] 

626 

627 

628@filters.register('password-policy') 

629class AccountPasswordPolicy(ValueFilter): 

630 """Check an account's password policy. 

631 

632 Note that on top of the default password policy fields, we also add an extra key, 

633 PasswordPolicyConfigured which will be set to true or false to signify if the given 

634 account has attempted to set a policy at all. 

635 

636 :example: 

637 

638 .. code-block:: yaml 

639 

640 policies: 

641 - name: password-policy-check 

642 resource: account 

643 region: us-east-1 

644 filters: 

645 - type: password-policy 

646 key: MinimumPasswordLength 

647 value: 10 

648 op: ge 

649 - type: password-policy 

650 key: RequireSymbols 

651 value: true 

652 """ 

653 schema = type_schema('password-policy', rinherit=ValueFilter.schema) 

654 schema_alias = False 

655 permissions = ('iam:GetAccountPasswordPolicy',) 

656 

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

658 account = resources[0] 

659 if not account.get('c7n:password_policy'): 

660 client = local_session(self.manager.session_factory).client('iam') 

661 policy = {} 

662 try: 

663 policy = client.get_account_password_policy().get('PasswordPolicy', {}) 

664 policy['PasswordPolicyConfigured'] = True 

665 except ClientError as e: 

666 if e.response['Error']['Code'] == 'NoSuchEntity': 

667 policy['PasswordPolicyConfigured'] = False 

668 else: 

669 raise 

670 account['c7n:password_policy'] = policy 

671 if self.match(account['c7n:password_policy']): 

672 return resources 

673 return [] 

674 

675 

676@actions.register('set-password-policy') 

677class SetAccountPasswordPolicy(BaseAction): 

678 """Set an account's password policy. 

679 

680 This only changes the policy for the items provided. 

681 If this is the first time setting a password policy and an item is not provided it will be 

682 set to the defaults defined in the boto docs for IAM.Client.update_account_password_policy 

683 

684 :example: 

685 

686 .. code-block:: yaml 

687 

688 policies: 

689 - name: set-account-password-policy 

690 resource: account 

691 filters: 

692 - not: 

693 - type: password-policy 

694 key: MinimumPasswordLength 

695 value: 10 

696 op: ge 

697 actions: 

698 - type: set-password-policy 

699 policy: 

700 MinimumPasswordLength: 20 

701 """ 

702 schema = type_schema( 

703 'set-password-policy', 

704 policy={ 

705 'type': 'object' 

706 }) 

707 shape = 'UpdateAccountPasswordPolicyRequest' 

708 service = 'iam' 

709 permissions = ('iam:GetAccountPasswordPolicy', 'iam:UpdateAccountPasswordPolicy') 

710 

711 def validate(self): 

712 return shape_validate( 

713 self.data.get('policy', {}), 

714 self.shape, 

715 self.service) 

716 

717 def process(self, resources): 

718 client = local_session(self.manager.session_factory).client('iam') 

719 account = resources[0] 

720 if account.get('c7n:password_policy'): 

721 config = account['c7n:password_policy'] 

722 else: 

723 try: 

724 config = client.get_account_password_policy().get('PasswordPolicy') 

725 except client.exceptions.NoSuchEntityException: 

726 config = {} 

727 params = dict(self.data['policy']) 

728 config.update(params) 

729 config = {k: v for (k, v) in config.items() if k not in ('ExpirePasswords', 

730 'PasswordPolicyConfigured')} 

731 client.update_account_password_policy(**config) 

732 

733 

734@filters.register('service-limit') 

735class ServiceLimit(Filter): 

736 """Check if account's service limits are past a given threshold. 

737 

738 Supported limits are per trusted advisor, which is variable based 

739 on usage in the account and support level enabled on the account. 

740 

741 The `names` attribute lets you filter which checks to query limits 

742 about. This is a case-insensitive globbing match on a check name. 

743 You can specify a name exactly or use globbing wildcards like `VPC*`. 

744 

745 The names are exactly what's shown on the trusted advisor page: 

746 

747 https://console.aws.amazon.com/trustedadvisor/home#/category/service-limits 

748 

749 or via the awscli: 

750 

751 aws --region us-east-1 support describe-trusted-advisor-checks --language en \ 

752 --query 'checks[?category==`service_limits`].[name]' --output text 

753 

754 While you can target individual checks via the `names` attribute, and 

755 that should be the preferred method, the following are provided for 

756 backward compatibility with the old style of checks: 

757 

758 - `services` 

759 

760 The resulting limit's `service` field must match one of these. 

761 These are case-insensitive globbing matches. 

762 

763 Note: If you haven't specified any `names` to filter, then 

764 these service names are used as a case-insensitive prefix match on 

765 the check name. This helps limit the number of API calls we need 

766 to make. 

767 

768 - `limits` 

769 

770 The resulting limit's `Limit Name` field must match one of these. 

771 These are case-insensitive globbing matches. 

772 

773 Some example names and their corresponding service and limit names: 

774 

775 Check Name Service Limit Name 

776 ---------------------------------- -------------- --------------------------------- 

777 Auto Scaling Groups AutoScaling Auto Scaling groups 

778 Auto Scaling Launch Configurations AutoScaling Launch configurations 

779 CloudFormation Stacks CloudFormation Stacks 

780 ELB Application Load Balancers ELB Active Application Load Balancers 

781 ELB Classic Load Balancers ELB Active load balancers 

782 ELB Network Load Balancers ELB Active Network Load Balancers 

783 VPC VPC VPCs 

784 VPC Elastic IP Address VPC VPC Elastic IP addresses (EIPs) 

785 VPC Internet Gateways VPC Internet gateways 

786 

787 Note: Some service limits checks are being migrated to service quotas, 

788 which is expected to largely replace service limit checks in trusted 

789 advisor. In this case, some of these checks have no results. 

790 

791 :example: 

792 

793 .. code-block:: yaml 

794 

795 policies: 

796 - name: specific-account-service-limits 

797 resource: account 

798 filters: 

799 - type: service-limit 

800 names: 

801 - IAM Policies 

802 - IAM Roles 

803 - "VPC*" 

804 threshold: 1.0 

805 

806 - name: increase-account-service-limits 

807 resource: account 

808 filters: 

809 - type: service-limit 

810 services: 

811 - EC2 

812 threshold: 1.0 

813 

814 - name: specify-region-for-global-service 

815 region: us-east-1 

816 resource: account 

817 filters: 

818 - type: service-limit 

819 services: 

820 - IAM 

821 limits: 

822 - Roles 

823 """ 

824 

825 schema = type_schema( 

826 'service-limit', 

827 threshold={'type': 'number'}, 

828 refresh_period={'type': 'integer', 

829 'title': 'how long should a check result be considered fresh'}, 

830 names={'type': 'array', 'items': {'type': 'string'}}, 

831 limits={'type': 'array', 'items': {'type': 'string'}}, 

832 services={'type': 'array', 'items': { 

833 'enum': ['AutoScaling', 'CloudFormation', 

834 'DynamoDB', 'EBS', 'EC2', 'ELB', 

835 'IAM', 'RDS', 'Route53', 'SES', 'VPC']}}) 

836 

837 permissions = ('support:DescribeTrustedAdvisorCheckRefreshStatuses', 

838 'support:DescribeTrustedAdvisorCheckResult', 

839 'support:DescribeTrustedAdvisorChecks', 

840 'support:RefreshTrustedAdvisorCheck') 

841 deprecated_check_ids = ['eW7HH0l7J9'] 

842 check_limit = ('region', 'service', 'check', 'limit', 'extant', 'color') 

843 

844 # When doing a refresh, how long to wait for the check to become ready. 

845 # Max wait here is 5 * 10 ~ 50 seconds. 

846 poll_interval = 5 

847 poll_max_intervals = 10 

848 global_services = {'IAM'} 

849 

850 def validate(self): 

851 region = self.manager.data.get('region', '') 

852 if len(self.global_services.intersection(self.data.get('services', []))): 

853 if region != 'us-east-1': 

854 raise PolicyValidationError( 

855 "Global services: %s must be targeted in us-east-1 on the policy" 

856 % ', '.join(self.global_services)) 

857 return self 

858 

859 @classmethod 

860 def get_check_result(cls, client, check_id): 

861 checks = client.describe_trusted_advisor_check_result( 

862 checkId=check_id, language='en')['result'] 

863 

864 # Check status and if necessary refresh checks 

865 if checks['status'] == 'not_available': 

866 try: 

867 client.refresh_trusted_advisor_check(checkId=check_id) 

868 except ClientError as e: 

869 if e.response['Error']['Code'] == 'InvalidParameterValueException': 

870 cls.log.warning("InvalidParameterValueException: %s", 

871 e.response['Error']['Message']) 

872 return 

873 

874 for _ in range(cls.poll_max_intervals): 

875 time.sleep(cls.poll_interval) 

876 refresh_response = client.describe_trusted_advisor_check_refresh_statuses( 

877 checkIds=[check_id]) 

878 if refresh_response['statuses'][0]['status'] == 'success': 

879 checks = client.describe_trusted_advisor_check_result( 

880 checkId=check_id, language='en')['result'] 

881 break 

882 return checks 

883 

884 def get_available_checks(self, client, category='service_limits'): 

885 checks = client.describe_trusted_advisor_checks(language='en') 

886 return [c for c in checks['checks'] 

887 if c['category'] == category and 

888 c['id'] not in self.deprecated_check_ids] 

889 

890 def match_patterns_to_value(self, patterns, value): 

891 for p in patterns: 

892 if fnmatch(value.lower(), p.lower()): 

893 return True 

894 return False 

895 

896 def should_process(self, name): 

897 # if names specified, limit to these names 

898 patterns = self.data.get('names') 

899 if patterns: 

900 return self.match_patterns_to_value(patterns, name) 

901 

902 # otherwise, if services specified, limit to those prefixes 

903 services = self.data.get('services') 

904 if services: 

905 patterns = ["{}*".format(i) for i in services] 

906 return self.match_patterns_to_value(patterns, name.replace(' ', '')) 

907 

908 return True 

909 

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

911 support_region = get_support_region(self.manager) 

912 client = local_session(self.manager.session_factory).client( 

913 'support', region_name=support_region) 

914 checks = self.get_available_checks(client) 

915 exceeded = [] 

916 for check in checks: 

917 if not self.should_process(check['name']): 

918 continue 

919 matched = self.process_check(client, check, resources, event) 

920 if matched: 

921 for m in matched: 

922 m['check_id'] = check['id'] 

923 m['name'] = check['name'] 

924 exceeded.extend(matched) 

925 if exceeded: 

926 resources[0]['c7n:ServiceLimitsExceeded'] = exceeded 

927 return resources 

928 return [] 

929 

930 def process_check(self, client, check, resources, event=None): 

931 region = self.manager.config.region 

932 results = self.get_check_result(client, check['id']) 

933 

934 if results is None or 'flaggedResources' not in results: 

935 return [] 

936 

937 # trim to only results for this region 

938 results['flaggedResources'] = [ 

939 r 

940 for r in results.get('flaggedResources', []) 

941 if r['metadata'][0] == region or (r['metadata'][0] == '-' and region == 'us-east-1') 

942 ] 

943 

944 # save all raw limit results to the account resource 

945 if 'c7n:ServiceLimits' not in resources[0]: 

946 resources[0]['c7n:ServiceLimits'] = [] 

947 resources[0]['c7n:ServiceLimits'].append(results) 

948 

949 # check if we need to refresh the check for next time 

950 delta = datetime.timedelta(self.data.get('refresh_period', 1)) 

951 check_date = parse_date(results['timestamp']) 

952 if datetime.datetime.now(tz=tzutc()) - delta > check_date: 

953 try: 

954 client.refresh_trusted_advisor_check(checkId=check['id']) 

955 except ClientError as e: 

956 if e.response['Error']['Code'] == 'InvalidParameterValueException': 

957 self.log.warning("InvalidParameterValueException: %s", 

958 e.response['Error']['Message']) 

959 return 

960 

961 services = self.data.get('services') 

962 limits = self.data.get('limits') 

963 threshold = self.data.get('threshold') 

964 exceeded = [] 

965 

966 for resource in results['flaggedResources']: 

967 if threshold is None and resource['status'] == 'ok': 

968 continue 

969 limit = dict(zip(self.check_limit, resource['metadata'])) 

970 if services and not self.match_patterns_to_value(services, limit['service']): 

971 continue 

972 if limits and not self.match_patterns_to_value(limits, limit['check']): 

973 continue 

974 limit['status'] = resource['status'] 

975 limit['percentage'] = ( 

976 float(limit['extant'] or 0) / float(limit['limit']) * 100 

977 ) 

978 if threshold and limit['percentage'] < threshold: 

979 continue 

980 exceeded.append(limit) 

981 return exceeded 

982 

983 

984@actions.register('request-limit-increase') 

985class RequestLimitIncrease(BaseAction): 

986 r"""File support ticket to raise limit. 

987 

988 :Example: 

989 

990 .. code-block:: yaml 

991 

992 policies: 

993 - name: raise-account-service-limits 

994 resource: account 

995 filters: 

996 - type: service-limit 

997 services: 

998 - EBS 

999 limits: 

1000 - Provisioned IOPS (SSD) storage (GiB) 

1001 threshold: 60.5 

1002 actions: 

1003 - type: request-limit-increase 

1004 notify: [email, email2] 

1005 ## You can use one of either percent-increase or an amount-increase. 

1006 percent-increase: 50 

1007 message: "Please raise the below account limit(s); \n {limits}" 

1008 """ 

1009 

1010 schema = { 

1011 'type': 'object', 

1012 'additionalProperties': False, 

1013 'properties': { 

1014 'type': {'enum': ['request-limit-increase']}, 

1015 'percent-increase': {'type': 'number', 'minimum': 1}, 

1016 'amount-increase': {'type': 'number', 'minimum': 1}, 

1017 'minimum-increase': {'type': 'number', 'minimum': 1}, 

1018 'subject': {'type': 'string'}, 

1019 'message': {'type': 'string'}, 

1020 'notify': {'type': 'array', 'items': {'type': 'string'}}, 

1021 'severity': {'type': 'string', 'enum': ['urgent', 'high', 'normal', 'low']} 

1022 }, 

1023 'oneOf': [ 

1024 {'required': ['type', 'percent-increase']}, 

1025 {'required': ['type', 'amount-increase']} 

1026 ] 

1027 } 

1028 

1029 permissions = ('support:CreateCase',) 

1030 

1031 default_subject = '[Account:{account}]Raise the following limit(s) of {service} in {region}' 

1032 default_template = 'Please raise the below account limit(s); \n {limits}' 

1033 default_severity = 'normal' 

1034 

1035 service_code_mapping = { 

1036 'AutoScaling': 'auto-scaling', 

1037 'CloudFormation': 'aws-cloudformation', 

1038 'DynamoDB': 'amazon-dynamodb', 

1039 'EBS': 'amazon-elastic-block-store', 

1040 'EC2': 'amazon-elastic-compute-cloud-linux', 

1041 'ELB': 'elastic-load-balancing', 

1042 'IAM': 'aws-identity-and-access-management', 

1043 'Kinesis': 'amazon-kinesis', 

1044 'RDS': 'amazon-relational-database-service-aurora', 

1045 'Route53': 'amazon-route53', 

1046 'SES': 'amazon-simple-email-service', 

1047 'VPC': 'amazon-virtual-private-cloud', 

1048 } 

1049 

1050 def process(self, resources): 

1051 support_region = get_support_region(self.manager) 

1052 client = local_session(self.manager.session_factory).client( 

1053 'support', region_name=support_region) 

1054 account_id = self.manager.config.account_id 

1055 service_map = {} 

1056 region_map = {} 

1057 limit_exceeded = resources[0].get('c7n:ServiceLimitsExceeded', []) 

1058 percent_increase = self.data.get('percent-increase') 

1059 amount_increase = self.data.get('amount-increase') 

1060 minimum_increase = self.data.get('minimum-increase', 1) 

1061 

1062 for s in limit_exceeded: 

1063 current_limit = int(s['limit']) 

1064 if percent_increase: 

1065 increase_by = current_limit * float(percent_increase) / 100 

1066 increase_by = max(increase_by, minimum_increase) 

1067 else: 

1068 increase_by = amount_increase 

1069 increase_by = round(increase_by) 

1070 msg = '\nIncrease %s by %d in %s \n\t Current Limit: %s\n\t Current Usage: %s\n\t ' \ 

1071 'Set New Limit to: %d' % ( 

1072 s['check'], increase_by, s['region'], s['limit'], s['extant'], 

1073 (current_limit + increase_by)) 

1074 service_map.setdefault(s['service'], []).append(msg) 

1075 region_map.setdefault(s['service'], s['region']) 

1076 

1077 for service in service_map: 

1078 subject = self.data.get('subject', self.default_subject).format( 

1079 service=service, region=region_map[service], account=account_id) 

1080 service_code = self.service_code_mapping.get(service) 

1081 body = self.data.get('message', self.default_template) 

1082 body = body.format(**{ 

1083 'service': service, 

1084 'limits': '\n\t'.join(service_map[service]), 

1085 }) 

1086 client.create_case( 

1087 subject=subject, 

1088 communicationBody=body, 

1089 serviceCode=service_code, 

1090 categoryCode='general-guidance', 

1091 severityCode=self.data.get('severity', self.default_severity), 

1092 ccEmailAddresses=self.data.get('notify', [])) 

1093 

1094 

1095def cloudtrail_policy(original, bucket_name, account_id, bucket_region): 

1096 '''add CloudTrail permissions to an S3 policy, preserving existing''' 

1097 ct_actions = [ 

1098 { 

1099 'Action': 's3:GetBucketAcl', 

1100 'Effect': 'Allow', 

1101 'Principal': {'Service': 'cloudtrail.amazonaws.com'}, 

1102 'Resource': generate_arn( 

1103 service='s3', resource=bucket_name, region=bucket_region), 

1104 'Sid': 'AWSCloudTrailAclCheck20150319', 

1105 }, 

1106 { 

1107 'Action': 's3:PutObject', 

1108 'Condition': { 

1109 'StringEquals': 

1110 {'s3:x-amz-acl': 'bucket-owner-full-control'}, 

1111 }, 

1112 'Effect': 'Allow', 

1113 'Principal': {'Service': 'cloudtrail.amazonaws.com'}, 

1114 'Resource': generate_arn( 

1115 service='s3', resource=bucket_name, region=bucket_region), 

1116 'Sid': 'AWSCloudTrailWrite20150319', 

1117 }, 

1118 ] 

1119 # parse original policy 

1120 if original is None: 

1121 policy = { 

1122 'Statement': [], 

1123 'Version': '2012-10-17', 

1124 } 

1125 else: 

1126 policy = json.loads(original['Policy']) 

1127 original_actions = [a.get('Action') for a in policy['Statement']] 

1128 for cta in ct_actions: 

1129 if cta['Action'] not in original_actions: 

1130 policy['Statement'].append(cta) 

1131 return json.dumps(policy) 

1132 

1133 

1134# AWS Account doesn't participate in events (not based on query resource manager) 

1135# so the event subscriber used by postfinding to register doesn't apply, manually 

1136# register it. 

1137Account.action_registry.register('post-finding', OtherResourcePostFinding) 

1138 

1139 

1140@actions.register('enable-cloudtrail') 

1141class EnableTrail(BaseAction): 

1142 """Enables logging on the trail(s) named in the policy 

1143 

1144 :Example: 

1145 

1146 .. code-block:: yaml 

1147 

1148 policies: 

1149 - name: trail-test 

1150 description: Ensure CloudTrail logging is enabled 

1151 resource: account 

1152 actions: 

1153 - type: enable-cloudtrail 

1154 trail: mytrail 

1155 bucket: trails 

1156 """ 

1157 

1158 permissions = ( 

1159 'cloudtrail:CreateTrail', 

1160 'cloudtrail:DescribeTrails', 

1161 'cloudtrail:GetTrailStatus', 

1162 'cloudtrail:StartLogging', 

1163 'cloudtrail:UpdateTrail', 

1164 's3:CreateBucket', 

1165 's3:GetBucketPolicy', 

1166 's3:PutBucketPolicy', 

1167 ) 

1168 schema = type_schema( 

1169 'enable-cloudtrail', 

1170 **{ 

1171 'trail': {'type': 'string'}, 

1172 'bucket': {'type': 'string'}, 

1173 'bucket-region': {'type': 'string'}, 

1174 'multi-region': {'type': 'boolean'}, 

1175 'global-events': {'type': 'boolean'}, 

1176 'notify': {'type': 'string'}, 

1177 'file-digest': {'type': 'boolean'}, 

1178 'kms': {'type': 'boolean'}, 

1179 'kms-key': {'type': 'string'}, 

1180 'required': ('bucket',), 

1181 } 

1182 ) 

1183 

1184 def process(self, accounts): 

1185 """Create or enable CloudTrail""" 

1186 session = local_session(self.manager.session_factory) 

1187 client = session.client('cloudtrail') 

1188 bucket_name = self.data['bucket'] 

1189 bucket_region = self.data.get('bucket-region', 'us-east-1') 

1190 trail_name = self.data.get('trail', 'default-trail') 

1191 multi_region = self.data.get('multi-region', True) 

1192 global_events = self.data.get('global-events', True) 

1193 notify = self.data.get('notify', '') 

1194 file_digest = self.data.get('file-digest', False) 

1195 kms = self.data.get('kms', False) 

1196 kms_key = self.data.get('kms-key', '') 

1197 

1198 s3client = session.client('s3', region_name=bucket_region) 

1199 try: 

1200 s3client.create_bucket( 

1201 Bucket=bucket_name, 

1202 CreateBucketConfiguration={'LocationConstraint': bucket_region} 

1203 ) 

1204 except ClientError as ce: 

1205 if not ('Error' in ce.response and 

1206 ce.response['Error']['Code'] == 'BucketAlreadyOwnedByYou'): 

1207 raise ce 

1208 

1209 try: 

1210 current_policy = s3client.get_bucket_policy(Bucket=bucket_name) 

1211 except ClientError: 

1212 current_policy = None 

1213 

1214 policy_json = cloudtrail_policy( 

1215 current_policy, bucket_name, 

1216 self.manager.config.account_id, bucket_region) 

1217 

1218 s3client.put_bucket_policy(Bucket=bucket_name, Policy=policy_json) 

1219 trails = client.describe_trails().get('trailList', ()) 

1220 if trail_name not in [t.get('Name') for t in trails]: 

1221 new_trail = client.create_trail( 

1222 Name=trail_name, 

1223 S3BucketName=bucket_name, 

1224 ) 

1225 if new_trail: 

1226 trails.append(new_trail) 

1227 # the loop below will configure the new trail 

1228 for trail in trails: 

1229 if trail.get('Name') != trail_name: 

1230 continue 

1231 # enable 

1232 arn = trail['TrailARN'] 

1233 status = client.get_trail_status(Name=arn) 

1234 if not status['IsLogging']: 

1235 client.start_logging(Name=arn) 

1236 # apply configuration changes (if any) 

1237 update_args = {} 

1238 if multi_region != trail.get('IsMultiRegionTrail'): 

1239 update_args['IsMultiRegionTrail'] = multi_region 

1240 if global_events != trail.get('IncludeGlobalServiceEvents'): 

1241 update_args['IncludeGlobalServiceEvents'] = global_events 

1242 if notify != trail.get('SNSTopicArn'): 

1243 update_args['SnsTopicName'] = notify 

1244 if file_digest != trail.get('LogFileValidationEnabled'): 

1245 update_args['EnableLogFileValidation'] = file_digest 

1246 if kms_key != trail.get('KmsKeyId'): 

1247 if not kms and 'KmsKeyId' in trail: 

1248 kms_key = '' 

1249 update_args['KmsKeyId'] = kms_key 

1250 if update_args: 

1251 update_args['Name'] = trail_name 

1252 client.update_trail(**update_args) 

1253 

1254 

1255@filters.register('has-virtual-mfa') 

1256class HasVirtualMFA(Filter): 

1257 """Is the account configured with a virtual MFA device? 

1258 

1259 :example: 

1260 

1261 .. code-block:: yaml 

1262 

1263 policies: 

1264 - name: account-with-virtual-mfa 

1265 resource: account 

1266 region: us-east-1 

1267 filters: 

1268 - type: has-virtual-mfa 

1269 value: true 

1270 """ 

1271 

1272 schema = type_schema('has-virtual-mfa', **{'value': {'type': 'boolean'}}) 

1273 

1274 permissions = ('iam:ListVirtualMFADevices',) 

1275 

1276 def mfa_belongs_to_root_account(self, mfa): 

1277 mfa_user = mfa.get('User', {}).get('Arn', '').split(':')[-1] 

1278 return mfa_user == 'root' 

1279 

1280 def account_has_virtual_mfa(self, account): 

1281 if not account.get('c7n:VirtualMFADevices'): 

1282 client = local_session(self.manager.session_factory).client('iam') 

1283 paginator = client.get_paginator('list_virtual_mfa_devices') 

1284 raw_list = paginator.paginate().build_full_result()['VirtualMFADevices'] 

1285 account['c7n:VirtualMFADevices'] = list(filter( 

1286 self.mfa_belongs_to_root_account, raw_list)) 

1287 expect_virtual_mfa = self.data.get('value', True) 

1288 has_virtual_mfa = any(account['c7n:VirtualMFADevices']) 

1289 return expect_virtual_mfa == has_virtual_mfa 

1290 

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

1292 return list(filter(self.account_has_virtual_mfa, resources)) 

1293 

1294 

1295@actions.register('enable-data-events') 

1296class EnableDataEvents(BaseAction): 

1297 """Ensure all buckets in account are setup to log data events. 

1298 

1299 Note this works via a single trail for data events per 

1300 https://aws.amazon.com/about-aws/whats-new/2017/09/aws-cloudtrail-enables-option-to-add-all-amazon-s3-buckets-to-data-events/ 

1301 

1302 This trail should NOT be used for api management events, the 

1303 configuration here is soley for data events. If directed to create 

1304 a trail this will do so without management events. 

1305 

1306 :example: 

1307 

1308 .. code-block:: yaml 

1309 

1310 policies: 

1311 - name: s3-enable-data-events-logging 

1312 resource: account 

1313 actions: 

1314 - type: enable-data-events 

1315 data-trail: 

1316 name: s3-events 

1317 multi-region: us-east-1 

1318 """ 

1319 

1320 schema = type_schema( 

1321 'enable-data-events', required=['data-trail'], **{ 

1322 'data-trail': { 

1323 'type': 'object', 

1324 'additionalProperties': False, 

1325 'required': ['name'], 

1326 'properties': { 

1327 'create': { 

1328 'title': 'Should we create trail if needed for events?', 

1329 'type': 'boolean'}, 

1330 'type': {'enum': ['ReadOnly', 'WriteOnly', 'All']}, 

1331 'name': { 

1332 'title': 'The name of the event trail', 

1333 'type': 'string'}, 

1334 'topic': { 

1335 'title': 'If creating, the sns topic for the trail to send updates', 

1336 'type': 'string'}, 

1337 's3-bucket': { 

1338 'title': 'If creating, the bucket to store trail event data', 

1339 'type': 'string'}, 

1340 's3-prefix': {'type': 'string'}, 

1341 'key-id': { 

1342 'title': 'If creating, Enable kms on the trail', 

1343 'type': 'string'}, 

1344 # region that we're aggregating via trails. 

1345 'multi-region': { 

1346 'title': 'If creating, use this region for all data trails', 

1347 'type': 'string'}}}}) 

1348 

1349 def validate(self): 

1350 if self.data['data-trail'].get('create'): 

1351 if 's3-bucket' not in self.data['data-trail']: 

1352 raise PolicyValidationError( 

1353 "If creating data trails, an s3-bucket is required on %s" % ( 

1354 self.manager.data)) 

1355 return self 

1356 

1357 def get_permissions(self): 

1358 perms = [ 

1359 'cloudtrail:DescribeTrails', 

1360 'cloudtrail:GetEventSelectors', 

1361 'cloudtrail:PutEventSelectors'] 

1362 

1363 if self.data.get('data-trail', {}).get('create'): 

1364 perms.extend([ 

1365 'cloudtrail:CreateTrail', 'cloudtrail:StartLogging']) 

1366 return perms 

1367 

1368 def add_data_trail(self, client, trail_cfg): 

1369 if not trail_cfg.get('create'): 

1370 raise ValueError( 

1371 "s3 data event trail missing and not configured to create") 

1372 params = dict( 

1373 Name=trail_cfg['name'], 

1374 S3BucketName=trail_cfg['s3-bucket'], 

1375 EnableLogFileValidation=True) 

1376 

1377 if 'key-id' in trail_cfg: 

1378 params['KmsKeyId'] = trail_cfg['key-id'] 

1379 if 's3-prefix' in trail_cfg: 

1380 params['S3KeyPrefix'] = trail_cfg['s3-prefix'] 

1381 if 'topic' in trail_cfg: 

1382 params['SnsTopicName'] = trail_cfg['topic'] 

1383 if 'multi-region' in trail_cfg: 

1384 params['IsMultiRegionTrail'] = True 

1385 

1386 client.create_trail(**params) 

1387 return {'Name': trail_cfg['name']} 

1388 

1389 def process(self, resources): 

1390 session = local_session(self.manager.session_factory) 

1391 region = self.data['data-trail'].get('multi-region') 

1392 

1393 if region: 

1394 client = session.client('cloudtrail', region_name=region) 

1395 else: 

1396 client = session.client('cloudtrail') 

1397 

1398 added = False 

1399 tconfig = self.data['data-trail'] 

1400 trails = client.describe_trails( 

1401 trailNameList=[tconfig['name']]).get('trailList', ()) 

1402 if not trails: 

1403 trail = self.add_data_trail(client, tconfig) 

1404 added = True 

1405 else: 

1406 trail = trails[0] 

1407 

1408 events = client.get_event_selectors( 

1409 TrailName=trail['Name']).get('EventSelectors', []) 

1410 

1411 for e in events: 

1412 found = False 

1413 if not e.get('DataResources'): 

1414 continue 

1415 for data_events in e['DataResources']: 

1416 if data_events['Type'] != 'AWS::S3::Object': 

1417 continue 

1418 for b in data_events['Values']: 

1419 if b.rsplit(':')[-1].strip('/') == '': 

1420 found = True 

1421 break 

1422 if found: 

1423 resources[0]['c7n_data_trail'] = trail 

1424 return 

1425 

1426 # Opinionated choice, separate api and data events. 

1427 event_count = len(events) 

1428 events = [e for e in events if not e.get('IncludeManagementEvents')] 

1429 if len(events) != event_count: 

1430 self.log.warning("removing api trail from data trail") 

1431 

1432 # future proof'd for other data events, for s3 this trail 

1433 # encompasses all the buckets in the account. 

1434 

1435 events.append({ 

1436 'IncludeManagementEvents': False, 

1437 'ReadWriteType': tconfig.get('type', 'All'), 

1438 'DataResources': [{ 

1439 'Type': 'AWS::S3::Object', 

1440 'Values': ['arn:aws:s3:::']}]}) 

1441 client.put_event_selectors( 

1442 TrailName=trail['Name'], 

1443 EventSelectors=events) 

1444 

1445 if added: 

1446 client.start_logging(Name=tconfig['name']) 

1447 

1448 resources[0]['c7n_data_trail'] = trail 

1449 

1450 

1451@filters.register('shield-enabled') 

1452class ShieldEnabled(Filter): 

1453 

1454 permissions = ('shield:DescribeSubscription',) 

1455 

1456 schema = type_schema( 

1457 'shield-enabled', 

1458 state={'type': 'boolean'}) 

1459 

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

1461 state = self.data.get('state', False) 

1462 client = local_session(self.manager.session_factory).client('shield') 

1463 try: 

1464 subscription = client.describe_subscription().get( 

1465 'Subscription', None) 

1466 except ClientError as e: 

1467 if e.response['Error']['Code'] != 'ResourceNotFoundException': 

1468 raise 

1469 subscription = None 

1470 

1471 resources[0]['c7n:ShieldSubscription'] = subscription 

1472 if state and subscription: 

1473 return resources 

1474 elif not state and not subscription: 

1475 return resources 

1476 return [] 

1477 

1478 

1479@actions.register('set-shield-advanced') 

1480class SetShieldAdvanced(BaseAction): 

1481 """Enable/disable Shield Advanced on an account.""" 

1482 

1483 permissions = ( 

1484 'shield:CreateSubscription', 'shield:DeleteSubscription') 

1485 

1486 schema = type_schema( 

1487 'set-shield-advanced', 

1488 state={'type': 'boolean'}) 

1489 

1490 def process(self, resources): 

1491 client = local_session(self.manager.session_factory).client('shield') 

1492 state = self.data.get('state', True) 

1493 

1494 if state: 

1495 client.create_subscription() 

1496 else: 

1497 try: 

1498 client.delete_subscription() 

1499 except ClientError as e: 

1500 if e.response['Error']['Code'] == 'ResourceNotFoundException': 

1501 return 

1502 raise 

1503 

1504 

1505@filters.register('xray-encrypt-key') 

1506class XrayEncrypted(Filter): 

1507 """Determine if xray is encrypted. 

1508 

1509 :example: 

1510 

1511 .. code-block:: yaml 

1512 

1513 policies: 

1514 - name: xray-encrypt-with-default 

1515 resource: aws.account 

1516 filters: 

1517 - type: xray-encrypt-key 

1518 key: default 

1519 - name: xray-encrypt-with-kms 

1520 resource: aws.account 

1521 filters: 

1522 - type: xray-encrypt-key 

1523 key: kms 

1524 - name: xray-encrypt-with-specific-key 

1525 resource: aws.account 

1526 filters: 

1527 - type: xray-encrypt-key 

1528 key: alias/my-alias or arn or keyid 

1529 """ 

1530 

1531 permissions = ('xray:GetEncryptionConfig',) 

1532 schema = type_schema( 

1533 'xray-encrypt-key', 

1534 required=['key'], 

1535 key={'type': 'string'} 

1536 ) 

1537 

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

1539 client = self.manager.session_factory().client('xray') 

1540 gec_result = client.get_encryption_config()['EncryptionConfig'] 

1541 resources[0]['c7n:XrayEncryptionConfig'] = gec_result 

1542 

1543 k = self.data.get('key') 

1544 if k not in ['default', 'kms']: 

1545 kmsclient = self.manager.session_factory().client('kms') 

1546 keyid = kmsclient.describe_key(KeyId=k)['KeyMetadata']['Arn'] 

1547 rc = resources if (gec_result['KeyId'] == keyid) else [] 

1548 else: 

1549 kv = 'KMS' if self.data.get('key') == 'kms' else 'NONE' 

1550 rc = resources if (gec_result['Type'] == kv) else [] 

1551 return rc 

1552 

1553 

1554@actions.register('set-xray-encrypt') 

1555class SetXrayEncryption(BaseAction): 

1556 """Enable specific xray encryption. 

1557 

1558 :example: 

1559 

1560 .. code-block:: yaml 

1561 

1562 policies: 

1563 - name: xray-default-encrypt 

1564 resource: aws.account 

1565 actions: 

1566 - type: set-xray-encrypt 

1567 key: default 

1568 - name: xray-kms-encrypt 

1569 resource: aws.account 

1570 actions: 

1571 - type: set-xray-encrypt 

1572 key: alias/some/alias/key 

1573 """ 

1574 

1575 permissions = ('xray:PutEncryptionConfig',) 

1576 schema = type_schema( 

1577 'set-xray-encrypt', 

1578 required=['key'], 

1579 key={'type': 'string'} 

1580 ) 

1581 

1582 def process(self, resources): 

1583 client = local_session(self.manager.session_factory).client('xray') 

1584 key = self.data.get('key') 

1585 req = {'Type': 'NONE'} if key == 'default' else {'Type': 'KMS', 'KeyId': key} 

1586 client.put_encryption_config(**req) 

1587 

1588 

1589@filters.register('default-ebs-encryption') 

1590class EbsEncryption(Filter): 

1591 """Filter an account by its ebs encryption status. 

1592 

1593 By default for key we match on the alias name for a key. 

1594 

1595 :example: 

1596 

1597 .. code-block:: yaml 

1598 

1599 policies: 

1600 - name: check-default-ebs-encryption 

1601 resource: aws.account 

1602 filters: 

1603 - type: default-ebs-encryption 

1604 key: "alias/aws/ebs" 

1605 state: true 

1606 

1607 It is also possible to match on specific key attributes (tags, origin) 

1608 

1609 :example: 

1610 

1611 .. code-block:: yaml 

1612 

1613 policies: 

1614 - name: check-ebs-encryption-key-origin 

1615 resource: aws.account 

1616 filters: 

1617 - type: default-ebs-encryption 

1618 key: 

1619 type: value 

1620 key: Origin 

1621 value: AWS_KMS 

1622 state: true 

1623 """ 

1624 permissions = ('ec2:GetEbsEncryptionByDefault',) 

1625 schema = type_schema( 

1626 'default-ebs-encryption', 

1627 state={'type': 'boolean'}, 

1628 key={'oneOf': [ 

1629 {'$ref': '#/definitions/filters/value'}, 

1630 {'type': 'string'}]}) 

1631 

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

1633 state = self.data.get('state', False) 

1634 client = local_session(self.manager.session_factory).client('ec2') 

1635 account_state = client.get_ebs_encryption_by_default().get( 

1636 'EbsEncryptionByDefault') 

1637 if account_state != state: 

1638 return [] 

1639 if state and 'key' in self.data: 

1640 vfd = (isinstance(self.data['key'], dict) and 

1641 self.data['key'] or {'c7n:AliasName': self.data['key']}) 

1642 vf = KmsRelatedFilter(vfd, self.manager) 

1643 vf.RelatedIdsExpression = 'KmsKeyId' 

1644 vf.annotate = False 

1645 key = client.get_ebs_default_kms_key_id().get('KmsKeyId') 

1646 if not vf.process([{'KmsKeyId': key}]): 

1647 return [] 

1648 return resources 

1649 

1650 

1651@actions.register('set-ebs-encryption') 

1652class SetEbsEncryption(BaseAction): 

1653 """Set AWS EBS default encryption on an account 

1654 

1655 :example: 

1656 

1657 .. code-block:: yaml 

1658 

1659 policies: 

1660 - name: set-default-ebs-encryption 

1661 resource: aws.account 

1662 filters: 

1663 - type: default-ebs-encryption 

1664 state: false 

1665 actions: 

1666 - type: set-ebs-encryption 

1667 state: true 

1668 key: alias/aws/ebs 

1669 """ 

1670 permissions = ('ec2:EnableEbsEncryptionByDefault', 

1671 'ec2:DisableEbsEncryptionByDefault') 

1672 

1673 schema = type_schema( 

1674 'set-ebs-encryption', 

1675 state={'type': 'boolean'}, 

1676 key={'type': 'string'}) 

1677 

1678 def process(self, resources): 

1679 client = local_session( 

1680 self.manager.session_factory).client('ec2') 

1681 state = self.data.get('state') 

1682 key = self.data.get('key') 

1683 if state: 

1684 client.enable_ebs_encryption_by_default() 

1685 else: 

1686 client.disable_ebs_encryption_by_default() 

1687 

1688 if state and key: 

1689 client.modify_ebs_default_kms_key_id( 

1690 KmsKeyId=self.data['key']) 

1691 

1692 

1693@filters.register('s3-public-block') 

1694class S3PublicBlock(ValueFilter): 

1695 """Check for s3 public blocks on an account. 

1696 

1697 https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html 

1698 """ 

1699 

1700 annotation_key = 'c7n:s3-public-block' 

1701 annotate = False # no annotation from value filter 

1702 schema = type_schema('s3-public-block', rinherit=ValueFilter.schema) 

1703 schema_alias = False 

1704 permissions = ('s3:GetAccountPublicAccessBlock',) 

1705 

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

1707 self.augment([r for r in resources if self.annotation_key not in r]) 

1708 return super(S3PublicBlock, self).process(resources, event) 

1709 

1710 def augment(self, resources): 

1711 client = local_session(self.manager.session_factory).client('s3control') 

1712 for r in resources: 

1713 try: 

1714 r[self.annotation_key] = client.get_public_access_block( 

1715 AccountId=r['account_id']).get('PublicAccessBlockConfiguration', {}) 

1716 except client.exceptions.NoSuchPublicAccessBlockConfiguration: 

1717 r[self.annotation_key] = {} 

1718 

1719 def __call__(self, r): 

1720 return super(S3PublicBlock, self).__call__(r[self.annotation_key]) 

1721 

1722 

1723@actions.register('set-s3-public-block') 

1724class SetS3PublicBlock(BaseAction): 

1725 """Configure S3 Public Access Block on an account. 

1726 

1727 All public access block attributes can be set. If not specified they are merged 

1728 with the extant configuration. 

1729 

1730 https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html 

1731 

1732 :example: 

1733 

1734 .. yaml: 

1735 

1736 policies: 

1737 - name: restrict-public-buckets 

1738 resource: aws.account 

1739 filters: 

1740 - not: 

1741 - type: s3-public-block 

1742 key: RestrictPublicBuckets 

1743 value: true 

1744 actions: 

1745 - type: set-s3-public-block 

1746 RestrictPublicBuckets: true 

1747 

1748 """ 

1749 schema = type_schema( 

1750 'set-s3-public-block', 

1751 state={'type': 'boolean', 'default': True}, 

1752 BlockPublicAcls={'type': 'boolean'}, 

1753 IgnorePublicAcls={'type': 'boolean'}, 

1754 BlockPublicPolicy={'type': 'boolean'}, 

1755 RestrictPublicBuckets={'type': 'boolean'}) 

1756 

1757 permissions = ('s3:PutAccountPublicAccessBlock', 's3:GetAccountPublicAccessBlock') 

1758 

1759 def validate(self): 

1760 config = self.data.copy() 

1761 config.pop('type') 

1762 if config.pop('state', None) is False and config: 

1763 raise PolicyValidationError( 

1764 "{} cant set state false with controls specified".format( 

1765 self.type)) 

1766 

1767 def process(self, resources): 

1768 client = local_session(self.manager.session_factory).client('s3control') 

1769 if self.data.get('state', True) is False: 

1770 for r in resources: 

1771 client.delete_public_access_block(AccountId=r['account_id']) 

1772 return 

1773 

1774 keys = ( 

1775 'BlockPublicPolicy', 'BlockPublicAcls', 'IgnorePublicAcls', 'RestrictPublicBuckets') 

1776 

1777 for r in resources: 

1778 # try to merge with existing configuration if not explicitly set. 

1779 base = {} 

1780 if S3PublicBlock.annotation_key in r: 

1781 base = r[S3PublicBlock.annotation_key] 

1782 else: 

1783 try: 

1784 base = client.get_public_access_block(AccountId=r['account_id']).get( 

1785 'PublicAccessBlockConfiguration') 

1786 except client.exceptions.NoSuchPublicAccessBlockConfiguration: 

1787 base = {} 

1788 

1789 config = {} 

1790 for k in keys: 

1791 if k in self.data: 

1792 config[k] = self.data[k] 

1793 elif k in base: 

1794 config[k] = base[k] 

1795 

1796 client.put_public_access_block( 

1797 AccountId=r['account_id'], 

1798 PublicAccessBlockConfiguration=config) 

1799 

1800 

1801@filters.register('ami-block-public-access') 

1802class AmiBlockPublicAccess(Filter): 

1803 """Scans for AWS accounts that have/do not have Block Public Access set for 

1804 their AMIs. 

1805 

1806 :example: 

1807 

1808 .. code-block:: yaml 

1809 

1810 policies: 

1811 - name: unblocked-ami-block-public-access 

1812 resource: aws.account 

1813 filters: 

1814 - type: ami-block-public-access 

1815 value: false 

1816 

1817 """ 

1818 

1819 annotation_key = 'c7n:ami-block-public-access' 

1820 schema = type_schema('ami-block-public-access', value={'type': 'boolean'}) 

1821 permissions = ('ec2:GetImageBlockPublicAccessState',) 

1822 

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

1824 client = local_session(self.manager.session_factory).client('ec2') 

1825 is_blocked = self.data.get('value', False) 

1826 results = [] 

1827 

1828 # Get account-wide block public access state 

1829 response = client.get_image_block_public_access_state() 

1830 account_state = response.get('ImageBlockPublicAccessState', 'unblocked') 

1831 account_blocked = account_state == 'block-new-sharing' 

1832 

1833 for r in resources: 

1834 r[self.annotation_key] = account_blocked 

1835 

1836 # Filter resources based on whether the account state matches the desired value 

1837 if account_blocked == is_blocked: 

1838 results.append(r) 

1839 

1840 return results 

1841 

1842 

1843class GlueCatalogEncryptionEnabled(MultiAttrFilter): 

1844 """ Filter glue catalog by its glue encryption status and KMS key 

1845 

1846 :example: 

1847 

1848 .. code-block:: yaml 

1849 

1850 policies: 

1851 - name: glue-catalog-security-config 

1852 resource: aws.glue-catalog 

1853 filters: 

1854 - type: glue-security-config 

1855 SseAwsKmsKeyId: alias/aws/glue 

1856 

1857 """ 

1858 retry = staticmethod(QueryResourceManager.retry) 

1859 

1860 schema = { 

1861 'type': 'object', 

1862 'additionalProperties': False, 

1863 'properties': { 

1864 'type': {'enum': ['glue-security-config']}, 

1865 'CatalogEncryptionMode': {'enum': ['DISABLED', 'SSE-KMS']}, 

1866 'SseAwsKmsKeyId': {'type': 'string'}, 

1867 'ReturnConnectionPasswordEncrypted': {'type': 'boolean'}, 

1868 'AwsKmsKeyId': {'type': 'string'} 

1869 } 

1870 } 

1871 

1872 annotation = "c7n:glue-security-config" 

1873 permissions = ('glue:GetDataCatalogEncryptionSettings',) 

1874 

1875 def validate(self): 

1876 attrs = set() 

1877 for key in self.data: 

1878 if key in ['CatalogEncryptionMode', 

1879 'ReturnConnectionPasswordEncrypted', 

1880 'SseAwsKmsKeyId', 

1881 'AwsKmsKeyId']: 

1882 attrs.add(key) 

1883 self.multi_attrs = attrs 

1884 return super(GlueCatalogEncryptionEnabled, self).validate() 

1885 

1886 def get_target(self, resource): 

1887 if self.annotation in resource: 

1888 return resource[self.annotation] 

1889 client = local_session(self.manager.session_factory).client('glue') 

1890 encryption_setting = resource.get('DataCatalogEncryptionSettings') 

1891 if self.manager.type != 'glue-catalog': 

1892 encryption_setting = client.get_data_catalog_encryption_settings().get( 

1893 'DataCatalogEncryptionSettings') 

1894 resource[self.annotation] = encryption_setting.get('EncryptionAtRest') 

1895 resource[self.annotation].update(encryption_setting.get('ConnectionPasswordEncryption')) 

1896 key_attrs = ('SseAwsKmsKeyId', 'AwsKmsKeyId') 

1897 for encrypt_attr in key_attrs: 

1898 if encrypt_attr not in self.data or not self.data[encrypt_attr].startswith('alias'): 

1899 continue 

1900 key = resource[self.annotation].get(encrypt_attr) 

1901 vfd = {'c7n:AliasName': self.data[encrypt_attr]} 

1902 vf = KmsRelatedFilter(vfd, self.manager) 

1903 vf.RelatedIdsExpression = 'KmsKeyId' 

1904 vf.annotate = False 

1905 if not vf.process([{'KmsKeyId': key}]): 

1906 return [] 

1907 resource[self.annotation][encrypt_attr] = self.data[encrypt_attr] 

1908 return resource[self.annotation] 

1909 

1910 

1911@filters.register('glue-security-config') 

1912class AccountCatalogEncryptionFilter(GlueCatalogEncryptionEnabled): 

1913 """Filter aws account by its glue encryption status and KMS key 

1914 

1915 :example: 

1916 

1917 .. code-block:: yaml 

1918 

1919 policies: 

1920 - name: glue-security-config 

1921 resource: aws.account 

1922 filters: 

1923 - type: glue-security-config 

1924 SseAwsKmsKeyId: alias/aws/glue 

1925 

1926 """ 

1927 

1928 

1929@filters.register('emr-block-public-access') 

1930class EMRBlockPublicAccessConfiguration(ValueFilter): 

1931 """Check for EMR block public access configuration on an account 

1932 

1933 :example: 

1934 

1935 .. code-block:: yaml 

1936 

1937 policies: 

1938 - name: get-emr-block-public-access 

1939 resource: account 

1940 filters: 

1941 - type: emr-block-public-access 

1942 """ 

1943 

1944 annotation_key = 'c7n:emr-block-public-access' 

1945 annotate = False # no annotation from value filter 

1946 schema = type_schema('emr-block-public-access', rinherit=ValueFilter.schema) 

1947 schema_alias = False 

1948 permissions = ("elasticmapreduce:GetBlockPublicAccessConfiguration",) 

1949 

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

1951 self.augment([r for r in resources if self.annotation_key not in r]) 

1952 return super().process(resources, event) 

1953 

1954 def augment(self, resources): 

1955 client = local_session(self.manager.session_factory).client( 

1956 'emr', region_name=self.manager.config.region) 

1957 

1958 for r in resources: 

1959 r[self.annotation_key] = self.manager.retry( 

1960 client.get_block_public_access_configuration) 

1961 r[self.annotation_key].pop('ResponseMetadata') 

1962 

1963 def __call__(self, r): 

1964 return super(EMRBlockPublicAccessConfiguration, self).__call__(r[self.annotation_key]) 

1965 

1966 

1967@actions.register('set-emr-block-public-access') 

1968class PutAccountBlockPublicAccessConfiguration(BaseAction): 

1969 """Action to put/update the EMR block public access configuration for your 

1970 AWS account in the current region 

1971 

1972 :example: 

1973 

1974 .. code-block:: yaml 

1975 

1976 policies: 

1977 - name: set-emr-block-public-access 

1978 resource: account 

1979 filters: 

1980 - type: emr-block-public-access 

1981 key: BlockPublicAccessConfiguration.BlockPublicSecurityGroupRules 

1982 value: False 

1983 actions: 

1984 - type: set-emr-block-public-access 

1985 config: 

1986 BlockPublicSecurityGroupRules: True 

1987 PermittedPublicSecurityGroupRuleRanges: 

1988 - MinRange: 22 

1989 MaxRange: 22 

1990 - MinRange: 23 

1991 MaxRange: 23 

1992 

1993 """ 

1994 

1995 schema = type_schema('set-emr-block-public-access', 

1996 config={"type": "object", 

1997 'properties': { 

1998 'BlockPublicSecurityGroupRules': {'type': 'boolean'}, 

1999 'PermittedPublicSecurityGroupRuleRanges': { 

2000 'type': 'array', 

2001 'items': { 

2002 'type': 'object', 

2003 'properties': { 

2004 'MinRange': {'type': 'number', "minimum": 0}, 

2005 'MaxRange': {'type': 'number', "minimum": 0} 

2006 }, 

2007 'required': ['MinRange'] 

2008 } 

2009 } 

2010 }, 

2011 'required': ['BlockPublicSecurityGroupRules'] 

2012 }, 

2013 required=('config',)) 

2014 

2015 permissions = ("elasticmapreduce:PutBlockPublicAccessConfiguration",) 

2016 

2017 def process(self, resources): 

2018 client = local_session(self.manager.session_factory).client('emr') 

2019 r = resources[0] 

2020 

2021 base = {} 

2022 if EMRBlockPublicAccessConfiguration.annotation_key in r: 

2023 base = r[EMRBlockPublicAccessConfiguration.annotation_key] 

2024 else: 

2025 try: 

2026 base = client.get_block_public_access_configuration() 

2027 base.pop('ResponseMetadata') 

2028 except client.exceptions.NoSuchPublicAccessBlockConfiguration: 

2029 base = {} 

2030 

2031 config = base['BlockPublicAccessConfiguration'] 

2032 updatedConfig = {**config, **self.data.get('config')} 

2033 

2034 if config == updatedConfig: 

2035 return 

2036 

2037 client.put_block_public_access_configuration( 

2038 BlockPublicAccessConfiguration=updatedConfig 

2039 ) 

2040 

2041 

2042@filters.register('securityhub') 

2043class SecHubEnabled(Filter): 

2044 """Filter an account depending on whether security hub is enabled or not. 

2045 

2046 :example: 

2047 

2048 .. code-block:: yaml 

2049 

2050 policies: 

2051 - name: check-securityhub-status 

2052 resource: aws.account 

2053 filters: 

2054 - type: securityhub 

2055 enabled: true 

2056 

2057 """ 

2058 

2059 permissions = ('securityhub:DescribeHub',) 

2060 

2061 schema = type_schema('securityhub', enabled={'type': 'boolean'}) 

2062 

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

2064 state = self.data.get('enabled', True) 

2065 client = local_session(self.manager.session_factory).client('securityhub') 

2066 sechub = self.manager.retry(client.describe_hub, ignore_err_codes=( 

2067 'InvalidAccessException',)) 

2068 if state == bool(sechub): 

2069 return resources 

2070 return [] 

2071 

2072 

2073@filters.register('lakeformation-s3-cross-account') 

2074class LakeformationFilter(Filter): 

2075 """Flags an account if its using a lakeformation s3 bucket resource from a different account. 

2076 

2077 :example: 

2078 

2079 .. code-block:: yaml 

2080 

2081 policies: 

2082 - name: lakeformation-cross-account-bucket 

2083 resource: aws.account 

2084 filters: 

2085 - type: lakeformation-s3-cross-account 

2086 

2087 """ 

2088 

2089 schema = type_schema('lakeformation-s3-cross-account', rinherit=ValueFilter.schema) 

2090 schema_alias = False 

2091 permissions = ('lakeformation:ListResources',) 

2092 annotation = 'c7n:lake-cross-account-s3' 

2093 

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

2095 results = [] 

2096 for r in resources: 

2097 if self.process_account(r): 

2098 results.append(r) 

2099 return results 

2100 

2101 def process_account(self, account): 

2102 client = local_session(self.manager.session_factory).client('lakeformation') 

2103 lake_buckets = { 

2104 Arn.parse(r).resource for r in jmespath_search( 

2105 'ResourceInfoList[].ResourceArn', 

2106 client.list_resources()) 

2107 } 

2108 buckets = { 

2109 b['Name'] for b in 

2110 self.manager.get_resource_manager('s3').resources(augment=False)} 

2111 cross_account = lake_buckets.difference(buckets) 

2112 if not cross_account: 

2113 return False 

2114 account[self.annotation] = list(cross_account) 

2115 return True 

2116 

2117 

2118@actions.register('toggle-config-managed-rule') 

2119class ToggleConfigManagedRule(BaseAction): 

2120 """Enables or disables an AWS Config Managed Rule 

2121 

2122 :example: 

2123 

2124 .. code-block:: yaml 

2125 

2126 policies: 

2127 - name: config-managed-s3-bucket-public-write-remediate-event 

2128 description: | 

2129 This policy detects if S3 bucket allows public write by the bucket policy 

2130 or ACL and remediates. 

2131 comment: | 

2132 This policy detects if S3 bucket policy or ACL allows public write access. 

2133 When the bucket is evaluated as 'NON_COMPLIANT', the action 

2134 'AWS-DisableS3BucketPublicReadWrite' is triggered and remediates. 

2135 resource: account 

2136 filters: 

2137 - type: missing 

2138 policy: 

2139 resource: config-rule 

2140 filters: 

2141 - type: remediation 

2142 rule_name: &rule_name 'config-managed-s3-bucket-public-write-remediate-event' 

2143 remediation: &remediation-config 

2144 TargetId: AWS-DisableS3BucketPublicReadWrite 

2145 Automatic: true 

2146 MaximumAutomaticAttempts: 5 

2147 RetryAttemptSeconds: 211 

2148 Parameters: 

2149 AutomationAssumeRole: 

2150 StaticValue: 

2151 Values: 

2152 - 'arn:aws:iam::{account_id}:role/myrole' 

2153 S3BucketName: 

2154 ResourceValue: 

2155 Value: RESOURCE_ID 

2156 actions: 

2157 - type: toggle-config-managed-rule 

2158 rule_name: *rule_name 

2159 managed_rule_id: S3_BUCKET_PUBLIC_WRITE_PROHIBITED 

2160 resource_types: 

2161 - 'AWS::S3::Bucket' 

2162 rule_parameters: '{}' 

2163 remediation: *remediation-config 

2164 """ 

2165 

2166 permissions = ( 

2167 'config:DescribeConfigRules', 

2168 'config:DescribeRemediationConfigurations', 

2169 'config:PutRemediationConfigurations', 

2170 'config:PutConfigRule', 

2171 ) 

2172 

2173 schema = type_schema('toggle-config-managed-rule', 

2174 enabled={'type': 'boolean', 'default': True}, 

2175 rule_name={'type': 'string'}, 

2176 rule_prefix={'type': 'string'}, 

2177 managed_rule_id={'type': 'string'}, 

2178 resource_types={'type': 'array', 'items': 

2179 {'pattern': '^AWS::*', 'type': 'string'}}, 

2180 resource_tag={ 

2181 'type': 'object', 

2182 'properties': { 

2183 'key': {'type': 'string'}, 

2184 'value': {'type': 'string'}, 

2185 }, 

2186 'required': ['key', 'value'], 

2187 }, 

2188 resource_id={'type': 'string'}, 

2189 rule_parameters={'type': 'string'}, 

2190 remediation={ 

2191 'type': 'object', 

2192 'properties': { 

2193 'TargetType': {'type': 'string'}, 

2194 'TargetId': {'type': 'string'}, 

2195 'Automatic': {'type': 'boolean'}, 

2196 'Parameters': {'type': 'object'}, 

2197 'MaximumAutomaticAttempts': { 

2198 'type': 'integer', 

2199 'minimum': 1, 'maximum': 25, 

2200 }, 

2201 'RetryAttemptSeconds': { 

2202 'type': 'integer', 

2203 'minimum': 1, 'maximum': 2678000, 

2204 }, 

2205 'ExecutionControls': {'type': 'object'}, 

2206 }, 

2207 }, 

2208 tags={'type': 'object'}, 

2209 required=['rule_name'], 

2210 ) 

2211 

2212 def validate(self): 

2213 if ( 

2214 self.data.get('enabled', True) and 

2215 not self.data.get('managed_rule_id') 

2216 ): 

2217 raise PolicyValidationError("managed_rule_id required to enable a managed rule") 

2218 return self 

2219 

2220 def process(self, accounts): 

2221 client = local_session(self.manager.session_factory).client('config') 

2222 rule = self.ConfigManagedRule(self.data) 

2223 params = self.get_rule_params(rule) 

2224 

2225 if self.data.get('enabled', True): 

2226 client.put_config_rule(**params) 

2227 

2228 if rule.remediation: 

2229 remediation_params = self.get_remediation_params(rule) 

2230 client.put_remediation_configurations( 

2231 RemediationConfigurations=[remediation_params] 

2232 ) 

2233 else: 

2234 with suppress(client.exceptions.NoSuchRemediationConfigurationException): 

2235 client.delete_remediation_configuration( 

2236 ConfigRuleName=rule.name 

2237 ) 

2238 

2239 with suppress(client.exceptions.NoSuchConfigRuleException): 

2240 client.delete_config_rule( 

2241 ConfigRuleName=rule.name 

2242 ) 

2243 

2244 def get_rule_params(self, rule): 

2245 params = dict( 

2246 ConfigRuleName=rule.name, 

2247 Description=rule.description, 

2248 Source={ 

2249 'Owner': 'AWS', 

2250 'SourceIdentifier': rule.managed_rule_id, 

2251 }, 

2252 InputParameters=rule.rule_parameters 

2253 ) 

2254 

2255 # A config rule scope can include one or more resource types, 

2256 # a combination of a tag key and value, or a combination of 

2257 # one resource type and one resource ID 

2258 params.update({'Scope': {'ComplianceResourceTypes': rule.resource_types}}) 

2259 if rule.resource_tag: 

2260 params.update({'Scope': { 

2261 'TagKey': rule.resource_tag['key'], 

2262 'TagValue': rule.resource_tag['value']} 

2263 }) 

2264 elif rule.resource_id: 

2265 params.update({'Scope': {'ComplianceResourceId': rule.resource_id}}) 

2266 

2267 return dict(ConfigRule=params) 

2268 

2269 def get_remediation_params(self, rule): 

2270 rule.remediation['ConfigRuleName'] = rule.name 

2271 if 'TargetType' not in rule.remediation: 

2272 rule.remediation['TargetType'] = 'SSM_DOCUMENT' 

2273 return rule.remediation 

2274 

2275 class ConfigManagedRule: 

2276 """Wraps the action data into an AWS Config Managed Rule. 

2277 """ 

2278 

2279 def __init__(self, data): 

2280 self.data = data 

2281 

2282 @property 

2283 def name(self): 

2284 prefix = self.data.get('rule_prefix', 'custodian-') 

2285 return "%s%s" % (prefix, self.data.get('rule_name', '')) 

2286 

2287 @property 

2288 def description(self): 

2289 return self.data.get( 

2290 'description', 'cloud-custodian AWS Config Managed Rule policy') 

2291 

2292 @property 

2293 def tags(self): 

2294 return self.data.get('tags', {}) 

2295 

2296 @property 

2297 def resource_types(self): 

2298 return self.data.get('resource_types', []) 

2299 

2300 @property 

2301 def managed_rule_id(self): 

2302 return self.data.get('managed_rule_id', '') 

2303 

2304 @property 

2305 def resource_tag(self): 

2306 return self.data.get('resource_tag', {}) 

2307 

2308 @property 

2309 def resource_id(self): 

2310 return self.data.get('resource_id', '') 

2311 

2312 @property 

2313 def rule_parameters(self): 

2314 return self.data.get('rule_parameters', '') 

2315 

2316 @property 

2317 def remediation(self): 

2318 return self.data.get('remediation', {}) 

2319 

2320 

2321@filters.register('ses-agg-send-stats') 

2322class SesAggStats(ValueFilter): 

2323 """This filter queries SES send statistics and aggregates all 

2324 the data points into a single report. 

2325 

2326 :example: 

2327 

2328 .. code-block:: yaml 

2329 

2330 policies: 

2331 - name: ses-aggregated-send-stats-policy 

2332 resource: account 

2333 filters: 

2334 - type: ses-agg-send-stats 

2335 """ 

2336 

2337 schema = type_schema('ses-agg-send-stats', rinherit=ValueFilter.schema) 

2338 annotation_key = 'c7n:ses-send-agg' 

2339 permissions = ("ses:GetSendStatistics",) 

2340 

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

2342 client = local_session(self.manager.session_factory).client('ses') 

2343 get_send_stats = client.get_send_statistics() 

2344 results = [] 

2345 

2346 if not get_send_stats or not get_send_stats.get('SendDataPoints'): 

2347 return results 

2348 

2349 resource_counter = {'DeliveryAttempts': 0, 

2350 'Bounces': 0, 

2351 'Complaints': 0, 

2352 'Rejects': 0, 

2353 'BounceRate': 0} 

2354 for d in get_send_stats.get('SendDataPoints', []): 

2355 resource_counter['DeliveryAttempts'] += d['DeliveryAttempts'] 

2356 resource_counter['Bounces'] += d['Bounces'] 

2357 resource_counter['Complaints'] += d['Complaints'] 

2358 resource_counter['Rejects'] += d['Rejects'] 

2359 resource_counter['BounceRate'] = round( 

2360 (resource_counter['Bounces'] / 

2361 resource_counter['DeliveryAttempts']) * 100) 

2362 resources[0][self.annotation_key] = resource_counter 

2363 

2364 return resources 

2365 

2366 

2367@filters.register('ses-send-stats') 

2368class SesConsecutiveStats(Filter): 

2369 """This filter annotates the account resource with SES send statistics for the 

2370 last n number of days, not including the current date. 

2371 

2372 The stats are aggregated into daily metrics. Additionally, the filter also 

2373 calculates and annotates the max daily bounce rate (percentage). Using this filter, 

2374 users can alert when the bounce rate for a particular day is higher than the limit. 

2375 

2376 :example: 

2377 

2378 .. code-block:: yaml 

2379 

2380 policies: 

2381 - name: ses-send-stats 

2382 resource: account 

2383 filters: 

2384 - type: ses-send-stats 

2385 days: 5 

2386 - type: value 

2387 key: '"c7n:ses-max-bounce-rate"' 

2388 op: ge 

2389 value: 10 

2390 """ 

2391 schema = type_schema('ses-send-stats', days={'type': 'number', 'minimum': 2}, 

2392 required=['days']) 

2393 send_stats_annotation = 'c7n:ses-send-stats' 

2394 max_bounce_annotation = 'c7n:ses-max-bounce-rate' 

2395 permissions = ("ses:GetSendStatistics",) 

2396 

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

2398 client = local_session(self.manager.session_factory).client('ses') 

2399 get_send_stats = client.get_send_statistics() 

2400 results = [] 

2401 check_days = self.data.get('days', 2) 

2402 utcnow = datetime.datetime.utcnow() 

2403 expected_dates = set() 

2404 

2405 for days in range(1, check_days + 1): 

2406 expected_dates.add((utcnow - datetime.timedelta(days=days)).strftime('%Y-%m-%d')) 

2407 

2408 if not get_send_stats or not get_send_stats.get('SendDataPoints'): 

2409 return results 

2410 

2411 metrics = {} 

2412 for d in get_send_stats.get('SendDataPoints', []): 

2413 ts = d['Timestamp'].strftime('%Y-%m-%d') 

2414 if ts not in expected_dates: 

2415 continue 

2416 

2417 if not metrics.get(ts): 

2418 metrics[ts] = {'DeliveryAttempts': 0, 

2419 'Bounces': 0, 

2420 'Complaints': 0, 

2421 'Rejects': 0} 

2422 metrics[ts]['DeliveryAttempts'] += d['DeliveryAttempts'] 

2423 metrics[ts]['Bounces'] += d['Bounces'] 

2424 metrics[ts]['Complaints'] += d['Complaints'] 

2425 metrics[ts]['Rejects'] += d['Rejects'] 

2426 

2427 max_bounce_rate = 0 

2428 for ts, metric in metrics.items(): 

2429 metric['BounceRate'] = round((metric['Bounces'] / metric['DeliveryAttempts']) * 100) 

2430 if max_bounce_rate < metric['BounceRate']: 

2431 max_bounce_rate = metric['BounceRate'] 

2432 metric['Date'] = ts 

2433 

2434 resources[0][self.send_stats_annotation] = list(metrics.values()) 

2435 resources[0][self.max_bounce_annotation] = max_bounce_rate 

2436 

2437 return resources 

2438 

2439 

2440@filters.register('bedrock-model-invocation-logging') 

2441class BedrockModelInvocationLogging(ListItemFilter): 

2442 """Filter for account to look at bedrock model invocation logging configuration 

2443 

2444 The schema to supply to the attrs follows the schema here: 

2445 https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock/client/get_model_invocation_logging_configuration.html 

2446 

2447 :example: 

2448 

2449 .. code-block:: yaml 

2450 

2451 policies: 

2452 - name: bedrock-model-invocation-logging-configuration 

2453 resource: account 

2454 filters: 

2455 - type: bedrock-model-invocation-logging 

2456 attrs: 

2457 - imageDataDeliveryEnabled: True 

2458 

2459 """ 

2460 schema = type_schema( 

2461 'bedrock-model-invocation-logging', 

2462 attrs={'$ref': '#/definitions/filters_common/list_item_attrs'}, 

2463 count={'type': 'number'}, 

2464 count_op={'$ref': '#/definitions/filters_common/comparison_operators'} 

2465 ) 

2466 permissions = ('bedrock:GetModelInvocationLoggingConfiguration',) 

2467 annotation_key = 'c7n:BedrockModelInvocationLogging' 

2468 

2469 def get_item_values(self, resource): 

2470 item_values = [] 

2471 client = local_session(self.manager.session_factory).client('bedrock') 

2472 invocation_logging_config = client \ 

2473 .get_model_invocation_logging_configuration().get('loggingConfig') 

2474 if invocation_logging_config is not None: 

2475 item_values.append(invocation_logging_config) 

2476 resource[self.annotation_key] = invocation_logging_config 

2477 return item_values 

2478 

2479 

2480@actions.register('set-bedrock-model-invocation-logging') 

2481class SetBedrockModelInvocationLogging(BaseAction): 

2482 """Set Bedrock Model Invocation Logging Configuration on an account. 

2483 https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock/client/put_model_invocation_logging_configuration.html 

2484 

2485 To delete a configuration, supply enabled to False 

2486 

2487 :example: 

2488 

2489 .. code-block:: yaml 

2490 

2491 policies: 

2492 - name: set-bedrock-model-invocation-logging 

2493 resource: account 

2494 actions: 

2495 - type: set-bedrock-model-invocation-logging 

2496 enabled: True 

2497 loggingConfig: 

2498 textDataDeliveryEnabled: True 

2499 s3Config: 

2500 bucketName: test-bedrock-1 

2501 keyPrefix: logging/ 

2502 

2503 - name: delete-bedrock-model-invocation-logging 

2504 resource: account 

2505 actions: 

2506 - type: set-bedrock-model-invocation-logging 

2507 enabled: False 

2508 """ 

2509 

2510 schema = { 

2511 'type': 'object', 

2512 'additionalProperties': False, 

2513 'properties': { 

2514 'type': {'enum': ['set-bedrock-model-invocation-logging']}, 

2515 'enabled': {'type': 'boolean'}, 

2516 'loggingConfig': {'type': 'object'} 

2517 }, 

2518 } 

2519 

2520 permissions = ('bedrock:PutModelInvocationLoggingConfiguration',) 

2521 shape = 'PutModelInvocationLoggingConfigurationRequest' 

2522 service = 'bedrock' 

2523 

2524 def validate(self): 

2525 cfg = dict(self.data) 

2526 enabled = cfg.get('enabled') 

2527 if enabled: 

2528 cfg.pop('type') 

2529 cfg.pop('enabled') 

2530 return shape_validate( 

2531 cfg, 

2532 self.shape, 

2533 self.service) 

2534 

2535 def process(self, resources): 

2536 client = local_session(self.manager.session_factory).client('bedrock') 

2537 if self.data.get('enabled'): 

2538 params = self.data.get('loggingConfig') 

2539 client.put_model_invocation_logging_configuration(loggingConfig=params) 

2540 else: 

2541 client.delete_model_invocation_logging_configuration() 

2542 

2543 

2544@filters.register('ec2-metadata-defaults') 

2545class EC2MetadataDefaults(ValueFilter): 

2546 """Filter on the default instance metadata service (IMDS) settings for the specified account and 

2547 region. NOTE: Any configuration that has never been set (or is set to 'No Preference'), will 

2548 not be returned in the response. 

2549 

2550 :example: 

2551 

2552 .. code-block:: yaml 

2553 

2554 policies: 

2555 - name: ec2-imds-defaults 

2556 resource: account 

2557 filters: 

2558 - or: 

2559 - type: ec2-metadata-defaults 

2560 key: HttpTokens 

2561 value: optional 

2562 - type: ec2-metadata-defaults 

2563 key: HttpTokens 

2564 value: absent 

2565 """ 

2566 

2567 annotation_key = 'c7n:EC2MetadataDefaults' 

2568 annotate = False # no annotation from value filter 

2569 schema = type_schema('ec2-metadata-defaults', rinherit=ValueFilter.schema) 

2570 permissions = ('ec2:GetInstanceMetadataDefaults',) 

2571 

2572 def augment(self, resources): 

2573 client = local_session(self.manager.session_factory).client('ec2') 

2574 for r in resources: 

2575 r[self.annotation_key] = self.manager.retry( 

2576 client.get_instance_metadata_defaults)["AccountLevel"] 

2577 

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

2579 self.augment([r for r in resources if self.annotation_key not in r]) 

2580 return super(EC2MetadataDefaults, self).process(resources, event) 

2581 

2582 def __call__(self, r): 

2583 return super(EC2MetadataDefaults, self).__call__(r[self.annotation_key]) 

2584 

2585 

2586@actions.register('set-ec2-metadata-defaults') 

2587class SetEC2MetadataDefaults(BaseAction): 

2588 """Modifies the default instance metadata service (IMDS) settings at the account level. 

2589 

2590 :example: 

2591 

2592 .. code-block:: yaml 

2593 

2594 policies: 

2595 - name: set-ec2-metadata-defaults 

2596 resource: account 

2597 filters: 

2598 - or: 

2599 - type: ec2-metadata-defaults 

2600 key: HttpTokens 

2601 op: eq 

2602 value: optional 

2603 - type: ec2-metadata-defaults 

2604 key: HttpTokens 

2605 value: absent 

2606 actions: 

2607 - type: set-ec2-metadata-defaults 

2608 HttpTokens: required 

2609 

2610 """ 

2611 

2612 schema = type_schema( 

2613 'set-ec2-metadata-defaults', 

2614 HttpTokens={'enum': ['optional', 'required', 'no-preference']}, 

2615 HttpPutResponseHopLimit={'type': 'integer'}, 

2616 HttpEndpoint={'enum': ['enabled', 'disabled', 'no-preference']}, 

2617 InstanceMetadataTags={'enum': ['enabled', 'disabled', 'no-preference']}, 

2618 ) 

2619 

2620 permissions = ('ec2:ModifyInstanceMetadataDefaults',) 

2621 service = 'ec2' 

2622 shape = 'ModifyInstanceMetadataDefaultsRequest' 

2623 

2624 def validate(self): 

2625 req = dict(self.data) 

2626 req.pop('type') 

2627 return shape_validate( 

2628 req, self.shape, self.service 

2629 ) 

2630 

2631 def process(self, resources): 

2632 client = local_session(self.manager.session_factory).client(self.service) 

2633 self.data.pop('type') 

2634 client.modify_instance_metadata_defaults(**self.data) 

2635 

2636 

2637@actions.register('set-security-token-service-preferences') 

2638class SetSecurityTokenServicePreferences(BaseAction): 

2639 """Action to set STS preferences.""" 

2640 

2641 """Action to set STS preferences. 

2642 

2643 :example: 

2644 

2645 .. code-block:: yaml 

2646 

2647 policies: 

2648 - name: set-sts-preferences 

2649 resource: account 

2650 filters: 

2651 - or: 

2652 - type: iam-summary 

2653 key: GlobalEndpointTokenVersion 

2654 value: absent 

2655 value: optional 

2656 - type: iam-summary 

2657 key: GlobalEndpointTokenVersion 

2658 op: ne 

2659 value: 2 

2660 actions: 

2661 - type: set-security-token-service-preferences 

2662 token_version: v2Token 

2663 

2664 """ 

2665 

2666 schema = type_schema( 

2667 'set-security-token-service-preferences', 

2668 token_version={'type': 'string', 'enum': ['v1Token', 'v2Token']} 

2669 ) 

2670 

2671 permissions = ('iam:SetSecurityTokenServicePreferences',) 

2672 

2673 def process(self, resources): 

2674 client = local_session(self.manager.session_factory).client('iam') 

2675 token_version = self.data.get('token_version', 'v2Token') 

2676 for resource in resources: 

2677 self.set_sts_preferences(client, token_version) 

2678 

2679 def set_sts_preferences(self, client, token_version): 

2680 client.set_security_token_service_preferences( 

2681 GlobalEndpointTokenVersion=token_version 

2682 )