├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── multi-account-deployment ├── AccessAnalyzer.png ├── README.md ├── architecture-diagram-multi-account-setup.png ├── hub.template.yaml └── stackset.template.yaml └── quicksight-dashboard-deployment └── quicksight-hub.template.yaml /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: ## Display this help screen. 2 | @grep -h -E '^[1-9a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "· \033[36m%-30s\033[0m %s\n", $$1, $$2}' 3 | 4 | deploy-hub: ## Deploys the hub.template.yaml to the central security account using parameter aws-region. 5 | $(eval pOrgId := $(shell aws organizations describe-organization --query "Organization.Id" | tr -d '"')) 6 | aws cloudformation deploy \ 7 | --stack-name access-analyzer-policy-validation-findings \ 8 | --template-file multi-account-deployment/hub.template.yaml \ 9 | --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \ 10 | --parameter-overrides pOrgId=$(pOrgId) pS3BucketName=access-analyzer-findings-$(pOrgId) \ 11 | --region $(aws-region) \ 12 | --no-fail-on-empty-changeset 13 | 14 | deploy-dashboard-hub: ## Deploys the quicksight-hub.template.yaml to the central security account using parameter quicksight-user-arn. 15 | aws cloudformation deploy \ 16 | --stack-name access-analyzer-findings-dashboard \ 17 | --template-file quicksight-dashboard-deployment/quicksight-hub.template.yaml \ 18 | --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \ 19 | --parameter-overrides pQuickSightUserNameArn=$(quicksight-user-arn) \ 20 | --region $(aws-region) 21 | 22 | describe-hub-outputs: ## Describes the hub.template.yaml CloudFormation Outputs using parameter aws-region. 23 | aws cloudformation describe-stacks \ 24 | --stack-name access-analyzer-policy-validation-findings \ 25 | --region $(aws-region) \ 26 | --query "Stacks[0].Outputs" 27 | 28 | delete-hub: ## Deletes the CloudFormation stack deployed using the template hub.template.yaml from the central security account using parameter aws-region. 29 | aws cloudformation delete-stack \ 30 | --stack-name access-analyzer-policy-validation-findings \ 31 | --region $(aws-region) 32 | 33 | deploy-members: ## Creates a StackSet and StackSet instances using the stackset.template.yaml file using parameters SQSQueueUrl, KMSKeyArn and aws-region. You must run this command in the AWS management account. 34 | $(eval pRootId := $(shell aws organizations list-roots --query "Roots[0].Id" | tr -d '"')) 35 | aws cloudformation create-stack-set \ 36 | --stack-set-name access-analyzer-policy-validation-resources \ 37 | --template-body file://multi-account-deployment/stackset.template.yaml \ 38 | --parameters ParameterKey=pSQSQueueUrl,ParameterValue=$(SQSQueueUrl) ParameterKey=pKMSKeyArn,ParameterValue=$(KMSKeyArn) \ 39 | --capabilities CAPABILITY_NAMED_IAM \ 40 | --permission-model SERVICE_MANAGED \ 41 | --auto-deployment Enabled=true,RetainStacksOnAccountRemoval=false \ 42 | --region $(aws-region) && \ 43 | aws cloudformation create-stack-instances \ 44 | --stack-set-name access-analyzer-policy-validation-resources \ 45 | --regions $(aws-region) \ 46 | --deployment-targets OrganizationalUnitIds=$(pRootId) \ 47 | --region $(aws-region) 48 | 49 | delete-stackset: ## Deletes created StackSet using parameter aws-region. You must run this command in the AWS management account. 50 | aws cloudformation delete-stack-set \ 51 | --stack-set-name access-analyzer-policy-validation-resources \ 52 | --region $(aws-region) 53 | 54 | delete-stackset-instances: ## Deletes created StackSet instances using parameter aws-region. You must run this command in the AWS management account. 55 | $(eval pRootId := $(shell aws organizations list-roots --query "Roots[0].Id" | tr -d '"')) 56 | aws cloudformation delete-stack-instances \ 57 | --stack-set-name access-analyzer-policy-validation-resources \ 58 | --deployment-targets OrganizationalUnitIds=$(pRootId) \ 59 | --regions $(aws-region) \ 60 | --no-retain-stacks \ 61 | --region $(aws-region) 62 | 63 | delete-dashboard: ## Deletes the CloudFormation stack access-analyzer-findings-dashboard from your AWS account using parameter aws-region. 64 | aws cloudformation delete-stack \ 65 | --stack-name access-analyzer-findings-dashboard \ 66 | --region $(aws-region) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Visualize AWS IAM Access Analyzer Policy Validation Findings 2 | 3 | In this implementation, we show you how to create an [Amazon QuickSight](https://aws.amazon.com/quicksight) dashboard to visualize the policy validation findings from [AWS Identity and Access Management (IAM) Access Analyzer](https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html). You can use this dashboard to better understand your policies and how to achieve [least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege) by periodically validating your [IAM](https://aws.amazon.com/iam/) roles against IAM best practices. This blog post walks you through the deployment for a multi-account environment using [AWS Organizations](https://aws.amazon.com/organizations/). 4 | 5 | Achieving least privilege is a continuous cycle to grant only the permissions that your users and systems require. To achieve least privilege, you start by setting fine-grained permissions. Then, you verify that the existing access meets your intent. Finally, you refine permissions by removing unused access. To learn more, see [IAM Access Analyzer makes it easier to implement least privilege permissions by generating IAM policies based on access activity](https://aws.amazon.com/blogs/security/iam-access-analyzer-makes-it-easier-to-implement-least-privilege-permissions-by-generating-iam-policies-based-on-access-activity/). 6 | 7 | [Policy validation](https://aws.amazon.com/blogs/aws/iam-access-analyzer-update-policy-validation/) is a feature of IAM Access Analyzer that guides you to author and validate secure and functional policies with more than 100 policy checks. You can use these checks when creating new policies or to validate existing policies. To learn how to use IAM Access Analyzer policy validation APIs when creating new policies, see [Validate IAM policies in CloudFormation templates using IAM Access Analyzer](https://aws.amazon.com/blogs/security/validate-iam-policies-in-cloudformation-templates-using-iam-access-analyzer/). In this post, we focus on how to validate existing IAM policies. 8 | 9 | For prerequisites and instructions for deploying and using this implementation, see the related AWS security blog post : [How to visualize IAM Access Analyzer policy validation findings with QuickSight](https://aws.amazon.com/blogs/security/how-to-visualize-iam-access-analyzer-policy-validation-findings-with-quicksight/). 10 | 11 | ## Overview 12 | 13 | ![Architecture Diagram Multi-account Setup](multi-account-deployment/architecture-diagram-multi-account-setup.png "Architecture Diagram") 14 | 15 | ## Security 16 | 17 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 18 | 19 | ## License 20 | 21 | This library is licensed under the MIT-0 License. See the [LICENSE](LICENSE) file. 22 | -------------------------------------------------------------------------------- /multi-account-deployment/AccessAnalyzer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/visualize-iam-access-analyzer-policy-validation-findings/d144581ed4dcd69ae2682bc25a9456f4d4e543fb/multi-account-deployment/AccessAnalyzer.png -------------------------------------------------------------------------------- /multi-account-deployment/README.md: -------------------------------------------------------------------------------- 1 | ### Multi-Account AWS Environment 2 | ![Architecture Diagram Multi-account Setup](architecture-diagram-multi-account-setup.png "Architecture Diagram") -------------------------------------------------------------------------------- /multi-account-deployment/architecture-diagram-multi-account-setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/visualize-iam-access-analyzer-policy-validation-findings/d144581ed4dcd69ae2682bc25a9456f4d4e543fb/multi-account-deployment/architecture-diagram-multi-account-setup.png -------------------------------------------------------------------------------- /multi-account-deployment/hub.template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: >- 3 | Deploys AWS resources to a central AWS account to analyze AWS IAM policies using IAM Access Analyzer ValidatePolicy API call. 4 | Evaluation results are stored in an Amazon S3 bucket. 5 | The results are queried via Amazon Athena. 6 | 7 | Parameters: 8 | pOrgId: 9 | Description: Organization Identifier. 10 | Type: String 11 | 12 | pS3BucketName: 13 | Description: S3 bucket name to store AWS IAM Access Analyzer findings. 14 | Type: String 15 | pLambdaPowertoolsPythonVersion: 16 | Type: String 17 | Default: 39 18 | 19 | Resources: 20 | ############################ 21 | # Amazon SQS resources # 22 | ############################ 23 | CFQueue: 24 | Type: "AWS::SQS::Queue" 25 | Properties: 26 | VisibilityTimeout: 900 27 | Tags: 28 | - Key: "CloudFormation::StackId" 29 | Value: !Ref AWS::StackId 30 | - Key: "CloudFormation::StackName" 31 | Value: !Ref AWS::StackName 32 | KmsMasterKeyId: !Ref KMSKey 33 | RedrivePolicy: 34 | deadLetterTargetArn: !GetAtt CFDeadLetterQueue.Arn 35 | maxReceiveCount: 5 36 | CFDeadLetterQueue: 37 | Type: AWS::SQS::Queue 38 | Properties: 39 | KmsMasterKeyId: !Ref KMSKey 40 | DelaySeconds: 60 41 | MessageRetentionPeriod: 1209600 42 | CFSQSPolicy: 43 | Type: AWS::SQS::QueuePolicy 44 | Metadata: 45 | cfn_nag: 46 | rules_to_suppress: 47 | - id: F21 48 | reason: "PrincipalOrgId condition is used." 49 | Properties: 50 | Queues: 51 | - !Ref CFQueue 52 | PolicyDocument: 53 | Statement: 54 | - Action: 55 | - "SQS:SendMessage" 56 | Effect: "Allow" 57 | Resource: !GetAtt CFQueue.Arn 58 | Principal: 59 | AWS: "*" 60 | Condition: 61 | StringEquals: 62 | aws:PrincipalOrgID: !Ref pOrgId 63 | CloudWatchAlarmForDLQ: 64 | Type: AWS::CloudWatch::Alarm 65 | Properties: 66 | AlarmDescription: "This alarm triggers when the IAM Access Analyzer policy validation SQS queue fails to handle a message." 67 | ComparisonOperator: "GreaterThanThreshold" 68 | DatapointsToAlarm: "1" 69 | Dimensions: 70 | - Name: "QueueName" 71 | Value: !GetAtt CFDeadLetterQueue.QueueName 72 | EvaluationPeriods: "1" 73 | MetricName: "ApproximateNumberOfMessagesVisible" 74 | Namespace: "AWS/SQS" 75 | Period: 300 76 | Statistic: "Sum" 77 | Threshold: "10" 78 | TreatMissingData: missing 79 | AlarmActions: 80 | - !Ref CWAlarmDLQTopic 81 | CWAlarmDLQTopic: 82 | Metadata: 83 | cfn_nag: 84 | rules_to_suppress: 85 | - id: W47 86 | reason: "SNS containes an alarm message for DLQ only" 87 | Type: AWS::SNS::Topic 88 | 89 | ################# 90 | # AWS KMS # 91 | ################# 92 | KMSKey: 93 | Type: AWS::KMS::Key 94 | Metadata: 95 | cfn_nag: 96 | rules_to_suppress: 97 | - id: F76 98 | reason: "PrincipalOrgId condition is used." 99 | Properties: 100 | EnableKeyRotation: true 101 | PendingWindowInDays: 20 102 | KeyPolicy: 103 | Version: "2012-10-17" 104 | Id: key-default-policy 105 | Statement: 106 | - Sid: Enable IAM Permissions 107 | Effect: Allow 108 | Principal: 109 | AWS: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root 110 | Action: kms:* 111 | Resource: "*" 112 | - Sid: Enable cross-account key usage within the org 113 | Effect: Allow 114 | Principal: 115 | AWS: "*" 116 | Action: 117 | - kms:Decrypt 118 | - kms:DescribeKey 119 | - kms:GenerateDataKey 120 | Resource: "*" 121 | Condition: 122 | StringEquals: 123 | aws:PrincipalOrgID: !Ref pOrgId 124 | StringLike: 125 | aws:PrincipalArn: !Sub arn:${AWS::Partition}:iam::*:role/access-analyzer/* 126 | 127 | - Sid: Enable Log groups encryption. 128 | Effect: Allow 129 | Principal: 130 | Service: !Sub "logs.${AWS::Region}.amazonaws.com" 131 | Action: 132 | - kms:Encrypt* 133 | - kms:Decrypt* 134 | - kms:ReEncrypt* 135 | - kms:GenerateDataKey* 136 | - kms:Describe* 137 | Resource: "*" 138 | Condition: 139 | ArnLike: 140 | kms:EncryptionContext:aws:logs:arn: !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:*" 141 | - Sid: Allow S3 to use the key 142 | Effect: Allow 143 | Principal: 144 | Service: s3.amazonaws.com 145 | Action: 146 | - kms:generatedatakey* 147 | - kms:decrypt 148 | Resource: "*" 149 | - Sid: QuickSight Grant 150 | Effect: Allow 151 | Principal: 152 | AWS: !GetAtt QuickSightServiceRole.Arn 153 | Action: 154 | - kms:CreateGrant 155 | - kms:ListGrants 156 | - kms:RevokeGrant 157 | Resource: "*" 158 | Condition: 159 | Bool: 160 | kms:GrantIsForAWSResource: true 161 | - Sid: QuickSight Allow use of the key 162 | Effect: Allow 163 | Principal: 164 | AWS: !GetAtt QuickSightServiceRole.Arn 165 | Action: 166 | - kms:Encrypt* 167 | - kms:Decrypt* 168 | - kms:ReEncrypt* 169 | - kms:GenerateDataKey* 170 | - kms:Describe* 171 | Resource: "*" 172 | KMSKeyAlias: 173 | Type: "AWS::KMS::Alias" 174 | Properties: 175 | AliasName: alias/access-analyzer-findings-key 176 | TargetKeyId: !Ref KMSKey 177 | 178 | ################################################################################# 179 | # AWS Lambda function validate-iam-policy-for-access-analyzer resources # 180 | ################################################################################# 181 | LambdaFunctionValidatePolicies: 182 | Type: AWS::Lambda::Function 183 | Metadata: 184 | cfn_nag: 185 | rules_to_suppress: 186 | - id: W89 187 | reason: "Serverlesss implementation. Does not require to be deployed in a VPC." 188 | Properties: 189 | FunctionName: access-analyzer-validate-iam-policy 190 | Layers: 191 | - !Sub "arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:${pLambdaPowertoolsPythonVersion}" 192 | Runtime: python3.9 193 | Architectures: 194 | - x86_64 195 | MemorySize: 1024 196 | Role: !GetAtt LambdaRoleValidatePolicies.Arn 197 | Handler: index.handler 198 | Timeout: 900 199 | Code: 200 | ZipFile: | 201 | import json 202 | import gzip 203 | import boto3 204 | import os 205 | from datetime import datetime 206 | from aws_lambda_powertools import Logger 207 | from aws_lambda_powertools.utilities.batch import BatchProcessor, EventType 208 | from aws_lambda_powertools.utilities.data_classes.sqs_event import SQSRecord 209 | from aws_lambda_powertools.utilities.typing import LambdaContext 210 | from botocore.config import Config 211 | 212 | config = Config( 213 | retries = { 214 | 'max_attempts': 10, 215 | 'mode': 'standard' 216 | } 217 | ) 218 | 219 | logger = Logger() 220 | accessanalyzer_client = boto3.client('accessanalyzer', config=config) 221 | s3 = boto3.resource("s3") 222 | S3_BUCKET = os.environ['S3_BUCKET'] 223 | processor = BatchProcessor(event_type=EventType.SQS) 224 | 225 | def record_handler(record: SQSRecord): 226 | finding = {} 227 | finding["policyArn"] = record.message_attributes["policyArn"].string_value 228 | finding["accountId"] = finding["policyArn"].split(":")[4] 229 | finding["policyType"] = record.message_attributes["policyType"].string_value 230 | finding["path"] = record.message_attributes["policyPath"].string_value 231 | finding["policyId"] = record.message_attributes["policyId"].string_value 232 | finding["policyName"] = record.message_attributes["policyName"].string_value 233 | finding["defaultVersionId"] = record.message_attributes["policyDefaultVersionId"].string_value 234 | finding["attachmentCount"] = record.message_attributes["policyAttachmentCount"].string_value 235 | finding["permissionsBoundaryUsageCount"] = record.message_attributes["policyPermissionsBoundaryUsageCount"].string_value 236 | access_analyzer_response = accessanalyzer_client.validate_policy( 237 | policyDocument=record["body"], 238 | policyType=finding["policyType"] 239 | ) 240 | finding["access_analyzer_findings"] = access_analyzer_response["findings"] 241 | 242 | # add date and time when policy was analyzed 243 | date_time_now = datetime.now() 244 | date_time_string = date_time_now.isoformat() 245 | finding["validatedAt"] = date_time_string 246 | return finding 247 | 248 | def upload_findings(findings: list) -> None: 249 | try: 250 | date_time_string = datetime.now() 251 | date_string = date_time_string.strftime("%Y/%m/%d") 252 | file_path = f"AWSAccessAnalyzerFindings/{date_string}/AccessAnalyzerOutput/" 253 | file_name = f"AccessAnalyzer_Output_{date_time_string.isoformat()}.gz" 254 | s3_key = file_path + file_name 255 | result = "\n".join([json.dumps(finding) for finding in findings]) 256 | encoded_string = result.encode("utf-8") 257 | gzip_string = gzip.compress(encoded_string) 258 | s3.Bucket(S3_BUCKET).put_object(Key=s3_key, Body=gzip_string) 259 | logger.info(f"SUCCESS - export to S3 bucket s3://{S3_BUCKET}/{file_path}/{file_name}") 260 | 261 | except Exception as e: 262 | logger.error(f"FAILED - export to S3 bucket s3://{S3_BUCKET}/{file_path}/{file_name}") 263 | logger.error(str(e)) 264 | raise Exception(f"Unable to upload findings to S3.") 265 | 266 | @logger.inject_lambda_context 267 | def handler(event, context: LambdaContext): 268 | batch = event["Records"] 269 | findings = [] 270 | logger.info(f"Processing {len(batch)} records.") 271 | with processor(records=batch, handler=record_handler): 272 | processed_messages = processor.process() # kick off processing, return list[tuple] 273 | 274 | for message in processed_messages: 275 | status: Union[Literal["success"], Literal["fail"]] = message[0] 276 | result: Any = message[1] 277 | if status == "success": 278 | findings.append(result) 279 | 280 | upload_findings(findings) 281 | logger.debug(f'Findings : {findings}') 282 | return processor.response() 283 | 284 | Environment: 285 | Variables: 286 | SQS_QUEUE_URL: !Ref CFQueue 287 | S3_BUCKET: !Ref pS3BucketName 288 | LOG_LEVEL: INFO 289 | POWERTOOLS_SERVICE_NAME: access_analyzer_findings 290 | Description: !Sub "Lambda function to validate customer IAM policies and save output to a S3 bucket: ${pS3BucketName}" 291 | KmsKeyArn: !GetAtt KMSKey.Arn 292 | LambdaRoleValidatePolicies: 293 | Type: AWS::IAM::Role 294 | Metadata: 295 | cfn_nag: 296 | rules_to_suppress: 297 | - id: W11 298 | reason: 'Access Analyzer ValidatePolicy API Call does not support a resource. you must specify all resources ("*") in the Resource element of your policy statement.' 299 | Properties: 300 | ManagedPolicyArns: 301 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 302 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole" 303 | Path: /access-analyzer/ 304 | AssumeRolePolicyDocument: 305 | Version: 2012-10-17 306 | Statement: 307 | - Action: 308 | - "sts:AssumeRole" 309 | Effect: Allow 310 | Principal: 311 | Service: 312 | - lambda.amazonaws.com 313 | Policies: 314 | - PolicyName: "ValidateIAMPolicyUsingAccessAnalyzer" 315 | PolicyDocument: 316 | Version: "2012-10-17" 317 | Statement: 318 | - Effect: "Allow" 319 | Action: 320 | - "kms:Decrypt" 321 | - "kms:DescribeKey" 322 | - "kms:GenerateDataKey" 323 | Resource: !GetAtt KMSKey.Arn 324 | - Effect: "Allow" 325 | Action: 326 | - "access-analyzer:ValidatePolicy" 327 | Resource: "*" 328 | - Effect: "Allow" 329 | Action: 330 | - "s3:PutObject" 331 | Resource: 332 | - !Sub "arn:${AWS::Partition}:s3:::${pS3BucketName}/*" 333 | - !Sub "arn:${AWS::Partition}:s3:::${pS3BucketName}" 334 | LambdaFunctionValidateLogGroup: 335 | Type: "AWS::Logs::LogGroup" 336 | Properties: 337 | RetentionInDays: 14 338 | LogGroupName: !Sub "/aws/lambda/${LambdaFunctionValidatePolicies}" 339 | KmsKeyId: !GetAtt KMSKey.Arn 340 | LambdaEventSourceMapping: 341 | Type: AWS::Lambda::EventSourceMapping 342 | Properties: 343 | BatchSize: 100 344 | MaximumBatchingWindowInSeconds: 100 345 | FunctionResponseTypes: 346 | - ReportBatchItemFailures 347 | FunctionName: !Ref LambdaFunctionValidatePolicies 348 | EventSourceArn: !GetAtt CFQueue.Arn 349 | 350 | ######################################## 351 | # Amazon S3 bucket to store findings # 352 | ######################################## 353 | s3Bucket: 354 | Type: AWS::S3::Bucket 355 | Metadata: 356 | cfn_nag: 357 | rules_to_suppress: 358 | - id: W51 359 | reason: "Access intended only within the same account" 360 | Properties: 361 | BucketName: !Ref pS3BucketName 362 | AccessControl: Private 363 | VersioningConfiguration: 364 | Status: Enabled 365 | LoggingConfiguration: 366 | DestinationBucketName: !Ref LoggingBucket 367 | PublicAccessBlockConfiguration: 368 | BlockPublicAcls: true 369 | BlockPublicPolicy: true 370 | IgnorePublicAcls: true 371 | RestrictPublicBuckets: true 372 | BucketEncryption: 373 | ServerSideEncryptionConfiguration: 374 | - ServerSideEncryptionByDefault: 375 | SSEAlgorithm: aws:kms 376 | KMSMasterKeyID: !GetAtt KMSKey.Arn 377 | BucketKeyEnabled: true 378 | LifecycleConfiguration: 379 | Rules: 380 | - Status: Enabled 381 | ExpirationInDays: 365 382 | s3BucketPolicy: 383 | Type: AWS::S3::BucketPolicy 384 | Properties: 385 | Bucket: !Ref s3Bucket 386 | PolicyDocument: 387 | Version: "2012-10-17" 388 | Statement: 389 | - Sid: Require Secure Transport 390 | Action: "s3:*" 391 | Effect: Deny 392 | Resource: 393 | - !Sub "arn:${AWS::Partition}:s3:::${s3Bucket}" 394 | - !Sub "arn:${AWS::Partition}:s3:::${s3Bucket}/*" 395 | Condition: 396 | Bool: 397 | "aws:SecureTransport": "false" 398 | Principal: "*" 399 | LoggingBucket: 400 | Type: "AWS::S3::Bucket" 401 | Metadata: 402 | cfn_nag: 403 | rules_to_suppress: 404 | - id: W51 405 | reason: "Access intended only within the same account" 406 | - id: W35 407 | reason: "This is a S3 bucket to store access logs from s3Bucket." 408 | Properties: 409 | AccessControl: LogDeliveryWrite 410 | OwnershipControls: 411 | Rules: 412 | - ObjectOwnership: BucketOwnerPreferred 413 | VersioningConfiguration: 414 | Status: Enabled 415 | PublicAccessBlockConfiguration: 416 | BlockPublicAcls: true 417 | BlockPublicPolicy: true 418 | IgnorePublicAcls: true 419 | RestrictPublicBuckets: true 420 | BucketEncryption: 421 | ServerSideEncryptionConfiguration: 422 | - ServerSideEncryptionByDefault: 423 | SSEAlgorithm: AES256 424 | LifecycleConfiguration: 425 | Rules: 426 | - Status: Enabled 427 | ExpirationInDays: 365 428 | LoggingBucketBucketPolicy: 429 | Type: AWS::S3::BucketPolicy 430 | Properties: 431 | Bucket: !Ref LoggingBucket 432 | PolicyDocument: 433 | Version: "2012-10-17" 434 | Statement: 435 | - Sid: Require Secure Transport 436 | Action: "s3:*" 437 | Effect: Deny 438 | Resource: 439 | - !Sub "arn:${AWS::Partition}:s3:::${LoggingBucket}" 440 | - !Sub "arn:${AWS::Partition}:s3:::${LoggingBucket}/*" 441 | Condition: 442 | Bool: 443 | "aws:SecureTransport": "false" 444 | Principal: "*" 445 | 446 | ########################### 447 | # AWS Glue resources # 448 | ########################### 449 | GlueDatabase: 450 | Type: AWS::Glue::Database 451 | Properties: 452 | CatalogId: !Ref AWS::AccountId 453 | DatabaseInput: 454 | Name: "access_analyzer_findings" 455 | GlueTable: 456 | Type: AWS::Glue::Table 457 | Properties: 458 | CatalogId: !Ref AWS::AccountId 459 | DatabaseName: !Ref GlueDatabase 460 | TableInput: 461 | Name: access_analyzer_findings 462 | Owner: owner 463 | Retention: 0 464 | StorageDescriptor: 465 | Location: !Sub s3://${pS3BucketName}/ 466 | Columns: 467 | - Name: accountId 468 | Type: string 469 | - Name: policyarn 470 | Type: string 471 | - Name: policytype 472 | Type: string 473 | - Name: path 474 | Type: string 475 | - Name: policyid 476 | Type: string 477 | - Name: policyname 478 | Type: string 479 | - Name: defaultversionid 480 | Type: string 481 | - Name: attachmentcount 482 | Type: string 483 | - Name: permissionsboundaryusagecount 484 | Type: string 485 | - Name: validatedAt 486 | Type: string 487 | - Name: access_analyzer_findings 488 | Type: array>,span:struct,start:struct>>>>> 489 | 490 | InputFormat: org.apache.hadoop.mapred.TextInputFormat 491 | OutputFormat: g.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat 492 | Compressed: true 493 | NumberOfBuckets: -1 494 | SerdeInfo: 495 | SerializationLibrary: org.openx.data.jsonserde.JsonSerDe 496 | Parameters: 497 | paths: "accountId,AttachmentCount,DefaultVersionId,Path,PermissionsBoundaryUsageCount,access_analyzer_findings,policyArn,policyId,policyName,policyType" 498 | BucketColumns: [] 499 | SortColumns: [] 500 | StoredAsSubDirectories: false 501 | PartitionKeys: 502 | - Name: datehour 503 | Type: string 504 | Parameters: 505 | projection.enabled: true 506 | projection.datehour.type: "date" 507 | projection.datehour.range: "2022/01/01,NOW" 508 | projection.datehour.format: "yyyy/MM/dd" 509 | projection.datehour.interval: "1" 510 | projection.datehour.interval.unit: "DAYS" 511 | storage.location.template: !Sub "s3://${pS3BucketName}/AWSAccessAnalyzerFindings/${!datehour}/AccessAnalyzerOutput" 512 | classification: json 513 | compressionType: gzip 514 | typeOfData: file 515 | TableType: EXTERNAL_TABLE 516 | GlueAccessAnalzyerFindingTable: 517 | DependsOn: GlueTable 518 | Type: "AWS::Glue::Table" 519 | Properties: 520 | CatalogId: !Ref AWS::AccountId 521 | DatabaseName: !Ref GlueDatabase 522 | TableInput: 523 | Description: Athena View Table for Access Analyzer Findings 524 | Name: view_access_analyzer_finding 525 | Parameters: 526 | presto_view: "true" 527 | comment: Presto View 528 | PartitionKeys: [] 529 | StorageDescriptor: 530 | Columns: 531 | - Name: accountId 532 | Type: string 533 | - Name: policyarn 534 | Type: string 535 | - Name: policytype 536 | Type: string 537 | - Name: path 538 | Type: string 539 | - Name: policyid 540 | Type: string 541 | - Name: policyname 542 | Type: string 543 | - Name: latest_validatedat 544 | Type: string 545 | - Name: latest_datehour 546 | Type: string 547 | - Name: findingtype 548 | Type: string 549 | - Name: issuecode 550 | Type: string 551 | - Name: findingdetails 552 | Type: string 553 | - Name: learnmorelink 554 | Type: string 555 | InputFormat: "" 556 | Location: "" 557 | NumberOfBuckets: 0 558 | OutputFormat: "" 559 | SerdeInfo: {} 560 | TableType: VIRTUAL_VIEW 561 | ViewExpandedText: /* Presto View */ 562 | ViewOriginalText: !Join 563 | - "" 564 | - - "/* Presto View: " 565 | - !Base64 >- 566 | {"originalSql":"SELECT policy.policyArn, policy.accountId, policy.policytype, 567 | policy.path, policy.policyid, policy.policyname, policy.latest_validatedat, policy.latest_datehour, 568 | finding.findingtype, finding.issuecode, 569 | finding.findingdetails, finding.learnmorelink FROM 570 | ((\"access_analyzer_findings\".\"access_analyzer_findings\" CROSS 571 | JOIN UNNEST(access_analyzer_findings) t (finding)) RIGHT JOIN 572 | ( SELECT \"max\"(validatedat) latest_validatedat, \"max\"(datehour) latest_datehour, accountId, policyname, policyid , policyArn, policytype , path 573 | FROM 574 | \"access_analyzer_findings\".\"access_analyzer_findings\" 575 | GROUP BY policyid, accountId,policyname, policyid, policyArn, policytype, path) policy ON (validatedat = 576 | policy.latest_validatedat))","catalog":"awsdatacatalog","schema":"access_analyzer_findings","columns":[{"name":"policyArn","type":"varchar"},{"name":"accountId","type":"varchar"},{"name":"policytype","type":"varchar"},{"name":"path","type":"varchar"},{"name":"policyid","type":"varchar"},{"name":"policyname","type":"varchar"},{"name":"latest_validatedat","type":"varchar"},{"name":"latest_datehour","type":"varchar"},{"name":"findingtype","type":"varchar"},{"name":"issuecode","type":"varchar"},{"name":"findingdetails","type":"varchar"},{"name":"learnmorelink","type":"varchar"}]} 577 | - " */" 578 | 579 | ########################### 580 | # Amazon Athena resources # 581 | ########################### 582 | AthenaWorkGroupS3Bucket: 583 | Type: AWS::S3::Bucket 584 | Metadata: 585 | cfn_nag: 586 | rules_to_suppress: 587 | - id: W51 588 | reason: "Access intended only within the same account" 589 | Properties: 590 | BucketName: !Sub aws-athena-query-results-${AWS::AccountId}-access-analyzer 591 | LoggingConfiguration: 592 | DestinationBucketName: !Ref LoggingBucket 593 | PublicAccessBlockConfiguration: 594 | BlockPublicAcls: true 595 | IgnorePublicAcls: true 596 | BlockPublicPolicy: true 597 | RestrictPublicBuckets: true 598 | VersioningConfiguration: 599 | Status: Enabled 600 | BucketEncryption: 601 | ServerSideEncryptionConfiguration: 602 | - ServerSideEncryptionByDefault: 603 | SSEAlgorithm: AES256 604 | OwnershipControls: 605 | Rules: 606 | - ObjectOwnership: BucketOwnerPreferred 607 | LifecycleConfiguration: 608 | Rules: 609 | - Status: Enabled 610 | ExpirationInDays: 365 611 | AthenaWorkGroupS3BucketPolicy: 612 | Type: AWS::S3::BucketPolicy 613 | Properties: 614 | Bucket: !Ref AthenaWorkGroupS3Bucket 615 | PolicyDocument: 616 | Version: "2012-10-17" 617 | Statement: 618 | - Sid: Require Secure Transport 619 | Action: "s3:*" 620 | Effect: Deny 621 | Resource: 622 | - !Sub "arn:${AWS::Partition}:s3:::${AthenaWorkGroupS3Bucket}" 623 | - !Sub "arn:${AWS::Partition}:s3:::${AthenaWorkGroupS3Bucket}/*" 624 | Condition: 625 | Bool: 626 | "aws:SecureTransport": "false" 627 | Principal: "*" 628 | AthenaWorkGroupConfig: 629 | Type: AWS::Athena::WorkGroup 630 | Properties: 631 | Name: access-analyzer-findings-workgroup-multi-account 632 | Description: IAM Access Analyzer findings workgroup. 633 | State: ENABLED 634 | WorkGroupConfiguration: 635 | EnforceWorkGroupConfiguration: true 636 | PublishCloudWatchMetricsEnabled: true 637 | ResultConfiguration: 638 | OutputLocation: !Sub "s3://${AthenaWorkGroupS3Bucket}/" 639 | EncryptionConfiguration: 640 | EncryptionOption: SSE_S3 641 | QuickSightServiceRole: 642 | Type: AWS::IAM::Role 643 | Properties: 644 | RoleName: aws-quicksight-service-role 645 | Path: /service-role/ 646 | AssumeRolePolicyDocument: 647 | Version: 2012-10-17 648 | Statement: 649 | - Action: sts:AssumeRole 650 | Effect: Allow 651 | Principal: 652 | Service: quicksight.amazonaws.com 653 | ManagedPolicyArns: 654 | - arn:aws:iam::aws:policy/service-role/AWSQuicksightAthenaAccess 655 | QuickSightServiceRolePolicyS3: 656 | Type: AWS::IAM::ManagedPolicy 657 | Properties: 658 | ManagedPolicyName: aws-quicksight-s3-policy 659 | Description: Grants Amazon QuickSight read permission to Amazon S3 resources. 660 | Roles: 661 | - !Ref QuickSightServiceRole 662 | Path: / 663 | PolicyDocument: 664 | Version: 2012-10-17 665 | Statement: 666 | - Effect: Allow 667 | Action: s3:ListAllMyBuckets 668 | Resource: "arn:aws:s3:::*" 669 | - Effect: Allow 670 | Action: s3:ListBucket 671 | Resource: 672 | - !GetAtt AthenaWorkGroupS3Bucket.Arn 673 | - !GetAtt s3Bucket.Arn 674 | - Effect: Allow 675 | Action: 676 | - s3:GetObject 677 | - s3:GetObjectVersion 678 | Resource: 679 | - !Sub "arn:aws:s3:::${AthenaWorkGroupS3Bucket}/*" 680 | - !Sub "arn:aws:s3:::${s3Bucket}/*" 681 | - Effect: Allow 682 | Action: 683 | - s3:ListBucketMultipartUploads 684 | - s3:GetBucketLocation 685 | Resource: 686 | - !GetAtt AthenaWorkGroupS3Bucket.Arn 687 | - Effect: Allow 688 | Action: 689 | - s3:PutObject 690 | - s3:AbortMultipartUpload 691 | - s3:ListMultipartUploadParts 692 | Resource: 693 | - !GetAtt AthenaWorkGroupS3Bucket.Arn 694 | QuickSightServiceRolePolicyIAM: 695 | Type: AWS::IAM::ManagedPolicy 696 | Properties: 697 | ManagedPolicyName: aws-quicksight-iam-policy 698 | Description: Grants Amazon QuickSight read permission to AWS IAM resources. 699 | Roles: 700 | - !Ref QuickSightServiceRole 701 | Path: / 702 | PolicyDocument: 703 | Version: 2012-10-17 704 | Statement: 705 | - Effect: Allow 706 | Action: iam:List* 707 | Resource: 708 | - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/*" 709 | 710 | ################################################################################# 711 | # AWS Lambda function list-iam-policy-for-access-analyzer resources # 712 | ################################################################################# 713 | LambdaFunctionListPolicies: 714 | Type: AWS::Lambda::Function 715 | Metadata: 716 | cfn_nag: 717 | rules_to_suppress: 718 | - id: W89 719 | reason: "Serverlesss implementation. Does not require to be deployed in a VPC." 720 | Properties: 721 | FunctionName: access-analyzer-list-iam-policy 722 | Runtime: python3.9 723 | Architectures: 724 | - x86_64 725 | Layers: 726 | - !Sub "arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:${pLambdaPowertoolsPythonVersion}" 727 | MemorySize: 1024 728 | Role: !GetAtt LambdaRoleListPolicies.Arn 729 | Handler: index.handler 730 | Timeout: 300 731 | Code: 732 | ZipFile: | 733 | import json 734 | import boto3 735 | import os 736 | from botocore.exceptions import ClientError 737 | from aws_lambda_powertools import Logger 738 | logger = Logger() 739 | sqs = boto3.client('sqs') 740 | SQS_QUEUE_URL = os.environ['SQS_QUEUE_URL'] 741 | 742 | def send_message_to_sqs_queue(policy,entity_type,entity): 743 | try: 744 | sqs.send_message( 745 | QueueUrl=SQS_QUEUE_URL, 746 | DelaySeconds=10, 747 | MessageAttributes={ 748 | 'policyArn': { 749 | 'DataType': 'String', 750 | 'StringValue': policy.arn if entity_type == 'CUSTOMER_MANAGED_IAM_POLICY' else entity.arn 751 | }, 752 | 'policyType':{ 753 | 'DataType': 'String', 754 | 'StringValue': 'IDENTITY_POLICY' 755 | }, 756 | 'policyName':{ 757 | 'DataType': 'String', 758 | 'StringValue': policy.policy_name 759 | }, 760 | 'policyPath':{ 761 | 'DataType': 'String', 762 | 'StringValue': policy.path if entity_type == 'CUSTOMER_MANAGED_IAM_POLICY' else entity_type.upper()+'_INLINE_POLICY' 763 | }, 764 | 'policyId':{ 765 | 'DataType': 'String', 766 | 'StringValue': policy.policy_id if entity_type == 'CUSTOMER_MANAGED_IAM_POLICY' else 'N/A' 767 | }, 768 | 'policyDefaultVersionId':{ 769 | 'DataType': 'String', 770 | 'StringValue': policy.default_version_id if entity_type == 'CUSTOMER_MANAGED_IAM_POLICY' else 'N/A' 771 | }, 772 | 'policyAttachmentCount':{ 773 | 'DataType': 'Number', 774 | 'StringValue': str(policy.attachment_count) if entity_type == 'CUSTOMER_MANAGED_IAM_POLICY' else '1' 775 | }, 776 | 'policyPermissionsBoundaryUsageCount':{ 777 | 'DataType': 'Number', 778 | 'StringValue': str(policy.permissions_boundary_usage_count) if entity_type == 'CUSTOMER_MANAGED_IAM_POLICY' else '0' 779 | } 780 | }, 781 | MessageBody=( 782 | json.dumps(policy.default_version.document) if entity_type == 'CUSTOMER_MANAGED_IAM_POLICY' else json.dumps(policy.policy_document) 783 | ) 784 | ) 785 | logger.info(f"Details sent to SQS on {policy.policy_name}.") 786 | except ClientError as e: 787 | logger.error("An error occured: {0}".format(e)) 788 | 789 | def collect_and_send_policies_to_sqs(iterator,type): 790 | if type == 'INLINE': 791 | for entity in iterator: 792 | for inline_p in entity.policies.all(): 793 | #-- Get entity type 'user', 'role', 'group' 794 | entity_type = inline_p.get_available_subresources()[0] 795 | logger.info(f"Inline IAM Policy - Currently working on {entity_type}: {entity.name} and policy: {inline_p.policy_name}.") 796 | send_message_to_sqs_queue(inline_p,entity_type,entity) 797 | logger.info(f"Inline IAM Policy - Details sent to SQS on policy: {inline_p.policy_name} for {entity_type}: {entity.name}.") 798 | elif type == 'CUSTOMER_MANAGED': 799 | for p in iterator: 800 | logger.info(f"Customer Managed IAM Policy - Currently working on policy: {p.policy_name}.") 801 | send_message_to_sqs_queue(p,'CUSTOMER_MANAGED_IAM_POLICY',None) 802 | logger.info(f"Customer Managed IAM Policy - Details sent to SQS on policy: {p.policy_name}.") 803 | 804 | @logger.inject_lambda_context 805 | def handler(event, context): 806 | iam_resource = boto3.resource('iam') 807 | logger.info("Execution started. Building iterators for IAM policies, roles, groups and users ...") 808 | group_iterator = iam_resource.groups.all() 809 | role_iterator = iam_resource.roles.all() 810 | user_iterator = iam_resource.users.all() 811 | iam_policies_iterator = iam_resource.policies.filter(Scope='Local') 812 | logger.info("Iterators built. Looping through IAM resources...") 813 | collect_and_send_policies_to_sqs(group_iterator,'INLINE') 814 | collect_and_send_policies_to_sqs(role_iterator,'INLINE') 815 | collect_and_send_policies_to_sqs(user_iterator,'INLINE') 816 | collect_and_send_policies_to_sqs(iam_policies_iterator,'CUSTOMER_MANAGED') 817 | logger.info("Execution complete. Exiting...") 818 | Environment: 819 | Variables: 820 | SQS_QUEUE_URL: !Ref CFQueue 821 | LOG_LEVEL: INFO 822 | POWERTOOLS_SERVICE_NAME: access_analyzer_list_policies 823 | Description: !Sub "Lambda function to list customer IAM policies and send them to the SQS queue ${CFQueue}." 824 | LambdaFunctionListPoliciesDLQ: 825 | Type: AWS::SQS::Queue 826 | Metadata: 827 | cfn_nag: 828 | rules_to_suppress: 829 | - id: W48 830 | reason: "No KMS key provided." 831 | Properties: 832 | KmsMasterKeyId: alias/aws/sqs 833 | LambdaRoleListPolicies: 834 | Type: AWS::IAM::Role 835 | Metadata: 836 | cfn_nag: 837 | rules_to_suppress: 838 | - id: W11 839 | reason: "Serverlesss implementation. Does not require to be deployed in a VPC." 840 | Properties: 841 | ManagedPolicyArns: 842 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 843 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole" 844 | Path: /access-analyzer/ 845 | AssumeRolePolicyDocument: 846 | Version: 2012-10-17 847 | Statement: 848 | - Action: 849 | - "sts:AssumeRole" 850 | Effect: Allow 851 | Principal: 852 | Service: 853 | - lambda.amazonaws.com 854 | Policies: 855 | - PolicyName: "ListIAMPolicyForAccessAnalyzer" 856 | PolicyDocument: 857 | Version: "2012-10-17" 858 | Statement: 859 | - Effect: "Allow" 860 | Action: 861 | - "iam:GetPolicyVersion" 862 | - "iam:ListPolicies" 863 | - "iam:GetUserPolicy" 864 | - "iam:GetRolePolicy" 865 | - "iam:GetGroupPolicy" 866 | - "iam:ListGroups" 867 | - "iam:ListUsers" 868 | - "iam:ListRoles" 869 | - "iam:ListGroupPolicies" 870 | - "iam:ListUserPolicies" 871 | - "iam:ListRolePolicies" 872 | Resource: 873 | - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/*" 874 | - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:group/*" 875 | - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/*" 876 | - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:user/*" 877 | - Effect: "Allow" 878 | Action: 879 | - "kms:Decrypt" 880 | - "kms:DescribeKey" 881 | - "kms:GenerateDataKey" 882 | Resource: !GetAtt KMSKey.Arn 883 | - Effect: "Allow" 884 | Action: 885 | - "sqs:SendMessage" 886 | Resource: 887 | - !GetAtt CFQueue.Arn 888 | - !GetAtt LambdaFunctionListPoliciesDLQ.Arn 889 | LambdaFunctionListLogGroup: 890 | Type: "AWS::Logs::LogGroup" 891 | Metadata: 892 | cfn_nag: 893 | rules_to_suppress: 894 | - id: W84 895 | reason: "No KMS key provided." 896 | Properties: 897 | RetentionInDays: 14 898 | LogGroupName: !Sub "/aws/lambda/${LambdaFunctionListPolicies}" 899 | 900 | ###################################### 901 | # AWS CloudWatch Events resources # 902 | ###################################### 903 | scheduledRule: 904 | Type: AWS::Events::Rule 905 | Properties: 906 | Description: Scheduled event rule to validate IAM policies using IAM Access Analyzer. 907 | ScheduleExpression: "rate(12 hours)" 908 | State: "ENABLED" 909 | Targets: 910 | - Arn: !GetAtt LambdaFunctionListPolicies.Arn 911 | Id: "TargetLambdaFunctionListPolicies" 912 | permissionForEventsToInvokeLambda: 913 | Type: AWS::Lambda::Permission 914 | Properties: 915 | FunctionName: !Ref LambdaFunctionListPolicies 916 | Action: "lambda:InvokeFunction" 917 | Principal: "events.amazonaws.com" 918 | SourceArn: 919 | Fn::GetAtt: 920 | - "scheduledRule" 921 | - "Arn" 922 | 923 | ############## 924 | # OUTPUTS # 925 | ############## 926 | Outputs: 927 | SQSQueueUrl: 928 | Description: SQS Queue URL used for IAM Access Analyzer Policy Validation. 929 | Value: !Ref CFQueue 930 | KMSKeyArn: 931 | Description: AWS KMS key ARN used to encrypt resources. 932 | Value: !GetAtt KMSKey.Arn 933 | -------------------------------------------------------------------------------- /multi-account-deployment/stackset.template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: Deploys AWS resources to member accounts to list and send IAM policy documents to a central account for evaluation. 3 | 4 | Parameters: 5 | pSQSQueueUrl: 6 | Description: SQS queue in the central security account where IAM policy documents are sent for evaluation. 7 | Type: String 8 | 9 | pKMSKeyArn: 10 | Description: AWS KMS key ARN in the central security account used to encrypt resources. 11 | Type: String 12 | 13 | pLambdaPowertoolsPythonVersion: 14 | Type: String 15 | Default: 7 16 | 17 | Conditions: 18 | IsSpokeAccountEqualToHubAccount: !Not 19 | - !Equals 20 | - !Sub 21 | - "${HubAccount}" 22 | - HubAccount: !Select 23 | - 3 24 | - !Split 25 | - / 26 | - !Ref pSQSQueueUrl 27 | - !Ref "AWS::AccountId" 28 | 29 | Resources: 30 | ################################################################################# 31 | # AWS Lambda function list-iam-policy-for-access-analyzer resources # 32 | ################################################################################# 33 | LambdaFunctionListPolicies: 34 | Condition: IsSpokeAccountEqualToHubAccount 35 | Type: AWS::Lambda::Function 36 | Metadata: 37 | cfn_nag: 38 | rules_to_suppress: 39 | - id: W89 40 | reason: "Serverlesss implementation. Does not require to be deployed in a VPC." 41 | Properties: 42 | FunctionName: access-analyzer-list-iam-policy 43 | Runtime: python3.9 44 | Architectures: 45 | - x86_64 46 | Layers: 47 | - !Sub "arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:${pLambdaPowertoolsPythonVersion}" 48 | MemorySize: 1024 49 | Role: !GetAtt LambdaRoleListPolicies.Arn 50 | Handler: index.handler 51 | Timeout: 300 52 | Code: 53 | ZipFile: | 54 | import json 55 | import boto3 56 | import os 57 | from botocore.exceptions import ClientError 58 | from aws_lambda_powertools import Logger 59 | logger = Logger() 60 | sqs = boto3.client('sqs') 61 | SQS_QUEUE_URL = os.environ['SQS_QUEUE_URL'] 62 | 63 | def send_message_to_sqs_queue(policy,entity_type,entity): 64 | try: 65 | sqs.send_message( 66 | QueueUrl=SQS_QUEUE_URL, 67 | DelaySeconds=10, 68 | MessageAttributes={ 69 | 'policyArn': { 70 | 'DataType': 'String', 71 | 'StringValue': policy.arn if entity_type == 'CUSTOMER_MANAGED_IAM_POLICY' else entity.arn 72 | }, 73 | 'policyType':{ 74 | 'DataType': 'String', 75 | 'StringValue': 'IDENTITY_POLICY' 76 | }, 77 | 'policyName':{ 78 | 'DataType': 'String', 79 | 'StringValue': policy.policy_name 80 | }, 81 | 'policyPath':{ 82 | 'DataType': 'String', 83 | 'StringValue': policy.path if entity_type == 'CUSTOMER_MANAGED_IAM_POLICY' else entity_type.upper()+'_INLINE_POLICY' 84 | }, 85 | 'policyId':{ 86 | 'DataType': 'String', 87 | 'StringValue': policy.policy_id if entity_type == 'CUSTOMER_MANAGED_IAM_POLICY' else 'N/A' 88 | }, 89 | 'policyDefaultVersionId':{ 90 | 'DataType': 'String', 91 | 'StringValue': policy.default_version_id if entity_type == 'CUSTOMER_MANAGED_IAM_POLICY' else 'N/A' 92 | }, 93 | 'policyAttachmentCount':{ 94 | 'DataType': 'Number', 95 | 'StringValue': str(policy.attachment_count) if entity_type == 'CUSTOMER_MANAGED_IAM_POLICY' else '1' 96 | }, 97 | 'policyPermissionsBoundaryUsageCount':{ 98 | 'DataType': 'Number', 99 | 'StringValue': str(policy.permissions_boundary_usage_count) if entity_type == 'CUSTOMER_MANAGED_IAM_POLICY' else '0' 100 | } 101 | }, 102 | MessageBody=( 103 | json.dumps(policy.default_version.document) if entity_type == 'CUSTOMER_MANAGED_IAM_POLICY' else json.dumps(policy.policy_document) 104 | ) 105 | ) 106 | logger.info(f"Details sent to SQS on {policy.policy_name}.") 107 | except ClientError as e: 108 | logger.error("An error occured: {0}".format(e)) 109 | 110 | def collect_and_send_policies_to_sqs(iterator,type): 111 | if type == 'INLINE': 112 | for entity in iterator: 113 | for inline_p in entity.policies.all(): 114 | #-- Get entity type 'user', 'role', 'group' 115 | entity_type = inline_p.get_available_subresources()[0] 116 | logger.info(f"Inline IAM Policy - Currently working on {entity_type}: {entity.name} and policy: {inline_p.policy_name}.") 117 | send_message_to_sqs_queue(inline_p,entity_type,entity) 118 | logger.info(f"Inline IAM Policy - Details sent to SQS on policy: {inline_p.policy_name} for {entity_type}: {entity.name}.") 119 | elif type == 'CUSTOMER_MANAGED': 120 | for p in iterator: 121 | logger.info(f"Customer Managed IAM Policy - Currently working on policy: {p.policy_name}.") 122 | send_message_to_sqs_queue(p,'CUSTOMER_MANAGED_IAM_POLICY',None) 123 | logger.info(f"Customer Managed IAM Policy - Details sent to SQS on policy: {p.policy_name}.") 124 | 125 | @logger.inject_lambda_context 126 | def handler(event, context): 127 | iam_resource = boto3.resource('iam') 128 | logger.info("Execution started. Building iterators for IAM policies, roles, groups and users ...") 129 | group_iterator = iam_resource.groups.all() 130 | role_iterator = iam_resource.roles.all() 131 | user_iterator = iam_resource.users.all() 132 | iam_policies_iterator = iam_resource.policies.filter(Scope='Local') 133 | logger.info("Iterators built. Looping through IAM resources...") 134 | collect_and_send_policies_to_sqs(group_iterator,'INLINE') 135 | collect_and_send_policies_to_sqs(role_iterator,'INLINE') 136 | collect_and_send_policies_to_sqs(user_iterator,'INLINE') 137 | collect_and_send_policies_to_sqs(iam_policies_iterator,'CUSTOMER_MANAGED') 138 | logger.info("Execution complete. Exiting...") 139 | Environment: 140 | Variables: 141 | SQS_QUEUE_URL: !Ref pSQSQueueUrl 142 | LOG_LEVEL: INFO 143 | POWERTOOLS_SERVICE_NAME: access_analyzer_list_policies 144 | Description: !Sub "Lambda function to list customer IAM policies and send them to the SQS queue ${pSQSQueueUrl}." 145 | LambdaFunctionListPoliciesDLQ: 146 | Condition: IsSpokeAccountEqualToHubAccount 147 | Type: AWS::SQS::Queue 148 | Metadata: 149 | cfn_nag: 150 | rules_to_suppress: 151 | - id: W48 152 | reason: "No KMS key provided." 153 | Properties: 154 | KmsMasterKeyId: alias/aws/sqs 155 | LambdaRoleListPolicies: 156 | Condition: IsSpokeAccountEqualToHubAccount 157 | Type: AWS::IAM::Role 158 | Metadata: 159 | cfn_nag: 160 | rules_to_suppress: 161 | - id: W11 162 | reason: "Serverlesss implementation. Does not require to be deployed in a VPC." 163 | Properties: 164 | ManagedPolicyArns: 165 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 166 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole" 167 | Path: /access-analyzer/ 168 | AssumeRolePolicyDocument: 169 | Version: 2012-10-17 170 | Statement: 171 | - Action: 172 | - "sts:AssumeRole" 173 | Effect: Allow 174 | Principal: 175 | Service: 176 | - lambda.amazonaws.com 177 | Policies: 178 | - PolicyName: "ListIAMPolicyForAccessAnalyzer" 179 | PolicyDocument: 180 | Version: "2012-10-17" 181 | Statement: 182 | - Effect: "Allow" 183 | Action: 184 | - "iam:GetPolicyVersion" 185 | - "iam:ListPolicies" 186 | - "iam:GetUserPolicy" 187 | - "iam:GetRolePolicy" 188 | - "iam:GetGroupPolicy" 189 | - "iam:ListGroups" 190 | - "iam:ListUsers" 191 | - "iam:ListRoles" 192 | - "iam:ListGroupPolicies" 193 | - "iam:ListUserPolicies" 194 | - "iam:ListRolePolicies" 195 | Resource: 196 | - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/*" 197 | - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:group/*" 198 | - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/*" 199 | - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:user/*" 200 | - Effect: "Allow" 201 | Action: 202 | - "kms:Decrypt" 203 | - "kms:DescribeKey" 204 | - "kms:GenerateDataKey" 205 | Resource: !Ref pKMSKeyArn 206 | - Effect: "Allow" 207 | Action: 208 | - "sqs:SendMessage" 209 | Resource: 210 | - !Sub 211 | - >- 212 | arn:${AWS::Partition}:sqs:${AWS::Region}:${AccountId}:${QueueName} 213 | - QueueName: !Select 214 | - 4 215 | - !Split 216 | - / 217 | - !Ref pSQSQueueUrl 218 | AccountId: !Select 219 | - 3 220 | - !Split 221 | - / 222 | - !Ref pSQSQueueUrl 223 | - !GetAtt LambdaFunctionListPoliciesDLQ.Arn 224 | LambdaFunctionListLogGroup: 225 | Condition: IsSpokeAccountEqualToHubAccount 226 | Type: "AWS::Logs::LogGroup" 227 | Metadata: 228 | cfn_nag: 229 | rules_to_suppress: 230 | - id: W84 231 | reason: "No KMS key provided." 232 | Properties: 233 | RetentionInDays: 14 234 | LogGroupName: !Sub "/aws/lambda/${LambdaFunctionListPolicies}" 235 | 236 | ###################################### 237 | # AWS CloudWatch Events resources # 238 | ###################################### 239 | scheduledRule: 240 | Condition: IsSpokeAccountEqualToHubAccount 241 | Type: AWS::Events::Rule 242 | Properties: 243 | Description: Scheduled event rule to validate IAM policies using IAM Access Analyzer. 244 | ScheduleExpression: "rate(12 hours)" 245 | State: "ENABLED" 246 | Targets: 247 | - Arn: !GetAtt LambdaFunctionListPolicies.Arn 248 | Id: "TargetLambdaFunctionListPolicies" 249 | 250 | permissionForEventsToInvokeLambda: 251 | Condition: IsSpokeAccountEqualToHubAccount 252 | Type: AWS::Lambda::Permission 253 | Properties: 254 | FunctionName: !Ref LambdaFunctionListPolicies 255 | Action: "lambda:InvokeFunction" 256 | Principal: "events.amazonaws.com" 257 | SourceArn: 258 | Fn::GetAtt: 259 | - "scheduledRule" 260 | - "Arn" 261 | -------------------------------------------------------------------------------- /quicksight-dashboard-deployment/quicksight-hub.template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: "Automated deployment of QuickSight Assets." 3 | 4 | Parameters: 5 | pQuickSightUserNameArn: 6 | Description: Enter the arn of the QuickSight Username. 7 | Type: String 8 | 9 | Resources: 10 | AccessAnalyzerFindingsDataSource: 11 | Type: AWS::QuickSight::DataSource 12 | Properties: 13 | DataSourceId: !Sub "access-analyzer-findings-data-source-${AWS::AccountId}" 14 | Name: "access-analyzer-findings-data-source" 15 | AwsAccountId: !Ref AWS::AccountId 16 | Type: ATHENA 17 | DataSourceParameters: 18 | AthenaParameters: 19 | WorkGroup: "access-analyzer-findings-workgroup-multi-account" 20 | SslProperties: 21 | DisableSsl: false 22 | 23 | AccessAnalyzerFindingsDataSet: 24 | Type: AWS::QuickSight::DataSet 25 | Properties: 26 | Permissions: 27 | - Actions: 28 | - "quicksight:UpdateDataSetPermissions" 29 | - "quicksight:DescribeDataSet" 30 | - "quicksight:DescribeDataSetPermissions" 31 | - "quicksight:PassDataSet" 32 | - "quicksight:DescribeIngestion" 33 | - "quicksight:ListIngestions" 34 | - "quicksight:UpdateDataSet" 35 | - "quicksight:DeleteDataSet" 36 | - "quicksight:CreateIngestion" 37 | - "quicksight:CancelIngestion" 38 | Principal: !Ref pQuickSightUserNameArn 39 | Name: "access-analyzer-findings-data-set" 40 | DataSetId: !Sub "access-analyzer-findings-data-set-${AWS::AccountId}" 41 | AwsAccountId: !Ref AWS::AccountId 42 | PhysicalTableMap: 43 | AccessAnalyzerFindingsTable: 44 | RelationalTable: 45 | Name: "view_access_analyzer_finding" 46 | Schema: "access_analyzer_findings" 47 | DataSourceArn: !GetAtt AccessAnalyzerFindingsDataSource.Arn 48 | InputColumns: 49 | - Name: policyarn 50 | Type: STRING 51 | - Name: policytype 52 | Type: STRING 53 | - Name: path 54 | Type: STRING 55 | - Name: policyid 56 | Type: STRING 57 | - Name: policyname 58 | Type: STRING 59 | - Name: latest_validatedat 60 | Type: STRING 61 | - Name: findingtype 62 | Type: STRING 63 | - Name: issuecode 64 | Type: STRING 65 | - Name: findingdetails 66 | Type: STRING 67 | - Name: learnmorelink 68 | Type: STRING 69 | - Name: latest_datehour 70 | Type: STRING 71 | - Name: accountid 72 | Type: STRING 73 | LogicalTableMap: 74 | AccessAnalyzerFindingsLogicalTable: 75 | Alias: "view_access_analyzer_finding" 76 | DataTransforms: 77 | - CastColumnTypeOperation: 78 | ColumnName: "latest_validatedat" 79 | NewColumnType: DATETIME 80 | Format: "yyyy-MM-dd'T'HH:mm:ss.SSSSSS" 81 | - CastColumnTypeOperation: 82 | ColumnName: "latest_datehour" 83 | NewColumnType: DATETIME 84 | Format: "yyyy/MM/dd" 85 | - ProjectOperation: 86 | ProjectedColumns: 87 | - "policyarn" 88 | - "policytype" 89 | - "path" 90 | - "policyid" 91 | - "policyname" 92 | - "latest_validatedat" 93 | - "findingtype" 94 | - "issuecode" 95 | - "findingdetails" 96 | - "learnmorelink" 97 | - "latest_datehour" 98 | - "accountid" 99 | Source: 100 | PhysicalTableId: AccessAnalyzerFindingsTable 101 | ImportMode: DIRECT_QUERY 102 | 103 | AccessAnalyzerFindingsTemplate: 104 | Type: AWS::QuickSight::Template 105 | Properties: 106 | TemplateId: !Sub "access-analyzer-findings-template-${AWS::AccountId}" 107 | Name: "access-analyzer-findings-template" 108 | AwsAccountId: !Ref AWS::AccountId 109 | SourceEntity: 110 | SourceTemplate: 111 | Arn: !Sub >- 112 | arn:aws:quicksight:${AWS::Region}:907413805921:template/shared-access-analyzer-findings-template-907413805921 113 | 114 | VersionDescription: Quicksight template copy of the shared access analyzer finding template. 115 | 116 | AccessAnalyzerFindingsAnalysis: 117 | Type: AWS::QuickSight::Analysis 118 | Properties: 119 | Permissions: 120 | - Actions: 121 | - "quicksight:RestoreAnalysis" 122 | - "quicksight:UpdateAnalysisPermissions" 123 | - "quicksight:DeleteAnalysis" 124 | - "quicksight:DescribeAnalysisPermissions" 125 | - "quicksight:QueryAnalysis" 126 | - "quicksight:DescribeAnalysis" 127 | - "quicksight:UpdateAnalysis" 128 | Principal: !Ref pQuickSightUserNameArn 129 | AnalysisId: !Sub "access-analyzer-findings-analysis-${AWS::AccountId}" 130 | Name: "access-analyzer-validation-findings" 131 | AwsAccountId: !Ref AWS::AccountId 132 | SourceEntity: 133 | SourceTemplate: 134 | Arn: !GetAtt AccessAnalyzerFindingsTemplate.Arn 135 | DataSetReferences: 136 | - DataSetPlaceholder: AccessAnalyzerFindingsDataSet 137 | DataSetArn: !GetAtt AccessAnalyzerFindingsDataSet.Arn 138 | ThemeArn: "arn:aws:quicksight::aws:theme/MIDNIGHT" 139 | 140 | AccessAnalyzerFindingsDashboard: 141 | Type: AWS::QuickSight::Dashboard 142 | Properties: 143 | Permissions: 144 | - Actions: 145 | - "quicksight:DescribeDashboard" 146 | - "quicksight:DescribeDashboard" 147 | - "quicksight:ListDashboardVersions" 148 | - "quicksight:UpdateDashboardPermissions" 149 | - "quicksight:QueryDashboard" 150 | - "quicksight:UpdateDashboard" 151 | - "quicksight:DeleteDashboard" 152 | - "quicksight:DescribeDashboardPermissions" 153 | - "quicksight:UpdateDashboardPublishedVersion" 154 | Principal: !Ref pQuickSightUserNameArn 155 | DashboardId: !Sub "access-analyzer-findings-dashboard-${AWS::AccountId}" 156 | Name: "access-analyzer-findings-dashboard" 157 | AwsAccountId: !Ref AWS::AccountId 158 | SourceEntity: 159 | SourceTemplate: 160 | Arn: !GetAtt AccessAnalyzerFindingsTemplate.Arn 161 | DataSetReferences: 162 | - DataSetPlaceholder: AccessAnalyzerFindingsDataSet 163 | DataSetArn: !GetAtt AccessAnalyzerFindingsDataSet.Arn 164 | 165 | ThemeArn: "arn:aws:quicksight::aws:theme/MIDNIGHT" 166 | DashboardPublishOptions: 167 | AdHocFilteringOption: 168 | AvailabilityStatus: DISABLED 169 | --------------------------------------------------------------------------------