--- AWSTemplateFormatVersion: 2010-09-09 Metadata: AWS::CloudFormation::Interface: ParameterLabels: ProtectResourceArn: default: The ARN of the Application Load Balancer or CloudFront Distribution AthenaReportCreationRate: default: cron rate to run Fire Extinguisher queries and public to S3 AutoAthenaLookBackExpression: default: Presto interval expression of time. used from now for automatic report generation. e.g. '1' day or '15' minutes https://prestodb.io/docs/current/functions/datetime.html#interval-functions CFNLogicalResourceName: default: Used to construct many resource names RateBasedRuleValue: default: Global rate based rule value SourceIPAddressSource: default: Define how to evaluate source IP from client. For source IP, specify SOURCE_IP, otherwise this value is the name of the header evaulated, e.g. X-FORWARDED-FOR, TRUE-CLIENT-IP RateBasedRuleValue: default: Rule Rate based on source IP by header (e.g. XFF, True-Client-IP) BlockCountryList: default: Comma separated list of ISO 3166 international standard country codes to block. https://www.iso.org/iso-3166-country-codes.html CountryRBRList: default: Comma separated list of ISO 3166 international standard country codes. Rate Limited https://www.iso.org/iso-3166-country-codes.html CountryRBRUsage: default: Should we rate limit the list of countries provided or all countries not from this list CountryListRuleRate: default: For Countries to rate limit, specify the rate by source ip UriValue: default: Used for URI based query rule URIPositionalConstraint: default: How should searchstring the provided URI value URIRateBasedRuleValue: default: Rate limit for specific URIs ReputationRateLimitValue: default: Rate limit for poor reputation source IPs AnonymousRateLimitValue: default: Rate limit for anonymous source IPs URIActions: default: Block or just count based login or search being a part of query path SubscribeAndConfigureShieldAdvanced: default: This will subscribe the account to Shield Advanced, configure contacts, and authorize AWS SRT access for this account EmergencyContactEmail1: default: E-mail address for contact 1 EmergencyContactEmail2: default: E-mail address for contact 2 EmergencyContactPhone1: default: Phone Number for contact 1, must be format +155 where 1 represents country code followed by 10 digit phone number EmergencyContactPhone2: default: Phone Number for contact 2, must be format +155 where 1 represents country code followed by 10 digit phone number ConfirmSubscribeAndConfigureShieldAdvanced: default: Confirming you want to subscribe to Shield Advanced RateBaseRuleAction: default: Action for Rate based rules to take when exceeded AuthorizeSRTAccess: default: Create and authorize SRT role for WebACL and AWS WAF log access RuleGroup1: default: Optional - Existing Rule Group Arn to add to WebACL RuleGroup2: default: Optional - Existing Rule Group Arn to add to WebACL RuleGroup3: default: Optional - Existing Rule Group Arn to add to WebACL RuleGroup4: default: Optional - Existing Rule Group Arn to add to WebACL RuleGroup5: default: Optional - Existing Rule Group Arn to add to WebACL BotControlRateValue: default: Rate Limit or Count bots on Rate Limit list BotControlRateLimitLabels: default: Comma separated list of Bot suffix Labels to rate limit BlockSpecificBotList: default: Comma separate list of Bot suffix labels to block CreateHealthChecks: default: Should we create Amazon Route 53 health checks for provided resources ParameterGroups: - Label: default: "Critical Items" Parameters: - ProtectResourceArn - CFNLogicalResourceName - SourceIPAddressSource - Label: default: "Blanket Rate Based Rule Options" Parameters: - RateBasedRuleValue - RateBaseRuleAction - Label: default: "IP Reputation Rule Options" Parameters: - ReputationRBRLimitLabels - ReputationRateLimitValue - ReputationBlockLabels - Label: default: "Anonymous IP Rule Options" Parameters: - AnonymousRateLimitLabels - AnonymousRateLimitValue - AnonymousBlockLables - Label: default: "Source Country Rule Options " Parameters: - CountryRBRList - CountryRBRUsage - CountryListRuleRate - BlockCountryList - Label: default: "Bot Control Options" Parameters: - BotControlRateLimitLabels - BotControlRateValue - BlockSpecificBotList - Label: default: "URI Rule Options" Parameters: - UriValue - URIActions - URIPositionalConstraint - URIRateBasedRuleValue - Label: default: "Shield Advanced Configurations" Parameters: - SubscribeAndConfigureShieldAdvanced - EmergencyContactEmail1 - EmergencyContactEmail2 - EmergencyContactPhone1 - EmergencyContactPhone2 - AuthorizeSRTAccess - CreateHealthChecks - Label: default: "If you have not already subscribed, confirm you REALLY want to subscribe, if your organization does not already have Shield Advanced, you will be billed the base $3,000 fee for Shield Advanced plus usage" Parameters: - ConfirmSubscribeAndConfigureShieldAdvanced - Label: default: AWS WAF WebACL Configurations Parameters: - RuleGroup1 - RuleGroup2 - RuleGroup3 - RuleGroup4 - RuleGroup5 - Label: default: Athena Query Configurations Parameters: - AutoAthenaLookBackExpression - AthenaReportCreationRate Parameters: CreateHealthChecks: Type: String Default: true AllowedValues: - true - false Prefix: Type: String Default: ddos-fire-extinguisher AutoAthenaLookBackExpression: Type: String Default: \'1\' day AthenaReportCreationRate: Type: String Default: rate(15 minutes) AuthorizeSRTAccess: Type: String Default: Disabled AllowedValues: - Disabled - Enabled RateBasedRuleValue: Type: String Default: 2000 SourceIPAddressSource: Type: String Default: SOURCE_IP RateBaseRuleAction: Type: String Default: Block AllowedValues: - Block - Count BlockCountryList: Type: CommaDelimitedList Default: CountryRBRList: Type: CommaDelimitedList Default: CN,DE,BZ CountryRBRUsage: Type: String Default: Rate Limit these Countries AllowedValues: - Rate Limit these Countries - Rate Limit countries not on this list CountryListRuleRate: Type: Number Default: 100 MinValue: 100 MaxValue: 20000000 SubscribeAndConfigureShieldAdvanced: Type: String Default: false AllowedValues: - true - false ConfirmSubscribeAndConfigureShieldAdvanced: Type: String Default: false AllowedValues: - true - false EmergencyContactEmail1: Type: String Default: example@example.com EmergencyContactEmail2: Type: String Default: example+1@example.com EmergencyContactPhone1: Type: String Default: '+15555555555' AllowedPattern: ^\+[0-9]{11} EmergencyContactPhone2: Type: String Default: '+15555555555' AllowedPattern: ^\+[0-9]{11} CFNLogicalResourceName: Type: String Default: fireextinguisher MaxLength: 24 AllowedPattern: "[a-z0-9\\-]+" ProtectResourceArn: Type: CommaDelimitedList UriValue: Type: String Default: /login URIPositionalConstraint: Type: String Default: STARTS_WITH AllowedValues: - CONTAINS - CONTAINS_WORD - ENDS_WITH - EXACTLY - STARTS_WITH URIRateBasedRuleValue: Type: Number Default: 5000 MinValue: 100 MaxValue: 20000000 ReputationBlockLabels: Type: String Default: none AllowedValues: - none - awswaf:managed:aws:amazon-ip-list:AWSManagedIPReputationList:AWSManagedIPReputationList - 3rdPartyReputationList - 3rdPartyReputationList,awswaf:managed:aws:amazon-ip-list:AWSManagedIPReputationList:AWSManagedIPReputationList ReputationRBRLimitLabels: Type: String Default: 3rdPartyReputationList,awswaf:managed:aws:amazon-ip-list:AWSManagedIPReputationList:AWSManagedIPReputationList AllowedValues: - none - awswaf:managed:aws:amazon-ip-list:AWSManagedIPReputationList:AWSManagedIPReputationList - 3rdPartyReputationList - 3rdPartyReputationList,awswaf:managed:aws:amazon-ip-list:AWSManagedIPReputationList:AWSManagedIPReputationList ReputationRateLimitValue: Type: Number Default: 100 MinValue: 100 MaxValue: 20000000 AnonymousRateLimitValue: Type: Number Default: 100 MinValue: 100 MaxValue: 20000000 AnonymousRateLimitLabels: Type: String Default: HostingProviderIPList AllowedValues: - none - AnonymousIPList - HostingProviderIPList - AnonymousIPList,HostingProviderIPList AnonymousBlockLables: Type: String Default: AnonymousIPList AllowedValues: - none - AnonymousIPList - HostingProviderIPList - AnonymousIPList,HostingProviderIPList URIActions: Type: String Default: Count AllowedValues: - Block - Count RuleGroup1: Type: String Default: RuleGroup2: Type: String Default: RuleGroup3: Type: String Default: RuleGroup4: Type: String Default: RuleGroup5: Type: String Default: BotControlRateLimitLabels: Type: String Default: BotControlRateValue: Type: Number Default: 500 MinValue: 100 MaxValue: 20000000 BlockSpecificBotList: Type: CommaDelimitedList Default: Conditions: #Global rate limit rule action, used to set rule to count or block RateBaseRuleActionFlag: !Equals [!Ref RateBaseRuleAction, 'Block'] #True when rules should use source IP for requester IP SourceIPForClientIP: !Equals [!Ref SourceIPAddressSource, "SOURCE_IP"] #True when rules should use a specified header for requester IP IPHeaderForClientIP: !Not [!Equals [!Ref SourceIPAddressSource, "SOURCE_IP" ] ] #True when provided resource is ALB (aka, regional) RegionalScope: !Equals [!Select [2, !Split [":", !Select [0, !Ref ProtectResourceArn]]], 'elasticloadbalancing'] #True when provided resource list is Cloudfront (AKA, global) GlobalScope: !Equals [!Select [2, !Split [":", !Select [0, !Ref ProtectResourceArn]]], 'cloudfront'] #Should we create health checks for resources CreateHealthChecksFlag: !And - !Equals [!Ref CreateHealthChecks, true] - !Condition SubscribeShield #BotControl Flags #If List of bot labels is not blank BlockSpecificBotsFlag: !Not [!Equals [!Join [',', !Ref BlockSpecificBotList], '']] #If list of rate limit bot labels is not blank. BotControlExcludeFlag: !Not [!Equals [!Ref BotControlRateLimitLabels, "" ] ] #CountryBased Flags #If list of countries to block is not blank BlockCountryListFlag: !Not [!Equals [!Join [',', !Ref BlockCountryList], '']] #If list of countries to rate limit is not blank CountryRBRListFlag: !Not [!Equals [!Join [',', !Ref CountryRBRList], '']] #If country list for Rate limit is for the list provided or countries not from that list. CountryRBRUsageFlag: !Equals [!Ref CountryRBRUsage, "Rate Limit these Countries"] #Shield Configuration #If we should enable SRT Access to Shield Advanced AuthorizeSRTAccessFlag: !Equals [!Ref AuthorizeSRTAccess, 'Enabled'] #If we should subscribe to Shield Advanced, both parameters must be true SubscribeShield: !Equals [!Ref SubscribeAndConfigureShieldAdvanced, 'true'] #AnonymousIP Flags #True if anonymous labels to rate limit is not blank AnonymousRateLimitFlag: !Not [ !Equals [!Ref AnonymousRateLimitLabels, 'none'] ] #True if anonymous labels to block is not blank AnonymousBlockFlag: !Not [ !Equals [!Ref AnonymousBlockLables, 'none'] ] #URI Flags #Specific URI URIActionFlag: !Equals [!Ref URIActions, 'Block'] #Reputation Flags ReputationBlockFlag: !Not [ !Equals [!Ref ReputationBlockLabels, 'none']] ReputationRBRLimitLabelsFlag: !Not [ !Equals [!Ref ReputationRBRLimitLabels, 'none']] #General WAF RuleGroup1Flag: !Not [!Equals [!Ref RuleGroup1, '']] RuleGroup2Flag: !Not [!Equals [!Ref RuleGroup2, '']] RuleGroup3Flag: !Not [!Equals [!Ref RuleGroup3, '']] RuleGroup4Flag: !Not [!Equals [!Ref RuleGroup4, '']] RuleGroup5Flag: !Not [!Equals [!Ref RuleGroup5, '']] Resources: IPSetAllowListIPv4A: Type: 'AWS::WAFv2::IPSet' Properties: Description: Allow List IPv4 Name: !Sub 'AllowListIPv4-${CFNLogicalResourceName}-A' Scope: !If [GlobalScope,"CLOUDFRONT","REGIONAL"] IPAddressVersion: IPV4 Addresses: - 1.2.1.1/32 IPSetAllowListIPv4B: Type: 'AWS::WAFv2::IPSet' Properties: Description: Allow List IPv4 Name: !Sub 'AllowListIPv4-${CFNLogicalResourceName}-B' Scope: !If [GlobalScope,"CLOUDFRONT","REGIONAL"] IPAddressVersion: IPV4 Addresses: - 1.2.1.1/32 IPSetAllowListIPv4C: Type: 'AWS::WAFv2::IPSet' Properties: Description: Allow List IPv4 Name: !Sub 'AllowListIPv4-${CFNLogicalResourceName}-C' Scope: !If [GlobalScope,"CLOUDFRONT","REGIONAL"] IPAddressVersion: IPV4 Addresses: - 1.2.1.1/32 IPSetAllowListIPv6A: Type: 'AWS::WAFv2::IPSet' Properties: Description: Allow List IPv4 Name: !Sub 'AllowListIPv6-${CFNLogicalResourceName}-A' Scope: !If [GlobalScope,"CLOUDFRONT","REGIONAL"] IPAddressVersion: IPV4 Addresses: - 1.2.1.1/32 IPSetAllowListIPv6B: Type: 'AWS::WAFv2::IPSet' Properties: Description: Allow List IPv4 Name: !Sub 'AllowListIPv6-${CFNLogicalResourceName}-B' Scope: !If [GlobalScope,"CLOUDFRONT","REGIONAL"] IPAddressVersion: IPV4 Addresses: - 1.2.1.1/32 IPSetAllowListIPv6C: Type: 'AWS::WAFv2::IPSet' Properties: Description: Allow List IPv4 Name: !Sub 'AllowListIPv6-${CFNLogicalResourceName}-C' Scope: !If [GlobalScope,"CLOUDFRONT","REGIONAL"] IPAddressVersion: IPV4 Addresses: - 1.2.1.1/32 IPSetBlockListIPv4A: Type: 'AWS::WAFv2::IPSet' Properties: Description: Allow List IPv4 Name: !Sub 'IPSetBlockListIPv4-${CFNLogicalResourceName}-A' Scope: !If [GlobalScope,"CLOUDFRONT","REGIONAL"] IPAddressVersion: IPV4 Addresses: - 1.2.1.1/32 IPSetBlockListIPv4B: Type: 'AWS::WAFv2::IPSet' Properties: Description: Allow List IPv4 Name: !Sub 'IPSetBlockListIPv4-${CFNLogicalResourceName}-B' Scope: !If [GlobalScope,"CLOUDFRONT","REGIONAL"] IPAddressVersion: IPV4 Addresses: - 1.2.1.1/32 IPSetBlockListIPv4C: Type: 'AWS::WAFv2::IPSet' Properties: Description: Allow List IPv4 Name: !Sub 'IPSetBlockListIPv4-${CFNLogicalResourceName}-C' Scope: !If [GlobalScope,"CLOUDFRONT","REGIONAL"] IPAddressVersion: IPV4 Addresses: - 1.2.1.1/32 IPSetBlockListIPv6A: Type: 'AWS::WAFv2::IPSet' Properties: Description: Allow List IPv4 Name: !Sub 'IPSetBlockListIPv6-${CFNLogicalResourceName}-A' Scope: !If [GlobalScope,"CLOUDFRONT","REGIONAL"] IPAddressVersion: IPV4 Addresses: - 1.2.1.1/32 IPSetBlockListIPv6B: Type: 'AWS::WAFv2::IPSet' Properties: Description: Allow List IPv4 Name: !Sub 'IPSetBlockListIPv6-${CFNLogicalResourceName}-B' Scope: !If [GlobalScope,"CLOUDFRONT","REGIONAL"] IPAddressVersion: IPV4 Addresses: - 1.2.1.1/32 IPSetBlockListIPv6C: Type: 'AWS::WAFv2::IPSet' Properties: Description: Allow List IPv4 Name: !Sub 'IPSetBlockListIPv6-${CFNLogicalResourceName}-C' Scope: !If [GlobalScope,"CLOUDFRONT","REGIONAL"] IPAddressVersion: IPV4 Addresses: - 1.2.1.1/32 IPReputationListsSetIPV4: Type: 'AWS::WAFv2::IPSet' Properties: Description: Allow List IPv4 Name: !Sub 'IPReputationListsSetIPV4-${CFNLogicalResourceName}' Scope: !If [GlobalScope,"CLOUDFRONT","REGIONAL"] IPAddressVersion: IPV4 Addresses: - 1.2.1.1/32 IPReputationListsSetIPV6: Type: 'AWS::WAFv2::IPSet' Properties: Description: Allow List IPv4 Name: !Sub 'IPReputationListsSetIPV6-${CFNLogicalResourceName}' Scope: !If [GlobalScope,"CLOUDFRONT","REGIONAL"] IPAddressVersion: IPV4 Addresses: - 1.2.1.1/32 FireExtinguisherWAFWebACL: Type: AWS::WAFv2::WebACL Properties: Name: !Sub 'DDOS-FireExtinguisher-${CFNLogicalResourceName}' Scope: !If [GlobalScope,"CLOUDFRONT","REGIONAL"] Description: DDOS FireExtinguisher WAFWebACL DefaultAction: Allow: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: FireExtinguisherWAF Rules: - Name: AllowListedCIDRs Priority: 0 Statement: OrStatement: Statements: - IPSetReferenceStatement: Arn: !GetAtt IPSetAllowListIPv4A.Arn - IPSetReferenceStatement: Arn: !GetAtt IPSetAllowListIPv4B.Arn - IPSetReferenceStatement: Arn: !GetAtt IPSetAllowListIPv4C.Arn - IPSetReferenceStatement: Arn: !GetAtt IPSetAllowListIPv6A.Arn - IPSetReferenceStatement: Arn: !GetAtt IPSetAllowListIPv6B.Arn - IPSetReferenceStatement: Arn: !GetAtt IPSetAllowListIPv6C.Arn Action: Allow: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: AWSWAFSecurityAutomationsWhitelistRule - Name: DenyListedCIDRs Priority: 1 Statement: OrStatement: Statements: - IPSetReferenceStatement: Arn: !GetAtt IPSetBlockListIPv4A.Arn - IPSetReferenceStatement: Arn: !GetAtt IPSetBlockListIPv4B.Arn - IPSetReferenceStatement: Arn: !GetAtt IPSetBlockListIPv4C.Arn - IPSetReferenceStatement: Arn: !GetAtt IPSetBlockListIPv6A.Arn - IPSetReferenceStatement: Arn: !GetAtt IPSetBlockListIPv6B.Arn - IPSetReferenceStatement: Arn: !GetAtt IPSetBlockListIPv6C.Arn Action: Block: {} RuleLabels: - Name: CustomerBlockList VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: AWSWAFSecurityAutomationsBlocklistRule - !If - RuleGroup1Flag - Name: !Select [2, !Split ["/", !Ref RuleGroup1]] Priority: 3 Statement: RuleGroupReferenceStatement: Arn: !Ref RuleGroup1 OverrideAction: None: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: !Select [2, !Split ["/", !Ref RuleGroup1]] - !Ref "AWS::NoValue" - !If - RuleGroup2Flag - Name: !Select [2, !Split ["/", !Ref RuleGroup2]] Priority: 4 Statement: RuleGroupReferenceStatement: Arn: !Ref RuleGroup1 OverrideAction: None: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: !Select [2, !Split ["/", !Ref RuleGroup2]] - !Ref "AWS::NoValue" - !If - RuleGroup3Flag - Name: !Select [2, !Split ["/", !Ref RuleGroup3]] Priority: 5 Statement: RuleGroupReferenceStatement: Arn: !Ref RuleGroup1 OverrideAction: None: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: !Select [2, !Split ["/", !Ref RuleGroup3]] - !Ref "AWS::NoValue" - !If - RuleGroup4Flag - Name: !Select [2, !Split ["/", !Ref RuleGroup4]] Priority: 6 Statement: RuleGroupReferenceStatement: Arn: !Ref RuleGroup1 OverrideAction: None: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: !Select [2, !Split ["/", !Ref RuleGroup4]] - !Ref "AWS::NoValue" - !If - RuleGroup5Flag - Name: !Select [2, !Split ["/", !Ref RuleGroup5]] Priority: 7 Statement: RuleGroupReferenceStatement: Arn: !Ref RuleGroup1 OverrideAction: None: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: !Select [2, !Split ["/", !Ref RuleGroup5]] - !Ref "AWS::NoValue" - Name: AWS-AWSManagedRulesAmazonIpReputationList Priority: 8 Statement: ManagedRuleGroupStatement: VendorName: AWS Name: AWSManagedRulesAmazonIpReputationList OverrideAction: Count: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: AWS-AWSManagedRulesAmazonIpReputationList - Name: Spamhaus-ToRProject-EmergingThreats-Block Priority: 9 Statement: OrStatement: Statements: - IPSetReferenceStatement: Arn: !GetAtt IPReputationListsSetIPV4.Arn IPSetForwardedIPConfig: !If - IPHeaderForClientIP - HeaderName: !Ref SourceIPAddressSource FallbackBehavior: MATCH Position: ANY - !Ref "AWS::NoValue" - IPSetReferenceStatement: Arn: !GetAtt IPReputationListsSetIPV6.Arn IPSetForwardedIPConfig: !If - IPHeaderForClientIP - HeaderName: !Ref SourceIPAddressSource FallbackBehavior: MATCH Position: ANY - !Ref "AWS::NoValue" Action: Count: {} RuleLabels: - Name: 3rdPartyReputationList VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: 3rdPartyIPreputationBlock - !If - ReputationRBRLimitLabelsFlag - Name: ReputationRateLimit Priority: 10 Statement: RateBasedStatement: Limit: !Ref ReputationRateLimitValue AggregateKeyType: !If [SourceIPForClientIP, "IP", "FORWARDED_IP"] ForwardedIPConfig: !If - IPHeaderForClientIP - HeaderName: !Ref SourceIPAddressSource FallbackBehavior: MATCH - !Ref "AWS::NoValue" ScopeDownStatement: !GetAtt ReputationScopeDownNamespaceCall.JSON Action: Block: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: ReputationRateLimit - !Ref "AWS::NoValue" - !If - ReputationBlockFlag - Name: BlockReputation Priority: 11 Statement: !If - ReputationBlockFlag - !GetAtt BlockReputationCall.JSON - LabelMatchStatement: Scope: LABEL Key: awswaf:managed:aws:amazon-ip-list:AWSManagedIPReputationList Action: Block: !If [ReputationBlockFlag, {}, !Ref "AWS::NoValue"] Count: !If [ReputationBlockFlag, !Ref "AWS::NoValue", {}] VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: BlockAnonymous - !Ref "AWS::NoValue" - Name: AWS-AWSManagedRulesAnonymousIpList Priority: 15 Statement: ManagedRuleGroupStatement: VendorName: AWS Name: AWSManagedRulesAnonymousIpList OverrideAction: Count: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: AWS-AWSManagedRulesAnonymousIpList - !If - AnonymousRateLimitFlag - Name: AnonymousRateLimit Priority: 16 Statement: RateBasedStatement: Limit: !Ref AnonymousRateLimitValue AggregateKeyType: !If [SourceIPForClientIP, "IP", "FORWARDED_IP"] ForwardedIPConfig: !If - IPHeaderForClientIP - HeaderName: !Ref SourceIPAddressSource FallbackBehavior: MATCH - !Ref "AWS::NoValue" ScopeDownStatement: !GetAtt AnonymousRateLimitCall.JSON Action: Block: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: AnonymousRateLimit - !Ref "AWS::NoValue" - !If - AnonymousBlockFlag - Name: BlockAnonymous Priority: 17 Statement: !GetAtt BlockAnonymousCall.JSON Action: Block: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: BlockAnonymous - !Ref "AWS::NoValue" - !If - BlockCountryListFlag - Name: Block-Country-List Priority: 20 Statement: GeoMatchStatement: CountryCodes: !Ref BlockCountryList ForwardedIPConfig: !If - IPHeaderForClientIP - HeaderName: !Ref SourceIPAddressSource FallbackBehavior: MATCH - !Ref "AWS::NoValue" Action: Block: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: Block-Country-List - !Ref "AWS::NoValue" - !If - CountryRBRListFlag - Name: Country-List-Rate-Based-Rule Priority: 21 Statement: RateBasedStatement: Limit: !Ref CountryListRuleRate AggregateKeyType: !If [SourceIPForClientIP, "IP", "FORWARDED_IP"] ForwardedIPConfig: !If - IPHeaderForClientIP - HeaderName: !Ref SourceIPAddressSource FallbackBehavior: MATCH - !Ref "AWS::NoValue" ScopeDownStatement: !If - CountryRBRUsageFlag - GeoMatchStatement: CountryCodes: !Ref CountryRBRList - NotStatement: Statement: GeoMatchStatement: CountryCodes: !Ref CountryRBRList Action: Block: !If [CountryRBRListFlag, {}, !Ref "AWS::NoValue"] Count: !If [CountryRBRListFlag, !Ref "AWS::NoValue", {}] VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: OFAC-Geo-Block-RBR - !Ref "AWS::NoValue" - Name: Blanket-Rate-Based-Rule Priority: 22 Statement: RateBasedStatement: Limit: !Ref RateBasedRuleValue AggregateKeyType: !If [SourceIPForClientIP, "IP", "FORWARDED_IP"] ForwardedIPConfig: !If - IPHeaderForClientIP - HeaderName: !Ref SourceIPAddressSource FallbackBehavior: MATCH - !Ref "AWS::NoValue" Action: Block: !If [RateBaseRuleActionFlag, {}, !Ref "AWS::NoValue"] Count: !If [RateBaseRuleActionFlag, !Ref "AWS::NoValue", {}] VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: Global-RBR - Name: URI-Rate-Based-Rule Priority: 23 Statement: RateBasedStatement: Limit: !Ref URIRateBasedRuleValue AggregateKeyType: !If [SourceIPForClientIP, "IP", "FORWARDED_IP"] ForwardedIPConfig: !If - IPHeaderForClientIP - HeaderName: !Ref SourceIPAddressSource FallbackBehavior: MATCH - !Ref "AWS::NoValue" ScopeDownStatement: ByteMatchStatement: SearchString: !Ref UriValue FieldToMatch: UriPath: {} TextTransformations: - Priority: 0 Type: CMD_LINE PositionalConstraint: !Ref URIPositionalConstraint Action: Block: !If [URIActionFlag, {}, !Ref "AWS::NoValue"] Count: !If [URIActionFlag, !Ref "AWS::NoValue", {}] VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: Login-Search-RBR - Name: AWS-AWSManagedRulesBotControlRuleSet Priority: 30 Statement: ManagedRuleGroupStatement: VendorName: AWS Name: AWSManagedRulesBotControlRuleSet OverrideAction: Count: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: AWS-AWSManagedRulesBotControlRuleSet - !If - BotControlExcludeFlag - Name: BotLabelsRateLimit Priority: 31 Statement: RateBasedStatement: Limit: !Ref BotControlRateValue AggregateKeyType: !If [SourceIPForClientIP, "IP", "FORWARDED_IP"] ForwardedIPConfig: !If - IPHeaderForClientIP - HeaderName: !Ref SourceIPAddressSource FallbackBehavior: MATCH - !Ref "AWS::NoValue" ScopeDownStatement: !GetAtt BotControlScopeDownNamespaceCall.JSON Action: Block: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: BotRateBasedRule - !Ref "AWS::NoValue" - !If - BlockSpecificBotsFlag - Name: BlockSpecificBots Priority: 32 Statement: !If - BlockSpecificBotsFlag - !GetAtt BlockSpecificBotsStatement.JSON - LabelMatchStatement: Scope: LABEL Key: awswaf:managed:aws:bot-control:signal:known_bot_data_center Action: Block: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: ExcludeTheseBots - !Ref "AWS::NoValue" WAFAssociateLambdaRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - 'sts:AssumeRole' Path: / WAFAssociateLambdaPolicy: Type: 'AWS::IAM::Policy' Metadata: cfn_nag: rules_to_suppress: - id: W12 reason: "Wildcard required for CFN, ACM, and ELBv2 actions" Properties: PolicyName: LocalPolicy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: 'arn:aws:logs:*:*:*' - Effect: Allow Action: - 'cloudfront:UpdateDistribution' - 'cloudfront:GetDistribution' - 'cloudfront:GetDistributionConfig' - 'acm:ListCertificates' - 'elasticloadbalancing:SetWebACL' Resource: '*' - Effect: Allow Action: - 'wafv2:AssociateWebACL' - 'wafv2:GetWebACL' Resource: !GetAtt FireExtinguisherWAFWebACL.Arn Roles: - !Ref WAFAssociateLambdaRole WAFAssociateCall: DependsOn: WAFAssociateLambdaPolicy Type: Custom::WAFAssociatetoCloudFronts Properties: ServiceToken: !GetAtt WAFAssociateLambdaFunction.Arn resourceArns: !Join [",", !Ref ProtectResourceArn] webACLArn: !GetAtt FireExtinguisherWAFWebACL.Arn WAFAssociateLambdaFunction: Type: 'AWS::Lambda::Function' Metadata: cfn_nag: rules_to_suppress: - id: W58 reason: "Permissions granted, CFN_Nag not parsing correctly?" - id: W89 reason: "Not applicable for use case" - id: W92 reason: "Not applicable for use case" Properties: Runtime: python3.8 Role: !GetAtt WAFAssociateLambdaRole.Arn Handler: index.lambda_handler Timeout: 300 Code: ZipFile: | import urllib3 import json import time import boto3, botocore http = urllib3.PoolManager() cf_client = boto3.client('cloudfront') wafv2_client = boto3.client('wafv2') def cfnrespond(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False): responseUrl = event['ResponseURL'] responseBody = {} responseBody['Status'] = responseStatus responseBody['Reason'] = 'See the details in CloudWatch Log Stream: ' + context.log_stream_name responseBody['PhysicalResourceId'] = physicalResourceId or context.log_stream_name responseBody['StackId'] = event['StackId'] responseBody['RequestId'] = event['RequestId'] responseBody['LogicalResourceId'] = event['LogicalResourceId'] responseBody['NoEcho'] = noEcho responseBody['Data'] = responseData json_responseBody = json.dumps(responseBody) print("Response body:\n" + json_responseBody) headers = { 'content-type' : '', 'content-length' : str(len(json_responseBody)) } try: response = http.request('PUT',responseUrl,body=json_responseBody.encode('utf-8'),headers=headers) print("Status code: " + response.reason) except Exception as e: print("send(..) failed executing requests.put(..): " + str(e)) def lambda_handler(event, context): responseData = {} resourceArns = event['ResourceProperties']['resourceArns'].split(",") webACLArn = event['ResourceProperties']['webACLArn'] if resourceArns[0].split(":")[2] == 'cloudfront': resourceType = 'cloudfront' else: resourceType = 'alb' print (resourceType) for resourceArn in resourceArns: if resourceType == 'cloudfront': cfId = resourceArn.split('/')[-1] dConfig = cf_client.get_distribution_config( Id=cfId) eTag = dConfig['ETag'] print (dConfig) dConfig['DistributionConfig']['WebACLId'] = webACLArn print (dConfig['DistributionConfig']) try: cf_client.update_distribution( Id=cfId, DistributionConfig=dConfig['DistributionConfig'], IfMatch=eTag ) except botocore.exceptions.ClientError as error: print (error.response['Error']['Message']) print (error.response) print (error) responseData['Data'] = error.response['Error']['Message'] cfnrespond(event, context, "FAILED", responseData, "WAFAssociate") return (error.response['Error']['Message']) elif resourceType == 'alb': retryAssociate = True while retryAssociate: try: wafv2_client.associate_web_acl( WebACLArn=webACLArn, ResourceArn=resourceArn ) retryAssociate = False except botocore.exceptions.ClientError as error: print (error.response['Error']['Message']) time.sleep(5) responseData['Data'] = "OK" cfnrespond(event, context, "SUCCESS", responseData, "WAFAssociate") ShieldProtectLambdaRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - 'sts:AssumeRole' Path: / ShieldProtectLambdaPolicy: Type: 'AWS::IAM::Policy' Metadata: cfn_nag: rules_to_suppress: - id: W12 reason: "Wildcard permissions requried for these API calls" Properties: PolicyName: LocalPolicy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: 'arn:aws:logs:*:*:*' - Effect: Allow Action: - 'shield:CreateProtection' - 'elasticloadbalancing:DescribeLoadBalancers' - 'cloudfront:GetDistribution' - 'acm:ListCertificates' - 'iam:CreateServiceLinkedRole' - 'shield:DisassociateSRTRole' Resource: '*' - Effect: Allow Action: - 'wafv2:UpdateWebACL' - 'wafv2:PutLoggingConfiguration' Resource: !GetAtt FireExtinguisherWAFWebACL.Arn Roles: - !Ref ShieldProtectLambdaRole CallShieldProtection: DependsOn: ShieldProtectLambdaPolicy Type: Custom::CallShieldProtection Properties: ServiceToken: !GetAtt ShieldProtectionLambda.Arn resourceArns: !Join [",", !Ref ProtectResourceArn] ShieldProtectionLambda: Type: 'AWS::Lambda::Function' Metadata: cfn_nag: rules_to_suppress: - id: W58 reason: "Permissions granted, CFN_Nag not parsing correctly?" - id: W89 reason: "Not applicable for use case" - id: W92 reason: "Not applicable for use case" Properties: Runtime: python3.8 Role: !GetAtt ShieldProtectLambdaRole.Arn Handler: index.lambda_handler Timeout: 300 Code: ZipFile: | import urllib3 import json import boto3, botocore http = urllib3.PoolManager() client = boto3.client('shield') def cfnrespond(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False): responseUrl = event['ResponseURL'] responseBody = {} responseBody['Status'] = responseStatus responseBody['Reason'] = 'See the details in CloudWatch Log Stream: ' + context.log_stream_name responseBody['PhysicalResourceId'] = physicalResourceId or context.log_stream_name responseBody['StackId'] = event['StackId'] responseBody['RequestId'] = event['RequestId'] responseBody['LogicalResourceId'] = event['LogicalResourceId'] responseBody['NoEcho'] = noEcho responseBody['Data'] = responseData json_responseBody = json.dumps(responseBody) print("Response body:\n" + json_responseBody) headers = { 'content-type' : '', 'content-length' : str(len(json_responseBody)) } try: response = http.request('PUT',responseUrl,body=json_responseBody.encode('utf-8'),headers=headers) print("Status code: " + response.reason) except Exception as e: print("send(..) failed executing requests.put(..): " + str(e)) def lambda_handler(event, context): responseData = {} if event['RequestType'] in ["Create","Update"]: resourceArns = event['ResourceProperties']['resourceArns'].split(",") for resourceArn in resourceArns: try: client.create_protection( Name="DDOSFireExtinguishProtection", ResourceArn=resourceArn ) except botocore.exceptions.ClientError as error: if error.response['Error']['Code'] == 'ResourceAlreadyExistsException': print ("Ok Resource Already Shield Protected") else: print (error.response['Error']['Message']) responseData['Data'] = error.response['Error']['Message'] cfnrespond(event, context, "FAILED", responseData, "ShieldProtection") responseData['Data'] = "OK" cfnrespond(event, context, "SUCCESS", responseData, "ShieldProtection") EnableWeblACLLogging: DependsOn: ShieldProtectLambdaPolicy Type: Custom::WAFV2Logging Properties: ServiceToken: !GetAtt WebACLLoggingLambdaFunction.Arn wafArn: !GetAtt FireExtinguisherWAFWebACL.Arn KinesisStreamArn: !GetAtt WAFdeliverystream.Arn WebACLLoggingLambdaFunction: Type: 'AWS::Lambda::Function' Metadata: cfn_nag: rules_to_suppress: - id: W58 reason: "Permissions granted, CFN_Nag not parsing correctly?" - id: W89 reason: "Not applicable for use case" - id: W92 reason: "Not applicable for use case" Properties: Runtime: python3.8 Role: !GetAtt ShieldProtectLambdaRole.Arn Handler: index.lambda_handler Timeout: 300 Environment: Variables: wafArn: !Ref FireExtinguisherWAFWebACL KinesisStreamArn: !GetAtt WAFdeliverystream.Arn Code: ZipFile: | import urllib3 import json import boto3, botocore http = urllib3.PoolManager() client = boto3.client('wafv2') def cfnrespond(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False): responseUrl = event['ResponseURL'] responseBody = {} responseBody['Status'] = responseStatus responseBody['Reason'] = 'See the details in CloudWatch Log Stream: ' + context.log_stream_name responseBody['PhysicalResourceId'] = physicalResourceId or context.log_stream_name responseBody['StackId'] = event['StackId'] responseBody['RequestId'] = event['RequestId'] responseBody['LogicalResourceId'] = event['LogicalResourceId'] responseBody['NoEcho'] = noEcho responseBody['Data'] = responseData json_responseBody = json.dumps(responseBody) print("Response body:\n" + json_responseBody) headers = { 'content-type' : '', 'content-length' : str(len(json_responseBody)) } try: response = http.request('PUT',responseUrl,body=json_responseBody.encode('utf-8'),headers=headers) print("Status code: " + response.reason) except Exception as e: print("send(..) failed executing requests.put(..): " + str(e)) def lambda_handler(event, context): print (event) wafArn = event['ResourceProperties']['wafArn'] KinesisStreamArn = event['ResourceProperties']['KinesisStreamArn'] client.put_logging_configuration( LoggingConfiguration={ 'ResourceArn': wafArn, 'LogDestinationConfigs': [ KinesisStreamArn ] } ) responseData = {} responseData['Data'] = "OK" cfnrespond(event, context, "SUCCESS", responseData, "ShieldProtection") WAFLogsS3Bucket: Type: 'AWS::S3::Bucket' Metadata: cfn_nag: rules_to_suppress: - id: W35 reason: "Not appropiate for use case, likely block users from access when speed is critical" - id: W41 reason: "Not appropiate for use case, likely block users from access when speed is critical" DeletionPolicy: Retain Properties: BucketName: !Sub "${CFNLogicalResourceName}-${AWS::AccountId}-${AWS::Region}" OwnershipControls: Rules: - ObjectOwnership: BucketOwnerPreferred S3BucketPolicy: Type: 'AWS::S3::BucketPolicy' Properties: Bucket: !Ref WAFLogsS3Bucket PolicyDocument: Statement: - Effect: Allow Principal: AWS: !GetAtt WAFdeliveryRole.Arn Action: - s3:AbortMultipartUpload - s3:GetBucketLocation - s3:GetObject - s3:ListBucket - s3:ListBucketMultipartUploads - s3:PutObject - s3:PutObjectAcl Resource: - !Sub "${WAFLogsS3Bucket.Arn}/*" - !GetAtt WAFLogsS3Bucket.Arn WAFdeliveryRole: Type: AWS::IAM::Role Metadata: cfn_nag: rules_to_suppress: - id: W28 reason: "Appropiate for use case" Properties: RoleName: !Sub 'WAFLogDeliveryRole-${CFNLogicalResourceName}' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Sid: "" Effect: Allow Principal: Service: firehose.amazonaws.com Action: "sts:AssumeRole" Condition: StringEquals: "sts:ExternalId": !Ref "AWS::AccountId" WAFdeliveryPolicy: Type: AWS::IAM::Policy Properties: PolicyName: firehose_delivery_policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - "logs:PutLogEvents" Resource: - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}-id:log-group:KinesisFirehose:log-stream:aws-waf-logs-delivery-${AWS::AccountId}-${AWS::Region}" - Effect: Allow Action: - "s3:AbortMultipartUpload" - "s3:GetBucketLocation" - "s3:GetObject" - "s3:ListBucket" - "s3:ListBucketMultipartUploads" - "s3:PutObject" - "s3:PutObjectAcl" Resource: - !Sub "${WAFLogsS3Bucket.Arn}/*" - !GetAtt WAFLogsS3Bucket.Arn Roles: - !Ref WAFdeliveryRole WAFDeliveryLogGroup: Type: AWS::Logs::LogGroup Metadata: cfn_nag: rules_to_suppress: - id: W84 reason: "Not Applicable" Properties: LogGroupName: !Sub '/aws/${CFNLogicalResourceName}kinesisfirehose' RetentionInDays: 90 WAFDeliveryLogStream: DependsOn: WAFDeliveryLogGroup Type: AWS::Logs::LogStream Properties: LogGroupName: !Sub '/aws/${CFNLogicalResourceName}kinesisfirehose' LogStreamName: !Sub "aws-waf-logs-${CFNLogicalResourceName}-${AWS::AccountId}-${AWS::Region}" WAFdeliverystream: Type: AWS::KinesisFirehose::DeliveryStream Metadata: cfn_nag: rules_to_suppress: - id: W88 reason: "Not Applicable" Properties: DeliveryStreamName: !Sub "aws-waf-logs-${CFNLogicalResourceName}-${AWS::AccountId}-${AWS::Region}" ExtendedS3DestinationConfiguration: BucketARN: !GetAtt WAFLogsS3Bucket.Arn CloudWatchLoggingOptions: Enabled: true LogGroupName: !Sub '/aws/${CFNLogicalResourceName}kinesisfirehose' LogStreamName: !Sub "aws-waf-logs-delivery-${CFNLogicalResourceName}-${AWS::Region}" BufferingHints: IntervalInSeconds: "60" SizeInMBs: "50" CompressionFormat: UNCOMPRESSED Prefix: !Sub 'waflogs/${AWS::AccountId}/${AWS::Region}/' RoleARN: !GetAtt WAFdeliveryRole.Arn GlueDatabaseWAFLogs: Type: AWS::Glue::Database Properties: CatalogId: !Ref AWS::AccountId DatabaseInput: Name: !Sub 'aws-ddos-fire-extinguisher-${CFNLogicalResourceName}' LocationUri: !Sub 's3://${WAFLogsS3Bucket}/waflogs/${AWS::AccountId}/${AWS::Region}/' Description: AWS DDOS Fire Extinguisher Glue database for WAF logs GlueTableWAFLogs: # Creating the table waits for the database to be created DependsOn: GlueDatabaseWAFLogs Type: AWS::Glue::Table Properties: CatalogId: !Ref AWS::AccountId DatabaseName: !Sub 'aws-ddos-fire-extinguisher-${CFNLogicalResourceName}' TableInput: Name: aws-ddos-fire-extinguisher Description: DDOS Fire Extinguisher WAF Logs TableType: EXTERNAL_TABLE StorageDescriptor: OutputFormat: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat Columns: - Name: timestamp Type: bigint - Name: formatversion Type: int - Name: webaclid Type: string - Name: terminatingruleid Type: string - Name: terminatingruletype Type: string - Name: action Type: string - Name: terminatingrulematchdetails Type: array - Name: httpsourcename Type: string - Name: labels Type: array> - Name: httpsourceid Type: string - Name: rulegrouplist Type: array,nonTerminatingMatchingRules:array,excludedRules:array>>> - Name: ratebasedrulelist Type: array> - Name: nonterminatingmatchingrules Type: array>> - Name: requestheadersinserted Type: string - Name: responsecodesent Type: string - Name: httprequest Type: struct>,uri:string,args:string,httpVersion:string,httpMethod:string,requestId:string> InputFormat: org.apache.hadoop.mapred.TextInputFormat Location: !Sub 's3://${WAFLogsS3Bucket}/waflogs/${AWS::AccountId}/${AWS::Region}/' SerdeInfo: SerializationLibrary: org.openx.data.jsonserde.JsonSerDe Parameters: paths: action,formatVersion,httpRequest,httpSourceId,httpSourceName,labels,nonTerminatingMatchingRules,rateBasedRuleList,requestHeadersInserted,responseCodeSent,ruleGroupList,terminatingRuleId,terminatingRuleMatchDetails,terminatingRuleType,timestamp,webaclId #paths: action,formatVersion,httpRequest,httpSourceId,httpSourceName,nonTerminatingMatchingRules,rateBasedRuleList,ruleGroupList,terminatingRuleId,terminatingRuleMatchDetails,terminatingRuleType,timestamp,webaclId AthenaWorkGroup: DeletionPolicy: Retain Type: AWS::Athena::WorkGroup Properties: Name: !Sub 'DDOS-FE-WG-${CFNLogicalResourceName}' Description: DDOS Fire Extinguisher Athena WorkGroup State: ENABLED WorkGroupConfiguration: EnforceWorkGroupConfiguration: false ResultConfiguration: OutputLocation: !Sub 's3://${WAFLogsS3Bucket}/athenaReports/' EncryptionConfiguration: EncryptionOption: SSE_S3 AthenaQueryRateEvent: DependsOn: AthenaQueryLambdaPolicy Type: 'AWS::Events::Rule' Properties: Description: CronAthenaQueries Name: !Sub 'CronAthenaQueries-${CFNLogicalResourceName}' ScheduleExpression: rate(15 minutes) Targets: - Arn: !GetAtt AthenaQueryLambda.Arn Id: CronAthenaQueries AthenaQueryEventPermissions: Type: 'AWS::Lambda::Permission' Properties: FunctionName: !GetAtt AthenaQueryLambda.Arn Action: 'lambda:InvokeFunction' Principal: events.amazonaws.com SourceArn: !GetAtt AthenaQueryRateEvent.Arn AthenaQueryLambdaRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - 'sts:AssumeRole' Path: / AthenaQueryLambdaPolicy: Type: 'AWS::IAM::Policy' Metadata: cfn_nag: rules_to_suppress: - id: F4 reason: "Wildcard permissions for Athena needed" Properties: PolicyName: LocalPolicy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'athena:GetQueryExecution' - 'athena:GetNamedQuery' - 'athena:ListNamedQueries' - 'athena:StartQueryExecution' Resource: !Sub "arn:aws:athena:${AWS::Region}:${AWS::AccountId}:workgroup/${AthenaWorkGroup}" - Effect: Allow Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: 'arn:aws:logs:*:*:*' - Effect: Allow Action: - 'glue:Get*' - 'glue:Update*' - 'glue:CreateTable' Resource: - !Sub arn:aws:glue:${AWS::Region}:${AWS::AccountId}:catalog - !Sub arn:aws:glue:${AWS::Region}:${AWS::AccountId}:database/default - !Sub arn:aws:glue:${AWS::Region}:${AWS::AccountId}:database/default/* - !Sub arn:aws:glue:${AWS::Region}:${AWS::AccountId}:database/aws-ddos-fire-extinguisher-${CFNLogicalResourceName} - !Sub arn:aws:glue:${AWS::Region}:${AWS::AccountId}:table/aws-ddos-fire-extinguisher-${CFNLogicalResourceName}/* - Effect: Allow Action: - 's3:*' Resource: - !Sub "${WAFLogsS3Bucket.Arn}/*" - !GetAtt "WAFLogsS3Bucket.Arn" Roles: - !Ref AthenaQueryLambdaRole BuildAthenaViewsCall: DependsOn: ShieldProtectLambdaPolicy Type: Custom::BuildAthenaViews Properties: ServiceToken: !GetAtt AthenaCreateViewsQueryLambda.Arn DetailedViewQueryId: !Ref AthenaNamedQueryIPDetailed AthenaCreateViewsQueryLambda: Type: 'AWS::Lambda::Function' Metadata: cfn_nag: rules_to_suppress: - id: W58 reason: "Permissions granted, CFN_Nag not parsing correctly?" - id: W89 reason: "Not applicable for use case" - id: W92 reason: "Not applicable for use case" Properties: Runtime: python3.8 Role: !GetAtt AthenaQueryLambdaRole.Arn Handler: index.lambda_handler Timeout: 300 Environment: Variables: s3BasePath: !Sub 's3://${WAFLogsS3Bucket}/athenaReports/' workGroupName: !Sub 'DDOS-FE-WG-${CFNLogicalResourceName}' glueDatabase: !Sub 'aws-ddos-fire-extinguisher-${CFNLogicalResourceName}' lookbackExpression: !Ref AutoAthenaLookBackExpression Code: ZipFile: | import boto3 import datetime import os import time import json import urllib3 import botocore s3BasePath = os.environ['s3BasePath'] workGroupName = os.environ['workGroupName'] database = os.environ['glueDatabase'] athena_client = boto3.client('athena') http = urllib3.PoolManager() responseData = {} def cfnrespond(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False): responseUrl = event['ResponseURL'] responseBody = {} responseBody['Status'] = responseStatus responseBody['Reason'] = 'See the details in CloudWatch Log Stream: ' + context.log_stream_name responseBody['PhysicalResourceId'] = physicalResourceId or context.log_stream_name responseBody['StackId'] = event['StackId'] responseBody['RequestId'] = event['RequestId'] responseBody['LogicalResourceId'] = event['LogicalResourceId'] responseBody['NoEcho'] = noEcho responseBody['Data'] = responseData json_responseBody = json.dumps(responseBody) print("Response body:\n" + json_responseBody) headers = { 'content-type' : '', 'content-length' : str(len(json_responseBody)) } try: response = http.request('PUT',responseUrl,body=json_responseBody.encode('utf-8'),headers=headers) print("Status code: " + response.reason) except Exception as e: print("send(..) failed executing requests.put(..): " + str(e)) def wait_for_queries_to_finish(executionIdList): while (executionIdList != []): for eId in executionIdList: currentState = athena_client.get_query_execution(QueryExecutionId=eId)['QueryExecution']['Status']['State'] if currentState in ['SUCCEEDED']: executionIdList.remove (eId) elif currentState in ['FAILED','CANCELLED']: return (executionIdList) time.sleep(1) return ([]) def lambda_handler(event, context): if event['RequestType'] == 'Delete': cfnrespond(event, context, "SUCCESS", {}, "Graceful Delete") else: executionIdList = [] transformQuery = False transformQuery = True detailedViewQueryId = event['ResourceProperties']['DetailedViewQueryId'] baseQueryString = athena_client.get_named_query( NamedQueryId=detailedViewQueryId)['NamedQuery']['QueryString'] queryString = "CREATE OR REPLACE VIEW DDOS_FE_DETAILED AS " + baseQueryString r = athena_client.start_query_execution( QueryString=queryString, QueryExecutionContext={ 'Database': database, 'Catalog': 'AwsDataCatalog'}, WorkGroup=workGroupName ) #Wait for query to finish, it should take a second but wait just in case if wait_for_queries_to_finish([r['QueryExecutionId']]) != []: cfnrespond(event, context, "FAILED", responseData, "CreateViewQueriesFailed") return ("QueriesFailed") #Get all named query IDs in WorkGroup namedQueries = athena_client.list_named_queries( WorkGroup=workGroupName)['NamedQueryIds'] #Get all Named Queries for queryId in namedQueries: queryResults = athena_client.get_named_query( NamedQueryId=queryId )['NamedQuery'] #Execut#Only run the ones that begin with DDOS-FE, except for DDOS-FE-DETAILED if queryResults['Name'].startswith ("DDOS-FE") and queryResults['Name'] != 'DDOS-FE-detailed': outputLocation = s3BasePath + queryResults['Name'].split('-')[-1] + '/' if transformQuery: queryString = "CREATE OR REPLACE VIEW " + '"' + database + '".' + queryResults['Name'].replace('-','_') + " AS " + queryResults['QueryString'] else: queryString = queryResults['QueryString'] print ("queryString") print (queryString) r = athena_client.start_query_execution( QueryString=queryString, ResultConfiguration={ 'OutputLocation': outputLocation, 'EncryptionConfiguration': { 'EncryptionOption': 'SSE_S3', } }, WorkGroup=workGroupName ) executionIdList.append(r['QueryExecutionId']) print (executionIdList) if wait_for_queries_to_finish(executionIdList) != []: cfnrespond(event, context, "FAILED", responseData, "CreateViewQueriesFailed") return ("QueriesFailed") else: cfnrespond(event, context, "SUCCESS", responseData, "CreateViewsSuccessful") AthenaQueryLambda: Type: 'AWS::Lambda::Function' Metadata: cfn_nag: rules_to_suppress: - id: W58 reason: "Permissions granted, CFN_Nag not parsing correctly?" - id: W89 reason: "Not applicable for use case" - id: W92 reason: "Not applicable for use case" Properties: Runtime: python3.8 Role: !GetAtt AthenaQueryLambdaRole.Arn Handler: index.lambda_handler Timeout: 300 Environment: Variables: s3BasePath: !Sub 's3://${WAFLogsS3Bucket}/athenaReports/' workGroupName: !Sub 'DDOS-FE-WG-${CFNLogicalResourceName}' glueDatabase: !Sub 'aws-ddos-fire-extinguisher-${CFNLogicalResourceName}' Code: ZipFile: | import boto3 import os s3BasePath = os.environ['s3BasePath'] workGroupName = os.environ['workGroupName'] glueDatabase = os.environ['glueDatabase'] athena_client = boto3.client('athena') def lambda_handler(event, context): executionIdList = [] allTables = athena_client.list_table_metadata( CatalogName='AwsDataCatalog', DatabaseName=glueDatabase)['TableMetadataList'] viewTables = [] for t in allTables: if t['TableType'] == 'VIRTUAL_VIEW': if t['Name'] != "ddos_fe_detailed": viewTables.append(t) else: detailedQuery = t print (viewTables) for table in viewTables: queryString = 'SELECT * FROM "GlueDatabasePlaceholder"."TableNamePlaceholder" WHERE tz_window between now() - interval \'1\' day and now() ' queryString = queryString.replace('GlueDatabasePlaceholder',glueDatabase).replace('TableNamePlaceholder',table['Name']) outputLocation = s3BasePath + table['Name'] + '/' r = athena_client.start_query_execution( QueryString=queryString, QueryExecutionContext={ 'Database': glueDatabase, 'Catalog': 'AwsDataCatalog'}, ResultConfiguration={ 'OutputLocation': outputLocation, 'EncryptionConfiguration': { 'EncryptionOption': 'SSE_S3', } }, WorkGroup=workGroupName ) queryString = 'SELECT count(sourceip) as count, tz_window, sourceip FROM "GlueDatabasePlaceholder"."ddos_fe_detailed" WHERE tz_window between now() - interval \'1\' day and now() group by tz_window, sourceip order by count desc, sourceip desc' queryString = queryString.replace('GlueDatabasePlaceholder',glueDatabase) outputLocation = s3BasePath + 'ddos_fe_detailed/' r = athena_client.start_query_execution( QueryString=queryString, QueryExecutionContext={ 'Database': glueDatabase, 'Catalog': 'AwsDataCatalog'}, ResultConfiguration={ 'OutputLocation': outputLocation, 'EncryptionConfiguration': { 'EncryptionOption': 'SSE_S3', } }, WorkGroup=workGroupName ) AthenaNamedQueryIPDetailed: DependsOn: AthenaWorkGroup Type: AWS::Athena::NamedQuery Properties: Database: !Sub 'aws-ddos-fire-extinguisher-${CFNLogicalResourceName}' Description: Detailed and Formatted Core RBR Data Name: "DDOS-FE-detailed" WorkGroup: !Sub 'DDOS-FE-WG-${CFNLogicalResourceName}' QueryString: !Sub | SELECT tz_window , sourceip , COALESCE(NULLIF(args, ''), args) args , COALESCE(NULLIF(httpSourceName, ''), httpSourceName) httpSourceName , country , uri , labels , accountId , webACLName , method , requestId , ntRules , region , scope , terminatingRuleId , action FROM ( SELECT httprequest.clientip sourceip , httprequest.country country , httprequest.uri uri , httprequest.args args , httprequest.httpMethod method , httprequest.requestId requestId , httpSourceName , "split_part"(webaclId, ':', 5) accountId , "split"("split_part"(webaclId, ':', 6), '/', 4)[4] webACLName , "split_part"(webaclId, ':', 4) region , "split"("split_part"(webaclId, ':', 6), '/', 4)[1] scope , webaclId , "array_join"("transform"(nonTerminatingMatchingRules, (x) -> x.ruleId), ',') ntRules , concat("transform"("filter"(labels, (x) -> (x.name LIKE 'awswaf%')), (x) -> "split"(x.name, 'awswaf:managed:aws:')[2]), "transform"("filter"(labels, (x) -> (NOT (x.name LIKE 'awswaf%'))), (x) -> x.name)) as labels , terminatingRuleId , "from_unixtime"(("floor"((timestamp / (1000 * 300))) * 300)) tz_window , action FROM "aws-ddos-fire-extinguisher-${CFNLogicalResourceName}"."aws-ddos-fire-extinguisher" ) AthenaNamedQueryByURIIP: DependsOn: AthenaWorkGroup Type: AWS::Athena::NamedQuery Properties: Database: !Sub 'aws-ddos-fire-extinguisher-${CFNLogicalResourceName}' Description: Count by URI then Source IP over time. Name: "DDOS-FE-URIRate" WorkGroup: !Sub 'DDOS-FE-WG-${CFNLogicalResourceName}' QueryString: !Sub | SELECT "count"(sourceip) as count , tz_window , sourceip , uri FROM ( SELECT * FROM "aws-ddos-fire-extinguisher-${CFNLogicalResourceName}"."ddos_fe_detailed" ) GROUP BY tz_window, sourceip, uri ORDER BY tz_window desc, count DESC AthenaNamedQueryByCountry: DependsOn: AthenaWorkGroup Type: AWS::Athena::NamedQuery Properties: Database: !Sub 'aws-ddos-fire-extinguisher-${CFNLogicalResourceName}' Description: Count by Country then Source IP over time. Name: "DDOS-FE-CountryRate" WorkGroup: !Sub 'DDOS-FE-WG-${CFNLogicalResourceName}' QueryString: !Sub | SELECT "count"(sourceip) as count , tz_window , sourceip , country FROM ( SELECT * FROM "aws-ddos-fire-extinguisher-${CFNLogicalResourceName}"."ddos_fe_detailed" ) GROUP BY tz_window, sourceip, country ORDER BY tz_window desc, count DESC AthenaNamedQueryIPRep: DependsOn: AthenaWorkGroup Type: AWS::Athena::NamedQuery Properties: Database: !Sub 'aws-ddos-fire-extinguisher-${CFNLogicalResourceName}' Description: !Sub "Identify Top Client IP to specific URI path based on header: ${SourceIPAddressSource}" Name: "DDOS-FE-SourceIPReputations" WorkGroup: !Sub "DDOS-FE-WG-${CFNLogicalResourceName}" QueryString: !Sub | SELECT if(reputation = array[], Null, array_join(reputation, ','))as reputation, count(sourceip) AS count, sourceip, uri, tz_window FROM ( SELECT sourceip, uri, tz_window, filter( labels, x -> ( (x LIKE '%amazon-ip-list%') OR (x LIKE '%Reputation%') ) ) as reputation FROM "aws-ddos-fire-extinguisher-${CFNLogicalResourceName}"."ddos_fe_detailed") WHERE reputation <> null GROUP BY tz_window,sourceip,uri,reputation order by reputation desc, count AthenaNamedQueryIPAnon: DependsOn: AthenaWorkGroup Type: AWS::Athena::NamedQuery Properties: Database: !Sub 'aws-ddos-fire-extinguisher-${CFNLogicalResourceName}' Description: !Sub "Identify Top Client IP to specific URI path based on header: ${SourceIPAddressSource}" Name: "DDOS-FE-SourceIPAnonymousorHiddenOwner" WorkGroup: !Sub "DDOS-FE-WG-${CFNLogicalResourceName}" QueryString: !Sub | SELECT if(anonymous = array[], Null, array_join(anonymous, ','))as anonymous, count(sourceip) AS count, sourceip, uri, tz_window FROM ( SELECT sourceip, uri, tz_window, filter( labels, x -> x LIKE '%anonymous-ip-list%') as anonymous FROM "aws-ddos-fire-extinguisher-${CFNLogicalResourceName}"."ddos_fe_detailed") WHERE anonymous <> Null GROUP BY tz_window,sourceip,uri,anonymous order by anonymous desc, count AthenaNamedQueryBotControl: DependsOn: AthenaWorkGroup Type: AWS::Athena::NamedQuery Properties: Database: !Sub 'aws-ddos-fire-extinguisher-${CFNLogicalResourceName}' Description: !Sub "Identify Bot Traffic" Name: "DDOS-FE-BotControlMatch" WorkGroup: !Sub "DDOS-FE-WG-${CFNLogicalResourceName}" QueryString: !Sub | select IF((botSignal = ARRAY[]), null, "split"(botSignal[1], 'bot-control:')[2]) botSignal, IF((botCategory = ARRAY[]), null, "split"(botCategory[1], 'bot-control:')[2]) botCategory, IF((botName = ARRAY[]), null, "split"(botName[1], 'bot-control:')[2]) botName, count(sourceip) as count, tz_window, sourceip, uri from (select filter(labels, x -> split(x,':')[2] = 'signal') as botSignal, filter(labels, x -> split(x,':')[2] = 'category') as botCategory, filter(labels, x -> split(x,':')[2] = 'name') as botName, tz_window, sourceip, uri from (SELECT sourceip, tz_window, filter(labels, x -> x LIKE 'bot-control%') AS botLabels, action, labels, uri FROM "aws-ddos-fire-extinguisher-${CFNLogicalResourceName}"."ddos_fe_detailed" ) where botLabels <> array[] ) Group By tz_window, sourceip, botSignal, botCategory, botName, uri ThirdPartyReputationRateEvent: DependsOn: ThirdPartyIPReputationBuilderLambdaPolicy Type: 'AWS::Events::Rule' Properties: Description: 3rdPartyIPReputationHourlyUpdate Name: !Sub 'ThirdPartyIPReputationBuilder-${CFNLogicalResourceName}' ScheduleExpression: rate(1 hour) Targets: - Arn: !GetAtt ThirdPartyIPReputationBuilderLambda.Arn Id: ThirdPartyIPReputationBuilder ThirdPartyIPReputationBuilderEventPermissions: Type: 'AWS::Lambda::Permission' Properties: FunctionName: !GetAtt ThirdPartyIPReputationBuilderLambda.Arn Action: 'lambda:InvokeFunction' Principal: events.amazonaws.com SourceArn: !GetAtt ThirdPartyReputationRateEvent.Arn ThirdPartyIPReputationBuilderLambdaRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - 'sts:AssumeRole' Path: / ThirdPartyIPReputationBuilderLambdaPolicy: Type: 'AWS::IAM::Policy' Properties: PolicyName: LocalPolicy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: 'arn:aws:logs:*:*:*' - Effect: Allow Action: - 'wafv2:GetIPSet' - 'wafv2:UpdateIPSet' Resource: !GetAtt IPReputationListsSetIPV4.Arn Roles: - !Ref ThirdPartyIPReputationBuilderLambdaRole ThirdPartyIPReputationBuilderLambda: Type: 'AWS::Lambda::Function' Metadata: cfn_nag: rules_to_suppress: - id: W58 reason: "Permissions granted, CFN_Nag not parsing correctly?" - id: W89 reason: "Not applicable for use case" - id: W92 reason: "Not applicable for use case" Properties: Runtime: python3.8 Role: !GetAtt ThirdPartyIPReputationBuilderLambdaRole.Arn Handler: index.lambda_handler Timeout: 300 Code: ZipFile: | import urllib3, boto3, json, os, botocore from datetime import datetime http = urllib3.PoolManager() cidrList = [] client = boto3.client('wafv2') ipsetIPv4 = os.environ['ipsetIPv4'] #ipsetIPv6 = os.environ['ipsetIPv6'] def cfnrespond(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False): responseUrl = event['ResponseURL'] responseBody = {} responseBody['Status'] = responseStatus responseBody['Reason'] = 'See the details in CloudWatch Log Stream: ' + context.log_stream_name responseBody['PhysicalResourceId'] = physicalResourceId or context.log_stream_name responseBody['StackId'] = event['StackId'] responseBody['RequestId'] = event['RequestId'] responseBody['LogicalResourceId'] = event['LogicalResourceId'] responseBody['NoEcho'] = noEcho responseBody['Data'] = responseData json_responseBody = json.dumps(responseBody) print("Response body:\n" + json_responseBody) headers = { 'content-type' : '', 'content-length' : str(len(json_responseBody)) } try: response = http.request('PUT',responseUrl,body=json_responseBody.encode('utf-8'),headers=headers) print("Status code: " + response.reason) except Exception as e: print("send(..) failed executing requests.put(..): " + str(e)) def requests(url): response = (http.request('GET',url)).data.decode('utf-8') return (response.split('\n')) def lambda_handler(event, context): print (event) for url in ["https://www.spamhaus.org/drop/edrop.txt","https://www.spamhaus.org/drop/drop.txt","https://www.spamhaus.org/drop/edrop.txt"]: rawlist = requests(url) for line in rawlist: if not line.startswith(';'): #1.10.16.0/20 ; SBL256894 tIP = line.split(' ')[0] if not "/" in tIP and not line == "": tIP = tIP + "/32" if not line == "": cidrList.append(tIP) rawlist = requests("https://check.torproject.org/exit-addresses") for line in rawlist: if line.startswith('ExitAddress'): tIP = line.split(' ')[1] if not "/" in tIP: tIP = tIP + "/32" if not line == "": cidrList.append(tIP) rawlist = requests("https://rules.emergingthreats.net/fwrules/emerging-Block-IPs.txt") for line in rawlist: if not line.startswith('#') and not line == "": if not "/" in line: line = line + "/32" if not line == "": cidrList.append(line) ipSetScope = ipsetIPv4.split(":")[5].split("/")[0].upper() if ipSetScope == 'GLOBAL': ipSetScope = "CLOUDFRONT" ipSetname = ipsetIPv4.split(":")[5].split("/")[2] ipSetid = ipsetIPv4.split(":")[5].split("/")[3] lockToken = client.get_ip_set( Name=ipSetname, Scope=ipSetScope, Id=ipSetid )['LockToken'] now = datetime.now() # current date and time updatedTime = now.strftime("%m/%d/%Y, %H:%M:%S") description = "Last Updated: " + updatedTime r =client.update_ip_set( Name=ipSetname, Scope=ipSetScope, Id=ipSetid, Description=description, Addresses=list(cidrList), LockToken=lockToken ) if "RequestType" in event: responseData = {} responseData['Data'] = "OK" cfnrespond(event, context, "SUCCESS", responseData, "ShieldProtection") Environment: Variables: ipsetIPv4: !GetAtt IPReputationListsSetIPV4.Arn UpdateThirdPartyIPReputationListNow: DependsOn: ShieldProtectLambdaPolicy Type: Custom::UpdateThirdPartyIPReputationListNow Properties: ServiceToken: !GetAtt ThirdPartyIPReputationBuilderLambda.Arn ConfigureShieldLambdaRole: Condition: SubscribeShield Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - 'sts:AssumeRole' Path: / ConfigureShieldLambdaPolicy: Condition: SubscribeShield Type: 'AWS::IAM::Policy' Properties: PolicyName: LambdaPolicy PolicyDocument: Version: 2012-10-17 Statement: - Sid: CloudWatchLogs Effect: Allow Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: "arn:aws:logs:*:*:*" - Sid: IAMPassrolltoSRT Effect: Allow Action: - iam:PassRole Resource: "*" - Sid: CreateAndManageSRTRole Effect: Allow Action: - iam:GetRole - iam:PassRole - iam:ListAttachedRolePolicies - iam:CreateRole - iam:AttachRolePolicy Resource: !Sub "arn:aws:iam::${AWS::AccountId}:role/AWSSRTAccess" - Sid: ShieldAdvancedConfiguration Effect: Allow Action: - shield:AssociateSRTLogBucket - shield:AssociateSRTRole - shield:AssociateProactiveEngagementDetails - shield:CreateSubscription - shield:DescribeSRTAccess - shield:DisableProactiveEngagement - shield:DisassociateSRTLogBucket - shield:DisassociateSRTRole - shield:EnableProactiveEngagement - shield:UpdateEmergencyContactSettings - shield:UpdateSubscription Resource: "*" - Sid: ShieldAdvancedS3Configuration Effect: Allow Action: - s3:PutBucketPolicy - s3:GetBucketPolicy Resource: "*" Roles: - !Ref ConfigureShieldLambdaRole ConfigureShieldCall: Condition: SubscribeShield DependsOn: ConfigureShieldLambdaPolicy Type: Custom::ConfigureShieldAdvanced Properties: ServiceToken: !GetAtt ConfigureShieldLambda.Arn EnabledProactiveEngagement: !If [AuthorizeSRTAccessFlag,'true','false'] AuthorizeSRTAccessFlag: !If [AuthorizeSRTAccessFlag,'true','false'] EmergencyContactEmail1: !Ref EmergencyContactEmail1 EmergencyContactEmail2: !Ref EmergencyContactEmail2 EmergencyContactPhone1: !Ref EmergencyContactPhone1 EmergencyContactPhone2: !Ref EmergencyContactPhone2 SRTS3LogBucket: !GetAtt WAFLogsS3Bucket.Arn WAFJSONTransformLambdaRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - 'sts:AssumeRole' Path: / WAFJSONTransformLambdaPolicy: Type: 'AWS::IAM::Policy' Properties: PolicyName: LambdaPolicy PolicyDocument: Version: 2012-10-17 Statement: - Sid: CloudWatchLogs Effect: Allow Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: "arn:aws:logs:*:*:*" Roles: - !Ref WAFJSONTransformLambdaRole BlockAnonymousCall: Condition: AnonymousBlockFlag DependsOn: WAFJSONTransformLambdaPolicy Type: Custom::BlockAnonymousCall Properties: ServiceToken: !GetAtt WAFJSONTransformLambda.Arn LabelMatchStatement: !Ref AnonymousBlockLables TransformType: "ScopeDownNameSpace" LabelRoot: "" AnonymousRateLimitCall: Condition: AnonymousRateLimitFlag DependsOn: WAFJSONTransformLambdaPolicy Type: Custom::ScopeDownNameSpace Properties: ServiceToken: !GetAtt WAFJSONTransformLambda.Arn LabelMatchStatement: !Ref AnonymousRateLimitLabels TransformType: "ScopeDownNameSpace" LabelRoot: "awswaf:managed:aws:anonymous-ip-list:" BlockReputationCall: Condition: ReputationBlockFlag DependsOn: WAFJSONTransformLambdaPolicy Type: Custom::BlockAnonymousCall Properties: ServiceToken: !GetAtt WAFJSONTransformLambda.Arn LabelMatchStatement: !Ref ReputationBlockLabels TransformType: "ScopeDownNameSpace" LabelRoot: "" BlockSpecificBotsStatement: Condition: BlockSpecificBotsFlag DependsOn: WAFJSONTransformLambdaPolicy Type: Custom::ScopeDownNameSpace Properties: ServiceToken: !GetAtt WAFJSONTransformLambda.Arn LabelMatchStatement: !Join [",", !Ref BlockSpecificBotList] TransformType: "ScopeDownNameSpace" LabelRoot: "awswaf:managed:bot-control:bot:" LabelRoot: "" BotControlScopeDownNamespaceCall: Condition: BotControlExcludeFlag DependsOn: WAFJSONTransformLambdaPolicy Type: Custom::ScopeDownNameSpace Properties: ServiceToken: !GetAtt WAFJSONTransformLambda.Arn LabelMatchStatement: !Ref BotControlRateLimitLabels TransformType: "ScopeDownNameSpace" LabelRoot: "awswaf:managed:bot-control:bot:" ReputationScopeDownNamespaceCall: Condition: ReputationRBRLimitLabelsFlag DependsOn: WAFJSONTransformLambdaPolicy Type: Custom::ScopeDownNameSpace Properties: ServiceToken: !GetAtt WAFJSONTransformLambda.Arn LabelMatchStatement: !Ref ReputationRBRLimitLabels TransformType: "ScopeDownNameSpace" LabelRoot: "" WAFJSONTransformLambda: Type: AWS::Lambda::Function Metadata: cfn_nag: rules_to_suppress: - id: W58 reason: "Permissions granted, CFN_Nag not parsing correctly?" - id: W89 reason: "Not applicable for use case" - id: W92 reason: "Not applicable for use case" Properties: Runtime: python3.8 Role: !GetAtt WAFJSONTransformLambdaRole.Arn Handler: index.lambda_handler Code: ZipFile: | import urllib3 import json import boto3 import botocore import copy http = urllib3.PoolManager() client = boto3.client('wafv2') def cfnrespond(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False): responseUrl = event['ResponseURL'] responseBody = {} responseBody['Status'] = responseStatus responseBody['Reason'] = 'See the details in CloudWatch Log Stream: ' + context.log_stream_name responseBody['PhysicalResourceId'] = physicalResourceId or context.log_stream_name responseBody['StackId'] = event['StackId'] responseBody['RequestId'] = event['RequestId'] responseBody['LogicalResourceId'] = event['LogicalResourceId'] responseBody['NoEcho'] = noEcho responseBody['Data'] = responseData json_responseBody = json.dumps(responseBody) print("Response body:\n" + json_responseBody) headers = { 'content-type' : '', 'content-length' : str(len(json_responseBody)) } try: response = http.request('PUT',responseUrl,body=json_responseBody.encode('utf-8'),headers=headers) print("Status code: " + response.reason) except Exception as e: print("send(..) failed executing requests.put(..): " + str(e)) def lambda_handler(event, context): print (json.dumps(event)) myJson = [] if event['ResourceProperties']['TransformType'] == 'ExcludeRulesNameList': for rName in event['ResourceProperties']['ExcludeRuleList'].split(","): myJson.append({"Name": rName}) elif event['ResourceProperties']['TransformType'] == 'ScopeDownNameSpace': LabelRoot = event['ResourceProperties']['LabelRoot'] namespaces = event['ResourceProperties']['LabelMatchStatement'].split(",") if len(namespaces) == 1: myJson = { "LabelMatchStatement": { "Scope": "LABEL", "Key": LabelRoot + namespaces[0] } } else: myJson = { "OrStatement": { "Statements": [ ] } } for nSpace in namespaces: myJson['OrStatement']['Statements'].append(copy.deepcopy({ "LabelMatchStatement": { "Scope": "LABEL", "Key": LabelRoot + nSpace } })) print (myJson) responseData = {} responseData['JSON'] = myJson cfnrespond(event, context, "SUCCESS", responseData, "TransformJSON") ConfigureShieldLambdaRole: Condition: SubscribeShield Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - 'sts:AssumeRole' Path: / ConfigureShieldLambdaPolicy: Condition: SubscribeShield Type: 'AWS::IAM::Policy' Metadata: cfn_nag: rules_to_suppress: - id: W12 reason: "ShieldAdvanced Does not support resources other than wildcard" Properties: PolicyName: LambdaPolicy PolicyDocument: Version: 2012-10-17 Statement: - Sid: CloudWatchLogs Effect: Allow Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: "arn:aws:logs:*:*:*" - Sid: IAMPassrolltoSRT Effect: Allow Action: - iam:PassRole Resource: !Sub "arn:aws:iam::${AWS::AccountId}:role/AWSSRTAccess" - Sid: CreateAndManageSRTRole Effect: Allow Action: - iam:GetRole - iam:PassRole - iam:ListAttachedRolePolicies - iam:CreateRole - iam:AttachRolePolicy Resource: !Sub "arn:aws:iam::${AWS::AccountId}:role/AWSSRTAccess" - Sid: ShieldAdvancedConfiguration Effect: Allow Action: - shield:AssociateSRTLogBucket - shield:AssociateSRTRole - shield:AssociateProactiveEngagementDetails - shield:CreateSubscription - shield:DescribeSRTAccess - shield:DisableProactiveEngagement - shield:DisassociateSRTLogBucket - shield:DisassociateSRTRole - shield:EnableProactiveEngagement - shield:UpdateEmergencyContactSettings - shield:UpdateSubscription Resource: "*" Roles: - !Ref ConfigureShieldLambdaRole ConfigureShieldCall: Condition: SubscribeShield DependsOn: ConfigureShieldLambdaPolicy Type: Custom::ConfigureShieldAdvanced Properties: ServiceToken: !GetAtt ConfigureShieldLambda.Arn EnabledProactiveEngagement: !If [AuthorizeSRTAccessFlag,'true','false'] AuthorizeSRTAccessFlag: !If [AuthorizeSRTAccessFlag,'true','false'] EmergencyContactEmail1: !Ref EmergencyContactEmail1 EmergencyContactEmail2: !Ref EmergencyContactEmail2 EmergencyContactPhone1: !Ref EmergencyContactPhone1 EmergencyContactPhone2: !Ref EmergencyContactPhone2 SRTS3LogBucket: !GetAtt WAFLogsS3Bucket.Arn ConfigureShieldLambda: Condition: SubscribeShield Metadata: cfn_nag: rules_to_suppress: - id: W58 reason: "Permissions granted, CFN_Nag not parsing correctly?" - id: W89 reason: "Not applicable for use case" - id: W92 reason: "Not applicable for use case" Type: AWS::Lambda::Function Properties: Runtime: python3.8 Role: !GetAtt ConfigureShieldLambdaRole.Arn Handler: lambda/configure-shield/index.lambda_handler Code: S3Bucket: !Sub "${Prefix}-${AWS::AccountId}-${AWS::Region}" S3Key: lambda.zip EstablishHealthChecksLambdaRole: Condition: SubscribeShield Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - 'sts:AssumeRole' Path: / EstablishHealthChecksLambdaPolicy: Condition: SubscribeShield DependsOn: ConfigureShieldCall Metadata: cfn_nag: rules_to_suppress: - id: W12 reason: "Permissions in place, cfn_nag not parsing correctly" Type: 'AWS::IAM::Policy' Properties: PolicyName: LambdaPolicy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: "arn:aws:logs:*:*:*" - Effect: Allow Action: - cloudformation:Describe* - cloudformation:CreateStack - cloudformation:UpdateStack - cloudformation:DeleteStack Resource: !Sub "arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/*Auto-HealthChecks/*" - Effect: Allow Action: - cloudformation:Describe* - cloudformation:CreateStack - cloudformation:UpdateStack - cloudformation:DeleteStack Resource: !Sub "arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/*Auto-HealthChecks/*" - Effect: Allow Action: - cloudformation:List* - cloudfront:GetDistribution - cloudwatch:DeleteAlarms - cloudwatch:DescribeAlarms - cloudwatch:PutMetricAlarm - route53:ChangeTagsForResource - route53:CreateHealthCheck - route53:DeleteHealthCheck Resource: "*" - Effect: Allow Action: - s3:GetObject Resource: !Sub "arn:aws:s3:::${Prefix}-${AWS::AccountId}-${AWS::Region}/cfn/*" Roles: - !Ref EstablishHealthChecksLambdaRole EstablishHealthChecksCall: Condition: CreateHealthChecksFlag DependsOn: EstablishHealthChecksLambdaPolicy Type: Custom::EstablishHealthChecksAdvanced Properties: ServiceToken: !GetAtt EstablishHealthChecksLambda.Arn ResourceArnList: !Ref ProtectResourceArn EstablishHealthChecksLambda: Condition: SubscribeShield Type: AWS::Lambda::Function Metadata: cfn_nag: rules_to_suppress: - id: W58 reason: "Permissions granted, CFN_Nag not parsing correctly?" - id: W89 reason: "Not applicable for use case" - id: W92 reason: "Not applicable for use case" Properties: Runtime: python3.8 Role: !GetAtt EstablishHealthChecksLambdaRole.Arn Handler: index.lambda_handler Environment: Variables: BucketName: !Sub "${Prefix}-${AWS::AccountId}-${AWS::Region}" Code: ZipFile: | import boto3 import os import urllib3 import botocore import json bucketName = os.environ['BucketName'] cloudformation_client = boto3.client('cloudformation') cloudfront_client = boto3.client('cloudfront') elbv2_client = boto3.client('elbv2') http = urllib3.PoolManager() def cfnrespond(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False): responseUrl = event['ResponseURL'] responseBody = {} responseBody['Status'] = responseStatus responseBody['Reason'] = 'See the details in CloudWatch Log Stream: ' + context.log_stream_name responseBody['PhysicalResourceId'] = physicalResourceId or context.log_stream_name responseBody['StackId'] = event['StackId'] responseBody['RequestId'] = event['RequestId'] responseBody['LogicalResourceId'] = event['LogicalResourceId'] responseBody['NoEcho'] = noEcho responseBody['Data'] = responseData json_responseBody = json.dumps(responseBody) print("Response body:\n" + json_responseBody) headers = { 'content-type' : '', 'content-length' : str(len(json_responseBody)) } try: response = http.request('PUT',responseUrl,body=json_responseBody.encode('utf-8'),headers=headers) print("Status code: " + response.reason) except Exception as e: print("send(..) failed executing requests.put(..): " + str(e)) def lambda_handler(event, context): print (event) responseData = {} resourceArns = event['ResourceProperties']['ResourceArnList'] for resource in resourceArns: resourceType = resource.split(":")[2] if resourceType == 'cloudfront': stackName = resource.split('/')[1] + "-DDOS-FE-Auto-HealthChecks" templateURL = "https://" + bucketName + ".s3.amazonaws.com/cfn/cf_health_check.yaml" probeFQDN = cloudfront_client.get_distribution( Id=resource.split('/')[1] )['Distribution']['DomainName'] parameters = [ { 'ParameterKey': 'CFArn', 'ParameterValue': resource }, { 'ParameterKey': 'probeFQDN', 'ParameterValue': probeFQDN }, ] elif resourceType == 'elasticloadbalancing': if resource.split('/')[1] == "app": stackName = resource.split('/')[2] + "-DDOS-FE-Auto-HealthChecks" templateURL = "https://" + bucketName + ".s3.amazonaws.com/cfn/alb_health_check.yaml" probeFQDN = elbv2_client.describe_load_balancers( LoadBalancerArns=[resource])['LoadBalancers'][0]['DNSName'] parameters = [ { 'ParameterKey': 'ALBArn', 'ParameterValue': resource }, { 'ParameterKey': 'probeFQDN', 'ParameterValue': probeFQDN }, ] print (parameters) if event['RequestType'] == 'Create': try: cloudformation_client.create_stack( StackName=stackName, TemplateURL=templateURL, Parameters=parameters, Capabilities=[ 'CAPABILITY_IAM','CAPABILITY_NAMED_IAM','CAPABILITY_AUTO_EXPAND', ] ) except botocore.exceptions.ClientError as error: if error.response['Error']['Code'] == 'AlreadyExistsException': print ("Stack already exists, ok") else: print ("Error") cfnrespond(event, context, "FAILED", responseData, (error.response['Error']['Message'])) return() elif event['RequestType'] == 'Update': try: cloudformation_client.update_stack( StackName=stackName, TemplateURL=templateURL, Parameters=parameters, Capabilities=[ 'CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND' ] ) except botocore.exceptions.ClientError as error: if (error.response['Error']['Message']) == 'No updates are to be performed.': print ("NoUpdates, OK") else: print ((error.response['Error']['Message'])) cfnrespond(event, context, "FAILED", responseData, (error.response['Error']['Message'])) return() elif event['RequestType'] == 'Delete': cloudformation_client.delete_stack( StackName=stackName ) cfnrespond(event, context, "SUCCESS", responseData, "SUCCESS")