├── docs └── images │ ├── NAAArchitectureOption01.png │ └── NAAArchitectureOption02.png ├── CODE_OF_CONDUCT.md ├── LICENSE ├── naa-execrole.yaml ├── README.md ├── naa-processfindings.py ├── naa-script.sh └── naa-resources.yaml /docs/images/NAAArchitectureOption01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/network-access-analyzer-multi-account-analysis/HEAD/docs/images/NAAArchitectureOption01.png -------------------------------------------------------------------------------- /docs/images/NAAArchitectureOption02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/network-access-analyzer-multi-account-analysis/HEAD/docs/images/NAAArchitectureOption02.png -------------------------------------------------------------------------------- /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 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /naa-execrole.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: > 3 | This template creates an AWS IAM Role which can be assumed by the NAAEC2Role 4 | 5 | Parameters: 6 | AuthorizedARN: 7 | Description: "ARN of IAM Admin Role which is authorized to assume the NAAExecRole role. (e.g. arn:${AWS::Partition}:iam::*:role/NAAEC2Role)" 8 | Type: String 9 | 10 | NAAExecRoleName: 11 | Description: "Name of the IAM role that will have these policies attached." 12 | Type: String 13 | Default: "NAAExecRole" 14 | 15 | Resources: 16 | NAAExecRole: 17 | Type: AWS::IAM::Role 18 | Metadata: 19 | cfn_nag: 20 | rules_to_suppress: 21 | - id: W11 22 | reason: "The resource must remain as * in order to process all resources in the account" 23 | - id: W28 24 | reason: "The IAM Role name is specified as an explicit for use within the scripting" 25 | Properties: 26 | AssumeRolePolicyDocument: 27 | Version: "2012-10-17" 28 | Statement: 29 | - Effect: Allow 30 | Principal: 31 | AWS: !Sub ${AuthorizedARN} 32 | Action: "sts:AssumeRole" 33 | MaxSessionDuration: 43200 34 | RoleName: !Sub ${NAAExecRoleName} 35 | Policies: 36 | - PolicyName: NAAExecRolePrivileges 37 | PolicyDocument: 38 | Version : "2012-10-17" 39 | Statement: 40 | - Effect: Allow 41 | Sid: ActionsPermittedForScriptAccountListGeneration 42 | Action: 43 | - "organizations:ListAccounts" 44 | Resource: "*" 45 | - Effect: Allow 46 | Sid: ActionsPermittedForCustomScriptCommands 47 | Action: 48 | - "s3:ListBucket" 49 | - "s3:ListAllMyBuckets" 50 | - "s3:GetEncryptionConfiguration" 51 | - "cloudformation:DescribeStacks" 52 | - "cloudformation:ListStackResources" 53 | - "ec2:CreateTags" 54 | - "ec2:DeleteTags" 55 | - "ec2:CreateNetworkInsightsAccessScope" 56 | - "ec2:DeleteNetworkInsightsAccessScopeAnalysis" 57 | - "ec2:DeleteNetworkInsightsAccessScope" 58 | - "ec2:Describe*" 59 | - "ec2:Get*" 60 | - "ec2:SearchTransitGatewayRoutes" 61 | - "ec2:StartNetworkInsightsAccessScopeAnalysis" 62 | - "elasticloadbalancing:Describe*" 63 | - "resource-groups:ListGroupResources" 64 | - "tag:GetResources" 65 | - "tiros:CreateQuery" 66 | - "tiros:GetQueryAnswer" 67 | - "network-firewall:Describe*" 68 | - "network-firewall:List*" 69 | - "directconnect:DescribeVirtualInterfaces" 70 | - "directconnect:DescribeDirectConnectGateways" 71 | Resource: "*" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **Network Access Analyzer Multi-Account Analysis** 2 | 3 | ## Summary 4 | 5 | Unintentional inbound internet access to AWS resources can pose risks to an organization’s data perimeter. [Network Access Analyzer](https://docs.aws.amazon.com/vpc/latest/network-access-analyzer/what-is-network-access-analyzer.html) is an Amazon Virtual Private Cloud (Amazon VPC) feature that helps you identify unintended network access to your resources on Amazon Web Services (AWS). You can use Network Access Analyzer to specify your network access requirements and to identify potential network paths that do not meet your specified requirements. You can use Network Access Analyzer to do the following: 6 | 7 | 1. Identify AWS resources that are accessible to the internet through internet gateways. 8 | 9 | 2. Validate that your virtual private clouds (VPCs) are appropriately segmented, such as isolating production and development environments and separating transactional workloads. 10 | 11 | Network Access Analyzer analyzes end-to-end network reachability conditions and not just a single component. To determine whether a resource is internet accessible, Network Access Analyzer evaluates the internet gateway, VPC route tables, network access control lists (ACLs), public IP addresses on elastic network interfaces, and security groups. If any of these components prevent internet access, Network Access Analyzer doesn’t generate a finding. For example, if an Amazon Elastic Compute Cloud (Amazon EC2) instance has an open security group that allows traffic from 0/0 but the instance is in a private subnet that isn’t routable from any internet gateway, then Network Access Analyzer wouldn’t generate a finding. This provides high-fidelity results so that you can identify resources that are truly accessible from the internet. 12 | 13 | When you run Network Access Analyzer, you use Network Access Scopes to specify your network access requirements. This solution identifies network paths between an internet gateway and an elastic network interface. In this pattern, you deploy the solution in a centralized AWS account in your organization, managed by AWS Organizations, and it analyzes all of the accounts, in any AWS Region, in the organization. 14 | 15 | This solution was designed with the following in mind: 16 | 17 | - The AWS CloudFormation templates reduce the effort required to deploy the AWS resources in this pattern. 18 | - You can adjust the parameters in the CloudFormation templates and naa-script.sh script at the time of deployment to customize them for your environment. 19 | - Bash scripting automatically provisions and analyzes the Network Access Scopes for multiple accounts, in parallel. 20 | - A Python script processes the findings, extracts the data, and then consolidates the results. You can choose to review the consolidated report of Network Access Analyzer findings in CSV format or in AWS Security Hub. An example of the CSV report is available in the Additional information section of this pattern. 21 | - You can remediate findings, or you can exclude them from future analyses by adding them to the naa-exclusions.csv file. 22 | 23 | ## **Deployment steps and supplemental information provided via AWS Prescriptive Guidance (APG)** 24 | 25 | [https://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/create-a-report-of-network-access-analyzer-findings-for-inbound-internet-access-in-multiple-aws-accounts.html](https://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/create-a-report-of-network-access-analyzer-findings-for-inbound-internet-access-in-multiple-aws-accounts.html) 26 | 27 | The code in this repository helps you set up the following target architectures. You can configure one option or both. 28 | ​ 29 | CSV Finding Output: 30 | ![TargetArchitectureDiagramOption1](docs/images/NAAArchitectureOption01.png) 31 | 32 | Security Hub Finding Import: 33 | ![TargetArchitectureDiagramOption2](docs/images/NAAArchitectureOption02.png) 34 | -------------------------------------------------------------------------------- /naa-processfindings.py: -------------------------------------------------------------------------------- 1 | # This sample, non-production-ready python script process network analyzer results of a specific scope and outputs an csv for further processing and analysis. 2 | # © 2023 Amazon Web Services, Inc. or its affiliates. All Rights Reserved. 3 | # This AWS Content is provided subject to the terms of the AWS Customer Agreement available at 4 | # http://aws.amazon.com/agreement or other written agreement between Customer and either 5 | # Amazon Web Services, Inc. or Amazon Web Services EMEA SARL or both. 6 | 7 | from errno import ENFILE 8 | import json 9 | import csv 10 | import sys 11 | import getopt 12 | import os 13 | import os.path 14 | import boto3 15 | import hashlib 16 | from datetime import datetime, timezone 17 | 18 | SECURITYHUB = boto3.client('securityhub') 19 | 20 | def main(): 21 | FIELDS = ['account','region','vpc_id','subnet_id','loadbalancer_id','loadbalancer_arn','instance_id','instance_arn','instance_name','resource_id','resource_arn','secgroup_id','sgrule_direction','sgrule_cidr','sgrule_protocol','sgrule_portrange'] 22 | EXCLUSIONF = '' 23 | OUTPUTF = '' 24 | INPUTF = '' 25 | naa_exclusions = '' 26 | FINDINGSCSV = '' 27 | FINDINGSSH = '' 28 | 29 | argv = sys.argv[1:] 30 | 31 | if len(sys.argv) == 1: 32 | print ("Pass required parameters with:") 33 | print ("REQUIRED: -i INPUTFILE") 34 | print ("REQUIRED: -o OUTPUTFILE") 35 | print ("REQUIRED: -e EXCLUSIONFILE") 36 | print ("REQUIRED: -c FINDINGSCSV") 37 | print ("REQUIRED: -s FINDINGSSH") 38 | print ("HELP: -h") 39 | quit() 40 | 41 | try: 42 | opts, args = getopt.getopt(argv, "he:i:o:c:s:") 43 | except getopt.GetoptError: 44 | print ("Pass required parameters with:") 45 | print ("REQUIRED: -i INPUTFILE") 46 | print ("REQUIRED: -o OUTPUTFILE") 47 | print ("REQUIRED: -e EXCLUSIONFILE") 48 | print ("REQUIRED: -c FINDINGSCSV") 49 | print ("REQUIRED: -s FINDINGSSH") 50 | print ("HELP: -h") 51 | quit() 52 | 53 | for opt, arg in opts: 54 | if opt == '-h': 55 | print ("Pass required parameters with:") 56 | print ("REQUIRED: -i INPUTFILE") 57 | print ("REQUIRED: -o OUTPUTFILE") 58 | print ("REQUIRED: -e EXCLUSIONFILE") 59 | print ("REQUIRED: -c FINDINGSCSV") 60 | print ("REQUIRED: -s FINDINGSSH") 61 | print ("HELP: -h") 62 | quit() 63 | elif opt in ['-i']: 64 | INPUTF = arg 65 | elif opt in ['-o']: 66 | OUTPUTF = arg 67 | elif opt in ['-e']: 68 | EXCLUSIONF = arg 69 | elif opt in ['-c']: 70 | FINDINGSCSVBOOL = arg 71 | elif opt in ['-s']: 72 | FINDINGSSHBOOL = arg 73 | 74 | # Opening NAA export JSON file 75 | if INPUTF: 76 | f = open(INPUTF) 77 | rows = [] 78 | if os.path.exists(OUTPUTF): 79 | append_write = 'a' # append if already exists 80 | else: 81 | append_write = 'w' # make a new file if not 82 | 83 | with open(OUTPUTF, append_write) as csvfile: 84 | csvwriter = csv.writer(csvfile) 85 | if append_write == 'w': 86 | csvwriter.writerow(FIELDS) 87 | else: 88 | pass 89 | 90 | # returns JSON object as a dictionary 91 | data = json.load(f) 92 | # Iterating through the json object 93 | for Finding in data['AnalysisFindings']: 94 | #Initialize variables 95 | instance_id = "N/A" 96 | instance_arn = "N/A" 97 | instance_name = "N/A" 98 | account = "N/A" 99 | region = "N/A" 100 | partition = "N/A" 101 | vpc_id = "N/A" 102 | subnet_id = "N/A" 103 | resource_id = "N/A" 104 | resource_arn = "N/A" 105 | secgroup_id = "N/A" 106 | sgrule_direction = "N/A" 107 | sgrule_cidr = "N/A" 108 | sgrule_protocol = "N/A" 109 | sgrule_portrange = "N/A" 110 | loadbalancer_id = "N/A" 111 | loadbalancer_arn = "N/A" 112 | skip_finding = False 113 | 114 | findingId = Finding['FindingId'] 115 | findingcomponents = Finding['FindingComponents'] 116 | for component in findingcomponents: 117 | if 'Component' in component: 118 | if 'network-interface' in component['Component']['Arn']: 119 | resource_id = component['Component']['Id'] 120 | resource_arn = component['Component']['Arn'] 121 | if 'loadbalancer' in component['Component']['Arn']: 122 | loadbalancer_id = component['Component']['Id'] 123 | loadbalancer_arn = component['Component']['Arn'] 124 | if 'internet-gateway' in component['Component']['Arn']: 125 | igw_id = component['Component']['Id'] 126 | igw_arn = component['Component']['Arn'] 127 | if 'Vpc' in component: 128 | if 'vpc' in component['Vpc']['Arn']: 129 | vpc_id = component['Vpc']['Id'] 130 | vpc_arn = component['Vpc']['Arn'] 131 | if 'security-group' in component['Component']['Arn']: 132 | secgroup_id = component['Component']['Id'] 133 | secgroup_arn = component['Component']['Arn'] 134 | secgroup_name = component['Component']['Name'] 135 | if 'SecurityGroupRule' in component: 136 | if 'Cidr' in component['SecurityGroupRule']: 137 | sgrule_cidr = component['SecurityGroupRule']['Cidr'] 138 | sgrule_direction = component['SecurityGroupRule']['Direction'] 139 | sgrule_protocol = component['SecurityGroupRule']['Protocol'] 140 | elif 'SecurityGroupId' in component['SecurityGroupRule']: 141 | sgrule_cidr = component['SecurityGroupRule']['SecurityGroupId'] 142 | sgrule_direction = component['SecurityGroupRule']['Direction'] 143 | sgrule_protocol = component['SecurityGroupRule']['Protocol'] 144 | if 'PortRange' in component['SecurityGroupRule']: 145 | sgrule_portrange = str(f"{component['SecurityGroupRule']['PortRange']['From']} to {component['SecurityGroupRule']['PortRange']['To']}") 146 | elif sgrule_protocol == 'all': 147 | sgrule_portrange = 'all' 148 | else: 149 | sgrule_portrange = '' 150 | if 'Subnet' in component: 151 | if 'subnet' in component['Subnet']['Arn']: 152 | subnet_id = component['Subnet']['Id'] 153 | subnet_arn = component['Subnet']['Arn'] 154 | if 'AttachedTo' in component: 155 | if 'instance' in component['AttachedTo']['Arn']: 156 | instance_id = component['AttachedTo']['Id'] 157 | instance_arn = component['AttachedTo']['Arn'] 158 | instance_name = component['AttachedTo']['Name'] 159 | split_arn = igw_arn.split(':') 160 | region = split_arn[3] 161 | account = split_arn[4] 162 | partition = split_arn[1] 163 | 164 | # Read in file of ENI exclusions (if it exists) so they are skipped from the output 165 | # Open file which contains ENI exclusions 166 | with open(EXCLUSIONF) as exclusioncsvfile: 167 | naa_exclusions = csv.reader(exclusioncsvfile, delimiter=',') 168 | for row in naa_exclusions: 169 | if ((resource_id == row[0]) and (secgroup_id == row[1]) and (sgrule_cidr == row[2]) and (sgrule_portrange == row[3]) and (sgrule_protocol == row[4])): 170 | skip_finding = True 171 | continue 172 | if ((loadbalancer_id == row[0])): 173 | skip_finding = True 174 | continue 175 | 176 | if not skip_finding: 177 | #If CSV output is enabled, write a row to the CSV file 178 | if (FINDINGSCSVBOOL == "true"): 179 | rows.append([account,region,vpc_id,subnet_id,loadbalancer_id,loadbalancer_arn,instance_id,instance_arn,instance_name,resource_id,resource_arn,secgroup_id,sgrule_direction,sgrule_cidr,sgrule_protocol,sgrule_portrange]) 180 | 181 | #If Security Hub import is enabled, create a dict and call SH function 182 | if (FINDINGSSHBOOL == "true"): 183 | finding_details = { 184 | "account": account, 185 | "region": region, 186 | "partition": partition, 187 | "vpc_id": vpc_id, 188 | "subnet_id": subnet_id, 189 | "loadbalancer_id": loadbalancer_id, 190 | "loadbalancer_arn": loadbalancer_arn, 191 | "instance_id": instance_id, 192 | "instance_arn": instance_arn, 193 | "instance_name": instance_name, 194 | "resource_id": resource_id, 195 | "resource_arn": resource_arn, 196 | "secgroup_id": secgroup_id, 197 | "sgrule_direction": sgrule_direction, 198 | "sgrule_cidr": sgrule_cidr, 199 | "sgrule_protocol": sgrule_protocol, 200 | "sgrule_portrange": sgrule_portrange 201 | } 202 | map_naa_finding_to_sh(finding_details) 203 | skip_finding = False 204 | 205 | csvwriter.writerows(rows) 206 | 207 | # Closing file 208 | f.close() 209 | 210 | def get_local_env(): 211 | client = boto3.client("sts") 212 | sts_arn = client.get_caller_identity()["Arn"] 213 | split_arn = sts_arn.split(':') 214 | partition = split_arn[1] 215 | account = split_arn[4] 216 | 217 | session = boto3.Session() 218 | region = session.region_name 219 | 220 | local_env = { 221 | "partition": partition, 222 | "account": account, 223 | "region": region 224 | } 225 | return local_env 226 | 227 | def map_naa_finding_to_sh(finding_details): 228 | naa_finding = [] 229 | finding_hash = hashlib.sha256(f"{finding_details['resource_id']}-{finding_details['sgrule_cidr']}-{finding_details['sgrule_protocol']}-{finding_details['sgrule_portrange']}-{finding_details['region']}".encode()).hexdigest() 230 | finding_id = (f"arn:{finding_details['partition']}:securityhub:{finding_details['region']}:{finding_details['account']}:vpn/naa/{finding_hash}") 231 | local_env = get_local_env() 232 | naa_finding.append({ 233 | "SchemaVersion": "2018-10-08", 234 | "Id": finding_id, 235 | "ProductArn": (f"arn:{local_env['partition']}:securityhub:{local_env['region']}:{local_env['account']}:product/{local_env['account']}/default"), 236 | "GeneratorId": "NetworkAccessAnalzyer", 237 | "AwsAccountId": local_env['account'], 238 | 'ProductFields': { 239 | 'ProviderName': 'Network Access Analyzer' 240 | }, 241 | "Types": [ 242 | "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" 243 | ], 244 | "CreatedAt": str(datetime.now().isoformat())+"Z", 245 | "UpdatedAt": str(datetime.now().isoformat())+"Z", 246 | "Severity": { 247 | "Label": "INFORMATIONAL" 248 | }, 249 | "Title": "Network Access Analyzer - Ingress Data Path From Internet", 250 | "Description": "An ingress data path from the Internet to an AWS resource has been located by Network Access Analyzer", 251 | 'Remediation': { 252 | 'Recommendation': { 253 | 'Text': 'Investigate the finding and determine if it is intended or not. Intended findings can be excluded and unintended findings should be remediated' 254 | } 255 | }, 256 | 'Resources': [ 257 | { 258 | 'Type': "Other", 259 | 'Id': (f"{finding_details['resource_id']},{finding_details['sgrule_cidr']},{finding_details['sgrule_protocol']},{finding_details['sgrule_portrange']},{finding_details['region']}".replace('/', '_')), 260 | "Partition": finding_details['partition'], 261 | 'Region': finding_details['region'], 262 | 'Details': {'Other': finding_details} 263 | } 264 | ] 265 | }) 266 | 267 | if naa_finding: 268 | try: 269 | response = SECURITYHUB.batch_import_findings(Findings=naa_finding) 270 | if response['FailedCount'] > 0: 271 | print( 272 | "Failed to import {} findings".format( 273 | response['FailedCount'])) 274 | except Exception as error: 275 | print("Error: ", error) 276 | print("Verify Security Hub is enabled in this account and region, as well as IAM permissions are correct via the EC2 IAM Policy") 277 | 278 | if __name__ == "__main__": 279 | main() -------------------------------------------------------------------------------- /naa-script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #Variable Descriptions: 4 | # 1) SPECIFIC_ACCOUNTID_LIST (SPACE DELIMITED): List specific accounts if you wish to run the command only against those 5 | # or leave "allaccounts" to detect and execute against all accounts in the AWS Org 6 | # 2) REGION_LIST (SPACE DELIMITED): Specify regions to analyze with Network Access Analyzer 7 | # 3) IAM_CROSS_ACCOUNT_ROLE: The IAM Role name created for cross account execution 8 | # 4) SCRIPT_EXECUTION_MODE: 9 | # Specify CREATE_ANALYZE to direct the script to create Network Access Analyzer scopes (if they don't exist already) and analyze them 10 | # Specify DELETE to direct the script to delete Network Access Analyzer scopes which have been provisioned (located by scope name tag) 11 | # In order to REDEPLOY scopes, utilize delete to remove all scopes, modify the Network Access Analyzer JSON file, and then execute with CREATE_ANALYZE 12 | # 5) Configure SCOPE_NAME_VALUE to specify the name tag which will be assigned to the scope. This tag is used to locate the scope for analysis 13 | # 6) Configure EXCLUSIONS_FILE to specify exclusions which will be removed from output during the JSON data parse 14 | # 7) Configure SCOPE_FILE to specify the file which will contain the Network Access Analyzer scope to be deployed 15 | # 8) Configure S3_BUCKET to specify the existing S3 bucket which will have findings uploaded to, as well as where the EXCLUSIONS_FILE may be located. 16 | # 9) Configure PARALLELISM for the number of accounts to process simultaneously 17 | # 10) Configure S3_EXCLUSION_FILE is set to true by default. This instructs the script to download the exclusion file present in s3://S3_BUCKET/EXCLUSIONS_FILE 18 | # and overwrites the local copy on EC2 upon script execution. Set to false to utilize a local exclusion file without the s3 download copy 19 | # 11) Configure FINDINGS_TO_CSV to specify if findings should be output to CSV 20 | # 12) Configure FINDINGS_TO_SH to specify if findings should be import into Security Hub 21 | 22 | SPECIFIC_ACCOUNTID_LIST="allaccounts" 23 | #SPECIFIC_ACCOUNTID_LIST="123456789012 210987654321" 24 | 25 | REGION_LIST="us-east-1" 26 | #REGION_LIST="us-east-1 us-east-2" 27 | 28 | IAM_CROSS_ACCOUNT_ROLE="NAAExecRole" 29 | 30 | SCRIPT_EXECUTION_MODE="CREATE_ANALYZE" 31 | 32 | SCOPE_NAME_VALUE="naa-external-ingress" 33 | 34 | EXCLUSIONS_FILE="naa-exclusions.csv" 35 | 36 | SCOPE_FILE="naa-scope.json" 37 | 38 | S3_BUCKET="SetS3Bucket" 39 | 40 | PARALLELISM="10" 41 | 42 | S3_EXCLUSION_FILE="true" 43 | 44 | FINDINGS_TO_CSV="true" 45 | 46 | FINDINGS_TO_SH="true" 47 | 48 | ######################################### 49 | 50 | #Create the network access analyzer scope JSON file 51 | cat << EOF > $SCOPE_FILE 52 | { 53 | "MatchPaths": [ 54 | { 55 | "Source": { 56 | "ResourceStatement": { 57 | "ResourceTypes": [ 58 | "AWS::EC2::InternetGateway" 59 | ] 60 | } 61 | }, 62 | "Destination": { 63 | "ResourceStatement": { 64 | "ResourceTypes": [ 65 | "AWS::EC2::NetworkInterface" 66 | ] 67 | } 68 | } 69 | } 70 | ] 71 | } 72 | EOF 73 | 74 | #Copy EXCLUSIONS_FILE from S3 to the local EC2 if enabled. Allows for exclusion file update within bucket and copied to EC2 upon script execution 75 | if [[ "$S3_EXCLUSION_FILE" == "true" ]]; then 76 | #Copy exclusion file from S3 bucket to EC2 77 | aws s3 cp s3://$S3_BUCKET/$EXCLUSIONS_FILE . 78 | #If an error occurs with the copy (most likely to initial execution and doens't yet exist), create a local exclusion file and copy to the S3 bucket 79 | if [ $? = 1 ]; then 80 | echo "" 81 | echo "There was an error copying the exclusion file from s3://$S3_BUCKET/$EXCLUSIONS_FILE" 82 | echo "If this is the first execution of the script, this is expected as the exclusion file doesn't yet exist in S3" 83 | echo "A local $EXCLUSIONS_FILE will be created if it doesn't exist and copied to $S3_BUCKET" 84 | echo "" 85 | if [ ! -f $EXCLUSIONS_FILE ]; then 86 | echo "Local exclusions file file not found. Creating..." 87 | echo "resource_id,secgroup_id,sgrule_cidr,sgrule_portrange,sgrule_protocol" > $EXCLUSIONS_FILE 88 | fi 89 | aws s3 cp $EXCLUSIONS_FILE s3://$S3_BUCKET/$EXCLUSIONS_FILE 90 | if [ $? = 1 ]; then 91 | echo "There was an error copying the exclusion file $EXCLUSIONS_FILE to the S3 Bucket $S3_BUCKET" 92 | echo "Review IAM and/or S3 bucket permissions" 93 | fi 94 | fi 95 | elif [[ "$S3_EXCLUSION_FILE" == "false" ]]; then 96 | #Create local exclusions file if it doesn't exist 97 | if [ ! -f $EXCLUSIONS_FILE ]; then 98 | echo "Local exclusions file file not found. Creating..." 99 | echo "resource_id,secgroup_id,sgrule_cidr,sgrule_portrange,sgrule_protocol" > $EXCLUSIONS_FILE 100 | fi 101 | fi 102 | 103 | #Create default exclusions file if it doesn't exist 104 | if [ ! -f $EXCLUSIONS_FILE ]; then 105 | echo "Exclusions file file not found. Creating..." 106 | echo "resource_id,secgroup_id,sgrule_cidr,sgrule_portrange,sgrule_protocol" > $EXCLUSIONS_FILE 107 | fi 108 | 109 | #Create default aws cli config file with default region for commands if it doesn't exist. 110 | if [ ! -f ~/.aws/config ]; then 111 | echo "" 112 | echo "AWS Config file not found. Creating..." 113 | aws configure set region `aws ec2 describe-availability-zones --output text --query 'AvailabilityZones[0].[RegionName]'` 114 | fi 115 | 116 | #Capture starting aws sts creds 117 | capture_starting_session() { 118 | export ORIG_AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID 119 | export ORIG_AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY 120 | export ORIG_AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN 121 | } 122 | capture_starting_session 123 | 124 | # Determine the executing account AWS Number and Partition 125 | CALLER_IDENTITY_ARN=$(aws sts get-caller-identity --output text --query "Arn") 126 | AWSPARTITION=$(echo "$CALLER_IDENTITY_ARN" | cut -d: -f2) 127 | echo "" 128 | 129 | 130 | # Function to Assume Role to Management Account and Create Session 131 | management_account_session() { 132 | AWSMANAGEMENT=$(aws organizations describe-organization --query Organization.MasterAccountId --output text) 133 | echo "AWS Organization Management Account: $AWSMANAGEMENT" 134 | echo "" 135 | echo "Assuming IAM Role in Management account to list all AWS Org accounts..." 136 | role_credentials=$(aws sts assume-role --role-arn arn:$AWSPARTITION:iam::$AWSMANAGEMENT:role/$IAM_CROSS_ACCOUNT_ROLE --role-session-name MgmtAccount --output json) 137 | AWS_ACCESS_KEY_ID=$(echo "$role_credentials" | jq -r .Credentials.AccessKeyId) 138 | AWS_SECRET_ACCESS_KEY=$(echo "$role_credentials" | jq -r .Credentials.SecretAccessKey) 139 | AWS_SESSION_TOKEN=$(echo "$role_credentials" | jq -r .Credentials.SessionToken) 140 | export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN 141 | } 142 | 143 | return_starting_session() { 144 | export AWS_ACCESS_KEY_ID=$ORIG_AWS_ACCESS_KEY_ID 145 | export AWS_SECRET_ACCESS_KEY=$ORIG_AWS_SECRET_ACCESS_KEY 146 | export AWS_SESSION_TOKEN=$ORIG_AWS_SESSION_TOKEN 147 | } 148 | 149 | execute_code() { 150 | #Assume role in each account 151 | echo "Assessing AWS Account: $1, using Role: $IAM_CROSS_ACCOUNT_ROLE" 152 | role_credentials=$(aws sts assume-role --role-arn arn:$AWSPARTITION:iam::$1:role/$IAM_CROSS_ACCOUNT_ROLE --role-session-name NAAAnalyze --output json) 153 | AWS_ACCESS_KEY_ID=$(echo "$role_credentials" | jq -r .Credentials.AccessKeyId) 154 | AWS_SECRET_ACCESS_KEY=$(echo "$role_credentials" | jq -r .Credentials.SecretAccessKey) 155 | AWS_SESSION_TOKEN=$(echo "$role_credentials" | jq -r .Credentials.SessionToken) 156 | export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN 157 | 158 | #Process each region in the $REGION_LIST array 159 | for region in $REGION_LIST; do 160 | { 161 | echo "Processing account: $1 / Region: $region" 162 | 163 | if [[ "$SCRIPT_EXECUTION_MODE" == "CREATE_ANALYZE" ]]; then 164 | #Create Subfolder for finding output 165 | mkdir -p naaoutput 166 | 167 | #Locate ScopeId and insert into variable.. Use this to discover the already existing scope for future analysis 168 | echo "Account: $1 / Region: $region - Detecting Network Access Analyzer scope..." 169 | ScopeId=$(aws ec2 describe-network-insights-access-scopes --region $region --filters Name=tag:Name,Values=$SCOPE_NAME_VALUE --query 'NetworkInsightsAccessScopes[].NetworkInsightsAccessScopeId' --output text) 170 | 171 | if [ -z $ScopeId ]; then 172 | #Create Scope 173 | echo "Account: $1 / Region: $region - Network Access Analyzer scope not detected. Creating new scope..." 174 | ScopeId=$(aws ec2 create-network-insights-access-scope --region $region --tag-specifications "ResourceType=network-insights-access-scope,Tags=[{Key=Name,Value=$SCOPE_NAME_VALUE}]" --cli-input-json file://$SCOPE_FILE | jq -r '.NetworkInsightsAccessScope.NetworkInsightsAccessScopeId') 175 | else 176 | #Continue with Analysis of existing scope 177 | echo "Account: $1 / Region: $region - Network Access Analyzer Scope detected." 178 | fi 179 | 180 | #Start Analysis and insert the AnalysisID into variable 181 | echo "Account: $1 / Region: $region - Continuing analysis with ScopeID. Accounts with many resources may take up to 1.5 hours" 182 | AnalysisId=$(aws ec2 --region $region start-network-insights-access-scope-analysis --network-insights-access-scope-id $ScopeId | jq -r '.NetworkInsightsAccessScopeAnalysis.NetworkInsightsAccessScopeAnalysisId') 183 | 184 | #Monitor Status of AnalysisID. While processing, Status is running and when done, changes to succeeded 185 | i=0 186 | Status="running" 187 | while [ $i -lt 360 ] 188 | do 189 | ((i++)) 190 | Status=$(aws ec2 --region $region describe-network-insights-access-scope-analyses --network-insights-access-scope-analysis-id $AnalysisId | jq -r '.NetworkInsightsAccessScopeAnalyses[].Status') 191 | if [[ "$Status" != "running" ]]; then 192 | break 193 | fi 194 | sleep 15 195 | done 196 | 197 | #Proceed depending on status of the scope analysis (If $Status == succeeded, bypass if statements) 198 | if [[ "$Status" == "running" ]]; then 199 | echo "Account: $1 / Region: $region / AnalysisId: $AnalysisId - Analysis timed out after 1 hour and may still be running" 200 | echo "Account: $1 / Region: $region / AnalysisId: $AnalysisId - Please review and execute again later" 201 | return 0 202 | elif [[ "$Status" == "failed" ]]; then 203 | AnalysisStatus=$(aws ec2 --region $region describe-network-insights-access-scope-analyses --network-insights-access-scope-analysis-id $AnalysisId | jq -r '.NetworkInsightsAccessScopeAnalyses[].StatusMessage') 204 | echo "Account: $1 / Region: $region / AnalysisId: $AnalysisId - Analysis failed to complete. Please review" 205 | echo "Account: $1 / Region: $region / AnalysisId: $AnalysisId - Status Message: $AnalysisStatus" 206 | return 0 207 | fi 208 | 209 | #Output findings from Analysis in JSON format. 210 | echo "Account: $1 / Region: $region - Outputting findings..." 211 | aws ec2 --region $region get-network-insights-access-scope-analysis-findings --network-insights-access-scope-analysis-id $AnalysisId --no-cli-pager > naaoutput/naa-unprocessed-$1-$region.json 212 | 213 | elif [[ "$SCRIPT_EXECUTION_MODE" == "DELETE" ]]; then 214 | #Locate ScopeId and insert into variable.. Use this to discover the already existing scope for future analysis 215 | ScopeId=$(aws ec2 describe-network-insights-access-scopes --region $region --filters Name=tag:Name,Values=$SCOPE_NAME_VALUE --query 'NetworkInsightsAccessScopes[].NetworkInsightsAccessScopeId' --output text) 216 | 217 | #Validate NAA Scope exists. If not, exit loop 218 | if [ -z $ScopeId ]; then 219 | continue 220 | fi 221 | 222 | #Generate AnalysisIdList and build space separated list 223 | AnalysisIdList=$(aws ec2 describe-network-insights-access-scope-analyses --region $region --network-insights-access-scope-id $ScopeId | jq -r '.NetworkInsightsAccessScopeAnalyses[].NetworkInsightsAccessScopeAnalysisId' |tr "\n" " ") 224 | 225 | #Delete each AnalysisId from the list 226 | for AnalysisId in $AnalysisIdList; do 227 | { 228 | #Delete AnalysisId associated with Scope 229 | echo "Account: $1 / Region: $region - Deleting Analysis $AnalysisId" 230 | aws ec2 delete-network-insights-access-scope-analysis --region $region --network-insights-access-scope-analysis-id $AnalysisId 231 | } 232 | done 233 | 234 | #Delete Scope 235 | echo "Account: $1 / Region: $region - Deleting Scope $ScopeId" 236 | aws ec2 delete-network-insights-access-scope --region $region --network-insights-access-scope-id $ScopeId 237 | fi 238 | } 239 | done 240 | 241 | echo "Account: $1 / Region: $region - Completed" 242 | echo "" 243 | echo "" 244 | 245 | #Return to original credentials 246 | return_starting_session 247 | } 248 | 249 | #Monitor the number of background processes and return to task execution for loop when bg jobs less than PARALLELISM limit 250 | process_monitor() { 251 | while [ "$(jobs | grep Running | wc -l)" -ge $PARALLELISM ] 252 | do 253 | sleep 2 254 | done 255 | } 256 | 257 | if [[ "$SPECIFIC_ACCOUNTID_LIST" == "allaccounts" ]]; then 258 | # Lookup All Accounts in AWS Organization 259 | management_account_session 260 | ACCOUNTS_TO_PROCESS=$(aws organizations list-accounts --output text --query 'Accounts[?Status==`ACTIVE`].Id') 261 | echo "" 262 | 263 | # Return to original credentials after generating list of AWS accounts 264 | return_starting_session 265 | else 266 | ACCOUNTS_TO_PROCESS=$SPECIFIC_ACCOUNTID_LIST 267 | fi 268 | 269 | # Execute command against accounts 270 | echo "" 271 | echo "AWS Accounts being processed..." 272 | echo "$ACCOUNTS_TO_PROCESS" 273 | echo "" 274 | 275 | #Process all accounts in the $ACCOUNTS_TO_PROCESS array, 8 at a time, and send them to the background 276 | for accountId in $ACCOUNTS_TO_PROCESS; do 277 | test "$(jobs | grep Running | wc -l)" -ge $PARALLELISM && process_monitor || true 278 | { 279 | execute_code $accountId 280 | } & 281 | done 282 | 283 | # Wait for all background processes to finish 284 | wait 285 | 286 | #### 287 | #### POST ACCOUNT AND REGION EXECUTION: COMMAND(S) BELOW TO BE EXECUTED ON RESULTS 288 | #### 289 | 290 | #Set variable with timestamp for use with file generation 291 | OUTPUT_SUFFIX=$(date +%m-%d-%Y-%H-%M) 292 | 293 | if [[ "$SCRIPT_EXECUTION_MODE" == "CREATE_ANALYZE" ]]; then 294 | echo "" 295 | echo "Network Access Analyzer assessments have been completed against all accounts" 296 | echo "" 297 | echo "Proceeding to Post Processing" 298 | echo "" 299 | 300 | #Remove previously processed data and zip 301 | rm -f naaoutput/naa-findings*.csv naaoutput/naa-unprocessed*.zip naaoutput/naa-processfindingsresults*.txt 302 | 303 | #Generate a list of individual output files and process them into csv with python 304 | FINDING_FILES=$(ls naaoutput/naa-unprocessed*.json) 305 | 306 | #Loop through files and process from json into csv 307 | for finding in $FINDING_FILES; do 308 | { 309 | echo "Processing file: $finding" | tee -a naaoutput/naa-processfindingsresults-$OUTPUT_SUFFIX.txt 310 | python3 ./naa-processfindings.py -i $finding -o naaoutput/naa-findings-$OUTPUT_SUFFIX.csv -e $EXCLUSIONS_FILE -c $FINDINGS_TO_CSV -s $FINDINGS_TO_SH >> naaoutput/naa-processfindingsresults-$OUTPUT_SUFFIX.txt 2>&1 311 | } 312 | done 313 | 314 | #Zip all individual findings into single file for archive 315 | echo "" 316 | echo "Zip files" 317 | zip naaoutput/naa-unprocessed-$OUTPUT_SUFFIX.zip naaoutput/naa-unprocessed*.json naaoutput/naa-processfindingsresults-$OUTPUT_SUFFIX.txt 318 | 319 | #Remove unprocessed finding files which now exist within the zip file 320 | rm -f naaoutput/naa-unprocessed-*.json 321 | 322 | #If the analysis contains findings, copy zip file to S3 bucket (A CSV file with 1 row contains only a header and no findings) 323 | NAAFINDINGSWC=$(wc -l < naaoutput/naa-findings-$OUTPUT_SUFFIX.csv) 324 | if [[ $NAAFINDINGSWC -gt 1 ]]; then 325 | aws s3 cp ./naaoutput s3://$S3_BUCKET --recursive --exclude "*" --include "naa*.zip" --include "naa-findings*.csv" 326 | fi 327 | 328 | if [ -f "naaoutput/naa-findings-$OUTPUT_SUFFIX.csv" ]; then 329 | echo "" 330 | echo "view output at command line with:" 331 | echo "column -s, -t < naaoutput/naa-findings-$OUTPUT_SUFFIX.csv | less -#2 -N -S" 332 | fi 333 | 334 | fi 335 | 336 | echo "" 337 | echo "Processing has been executed against all accounts in the AWS account list" 338 | echo " " 339 | -------------------------------------------------------------------------------- /naa-resources.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: > 3 | Deploys an EC2 Instance, S3 bucket, and IAM Cross-Account trusted Role for use with the Network Access Analyzer script. 4 | 5 | Metadata: 6 | AWS::CloudFormation::Interface: 7 | ParameterGroups: 8 | - Label: 9 | default: "Network Configuration" 10 | Parameters: 11 | - VpcId 12 | - SubnetId 13 | - Label: 14 | default: "EC2 Configuration" 15 | Parameters: 16 | - InstanceType 17 | - InstanceImageId 18 | - KeyPairName 19 | - PermittedSSHInbound 20 | - Label: 21 | default: "S3 Configuration" 22 | Parameters: 23 | - BucketName 24 | - Label: 25 | default: "SNS Configuration" 26 | Parameters: 27 | - EmailAddress 28 | - EmailNotificationsForSecurityHub 29 | - Label: 30 | default: "IAM Configuration" 31 | Parameters: 32 | - IAMNAAEC2Role 33 | - IAMNAAExecRole 34 | - Label: 35 | default: "Network Access Analyzer Script Parameters (Note: After EC2 provisioning, local files mentioned in the description must be modified to further adjust)" 36 | Parameters: 37 | - Parallelism 38 | - Regions 39 | - ScopeNameValue 40 | - ExclusionsFile 41 | - FindingsToCSV 42 | - FindingsToSecurityHub 43 | - ScheduledAnalysis 44 | - CronScheduleExpression 45 | 46 | Parameters: 47 | VpcId: 48 | Type: AWS::EC2::VPC::Id 49 | Description: Select a VPC 50 | SubnetId: 51 | Type: AWS::EC2::Subnet::Id 52 | Description: Select a private subnet with Internet access. (User data is dependent on Internet for downloading binaries during EC2 provisioning) 53 | InstanceImageId: 54 | Type: "AWS::SSM::Parameter::Value" 55 | Description: Amazon Linux 2023 Image 56 | Default: "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64" 57 | InstanceType: 58 | Type: String 59 | Description: "Specify the instance size to use" 60 | Default: t3.small 61 | AllowedValues: 62 | - t3.small 63 | - t2.small 64 | BucketName: 65 | Type: String 66 | Description: Specify the Bucket Name for the NAA output and exception file download (Account ID and Region will be appended e.g. naa--) 67 | Default: naa 68 | EmailAddress: 69 | Type: String 70 | Description: "Optional: If you wish to receive a notification when NAA is completed and has uploaded the zip file containing findings, enter an email address and accept the topic subscription before NAA completes the assessment" 71 | IAMNAAEC2Role: 72 | Type: "String" 73 | Description: "Name of IAM Role to be created for use with the NAA EC2 Instance. This role's ARN is used with the NAAExecRole CFN template" 74 | Default: "NAAEC2Role" 75 | IAMNAAExecRole: 76 | Type: "String" 77 | Description: "Name of IAM Role to be assumed in the member accounts. This name must match the IAM Role deployed via the NAAExecRole CFN template" 78 | Default: "NAAExecRole" 79 | Parallelism: 80 | Type: String 81 | Description: "Specify the number of accounts to assess in parallel (Utilized in /usr/local/naa/naa-script.sh)" 82 | Default: 10 83 | AllowedValues: 84 | - 10 85 | - 12 86 | - 14 87 | Regions: 88 | Type: String 89 | Description: "Specify the regions which will be analyzed. Use space separation when listing multiple regions (e.g. us-east-1 us-east-2) (Utilized in /usr/local/naa/naa-script.sh)" 90 | Default: us-east-1 91 | KeyPairName: 92 | Type: "String" 93 | Description: "Optional: Specify the name of a pre-existing EC2 KeyPair if you require ssh to the NAA instance. Recommendation is to leave blank and use SSM Connect" 94 | PermittedSSHInbound: 95 | Type: "String" 96 | Description: "Optional: If allowing inbound SSH, specify the permitted CIDR else leave the default 127.0.0.1" 97 | Default: "127.0.0.1/32" 98 | ScopeNameValue: 99 | Type: "String" 100 | Description: "Name of Network Access Analyzer scope tag which is assigned during deployment (Utilized in /usr/local/naa/naa-script.sh)" 101 | Default: "naa-external-ingress" 102 | ExclusionsFile: 103 | Type: "String" 104 | Description: "Name of the exclusions file which is used to exclude findings from CSV output (Utilized in /usr/local/naa/naa-script.sh)" 105 | Default: "naa-exclusions.csv" 106 | FindingsToCSV: 107 | Type: "String" 108 | Description: "Specify true to output findings to a CSV and have it uploaded to the S3 bucket or false to disable (Utilized in /usr/local/naa/naa-script.sh)" 109 | Default: "true" 110 | AllowedValues: 111 | - "true" 112 | - "false" 113 | FindingsToSecurityHub: 114 | Type: "String" 115 | Description: "Specify true to import findings into Security Hub or false to disable. Note: Security Hub must be enabled in the AWS account where the NAA EC2 is deployed (Utilized in /usr/local/naa/naa-script.sh)" 116 | Default: "true" 117 | AllowedValues: 118 | - "true" 119 | - "false" 120 | EmailNotificationsForSecurityHub: 121 | Type: "String" 122 | Description: "Specify true to send email notifications when findings are import into Security Hub. Requires an Email Address to be provided, as well as, FindingsToSecurityHub to be true" 123 | Default: "true" 124 | AllowedValues: 125 | - "true" 126 | - "false" 127 | ScheduledAnalysis: 128 | Type: "String" 129 | Description: "Schedule automated analysis via cron. If true, the CronScheduleExpression parameter is used, else it is ignored (Utilized in /etc/cron.d/naa-schedule. Delete this file to remove the cron schedule)" 130 | Default: "true" 131 | AllowedValues: 132 | - true 133 | - false 134 | CronScheduleExpression: 135 | Type: "String" 136 | Description: "Specify the frequency of Network Access Analyzer analysis via cron expression (e.g. Midnight on Sunday 0 0 * * 0 OR Midnight on First Sunday of each month 0 0 * 1-12 0) (Utilized in /etc/cron.d/naa-schedule)" 137 | Default: "0 0 * * 0" 138 | 139 | Mappings: 140 | PartitionMap: 141 | aws: 142 | ec2service: ec2.amazonaws.com 143 | aws-us-gov: 144 | ec2service: ec2.amazonaws.com 145 | aws-cn: 146 | ec2service: ec2.amazonaws.com.cn 147 | 148 | Conditions: 149 | KeyProvided: 150 | Fn::Not: 151 | - Fn::Equals: 152 | - "" 153 | - Ref: KeyPairName 154 | 155 | EmailProvided: 156 | Fn::Not: 157 | - Fn::Equals: 158 | - "" 159 | - Ref: EmailAddress 160 | 161 | SHEmailConfirmed: 162 | Fn::Equals: 163 | - "true" 164 | - Ref: EmailNotificationsForSecurityHub 165 | 166 | Resources: 167 | NAAEC2RolePolicy: 168 | Type: "AWS::IAM::ManagedPolicy" 169 | Metadata: 170 | cfn_nag: 171 | rules_to_suppress: 172 | - id: W13 173 | reason: "The resource must remain as * in order to list accounts in the AWS Org." 174 | - id: W28 175 | reason: "The IAM Role name is specified as an explicit for use within the scripting" 176 | Properties: 177 | Description: "This policy grants necessary permissions to assume NAAExecRole in AWS accounts" 178 | ManagedPolicyName: !Sub "${IAMNAAEC2Role}Policy" 179 | PolicyDocument: 180 | Version: "2012-10-17" 181 | Statement: 182 | - Effect: Allow 183 | Action: 184 | - sts:AssumeRole 185 | Resource: !Sub "arn:${AWS::Partition}:iam::*:role/${IAMNAAExecRole}" 186 | - Effect: Allow 187 | Sid: AllowDescribeOrg 188 | Action: 189 | - "organizations:DescribeOrganization" 190 | Resource: "*" 191 | - Effect: Allow 192 | Sid: AllowDescribeAZ 193 | Action: 194 | - "ec2:DescribeAvailabilityZones" 195 | Resource: "*" 196 | - Effect: Allow 197 | Sid: AllowImportFindingsToSecHub 198 | Action: 199 | - "securityhub:BatchImportFindings" 200 | Resource: "*" 201 | - Effect: Allow 202 | Sid: AllowS3BucketPutObject 203 | Action: 204 | - "s3:PutObject" 205 | - "s3:GetObject" 206 | Resource: !Sub "arn:${AWS::Partition}:s3:::${S3Bucket}/*" 207 | Roles: 208 | - Ref: "NAAEC2Role" 209 | 210 | NAAEC2Role: 211 | Type: "AWS::IAM::Role" 212 | Metadata: 213 | cfn_nag: 214 | rules_to_suppress: 215 | - id: W28 216 | reason: "The IAM Role name is specified as an explicit for use within the scripting" 217 | Properties: 218 | AssumeRolePolicyDocument: 219 | Version: "2012-10-17" 220 | Statement: 221 | - Effect: "Allow" 222 | Principal: 223 | Service: 224 | - !FindInMap [PartitionMap, !Ref "AWS::Partition", ec2service] 225 | Action: 226 | - "sts:AssumeRole" 227 | Description: "This role grants necessary permissions for the NAA Script EC2 instance to assume roles in accounts" 228 | ManagedPolicyArns: 229 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore" 230 | Path: "/" 231 | RoleName: !Sub "${IAMNAAEC2Role}" 232 | 233 | RootInstanceProfile: 234 | Type: "AWS::IAM::InstanceProfile" 235 | Properties: 236 | InstanceProfileName: !Sub "${IAMNAAEC2Role}" 237 | Path: "/" 238 | Roles: 239 | - Ref: "NAAEC2Role" 240 | 241 | NAASNSTopic: 242 | Condition: EmailProvided 243 | Type: AWS::SNS::Topic 244 | Metadata: 245 | cfn_nag: 246 | rules_to_suppress: 247 | - id: W47 248 | reason: "The SNS Topic is used to send a notification when the NAA analysis is completed and objects are uploaded to S3" 249 | Properties: 250 | TopicName: NAANotifications 251 | 252 | NAASNSSubscription: 253 | Condition: EmailProvided 254 | Type: AWS::SNS::Subscription 255 | Properties: 256 | Protocol: email 257 | Endpoint: !Ref EmailAddress 258 | TopicArn: !Ref NAASNSTopic 259 | 260 | NAASNSTopicPolicy: 261 | Condition: EmailProvided 262 | Type: AWS::SNS::TopicPolicy 263 | Properties: 264 | PolicyDocument: 265 | Version: "2012-10-17" 266 | Statement: 267 | - Effect: Allow 268 | Principal: 269 | Service: events.amazonaws.com 270 | Action: 271 | - sns:Publish 272 | Resource: 273 | Ref: NAASNSTopic 274 | Condition: 275 | StringEquals: 276 | aws:SourceAccount: !Sub "${AWS::AccountId}" 277 | Topics: 278 | - !Ref NAASNSTopic 279 | 280 | S3EventRule: 281 | Condition: EmailProvided 282 | Type: "AWS::Events::Rule" 283 | Properties: 284 | Description: NAA S3 Bucket Event 285 | Name: NAAS3BucketEvent 286 | EventPattern: 287 | source: 288 | - aws.s3 289 | detail-type: 290 | - Object Created 291 | detail: 292 | bucket: 293 | name: 294 | - !Ref S3Bucket 295 | object: 296 | key: 297 | - prefix: "naa-findings" 298 | State: ENABLED 299 | Targets: 300 | - Arn: !Ref NAASNSTopic 301 | Id: NAASNSTopic 302 | InputTransformer: 303 | InputPathsMap: 304 | "s3bucket": "$.detail.bucket.name" 305 | "s3objectkey": "$.detail.object.key" 306 | InputTemplate: | 307 | "NAA analysis has completed and the report has been uploaded to the S3 Bucket." 308 | "S3 Bucket Name: " 309 | "S3 Object Key: " 310 | RetryPolicy: 311 | MaximumRetryAttempts: 4 312 | MaximumEventAgeInSeconds: 400 313 | 314 | SHEventRule: 315 | Condition: SHEmailConfirmed 316 | Type: "AWS::Events::Rule" 317 | Properties: 318 | Description: Network Access Analyzer Security Hub Bucket Event 319 | Name: SecurityHub-NetworkAccessAnalyzer-Internet_Ingress 320 | EventPattern: 321 | source: 322 | - aws.securityhub 323 | detail-type: 324 | - Security Hub Findings - Imported 325 | detail: 326 | findings: 327 | Title: 328 | - Network Access Analyzer - Ingress Data Path From Internet 329 | State: ENABLED 330 | Targets: 331 | - Arn: !Ref NAASNSTopic 332 | Id: NAASNSTopic 333 | InputTransformer: 334 | InputPathsMap: 335 | "description": "$.detail.findings[0].Description" 336 | "id": "$.detail.findings[0].Resources[0].Id" 337 | "updatedat": "$.detail.findings[0].UpdatedAt" 338 | "account": "$.detail.findings[0].Resources[0].Details.Other.account" 339 | "region": "$.detail.findings[0].Resources[0].Details.Other.region" 340 | "partition": "$.detail.findings[0].Resources[0].Details.Other.partition" 341 | "vpc_id": "$.detail.findings[0].Resources[0].Details.Other.vpc_id" 342 | "subnet_id": "$.detail.findings[0].Resources[0].Details.Other.subnet_id" 343 | "instance_id": "$.detail.findings[0].Resources[0].Details.Other.instance_id" 344 | "instance_arn": "$.detail.findings[0].Resources[0].Details.Other.instance_arn" 345 | "instance_name": "$.detail.findings[0].Resources[0].Details.Other.instance_name" 346 | "resource_id": "$.detail.findings[0].Resources[0].Details.Other.resource_id" 347 | "resource_arn": "$.detail.findings[0].Resources[0].Details.Other.resource_arn" 348 | "secgroup_id": "$.detail.findings[0].Resources[0].Details.Other.secgroup_id" 349 | "sgrule_direction": "$.detail.findings[0].Resources[0].Details.Other.sgrule_direction" 350 | "sgrule_cidr": "$.detail.findings[0].Resources[0].Details.Other.sgrule_cidr" 351 | "sgrule_protocol": "$.detail.findings[0].Resources[0].Details.Other.sgrule_protocol" 352 | "sgrule_portrange": "$.detail.findings[0].Resources[0].Details.Other.sgrule_portrange" 353 | InputTemplate: | 354 | "Description: " 355 | "Id: " 356 | "UpdateAt: " 357 | "Account: " 358 | "Region: " 359 | "Partition: " 360 | "VPC_ID: " 361 | "Instance_ID: " 362 | "Instance_ARN: " 363 | "Instance_Name: " 364 | "Resource_ID: " 365 | "Resource_ARN: " 366 | "Secgroup_ID: " 367 | "Secgroup_Rule_direction: " 368 | "SecGroup_Rule_CIDR: " 369 | "SecGroup_Rule_Protocol: " 370 | "SecGroup_Rule_PortRange: " 371 | RetryPolicy: 372 | MaximumRetryAttempts: 4 373 | MaximumEventAgeInSeconds: 400 374 | 375 | NAASG: 376 | Type: "AWS::EC2::SecurityGroup" 377 | Metadata: 378 | cfn_nag: 379 | rules_to_suppress: 380 | - id: W28 381 | reason: "The Security Group name is specified explicitly." 382 | - id: W5 383 | reason: "The Security Group has egress rules with cidr open to world to download packages from repos." 384 | Properties: 385 | GroupDescription: "Security Group which allows outbound Internet and SSM access" 386 | VpcId: !Ref VpcId 387 | SecurityGroupEgress: 388 | - Description: "Download packages from Internet, SSM Connect, and write to S3" 389 | IpProtocol: "tcp" 390 | FromPort: "443" 391 | ToPort: "443" 392 | CidrIp: 0.0.0.0/0 393 | - Description: "DNS resolution" 394 | IpProtocol: "udp" 395 | FromPort: "53" 396 | ToPort: "53" 397 | CidrIp: 0.0.0.0/0 398 | SecurityGroupIngress: 399 | - Description: "Inbound SSH" 400 | IpProtocol: "tcp" 401 | FromPort: "22" 402 | ToPort: "22" 403 | CidrIp: !Ref PermittedSSHInbound 404 | GroupName: "naa-sg" 405 | Tags: 406 | - Key: "Name" 407 | Value: "naa-sg" 408 | 409 | S3BucketPolicy: 410 | Type: AWS::S3::BucketPolicy 411 | Properties: 412 | Bucket: !Ref S3Bucket 413 | PolicyDocument: 414 | Version: "2012-10-17" 415 | Statement: 416 | - Effect: Allow 417 | Principal: 418 | AWS: !GetAtt NAAEC2Role.Arn 419 | Action: 420 | - "s3:PutObject" 421 | - "s3:GetObject" 422 | Resource: !Sub "arn:${AWS::Partition}:s3:::${S3Bucket}/*" 423 | - Sid: Deny non-HTTPS access 424 | Effect: Deny 425 | Principal: "*" 426 | Action: s3:* 427 | Resource: 428 | - !Sub "arn:${AWS::Partition}:s3:::${S3Bucket}" 429 | - !Sub "arn:${AWS::Partition}:s3:::${S3Bucket}/*" 430 | Condition: 431 | Bool: 432 | aws:SecureTransport: "false" 433 | 434 | S3Bucket: 435 | Type: "AWS::S3::Bucket" 436 | Properties: 437 | BucketName: !Sub "${BucketName}-${AWS::AccountId}-${AWS::Region}" 438 | BucketEncryption: 439 | ServerSideEncryptionConfiguration: 440 | - ServerSideEncryptionByDefault: 441 | SSEAlgorithm: "AES256" 442 | LoggingConfiguration: 443 | DestinationBucketName: !Ref S3LoggingBucket 444 | PublicAccessBlockConfiguration: 445 | BlockPublicAcls: true 446 | BlockPublicPolicy: true 447 | IgnorePublicAcls: true 448 | RestrictPublicBuckets: true 449 | LifecycleConfiguration: 450 | Rules: 451 | - Id: LoggingLifeCycle 452 | Status: Enabled 453 | ExpirationInDays: '365' 454 | NoncurrentVersionExpirationInDays: '365' 455 | OwnershipControls: 456 | Rules: 457 | - ObjectOwnership: BucketOwnerEnforced 458 | NotificationConfiguration: 459 | EventBridgeConfiguration: 460 | EventBridgeEnabled: true 461 | VersioningConfiguration: 462 | Status: Enabled 463 | Tags: 464 | - Key: "Name" 465 | Value: !Sub "${BucketName}-${AWS::AccountId}-${AWS::Region}" 466 | 467 | S3LoggingBucketPolicy: 468 | Type: AWS::S3::BucketPolicy 469 | Properties: 470 | Bucket: !Ref S3LoggingBucket 471 | PolicyDocument: 472 | Version: "2012-10-17" 473 | Statement: 474 | - Action: 475 | - 's3:PutObject' 476 | Effect: Allow 477 | Principal: 478 | Service: logging.s3.amazonaws.com 479 | Resource: !Sub "arn:${AWS::Partition}:s3:::${S3LoggingBucket}/*" 480 | Condition: 481 | ArnLike: 482 | aws:SourceArn: !GetAtt S3Bucket.Arn 483 | StringEquals: 484 | aws:SourceAccount: !Sub "${AWS::AccountId}" 485 | - Sid: Deny non-HTTPS access 486 | Effect: Deny 487 | Principal: "*" 488 | Action: s3:* 489 | Resource: 490 | - !Sub "arn:${AWS::Partition}:s3:::${S3LoggingBucket}" 491 | - !Sub "arn:${AWS::Partition}:s3:::${S3LoggingBucket}/*" 492 | Condition: 493 | Bool: 494 | aws:SecureTransport: "false" 495 | 496 | S3LoggingBucket: 497 | Type: 'AWS::S3::Bucket' 498 | Metadata: 499 | cfn_nag: 500 | rules_to_suppress: 501 | - id: W35 502 | reason: "S3 access logging is not enable as this is the logging bucket" 503 | Properties: 504 | BucketName: !Sub "${BucketName}-accesslogs-${AWS::AccountId}-${AWS::Region}" 505 | BucketEncryption: 506 | ServerSideEncryptionConfiguration: 507 | - ServerSideEncryptionByDefault: 508 | SSEAlgorithm: "AES256" 509 | LifecycleConfiguration: 510 | Rules: 511 | - Id: LoggingLifeCycle 512 | Status: Enabled 513 | ExpirationInDays: '180' 514 | NoncurrentVersionExpirationInDays: '180' 515 | PublicAccessBlockConfiguration: 516 | BlockPublicAcls: true 517 | BlockPublicPolicy: true 518 | IgnorePublicAcls: true 519 | RestrictPublicBuckets: true 520 | OwnershipControls: 521 | Rules: 522 | - ObjectOwnership: BucketOwnerEnforced 523 | VersioningConfiguration: 524 | Status: Enabled 525 | Tags: 526 | - Key: "Name" 527 | Value: !Sub "${BucketName}-access-logs-${AWS::AccountId}-${AWS::Region}" 528 | 529 | LaunchTemplate: 530 | Type: "AWS::EC2::LaunchTemplate" 531 | Properties: 532 | LaunchTemplateData: 533 | MetadataOptions: 534 | HttpTokens: "required" 535 | 536 | Ec2Instance: 537 | Type: "AWS::EC2::Instance" 538 | Properties: 539 | ImageId: 540 | Ref: "InstanceImageId" 541 | InstanceType: !Ref InstanceType 542 | BlockDeviceMappings: 543 | - DeviceName: "/dev/xvda" 544 | Ebs: 545 | VolumeSize: "12" 546 | DeleteOnTermination: true 547 | VolumeType: "gp3" 548 | Encrypted: true 549 | SubnetId: !Ref SubnetId 550 | IamInstanceProfile: !Ref NAAEC2Role 551 | LaunchTemplate: 552 | LaunchTemplateId: 553 | Ref: "LaunchTemplate" 554 | Version: "1" 555 | KeyName: 556 | Fn::If: 557 | - KeyProvided 558 | - Ref: KeyPairName 559 | - Ref: AWS::NoValue 560 | SecurityGroupIds: 561 | - !GetAtt "NAASG.GroupId" 562 | UserData: 563 | Fn::Base64: 564 | Fn::Sub: | 565 | #!/bin/bash 566 | 567 | #Upgrade the OS 568 | sudo dnf upgrade -y 569 | 570 | #Sleep 5 seconds to allow dnf to release RPM lock (if exists) 571 | sleep 5 572 | 573 | #Install script dependencies 574 | sudo dnf install -y jq pip git cronie cronie-anacron 575 | pip install csvkit boto3 576 | ln -s /usr/local/bin/csvjoin /usr/bin 577 | sudo systemctl enable crond.service && systemctl start crond.service 578 | 579 | #Clone Repo 580 | cd /usr/local 581 | git clone https://github.com/aws-samples/network-access-analyzer-multi-account-analysis naa 582 | 583 | chmod +x /usr/local/naa/naa-script.sh 584 | 585 | #Replace default script variable values in /usr/local/naa/naa-script.sh with parameters configured during CFT deploy 586 | #Note: This occurs ONCE during EC2 deployment and must be manually configured after deploy if additional tuning is required 587 | # Commented variables are left unchanged in the script 588 | # Multiple individual sed commands used for readability 589 | 590 | #SPECIFIC_ACCOUNTID_LIST="allaccounts" 591 | sed -i 's/REGION_LIST="us-east-1"/REGION_LIST="${Regions}"/' /usr/local/naa/naa-script.sh 592 | sed -i 's/IAM_CROSS_ACCOUNT_ROLE="NAAExecRole"/IAM_CROSS_ACCOUNT_ROLE="${IAMNAAExecRole}"/' /usr/local/naa/naa-script.sh 593 | #SCRIPT_EXECUTION_MODE="CREATE_ANALYZE" 594 | sed -i 's/SCOPE_NAME_VALUE="naa-external-ingress"/SCOPE_NAME_VALUE="${ScopeNameValue}"/' /usr/local/naa/naa-script.sh 595 | sed -i 's/EXCLUSIONS_FILE="naa-exclusions.csv"/EXCLUSIONS_FILE="${ExclusionsFile}"/' /usr/local/naa/naa-script.sh 596 | #SCOPE_FILE="naa-scope.json" 597 | sed -i 's/S3_BUCKET="SetS3Bucket"/S3_BUCKET="${S3Bucket}"/' /usr/local/naa/naa-script.sh 598 | sed -i 's/PARALLELISM="10"/PARALLELISM="${Parallelism}"/' /usr/local/naa/naa-script.sh 599 | sed -i 's/REGION_LIST="us-east-1"/REGION_LIST="${Regions}"/' /usr/local/naa/naa-script.sh 600 | #S3_EXCLUSION_FILE="true" 601 | sed -i 's/FINDINGS_TO_CSV="true"/FINDINGS_TO_CSV="${FindingsToCSV}"/' /usr/local/naa/naa-script.sh 602 | sed -i 's/FINDINGS_TO_SH="true"/FINDINGS_TO_SH="${FindingsToSecurityHub}"/' /usr/local/naa/naa-script.sh 603 | 604 | #Set cron if ScheduledAnalysis == true and use the CronScheduleExpression value 605 | if [[ "${ScheduledAnalysis}" == "true" ]]; then 606 | echo "${CronScheduleExpression} root BASH_ENV=/etc/profile cd /usr/local/naa && /usr/local/naa/naa-script.sh >> /usr/local/naa/naa-cron.log 2>&1" > /etc/cron.d/naa-schedule 607 | fi 608 | 609 | Tags: 610 | - Key: "Name" 611 | Value: "NetworkAccessAnalyzerEC2" 612 | 613 | Outputs: 614 | NAAEC2Role: 615 | Description: The ARN of the NAAEC2Role 616 | Value: !GetAtt NAAEC2Role.Arn --------------------------------------------------------------------------------