├── img ├── idc.png ├── architecture.png ├── graph-example.png └── relationships.png ├── SECURITY.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── source ├── listaccounts │ └── lambda_function.py ├── listusers │ └── lambda_function.py ├── listgroups │ └── lambda_function.py ├── listpermissionsets │ └── lambda_function.py ├── idciaminventoryrole │ ├── idc-iam-inventory-role.yaml │ └── stack-set-creation.md ├── updatefunctioncode │ └── lambda_function.py ├── listgroupmembership │ └── lambda_function.py ├── listuseraccountassignments │ └── lambda_function.py ├── listgroupaccountassignments │ └── lambda_function.py ├── listprovisionedpermissionsets │ └── lambda_function.py ├── createtables │ └── lambda_function.py ├── getiamroles │ └── lambda_function.py ├── accessanalyzerfindingingestion │ └── lambda_function.py └── s3export │ └── lambda_function.py ├── CONTRIBUTING.md ├── aria-bootstrap.sh ├── templates ├── eventbridge-stack.yaml ├── neptune-notebook.yaml ├── ssm-stack.yaml ├── core-infrastructure.yaml ├── step-functions.yaml ├── main-stack.yaml └── neptune-analytics.yaml ├── SCHEDULING_GUIDE.md ├── README.md └── deploy-nested-stacks.sh /img/idc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/sample-visualizing-access-rights-for-identity-on-aws/HEAD/img/idc.png -------------------------------------------------------------------------------- /img/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/sample-visualizing-access-rights-for-identity-on-aws/HEAD/img/architecture.png -------------------------------------------------------------------------------- /img/graph-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/sample-visualizing-access-rights-for-identity-on-aws/HEAD/img/graph-example.png -------------------------------------------------------------------------------- /img/relationships.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/sample-visualizing-access-rights-for-identity-on-aws/HEAD/img/relationships.png -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security issue notifications 2 | 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. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /source/listaccounts/lambda_function.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | import time 4 | from datetime import datetime 5 | 6 | # List all permission sets and store in DynamoDB 7 | def list_accounts(dynamodb): 8 | 9 | table = dynamodb.Table('AriaIdCAccounts') 10 | 11 | # Get a list of all accounts in the organization 12 | organizations = boto3.client('organizations') 13 | accounts = [] 14 | paginator = organizations.get_paginator('list_accounts') 15 | for page in paginator.paginate(): 16 | accounts.extend(page['Accounts']) 17 | 18 | for account in accounts: 19 | try: 20 | # print(f"Account info: {account}") 21 | table.put_item(Item={ 22 | 'AccountId': account['Id'], 23 | 'Name': account['Name'], 24 | 'Status': account['Status'], 25 | 'UpdatedAt': datetime.now().isoformat() 26 | }) 27 | except Exception as e: 28 | print(f"Error processing accounts") 29 | 30 | # Initialize clients 31 | def initialize_clients(): 32 | # Initialize required AWS clients 33 | dynamodb = boto3.resource('dynamodb') 34 | 35 | return dynamodb 36 | 37 | def lambda_handler(event, context): 38 | 39 | dynamodb = initialize_clients() 40 | 41 | # List accounts 42 | try: 43 | list_accounts(dynamodb) 44 | print("Listed accounts successfully") 45 | return { 46 | 'statusCode': 200, 47 | 'body': json.dumps('Listed accounts successfully') 48 | } 49 | # Return success response 50 | except Exception as e: 51 | print(f"Error listing accounts: {e}") 52 | return { 53 | 'statusCode': 500, 54 | 'body': json.dumps('Error listing accounts') 55 | } 56 | -------------------------------------------------------------------------------- /source/listusers/lambda_function.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | import time 4 | from datetime import datetime 5 | 6 | # List all users and store in DynamoDB 7 | def list_users(identitystore, dynamodb, identity_store_id): 8 | print(f"Listing all users") 9 | table = dynamodb.Table('AriaIdCUsers') 10 | paginator = identitystore.get_paginator('list_users') 11 | 12 | for page in paginator.paginate(IdentityStoreId=identity_store_id): 13 | for user in page['Users']: 14 | table.put_item(Item={ 15 | 'UserId': user['UserId'], 16 | 'UserName': user['UserName'], 17 | 'Email': user.get('Emails', [{}])[0].get('Value', ''), 18 | 'UpdatedAt': datetime.now().isoformat() 19 | }) 20 | 21 | # Initialize clients and get Identity Store ID 22 | def initialize_clients(): 23 | # Initialize required AWS clients 24 | identitystore = boto3.client('identitystore') 25 | dynamodb = boto3.resource('dynamodb') 26 | 27 | # Get Identity Store ID 28 | sso = boto3.client('sso-admin') 29 | identity_store_id = sso.list_instances()['Instances'][0]['IdentityStoreId'] 30 | 31 | return identitystore, dynamodb, identity_store_id 32 | 33 | def lambda_handler(event, context): 34 | 35 | identitystore, dynamodb, identity_store_id = initialize_clients() 36 | 37 | # List users 38 | try: 39 | list_users(identitystore, dynamodb, identity_store_id) 40 | print("Listed users successfully") 41 | return { 42 | 'statusCode': 200, 43 | 'body': json.dumps('Listed users successfully') 44 | } 45 | # Return success response 46 | except Exception as e: 47 | print(f"Error listing users: {e}") 48 | return { 49 | 'statusCode': 500, 50 | 'body': json.dumps('Error listing users') 51 | } 52 | -------------------------------------------------------------------------------- /source/listgroups/lambda_function.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | import time 4 | from datetime import datetime 5 | 6 | # List all groups and store in DynamoDB 7 | def list_groups(identitystore, dynamodb, identity_store_id): 8 | # List all groups and store in DynamoDB 9 | print(f"Listing all groups") 10 | table = dynamodb.Table('AriaIdCGroups') 11 | paginator = identitystore.get_paginator('list_groups') 12 | 13 | for page in paginator.paginate(IdentityStoreId=identity_store_id): 14 | for group in page['Groups']: 15 | table.put_item(Item={ 16 | 'GroupId': group['GroupId'], 17 | 'GroupName': group['DisplayName'], 18 | 'UpdatedAt': datetime.now().isoformat() 19 | }) 20 | 21 | 22 | # Initialize clients 23 | def initialize_clients(): 24 | # Initialize required AWS clients 25 | identitystore = boto3.client('identitystore') 26 | sso_admin = boto3.client('sso-admin') 27 | dynamodb = boto3.resource('dynamodb') 28 | 29 | # Get Identity Store ID 30 | sso = boto3.client('sso-admin') 31 | identity_store_id = sso.list_instances()['Instances'][0]['IdentityStoreId'] 32 | instance_arn = sso.list_instances()['Instances'][0]['InstanceArn'] 33 | 34 | return identitystore, sso_admin, dynamodb, identity_store_id, instance_arn 35 | 36 | def lambda_handler(event, context): 37 | 38 | identitystore, sso_admin, dynamodb, identity_store_id, instance_arn = initialize_clients() 39 | 40 | # List groups 41 | try: 42 | list_groups(identitystore, dynamodb, identity_store_id) 43 | print("Listed groups successfully") 44 | return { 45 | 'statusCode': 200, 46 | 'body': json.dumps('Listed groups successfully') 47 | } 48 | # Return success response 49 | except Exception as e: 50 | print(f"Error listing groups: {e}") 51 | return { 52 | 'statusCode': 500, 53 | 'body': json.dumps('Error listing groups') 54 | } 55 | -------------------------------------------------------------------------------- /source/listpermissionsets/lambda_function.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | import time 4 | from datetime import datetime 5 | 6 | # List all permission sets and store in DynamoDB 7 | def list_permission_sets(sso_admin, dynamodb, instance_arn): 8 | # List all permission sets and store in DynamoDB 9 | print(f"Listing all permission sets") 10 | table = dynamodb.Table('AriaIdCPermissionSets') 11 | paginator = sso_admin.get_paginator('list_permission_sets') 12 | 13 | for page in paginator.paginate(InstanceArn=instance_arn): 14 | for permission_set_arn in page['PermissionSets']: 15 | details = sso_admin.describe_permission_set( 16 | InstanceArn=instance_arn, 17 | PermissionSetArn=permission_set_arn 18 | )['PermissionSet'] 19 | 20 | table.put_item(Item={ 21 | 'PermissionSetArn': permission_set_arn, 22 | 'Name': details['Name'], 23 | 'Description': details.get('Description', ''), 24 | 'UpdatedAt': datetime.now().isoformat() 25 | }) 26 | 27 | # Initialize clients 28 | def initialize_clients(): 29 | # Initialize required AWS clients 30 | sso_admin = boto3.client('sso-admin') 31 | dynamodb = boto3.resource('dynamodb') 32 | 33 | # Get Identity Store ID 34 | sso = boto3.client('sso-admin') 35 | instance_arn = sso.list_instances()['Instances'][0]['InstanceArn'] 36 | 37 | return sso_admin, dynamodb, instance_arn 38 | 39 | def lambda_handler(event, context): 40 | 41 | sso_admin, dynamodb, instance_arn = initialize_clients() 42 | 43 | # List permission sets 44 | try: 45 | list_permission_sets(sso_admin, dynamodb, instance_arn) 46 | print("Listed permission sets successfully") 47 | return { 48 | 'statusCode': 200, 49 | 'body': json.dumps('Listed permission sets successfully') 50 | } 51 | # Return success response 52 | except Exception as e: 53 | print(f"Error listing permissionsets: {e}") 54 | return { 55 | 'statusCode': 500, 56 | 'body': json.dumps('Error listing permissionsets') 57 | } 58 | -------------------------------------------------------------------------------- /source/idciaminventoryrole/idc-iam-inventory-role.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: 'Create IAM role for IAM role inventory with read-only permissions, assumable by IdCInventoryAccessRole' 3 | 4 | Parameters: 5 | TrustedAccountId: 6 | Type: String 7 | Description: AWS Account ID containing the AriaIdCInventoryAccessRole 8 | IAMRolesLambdaCrossAccountRole: 9 | Type: String 10 | Description: IAM role ARN of Lambda running in other account to assume to perform cross-account operations 11 | Default: 'arn:aws:iam::<01234567890>:role/-GetIAMRolesLambdaExecutionRole-' 12 | 13 | Resources: 14 | AriaIdCInventoryAccessRole: 15 | Type: 'AWS::IAM::Role' 16 | Metadata: 17 | cfn_nag: 18 | rules_to_suppress: 19 | - id: W11 20 | reason: The Lambda function that will assume this role requires the ability to list all roles and all attached role policies in the AWS account 21 | - id: W28 22 | reason: The role name is set here so that it can be referenced in the main CloudFormation template without any user input 23 | Properties: 24 | RoleName: 'AriaIdCInventoryAccessRole-LimitedReadOnly' 25 | Description: 'Role for reading IdC IAM role information across accounts' 26 | AssumeRolePolicyDocument: 27 | Version: '2012-10-17' 28 | Statement: 29 | - Effect: Allow 30 | Principal: 31 | AWS: !Sub 'arn:aws:iam::${TrustedAccountId}:root' 32 | Action: sts:AssumeRole 33 | Condition: 34 | StringEquals: 35 | 'aws:PrincipalArn': !Ref IAMRolesLambdaCrossAccountRole 36 | Policies: 37 | - PolicyName: 'AriaIdCInventoryAccessRole-LimitedReadOnlyPolicy' 38 | PolicyDocument: 39 | Version: '2012-10-17' 40 | Statement: 41 | - Effect: Allow 42 | Action: 43 | - iam:ListRoles 44 | - iam:ListAttachedRolePolicies 45 | Resource: 46 | - '*' 47 | Tags: 48 | - Key: aria 49 | Value: role 50 | 51 | Outputs: 52 | RoleARN: 53 | Description: 'ARN of the created IAM role' 54 | Value: !GetAtt AriaIdCInventoryAccessRole.Arn 55 | -------------------------------------------------------------------------------- /source/updatefunctioncode/lambda_function.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import os 4 | from botocore.exceptions import ClientError 5 | 6 | def lambda_handler(event, context): 7 | try: 8 | # Get S3 bucket and key from the event 9 | bucket = event['detail']['bucket']['name'] 10 | key = event['detail']['object']['key'] 11 | 12 | # Remove .zip extension to get SSM parameter name 13 | parameter_name = "/aria/lambda/" + os.path.splitext(key)[0] 14 | 15 | # Initialize AWS clients 16 | ssm = boto3.client('ssm') 17 | lambda_client = boto3.client('lambda') 18 | s3 = boto3.client('s3') 19 | 20 | # Get the Lambda ARN from Parameter Store using the S3 object key as parameter name 21 | try: 22 | parameter = ssm.get_parameter( 23 | Name=parameter_name, 24 | WithDecryption=True 25 | ) 26 | target_lambda_arn = parameter['Parameter']['Value'] 27 | except ClientError as e: 28 | print(f"Error getting parameter {key}: {str(e)}") 29 | raise 30 | 31 | # Update the Lambda function code 32 | try: 33 | response = lambda_client.update_function_code( 34 | FunctionName=target_lambda_arn, 35 | S3Bucket=bucket, 36 | S3Key=key, 37 | Publish=True # Create new version 38 | ) 39 | 40 | print(f"Successfully updated Lambda function: {target_lambda_arn}") 41 | print(f"New version: {response['Version']}") 42 | 43 | return { 44 | 'statusCode': 200, 45 | 'body': json.dumps({ 46 | 'message': 'Lambda function updated successfully', 47 | 'functionArn': target_lambda_arn, 48 | 'version': response['Version'] 49 | }) 50 | } 51 | 52 | except ClientError as e: 53 | print(f"Error updating Lambda function: {str(e)}") 54 | raise 55 | 56 | except Exception as e: 57 | print(f"Error: {str(e)}") 58 | return { 59 | 'statusCode': 500, 60 | 'body': json.dumps({ 61 | 'message': f'Error occurred: {str(e)}' 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /source/listgroupmembership/lambda_function.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | import time 4 | from datetime import datetime 5 | 6 | # List all group memberships and store in DynamoDB 7 | def list_group_memberships(identitystore, dynamodb, identity_store_id): 8 | # List all group memberships and store in DynamoDB 9 | print(f"Listing all group memberships") 10 | table = dynamodb.Table('AriaIdCGroupMembership') 11 | groups_table = dynamodb.Table('AriaIdCGroups') 12 | groups = groups_table.scan()['Items'] 13 | 14 | for group in groups: 15 | paginator = identitystore.get_paginator('list_group_memberships') 16 | for page in paginator.paginate( 17 | IdentityStoreId=identity_store_id, 18 | GroupId=group['GroupId'] 19 | ): 20 | for membership in page['GroupMemberships']: 21 | table.put_item(Item={ 22 | 'GroupId': group['GroupId'], 23 | 'UserId': membership['MemberId']['UserId'], 24 | 'UpdatedAt': datetime.now().isoformat() 25 | }) 26 | 27 | 28 | # Initialize clients 29 | def initialize_clients(): 30 | # Initialize required AWS clients 31 | identitystore = boto3.client('identitystore') 32 | sso_admin = boto3.client('sso-admin') 33 | dynamodb = boto3.resource('dynamodb') 34 | 35 | # Get Identity Store ID 36 | sso = boto3.client('sso-admin') 37 | identity_store_id = sso.list_instances()['Instances'][0]['IdentityStoreId'] 38 | instance_arn = sso.list_instances()['Instances'][0]['InstanceArn'] 39 | 40 | return identitystore, sso_admin, dynamodb, identity_store_id, instance_arn 41 | 42 | def lambda_handler(event, context): 43 | 44 | identitystore, sso_admin, dynamodb, identity_store_id, instance_arn = initialize_clients() 45 | 46 | # List group memberships 47 | try: 48 | list_group_memberships(identitystore, dynamodb, identity_store_id) 49 | print("Listed group memberships successfully") 50 | return { 51 | 'statusCode': 200, 52 | 'body': json.dumps('Listed group memberships successfully') 53 | } 54 | # Return success response 55 | except Exception as e: 56 | print(f"Error listing group memberships: {e}") 57 | return { 58 | 'statusCode': 500, 59 | 'body': json.dumps('Error listing group memberships') 60 | } 61 | -------------------------------------------------------------------------------- /source/idciaminventoryrole/stack-set-creation.md: -------------------------------------------------------------------------------- 1 | ### Creating and deploying IAM role to enable cross-account IAM role for collection of IAM Identity Center provisioned IAM roles 2 | 3 | Run this in the Management account to deploy a CloudFormation stackset that will create the IAM role needed to allow inventory of IAM roles in all accounts 4 | 5 | *Caveat: Skip step 1 if your AWS IAM Identity Center is running in your management account and you have NOT delegated the administration to a member account* 6 | 7 | #### 1. Create stackset 8 | 9 | **NOTE:** 10 | * Replace `` with the 12-digit account ID that you have nominated as the administration account for AWS IAM Identity Center 11 | * Replace `` with the IAM Role Arn for the GetIamRoles Lambda function - see the CloudFormation output of the Aria-Setup stack for the required Arn 12 | 13 | ``` 14 | aws cloudformation create-stack-set \ 15 | --stack-set-name aria-orglevel-iamlistroles \ 16 | --template-body file://idc-iam-inventory-role.yaml \ 17 | --parameters ParameterKey=TrustedAccountId,ParameterValue= ParameterKey=IAMRolesLambdaCrossAccountRole,ParameterValue= \ 18 | --permission-model SERVICE_MANAGED \ 19 | --auto-deployment Enabled=true,RetainStacksOnAccountRemoval=false \ 20 | --capabilities CAPABILITY_NAMED_IAM 21 | ``` 22 | 23 | #### 2. Create stack instances in all member accounts 24 | 25 | **NOTE:** 26 | * This will ONLY create stack instances in *Member* accounts - it will NOT create a stack instance in the Management account as we are using service managed permissions. See step 3 for how to address that 27 | * Replace `` with the Organizations root OU - you can programmatically retrieve this using `aws organizations list-roots | jq '.Roots[0].Id'` 28 | 29 | ``` 30 | aws cloudformation create-stack-instances \ 31 | --stack-set-name aria-orglevel-iamlistroles \ 32 | --regions us-east-1 \ 33 | --deployment-targets OrganizationalUnitIds= 34 | ``` 35 | 36 | #### 3. Deploy the stack in the management account 37 | 38 | In the management account you must manually deploy the cloudformation stack using file `idc-iam-inventory-role.yaml` 39 | 40 | **Note:** 41 | * Replace `arn:aws:iam::<01234567890>:role/-GetIAMRolesLambdaExecutionRole-` in the CloudFormation parameter screen with the IAM Role Arn for the GetIamRoles Lambda function - see the CloudFormation output for `GetIAMRolesLambdaFunctionExecutionRoleArn` for the required Arn 42 | 43 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /source/listuseraccountassignments/lambda_function.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | import time 4 | from datetime import datetime 5 | 6 | # Get all accounts cached in the dynamodb table 7 | def get_all_accounts(): 8 | # Get list of all accounts in the organization 9 | dynamodb = boto3.resource('dynamodb') 10 | accounts_table = dynamodb.Table('AriaIdCAccounts') 11 | accounts = [] 12 | 13 | try: 14 | response = accounts_table.scan() 15 | 16 | # Process items in the response 17 | for item in response['Items']: 18 | account = { 19 | 'Name': item.get('Name', 'N/A'), 20 | 'AccountId': item.get('AccountId', 'N/A') 21 | } 22 | accounts.append(account) 23 | return accounts 24 | 25 | except ClientError as e: 26 | print(f"An error occurred: {e.response['Error']['Message']}") 27 | return None 28 | 29 | # List all account assignments for principals with permission sets in DynamoDB 30 | def list_account_assignments_for_users(sso_admin, dynamodb, instance_arn): 31 | # List all account assignments for users and store in DynamoDB 32 | print(f"Listing all account assigments and permission set assignments for principals") 33 | 34 | table = dynamodb.Table('AriaIdCUserAccountAssignments') 35 | permset_table = dynamodb.Table('AriaIdCPermissionSets') 36 | users_table = dynamodb.Table('AriaIdCUsers') 37 | users = users_table.scan()['Items'] 38 | 39 | # Get all accounts 40 | print("Fetching account list...") 41 | accounts = get_all_accounts() 42 | 43 | for user in users: 44 | for account in accounts: 45 | try: 46 | #print(f"Processing assignments for user {user['UserId']} in account {account['AccountId']}") 47 | paginator = sso_admin.get_paginator('list_account_assignments_for_principal') 48 | for page in paginator.paginate( 49 | InstanceArn=instance_arn, 50 | Filter={'AccountId': account['AccountId']}, 51 | PrincipalType='USER', 52 | PrincipalId=user['UserId'] 53 | ): 54 | for assignment in page['AccountAssignments']: 55 | # print(f"Processing assignments for user {user['UserId']} in account {account['Name']} with assignment {assignment['PermissionSetArn']}") 56 | permset_name = permset_table.get_item(Key={'PermissionSetArn': assignment['PermissionSetArn']})['Item']['Name'] 57 | # print(f"Processing {permset_name}") 58 | table.put_item(Item={ 59 | 'UserId': user['UserId'], 60 | 'AccountId': account['AccountId'], 61 | 'PrincipalType': 'USER', 62 | 'PrincipalName': user.get('UserName', 'N/A'), 63 | 'AccountName': account['Name'], 64 | 'PermissionSetArn': assignment['PermissionSetArn'], 65 | 'Name': permset_name, 66 | 'UpdatedAt': datetime.now().isoformat() 67 | }) 68 | except Exception as e: 69 | print(f"Error processing assignments for user {user['UserId']} in account {account['AccountId']}: {str(e)}") 70 | 71 | # Initialize clients 72 | def initialize_clients(): 73 | # Initialize required AWS clients 74 | sso_admin = boto3.client('sso-admin') 75 | dynamodb = boto3.resource('dynamodb') 76 | 77 | # Get Identity Store ID 78 | sso = boto3.client('sso-admin') 79 | instance_arn = sso.list_instances()['Instances'][0]['InstanceArn'] 80 | 81 | return sso_admin, dynamodb, instance_arn 82 | 83 | def lambda_handler(event, context): 84 | 85 | sso_admin, dynamodb, instance_arn = initialize_clients() 86 | 87 | # List account assignments for all users 88 | try: 89 | list_account_assignments_for_users(sso_admin, dynamodb, instance_arn) 90 | print("Listed account assignments for USER principals successfully") 91 | return { 92 | 'statusCode': 200, 93 | 'body': json.dumps('Listed account assignments for USER principals successfully') 94 | } 95 | # Return success response 96 | except Exception as e: 97 | print(f"Error listing account assignments for USER principals: {e}") 98 | return { 99 | 'statusCode': 500, 100 | 'body': json.dumps('Error listing account assignments for USER principals') 101 | } 102 | -------------------------------------------------------------------------------- /aria-bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script sets up the necessary AWS resources for the Aria application 3 | # It creates S3 buckets for source code and exports, and prepares Lambda function zip files 4 | 5 | echo "+---------------------------------------------------+" 6 | echo "| Aria Bootstrap Script |" 7 | echo "+---------------------------------------------------+" 8 | 9 | # Set the region - NOTE CHANGE THIS TO DESIRED REGION 10 | REGION=us-east-1 11 | 12 | # Get account id and get the last 4 digits 13 | ACCOUNTID=$(aws sts get-caller-identity --output json |grep Account |awk -F ': "' '{print$2}' |sed 's/\".*//') 14 | ACCOUNTIDSHORT=$(echo "$ACCOUNTID" | cut -c 9-12) 15 | 16 | # Generate a random 8 character string in lower case 17 | RANDOMSTRING="$(mktemp -u XXXXXXXX | tr 'A-Z' 'a-z')" 18 | 19 | # Check SSM parameter store to see if the source bucket name has already been generated 20 | SOURCE_BUCKET=$(aws ssm get-parameter --name "aria-source-bucket" --region "$REGION" --query "Parameter.Value" --output text) 21 | 22 | # Check if the parameter store value exists, if not then set the variable (will be stored in SSM parameter store later) 23 | if [ -z "$SOURCE_BUCKET" ]; then 24 | echo "Source Bucket variable not set...creating..." 25 | SOURCE_BUCKET="aria-source-$ACCOUNTIDSHORT-$RANDOMSTRING" 26 | fi 27 | 28 | # Define the local directory for storing zip files temporarily 29 | SOURCE_DIR="./zip/" 30 | 31 | # Check if the source directory exists 32 | if [ ! -d "$SOURCE_DIR" ]; then 33 | echo "Creating temporary local source directory..." 34 | mkdir zip 35 | fi 36 | 37 | # Check SSM parameter store to see if the export bucket name has already been generated 38 | EXPORT_BUCKET=$(aws ssm get-parameter --name "aria-export-bucket" --region "$REGION" --query "Parameter.Value" --output text) 39 | 40 | # Check if the parameter store value exists, if not then set the variable (will be stored in SSM parameter store later) 41 | if [ -z "$EXPORT_BUCKET" ]; then 42 | echo "Export Bucket variable not set...creating..." 43 | EXPORT_BUCKET="aria-export-$ACCOUNTIDSHORT-$RANDOMSTRING" 44 | fi 45 | 46 | echo "Aria Source Bucket is : $SOURCE_BUCKET" 47 | 48 | # Check if aria SOURCE_BUCKET exists - create if missing, add configure bucket for EventBridge notifications 49 | if aws s3api head-bucket --bucket "$SOURCE_BUCKET" 2>/dev/null; then 50 | echo "Source Bucket does not exist...creating..." 51 | aws s3api create-bucket \ 52 | --bucket "$SOURCE_BUCKET" \ 53 | --region "$REGION" \ 54 | $(if [ "$REGION" != "us-east-1" ]; then echo "--create-bucket-configuration LocationConstraint=$REGION"; fi) 55 | fi 56 | 57 | # Configure EventBridge notifications (done once regardless of bucket existence) 58 | aws s3api put-bucket-notification-configuration \ 59 | --bucket "$SOURCE_BUCKET" \ 60 | --notification-configuration '{"EventBridgeConfiguration": {}}' 61 | 62 | echo "Aria Export Bucket is : $EXPORT_BUCKET" 63 | 64 | # Check if aria EXPORT_BUCKET exists 65 | if aws s3api head-bucket --bucket "$EXPORT_BUCKET" 2>/dev/null; then 66 | echo "Export Bucket already exists" 67 | else 68 | echo "Export Bucket does not exist...creating..." 69 | aws s3api create-bucket \ 70 | --bucket "$EXPORT_BUCKET" \ 71 | --region "$REGION" \ 72 | $(if [ "$REGION" != "us-east-1" ]; then echo "--create-bucket-configuration LocationConstraint=$REGION"; fi) 73 | fi 74 | # Create Lambda function zip files 75 | echo "Creating directories and zip files..." 76 | 77 | # Define the list of Lambda functions 78 | LAMBDA_FUNCTIONS=( 79 | "createtables" 80 | "listusers" 81 | "listgroups" 82 | "listgroupmembership" 83 | "listpermissionsets" 84 | "listprovisionedpermissionsets" 85 | "listaccounts" 86 | "listuseraccountassignments" 87 | "listgroupaccountassignments" 88 | "getiamroles" 89 | "accessanalyzerfindingingestion" 90 | "s3export" 91 | "updatefunctioncode" 92 | ) 93 | 94 | # Remove existing zip files 95 | rm -f ./zip/*.zip 96 | 97 | # Create directories and zip files in a loop 98 | for func in "${LAMBDA_FUNCTIONS[@]}"; do 99 | echo "Processing ${func}..." 100 | mkdir -p "./source/${func}" 101 | zip -j "./zip/${func}.zip" "./source/${func}/lambda_function.py" 102 | done 103 | 104 | echo "Zip files created successfully!" 105 | 106 | # Copy files to SOURCE_BUCKET 107 | echo "Uploading zip files to S3 bucket: $SOURCE_BUCKET" 108 | aws s3 rm "s3://$SOURCE_BUCKET/" --recursive 109 | aws s3 cp "$SOURCE_DIR" "s3://$SOURCE_BUCKET/" --recursive --exclude "*" --include "*.zip" 110 | 111 | # Delete files from zip bucket 112 | echo "Cleaning up..." 113 | rm -rf "$SOURCE_DIR"*.zip 114 | rmdir "$SOURCE_DIR" 115 | 116 | echo "-----" 117 | echo "Storing generated bucket names in SSM parameter store..." 118 | # Save the bucket names to SSM parameter store for future reference 119 | aws ssm put-parameter --name "aria-source-bucket" --value "$SOURCE_BUCKET" --type "String" --overwrite > /dev/null 120 | aws ssm put-parameter --name "aria-export-bucket" --value "$EXPORT_BUCKET" --type "String" --overwrite > /dev/null 121 | echo "Source Bucket: $SOURCE_BUCKET" 122 | echo "Export Bucket: $EXPORT_BUCKET" 123 | echo "-----" 124 | echo "Pre-requisites setup complete." -------------------------------------------------------------------------------- /source/listgroupaccountassignments/lambda_function.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | import time 4 | from datetime import datetime 5 | 6 | # Get all accounts cached in the dynamodb table 7 | def get_all_accounts(): 8 | # Get list of all accounts in the organization 9 | dynamodb = boto3.resource('dynamodb') 10 | accounts_table = dynamodb.Table('AriaIdCAccounts') 11 | accounts = [] 12 | 13 | try: 14 | response = accounts_table.scan() 15 | 16 | # Process items in the response 17 | for item in response['Items']: 18 | account = { 19 | 'Name': item.get('Name', 'N/A'), 20 | 'AccountId': item.get('AccountId', 'N/A') 21 | } 22 | accounts.append(account) 23 | return accounts 24 | 25 | except ClientError as e: 26 | print(f"An error occurred: {e.response['Error']['Message']}") 27 | return None 28 | 29 | # List all account assignments for principals with permission sets in DynamoDB 30 | def list_account_assignments_for_groups(sso_admin, dynamodb, instance_arn): 31 | # List all account assignments for groups and store in DynamoDB 32 | print(f"Listing all account assigments and permission set assignments for principals") 33 | 34 | table = dynamodb.Table('AriaIdCGroupAccountAssignments') 35 | permset_table = dynamodb.Table('AriaIdCPermissionSets') 36 | groups_table = dynamodb.Table('AriaIdCGroups') 37 | groups = groups_table.scan()['Items'] 38 | 39 | # Get a list of all accounts in the organization 40 | organizations = boto3.client('organizations') 41 | 42 | # Get all accounts 43 | print("Fetching account list...") 44 | accounts = get_all_accounts() 45 | 46 | for group in groups: 47 | for account in accounts: 48 | try: 49 | # print(f"Processing assignments for group {group['GroupId']} in account {account['AccountId']}") 50 | paginator = sso_admin.get_paginator('list_account_assignments_for_principal') 51 | for page in paginator.paginate( 52 | InstanceArn=instance_arn, 53 | Filter={'AccountId': account['AccountId']}, 54 | PrincipalType='GROUP', 55 | PrincipalId=group['GroupId'] 56 | ): 57 | for assignment in page['AccountAssignments']: 58 | permset_name = permset_table.get_item(Key={'PermissionSetArn': assignment['PermissionSetArn']})['Item']['Name'] 59 | # print(f"Processing {permset_name}") 60 | table.put_item(Item={ 61 | 'GroupId': group['GroupId'], 62 | 'AccountId': account['AccountId'], 63 | 'PrincipalType': 'GROUP', 64 | 'PrincipalName': group.get('GroupName', 'N/A'), 65 | 'AccountName': account['Name'], 66 | 'PermissionSetArn': assignment['PermissionSetArn'], 67 | 'Name': permset_name, 68 | 'UpdatedAt': datetime.now().isoformat() 69 | }) 70 | except Exception as e: 71 | print(f"Error processing assignments for group {group['GroupId']} in account {account['AccountId']}: {str(e)}") 72 | 73 | # Initialize clients 74 | def initialize_clients(): 75 | # Initialize required AWS clients 76 | sso_admin = boto3.client('sso-admin') 77 | dynamodb = boto3.resource('dynamodb') 78 | 79 | # Get Identity Store ID 80 | sso = boto3.client('sso-admin') 81 | instance_arn = sso.list_instances()['Instances'][0]['InstanceArn'] 82 | 83 | return sso_admin, dynamodb, instance_arn 84 | 85 | def empty_group_account_assignments_table(): 86 | # Empty the group account assignments table 87 | dynamodb = boto3.resource('dynamodb') 88 | table = dynamodb.Table('AriaIdCGroupAccountAssignments') 89 | scan = table.scan() 90 | with table.batch_writer() as batch: 91 | for each in scan['Items']: 92 | batch.delete_item( 93 | Key={ 94 | 'GroupId': each['GroupId'], 95 | 'AccountId': each['AccountId'] 96 | } 97 | ) 98 | 99 | def lambda_handler(event, context): 100 | 101 | sso_admin, dynamodb, instance_arn = initialize_clients() 102 | 103 | # List account assignments for all groups 104 | try: 105 | empty_group_account_assignments_table() 106 | list_account_assignments_for_groups(sso_admin, dynamodb, instance_arn) 107 | print("Listed account assignments for GROUP principals successfully") 108 | return { 109 | 'statusCode': 200, 110 | 'body': json.dumps('Listed account assignments for GROUP principals successfully') 111 | } 112 | # Return success response 113 | except Exception as e: 114 | print(f"Error listing account assignments for GROUP principals: {e}") 115 | return { 116 | 'statusCode': 500, 117 | 'body': json.dumps('Error listing account assignments for GROUP principals') 118 | } 119 | -------------------------------------------------------------------------------- /source/listprovisionedpermissionsets/lambda_function.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | import time 4 | import os 5 | from datetime import datetime 6 | 7 | # Get all accounts in the AriaIdCAccounts table 8 | def get_all_accounts(): 9 | # List all accounts in AriaIdCAccounts 10 | dynamodb = boto3.resource('dynamodb') 11 | table = dynamodb.Table('AriaIdCAccounts') 12 | response = table.scan() 13 | accounts = response['Items'] 14 | return accounts 15 | 16 | # Get all permission sets in the AriaIdCPermissionSets table 17 | def get_all_permission_sets(): 18 | # List all permission sets in AriaIdCPermissionSets 19 | dynamodb = boto3.resource('dynamodb') 20 | table = dynamodb.Table('AriaIdCPermissionSets') 21 | response = table.scan() 22 | permission_sets = response['Items'] 23 | return permission_sets 24 | 25 | # List all provisioned permission sets and store in DynamoDB 26 | def list_provisioned_permission_sets(sso_admin, dynamodb, instance_arn): 27 | # List all provisioned permission sets by account and store in DynamoDB 28 | print(f"Listing all provisioned permission sets") 29 | 30 | table = dynamodb.Table('AriaIdCProvisionedPermissionSets') 31 | organizations = boto3.client('organizations') 32 | management_account_id = organizations.describe_organization()["Organization"]["MasterAccountId"] 33 | # print(f"Management account ID: {management_account_id}") 34 | 35 | accounts = get_all_accounts() 36 | permission_sets = get_all_permission_sets() 37 | # print(f"Permission sets from AriaIdCPermissionSets table: {permission_sets}") 38 | 39 | for account in accounts: 40 | try: 41 | # print(f"Listing provisioned permission sets for account {account['AccountId']}") 42 | #if account['Id'] == management_account_id: 43 | # print(f"Skipping management account {account['Id']}") 44 | # continue 45 | # Skip Management account 46 | if account['Status'] != 'ACTIVE': 47 | print(f"Skipping inactive account {account['AccountId']}") 48 | continue 49 | # Skip inactive accounts 50 | else: 51 | response = sso_admin.list_permission_sets_provisioned_to_account( 52 | InstanceArn=instance_arn, 53 | AccountId=account['AccountId'] 54 | ) 55 | provisioned_permission_sets = response['PermissionSets'] 56 | # print(f"Provisioned permission sets for account {account['AccountId']}: {provisioned_permission_sets}") 57 | 58 | for permission_set_arn in provisioned_permission_sets: 59 | # Find Name in permission_sets where PermissionSetArn = permission_set_arn 60 | permission_set_name = next((item['Name'] for item in permission_sets if item['PermissionSetArn'] == permission_set_arn), None) 61 | 62 | table.put_item(Item={ 63 | 'PermissionSetArn': permission_set_arn, 64 | 'PermissionSetName': permission_set_name, 65 | 'AccountId': account['AccountId'], 66 | 'AccountName': account['Name'], 67 | 'UpdatedAt': datetime.now().isoformat() 68 | }) 69 | except Exception as e: 70 | print(f"Error processing account {account['AccountId']}: {str(e)}") 71 | 72 | # Initialize clients 73 | def initialize_clients(): 74 | # Initialize required AWS clients 75 | sso_admin = boto3.client('sso-admin') 76 | dynamodb = boto3.resource('dynamodb') 77 | 78 | # Get Identity Store ID 79 | sso = boto3.client('sso-admin') 80 | instance_arn = sso.list_instances()['Instances'][0]['InstanceArn'] 81 | 82 | return sso_admin, dynamodb, instance_arn 83 | 84 | def empty_provisioned_permission_sets_table(): 85 | # Empty the provisioned permission sets table 86 | dynamodb = boto3.resource('dynamodb') 87 | table = dynamodb.Table('AriaIdCProvisionedPermissionSets') 88 | scan = table.scan() 89 | with table.batch_writer() as batch: 90 | for each in scan['Items']: 91 | batch.delete_item( 92 | Key={ 93 | 'PermissionSetArn': each['PermissionSetArn'], 94 | 'AccountId': each['AccountId'] 95 | } 96 | ) 97 | 98 | def lambda_handler(event, context): 99 | 100 | sso_admin, dynamodb, instance_arn = initialize_clients() 101 | 102 | # List permission sets 103 | try: 104 | empty_provisioned_permission_sets_table() 105 | list_provisioned_permission_sets(sso_admin, dynamodb, instance_arn) 106 | print("Listed provisioned permission sets successfully") 107 | return { 108 | 'statusCode': 200, 109 | 'body': json.dumps('Listed provisioned permission sets successfully') 110 | } 111 | # Return success response 112 | except Exception as e: 113 | print(f"Error listing provisioned permission sets: {e}") 114 | return { 115 | 'statusCode': 500, 116 | 'body': json.dumps('Error listing provisioned permission sets') 117 | } 118 | -------------------------------------------------------------------------------- /templates/eventbridge-stack.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: 'EventBridge rules and related resources for Workforce Identity Visualization' 3 | 4 | Parameters: 5 | S3SourceBucketName: 6 | Type: String 7 | Description: Name of the S3 source bucket 8 | UpdateFunctionCodeLambdaArn: 9 | Type: String 10 | Description: ARN of UpdateFunctionCode Lambda function 11 | AccessAnalyzerFindingIngestionLambdaArn: 12 | Type: String 13 | Description: ARN of AccessAnalyzerFindingIngestion Lambda function 14 | StackName: 15 | Type: String 16 | Description: The parent stack name 17 | 18 | Resources: 19 | # IAM Role for EventBridge to invoke UpdateFunctionCode Lambda 20 | InvokeUpdateFunctionCodeEventBridgeInvokeRole: 21 | Type: AWS::IAM::Role 22 | Properties: 23 | AssumeRolePolicyDocument: 24 | Version: '2012-10-17' 25 | Statement: 26 | - Effect: Allow 27 | Principal: 28 | Service: events.amazonaws.com 29 | Action: 'sts:AssumeRole' 30 | Condition: 31 | StringEquals: 32 | aws:SourceArn: !Sub 'arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${StackName}-Source-Trigger-UpdateFunctionCode' 33 | Policies: 34 | - PolicyName: InvokeUpdateFunctionCodeEventBridgeInvokePolicy 35 | PolicyDocument: 36 | Version: '2012-10-17' 37 | Statement: 38 | - Effect: Allow 39 | Action: 40 | - lambda:InvokeFunction 41 | Resource: !Ref UpdateFunctionCodeLambdaArn 42 | Tags: 43 | - Key: aria 44 | Value: role 45 | 46 | # EventBridge rule to invoke UpdateFunctionCode lambda when objects are uploaded to source bucket 47 | InvokeUpdateFunctionCodeEventBridgeRule: 48 | Type: AWS::Events::Rule 49 | Properties: 50 | Name: !Sub '${StackName}-Source-Trigger-UpdateFunctionCode' 51 | EventPattern: !Sub '{"source":["aws.s3"],"detail-type":["Object Created"],"detail":{"bucket":{"name":["${S3SourceBucketName}"]}}}' 52 | State: ENABLED 53 | EventBusName: default 54 | Targets: 55 | - Id: Id3b1ca365-df10-4d67-a299-1b553942d434 56 | Arn: !Ref UpdateFunctionCodeLambdaArn 57 | RoleArn: !GetAtt InvokeUpdateFunctionCodeEventBridgeInvokeRole.Arn 58 | 59 | # IAM Role for EventBridge to invoke AccessAnalyzer Lambda 60 | AriaAccessAnalyzerFindingIngestionEventBridgeInvokeRole: 61 | Type: AWS::IAM::Role 62 | Properties: 63 | AssumeRolePolicyDocument: 64 | Version: '2012-10-17' 65 | Statement: 66 | - Effect: Allow 67 | Principal: 68 | Service: events.amazonaws.com 69 | Action: 'sts:AssumeRole' 70 | Condition: 71 | StringEquals: 72 | aws:SourceArn: 73 | - !Sub 'arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${StackName}-Internal-AA-Trigger-Ingestion' 74 | - !Sub 'arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${StackName}-Unused-AA-Trigger-Ingestion' 75 | - !Sub 'arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${StackName}-External-AA-Trigger-Ingestion' 76 | Policies: 77 | - PolicyName: AccessAnalyzerFindingIngestionEventBridgeInvokePolicy 78 | PolicyDocument: 79 | Version: '2012-10-17' 80 | Statement: 81 | - Effect: Allow 82 | Action: 83 | - lambda:InvokeFunction 84 | Resource: !Ref AccessAnalyzerFindingIngestionLambdaArn 85 | Tags: 86 | - Key: aria 87 | Value: role 88 | 89 | # EventBridge rules for Access Analyzer findings 90 | AriaInternalAAFindingEventBridgeRule: 91 | Type: AWS::Events::Rule 92 | Properties: 93 | Name: !Sub '${StackName}-Internal-AA-Trigger-Ingestion' 94 | EventPattern: >- 95 | {"source":["aws.access-analyzer"],"detail-type":["Internal Access 96 | Finding"],"detail":{"principal":{"AWS":[{"wildcard":"arn:aws:iam::*:role/aws-reserved/sso.amazonaws.com/AWSReservedSSO_*"}]}}} 97 | State: ENABLED 98 | EventBusName: default 99 | Targets: 100 | - Id: Idd97798e1-e511-4494-8c47-3f25568cd5bc 101 | Arn: !Ref AccessAnalyzerFindingIngestionLambdaArn 102 | RoleArn: !GetAtt AriaAccessAnalyzerFindingIngestionEventBridgeInvokeRole.Arn 103 | 104 | AriaUnusedAAFindingEventBridgeRule: 105 | Type: AWS::Events::Rule 106 | Properties: 107 | Name: !Sub '${StackName}-Unused-AA-Trigger-Ingestion' 108 | EventPattern: >- 109 | {"source":["aws.access-analyzer"],"detail-type":["Unused Access Finding for IAM entities"],"detail.resource":[{"wildcard":"arn:aws:iam::*:role/aws-reserved/sso.amazonaws.com/AWSReservedSSO_*"}]} 110 | State: ENABLED 111 | EventBusName: default 112 | Targets: 113 | - Id: Idd97798e1-e511-4494-8c47-3f25568cd5bc 114 | Arn: !Ref AccessAnalyzerFindingIngestionLambdaArn 115 | RoleArn: !GetAtt AriaAccessAnalyzerFindingIngestionEventBridgeInvokeRole.Arn 116 | 117 | AriaExternalAAFindingEventBridgeRule: 118 | Type: AWS::Events::Rule 119 | Properties: 120 | Name: !Sub '${StackName}-External-AA-Trigger-Ingestion' 121 | EventPattern: >- 122 | {"source":["aws.access-analyzer"],"detail-type":["Access Analyzer Finding"]} 123 | State: ENABLED 124 | EventBusName: default 125 | Targets: 126 | - Id: Idd97798e1-e511-4494-8c47-3f25568cd5bc 127 | Arn: !Ref AccessAnalyzerFindingIngestionLambdaArn 128 | RoleArn: !GetAtt AriaAccessAnalyzerFindingIngestionEventBridgeInvokeRole.Arn 129 | 130 | Outputs: 131 | UpdateFunctionCodeEventBridgeRuleArn: 132 | Description: ARN of the UpdateFunctionCode EventBridge rule 133 | Value: !GetAtt InvokeUpdateFunctionCodeEventBridgeRule.Arn 134 | AccessAnalyzerEventBridgeRoleArn: 135 | Description: ARN of the Access Analyzer EventBridge role 136 | Value: !GetAtt AriaAccessAnalyzerFindingIngestionEventBridgeInvokeRole.Arn -------------------------------------------------------------------------------- /source/createtables/lambda_function.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | import time 4 | from datetime import datetime 5 | 6 | # Create required tables in DynamoDB 7 | def create_tables(dynamodb): 8 | # Create DynamoDB tables if they don't exist 9 | tables = { 10 | 'AriaIdCUsers': { 11 | 'KeySchema': [ 12 | {'AttributeName': 'UserId', 'KeyType': 'HASH'} 13 | ], 14 | 'AttributeDefinitions': [ 15 | {'AttributeName': 'UserId', 'AttributeType': 'S'} 16 | ] 17 | }, 18 | 'AriaIdCGroups': { 19 | 'KeySchema': [ 20 | {'AttributeName': 'GroupId', 'KeyType': 'HASH'} 21 | ], 22 | 'AttributeDefinitions': [ 23 | {'AttributeName': 'GroupId', 'AttributeType': 'S'} 24 | ] 25 | }, 26 | 'AriaIdCGroupMembership': { 27 | 'KeySchema': [ 28 | {'AttributeName': 'GroupId', 'KeyType': 'HASH'}, 29 | {'AttributeName': 'UserId', 'KeyType': 'RANGE'} 30 | ], 31 | 'AttributeDefinitions': [ 32 | {'AttributeName': 'GroupId', 'AttributeType': 'S'}, 33 | {'AttributeName': 'UserId', 'AttributeType': 'S'} 34 | ] 35 | }, 36 | 'AriaIdCPermissionSets': { 37 | 'KeySchema': [ 38 | {'AttributeName': 'PermissionSetArn', 'KeyType': 'HASH'}, 39 | ], 40 | 'AttributeDefinitions': [ 41 | {'AttributeName': 'PermissionSetArn', 'AttributeType': 'S'}, 42 | ] 43 | }, 44 | 'AriaIdCProvisionedPermissionSets': { 45 | 'KeySchema': [ 46 | {'AttributeName': 'PermissionSetArn', 'KeyType': 'HASH'}, 47 | {'AttributeName': 'AccountId', 'KeyType': 'RANGE'} 48 | ], 49 | 'AttributeDefinitions': [ 50 | {'AttributeName': 'PermissionSetArn', 'AttributeType': 'S'}, 51 | {'AttributeName': 'AccountId', 'AttributeType': 'S'} 52 | ] 53 | }, 54 | 'AriaIdCAccounts': { 55 | 'KeySchema': [ 56 | {'AttributeName': 'AccountId', 'KeyType': 'HASH'} 57 | ], 58 | 'AttributeDefinitions': [ 59 | {'AttributeName': 'AccountId', 'AttributeType': 'S'} 60 | ] 61 | }, 62 | 'AriaIdCUserAccountAssignments': { 63 | 'KeySchema': [ 64 | {'AttributeName': 'AccountId', 'KeyType': 'HASH'}, 65 | {'AttributeName': 'UserId', 'KeyType': 'RANGE'} 66 | ], 67 | 'AttributeDefinitions': [ 68 | {'AttributeName': 'AccountId', 'AttributeType': 'S'}, 69 | {'AttributeName': 'UserId', 'AttributeType': 'S'} 70 | ] 71 | }, 72 | 'AriaIdCGroupAccountAssignments': { 73 | 'KeySchema': [ 74 | {'AttributeName': 'GroupId', 'KeyType': 'HASH'}, 75 | {'AttributeName': 'AccountId', 'KeyType': 'RANGE'} 76 | ], 77 | 'AttributeDefinitions': [ 78 | {'AttributeName': 'GroupId', 'AttributeType': 'S'}, 79 | {'AttributeName': 'AccountId', 'AttributeType': 'S'} 80 | ] 81 | }, 82 | 'AriaIdCIAMRoles': { 83 | 'KeySchema': [ 84 | {'AttributeName': 'IamRoleArn', 'KeyType': 'HASH'} 85 | ], 86 | 'AttributeDefinitions': [ 87 | {'AttributeName': 'IamRoleArn', 'AttributeType': 'S'} 88 | ] 89 | }, 90 | 'AriaIdCInternalAAFindings': { 91 | 'KeySchema': [ 92 | {'AttributeName': 'FindingId', 'KeyType': 'HASH'} 93 | ], 94 | 'AttributeDefinitions': [ 95 | {'AttributeName': 'FindingId', 'AttributeType': 'S'} 96 | ] 97 | }, 98 | 'AriaIdCUnusedAAFindings': { 99 | 'KeySchema': [ 100 | {'AttributeName': 'FindingId', 'KeyType': 'HASH'} 101 | ], 102 | 'AttributeDefinitions': [ 103 | {'AttributeName': 'FindingId', 'AttributeType': 'S'} 104 | ] 105 | }, 106 | 'AriaIdCExternalAAFindings': { 107 | 'KeySchema': [ 108 | {'AttributeName': 'FindingId', 'KeyType': 'HASH'} 109 | ], 110 | 'AttributeDefinitions': [ 111 | {'AttributeName': 'FindingId', 'AttributeType': 'S'} 112 | ] 113 | } 114 | } 115 | 116 | for table_name, schema in tables.items(): 117 | print(f"Creating dynamoDB table: {table_name}") 118 | try: 119 | table = dynamodb.create_table( 120 | TableName=table_name, 121 | KeySchema=schema['KeySchema'], 122 | AttributeDefinitions=schema['AttributeDefinitions'], 123 | BillingMode='PAY_PER_REQUEST', 124 | SSESpecification={ 125 | 'Enabled': True, 126 | 'SSEType': 'KMS' 127 | }, 128 | Tags=[ 129 | { 130 | 'Key': 'aria', 131 | 'Value': 'data' 132 | } 133 | ] 134 | ) 135 | table.wait_until_exists() 136 | print(f"Table created: {table_name}") 137 | except dynamodb.meta.client.exceptions.ResourceInUseException: 138 | print(f"Table {table_name} already exists so did not create") 139 | 140 | # Initialize clients 141 | def initialize_clients(): 142 | # Initialize AWS clients 143 | dynamodb = boto3.resource('dynamodb') 144 | return dynamodb 145 | 146 | def lambda_handler(event, context): 147 | 148 | dynamodb = initialize_clients() 149 | 150 | # Create tables 151 | try: 152 | create_tables(dynamodb) 153 | print("Tables created successfully") 154 | return { 155 | 'statusCode': 200, 156 | 'body': json.dumps('Tables created successfully') 157 | } 158 | # Return success response 159 | except Exception as e: 160 | print(f"Error creating tables: {e}") 161 | return { 162 | 'statusCode': 500, 163 | 'body': json.dumps('Error creating tables') 164 | } 165 | -------------------------------------------------------------------------------- /source/getiamroles/lambda_function.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | import time 4 | from datetime import datetime 5 | from boto3.dynamodb.conditions import Attr 6 | from botocore.exceptions import ClientError 7 | 8 | def assume_role(account_id, role_name): 9 | # Assume a role in target account 10 | sts_client = boto3.client('sts') 11 | try: 12 | # print(f"Assuming role {role_name} in account {account_id}") 13 | response = sts_client.assume_role( 14 | RoleArn=f'arn:aws:iam::{account_id}:role/{role_name}', 15 | RoleSessionName='ListSSORolesSession' 16 | ) 17 | return response['Credentials'] 18 | except ClientError as e: 19 | print(f"Error assuming role in account {account_id}: {e}") 20 | return None 21 | 22 | def list_idc_roles_in_account(credentials, account_id): 23 | # List IAM roles created by IAM Identity Center in a specific account 24 | if not credentials: 25 | return [] 26 | else: 27 | iam = boto3.client('iam', 28 | aws_access_key_id=credentials['AccessKeyId'], 29 | aws_secret_access_key=credentials['SecretAccessKey'], 30 | aws_session_token=credentials['SessionToken'] 31 | ) 32 | 33 | try: 34 | idc_roles = [] 35 | paginator = iam.get_paginator('list_roles') 36 | 37 | for page in paginator.paginate(): 38 | for role in page['Roles']: 39 | if role['RoleName'].startswith('AWSReservedSSO_'): 40 | # print(f"Found Identity Center IAM role: {role['RoleName']}") 41 | 42 | # Get attached policies 43 | policies = iam.list_attached_role_policies(RoleName=role['RoleName']) 44 | idc_roles.append({ 45 | 'AccountId': account_id, 46 | 'RoleName': role['RoleName'], 47 | 'RoleId': role['RoleId'], 48 | 'Arn': role['Arn'], 49 | 'AttachedPolicies': [p['PolicyName'] for p in policies['AttachedPolicies']], 50 | 'CreateDate': role['CreateDate'] 51 | }) 52 | return idc_roles 53 | except ClientError as e: 54 | print(f"Error listing roles in account {account_id}: {e}") 55 | return [] 56 | 57 | # Get all permission sets and accounts cached in dynamodb table AriaIdCProvisionedPermissionSets 58 | def get_provisioned_permission_sets(account_id): 59 | # Get list of all permission sets in AriaIdCProvisionedPermissionSets for a given Account 60 | dynamodb = boto3.resource('dynamodb') 61 | provisioned_permission_sets_table = dynamodb.Table('AriaIdCProvisionedPermissionSets') 62 | provisioned_permission_sets = [] 63 | 64 | try: 65 | # Construct the Filter Expression 66 | filter_expression = (Attr('AccountId').eq(account_id)) 67 | response = provisioned_permission_sets_table.scan( 68 | FilterExpression=filter_expression 69 | ) 70 | 71 | # Process items in the response 72 | for item in response['Items']: 73 | provisioned_permission_set = { 74 | 'AccountId': item.get('AccountId', 'N/A'), 75 | 'AccountName': item.get('AccountName', 'N/A'), 76 | 'PermissionSetArn': item.get('PermissionSetArn', 'N/A'), 77 | 'PermissionSetName': item.get('PermissionSetName', 'N/A') 78 | } 79 | provisioned_permission_sets.append(provisioned_permission_set) 80 | return provisioned_permission_sets 81 | except ClientError as e: 82 | print(f"An error occurred: {e.response['Error']['Message']}") 83 | return None 84 | 85 | # Initialize clients 86 | def initialize_clients(): 87 | # Initialize required AWS clients 88 | dynamodb = boto3.resource('dynamodb') 89 | return dynamodb 90 | 91 | def empty_iam_roles_table(): 92 | # Empty the provisioned permission sets table 93 | dynamodb = boto3.resource('dynamodb') 94 | table = dynamodb.Table('AriaIdCIAMRoles') 95 | scan = table.scan() 96 | with table.batch_writer() as batch: 97 | for each in scan['Items']: 98 | batch.delete_item( 99 | Key={ 100 | 'IamRoleArn': each['IamRoleArn'] 101 | } 102 | ) 103 | 104 | def lambda_handler(event, context): 105 | 106 | # Role to assume in member accounts (needs to exist in all accounts) - created using stackset (see elsewhere) 107 | role_to_assume = 'AriaIdCInventoryAccessRole-LimitedReadOnly' 108 | 109 | dynamodb = initialize_clients() 110 | accounts_table = dynamodb.Table('AriaIdCAccounts') 111 | iamroles_table = dynamodb.Table('AriaIdCIAMRoles') 112 | 113 | empty_iam_roles_table() 114 | 115 | for account in accounts_table.scan()['Items']: 116 | account_id=account['AccountId'] 117 | 118 | # Get all provisioned permission sets in account 119 | # print(f"Fetching list of provisioned permission sets in account {account_id}") 120 | provisioned_permission_sets_in_account = [] 121 | provisioned_permission_sets_in_account = get_provisioned_permission_sets(account_id) 122 | 123 | credentials = assume_role(account_id, role_to_assume) 124 | idc_roles = [] 125 | idc_roles = list_idc_roles_in_account(credentials, account_id) 126 | 127 | for role in idc_roles: 128 | permsetname = role['RoleName'].replace('AWSReservedSSO_', '') 129 | permsetname = permsetname[:-17] 130 | 131 | # If permsetname exists in provisioned_permission_sets then get the permsetarn 132 | for permissionset in provisioned_permission_sets_in_account: 133 | # for permset in permissionset: 134 | if permissionset['PermissionSetName'] == permsetname: 135 | permsetarn = permissionset['PermissionSetArn'] 136 | # print(f"Permission Set Name: {permsetname}, Permission Set Arn: {permsetarn}, IAM Role Name: {role['RoleName']}, IAM Role Arn: {role['Arn']}") 137 | break 138 | 139 | # Write role information to DynamoDB 140 | # print(f"Writing role information to DynamoDB table for account {account_id}") 141 | iamroles_table.put_item(Item={ 142 | 'IamRoleArn': role['Arn'], 143 | 'RoleName': role['RoleName'], 144 | 'AccountId': role['AccountId'], 145 | 'RoleId': role['RoleId'], 146 | 'AttachedPolicies': role['AttachedPolicies'], 147 | 'PermissionSetName': permsetname, 148 | 'PermissionSetArn': permsetarn, 149 | 'CreateDate': role['CreateDate'].isoformat() 150 | }) 151 | -------------------------------------------------------------------------------- /source/accessanalyzerfindingingestion/lambda_function.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | from datetime import datetime 4 | import re 5 | from botocore.exceptions import ClientError 6 | 7 | # Internal Access Finding 8 | def parse_internalaccess_finding(event,table_ia): 9 | # Parse the event detail 10 | detail = (event['detail']) 11 | #print(f"Event detail:{detail}") 12 | 13 | delimiter = ", " # Define a delimiter 14 | 15 | # Extract relevant information from the event 16 | finding_id = detail['id'] 17 | finding_type = detail['findingType'] 18 | 19 | print(f"Parsing Internal Access finding {finding_id} and extracting relevant attributes...") 20 | 21 | principal = detail['principal']['AWS'] 22 | principal_type = detail.get('principalType', 'N/A') 23 | principal_owner_account = detail.get('principalOwnerAccount', 'N/A') 24 | principal_name = extract_role_name(principal) 25 | 26 | resource_type = detail.get('resourceType', 'N/A') 27 | resource_arn = detail.get('resource', 'N/A') 28 | resource_account = detail.get('accountId', 'N/A') 29 | 30 | rcp_policyrestriction_type = detail.get('resourceControlPolicyRestrictionType', 'N/A') 31 | scp_policyrestriction_type = detail.get('serviceControlPolicyRestrictionType', 'N/A') 32 | 33 | access_type = detail.get('accessType', 'N/A') 34 | status = detail['status'] 35 | 36 | action_array = detail.get('action', '') 37 | action = delimiter.join(action_array) 38 | 39 | created_at = detail['createdAt'] 40 | updated_at = detail['updatedAt'] 41 | 42 | # Prepare the item to be inserted into DynamoDB 43 | item = { 44 | 'FindingId': finding_id, 45 | 'FindingType': finding_type, 46 | 'Principal': principal, 47 | 'PrincipalName': principal_name, 48 | 'PrincipalOwnerAccount': principal_owner_account, 49 | 'PrincipalType': principal_type, 50 | 'ResourceType': resource_type, 51 | 'ResourceARN': resource_arn, 52 | 'ResourceAccount': resource_account, 53 | 'ResourceControlPolicyRestrictionType': rcp_policyrestriction_type, 54 | 'ServiceControlPolicyRestrictionType': scp_policyrestriction_type, 55 | 'AccessType': access_type, 56 | 'Action': action, 57 | 'Status': status, 58 | 'CreatedAt': created_at, 59 | 'UpdatedAt': updated_at, 60 | 'ProcessedAt': datetime.now().isoformat() 61 | } 62 | 63 | # Add the item to the DynamoDB table 64 | table_ia.put_item(Item=item) 65 | 66 | # Unused Access Finding 67 | def parse_unusedaccess_finding(event,table_ua): 68 | # Parse the event detail 69 | detail = (event['detail']) 70 | #print(f"Event detail:{detail}") 71 | 72 | delimiter = ", " # Define a delimiter 73 | 74 | # Extract relevant information from the event 75 | finding_id = detail['id'] 76 | finding_type = detail['findingType'] 77 | 78 | print(f"Parsing Unused Access Analyzer finding {finding_id} and extracting relevant attributes...") 79 | 80 | num_unused_services = detail['numberOfUnusedServices'] 81 | num_unused_actions = detail['numberOfUnusedActions'] 82 | 83 | principal = detail['resource'] 84 | principal_name = extract_role_name(principal) 85 | principal_type = detail.get('resourceType', 'N/A') 86 | principal_owner_account = detail.get('accountId', 'N/A') 87 | 88 | resource_arn = detail.get('resource', 'N/A') 89 | resource_account = detail.get('accountId', 'N/A') 90 | resource_type = detail.get('resourceType', 'N/A') 91 | 92 | status = detail['status'] 93 | 94 | created_at = detail['createdAt'] 95 | updated_at = detail['updatedAt'] 96 | analyzed_at = detail['analyzedAt'] 97 | 98 | # Prepare the item to be inserted into DynamoDB 99 | item = { 100 | 'FindingId': finding_id, 101 | 'AccessType': 'UNUSED', 102 | 'FindingType': finding_type, 103 | 'Principal': principal, 104 | 'PrincipalName': principal_name, 105 | 'PrincipalType': principal_type, 106 | 'PrincipalOwnerAccount': principal_owner_account, 107 | 'ResourceARN': resource_arn, 108 | 'ResourceType': resource_type, 109 | 'ResourceAccount': resource_account, 110 | 'Status': status, 111 | 'NumberOfUnusedServices': num_unused_services, 112 | 'NumberOfUnusedActions': num_unused_actions, 113 | 'CreatedAt': created_at, 114 | 'UpdatedAt': updated_at, 115 | 'AnalyzedAt': analyzed_at, 116 | 'ProcessedAt': datetime.now().isoformat() 117 | } 118 | 119 | # Add the item to the DynamoDB table 120 | table_ua.put_item(Item=item) 121 | 122 | def delete_item_by_finding_id(finding_id, table_name): 123 | print(f"Item with FindingId {finding_id} to be deleted...") 124 | try: 125 | response = table_name.delete_item( 126 | Key={ 127 | 'FindingId': str(finding_id) 128 | }, 129 | ConditionExpression='attribute_exists(FindingId)' 130 | ) 131 | 132 | print(f"Item with FindingId {finding_id} was successfully deleted.") 133 | return True 134 | 135 | except ClientError as e: 136 | if e.response['Error']['Code'] == 'ConditionalCheckFailedException': 137 | print(f"Item with FindingId {finding_id} does not exist.") 138 | else: 139 | print(f"An error occurred: {e.response['Error']['Message']}") 140 | return False 141 | 142 | 143 | def extract_role_name(arn): 144 | # Split by '/' and get the last element 145 | role_name = arn.split('/')[-1] 146 | return role_name 147 | 148 | def lambda_handler(event, context): 149 | dynamodb = boto3.resource('dynamodb') 150 | table_ia = dynamodb.Table('AriaIdCInternalAAFindings') 151 | table_ua = dynamodb.Table('AriaIdCUnusedAAFindings') 152 | table_ea = dynamodb.Table('AriaIdCExternalAAFindings') 153 | 154 | finding_id = event['detail']['id'] 155 | finding_type = event['detail']['findingType'] 156 | 157 | try: 158 | match finding_type: 159 | case 'InternalAccess': 160 | print("Finding type is Internal Access...") 161 | if (event['detail']['status'] == 'RESOLVED'): 162 | print("Deleting Internal Access Analyzer Finding...") 163 | delete_item_by_finding_id(finding_id, table_ia) 164 | else: 165 | print("Parsing Internal Access Analyzer Finding...") 166 | parse_internalaccess_finding(event, table_ia) 167 | case 'UnusedPermission': 168 | print("Finding type is Unused Access - Unused Permission...") 169 | if (event['detail']['status'] == 'RESOLVED'): 170 | print("Deleting Unused Access Analyzer Finding...") 171 | delete_item_by_finding_id(finding_id, table_ua) 172 | else: 173 | print("Parsing Unused Access Analyzer Finding...") 174 | parse_unusedaccess_finding(event, table_ua) 175 | case 'UnusedIAMRole': 176 | print("Finding type is Unused Access - Unused IAM Role...") 177 | if (event['detail']['status'] == 'RESOLVED'): 178 | print("Deleting Unused Access Analyzer Finding...") 179 | delete_item_by_finding_id(finding_id, table_ua) 180 | else: 181 | print("Parsing Unused Access Analyzer Finding...") 182 | parse_unusedaccess_finding(event, table_ua) 183 | 184 | print(f"Successfully processed finding {finding_id}") 185 | return { 186 | 'statusCode': 200, 187 | 'body': json.dumps('Finding processed OK') 188 | } 189 | except Exception as e: 190 | detail = (event['detail']) 191 | #print(f"Error processing event detail:{detail}") 192 | print(f"Error processing finding: {finding_id}") 193 | return { 194 | 'statusCode': 500, 195 | 'body': json.dumps('Error processing finding') 196 | } 197 | -------------------------------------------------------------------------------- /templates/neptune-notebook.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | Description: A template to deploy Neptune Notebooks using CloudFormation resources 3 | Parameters: 4 | NotebookInstanceType: 5 | Type: String 6 | NotebookName: 7 | Type: String 8 | GraphPort: 9 | Description: Port to access your Analytics Graph. 10 | Type: String 11 | GraphVPC: 12 | Type: AWS::EC2::VPC::Id 13 | GraphSubnet: 14 | Type: AWS::EC2::Subnet::Id 15 | GraphSecurityGroup: 16 | Type: AWS::EC2::SecurityGroup::Id 17 | NeptuneGraphEndpoint: 18 | Type: String 19 | Description: Neptune Graph Endpoint from Neptune Analytics stack 20 | NeptuneGraphId: 21 | Type: String 22 | Description: Neptune Graph ID from Neptune Analytics stack 23 | NeptuneGraphName: 24 | Type: String 25 | Description: Neptune Graph Name from Neptune Analytics stack 26 | 27 | Conditions: 28 | AddAnalyticsGraphVpc: !Not 29 | - !Equals 30 | - !Ref GraphVPC 31 | - '' 32 | AddAnalyticsGraphSubnet: !Not 33 | - !Equals 34 | - !Ref GraphSubnet 35 | - '' 36 | AddAnalyticsGraphSecurityGroup: !Not 37 | - !Equals 38 | - !Ref GraphSecurityGroup 39 | - '' 40 | AddNetworkOptions: !And 41 | - !Condition AddAnalyticsGraphVpc 42 | - !Condition AddAnalyticsGraphSubnet 43 | CreateSagemakerSecurityGroup: !And 44 | - !Condition AddNetworkOptions 45 | - !Not 46 | - !Condition AddAnalyticsGraphSecurityGroup 47 | IsIadRegion: !Equals 48 | - !Ref AWS::Region 49 | - us-east-1 50 | 51 | Resources: 52 | SageMakerSecurityGroup: 53 | Type: AWS::EC2::SecurityGroup 54 | Metadata: 55 | cfn_nag: 56 | rules_to_suppress: 57 | - id: W5 58 | reason: Required for Neptune to function 59 | Condition: CreateSagemakerSecurityGroup 60 | Properties: 61 | GroupDescription: Allow Access 62 | VpcId: !Ref GraphVPC 63 | SecurityGroupEgress: 64 | - Description: Allow HTTPS traffic outbound 65 | IpProtocol: tcp 66 | FromPort: 443 67 | ToPort: 443 68 | CidrIp: 0.0.0.0/0 69 | Tags: 70 | - Key: Name 71 | Value: !Sub Neptune-Analytics 72 | - Key: StackId 73 | Value: !Sub ${AWS::StackId} 74 | - Key: Stack 75 | Value: !Sub ${AWS::Region}-${AWS::StackName} 76 | - Key: Application 77 | Value: NeptuneCloudformation 78 | - Key: aria 79 | Value: securitygroup 80 | 81 | NeptuneAnalyticsNotebookInstance: 82 | Type: AWS::SageMaker::NotebookInstance 83 | #checkov:skip=CKV_AWS_187:This is an example notebook instance kmskeyid and associated KMS key would be created to run in production 84 | Metadata: 85 | cfn_nag: 86 | rules_to_suppress: 87 | - id: W1201 88 | reason: This is an example notebook instance kmskeyid and associated KMS key would be created to run in production 89 | Properties: 90 | InstanceType: !Ref NotebookInstanceType 91 | PlatformIdentifier: notebook-al2-v3 92 | NotebookInstanceName: !Join 93 | - '' 94 | - - aws-neptune-analytics- 95 | - !Ref NotebookName 96 | SubnetId: !If 97 | - AddNetworkOptions 98 | - !Ref GraphSubnet 99 | - !Ref AWS::NoValue 100 | SecurityGroupIds: !If 101 | - AddNetworkOptions 102 | - !If 103 | - AddAnalyticsGraphSecurityGroup 104 | - - !Ref GraphSecurityGroup 105 | - - !GetAtt SageMakerSecurityGroup.GroupId 106 | - !Ref AWS::NoValue 107 | RoleArn: !GetAtt ExecutionRole.Arn 108 | LifecycleConfigName: !GetAtt NeptuneAnalyticsNotebookInstanceLifecycleConfig.NotebookInstanceLifecycleConfigName 109 | Tags: 110 | - Key: StackId 111 | Value: !Sub ${AWS::StackId} 112 | - Key: Stack 113 | Value: !Sub ${AWS::Region}-${AWS::StackName} 114 | - Key: Application 115 | Value: NeptuneCloudformation 116 | - Key: aws-neptune-analytics-graph-endpoint 117 | Value: !Ref NeptuneGraphEndpoint 118 | - Key: aws-neptune-graph-name 119 | Value: !Ref NeptuneGraphName 120 | - Key: aws-neptune-graph-id 121 | Value: !Ref NeptuneGraphId 122 | - Key: aria 123 | Value: notebookinstance 124 | 125 | NeptuneAnalyticsNotebookInstanceLifecycleConfig: 126 | Type: AWS::SageMaker::NotebookInstanceLifecycleConfig 127 | Properties: 128 | OnStart: 129 | - Content: 130 | Fn::Base64: !Sub 131 | - | 132 | #!/bin/bash 133 | 134 | sudo -u ec2-user -i <<'EOF' 135 | 136 | echo "export GRAPH_NOTEBOOK_AUTH_MODE=IAM" >> ~/.bashrc 137 | echo "export GRAPH_NOTEBOOK_SSL=True" >> ~/.bashrc 138 | echo "export GRAPH_NOTEBOOK_SERVICE=neptune-graph" >> ~/.bashrc 139 | echo "export GRAPH_NOTEBOOK_HOST=${NeptuneGraphEndpoint}" >> ~/.bashrc 140 | echo "export GRAPH_NOTEBOOK_PORT=${GraphPort}" >> ~/.bashrc 141 | echo "export NEPTUNE_LOAD_FROM_S3_ROLE_ARN=" >> ~/.bashrc 142 | echo "export AWS_REGION=${AWS::Region}" >> ~/.bashrc 143 | 144 | aws s3 cp s3://aws-neptune-notebook-${AWS::Region}/graph_notebook.tar.gz /tmp/graph_notebook.tar.gz 145 | 146 | rm -rf /tmp/graph_notebook 147 | tar -zxvf /tmp/graph_notebook.tar.gz -C /tmp 148 | chmod +x /tmp/graph_notebook/install.sh 149 | /tmp/graph_notebook/install.sh 150 | 151 | EOF 152 | 153 | - NeptuneGraphEndpoint: !Ref NeptuneGraphEndpoint 154 | GraphPort: !Ref GraphPort 155 | NeptuneNoteBookCopy: !If [ 156 | "IsIadRegion", 157 | "aws-neptune-notebook", 158 | !Sub "aws-neptune-notebook-${AWS::Region}" 159 | ] 160 | 161 | ExecutionRole: 162 | Type: AWS::IAM::Role 163 | Metadata: 164 | cfn_nag: 165 | rules_to_suppress: 166 | - id: F3 167 | reason: This is a role assumed by SageMaker and is not used by an end user 168 | Properties: 169 | AssumeRolePolicyDocument: 170 | Version: '2012-10-17' 171 | Statement: 172 | - Effect: Allow 173 | Principal: 174 | Service: 175 | - sagemaker.amazonaws.com 176 | Action: 177 | - sts:AssumeRole 178 | Path: / 179 | Policies: 180 | - PolicyName: SagemakerNotebookNeptuneAnalyticsPolicy 181 | PolicyDocument: 182 | Version: '2012-10-17' 183 | Statement: 184 | - Effect: Allow 185 | Action: 186 | - s3:GetObject 187 | - s3:ListBucket 188 | Resource: !Sub arn:${AWS::Partition}:s3:::* 189 | - Effect: Allow 190 | Action: 191 | - logs:CreateLogGroup 192 | - logs:CreateLogStream 193 | - logs:PutLogEvents 194 | Resource: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/sagemaker/* 195 | - Effect: Allow 196 | Action: neptune-graph:* 197 | Resource: !Sub arn:aws:neptune-graph:${AWS::Region}:${AWS::AccountId}:*/* 198 | - Effect: Allow 199 | Action: sagemaker:DescribeNotebookInstance 200 | Resource: !Sub arn:aws:sagemaker:${AWS::Region}:${AWS::AccountId}:notebook-instance/* 201 | Tags: 202 | - Key: aria 203 | Value: role 204 | 205 | Outputs: 206 | NeptuneAnalyticsNotebookInstanceId: 207 | Value: !Ref NeptuneAnalyticsNotebookInstance 208 | NeptuneAnalyticsSagemakerNotebook: 209 | Value: !Join 210 | - '' 211 | - - https:// 212 | - !Select 213 | - 1 214 | - !Split 215 | - / 216 | - !Ref NeptuneAnalyticsNotebookInstance 217 | - .notebook. 218 | - !Ref AWS::Region 219 | - .sagemaker.aws/ 220 | NeptuneAnalyticsNotebookInstanceLifecycleConfigId: 221 | Value: !Ref NeptuneAnalyticsNotebookInstanceLifecycleConfig -------------------------------------------------------------------------------- /templates/ssm-stack.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: 'SSM Parameters for Lambda function ARNs - Workforce Identity Visualization' 3 | 4 | Parameters: 5 | CreateTablesS3Key: 6 | Type: String 7 | Description: S3 key for CreateTables Lambda function 8 | ListUsersS3Key: 9 | Type: String 10 | Description: S3 key for ListUsers Lambda function 11 | ListGroupsS3Key: 12 | Type: String 13 | Description: S3 key for ListGroups Lambda function 14 | ListGroupMembershipS3Key: 15 | Type: String 16 | Description: S3 key for ListGroupMembership Lambda function 17 | ListPermissionSetsS3Key: 18 | Type: String 19 | Description: S3 key for ListPermissionSets Lambda function 20 | ListProvisionedPermissionSetsS3Key: 21 | Type: String 22 | Description: S3 key for ListProvisionedPermissionSets Lambda function 23 | ListAccountsS3Key: 24 | Type: String 25 | Description: S3 key for ListAccounts Lambda function 26 | ListUserAccountAssignmentsS3Key: 27 | Type: String 28 | Description: S3 key for ListUserAccountAssignments Lambda function 29 | ListGroupAccountAssignmentsS3Key: 30 | Type: String 31 | Description: S3 key for ListGroupAccountAssignments Lambda function 32 | GetIAMRolesS3Key: 33 | Type: String 34 | Description: S3 key for GetIAMRoles Lambda function 35 | S3ExportS3Key: 36 | Type: String 37 | Description: S3 key for S3Export Lambda function 38 | AccessAnalyzerFindingIngestionS3Key: 39 | Type: String 40 | Description: S3 key for AccessAnalyzerFindingIngestion Lambda function 41 | UpdateFunctionCodeS3Key: 42 | Type: String 43 | Description: S3 key for UpdateFunctionCode Lambda function 44 | 45 | # Lambda ARN Parameters 46 | CreateTablesLambdaArn: 47 | Type: String 48 | Description: ARN of CreateTables Lambda function 49 | ListUsersLambdaArn: 50 | Type: String 51 | Description: ARN of ListUsers Lambda function 52 | ListGroupsLambdaArn: 53 | Type: String 54 | Description: ARN of ListGroups Lambda function 55 | ListGroupMembershipLambdaArn: 56 | Type: String 57 | Description: ARN of ListGroupMembership Lambda function 58 | ListPermissionSetsLambdaArn: 59 | Type: String 60 | Description: ARN of ListPermissionSets Lambda function 61 | ListProvisionedPermissionSetsLambdaArn: 62 | Type: String 63 | Description: ARN of ListProvisionedPermissionSets Lambda function 64 | ListAccountsLambdaArn: 65 | Type: String 66 | Description: ARN of ListAccounts Lambda function 67 | ListUserAccountAssignmentsLambdaArn: 68 | Type: String 69 | Description: ARN of ListUserAccountAssignments Lambda function 70 | ListGroupAccountAssignmentsLambdaArn: 71 | Type: String 72 | Description: ARN of ListGroupAccountAssignments Lambda function 73 | GetIAMRolesLambdaArn: 74 | Type: String 75 | Description: ARN of GetIAMRoles Lambda function 76 | S3ExportLambdaArn: 77 | Type: String 78 | Description: ARN of S3Export Lambda function 79 | AccessAnalyzerFindingIngestionLambdaArn: 80 | Type: String 81 | Description: ARN of AccessAnalyzerFindingIngestion Lambda function 82 | UpdateFunctionCodeLambdaArn: 83 | Type: String 84 | Description: ARN of UpdateFunctionCode Lambda function 85 | 86 | Resources: 87 | # SSM Parameters for Lambda ARNs 88 | CreateTablesLambdaArnSSMParameter: 89 | Type: AWS::SSM::Parameter 90 | Properties: 91 | Name: !Join ['', ['/aria/lambda/', !Select [0, !Split ['.', !Ref CreateTablesS3Key]]]] 92 | Type: String 93 | Value: !Ref CreateTablesLambdaArn 94 | Description: !Sub 'ARN for Lambda function ${CreateTablesS3Key}' 95 | Tags: 96 | aria: ssm 97 | 98 | ListUsersLambdaArnSSMParameter: 99 | Type: AWS::SSM::Parameter 100 | Properties: 101 | Name: !Join ['', ['/aria/lambda/', !Select [0, !Split ['.', !Ref ListUsersS3Key]]]] 102 | Type: String 103 | Value: !Ref ListUsersLambdaArn 104 | Description: !Sub 'ARN for Lambda function ${ListUsersS3Key}' 105 | Tags: 106 | aria: ssm 107 | 108 | ListGroupsLambdaArnSSMParameter: 109 | Type: AWS::SSM::Parameter 110 | Properties: 111 | Name: !Join ['', ['/aria/lambda/', !Select [0, !Split ['.', !Ref ListGroupsS3Key]]]] 112 | Type: String 113 | Value: !Ref ListGroupsLambdaArn 114 | Description: !Sub 'ARN for Lambda function ${ListGroupsS3Key}' 115 | Tags: 116 | aria: ssm 117 | 118 | ListGroupMembershipLambdaArnSSMParameter: 119 | Type: AWS::SSM::Parameter 120 | Properties: 121 | Name: !Join ['', ['/aria/lambda/', !Select [0, !Split ['.', !Ref ListGroupMembershipS3Key]]]] 122 | Type: String 123 | Value: !Ref ListGroupMembershipLambdaArn 124 | Description: !Sub 'ARN for Lambda function ${ListGroupMembershipS3Key}' 125 | Tags: 126 | aria: ssm 127 | 128 | ListPermissionSetsLambdaArnSSMParameter: 129 | Type: AWS::SSM::Parameter 130 | Properties: 131 | Name: !Join ['', ['/aria/lambda/', !Select [0, !Split ['.', !Ref ListPermissionSetsS3Key]]]] 132 | Type: String 133 | Value: !Ref ListPermissionSetsLambdaArn 134 | Description: !Sub 'ARN for Lambda function ${ListPermissionSetsS3Key}' 135 | Tags: 136 | aria: ssm 137 | 138 | ListProvisionedPermissionSetsLambdaArnSSMParameter: 139 | Type: AWS::SSM::Parameter 140 | Properties: 141 | Name: !Join ['', ['/aria/lambda/', !Select [0, !Split ['.', !Ref ListProvisionedPermissionSetsS3Key]]]] 142 | Type: String 143 | Value: !Ref ListProvisionedPermissionSetsLambdaArn 144 | Description: !Sub 'ARN for Lambda function ${ListProvisionedPermissionSetsS3Key}' 145 | Tags: 146 | aria: ssm 147 | 148 | ListAccountsLambdaArnSSMParameter: 149 | Type: AWS::SSM::Parameter 150 | Properties: 151 | Name: !Join ['', ['/aria/lambda/', !Select [0, !Split ['.', !Ref ListAccountsS3Key]]]] 152 | Type: String 153 | Value: !Ref ListAccountsLambdaArn 154 | Description: !Sub 'ARN for Lambda function ${ListAccountsS3Key}' 155 | Tags: 156 | aria: ssm 157 | 158 | ListUserAccountAssignmentsLambdaArnSSMParameter: 159 | Type: AWS::SSM::Parameter 160 | Properties: 161 | Name: !Join ['', ['/aria/lambda/', !Select [0, !Split ['.', !Ref ListUserAccountAssignmentsS3Key]]]] 162 | Type: String 163 | Value: !Ref ListUserAccountAssignmentsLambdaArn 164 | Description: !Sub 'ARN for Lambda function ${ListUserAccountAssignmentsS3Key}' 165 | Tags: 166 | aria: ssm 167 | 168 | ListGroupAccountAssignmentsLambdaArnSSMParameter: 169 | Type: AWS::SSM::Parameter 170 | Properties: 171 | Name: !Join ['', ['/aria/lambda/', !Select [0, !Split ['.', !Ref ListGroupAccountAssignmentsS3Key]]]] 172 | Type: String 173 | Value: !Ref ListGroupAccountAssignmentsLambdaArn 174 | Description: !Sub 'ARN for Lambda function ${ListGroupAccountAssignmentsS3Key}' 175 | Tags: 176 | aria: ssm 177 | 178 | GetIAMRolesLambdaArnSSMParameter: 179 | Type: AWS::SSM::Parameter 180 | Properties: 181 | Name: !Join ['', ['/aria/lambda/', !Select [0, !Split ['.', !Ref GetIAMRolesS3Key]]]] 182 | Type: String 183 | Value: !Ref GetIAMRolesLambdaArn 184 | Description: !Sub 'ARN for Lambda function ${GetIAMRolesS3Key}' 185 | Tags: 186 | aria: ssm 187 | 188 | S3ExportLambdaArnSSMParameter: 189 | Type: AWS::SSM::Parameter 190 | Properties: 191 | Name: !Join ['', ['/aria/lambda/', !Select [0, !Split ['.', !Ref S3ExportS3Key]]]] 192 | Type: String 193 | Value: !Ref S3ExportLambdaArn 194 | Description: !Sub 'ARN for Lambda function ${S3ExportS3Key}' 195 | Tags: 196 | aria: ssm 197 | 198 | AccessAnalyzerFindingIngestionLambdaArnSSMParameter: 199 | Type: AWS::SSM::Parameter 200 | Properties: 201 | Name: !Join ['', ['/aria/lambda/', !Select [0, !Split ['.', !Ref AccessAnalyzerFindingIngestionS3Key]]]] 202 | Type: String 203 | Value: !Ref AccessAnalyzerFindingIngestionLambdaArn 204 | Description: !Sub 'ARN for Lambda function ${AccessAnalyzerFindingIngestionS3Key}' 205 | Tags: 206 | aria: ssm 207 | 208 | UpdateFunctionCodeLambdaArnSSMParameter: 209 | Type: AWS::SSM::Parameter 210 | Properties: 211 | Name: !Join ['', ['/aria/lambda/', !Select [0, !Split ['.', !Ref UpdateFunctionCodeS3Key]]]] 212 | Type: String 213 | Value: !Ref UpdateFunctionCodeLambdaArn 214 | Description: !Sub 'ARN for Lambda function ${UpdateFunctionCodeS3Key}' 215 | Tags: 216 | aria: ssm 217 | 218 | Outputs: 219 | CreateTablesSSMParameterName: 220 | Description: Name of SSM parameter for CreateTables Lambda ARN 221 | Value: !Ref CreateTablesLambdaArnSSMParameter 222 | ListUsersSSMParameterName: 223 | Description: Name of SSM parameter for ListUsers Lambda ARN 224 | Value: !Ref ListUsersLambdaArnSSMParameter 225 | ListGroupsSSMParameterName: 226 | Description: Name of SSM parameter for ListGroups Lambda ARN 227 | Value: !Ref ListGroupsLambdaArnSSMParameter 228 | ListGroupMembershipSSMParameterName: 229 | Description: Name of SSM parameter for ListGroupMembership Lambda ARN 230 | Value: !Ref ListGroupMembershipLambdaArnSSMParameter 231 | ListPermissionSetsSSMParameterName: 232 | Description: Name of SSM parameter for ListPermissionSets Lambda ARN 233 | Value: !Ref ListPermissionSetsLambdaArnSSMParameter 234 | ListProvisionedPermissionSetsSSMParameterName: 235 | Description: Name of SSM parameter for ListProvisionedPermissionSets Lambda ARN 236 | Value: !Ref ListProvisionedPermissionSetsLambdaArnSSMParameter 237 | ListAccountsSSMParameterName: 238 | Description: Name of SSM parameter for ListAccounts Lambda ARN 239 | Value: !Ref ListAccountsLambdaArnSSMParameter 240 | ListUserAccountAssignmentsSSMParameterName: 241 | Description: Name of SSM parameter for ListUserAccountAssignments Lambda ARN 242 | Value: !Ref ListUserAccountAssignmentsLambdaArnSSMParameter 243 | ListGroupAccountAssignmentsSSMParameterName: 244 | Description: Name of SSM parameter for ListGroupAccountAssignments Lambda ARN 245 | Value: !Ref ListGroupAccountAssignmentsLambdaArnSSMParameter 246 | GetIAMRolesSSMParameterName: 247 | Description: Name of SSM parameter for GetIAMRoles Lambda ARN 248 | Value: !Ref GetIAMRolesLambdaArnSSMParameter 249 | S3ExportSSMParameterName: 250 | Description: Name of SSM parameter for S3Export Lambda ARN 251 | Value: !Ref S3ExportLambdaArnSSMParameter 252 | AccessAnalyzerFindingIngestionSSMParameterName: 253 | Description: Name of SSM parameter for AccessAnalyzerFindingIngestion Lambda ARN 254 | Value: !Ref AccessAnalyzerFindingIngestionLambdaArnSSMParameter 255 | UpdateFunctionCodeSSMParameterName: 256 | Description: Name of SSM parameter for UpdateFunctionCode Lambda ARN 257 | Value: !Ref UpdateFunctionCodeLambdaArnSSMParameter -------------------------------------------------------------------------------- /SCHEDULING_GUIDE.md: -------------------------------------------------------------------------------- 1 | # ARIA-gv Scheduling Guide 2 | 3 | ## Overview 4 | 5 | The ARIA-gv solution now supports optional automatic scheduling for both core components: 6 | 7 | 1. **Data Collection Scheduling**: Automatically runs the `AriaStateMachine` to collect fresh identity data from AWS IAM Identity Center 8 | 2. **Graph Export Scheduling**: Automatically runs the `AriaExportGraphStateMachine` to refresh your Neptune Analytics graph with the latest data 9 | 10 | This two-tier scheduling approach allows you to optimize data freshness while managing costs effectively. 11 | 12 | ## Scheduling Parameters 13 | 14 | ### Data Collection Scheduling (AriaStateMachine) 15 | 16 | Configure automatic data collection from AWS IAM Identity Center: 17 | 18 | #### EnableDataCollectionScheduling 19 | - **Type**: String 20 | - **Default**: `false` 21 | - **Values**: `true` | `false` 22 | - **Description**: Enable or disable automatic scheduling of identity data collection 23 | 24 | #### DataCollectionScheduleExpression 25 | - **Type**: String 26 | - **Default**: `rate(6 hours)` 27 | - **Description**: Schedule expression for data collection frequency 28 | - **Examples**: 29 | - `rate(6 hours)` - Collect data every 6 hours 30 | - `rate(1 day)` - Collect data once per day 31 | - `cron(0 */4 * * ? *)` - Collect data every 4 hours 32 | 33 | #### DataCollectionScheduleDescription 34 | - **Type**: String 35 | - **Default**: `Automated ARIA identity data collection every 6 hours` 36 | - **Description**: Human-readable description for the scheduled data collection 37 | 38 | #### DataCollectionScheduleTimezone 39 | - **Type**: String 40 | - **Default**: `UTC` 41 | - **Description**: Timezone for cron-based schedules 42 | 43 | ### Graph Export Scheduling (AriaExportGraphStateMachine) 44 | 45 | Configure automatic Neptune Analytics graph updates. The graph export now uses a **dual-trigger approach**: 46 | 47 | 1. **Event-Driven Trigger**: Automatically executes after data collection completes successfully 48 | 2. **Time-Based Schedule**: Independent execution based on your schedule (optional backup/additional runs) 49 | 50 | ### EnableScheduling 51 | - **Type**: String 52 | - **Default**: `false` 53 | - **Values**: `true` | `false` 54 | - **Description**: Enable or disable automatic scheduling of the graph export/import process 55 | 56 | ### ScheduleExpression 57 | - **Type**: String 58 | - **Default**: `rate(1 day)` 59 | - **Description**: Schedule expression defining when to execute the state machine 60 | - **Examples**: 61 | - `rate(1 day)` - Execute once per day 62 | - `rate(12 hours)` - Execute every 12 hours 63 | - `rate(1 week)` - Execute once per week 64 | - `cron(0 2 * * ? *)` - Execute daily at 2:00 AM UTC 65 | - `cron(0 9 ? * MON-FRI *)` - Execute weekdays at 9:00 AM UTC 66 | 67 | ### ScheduleDescription 68 | - **Type**: String 69 | - **Default**: `Daily execution of ARIA graph export and import` 70 | - **Description**: Human-readable description for the scheduled execution 71 | 72 | ### ScheduleTimezone 73 | - **Type**: String 74 | - **Default**: `UTC` 75 | - **Description**: Timezone for cron-based schedules 76 | - **Examples**: `America/New_York`, `Europe/London`, `Asia/Tokyo`, `UTC` 77 | 78 | ## Schedule Expression Formats 79 | 80 | ### Rate Expressions 81 | Rate expressions execute at regular intervals: 82 | - `rate(value unit)` 83 | - Units: `minute`, `minutes`, `hour`, `hours`, `day`, `days` 84 | - Examples: 85 | - `rate(30 minutes)` - Every 30 minutes 86 | - `rate(2 hours)` - Every 2 hours 87 | - `rate(1 day)` - Every day 88 | 89 | ### Cron Expressions 90 | Cron expressions provide more precise scheduling: 91 | - Format: `cron(minute hour day-of-month month day-of-week year)` 92 | - Examples: 93 | - `cron(0 2 * * ? *)` - Daily at 2:00 AM 94 | - `cron(0 9 ? * MON-FRI *)` - Weekdays at 9:00 AM 95 | - `cron(0 0 1 * ? *)` - First day of every month at midnight 96 | 97 | ## Deployment Examples 98 | 99 | ### Enable Both Data Collection and Graph Export Scheduling 100 | ```bash 101 | aws cloudformation deploy \ 102 | --template-file templates/main-stack.yaml \ 103 | --stack-name aria-gv-setup \ 104 | --parameter-overrides \ 105 | EnableDataCollectionScheduling=true \ 106 | DataCollectionScheduleExpression="rate(6 hours)" \ 107 | EnableScheduling=true \ 108 | ScheduleExpression="rate(1 day)" \ 109 | --capabilities CAPABILITY_IAM 110 | ``` 111 | 112 | ### Enable Only Data Collection Scheduling 113 | ```bash 114 | aws cloudformation deploy \ 115 | --template-file templates/main-stack.yaml \ 116 | --stack-name aria-gv-setup \ 117 | --parameter-overrides \ 118 | EnableDataCollectionScheduling=true \ 119 | DataCollectionScheduleExpression="rate(4 hours)" \ 120 | EnableScheduling=false \ 121 | --capabilities CAPABILITY_IAM 122 | ``` 123 | 124 | ### Enable Daily Graph Export Scheduling 125 | ```bash 126 | aws cloudformation deploy \ 127 | --template-file templates/main-stack.yaml \ 128 | --stack-name aria-gv-setup \ 129 | --parameter-overrides \ 130 | EnableScheduling=true \ 131 | ScheduleExpression="rate(1 day)" \ 132 | ScheduleDescription="Daily ARIA graph refresh" \ 133 | --capabilities CAPABILITY_IAM 134 | ``` 135 | 136 | ### Enable Business Hours Data Collection 137 | ```bash 138 | aws cloudformation deploy \ 139 | --template-file templates/main-stack.yaml \ 140 | --stack-name aria-gv-setup \ 141 | --parameter-overrides \ 142 | EnableDataCollectionScheduling=true \ 143 | DataCollectionScheduleExpression="cron(0 9 ? * MON-FRI *)" \ 144 | DataCollectionScheduleDescription="Business hours ARIA data collection" \ 145 | DataCollectionScheduleTimezone="America/New_York" \ 146 | --capabilities CAPABILITY_IAM 147 | ``` 148 | 149 | ### Disable All Scheduling 150 | ```bash 151 | aws cloudformation deploy \ 152 | --template-file templates/main-stack.yaml \ 153 | --stack-name aria-gv-setup \ 154 | --parameter-overrides \ 155 | EnableDataCollectionScheduling=false \ 156 | EnableScheduling=false \ 157 | --capabilities CAPABILITY_IAM 158 | ``` 159 | 160 | ## Execution Flow 161 | 162 | ### Automatic Execution Chain (When Both Scheduling Options Enabled) 163 | 164 | 1. **Data Collection**: AriaStateMachine runs on schedule (e.g., every 6 hours) 165 | 2. **Automatic Trigger**: Upon successful completion, EventBridge automatically triggers AriaExportGraphStateMachine 166 | 3. **Graph Update**: Neptune Analytics graph is refreshed with the latest data 167 | 4. **Independent Schedule**: AriaExportGraphStateMachine also runs on its own schedule as a backup 168 | 169 | ### Benefits of This Approach 170 | 171 | ✅ **Always Fresh Data**: Graph export always uses the most recently collected data 172 | ✅ **Automatic Chaining**: No manual intervention required 173 | ✅ **Fault Tolerance**: Independent schedule provides backup execution 174 | ✅ **Cost Efficiency**: Graph export only runs when there's new data to process 175 | ✅ **Flexible Timing**: Can still run graph export independently if needed 176 | 177 | ## Architecture Components 178 | 179 | When scheduling is enabled, the following resources are created: 180 | 181 | ### Data Collection Scheduling 182 | - **AriaDataCollectionSchedule**: EventBridge Scheduler for data collection 183 | - **AriaDataCollectionScheduleRole**: IAM role for the data collection scheduler 184 | 185 | ### Graph Export Triggering 186 | - **AriaExportGraphTriggerRule**: EventBridge Rule that triggers after data collection completes 187 | - **AriaExportGraphTriggerRole**: IAM role for the EventBridge trigger 188 | - **AriaExportGraphSchedule**: Optional independent scheduler for graph export 189 | - **AriaExportGraphScheduleRole**: IAM role for the independent scheduler 190 | 191 | ### Monitoring & Error Handling 192 | - **AriaDataCollectionScheduleDLQ**: Dead Letter Queue for failed data collection executions 193 | - **AriaExportGraphTriggerDLQ**: Dead Letter Queue for failed event-triggered executions 194 | - **AriaExportGraphScheduleDLQ**: Dead Letter Queue for failed scheduled executions 195 | - **Multiple Log Groups**: CloudWatch logs for comprehensive monitoring 196 | 197 | ## Monitoring Scheduled Executions 198 | 199 | ### CloudWatch Metrics 200 | Monitor your scheduled executions using CloudWatch: 201 | - Navigate to CloudWatch → Metrics → AWS/Scheduler 202 | - View metrics for successful/failed executions 203 | 204 | ### Step Functions Console 205 | - Navigate to Step Functions console 206 | - View execution history for `AriaExportGraphStateMachine` 207 | - Monitor execution status and duration 208 | 209 | ### Dead Letter Queue 210 | Failed executions are sent to the DLQ: 211 | ```bash 212 | # Check for failed executions 213 | aws sqs get-queue-attributes \ 214 | --queue-url https://sqs.region.amazonaws.com/account/stack-name-AriaExportGraphSchedule-DLQ \ 215 | --attribute-names ApproximateNumberOfMessages 216 | ``` 217 | 218 | ## Cost Considerations 219 | 220 | ### Scheduling Costs 221 | - EventBridge Scheduler: $1.00 per million invocations 222 | - Step Functions: $0.025 per 1,000 state transitions 223 | - Lambda: Based on execution time and memory 224 | 225 | ### Optimization Tips 226 | 1. **Choose appropriate frequency**: Don't schedule more frequently than your data changes 227 | 2. **Monitor execution duration**: Optimize Lambda functions for faster execution 228 | 3. **Use business hours scheduling**: Avoid unnecessary weekend/holiday executions 229 | 230 | ## Troubleshooting 231 | 232 | ### Common Issues 233 | 234 | #### Schedule Not Triggering 235 | 1. Check if `EnableScheduling` is set to `true` 236 | 2. Verify the schedule expression syntax 237 | 3. Check IAM permissions for the scheduler role 238 | 239 | #### State Machine Failures 240 | 1. Check Step Functions execution logs 241 | 2. Verify Lambda function permissions 242 | 3. Check DynamoDB table accessibility 243 | 244 | #### Timezone Issues 245 | 1. Ensure timezone is valid (e.g., `America/New_York`) 246 | 2. Remember that rate expressions ignore timezone 247 | 3. Use cron expressions for timezone-specific scheduling 248 | 249 | ### Useful Commands 250 | 251 | ```bash 252 | # List all schedules 253 | aws scheduler list-schedules 254 | 255 | # Get schedule details 256 | aws scheduler get-schedule --name stack-name-AriaExportGraphSchedule 257 | 258 | # Manually trigger the state machine 259 | aws stepfunctions start-execution \ 260 | --state-machine-arn arn:aws:states:region:account:stateMachine:AriaExportGraphStateMachine 261 | 262 | # Check recent executions 263 | aws stepfunctions list-executions \ 264 | --state-machine-arn arn:aws:states:region:account:stateMachine:AriaExportGraphStateMachine \ 265 | --max-items 10 266 | ``` 267 | 268 | ## Best Practices 269 | 270 | 1. **Start with manual execution**: Test the state machine manually before enabling scheduling 271 | 2. **Use appropriate frequency**: Match schedule frequency to your data change rate 272 | 3. **Monitor costs**: Track execution costs and optimize as needed 273 | 4. **Set up alerts**: Create CloudWatch alarms for failed executions 274 | 5. **Document your schedule**: Use descriptive schedule descriptions 275 | 6. **Test timezone settings**: Verify cron expressions work as expected in your timezone 276 | 277 | ## Security Considerations 278 | 279 | - The scheduler role has minimal permissions (only `states:StartExecution`) 280 | - Failed executions are logged but don't expose sensitive data 281 | - All resources follow least-privilege access principles 282 | - Dead letter queue messages are retained for 14 days maximum -------------------------------------------------------------------------------- /templates/core-infrastructure.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "Core infrastructure components - DynamoDB tables and shared resources" 3 | 4 | Parameters: 5 | StackName: 6 | Type: String 7 | Description: Name of the parent stack 8 | 9 | Resources: 10 | # DynamoDB Tables 11 | AriaIdCUsersTable: 12 | Type: AWS::DynamoDB::Table 13 | DeletionPolicy: Delete 14 | UpdateReplacePolicy: Delete 15 | Metadata: 16 | cfn_nag: 17 | rules_to_suppress: 18 | - id: W74 19 | reason: "Default AWS managed encryption is sufficient for this identity data table" 20 | - id: W76 21 | reason: "TTL not required for user identity data" 22 | - id: W28 23 | reason: "Explicit table name required for consistent cross-stack references" 24 | - id: W78 25 | reason: "Point-in-time recovery not required for this identity data table" 26 | checkov: 27 | skip: 28 | - id: CKV_AWS_119 29 | comment: "Default AWS managed encryption is sufficient for this identity data table" 30 | - id: CKV_AWS_28 31 | comment: "Point-in-time recovery not required for this identity data table" 32 | Properties: 33 | TableName: AriaIdCUsers 34 | BillingMode: PAY_PER_REQUEST 35 | AttributeDefinitions: 36 | - AttributeName: UserId 37 | AttributeType: S 38 | KeySchema: 39 | - AttributeName: UserId 40 | KeyType: HASH 41 | Tags: 42 | - Key: aria 43 | Value: dynamodb-table 44 | 45 | AriaIdCGroupsTable: 46 | Type: AWS::DynamoDB::Table 47 | DeletionPolicy: Delete 48 | UpdateReplacePolicy: Delete 49 | Metadata: 50 | cfn_nag: 51 | rules_to_suppress: 52 | - id: W74 53 | reason: "Default AWS managed encryption is sufficient for this identity data table" 54 | - id: W76 55 | reason: "TTL not required for group identity data" 56 | - id: W28 57 | reason: "Explicit table name required for consistent cross-stack references" 58 | - id: W78 59 | reason: "Point-in-time recovery not required for this identity data table" 60 | checkov: 61 | skip: 62 | - id: CKV_AWS_119 63 | comment: "Default AWS managed encryption is sufficient for this identity data table" 64 | - id: CKV_AWS_28 65 | comment: "Point-in-time recovery not required for this identity data table" 66 | Properties: 67 | TableName: AriaIdCGroups 68 | BillingMode: PAY_PER_REQUEST 69 | AttributeDefinitions: 70 | - AttributeName: GroupId 71 | AttributeType: S 72 | KeySchema: 73 | - AttributeName: GroupId 74 | KeyType: HASH 75 | Tags: 76 | - Key: aria 77 | Value: dynamodb-table 78 | 79 | AriaIdCGroupMembershipTable: 80 | Type: AWS::DynamoDB::Table 81 | DeletionPolicy: Delete 82 | UpdateReplacePolicy: Delete 83 | Metadata: 84 | cfn_nag: 85 | rules_to_suppress: 86 | - id: W74 87 | reason: "Default AWS managed encryption is sufficient for this identity data table" 88 | - id: W76 89 | reason: "TTL not required for group membership data" 90 | - id: W28 91 | reason: "Explicit table name required for consistent cross-stack references" 92 | - id: W78 93 | reason: "Point-in-time recovery not required for this identity data table" 94 | checkov: 95 | skip: 96 | - id: CKV_AWS_119 97 | comment: "Default AWS managed encryption is sufficient for this identity data table" 98 | - id: CKV_AWS_28 99 | comment: "Point-in-time recovery not required for this identity data table" 100 | Properties: 101 | TableName: AriaIdCGroupMembership 102 | BillingMode: PAY_PER_REQUEST 103 | AttributeDefinitions: 104 | - AttributeName: MembershipId 105 | AttributeType: S 106 | KeySchema: 107 | - AttributeName: MembershipId 108 | KeyType: HASH 109 | Tags: 110 | - Key: aria 111 | Value: dynamodb-table 112 | 113 | AriaIdCPermissionSetsTable: 114 | Type: AWS::DynamoDB::Table 115 | DeletionPolicy: Delete 116 | UpdateReplacePolicy: Delete 117 | Metadata: 118 | cfn_nag: 119 | rules_to_suppress: 120 | - id: W74 121 | reason: "Default AWS managed encryption is sufficient for this identity data table" 122 | - id: W76 123 | reason: "TTL not required for permission set data" 124 | - id: W28 125 | reason: "Explicit table name required for consistent cross-stack references" 126 | - id: W78 127 | reason: "Point-in-time recovery not required for this identity data table" 128 | checkov: 129 | skip: 130 | - id: CKV_AWS_119 131 | comment: "Default AWS managed encryption is sufficient for this identity data table" 132 | - id: CKV_AWS_28 133 | comment: "Point-in-time recovery not required for this identity data table" 134 | Properties: 135 | TableName: AriaIdCPermissionSets 136 | BillingMode: PAY_PER_REQUEST 137 | AttributeDefinitions: 138 | - AttributeName: PermissionSetArn 139 | AttributeType: S 140 | KeySchema: 141 | - AttributeName: PermissionSetArn 142 | KeyType: HASH 143 | Tags: 144 | - Key: aria 145 | Value: dynamodb-table 146 | 147 | AriaIdCProvisionedPermissionSetsTable: 148 | Type: AWS::DynamoDB::Table 149 | DeletionPolicy: Delete 150 | UpdateReplacePolicy: Delete 151 | Metadata: 152 | cfn_nag: 153 | rules_to_suppress: 154 | - id: W74 155 | reason: "Default AWS managed encryption is sufficient for this identity data table" 156 | - id: W76 157 | reason: "TTL not required for provisioned permission set data" 158 | - id: W28 159 | reason: "Explicit table name required for consistent cross-stack references" 160 | - id: W78 161 | reason: "Point-in-time recovery not required for this identity data table" 162 | checkov: 163 | skip: 164 | - id: CKV_AWS_119 165 | comment: "Default AWS managed encryption is sufficient for this identity data table" 166 | - id: CKV_AWS_28 167 | comment: "Point-in-time recovery not required for this identity data table" 168 | Properties: 169 | TableName: AriaIdCProvisionedPermissionSets 170 | BillingMode: PAY_PER_REQUEST 171 | AttributeDefinitions: 172 | - AttributeName: ProvisionedPermissionSetId 173 | AttributeType: S 174 | KeySchema: 175 | - AttributeName: ProvisionedPermissionSetId 176 | KeyType: HASH 177 | Tags: 178 | - Key: aria 179 | Value: dynamodb-table 180 | 181 | AriaIdCAccountsTable: 182 | Type: AWS::DynamoDB::Table 183 | DeletionPolicy: Delete 184 | UpdateReplacePolicy: Delete 185 | Metadata: 186 | cfn_nag: 187 | rules_to_suppress: 188 | - id: W74 189 | reason: "Default AWS managed encryption is sufficient for this identity data table" 190 | - id: W76 191 | reason: "TTL not required for account data" 192 | - id: W28 193 | reason: "Explicit table name required for consistent cross-stack references" 194 | - id: W78 195 | reason: "Point-in-time recovery not required for this identity data table" 196 | checkov: 197 | skip: 198 | - id: CKV_AWS_119 199 | comment: "Default AWS managed encryption is sufficient for this identity data table" 200 | - id: CKV_AWS_28 201 | comment: "Point-in-time recovery not required for this identity data table" 202 | Properties: 203 | TableName: AriaIdCAccounts 204 | BillingMode: PAY_PER_REQUEST 205 | AttributeDefinitions: 206 | - AttributeName: AccountId 207 | AttributeType: S 208 | KeySchema: 209 | - AttributeName: AccountId 210 | KeyType: HASH 211 | Tags: 212 | - Key: aria 213 | Value: dynamodb-table 214 | 215 | AriaIdCUserAccountAssignmentsTable: 216 | Type: AWS::DynamoDB::Table 217 | DeletionPolicy: Delete 218 | UpdateReplacePolicy: Delete 219 | Metadata: 220 | cfn_nag: 221 | rules_to_suppress: 222 | - id: W74 223 | reason: "Default AWS managed encryption is sufficient for this identity data table" 224 | - id: W76 225 | reason: "TTL not required for user account assignment data" 226 | - id: W28 227 | reason: "Explicit table name required for consistent cross-stack references" 228 | - id: W78 229 | reason: "Point-in-time recovery not required for this identity data table" 230 | checkov: 231 | skip: 232 | - id: CKV_AWS_119 233 | comment: "Default AWS managed encryption is sufficient for this identity data table" 234 | - id: CKV_AWS_28 235 | comment: "Point-in-time recovery not required for this identity data table" 236 | Properties: 237 | TableName: AriaIdCUserAccountAssignments 238 | BillingMode: PAY_PER_REQUEST 239 | AttributeDefinitions: 240 | - AttributeName: AssignmentId 241 | AttributeType: S 242 | KeySchema: 243 | - AttributeName: AssignmentId 244 | KeyType: HASH 245 | Tags: 246 | - Key: aria 247 | Value: dynamodb-table 248 | 249 | AriaIdCGroupAccountAssignmentsTable: 250 | Type: AWS::DynamoDB::Table 251 | DeletionPolicy: Delete 252 | UpdateReplacePolicy: Delete 253 | Metadata: 254 | cfn_nag: 255 | rules_to_suppress: 256 | - id: W74 257 | reason: "Default AWS managed encryption is sufficient for this identity data table" 258 | - id: W76 259 | reason: "TTL not required for group account assignment data" 260 | - id: W28 261 | reason: "Explicit table name required for consistent cross-stack references" 262 | - id: W78 263 | reason: "Point-in-time recovery not required for this identity data table" 264 | checkov: 265 | skip: 266 | - id: CKV_AWS_119 267 | comment: "Default AWS managed encryption is sufficient for this identity data table" 268 | - id: CKV_AWS_28 269 | comment: "Point-in-time recovery not required for this identity data table" 270 | Properties: 271 | TableName: AriaIdCGroupAccountAssignments 272 | BillingMode: PAY_PER_REQUEST 273 | AttributeDefinitions: 274 | - AttributeName: AssignmentId 275 | AttributeType: S 276 | KeySchema: 277 | - AttributeName: AssignmentId 278 | KeyType: HASH 279 | Tags: 280 | - Key: aria 281 | Value: dynamodb-table 282 | 283 | AriaIdCIAMRolesTable: 284 | Type: AWS::DynamoDB::Table 285 | DeletionPolicy: Delete 286 | UpdateReplacePolicy: Delete 287 | Metadata: 288 | cfn_nag: 289 | rules_to_suppress: 290 | - id: W74 291 | reason: "Default AWS managed encryption is sufficient for this identity data table" 292 | - id: W76 293 | reason: "TTL not required for IAM role data" 294 | - id: W28 295 | reason: "Explicit table name required for consistent cross-stack references" 296 | - id: W78 297 | reason: "Point-in-time recovery not required for this identity data table" 298 | checkov: 299 | skip: 300 | - id: CKV_AWS_119 301 | comment: "Default AWS managed encryption is sufficient for this identity data table" 302 | - id: CKV_AWS_28 303 | comment: "Point-in-time recovery not required for this identity data table" 304 | Properties: 305 | TableName: AriaIdCIAMRoles 306 | BillingMode: PAY_PER_REQUEST 307 | AttributeDefinitions: 308 | - AttributeName: RoleArn 309 | AttributeType: S 310 | KeySchema: 311 | - AttributeName: RoleArn 312 | KeyType: HASH 313 | Tags: 314 | - Key: aria 315 | Value: dynamodb-table 316 | 317 | AriaIdCAccessAnalyzerFindingsTable: 318 | Type: AWS::DynamoDB::Table 319 | DeletionPolicy: Delete 320 | UpdateReplacePolicy: Delete 321 | Metadata: 322 | cfn_nag: 323 | rules_to_suppress: 324 | - id: W74 325 | reason: "Default AWS managed encryption is sufficient for this identity data table" 326 | - id: W76 327 | reason: "TTL not required for access analyzer findings data" 328 | - id: W28 329 | reason: "Explicit table name required for consistent cross-stack references" 330 | - id: W78 331 | reason: "Point-in-time recovery not required for this identity data table" 332 | checkov: 333 | skip: 334 | - id: CKV_AWS_119 335 | comment: "Default AWS managed encryption is sufficient for this identity data table" 336 | - id: CKV_AWS_28 337 | comment: "Point-in-time recovery not required for this identity data table" 338 | Properties: 339 | TableName: AriaIdCAccessAnalyzerFindings 340 | BillingMode: PAY_PER_REQUEST 341 | AttributeDefinitions: 342 | - AttributeName: FindingId 343 | AttributeType: S 344 | KeySchema: 345 | - AttributeName: FindingId 346 | KeyType: HASH 347 | Tags: 348 | - Key: aria 349 | Value: dynamodb-table 350 | 351 | Outputs: 352 | UsersTableName: 353 | Description: Users DynamoDB table name 354 | Value: !Ref AriaIdCUsersTable 355 | Export: 356 | Name: !Sub "${StackName}-UsersTable" 357 | 358 | GroupsTableName: 359 | Description: Groups DynamoDB table name 360 | Value: !Ref AriaIdCGroupsTable 361 | Export: 362 | Name: !Sub "${StackName}-GroupsTable" 363 | 364 | # Add other table exports as needed 365 | -------------------------------------------------------------------------------- /source/s3export/lambda_function.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import csv 3 | import io 4 | import json 5 | import uuid 6 | from botocore.exceptions import ClientError 7 | 8 | # This function uses the standard python csv library, semgrep may flag this as a potential for a malicious csv to be 9 | # created, however all csv generation is programmatic with no user input so the risk is low 10 | def convert_to_csv(items, table_headers, csv_headers, generate_uuid=False, label=None): 11 | csv_buffer = io.StringIO() 12 | writer = csv.writer(csv_buffer) 13 | writer.writerow(csv_headers) 14 | 15 | writer = csv.DictWriter(csv_buffer, fieldnames=table_headers, extrasaction='ignore') 16 | 17 | for item in items: 18 | # print(f"{item}") 19 | row={} 20 | for header in table_headers: 21 | if header in item: 22 | row[header] = item[header] 23 | else: 24 | # Handle special columns 25 | if header == 'UniqueId' and generate_uuid: 26 | row[header] = str(uuid.uuid4()) 27 | elif header == 'Label' and label: 28 | row[header] = label 29 | else: 30 | row[header] = '' 31 | 32 | # print(f"{row}") 33 | writer.writerow(row) 34 | 35 | return csv_buffer.getvalue() 36 | 37 | #REMOVES DUPLICATES: 38 | def remove_duplicates_from_items(items, unique_key_fields): 39 | unique_items = {} 40 | for item in items: 41 | # Create a tuple of values from the specified fields 42 | unique_key = tuple(item.get(field, '') for field in unique_key_fields) 43 | 44 | # Keep only the first occurrence of each unique combination 45 | if unique_key not in unique_items: 46 | unique_items[unique_key] = item 47 | 48 | return list(unique_items.values()) 49 | 50 | def export_dynamodb_to_s3(dynamodb_table, s3_bucket, s3_key, table_headers, csv_headers, generate_uuid=False, label=None, dedup_fields=None): 51 | print(f"Exporting {dynamodb_table} to {s3_bucket}/{s3_key}") 52 | dynamodb = boto3.resource('dynamodb') 53 | s3 = boto3.client('s3') 54 | s3.delete_object(Bucket=s3_bucket, Key=s3_key) 55 | table = dynamodb.Table(dynamodb_table) 56 | response = table.scan() 57 | items = response['Items'] 58 | if items: 59 | if dedup_fields: 60 | items = remove_duplicates_from_items(items, dedup_fields) 61 | csv_data = convert_to_csv(items, table_headers, csv_headers, generate_uuid, label) 62 | s3.put_object(Bucket=s3_bucket, Key=s3_key, Body=csv_data) 63 | print(f"Data exported to S3: {s3_bucket}/{s3_key}") 64 | 65 | def check_table_has_items(dynamodb_table): 66 | try: 67 | # Create DynamoDB client 68 | dynamodb = boto3.resource('dynamodb') 69 | table = dynamodb.Table(dynamodb_table) 70 | 71 | # Scan table with limit 1 to check for any items 72 | response = table.scan( 73 | Select='COUNT', 74 | Limit=1 75 | ) 76 | 77 | # Check if count is greater than 0 78 | item_count = response['Count'] 79 | has_items = item_count > 0 80 | 81 | return has_items 82 | 83 | except ClientError as e: 84 | print(f"Error checking table: {e}") 85 | raise 86 | 87 | 88 | def lambda_handler(event, context): 89 | 90 | # The s3bucket parameter is passed in to the function from the calling step function 91 | s3_bucket = event['s3bucket'] 92 | 93 | #NODES 94 | # Export AriaIdCUsers to csv file 95 | table_headers = ["UserId", "UserName", "Label"] 96 | csv_headers = ["~id", "username:String","~label"] 97 | export_dynamodb_to_s3("AriaIdCUsers", s3_bucket, "AriaIdCUsers.csv", table_headers, csv_headers,label="UserName") 98 | 99 | # Export AriaIdCGroups to csv file 100 | table_headers = ["GroupId", "GroupName", "Label"] 101 | csv_headers = ["~id", "groupname:String","~label"] 102 | export_dynamodb_to_s3("AriaIdCGroups", s3_bucket, "AriaIdCGroups.csv", table_headers, csv_headers,label="GroupName") 103 | 104 | # Export AriaIdCPermissionSets to csv file 105 | table_headers = ["PermissionSetArn", "Name", "Description", "Label"] 106 | csv_headers = ["~id", "name:String", "description:String","~label"] 107 | export_dynamodb_to_s3("AriaIdCPermissionSets", s3_bucket, "AriaIdCPermissionSets.csv", table_headers, csv_headers,label="PermissionSet") 108 | 109 | # Export AriaIdCAccounts to csv file 110 | table_headers = ["AccountId", "Name", "Label"] 111 | csv_headers = ["~id", "name:String","~label"] 112 | export_dynamodb_to_s3("AriaIdCAccounts", s3_bucket, "AriaIdCAccounts.csv", table_headers, csv_headers,label="AccountName") 113 | 114 | # Export AriaIdCIAMRoles to csv file 115 | table_headers = ["IamRoleArn", "AccountId", "RoleId", "RoleName", "AttachedPolicies", "Label"] 116 | csv_headers = ["~id", "accountid:String", "roleid:String", "rolename:String", "attachedpolicies:String","~label"] 117 | export_dynamodb_to_s3("AriaIdCIAMRoles", s3_bucket, "AriaIdCIAMRoles.csv", table_headers, csv_headers,label="RoleName") 118 | 119 | # Only export Internal Access Analyzer Findings if the table has items 120 | if check_table_has_items("AriaIdCInternalAAFindings"): 121 | #Export InternalAccessAnalyzerFindings to csv file 122 | table_headers = ["FindingId", "ResourceARN", "FindingType", "AccessType", "Principal", "PrincipalName", "PrincipalOwnerAccount", "ResourceType", "Action", "ResourceControlPolicyRestrictionType", "ServiceControlPolicyRestrictionType", "Status", "NumberofUnusedActions", "NumberofUnusedServices", "Label"] 123 | csv_headers = ["~id", "resourcearn:String", "findingtype:String", "accesstype:String", "principal:String", "principalname:String", "principalowneraccount:String", "resourcetype:String", "action:String", "resourcecontrolpolicyrestrictiontype:String", "servicecontrolpolicyrestrictiontype:String", "status:String", "numberofunusedactions:String", "numberofunusedservices:String", "~label"] 124 | export_dynamodb_to_s3("AriaIdCInternalAAFindings", s3_bucket, "AriaIdCInternalAAFindings.csv", table_headers, csv_headers,label="InternalAccessFinding") 125 | 126 | #Export Critical Resources to csv file 127 | table_headers = ["ResourceARN", "ResourceType", "Label"] 128 | csv_headers = ["~id", "resourcetype:String", "~label"] 129 | export_dynamodb_to_s3("AriaIdCInternalAAFindings", s3_bucket, "AriaIdCCriticalResources.csv", table_headers, csv_headers,label="CriticalResources") 130 | 131 | # Only export Unused Access Analyzer Findings if the table has items 132 | if check_table_has_items("AriaIdCUnusedAAFindings"): 133 | #Export UnusedAccessAnalyzerFindings to csv file 134 | table_headers = ["FindingId", "ResourceARN", "FindingType", "AccessType", "ResourceType", "Status", "NumberOfUnusedActions", "NumberOfUnusedServices", "Label"] 135 | csv_headers = ["~id", "resourcearn:String", "findingtype:String", "accesstype:String", "resourcetype:String", "status:String", "numberofunusedactions:String", "numberofunusedservices:String", "~label"] 136 | export_dynamodb_to_s3("AriaIdCUnusedAAFindings", s3_bucket, "AriaIdCUnusedAAFindings.csv", table_headers, csv_headers,label="UnusedAccessFinding") 137 | 138 | 139 | #EDGES 140 | 141 | # Modified Users to Groups (GroupMembership) - EDGE 142 | table_headers = ["UniqueId", "GroupId", "UserId", "Label"] 143 | csv_headers = ["~id", "~from", "~to", "~label"] 144 | export_dynamodb_to_s3( 145 | "AriaIdCGroupMembership", 146 | s3_bucket, 147 | "AriaIdCGroupMembership_Edge.csv", 148 | table_headers, 149 | csv_headers, 150 | generate_uuid=True, 151 | label="HAS_MEMBERS" 152 | ) 153 | 154 | # Export User to PermissionsSets to csv file - EDGE 155 | table_headers = ["UniqueId", "UserId", "PermissionSetArn", "Label"] 156 | csv_headers = ["~id", "~from", "~to", "~label"] 157 | export_dynamodb_to_s3( 158 | "AriaIdCUserAccountAssignments", 159 | s3_bucket, 160 | "AriaIdCUserAssignments_Edge.csv", 161 | table_headers, 162 | csv_headers, 163 | generate_uuid=True, 164 | dedup_fields=["UserId", "PermissionSetArn"], 165 | label="ASSIGNED_PERMISSIONSET" 166 | ) 167 | 168 | # Export Group to PermissionsSets to csv file - EDGE 169 | table_headers = ["UniqueId", "GroupId", "PermissionSetArn", "Label"] 170 | csv_headers = ["~id", "~from", "~to", "~label"] 171 | export_dynamodb_to_s3( 172 | "AriaIdCGroupAccountAssignments", 173 | s3_bucket, 174 | "AriaIdCGroupAssignments_Edge.csv", 175 | table_headers, 176 | csv_headers, 177 | generate_uuid=True, 178 | label="ASSIGNED_PERMISSIONSET", 179 | dedup_fields=["GroupId", "PermissionSetArn"] 180 | ) 181 | 182 | # Export User to Accounts to csv file - EDGE 183 | table_headers = ["UniqueId", "UserId", "AccountId", "Label"] 184 | csv_headers = ["~id", "~from", "~to", "~label"] 185 | export_dynamodb_to_s3( 186 | "AriaIdCUserAccountAssignments", 187 | s3_bucket, 188 | "AriaIdCUserAccount_Edge.csv", 189 | table_headers, 190 | csv_headers, 191 | generate_uuid=True, 192 | label="ASSIGNED_ACCOUNT" 193 | ) 194 | 195 | # Export Groups to Accounts to csv file - EDGE 196 | table_headers = ["UniqueId", "GroupId", "AccountId", "Label"] 197 | csv_headers = ["~id", "~from", "~to", "~label"] 198 | export_dynamodb_to_s3( 199 | "AriaIdCGroupAccountAssignments", 200 | s3_bucket, 201 | "AriaIdCGroupAccount_Edge.csv", 202 | table_headers, 203 | csv_headers, 204 | generate_uuid=True, 205 | label="ASSIGNED_ACCOUNT" 206 | ) 207 | 208 | # Export Account to PermissionSets to csv file - EDGE 209 | table_headers = ["UniqueId", "PermissionSetArn", "AccountId", "Label"] 210 | csv_headers = ["~id", "~from", "~to", "~label"] 211 | export_dynamodb_to_s3( 212 | "AriaIdCProvisionedPermissionSets", 213 | s3_bucket, 214 | "AriaIdCProvisionedPermissionSets_Edge.csv", 215 | table_headers, 216 | csv_headers, 217 | generate_uuid=True, 218 | label="PROVISIONED_INTO" 219 | ) 220 | 221 | #Export Roles to Accounts to csv file - EDGE 222 | table_headers = ["UniqueId", "IamRoleArn", "AccountId", "Label"] 223 | csv_headers = ["~id", "~from", "~to", "~label"] 224 | export_dynamodb_to_s3( 225 | "AriaIdCIAMRoles", 226 | s3_bucket, 227 | "AriaIdCIAMRoles_Account_Edge.csv", 228 | table_headers, 229 | csv_headers, 230 | generate_uuid=True, 231 | label="CREATED_IN" 232 | ) 233 | 234 | #Export PermissionsSets to Roles to csv file - EDGE 235 | table_headers = ["UniqueId", "PermissionSetArn", "IamRoleArn", "Label"] 236 | csv_headers = ["~id", "~from", "~to", "~label"] 237 | export_dynamodb_to_s3( 238 | "AriaIdCIAMRoles", 239 | s3_bucket, 240 | "AriaIdCRole_PS_Edge.csv", 241 | table_headers, 242 | csv_headers, 243 | generate_uuid=True, 244 | label="CREATED_AS" 245 | ) 246 | 247 | # Only export Internal Access Analyzer Findings if the table has items 248 | if check_table_has_items("AriaIdCInternalAAFindings"): 249 | 250 | #Export Internal Access Analyzer Findings to Roles csv file - EDGE 251 | table_headers = ["UniqueId", "FindingId", "Principal", "Label"] 252 | csv_headers = ["~id", "~from", "~to", "~label"] 253 | export_dynamodb_to_s3( 254 | "AriaIdCInternalAAFindings", 255 | s3_bucket, 256 | "AriaIdCInternalAAFindingsRole_Edge.csv", 257 | table_headers, 258 | csv_headers, 259 | generate_uuid=True, 260 | label="LINKED_TO" 261 | ) 262 | 263 | #Export Internal Access Analyzer Findings to Resource csv file - EDGE 264 | table_headers = ["UniqueId", "FindingId", "ResourceARN", "Label"] 265 | csv_headers = ["~id", "~from", "~to", "~label"] 266 | export_dynamodb_to_s3( 267 | "AriaIdCInternalAAFindings", 268 | s3_bucket, 269 | "AriaIdCInternalAAFindingsResource_Edge.csv", 270 | table_headers, 271 | csv_headers, 272 | generate_uuid=True, 273 | label="LINKED_TO" 274 | ) 275 | 276 | #Export Internal Access Analyzer Findings Principal to Resource csv file - EDGE 277 | table_headers = ["UniqueId", "Principal", "ResourceARN", "Label"] 278 | csv_headers = ["~id", "~from", "~to", "~label"] 279 | export_dynamodb_to_s3( 280 | "AriaIdCInternalAAFindings", 281 | s3_bucket, 282 | "AriaIdCInternalAAF_Principal_Resource_Edge.csv", 283 | table_headers, 284 | csv_headers, 285 | generate_uuid=True, 286 | label="GRANTS_ACCESS_TO", 287 | dedup_fields=["Principal", "ResourceARN"] 288 | ) 289 | 290 | #Export Internal Access Analyzer Findings Resource to Account csv file - EDGE 291 | table_headers = ["UniqueId", "ResourceARN", "ResourceAccount", "Label"] 292 | csv_headers = ["~id", "~from", "~to", "~label"] 293 | export_dynamodb_to_s3( 294 | "AriaIdCInternalAAFindings", 295 | s3_bucket, 296 | "AriaIdCInternalAAFindingsResource_Account_Edge.csv", 297 | table_headers, 298 | csv_headers, 299 | generate_uuid=True, 300 | label="BELONGS_TO", 301 | dedup_fields=["ResourceARN", "ResourceAccount"] 302 | ) 303 | 304 | # Only export Unused Access Analyzer Findings if the table has items 305 | if check_table_has_items("AriaIdCUnusedAAFindings"): 306 | 307 | # Modified Unused Finding to Roles - EDGE 308 | table_headers = ["UniqueId", "ResourceARN", "FindingId", "Label"] 309 | csv_headers = ["~id", "~from", "~to", "~label"] 310 | export_dynamodb_to_s3( 311 | "AriaIdCUnusedAAFindings", 312 | s3_bucket, 313 | "AriaUnusedAAFindings_Edge.csv", 314 | table_headers, 315 | csv_headers, 316 | generate_uuid=True, 317 | label="HAS_UNUSED_ACCESS" 318 | ) 319 | 320 | return { 321 | 'statusCode': 200, 322 | 'body': json.dumps('Data exported to S3') 323 | } 324 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to ARIA-gv (Access Rights for Identity on AWS - graph visualization) 2 | 3 | **NOTE:** This sample code was shown at AWS re:Inforce 2025 in Code Talk session IAM341, presented by Meg Peddada and Alex Waddell. Watch the session recording here: [https://www.youtube.com/watch?v=JsPug0rh7BM](https://www.youtube.com/watch?v=JsPug0rh7BM). 4 | 5 | ## What are the challenges we are trying to solve? 6 | Customers connect their Identity Provider (IdP) to AWS IAM Identity Center to enable easier management of access to AWS applications and accounts. This single connection then allows users & groups from the IdP to be easily synchronised to Identity Center and used to provide access to (for example) AWS Accounts. 7 | 8 | ![AWS IAM Identity Center](/img/idc.png) 9 | 10 | This helps Identity administrators to manage AWS access more simply through IdP group membership. However those same Identity teams are also being asked questions like 11 | 12 | *“Who in our company can access our cloud resources and what can they do to them?”* 13 | 14 | *“Can you show me how Bob was able to update the customer data in our production account?”* 15 | 16 | *“Do users with access to our cloud resources have access rights that follow least privilege?”* 17 | 18 | *“Can you give me a report of everything that Alice has access to in our production account?”* 19 | 20 | Some challenges when attempting to answer those questions are: 21 | 22 | * Basing resource access assumptions on IdP group membership doesn’t tell the whole story 23 | * Resource access may be granted using a combination of 24 | * Identity-based policies 25 | * Resource-based policies 26 | * Service Control Policies (SCPs) 27 | * Resource Control Policies (RCPs) 28 | * Permissions Boundaries 29 | * Session Policies 30 | * Teams might deploy custom IAM roles & policies into accounts 31 | * Providing AWS account & resource access visibility to teams beyond just CloudOps 32 | 33 | ## So what can we do? 34 | 35 | What if we were able to get various data sets and understand the relationships between those? Then perhaps we could visualize in ways that could help Identity administrators have a better understanding of the potential access that principals (users & roles) have to critical resources like Amazon Simple Storage Service (S3) buckets, Amazon DynamoDB tables, and in addition it could help you on your journey to implementing Least Privilege. 36 | 37 | As a side effect if we can create csv exports then the data could be used in other solutions that may have additional company-specific context to provide even greater value and improved fidelity. 38 | 39 | To achieve this will require acquisition and storage of Identity-related data from a number of AWS services - primarily AWS Identity and Access Management, AWS IAM Identity Center and AWS IAM Access Analyzer. Fortunately we can use APIs to get the majority of this data, which we can then process, enrich and finally put it into the right format to create csv exports and to visualize it. 40 | 41 | We will get most of the required data from two different Identity Center APIs: 42 | 1. [Identity Store](https://docs.aws.amazon.com/singlesignon/latest/IdentityStoreAPIReference/welcome.html) 43 | 2. [Identity Center](https://docs.aws.amazon.com/singlesignon/latest/APIReference/welcome.html) 44 | 45 | We also need to ingest Unused Access findings and Internal Access findings from IAM Access Analyzer, which we will do using EventBridge. To achieve this, this solution expects that you have setup [Unused Access Analyzer](https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-create-unused.html) and [Internal Access Analyzer](https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-create-internal.html) to create the necessary findings. 46 | 47 | However we also need to build relationships between different entities, such as the relation between principals and permission sets, and understand what AWS IAM Identity Center permission sets are provisioned as IAM Roles into each account. 48 | 49 | In order to understand these relationships, lets conceptualize the relationships we need to build using this diagram. 50 | 51 | ![Relationships](/img/relationships.png) 52 | 53 | So we finally have an idea of how our graph needs to be built. Using the power of automation, lets construct an architecture that can help us put all this together. 54 | 55 | ![Our Architecture](/img/architecture.png) 56 | 57 | As shown above, this solution will use AWS Step Functions, AWS Lambda and Amazon EventBridge to orchestrate the processing of data capture, enrichment and processing to enable it to be visualized in Amazon Neptune. Amazon DynamoDB will be used to store the data to enable more efficient processing and reduce the need to repeatedly call APIs. 58 | 59 | NOTE: 60 | * This solution provides a snapshot into access rights at a moment in time (based on when the data was acquired from IAM Identity Center, IAM and IAM Access Analyzer). 61 | * This solution does **not** factor in contextual data from third-party IdPs or IAM trust policy statements that may affect access to your critical resources. 62 | 63 | ## Scheduling Automation 64 | 65 | The ARIA-gv solution supports **intelligent automatic scheduling** with event-driven execution chaining: 66 | 67 | 1. **Data Collection**: Automatically runs the `AriaStateMachine` to collect fresh identity data 68 | 2. **Automatic Graph Export**: When data collection completes successfully, the `AriaExportGraphStateMachine` automatically triggers to refresh your Neptune Analytics graph 69 | 3. **Independent Scheduling**: Optional time-based scheduling for additional graph export runs 70 | 71 | This intelligent approach helps your Neptune graph to reflect the most current data while optimizing costs and execution efficiency. 72 | 73 | ## Let's build! 74 | 75 | **IMPORTANT:** If you are deploying this solution and have ALREADY deployed prior to July 9th 2025, you must manually delete the old cloudformation stacks in this order: 76 | * aria-neptune-notebook 77 | * aria-neptune-analytics 78 | * aria-setup 79 | 80 | Once you deploy the updated solution as described below, ensure that you update the `aria-orglevel-iamlistroles` stackset and the cloudformation stack you created in your Management account with the Arn of the `GetIamRoles` Lambda function - see Quick Start step 2 below. 81 | 82 | The ARIA-gv solution supports **intelligent automatic scheduling** with event-driven execution chaining: 83 | 84 | 1. **Data Collection**: Automatically runs the `AriaStateMachine` to collect fresh identity data 85 | 2. **Automatic Graph Export**: When data collection completes successfully, the `AriaExportGraphStateMachine` automatically triggers to refresh your Neptune Analytics graph 86 | 3. **Independent Scheduling**: Optional time-based scheduling for additional graph export runs 87 | 88 | This intelligent approach ensures your Neptune graph has 'fresh' data while optimizing costs and execution efficiency. 89 | 90 | ### Quick Start 91 | 92 | The fastest way to deploy ARIA-gv is using the enhanced deployment script: 93 | 94 | #### 1. Pre-requisites 95 | 96 | Once you have cloned this repository locally: 97 | * Obtain temporary credentials for the AWS Account that you have designated as the delegated Administration account for AWS IAM Identity Center 98 | * Run `aria-bootstrap.sh` to create required S3 buckets and upload Lambda code 99 | 100 | #### 2. Create cross-account IAM roles 101 | 102 | Deploy the cross-account IAM roles in your AWS organization as described [here](source/idciaminventoryrole/stack-set-creation.md). This allows the solution to collect IAM role information from all accounts. 103 | 104 | #### 3. Deploy everything using the Enhanced Deployment Script (Recommended) 105 | 106 | The easiest way to deploy with optional scheduling is using the enhanced deployment script with presets: 107 | 108 | ```bash 109 | # Deploy with daily data collection and graph export 110 | ./deploy-nested-stacks.sh --scheduling-preset daily-collection-and-export 111 | 112 | # Deploy with business hours scheduling 113 | ./deploy-nested-stacks.sh --scheduling-preset business-hours 114 | 115 | # Deploy with frequent data collection (6 hours) and daily graph export 116 | ./deploy-nested-stacks.sh --scheduling-preset frequent-collection-daily-export 117 | 118 | # Or deploy basic setup without scheduling 119 | ./deploy-nested-stacks.sh --deploy-neptune true 120 | ``` 121 | 122 | #### Using CloudFormation Directly 123 | 124 | Alternatively, deploy directly with CloudFormation: 125 | 126 | ```bash 127 | aws cloudformation deploy \ 128 | --template-file templates/main-stack.yaml \ 129 | --stack-name aria-gv-setup \ 130 | --parameter-overrides \ 131 | EnableDataCollectionScheduling=true \ 132 | DataCollectionScheduleExpression="rate(6 hours)" \ 133 | EnableScheduling=true \ 134 | ScheduleExpression="rate(1 day)" \ 135 | --capabilities CAPABILITY_IAM 136 | ``` 137 | 138 | NOTE: To disable scheduling you would set `EnableScheduling=false` in the above. 139 | 140 | #### 4. Visualizing workforce identities 141 | 142 | * Navigate to Amazon Neptune in the AWS Console 143 | * Click on **Notebooks** and find your notebook (e.g., `aws-neptune-analytics-Aria-Neptune-Notebook`) 144 | * Click **Actions > Open Graph Explorer** 145 | * Add all nodes and edges to explore your identity relationships! 146 | 147 | ### Manual Deployment (Alternative) 148 | 149 | If you prefer manual control over the deployment process: 150 | 151 | #### 1. Deploy Core Infrastructure 152 | ```bash 153 | aws cloudformation deploy \ 154 | --template-file templates/main-stack.yaml \ 155 | --stack-name aria-gv-setup \ 156 | --parameter-overrides DeployNeptune=true \ 157 | --capabilities CAPABILITY_IAM 158 | ``` 159 | 160 | #### 2. Create cross-account IAM roles 161 | 162 | Deploy the cross-account IAM roles in your AWS organization as described [here](source/idciaminventoryrole/stack-set-creation.md). This allows the solution to collect IAM role information from all accounts. 163 | 164 | #### 3. Execute State Machines 165 | 166 | Navigate to AWS Step Functions and execute the state machines: 167 | 1. **AriaStateMachine** (collects identity data) 168 | 2. **AriaExportGraphStateMachine** (exports to Neptune) 169 | 170 | Or enable automatic scheduling to run these automatically. 171 | 172 | 173 | ### Scheduling Options 174 | 175 | #### Data Collection (AriaStateMachine) 176 | - **Frequent Updates**: `rate(6 hours)` - Collect fresh identity data every 6 hours 177 | - **Daily Updates**: `rate(1 day)` - Collect data once per day 178 | - **Business Hours**: `cron(0 9 ? * MON-FRI *)` - Collect data at 9 AM on weekdays 179 | 180 | #### Graph Export (AriaExportGraphStateMachine) 181 | - **Daily Export**: `rate(1 day)` - Update Neptune graph daily 182 | - **Weekly Export**: `rate(1 week)` - Update Neptune graph weekly 183 | - **End of Business**: `cron(0 18 ? * MON-FRI *)` - Update graph at 6 PM on weekdays 184 | 185 | #### Deployment Script Presets 186 | - **daily-collection-and-export**: Daily data collection and graph export 187 | - **frequent-collection-daily-export**: 6-hour data collection, daily graph export 188 | - **business-hours**: 9 AM data collection, 6 PM graph export (EST) 189 | - **disabled**: All scheduling disabled (manual execution only) 190 | 191 | ### Features 192 | 193 | ✅ **Event-Driven Execution**: Graph export automatically triggers after data collection completes 194 | ✅ **Intelligent Chaining**: Ensures graph always uses the freshest data 195 | ✅ **Dual-Trigger System**: Event-driven + optional time-based scheduling 196 | ✅ **Flexible Scheduling**: Rate-based or cron-based expressions 197 | ✅ **Timezone Support**: Configure schedules for your local timezone 198 | ✅ **Error Handling**: Dead letter queues for failed executions 199 | ✅ **Monitoring**: CloudWatch logs and metrics 200 | ✅ **Cost Optimization**: Only runs graph export when there's new data 201 | ✅ **Easy Deployment**: Enhanced script with common presets 202 | ✅ **Validation**: Built-in parameter validation and configuration summary 203 | 204 | For detailed scheduling configuration, see the [Scheduling Guide](SCHEDULING_GUIDE.md). 205 | 206 | **NOTE:** Consider how often to run the scheduled updates to keep your data 'fresh' while managing costs effectively. 207 | 208 | ### Deployment Architecture 209 | 210 | #### 1. Deployment Approach 211 | After running the `aria-boostrap.sh` script to prepare your environment, the solution uses a **single-step deployment** approach with: 212 | - **Nested CloudFormation stacks** for modular architecture 213 | - **Direct stack output references** (no export dependencies) 214 | - **Automatic parameter passing** between stacks 215 | - **Built-in validation** and error handling 216 | 217 | #### 2. Architecture Benefits 218 | - ✅ Eliminates CloudFormation export conflicts 219 | - ✅ Enables reliable, repeatable deployments 220 | - ✅ Simplifies updates and maintenance 221 | - ✅ Provides cleaner dependency management 222 | 223 | #### 3. What Gets Deployed 224 | 225 | The solution creates: 226 | - **Lambda Functions**: Data collection and processing 227 | - **Step Functions**: Orchestration workflows 228 | - **DynamoDB Tables**: Identity data storage 229 | - **Neptune Analytics**: Graph database and visualization 230 | - **EventBridge Rules**: Automatic scheduling (optional) 231 | - **IAM Roles**: Least-privilege access controls 232 | 233 | ### Execution Flow 234 | 235 | 1. **Data Collection**: Gathers identity data from IAM Identity Center 236 | 2. **Data Processing**: Enriches and stores data in DynamoDB 237 | 3. **Graph Export**: Converts data to Neptune-compatible format 238 | 4. **Visualization**: Creates interactive graph in Neptune Analytics 239 | 240 | * Navigate to Amazon Neptune, and click on Notebooks 241 | * You should have a notebook named `aws-neptune-analytics-Aria-Neptune-Notebook` or something similar 242 | * Click on `radio button > Actions > Open Graph Explorer` 243 | * Make sure you add all the nodes and edges and experiment away! 244 | 245 | Here is an example of a graph visualization that you can create that looks very similar to the relationships diagram shown above. 246 | 247 | ![Example Graph](/img/graph-example.png) 248 | 249 | ## Updating the solution 250 | 251 | This solution is under active development so there will be various updates and improvements that you may want to take advantage of over time. To keep your implementation up to date: 252 | 253 | 1. Perform a `git pull` to get your local copy up to date 254 | 2. Obtain credentials for the account that you originally deployed the solution into 255 | 3. At the command line run the `aria-bootstrap.sh` script 256 | * This script will upload the latest code to the S3 bucket in your account 257 | * A lambda function will then be triggered following the S3 upload to update the Lambda code for the various Lambda functions, ensuring that each of them are running the latest code 258 | 4. Once the Lambda functions are updated, re-run the deployment script (`deploy-nested-stacks.sh`) using the same command line as you used previously. 259 | 260 | **NOTE:** If you have made *any* changes to the solution then performing this update **will overwrite** your changes - take note! 261 | 262 | ## Troubleshooting 263 | 264 | ### Common Deployment Issues 265 | 266 | #### 1. Resource Name Length Limits 267 | If you encounter errors about resource names being too long: 268 | - Use shorter stack names (recommended: 20 characters or less) 269 | - The deployment script automatically handles name length optimization 270 | 271 | #### 2. CloudFormation Export Conflicts 272 | **Issue Resolved**: Templates updated to eliminate all export dependencies. 273 | 274 | **Common Error Messages:** 275 | - "No export named NeptuneGraphEndpoint found" 276 | - "Cannot delete export as it is in use" 277 | 278 | **Solution**: The architecture has been improved to use direct parameter passing instead of exports. 279 | 280 | **For New Deployments:** 281 | ```bash 282 | ./deploy-nested-stacks.sh --deploy-neptune true 283 | ``` 284 | 285 | **For Existing Deployments with Conflicts:** 286 | ```bash 287 | # Recommended: Clean deployment 288 | aws cloudformation delete-stack --stack-name aria-gv-setup 289 | aws cloudformation wait stack-delete-complete --stack-name aria-gv-setup 290 | ./deploy-nested-stacks.sh --deploy-neptune true 291 | ``` 292 | 293 | **Why This Happens**: Existing deployments use the old export-based architecture. The new parameter-based architecture eliminates these conflicts permanently. 294 | 295 | #### 3. EventBridge Rule Creation Failures 296 | If EventBridge rules fail to create: 297 | - Verify that both data collection and graph export scheduling are properly configured 298 | - Check that the AriaStateMachine ARN is correctly passed between stacks 299 | 300 | #### 4. Security Group Updates 301 | If security group updates fail due to custom naming: 302 | - The templates now use CloudFormation-generated names to avoid replacement conflicts 303 | - Existing deployments will automatically migrate to the new naming approach 304 | 305 | ### Deployment Best Practices 306 | 307 | 1. **Use the Enhanced Script**: `./deploy-nested-stacks.sh` handles most common issues automatically 308 | 2. **Choose Appropriate Scheduling**: Match your scheduling frequency to your data change patterns 309 | 3. **Monitor Costs**: Frequent scheduling increases Lambda and Step Functions costs 310 | 4. **Test Scheduling**: Start with manual execution before enabling automatic scheduling 311 | 312 | ### Getting Help 313 | 314 | - Check the [Scheduling Guide](SCHEDULING_GUIDE.md) for detailed configuration options 315 | - Review CloudFormation stack events for specific error details 316 | - Ensure all prerequisites are met (IAM permissions, cross-account roles) 317 | 318 | Got an idea for how this solution could be extended and improved? Let us know! -------------------------------------------------------------------------------- /templates/step-functions.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "Step Functions state machine for Access Rights for Identity on AWS" 3 | 4 | Parameters: 5 | CreateTablesLambdaArn: 6 | Type: String 7 | Description: ARN of CreateTables Lambda function 8 | ListUsersLambdaArn: 9 | Type: String 10 | Description: ARN of ListUsers Lambda function 11 | ListGroupsLambdaArn: 12 | Type: String 13 | Description: ARN of ListGroups Lambda function 14 | ListGroupMembershipLambdaArn: 15 | Type: String 16 | Description: ARN of ListGroupMembership Lambda function 17 | ListAccountsLambdaArn: 18 | Type: String 19 | Description: ARN of ListAccounts Lambda function 20 | ListPermissionSetsLambdaArn: 21 | Type: String 22 | Description: ARN of ListPermissionSets Lambda function 23 | ListProvisionedPermissionSetsLambdaArn: 24 | Type: String 25 | Description: ARN of ListProvisionedPermissionSets Lambda function 26 | ListUserAccountAssignmentsLambdaArn: 27 | Type: String 28 | Description: ARN of ListUserAccountAssignments Lambda function 29 | ListGroupAccountAssignmentsLambdaArn: 30 | Type: String 31 | Description: ARN of ListGroupAccountAssignments Lambda function 32 | GetIAMRolesLambdaArn: 33 | Type: String 34 | Description: ARN of GetIAMRoles Lambda function 35 | 36 | # Scheduling Parameters for AriaStateMachine 37 | EnableDataCollectionScheduling: 38 | Type: String 39 | Description: Enable automatic scheduling of the AriaStateMachine for data collection 40 | 41 | DataCollectionScheduleExpression: 42 | Type: String 43 | Description: "Schedule expression for automatic data collection (e.g., rate(6 hours), cron(0 */6 * * ? *))" 44 | 45 | DataCollectionScheduleDescription: 46 | Type: String 47 | Description: "Description for the scheduled data collection execution" 48 | 49 | DataCollectionScheduleTimezone: 50 | Type: String 51 | Description: "Timezone for cron-based schedules (e.g., America/New_York, UTC)" 52 | 53 | Conditions: 54 | ShouldEnableDataCollectionScheduling: 55 | !Equals [!Ref EnableDataCollectionScheduling, "true"] 56 | 57 | Resources: 58 | AriaStateMachine: 59 | Type: "AWS::StepFunctions::StateMachine" 60 | DeletionPolicy: Delete 61 | UpdateReplacePolicy: Delete 62 | DependsOn: 63 | - AriaStateMachineRolePolicy 64 | Properties: 65 | Definition: 66 | Comment: State machine for Aria 67 | StartAt: Create DynamoDB Tables 68 | States: 69 | Create DynamoDB Tables: 70 | Type: Task 71 | Resource: arn:aws:states:::lambda:invoke 72 | Output: "{% $states.result.Payload %}" 73 | Arguments: 74 | FunctionName: !Ref CreateTablesLambdaArn 75 | Retry: 76 | - ErrorEquals: 77 | - Lambda.ServiceException 78 | - Lambda.AWSLambdaException 79 | - Lambda.SdkClientException 80 | - Lambda.TooManyRequestsException 81 | IntervalSeconds: 1 82 | MaxAttempts: 3 83 | BackoffRate: 2 84 | JitterStrategy: FULL 85 | Next: Parallel1 86 | Parallel1: 87 | Type: Parallel 88 | Next: Parallel2 89 | Branches: 90 | - StartAt: List IdC Users 91 | States: 92 | List IdC Users: 93 | Type: Task 94 | Resource: arn:aws:states:::lambda:invoke 95 | Output: "{% $states.result.Payload %}" 96 | Arguments: 97 | FunctionName: !Ref ListUsersLambdaArn 98 | Retry: 99 | - ErrorEquals: 100 | - Lambda.ServiceException 101 | - Lambda.AWSLambdaException 102 | - Lambda.SdkClientException 103 | - Lambda.TooManyRequestsException 104 | IntervalSeconds: 1 105 | MaxAttempts: 3 106 | BackoffRate: 2 107 | JitterStrategy: FULL 108 | End: true 109 | - StartAt: List IdC Groups 110 | States: 111 | List IdC Groups: 112 | Type: Task 113 | Resource: arn:aws:states:::lambda:invoke 114 | Output: "{% $states.result.Payload %}" 115 | Arguments: 116 | FunctionName: !Ref ListGroupsLambdaArn 117 | Retry: 118 | - ErrorEquals: 119 | - Lambda.ServiceException 120 | - Lambda.AWSLambdaException 121 | - Lambda.SdkClientException 122 | - Lambda.TooManyRequestsException 123 | IntervalSeconds: 1 124 | MaxAttempts: 3 125 | BackoffRate: 2 126 | JitterStrategy: FULL 127 | End: true 128 | - StartAt: List IdC Accounts 129 | States: 130 | List IdC Accounts: 131 | Type: Task 132 | Resource: arn:aws:states:::lambda:invoke 133 | Output: "{% $states.result.Payload %}" 134 | Arguments: 135 | FunctionName: !Ref ListAccountsLambdaArn 136 | Retry: 137 | - ErrorEquals: 138 | - Lambda.ServiceException 139 | - Lambda.AWSLambdaException 140 | - Lambda.SdkClientException 141 | - Lambda.TooManyRequestsException 142 | IntervalSeconds: 1 143 | MaxAttempts: 3 144 | BackoffRate: 2 145 | JitterStrategy: FULL 146 | End: true 147 | - StartAt: List IdC Permission Sets 148 | States: 149 | List IdC Permission Sets: 150 | Type: Task 151 | Resource: arn:aws:states:::lambda:invoke 152 | Output: "{% $states.result.Payload %}" 153 | Arguments: 154 | FunctionName: !Ref ListPermissionSetsLambdaArn 155 | Retry: 156 | - ErrorEquals: 157 | - Lambda.ServiceException 158 | - Lambda.AWSLambdaException 159 | - Lambda.SdkClientException 160 | - Lambda.TooManyRequestsException 161 | IntervalSeconds: 1 162 | MaxAttempts: 3 163 | BackoffRate: 2 164 | JitterStrategy: FULL 165 | End: true 166 | Parallel2: 167 | Type: Parallel 168 | Next: List IAM Roles created by IAM Identity Center 169 | Branches: 170 | - StartAt: List IdC Group Memberships 171 | States: 172 | List IdC Group Memberships: 173 | Type: Task 174 | Resource: arn:aws:states:::lambda:invoke 175 | Output: "{% $states.result.Payload %}" 176 | Arguments: 177 | FunctionName: !Ref ListGroupMembershipLambdaArn 178 | Retry: 179 | - ErrorEquals: 180 | - Lambda.ServiceException 181 | - Lambda.AWSLambdaException 182 | - Lambda.SdkClientException 183 | - Lambda.TooManyRequestsException 184 | IntervalSeconds: 1 185 | MaxAttempts: 3 186 | BackoffRate: 2 187 | JitterStrategy: FULL 188 | End: true 189 | - StartAt: List IdC Provisioned Permission Sets 190 | States: 191 | List IdC Provisioned Permission Sets: 192 | Type: Task 193 | Resource: arn:aws:states:::lambda:invoke 194 | Output: "{% $states.result.Payload %}" 195 | Arguments: 196 | FunctionName: !Ref ListProvisionedPermissionSetsLambdaArn 197 | Retry: 198 | - ErrorEquals: 199 | - Lambda.ServiceException 200 | - Lambda.AWSLambdaException 201 | - Lambda.SdkClientException 202 | - Lambda.TooManyRequestsException 203 | IntervalSeconds: 1 204 | MaxAttempts: 3 205 | BackoffRate: 2 206 | JitterStrategy: FULL 207 | End: true 208 | - StartAt: List IdC User Account Assignments 209 | States: 210 | List IdC User Account Assignments: 211 | Type: Task 212 | Resource: arn:aws:states:::lambda:invoke 213 | Output: "{% $states.result.Payload %}" 214 | Arguments: 215 | FunctionName: !Ref ListUserAccountAssignmentsLambdaArn 216 | Retry: 217 | - ErrorEquals: 218 | - Lambda.ServiceException 219 | - Lambda.AWSLambdaException 220 | - Lambda.SdkClientException 221 | - Lambda.TooManyRequestsException 222 | IntervalSeconds: 1 223 | MaxAttempts: 3 224 | BackoffRate: 2 225 | JitterStrategy: FULL 226 | End: true 227 | - StartAt: List IdC Group Account Assignments 228 | States: 229 | List IdC Group Account Assignments: 230 | Type: Task 231 | Resource: arn:aws:states:::lambda:invoke 232 | Output: "{% $states.result.Payload %}" 233 | Arguments: 234 | FunctionName: !Ref ListGroupAccountAssignmentsLambdaArn 235 | Retry: 236 | - ErrorEquals: 237 | - Lambda.ServiceException 238 | - Lambda.AWSLambdaException 239 | - Lambda.SdkClientException 240 | - Lambda.TooManyRequestsException 241 | IntervalSeconds: 1 242 | MaxAttempts: 3 243 | BackoffRate: 2 244 | JitterStrategy: FULL 245 | End: true 246 | List IAM Roles created by IAM Identity Center: 247 | Type: Task 248 | Resource: arn:aws:states:::lambda:invoke 249 | Output: "{% $states.result.Payload %}" 250 | Arguments: 251 | FunctionName: !Ref GetIAMRolesLambdaArn 252 | Retry: 253 | - ErrorEquals: 254 | - Lambda.ServiceException 255 | - Lambda.AWSLambdaException 256 | - Lambda.SdkClientException 257 | - Lambda.TooManyRequestsException 258 | IntervalSeconds: 1 259 | MaxAttempts: 3 260 | BackoffRate: 2 261 | JitterStrategy: FULL 262 | End: true 263 | QueryLanguage: JSONata 264 | RoleArn: !GetAtt AriaStateMachineRole.Arn 265 | StateMachineName: AriaStateMachine 266 | StateMachineType: STANDARD 267 | EncryptionConfiguration: 268 | Type: AWS_OWNED_KEY 269 | 270 | Tags: 271 | - Key: aria 272 | Value: state 273 | 274 | AriaStateMachineRole: 275 | Type: AWS::IAM::Role 276 | DeletionPolicy: Delete 277 | UpdateReplacePolicy: Delete 278 | Properties: 279 | AssumeRolePolicyDocument: 280 | Version: "2012-10-17" 281 | Statement: 282 | - Effect: Allow 283 | Principal: 284 | Service: states.amazonaws.com 285 | Action: sts:AssumeRole 286 | MaxSessionDuration: 3600 287 | Tags: 288 | - Key: aria 289 | Value: role 290 | 291 | AriaStateMachineRolePolicy: 292 | Type: AWS::IAM::RolePolicy 293 | Properties: 294 | PolicyName: StateMachineLambdaInvokeScopedAccessPolicy 295 | RoleName: !Ref AriaStateMachineRole 296 | PolicyDocument: 297 | Version: "2012-10-17" 298 | Statement: 299 | - Effect: Allow 300 | Action: 301 | - lambda:InvokeFunction 302 | Resource: 303 | - !Ref CreateTablesLambdaArn 304 | - !Ref ListUsersLambdaArn 305 | - !Ref ListGroupsLambdaArn 306 | - !Ref ListGroupMembershipLambdaArn 307 | - !Ref ListAccountsLambdaArn 308 | - !Ref ListPermissionSetsLambdaArn 309 | - !Ref ListProvisionedPermissionSetsLambdaArn 310 | - !Ref ListUserAccountAssignmentsLambdaArn 311 | - !Ref ListGroupAccountAssignmentsLambdaArn 312 | - !Ref GetIAMRolesLambdaArn 313 | - Effect: Allow 314 | Action: 315 | - logs:CreateLogDelivery 316 | - logs:GetLogDelivery 317 | - logs:UpdateLogDelivery 318 | - logs:DeleteLogDelivery 319 | - logs:ListLogDeliveries 320 | - logs:PutResourcePolicy 321 | - logs:DescribeResourcePolicies 322 | - logs:DescribeLogGroups 323 | - logs:CreateLogGroup 324 | - logs:CreateLogStream 325 | - logs:PutLogEvents 326 | - logs:DescribeLogStreams 327 | Resource: "*" 328 | 329 | # EventBridge Scheduler for AriaStateMachine Data Collection 330 | AriaDataCollectionScheduleRole: 331 | Type: AWS::IAM::Role 332 | Condition: ShouldEnableDataCollectionScheduling 333 | DeletionPolicy: Delete 334 | UpdateReplacePolicy: Delete 335 | Properties: 336 | AssumeRolePolicyDocument: 337 | Version: "2012-10-17" 338 | Statement: 339 | - Effect: Allow 340 | Principal: 341 | Service: scheduler.amazonaws.com 342 | Action: sts:AssumeRole 343 | Policies: 344 | - PolicyName: AriaDataCollectionSchedulePolicy 345 | PolicyDocument: 346 | Version: "2012-10-17" 347 | Statement: 348 | - Effect: Allow 349 | Action: 350 | - states:StartExecution 351 | Resource: !GetAtt AriaStateMachine.Arn 352 | Tags: 353 | - Key: aria 354 | Value: scheduler-role 355 | 356 | AriaDataCollectionSchedule: 357 | Type: AWS::Scheduler::Schedule 358 | Condition: ShouldEnableDataCollectionScheduling 359 | DeletionPolicy: Delete 360 | UpdateReplacePolicy: Delete 361 | Properties: 362 | Name: !Sub "${AWS::StackName}-DataCollection" 363 | Description: !Ref DataCollectionScheduleDescription 364 | State: ENABLED 365 | FlexibleTimeWindow: 366 | Mode: "OFF" 367 | ScheduleExpression: !Ref DataCollectionScheduleExpression 368 | ScheduleExpressionTimezone: !Ref DataCollectionScheduleTimezone 369 | Target: 370 | Arn: !GetAtt AriaStateMachine.Arn 371 | RoleArn: !GetAtt AriaDataCollectionScheduleRole.Arn 372 | RetryPolicy: 373 | MaximumRetryAttempts: 2 374 | DeadLetterConfig: 375 | Arn: !GetAtt AriaDataCollectionScheduleDLQ.Arn 376 | 377 | # Dead Letter Queue for failed scheduled data collection executions 378 | AriaDataCollectionScheduleDLQ: 379 | Type: AWS::SQS::Queue 380 | Condition: ShouldEnableDataCollectionScheduling 381 | DeletionPolicy: Delete 382 | UpdateReplacePolicy: Delete 383 | Metadata: 384 | cfn_nag: 385 | rules_to_suppress: 386 | - id: W48 387 | reason: "Default SQS encryption is sufficient for this dead letter queue" 388 | checkov: 389 | skip: 390 | - id: CKV_AWS_27 391 | comment: "Default SQS encryption is sufficient for this dead letter queue" 392 | Properties: 393 | QueueName: !Sub "${AWS::StackName}-DataCollection-DLQ" 394 | MessageRetentionPeriod: 1209600 # 14 days 395 | Tags: 396 | - Key: aria 397 | Value: dlq 398 | 399 | # CloudWatch Log Group for data collection scheduler monitoring 400 | AriaDataCollectionScheduleLogGroup: 401 | Type: AWS::Logs::LogGroup 402 | Condition: ShouldEnableDataCollectionScheduling 403 | DeletionPolicy: Delete 404 | UpdateReplacePolicy: Delete 405 | Metadata: 406 | cfn_nag: 407 | rules_to_suppress: 408 | - id: W84 409 | reason: "Default CloudWatch Logs encryption is sufficient for this log group" 410 | checkov: 411 | skip: 412 | - id: CKV_AWS_158 413 | comment: "Default CloudWatch Logs encryption is sufficient for this scheduler log group" 414 | Properties: 415 | LogGroupName: !Sub "/aws/scheduler/${AWS::StackName}-DataCollection" 416 | RetentionInDays: 30 417 | Tags: 418 | - Key: aria 419 | Value: log 420 | 421 | Outputs: 422 | StateMachineArn: 423 | Description: ARN of the Step Functions state machine 424 | Value: !GetAtt AriaStateMachine.Arn 425 | Export: 426 | Name: !Sub "${AWS::StackName}-StateMachineArn" 427 | StateMachineName: 428 | Description: Name of the Step Functions state machine 429 | Value: !Ref AriaStateMachine 430 | Export: 431 | Name: !Sub "${AWS::StackName}-StateMachineName" 432 | 433 | # Data Collection Scheduling Outputs 434 | DataCollectionSchedulingEnabled: 435 | Description: "Whether scheduling is enabled for the AriaStateMachine data collection" 436 | Value: !Ref EnableDataCollectionScheduling 437 | Export: 438 | Name: !Sub "${AWS::StackName}-DataCollectionSchedulingEnabled" 439 | 440 | DataCollectionScheduleExpression: 441 | Condition: ShouldEnableDataCollectionScheduling 442 | Description: "Schedule expression for the AriaStateMachine data collection" 443 | Value: !Ref DataCollectionScheduleExpression 444 | Export: 445 | Name: !Sub "${AWS::StackName}-DataCollectionScheduleExpression" 446 | 447 | DataCollectionScheduleName: 448 | Condition: ShouldEnableDataCollectionScheduling 449 | Description: "Name of the EventBridge Schedule for data collection" 450 | Value: !Ref AriaDataCollectionSchedule 451 | Export: 452 | Name: !Sub "${AWS::StackName}-DataCollectionScheduleName" 453 | -------------------------------------------------------------------------------- /templates/main-stack.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: 'Main CloudFormation template for Workforce Identity Visualization - orchestrates nested stacks (uksb-zilgml23n9)' 3 | 4 | Parameters: 5 | S3SourceBucketName: 6 | Type: AWS::SSM::Parameter::Value 7 | Description: Name of the SSM parameter containing the S3 bucket with all Lambda function source code 8 | Default: 'aria-source-bucket' 9 | 10 | S3ExportBucketName: 11 | Type: AWS::SSM::Parameter::Value 12 | Description: Name of the SSM parameter containing the S3 bucket to export all data to 13 | Default: 'aria-export-bucket' 14 | 15 | CreateTablesS3Key: 16 | Type: String 17 | Description: S3 key (path) to the CreateTables Lambda function code zip file 18 | Default: 'createtables.zip' 19 | 20 | ListUsersS3Key: 21 | Type: String 22 | Description: S3 key (path) to the ListUsers Lambda function code zip file 23 | Default: 'listusers.zip' 24 | 25 | ListGroupsS3Key: 26 | Type: String 27 | Description: S3 key (path) to the ListGroups Lambda function code zip file 28 | Default: 'listgroups.zip' 29 | 30 | ListGroupMembershipS3Key: 31 | Type: String 32 | Description: S3 key (path) to the ListGroupMembership Lambda function code zip file 33 | Default: 'listgroupmembership.zip' 34 | 35 | ListPermissionSetsS3Key: 36 | Type: String 37 | Description: S3 key (path) to the ListPermissionSets Lambda function code zip file 38 | Default: 'listpermissionsets.zip' 39 | 40 | ListProvisionedPermissionSetsS3Key: 41 | Type: String 42 | Description: S3 key (path) to the ListProvisionedPermissionSets Lambda function code zip file 43 | Default: 'listprovisionedpermissionsets.zip' 44 | 45 | ListAccountsS3Key: 46 | Type: String 47 | Description: S3 key (path) to the ListAccounts Lambda function code zip file 48 | Default: 'listaccounts.zip' 49 | 50 | ListUserAccountAssignmentsS3Key: 51 | Type: String 52 | Description: S3 key (path) to the ListUserAccountAssignments Lambda function code zip file 53 | Default: 'listuseraccountassignments.zip' 54 | 55 | ListGroupAccountAssignmentsS3Key: 56 | Type: String 57 | Description: S3 key (path) to the ListGroupAccountAssignments Lambda function code zip file 58 | Default: 'listgroupaccountassignments.zip' 59 | 60 | GetIAMRolesS3Key: 61 | Type: String 62 | Description: S3 key (path) to the GetIAMRoles Lambda function code zip file 63 | Default: 'getiamroles.zip' 64 | 65 | S3ExportS3Key: 66 | Type: String 67 | Description: S3 key (path) to the S3Export Lambda function code zip file 68 | Default: 's3export.zip' 69 | 70 | AccessAnalyzerFindingIngestionS3Key: 71 | Type: String 72 | Description: S3 key (path) to the AccessAnalyzerFindingIngestion Lambda function code zip file 73 | Default: 'accessanalyzerfindingingestion.zip' 74 | 75 | UpdateFunctionCodeS3Key: 76 | Type: String 77 | Description: S3 key (path) to the UpdateFunctionCode Lambda function code zip file 78 | Default: 'updatefunctioncode.zip' 79 | 80 | PythonHandler: 81 | Type: String 82 | Description: The Python handler function 83 | Default: lambda_function.lambda_handler 84 | 85 | TemplatesBucketName: 86 | Type: String 87 | Description: S3 bucket containing nested CloudFormation templates 88 | Default: 'aria-templates-bucket' 89 | 90 | # Neptune Analytics Parameters 91 | GraphName: 92 | Type: String 93 | Description: 'Name of the Neptune graph' 94 | Default: 'aria-identitycenter' 95 | 96 | PublicIPAddress: 97 | Type: String 98 | Description: Provide your public IP address in CIDR format e.g. 1.2.3.4/32 99 | Default: '0.0.0.0/0' 100 | 101 | # Neptune Notebook Parameters 102 | NotebookInstanceType: 103 | Type: String 104 | Description: SageMaker Notebook instance type 105 | Default: ml.t2.large 106 | AllowedValues: 107 | - ml.t2.medium 108 | - ml.t2.large 109 | - ml.t2.xlarge 110 | - ml.t2.2xlarge 111 | - ml.t3.medium 112 | - ml.t3.large 113 | - ml.t3.xlarge 114 | - ml.t3.2xlarge 115 | - ml.m4.xlarge 116 | - ml.m4.2xlarge 117 | - ml.m4.4xlarge 118 | - ml.m4.10xlarge 119 | - ml.m4.16xlarge 120 | - ml.m5.xlarge 121 | - ml.m5.2xlarge 122 | - ml.m5.4xlarge 123 | - ml.m5.12xlarge 124 | - ml.m5.24xlarge 125 | 126 | NotebookName: 127 | Type: String 128 | Description: Name for the notebook instance 129 | Default: 'Aria-Jupyter-Notebook' 130 | MaxLength: 38 131 | 132 | DeployNeptune: 133 | Type: String 134 | Description: Whether to deploy Neptune Analytics and Notebook 135 | Default: 'true' 136 | AllowedValues: 137 | - 'true' 138 | - 'false' 139 | 140 | # Scheduling Parameters 141 | EnableScheduling: 142 | Type: String 143 | Description: Enable automatic scheduling of the AriaExportGraphStateMachine 144 | Default: 'false' 145 | AllowedValues: 146 | - 'true' 147 | - 'false' 148 | 149 | ScheduleExpression: 150 | Type: String 151 | Description: 'Schedule expression for automatic execution (e.g., rate(1 day), cron(0 2 * * ? *))' 152 | Default: 'rate(1 day)' 153 | 154 | ScheduleDescription: 155 | Type: String 156 | Description: 'Description for the scheduled execution' 157 | Default: 'Daily execution of ARIA graph export and import' 158 | 159 | ScheduleTimezone: 160 | Type: String 161 | Description: 'Timezone for cron-based schedules (e.g., America/New_York, UTC)' 162 | Default: 'UTC' 163 | 164 | # Data Collection Scheduling Parameters 165 | EnableDataCollectionScheduling: 166 | Type: String 167 | Description: Enable automatic scheduling of the AriaStateMachine for data collection 168 | Default: 'false' 169 | AllowedValues: 170 | - 'true' 171 | - 'false' 172 | 173 | DataCollectionScheduleExpression: 174 | Type: String 175 | Description: 'Schedule expression for automatic data collection (e.g., rate(6 hours), cron(0 */6 * * ? *))' 176 | Default: 'rate(6 hours)' 177 | 178 | DataCollectionScheduleDescription: 179 | Type: String 180 | Description: 'Description for the scheduled data collection execution' 181 | Default: 'Automated ARIA identity data collection every 6 hours' 182 | 183 | DataCollectionScheduleTimezone: 184 | Type: String 185 | Description: 'Timezone for cron-based schedules (e.g., America/New_York, UTC)' 186 | Default: 'UTC' 187 | 188 | Conditions: 189 | ShouldDeployNeptune: !Equals [!Ref DeployNeptune, 'true'] 190 | ShouldDeployNeptuneWithScheduling: !And 191 | - !Equals [!Ref DeployNeptune, 'true'] 192 | - !Equals [!Ref EnableScheduling, 'true'] 193 | 194 | Metadata: 195 | AWS::CloudFormation::Interface: 196 | ParameterGroups: 197 | - 198 | Label: 199 | default: "S3 Bucket SSM Parameters" 200 | Parameters: 201 | - S3SourceBucketName 202 | - S3ExportBucketName 203 | - TemplatesBucketName 204 | - 205 | Label: 206 | default: "Lambda Functions" 207 | Parameters: 208 | - CreateTablesS3Key 209 | - ListUsersS3Key 210 | - ListGroupsS3Key 211 | - ListGroupMembershipS3Key 212 | - ListPermissionSetsS3Key 213 | - ListProvisionedPermissionSetsS3Key 214 | - ListAccountsS3Key 215 | - ListUserAccountAssignmentsS3Key 216 | - ListGroupAccountAssignmentsS3Key 217 | - GetIAMRolesS3Key 218 | - S3ExportS3Key 219 | - AccessAnalyzerFindingIngestionS3Key 220 | - UpdateFunctionCodeS3Key 221 | - 222 | Label: 223 | default: "Python Handler" 224 | Parameters: 225 | - PythonHandler 226 | - 227 | Label: 228 | default: "Neptune Analytics" 229 | Parameters: 230 | - DeployNeptune 231 | - GraphName 232 | - PublicIPAddress 233 | - 234 | Label: 235 | default: "Neptune Notebook" 236 | Parameters: 237 | - NotebookInstanceType 238 | - NotebookName 239 | - 240 | Label: 241 | default: "Graph Export Scheduling" 242 | Parameters: 243 | - EnableScheduling 244 | - ScheduleExpression 245 | - ScheduleDescription 246 | - ScheduleTimezone 247 | - 248 | Label: 249 | default: "Data Collection Scheduling" 250 | Parameters: 251 | - EnableDataCollectionScheduling 252 | - DataCollectionScheduleExpression 253 | - DataCollectionScheduleDescription 254 | - DataCollectionScheduleTimezone 255 | 256 | Resources: 257 | # Lambda Functions Stack 258 | LambdaStack: 259 | Type: AWS::CloudFormation::Stack 260 | DeletionPolicy: Delete 261 | UpdateReplacePolicy: Delete 262 | Properties: 263 | TemplateURL: !Sub 'https://${TemplatesBucketName}.s3.${AWS::Region}.amazonaws.com/lambda-functions.yaml' 264 | Parameters: 265 | S3SourceBucketName: !Ref S3SourceBucketName 266 | S3ExportBucketName: !Ref S3ExportBucketName 267 | CreateTablesS3Key: !Ref CreateTablesS3Key 268 | ListUsersS3Key: !Ref ListUsersS3Key 269 | ListGroupsS3Key: !Ref ListGroupsS3Key 270 | ListGroupMembershipS3Key: !Ref ListGroupMembershipS3Key 271 | ListPermissionSetsS3Key: !Ref ListPermissionSetsS3Key 272 | ListProvisionedPermissionSetsS3Key: !Ref ListProvisionedPermissionSetsS3Key 273 | ListAccountsS3Key: !Ref ListAccountsS3Key 274 | ListUserAccountAssignmentsS3Key: !Ref ListUserAccountAssignmentsS3Key 275 | ListGroupAccountAssignmentsS3Key: !Ref ListGroupAccountAssignmentsS3Key 276 | GetIAMRolesS3Key: !Ref GetIAMRolesS3Key 277 | S3ExportS3Key: !Ref S3ExportS3Key 278 | AccessAnalyzerFindingIngestionS3Key: !Ref AccessAnalyzerFindingIngestionS3Key 279 | UpdateFunctionCodeS3Key: !Ref UpdateFunctionCodeS3Key 280 | PythonHandler: !Ref PythonHandler 281 | StackName: !Ref AWS::StackName 282 | Tags: 283 | - Key: aria 284 | Value: nested-stack 285 | 286 | # SSM Parameters Stack 287 | SSMStack: 288 | Type: AWS::CloudFormation::Stack 289 | DeletionPolicy: Delete 290 | UpdateReplacePolicy: Delete 291 | Properties: 292 | TemplateURL: !Sub 'https://${TemplatesBucketName}.s3.${AWS::Region}.amazonaws.com/ssm-stack.yaml' 293 | Parameters: 294 | CreateTablesS3Key: !Ref CreateTablesS3Key 295 | ListUsersS3Key: !Ref ListUsersS3Key 296 | ListGroupsS3Key: !Ref ListGroupsS3Key 297 | ListGroupMembershipS3Key: !Ref ListGroupMembershipS3Key 298 | ListPermissionSetsS3Key: !Ref ListPermissionSetsS3Key 299 | ListProvisionedPermissionSetsS3Key: !Ref ListProvisionedPermissionSetsS3Key 300 | ListAccountsS3Key: !Ref ListAccountsS3Key 301 | ListUserAccountAssignmentsS3Key: !Ref ListUserAccountAssignmentsS3Key 302 | ListGroupAccountAssignmentsS3Key: !Ref ListGroupAccountAssignmentsS3Key 303 | GetIAMRolesS3Key: !Ref GetIAMRolesS3Key 304 | S3ExportS3Key: !Ref S3ExportS3Key 305 | AccessAnalyzerFindingIngestionS3Key: !Ref AccessAnalyzerFindingIngestionS3Key 306 | UpdateFunctionCodeS3Key: !Ref UpdateFunctionCodeS3Key 307 | CreateTablesLambdaArn: !GetAtt LambdaStack.Outputs.CreateTablesLambdaArn 308 | ListUsersLambdaArn: !GetAtt LambdaStack.Outputs.ListUsersLambdaArn 309 | ListGroupsLambdaArn: !GetAtt LambdaStack.Outputs.ListGroupsLambdaArn 310 | ListGroupMembershipLambdaArn: !GetAtt LambdaStack.Outputs.ListGroupMembershipLambdaArn 311 | ListPermissionSetsLambdaArn: !GetAtt LambdaStack.Outputs.ListPermissionSetsLambdaArn 312 | ListProvisionedPermissionSetsLambdaArn: !GetAtt LambdaStack.Outputs.ListProvisionedPermissionSetsLambdaArn 313 | ListAccountsLambdaArn: !GetAtt LambdaStack.Outputs.ListAccountsLambdaArn 314 | ListUserAccountAssignmentsLambdaArn: !GetAtt LambdaStack.Outputs.ListUserAccountAssignmentsLambdaArn 315 | ListGroupAccountAssignmentsLambdaArn: !GetAtt LambdaStack.Outputs.ListGroupAccountAssignmentsLambdaArn 316 | GetIAMRolesLambdaArn: !GetAtt LambdaStack.Outputs.GetIAMRolesLambdaArn 317 | S3ExportLambdaArn: !GetAtt LambdaStack.Outputs.S3ExportLambdaArn 318 | AccessAnalyzerFindingIngestionLambdaArn: !GetAtt LambdaStack.Outputs.AccessAnalyzerFindingIngestionLambdaArn 319 | UpdateFunctionCodeLambdaArn: !GetAtt LambdaStack.Outputs.UpdateFunctionCodeLambdaArn 320 | Tags: 321 | - Key: aria 322 | Value: nested-stack 323 | 324 | # EventBridge Stack 325 | EventBridgeStack: 326 | Type: AWS::CloudFormation::Stack 327 | DeletionPolicy: Delete 328 | UpdateReplacePolicy: Delete 329 | Properties: 330 | TemplateURL: !Sub 'https://${TemplatesBucketName}.s3.${AWS::Region}.amazonaws.com/eventbridge-stack.yaml' 331 | Parameters: 332 | S3SourceBucketName: !Ref S3SourceBucketName 333 | UpdateFunctionCodeLambdaArn: !GetAtt LambdaStack.Outputs.UpdateFunctionCodeLambdaArn 334 | AccessAnalyzerFindingIngestionLambdaArn: !GetAtt LambdaStack.Outputs.AccessAnalyzerFindingIngestionLambdaArn 335 | StackName: !Ref AWS::StackName 336 | Tags: 337 | - Key: aria 338 | Value: nested-stack 339 | 340 | # Step Functions Stack 341 | StepFunctionsStack: 342 | Type: AWS::CloudFormation::Stack 343 | DeletionPolicy: Delete 344 | UpdateReplacePolicy: Delete 345 | Properties: 346 | TemplateURL: !Sub 'https://${TemplatesBucketName}.s3.${AWS::Region}.amazonaws.com/step-functions.yaml' 347 | Parameters: 348 | CreateTablesLambdaArn: !GetAtt LambdaStack.Outputs.CreateTablesLambdaArn 349 | ListUsersLambdaArn: !GetAtt LambdaStack.Outputs.ListUsersLambdaArn 350 | ListGroupsLambdaArn: !GetAtt LambdaStack.Outputs.ListGroupsLambdaArn 351 | ListGroupMembershipLambdaArn: !GetAtt LambdaStack.Outputs.ListGroupMembershipLambdaArn 352 | ListAccountsLambdaArn: !GetAtt LambdaStack.Outputs.ListAccountsLambdaArn 353 | ListPermissionSetsLambdaArn: !GetAtt LambdaStack.Outputs.ListPermissionSetsLambdaArn 354 | ListProvisionedPermissionSetsLambdaArn: !GetAtt LambdaStack.Outputs.ListProvisionedPermissionSetsLambdaArn 355 | ListUserAccountAssignmentsLambdaArn: !GetAtt LambdaStack.Outputs.ListUserAccountAssignmentsLambdaArn 356 | ListGroupAccountAssignmentsLambdaArn: !GetAtt LambdaStack.Outputs.ListGroupAccountAssignmentsLambdaArn 357 | GetIAMRolesLambdaArn: !GetAtt LambdaStack.Outputs.GetIAMRolesLambdaArn 358 | EnableDataCollectionScheduling: !Ref EnableDataCollectionScheduling 359 | DataCollectionScheduleExpression: !Ref DataCollectionScheduleExpression 360 | DataCollectionScheduleDescription: !Ref DataCollectionScheduleDescription 361 | DataCollectionScheduleTimezone: !Ref DataCollectionScheduleTimezone 362 | Tags: 363 | - Key: aria 364 | Value: nested-stack 365 | 366 | # Neptune Analytics Stack 367 | NeptuneAnalyticsStack: 368 | Type: AWS::CloudFormation::Stack 369 | Condition: ShouldDeployNeptune 370 | DeletionPolicy: Delete 371 | UpdateReplacePolicy: Delete 372 | Properties: 373 | TemplateURL: !Sub 'https://${TemplatesBucketName}.s3.${AWS::Region}.amazonaws.com/neptune-analytics.yaml' 374 | Parameters: 375 | AriaSetupStackName: !Ref AWS::StackName 376 | S3ExportBucketName: !Ref S3ExportBucketName 377 | AriaStateMachineArn: !GetAtt StepFunctionsStack.Outputs.StateMachineArn 378 | GraphName: !Ref GraphName 379 | PublicIPAddress: !Ref PublicIPAddress 380 | EnableGraphExportScheduling: !Ref EnableScheduling 381 | GraphExportScheduleExpression: !Ref ScheduleExpression 382 | GraphExportScheduleDescription: !Ref ScheduleDescription 383 | GraphExportScheduleTimezone: !Ref ScheduleTimezone 384 | Tags: 385 | - Key: aria 386 | Value: nested-stack 387 | 388 | # Neptune Notebook Stack 389 | NeptuneNotebookStack: 390 | Type: AWS::CloudFormation::Stack 391 | Condition: ShouldDeployNeptune 392 | DeletionPolicy: Delete 393 | UpdateReplacePolicy: Delete 394 | Properties: 395 | TemplateURL: !Sub 'https://${TemplatesBucketName}.s3.${AWS::Region}.amazonaws.com/neptune-notebook.yaml' 396 | Parameters: 397 | NotebookInstanceType: !Ref NotebookInstanceType 398 | NotebookName: !Ref NotebookName 399 | GraphPort: '8182' 400 | GraphVPC: !GetAtt NeptuneAnalyticsStack.Outputs.NeptuneVPC 401 | GraphSubnet: !GetAtt NeptuneAnalyticsStack.Outputs.NeptunePrivateSubnet1 402 | GraphSecurityGroup: !GetAtt NeptuneAnalyticsStack.Outputs.NeptuneAnalyticsSecurityGroup 403 | NeptuneGraphEndpoint: !GetAtt NeptuneAnalyticsStack.Outputs.NeptuneGraphEndpoint 404 | NeptuneGraphId: !GetAtt NeptuneAnalyticsStack.Outputs.NeptuneGraphId 405 | NeptuneGraphName: !GetAtt NeptuneAnalyticsStack.Outputs.NeptuneGraphName 406 | Tags: 407 | - Key: aria 408 | Value: nested-stack 409 | 410 | Outputs: 411 | GetIAMRolesLambdaFunctionArn: 412 | Description: 'ARN of the GetIAMRoles Lambda function' 413 | Value: !GetAtt LambdaStack.Outputs.GetIAMRolesLambdaArn 414 | GetIAMRolesLambdaFunctionName: 415 | Description: 'Name of the GetIAMRoles Lambda function' 416 | Value: !GetAtt LambdaStack.Outputs.GetIAMRolesLambdaName 417 | GetIAMRolesLambdaFunctionExecutionRoleArn: 418 | Description: ARN of IAM role created for GetIAMRoles Lambda function 419 | Value: !GetAtt LambdaStack.Outputs.GetIAMRolesLambdaExecutionRoleArn 420 | S3ExportLambdaFunctionArn: 421 | Description: 'ARN of the S3Export Lambda function' 422 | Value: !GetAtt LambdaStack.Outputs.S3ExportLambdaArn 423 | Export: 424 | Name: 425 | Fn::Sub: ${AWS::StackName}-S3ExportLambdaFunctionArn 426 | S3ExportBucketName: 427 | Description: 'Name of the S3 Export Bucket' 428 | Value: !Ref S3ExportBucketName 429 | Export: 430 | Name: 431 | Fn::Sub: ${AWS::StackName}-S3ExportBucketName 432 | StackName: 433 | Description: 'The stack name' 434 | Value: !Sub '${AWS::StackName}' 435 | Export: 436 | Name: 'Aria-setup-stackname' 437 | NeptuneGraphEndpoint: 438 | Condition: ShouldDeployNeptune 439 | Description: 'Neptune Analytics Graph Endpoint' 440 | Value: !GetAtt NeptuneAnalyticsStack.Outputs.NeptuneGraphEndpoint 441 | NeptuneGraphId: 442 | Condition: ShouldDeployNeptune 443 | Description: 'Neptune Analytics Graph ID' 444 | Value: !GetAtt NeptuneAnalyticsStack.Outputs.NeptuneGraphId 445 | NeptuneNotebookURL: 446 | Condition: ShouldDeployNeptune 447 | Description: 'Neptune Analytics Notebook URL' 448 | Value: !GetAtt NeptuneNotebookStack.Outputs.NeptuneAnalyticsSagemakerNotebook 449 | 450 | # Graph Export Scheduling Outputs (only available when Neptune is deployed and scheduling is enabled) 451 | AriaExportGraphStateMachineArn: 452 | Condition: ShouldDeployNeptune 453 | Description: 'ARN of the AriaExportGraphStateMachine' 454 | Value: !GetAtt NeptuneAnalyticsStack.Outputs.AriaExportGraphStateMachineArn 455 | GraphExportSchedulingEnabled: 456 | Condition: ShouldDeployNeptune 457 | Description: 'Whether scheduling is enabled for the AriaExportGraphStateMachine' 458 | Value: !GetAtt NeptuneAnalyticsStack.Outputs.GraphExportSchedulingEnabled 459 | GraphExportScheduleExpression: 460 | Condition: ShouldDeployNeptuneWithScheduling 461 | Description: 'Schedule expression for the AriaExportGraphStateMachine (if enabled)' 462 | Value: !Ref ScheduleExpression 463 | 464 | GraphExportTriggerRule: 465 | Condition: ShouldDeployNeptuneWithScheduling 466 | Description: 'EventBridge Rule that triggers graph export after data collection completes' 467 | Value: !GetAtt NeptuneAnalyticsStack.Outputs.GraphExportTriggerRuleName 468 | 469 | # Data Collection Scheduling Outputs 470 | AriaStateMachineArn: 471 | Description: 'ARN of the AriaStateMachine for data collection' 472 | Value: !GetAtt StepFunctionsStack.Outputs.StateMachineArn 473 | DataCollectionSchedulingEnabled: 474 | Description: 'Whether scheduling is enabled for the AriaStateMachine data collection' 475 | Value: !GetAtt StepFunctionsStack.Outputs.DataCollectionSchedulingEnabled 476 | DataCollectionScheduleExpression: 477 | Description: 'Schedule expression for the AriaStateMachine data collection (if enabled)' 478 | Value: !Ref DataCollectionScheduleExpression -------------------------------------------------------------------------------- /deploy-nested-stacks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Deploy nested CloudFormation stacks for Access Rights for Identity on AWS 4 | # This script uploads templates to S3 and deploys the main stack 5 | 6 | set -e 7 | 8 | # Configuration 9 | STACK_NAME="aria-gv-setup" 10 | REGION="us-east-1" 11 | DEPLOY_NEPTUNE="true" 12 | PUBLIC_IP="0.0.0.0/0" 13 | 14 | # Data Collection Scheduling Configuration 15 | ENABLE_DATA_COLLECTION_SCHEDULING="false" 16 | DATA_COLLECTION_SCHEDULE_EXPRESSION="rate(6 hours)" 17 | DATA_COLLECTION_SCHEDULE_DESCRIPTION="Automated ARIA identity data collection every 6 hours" 18 | DATA_COLLECTION_SCHEDULE_TIMEZONE="UTC" 19 | 20 | # Graph Export Scheduling Configuration 21 | ENABLE_GRAPH_EXPORT_SCHEDULING="false" 22 | GRAPH_EXPORT_SCHEDULE_EXPRESSION="rate(1 day)" 23 | GRAPH_EXPORT_SCHEDULE_DESCRIPTION="Daily execution of ARIA graph export and import" 24 | GRAPH_EXPORT_SCHEDULE_TIMEZONE="UTC" 25 | 26 | # Colors for output 27 | RED='\033[0;31m' 28 | GREEN='\033[0;32m' 29 | YELLOW='\033[1;33m' 30 | NC='\033[0m' # No Color 31 | 32 | echo_info() { 33 | echo -e "${GREEN}[INFO]${NC} $1" 34 | } 35 | 36 | echo_warn() { 37 | echo -e "${YELLOW}[WARN]${NC} $1" 38 | } 39 | 40 | echo_error() { 41 | echo -e "${RED}[ERROR]${NC} $1" 42 | } 43 | 44 | # Function to check if S3 bucket exists 45 | check_bucket() { 46 | local bucket_name=$1 47 | if aws s3api head-bucket --bucket "$bucket_name" 2>/dev/null; then 48 | echo_info "S3 bucket $bucket_name exists" 49 | return 0 50 | else 51 | echo_error "S3 bucket $bucket_name does not exist" 52 | return 1 53 | fi 54 | } 55 | 56 | # Function to create S3 bucket if it doesn't exist 57 | create_bucket() { 58 | local bucket_name=$1 59 | echo_info "Creating S3 bucket: $bucket_name" 60 | 61 | if [ "$REGION" = "us-east-1" ]; then 62 | aws s3api create-bucket --bucket "$bucket_name" 63 | else 64 | aws s3api create-bucket --bucket "$bucket_name" --region "$REGION" \ 65 | --create-bucket-configuration LocationConstraint="$REGION" 66 | fi 67 | 68 | # Enable versioning 69 | aws s3api put-bucket-versioning --bucket "$bucket_name" \ 70 | --versioning-configuration Status=Enabled 71 | 72 | echo_info "S3 bucket $bucket_name created successfully" 73 | 74 | # Store the bucket name in SSM parameter store for future reference 75 | if [ "$bucket_name" = "$TEMPLATES_BUCKET" ]; then 76 | aws ssm put-parameter --name "aria-templates-bucket" --value "$bucket_name" --type "String" --overwrite --region "$REGION" > /dev/null 77 | echo_info "Templates bucket name stored in SSM parameter store" 78 | fi 79 | } 80 | 81 | # Function to upload templates to S3 82 | upload_templates() { 83 | echo_info "Uploading CloudFormation templates to S3..." 84 | 85 | # Upload all template files 86 | aws s3 cp templates/ s3://"$TEMPLATES_BUCKET"/ --recursive 87 | 88 | echo_info "Templates uploaded successfully" 89 | } 90 | 91 | # Function to validate CloudFormation template 92 | validate_template() { 93 | local template_file=$1 94 | echo_info "Validating template: $template_file" 95 | 96 | aws cloudformation validate-template --template-body file://"$template_file" > /dev/null 97 | 98 | echo_info "Template $template_file is valid" 99 | } 100 | 101 | # Function to validate scheduling parameters 102 | validate_scheduling_parameters() { 103 | # Validate boolean values 104 | if [[ "$ENABLE_DATA_COLLECTION_SCHEDULING" != "true" && "$ENABLE_DATA_COLLECTION_SCHEDULING" != "false" ]]; then 105 | echo_error "Invalid value for --enable-data-collection-scheduling: $ENABLE_DATA_COLLECTION_SCHEDULING (must be 'true' or 'false')" 106 | exit 1 107 | fi 108 | 109 | if [[ "$ENABLE_GRAPH_EXPORT_SCHEDULING" != "true" && "$ENABLE_GRAPH_EXPORT_SCHEDULING" != "false" ]]; then 110 | echo_error "Invalid value for --enable-graph-export-scheduling: $ENABLE_GRAPH_EXPORT_SCHEDULING (must be 'true' or 'false')" 111 | exit 1 112 | fi 113 | 114 | # Validate schedule expressions (basic validation) 115 | if [[ "$ENABLE_DATA_COLLECTION_SCHEDULING" == "true" ]]; then 116 | if [[ ! "$DATA_COLLECTION_SCHEDULE_EXPRESSION" =~ ^(rate\(|cron\() ]]; then 117 | echo_error "Invalid data collection schedule expression: $DATA_COLLECTION_SCHEDULE_EXPRESSION" 118 | echo_error "Must start with 'rate(' or 'cron('" 119 | exit 1 120 | fi 121 | fi 122 | 123 | if [[ "$ENABLE_GRAPH_EXPORT_SCHEDULING" == "true" ]]; then 124 | if [[ ! "$GRAPH_EXPORT_SCHEDULE_EXPRESSION" =~ ^(rate\(|cron\() ]]; then 125 | echo_error "Invalid graph export schedule expression: $GRAPH_EXPORT_SCHEDULE_EXPRESSION" 126 | echo_error "Must start with 'rate(' or 'cron('" 127 | exit 1 128 | fi 129 | fi 130 | 131 | echo_info "Scheduling parameters validated successfully" 132 | } 133 | 134 | # Function to deploy CloudFormation stack 135 | deploy_stack() { 136 | echo_info "Deploying CloudFormation stack: $STACK_NAME" 137 | 138 | # Check if stack exists 139 | if aws cloudformation describe-stacks --stack-name "$STACK_NAME" 2>/dev/null; then 140 | echo_info "Stack exists, updating..." 141 | OPERATION="update-stack" 142 | else 143 | echo_info "Stack does not exist, creating..." 144 | OPERATION="create-stack" 145 | fi 146 | 147 | # Deploy the stack 148 | aws cloudformation "$OPERATION" \ 149 | --stack-name "$STACK_NAME" \ 150 | --template-body file://templates/main-stack.yaml \ 151 | --parameters \ 152 | ParameterKey=TemplatesBucketName,ParameterValue="$TEMPLATES_BUCKET" \ 153 | ParameterKey=DeployNeptune,ParameterValue="$DEPLOY_NEPTUNE" \ 154 | ParameterKey=PublicIPAddress,ParameterValue="$PUBLIC_IP" \ 155 | ParameterKey=EnableDataCollectionScheduling,ParameterValue="$ENABLE_DATA_COLLECTION_SCHEDULING" \ 156 | ParameterKey=DataCollectionScheduleExpression,ParameterValue="$DATA_COLLECTION_SCHEDULE_EXPRESSION" \ 157 | ParameterKey=DataCollectionScheduleDescription,ParameterValue="$DATA_COLLECTION_SCHEDULE_DESCRIPTION" \ 158 | ParameterKey=DataCollectionScheduleTimezone,ParameterValue="$DATA_COLLECTION_SCHEDULE_TIMEZONE" \ 159 | ParameterKey=EnableScheduling,ParameterValue="$ENABLE_GRAPH_EXPORT_SCHEDULING" \ 160 | ParameterKey=ScheduleExpression,ParameterValue="$GRAPH_EXPORT_SCHEDULE_EXPRESSION" \ 161 | ParameterKey=ScheduleDescription,ParameterValue="$GRAPH_EXPORT_SCHEDULE_DESCRIPTION" \ 162 | ParameterKey=ScheduleTimezone,ParameterValue="$GRAPH_EXPORT_SCHEDULE_TIMEZONE" \ 163 | --capabilities CAPABILITY_IAM 164 | 165 | echo_info "Stack deployment initiated. Waiting for completion..." 166 | 167 | # Wait for stack operation to complete 168 | if [ "$OPERATION" = "create-stack" ]; then 169 | aws cloudformation wait stack-create-complete --stack-name "$STACK_NAME" 170 | else 171 | aws cloudformation wait stack-update-complete --stack-name "$STACK_NAME" 172 | fi 173 | 174 | echo_info "Stack deployment completed successfully" 175 | } 176 | 177 | # Function to show stack outputs 178 | show_outputs() { 179 | echo_info "Stack outputs:" 180 | aws cloudformation describe-stacks --stack-name "$STACK_NAME" \ 181 | --query 'Stacks[0].Outputs[*].[OutputKey,OutputValue]' --output table 182 | } 183 | 184 | # Function to set common scheduling presets 185 | set_scheduling_preset() { 186 | local preset=$1 187 | case $preset in 188 | "daily-collection-and-export") 189 | ENABLE_DATA_COLLECTION_SCHEDULING="true" 190 | DATA_COLLECTION_SCHEDULE_EXPRESSION="rate(1 day)" 191 | DATA_COLLECTION_SCHEDULE_DESCRIPTION="Daily ARIA identity data collection with graph export" 192 | ENABLE_GRAPH_EXPORT_SCHEDULING="false" 193 | echo_info "Applied preset: Daily ARIA identity data collection and graph export" 194 | ;; 195 | "frequent-collection-daily-export") 196 | ENABLE_DATA_COLLECTION_SCHEDULING="true" 197 | DATA_COLLECTION_SCHEDULE_EXPRESSION="rate(6 hours)" 198 | DATA_COLLECTION_SCHEDULE_DESCRIPTION="Frequent ARIA identity data collection every 6 hours" 199 | ENABLE_GRAPH_EXPORT_SCHEDULING="true" 200 | GRAPH_EXPORT_SCHEDULE_EXPRESSION="rate(1 day)" 201 | GRAPH_EXPORT_SCHEDULE_DESCRIPTION="Daily ARIA graph export and import" 202 | echo_info "Applied preset: Frequent data collection (6 hours) with daily graph export" 203 | ;; 204 | "business-hours") 205 | ENABLE_DATA_COLLECTION_SCHEDULING="true" 206 | DATA_COLLECTION_SCHEDULE_EXPRESSION="cron(0 9 ? * MON-FRI *)" 207 | DATA_COLLECTION_SCHEDULE_DESCRIPTION="Business hours ARIA identity data collection" 208 | DATA_COLLECTION_SCHEDULE_TIMEZONE="America/New_York" 209 | ENABLE_GRAPH_EXPORT_SCHEDULING="true" 210 | GRAPH_EXPORT_SCHEDULE_EXPRESSION="cron(0 18 ? * MON-FRI *)" 211 | GRAPH_EXPORT_SCHEDULE_DESCRIPTION="End of business day ARIA graph export" 212 | GRAPH_EXPORT_SCHEDULE_TIMEZONE="America/New_York" 213 | echo_info "Applied preset: Business hours scheduling (9 AM data collection, 6 PM graph export, EST)" 214 | ;; 215 | "disabled") 216 | ENABLE_DATA_COLLECTION_SCHEDULING="false" 217 | ENABLE_GRAPH_EXPORT_SCHEDULING="false" 218 | echo_info "Applied preset: All scheduling disabled" 219 | ;; 220 | *) 221 | echo_error "Unknown scheduling preset: $preset" 222 | echo_info "Available presets:" 223 | echo_info " daily-collection-and-export - Daily data collection and graph export" 224 | echo_info " frequent-collection-daily-export - 6-hour data collection, daily graph export" 225 | echo_info " business-hours - Business hours scheduling (EST)" 226 | echo_info " disabled - Disable all scheduling" 227 | exit 1 228 | ;; 229 | esac 230 | } 231 | 232 | # Function to show scheduling summary 233 | show_scheduling_summary() { 234 | echo_info "Scheduling Summary:" 235 | 236 | if [[ "$ENABLE_DATA_COLLECTION_SCHEDULING" == "true" ]]; then 237 | echo_info " ✅ Data Collection & Export Scheduling: ENABLED" 238 | echo_info " Expression: $DATA_COLLECTION_SCHEDULE_EXPRESSION" 239 | echo_info " Timezone: $DATA_COLLECTION_SCHEDULE_TIMEZONE" 240 | else 241 | echo_info " ❌ Data Collection Scheduling: DISABLED" 242 | fi 243 | 244 | if [[ "$ENABLE_GRAPH_EXPORT_SCHEDULING" == "true" ]]; then 245 | echo_info " ✅ Graph Export Independent Scheduling: ENABLED" 246 | echo_info " Expression: $GRAPH_EXPORT_SCHEDULE_EXPRESSION" 247 | echo_info " Timezone: $GRAPH_EXPORT_SCHEDULE_TIMEZONE" 248 | else 249 | echo_info " ❌ Graph Export Independent Scheduling: DISABLED" 250 | fi 251 | 252 | if [[ "$ENABLE_DATA_COLLECTION_SCHEDULING" == "false" && "$ENABLE_GRAPH_EXPORT_SCHEDULING" == "false" ]]; then 253 | echo_warn "Both scheduling options are disabled. You will need to manually execute state machines." 254 | fi 255 | 256 | echo "" 257 | } 258 | 259 | # Generate unique templates bucket name 260 | ACCOUNTID=$(aws sts get-caller-identity --output json | grep Account | awk -F ': "' '{print$2}' | sed 's/\".*//') 261 | ACCOUNTIDSHORT=$(echo "$ACCOUNTID" | cut -c 9-12) 262 | 263 | # Check SSM parameter store to see if the templates bucket name has already been generated 264 | TEMPLATES_BUCKET=$(aws ssm get-parameter --name "aria-templates-bucket" --region "$REGION" --query "Parameter.Value" --output text 2>/dev/null || echo "") 265 | 266 | # Check if the parameter store value exists, if not then set the variable (will be stored in SSM parameter store later) 267 | if [ -z "$TEMPLATES_BUCKET" ]; then 268 | echo_info "Templates Bucket variable not set...creating..." 269 | RANDOMSTRING="$(mktemp -u XXXXXXXX | tr 'A-Z' 'a-z')" 270 | TEMPLATES_BUCKET="aria-templates-$ACCOUNTIDSHORT-$RANDOMSTRING" 271 | fi 272 | 273 | 274 | # Main execution 275 | main() { 276 | echo_info "Starting deployment of nested CloudFormation stacks" 277 | echo_info "Stack Name: $STACK_NAME" 278 | echo_info "Templates Bucket: $TEMPLATES_BUCKET" 279 | echo_info "Region: $REGION" 280 | echo_info "Deploy Neptune: $DEPLOY_NEPTUNE" 281 | echo_info "Public IP: $PUBLIC_IP" 282 | echo "" 283 | echo_info "Data Collection and Export Scheduling Configuration:" 284 | echo_info " Enable Data Collection and Export Scheduling: $ENABLE_DATA_COLLECTION_SCHEDULING" 285 | echo_info " Data Collection Schedule Expression: $DATA_COLLECTION_SCHEDULE_EXPRESSION" 286 | echo_info " Data Collection Schedule Timezone: $DATA_COLLECTION_SCHEDULE_TIMEZONE" 287 | echo "" 288 | echo_info "Graph Export Independent Scheduling Configuration:" 289 | echo_info " Enable Graph Export Independent Scheduling: $ENABLE_GRAPH_EXPORT_SCHEDULING" 290 | echo_info " Graph Export Schedule Expression: $GRAPH_EXPORT_SCHEDULE_EXPRESSION" 291 | echo_info " Graph Export Schedule Timezone: $GRAPH_EXPORT_SCHEDULE_TIMEZONE" 292 | echo "" 293 | 294 | # Show scheduling summary 295 | show_scheduling_summary 296 | 297 | # Validate scheduling parameters 298 | validate_scheduling_parameters 299 | 300 | # Check if templates bucket exists, create if not 301 | if ! check_bucket "$TEMPLATES_BUCKET"; then 302 | create_bucket "$TEMPLATES_BUCKET" 303 | fi 304 | 305 | # Validate main template 306 | validate_template "templates/main-stack.yaml" 307 | 308 | # Upload templates to S3 309 | upload_templates 310 | 311 | # Deploy the stack 312 | deploy_stack 313 | 314 | # Show outputs 315 | show_outputs 316 | 317 | echo_info "Deployment completed successfully!" 318 | } 319 | 320 | # Parse command line arguments 321 | while [[ $# -gt 0 ]]; do 322 | case $1 in 323 | --stack-name) 324 | STACK_NAME="$2" 325 | shift 2 326 | ;; 327 | --templates-bucket) 328 | TEMPLATES_BUCKET="$2" 329 | shift 2 330 | ;; 331 | --region) 332 | REGION="$2" 333 | shift 2 334 | ;; 335 | --deploy-neptune) 336 | DEPLOY_NEPTUNE="$2" 337 | shift 2 338 | ;; 339 | --public-ip) 340 | PUBLIC_IP="$2" 341 | shift 2 342 | ;; 343 | --enable-data-collection-scheduling) 344 | ENABLE_DATA_COLLECTION_SCHEDULING="$2" 345 | shift 2 346 | ;; 347 | --data-collection-schedule-expression) 348 | DATA_COLLECTION_SCHEDULE_EXPRESSION="$2" 349 | shift 2 350 | ;; 351 | --data-collection-schedule-description) 352 | DATA_COLLECTION_SCHEDULE_DESCRIPTION="$2" 353 | shift 2 354 | ;; 355 | --data-collection-schedule-timezone) 356 | DATA_COLLECTION_SCHEDULE_TIMEZONE="$2" 357 | shift 2 358 | ;; 359 | --enable-graph-export-scheduling) 360 | ENABLE_GRAPH_EXPORT_SCHEDULING="$2" 361 | shift 2 362 | ;; 363 | --graph-export-schedule-expression) 364 | GRAPH_EXPORT_SCHEDULE_EXPRESSION="$2" 365 | shift 2 366 | ;; 367 | --graph-export-schedule-description) 368 | GRAPH_EXPORT_SCHEDULE_DESCRIPTION="$2" 369 | shift 2 370 | ;; 371 | --graph-export-schedule-timezone) 372 | GRAPH_EXPORT_SCHEDULE_TIMEZONE="$2" 373 | shift 2 374 | ;; 375 | --scheduling-preset) 376 | set_scheduling_preset "$2" 377 | shift 2 378 | ;; 379 | --help) 380 | echo "Usage: $0 [OPTIONS]" 381 | echo "" 382 | echo "Basic Options:" 383 | echo " --stack-name STACK_NAME CloudFormation stack name (default: aria-gv-setup)" 384 | echo " --templates-bucket BUCKET_NAME S3 bucket for templates (default: auto-generated unique name)" 385 | echo " Note: Bucket names are automatically generated per account and stored in SSM" 386 | echo " --region REGION AWS region (default: us-east-1)" 387 | echo " --deploy-neptune true|false Deploy Neptune Analytics and Notebook (default: true)" 388 | echo " --public-ip CIDR Public IP address in CIDR format (default: 0.0.0.0/0)" 389 | echo "" 390 | echo "Data Collection Scheduling Options:" 391 | echo " --enable-data-collection-scheduling true|false" 392 | echo " Enable automatic data collection scheduling (default: false)" 393 | echo " --data-collection-schedule-expression EXPRESSION" 394 | echo " Schedule expression for data collection (default: rate(6 hours))" 395 | echo " --data-collection-schedule-description DESCRIPTION" 396 | echo " Description for data collection schedule" 397 | echo " --data-collection-schedule-timezone TIMEZONE" 398 | echo " Timezone for data collection schedule (default: UTC)" 399 | echo "" 400 | echo "Graph Export Scheduling Options:" 401 | echo " --enable-graph-export-scheduling true|false" 402 | echo " Enable automatic graph export scheduling (default: false)" 403 | echo " --graph-export-schedule-expression EXPRESSION" 404 | echo " Schedule expression for graph export (default: rate(1 day))" 405 | echo " --graph-export-schedule-description DESCRIPTION" 406 | echo " Description for graph export schedule" 407 | echo " --graph-export-schedule-timezone TIMEZONE" 408 | echo " Timezone for graph export schedule (default: UTC)" 409 | echo "" 410 | echo "Scheduling Presets:" 411 | echo " --scheduling-preset PRESET Apply a common scheduling configuration" 412 | echo " Available presets:" 413 | echo " daily-collection-and-export" 414 | echo " frequent-collection-daily-export" 415 | echo " business-hours" 416 | echo " disabled" 417 | echo "" 418 | echo "Other Options:" 419 | echo " --help Show this help message" 420 | echo "" 421 | echo "Examples:" 422 | echo " # Deploy with daily scheduling preset" 423 | echo " $0 --scheduling-preset daily-collection-and-export" 424 | echo "" 425 | echo " # Deploy with business hours preset" 426 | echo " $0 --scheduling-preset business-hours" 427 | echo "" 428 | echo " # Deploy with custom data collection scheduling" 429 | echo " $0 --enable-data-collection-scheduling true \\" 430 | echo " --data-collection-schedule-expression 'rate(4 hours)' \\" 431 | echo " --enable-graph-export-scheduling false" 432 | echo "" 433 | echo " # Deploy with custom business hours scheduling" 434 | echo " $0 --enable-data-collection-scheduling true \\" 435 | echo " --data-collection-schedule-expression 'cron(0 9 ? * MON-FRI *)' \\" 436 | echo " --data-collection-schedule-timezone 'America/New_York' \\" 437 | echo " --enable-graph-export-scheduling true \\" 438 | echo " --graph-export-schedule-expression 'cron(0 18 ? * MON-FRI *)' \\" 439 | echo " --graph-export-schedule-timezone 'America/New_York'" 440 | exit 0 441 | ;; 442 | *) 443 | echo_error "Unknown option: $1" 444 | echo "Use --help for usage information" 445 | exit 1 446 | ;; 447 | esac 448 | done 449 | 450 | # Run main function 451 | main -------------------------------------------------------------------------------- /templates/neptune-analytics.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "CloudFormation template to deploy Neptune graph and notebook instance" 3 | 4 | Parameters: 5 | AriaSetupStackName: 6 | Type: String 7 | Description: "Name of the main Aria setup stack" 8 | 9 | S3ExportBucketName: 10 | Type: String 11 | Description: Name of the S3 bucket to export all IdC csv data to 12 | 13 | AriaStateMachineArn: 14 | Type: String 15 | Description: "ARN of the AriaStateMachine from the Step Functions stack" 16 | 17 | GraphName: 18 | Type: String 19 | Description: "Name of the Neptune graph" 20 | Default: "aria-identitycenter" 21 | 22 | PublicIPAddress: 23 | Type: String 24 | Description: Provide your public IP address in CIDR format e.g. 1.2.3.4/32 25 | 26 | # Scheduling Parameters for AriaExportGraphStateMachine 27 | EnableGraphExportScheduling: 28 | Type: String 29 | Description: Enable automatic scheduling of the AriaExportGraphStateMachine 30 | 31 | GraphExportScheduleExpression: 32 | Type: String 33 | Description: "Schedule expression for automatic graph export (e.g., rate(1 day), cron(0 2 * * ? *))" 34 | 35 | GraphExportScheduleDescription: 36 | Type: String 37 | Description: "Description for the scheduled graph export execution" 38 | 39 | GraphExportScheduleTimezone: 40 | Type: String 41 | Description: "Timezone for cron-based schedules (e.g., America/New_York, UTC)" 42 | 43 | Conditions: 44 | ShouldEnableGraphExportScheduling: 45 | !Equals [!Ref EnableGraphExportScheduling, "true"] 46 | 47 | Resources: 48 | # VPC Configuration 49 | NeptuneVPC: 50 | Type: AWS::EC2::VPC 51 | Metadata: 52 | cfn_nag: 53 | rules_to_suppress: 54 | - id: W60 55 | reason: Not required for this sample deployment but strongly recommended for production implementation 56 | Properties: 57 | CidrBlock: 172.32.0.0/16 58 | EnableDnsHostnames: true 59 | EnableDnsSupport: true 60 | Tags: 61 | - Key: aria 62 | Value: vpc 63 | 64 | # Subnets 65 | NeptunePrivateSubnet1: 66 | Type: AWS::EC2::Subnet 67 | Properties: 68 | VpcId: !Ref NeptuneVPC 69 | CidrBlock: 172.32.1.0/24 70 | AvailabilityZone: !Select [0, !GetAZs ""] 71 | Tags: 72 | - Key: aria 73 | Value: private-subnet-1 74 | 75 | NeptunePrivateSubnet2: 76 | Type: AWS::EC2::Subnet 77 | Properties: 78 | VpcId: !Ref NeptuneVPC 79 | CidrBlock: 172.32.2.0/24 80 | AvailabilityZone: !Select [1, !GetAZs ""] 81 | Tags: 82 | - Key: aria 83 | Value: private-subnet-2 84 | 85 | # Security Group 86 | NeptuneAnalyticsSecurityGroup: 87 | Type: AWS::EC2::SecurityGroup 88 | Metadata: 89 | cfn_nag: 90 | rules_to_suppress: 91 | - id: F1000 92 | reason: Permitting egress traffic is a requirement of Neptune Analytics 93 | - id: W2 94 | reason: This is required for Neptune to function as per documentation 95 | - id: W60 96 | reason: This is an example solution so not required however in a production implementation vpc flow logs should be enabled 97 | - id: W9 98 | reason: This is required for Neptune to function as per documentation 99 | Properties: 100 | GroupDescription: Security group for Neptune Analytics VPC endpoint 101 | VpcId: !Ref NeptuneVPC 102 | SecurityGroupIngress: 103 | - Description: Allow ingress from external IP address to Neptune Analytics 104 | IpProtocol: tcp 105 | FromPort: 443 106 | ToPort: 443 107 | CidrIp: !Sub ${PublicIPAddress} 108 | - Description: Allow ability to pull libraries 109 | IpProtocol: tcp 110 | FromPort: 8182 111 | ToPort: 8182 112 | CidrIp: 0.0.0.0/0 113 | Tags: 114 | - Key: aria 115 | Value: neptune-analytics-security-group 116 | 117 | # VPC Endpoints - Neptune Control Plane 118 | NeptuneAnalyticsVPCEndpoint: 119 | Type: AWS::EC2::VPCEndpoint 120 | Properties: 121 | ServiceName: !Sub com.amazonaws.${AWS::Region}.neptune-graph 122 | VpcId: !Ref NeptuneVPC 123 | VpcEndpointType: Interface 124 | SubnetIds: 125 | - !Ref NeptunePrivateSubnet1 126 | - !Ref NeptunePrivateSubnet2 127 | SecurityGroupIds: 128 | - !Ref NeptuneAnalyticsSecurityGroup 129 | PrivateDnsEnabled: true 130 | Tags: 131 | - Key: aria 132 | Value: neptune-analytics-vpc-graph-endpoint 133 | 134 | # VPC Endpoints - Neptune Data Plane 135 | NeptuneAnalyticsVPCDataEndpoint: 136 | Type: AWS::EC2::VPCEndpoint 137 | Properties: 138 | ServiceName: !Sub com.amazonaws.${AWS::Region}.neptune-graph-data 139 | VpcId: !Ref NeptuneVPC 140 | VpcEndpointType: Interface 141 | SubnetIds: 142 | - !Ref NeptunePrivateSubnet1 143 | - !Ref NeptunePrivateSubnet2 144 | SecurityGroupIds: 145 | - !Ref NeptuneAnalyticsSecurityGroup 146 | PrivateDnsEnabled: true 147 | Tags: 148 | - Key: aria 149 | Value: neptune-analytics-vpc-graph-data-endpoint 150 | PolicyDocument: 151 | Version: "2012-10-17" 152 | Statement: 153 | - Effect: Allow 154 | Principal: "*" 155 | Action: "neptune-graph:*" 156 | Resource: !Sub "arn:${AWS::Partition}:neptune-graph:${AWS::Region}:${AWS::AccountId}:graph/${CreateNeptuneAnalytics}" 157 | 158 | CreateNeptuneLoadRole: #Role to load data into Neptune analytics 159 | Type: "AWS::IAM::Role" 160 | Properties: 161 | AssumeRolePolicyDocument: 162 | Version: "2012-10-17" 163 | Statement: 164 | - Effect: Allow 165 | Principal: 166 | Service: 167 | - neptune-graph.amazonaws.com 168 | Action: 169 | - "sts:AssumeRole" 170 | Description: Role to load data into Neptune analytics 171 | ManagedPolicyArns: 172 | - arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess 173 | Tags: 174 | - Key: aria 175 | Value: role 176 | 177 | CreateNeptuneAnalytics: #Create Neptune Analytics Graph 178 | Type: "AWS::NeptuneGraph::Graph" 179 | DeletionPolicy: Delete 180 | UpdateReplacePolicy: Delete 181 | Properties: 182 | DeletionProtection: False 183 | ProvisionedMemory: 16 184 | PublicConnectivity: false 185 | ReplicaCount: 0 186 | Tags: 187 | - Key: "Name" 188 | Value: !Ref GraphName 189 | - Key: aria 190 | Value: analyticsgraph 191 | 192 | # Private Graph Endpoint 193 | PrivateGraphEndpoint: 194 | Type: AWS::NeptuneGraph::PrivateGraphEndpoint 195 | DeletionPolicy: Delete 196 | UpdateReplacePolicy: Delete 197 | Properties: 198 | GraphIdentifier: !Ref CreateNeptuneAnalytics 199 | VpcId: !Ref NeptuneVPC 200 | SubnetIds: 201 | - !Ref NeptunePrivateSubnet1 202 | - !Ref NeptunePrivateSubnet2 203 | SecurityGroupIds: 204 | - !Ref NeptuneAnalyticsSecurityGroup 205 | 206 | # AriaExportGraphStateMachine and associated resources 207 | AriaExportGraphStateMachine: 208 | Type: "AWS::StepFunctions::StateMachine" 209 | DeletionPolicy: Delete 210 | UpdateReplacePolicy: Delete 211 | DependsOn: 212 | - AriaExportStateMachineRolePolicy 213 | Properties: 214 | Definition: 215 | Comment: This state machine will export DynamoDB data to CSV in graph format and import data into a Neptune graph 216 | StartAt: S3 Export Lambda Function 217 | States: 218 | S3 Export Lambda Function: 219 | Type: Task 220 | Resource: arn:aws:states:::lambda:invoke 221 | Output: "{% $states.result.Payload %}" 222 | Arguments: 223 | FunctionName: !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${AriaSetupStackName}-S3Export-function" 224 | Payload: { "s3bucket": !Ref S3ExportBucketName } 225 | Retry: 226 | - ErrorEquals: 227 | - Lambda.ServiceException 228 | - Lambda.AWSLambdaException 229 | - Lambda.SdkClientException 230 | - Lambda.TooManyRequestsException 231 | IntervalSeconds: 1 232 | MaxAttempts: 3 233 | BackoffRate: 2 234 | JitterStrategy: FULL 235 | Next: Reset Neptune Graph 236 | Reset Neptune Graph: 237 | Type: Task 238 | Arguments: 239 | GraphIdentifier: !GetAtt CreateNeptuneAnalytics.GraphId 240 | SkipSnapshot: "true" 241 | Resource: arn:aws:states:::aws-sdk:neptunegraph:resetGraph 242 | Next: Wait 243 | Wait: 244 | Type: Wait 245 | Seconds: 120 246 | Next: Start Neptune Import Task 247 | Start Neptune Import Task: 248 | Type: Task 249 | Resource: arn:aws:states:::aws-sdk:neptunegraph:startImportTask 250 | Arguments: 251 | GraphIdentifier: !GetAtt CreateNeptuneAnalytics.GraphId 252 | RoleArn: !GetAtt CreateNeptuneLoadRole.Arn 253 | Source: !Sub "s3://${S3ExportBucketName}/" 254 | Format: CSV 255 | Next: Wait2 256 | Wait2: 257 | Type: Wait 258 | Seconds: 180 259 | End: true 260 | QueryLanguage: JSONata 261 | RoleArn: !GetAtt AriaExportStateMachineRole.Arn 262 | StateMachineName: AriaExportGraphStateMachine 263 | StateMachineType: STANDARD 264 | EncryptionConfiguration: 265 | Type: AWS_OWNED_KEY 266 | Tags: 267 | - Key: aria 268 | Value: state 269 | 270 | AriaExportStateMachineRole: 271 | Type: AWS::IAM::Role 272 | Properties: 273 | AssumeRolePolicyDocument: 274 | Version: "2012-10-17" 275 | Statement: 276 | - Effect: Allow 277 | Principal: 278 | Service: states.amazonaws.com 279 | Action: sts:AssumeRole 280 | MaxSessionDuration: 3600 281 | Tags: 282 | - Key: aria 283 | Value: role 284 | 285 | AriaExportStateMachineRolePolicy: 286 | Type: AWS::IAM::RolePolicy 287 | Properties: 288 | PolicyName: StateMachineNeptuneScopedAccessPolicy 289 | RoleName: !Ref AriaExportStateMachineRole 290 | PolicyDocument: 291 | Version: "2012-10-17" 292 | Statement: 293 | - Effect: Allow 294 | Action: 295 | - neptune-graph:ResetGraph 296 | - neptune-graph:StartImportTask 297 | - neptune-graph:CreateGraphSnapshot 298 | Resource: "*" 299 | - Effect: Allow 300 | Action: 301 | - lambda:InvokeFunction 302 | Resource: !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${AriaSetupStackName}-S3Export-function" 303 | - Effect: Allow 304 | Action: 305 | - iam:PassRole 306 | Resource: !GetAtt CreateNeptuneLoadRole.Arn 307 | - Effect: Allow 308 | Action: 309 | - s3:ListBucket 310 | - s3:GetObject 311 | - s3:PutObject 312 | Resource: 313 | - !Sub "arn:aws:s3:::${S3ExportBucketName}/*" 314 | - !Sub "arn:aws:s3:::${S3ExportBucketName}" 315 | 316 | # EventBridge Rule to trigger AriaExportGraphStateMachine after AriaStateMachine completes 317 | AriaExportGraphTriggerRole: 318 | Type: AWS::IAM::Role 319 | Condition: ShouldEnableGraphExportScheduling 320 | DeletionPolicy: Delete 321 | UpdateReplacePolicy: Delete 322 | Properties: 323 | AssumeRolePolicyDocument: 324 | Version: "2012-10-17" 325 | Statement: 326 | - Effect: Allow 327 | Principal: 328 | Service: events.amazonaws.com 329 | Action: sts:AssumeRole 330 | Policies: 331 | - PolicyName: AriaExportGraphTriggerPolicy 332 | PolicyDocument: 333 | Version: "2012-10-17" 334 | Statement: 335 | - Effect: Allow 336 | Action: 337 | - states:StartExecution 338 | Resource: !GetAtt AriaExportGraphStateMachine.Arn 339 | Tags: 340 | - Key: aria 341 | Value: eventbridge-role 342 | 343 | AriaExportGraphTriggerRule: 344 | Type: AWS::Events::Rule 345 | Condition: ShouldEnableGraphExportScheduling 346 | DeletionPolicy: Delete 347 | UpdateReplacePolicy: Delete 348 | Properties: 349 | Name: !Sub "${AWS::StackName}-GraphTrigger" 350 | Description: !Sub "Trigger ${AriaSetupStackName} graph export after data collection completes" 351 | State: ENABLED 352 | EventPattern: 353 | source: 354 | - aws.states 355 | detail-type: 356 | - Step Functions Execution Status Change 357 | detail: 358 | status: 359 | - SUCCEEDED 360 | stateMachineArn: 361 | - !Ref AriaStateMachineArn 362 | Targets: 363 | - Id: AriaExportGraphTarget 364 | Arn: !GetAtt AriaExportGraphStateMachine.Arn 365 | RoleArn: !GetAtt AriaExportGraphTriggerRole.Arn 366 | RetryPolicy: 367 | MaximumRetryAttempts: 2 368 | DeadLetterConfig: 369 | Arn: !GetAtt AriaExportGraphTriggerDLQ.Arn 370 | 371 | # Optional time-based schedule for independent graph export execution 372 | AriaExportGraphScheduleRole: 373 | Type: AWS::IAM::Role 374 | Condition: ShouldEnableGraphExportScheduling 375 | DeletionPolicy: Delete 376 | UpdateReplacePolicy: Delete 377 | Properties: 378 | AssumeRolePolicyDocument: 379 | Version: "2012-10-17" 380 | Statement: 381 | - Effect: Allow 382 | Principal: 383 | Service: scheduler.amazonaws.com 384 | Action: sts:AssumeRole 385 | Policies: 386 | - PolicyName: AriaExportGraphSchedulePolicy 387 | PolicyDocument: 388 | Version: "2012-10-17" 389 | Statement: 390 | - Effect: Allow 391 | Action: 392 | - states:StartExecution 393 | Resource: !GetAtt AriaExportGraphStateMachine.Arn 394 | Tags: 395 | - Key: aria 396 | Value: scheduler-role 397 | 398 | AriaExportGraphSchedule: 399 | Type: AWS::Scheduler::Schedule 400 | Condition: ShouldEnableGraphExportScheduling 401 | DeletionPolicy: Delete 402 | UpdateReplacePolicy: Delete 403 | Properties: 404 | Name: !Sub "${AWS::StackName}-Export" 405 | Description: !Sub "${GraphExportScheduleDescription} (Independent schedule - also triggers after data collection)" 406 | State: ENABLED 407 | FlexibleTimeWindow: 408 | Mode: "OFF" 409 | ScheduleExpression: !Ref GraphExportScheduleExpression 410 | ScheduleExpressionTimezone: !Ref GraphExportScheduleTimezone 411 | Target: 412 | Arn: !GetAtt AriaExportGraphStateMachine.Arn 413 | RoleArn: !GetAtt AriaExportGraphScheduleRole.Arn 414 | RetryPolicy: 415 | MaximumRetryAttempts: 2 416 | DeadLetterConfig: 417 | Arn: !GetAtt AriaExportGraphScheduleDLQ.Arn 418 | 419 | # Dead Letter Queue for failed EventBridge triggered executions 420 | AriaExportGraphTriggerDLQ: 421 | Type: AWS::SQS::Queue 422 | Condition: ShouldEnableGraphExportScheduling 423 | DeletionPolicy: Delete 424 | UpdateReplacePolicy: Delete 425 | Metadata: 426 | cfn_nag: 427 | rules_to_suppress: 428 | - id: W48 429 | reason: "Default SQS encryption is sufficient for this dead letter queue" 430 | checkov: 431 | skip: 432 | - id: CKV_AWS_27 433 | comment: "Default SQS encryption is sufficient for this dead letter queue" 434 | Properties: 435 | QueueName: !Sub "${AWS::StackName}-Trigger-DLQ" 436 | MessageRetentionPeriod: 1209600 # 14 days 437 | Tags: 438 | - Key: aria 439 | Value: dlq 440 | 441 | # Dead Letter Queue for failed scheduled executions 442 | AriaExportGraphScheduleDLQ: 443 | Type: AWS::SQS::Queue 444 | Condition: ShouldEnableGraphExportScheduling 445 | DeletionPolicy: Delete 446 | UpdateReplacePolicy: Delete 447 | Metadata: 448 | cfn_nag: 449 | rules_to_suppress: 450 | - id: W48 451 | reason: "Default SQS encryption is sufficient for this dead letter queue" 452 | checkov: 453 | skip: 454 | - id: CKV_AWS_27 455 | comment: "Default SQS encryption is sufficient for this dead letter queue" 456 | Properties: 457 | QueueName: !Sub "${AWS::StackName}-Export-DLQ" 458 | MessageRetentionPeriod: 1209600 # 14 days 459 | Tags: 460 | - Key: aria 461 | Value: dlq 462 | 463 | # CloudWatch Log Group for EventBridge rule monitoring 464 | AriaExportGraphTriggerLogGroup: 465 | Type: AWS::Logs::LogGroup 466 | Condition: ShouldEnableGraphExportScheduling 467 | DeletionPolicy: Delete 468 | UpdateReplacePolicy: Delete 469 | Metadata: 470 | cfn_nag: 471 | rules_to_suppress: 472 | - id: W84 473 | reason: "Default CloudWatch Logs encryption is sufficient for this log group" 474 | checkov: 475 | skip: 476 | - id: CKV_AWS_158 477 | comment: "Default CloudWatch Logs encryption is sufficient for this log group" 478 | Properties: 479 | LogGroupName: !Sub "/aws/events/rule/${AWS::StackName}-GraphTrigger" 480 | RetentionInDays: 30 481 | Tags: 482 | - Key: aria 483 | Value: log 484 | 485 | # CloudWatch Log Group for scheduler monitoring 486 | AriaExportGraphScheduleLogGroup: 487 | Type: AWS::Logs::LogGroup 488 | Condition: ShouldEnableGraphExportScheduling 489 | DeletionPolicy: Delete 490 | UpdateReplacePolicy: Delete 491 | Metadata: 492 | cfn_nag: 493 | rules_to_suppress: 494 | - id: W84 495 | reason: "Default CloudWatch Logs encryption is sufficient for this log group" 496 | checkov: 497 | skip: 498 | - id: CKV_AWS_158 499 | comment: "Default CloudWatch Logs encryption is sufficient for this log group" 500 | Properties: 501 | LogGroupName: !Sub "/aws/scheduler/${AWS::StackName}-Export" 502 | RetentionInDays: 30 503 | Tags: 504 | - Key: aria 505 | Value: log 506 | 507 | Outputs: 508 | NeptuneGraphEndpoint: 509 | Value: !GetAtt CreateNeptuneAnalytics.Endpoint 510 | Description: "Neptune Graph Endpoint" 511 | NeptuneGraphId: 512 | Value: !GetAtt CreateNeptuneAnalytics.GraphId 513 | Description: "Neptune Graph ID" 514 | NeptuneGraphName: 515 | Value: !Sub "${GraphName}-${AWS::StackName}" 516 | Description: "Neptune Graph Name" 517 | NeptuneGraphArn: 518 | Value: !GetAtt CreateNeptuneAnalytics.GraphArn 519 | CreateNeptuneLoadRole: 520 | Value: !GetAtt CreateNeptuneLoadRole.Arn 521 | NeptuneVPC: 522 | Value: !GetAtt NeptuneVPC.VpcId 523 | Export: 524 | Name: !Sub "${AWS::StackName}-VPC" 525 | NeptunePrivateSubnet1: 526 | Value: !GetAtt NeptunePrivateSubnet1.SubnetId 527 | Export: 528 | Name: !Sub "${AWS::StackName}-PrivateSubnet1" 529 | NeptunePrivateSubnet2: 530 | Value: !GetAtt NeptunePrivateSubnet2.SubnetId 531 | Export: 532 | Name: !Sub "${AWS::StackName}-PrivateSubnet2" 533 | NeptuneAnalyticsSecurityGroup: 534 | Value: !GetAtt NeptuneAnalyticsSecurityGroup.GroupId 535 | Export: 536 | Name: !Sub "${AWS::StackName}-NeptuneSG" 537 | NeptuneAnalyticsVPCEndpoint: 538 | Value: !GetAtt NeptuneAnalyticsVPCEndpoint.Id 539 | Export: 540 | Name: !Sub "${AWS::StackName}-NeptuneVPCEndpoint" 541 | NeptuneAnalyticsVPCDataEndpoint: 542 | Value: !GetAtt NeptuneAnalyticsVPCDataEndpoint.Id 543 | Export: 544 | Name: !Sub "${AWS::StackName}-NeptuneVPCDataEndpoint" 545 | 546 | # AriaExportGraphStateMachine Outputs 547 | AriaExportGraphStateMachineArn: 548 | Description: "ARN of the AriaExportGraphStateMachine" 549 | Value: !GetAtt AriaExportGraphStateMachine.Arn 550 | Export: 551 | Name: !Sub "${AWS::StackName}-ExportStateMachineArn" 552 | 553 | GraphExportSchedulingEnabled: 554 | Description: "Whether scheduling is enabled for the AriaExportGraphStateMachine" 555 | Value: !Ref EnableGraphExportScheduling 556 | Export: 557 | Name: !Sub "${AWS::StackName}-ExportSchedulingEnabled" 558 | 559 | GraphExportScheduleExpression: 560 | Condition: ShouldEnableGraphExportScheduling 561 | Description: "Schedule expression for the AriaExportGraphStateMachine" 562 | Value: !Ref GraphExportScheduleExpression 563 | Export: 564 | Name: !Sub "${AWS::StackName}-ExportScheduleExpression" 565 | 566 | GraphExportScheduleName: 567 | Condition: ShouldEnableGraphExportScheduling 568 | Description: "Name of the EventBridge Schedule (independent execution)" 569 | Value: !Ref AriaExportGraphSchedule 570 | Export: 571 | Name: !Sub "${AWS::StackName}-ExportScheduleName" 572 | 573 | GraphExportTriggerRuleName: 574 | Condition: ShouldEnableGraphExportScheduling 575 | Description: "Name of the EventBridge Rule that triggers graph export after data collection" 576 | Value: !Ref AriaExportGraphTriggerRule 577 | Export: 578 | Name: !Sub "${AWS::StackName}-ExportTriggerRuleName" 579 | --------------------------------------------------------------------------------