1# Copyright The Cloud Custodian Authors.
2# SPDX-License-Identifier: Apache-2.0
3from collections import OrderedDict
4import csv
5import datetime
6import functools
7import json
8import io
9from datetime import timedelta
10import itertools
11import time
12
13# Used to parse saml provider metadata configuration.
14from xml.etree import ElementTree # nosec nosemgrep
15
16from concurrent.futures import as_completed
17from dateutil.tz import tzutc
18from dateutil.parser import parse as parse_date
19
20from botocore.exceptions import ClientError
21
22from c7n import deprecated
23from c7n.actions import BaseAction
24from c7n.exceptions import PolicyValidationError
25from c7n.filters import ValueFilter, Filter
26from c7n.filters.multiattr import MultiAttrFilter
27from c7n.filters.iamaccess import CrossAccountAccessFilter
28from c7n.manager import resources
29from c7n.query import ConfigSource, QueryResourceManager, DescribeSource, TypeInfo
30from c7n.resolver import ValuesFrom
31from c7n.tags import TagActionFilter, TagDelayedAction, Tag, RemoveTag, universal_augment
32from c7n.utils import (
33 get_partition, local_session, type_schema, chunks, filter_empty, QueryParser,
34 select_keys
35)
36
37from c7n.resources.aws import Arn
38from c7n.resources.securityhub import OtherResourcePostFinding
39
40
41class DescribeGroup(DescribeSource):
42
43 def get_resources(self, resource_ids, cache=True):
44 """For IAM Groups on events, resource ids are Group Names."""
45 client = local_session(self.manager.session_factory).client('iam')
46 resources = []
47 for rid in resource_ids:
48 try:
49 result = self.manager.retry(client.get_group, GroupName=rid)
50 except client.exceptions.NoSuchEntityException:
51 continue
52 group = result.pop('Group')
53 group['c7n:Users'] = result['Users']
54 resources.append(group)
55 return resources
56
57
58@resources.register('iam-group')
59class Group(QueryResourceManager):
60
61 class resource_type(TypeInfo):
62 service = 'iam'
63 arn_type = 'group'
64 enum_spec = ('list_groups', 'Groups', None)
65 id = name = 'GroupName'
66 date = 'CreateDate'
67 cfn_type = config_type = "AWS::IAM::Group"
68 # Denotes this resource type exists across regions
69 global_resource = True
70 arn = 'Arn'
71
72 source_mapping = {
73 'describe': DescribeGroup,
74 'config': ConfigSource
75 }
76
77
78class DescribeRole(DescribeSource):
79
80 def get_resources(self, resource_ids, cache=True):
81 client = local_session(self.manager.session_factory).client('iam')
82 resources = []
83 for rid in resource_ids:
84 if rid.startswith('arn'):
85 rid = Arn.parse(rid).resource
86 try:
87 result = self.manager.retry(client.get_role, RoleName=rid)
88 except client.exceptions.NoSuchEntityException:
89 continue
90 resources.append(result.pop('Role'))
91 return resources
92
93
94@resources.register('iam-role')
95class Role(QueryResourceManager):
96
97 class resource_type(TypeInfo):
98 service = 'iam'
99 arn_type = 'role'
100 enum_spec = ('list_roles', 'Roles', None)
101 detail_spec = ('get_role', 'RoleName', 'RoleName', 'Role')
102 id = name = 'RoleName'
103 config_id = 'RoleId'
104 date = 'CreateDate'
105 cfn_type = config_type = "AWS::IAM::Role"
106 # Denotes this resource type exists across regions
107 global_resource = True
108 arn = 'Arn'
109
110 source_mapping = {
111 'describe': DescribeRole,
112 'config': ConfigSource
113 }
114
115
116Role.action_registry.register('mark-for-op', TagDelayedAction)
117Role.filter_registry.register('marked-for-op', TagActionFilter)
118
119
120@Role.action_registry.register('post-finding')
121class RolePostFinding(OtherResourcePostFinding):
122
123 resource_type = 'AwsIamRole'
124
125 def format_resource(self, r):
126 envelope, payload = self.format_envelope(r)
127 payload.update(self.filter_empty(
128 select_keys(r, ['AssumeRolePolicyDocument', 'CreateDate',
129 'MaxSessionDuration', 'Path', 'RoleId',
130 'RoleName'])))
131 payload['AssumeRolePolicyDocument'] = json.dumps(
132 payload['AssumeRolePolicyDocument'])
133 payload['CreateDate'] = payload['CreateDate'].isoformat()
134 return envelope
135
136
137@Role.action_registry.register('tag')
138class RoleTag(Tag):
139 """Tag an iam role."""
140
141 permissions = ('iam:TagRole',)
142
143 def process_resource_set(self, client, roles, tags):
144 for role in roles:
145 try:
146 self.manager.retry(
147 client.tag_role, RoleName=role['RoleName'], Tags=tags)
148 except client.exceptions.NoSuchEntityException:
149 continue
150
151
152@Role.action_registry.register('remove-tag')
153class RoleRemoveTag(RemoveTag):
154 """Remove tags from an iam role."""
155
156 permissions = ('iam:UntagRole',)
157
158 def process_resource_set(self, client, roles, tags):
159 for role in roles:
160 try:
161 self.manager.retry(
162 client.untag_role, RoleName=role['RoleName'], TagKeys=tags)
163 except client.exceptions.NoSuchEntityException:
164 continue
165
166
167class SetBoundary(BaseAction):
168 """Set IAM Permission boundary on an IAM Role or User.
169
170 A role or user can only have a single permission boundary set.
171 """
172
173 schema = type_schema(
174 'set-boundary',
175 state={'enum': ['present', 'absent']},
176 policy={'type': 'string'})
177
178 def validate(self):
179 state = self.data.get('state', 'present') == 'present'
180 if state and not self.data.get('policy'):
181 raise PolicyValidationError("set-boundary requires policy arn")
182
183 def process(self, resources):
184 state = self.data.get('state', 'present') == 'present'
185 client = self.manager.session_factory().client('iam')
186 policy = self.data.get('policy')
187 if policy and not policy.startswith('arn'):
188 policy = 'arn:{}:iam::{}:policy/{}'.format(
189 get_partition(self.manager.config.region),
190 self.manager.account_id, policy)
191 for r in resources:
192 method, params = self.get_method(client, state, policy, r)
193 try:
194 self.manager.retry(method, **params)
195 except client.exceptions.NoSuchEntityException:
196 continue
197
198 def get_method(self, client, state, policy, resource):
199 raise NotImplementedError()
200
201
202@Role.action_registry.register('set-boundary')
203class RoleSetBoundary(SetBoundary):
204
205 def get_permissions(self):
206 if self.data.get('state', True):
207 return ('iam:PutRolePermissionsBoundary',)
208 return ('iam:DeleteRolePermissionsBoundary',)
209
210 def get_method(self, client, state, policy, resource):
211 if state:
212 return client.put_role_permissions_boundary, {
213 'RoleName': resource['RoleName'],
214 'PermissionsBoundary': policy}
215 else:
216 return client.delete_role_permissions_boundary, {
217 'RoleName': resource['RoleName']}
218
219
220class DescribeUser(DescribeSource):
221
222 def augment(self, resources):
223 # iam has a race condition, where listing will potentially return a
224 # new user prior it to its availability to get user
225 client = local_session(self.manager.session_factory).client('iam')
226 results = []
227 for r in resources:
228 ru = self.manager.retry(
229 client.get_user, UserName=r['UserName'],
230 ignore_err_codes=client.exceptions.NoSuchEntityException)
231 if ru:
232 results.append(ru['User'])
233 return list(filter(None, results))
234
235 def get_resources(self, resource_ids, cache=True):
236 client = local_session(self.manager.session_factory).client('iam')
237 results = []
238
239 for r in resource_ids:
240 try:
241 results.append(client.get_user(UserName=r)['User'])
242 except client.exceptions.NoSuchEntityException:
243 continue
244 return results
245
246
247@resources.register('iam-user')
248class User(QueryResourceManager):
249
250 class resource_type(TypeInfo):
251 service = 'iam'
252 arn_type = 'user'
253 detail_spec = ('get_user', 'UserName', 'UserName', 'User')
254 enum_spec = ('list_users', 'Users', None)
255 id = name = 'UserName'
256 date = 'CreateDate'
257 cfn_type = config_type = "AWS::IAM::User"
258 # Denotes this resource type exists across regions
259 global_resource = True
260 arn = 'Arn'
261 config_id = 'UserId'
262
263 source_mapping = {
264 'describe': DescribeUser,
265 'config': ConfigSource
266 }
267
268
269@User.action_registry.register('tag')
270class UserTag(Tag):
271 """Tag an iam user."""
272
273 permissions = ('iam:TagUser',)
274
275 def process_resource_set(self, client, users, tags):
276 for u in users:
277 try:
278 self.manager.retry(
279 client.tag_user, UserName=u['UserName'], Tags=tags)
280 except client.exceptions.NoSuchEntityException:
281 continue
282
283
284@User.action_registry.register('remove-tag')
285class UserRemoveTag(RemoveTag):
286 """Remove tags from an iam user."""
287
288 permissions = ('iam:UntagUser',)
289
290 def process_resource_set(self, client, users, tags):
291 for u in users:
292 try:
293 self.manager.retry(
294 client.untag_user, UserName=u['UserName'], TagKeys=tags)
295 except client.exceptions.NoSuchEntityException:
296 continue
297
298
299User.action_registry.register('mark-for-op', TagDelayedAction)
300User.filter_registry.register('marked-for-op', TagActionFilter)
301
302
303Role.action_registry.register('mark-for-op', TagDelayedAction)
304Role.filter_registry.register('marked-for-op', TagActionFilter)
305
306
307@User.action_registry.register('set-groups')
308class SetGroups(BaseAction):
309 """Set a specific IAM user as added/removed from a group
310
311 :example:
312
313 .. code-block:: yaml
314
315 - name: iam-user-add-remove
316 resource: iam-user
317 filters:
318 - type: value
319 key: UserName
320 value: Bob
321 actions:
322 - type: set-groups
323 state: remove
324 group: Admin
325
326 """
327 schema = type_schema(
328 'set-groups',
329 state={'enum': ['add', 'remove']},
330 group={'type': 'string'},
331 required=['state', 'group']
332 )
333
334 permissions = ('iam:AddUserToGroup', 'iam:RemoveUserFromGroup',)
335
336 def validate(self):
337 if self.data.get('group') == '':
338 raise PolicyValidationError('group cannot be empty on %s'
339 % (self.manager.data))
340
341 def process(self, resources):
342 group_name = self.data['group']
343 state = self.data['state']
344 client = local_session(self.manager.session_factory).client('iam')
345 op_map = {
346 'add': client.add_user_to_group,
347 'remove': client.remove_user_from_group
348 }
349 for r in resources:
350 try:
351 op_map[state](GroupName=group_name, UserName=r['UserName'])
352 except client.exceptions.NoSuchEntityException:
353 continue
354
355
356@User.action_registry.register('set-boundary')
357class UserSetBoundary(SetBoundary):
358
359 def get_permissions(self):
360 if self.data.get('state', True):
361 return ('iam:PutUserPermissionsBoundary',)
362 return ('iam:DeleteUserPermissionsBoundary',)
363
364 def get_method(self, client, state, policy, resource):
365 if state:
366 return client.put_user_permissions_boundary, {
367 'UserName': resource['UserName'],
368 'PermissionsBoundary': policy}
369 else:
370 return client.delete_user_permissions_boundary, {
371 'UserName': resource['UserName']}
372
373
374class DescribePolicy(DescribeSource):
375
376 def resources(self, query=None):
377 qfilters = PolicyQueryParser.parse(self.manager.data.get('query', []))
378 query = query or {}
379 if qfilters:
380 query = {t['Name']: t['Value'] for t in qfilters}
381 return super(DescribePolicy, self).resources(query=query)
382
383 def get_resources(self, resource_ids, cache=True):
384 client = local_session(self.manager.session_factory).client('iam')
385 results = []
386
387 for r in resource_ids:
388 try:
389 results.append(client.get_policy(PolicyArn=r)['Policy'])
390 except ClientError as e:
391 if e.response['Error']['Code'] == 'NoSuchEntityException':
392 continue
393 return results
394
395 def augment(self, resources):
396 return universal_augment(self.manager, super().augment(resources))
397
398
399@resources.register('iam-policy')
400class Policy(QueryResourceManager):
401
402 class resource_type(TypeInfo):
403 service = 'iam'
404 arn_type = 'policy'
405 enum_spec = ('list_policies', 'Policies', {'Scope': 'Local'})
406 id = 'PolicyId'
407 name = 'PolicyName'
408 date = 'CreateDate'
409 cfn_type = config_type = "AWS::IAM::Policy"
410 # Denotes this resource type exists across regions
411 global_resource = True
412 arn = 'Arn'
413 universal_taggable = object()
414
415 source_mapping = {
416 'describe': DescribePolicy,
417 'config': ConfigSource
418 }
419
420
421class PolicyQueryParser(QueryParser):
422
423 QuerySchema = {
424 'Scope': ('All', 'AWS', 'Local'),
425 'PolicyUsageFilter': ('PermissionsPolicy', 'PermissionsBoundary'),
426 'PathPrefix': str,
427 'OnlyAttached': bool
428 }
429 multi_value = False
430 value_key = 'Value'
431
432
433@resources.register('iam-profile')
434class InstanceProfile(QueryResourceManager):
435
436 class resource_type(TypeInfo):
437 service = 'iam'
438 arn_type = 'instance-profile'
439 enum_spec = ('list_instance_profiles', 'InstanceProfiles', None)
440 name = id = 'InstanceProfileName'
441 date = 'CreateDate'
442 # Denotes this resource type exists across regions
443 global_resource = True
444 arn = 'Arn'
445 cfn_type = 'AWS::IAM::InstanceProfile'
446
447
448@resources.register('iam-certificate')
449class ServerCertificate(QueryResourceManager):
450
451 class resource_type(TypeInfo):
452 service = 'iam'
453 arn_type = 'server-certificate'
454 enum_spec = ('list_server_certificates',
455 'ServerCertificateMetadataList',
456 None)
457 name = id = 'ServerCertificateName'
458 config_type = "AWS::IAM::ServerCertificate"
459 name = 'ServerCertificateName'
460 date = 'Expiration'
461 # Denotes this resource type exists across regions
462 global_resource = True
463
464
465@ServerCertificate.action_registry.register('delete')
466class CertificateDelete(BaseAction):
467 """Delete an IAM Certificate
468
469 For example, if you want to automatically delete an unused IAM certificate.
470
471 :example:
472
473 .. code-block:: yaml
474
475 - name: aws-iam-certificate-delete-expired
476 resource: iam-certificate
477 filters:
478 - type: value
479 key: Expiration
480 value_type: expiration
481 op: greater-than
482 value: 0
483 actions:
484 - type: delete
485
486 """
487 schema = type_schema('delete')
488 permissions = ('iam:DeleteServerCertificate',)
489
490 def process(self, resources):
491 client = local_session(self.manager.session_factory).client('iam')
492 for cert in resources:
493 self.manager.retry(
494 client.delete_server_certificate,
495 ServerCertificateName=cert['ServerCertificateName'],
496 ignore_err_codes=(
497 'NoSuchEntityException',
498 'DeleteConflictException',
499 ),
500 )
501
502
503@User.filter_registry.register('usage')
504@Role.filter_registry.register('usage')
505@Group.filter_registry.register('usage')
506@Policy.filter_registry.register('usage')
507class ServiceUsage(Filter):
508 """Filter iam resources by their api/service usage.
509
510 Note recent activity (last 4hrs) may not be shown, evaluation
511 is against the last 365 days of data.
512
513 Each service access record is evaluated against all specified
514 attributes. Attribute filters can be specified in short form k:v
515 pairs or in long form as a value type filter.
516
517 match-operator allows to specify how a resource is treated across
518 service access record matches. 'any' means a single matching
519 service record will return the policy resource as matching. 'all'
520 means all service access records have to match.
521
522
523 Find iam users that have not used any services in the last year
524
525 :example:
526
527 .. code-block:: yaml
528
529 - name: usage-unused-users
530 resource: iam-user
531 filters:
532 - type: usage
533 match-operator: all
534 LastAuthenticated: null
535
536 Find iam users that have used dynamodb in last 30 days
537
538 :example:
539
540 .. code-block:: yaml
541
542 - name: unused-users
543 resource: iam-user
544 filters:
545 - type: usage
546 ServiceNamespace: dynamodb
547 TotalAuthenticatedEntities: 1
548 LastAuthenticated:
549 type: value
550 value_type: age
551 op: less-than
552 value: 30
553 match-operator: any
554
555 https://aws.amazon.com/blogs/security/automate-analyzing-permissions-using-iam-access-advisor/
556
557 """
558
559 JOB_COMPLETE = 'COMPLETED'
560 SERVICE_ATTR = {
561 'ServiceName', 'ServiceNamespace', 'TotalAuthenticatedEntities',
562 'LastAuthenticated', 'LastAuthenticatedEntity'}
563
564 schema_alias = True
565 schema_attr = {
566 sa: {'oneOf': [
567 {'type': 'string'},
568 {'type': 'boolean'},
569 {'type': 'number'},
570 {'type': 'null'},
571 {'$ref': '#/definitions/filters/value'}]}
572 for sa in sorted(SERVICE_ATTR)}
573 schema_attr['match-operator'] = {'enum': ['all', 'any']}
574 schema_attr['poll-delay'] = {'type': 'number'}
575 schema = type_schema(
576 'usage',
577 required=('match-operator',),
578 **schema_attr)
579 permissions = ('iam:GenerateServiceLastAccessedDetails',
580 'iam:GetServiceLastAccessedDetails')
581
582 def process(self, resources, event=None):
583 client = local_session(self.manager.session_factory).client('iam')
584
585 job_resource_map = {}
586 for arn, r in zip(self.manager.get_arns(resources), resources):
587 try:
588 jid = self.manager.retry(
589 client.generate_service_last_accessed_details,
590 Arn=arn)['JobId']
591 job_resource_map[jid] = r
592 except client.exceptions.NoSuchEntityException:
593 continue
594
595 conf = dict(self.data)
596 conf.pop('match-operator')
597 saf = MultiAttrFilter(conf)
598 saf.multi_attrs = self.SERVICE_ATTR
599
600 results = []
601 match_operator = self.data.get('match-operator', 'all')
602
603 while job_resource_map:
604 job_results_map = {}
605 for jid, r in job_resource_map.items():
606 result = self.manager.retry(
607 client.get_service_last_accessed_details, JobId=jid)
608 if result['JobStatus'] != self.JOB_COMPLETE:
609 continue
610 job_results_map[jid] = result['ServicesLastAccessed']
611
612 for jid, saf_results in job_results_map.items():
613 r = job_resource_map.pop(jid)
614 saf_matches = saf.process(saf_results)
615 if match_operator == 'all' and len(saf_matches) == len(saf_results):
616 results.append(r)
617 elif match_operator != 'all' and saf_matches:
618 results.append(r)
619
620 time.sleep(self.data.get('poll-delay', 2))
621
622 return results
623
624
625@User.filter_registry.register('check-permissions')
626@Group.filter_registry.register('check-permissions')
627@Role.filter_registry.register('check-permissions')
628@Policy.filter_registry.register('check-permissions')
629class CheckPermissions(Filter):
630 """Check IAM permissions associated with a resource.
631
632 :example:
633
634 Find users that can create other users
635
636 .. code-block:: yaml
637
638 policies:
639 - name: super-users
640 resource: aws.iam-user
641 filters:
642 - type: check-permissions
643 match: allowed
644 actions:
645 - iam:CreateUser
646
647 :example:
648
649 Find users with access to all services and actions
650
651 .. code-block:: yaml
652
653 policies:
654 - name: admin-users
655 resource: aws.iam-user
656 filters:
657 - type: check-permissions
658 match: allowed
659 actions:
660 - '*:*'
661
662 By default permission boundaries are checked.
663 """
664
665 schema = type_schema(
666 'check-permissions', **{
667 'match': {'oneOf': [
668 {'enum': ['allowed', 'denied']},
669 {'$ref': '#/definitions/filters/valuekv'},
670 {'$ref': '#/definitions/filters/value'}]},
671 'boundaries': {'type': 'boolean'},
672 'match-operator': {'enum': ['and', 'or']},
673 'actions': {'type': 'array', 'items': {'type': 'string'}},
674 'required': ('actions', 'match')})
675 schema_alias = True
676 policy_annotation = 'c7n:policy'
677 eval_annotation = 'c7n:perm-matches'
678
679 def validate(self):
680 # This filter relies on IAM policy simulator APIs. From the docs concerning action names:
681 #
682 # "Each operation must include the service identifier, such as iam:CreateUser. This
683 # operation does not support using wildcards (*) in an action name."
684 #
685 # We can catch invalid actions during policy validation, rather than waiting to hit
686 # runtime exceptions.
687 for action in self.data['actions']:
688 if ':' not in action[1:-1]:
689 raise PolicyValidationError(
690 "invalid check-permissions action: '%s' must be in the form <service>:<action>"
691 % (action,))
692 return self
693
694 def get_permissions(self):
695 if self.manager.type == 'iam-policy':
696 return ('iam:SimulateCustomPolicy', 'iam:GetPolicyVersion')
697 perms = ('iam:SimulatePrincipalPolicy', 'iam:GetPolicy', 'iam:GetPolicyVersion')
698 if self.manager.type not in ('iam-user', 'iam-role',):
699 # for simulating w/ permission boundaries
700 perms += ('iam:GetRole',)
701 return perms
702
703 def process(self, resources, event=None):
704 client = local_session(self.manager.session_factory).client('iam')
705 actions = self.data['actions']
706 matcher = self.get_eval_matcher()
707 operator = self.data.get('match-operator', 'and') == 'and' and all or any
708 arn_resources = list(zip(self.get_iam_arns(resources), resources))
709
710 # To ignore permission boundaries, override with an allow-all policy
711 self.simulation_boundary_override = '''
712 {
713 "Version": "2012-10-17",
714 "Statement": [{
715 "Effect": "Allow",
716 "Action": "*",
717 "Resource": "*"
718 }]
719 }
720 ''' if not self.data.get('boundaries', True) else None
721 results = []
722 eval_cache = {}
723 for arn, r in arn_resources:
724 if arn is None:
725 continue
726 if arn in eval_cache:
727 evaluations = eval_cache[arn]
728 else:
729 evaluations = self.get_evaluations(client, arn, r, actions)
730 eval_cache[arn] = evaluations
731 if not evaluations:
732 continue
733 matches = []
734 matched = []
735 for e in evaluations:
736 match = matcher(e)
737 if match:
738 matched.append(e)
739 matches.append(match)
740 if operator(matches):
741 r[self.eval_annotation] = matched
742 results.append(r)
743 return results
744
745 def get_iam_arns(self, resources):
746 return self.manager.get_arns(resources)
747
748 def get_evaluations(self, client, arn, r, actions):
749 if self.manager.type == 'iam-policy':
750 policy = r.get(self.policy_annotation)
751 if policy is None:
752 r['c7n:policy'] = policy = client.get_policy_version(
753 PolicyArn=r['Arn'],
754 VersionId=r['DefaultVersionId']).get('PolicyVersion', {})
755 evaluations = self.manager.retry(
756 client.simulate_custom_policy,
757 PolicyInputList=[json.dumps(policy['Document'])],
758 ActionNames=actions).get('EvaluationResults', ())
759 return evaluations
760
761 params = dict(
762 PolicySourceArn=arn,
763 ActionNames=actions,
764 ignore_err_codes=('NoSuchEntity',))
765
766 # simulate_principal_policy() respects permission boundaries by default. To opt out of
767 # considering boundaries for this filter, we can provide an allow-all policy
768 # as the boundary.
769 #
770 # Note: Attempting to use an empty list for the boundary seems like a reasonable
771 # impulse too, but that seems to cause the simulator to fall back to its default
772 # of using existing boundaries.
773 if self.simulation_boundary_override:
774 params['PermissionsBoundaryPolicyInputList'] = [self.simulation_boundary_override]
775
776 evaluations = (self.manager.retry(
777 client.simulate_principal_policy,
778 **params) or {}).get('EvaluationResults', ())
779 return evaluations
780
781 def get_eval_matcher(self):
782 if isinstance(self.data['match'], str):
783 if self.data['match'] == 'denied':
784 values = ['explicitDeny', 'implicitDeny']
785 else:
786 values = ['allowed']
787 vf = ValueFilter({'type': 'value', 'key':
788 'EvalDecision', 'value': values,
789 'op': 'in'})
790 else:
791 vf = ValueFilter(self.data['match'])
792 vf.annotate = False
793 return vf
794
795
796class IamRoleUsage(Filter):
797
798 def get_permissions(self):
799 perms = list(itertools.chain(*[
800 self.manager.get_resource_manager(m).get_permissions()
801 for m in ['lambda', 'launch-config', 'ec2']]))
802 perms.extend(['ecs:DescribeClusters', 'ecs:DescribeServices'])
803 return perms
804
805 def service_role_usage(self):
806 results = set()
807 results.update(self.scan_lambda_roles())
808 results.update(self.scan_ecs_roles())
809 results.update(self.collect_profile_roles())
810 return results
811
812 def instance_profile_usage(self):
813 results = set()
814 results.update(self.scan_asg_roles())
815 results.update(self.scan_ec2_roles())
816 return results
817
818 def scan_lambda_roles(self):
819 manager = self.manager.get_resource_manager('lambda')
820 return [r['Role'] for r in manager.resources() if 'Role' in r]
821
822 def scan_ecs_roles(self):
823 results = []
824 client = local_session(self.manager.session_factory).client('ecs')
825 for cluster in client.describe_clusters()['clusters']:
826 services = client.list_services(
827 cluster=cluster['clusterName'])['serviceArns']
828 if services:
829 for service in client.describe_services(
830 cluster=cluster['clusterName'],
831 services=services)['services']:
832 if 'roleArn' in service:
833 results.append(service['roleArn'])
834 return results
835
836 def collect_profile_roles(self):
837 # Collect iam roles attached to instance profiles of EC2/ASG resources
838 profiles = set()
839 profiles.update(self.scan_asg_roles())
840 profiles.update(self.scan_ec2_roles())
841
842 manager = self.manager.get_resource_manager('iam-profile')
843 iprofiles = manager.resources()
844 results = []
845 for p in iprofiles:
846 if p['InstanceProfileName'] not in profiles:
847 continue
848 for role in p.get('Roles', []):
849 results.append(role['RoleName'])
850 return results
851
852 def scan_asg_roles(self):
853 manager = self.manager.get_resource_manager('launch-config')
854 return [r['IamInstanceProfile'] for r in manager.resources() if (
855 'IamInstanceProfile' in r)]
856
857 def scan_ec2_roles(self):
858 manager = self.manager.get_resource_manager('ec2')
859 results = []
860 for e in manager.resources():
861 # do not include instances that have been recently terminated
862 if e['State']['Name'] == 'terminated':
863 continue
864 profile_arn = e.get('IamInstanceProfile', {}).get('Arn', None)
865 if not profile_arn:
866 continue
867 # split arn to get the profile name
868 results.append(profile_arn.split('/')[-1])
869 return results
870
871
872###################
873# IAM Roles #
874###################
875
876@Role.filter_registry.register('used')
877class UsedIamRole(IamRoleUsage):
878 """Filter IAM roles that are either being used or not
879
880 Checks for usage on EC2, Lambda, ECS only
881
882 :example:
883
884 .. code-block:: yaml
885
886 policies:
887 - name: iam-role-in-use
888 resource: iam-role
889 filters:
890 - type: used
891 state: true
892 """
893
894 schema = type_schema(
895 'used',
896 state={'type': 'boolean'})
897
898 def process(self, resources, event=None):
899 roles = self.service_role_usage()
900 if self.data.get('state', True):
901 return [r for r in resources if (
902 r['Arn'] in roles or r['RoleName'] in roles)]
903
904 return [r for r in resources if (
905 r['Arn'] not in roles and r['RoleName'] not in roles)]
906
907
908@Role.filter_registry.register('unused')
909class UnusedIamRole(IamRoleUsage):
910 """Filter IAM roles that are either being used or not
911
912 This filter has been deprecated. Please use the 'used' filter
913 with the 'state' attribute to get unused iam roles
914
915 Checks for usage on EC2, Lambda, ECS only
916
917 :example:
918
919 .. code-block:: yaml
920
921 policies:
922 - name: iam-roles-not-in-use
923 resource: iam-role
924 filters:
925 - type: used
926 state: false
927 """
928 deprecations = (
929 deprecated.filter("use the 'used' filter with 'state' attribute"),
930 )
931
932 schema = type_schema('unused')
933
934 def process(self, resources, event=None):
935 return UsedIamRole({'state': False}, self.manager).process(resources)
936
937
938@Role.filter_registry.register('cross-account')
939class RoleCrossAccountAccess(CrossAccountAccessFilter):
940
941 policy_attribute = 'AssumeRolePolicyDocument'
942 permissions = ('iam:ListRoles',)
943
944 schema = type_schema(
945 'cross-account',
946 # white list accounts
947 whitelist_from=ValuesFrom.schema,
948 whitelist={'type': 'array', 'items': {'type': 'string'}})
949
950
951@Role.filter_registry.register('has-inline-policy')
952class IamRoleInlinePolicy(Filter):
953 """Filter IAM roles that have an inline-policy attached
954 True: Filter roles that have an inline-policy
955 False: Filter roles that do not have an inline-policy
956
957 :example:
958
959 .. code-block:: yaml
960
961 policies:
962 - name: iam-roles-with-inline-policies
963 resource: iam-role
964 filters:
965 - type: has-inline-policy
966 value: True
967 """
968
969 schema = type_schema('has-inline-policy', value={'type': 'boolean'})
970 permissions = ('iam:ListRolePolicies',)
971
972 def _inline_policies(self, client, resource):
973 policies = client.list_role_policies(
974 RoleName=resource['RoleName'])['PolicyNames']
975 resource['c7n:InlinePolicies'] = policies
976 return resource
977
978 def process(self, resources, event=None):
979 c = local_session(self.manager.session_factory).client('iam')
980 res = []
981 value = self.data.get('value', True)
982 for r in resources:
983 r = self._inline_policies(c, r)
984 if len(r['c7n:InlinePolicies']) > 0 and value:
985 res.append(r)
986 if len(r['c7n:InlinePolicies']) == 0 and not value:
987 res.append(r)
988 return res
989
990
991@Role.filter_registry.register('has-specific-managed-policy')
992class SpecificIamRoleManagedPolicy(ValueFilter):
993 """Find IAM roles that have a specific policy attached
994
995 :example:
996
997 Check for roles with 'admin-policy' attached:
998
999 .. code-block:: yaml
1000
1001 policies:
1002 - name: iam-roles-have-admin
1003 resource: aws.iam-role
1004 filters:
1005 - type: has-specific-managed-policy
1006 value: admin-policy
1007
1008 :example:
1009
1010 Check for roles with an attached policy matching
1011 a given list:
1012
1013 .. code-block:: yaml
1014
1015 policies:
1016 - name: iam-roles-with-selected-policies
1017 resource: aws.iam-role
1018 filters:
1019 - type: has-specific-managed-policy
1020 op: in
1021 value:
1022 - AmazonS3FullAccess
1023 - AWSOrganizationsFullAccess
1024
1025 :example:
1026
1027 Check for roles with attached policy names matching a pattern:
1028
1029 .. code-block:: yaml
1030
1031 policies:
1032 - name: iam-roles-with-full-access-policies
1033 resource: aws.iam-role
1034 filters:
1035 - type: has-specific-managed-policy
1036 op: glob
1037 value: "*FullAccess"
1038
1039 Check for roles with attached policy ARNs matching a pattern:
1040
1041 .. code-block:: yaml
1042
1043 policies:
1044 - name: iam-roles-with-aws-full-access-policies
1045 resource: aws.iam-role
1046 filters:
1047 - type: has-specific-managed-policy
1048 key: PolicyArn
1049 op: regex
1050 value: "arn:aws:iam::aws:policy/.*FullAccess"
1051 """
1052
1053 schema = type_schema('has-specific-managed-policy', rinherit=ValueFilter.schema)
1054 permissions = ('iam:ListAttachedRolePolicies',)
1055 annotation_key = 'c7n:AttachedPolicies'
1056 matched_annotation_key = 'c7n:MatchedPolicies'
1057 schema_alias = False
1058
1059 def __init__(self, data, manager=None):
1060 # Preserve backward compatibility
1061 if 'key' not in data:
1062 data['key'] = 'PolicyName'
1063 super(SpecificIamRoleManagedPolicy, self).__init__(data, manager)
1064
1065 def get_managed_policies(self, client, role_set):
1066 for role in role_set:
1067 role[self.annotation_key] = [
1068 role_policy
1069 for role_policy
1070 in client.list_attached_role_policies(
1071 RoleName=role['RoleName'])['AttachedPolicies']
1072 ]
1073
1074 def process(self, resources, event=None):
1075 client = local_session(self.manager.session_factory).client('iam')
1076 with self.executor_factory(max_workers=2) as w:
1077 augment_set = [r for r in resources if self.annotation_key not in r]
1078 self.log.debug(
1079 "Querying %d roles' attached policies" % len(augment_set))
1080 list(w.map(
1081 functools.partial(self.get_managed_policies, client),
1082 chunks(augment_set, 50)))
1083
1084 matched = []
1085 for r in resources:
1086 matched_keys = [k for k in r[self.annotation_key] if self.match(k)]
1087 self.merge_annotation(r, self.matched_annotation_key, matched_keys)
1088 if matched_keys:
1089 matched.append(r)
1090 return matched
1091
1092
1093@Role.filter_registry.register('no-specific-managed-policy')
1094class NoSpecificIamRoleManagedPolicy(Filter):
1095 """Filter IAM roles that do not have a specific policy attached
1096
1097 For example, if the user wants to check all roles without 'ip-restriction':
1098
1099 :example:
1100
1101 .. code-block:: yaml
1102
1103 policies:
1104 - name: iam-roles-no-ip-restriction
1105 resource: iam-role
1106 filters:
1107 - type: no-specific-managed-policy
1108 value: ip-restriction
1109 """
1110
1111 schema = type_schema('no-specific-managed-policy', value={'type': 'string'})
1112 permissions = ('iam:ListAttachedRolePolicies',)
1113
1114 def _managed_policies(self, client, resource):
1115 return [r['PolicyName'] for r in client.list_attached_role_policies(
1116 RoleName=resource['RoleName'])['AttachedPolicies']]
1117
1118 def process(self, resources, event=None):
1119 c = local_session(self.manager.session_factory).client('iam')
1120 if self.data.get('value'):
1121 return [r for r in resources if self.data.get('value') not in
1122 self._managed_policies(c, r)]
1123 return []
1124
1125
1126@Role.action_registry.register('set-policy')
1127class SetPolicy(BaseAction):
1128 """Set a specific IAM policy as attached or detached on a role.
1129
1130 You will identify the policy by its arn.
1131
1132 Returns a list of roles modified by the action.
1133
1134 For example, if you want to automatically attach a policy to all roles which don't have it...
1135
1136 :example:
1137
1138 .. code-block:: yaml
1139
1140 - name: iam-attach-role-policy
1141 resource: iam-role
1142 filters:
1143 - type: no-specific-managed-policy
1144 value: my-iam-policy
1145 actions:
1146 - type: set-policy
1147 state: detached
1148 arn: "*"
1149 - type: set-policy
1150 state: attached
1151 arn: arn:aws:iam::123456789012:policy/my-iam-policy
1152
1153 """
1154 schema = type_schema(
1155 'set-policy',
1156 state={'enum': ['attached', 'detached']},
1157 arn={'type': 'string'},
1158 required=['state', 'arn'])
1159
1160 permissions = ('iam:AttachRolePolicy', 'iam:DetachRolePolicy', "iam:ListAttachedRolePolicies",)
1161
1162 def validate(self):
1163 if self.data.get('state') == 'attached' and self.data.get('arn') == "*":
1164 raise PolicyValidationError(
1165 '* operator is not supported for state: attached on %s' % (self.manager.data))
1166
1167 def attach_policy(self, client, resource, policy_arn):
1168 client.attach_role_policy(
1169 RoleName=resource['RoleName'],
1170 PolicyArn=policy_arn
1171 )
1172
1173 def detach_policy(self, client, resource, policy_arn):
1174 try:
1175 client.detach_role_policy(
1176 RoleName=resource['RoleName'],
1177 PolicyArn=policy_arn
1178 )
1179 except client.exceptions.NoSuchEntityException:
1180 return
1181
1182 def list_attached_policies(self, client, resource):
1183 attached_policy = client.list_attached_role_policies(RoleName=resource['RoleName'])
1184 policy_arns = [p.get('PolicyArn') for p in attached_policy['AttachedPolicies']]
1185 return policy_arns
1186
1187 def process(self, resources):
1188 client = local_session(self.manager.session_factory).client('iam')
1189 policy_arn = self.data['arn']
1190 if policy_arn != "*" and not policy_arn.startswith('arn'):
1191 policy_arn = 'arn:{}:iam::{}:policy/{}'.format(
1192 get_partition(self.manager.config.region),
1193 self.manager.account_id, policy_arn)
1194 state = self.data['state']
1195 for r in resources:
1196 if state == 'attached':
1197 self.attach_policy(client, r, policy_arn)
1198 elif state == 'detached' and policy_arn != "*":
1199 self.detach_policy(client, r, policy_arn)
1200 elif state == 'detached' and policy_arn == "*":
1201 try:
1202 self.detach_all_policies(client, r)
1203 except client.exceptions.NoSuchEntityException:
1204 continue
1205
1206 def detach_all_policies(self, client, resource):
1207 policy_arns = self.list_attached_policies(client, resource)
1208 for parn in policy_arns:
1209 self.detach_policy(client, resource, parn)
1210
1211
1212@User.action_registry.register("set-policy")
1213class SetUserPolicy(SetPolicy):
1214 """Set a specific IAM policy as attached or detached on a user.
1215
1216 You will identify the policy by its arn.
1217
1218 Returns a list of roles modified by the action.
1219
1220 For example, if you want to automatically attach a single policy while
1221 detaching all exisitng policies:
1222
1223 :example:
1224
1225 .. code-block:: yaml
1226
1227 - name: iam-attach-user-policy
1228 resource: iam-user
1229 filters:
1230 - type: value
1231 key: UserName
1232 op: not-in
1233 value:
1234 - AdminUser1
1235 - AdminUser2
1236 actions:
1237 - type: set-policy
1238 state: detached
1239 arn: arn:aws:iam::aws:policy/AdministratorAccess
1240
1241 """
1242
1243 permissions = (
1244 "iam:AttachUserPolicy", "iam:DetachUserPolicy", "iam:ListAttachedUserPolicies",)
1245
1246 def attach_policy(self, client, resource, policy_arn):
1247 client.attach_user_policy(
1248 UserName=resource["UserName"], PolicyArn=policy_arn)
1249
1250 def detach_policy(self, client, resource, policy_arn):
1251 try:
1252 client.detach_user_policy(
1253 UserName=resource["UserName"], PolicyArn=policy_arn)
1254 except client.exceptions.NoSuchEntityException:
1255 return
1256
1257 def list_attached_policies(self, client, resource):
1258 attached_policies = client.list_attached_user_policies(
1259 UserName=resource["UserName"]
1260 )
1261 policy_arns = [p.get('PolicyArn') for p in attached_policies['AttachedPolicies']]
1262 return policy_arns
1263
1264
1265@Group.action_registry.register("set-policy")
1266class SetGroupPolicy(SetPolicy):
1267 """Set a specific IAM policy as attached or detached on a group.
1268
1269 You will identify the policy by its arn.
1270
1271 Returns a list of roles modified by the action.
1272
1273 For example, if you want to automatically attach a single policy while
1274 detaching all exisitng policies:
1275
1276 :example:
1277
1278 .. code-block:: yaml
1279
1280 - name: iam-attach-group-policy
1281 resource: iam-group
1282 actions:
1283 - type: set-policy
1284 state: detached
1285 arn: "*"
1286 - type: set-policy
1287 state: attached
1288 arn: arn:aws:iam::{account_id}:policy/my-iam-policy
1289
1290 """
1291
1292 permissions = (
1293 "iam:AttachGroupPolicy", "iam:DetachGroupPolicy", "iam:ListAttachedGroupPolicies",)
1294
1295 def attach_policy(self, client, resource, policy_arn):
1296 client.attach_group_policy(
1297 GroupName=resource["GroupName"], PolicyArn=policy_arn)
1298
1299 def detach_policy(self, client, resource, policy_arn):
1300 try:
1301 client.detach_group_policy(
1302 GroupName=resource["GroupName"], PolicyArn=policy_arn)
1303 except client.exceptions.NoSuchEntityException:
1304 return
1305
1306 def list_attached_policies(self, client, resource):
1307 attached_policies = client.list_attached_group_policies(
1308 GroupName=resource["GroupName"]
1309 )
1310 policy_arns = [p.get('PolicyArn') for p in attached_policies['AttachedPolicies']]
1311 return policy_arns
1312
1313
1314@Role.action_registry.register('delete')
1315class RoleDelete(BaseAction):
1316 """Delete an IAM Role.
1317
1318 To delete IAM Role you must first delete the policies
1319 that are associated with the role. Also, you need to remove
1320 the role from all instance profiles that the role is in.
1321
1322 https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_manage_delete.html
1323
1324 For this case option 'force' is used. If you set it as 'true',
1325 policies that are associated with the role would be detached
1326 (inline policies would be removed) and all instance profiles
1327 the role is in would be removed as well as the role.
1328
1329 For example, if you want to automatically delete an unused IAM role.
1330
1331 :example:
1332
1333 .. code-block:: yaml
1334
1335 - name: iam-delete-unused-role
1336 resource: iam-role
1337 filters:
1338 - type: usage
1339 match-operator: all
1340 LastAuthenticated: null
1341 actions:
1342 - type: delete
1343 force: true
1344
1345 """
1346 schema = type_schema('delete', force={'type': 'boolean'})
1347 permissions = ('iam:DeleteRole', 'iam:DeleteInstanceProfile',)
1348
1349 def detach_inline_policies(self, client, r):
1350 policies = (self.manager.retry(
1351 client.list_role_policies, RoleName=r['RoleName'],
1352 ignore_err_codes=('NoSuchEntityException',)) or {}).get('PolicyNames', ())
1353 for p in policies:
1354 self.manager.retry(
1355 client.delete_role_policy,
1356 RoleName=r['RoleName'], PolicyName=p,
1357 ignore_err_codes=('NoSuchEntityException',))
1358
1359 def delete_instance_profiles(self, client, r):
1360 # An instance profile can contain only one IAM role,
1361 # although a role can be included in multiple instance profiles
1362 profile_names = []
1363 profiles = self.manager.retry(
1364 client.list_instance_profiles_for_role,
1365 RoleName=r['RoleName'],
1366 ignore_err_codes=('NoSuchEntityException',))
1367 if profiles:
1368 profile_names = [p.get('InstanceProfileName') for p in profiles['InstanceProfiles']]
1369 for p in profile_names:
1370 self.manager.retry(
1371 client.remove_role_from_instance_profile,
1372 RoleName=r['RoleName'], InstanceProfileName=p,
1373 ignore_err_codes=('NoSuchEntityException',))
1374 self.manager.retry(
1375 client.delete_instance_profile,
1376 InstanceProfileName=p,
1377 ignore_err_codes=('NoSuchEntityException',))
1378
1379 def process(self, resources):
1380 client = local_session(self.manager.session_factory).client('iam')
1381 error = None
1382 if self.data.get('force', False):
1383 policy_setter = self.manager.action_registry['set-policy'](
1384 {'state': 'detached', 'arn': '*'}, self.manager)
1385 policy_setter.process(resources)
1386
1387 for r in resources:
1388 if self.data.get('force', False):
1389 self.detach_inline_policies(client, r)
1390 self.delete_instance_profiles(client, r)
1391 try:
1392 client.delete_role(RoleName=r['RoleName'])
1393 except client.exceptions.DeleteConflictException as e:
1394 self.log.warning(
1395 ("Role:%s cannot be deleted, set force "
1396 "to detach policy, instance profile and delete, error: %s") % (
1397 r['Arn'], str(e)))
1398 error = e
1399 except (client.exceptions.NoSuchEntityException,
1400 client.exceptions.UnmodifiableEntityException):
1401 continue
1402 if error:
1403 raise error
1404
1405
1406######################
1407# IAM Policies #
1408######################
1409
1410
1411@Policy.filter_registry.register('used')
1412class UsedIamPolicies(Filter):
1413 """Filter IAM policies that are being used
1414 (either attached to some roles or used as a permissions boundary).
1415
1416 :example:
1417
1418 .. code-block:: yaml
1419
1420 policies:
1421 - name: iam-policy-used
1422 resource: iam-policy
1423 filters:
1424 - type: used
1425 """
1426
1427 schema = type_schema('used')
1428 permissions = ('iam:ListPolicies',)
1429
1430 def process(self, resources, event=None):
1431 return [r for r in resources if
1432 r['AttachmentCount'] > 0 or r.get('PermissionsBoundaryUsageCount', 0) > 0]
1433
1434
1435@Policy.filter_registry.register('unused')
1436class UnusedIamPolicies(Filter):
1437 """Filter IAM policies that are not being used
1438 (neither attached to any roles nor used as a permissions boundary).
1439
1440 :example:
1441
1442 .. code-block:: yaml
1443
1444 policies:
1445 - name: iam-policy-unused
1446 resource: iam-policy
1447 filters:
1448 - type: unused
1449 """
1450
1451 schema = type_schema('unused')
1452 permissions = ('iam:ListPolicies',)
1453
1454 def process(self, resources, event=None):
1455 return [r for r in resources if
1456 r['AttachmentCount'] == 0 and r.get('PermissionsBoundaryUsageCount', 0) == 0]
1457
1458
1459@Policy.filter_registry.register('has-allow-all')
1460class AllowAllIamPolicies(Filter):
1461 """Check if IAM policy resource(s) have allow-all IAM policy statement block.
1462
1463 This allows users to implement CIS AWS check 1.24 which states that no
1464 policy must exist with the following requirements.
1465
1466 Policy must have 'Action' and Resource = '*' with 'Effect' = 'Allow'
1467
1468 The policy will trigger on the following IAM policy (statement).
1469 For example:
1470
1471 .. code-block:: json
1472
1473 {
1474 "Version": "2012-10-17",
1475 "Statement": [{
1476 "Action": "*",
1477 "Resource": "*",
1478 "Effect": "Allow"
1479 }]
1480 }
1481
1482 Additionally, the policy checks if the statement has no 'Condition' or
1483 'NotAction'.
1484
1485 For example, if the user wants to check all used policies and filter on
1486 allow all:
1487
1488 .. code-block:: yaml
1489
1490 - name: iam-no-used-all-all-policy
1491 resource: iam-policy
1492 filters:
1493 - type: used
1494 - type: has-allow-all
1495
1496 Note that scanning and getting all policies and all statements can take
1497 a while. Use it sparingly or combine it with filters such as 'used' as
1498 above.
1499
1500 """
1501 schema = type_schema('has-allow-all')
1502 permissions = ('iam:ListPolicies', 'iam:ListPolicyVersions')
1503
1504 def has_allow_all_policy(self, client, resource):
1505 statements = client.get_policy_version(
1506 PolicyArn=resource['Arn'],
1507 VersionId=resource['DefaultVersionId']
1508 )['PolicyVersion']['Document']['Statement']
1509 if isinstance(statements, dict):
1510 statements = [statements]
1511
1512 for s in statements:
1513 if ('Condition' not in s and
1514 'Action' in s and
1515 isinstance(s['Action'], str) and
1516 s['Action'] == "*" and
1517 'Resource' in s and
1518 isinstance(s['Resource'], str) and
1519 s['Resource'] == "*" and
1520 s['Effect'] == "Allow"):
1521 return True
1522 return False
1523
1524 def process(self, resources, event=None):
1525 c = local_session(self.manager.session_factory).client('iam')
1526 results = [r for r in resources if self.has_allow_all_policy(c, r)]
1527 self.log.info(
1528 "%d of %d iam policies have allow all.",
1529 len(results), len(resources))
1530 return results
1531
1532
1533@Policy.action_registry.register('delete')
1534class PolicyDelete(BaseAction):
1535 """Delete an IAM Policy.
1536
1537 For example, if you want to automatically delete all unused IAM policies.
1538
1539 :example:
1540
1541 .. code-block:: yaml
1542
1543 - name: iam-delete-unused-policies
1544 resource: iam-policy
1545 filters:
1546 - type: unused
1547 actions:
1548 - delete
1549
1550 """
1551 schema = type_schema('delete')
1552 permissions = ('iam:DeletePolicy',)
1553
1554 def process(self, resources):
1555 client = local_session(self.manager.session_factory).client('iam')
1556
1557 rcount = len(resources)
1558 resources = [r for r in resources if Arn.parse(r['Arn']).account_id != 'aws']
1559 if len(resources) != rcount:
1560 self.log.warning("Implicitly filtering AWS managed policies: %d -> %d",
1561 rcount, len(resources))
1562
1563 for r in resources:
1564 if r.get('DefaultVersionId', '') != 'v1':
1565 versions = [v['VersionId'] for v in client.list_policy_versions(
1566 PolicyArn=r['Arn']).get('Versions') if not v.get('IsDefaultVersion')]
1567 for v in versions:
1568 client.delete_policy_version(PolicyArn=r['Arn'], VersionId=v)
1569 client.delete_policy(PolicyArn=r['Arn'])
1570
1571
1572###############################
1573# IAM Instance Profiles #
1574###############################
1575
1576
1577@InstanceProfile.filter_registry.register('used')
1578class UsedInstanceProfiles(IamRoleUsage):
1579 """Filter IAM profiles that are being used.
1580
1581 :example:
1582
1583 .. code-block:: yaml
1584
1585 policies:
1586 - name: iam-instance-profiles-in-use
1587 resource: iam-profile
1588 filters:
1589 - type: used
1590 """
1591
1592 schema = type_schema('used')
1593
1594 def process(self, resources, event=None):
1595 results = []
1596 profiles = self.instance_profile_usage()
1597 for r in resources:
1598 if r['Arn'] in profiles or r['InstanceProfileName'] in profiles:
1599 results.append(r)
1600 self.log.info(
1601 "%d of %d instance profiles currently in use." % (
1602 len(results), len(resources)))
1603 return results
1604
1605
1606@InstanceProfile.filter_registry.register('unused')
1607class UnusedInstanceProfiles(IamRoleUsage):
1608 """Filter IAM profiles that are not being used
1609
1610 :example:
1611
1612 .. code-block:: yaml
1613
1614 policies:
1615 - name: iam-instance-profiles-not-in-use
1616 resource: iam-profile
1617 filters:
1618 - type: unused
1619 """
1620
1621 schema = type_schema('unused')
1622
1623 def process(self, resources, event=None):
1624 results = []
1625 profiles = self.instance_profile_usage()
1626 for r in resources:
1627 if (r['Arn'] not in profiles and r['InstanceProfileName'] not in profiles):
1628 results.append(r)
1629 self.log.info(
1630 "%d of %d instance profiles currently not in use." % (
1631 len(results), len(resources)))
1632 return results
1633
1634
1635@InstanceProfile.action_registry.register('set-role')
1636class InstanceProfileSetRole(BaseAction):
1637 """Upserts specified role name for IAM instance profiles.
1638 Instance profile roles are removed when empty role name is specified.
1639
1640 :example:
1641
1642 .. code-block:: yaml
1643
1644 policies:
1645 - name: iam-instance-profile-set-role
1646 resource: iam-profile
1647 actions:
1648 - type: set-role
1649 role: my-test-role
1650 """
1651
1652 schema = type_schema('set-role',
1653 role={'type': 'string'})
1654 permissions = ('iam:AddRoleToInstanceProfile', 'iam:RemoveRoleFromInstanceProfile',)
1655
1656 def add_role(self, client, resource, role):
1657 self.manager.retry(
1658 client.add_role_to_instance_profile,
1659 InstanceProfileName=resource['InstanceProfileName'],
1660 RoleName=role
1661 )
1662 return
1663
1664 def remove_role(self, client, resource):
1665 self.manager.retry(
1666 client.remove_role_from_instance_profile,
1667 InstanceProfileName=resource['InstanceProfileName'],
1668 RoleName=resource['Roles'][0]['RoleName']
1669 )
1670 return
1671
1672 def process(self, resources):
1673 client = local_session(self.manager.session_factory).client('iam')
1674 role = self.data.get('role', '')
1675 for r in resources:
1676 if not role:
1677 if len(r['Roles']) == 0:
1678 continue
1679 else:
1680 self.remove_role(client, r)
1681 else:
1682 if len(r['Roles']) == 0:
1683 self.add_role(client, r, role)
1684 elif role == r['Roles'][0]['RoleName']:
1685 continue
1686 else:
1687 self.remove_role(client, r)
1688 self.add_role(client, r, role)
1689
1690
1691@InstanceProfile.action_registry.register("set-policy")
1692class SetInstanceProfileRolePolicy(SetPolicy):
1693 """Set a specific IAM policy as attached or detached on an instance profile role.
1694
1695 You will identify the policy by its arn.
1696
1697 Returns a list of roles modified by the action.
1698
1699 For example, if you want to automatically attach a single policy while
1700 detaching all exisitng policies:
1701
1702 :example:
1703
1704 .. code-block:: yaml
1705
1706 - name: iam-attach-instance-profile-role-policy
1707 resource: iam-profile
1708 filters:
1709 - not:
1710 - type: has-specific-managed-policy
1711 value: my-iam-policy
1712 actions:
1713 - type: set-policy
1714 state: attached
1715 arn: arn:aws:iam::123456789012:policy/my-iam-policy
1716
1717 """
1718
1719 permissions = ('iam:AttachRolePolicy', 'iam:DetachRolePolicy', "iam:ListAttachedRolePolicies",)
1720
1721 def attach_policy(self, client, resource, policy_arn):
1722 client.attach_role_policy(
1723 RoleName=resource["Roles"][0]["RoleName"],
1724 PolicyArn=policy_arn
1725 )
1726
1727 def detach_policy(self, client, resource, policy_arn):
1728 try:
1729 client.detach_role_policy(
1730 RoleName=resource["Roles"][0]["RoleName"],
1731 PolicyArn=policy_arn
1732 )
1733 except client.exceptions.NoSuchEntityException:
1734 return
1735
1736 def list_attached_policies(self, client, resource):
1737 attached_policies = client.list_attached_role_policies(
1738 RoleName=resource["Roles"][0]["RoleName"]
1739 )
1740 policy_arns = [p.get('PolicyArn') for p in attached_policies['AttachedPolicies']]
1741 return policy_arns
1742
1743
1744###################
1745# IAM Users #
1746###################
1747
1748class CredentialReport(Filter):
1749 """Use IAM Credential report to filter users.
1750
1751 The IAM Credential report aggregates multiple pieces of
1752 information on iam users. This makes it highly efficient for
1753 querying multiple aspects of a user that would otherwise require
1754 per user api calls.
1755
1756 https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_getting-report.html
1757
1758 For example if we wanted to retrieve all users with mfa who have
1759 never used their password but have active access keys from the
1760 last month
1761
1762 .. code-block:: yaml
1763
1764 - name: iam-mfa-active-keys-no-login
1765 resource: iam-user
1766 filters:
1767 - type: credential
1768 key: mfa_active
1769 value: true
1770 - type: credential
1771 key: password_last_used
1772 value: absent
1773 - type: credential
1774 key: access_keys.last_used_date
1775 value_type: age
1776 value: 30
1777 op: less-than
1778
1779 Credential Report Transforms
1780
1781 We perform some default transformations from the raw
1782 credential report. Sub-objects (access_key_1, cert_2)
1783 are turned into array of dictionaries for matching
1784 purposes with their common prefixes stripped.
1785 N/A values are turned into None, TRUE/FALSE are turned
1786 into boolean values.
1787
1788 """
1789 schema = type_schema(
1790 'credential',
1791 value_type={'$ref': '#/definitions/filters_common/value_types'},
1792 key={'type': 'string',
1793 'title': 'report key to search',
1794 'enum': [
1795 'user',
1796 'arn',
1797 'user_creation_time',
1798 'password_enabled',
1799 'password_last_used',
1800 'password_last_changed',
1801 'password_next_rotation',
1802 'mfa_active',
1803 'access_keys',
1804 'access_keys.active',
1805 'access_keys.last_used_date',
1806 'access_keys.last_used_region',
1807 'access_keys.last_used_service',
1808 'access_keys.last_rotated',
1809 'certs',
1810 'certs.active',
1811 'certs.last_rotated',
1812 ]},
1813 value={'$ref': '#/definitions/filters_common/value'},
1814 op={'$ref': '#/definitions/filters_common/comparison_operators'},
1815 report_generate={
1816 'title': 'Generate a report if none is present.',
1817 'default': True,
1818 'type': 'boolean'},
1819 report_delay={
1820 'title': 'Number of seconds to wait for report generation.',
1821 'default': 10,
1822 'type': 'number'},
1823 report_max_age={
1824 'title': 'Number of seconds to consider a report valid.',
1825 'default': 60 * 60 * 24,
1826 'type': 'number'})
1827
1828 list_sub_objects = (
1829 ('access_key_1_', 'access_keys'),
1830 ('access_key_2_', 'access_keys'),
1831 ('cert_1_', 'certs'),
1832 ('cert_2_', 'certs'))
1833
1834 # for access keys only
1835 matched_annotation_key = 'c7n:matched-keys'
1836
1837 permissions = ('iam:GenerateCredentialReport',
1838 'iam:GetCredentialReport')
1839
1840 def get_value_or_schema_default(self, k):
1841 if k in self.data:
1842 return self.data[k]
1843 return self.schema['properties'][k]['default']
1844
1845 def get_credential_report(self):
1846 cache = self.manager._cache
1847 with cache:
1848 cache_key = {'account': self.manager.config.account_id, 'iam-credential-report': True}
1849 report = cache.get(cache_key)
1850
1851 if report:
1852 return report
1853 data = self.fetch_credential_report()
1854 report = {}
1855 if isinstance(data, bytes):
1856 reader = csv.reader(io.StringIO(data.decode('utf-8')))
1857 else:
1858 reader = csv.reader(io.StringIO(data))
1859 headers = next(reader)
1860 for line in reader:
1861 info = dict(zip(headers, line))
1862 report[info['user']] = self.process_user_record(info)
1863 cache.save(cache_key, report)
1864
1865 return report
1866
1867 @classmethod
1868 def process_user_record(cls, info):
1869 """Type convert the csv record, modifies in place."""
1870 keys = list(info.keys())
1871 # Value conversion
1872 for k in keys:
1873 v = info[k]
1874 if v in ('N/A', 'no_information'):
1875 info[k] = None
1876 elif v == 'false':
1877 info[k] = False
1878 elif v == 'true':
1879 info[k] = True
1880 # Object conversion
1881 for p, t in cls.list_sub_objects:
1882 obj = dict([(k[len(p):], info.pop(k))
1883 for k in keys if k.startswith(p)])
1884 if obj.get('active', False) or obj.get('last_rotated', False):
1885 info.setdefault(t, []).append(obj)
1886 return info
1887
1888 def fetch_credential_report(self):
1889 client = local_session(self.manager.session_factory).client('iam')
1890 try:
1891 report = client.get_credential_report()
1892 except ClientError as e:
1893 if e.response['Error']['Code'] == 'ReportNotPresent':
1894 report = None
1895 elif e.response['Error']['Code'] == 'ReportInProgress':
1896 # Someone else asked for the report before it was done. Wait
1897 # for it again.
1898 time.sleep(self.get_value_or_schema_default('report_delay'))
1899 report = client.get_credential_report()
1900 else:
1901 raise
1902 if report:
1903 threshold = datetime.datetime.now(tz=tzutc()) - timedelta(
1904 seconds=self.get_value_or_schema_default(
1905 'report_max_age'))
1906 if not report['GeneratedTime'].tzinfo:
1907 threshold = threshold.replace(tzinfo=None)
1908 if report['GeneratedTime'] < threshold:
1909 report = None
1910 if report is None:
1911 if not self.get_value_or_schema_default('report_generate'):
1912 raise ValueError("Credential Report Not Present")
1913 client.generate_credential_report()
1914 time.sleep(self.get_value_or_schema_default('report_delay'))
1915 report = client.get_credential_report()
1916 return report['Content']
1917
1918 def process(self, resources, event=None):
1919 if '.' in self.data['key']:
1920 self.matcher_config = dict(self.data)
1921 self.matcher_config['key'] = self.data['key'].split('.', 1)[1]
1922 return []
1923
1924 def match(self, resource, info):
1925 if info is None:
1926 return False
1927 k = self.data.get('key')
1928 if '.' not in k:
1929 vf = ValueFilter(self.data)
1930 vf.annotate = False
1931 return vf(info)
1932
1933 # access key matching
1934 prefix, _ = k.split('.', 1)
1935 vf = ValueFilter(self.matcher_config)
1936 vf.annotate = False
1937
1938 # annotation merging with previous respecting block operators
1939 k_matched = []
1940 for v in info.get(prefix, ()):
1941 if vf.match(v):
1942 k_matched.append(v)
1943
1944 for k in k_matched:
1945 k['c7n:match-type'] = 'credential'
1946
1947 self.merge_annotation(resource, self.matched_annotation_key, k_matched)
1948 return bool(k_matched)
1949
1950
1951@User.filter_registry.register('credential')
1952class UserCredentialReport(CredentialReport):
1953
1954 def process(self, resources, event=None):
1955 super(UserCredentialReport, self).process(resources, event)
1956 report = self.get_credential_report()
1957 if report is None:
1958 return []
1959 results = []
1960 for r in resources:
1961 info = report.get(r['UserName'])
1962 if self.match(r, info):
1963 r['c7n:credential-report'] = info
1964 results.append(r)
1965 return results
1966
1967
1968@User.filter_registry.register('has-inline-policy')
1969class IamUserInlinePolicy(Filter):
1970 """
1971 Filter IAM users that have an inline-policy attached
1972
1973 True: Filter users that have an inline-policy
1974 False: Filter users that do not have an inline-policy
1975 """
1976
1977 schema = type_schema('has-inline-policy', value={'type': 'boolean'})
1978 permissions = ('iam:ListUserPolicies',)
1979
1980 def _inline_policies(self, client, resource):
1981 resource['c7n:InlinePolicies'] = client.list_user_policies(
1982 UserName=resource['UserName'])['PolicyNames']
1983 return resource
1984
1985 def process(self, resources, event=None):
1986 c = local_session(self.manager.session_factory).client('iam')
1987 value = self.data.get('value', True)
1988 res = []
1989 for r in resources:
1990 r = self._inline_policies(c, r)
1991 if len(r['c7n:InlinePolicies']) > 0 and value:
1992 res.append(r)
1993 if len(r['c7n:InlinePolicies']) == 0 and not value:
1994 res.append(r)
1995 return res
1996
1997
1998@User.filter_registry.register('policy')
1999class UserPolicy(ValueFilter):
2000 """Filter IAM users based on attached policy values
2001
2002 :example:
2003
2004 .. code-block:: yaml
2005
2006 policies:
2007 - name: iam-users-with-admin-access
2008 resource: iam-user
2009 filters:
2010 - type: policy
2011 key: PolicyName
2012 value: AdministratorAccess
2013 include-via: true
2014 """
2015
2016 schema = type_schema('policy', rinherit=ValueFilter.schema,
2017 **{'include-via': {'type': 'boolean'}})
2018 schema_alias = False
2019 permissions = (
2020 'iam:ListAttachedUserPolicies',
2021 'iam:ListGroupsForUser',
2022 'iam:ListAttachedGroupPolicies',
2023 )
2024
2025 def find_in_user_set(self, user_set, search_key, arn_key, arn):
2026 for u in user_set:
2027 if search_key in u:
2028 searched = next((v for v in u[search_key] if v.get(arn_key) == arn), None)
2029 if searched is not None:
2030 return searched
2031
2032 return None
2033
2034 def user_groups_policies(self, client, user_set, u):
2035 u['c7n:Groups'] = client.list_groups_for_user(
2036 UserName=u['UserName'])['Groups']
2037
2038 for ug in u['c7n:Groups']:
2039 ug_searched = self.find_in_user_set(user_set, 'c7n:Groups', 'Arn', ug['Arn'])
2040 if ug_searched and ug_searched.get('AttachedPolicies'):
2041 ug['AttachedPolicies'] = ug_searched['AttachedPolicies']
2042 else:
2043 ug['AttachedPolicies'] = client.list_attached_group_policies(
2044 GroupName=ug['GroupName'])['AttachedPolicies']
2045
2046 for ap in ug['AttachedPolicies']:
2047 p_searched = self.find_in_user_set([u], 'c7n:Policies', 'Arn', ap['PolicyArn'])
2048 if not p_searched:
2049 p_searched = self.find_in_user_set(
2050 user_set, 'c7n:Policies', 'Arn', ap['PolicyArn']
2051 )
2052 if p_searched:
2053 u['c7n:Policies'].append(p_searched)
2054 else:
2055 u['c7n:Policies'].append(
2056 client.get_policy(PolicyArn=ap['PolicyArn'])['Policy'])
2057
2058 return u
2059
2060 def user_policies(self, user_set):
2061 client = local_session(self.manager.session_factory).client('iam')
2062 for u in user_set:
2063 if 'c7n:Policies' not in u:
2064 u['c7n:Policies'] = []
2065 aps = client.list_attached_user_policies(
2066 UserName=u['UserName'])['AttachedPolicies']
2067 for ap in aps:
2068 u['c7n:Policies'].append(
2069 client.get_policy(PolicyArn=ap['PolicyArn'])['Policy'])
2070 if self.data.get('include-via'):
2071 u = self.user_groups_policies(client, user_set, u)
2072
2073 def process(self, resources, event=None):
2074 user_set = chunks(resources, size=50)
2075 with self.executor_factory(max_workers=2) as w:
2076 self.log.debug(
2077 "Querying %d users policies" % len(resources))
2078 list(w.map(self.user_policies, user_set))
2079
2080 matched = []
2081 for r in resources:
2082 for p in r['c7n:Policies']:
2083 if self.match(p) and r not in matched:
2084 matched.append(r)
2085 return matched
2086
2087
2088@User.filter_registry.register('group')
2089class GroupMembership(ValueFilter):
2090 """Filter IAM users based on attached group values
2091
2092 :example:
2093
2094 .. code-block:: yaml
2095
2096 policies:
2097 - name: iam-users-in-admin-group
2098 resource: iam-user
2099 filters:
2100 - type: group
2101 key: GroupName
2102 value: Admins
2103 """
2104
2105 schema = type_schema('group', rinherit=ValueFilter.schema)
2106 schema_alias = False
2107 permissions = ('iam:ListGroupsForUser',)
2108
2109 def get_user_groups(self, client, user_set):
2110 for u in user_set:
2111 u['c7n:Groups'] = client.list_groups_for_user(
2112 UserName=u['UserName'])['Groups']
2113
2114 def process(self, resources, event=None):
2115 client = local_session(self.manager.session_factory).client('iam')
2116 with self.executor_factory(max_workers=2) as w:
2117 futures = []
2118 for user_set in chunks(
2119 [r for r in resources if 'c7n:Groups' not in r], size=50):
2120 futures.append(
2121 w.submit(self.get_user_groups, client, user_set))
2122 for f in as_completed(futures):
2123 pass
2124
2125 matched = []
2126 for r in resources:
2127 for p in r.get('c7n:Groups', []):
2128 if self.match(p) and r not in matched:
2129 matched.append(r)
2130 return matched
2131
2132
2133@User.filter_registry.register('access-key')
2134class UserAccessKey(ValueFilter):
2135 """Filter IAM users based on access-key values
2136
2137 By default multiple uses of this filter will match
2138 on any user key satisfying either filter. To find
2139 specific keys that match multiple access-key filters,
2140 use `match-operator: and`
2141
2142 :example:
2143
2144 .. code-block:: yaml
2145
2146 policies:
2147 - name: iam-users-with-active-keys
2148 resource: iam-user
2149 filters:
2150 - type: access-key
2151 key: Status
2152 value: Active
2153 - type: access-key
2154 match-operator: and
2155 key: CreateDate
2156 value_type: age
2157 value: 90
2158 """
2159
2160 schema = type_schema(
2161 'access-key',
2162 rinherit=ValueFilter.schema,
2163 **{'match-operator': {'enum': ['and', 'or']}})
2164 schema_alias = False
2165 permissions = ('iam:ListAccessKeys',)
2166 annotation_key = 'c7n:AccessKeys'
2167 matched_annotation_key = 'c7n:matched-keys'
2168 annotate = False
2169
2170 def get_user_keys(self, client, user_set):
2171 for u in user_set:
2172 u[self.annotation_key] = self.manager.retry(
2173 client.list_access_keys,
2174 UserName=u['UserName'])['AccessKeyMetadata']
2175
2176 def process(self, resources, event=None):
2177 client = local_session(self.manager.session_factory).client('iam')
2178 with self.executor_factory(max_workers=2) as w:
2179 augment_set = [r for r in resources if self.annotation_key not in r]
2180 self.log.debug(
2181 "Querying %d users' api keys" % len(augment_set))
2182 list(w.map(
2183 functools.partial(self.get_user_keys, client),
2184 chunks(augment_set, 50)))
2185
2186 matched = []
2187 match_op = self.data.get('match-operator', 'or')
2188 for r in resources:
2189 keys = r[self.annotation_key]
2190 if self.matched_annotation_key in r and match_op == 'and':
2191 keys = r[self.matched_annotation_key]
2192 k_matched = []
2193 for k in keys:
2194 if self.match(k):
2195 k_matched.append(k)
2196 for k in k_matched:
2197 k['c7n:match-type'] = 'access'
2198 self.merge_annotation(r, self.matched_annotation_key, k_matched)
2199 if k_matched:
2200 matched.append(r)
2201 return matched
2202
2203
2204@User.filter_registry.register('ssh-key')
2205class UserSSHKeyFilter(ValueFilter):
2206 """Filter IAM users based on uploaded SSH public keys
2207
2208 :example:
2209
2210 .. code-block:: yaml
2211
2212 policies:
2213 - name: iam-users-with-old-ssh-keys
2214 resource: iam-user
2215 filters:
2216 - type: ssh-key
2217 key: Status
2218 value: Active
2219 - type: ssh-key
2220 key: UploadDate
2221 value_type: age
2222 value: 90
2223 """
2224
2225 schema = type_schema(
2226 'ssh-key',
2227 rinherit=ValueFilter.schema)
2228 schema_alias = False
2229 permissions = ('iam:ListSSHPublicKeys',)
2230 annotation_key = 'c7n:SSHKeys'
2231 matched_annotation_key = 'c7n:matched-ssh-keys'
2232 annotate = False
2233
2234 def get_user_ssh_keys(self, client, user_set):
2235 for u in user_set:
2236 u[self.annotation_key] = self.manager.retry(
2237 client.list_ssh_public_keys,
2238 UserName=u['UserName'])['SSHPublicKeys']
2239
2240 def process(self, resources, event=None):
2241 client = local_session(self.manager.session_factory).client('iam')
2242 with self.executor_factory(max_workers=2) as w:
2243 augment_set = [r for r in resources if self.annotation_key not in r]
2244 self.log.debug(
2245 "Querying %d users' SSH keys" % len(augment_set))
2246 list(w.map(
2247 functools.partial(self.get_user_ssh_keys, client),
2248 chunks(augment_set, 50)))
2249
2250 matched = []
2251 for r in resources:
2252 matched_keys = [k for k in r[self.annotation_key] if self.match(k)]
2253 self.merge_annotation(r, self.matched_annotation_key, matched_keys)
2254 if matched_keys:
2255 matched.append(r)
2256 return matched
2257
2258
2259@User.filter_registry.register('login-profile')
2260class UserLoginProfile(ValueFilter):
2261 """Filter IAM users that have an associated login-profile
2262
2263 For quicker evaluation and reduced API traffic, it is recommended to
2264 instead use the 'credential' filter with 'password_enabled': true when
2265 a delay of up to four hours for credential report syncing is acceptable.
2266
2267 (https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_getting-report.html)
2268
2269 :example:
2270
2271 .. code-block: yaml
2272
2273 policies:
2274 - name: iam-users-with-console-access
2275 resource: iam-user
2276 filters:
2277 - type: login-profile
2278 """
2279
2280 schema = type_schema('login-profile', rinherit=ValueFilter.schema)
2281 permissions = ('iam:GetLoginProfile',)
2282 annotation_key = 'c7n:LoginProfile'
2283
2284 def user_login_profiles(self, user_set):
2285 client = local_session(self.manager.session_factory).client('iam')
2286 for u in user_set:
2287 u[self.annotation_key] = False
2288 try:
2289 login_profile_resp = client.get_login_profile(UserName=u['UserName'])
2290 if u['UserName'] == login_profile_resp['LoginProfile']['UserName']:
2291 u[self.annotation_key] = True
2292 except ClientError as e:
2293 if e.response['Error']['Code'] not in ('NoSuchEntity',):
2294 raise
2295
2296 def process(self, resources, event=None):
2297 user_set = chunks(resources, size=50)
2298 with self.executor_factory(max_workers=2) as w:
2299 self.log.debug(
2300 "Querying %d users for login profile" % len(resources))
2301 list(w.map(self.user_login_profiles, user_set))
2302
2303 matched = []
2304 for r in resources:
2305 if r[self.annotation_key]:
2306 matched.append(r)
2307 return matched
2308
2309
2310# Mfa-device filter for iam-users
2311@User.filter_registry.register('mfa-device')
2312class UserMfaDevice(ValueFilter):
2313 """Filter iam-users based on mfa-device status
2314
2315 :example:
2316
2317 .. code-block:: yaml
2318
2319 policies:
2320 - name: mfa-enabled-users
2321 resource: iam-user
2322 filters:
2323 - type: mfa-device
2324 key: UserName
2325 value: not-null
2326 """
2327
2328 schema = type_schema('mfa-device', rinherit=ValueFilter.schema)
2329 schema_alias = False
2330 permissions = ('iam:ListMFADevices',)
2331
2332 def __init__(self, *args, **kw):
2333 super(UserMfaDevice, self).__init__(*args, **kw)
2334 self.data['key'] = 'MFADevices'
2335
2336 def process(self, resources, event=None):
2337
2338 def _user_mfa_devices(resource):
2339 client = local_session(self.manager.session_factory).client('iam')
2340 resource['MFADevices'] = client.list_mfa_devices(
2341 UserName=resource['UserName'])['MFADevices']
2342
2343 with self.executor_factory(max_workers=2) as w:
2344 query_resources = [
2345 r for r in resources if 'MFADevices' not in r]
2346 self.log.debug(
2347 "Querying %d users' mfa devices" % len(query_resources))
2348 list(w.map(_user_mfa_devices, query_resources))
2349
2350 matched = []
2351 for r in resources:
2352 if self.match(r):
2353 matched.append(r)
2354
2355 return matched
2356
2357
2358@User.action_registry.register('post-finding')
2359class UserFinding(OtherResourcePostFinding):
2360
2361 def format_resource(self, r):
2362 if any(filter(lambda x: isinstance(x, UserAccessKey), self.manager.iter_filters())):
2363 details = {
2364 "UserName": "arn:aws:iam:{}:user/{}".format(
2365 self.manager.config.account_id, r["c7n:AccessKeys"][0]["UserName"]
2366 ),
2367 "Status": r["c7n:AccessKeys"][0]["Status"],
2368 "CreatedAt": r["c7n:AccessKeys"][0]["CreateDate"].isoformat(),
2369 }
2370 accesskey = {
2371 "Type": "AwsIamAccessKey",
2372 "Id": r["c7n:AccessKeys"][0]["AccessKeyId"],
2373 "Region": self.manager.config.region,
2374 "Details": {"AwsIamAccessKey": filter_empty(details)},
2375 }
2376 return filter_empty(accesskey)
2377 else:
2378 return super(UserFinding, self).format_resource(r)
2379
2380
2381@User.action_registry.register('delete')
2382class UserDelete(BaseAction):
2383 """Delete a user or properties of a user.
2384
2385 For example if you want to have a whitelist of valid (machine-)users
2386 and want to ensure that no users have been clicked without documentation.
2387
2388 You can use both the 'credential' or the 'username'
2389 filter. 'credential' will have an SLA of 4h,
2390 (http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_getting-report.html),
2391 but the added benefit of performing less API calls, whereas
2392 'username' will make more API calls, but have a SLA of your cache.
2393
2394 :example:
2395
2396 .. code-block:: yaml
2397
2398 # using a 'credential' filter'
2399 - name: iam-only-whitelisted-users-credential
2400 resource: iam-user
2401 filters:
2402 - type: credential
2403 key: user
2404 op: not-in
2405 value:
2406 - valid-user-1
2407 - valid-user-2
2408 actions:
2409 - delete
2410
2411 # using a 'username' filter with 'UserName'
2412 - name: iam-only-whitelisted-users-username
2413 resource: iam-user
2414 filters:
2415 - type: value
2416 key: UserName
2417 op: not-in
2418 value:
2419 - valid-user-1
2420 - valid-user-2
2421 actions:
2422 - delete
2423
2424 # using a 'username' filter with 'Arn'
2425 - name: iam-only-whitelisted-users-arn
2426 resource: iam-user
2427 filters:
2428 - type: value
2429 key: Arn
2430 op: not-in
2431 value:
2432 - arn:aws:iam:123456789012:user/valid-user-1
2433 - arn:aws:iam:123456789012:user/valid-user-2
2434 actions:
2435 - delete
2436
2437 Additionally, you can specify the options to delete properties of an iam-user,
2438 including console-access, access-keys, attached-user-policies,
2439 inline-user-policies, mfa-devices, groups,
2440 ssh-keys, signing-certificates, and service-specific-credentials.
2441
2442 Note: using options will _not_ delete the user itself, only the items specified
2443 by ``options`` that are attached to the respective iam-user. To delete a user
2444 completely, use the ``delete`` action without specifying ``options``.
2445
2446 :example:
2447
2448 .. code-block:: yaml
2449
2450 - name: delete-console-access-unless-valid
2451 comment: |
2452 finds iam-users with console access and deletes console access unless
2453 the username is included in whitelist
2454 resource: iam-user
2455 filters:
2456 - type: value
2457 key: UserName
2458 op: not-in
2459 value:
2460 - valid-user-1
2461 - valid-user-2
2462 - type: credential
2463 key: password_enabled
2464 value: true
2465 actions:
2466 - type: delete
2467 options:
2468 - console-access
2469
2470 - name: delete-misc-access-for-iam-user
2471 comment: |
2472 deletes multiple options from test_user
2473 resource: iam-user
2474 filters:
2475 - UserName: test_user
2476 actions:
2477 - type: delete
2478 options:
2479 - mfa-devices
2480 - access-keys
2481 - ssh-keys
2482 """
2483
2484 ORDERED_OPTIONS = OrderedDict([
2485 ('console-access', 'delete_console_access'),
2486 ('access-keys', 'delete_access_keys'),
2487 ('attached-user-policies', 'delete_attached_user_policies'),
2488 ('inline-user-policies', 'delete_inline_user_policies'),
2489 ('mfa-devices', 'delete_hw_mfa_devices'),
2490 ('groups', 'delete_groups'),
2491 ('ssh-keys', 'delete_ssh_keys'),
2492 ('signing-certificates', 'delete_signing_certificates'),
2493 ('service-specific-credentials', 'delete_service_specific_credentials'),
2494 ])
2495 COMPOUND_OPTIONS = {
2496 'user-policies': ['attached-user-policies', 'inline-user-policies'],
2497 }
2498
2499 schema = type_schema(
2500 'delete',
2501 options={
2502 'type': 'array',
2503 'items': {
2504 'type': 'string',
2505 'enum': list(ORDERED_OPTIONS.keys()) + list(COMPOUND_OPTIONS.keys()),
2506 }
2507 })
2508
2509 permissions = (
2510 'iam:ListAttachedUserPolicies',
2511 'iam:ListAccessKeys',
2512 'iam:ListGroupsForUser',
2513 'iam:ListMFADevices',
2514 'iam:ListServiceSpecificCredentials',
2515 'iam:ListSigningCertificates',
2516 'iam:ListSSHPublicKeys',
2517 'iam:DeactivateMFADevice',
2518 'iam:DeleteAccessKey',
2519 'iam:DeleteLoginProfile',
2520 'iam:DeleteSigningCertificate',
2521 'iam:DeleteSSHPublicKey',
2522 'iam:DeleteUser',
2523 'iam:DeleteUserPolicy',
2524 'iam:DetachUserPolicy',
2525 'iam:RemoveUserFromGroup')
2526
2527 @staticmethod
2528 def delete_console_access(client, r):
2529 try:
2530 client.delete_login_profile(
2531 UserName=r['UserName'])
2532 except ClientError as e:
2533 if e.response['Error']['Code'] not in ('NoSuchEntity',):
2534 raise
2535
2536 @staticmethod
2537 def delete_access_keys(client, r):
2538 response = client.list_access_keys(UserName=r['UserName'])
2539 for access_key in response['AccessKeyMetadata']:
2540 client.delete_access_key(UserName=r['UserName'],
2541 AccessKeyId=access_key['AccessKeyId'])
2542
2543 @staticmethod
2544 def delete_attached_user_policies(client, r):
2545 response = client.list_attached_user_policies(UserName=r['UserName'])
2546 for user_policy in response['AttachedPolicies']:
2547 client.detach_user_policy(
2548 UserName=r['UserName'], PolicyArn=user_policy['PolicyArn'])
2549
2550 @staticmethod
2551 def delete_inline_user_policies(client, r):
2552 response = client.list_user_policies(UserName=r['UserName'])
2553 for user_policy_name in response['PolicyNames']:
2554 client.delete_user_policy(
2555 UserName=r['UserName'], PolicyName=user_policy_name)
2556
2557 @staticmethod
2558 def delete_hw_mfa_devices(client, r):
2559 response = client.list_mfa_devices(UserName=r['UserName'])
2560 for mfa_device in response['MFADevices']:
2561 client.deactivate_mfa_device(
2562 UserName=r['UserName'], SerialNumber=mfa_device['SerialNumber'])
2563
2564 @staticmethod
2565 def delete_groups(client, r):
2566 response = client.list_groups_for_user(UserName=r['UserName'])
2567 for user_group in response['Groups']:
2568 client.remove_user_from_group(
2569 UserName=r['UserName'], GroupName=user_group['GroupName'])
2570
2571 @staticmethod
2572 def delete_ssh_keys(client, r):
2573 response = client.list_ssh_public_keys(UserName=r['UserName'])
2574 for key in response.get('SSHPublicKeys', ()):
2575 client.delete_ssh_public_key(
2576 UserName=r['UserName'], SSHPublicKeyId=key['SSHPublicKeyId'])
2577
2578 @staticmethod
2579 def delete_signing_certificates(client, r):
2580 response = client.list_signing_certificates(UserName=r['UserName'])
2581 for cert in response.get('Certificates', ()):
2582 client.delete_signing_certificate(
2583 UserName=r['UserName'], CertificateId=cert['CertificateId'])
2584
2585 @staticmethod
2586 def delete_service_specific_credentials(client, r):
2587 # Service specific user credentials (codecommit)
2588 response = client.list_service_specific_credentials(UserName=r['UserName'])
2589 for screds in response.get('ServiceSpecificCredentials', ()):
2590 client.delete_service_specific_credential(
2591 UserName=r['UserName'],
2592 ServiceSpecificCredentialId=screds['ServiceSpecificCredentialId'])
2593
2594 @staticmethod
2595 def delete_user(client, r):
2596 client.delete_user(UserName=r['UserName'])
2597
2598 def process(self, resources):
2599 client = local_session(self.manager.session_factory).client('iam')
2600 self.log.debug('Deleting user %s options: %s' %
2601 (len(resources), self.data.get('options', 'all')))
2602 for r in resources:
2603 self.process_user(client, r)
2604
2605 def process_user(self, client, r):
2606 user_options = self.data.get('options', list(self.ORDERED_OPTIONS.keys()))
2607 # resolve compound options
2608 for cmd in self.COMPOUND_OPTIONS:
2609 if cmd in user_options:
2610 user_options += self.COMPOUND_OPTIONS[cmd]
2611 # process options in ordered fashion
2612 for cmd in self.ORDERED_OPTIONS:
2613 if cmd in user_options:
2614 op = getattr(self, self.ORDERED_OPTIONS[cmd])
2615 op(client, r)
2616 if not self.data.get('options'):
2617 self.delete_user(client, r)
2618
2619
2620@User.action_registry.register('remove-keys')
2621class UserRemoveAccessKey(BaseAction):
2622 """Delete or disable user's access keys.
2623
2624 For example if we wanted to disable keys after 90 days of non-use and
2625 delete them after 180 days of nonuse:
2626
2627 :example:
2628
2629 .. code-block:: yaml
2630
2631 - name: iam-mfa-active-key-no-login
2632 resource: iam-user
2633 actions:
2634 - type: remove-keys
2635 disable: true
2636 age: 90
2637 - type: remove-keys
2638 age: 180
2639 """
2640
2641 schema = type_schema(
2642 'remove-keys',
2643 matched={'type': 'boolean'},
2644 age={'type': 'number'},
2645 disable={'type': 'boolean'})
2646 permissions = ('iam:ListAccessKeys', 'iam:UpdateAccessKey',
2647 'iam:DeleteAccessKey')
2648
2649 def validate(self):
2650 if self.data.get('matched') and self.data.get('age'):
2651 raise PolicyValidationError(
2652 "policy:%s cant mix matched and age parameters")
2653 ftypes = {f.type for f in self.manager.iter_filters()}
2654 if 'credential' in ftypes and 'access-key' in ftypes:
2655 raise PolicyValidationError(
2656 "policy:%s cant mix credential and access-key filters w/ delete action")
2657 return self
2658
2659 def process(self, resources):
2660 client = local_session(self.manager.session_factory).client('iam')
2661
2662 age = self.data.get('age')
2663 disable = self.data.get('disable')
2664 matched = self.data.get('matched')
2665
2666 if age:
2667 threshold_date = datetime.datetime.now(tz=tzutc()) - timedelta(age)
2668
2669 for r in resources:
2670 if 'c7n:AccessKeys' not in r:
2671 r['c7n:AccessKeys'] = client.list_access_keys(
2672 UserName=r['UserName'])['AccessKeyMetadata']
2673
2674 keys = r['c7n:AccessKeys']
2675 if matched:
2676 m_keys = resolve_credential_keys(
2677 r.get(CredentialReport.matched_annotation_key),
2678 keys)
2679 # It is possible for a _user_ to match multiple credential filters
2680 # without having any single key match them all.
2681 if not m_keys:
2682 continue
2683 keys = m_keys
2684
2685 for k in keys:
2686 if age:
2687 if not k['CreateDate'] < threshold_date:
2688 continue
2689 if disable:
2690 client.update_access_key(
2691 UserName=r['UserName'],
2692 AccessKeyId=k['AccessKeyId'],
2693 Status='Inactive')
2694 else:
2695 client.delete_access_key(
2696 UserName=r['UserName'],
2697 AccessKeyId=k['AccessKeyId'])
2698
2699
2700@User.action_registry.register('delete-ssh-keys')
2701class UserDeleteSSHKey(BaseAction):
2702 """Delete or disable a user's SSH keys.
2703
2704 For example to delete keys after 90 days:
2705
2706 :example:
2707
2708 .. code-block:: yaml
2709
2710 - name: iam-user-delete-ssh-keys
2711 resource: iam-user
2712 actions:
2713 - type: delete-ssh-keys
2714 """
2715
2716 schema = type_schema(
2717 'delete-ssh-keys',
2718 matched={'type': 'boolean'},
2719 disable={'type': 'boolean'})
2720 annotation_key = 'c7n:SSHKeys'
2721 permissions = ('iam:ListSSHPublicKeys', 'iam:UpdateSSHPublicKey',
2722 'iam:DeleteSSHPublicKey')
2723
2724 def process(self, resources):
2725 client = local_session(self.manager.session_factory).client('iam')
2726
2727 for r in resources:
2728 if self.annotation_key not in r:
2729 r[self.annotation_key] = client.list_ssh_public_keys(
2730 UserName=r['UserName'])['SSHPublicKeys']
2731
2732 keys = (r.get(UserSSHKeyFilter.matched_annotation_key, [])
2733 if self.data.get('matched') else r[self.annotation_key])
2734
2735 for k in keys:
2736 if self.data.get('disable'):
2737 client.update_ssh_public_key(
2738 UserName=r['UserName'],
2739 SSHPublicKeyId=k['SSHPublicKeyId'],
2740 Status='Inactive')
2741 else:
2742 client.delete_ssh_public_key(
2743 UserName=r['UserName'],
2744 SSHPublicKeyId=k['SSHPublicKeyId'])
2745
2746
2747def resolve_credential_keys(m_keys, keys):
2748 res = []
2749 for k in m_keys:
2750 if k['c7n:match-type'] == 'credential':
2751 c_date = parse_date(k['last_rotated'])
2752 for ak in keys:
2753 if c_date == ak['CreateDate']:
2754 ak = dict(ak)
2755 ak['c7n:match-type'] = 'access'
2756 if ak not in res:
2757 res.append(ak)
2758 elif k not in res:
2759 res.append(k)
2760 return res
2761
2762
2763#################
2764# IAM Groups #
2765#################
2766
2767
2768@Group.filter_registry.register('has-specific-managed-policy')
2769class SpecificIamGroupManagedPolicy(Filter):
2770 """Filter IAM groups that have a specific policy attached
2771
2772 For example, if the user wants to check all groups with 'admin-policy':
2773
2774 :example:
2775
2776 .. code-block:: yaml
2777
2778 policies:
2779 - name: iam-groups-have-admin
2780 resource: iam-group
2781 filters:
2782 - type: has-specific-managed-policy
2783 value: admin-policy
2784 """
2785
2786 schema = type_schema('has-specific-managed-policy', value={'type': 'string'})
2787 permissions = ('iam:ListAttachedGroupPolicies',)
2788
2789 def _managed_policies(self, client, resource):
2790 return [r['PolicyName'] for r in client.list_attached_group_policies(
2791 GroupName=resource['GroupName'])['AttachedPolicies']]
2792
2793 def process(self, resources, event=None):
2794 c = local_session(self.manager.session_factory).client('iam')
2795 if self.data.get('value'):
2796 results = []
2797 for r in resources:
2798 r["ManagedPolicies"] = self._managed_policies(c, r)
2799 if self.data.get('value') in r["ManagedPolicies"]:
2800 results.append(r)
2801 return results
2802 return []
2803
2804
2805@Group.filter_registry.register('has-users')
2806class IamGroupUsers(Filter):
2807 """Filter IAM groups that have users attached based on True/False value:
2808 True: Filter all IAM groups with users assigned to it
2809 False: Filter all IAM groups without any users assigned to it
2810
2811 :example:
2812
2813 .. code-block:: yaml
2814
2815 - name: empty-iam-group
2816 resource: iam-group
2817 filters:
2818 - type: has-users
2819 value: False
2820 """
2821 schema = type_schema('has-users', value={'type': 'boolean'})
2822 permissions = ('iam:GetGroup',)
2823
2824 def _user_count(self, client, resource):
2825 return len(client.get_group(GroupName=resource['GroupName'])['Users'])
2826
2827 def process(self, resources, events=None):
2828 c = local_session(self.manager.session_factory).client('iam')
2829 if self.data.get('value', True):
2830 return [r for r in resources if self._user_count(c, r) > 0]
2831 return [r for r in resources if self._user_count(c, r) == 0]
2832
2833
2834@Group.filter_registry.register('has-inline-policy')
2835class IamGroupInlinePolicy(Filter):
2836 """Filter IAM groups that have an inline-policy based on boolean value:
2837 True: Filter all groups that have an inline-policy attached
2838 False: Filter all groups that do not have an inline-policy attached
2839
2840 :example:
2841
2842 .. code-block:: yaml
2843
2844 - name: iam-groups-with-inline-policy
2845 resource: iam-group
2846 filters:
2847 - type: has-inline-policy
2848 value: True
2849 """
2850 schema = type_schema('has-inline-policy', value={'type': 'boolean'})
2851 permissions = ('iam:ListGroupPolicies',)
2852
2853 def _inline_policies(self, client, resource):
2854 resource['c7n:InlinePolicies'] = client.list_group_policies(
2855 GroupName=resource['GroupName'])['PolicyNames']
2856 return resource
2857
2858 def process(self, resources, events=None):
2859 c = local_session(self.manager.session_factory).client('iam')
2860 value = self.data.get('value', True)
2861 res = []
2862 for r in resources:
2863 r = self._inline_policies(c, r)
2864 if len(r['c7n:InlinePolicies']) > 0 and value:
2865 res.append(r)
2866 if len(r['c7n:InlinePolicies']) == 0 and not value:
2867 res.append(r)
2868 return res
2869
2870
2871@Group.action_registry.register('delete-inline-policies')
2872class GroupInlinePolicyDelete(BaseAction):
2873 """Delete inline policies embedded in an IAM group.
2874
2875 :example:
2876
2877 .. code-block:: yaml
2878
2879 - name: iam-delete-group-policies
2880 resource: aws.iam-group
2881 filters:
2882 - type: value
2883 key: GroupName
2884 value: test
2885 actions:
2886 - type: delete-inline-policies
2887 """
2888 schema = type_schema('delete-inline-policies')
2889 permissions = ('iam:ListGroupPolicies', 'iam:DeleteGroupPolicy',)
2890
2891 def process(self, resources):
2892 client = local_session(self.manager.session_factory).client('iam')
2893 for r in resources:
2894 self.process_group(client, r)
2895
2896 def process_group(self, client, r):
2897 if 'c7n:InlinePolicies' not in r:
2898 r['c7n:InlinePolicies'] = client.list_group_policies(
2899 GroupName=r['GroupName'])['PolicyNames']
2900 for policy in r.get('c7n:InlinePolicies', []):
2901 try:
2902 self.manager.retry(client.delete_group_policy,
2903 GroupName=r['GroupName'], PolicyName=policy)
2904 except client.exceptions.NoSuchEntityException:
2905 continue
2906
2907
2908@Group.action_registry.register('delete')
2909class UserGroupDelete(BaseAction):
2910 """Delete an IAM User Group.
2911
2912 For example, if you want to delete a group named 'test'.
2913
2914 :example:
2915
2916 .. code-block:: yaml
2917
2918 - name: iam-delete-user-group
2919 resource: aws.iam-group
2920 filters:
2921 - type: value
2922 key: GroupName
2923 value: test
2924 actions:
2925 - type: delete
2926 force: True
2927 """
2928 schema = type_schema('delete', force={'type': 'boolean'})
2929 permissions = ('iam:DeleteGroup', 'iam:RemoveUserFromGroup')
2930
2931 def process(self, resources):
2932 client = local_session(self.manager.session_factory).client('iam')
2933 for r in resources:
2934 self.process_group(client, r)
2935
2936 def process_group(self, client, r):
2937 error = None
2938 force = self.data.get('force', False)
2939 if force:
2940 users = client.get_group(GroupName=r['GroupName']).get('Users', [])
2941 for user in users:
2942 client.remove_user_from_group(
2943 UserName=user['UserName'], GroupName=r['GroupName'])
2944
2945 try:
2946 client.delete_group(GroupName=r['GroupName'])
2947 except client.exceptions.DeleteConflictException as e:
2948 self.log.warning(
2949 ("Group:%s cannot be deleted, "
2950 "set force to remove all users from group")
2951 % r['Arn'])
2952 error = e
2953 except (client.exceptions.NoSuchEntityException,
2954 client.exceptions.UnmodifiableEntityException):
2955 pass
2956 if error:
2957 raise error
2958
2959
2960class SamlProviderDescribe(DescribeSource):
2961
2962 def augment(self, resources):
2963 super().augment(resources)
2964 for r in resources:
2965 md = r.get('SAMLMetadataDocument')
2966 if not md:
2967 continue
2968 root = sso_metadata(md)
2969 r['IDPSSODescriptor'] = root['IDPSSODescriptor']
2970 return resources
2971
2972 def get_permissions(self):
2973 return ('iam:GetSAMLProvider', 'iam:ListSAMLProviders')
2974
2975
2976def sso_metadata(md):
2977 root = ElementTree.fromstringlist(md)
2978 d = {}
2979 _sso_recurse(root, d)
2980 return d
2981
2982
2983def _sso_recurse(node, d):
2984 d.update(node.attrib)
2985 for c in node:
2986 k = c.tag.split('}', 1)[-1]
2987 cd = {}
2988 if k in d:
2989 if not isinstance(d[k], list):
2990 d[k] = [d[k]]
2991 d[k].append(cd)
2992 else:
2993 d[k] = cd
2994 _sso_recurse(c, cd)
2995 if node.text and node.text.strip():
2996 d['Value'] = node.text.strip()
2997
2998
2999@resources.register('iam-saml-provider')
3000class SamlProvider(QueryResourceManager):
3001 """SAML SSO Provider
3002
3003 we parse and expose attributes of the SAML Metadata XML Document
3004 as resources attribute for use with custodian's standard value filter.
3005 """
3006
3007 class resource_type(TypeInfo):
3008
3009 service = 'iam'
3010 name = id = 'Arn'
3011 enum_spec = ('list_saml_providers', 'SAMLProviderList', None)
3012 detail_spec = ('get_saml_provider', 'SAMLProviderArn', 'Arn', None)
3013 arn = 'Arn'
3014 arn_type = 'saml-provider'
3015 config_type = "AWS::IAM::SAMLProvider"
3016 global_resource = True
3017
3018 source_mapping = {'describe': SamlProviderDescribe}
3019
3020
3021class OpenIdDescribe(DescribeSource):
3022
3023 def get_permissions(self):
3024 return ('iam:GetOpenIDConnectProvider', 'iam:ListOpenIDConnectProviders')
3025
3026
3027@resources.register('iam-oidc-provider')
3028class OpenIdProvider(QueryResourceManager):
3029
3030 class resource_type(TypeInfo):
3031
3032 service = 'iam'
3033 name = id = 'Arn'
3034 enum_spec = ('list_open_id_connect_providers', 'OpenIDConnectProviderList', None)
3035 detail_spec = ('get_open_id_connect_provider', 'OpenIDConnectProviderArn', 'Arn', None)
3036 arn = 'Arn'
3037 arn_type = 'oidc-provider'
3038 global_resource = True
3039
3040 source_mapping = {'describe': OpenIdDescribe}
3041
3042
3043@OpenIdProvider.action_registry.register('delete')
3044class OpenIdProviderDelete(BaseAction):
3045 """Delete an OpenID Connect IAM Identity Provider
3046
3047 For example, if you want to automatically delete an OIDC IdP for example.com
3048
3049 :example:
3050
3051 .. code-block:: yaml
3052
3053 - name: aws-iam-oidc-provider-delete
3054 resource: iam-oidc-provider
3055 filters:
3056 - type: value
3057 key: Url
3058 value: example.com
3059 actions:
3060 - type: delete
3061
3062 """
3063 schema = type_schema('delete')
3064 permissions = ('iam:DeleteOpenIDConnectProvider',)
3065
3066 def process(self, resources):
3067 client = local_session(self.manager.session_factory).client('iam')
3068 for provider in resources:
3069 self.manager.retry(
3070 client.delete_open_id_connect_provider,
3071 OpenIDConnectProviderArn=provider['Arn'],
3072 ignore_err_codes=(
3073 'NoSuchEntityException',
3074 'DeleteConflictException',
3075 ),
3076 )
3077
3078
3079@InstanceProfile.filter_registry.register('has-specific-managed-policy')
3080class SpecificIamProfileManagedPolicy(ValueFilter):
3081 """Filter an IAM instance profile that contains an IAM role that has a specific managed IAM
3082 policy. If an IAM instance profile does not contain an IAM role, then it will be treated
3083 as not having the policy.
3084
3085 :example:
3086
3087 Check for instance profile roles with 'admin-policy' attached:
3088
3089 .. code-block:: yaml
3090
3091 policies:
3092 - name: iam-profiles-have-admin
3093 resource: aws.iam-profile
3094 filters:
3095 - type: has-specific-managed-policy
3096 value: admin-policy
3097
3098 :example:
3099
3100 Check for instance profile roles with an attached policy matching
3101 a given list:
3102
3103 .. code-block:: yaml
3104
3105 policies:
3106 - name: iam-profiles-with-selected-policies
3107 resource: aws.iam-profile
3108 filters:
3109 - type: has-specific-managed-policy
3110 value:
3111 - AmazonS3FullAccess
3112 - AWSOrganizationsFullAccess
3113
3114 :example:
3115
3116 Check for instance profile roles with attached policy names matching
3117 a pattern:
3118
3119 .. code-block:: yaml
3120
3121 policies:
3122 - name: iam-profiles-with-full-access-policies
3123 resource: aws.iam-profile
3124 filters:
3125 - type: has-specific-managed-policy
3126 op: glob
3127 value: "*FullAccess"
3128
3129 Check for instance profile roles with attached policy ARNs matching
3130 a pattern:
3131
3132 .. code-block:: yaml
3133
3134 policies:
3135 - name: iam-profiles-with-aws-full-access-policies
3136 resource: aws.iam-profile
3137 filters:
3138 - type: has-specific-managed-policy
3139 key: PolicyArn
3140 op: regex
3141 value: "arn:aws:iam::aws:policy/.*FullAccess"
3142 """
3143
3144 schema = type_schema('has-specific-managed-policy', rinherit=ValueFilter.schema)
3145 permissions = ('iam:ListAttachedRolePolicies',)
3146 annotation_key = 'c7n:AttachedPolicies'
3147 matched_annotation_key = 'c7n:MatchedPolicies'
3148 schema_alias = False
3149
3150 def __init__(self, data, manager=None):
3151 # Preserve backward compatibility
3152 if 'key' not in data:
3153 data['key'] = 'PolicyName'
3154 super(SpecificIamProfileManagedPolicy, self).__init__(data, manager)
3155
3156 # This code assumes a single IAM role in the profile and the annotation
3157 # is added to the profile resource, not the role
3158 def get_managed_policies(self, client, prof_set):
3159 for prof in prof_set:
3160 prof[self.annotation_key] = []
3161 for role in prof.get('Roles', []):
3162 prof[self.annotation_key] = [
3163 role_policy
3164 for role_policy
3165 in client.list_attached_role_policies(
3166 RoleName=role['RoleName'])['AttachedPolicies']
3167 ]
3168
3169 def process(self, resources, event=None):
3170 client = local_session(self.manager.session_factory).client('iam')
3171 with self.executor_factory(max_workers=2) as w:
3172 augment_set = [r for r in resources if self.annotation_key not in r]
3173 self.log.debug(
3174 "Querying %d roles' attached policies" % len(augment_set))
3175 list(w.map(
3176 functools.partial(self.get_managed_policies, client),
3177 chunks(augment_set, 50)))
3178
3179 matched = []
3180 for r in resources:
3181 matched_keys = [k for k in r[self.annotation_key] if self.match(k)]
3182 self.merge_annotation(r, self.matched_annotation_key, matched_keys)
3183 if matched_keys:
3184 matched.append(r)
3185 return matched