diff --git a/CHANGELOG.md b/CHANGELOG.md index 657cdb12..0d77828d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,11 @@ # Change Log ## Released +## 2.1.2 +### Added + +- Price calculation for your WAF ## 2.1.1 ### Fixed diff --git a/README.md b/README.md index 119a5ad2..167f9e04 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ If you want to learn more about the AWS Firewall Factory feel free to look at th - Components of a label are separated by a colon (:). +19. While Deployment the Price for your WAF will be calculated using the Pricing API ### Coming soon - Deployment via Teamcity diff --git a/bin/plattform-wafv2-cdk-automation.ts b/bin/plattform-wafv2-cdk-automation.ts index 14aa1756..21e85782 100644 --- a/bin/plattform-wafv2-cdk-automation.ts +++ b/bin/plattform-wafv2-cdk-automation.ts @@ -3,8 +3,9 @@ import { PlattformWafv2CdkAutomationStack } from "../lib/plattform-wafv2-cdk-aut import * as cdk from "aws-cdk-lib"; import { realpathSync, existsSync } from "fs"; import { validate } from "../lib/tools/config-validator"; -import { Config } from "../lib/types/config"; +import { Config, PriceRegions, RegionString } from "../lib/types/config"; import { isPolicyQuotaReached, isWcuQuotaReached, setOutputsFromStack, initRuntimeProperties } from "../lib/tools/helpers"; +import {isPriceCalculated, GetCurrentPrices} from "../lib/tools/price-calculator"; import * as packageJsonObject from "../package.json"; /** @@ -80,6 +81,8 @@ if (configFile && existsSync(configFile)) { account: process.env.CDK_DEFAULT_ACCOUNT, }, }); + const Prices = await GetCurrentPrices(PriceRegions[deploymentRegion as RegionString], runtimeProperties) + const PriceCalculated = await isPriceCalculated(runtimeProperties) })(); } else { console.log(` diff --git a/lib/tools/helpers.ts b/lib/tools/helpers.ts index bb3fa276..43a62913 100644 --- a/lib/tools/helpers.ts +++ b/lib/tools/helpers.ts @@ -220,7 +220,9 @@ async function calculateCapacities( ); } else { while (count < config.WebAcl.PreProcess.CustomRules.length) { + runtimeProperties.PreProcess.CustomRuleCount += 1; if ("Captcha" in config.WebAcl.PreProcess.CustomRules[count].Action) { + runtimeProperties.PreProcess.CustomCaptchaRuleCount += 1; const rules : Rule[] = []; const { CloudWatchMetricsEnabled, SampledRequestsEnabled } = config.WebAcl.PreProcess.CustomRules[count].VisibilityConfig; @@ -282,6 +284,7 @@ async function calculateCapacities( ); } else { while (count < config.WebAcl.PostProcess.CustomRules.length) { + runtimeProperties.PostProcess.CustomRuleCount += 1; const rule_calculated_capacity_json = []; const { CloudWatchMetricsEnabled, SampledRequestsEnabled } = config.WebAcl.PostProcess.CustomRules[count].VisibilityConfig; @@ -296,6 +299,7 @@ async function calculateCapacities( }, }; if ("Captcha" in config.WebAcl.PostProcess.CustomRules[count].Action) { + runtimeProperties.PostProcess.CustomCaptchaRuleCount += 1; rule.CaptchaConfig = config.WebAcl.PostProcess.CustomRules[count].CaptchaConfig; } @@ -341,6 +345,9 @@ async function calculateCapacities( "]" ); runtimeProperties.ManagedRuleCapacity += capacity; + runtimeProperties.PreProcess.ManagedRuleGroupCount += 1; + managedrule.Name == "AWSManagedRulesBotControlRuleSet" ? runtimeProperties.PreProcess.ManagedRuleBotControlCount +=1 : "" + managedrule.Name == "AWSManagedRulesATPRuleSet" ? runtimeProperties.PreProcess.ManagedRuleATPCount += 1 : "" } } if (!config.WebAcl.PostProcess.ManagedRuleGroups) { @@ -364,6 +371,9 @@ async function calculateCapacities( "]" ); runtimeProperties.ManagedRuleCapacity += capacity; + runtimeProperties.PostProcess.ManagedRuleGroupCount += 1; + managedrule.Name == "AWSManagedRulesBotControlRuleSet" ? runtimeProperties.PostProcess.ManagedRuleBotControlCount +=1 : "" + managedrule.Name == "AWSManagedRulesATPRuleSet" ? runtimeProperties.PostProcess.ManagedRuleATPCount += 1 : "" } } runtimeProperties.PostProcess.Capacity = PostProcessCapacity; @@ -428,15 +438,38 @@ export function initRuntimeProperties() : RuntimeProperties { DeployedRuleGroupCapacities: [], DeployedRuleGroupIdentifier: [], DeployedRuleGroupNames: [], - RuleCapacities: [] + RuleCapacities: [], + ManagedRuleGroupCount: 0, + ManagedRuleBotControlCount: 0, + ManagedRuleATPCount: 0, + CustomRuleCount: 0, + CustomRuleGroupCount: 0, + CustomCaptchaRuleCount: 0 }, PreProcess: { Capacity: 0, DeployedRuleGroupCapacities: [], DeployedRuleGroupIdentifier: [], DeployedRuleGroupNames: [], - RuleCapacities: [] + RuleCapacities: [], + ManagedRuleGroupCount: 0, + ManagedRuleBotControlCount: 0, + ManagedRuleATPCount: 0, + CustomRuleCount: 0, + CustomRuleGroupCount: 0, + CustomCaptchaRuleCount: 0 }, + Pricing: { + Policy: 0, + Rule: 0, + WebACL: 0, + Request: 0, + BotControl: 0, + BotControlRequest: 0, + Captcha: 0, + AccountTakeoverPrevention: 0, + AccountTakeoverPreventionRequest: 0, + } }; } diff --git a/lib/tools/price-calculator.ts b/lib/tools/price-calculator.ts new file mode 100644 index 00000000..9ac9ee45 --- /dev/null +++ b/lib/tools/price-calculator.ts @@ -0,0 +1,143 @@ +import { PricingClient, GetProductsCommand, GetProductsCommandInput } from "@aws-sdk/client-pricing"; +import { RuntimeProperties } from "../types/runtimeprops"; +import { PriceRegions } from "../types/config"; +/** + * Amazon Web Services Price List Service API Endpoint + */ +const PRICING_API_ENDPOINT_REGION = "us-east-1"; + + +/** + * + * @param obj object where the key is included + * @param key key which includes the needed value + * @returns value from key + */ +function findValues(obj: any, key: string){ + return findValuesHelper(obj, key, []); +} + +function findValuesHelper(obj:any, key:string, list: any) { + if (!obj) return list; + if (obj instanceof Array) { + for (const i in obj) { + list = list.concat(findValuesHelper(obj[i], key, [])); + } + return list; + } + if (obj[key]) list.push(obj[key]); + + if ((typeof obj === "object") && (obj !== null) ){ + const children = Object.keys(obj); + if (children.length > 0){ + for (let i = 0; i < children.length; i++ ){ + list = list.concat(findValuesHelper(obj[children[i]], key, [])); + } + } + } + return list; +} + +/** + * + * @param deploymentRegion AWS region, e.g. eu-central-1 + * @param runtimeProps runtime properties object, where to store prices + * @returns true if prices are update in runtimeprops + */ +export async function GetCurrentPrices(deploymentRegion: PriceRegions, runtimeProps: RuntimeProperties): Promise { + try{ + runtimeProps.Pricing.Policy = Number(await getProductPrice(deploymentRegion,"AWSFMS","WAFv2")); + runtimeProps.Pricing.Rule = Number(await getProductPrice(deploymentRegion,"awswaf",undefined,"Rule")); + runtimeProps.Pricing.WebACL = Number(await getProductPrice(deploymentRegion,"awswaf",undefined,"Web ACL")); + runtimeProps.Pricing.Request = (await getProductPrice(deploymentRegion,"awswaf",undefined,"Request") * 1000000); + runtimeProps.Pricing.BotControl = Number(await getProductPrice(deploymentRegion,"awswaf",undefined,"AMR Bot Control Entity")); + const BotControlRequest: any = await getProductPrice(deploymentRegion,"awswaf",undefined,undefined,"AMR Bot Control Request Processed"); + runtimeProps.Pricing.BotControlRequest = (BotControlRequest[0] * 1000000); + runtimeProps.Pricing.Captcha = 0.4; + runtimeProps.Pricing.AccountTakeoverPrevention = Number(await getProductPrice(deploymentRegion,"awswaf",undefined,"AMR ATP Entity")); + const AccountTakeoverPreventionRequest: any = await getProductPrice(deploymentRegion,"awswaf",undefined,"AMR ATP Login Attempt"); + runtimeProps.Pricing.AccountTakeoverPreventionRequest = (AccountTakeoverPreventionRequest[0] * 1000); + return true; + } + catch{ + return false; + } +} + +/** + * + * @param deploymentRegion AWS region, e.g. eu-central-1 + * @param servicecode The code for the service whose products you want to retrieve. + * @param operation + * @returns price for one product + */ +async function getProductPrice(deploymentRegion: PriceRegions, servicecode: string, operation?: string,group?: string, groupDescription?: string): Promise { + const client = new PricingClient({region: PRICING_API_ENDPOINT_REGION}); + const Filters: {Type: string, Field: string, Value: string}[] = []; + if(groupDescription){ + Filters.push({ + Type: "TERM_MATCH", + Field: "groupDescription", + Value: groupDescription}); + } + if(group){ + Filters.push({ + Type: "TERM_MATCH", + Field: "group", + Value: group}); + } + if(operation){ + Filters.push({ + Type: "TERM_MATCH", + Field: "operation", + Value: operation + }); + } + Filters.push({ + Type: "TERM_MATCH", + Field: "location", + Value: deploymentRegion + }); + + const input: GetProductsCommandInput = { + Filters, + ServiceCode: servicecode + }; + const command = new GetProductsCommand(input); + const response : any = await client.send(command); + if (!response.PriceList || !response.PriceList[0]) { + throw new Error("Price list does not exist"); + } + const priceList = response.PriceList[0] as any; + const USD = findValues(JSON.parse(priceList.toJSON()),"USD"); + return USD || 0; +} + + +/** + * The function calculated the price of the deployed WAF + * @param runtimeProps runtime properties object, where to get prices + * @returns whether price is successfully calculated or not + */ +export async function isPriceCalculated(runtimeProps: RuntimeProperties): Promise { + const preprocessfixedcost = (runtimeProps.PreProcess.CustomRuleCount * runtimeProps.Pricing.Rule) + runtimeProps.PreProcess.CustomRuleGroupCount + runtimeProps.PreProcess.ManagedRuleGroupCount; + const postprocessfixedcost = (runtimeProps.PostProcess.CustomRuleCount * runtimeProps.Pricing.Rule) + runtimeProps.PostProcess.CustomRuleGroupCount + runtimeProps.PostProcess.ManagedRuleGroupCount; + const captchacost = (runtimeProps.PostProcess.CustomCaptchaRuleCount + runtimeProps.PreProcess.CustomCaptchaRuleCount) * runtimeProps.Pricing.Captcha; + const botcontrolfixedcost = (runtimeProps.PostProcess.ManagedRuleBotControlCount + runtimeProps.PreProcess.ManagedRuleBotControlCount) * runtimeProps.Pricing.BotControl; + const atpfixedcost = (runtimeProps.PostProcess.ManagedRuleATPCount + runtimeProps.PreProcess.ManagedRuleATPCount) * runtimeProps.Pricing.AccountTakeoverPrevention; + const fixedcost = runtimeProps.Pricing.Policy + runtimeProps.Pricing.WebACL + postprocessfixedcost + preprocessfixedcost + botcontrolfixedcost + atpfixedcost; + const requestscost = runtimeProps.Pricing.Request; + const totalcost = fixedcost + (requestscost * 5) + (captchacost * 5); + console.log("\n💰 Cost: \n"); + console.log(" WAF Rules cost: " + fixedcost + " $ per month"); + console.log(" WAF Requests: "+ requestscost + " $ per 1 mio requests"); + (captchacost > 0) ? console.log(" WAF Analysis fee:\n Captcha: " +captchacost +"$ per thousand challenge attempts analyzed") : " "; + console.log("\n Total WAF cost (monthly): "+ totalcost + " $ *"); + console.log("\n * This costs are based on expectation that the WAF gets 5 mio requests per month. "); + (atpfixedcost !== 0) ? console.log("\n *This costs are based on expectation that 10.000 login attempts where analyzed. ") : ""; + console.log("\n ℹ The costs are calculated based on the provided information at https://aws.amazon.com/waf/pricing/. "); + (botcontrolfixedcost !== 0) ? console.log(" The deployed WAF includes BotControl rules this costs an extra fee of "+runtimeProps.Pricing.BotControl +" $ and " +runtimeProps.Pricing.BotControlRequest +"$ per 1 mio requests (10 mio request Free Tier). \n These costs are already included in the price calculation.") : ""; + (atpfixedcost !== 0) ? console.log(" The deployed WAF includes Account Takeover Prevention rules this costs an extra fee of "+runtimeProps.Pricing.AccountTakeoverPrevention+" $ and " + runtimeProps.Pricing.AccountTakeoverPreventionRequest +" $ per thousand login attempts analyzed (10,000 attempts analyzed Free Tier). \n These costs are already included in the price calculation.") : ""; + const pricecalculated = "True"; + return pricecalculated; +} \ No newline at end of file diff --git a/lib/types/config.ts b/lib/types/config.ts index c64b2459..4211529a 100644 --- a/lib/types/config.ts +++ b/lib/types/config.ts @@ -17,7 +17,33 @@ export interface Config { readonly PostProcess: RuleGroupSet }, } +export type RegionString = "us-west-2" | "us-west-1" | "us-east-2" | "us-east-1" | "ap-south-1"| "ap-northeast-2" | "ap-northeast-1" | "ap-southeast-1" | "ap-southeast-2" | "ca-central-1" | "cn-north-1" | "eu-central-1" | "eu-west-1" | "eu-west-2" | "eu-west-3" | "sa-east-1" | "us-gov-west-1" | "ap-east-1" | "ap-southeast-3" | "ap-northeast-3" | "eu-south-1" | "eu-north-1" | "me-south-1"; +export enum PriceRegions{ + "us-west-2"= "US West (Oregon)", + "us-west-1"= "US West (N. California)", + "us-east-2"= "US East (Ohio)", + "us-east-1"= "US East (N. Virginia)", + "ap-south-1"= "Asia Pacific (Mumbai)", + "ap-northeast-2"= "Asia Pacific (Seoul)", + "ap-northeast-1"= "Asia Pacific (Tokyo)", + "ap-southeast-1"= "Asia Pacific (Singapore)", + "ap-southeast-2"= "Asia Pacific (Sydney)", + "ca-central-1"= "Canada (Central)", + "cn-north-1"= "China (Beijing)", + "eu-central-1"= "EU (Frankfurt)", + "eu-west-1"= "EU (Ireland)", + "eu-west-2"= "EU (London)", + "eu-west-3"= "EU (Paris)", + "sa-east-1"= "South America (São Paulo)", + "us-gov-west-1"= "AWS GovCloud (US)", + "ap-east-1" = "Asia Pacific (Hong Kong)", + "ap-southeast-3" = "Asia Pacific (Jakarta)", + "ap-northeast-3" = "Asia Pacific (Osaka)", + "eu-south-1" = "Europe (Milan)", + "eu-north-1" = "Europe (Stockholm)", + "me-south-1" = "Middle East (Bahrain)" +} export interface RuleGroupSet { CustomRules?: Rule[], ManagedRuleGroups?: ManagedRuleGroup[]; diff --git a/lib/types/runtimeprops.ts b/lib/types/runtimeprops.ts index f6ac8b99..b56899ed 100644 --- a/lib/types/runtimeprops.ts +++ b/lib/types/runtimeprops.ts @@ -1,7 +1,19 @@ export interface RuntimeProperties { PreProcess: ProcessProperties, PostProcess: ProcessProperties, - ManagedRuleCapacity: number + ManagedRuleCapacity: number, + Pricing: ResourcePrices, +} +export interface ResourcePrices { + Policy: number, + Rule: number, + WebACL: number, + Request: number, + BotControl: number, + BotControlRequest: number, + Captcha: number, + AccountTakeoverPrevention: number, + AccountTakeoverPreventionRequest: number, } export interface ProcessProperties { @@ -9,5 +21,12 @@ export interface ProcessProperties { RuleCapacities: number[], DeployedRuleGroupCapacities: number[], DeployedRuleGroupNames: string[], - DeployedRuleGroupIdentifier: string[] + DeployedRuleGroupIdentifier: string[], + ManagedRuleGroupCount: number, + ManagedRuleBotControlCount: number, + ManagedRuleATPCount: number, + CustomRuleCount: number, + CustomRuleGroupCount: number, + CustomCaptchaRuleCount: number + } \ No newline at end of file diff --git a/package.json b/package.json index 89716059..a6e0ab08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plattform-wafv2-cdk-automation", - "version": "2.1.1", + "version": "2.1.2", "bin": { "plattform-wafv2-cdk-automation": "bin/plattform-wafv2-cdk-automation.js" }, @@ -29,6 +29,7 @@ "dependencies": { "@aws-sdk/client-cloudformation": "^3.52.0", "@aws-sdk/client-fms": "^3.52.0", + "@aws-sdk/client-pricing": "^3.54.1", "@aws-sdk/client-service-quotas": "^3.52.0", "@aws-sdk/client-wafv2": "^3.52.0", "@mhlabs/cfn-diagram": "^1.1.32",