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

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

1067 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 Master.AccountId: "00011001" 

411 Master.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 '^Master': {'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 master = client.get_administrator_account(DetectorId=detector_id).get('Master') 

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

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 

1801class GlueCatalogEncryptionEnabled(MultiAttrFilter): 

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

1803 

1804 :example: 

1805 

1806 .. code-block:: yaml 

1807 

1808 policies: 

1809 - name: glue-catalog-security-config 

1810 resource: aws.glue-catalog 

1811 filters: 

1812 - type: glue-security-config 

1813 SseAwsKmsKeyId: alias/aws/glue 

1814 

1815 """ 

1816 retry = staticmethod(QueryResourceManager.retry) 

1817 

1818 schema = { 

1819 'type': 'object', 

1820 'additionalProperties': False, 

1821 'properties': { 

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

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

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

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

1826 'AwsKmsKeyId': {'type': 'string'} 

1827 } 

1828 } 

1829 

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

1831 permissions = ('glue:GetDataCatalogEncryptionSettings',) 

1832 

1833 def validate(self): 

1834 attrs = set() 

1835 for key in self.data: 

1836 if key in ['CatalogEncryptionMode', 

1837 'ReturnConnectionPasswordEncrypted', 

1838 'SseAwsKmsKeyId', 

1839 'AwsKmsKeyId']: 

1840 attrs.add(key) 

1841 self.multi_attrs = attrs 

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

1843 

1844 def get_target(self, resource): 

1845 if self.annotation in resource: 

1846 return resource[self.annotation] 

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

1848 encryption_setting = resource.get('DataCatalogEncryptionSettings') 

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

1850 encryption_setting = client.get_data_catalog_encryption_settings().get( 

1851 'DataCatalogEncryptionSettings') 

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

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

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

1855 for encrypt_attr in key_attrs: 

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

1857 continue 

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

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

1860 vf = KmsRelatedFilter(vfd, self.manager) 

1861 vf.RelatedIdsExpression = 'KmsKeyId' 

1862 vf.annotate = False 

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

1864 return [] 

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

1866 return resource[self.annotation] 

1867 

1868 

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

1870class AccountCatalogEncryptionFilter(GlueCatalogEncryptionEnabled): 

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

1872 

1873 :example: 

1874 

1875 .. code-block:: yaml 

1876 

1877 policies: 

1878 - name: glue-security-config 

1879 resource: aws.account 

1880 filters: 

1881 - type: glue-security-config 

1882 SseAwsKmsKeyId: alias/aws/glue 

1883 

1884 """ 

1885 

1886 

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

1888class EMRBlockPublicAccessConfiguration(ValueFilter): 

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

1890 

1891 :example: 

1892 

1893 .. code-block:: yaml 

1894 

1895 policies: 

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

1897 resource: account 

1898 filters: 

1899 - type: emr-block-public-access 

1900 """ 

1901 

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

1903 annotate = False # no annotation from value filter 

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

1905 schema_alias = False 

1906 permissions = ("elasticmapreduce:GetBlockPublicAccessConfiguration",) 

1907 

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

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

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

1911 

1912 def augment(self, resources): 

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

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

1915 

1916 for r in resources: 

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

1918 client.get_block_public_access_configuration) 

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

1920 

1921 def __call__(self, r): 

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

1923 

1924 

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

1926class PutAccountBlockPublicAccessConfiguration(BaseAction): 

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

1928 AWS account in the current region 

1929 

1930 :example: 

1931 

1932 .. code-block:: yaml 

1933 

1934 policies: 

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

1936 resource: account 

1937 filters: 

1938 - type: emr-block-public-access 

1939 key: BlockPublicAccessConfiguration.BlockPublicSecurityGroupRules 

1940 value: False 

1941 actions: 

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

1943 config: 

1944 BlockPublicSecurityGroupRules: True 

1945 PermittedPublicSecurityGroupRuleRanges: 

1946 - MinRange: 22 

1947 MaxRange: 22 

1948 - MinRange: 23 

1949 MaxRange: 23 

1950 

1951 """ 

1952 

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

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

1955 'properties': { 

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

1957 'PermittedPublicSecurityGroupRuleRanges': { 

1958 'type': 'array', 

1959 'items': { 

1960 'type': 'object', 

1961 'properties': { 

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

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

1964 }, 

1965 'required': ['MinRange'] 

1966 } 

1967 } 

1968 }, 

1969 'required': ['BlockPublicSecurityGroupRules'] 

1970 }, 

1971 required=('config',)) 

1972 

1973 permissions = ("elasticmapreduce:PutBlockPublicAccessConfiguration",) 

1974 

1975 def process(self, resources): 

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

1977 r = resources[0] 

1978 

1979 base = {} 

1980 if EMRBlockPublicAccessConfiguration.annotation_key in r: 

1981 base = r[EMRBlockPublicAccessConfiguration.annotation_key] 

1982 else: 

1983 try: 

1984 base = client.get_block_public_access_configuration() 

1985 base.pop('ResponseMetadata') 

1986 except client.exceptions.NoSuchPublicAccessBlockConfiguration: 

1987 base = {} 

1988 

1989 config = base['BlockPublicAccessConfiguration'] 

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

1991 

1992 if config == updatedConfig: 

1993 return 

1994 

1995 client.put_block_public_access_configuration( 

1996 BlockPublicAccessConfiguration=updatedConfig 

1997 ) 

1998 

1999 

2000@filters.register('securityhub') 

2001class SecHubEnabled(Filter): 

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

2003 

2004 :example: 

2005 

2006 .. code-block:: yaml 

2007 

2008 policies: 

2009 - name: check-securityhub-status 

2010 resource: aws.account 

2011 filters: 

2012 - type: securityhub 

2013 enabled: true 

2014 

2015 """ 

2016 

2017 permissions = ('securityhub:DescribeHub',) 

2018 

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

2020 

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

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

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

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

2025 'InvalidAccessException',)) 

2026 if state == bool(sechub): 

2027 return resources 

2028 return [] 

2029 

2030 

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

2032class LakeformationFilter(Filter): 

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

2034 

2035 :example: 

2036 

2037 .. code-block:: yaml 

2038 

2039 policies: 

2040 - name: lakeformation-cross-account-bucket 

2041 resource: aws.account 

2042 filters: 

2043 - type: lakeformation-s3-cross-account 

2044 

2045 """ 

2046 

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

2048 schema_alias = False 

2049 permissions = ('lakeformation:ListResources',) 

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

2051 

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

2053 results = [] 

2054 for r in resources: 

2055 if self.process_account(r): 

2056 results.append(r) 

2057 return results 

2058 

2059 def process_account(self, account): 

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

2061 lake_buckets = { 

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

2063 'ResourceInfoList[].ResourceArn', 

2064 client.list_resources()) 

2065 } 

2066 buckets = { 

2067 b['Name'] for b in 

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

2069 cross_account = lake_buckets.difference(buckets) 

2070 if not cross_account: 

2071 return False 

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

2073 return True 

2074 

2075 

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

2077class ToggleConfigManagedRule(BaseAction): 

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

2079 

2080 :example: 

2081 

2082 .. code-block:: yaml 

2083 

2084 policies: 

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

2086 description: | 

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

2088 or ACL and remediates. 

2089 comment: | 

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

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

2092 'AWS-DisableS3BucketPublicReadWrite' is triggered and remediates. 

2093 resource: account 

2094 filters: 

2095 - type: missing 

2096 policy: 

2097 resource: config-rule 

2098 filters: 

2099 - type: remediation 

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

2101 remediation: &remediation-config 

2102 TargetId: AWS-DisableS3BucketPublicReadWrite 

2103 Automatic: true 

2104 MaximumAutomaticAttempts: 5 

2105 RetryAttemptSeconds: 211 

2106 Parameters: 

2107 AutomationAssumeRole: 

2108 StaticValue: 

2109 Values: 

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

2111 S3BucketName: 

2112 ResourceValue: 

2113 Value: RESOURCE_ID 

2114 actions: 

2115 - type: toggle-config-managed-rule 

2116 rule_name: *rule_name 

2117 managed_rule_id: S3_BUCKET_PUBLIC_WRITE_PROHIBITED 

2118 resource_types: 

2119 - 'AWS::S3::Bucket' 

2120 rule_parameters: '{}' 

2121 remediation: *remediation-config 

2122 """ 

2123 

2124 permissions = ( 

2125 'config:DescribeConfigRules', 

2126 'config:DescribeRemediationConfigurations', 

2127 'config:PutRemediationConfigurations', 

2128 'config:PutConfigRule', 

2129 ) 

2130 

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

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

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

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

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

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

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

2138 resource_tag={ 

2139 'type': 'object', 

2140 'properties': { 

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

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

2143 }, 

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

2145 }, 

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

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

2148 remediation={ 

2149 'type': 'object', 

2150 'properties': { 

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

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

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

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

2155 'MaximumAutomaticAttempts': { 

2156 'type': 'integer', 

2157 'minimum': 1, 'maximum': 25, 

2158 }, 

2159 'RetryAttemptSeconds': { 

2160 'type': 'integer', 

2161 'minimum': 1, 'maximum': 2678000, 

2162 }, 

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

2164 }, 

2165 }, 

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

2167 required=['rule_name'], 

2168 ) 

2169 

2170 def validate(self): 

2171 if ( 

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

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

2174 ): 

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

2176 return self 

2177 

2178 def process(self, accounts): 

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

2180 rule = self.ConfigManagedRule(self.data) 

2181 params = self.get_rule_params(rule) 

2182 

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

2184 client.put_config_rule(**params) 

2185 

2186 if rule.remediation: 

2187 remediation_params = self.get_remediation_params(rule) 

2188 client.put_remediation_configurations( 

2189 RemediationConfigurations=[remediation_params] 

2190 ) 

2191 else: 

2192 with suppress(client.exceptions.NoSuchRemediationConfigurationException): 

2193 client.delete_remediation_configuration( 

2194 ConfigRuleName=rule.name 

2195 ) 

2196 

2197 with suppress(client.exceptions.NoSuchConfigRuleException): 

2198 client.delete_config_rule( 

2199 ConfigRuleName=rule.name 

2200 ) 

2201 

2202 def get_rule_params(self, rule): 

2203 params = dict( 

2204 ConfigRuleName=rule.name, 

2205 Description=rule.description, 

2206 Source={ 

2207 'Owner': 'AWS', 

2208 'SourceIdentifier': rule.managed_rule_id, 

2209 }, 

2210 InputParameters=rule.rule_parameters 

2211 ) 

2212 

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

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

2215 # one resource type and one resource ID 

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

2217 if rule.resource_tag: 

2218 params.update({'Scope': { 

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

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

2221 }) 

2222 elif rule.resource_id: 

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

2224 

2225 return dict(ConfigRule=params) 

2226 

2227 def get_remediation_params(self, rule): 

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

2229 if 'TargetType' not in rule.remediation: 

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

2231 return rule.remediation 

2232 

2233 class ConfigManagedRule: 

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

2235 """ 

2236 

2237 def __init__(self, data): 

2238 self.data = data 

2239 

2240 @property 

2241 def name(self): 

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

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

2244 

2245 @property 

2246 def description(self): 

2247 return self.data.get( 

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

2249 

2250 @property 

2251 def tags(self): 

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

2253 

2254 @property 

2255 def resource_types(self): 

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

2257 

2258 @property 

2259 def managed_rule_id(self): 

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

2261 

2262 @property 

2263 def resource_tag(self): 

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

2265 

2266 @property 

2267 def resource_id(self): 

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

2269 

2270 @property 

2271 def rule_parameters(self): 

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

2273 

2274 @property 

2275 def remediation(self): 

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

2277 

2278 

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

2280class SesAggStats(ValueFilter): 

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

2282 the data points into a single report. 

2283 

2284 :example: 

2285 

2286 .. code-block:: yaml 

2287 

2288 policies: 

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

2290 resource: account 

2291 filters: 

2292 - type: ses-agg-send-stats 

2293 """ 

2294 

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

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

2297 permissions = ("ses:GetSendStatistics",) 

2298 

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

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

2301 get_send_stats = client.get_send_statistics() 

2302 results = [] 

2303 

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

2305 return results 

2306 

2307 resource_counter = {'DeliveryAttempts': 0, 

2308 'Bounces': 0, 

2309 'Complaints': 0, 

2310 'Rejects': 0, 

2311 'BounceRate': 0} 

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

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

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

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

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

2317 resource_counter['BounceRate'] = round( 

2318 (resource_counter['Bounces'] / 

2319 resource_counter['DeliveryAttempts']) * 100) 

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

2321 

2322 return resources 

2323 

2324 

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

2326class SesConsecutiveStats(Filter): 

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

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

2329 

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

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

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

2333 

2334 :example: 

2335 

2336 .. code-block:: yaml 

2337 

2338 policies: 

2339 - name: ses-send-stats 

2340 resource: account 

2341 filters: 

2342 - type: ses-send-stats 

2343 days: 5 

2344 - type: value 

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

2346 op: ge 

2347 value: 10 

2348 """ 

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

2350 required=['days']) 

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

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

2353 permissions = ("ses:GetSendStatistics",) 

2354 

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

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

2357 get_send_stats = client.get_send_statistics() 

2358 results = [] 

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

2360 utcnow = datetime.datetime.utcnow() 

2361 expected_dates = set() 

2362 

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

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

2365 

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

2367 return results 

2368 

2369 metrics = {} 

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

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

2372 if ts not in expected_dates: 

2373 continue 

2374 

2375 if not metrics.get(ts): 

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

2377 'Bounces': 0, 

2378 'Complaints': 0, 

2379 'Rejects': 0} 

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

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

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

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

2384 

2385 max_bounce_rate = 0 

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

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

2388 if max_bounce_rate < metric['BounceRate']: 

2389 max_bounce_rate = metric['BounceRate'] 

2390 metric['Date'] = ts 

2391 

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

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

2394 

2395 return resources 

2396 

2397 

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

2399class BedrockModelInvocationLogging(ListItemFilter): 

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

2401 

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

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

2404 

2405 :example: 

2406 

2407 .. code-block:: yaml 

2408 

2409 policies: 

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

2411 resource: account 

2412 filters: 

2413 - type: bedrock-model-invocation-logging 

2414 attrs: 

2415 - imageDataDeliveryEnabled: True 

2416 

2417 """ 

2418 schema = type_schema( 

2419 'bedrock-model-invocation-logging', 

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

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

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

2423 ) 

2424 permissions = ('bedrock:GetModelInvocationLoggingConfiguration',) 

2425 annotation_key = 'c7n:BedrockModelInvocationLogging' 

2426 

2427 def get_item_values(self, resource): 

2428 item_values = [] 

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

2430 invocation_logging_config = client \ 

2431 .get_model_invocation_logging_configuration().get('loggingConfig') 

2432 if invocation_logging_config is not None: 

2433 item_values.append(invocation_logging_config) 

2434 resource[self.annotation_key] = invocation_logging_config 

2435 return item_values 

2436 

2437 

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

2439class SetBedrockModelInvocationLogging(BaseAction): 

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

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

2442 

2443 To delete a configuration, supply enabled to False 

2444 

2445 :example: 

2446 

2447 .. code-block:: yaml 

2448 

2449 policies: 

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

2451 resource: account 

2452 actions: 

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

2454 enabled: True 

2455 loggingConfig: 

2456 textDataDeliveryEnabled: True 

2457 s3Config: 

2458 bucketName: test-bedrock-1 

2459 keyPrefix: logging/ 

2460 

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

2462 resource: account 

2463 actions: 

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

2465 enabled: False 

2466 """ 

2467 

2468 schema = { 

2469 'type': 'object', 

2470 'additionalProperties': False, 

2471 'properties': { 

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

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

2474 'loggingConfig': {'type': 'object'} 

2475 }, 

2476 } 

2477 

2478 permissions = ('bedrock:PutModelInvocationLoggingConfiguration',) 

2479 shape = 'PutModelInvocationLoggingConfigurationRequest' 

2480 service = 'bedrock' 

2481 

2482 def validate(self): 

2483 cfg = dict(self.data) 

2484 enabled = cfg.get('enabled') 

2485 if enabled: 

2486 cfg.pop('type') 

2487 cfg.pop('enabled') 

2488 return shape_validate( 

2489 cfg, 

2490 self.shape, 

2491 self.service) 

2492 

2493 def process(self, resources): 

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

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

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

2497 client.put_model_invocation_logging_configuration(loggingConfig=params) 

2498 else: 

2499 client.delete_model_invocation_logging_configuration() 

2500 

2501 

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

2503class EC2MetadataDefaults(ValueFilter): 

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

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

2506 not be returned in the response. 

2507 

2508 :example: 

2509 

2510 .. code-block:: yaml 

2511 

2512 policies: 

2513 - name: ec2-imds-defaults 

2514 resource: account 

2515 filters: 

2516 - or: 

2517 - type: ec2-metadata-defaults 

2518 key: HttpTokens 

2519 value: optional 

2520 - type: ec2-metadata-defaults 

2521 key: HttpTokens 

2522 value: absent 

2523 """ 

2524 

2525 annotation_key = 'c7n:EC2MetadataDefaults' 

2526 annotate = False # no annotation from value filter 

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

2528 permissions = ('ec2:GetInstanceMetadataDefaults',) 

2529 

2530 def augment(self, resources): 

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

2532 for r in resources: 

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

2534 client.get_instance_metadata_defaults)["AccountLevel"] 

2535 

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

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

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

2539 

2540 def __call__(self, r): 

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

2542 

2543 

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

2545class SetEC2MetadataDefaults(BaseAction): 

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

2547 

2548 :example: 

2549 

2550 .. code-block:: yaml 

2551 

2552 policies: 

2553 - name: set-ec2-metadata-defaults 

2554 resource: account 

2555 filters: 

2556 - or: 

2557 - type: ec2-metadata-defaults 

2558 key: HttpTokens 

2559 op: eq 

2560 value: optional 

2561 - type: ec2-metadata-defaults 

2562 key: HttpTokens 

2563 value: absent 

2564 actions: 

2565 - type: set-ec2-metadata-defaults 

2566 HttpTokens: required 

2567 

2568 """ 

2569 

2570 schema = type_schema( 

2571 'set-ec2-metadata-defaults', 

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

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

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

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

2576 ) 

2577 

2578 permissions = ('ec2:ModifyInstanceMetadataDefaults',) 

2579 service = 'ec2' 

2580 shape = 'ModifyInstanceMetadataDefaultsRequest' 

2581 

2582 def validate(self): 

2583 req = dict(self.data) 

2584 req.pop('type') 

2585 return shape_validate( 

2586 req, self.shape, self.service 

2587 ) 

2588 

2589 def process(self, resources): 

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

2591 self.data.pop('type') 

2592 client.modify_instance_metadata_defaults(**self.data) 

2593 

2594 

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

2596class SetSecurityTokenServicePreferences(BaseAction): 

2597 """Action to set STS preferences.""" 

2598 

2599 """Action to set STS preferences. 

2600 

2601 :example: 

2602 

2603 .. code-block:: yaml 

2604 

2605 policies: 

2606 - name: set-sts-preferences 

2607 resource: account 

2608 filters: 

2609 - or: 

2610 - type: iam-summary 

2611 key: GlobalEndpointTokenVersion 

2612 value: absent 

2613 value: optional 

2614 - type: iam-summary 

2615 key: GlobalEndpointTokenVersion 

2616 op: ne 

2617 value: 2 

2618 actions: 

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

2620 token_version: v2Token 

2621 

2622 """ 

2623 

2624 schema = type_schema( 

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

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

2627 ) 

2628 

2629 permissions = ('iam:SetSecurityTokenServicePreferences',) 

2630 

2631 def process(self, resources): 

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

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

2634 for resource in resources: 

2635 self.set_sts_preferences(client, token_version) 

2636 

2637 def set_sts_preferences(self, client, token_version): 

2638 client.set_security_token_service_preferences( 

2639 GlobalEndpointTokenVersion=token_version 

2640 )