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