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

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

1026 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 master 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:GetMasterAccount',) 

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_master_account().get('master') 

207 except (client.exceptions.AccessDeniedException, 

208 client.exceptions.ResourceNotFoundException): 

209 info['master'] = {} 

210 else: 

211 info['master'] = minfo 

212 account[self.annotation_key] = info 

213 

214 

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

216class CloudTrailEnabled(Filter): 

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

218 

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

220 

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

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

223 

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

225 corresponding SNS subscription for a specific filter pattern 

226 

227 :example: 

228 

229 .. code-block:: yaml 

230 

231 policies: 

232 - name: account-cloudtrail-enabled 

233 resource: account 

234 region: us-east-1 

235 filters: 

236 - type: check-cloudtrail 

237 global-events: true 

238 multi-region: true 

239 running: true 

240 include-management-events: true 

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

242 

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

244 matching a regex pattern: 

245 

246 .. code-block:: yaml 

247 

248 policies: 

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

250 resource: account 

251 region: us-east-1 

252 filters: 

253 - type: check-cloudtrail 

254 log-metric-filter-pattern: 

255 type: value 

256 op: regex 

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

258 """ 

259 schema = type_schema( 

260 'check-cloudtrail', 

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

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

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

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

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

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

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

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

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

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

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

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

273 

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

275 'cloudtrail:GetEventSelectors', 'cloudwatch:DescribeAlarmsForMetric', 

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

277 

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

279 session = local_session(self.manager.session_factory) 

280 client = session.client('cloudtrail') 

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

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

283 

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

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

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

287 current_region = session.region_name 

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

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

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

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

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

293 trails = [t for t in trails 

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

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

296 trails = [t for t in trails 

297 if t.get('LogFileValidationEnabled')] 

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

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

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

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

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

303 running = [] 

304 for t in list(trails): 

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

306 Name=t['TrailARN']) 

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

308 'LatestDeliveryError'): 

309 running.append(t) 

310 trails = running 

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

312 matched = [] 

313 for t in list(trails): 

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

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

316 for s in selectors['EventSelectors']: 

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

318 matched.append(t) 

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

320 for s in selectors['AdvancedEventSelectors']: 

321 management = False 

322 readonly = False 

323 for field_selector in s['FieldSelectors']: 

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

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

326 management = True 

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

328 readonly = True 

329 if management and not readonly: 

330 matched.append(t) 

331 

332 trails = matched 

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

334 client_logs = session.client('logs') 

335 client_cw = session.client('cloudwatch') 

336 client_sns = session.client('sns') 

337 matched = [] 

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

339 if isinstance(pattern, str): 

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

341 else: 

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

343 vf = ValueFilter(pattern) 

344 

345 for t in list(trails): 

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

347 continue 

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

349 try: 

350 metric_filters_log_group = \ 

351 client_logs.describe_metric_filters( 

352 logGroupName=log_group_name)['metricFilters'] 

353 except ClientError as e: 

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

355 continue 

356 filter_matched = None 

357 if metric_filters_log_group: 

358 for f in metric_filters_log_group: 

359 if vf(f): 

360 filter_matched = f 

361 break 

362 if not filter_matched: 

363 continue 

364 alarms = client_cw.describe_alarms_for_metric( 

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

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

367 )['MetricAlarms'] 

368 alarm_actions = [] 

369 for a in alarms: 

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

371 if not alarm_actions: 

372 continue 

373 alarm_actions = set(alarm_actions) 

374 for a in alarm_actions: 

375 try: 

376 sns_topic_attributes = client_sns.get_topic_attributes(TopicArn=a) 

377 sns_topic_attributes = sns_topic_attributes.get('Attributes') 

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

379 matched.append(t) 

380 except client_sns.exceptions.InvalidParameterValueException: 

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

382 # not be an sns topic for instance 

383 continue 

384 trails = matched 

385 if trails: 

386 return [] 

387 return resources 

388 

389 

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

391class GuardDutyEnabled(MultiAttrFilter): 

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

393 

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

395 master if any. 

396 

397 :example: 

398 

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

400 

401 .. code-block:: yaml 

402 

403 policies: 

404 - name: guardduty-enabled 

405 resource: account 

406 filters: 

407 - type: guard-duty 

408 Detector.Status: ENABLED 

409 Master.AccountId: "00011001" 

410 Master.RelationshipStatus: "Enabled" 

411 """ 

412 

413 schema = { 

414 'type': 'object', 

415 'additionalProperties': False, 

416 'properties': { 

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

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

419 'patternProperties': { 

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

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

422 } 

423 

424 annotation = "c7n:guard-duty" 

425 permissions = ( 

426 'guardduty:GetMasterAccount', 

427 'guardduty:ListDetectors', 

428 'guardduty:GetDetector') 

429 

430 def validate(self): 

431 attrs = set() 

432 for k in self.data: 

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

434 attrs.add(k) 

435 self.multi_attrs = attrs 

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

437 

438 def get_target(self, resource): 

439 if self.annotation in resource: 

440 return resource[self.annotation] 

441 

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

443 # detectors are singletons too. 

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

445 

446 if not detector_ids: 

447 return None 

448 else: 

449 detector_id = detector_ids.pop() 

450 

451 detector = client.get_detector(DetectorId=detector_id) 

452 detector.pop('ResponseMetadata', None) 

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

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

455 return r 

456 

457 

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

459class ConfigEnabled(Filter): 

460 """Is config service enabled for this account 

461 

462 :example: 

463 

464 .. code-block:: yaml 

465 

466 policies: 

467 - name: account-check-config-services 

468 resource: account 

469 region: us-east-1 

470 filters: 

471 - type: check-config 

472 all-resources: true 

473 global-resources: true 

474 running: true 

475 """ 

476 

477 schema = type_schema( 

478 'check-config', **{ 

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

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

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

482 

483 permissions = ('config:DescribeDeliveryChannels', 

484 'config:DescribeConfigurationRecorders', 

485 'config:DescribeConfigurationRecorderStatus') 

486 

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

488 client = local_session( 

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

490 channels = client.describe_delivery_channels()[ 

491 'DeliveryChannels'] 

492 recorders = client.describe_configuration_recorders()[ 

493 'ConfigurationRecorders'] 

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

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

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

497 recorders = [ 

498 r for r in recorders 

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

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

501 recorders = [r for r in recorders 

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

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

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

505 s in client.describe_configuration_recorder_status( 

506 )['ConfigurationRecordersStatus']} 

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

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

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

510 if channels and recorders: 

511 return [] 

512 return resources 

513 

514 

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

516class IAMSummary(ValueFilter): 

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

518 

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

520 

521 Example iam summary wrt to matchable fields:: 

522 

523 { 

524 "AccessKeysPerUserQuota": 2, 

525 "AccountAccessKeysPresent": 0, 

526 "AccountMFAEnabled": 1, 

527 "AccountSigningCertificatesPresent": 0, 

528 "AssumeRolePolicySizeQuota": 2048, 

529 "AttachedPoliciesPerGroupQuota": 10, 

530 "AttachedPoliciesPerRoleQuota": 10, 

531 "AttachedPoliciesPerUserQuota": 10, 

532 "GroupPolicySizeQuota": 5120, 

533 "Groups": 1, 

534 "GroupsPerUserQuota": 10, 

535 "GroupsQuota": 100, 

536 "InstanceProfiles": 0, 

537 "InstanceProfilesQuota": 100, 

538 "MFADevices": 3, 

539 "MFADevicesInUse": 2, 

540 "Policies": 3, 

541 "PoliciesQuota": 1000, 

542 "PolicySizeQuota": 5120, 

543 "PolicyVersionsInUse": 5, 

544 "PolicyVersionsInUseQuota": 10000, 

545 "Providers": 0, 

546 "RolePolicySizeQuota": 10240, 

547 "Roles": 4, 

548 "RolesQuota": 250, 

549 "ServerCertificates": 0, 

550 "ServerCertificatesQuota": 20, 

551 "SigningCertificatesPerUserQuota": 2, 

552 "UserPolicySizeQuota": 2048, 

553 "Users": 5, 

554 "UsersQuota": 5000, 

555 "VersionsPerPolicyQuota": 5, 

556 } 

557 

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

559 enabled with root mfa or has root api keys. 

560 

561 .. code-block:: yaml 

562 

563 policies: 

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

565 resource: account 

566 filters: 

567 - type: iam-summary 

568 key: AccountMFAEnabled 

569 value: true 

570 op: eq 

571 value_type: swap 

572 """ 

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

574 schema_alias = False 

575 permissions = ('iam:GetAccountSummary',) 

576 

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

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

579 client = local_session( 

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

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

582 )['SummaryMap'] 

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

584 return resources 

585 return [] 

586 

587 

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

589class AccessAnalyzer(ValueFilter): 

590 """Check for access analyzers in an account 

591 

592 :example: 

593 

594 .. code-block:: yaml 

595 

596 policies: 

597 - name: account-access-analyzer 

598 resource: account 

599 filters: 

600 - type: access-analyzer 

601 key: 'status' 

602 value: ACTIVE 

603 op: eq 

604 """ 

605 

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

607 schema_alias = False 

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

609 annotation_key = 'c7n:matched-analyzers' 

610 

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

612 account = resources[0] 

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

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

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

616 else: 

617 analyzers = account.get(self.annotation_key) 

618 

619 matched_analyzers = [] 

620 for analyzer in analyzers: 

621 if self.match(analyzer): 

622 matched_analyzers.append(analyzer) 

623 account[self.annotation_key] = matched_analyzers 

624 return matched_analyzers and resources or [] 

625 

626 

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

628class AccountPasswordPolicy(ValueFilter): 

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

630 

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

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

633 account has attempted to set a policy at all. 

634 

635 :example: 

636 

637 .. code-block:: yaml 

638 

639 policies: 

640 - name: password-policy-check 

641 resource: account 

642 region: us-east-1 

643 filters: 

644 - type: password-policy 

645 key: MinimumPasswordLength 

646 value: 10 

647 op: ge 

648 - type: password-policy 

649 key: RequireSymbols 

650 value: true 

651 """ 

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

653 schema_alias = False 

654 permissions = ('iam:GetAccountPasswordPolicy',) 

655 

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

657 account = resources[0] 

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

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

660 policy = {} 

661 try: 

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

663 policy['PasswordPolicyConfigured'] = True 

664 except ClientError as e: 

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

666 policy['PasswordPolicyConfigured'] = False 

667 else: 

668 raise 

669 account['c7n:password_policy'] = policy 

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

671 return resources 

672 return [] 

673 

674 

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

676class SetAccountPasswordPolicy(BaseAction): 

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

678 

679 This only changes the policy for the items provided. 

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

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

682 

683 :example: 

684 

685 .. code-block:: yaml 

686 

687 policies: 

688 - name: set-account-password-policy 

689 resource: account 

690 filters: 

691 - not: 

692 - type: password-policy 

693 key: MinimumPasswordLength 

694 value: 10 

695 op: ge 

696 actions: 

697 - type: set-password-policy 

698 policy: 

699 MinimumPasswordLength: 20 

700 """ 

701 schema = type_schema( 

702 'set-password-policy', 

703 policy={ 

704 'type': 'object' 

705 }) 

706 shape = 'UpdateAccountPasswordPolicyRequest' 

707 service = 'iam' 

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

709 

710 def validate(self): 

711 return shape_validate( 

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

713 self.shape, 

714 self.service) 

715 

716 def process(self, resources): 

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

718 account = resources[0] 

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

720 config = account['c7n:password_policy'] 

721 else: 

722 try: 

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

724 except client.exceptions.NoSuchEntityException: 

725 config = {} 

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

727 config.update(params) 

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

729 'PasswordPolicyConfigured')} 

730 client.update_account_password_policy(**config) 

731 

732 

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

734class ServiceLimit(Filter): 

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

736 

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

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

739 

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

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

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

743 

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

745 

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

747 

748 or via the awscli: 

749 

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

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

752 

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

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

755 backward compatibility with the old style of checks: 

756 

757 - `services` 

758 

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

760 These are case-insensitive globbing matches. 

761 

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

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

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

765 to make. 

766 

767 - `limits` 

768 

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

770 These are case-insensitive globbing matches. 

771 

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

773 

774 Check Name Service Limit Name 

775 ---------------------------------- -------------- --------------------------------- 

776 Auto Scaling Groups AutoScaling Auto Scaling groups 

777 Auto Scaling Launch Configurations AutoScaling Launch configurations 

778 CloudFormation Stacks CloudFormation Stacks 

779 ELB Application Load Balancers ELB Active Application Load Balancers 

780 ELB Classic Load Balancers ELB Active load balancers 

781 ELB Network Load Balancers ELB Active Network Load Balancers 

782 VPC VPC VPCs 

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

784 VPC Internet Gateways VPC Internet gateways 

785 

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

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

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

789 

790 :example: 

791 

792 .. code-block:: yaml 

793 

794 policies: 

795 - name: specific-account-service-limits 

796 resource: account 

797 filters: 

798 - type: service-limit 

799 names: 

800 - IAM Policies 

801 - IAM Roles 

802 - "VPC*" 

803 threshold: 1.0 

804 

805 - name: increase-account-service-limits 

806 resource: account 

807 filters: 

808 - type: service-limit 

809 services: 

810 - EC2 

811 threshold: 1.0 

812 

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

814 region: us-east-1 

815 resource: account 

816 filters: 

817 - type: service-limit 

818 services: 

819 - IAM 

820 limits: 

821 - Roles 

822 """ 

823 

824 schema = type_schema( 

825 'service-limit', 

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

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

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

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

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

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

832 'enum': ['AutoScaling', 'CloudFormation', 

833 'DynamoDB', 'EBS', 'EC2', 'ELB', 

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

835 

836 permissions = ('support:DescribeTrustedAdvisorCheckRefreshStatuses', 

837 'support:DescribeTrustedAdvisorCheckResult', 

838 'support:DescribeTrustedAdvisorChecks', 

839 'support:RefreshTrustedAdvisorCheck') 

840 deprecated_check_ids = ['eW7HH0l7J9'] 

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

842 

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

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

845 poll_interval = 5 

846 poll_max_intervals = 10 

847 global_services = {'IAM'} 

848 

849 def validate(self): 

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

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

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

853 raise PolicyValidationError( 

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

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

856 return self 

857 

858 @classmethod 

859 def get_check_result(cls, client, check_id): 

860 checks = client.describe_trusted_advisor_check_result( 

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

862 

863 # Check status and if necessary refresh checks 

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

865 try: 

866 client.refresh_trusted_advisor_check(checkId=check_id) 

867 except ClientError as e: 

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

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

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

871 return 

872 

873 for _ in range(cls.poll_max_intervals): 

874 time.sleep(cls.poll_interval) 

875 refresh_response = client.describe_trusted_advisor_check_refresh_statuses( 

876 checkIds=[check_id]) 

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

878 checks = client.describe_trusted_advisor_check_result( 

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

880 break 

881 return checks 

882 

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

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

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

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

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

888 

889 def match_patterns_to_value(self, patterns, value): 

890 for p in patterns: 

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

892 return True 

893 return False 

894 

895 def should_process(self, name): 

896 # if names specified, limit to these names 

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

898 if patterns: 

899 return self.match_patterns_to_value(patterns, name) 

900 

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

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

903 if services: 

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

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

906 

907 return True 

908 

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

910 support_region = get_support_region(self.manager) 

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

912 'support', region_name=support_region) 

913 checks = self.get_available_checks(client) 

914 exceeded = [] 

915 for check in checks: 

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

917 continue 

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

919 if matched: 

920 for m in matched: 

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

922 m['name'] = check['name'] 

923 exceeded.extend(matched) 

924 if exceeded: 

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

926 return resources 

927 return [] 

928 

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

930 region = self.manager.config.region 

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

932 

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

934 return [] 

935 

936 # trim to only results for this region 

937 results['flaggedResources'] = [ 

938 r 

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

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

941 ] 

942 

943 # save all raw limit results to the account resource 

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

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

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

947 

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

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

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

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

952 try: 

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

954 except ClientError as e: 

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

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

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

958 return 

959 

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

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

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

963 exceeded = [] 

964 

965 for resource in results['flaggedResources']: 

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

967 continue 

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

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

970 continue 

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

972 continue 

973 limit['status'] = resource['status'] 

974 limit['percentage'] = ( 

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

976 ) 

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

978 continue 

979 exceeded.append(limit) 

980 return exceeded 

981 

982 

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

984class RequestLimitIncrease(BaseAction): 

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

986 

987 :Example: 

988 

989 .. code-block:: yaml 

990 

991 policies: 

992 - name: raise-account-service-limits 

993 resource: account 

994 filters: 

995 - type: service-limit 

996 services: 

997 - EBS 

998 limits: 

999 - Provisioned IOPS (SSD) storage (GiB) 

1000 threshold: 60.5 

1001 actions: 

1002 - type: request-limit-increase 

1003 notify: [email, email2] 

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

1005 percent-increase: 50 

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

1007 """ 

1008 

1009 schema = { 

1010 'type': 'object', 

1011 'additionalProperties': False, 

1012 'properties': { 

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

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

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

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

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

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

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

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

1021 }, 

1022 'oneOf': [ 

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

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

1025 ] 

1026 } 

1027 

1028 permissions = ('support:CreateCase',) 

1029 

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

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

1032 default_severity = 'normal' 

1033 

1034 service_code_mapping = { 

1035 'AutoScaling': 'auto-scaling', 

1036 'CloudFormation': 'aws-cloudformation', 

1037 'DynamoDB': 'amazon-dynamodb', 

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

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

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

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

1042 'Kinesis': 'amazon-kinesis', 

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

1044 'Route53': 'amazon-route53', 

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

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

1047 } 

1048 

1049 def process(self, resources): 

1050 support_region = get_support_region(self.manager) 

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

1052 'support', region_name=support_region) 

1053 account_id = self.manager.config.account_id 

1054 service_map = {} 

1055 region_map = {} 

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

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

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

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

1060 

1061 for s in limit_exceeded: 

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

1063 if percent_increase: 

1064 increase_by = current_limit * float(percent_increase) / 100 

1065 increase_by = max(increase_by, minimum_increase) 

1066 else: 

1067 increase_by = amount_increase 

1068 increase_by = round(increase_by) 

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

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

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

1072 (current_limit + increase_by)) 

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

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

1075 

1076 for service in service_map: 

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

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

1079 service_code = self.service_code_mapping.get(service) 

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

1081 body = body.format(**{ 

1082 'service': service, 

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

1084 }) 

1085 client.create_case( 

1086 subject=subject, 

1087 communicationBody=body, 

1088 serviceCode=service_code, 

1089 categoryCode='general-guidance', 

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

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

1092 

1093 

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

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

1096 ct_actions = [ 

1097 { 

1098 'Action': 's3:GetBucketAcl', 

1099 'Effect': 'Allow', 

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

1101 'Resource': generate_arn( 

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

1103 'Sid': 'AWSCloudTrailAclCheck20150319', 

1104 }, 

1105 { 

1106 'Action': 's3:PutObject', 

1107 'Condition': { 

1108 'StringEquals': 

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

1110 }, 

1111 'Effect': 'Allow', 

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

1113 'Resource': generate_arn( 

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

1115 'Sid': 'AWSCloudTrailWrite20150319', 

1116 }, 

1117 ] 

1118 # parse original policy 

1119 if original is None: 

1120 policy = { 

1121 'Statement': [], 

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

1123 } 

1124 else: 

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

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

1127 for cta in ct_actions: 

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

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

1130 return json.dumps(policy) 

1131 

1132 

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

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

1135# register it. 

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

1137 

1138 

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

1140class EnableTrail(BaseAction): 

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

1142 

1143 :Example: 

1144 

1145 .. code-block:: yaml 

1146 

1147 policies: 

1148 - name: trail-test 

1149 description: Ensure CloudTrail logging is enabled 

1150 resource: account 

1151 actions: 

1152 - type: enable-cloudtrail 

1153 trail: mytrail 

1154 bucket: trails 

1155 """ 

1156 

1157 permissions = ( 

1158 'cloudtrail:CreateTrail', 

1159 'cloudtrail:DescribeTrails', 

1160 'cloudtrail:GetTrailStatus', 

1161 'cloudtrail:StartLogging', 

1162 'cloudtrail:UpdateTrail', 

1163 's3:CreateBucket', 

1164 's3:GetBucketPolicy', 

1165 's3:PutBucketPolicy', 

1166 ) 

1167 schema = type_schema( 

1168 'enable-cloudtrail', 

1169 **{ 

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

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

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

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

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

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

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

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

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

1179 'required': ('bucket',), 

1180 } 

1181 ) 

1182 

1183 def process(self, accounts): 

1184 """Create or enable CloudTrail""" 

1185 session = local_session(self.manager.session_factory) 

1186 client = session.client('cloudtrail') 

1187 bucket_name = self.data['bucket'] 

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

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

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

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

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

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

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

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

1196 

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

1198 try: 

1199 s3client.create_bucket( 

1200 Bucket=bucket_name, 

1201 CreateBucketConfiguration={'LocationConstraint': bucket_region} 

1202 ) 

1203 except ClientError as ce: 

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

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

1206 raise ce 

1207 

1208 try: 

1209 current_policy = s3client.get_bucket_policy(Bucket=bucket_name) 

1210 except ClientError: 

1211 current_policy = None 

1212 

1213 policy_json = cloudtrail_policy( 

1214 current_policy, bucket_name, 

1215 self.manager.config.account_id, bucket_region) 

1216 

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

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

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

1220 new_trail = client.create_trail( 

1221 Name=trail_name, 

1222 S3BucketName=bucket_name, 

1223 ) 

1224 if new_trail: 

1225 trails.append(new_trail) 

1226 # the loop below will configure the new trail 

1227 for trail in trails: 

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

1229 continue 

1230 # enable 

1231 arn = trail['TrailARN'] 

1232 status = client.get_trail_status(Name=arn) 

1233 if not status['IsLogging']: 

1234 client.start_logging(Name=arn) 

1235 # apply configuration changes (if any) 

1236 update_args = {} 

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

1238 update_args['IsMultiRegionTrail'] = multi_region 

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

1240 update_args['IncludeGlobalServiceEvents'] = global_events 

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

1242 update_args['SnsTopicName'] = notify 

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

1244 update_args['EnableLogFileValidation'] = file_digest 

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

1246 if not kms and 'KmsKeyId' in trail: 

1247 kms_key = '' 

1248 update_args['KmsKeyId'] = kms_key 

1249 if update_args: 

1250 update_args['Name'] = trail_name 

1251 client.update_trail(**update_args) 

1252 

1253 

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

1255class HasVirtualMFA(Filter): 

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

1257 

1258 :example: 

1259 

1260 .. code-block:: yaml 

1261 

1262 policies: 

1263 - name: account-with-virtual-mfa 

1264 resource: account 

1265 region: us-east-1 

1266 filters: 

1267 - type: has-virtual-mfa 

1268 value: true 

1269 """ 

1270 

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

1272 

1273 permissions = ('iam:ListVirtualMFADevices',) 

1274 

1275 def mfa_belongs_to_root_account(self, mfa): 

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

1277 

1278 def account_has_virtual_mfa(self, account): 

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

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

1281 paginator = client.get_paginator('list_virtual_mfa_devices') 

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

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

1284 self.mfa_belongs_to_root_account, raw_list)) 

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

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

1287 return expect_virtual_mfa == has_virtual_mfa 

1288 

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

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

1291 

1292 

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

1294class EnableDataEvents(BaseAction): 

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

1296 

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

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

1299 

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

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

1302 a trail this will do so without management events. 

1303 

1304 :example: 

1305 

1306 .. code-block:: yaml 

1307 

1308 policies: 

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

1310 resource: account 

1311 actions: 

1312 - type: enable-data-events 

1313 data-trail: 

1314 name: s3-events 

1315 multi-region: us-east-1 

1316 """ 

1317 

1318 schema = type_schema( 

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

1320 'data-trail': { 

1321 'type': 'object', 

1322 'additionalProperties': False, 

1323 'required': ['name'], 

1324 'properties': { 

1325 'create': { 

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

1327 'type': 'boolean'}, 

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

1329 'name': { 

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

1331 'type': 'string'}, 

1332 'topic': { 

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

1334 'type': 'string'}, 

1335 's3-bucket': { 

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

1337 'type': 'string'}, 

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

1339 'key-id': { 

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

1341 'type': 'string'}, 

1342 # region that we're aggregating via trails. 

1343 'multi-region': { 

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

1345 'type': 'string'}}}}) 

1346 

1347 def validate(self): 

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

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

1350 raise PolicyValidationError( 

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

1352 self.manager.data)) 

1353 return self 

1354 

1355 def get_permissions(self): 

1356 perms = [ 

1357 'cloudtrail:DescribeTrails', 

1358 'cloudtrail:GetEventSelectors', 

1359 'cloudtrail:PutEventSelectors'] 

1360 

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

1362 perms.extend([ 

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

1364 return perms 

1365 

1366 def add_data_trail(self, client, trail_cfg): 

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

1368 raise ValueError( 

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

1370 params = dict( 

1371 Name=trail_cfg['name'], 

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

1373 EnableLogFileValidation=True) 

1374 

1375 if 'key-id' in trail_cfg: 

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

1377 if 's3-prefix' in trail_cfg: 

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

1379 if 'topic' in trail_cfg: 

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

1381 if 'multi-region' in trail_cfg: 

1382 params['IsMultiRegionTrail'] = True 

1383 

1384 client.create_trail(**params) 

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

1386 

1387 def process(self, resources): 

1388 session = local_session(self.manager.session_factory) 

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

1390 

1391 if region: 

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

1393 else: 

1394 client = session.client('cloudtrail') 

1395 

1396 added = False 

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

1398 trails = client.describe_trails( 

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

1400 if not trails: 

1401 trail = self.add_data_trail(client, tconfig) 

1402 added = True 

1403 else: 

1404 trail = trails[0] 

1405 

1406 events = client.get_event_selectors( 

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

1408 

1409 for e in events: 

1410 found = False 

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

1412 continue 

1413 for data_events in e['DataResources']: 

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

1415 continue 

1416 for b in data_events['Values']: 

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

1418 found = True 

1419 break 

1420 if found: 

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

1422 return 

1423 

1424 # Opinionated choice, separate api and data events. 

1425 event_count = len(events) 

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

1427 if len(events) != event_count: 

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

1429 

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

1431 # encompasses all the buckets in the account. 

1432 

1433 events.append({ 

1434 'IncludeManagementEvents': False, 

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

1436 'DataResources': [{ 

1437 'Type': 'AWS::S3::Object', 

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

1439 client.put_event_selectors( 

1440 TrailName=trail['Name'], 

1441 EventSelectors=events) 

1442 

1443 if added: 

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

1445 

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

1447 

1448 

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

1450class ShieldEnabled(Filter): 

1451 

1452 permissions = ('shield:DescribeSubscription',) 

1453 

1454 schema = type_schema( 

1455 'shield-enabled', 

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

1457 

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

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

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

1461 try: 

1462 subscription = client.describe_subscription().get( 

1463 'Subscription', None) 

1464 except ClientError as e: 

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

1466 raise 

1467 subscription = None 

1468 

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

1470 if state and subscription: 

1471 return resources 

1472 elif not state and not subscription: 

1473 return resources 

1474 return [] 

1475 

1476 

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

1478class SetShieldAdvanced(BaseAction): 

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

1480 

1481 permissions = ( 

1482 'shield:CreateSubscription', 'shield:DeleteSubscription') 

1483 

1484 schema = type_schema( 

1485 'set-shield-advanced', 

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

1487 

1488 def process(self, resources): 

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

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

1491 

1492 if state: 

1493 client.create_subscription() 

1494 else: 

1495 try: 

1496 client.delete_subscription() 

1497 except ClientError as e: 

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

1499 return 

1500 raise 

1501 

1502 

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

1504class XrayEncrypted(Filter): 

1505 """Determine if xray is encrypted. 

1506 

1507 :example: 

1508 

1509 .. code-block:: yaml 

1510 

1511 policies: 

1512 - name: xray-encrypt-with-default 

1513 resource: aws.account 

1514 filters: 

1515 - type: xray-encrypt-key 

1516 key: default 

1517 - name: xray-encrypt-with-kms 

1518 resource: aws.account 

1519 filters: 

1520 - type: xray-encrypt-key 

1521 key: kms 

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

1523 resource: aws.account 

1524 filters: 

1525 - type: xray-encrypt-key 

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

1527 """ 

1528 

1529 permissions = ('xray:GetEncryptionConfig',) 

1530 schema = type_schema( 

1531 'xray-encrypt-key', 

1532 required=['key'], 

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

1534 ) 

1535 

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

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

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

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

1540 

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

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

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

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

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

1546 else: 

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

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

1549 return rc 

1550 

1551 

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

1553class SetXrayEncryption(BaseAction): 

1554 """Enable specific xray encryption. 

1555 

1556 :example: 

1557 

1558 .. code-block:: yaml 

1559 

1560 policies: 

1561 - name: xray-default-encrypt 

1562 resource: aws.account 

1563 actions: 

1564 - type: set-xray-encrypt 

1565 key: default 

1566 - name: xray-kms-encrypt 

1567 resource: aws.account 

1568 actions: 

1569 - type: set-xray-encrypt 

1570 key: alias/some/alias/key 

1571 """ 

1572 

1573 permissions = ('xray:PutEncryptionConfig',) 

1574 schema = type_schema( 

1575 'set-xray-encrypt', 

1576 required=['key'], 

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

1578 ) 

1579 

1580 def process(self, resources): 

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

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

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

1584 client.put_encryption_config(**req) 

1585 

1586 

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

1588class EbsEncryption(Filter): 

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

1590 

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

1592 

1593 :example: 

1594 

1595 .. code-block:: yaml 

1596 

1597 policies: 

1598 - name: check-default-ebs-encryption 

1599 resource: aws.account 

1600 filters: 

1601 - type: default-ebs-encryption 

1602 key: "alias/aws/ebs" 

1603 state: true 

1604 

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

1606 

1607 :example: 

1608 

1609 .. code-block:: yaml 

1610 

1611 policies: 

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

1613 resource: aws.account 

1614 filters: 

1615 - type: default-ebs-encryption 

1616 key: 

1617 type: value 

1618 key: Origin 

1619 value: AWS_KMS 

1620 state: true 

1621 """ 

1622 permissions = ('ec2:GetEbsEncryptionByDefault',) 

1623 schema = type_schema( 

1624 'default-ebs-encryption', 

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

1626 key={'oneOf': [ 

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

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

1629 

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

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

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

1633 account_state = client.get_ebs_encryption_by_default().get( 

1634 'EbsEncryptionByDefault') 

1635 if account_state != state: 

1636 return [] 

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

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

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

1640 vf = KmsRelatedFilter(vfd, self.manager) 

1641 vf.RelatedIdsExpression = 'KmsKeyId' 

1642 vf.annotate = False 

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

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

1645 return [] 

1646 return resources 

1647 

1648 

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

1650class SetEbsEncryption(BaseAction): 

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

1652 

1653 :example: 

1654 

1655 .. code-block:: yaml 

1656 

1657 policies: 

1658 - name: set-default-ebs-encryption 

1659 resource: aws.account 

1660 filters: 

1661 - type: default-ebs-encryption 

1662 state: false 

1663 actions: 

1664 - type: set-ebs-encryption 

1665 state: true 

1666 key: alias/aws/ebs 

1667 """ 

1668 permissions = ('ec2:EnableEbsEncryptionByDefault', 

1669 'ec2:DisableEbsEncryptionByDefault') 

1670 

1671 schema = type_schema( 

1672 'set-ebs-encryption', 

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

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

1675 

1676 def process(self, resources): 

1677 client = local_session( 

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

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

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

1681 if state: 

1682 client.enable_ebs_encryption_by_default() 

1683 else: 

1684 client.disable_ebs_encryption_by_default() 

1685 

1686 if state and key: 

1687 client.modify_ebs_default_kms_key_id( 

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

1689 

1690 

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

1692class S3PublicBlock(ValueFilter): 

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

1694 

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

1696 """ 

1697 

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

1699 annotate = False # no annotation from value filter 

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

1701 schema_alias = False 

1702 permissions = ('s3:GetAccountPublicAccessBlock',) 

1703 

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

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

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

1707 

1708 def augment(self, resources): 

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

1710 for r in resources: 

1711 try: 

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

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

1714 except client.exceptions.NoSuchPublicAccessBlockConfiguration: 

1715 r[self.annotation_key] = {} 

1716 

1717 def __call__(self, r): 

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

1719 

1720 

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

1722class SetS3PublicBlock(BaseAction): 

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

1724 

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

1726 with the extant configuration. 

1727 

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

1729 

1730 :example: 

1731 

1732 .. yaml: 

1733 

1734 policies: 

1735 - name: restrict-public-buckets 

1736 resource: aws.account 

1737 filters: 

1738 - not: 

1739 - type: s3-public-block 

1740 key: RestrictPublicBuckets 

1741 value: true 

1742 actions: 

1743 - type: set-s3-public-block 

1744 RestrictPublicBuckets: true 

1745 

1746 """ 

1747 schema = type_schema( 

1748 'set-s3-public-block', 

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

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

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

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

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

1754 

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

1756 

1757 def validate(self): 

1758 config = self.data.copy() 

1759 config.pop('type') 

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

1761 raise PolicyValidationError( 

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

1763 self.type)) 

1764 

1765 def process(self, resources): 

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

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

1768 for r in resources: 

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

1770 return 

1771 

1772 keys = ( 

1773 'BlockPublicPolicy', 'BlockPublicAcls', 'IgnorePublicAcls', 'RestrictPublicBuckets') 

1774 

1775 for r in resources: 

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

1777 base = {} 

1778 if S3PublicBlock.annotation_key in r: 

1779 base = r[S3PublicBlock.annotation_key] 

1780 else: 

1781 try: 

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

1783 'PublicAccessBlockConfiguration') 

1784 except client.exceptions.NoSuchPublicAccessBlockConfiguration: 

1785 base = {} 

1786 

1787 config = {} 

1788 for k in keys: 

1789 if k in self.data: 

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

1791 elif k in base: 

1792 config[k] = base[k] 

1793 

1794 client.put_public_access_block( 

1795 AccountId=r['account_id'], 

1796 PublicAccessBlockConfiguration=config) 

1797 

1798 

1799class GlueCatalogEncryptionEnabled(MultiAttrFilter): 

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

1801 

1802 :example: 

1803 

1804 .. code-block:: yaml 

1805 

1806 policies: 

1807 - name: glue-catalog-security-config 

1808 resource: aws.glue-catalog 

1809 filters: 

1810 - type: glue-security-config 

1811 SseAwsKmsKeyId: alias/aws/glue 

1812 

1813 """ 

1814 retry = staticmethod(QueryResourceManager.retry) 

1815 

1816 schema = { 

1817 'type': 'object', 

1818 'additionalProperties': False, 

1819 'properties': { 

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

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

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

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

1824 'AwsKmsKeyId': {'type': 'string'} 

1825 } 

1826 } 

1827 

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

1829 permissions = ('glue:GetDataCatalogEncryptionSettings',) 

1830 

1831 def validate(self): 

1832 attrs = set() 

1833 for key in self.data: 

1834 if key in ['CatalogEncryptionMode', 

1835 'ReturnConnectionPasswordEncrypted', 

1836 'SseAwsKmsKeyId', 

1837 'AwsKmsKeyId']: 

1838 attrs.add(key) 

1839 self.multi_attrs = attrs 

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

1841 

1842 def get_target(self, resource): 

1843 if self.annotation in resource: 

1844 return resource[self.annotation] 

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

1846 encryption_setting = resource.get('DataCatalogEncryptionSettings') 

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

1848 encryption_setting = client.get_data_catalog_encryption_settings().get( 

1849 'DataCatalogEncryptionSettings') 

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

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

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

1853 for encrypt_attr in key_attrs: 

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

1855 continue 

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

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

1858 vf = KmsRelatedFilter(vfd, self.manager) 

1859 vf.RelatedIdsExpression = 'KmsKeyId' 

1860 vf.annotate = False 

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

1862 return [] 

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

1864 return resource[self.annotation] 

1865 

1866 

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

1868class AccountCatalogEncryptionFilter(GlueCatalogEncryptionEnabled): 

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

1870 

1871 :example: 

1872 

1873 .. code-block:: yaml 

1874 

1875 policies: 

1876 - name: glue-security-config 

1877 resource: aws.account 

1878 filters: 

1879 - type: glue-security-config 

1880 SseAwsKmsKeyId: alias/aws/glue 

1881 

1882 """ 

1883 

1884 

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

1886class EMRBlockPublicAccessConfiguration(ValueFilter): 

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

1888 

1889 :example: 

1890 

1891 .. code-block:: yaml 

1892 

1893 policies: 

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

1895 resource: account 

1896 filters: 

1897 - type: emr-block-public-access 

1898 """ 

1899 

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

1901 annotate = False # no annotation from value filter 

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

1903 schema_alias = False 

1904 permissions = ("elasticmapreduce:GetBlockPublicAccessConfiguration",) 

1905 

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

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

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

1909 

1910 def augment(self, resources): 

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

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

1913 

1914 for r in resources: 

1915 try: 

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

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

1918 except client.exceptions.NoSuchPublicAccessBlockConfiguration: 

1919 r[self.annotation_key] = {} 

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()