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

994 statements  

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

1# Copyright The Cloud Custodian Authors. 

2# SPDX-License-Identifier: Apache-2.0 

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 ResourceManager, resources 

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

22from c7n.query import QueryResourceManager, TypeInfo 

23 

24from c7n.resources.iam import CredentialReport 

25from c7n.resources.securityhub import OtherResourcePostFinding 

26 

27from .aws import shape_validate, Arn 

28 

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

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

31 

32retry = staticmethod(QueryResourceManager.retry) 

33filters.register('missing', Missing) 

34 

35 

36def get_account(session_factory, config): 

37 session = local_session(session_factory) 

38 client = session.client('iam') 

39 aliases = client.list_account_aliases().get( 

40 'AccountAliases', ('',)) 

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

42 return {'account_id': config.account_id, 

43 'account_name': name} 

44 

45 

46@resources.register('account') 

47class Account(ResourceManager): 

48 

49 filter_registry = filters 

50 action_registry = actions 

51 retry = staticmethod(QueryResourceManager.retry) 

52 source_type = 'describe' 

53 

54 class resource_type(TypeInfo): 

55 id = 'account_id' 

56 name = 'account_name' 

57 filter_name = None 

58 global_resource = True 

59 # fake this for doc gen 

60 service = "account" 

61 # for posting config rule evaluations 

62 cfn_type = 'AWS::::Account' 

63 

64 @classmethod 

65 def get_permissions(cls): 

66 return ('iam:ListAccountAliases',) 

67 

68 @classmethod 

69 def has_arn(cls): 

70 return True 

71 

72 def get_arns(self, resources): 

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

74 

75 def get_model(self): 

76 return self.resource_type 

77 

78 def resources(self): 

79 return self.filter_resources([get_account(self.session_factory, self.config)]) 

80 

81 def get_resources(self, resource_ids): 

82 return [get_account(self.session_factory, self.config)] 

83 

84 

85@filters.register('credential') 

86class AccountCredentialReport(CredentialReport): 

87 

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

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

90 report = self.get_credential_report() 

91 if report is None: 

92 return [] 

93 results = [] 

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

95 for r in resources: 

96 if self.match(r, info): 

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

98 results.append(r) 

99 return results 

100 

101 

102@filters.register('organization') 

103class AccountOrganization(ValueFilter): 

104 """Check organization enrollment and configuration 

105 

106 :example: 

107 

108 determine if an account is not in an organization 

109 

110 .. code-block:: yaml 

111 

112 policies: 

113 - name: no-org 

114 resource: account 

115 filters: 

116 - type: organization 

117 key: Id 

118 value: absent 

119 

120 

121 :example: 

122 

123 determine if an account is setup for organization policies 

124 

125 .. code-block:: yaml 

126 

127 policies: 

128 - name: org-policies-not-enabled 

129 resource: account 

130 filters: 

131 - type: organization 

132 key: FeatureSet 

133 value: ALL 

134 op: not-equal 

135 """ 

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

137 schema_alias = False 

138 

139 annotation_key = 'c7n:org' 

140 annotate = False 

141 

142 permissions = ('organizations:DescribeOrganization',) 

143 

144 def get_org_info(self, account): 

145 client = local_session( 

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

147 try: 

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

149 except client.exceptions.AWSOrganizationsNotInUseException: 

150 org_info = {} 

151 except ClientError as e: 

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

153 org_info = None 

154 account[self.annotation_key] = org_info 

155 

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

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

158 self.get_org_info(resources[0]) 

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

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

161 return [] 

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

163 return resources 

164 return [] 

165 

166 

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

168class MacieEnabled(ValueFilter): 

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

170 

171 Gets the macie session info for the account, and 

172 the macie master account for the current account if 

173 configured. 

174 """ 

175 

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

177 schema_alias = False 

178 annotation_key = 'c7n:macie' 

179 annotate = False 

180 permissions = ('macie2:GetMacieSession', 'macie2:GetMasterAccount',) 

181 

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

183 

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

185 self.get_macie_info(resources[0]) 

186 

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

188 return resources 

189 

190 return [] 

191 

192 def get_macie_info(self, account): 

193 client = local_session( 

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

195 

196 try: 

197 info = client.get_macie_session() 

198 info.pop('ResponseMetadata') 

199 except client.exceptions.AccessDeniedException: 

200 info = {} 

201 

202 try: 

203 minfo = client.get_master_account().get('master') 

204 except (client.exceptions.AccessDeniedException, 

205 client.exceptions.ResourceNotFoundException): 

206 info['master'] = {} 

207 else: 

208 info['master'] = minfo 

209 account[self.annotation_key] = info 

210 

211 

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

213class CloudTrailEnabled(Filter): 

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

215 

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

217 

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

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

220 

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

222 corresponding SNS subscription for a specific filter pattern 

223 

224 :example: 

225 

226 .. code-block:: yaml 

227 

228 policies: 

229 - name: account-cloudtrail-enabled 

230 resource: account 

231 region: us-east-1 

232 filters: 

233 - type: check-cloudtrail 

234 global-events: true 

235 multi-region: true 

236 running: true 

237 include-management-events: true 

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

239 

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

241 matching a regex pattern: 

242 

243 .. code-block:: yaml 

244 

245 policies: 

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

247 resource: account 

248 region: us-east-1 

249 filters: 

250 - type: check-cloudtrail 

251 log-metric-filter-pattern: 

252 type: value 

253 op: regex 

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

255 """ 

256 schema = type_schema( 

257 'check-cloudtrail', 

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

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

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

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

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

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

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

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

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

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

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

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

270 

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

272 'cloudtrail:GetEventSelectors', 'cloudwatch:DescribeAlarmsForMetric', 

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

274 

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

276 session = local_session(self.manager.session_factory) 

277 client = session.client('cloudtrail') 

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

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

280 

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

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

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

284 current_region = session.region_name 

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

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

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

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

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

290 trails = [t for t in trails 

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

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

293 trails = [t for t in trails 

294 if t.get('LogFileValidationEnabled')] 

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

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

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

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

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

300 running = [] 

301 for t in list(trails): 

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

303 Name=t['TrailARN']) 

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

305 'LatestDeliveryError'): 

306 running.append(t) 

307 trails = running 

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

309 matched = [] 

310 for t in list(trails): 

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

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

313 for s in selectors['EventSelectors']: 

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

315 matched.append(t) 

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

317 for s in selectors['AdvancedEventSelectors']: 

318 management = False 

319 readonly = False 

320 for field_selector in s['FieldSelectors']: 

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

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

323 management = True 

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

325 readonly = True 

326 if management and not readonly: 

327 matched.append(t) 

328 

329 trails = matched 

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

331 client_logs = session.client('logs') 

332 client_cw = session.client('cloudwatch') 

333 client_sns = session.client('sns') 

334 matched = [] 

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

336 if isinstance(pattern, str): 

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

338 else: 

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

340 vf = ValueFilter(pattern) 

341 

342 for t in list(trails): 

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

344 continue 

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

346 try: 

347 metric_filters_log_group = \ 

348 client_logs.describe_metric_filters( 

349 logGroupName=log_group_name)['metricFilters'] 

350 except ClientError as e: 

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

352 continue 

353 filter_matched = None 

354 if metric_filters_log_group: 

355 for f in metric_filters_log_group: 

356 if vf(f): 

357 filter_matched = f 

358 break 

359 if not filter_matched: 

360 continue 

361 alarms = client_cw.describe_alarms_for_metric( 

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

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

364 )['MetricAlarms'] 

365 alarm_actions = [] 

366 for a in alarms: 

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

368 if not alarm_actions: 

369 continue 

370 alarm_actions = set(alarm_actions) 

371 for a in alarm_actions: 

372 try: 

373 sns_topic_attributes = client_sns.get_topic_attributes(TopicArn=a) 

374 sns_topic_attributes = sns_topic_attributes.get('Attributes') 

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

376 matched.append(t) 

377 except client_sns.exceptions.InvalidParameterValueException: 

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

379 # not be an sns topic for instance 

380 continue 

381 trails = matched 

382 if trails: 

383 return [] 

384 return resources 

385 

386 

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

388class GuardDutyEnabled(MultiAttrFilter): 

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

390 

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

392 master if any. 

393 

394 :example: 

395 

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

397 

398 .. code-block:: yaml 

399 

400 policies: 

401 - name: guardduty-enabled 

402 resource: account 

403 filters: 

404 - type: guard-duty 

405 Detector.Status: ENABLED 

406 Master.AccountId: "00011001" 

407 Master.RelationshipStatus: "Enabled" 

408 """ 

409 

410 schema = { 

411 'type': 'object', 

412 'additionalProperties': False, 

413 'properties': { 

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

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

416 'patternProperties': { 

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

418 '^Master': {'oneOf': [{'type': 'object'}, {'type': 'string'}]}}, 

419 } 

420 

421 annotation = "c7n:guard-duty" 

422 permissions = ( 

423 'guardduty:GetMasterAccount', 

424 'guardduty:ListDetectors', 

425 'guardduty:GetDetector') 

426 

427 def validate(self): 

428 attrs = set() 

429 for k in self.data: 

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

431 attrs.add(k) 

432 self.multi_attrs = attrs 

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

434 

435 def get_target(self, resource): 

436 if self.annotation in resource: 

437 return resource[self.annotation] 

438 

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

440 # detectors are singletons too. 

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

442 

443 if not detector_ids: 

444 return None 

445 else: 

446 detector_id = detector_ids.pop() 

447 

448 detector = client.get_detector(DetectorId=detector_id) 

449 detector.pop('ResponseMetadata', None) 

450 master = client.get_master_account(DetectorId=detector_id).get('Master') 

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

452 return r 

453 

454 

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

456class ConfigEnabled(Filter): 

457 """Is config service enabled for this account 

458 

459 :example: 

460 

461 .. code-block:: yaml 

462 

463 policies: 

464 - name: account-check-config-services 

465 resource: account 

466 region: us-east-1 

467 filters: 

468 - type: check-config 

469 all-resources: true 

470 global-resources: true 

471 running: true 

472 """ 

473 

474 schema = type_schema( 

475 'check-config', **{ 

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

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

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

479 

480 permissions = ('config:DescribeDeliveryChannels', 

481 'config:DescribeConfigurationRecorders', 

482 'config:DescribeConfigurationRecorderStatus') 

483 

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

485 client = local_session( 

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

487 channels = client.describe_delivery_channels()[ 

488 'DeliveryChannels'] 

489 recorders = client.describe_configuration_recorders()[ 

490 'ConfigurationRecorders'] 

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

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

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

494 recorders = [ 

495 r for r in recorders 

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

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

498 recorders = [r for r in recorders 

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

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

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

502 s in client.describe_configuration_recorder_status( 

503 )['ConfigurationRecordersStatus']} 

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

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

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

507 if channels and recorders: 

508 return [] 

509 return resources 

510 

511 

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

513class IAMSummary(ValueFilter): 

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

515 

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

517 

518 Example iam summary wrt to matchable fields:: 

519 

520 { 

521 "AccessKeysPerUserQuota": 2, 

522 "AccountAccessKeysPresent": 0, 

523 "AccountMFAEnabled": 1, 

524 "AccountSigningCertificatesPresent": 0, 

525 "AssumeRolePolicySizeQuota": 2048, 

526 "AttachedPoliciesPerGroupQuota": 10, 

527 "AttachedPoliciesPerRoleQuota": 10, 

528 "AttachedPoliciesPerUserQuota": 10, 

529 "GroupPolicySizeQuota": 5120, 

530 "Groups": 1, 

531 "GroupsPerUserQuota": 10, 

532 "GroupsQuota": 100, 

533 "InstanceProfiles": 0, 

534 "InstanceProfilesQuota": 100, 

535 "MFADevices": 3, 

536 "MFADevicesInUse": 2, 

537 "Policies": 3, 

538 "PoliciesQuota": 1000, 

539 "PolicySizeQuota": 5120, 

540 "PolicyVersionsInUse": 5, 

541 "PolicyVersionsInUseQuota": 10000, 

542 "Providers": 0, 

543 "RolePolicySizeQuota": 10240, 

544 "Roles": 4, 

545 "RolesQuota": 250, 

546 "ServerCertificates": 0, 

547 "ServerCertificatesQuota": 20, 

548 "SigningCertificatesPerUserQuota": 2, 

549 "UserPolicySizeQuota": 2048, 

550 "Users": 5, 

551 "UsersQuota": 5000, 

552 "VersionsPerPolicyQuota": 5, 

553 } 

554 

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

556 enabled with root mfa or has root api keys. 

557 

558 .. code-block:: yaml 

559 

560 policies: 

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

562 resource: account 

563 filters: 

564 - type: iam-summary 

565 key: AccountMFAEnabled 

566 value: true 

567 op: eq 

568 value_type: swap 

569 """ 

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

571 schema_alias = False 

572 permissions = ('iam:GetAccountSummary',) 

573 

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

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

576 client = local_session( 

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

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

579 )['SummaryMap'] 

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

581 return resources 

582 return [] 

583 

584 

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

586class AccessAnalyzer(ValueFilter): 

587 """Check for access analyzers in an account 

588 

589 :example: 

590 

591 .. code-block:: yaml 

592 

593 policies: 

594 - name: account-access-analyzer 

595 resource: account 

596 filters: 

597 - type: access-analyzer 

598 key: 'status' 

599 value: ACTIVE 

600 op: eq 

601 """ 

602 

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

604 schema_alias = False 

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

606 annotation_key = 'c7n:matched-analyzers' 

607 

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

609 account = resources[0] 

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

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

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

613 else: 

614 analyzers = account.get(self.annotation_key) 

615 

616 matched_analyzers = [] 

617 for analyzer in analyzers: 

618 if self.match(analyzer): 

619 matched_analyzers.append(analyzer) 

620 account[self.annotation_key] = matched_analyzers 

621 return matched_analyzers and resources or [] 

622 

623 

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

625class AccountPasswordPolicy(ValueFilter): 

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

627 

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

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

630 account has attempted to set a policy at all. 

631 

632 :example: 

633 

634 .. code-block:: yaml 

635 

636 policies: 

637 - name: password-policy-check 

638 resource: account 

639 region: us-east-1 

640 filters: 

641 - type: password-policy 

642 key: MinimumPasswordLength 

643 value: 10 

644 op: ge 

645 - type: password-policy 

646 key: RequireSymbols 

647 value: true 

648 """ 

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

650 schema_alias = False 

651 permissions = ('iam:GetAccountPasswordPolicy',) 

652 

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

654 account = resources[0] 

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

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

657 policy = {} 

658 try: 

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

660 policy['PasswordPolicyConfigured'] = True 

661 except ClientError as e: 

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

663 policy['PasswordPolicyConfigured'] = False 

664 else: 

665 raise 

666 account['c7n:password_policy'] = policy 

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

668 return resources 

669 return [] 

670 

671 

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

673class SetAccountPasswordPolicy(BaseAction): 

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

675 

676 This only changes the policy for the items provided. 

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

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

679 

680 :example: 

681 

682 .. code-block:: yaml 

683 

684 policies: 

685 - name: set-account-password-policy 

686 resource: account 

687 filters: 

688 - not: 

689 - type: password-policy 

690 key: MinimumPasswordLength 

691 value: 10 

692 op: ge 

693 actions: 

694 - type: set-password-policy 

695 policy: 

696 MinimumPasswordLength: 20 

697 """ 

698 schema = type_schema( 

699 'set-password-policy', 

700 policy={ 

701 'type': 'object' 

702 }) 

703 shape = 'UpdateAccountPasswordPolicyRequest' 

704 service = 'iam' 

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

706 

707 def validate(self): 

708 return shape_validate( 

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

710 self.shape, 

711 self.service) 

712 

713 def process(self, resources): 

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

715 account = resources[0] 

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

717 config = account['c7n:password_policy'] 

718 else: 

719 try: 

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

721 except client.exceptions.NoSuchEntityException: 

722 config = {} 

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

724 config.update(params) 

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

726 'PasswordPolicyConfigured')} 

727 client.update_account_password_policy(**config) 

728 

729 

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

731class ServiceLimit(Filter): 

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

733 

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

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

736 

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

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

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

740 

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

742 

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

744 

745 or via the awscli: 

746 

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

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

749 

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

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

752 backward compatibility with the old style of checks: 

753 

754 - `services` 

755 

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

757 These are case-insensitive globbing matches. 

758 

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

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

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

762 to make. 

763 

764 - `limits` 

765 

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

767 These are case-insensitive globbing matches. 

768 

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

770 

771 Check Name Service Limit Name 

772 ---------------------------------- -------------- --------------------------------- 

773 Auto Scaling Groups AutoScaling Auto Scaling groups 

774 Auto Scaling Launch Configurations AutoScaling Launch configurations 

775 CloudFormation Stacks CloudFormation Stacks 

776 ELB Application Load Balancers ELB Active Application Load Balancers 

777 ELB Classic Load Balancers ELB Active load balancers 

778 ELB Network Load Balancers ELB Active Network Load Balancers 

779 VPC VPC VPCs 

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

781 VPC Internet Gateways VPC Internet gateways 

782 

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

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

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

786 

787 :example: 

788 

789 .. code-block:: yaml 

790 

791 policies: 

792 - name: specific-account-service-limits 

793 resource: account 

794 filters: 

795 - type: service-limit 

796 names: 

797 - IAM Policies 

798 - IAM Roles 

799 - "VPC*" 

800 threshold: 1.0 

801 

802 - name: increase-account-service-limits 

803 resource: account 

804 filters: 

805 - type: service-limit 

806 services: 

807 - EC2 

808 threshold: 1.0 

809 

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

811 region: us-east-1 

812 resource: account 

813 filters: 

814 - type: service-limit 

815 services: 

816 - IAM 

817 limits: 

818 - Roles 

819 """ 

820 

821 schema = type_schema( 

822 'service-limit', 

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

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

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

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

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

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

829 'enum': ['AutoScaling', 'CloudFormation', 

830 'DynamoDB', 'EBS', 'EC2', 'ELB', 

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

832 

833 permissions = ('support:DescribeTrustedAdvisorCheckRefreshStatuses', 

834 'support:DescribeTrustedAdvisorCheckResult', 

835 'support:DescribeTrustedAdvisorChecks', 

836 'support:RefreshTrustedAdvisorCheck') 

837 deprecated_check_ids = ['eW7HH0l7J9'] 

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

839 

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

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

842 poll_interval = 5 

843 poll_max_intervals = 10 

844 global_services = {'IAM'} 

845 

846 def validate(self): 

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

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

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

850 raise PolicyValidationError( 

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

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

853 return self 

854 

855 @classmethod 

856 def get_check_result(cls, client, check_id): 

857 checks = client.describe_trusted_advisor_check_result( 

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

859 

860 # Check status and if necessary refresh checks 

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

862 try: 

863 client.refresh_trusted_advisor_check(checkId=check_id) 

864 except ClientError as e: 

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

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

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

868 return 

869 

870 for _ in range(cls.poll_max_intervals): 

871 time.sleep(cls.poll_interval) 

872 refresh_response = client.describe_trusted_advisor_check_refresh_statuses( 

873 checkIds=[check_id]) 

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

875 checks = client.describe_trusted_advisor_check_result( 

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

877 break 

878 return checks 

879 

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

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

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

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

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

885 

886 def match_patterns_to_value(self, patterns, value): 

887 for p in patterns: 

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

889 return True 

890 return False 

891 

892 def should_process(self, name): 

893 # if names specified, limit to these names 

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

895 if patterns: 

896 return self.match_patterns_to_value(patterns, name) 

897 

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

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

900 if services: 

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

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

903 

904 return True 

905 

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

907 support_region = get_support_region(self.manager) 

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

909 'support', region_name=support_region) 

910 checks = self.get_available_checks(client) 

911 exceeded = [] 

912 for check in checks: 

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

914 continue 

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

916 if matched: 

917 for m in matched: 

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

919 m['name'] = check['name'] 

920 exceeded.extend(matched) 

921 if exceeded: 

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

923 return resources 

924 return [] 

925 

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

927 region = self.manager.config.region 

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

929 

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

931 return [] 

932 

933 # trim to only results for this region 

934 results['flaggedResources'] = [ 

935 r 

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

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

938 ] 

939 

940 # save all raw limit results to the account resource 

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

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

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

944 

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

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

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

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

949 try: 

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

951 except ClientError as e: 

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

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

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

955 return 

956 

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

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

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

960 exceeded = [] 

961 

962 for resource in results['flaggedResources']: 

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

964 continue 

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

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

967 continue 

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

969 continue 

970 limit['status'] = resource['status'] 

971 limit['percentage'] = ( 

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

973 ) 

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

975 continue 

976 exceeded.append(limit) 

977 return exceeded 

978 

979 

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

981class RequestLimitIncrease(BaseAction): 

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

983 

984 :Example: 

985 

986 .. code-block:: yaml 

987 

988 policies: 

989 - name: raise-account-service-limits 

990 resource: account 

991 filters: 

992 - type: service-limit 

993 services: 

994 - EBS 

995 limits: 

996 - Provisioned IOPS (SSD) storage (GiB) 

997 threshold: 60.5 

998 actions: 

999 - type: request-limit-increase 

1000 notify: [email, email2] 

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

1002 percent-increase: 50 

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

1004 """ 

1005 

1006 schema = { 

1007 'type': 'object', 

1008 'additionalProperties': False, 

1009 'properties': { 

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

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

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

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

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

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

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

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

1018 }, 

1019 'oneOf': [ 

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

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

1022 ] 

1023 } 

1024 

1025 permissions = ('support:CreateCase',) 

1026 

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

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

1029 default_severity = 'normal' 

1030 

1031 service_code_mapping = { 

1032 'AutoScaling': 'auto-scaling', 

1033 'CloudFormation': 'aws-cloudformation', 

1034 'DynamoDB': 'amazon-dynamodb', 

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

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

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

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

1039 'Kinesis': 'amazon-kinesis', 

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

1041 'Route53': 'amazon-route53', 

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

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

1044 } 

1045 

1046 def process(self, resources): 

1047 support_region = get_support_region(self.manager) 

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

1049 'support', region_name=support_region) 

1050 account_id = self.manager.config.account_id 

1051 service_map = {} 

1052 region_map = {} 

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

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

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

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

1057 

1058 for s in limit_exceeded: 

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

1060 if percent_increase: 

1061 increase_by = current_limit * float(percent_increase) / 100 

1062 increase_by = max(increase_by, minimum_increase) 

1063 else: 

1064 increase_by = amount_increase 

1065 increase_by = round(increase_by) 

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

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

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

1069 (current_limit + increase_by)) 

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

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

1072 

1073 for service in service_map: 

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

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

1076 service_code = self.service_code_mapping.get(service) 

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

1078 body = body.format(**{ 

1079 'service': service, 

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

1081 }) 

1082 client.create_case( 

1083 subject=subject, 

1084 communicationBody=body, 

1085 serviceCode=service_code, 

1086 categoryCode='general-guidance', 

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

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

1089 

1090 

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

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

1093 ct_actions = [ 

1094 { 

1095 'Action': 's3:GetBucketAcl', 

1096 'Effect': 'Allow', 

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

1098 'Resource': generate_arn( 

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

1100 'Sid': 'AWSCloudTrailAclCheck20150319', 

1101 }, 

1102 { 

1103 'Action': 's3:PutObject', 

1104 'Condition': { 

1105 'StringEquals': 

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

1107 }, 

1108 'Effect': 'Allow', 

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

1110 'Resource': generate_arn( 

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

1112 'Sid': 'AWSCloudTrailWrite20150319', 

1113 }, 

1114 ] 

1115 # parse original policy 

1116 if original is None: 

1117 policy = { 

1118 'Statement': [], 

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

1120 } 

1121 else: 

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

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

1124 for cta in ct_actions: 

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

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

1127 return json.dumps(policy) 

1128 

1129 

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

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

1132# register it. 

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

1134 

1135 

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

1137class EnableTrail(BaseAction): 

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

1139 

1140 :Example: 

1141 

1142 .. code-block:: yaml 

1143 

1144 policies: 

1145 - name: trail-test 

1146 description: Ensure CloudTrail logging is enabled 

1147 resource: account 

1148 actions: 

1149 - type: enable-cloudtrail 

1150 trail: mytrail 

1151 bucket: trails 

1152 """ 

1153 

1154 permissions = ( 

1155 'cloudtrail:CreateTrail', 

1156 'cloudtrail:DescribeTrails', 

1157 'cloudtrail:GetTrailStatus', 

1158 'cloudtrail:StartLogging', 

1159 'cloudtrail:UpdateTrail', 

1160 's3:CreateBucket', 

1161 's3:GetBucketPolicy', 

1162 's3:PutBucketPolicy', 

1163 ) 

1164 schema = type_schema( 

1165 'enable-cloudtrail', 

1166 **{ 

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

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

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

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

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

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

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

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

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

1176 'required': ('bucket',), 

1177 } 

1178 ) 

1179 

1180 def process(self, accounts): 

1181 """Create or enable CloudTrail""" 

1182 session = local_session(self.manager.session_factory) 

1183 client = session.client('cloudtrail') 

1184 bucket_name = self.data['bucket'] 

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

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

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

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

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

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

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

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

1193 

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

1195 try: 

1196 s3client.create_bucket( 

1197 Bucket=bucket_name, 

1198 CreateBucketConfiguration={'LocationConstraint': bucket_region} 

1199 ) 

1200 except ClientError as ce: 

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

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

1203 raise ce 

1204 

1205 try: 

1206 current_policy = s3client.get_bucket_policy(Bucket=bucket_name) 

1207 except ClientError: 

1208 current_policy = None 

1209 

1210 policy_json = cloudtrail_policy( 

1211 current_policy, bucket_name, 

1212 self.manager.config.account_id, bucket_region) 

1213 

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

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

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

1217 new_trail = client.create_trail( 

1218 Name=trail_name, 

1219 S3BucketName=bucket_name, 

1220 ) 

1221 if new_trail: 

1222 trails.append(new_trail) 

1223 # the loop below will configure the new trail 

1224 for trail in trails: 

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

1226 continue 

1227 # enable 

1228 arn = trail['TrailARN'] 

1229 status = client.get_trail_status(Name=arn) 

1230 if not status['IsLogging']: 

1231 client.start_logging(Name=arn) 

1232 # apply configuration changes (if any) 

1233 update_args = {} 

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

1235 update_args['IsMultiRegionTrail'] = multi_region 

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

1237 update_args['IncludeGlobalServiceEvents'] = global_events 

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

1239 update_args['SnsTopicName'] = notify 

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

1241 update_args['EnableLogFileValidation'] = file_digest 

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

1243 if not kms and 'KmsKeyId' in trail: 

1244 kms_key = '' 

1245 update_args['KmsKeyId'] = kms_key 

1246 if update_args: 

1247 update_args['Name'] = trail_name 

1248 client.update_trail(**update_args) 

1249 

1250 

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

1252class HasVirtualMFA(Filter): 

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

1254 

1255 :example: 

1256 

1257 .. code-block:: yaml 

1258 

1259 policies: 

1260 - name: account-with-virtual-mfa 

1261 resource: account 

1262 region: us-east-1 

1263 filters: 

1264 - type: has-virtual-mfa 

1265 value: true 

1266 """ 

1267 

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

1269 

1270 permissions = ('iam:ListVirtualMFADevices',) 

1271 

1272 def mfa_belongs_to_root_account(self, mfa): 

1273 return mfa['SerialNumber'].endswith(':mfa/root-account-mfa-device') 

1274 

1275 def account_has_virtual_mfa(self, account): 

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

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

1278 paginator = client.get_paginator('list_virtual_mfa_devices') 

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

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

1281 self.mfa_belongs_to_root_account, raw_list)) 

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

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

1284 return expect_virtual_mfa == has_virtual_mfa 

1285 

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

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

1288 

1289 

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

1291class EnableDataEvents(BaseAction): 

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

1293 

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

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

1296 

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

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

1299 a trail this will do so without management events. 

1300 

1301 :example: 

1302 

1303 .. code-block:: yaml 

1304 

1305 policies: 

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

1307 resource: account 

1308 actions: 

1309 - type: enable-data-events 

1310 data-trail: 

1311 name: s3-events 

1312 multi-region: us-east-1 

1313 """ 

1314 

1315 schema = type_schema( 

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

1317 'data-trail': { 

1318 'type': 'object', 

1319 'additionalProperties': False, 

1320 'required': ['name'], 

1321 'properties': { 

1322 'create': { 

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

1324 'type': 'boolean'}, 

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

1326 'name': { 

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

1328 'type': 'string'}, 

1329 'topic': { 

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

1331 'type': 'string'}, 

1332 's3-bucket': { 

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

1334 'type': 'string'}, 

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

1336 'key-id': { 

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

1338 'type': 'string'}, 

1339 # region that we're aggregating via trails. 

1340 'multi-region': { 

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

1342 'type': 'string'}}}}) 

1343 

1344 def validate(self): 

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

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

1347 raise PolicyValidationError( 

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

1349 self.manager.data)) 

1350 return self 

1351 

1352 def get_permissions(self): 

1353 perms = [ 

1354 'cloudtrail:DescribeTrails', 

1355 'cloudtrail:GetEventSelectors', 

1356 'cloudtrail:PutEventSelectors'] 

1357 

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

1359 perms.extend([ 

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

1361 return perms 

1362 

1363 def add_data_trail(self, client, trail_cfg): 

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

1365 raise ValueError( 

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

1367 params = dict( 

1368 Name=trail_cfg['name'], 

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

1370 EnableLogFileValidation=True) 

1371 

1372 if 'key-id' in trail_cfg: 

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

1374 if 's3-prefix' in trail_cfg: 

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

1376 if 'topic' in trail_cfg: 

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

1378 if 'multi-region' in trail_cfg: 

1379 params['IsMultiRegionTrail'] = True 

1380 

1381 client.create_trail(**params) 

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

1383 

1384 def process(self, resources): 

1385 session = local_session(self.manager.session_factory) 

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

1387 

1388 if region: 

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

1390 else: 

1391 client = session.client('cloudtrail') 

1392 

1393 added = False 

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

1395 trails = client.describe_trails( 

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

1397 if not trails: 

1398 trail = self.add_data_trail(client, tconfig) 

1399 added = True 

1400 else: 

1401 trail = trails[0] 

1402 

1403 events = client.get_event_selectors( 

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

1405 

1406 for e in events: 

1407 found = False 

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

1409 continue 

1410 for data_events in e['DataResources']: 

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

1412 continue 

1413 for b in data_events['Values']: 

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

1415 found = True 

1416 break 

1417 if found: 

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

1419 return 

1420 

1421 # Opinionated choice, separate api and data events. 

1422 event_count = len(events) 

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

1424 if len(events) != event_count: 

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

1426 

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

1428 # encompasses all the buckets in the account. 

1429 

1430 events.append({ 

1431 'IncludeManagementEvents': False, 

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

1433 'DataResources': [{ 

1434 'Type': 'AWS::S3::Object', 

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

1436 client.put_event_selectors( 

1437 TrailName=trail['Name'], 

1438 EventSelectors=events) 

1439 

1440 if added: 

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

1442 

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

1444 

1445 

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

1447class ShieldEnabled(Filter): 

1448 

1449 permissions = ('shield:DescribeSubscription',) 

1450 

1451 schema = type_schema( 

1452 'shield-enabled', 

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

1454 

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

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

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

1458 try: 

1459 subscription = client.describe_subscription().get( 

1460 'Subscription', None) 

1461 except ClientError as e: 

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

1463 raise 

1464 subscription = None 

1465 

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

1467 if state and subscription: 

1468 return resources 

1469 elif not state and not subscription: 

1470 return resources 

1471 return [] 

1472 

1473 

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

1475class SetShieldAdvanced(BaseAction): 

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

1477 

1478 permissions = ( 

1479 'shield:CreateSubscription', 'shield:DeleteSubscription') 

1480 

1481 schema = type_schema( 

1482 'set-shield-advanced', 

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

1484 

1485 def process(self, resources): 

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

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

1488 

1489 if state: 

1490 client.create_subscription() 

1491 else: 

1492 try: 

1493 client.delete_subscription() 

1494 except ClientError as e: 

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

1496 return 

1497 raise 

1498 

1499 

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

1501class XrayEncrypted(Filter): 

1502 """Determine if xray is encrypted. 

1503 

1504 :example: 

1505 

1506 .. code-block:: yaml 

1507 

1508 policies: 

1509 - name: xray-encrypt-with-default 

1510 resource: aws.account 

1511 filters: 

1512 - type: xray-encrypt-key 

1513 key: default 

1514 - name: xray-encrypt-with-kms 

1515 resource: aws.account 

1516 filters: 

1517 - type: xray-encrypt-key 

1518 key: kms 

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

1520 resource: aws.account 

1521 filters: 

1522 - type: xray-encrypt-key 

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

1524 """ 

1525 

1526 permissions = ('xray:GetEncryptionConfig',) 

1527 schema = type_schema( 

1528 'xray-encrypt-key', 

1529 required=['key'], 

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

1531 ) 

1532 

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

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

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

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

1537 

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

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

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

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

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

1543 else: 

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

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

1546 return rc 

1547 

1548 

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

1550class SetXrayEncryption(BaseAction): 

1551 """Enable specific xray encryption. 

1552 

1553 :example: 

1554 

1555 .. code-block:: yaml 

1556 

1557 policies: 

1558 - name: xray-default-encrypt 

1559 resource: aws.account 

1560 actions: 

1561 - type: set-xray-encrypt 

1562 key: default 

1563 - name: xray-kms-encrypt 

1564 resource: aws.account 

1565 actions: 

1566 - type: set-xray-encrypt 

1567 key: alias/some/alias/key 

1568 """ 

1569 

1570 permissions = ('xray:PutEncryptionConfig',) 

1571 schema = type_schema( 

1572 'set-xray-encrypt', 

1573 required=['key'], 

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

1575 ) 

1576 

1577 def process(self, resources): 

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

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

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

1581 client.put_encryption_config(**req) 

1582 

1583 

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

1585class EbsEncryption(Filter): 

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

1587 

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

1589 

1590 :example: 

1591 

1592 .. code-block:: yaml 

1593 

1594 policies: 

1595 - name: check-default-ebs-encryption 

1596 resource: aws.account 

1597 filters: 

1598 - type: default-ebs-encryption 

1599 key: "alias/aws/ebs" 

1600 state: true 

1601 

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

1603 

1604 :example: 

1605 

1606 .. code-block:: yaml 

1607 

1608 policies: 

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

1610 resource: aws.account 

1611 filters: 

1612 - type: default-ebs-encryption 

1613 key: 

1614 type: value 

1615 key: Origin 

1616 value: AWS_KMS 

1617 state: true 

1618 """ 

1619 permissions = ('ec2:GetEbsEncryptionByDefault',) 

1620 schema = type_schema( 

1621 'default-ebs-encryption', 

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

1623 key={'oneOf': [ 

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

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

1626 

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

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

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

1630 account_state = client.get_ebs_encryption_by_default().get( 

1631 'EbsEncryptionByDefault') 

1632 if account_state != state: 

1633 return [] 

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

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

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

1637 vf = KmsRelatedFilter(vfd, self.manager) 

1638 vf.RelatedIdsExpression = 'KmsKeyId' 

1639 vf.annotate = False 

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

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

1642 return [] 

1643 return resources 

1644 

1645 

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

1647class SetEbsEncryption(BaseAction): 

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

1649 

1650 :example: 

1651 

1652 .. code-block:: yaml 

1653 

1654 policies: 

1655 - name: set-default-ebs-encryption 

1656 resource: aws.account 

1657 filters: 

1658 - type: default-ebs-encryption 

1659 state: false 

1660 actions: 

1661 - type: set-ebs-encryption 

1662 state: true 

1663 key: alias/aws/ebs 

1664 """ 

1665 permissions = ('ec2:EnableEbsEncryptionByDefault', 

1666 'ec2:DisableEbsEncryptionByDefault') 

1667 

1668 schema = type_schema( 

1669 'set-ebs-encryption', 

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

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

1672 

1673 def process(self, resources): 

1674 client = local_session( 

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

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

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

1678 if state: 

1679 client.enable_ebs_encryption_by_default() 

1680 else: 

1681 client.disable_ebs_encryption_by_default() 

1682 

1683 if state and key: 

1684 client.modify_ebs_default_kms_key_id( 

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

1686 

1687 

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

1689class S3PublicBlock(ValueFilter): 

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

1691 

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

1693 """ 

1694 

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

1696 annotate = False # no annotation from value filter 

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

1698 schema_alias = False 

1699 permissions = ('s3:GetAccountPublicAccessBlock',) 

1700 

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

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

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

1704 

1705 def augment(self, resources): 

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

1707 for r in resources: 

1708 try: 

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

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

1711 except client.exceptions.NoSuchPublicAccessBlockConfiguration: 

1712 r[self.annotation_key] = {} 

1713 

1714 def __call__(self, r): 

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

1716 

1717 

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

1719class SetS3PublicBlock(BaseAction): 

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

1721 

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

1723 with the extant configuration. 

1724 

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

1726 

1727 :example: 

1728 

1729 .. yaml: 

1730 

1731 policies: 

1732 - name: restrict-public-buckets 

1733 resource: aws.account 

1734 filters: 

1735 - not: 

1736 - type: s3-public-block 

1737 key: RestrictPublicBuckets 

1738 value: true 

1739 actions: 

1740 - type: set-s3-public-block 

1741 RestrictPublicBuckets: true 

1742 

1743 """ 

1744 schema = type_schema( 

1745 'set-s3-public-block', 

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

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

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

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

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

1751 

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

1753 

1754 def validate(self): 

1755 config = self.data.copy() 

1756 config.pop('type') 

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

1758 raise PolicyValidationError( 

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

1760 self.type)) 

1761 

1762 def process(self, resources): 

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

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

1765 for r in resources: 

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

1767 return 

1768 

1769 keys = ( 

1770 'BlockPublicPolicy', 'BlockPublicAcls', 'IgnorePublicAcls', 'RestrictPublicBuckets') 

1771 

1772 for r in resources: 

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

1774 base = {} 

1775 if S3PublicBlock.annotation_key in r: 

1776 base = r[S3PublicBlock.annotation_key] 

1777 else: 

1778 try: 

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

1780 'PublicAccessBlockConfiguration') 

1781 except client.exceptions.NoSuchPublicAccessBlockConfiguration: 

1782 base = {} 

1783 

1784 config = {} 

1785 for k in keys: 

1786 if k in self.data: 

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

1788 elif k in base: 

1789 config[k] = base[k] 

1790 

1791 client.put_public_access_block( 

1792 AccountId=r['account_id'], 

1793 PublicAccessBlockConfiguration=config) 

1794 

1795 

1796class GlueCatalogEncryptionEnabled(MultiAttrFilter): 

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

1798 

1799 :example: 

1800 

1801 .. code-block:: yaml 

1802 

1803 policies: 

1804 - name: glue-catalog-security-config 

1805 resource: aws.glue-catalog 

1806 filters: 

1807 - type: glue-security-config 

1808 SseAwsKmsKeyId: alias/aws/glue 

1809 

1810 """ 

1811 retry = staticmethod(QueryResourceManager.retry) 

1812 

1813 schema = { 

1814 'type': 'object', 

1815 'additionalProperties': False, 

1816 'properties': { 

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

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

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

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

1821 'AwsKmsKeyId': {'type': 'string'} 

1822 } 

1823 } 

1824 

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

1826 permissions = ('glue:GetDataCatalogEncryptionSettings',) 

1827 

1828 def validate(self): 

1829 attrs = set() 

1830 for key in self.data: 

1831 if key in ['CatalogEncryptionMode', 

1832 'ReturnConnectionPasswordEncrypted', 

1833 'SseAwsKmsKeyId', 

1834 'AwsKmsKeyId']: 

1835 attrs.add(key) 

1836 self.multi_attrs = attrs 

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

1838 

1839 def get_target(self, resource): 

1840 if self.annotation in resource: 

1841 return resource[self.annotation] 

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

1843 encryption_setting = resource.get('DataCatalogEncryptionSettings') 

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

1845 encryption_setting = client.get_data_catalog_encryption_settings().get( 

1846 'DataCatalogEncryptionSettings') 

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

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

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

1850 for encrypt_attr in key_attrs: 

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

1852 continue 

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

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

1855 vf = KmsRelatedFilter(vfd, self.manager) 

1856 vf.RelatedIdsExpression = 'KmsKeyId' 

1857 vf.annotate = False 

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

1859 return [] 

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

1861 return resource[self.annotation] 

1862 

1863 

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

1865class AccountCatalogEncryptionFilter(GlueCatalogEncryptionEnabled): 

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

1867 

1868 :example: 

1869 

1870 .. code-block:: yaml 

1871 

1872 policies: 

1873 - name: glue-security-config 

1874 resource: aws.account 

1875 filters: 

1876 - type: glue-security-config 

1877 SseAwsKmsKeyId: alias/aws/glue 

1878 

1879 """ 

1880 

1881 

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

1883class EMRBlockPublicAccessConfiguration(ValueFilter): 

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

1885 

1886 :example: 

1887 

1888 .. code-block:: yaml 

1889 

1890 policies: 

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

1892 resource: account 

1893 filters: 

1894 - type: emr-block-public-access 

1895 """ 

1896 

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

1898 annotate = False # no annotation from value filter 

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

1900 schema_alias = False 

1901 permissions = ("elasticmapreduce:GetBlockPublicAccessConfiguration",) 

1902 

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

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

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

1906 

1907 def augment(self, resources): 

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

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

1910 

1911 for r in resources: 

1912 try: 

1913 r[self.annotation_key] = client.get_block_public_access_configuration() 

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

1915 except client.exceptions.NoSuchPublicAccessBlockConfiguration: 

1916 r[self.annotation_key] = {} 

1917 

1918 def __call__(self, r): 

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

1920 

1921 

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

1923class PutAccountBlockPublicAccessConfiguration(BaseAction): 

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

1925 AWS account in the current region 

1926 

1927 :example: 

1928 

1929 .. code-block:: yaml 

1930 

1931 policies: 

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

1933 resource: account 

1934 filters: 

1935 - type: emr-block-public-access 

1936 key: BlockPublicAccessConfiguration.BlockPublicSecurityGroupRules 

1937 value: False 

1938 actions: 

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

1940 config: 

1941 BlockPublicSecurityGroupRules: True 

1942 PermittedPublicSecurityGroupRuleRanges: 

1943 - MinRange: 22 

1944 MaxRange: 22 

1945 - MinRange: 23 

1946 MaxRange: 23 

1947 

1948 """ 

1949 

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

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

1952 'properties': { 

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

1954 'PermittedPublicSecurityGroupRuleRanges': { 

1955 'type': 'array', 

1956 'items': { 

1957 'type': 'object', 

1958 'properties': { 

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

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

1961 }, 

1962 'required': ['MinRange'] 

1963 } 

1964 } 

1965 }, 

1966 'required': ['BlockPublicSecurityGroupRules'] 

1967 }, 

1968 required=('config',)) 

1969 

1970 permissions = ("elasticmapreduce:PutBlockPublicAccessConfiguration",) 

1971 

1972 def process(self, resources): 

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

1974 r = resources[0] 

1975 

1976 base = {} 

1977 if EMRBlockPublicAccessConfiguration.annotation_key in r: 

1978 base = r[EMRBlockPublicAccessConfiguration.annotation_key] 

1979 else: 

1980 try: 

1981 base = client.get_block_public_access_configuration() 

1982 base.pop('ResponseMetadata') 

1983 except client.exceptions.NoSuchPublicAccessBlockConfiguration: 

1984 base = {} 

1985 

1986 config = base['BlockPublicAccessConfiguration'] 

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

1988 

1989 if config == updatedConfig: 

1990 return 

1991 

1992 client.put_block_public_access_configuration( 

1993 BlockPublicAccessConfiguration=updatedConfig 

1994 ) 

1995 

1996 

1997@filters.register('securityhub') 

1998class SecHubEnabled(Filter): 

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

2000 

2001 :example: 

2002 

2003 .. code-block:: yaml 

2004 

2005 policies: 

2006 - name: check-securityhub-status 

2007 resource: aws.account 

2008 filters: 

2009 - type: securityhub 

2010 enabled: true 

2011 

2012 """ 

2013 

2014 permissions = ('securityhub:DescribeHub',) 

2015 

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

2017 

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

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

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

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

2022 'InvalidAccessException',)) 

2023 if state == bool(sechub): 

2024 return resources 

2025 return [] 

2026 

2027 

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

2029class LakeformationFilter(Filter): 

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

2031 

2032 :example: 

2033 

2034 .. code-block:: yaml 

2035 

2036 policies: 

2037 - name: lakeformation-cross-account-bucket 

2038 resource: aws.account 

2039 filters: 

2040 - type: lakeformation-s3-cross-account 

2041 

2042 """ 

2043 

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

2045 schema_alias = False 

2046 permissions = ('lakeformation:ListResources',) 

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

2048 

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

2050 results = [] 

2051 for r in resources: 

2052 if self.process_account(r): 

2053 results.append(r) 

2054 return results 

2055 

2056 def process_account(self, account): 

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

2058 lake_buckets = { 

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

2060 'ResourceInfoList[].ResourceArn', 

2061 client.list_resources()) 

2062 } 

2063 buckets = { 

2064 b['Name'] for b in 

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

2066 cross_account = lake_buckets.difference(buckets) 

2067 if not cross_account: 

2068 return False 

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

2070 return True 

2071 

2072 

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

2074class ToggleConfigManagedRule(BaseAction): 

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

2076 

2077 :example: 

2078 

2079 .. code-block:: yaml 

2080 

2081 policies: 

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

2083 description: | 

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

2085 or ACL and remediates. 

2086 comment: | 

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

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

2089 'AWS-DisableS3BucketPublicReadWrite' is triggered and remediates. 

2090 resource: account 

2091 filters: 

2092 - type: missing 

2093 policy: 

2094 resource: config-rule 

2095 filters: 

2096 - type: remediation 

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

2098 remediation: &remediation-config 

2099 TargetId: AWS-DisableS3BucketPublicReadWrite 

2100 Automatic: true 

2101 MaximumAutomaticAttempts: 5 

2102 RetryAttemptSeconds: 211 

2103 Parameters: 

2104 AutomationAssumeRole: 

2105 StaticValue: 

2106 Values: 

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

2108 S3BucketName: 

2109 ResourceValue: 

2110 Value: RESOURCE_ID 

2111 actions: 

2112 - type: toggle-config-managed-rule 

2113 rule_name: *rule_name 

2114 managed_rule_id: S3_BUCKET_PUBLIC_WRITE_PROHIBITED 

2115 resource_types: 

2116 - 'AWS::S3::Bucket' 

2117 rule_parameters: '{}' 

2118 remediation: *remediation-config 

2119 """ 

2120 

2121 permissions = ( 

2122 'config:DescribeConfigRules', 

2123 'config:DescribeRemediationConfigurations', 

2124 'config:PutRemediationConfigurations', 

2125 'config:PutConfigRule', 

2126 ) 

2127 

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

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

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

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

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

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

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

2135 resource_tag={ 

2136 'type': 'object', 

2137 'properties': { 

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

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

2140 }, 

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

2142 }, 

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

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

2145 remediation={ 

2146 'type': 'object', 

2147 'properties': { 

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

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

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

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

2152 'MaximumAutomaticAttempts': { 

2153 'type': 'integer', 

2154 'minimum': 1, 'maximum': 25, 

2155 }, 

2156 'RetryAttemptSeconds': { 

2157 'type': 'integer', 

2158 'minimum': 1, 'maximum': 2678000, 

2159 }, 

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

2161 }, 

2162 }, 

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

2164 required=['rule_name'], 

2165 ) 

2166 

2167 def validate(self): 

2168 if ( 

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

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

2171 ): 

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

2173 return self 

2174 

2175 def process(self, accounts): 

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

2177 rule = self.ConfigManagedRule(self.data) 

2178 params = self.get_rule_params(rule) 

2179 

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

2181 client.put_config_rule(**params) 

2182 

2183 if rule.remediation: 

2184 remediation_params = self.get_remediation_params(rule) 

2185 client.put_remediation_configurations( 

2186 RemediationConfigurations=[remediation_params] 

2187 ) 

2188 else: 

2189 with suppress(client.exceptions.NoSuchRemediationConfigurationException): 

2190 client.delete_remediation_configuration( 

2191 ConfigRuleName=rule.name 

2192 ) 

2193 

2194 with suppress(client.exceptions.NoSuchConfigRuleException): 

2195 client.delete_config_rule( 

2196 ConfigRuleName=rule.name 

2197 ) 

2198 

2199 def get_rule_params(self, rule): 

2200 params = dict( 

2201 ConfigRuleName=rule.name, 

2202 Description=rule.description, 

2203 Source={ 

2204 'Owner': 'AWS', 

2205 'SourceIdentifier': rule.managed_rule_id, 

2206 }, 

2207 InputParameters=rule.rule_parameters 

2208 ) 

2209 

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

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

2212 # one resource type and one resource ID 

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

2214 if rule.resource_tag: 

2215 params.update({'Scope': { 

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

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

2218 }) 

2219 elif rule.resource_id: 

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

2221 

2222 return dict(ConfigRule=params) 

2223 

2224 def get_remediation_params(self, rule): 

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

2226 if 'TargetType' not in rule.remediation: 

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

2228 return rule.remediation 

2229 

2230 class ConfigManagedRule: 

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

2232 """ 

2233 

2234 def __init__(self, data): 

2235 self.data = data 

2236 

2237 @property 

2238 def name(self): 

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

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

2241 

2242 @property 

2243 def description(self): 

2244 return self.data.get( 

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

2246 

2247 @property 

2248 def tags(self): 

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

2250 

2251 @property 

2252 def resource_types(self): 

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

2254 

2255 @property 

2256 def managed_rule_id(self): 

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

2258 

2259 @property 

2260 def resource_tag(self): 

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

2262 

2263 @property 

2264 def resource_id(self): 

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

2266 

2267 @property 

2268 def rule_parameters(self): 

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

2270 

2271 @property 

2272 def remediation(self): 

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

2274 

2275 

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

2277class SesAggStats(ValueFilter): 

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

2279 the data points into a single report. 

2280 

2281 :example: 

2282 

2283 .. code-block:: yaml 

2284 

2285 policies: 

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

2287 resource: account 

2288 filters: 

2289 - type: ses-agg-send-stats 

2290 """ 

2291 

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

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

2294 permissions = ("ses:GetSendStatistics",) 

2295 

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

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

2298 get_send_stats = client.get_send_statistics() 

2299 results = [] 

2300 

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

2302 return results 

2303 

2304 resource_counter = {'DeliveryAttempts': 0, 

2305 'Bounces': 0, 

2306 'Complaints': 0, 

2307 'Rejects': 0, 

2308 'BounceRate': 0} 

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

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

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

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

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

2314 resource_counter['BounceRate'] = round( 

2315 (resource_counter['Bounces'] / 

2316 resource_counter['DeliveryAttempts']) * 100) 

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

2318 

2319 return resources 

2320 

2321 

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

2323class SesConsecutiveStats(Filter): 

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

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

2326 

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

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

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

2330 

2331 :example: 

2332 

2333 .. code-block:: yaml 

2334 

2335 policies: 

2336 - name: ses-send-stats 

2337 resource: account 

2338 filters: 

2339 - type: ses-send-stats 

2340 days: 5 

2341 - type: value 

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

2343 op: ge 

2344 value: 10 

2345 """ 

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

2347 required=['days']) 

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

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

2350 permissions = ("ses:GetSendStatistics",) 

2351 

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

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

2354 get_send_stats = client.get_send_statistics() 

2355 results = [] 

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

2357 utcnow = datetime.datetime.utcnow() 

2358 expected_dates = set() 

2359 

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

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

2362 

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

2364 return results 

2365 

2366 metrics = {} 

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

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

2369 if ts not in expected_dates: 

2370 continue 

2371 

2372 if not metrics.get(ts): 

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

2374 'Bounces': 0, 

2375 'Complaints': 0, 

2376 'Rejects': 0} 

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

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

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

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

2381 

2382 max_bounce_rate = 0 

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

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

2385 if max_bounce_rate < metric['BounceRate']: 

2386 max_bounce_rate = metric['BounceRate'] 

2387 metric['Date'] = ts 

2388 

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

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

2391 

2392 return resources